一、基础知识
- C 语⾔中的字符串是⼀个以“\0”结尾的 char 数组,即空字符串在内存中也占用⼀个字节,包含⼀个 NULL 字符。
- 字符常量默认是 int 类型,除非用前置L表示 wchar_t 宽字符类型。
- 对于源代码中超⻓的字符串,除了使⽤相邻字符串外,还可以⽤“\”在⾏尾换⾏。
- 逗号“,”是⼀个⼆元运算符,确保操作数从左到右被顺序处理,并返回右操作数的值和类型。
- C语言的优先级是个大麻烦,不要吝啬使用“()”。
- 在宏⾥使⽤变量通常会添加下划线前缀,以避免展开后跟上层语句块的同名变量冲突。
- 语句块代表了⼀个作⽤域,在语句块内声明的⾃动变量超出范围后⽴即被释放
- GCC ⽀持 switch 范围扩展
int x = 1;
switch (x)
{
case 0 ... 9: printf("0..9\n"); break;
case 10 ... 99: printf("10..99\n"); break;
default: printf("default\n"); break;
}
- typedef可以定义函数指针类型别名:
typedef int (*MathFunc)(int, int);
在上述示例中,使用typedef定义了函数指针类型别名MathFunc,该函数指针接受两个int类型参数并返回int类型。现在可以使用MathFunc来声明相应的函数指针变量。
- 多维数组
实际上就是 "元素为数组" 的数组,注意元素是数组,并不是数组指针。多维数组的第⼀个维度下标可以不指定
int x[][2] =
{
{ 1, 11 },
{ 2, 22 },
{ 3, 33 }
};
int col = 2, row = sizeof(x) / sizeof(int) / col;
- void* 又被称为万能指针,可以代表任何对象的地址,但没有该对象的类型。也就是说必须转型后 才能进行对象操作。void* 指针可以与其他任何类型指针进行隐式转换。
- 数组指针:指向数组本⾝的指针,而非指向第⼀元素的指针。
- 指针数组:元素是指针的数组,通常⽤于表⽰字符串数组或交错数组。
- 利⽤ stddef.h 中的 ofsetof 宏可以获取结构成员的偏移量
- 弹性结构体:在结构体尾部声明⼀个未指定长度的数组,不能直接对弹性成员进⾏初始化
- 位字段:可以把结构或联合的多个成员 "压缩存储" 在⼀个字段中,以节约内存
struct
{
unsigned int year : 22;
unsigned int month : 4;
unsigned int day : 5;
} d = { 2010, 4, 30 };
- volatile:使用该变量前,都须从主存重新获取
- 宏函数
利用宏可以定义伪函数,通常⽤ ({ ... }) 来组织多行语句,最后⼀个表达式作为返回值 (无 return, 且有个 ";" 结束)。
#define test(x, y) ({ \
int _z = x + y; \
_z; })
int main()
{
printf("%d\n", test(1, 2));
return 0;
}
- 单位运算符#将一个宏参数转换为字符串
#define test(name) ({ \
printf("%s\n", #name); })
int main()
{
test(main);
test("\"main");
return 0;
}
- 二元运算符 ## 将左和右操作数结合成⼀个记号
#define test(name, index) ({ \
int i, len = sizeof(name ## index) / sizeof(int); \
for (i = 0; i < len; i++) \
{ \
printf("%d\n", name ## index[i]); \
}})
int main()
{
int x1[] = { 1, 2, 3 };
int x2[] = { 11, 22, 33, 44, 55 };
test(x, 1);
test(x, 2);
return 0;
}
- 条件编译
可以使⽤ "#if ... #elif ... #else ... #endif"、#define、#undef 进⾏条件编译
22.使⽤ GCC 扩展 typeof 可以获知参数的类型。
#define test(x) ({ \
typeof(x) _x = (x); \
_x += 1; \
_x; \
})
int main()
{
float f = 0.5F;
float f2 = test(f);
printf("%f\n", f2);
return 0;
}
- assert(断言)
要习惯使⽤ assert 宏进⾏函数参数和执⾏条件判断,这可以省却很多⿇烦,即assert条件表达式不为true,则出错并终止进程。
#include <assert.h>
void test(int x)
{
assert(x > 0);
printf("%d\n", x);
}
int main()
{
test(-1);
return 0;
}
- C语言的各部分出现在哪些段中:
BSS:Block Started by Symbol(由符号开始的块),由于BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BSS段的大小记录在目标文件中,但BSS段并不占据目标文件的任何空间。
文本段:包含程序的指令。
- 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
- 总线错误:几乎都是由于未对齐的读或写引起的,因为出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。对齐的意思就是数据项只能存储在地址是数据项大小的整数倍的内存位置上。
- 段错误:由于内存管理单元的异常所致,而该异常则通常是由于解引用一个未初始化或非法值的指针引起的。
- 推荐的空指针判断程序:
printf("%s\n", p->name ? p->name : "null");
二、高级知识
1.指针概要
1.1指针常量
指针常量意指 "类型为指针的常量",初始化后不能被修改,固定指向某个内存地址。 我们无法修改指针自身的值,但可以修改指针所指目标的内容。
int* const p;
1.2常量指针
常量指针是说 "指向常量数据的指针",指针目标被当做常量处理 (尽管原目标不⼀定是常量),不能 ⽤通过指针做赋值处理。指针⾃⾝并⾮常量,可以指向其他位置,但依然不能做赋值操作
const int* p;
1.3函数指针
默认情况下,函数名就是指向该函数的指针常量。
void (*f)(int*);
或者
typedef void (*f)(int*);
1.4字符串指针
直接使用一个指针指向字符串
char *p = "hello world";
也可以用字符数组来存储字符串的每个字符
char str[] = "hello world";
两者的区别在于字符串指针只是用一个指针指向一个字符串常量,不能够修改字符串的值,而字符数组是用数组来存储字符串的每个字符,因此可以修改字符串中的每个字符
2.数组指针
数组指针把数组当做⼀个整体,因为从类型⾓度来说,数组类型和数组元素类型是两个概念。因此 "p2 = &x" 当中 x 代表的是数组本⾝⽽不是数组的第⼀个元素地址,&x 取的是数组指针,而不是"第⼀个元素指针的指针"
int a[] = {1,2,3,4,5};
int (*p)[] = &a;
p 的目标类型是数组,因此 p++ 指向的不是数组下⼀个元素,而是 "整个数组之后" 位置
3.指针数组
指针数组是指元素为指针类型的数组,通常⽤来处理 "交错数组"(每个元素数组的⻓度可以不同),⼜称之为数组的数组
int main()
{
int x[] = {1,2,3};
int y[] = {4,5};
int z[] = {6,7,8,9};
int* ints[] = { NULL, NULL, NULL };
ints[0] = x;
ints[1] = y;
ints[2] = z;
printf("%d\n", ints[2][2]);
for(int i = 0; i < 4; i++)
{
printf("[2,%d] = %d\n", i, ints[2][i]);
}
return 0;
}
指针数组经常出现在操作字符串数组的场合
int main (int args, char* argv[])
{
char* strings[] = { "Ubuntu", "C", "C#", "NASM" };
for (int i = 0; i < 4; i++)
{
printf("%s\n", strings[i]);
}
printf("------------------\n");
printf("[2,1] = '%c'\n", strings[2][1]);
strings[2] = "CSharp";
printf("%s\n", strings[2]);
printf("------------------\n");
char** p = strings;
printf("%s\n", *(p + 2));
return 0;
}
三、系统
1.GDB
作为内置和最常用的调试器,GDB 显然有着无可辩驳的地位
编译命令 (注意使⽤ -g 参数⽣成调试符号):
$ gcc -g -o hello hello.c
$ gdb hello
1.1断点
可以使⽤函数名或者源代码⾏号设置断点
(gdb) b main # 设置函数断点
(gdb) b 13 # 设置源代码⾏断点
(gdb) b test if a == 10 #条件式中断
1.2执行
(gdb) r # 开始执⾏ (Run)
(gdb) n # 单步执⾏ (不跟踪到函数内部, Step Over)
(gdb) s # 单步执⾏ (跟踪到函数内部, Step In)
(gdb) finish # 继续执⾏直到当前函数结束 (Step Out)
(gdb) c # Continue: 继续执⾏,直到下⼀个断点。
1.3堆栈
(gdb) where # 查看调⽤堆栈 (相同作⽤的命令还有 info s 和 bt)
(gdb) frame # 查看当前堆栈帧,还可显⽰当前代码
(gdb) info frame # 获取当前堆栈帧更详细的信息
1.3变量和参数
(gdb) info locals # 显⽰局部变量
(gdb) info args # 显⽰函数参数
(gdb) p a # print 命令可显⽰局部变量和参数值
(gdb) p/x a # ⼗六进制输出
(gdb) p a + b # 还可以进⾏表达式计算
1.4内存和寄存器
x 命令可以显示指定地址的内存数据。
格式: x/nfu [address]
- n: 显示内存单位 (组或者行)。
- f: 格式 (除了 print 格式外,还有 字符串 s 和 汇编 i)。
- u: 内存单位 (b: 1字节; h: 2字节; w: 4字节; g: 8字节)。
1.5反汇编
(gdb) set disassembly-flavor intel # 设置反汇编格式
(gdb) disass main # 反汇编函数