C语言

旧街凉风 提交于 2019-11-26 02:31:56

一、编译链接过程

编译

  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\n";
}

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++)) != '\0')
    {...;}
    return ret;
 }

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

memcpy和strcpy的区别

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

strcat

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

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

strcmp

   int myStrcmp(char *s1, char *s2) {
    assert(s1 != NULL && s2 != NULL);
    int index = 0;
    while (s1[index] != '\0' && s2[index] != '\0' && 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;
   }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!