C语言知识重点

本文详细介绍了C语言中的指针、数组、宏、静态、const、函数传参、内存管理和函数调用栈帧过程等核心知识点。讲解了指针与数组的联系与区别,宏的优缺点以及与函数、枚举的比较,static修饰变量和函数的作用,const修饰变量和函数参数的意义,以及内存分配中的栈和堆。此外,还涉及了函数调用栈帧的创建与销毁过程,内存对齐的重要性,以及C语言中struct的定义与使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、指针、数组

       1.指针的概念:是指指针所存在的内存单元存放的地址,这个地址指向的是

       变量单元,看似指针指向变量。

       2.指针的使用

              指针初始化:1.  0值常量表达式,将const的0值赋给指针

                                   (const int a =0; int *p=0; p=a;int *p=NULL;)

                               2.类型匹配的变量地址(int *p =&a;)

                               3.另一对象末的下一地址

                               4.同类型的另一个有效地址

       3.指针与数组

              (1)联系:

                     a.表达式中数组名就是指针

                     b.C语言中把数组下标作为指针的偏移量

                     c.作为函数的数组名等同于指针

              (2)区别:

                     a.数组名在传参时退化为指针,指针不会退化。

                     (不能在函数内部给传参进来的指针sizeof)

                     b.数组是连续开辟的内存空间,指针不是,指针只用

                     一个地址空间(32位4个字节,64位8个字节)

                     c.数组可以通过下标随机访问,指针必须经过计算访问

                     d.数组不能进行++、--操作,指针可以

              (3)指针数组、数组指针

                     指针数组是int *p[n],存有n个指针的数组

                     数组指针是int (*p)[n],指向n个int型数组的指针

                     区别:指针和数组的占用内存不同,

                            指针数组是数组,占用n个相应类型大小的空间;

                            数组指针是指针,占用一个指针的内存大小。

              (4)指针与引用(只有在c++中才有引用)

                            引用总是指向一个对象,没有空引用(null reference),指针可以变化指向不同对象。(c++要求引用总是指向一个对象,所以必须有初值)

                            operator[ ]是必须返回的操作符,需要给其赋值,所以要有引用

                     区别:

                            1) 引用只是别名,指针是个实体

                            2) 引用使用不需要解引用,指针需要解引用

                            3) 引用定义时初始化,之后不可变化,指针可以变化

                            4) 引用没有const,指针有const,const指针不可变

                            5) 引用不能为空,指针可以

                            6) sizeof引用得到的是变量的大小,指针是本身存储的地址的大小(4个字节)

                            7) 引用和指针的++不一样

 

 

int *p="abcd";

sizeof(p)是4个字节

int a[ ]="abcd";

sizeof(a)是5个字节,有\0占一个字节

 

数组名看做是指针,取数组名地址++相当于移动整个数组大小

数组名不能是左值

 

结果

 

数组名取地址是代表整个数组

 

指针数组的初始化:

       1. 利用循环初始化

       2. 指针数组定义的时候直接初始化

指针数组在初始化的时候可以看做是二维数组实际上就是二维数组;

不能直接将数组名赋给指针数组,两者类型不同。

 

数组指针的初始化:

       1. int (*p)[2]=NULL;

       p=arr;

       2. int (*p)[2]=NULL;

       p=&arr[0];

 

二、宏

       1.宏是什么?

              语法替换,根据预定义的规则在预处理阶段进行宏展开

       2.宏的优缺点

              优点:执行速度快,在预处理阶段完成替换

              缺点:

                     i. 多处调用宏,会造成代码膨胀

                     ii. 宏的优先级要注意

                     iii. 宏与类型无关,不进行类型检查

                     iv. 带有副作用的宏参数进行多次求值,可能得到意外的结果(++、--)

                     v. 由于宏在预处理阶段就进行替换,无法调试

       3.宏和函数比较

              ► 代码长度

                     § 宏替换后会使得代码的长度变长,增加代码体积

                     § 函数调用的时候都会跳转至函数所在位置

              ► 执行速度

                     § 宏因为在预处理阶段,执行速度快

                     § 函数需要创建栈针,压参传参,返回值开销,速度较慢

              ► 操作符优先级

                     § 宏需要注意优先级,直接替换,会出现意外的结果

                     § 函数的结果可预估

              ► 参数类型

                     § 无关参数类型,不进行类型检查

                     § 函数与参数类型有关,参数类型不同,执行代码不同

       4.宏和枚举的比较

              a. 宏在预编译阶段替换,枚举在编译阶段确定值

              b. 宏不能调试,枚举可以

              c. 枚举一次可以定义多个相关值,宏只能定义一个

       5.宏和内联函数的比较

              a. 宏不能调试,内联函数可以

              b. 宏对参数不进行类型检查,内联函数需要

              c. 宏肯定被替换,内联是一种建议

              d. 宏优先级会出问题,内联不会

       6.typedef和define比较

              a. typedef可以保证生命中的变量均为同一类型,define不能保证

              b. define的类型可以扩展,typedef不可以扩展

      

