C语言总结

    xiaoxiao2023-10-21  171

    C 编译链接过程 编译 预处理 删除注释,宏替换,头文件展开,条件编译 编译 词法分析,语法分析,语义分析,符号汇总(生成汇编代码) 汇编 将形成的汇编代码转为二进制代码,形成对应的符号表 链接 合并段表以及符号表的重定位 指针和数组 数组和指针的区别和联系 联系 1、表达式中的数组名就是指针 2、C语言中把数组下标作为指针的偏移量 3、作为函数参数的数组名等同于指针 区别 1、数组名在传参时会退化为指针,指针不会(所以一定不要在函数内部对传参后的数组名进行sizeof大小) 2、内存中数组是一块连续开辟的空间,指针只占一个指针类型的大小空间(32位为4字节,64位为8字节) 3、数组可以通过下标直接进行访问,指针需要进行计算间接访问 4、数组名具有常属性,不能进行++,–操作;指针可以 数组指针和指针数组区别 数组指针(int (p)[n] 代表一个指向有n个int类型的数组的指针 指针数组(int p[n]) 代表一个有n个int 指针的数组 区别:数组指针由于是一个指针,所以内存中只消耗一个指针大小的空间;指针数组是一个数组,消耗n个对应类型指针大小的空间。 需要注意的点: 1、sizeof字符串指针大小为对应平台下指针大小;sizeof字符串数组为数组个数再+1; 2、数组名可看做是一个指针,取数组名地址再进行++操作将移动整个数组大小。 3、数组名不能作为左值!! 宏 宏的优缺点: 优点 执行速度快,在预处理期间就完成替换 缺点 1、多处调用宏,可能会造成代码的膨胀 2、宏的优先级一定要注意,多用括号 3、宏与类型无关,不进行类型检查 4、带有副作用的宏参数进行多次求值,可能得到意料之外的结果(++, --) 5、由于宏在预处理阶段就进行替换,无法调试 宏和函数的比较 代码长度 宏:每次使用宏都会使得宏代码被替换到代码中,可能会造成代码体积的大幅度增长 函数:函数代码只出现于一个地方,每次使用该函数,都会跳转到同一个位置 执行速度 宏:预处理器就完成了宏替换 函数:需要创建栈帧,压参传参,返回值的开销 操作符优先级 宏:由于直接进行替换,容易造成意料之外的错误。尽量多用()来表示优先级 函数:更容易预估结果 参数求值 宏:参数每出现一次都会被重新求值一次 函数:只在调用前计算一次,不会出现副作用 参数类型 宏:宏与类型无关,只要对参数操作合法,使用于任何参数类型 函数:函数参数与类型有关,参数类型不同,执行不同的代码(函数重载) 宏和枚举的区别: 1、宏在预编译阶段进行简单替换,枚举是在编译阶段确定值 2、宏不能调试,枚举可以 3、枚举一次可以定义大量相关的常量,而define只可定义一个 宏和内联函数的比较: 1、宏不能调试,内联可以 2、宏对参数不进行类型检查,内联函数进行 3、宏肯定会被替换,内联是一种建议 4、宏优先级若不注意可能会出现预期之外的结果,内联不会 typedef和define的比较 1、可以其他使用类型说明符对宏类型名进行扩展,typedef所定义的类型名却不能这样做 2、用typedef定义的类型能够保证生命中所有的变量均为同一类型,#define定义的类型则无法保证 static 修饰变量 静态全局变量 作用域仅限于从定义之处一直到文件结尾,失去外部链接属性 静态局部变量 在函数体内部定义,只能在此函数中使用 注:static修饰的变量存放在进程地址空间的静态区,即使函数运行结束,该静态变量的值还是不会被销毁。 修饰函数 使得该函数失去外部链接属性,作用域仅限本文件(避免和别人的文件命名冲突) const 修饰变量:只是具有只读属性,并不是常量 节省空间,提高效率(编译器通常不为普通const变量分配存储空间,而是直接保存在符号表中,不需要多次访问内存) 修饰指针 const int p 等价于 int const p,p的值可以变,但是p不可改 int* const p:p不可修改(p的指向),p可以修改 const int cosnt p:指针p的指向和指向对象的内容都不可修改 修饰函数的参数:代表该参数的值在此函数中不可修改 修饰函数的返回值:代表返回值不可改变(例:const int Fun(void) 函数传参 传参方式 传值调用:接受参数的函数实际上获得了参数的一份临时拷贝 传址调用:直接将参数地址传给调用函数,直接对原参数进行修改 数组传参 一维数组传参:当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。 二维数组传参 int arr[2][3] 1、利用数组指针接收void fun(int (*arr)[3]) 2、利用二维数组接收 void fun(int arr[][3]) 多维数组传参需要提供除最左边一维之外的其他维的长度 指针传参 想要修改指针变量需要用二级指针来接收,实参传当前指针的地址。 结构体 内存对齐 为什么要进行内存对齐? 尽量减少访问内存次数,提高效率。 对齐原则: 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。 为什么要对齐 1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。 如何计算: 注:空结构体大小为1(进行占位),gcc中默认对齐数为4,VS默认为8 内存管理 内存地址空间分布 栈:保存局部变量。栈空间有限,且出了作用域自动销毁(函数栈帧)高地址向低地址生长 堆:动态内存分配的空间,malloc,free。注意内存泄漏 静态区(数据段):保存全局变量和static变量,生命周期伴随整个程序结束 代码段:保存可执行的代码和只读常量 malloc malloc函数注意对返回类型进行强制转换,以及判断是否申请成功 (void*) malloc(int size) 注意: malloc函数申请0字节内存并不会返回NULL指针 malloc申请的是虚拟内存,不是直接申请物理地址 free free只负责将内存释放,并不负责将指针置NULL。此时的指针就是所谓的野指针 注意:malloc和free的次数必须一致 函数调用栈帧过程 1、创建栈帧初始化:push ebp,sub esp xxx,压寄存器,完成初始化 2、创建局部变量,push参数(从右向左的顺序依次压入) 3、push : call指令的下一条指令的地址,一会返回使用 4、再次挪动ebp,esp创建栈帧,取参数进行计算并将返回值放入寄存器返回 5、销毁栈帧,pop掉寄存器,找到对应上一个函数的下一条指令的地址进行回退 语法细节 volatile关键字 volatile关键字解决编译器优化的问题,保证对于该变量的修改都是直接对内存中存放的值进行操作。 sizeof关键字 sizeof括号内的表达式只进行计算,并不改变原始的值 大端和小端 大端:高字节数据存储的低地址中 小端:高字节数据存储在高字节中 判断当前系统是大端还是小端 头文件包含 #include<>:表示预处理到系统规定的路径中取获得该文件 #include " ":表示先在当前目录中找该文件,找不到再去规定的路径下 #pragma预处理 #pragma once:保证头文件只被包含一次 #pragma pack( ):设置默认对齐数 逗号表达式 注意:每个表达式都会进行运算,但是整个逗号表达式值为最后一个表达式的值 C库函数实现 str系列函数 strcpy char* my_strcpy(char* dest, const char* src) 注意:strcpy会将\0也拷贝,但是需要注意目标地址有足够的空间进行拷贝 memcpy和strcpy的区别 1、复制内容不同,strcpy只能复制字符串,memcpy可以复制任意内容 2、strcpy一直复制到有\0,memcpy是按照给定长度复制 strcat char* my_strcat(char* dest, const char* src) strcat也会把src的\0拷贝到末尾,也需要保证空间足够 strcmp int my_strcmp(const char*s1, const char* s2) 若s1>s2,返回1; s1==s2,返回0;若s1<s2,返回-1; strchr 查找一个字符串中的一个字符 strstr 查找子串 mem系列函数 memcpy void* my_memcpy(void* dst, const void* src, size_t num) memmove void *memmove( void* dest, const void* src, size_tcount );
    最新回复(0)