Review on C Programming Language
1. GCC命令的使用
单个文件,一步到位:
gcc ./src/test.c -o ./bin/test -I ./include -std=gnu99
多个文件,一步到位:
gcc ./src/test.c ./src/other.c -o ./bin/test -I ./include -std=gnu99
相当于先逐个编译,再一步链接:
gcc -c ./src/test.c -o ./test.o -I ./include -std=gnu99 gcc -c ./src/other.c -o ./other.o gcc ./test.o ./other.o -o ./bin/test
根据CSAPP,该过程详细分为四步:
Step 1. Preprocessing
这一步将头文件嵌入源代码文件(库包含),并将代码中的宏名进行替换。Linux系统默认头文件目录为/usr/include/,默认库文件目录为/usr/lib/(用以实现头文件的接口)。引用系统默认目录中的头文件时可用单书名号声明文件名;若欲引用非系统默认目录中的头文件,则须使用引号声明文件名,且在不使用-I参数时默认头文件目录为gcc当前工作目录(不一定是源代码目录)。
gcc -E ./src/test.c -o ./test.i -I ./include
Step 2. Compilation
这一步根据运行环境生成汇编代码,可选用C99。
gcc -S ./test.i -o ./test.s -std=gnu99
Step 3. Assembly
这一步由汇编代码生成对象文件。
gcc -c ./test.s -o ./test.o
Step 4. Linking
这一步将(多个)对象文件与库文件链接在一起。
gcc ./test.o ./other.o -o ./bin/test
2. 语法中的细节
(1) ANSI C中任何语句块开头都可以定义变量(课设里也是这样实现的~~)。
(2) 定义 char 尽量声明为 signed char 或 unsigned char,定义 int 尽量声明为 signed long 或 unsigned long。
(3) static和extern型的变量被缺省初始化为0;任何数组若被提供少于其长度的初始值,则剩余元素被缺省初始化为0,因此有个开数组并赋初值的技巧是:
int arr[1024] = {};
(4) 声明无参数函数时要显式使用void关键字。
(5) 多个运算表达式可用逗号隔开,自左向右依次求值:
// call swap(&x,&y) void swap(int *x, int *y) { *x ^= *y, *y ^= *x, *x ^= *y; }
(6) 尽量不要在同一表达式中调用多个函数或向同一函数中传入多个函数值,因为表达式求值与函数传参顺序依赖于运行环境,而由于全局变量的存在函数调用顺序不同可能造成运行结果不同(以前在C++输出流中犯过类似的错误,调了一晚上)。
(7) 有参宏定义中,给宏展开里的形参前加#可使其在被替换时被引号包裹,并且替换时会自动为实参中的一些符号添加转义符号:
#define STR(str) #str // STR(Hello!) will be replaced by "Hello!"
(8) 有参宏定义中,用##可将参数嵌入到字符串当中:
#define FUN(name) Hello,##name##!
(9) 多行宏定义举例:
#define min(X,Y) ({\ typeof(X) x_ = (X);\ typeof(Y) y_ = (Y);\ (x_ < y<)? x_:y_;})
(10) 防止头文件被重复包含:
#ifndef HEADER_H #define HEADER_H /* content of header.h */ #endif
(11) 条件编译举例:
#if NUM_OF_ATOM == 1 #define FREDEG 3 #elif NUM_OF_ATOM == 2 #define FREDEG 5 #else #define FREDEG 6 #endif
(12) C语言中,一个指针在内存中占据8字节,记录着指向区域的首地址(是虚拟地址,并不是什么数据段偏移量,以前弄错了T_T);其指向区域的大小由其本身类型决定,只在编译期间被符号表记录。对于一个数组 int arr[1024],指针 &arr[0]、arr 与 &arr 均指向相同首地址,但后者与前两者类型不同,因此编译器计算 (&arr+1) 时会得出与 (arr+1) 和 (&arr[0]+1) 完全不同的结果(指针加一实际上是指针值加一倍的指向数据的长度,即 PTRVAL+=sizeof(*ptr))。另外,尽管数组名在参与加减运算时被当做指向数组首元素地址的常指针,但 sizeof(arr) 和 sizeof(&arr[0]) 并不相同。
(13) 参数分为输入参数和输出参数:输入参数为值传递,通常放在前边;输出参数以指针或数组形式传递,通常放在后边。
(14) const char *ptr 为常量指针,表明不可以通过ptr修改其指向的内存区域;char* const ptr 为指针常量,表明不可改变ptr指向的内存区域。还有, 弄不懂char *argv[] 是什么类型的请戳 这里 。
(15) 函数指针形式为 int (*comp) (int,int),常作为函数参数。
(16) 数据段有三种变量:第一种是动态全局变量,允许其他源文件和本文件该变量声明前的代码以外部变量方式引用;第二种是静态全局变量,作用域为本文件该变量声明之后的代码;第三种是静态局部变量,作用域为声明该变量的函数(与是不是静态函数没关系)。
(17) 字符串常量属于指令的一部分,存储在代码段,因此以字符指针形式修改其中的字符就会造成 segmentation fault;但初始化为字符串常量的字符数组却不同,因为数组初始化已将指令中的常量拷贝到了数据段或堆栈段分配给该数组的空间。
(18) 结构体占用空间不一定等于各成员所占空间之和,因为成员之间可能存在空穴。