HFC读书笔记
数组变量可以当成指针使用, 但有不同的地方:
1. sizeof(数组) 是数组的大小, 而用在指针上返回4或8(地址的长度)
2. &数组变量 == 数组变量, 而&指针不同(是指针变量自己的地址)
3. 数组变量并没有分配存储空间, 所以不能指向其它地方
scanf()的用法可参考printf()的用法, 需要设置长度, 例如字符数组, 需要减1, 留给\0, 避免缓冲区溢出, 对于字符串更好的是用fgets(),
例如:fgets(数组,sizeof(数组),stdin); 注意无须减1 配合sizeof运算符使用 备注 scanf -> scan formatted
char指针设置为字符串字面值, 确保使用const关键字: const char *s = "some string"; 因为字面值是不可修改的
存储在只读区域,这样在编译时会报错
---------------------------------------------------------------------------------------------------------------------------------------------------:-)
熟悉使用string.h标准库, 处理字符串
---------------------------------------------------------------------------------------------------------------------------------------------------:-)
使用小工具:
Ctrl + D 用于停止程序 但好像中止命令行程序的输出是Ctrl + C
程序结束时检查错误状态: UNIX用 echo $? windows用 echo %ERRORLEVEL%
进程有标准输入和标准输出, 标准错误 stdin, stdout, stderr 它们都是流
当调用printf()时, 其实是调用了fprintf()函数的特殊 fprintf(stdout, "我喜欢乌龟!");
可以用fscanf()来读取标准输入, 用法和scanf()很像
可以用2>重定向标准错误
注意,运行程序时,包括重定向时一定注意程序路径与输入文件路径问题,否则会报错“没有那个文件或目录”
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
符号 | 表示管道(pipe), 它能连接一个进程的标准输出与另一个进程的标准输入
例如: (./bermuda | ./geo2json) < gpsdata.csv > output.json 数据流由管道从左到右流动,括号是必须的
除了标准输入, 标准输出, 标准错误, 还可以自建数据流. 使用fopen()函数
FILE *in_file = fopen("input.txt", "r");
w = 写; r = 读; a = 追加 (注意, 全是小写, 按照书上的例子, 不小心大写了, 运行时报"段错误")
创建数据流后, 可以使用fscanf()函数读数据, fprintf()函数写数据
使用完后需要关闭流: fclose(in_file);
如果出现错误, fopen()会返回0, 最好检查一个有没有错误发生! 相当于增加健壮性
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
int main(int argc, char *argv[])
可以向函数传递参数, 前面参数个数从程序名本身算起; 后面是char指针数组, 实际也就是字符串指针数组的意思
熟悉POSIX库中的unistd.h头文件中的函数getopt(), 用于处理命令行选项(即以"-"开头的命令行参数)
似乎运行示例碰到了问题,经过测试改进,发现去掉改变argc,argv的2行代码,后面打印材料循环初值改为optind
这样,就运行正常了 ,optind中的ind可能就是索引的意思,代表处理到第几个参数的索引了,可能是升级了库
vscode格式化代码快捷键(windows) shift + alt + f
命令行选项是负数的,比如 -d -4 中间用--隔开即可 -d -- -4
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
创建头文件只需要在目录中新建.h文件, 写上内容, 保存, 在其它程序的开头加上预处理命令#include "xxx.h"
注意引号是相对路径查找头文件, 而尖括号是绝对路径查找头文件, 函数声明通常放在头文件中
头文件的作用: 一是函数顺序问题; 另一个是共享代码, 到编译后期链接时有用
共享变量: 在头文件中声明, 在使用时加上extern, 例如: extern int passcode;
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
gcc -c *.c 此命令会为目录内的所有源文件创建目标文件, 但不会链接成完整的可执行文件
gcc *.o -o launch 然后, 把目录内的目标文件链接起来
总结来说, 如果只修改了一个文件, 只需要重新编译这一个文件, 再重新链接成程序即可
但注意, gcc -c xxx.c -o xxx.o 就不会生成默认的目标文件, 其中后面一个xxx是可以指定的文件名
make是如何自动化代码的构建过程的:
需要在目录中写出(约定的)makefile文件, 描述目标,依赖项和生成方法, 再用命令make launch
其实原理非常简单, 只是要使用才能记住, 不然明天就忘得一干二净:-)
2024/3/25 今天写了一个makefile,使用make时却只执行了第一行:原因是因为最后的可执行文件生成语句放在了最后
放在前面第一行就没事,好奇怪,没听说过!
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
为结构变量赋值相当于叫计算机复制数据
嵌套的结构, 在初始化数据时, 要使用多层花括号的写法
可以使用 typedef 为结构(/类型)创建别名, 就可以当成类型名使用, 其实只写类型名, 结构名省略, 编译器也没有意见:-)
对了, 这样的话叫做匿名结构哦, 其实也没啥
在C语言中,参数按值传递给函数。要更新结构的值, 需要传递结构的指针,
而且可以使用简洁易阅读的语法更新字段: t->age 代表 (*t).age
注意, 上面是指针时用->, 非指针的情况下, 还是要用.号访问字段的!
联合: 声明后设置它的值常用方法 C89方式 quantity q = {4}; 它会设置联合的第一个字段的值
指定初始化器方法: quantity q = {.weight=1.5};
"点"表示法: quantity q; q.volume = 3.7; 这里使用了分行
c语言的位字段: typedef struct { unsigned int lowPassVcf:1; } 必须是unsigned int, 表示该字段只占用一位存储空间
也即是说C语言的位是可以自定义的, 1位或多位空间占用,只要在冒号后面表达需要使用的位即可(2^位)
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
如果一个结构包含一个链向同种结构的链接, 那个这个结构就被称为递归结构.
另外递归结构要有名字, 因为里面有个相同类型的指针, 而C语言的语法不允许用typedef别名来声明它.
在C语言中, NULL的值实际为0, NULL实际专门用来把某个指针设为0
以前想不通,为什么结构的别名跟结构名可以相同,现在想如果加struct那就是结构名,否则是别名
申请存储器(堆空间)的函数叫malloc(), 是memory allocation (存储器分配的意思), 通常与sizeof运算符一起使用
malloc(sizeof(island)); 当然需要头文件 #include <stdlib.h>
malloc()返回通用指针(void*类型), island *p = malloc(sizeof(island));
用完需要调用free()函数释放存储器: free(p);
注意, 使用strdup()之后, 也要调用free(), 因为它通常调用了malloc(), 这要看标准库是如何实现的
堆之所以叫做堆: 计算机不会自动组织它, 它只是一大堆数据而已
可能安装valgrind来检测是否存在内存泄漏, 加上选项, 并把程序传给valgrind
valgrind --leak-check=full ./spies
当然, 可以在可执行文件中加入调试信息, 记录要编译代码的行号 -g 开关
gcc -g spies.c -o spies
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
在C语言中, 函数名就是指向函数的指针, 有了函数指针, 就能把函数传给函数
但还是需要一个函数指针变量(写法): 返回类型 (*指针变量)(参数类型)
例如: char** (*names_fn)(char*, int) 多个参数用逗号隔开
函数指针使用中, 省略* 和&, 编译器也能识别它们, 而且更好读
这样可以把相同签名的函数传递给函数使用
names_fn = album_names;
char** results = names_fn("Sacha Distel", 1972);
char** 并没有打错, 这是一个指什, 通常用来指向字符串数组
qsort()函数来自于标准库stdlib.h, 用于排序, 接收指向比较器函数的指针, 比较器函数可以比较两个值的大小
比较器函数接收2个指针, 分别指向待排序数组中的两项
函数指针数组写法: 返回类型 (*指针变量)(参数类型) 例如: void (*replies[])(response);
函数指针数组为调用一些相关函数提供了方便, 让代码易于管理和扩展, 从而可以伸缩.
有了函数指针数组,就可以根据不同类型的数据运行不同的函数
参数可变的函数就称为可变参数函数, 需要包含stdarg.h, 函数写法固定如下
#include <stdarg.h> //写可变参数函数需要 #include <stdio.h> void print_ints(int args, ...) //代表有可变参数 { va_list ap; //保存可变参数 va_start(ap, args); //从args参数开始后面都是 int i; for (i = 0; i < args; i++) //va_arg读取,包括读取参数的类型 printf("argument: %i\n", va_arg(ap, int)); va_end(ap); //代表需要销毁va_list } int main() { print_ints(3, 79, 101, 32); return 0; }
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
通常类UNIX操作系统会在 /usr/local/include 和 /usr/include 目录查找头文件, 有3种方式使用头文件
1 只要头文件在标准目录中, 就可以使用尖括号包含它们: #include <stdio.h>
2 也可以使用完整路径名: #include "/my_header_files/encrypt.h"
3 也可以告诉编译器去哪里找头文件: gcc -I/my_header_files test_code.c
/my_object_files/encrypt.o ... -o test_code
用完整路径名共享.o目录文件, 跟指定头文件所在路径差不多写法, 以上方法适合共享几个文件的情况
那如何共享一大堆目标文件呢: 创建目标文件存档, 命令如下图
可以使用nm命令查看存档中的内容:
只要创建目标文件存档, 就可以一次告诉编译器一批目标文件(应该就是说的压缩包吧:-)
用ar 命令创建存档: ar -rcs libhfsecurity.a encrypt.o checksum.o
r表示存在就更新, c表示创建时不显示反馈, s表示要在开头建立索引, 后面是存档文件名, .a文件
务必把存档命名为libXXX.a, 否则编译器找不到它们, 存档是静态库
nm命令可以查看存档中的内容, 它列出存档中保存的文件的名字: nm libhfsecurity.a
另外 ar -t 文件名 会列出存档中的目标文件, 如下图
ar -x libhfsecurity.a encrypt.o 该命令可以从存档中提取文件, 但必须是存档中有的才行!
当存档安装到标准目录(如/usr/local/lib): gcc test_code.c -lhfsecurity -o test_code
其它目录不是标准目录的情况: gcc test_code.c -L/my_lib -lhfsecurity -o test_code
由上面笔记及练习题指明: -I (大写字母i)用于指定头文件所在 -L用于指定存档文件所在
-l (小写L)使用存档, 如果在当前目录, 后面跟点 .
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
创建动态链接库
创建目标文件 gcc -I. -fPIC -c hfcal.c -o hfcal.o -fPIC意思是创建位置无关代码(事实上不加也没关系)
-shard选项告诉gcc把.o目标文件转化为动态库, 一个平台一个叫法, 然后扩展名也不同, 如上图
Linux叫共享目标文件, 里面还保存了库名, 所以编译后不能修改文件名, 除非重新编译
不同操作系统寻找动态库的方式并不相同, 有点复杂, 在Linux中, 通常保存在/usr/lib或/usr/local/lib中
不知道为何, 当动态库放在标准目录中, 还是不能运行程序, 试了好久, 网上查也没有解决:-(
经过不懈努力, 终于找到了, 原因是我装的64位Linux(CentOS7), 相应需要把动态库拷贝到lib64目录中,Yes! Cool!
而在Window中, 通常把.dll和可执行文件放在同一个目录
在Linux中, 需要设置LD_LIBRARY_PATH变量, 这样程序才能发现动态库
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/libs 这里libs是共享目标文件所在目录, 上图是放在这个libs文件夹, 所以
下面是编译和链接程序命令
gcc -I. -c elliptical.c -o elliptical.o
gcc elliptical.o -L. -lhfcal -o elliptical
使用动态库的好处是, 如果修改(更新)了动态库,那么除了库文件外,源程序不需要重新编译
----------------------------------------------------------------------------------------------------------------------------------------------------:-)
操作系统内核管理进程, 存储器, 硬件, 而系统调用是程序用来与内核对话的函数
system()函数, 操作系统解释命令, 然后才运行
然后是exec()和它的两类变体,再说是fork(), 不过书本上的例子RSS什么的,没做成功,运行啥也没有,暂时这样吧先
----------------------------------------------------------------------------------------------------------------------------------------------------------:-)
由于书上讲的创建子进程和父子进程间使用管道通信的例子,实现碰到了问题,感觉例子过时,我反正没成功,无耐,
只好自己改写书上的例子加由实践理解了:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char *w = getenv("EXTRA"); if (!w) w = getenv("FOOD"); if (!w) w = argv[argc - 1]; char *c = getenv("EXTRA"); if (!c) c = argv[argc - 1]; printf("\n%s with %s\n", c, w); return 0; }
coffee.c只在输出前加了换行符,其它没变,下面的test_coff.c改写如下:
#include <string.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> void error(char *msg) { fprintf(stderr, "%s: %s\n", msg, strerror(errno)); exit(1); } int main(int argc, char *argv[]) { char *ingredient = argv[1]; char *my_env[] = {"FOOD=coffee", "FOOD=cream", NULL}; // FILE *f = fopen("menu.txt", "w"); // if (!f) // error("Can't open menu.txt"); int fd[2]; //建立父子通信管道 if (pipe(fd) == -1) error("Can't create the pipe"); int i; for (i = 0; i < 3; i++) { pid_t pid = fork(); if (pid == -1) error("Can't fork process"); if (!pid) { //重定位到文件的部分,允许在复制新进程时做各种处理 // if (dup2(fileno(f), 1) == -1) // error("Can't redirect Standard Output"); close(fd[0]); //关闭管道读 dup2(fd[1], 1); //用管道写标准输出 if (execle("./coffee", "./coffee", ingredient, NULL, &my_env[i]) == -1) error("Can't run process 0"); } int pid_status; if (waitpid(pid, &pid_status, 0) == -1) error("等待子进程时发生了错误"); dup2(fd[0], 0); //用管道读标准输入 close(fd[1]); //关闭管道写 char line[255]; while (fgets(line, 255, stdin)) printf("%s", line); printf("子进程id=%d ", pid); } printf("父进程id=%d", getpid()); return 0; }
操作系统用信号控制进程 信号是O/S发出的消息
简单来说是利用信号映射表:信号(整型值)与处理函数的列表
可以用struct sigaction包装信号处理器函数,如果IDE提示不允许使用不完整类型,
可在代码开头加上#define _XOPEN_SOURCE _XOPEN_SOURCE为了实现XPG:The X/Open Portability Guide 的功能
再用sigaction(signal_no, &new_action, &old_action)来向操作系统注册
用kill可以向进程发信号,而且一定可以杀死进程kill -KILL
进程可以用raise(SIGTERM)向自己发信号(通常接收低级别信号引发更高级别的信号--信号升级)
定时器alarm(120),120秒后可以发出SIGALRM信号,但注意与sleep()函数冲突
自定信号处理器后也可以重置SIG_DFL和忽略SIG_IGN
----------------------------------------------------------------------------------------------------------------------------------------------------------:-)
可使用最流行的POSIX线程库, 也就是pthread, 头文件是pthread.h
线程函数的返回类型必须是void*
创建并运行线程:
pthread_t t0; if (pthread_create(&t0, NULL, does_not, NULL) == -1) error("无法创建");
等待线程结束, 得到结果:
void* result; if (pthread_join(t0, &result) == -1) error("无法回收线程t0");
编译程序时, 需要链接pthread库(这里是动态链接):
> gcc argument.c -lpthread -o argument
#include <stdio.h> int main() { //对于普通类型变量 int i = 5; printf("i的地址是%p, i的值是%d\n", &i, i); //对于指针变量 int *m = &i; printf("指针变量m保存的内容是:%p, 指针m指向的值是%d\n", m, *m); printf("指针变量m的地址是%p\n", &m); //二级指针,指针变量的类型是在定义时确定的,并且不能在运行时改变 printf("可以指向任意类型的指针,这个指针的地址是%p\n", (void**)&m); return 0; }
再谈指针