三、static

       > 修饰变量【static修饰的变量存在静态区,函数运行结束也不会销毁该静态变量的值】

              ○ 静态全局变量

                     § 作用域是从定义处到文件末尾,无外链接属

              ○ 静态局部变量

                     § 在函数体内部可以调用,函数外不可以调用

       > 修饰函数

              ○ 使得函数失去外部链接属性,作用域只在本文件内部(避免函数名冲突)

             

初始化,未初始化的静态变量和全局变量程序会默认初始化为0,

            未初始化的局部变量默认初始化为随机值

静态变量和全局变量的数据都存储在全局数据区。

函数内的一般变量存储栈区,函数结束就会销毁,而静态变量在结束时不会被销毁。

 

注:静态全局变量和全局变量的区别:

       → 全局变量有外部链接属性,作用域是整个工程;

       → 静态全局变量没有外部链接属性,作用域是本文件

       → 使用extern声明可以在别的文件中使用全局变量,静态全局变量不行

      

静态成员函数没有this指针

 

四、const

       Ø 修饰变量

              ○ 只读属性,不是常量!!

              ○ 节省空间,提高效率,编译器通常不为const变量分配存储空间,而是直接保存在符号表,不需要多次访问保存

       Ø 修饰函数

              ○ 修饰函数参数

                     § 参数值在函数内部不可更改

              ○ 修饰函数返回值

                     § 函数返回值不可更改

       Ø 修饰指针(去掉类型名,const离谁近,谁就不可变)

              ○ const int *p等价于int const *p,p值可以更改,*p不能改

              ○ int* const p ,p值不能修改(p的指向),*p可以修改,也就是指向的内容可以更改但是指向的地址空间不可修改

              ○ const int* const p ,p和*p都不可修改,也就是p指向的空间的内容不可修改,p指向的地址空间也不可修改

      

const作为参数,以值传递的方式,减少参数的构造,拷贝,析构函数的重复使用。

 

非const可以转化为const

const不可以转化为非const

 

两个const成员函数的常量性不同可以重载

 

 

const关键字的优点:

1.在C++中可以复用代码,提高程序复用性。

2.相比于#define 可以指定类型,而#define不可以

 

五、函数传参

       * 传参方式

              ○ 传值调用

                     § 接受参数的函数获得参数的临时拷贝

              ○ 传址调用

                     § 直接将参数地址传给调用函数,直接对原参数进行修改

       * 数组传参

              ○ 一维数组传参

                     § 当一维数组作为参数,编译器总是解析为指向其首元素的首地址的指针

              ○ 二维数组传参

                     § 数组指针接收void fun(int(*arr)[3]);

                     § 二维数组接收void fun(int arr[][3]);

              ○ 多维数组传参

                     § 需要提供除最左边的其余维度的长度

       * 指针传参

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

 

六、结构体内存对齐

       ► 内存对齐?

              ○ 编译器为每个“数据单元”安排合适的位置,C语言允许对内存对齐进行干预

       ► 为什么要进行内存对齐?

              ○ 合理的分配地址空间,提高效率,尽量减少访问内存次数

       ► 对齐规则

              ○ 数据成员对齐规则:结构体或联合的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack设定的数值和结构最大数据

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

       ► 为什么要对齐?

              ○ 平台原因(移植原因):某些硬件平台只能从某些特定的地址取特定的数据,不能任意访问数据,否则抛出异常。

              ○ 性能原因:栈应该尽可能在自然边界上对齐,为了访问未对齐的内存,处理器要做两次访问内存,对齐的内存只需要访问一次

       ► 计算内存对齐

              ○ 结构体struct

                     § 结构体必须按照顺序来计算占用字节数

                     § 最后的整体字节数是根据min(max(结构体类型的字节数),x)计算得到的结果

                     § 必须是结果的倍数

              ○ 联合体union

                     § 计算union中最长的占用字节数

                     § 需要时最长字节类型的倍数

              ○ 嵌套类型

空结构体大小为1 ,进行占位,gcc默认对齐数为4,vs默认对齐数为8

 

 

 

七、内存管理

       > 内存地址空间分布

              ○ 栈:保存局部变量,栈空间有限,且出了作用域自动销毁(函数栈帧),高地址向低地址生长

              ○ 堆:动态内存分配的空间,malloc,free,注意内存泄漏

              ○ 静态区(数据段):保存全局变量和static静态变量,生命周期伴随整个程序结束

              ○ 代码段:保存可执行的代码和只读常量

       > malloc

              ○ malloc函数对返回值类型的强制转换,以及判断是否申请成功

                     § (void*)malloc(int size)

              ○ malloc盛情0字节内存不会返回NULL指针

              ○ malloc申请的是虚拟内存,不是直接申请物理内存

       > free

              ○ free只负责将内存释放,并不负责将指针置NULL,如果没有手动将指针置NULL,就会成为野指针,内存会泄漏,这个内存不能再次被使用

       > malloc和free次数一致,成套使用

              int *pp;

              pp = (int*)malloc(sizeof(int) * 10);

              malloc必须经过计算得到初始化的值,并且得强转类型

       八、函数调用栈帧过程

       1. 创建栈帧初始化:push ebp,sub esp xxx,压寄存器,完成初始化

       2. 创建局部变量,push参数(从左向右的顺序依次压入)

       3. push:call指令的下一条指令的地址,一会返回使用

       4. 再次挪动ebp,esp创建栈帧,取参数进行计算并将返回值放入寄存器返回

       5. 销毁栈帧,pop掉寄存器,找到对应上一个函数的下一条指令的地址进行回退

       · 每个函数的每次调用,都有独立的栈帧

 

