一、编译链接过程
编译
- 预处理
删除注释,宏替换,头文件展开,条件编译
- 编译
词法分析,语法分析,语义分析,符号汇总(生成汇编代码)
- 汇编
将形成的汇编代码转为二进制代码,形成对应的符号表
链接
合并段表以及符号表的重定位
二、指针和数组
数组和指针的区别和联系
联系
- 表达式中的数组名就是指针
- C语言中把数组下标作为指针的偏移量
- 作为函数参数的数组名等同于指针
区别
- 数组名在传参时会退化为指针,指针不会(所以一定不要在函数内部对传参后的数组名进行sizeof大小)
- 内存中数组是一块连续开辟的空间,指针只占一个指针类型的大小空间(32位为4字节,64位为8字节)
- 数组可以通过下标直接进行访问,指针需要进行计算间接访问
- 数组名具有常属性,不能进行++,--操作;指针可以
数组指针和指针数组区别
以 int (*p)[n] 为例,从右往左理解,() 高优先级,先定义一个指针,然后看[],说明这是一个指向数组的指针,最后 int 型。
- 数组指针(int (*p)[n])
代表一个指向有n个int类型的数组的指针,也叫行指针 - 指针数组(int *p[n])
代表一个有n个int* 指针的数组
- 区别:数组指针由于是一个指针,所以内存中只消耗一个指针大小的空间;指针数组是一个数组,消耗n个对应类型指针大小的空间。
需要注意的点
-
sizeof字符串指针大小为对应平台下指针大小;sizeof字符串数组为数组个数再+1;
-
数组名可看做是一个指针,取数组名地址再进行++操作将移动整个数组大小。
-
数组名不能作为左值!!
三、宏
宏的优缺点
- 优点
执行速度快,在预处理期间就完成替换 - 缺点
- 多处调用宏,可能会造成代码的膨胀
- 宏的优先级一定要注意,多用括号
- 宏与类型无关,不进行类型检查
- 带有副作用的宏参数进行多次求值,可能得到意料之外的结果(++, --)
- 由于宏在预处理阶段就进行替换,无法调试
宏和函数的比较
左值:可被引用的参量
副作用(side effect):计算表达式时对某些东西(如存储在变量中的值)有修改;
- 代码长度
宏:每次使用宏都会使得宏代码被替换到代码中,可能会造成代码体积的大幅度增长
函数:函数代码只出现于一个地方,每次使用该函数,都会跳转到同一个位置 - 执行速度
宏:预处理器就完成了宏替换
函数:需要创建栈帧,压参传参,返回值的开销 - 操作符优先级
宏:由于直接进行替换,容易造成意料之外的错误。尽量多用()来表示优先级
函数:更容易预估结果 - 参数求值
宏:参数每出现一次都会被重新求值一次
函数:只在调用前计算一次,不会出现副作用 - 参数类型
宏:宏与类型无关,只要对参数操作合法,使用于任何参数类型
函数:函数参数与类型有关,参数类型不同,执行不同的代码(函数重载)
宏和枚举的区别:
- 宏在预编译阶段进行简单替换,枚举是在编译阶段确定值
- 宏不能调试,枚举可以
- 枚举一次可以定义大量相关的常量,而define只可定义一个
宏和内联函数的比较
- 宏不能调试,内联可以
- 宏对参数不进行类型检查,内联函数进行
- 宏肯定会被替换,内联是一种建议
- 宏优先级若不注意可能会出现预期之外的结果,内联不会
typedef和define的比较
- 可以其他使用类型说明符对宏类型名进行扩展,typedef所定义的类型名却不能这样做
- 用typedef定义的类型能够保证声明中所有的变量均为同一类型,#define定义的类型则无法保证
四、static
修饰变量
- 静态全局变量
作用域仅限于从定义之处一直到文件结尾,失去外部链接属性 - 静态局部变量
在函数体内部定义,只能在此函数中使用
注:static修饰的变量存放在进程地址空间的静态区,即使函数运行结束,该静态变量的值还是不会被销毁。
修饰函数
使得该函数失去外部链接属性,作用域仅限本文件(避免和别人的文件命名冲突)
五、const
修饰变量
只是具有只读属性,并不是常量
节省空间,提高效率(编译器通常不为普通const变量分配存储空间,而是直接保存在符号表中,不需要多次访问内存)
修饰指针
- const int* p 等价于 int const *p,p的值可以变,但是*p不可改
- int* const p:p不可修改(p的指向),*p可以修改
- const int* const p:指针p的指向和指向对象的内容都不可修改
修饰函数的参数
int function(const x);
代表该参数的值在此函数中不可修改
修饰函数的返回值
代表返回值不可改变(例:const int Fun(void))
六、函数传参
传参方式
- 传值调用:接受参数的函数实际上获得了参数的一份临时拷贝
- 传址调用:直接将参数地址传给调用函数,直接对原参数进行修改
数组传参
-
一维数组传参
当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针
-
二维数组传参
int arr[2][3]
- 利用数组指针接收void fun(int (*arr)[3])
- 利用二维数组接收 void fun(int arr[] [3])
-
多维数组传参需要提供除最左边一维之外的其他维的长度
指针传参
想要修改指针变量需要用二级指针来接收,实参传当前指针的地址。
void func(char **p)
{
*p = "hello
";
}
int main()
{
char *p = NULL;
func(&p);
printf("%s", p);
system("pause");
return 0;
}
七、结构体
内存对齐
- 为什么要进行内存对齐?
尽量减少访问内存次数,提高效率。
对齐原则
- 数据成员对齐规则
结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中比较小的那个进行对齐。
- 结构(或联合)的整体对齐规则
在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
- 结构体作为成员
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
为什么要对齐
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
如何计算
参考[关于结构体内存对齐总结][https://blog.csdn.net/sssssuuuuu666/article/details/75175108]
注:空结构体大小为1(进行占位),gcc中默认对齐数为4,VS默认为8
八、内存管理
内存地址空间分布
栈:保存局部变量。栈空间有限,且出了作用域自动销毁(函数栈帧)高地址向低地址生长
堆:动态内存分配的空间,malloc,free。注意内存泄漏
静态区(数据段):保存全局变量和static变量,生命周期伴随整个程序结束
代码段:保存可执行的代码和只读常量
malloc
malloc函数注意对返回类型进行强制转换,以及判断是否申请成功。
(void*) malloc(int size)
注意:
- malloc函数申请0字节内存并不会返回NULL指针
- malloc申请的是虚拟内存,不是直接申请物理地址
free
free只负责将内存释放,并不负责将指针置NULL。此时的指针就是所谓的野指针。
注意:malloc和free的次数必须一致
函数调用栈帧过程
-
创建栈帧初始化:push ebp,sub esp xxx,压寄存器,完成初始化
-
创建局部变量,push参数(从右向左的顺序依次压入)
-
push : call指令的下一条指令的地址,一会返回使用
-
再次挪动ebp,esp创建栈帧,取参数进行计算并将返回值放入寄存器返回
-
销毁栈帧,pop掉寄存器,找到对应上一个函数的下一条指令的地址进行回退
九、语法细节
volatile关键字
volatile关键字解决编译器优化的问题,保证对于该变量的修改都是直接对内存中存放的值进行操作。
sizeof关键字
sizeof括号内的表达式只进行计算,并不改变原始的值
大端和小端
大端:高字节数据存储的低地址中
小端:高字节数据存储在高字节中
判断当前系统是大端还是小端
头文件包含
#include<>:表示预处理到系统规定的路径中取获得该文件
#include " ":表示先在当前目录中找该文件,找不到再去规定的路径下
#pragma预处理
pragma once:保证头文件只被包含一次
pragma pack( ):设置默认对齐数
逗号表达式
注意:每个表达式都会进行运算,但是整个逗号表达式值为最后一个表达式的值
十、C库函数实现
str系列函数
strcpy
char* my_strcpy(char* dest, const char* src)
{
assert(src != NULL);
assert(dest != NULL);
char *ret = dest;
while ((*(dest++) = *(src++)) != ' ')
{...;}
return ret;
}
注意:strcpy会将