• C语言


    一、编译链接过程

    编译

    1. 预处理

    删除注释,宏替换,头文件展开,条件编译

    1. 编译

    词法分析,语法分析,语义分析,符号汇总(生成汇编代码)

    1. 汇编

    将形成的汇编代码转为二进制代码,形成对应的符号表

    链接

    合并段表以及符号表的重定位

    二、指针和数组

    数组和指针的区别和联系

    联系

    1. 表达式中的数组名就是指针
    2. C语言中把数组下标作为指针的偏移量
    3. 作为函数参数的数组名等同于指针

    区别

    1. 数组名在传参时会退化为指针,指针不会(所以一定不要在函数内部对传参后的数组名进行sizeof大小)
    2. 内存中数组是一块连续开辟的空间,指针只占一个指针类型的大小空间(32位为4字节,64位为8字节)
    3. 数组可以通过下标直接进行访问,指针需要进行计算间接访问
    4. 数组名具有常属性,不能进行++,--操作;指针可以

    数组指针和指针数组区别

    以 int (*p)[n] 为例,从右往左理解,() 高优先级,先定义一个指针,然后看[],说明这是一个指向数组的指针,最后 int 型。

    1. 数组指针(int (*p)[n])
      代表一个指向有n个int类型的数组的指针,也叫行指针
    2. 指针数组(int *p[n])
      代表一个有n个int* 指针的数组
    • 区别:数组指针由于是一个指针,所以内存中只消耗一个指针大小的空间;指针数组是一个数组,消耗n个对应类型指针大小的空间。

    需要注意的点

    1. sizeof字符串指针大小为对应平台下指针大小;sizeof字符串数组为数组个数再+1;

    2. 数组名可看做是一个指针,取数组名地址再进行++操作将移动整个数组大小。

    3. 数组名不能作为左值!!

    三、宏

    宏的优缺点

    1. 优点
      执行速度快,在预处理期间就完成替换
    2. 缺点
      1. 多处调用宏,可能会造成代码的膨胀
      2. 宏的优先级一定要注意,多用括号
      3. 宏与类型无关,不进行类型检查
      4. 带有副作用的宏参数进行多次求值,可能得到意料之外的结果(++, --)
      5. 由于宏在预处理阶段就进行替换,无法调试

    宏和函数的比较

    左值:可被引用的参量

    副作用(side effect):计算表达式时对某些东西(如存储在变量中的值)有修改;

    1. 代码长度
      宏:每次使用宏都会使得宏代码被替换到代码中,可能会造成代码体积的大幅度增长
      函数:函数代码只出现于一个地方,每次使用该函数,都会跳转到同一个位置
    2. 执行速度
      宏:预处理器就完成了宏替换
      函数:需要创建栈帧,压参传参,返回值的开销
    3. 操作符优先级
      宏:由于直接进行替换,容易造成意料之外的错误。尽量多用()来表示优先级
      函数:更容易预估结果
    4. 参数求值
      宏:参数每出现一次都会被重新求值一次
      函数:只在调用前计算一次,不会出现副作用
    5. 参数类型
      宏:宏与类型无关,只要对参数操作合法,使用于任何参数类型
      函数:函数参数与类型有关,参数类型不同,执行不同的代码(函数重载)

    宏和枚举的区别:

    1. 宏在预编译阶段进行简单替换,枚举是在编译阶段确定值
    2. 宏不能调试,枚举可以
    3. 枚举一次可以定义大量相关的常量,而define只可定义一个

    宏和内联函数的比较

    1. 宏不能调试,内联可以
    2. 宏对参数不进行类型检查,内联函数进行
    3. 宏肯定会被替换,内联是一种建议
    4. 宏优先级若不注意可能会出现预期之外的结果,内联不会

    typedef和define的比较

    1. 可以其他使用类型说明符对宏类型名进行扩展,typedef所定义的类型名却不能这样做
    2. 用typedef定义的类型能够保证声明中所有的变量均为同一类型,#define定义的类型则无法保证

    四、static

    修饰变量

    1. 静态全局变量
      作用域仅限于从定义之处一直到文件结尾,失去外部链接属性
    2. 静态局部变量
      在函数体内部定义,只能在此函数中使用

    注:static修饰的变量存放在进程地址空间的静态区,即使函数运行结束,该静态变量的值还是不会被销毁。

    修饰函数

    使得该函数失去外部链接属性,作用域仅限本文件(避免和别人的文件命名冲突)

    五、const

    修饰变量

    只是具有只读属性,并不是常量
    节省空间,提高效率(编译器通常不为普通const变量分配存储空间,而是直接保存在符号表中,不需要多次访问内存)

    修饰指针

    1. const int* p 等价于 int const *p,p的值可以变,但是*p不可改
    2. int* const p:p不可修改(p的指向),*p可以修改
    3. const int* const p:指针p的指向和指向对象的内容都不可修改

    修饰函数的参数

    int function(const x);
    

    代表该参数的值在此函数中不可修改

    修饰函数的返回值

    代表返回值不可改变(例:const int Fun(void))

    六、函数传参

    传参方式

    1. 传值调用:接受参数的函数实际上获得了参数的一份临时拷贝
    2. 传址调用:直接将参数地址传给调用函数,直接对原参数进行修改

    数组传参

    1. 一维数组传参

      当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针

    2. 二维数组传参 int arr[2][3]

      1. 利用数组指针接收void fun(int (*arr)[3])
      2. 利用二维数组接收 void fun(int arr[] [3])
    3. 多维数组传参需要提供除最左边一维之外的其他维的长度

    指针传参

    想要修改指针变量需要用二级指针来接收,实参传当前指针的地址。

    void func(char **p)
    {
    	*p = "hello
    ";
    }
    
    int main()
    {
    	char *p = NULL;
    	func(&p);
    	printf("%s", p);
    	system("pause");
    	return 0;
    }
    

    七、结构体

    内存对齐

    • 为什么要进行内存对齐?
      尽量减少访问内存次数,提高效率。

    对齐原则

    1. 数据成员对齐规则

    结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中比较小的那个进行对齐。

    1. 结构(或联合)的整体对齐规则

    在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

    1. 结构体作为成员

    如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

    为什么要对齐

    1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
    2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

    如何计算

    参考[关于结构体内存对齐总结][https://blog.csdn.net/sssssuuuuu666/article/details/75175108]

    注:空结构体大小为1(进行占位),gcc中默认对齐数为4,VS默认为8

    八、内存管理

    内存地址空间分布

    栈:保存局部变量。栈空间有限,且出了作用域自动销毁(函数栈帧)高地址向低地址生长
    堆:动态内存分配的空间,malloc,free。注意内存泄漏
    静态区(数据段):保存全局变量和static变量,生命周期伴随整个程序结束
    代码段:保存可执行的代码和只读常量

    malloc

    malloc函数注意对返回类型进行强制转换,以及判断是否申请成功。

    (void*) malloc(int size)
    

    注意:

    1. malloc函数申请0字节内存并不会返回NULL指针
    2. 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)
    {
        assert(src != NULL);
        assert(dest != NULL);
        char *ret = dest;
        while ((*(dest++) = *(src++)) != '')
        {...;}
        return ret;
     }
    

    注意:strcpy会将 也拷贝,但是需要注意目标地址有足够的空间进行拷贝

    memcpy和strcpy的区别

    1. 复制内容不同,strcpy只能复制字符串,memcpy可以复制任意内容
    2. strcpy一直复制到有 ,memcpy是按照给定长度复制

    strcat

       char* my_strcat(char* dest, const char* src)
       {
       	assert(dest != NULL);
       	assert(src != NULL);
       	char *ret = dest;
       	while (*dest != '')
       		dest++;
       	while ((*(dest++) = *(src++)) != '')
       	{...;}
       	return ret;
       }
    

    strcat也会把src的拷贝到末尾,也需要保证空间足够

    strcmp

       int myStrcmp(char *s1, char *s2) {
       	assert(s1 != NULL && s2 != NULL);
       	int index = 0;
       	while (s1[index] != '' && s2[index] != '' && s1[index] == s2[index])
       		++index;
       	return s1[index] == s2[index] ? 0 : (s1[index] > s2[index] ? 1 : -1);
       }
    

    若s1>s2,返回1; s1==s2,返回0;若s1<s2,返回-1;

    strchr

    查找一个字符串中的一个字符

    strstr

    查找子串

    mem系列函数

    memcpy

       void* my_memcpy(void* dst, const void* src, size_t num)
       {
       	assert((dst != NULL) && (src != NULL));
       	char* pdst = (char*)dst;
       	char* psrc = (char*)src;
       	while (num-- > 0)
       		*(pdst++) = *(psrc++);
       	return dst;
       }
    

    C语言中void * 为 “不确定类型指针”,void *可以用来声明指针,可以接受任何类型的赋值。

    void *a = NULL;
    int * b = NULL;
    a  =  b;//a是void * 型指针,任何类型的指针都可以直接赋值给它,无需进行强制类型转换
    

    memmove

       void* my_memmove(void* dst, const void* src, size_t num)
       {
       	assert(dst != NULL && src != NULL);
       	char *ret = dst;
       	char *pdst = (char*)dst;
       	char *psrc = (char*)src;
       	if (pdst >= psrc + num || pdst <= psrc)
       	{
       		//正向拷贝
       		while (num--)
       			*(pdst++) = *(psrc++);
       	}
       	else
       	{
       		//反向拷贝
       		pdst += num - 1;
       		psrc += num - 1;
       		while (num--)
       			*(pdst--) = *(psrc--);
       	}
       	return ret;
       }
    
  • 相关阅读:
    Java学习之路(三)--Thinking in Java
    Java学习之路(二)--Thinking in Java
    Java学习之路(一)--Thinking in Java
    ES6中y修饰符合u修饰符
    map数据结构
    Set数据结构
    不确定参数的处理
    函数参数的默认值
    class基础语法
    生成新数组的方法和在数组中查找
  • 原文地址:https://www.cnblogs.com/lvjincheng/p/11187677.html
Copyright © 2020-2023  润新知