堆和栈的关系

       一般说的堆栈其实指的是栈,

       1.堆需要显示申请,栈不用显示申请,系统自动完成

              申请和释放成对使用

              C——malloc/free

              c++——new/delete

       2.堆的空间比栈大,大的静态数组就在堆的位置放置,栈空间小,比较深的迭代会使得栈的空间内存急剧缩小。

       3.栈的生命周期较短,一般在函数结束,本函数的栈也就结束使用,堆看具体情况,什么时候生命周期结束,函数的栈就退出

 

 

九、语法细节

       v volatile关键字

              ○ volatile关键字解决编译器优化问题

              ○ 保证对该变量的修改都是直接对内存存放的值操作

       v sizeof关键字

              ○ 计算括号内的字节大小,不修改值本身

       v 大端和小端

              ○ 小端:高高/低低(高字节数据存储在高地址中,反之....)

              ○ 大端:高低/低高(高字节数据存储在低地址中,反之....)

              ○ 判断当前系统是大端还是小端??

              ○ 解决的问题不同:小端可以避免类型转换时内容变换,不需要调整地址

                                     大端可以清楚的看到符号位一直在高位,看清正负

大小端的判断代码

                  

 

       v 头文件包含

              ○ #include<>:表示预处理到系统规定的路径中去获得该文件

              ○ #include"":表示先在当前目录找该文件,找不到再到规定目录寻找

       v #pragma 预处理

              ○ #pragma once:保证头文件只被包含一次

              ○ #pragma pack():设置默认对齐数

       v 逗号表达式

              ○ 整个表达式都会进行运算,但是最终的结果是最后的表达式运算结果

 

十、C语言/c++中struct结构体的定义和使用

       C语言

              ○ struct A{

                     int aa;

                     int _a;

              }a;

                     § 在实例化的时候,这种方法需要带上struct定义结构体对象

                     § struct A aaa;

                     § 这里的a是定义了类A和类A的对象实例a

              ○ typedef struct B{

                     int bb;

                     int _b;

              }b;

                     § 在实例化的时候,这种方法不需要带上struct,typedef相当于是定义了类B的别名是b

                     § b bbb;

                     § b是类B的别名,b相当于是struct B

              ○ 如果不写类名也可以

                     typdef struct {

                            int a;

                     }Stu;

                     § 不写类名就不能struct xxx xx;可以使用别名进行实例化,Stu s;

              ○ typedef struct{

                     int num;

                     int age;

              }aaa;

              typedef aaa bbb;

              typedef aaa ccc;

              上面和下面是一样的;

              typedef struct{

                     int num;

                     int age;

              }aaa,bbb,ccc;

              这里的bbb,ccc相当于aaa的别名

              三者都是结构体类型

              ○ 在C中,struct结构体不能包含函数,C++可以

C++

       ○ struct Student{

              int x;

              int y;

       };

              § C++中可以直接利用类名实例化

                     □ Student stu;

       ○ struct Student{

              int x;

              int y;

       }Stu1;

              § 这里的Stu1是对象,可以直接Stu1.x;Stu1.y;调用结构体内的对象

       ○ typedef struct Student{

              int x;

              int y;

       }Stu2;

              § Stu2是一个结构体类型

              § 实例化的时候必须Stu2 s2; s2.x;s2.y;这样访问

       ○ typedef struct{

              int num;

              int age;

       }aaa,bbb,ccc;

       C++中有typedef表示aaa,bbb,ccc都是结构体类型

       struct{

              int num;

              int age;

       }aaa,bbb,ccc;

       如果没有typedef,则aaa,bbb,ccc是三种不同的对象

 

结构体typedef的用途:

       1. 定义一种类型的别名,不是简单的宏替换,可以用作同时声明指针型的多个对象

              char* pa,pb;//只声明了一个指针对象和一个字符变量

             

              typedef char* PCHAR;

              PCHAR pa;

              PCHAR pb;

              在需要大量指针声明的地方使用别名无疑是方便快捷的

       2. 在旧的C语言中,声明struct新对象的时候,需要写struct+类名+xx;

       有了typedef就可以省略struct,在大量使用声明定义的时候会更加方便

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值