C
宏
宏
[!important]
宏是一个纯粹的文本替换
1 |
当宏体包含运算符时,需要增加括号来==限制==宏体和外部的计算优先级
1 |
条件编译
条件编译语句:编译中的分支语句
1 |
提示编译语句
1 |
宏函数
[!important]
宏函数可以减少内存开销
简单使用
1 |
|
使用\可以实现宏函数的多行显示
1 |
|
[!note]
在多行显示的代码中,可以看到(a++)后没有==冒号==
实际上由于宏是纯粹的文本替代,所以在调用FUNC函数时会加上冒号,所以在宏体中不需要加冒号
宏函数与分支语句的联动,如果不适用花括号来控制分支语句的作用域,可能会出现不符合预期的结果
1 | if(0) |
要保证多分支语句的正确运行,我们可以使用do-while语句来控制
1 |
|
调试宏
使用#来实现字符化
1 |
|
使用##进行连接,一般用于有规律的变量
1 |
|
使用系统自定义的宏来打印相关信息
1 | __FUNCTION__ //用于打印函数名 |
关键字
关键字可以被分为3大类:
- 数据类型关键字
- 修饰关键字
- 逻辑关键字
sizeof
[!important]
在编译过程中就已经能算出大小,并不是在运行过程中计算的
sizeof并不是函数,而是关键字,所以我们也可以使用sizeof a,来定义一个常量,但是这里需要注意的是a需要在之前已经定义了,且不可以使用sizeof(int)这样的表示
数据类型关键字
标准类型
char
char类型的关键字是最小内存空间的数据类型,也是最适合操作硬件的数据类型。
[!tip]
我们知道硬件上最小的状态是高低电平,对于软件来讲就是0和1状态,那C语言中为什么没有byte这个数据类型关键字呢?
因为0-1只有这两个状态,对于内存来讲当然是充分利用,但对于CPU来讲处理效率非常低。比如要把32(0b100000)这个数据从内存读到CPU内部寄存器,至少需要执行6个指令周期(1个指令周期:寻址->发读的控制信息->数据通过数据总线送到寄存器)才能读到0b100000。所以兼顾CPU性能和内存管理,计算机科学家把操作内存最小单位为8bit,也称1byte,也就是char类型,可以表示256种状态。当编译器看到
chār关键字的时候,就知道圈定的内存大小是1byte:
[!NOTE]
这里的
char a=10;相当于在内存中圈出一块1byte的内存,内存的标签叫a(也可以理解成这块内存属于a),内存里面的值是10Q:想一想,声明和定义的区别?extern char a和char a;
A:声明没有分配内存,定义会分配内存。(声明实际上是引用,并不会分配空间)
int
int类型是最适合CPU的数据类型,大小和编译器有关

为了充分发挥CPU的数据处理能力,数据总线尽量要充分利用,在系统一个周期内所能接受的最大处理单位就是int。所以对于32位的CPU,int就是32bit(4byte),16位的CPU,int就是16bit(2byte)。对于一个数字常量默认就是int的,体会以下代码:
void
void一般用于占位符
[!tip]
为了减少编译器报未使用参数的警告,我们可以使用void来规避
1
2
3
4
5
6 int main(void)
{
int a = 0;
(void)a; //此时可以规避编译器出现警告的情况
return 0 ;
}
[!tip]
void*是一个通用类型指针,可以将这个指针转化为任意类型的指针
自定义类型
struct
C语言通过struct关键字表示新的组合类型,内存表现为累加且对齐,我们叫这种数据类型为结构体。声明规则如下:
1 | struct 结构体类型名 |
[!note]
当我们申明了一个结构体时会分配内存吗?
1
2
3
4
5 struct abc
{
int a;
int b;
};答案是否定的,注意,申明不会申请内存
[!tip]
数据对齐和CPU总线有关,如果CPU总线为4字节,那么地址4字节对齐会对CPU的读写有利

union
union关键字表示新的类型叫联合体,内存表现为:==共享一份内存==(以最大数据类型作为分配空间)和内存的首地址,申明规则如下:
1 | union 名字 |

struct+union
1 |
|
[!tip]
注意,编译器实际上是会对数据体进行对齐的,所以使用编译器选项
__attribute__((packed))可以防止编译器进行字节对齐,也就是不会自动的填充0。同时,上述的代码中的union并没有命名,这样不报错的原因是在结构体的开头增加了
#pragma anon_unions
enum
enum关键字表示的类型叫枚举类型,内存表现为:内存根据定义值的大小默认选择整数常量大小(int),如果超出int大小,编译器会选择更大的整型类型,比如Iong,所以内存大小不是固定的。声明的规则如下:
1 | enum 名字 |
[!note]
实际上,枚举类型是建议型类型,如下列代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14 enum abc
{
A = 1,
B = 2,
C = 3,
};
enum abc func(void)
{
return 10;
}
enum abc a = func(); //此时打印a的值可以是10,所以说是一个建议类型
地址(指针)类型
C语言中,地址类型也称指针类型,圈定的内存用来存放地址编号的值。==指针类型的内存表现限编译器有关或者说跟CPU的地址总线有关==。在32位的系统中,指针类型占用4byte,在64位的系统中,指针类型占用8byte。

[!TIP]
指针所占用的内存空间和申明的类型没有关系,只和编译器和CPU的地址总线宽度有关
申明类型只和访问的数据的范围有关,如int类型的指针就要访问4个字节的数据,char类型的指针只能访问1个字节的数据
1 |
|
[!tip]
- 首先,p也是一个变量(没啥神秘的),这本质上也是圈定一块内存,内存的大小跟编译器有关,p是这块内存的标签。到这里和的在变量的视角中一摸一样
- 然后,p的特殊性在于,它圈定的那块内存,存放的不是一般的值而是一个地址编号。所以除了通过标签读出这个地址编号外,C语言还提供一种直接访问这个地址编号的方法,那就是在p前面加一个
*号。举一个形象的例子:
你要去访问A朋友家,第一种是直接让A带你去,第二种是P知道A家的地址,通过P带你去。a和*p就是这种感觉
typedef
typedef:关键字是给数据类型起一个别名,让程序的可读性更高,特别是复杂的类型(如函数指针类型),做到见字知意。使用的规则如下:
1 |
|
修饰关键字
auto
auto关键字是最无感的存在。编译器在默认缺省的情况下,所有变量都是uto的。你就当做它不存在,不曾来过。
register
register关键字是一个建议性关键字。设计的初衷是想要定义的变量放到cpu的寄存器中,但是往往编译器看到这个关键字的时候,发出一句呐喊:臣妾做不到啊。寄存器相对于内存,cpu在访问速度上更快,但也很稀缺,不是说想放就放的。所以编译器只能尽量的去做,它不保证一定能做得到,大家也就不要奢望了。
[!tip]
但值得注意的是加上registerf修饰后的变量,无法通过&来取地址。即使大概率放进寄存器会失败,但编译器就不给你寻址,万一成功了呢?编译器还是有梦想的。
static
在C语言中static关键字有两个作用:
- 规定变量的内存从静态全局数据区分配;
- 限定变量或函数的作用域
生命周期:不管是修饰局部变量还是全局变量,最后都在静态全局数据区分配。这意味着变量在整个程序的生命周期内都是有效合法的。
限定范围:限定全局变量只在当前定义的文件可见,其他文件即使加上exterr修饰,也无法引用。会编译报错
extern
extern用来声明一个没有被static修饰的变量或者函数。
在架构设计中尽量不要用extern,会导致代码耦合度非常高而且不太可控。建议使用get、set方法来获取成员变量的值,从而避免出现文件的互相调用。
1 | static int a = 0; |
extern"C",用C++的编译器(g++)按照C的规则进行编译。C++和C语言在编译规则上有很多不同,所以在C++中想完全复用C写的代码,就可以用extern"C"来修饰。这种在C++使用C的Iib库的时候比较常见。
[!tip]
实际上,extern c就是在c++代码中使用c的编译规则去编译代码
const
const是constant的缩写,是恒定不变的意思,被修饰的变量经常被人误解成常数或者常量。这个理解不太准确,准确的理解应该是read only。.本质上还是变量,编译器只能尽量的不让你去修改,但是实际上总有一些方法可以做到修改const修饰变量的值,往往都是一些异常的操作导致(比如数组越界、指针越界访问、栈溢出等等)。我们要建立一个正确的编程理念:const修饰的变量技术上能改,但是不要去改。
1 | //const 数据类型 变量名 or 数据类型 const 变量名 |
[!tip]
实际上这里的变量a和b也是可以修改的,之前说过,访问一个变量有两种方法:
- 通过标签去访问值,如访问a和b
- 通过地址去访问值,如访问&a和&b
上面的代码如果使用地址来修改就可以修改值,在c++中编译器会自动拦截修改const类型的值,但是c中不会
==const修饰指针==
很多同学对const修饰指针变量,有时候分不清楚是修饰指针变量还是限定指针指向的内容。今天教大家一个方法,叫"近水楼台先得月":
- 忽略数据类型
- 观察const向右靠近什么
- 靠近 *:表示指针指向的内容不能变
- 靠近变量:表示指针的指向(指针中存的内容)不能变
- 既靠近*又靠近变量:表示指针内容和指针指向的内容都不能变
[!tip]
在定义指针的时候,编译器会先向右遍历,再向左遍历,然后才会确定一个变量的具体限制
1 |
|
使用const可以提高运行效率:如果用const修饰,编译器会直接把立即数200赋值给变量a,而没有const修饰的则每次都需要读取内存中ABC值。这样用const修饰时,执行效率就比较高(我们知道读取内存是相对慢的操作)
1 |
|
1 | //int a ABC; |
[!tip]
Cost设计的初衷就是为了代替宏,消除它的缺点,继承它的优点。
- 编译器处理方式:define宏是在预处理阶段展开;const变量是编译运行阶段使用
- 安全检查:define宏不做任何类型检查,仅仅是展开;const变量编译阶段会执行类型检查
- 内存位置:define宏在代码段;const常量可以在静态全局数据段、栈中
volatile
volatile关键字用于告诉编译器,被声明为volatile的变量的值可能在程序的控制之外发生变化,因此编译器不应该对其进行某些优化。主要用于处理硬件寄存器、中断服务程序和多线程等情况:
下面的代码中,如果buff变量是uart的DR寄存器映射,即使代码中设有地方更新buff的值,也应该告诉编译器不要优化,如果有地方读取buff的值的时候,都需要从内存中读取,因为外部uart会触发更新。
1 |
|

逻辑关键字
在Cpu的眼里很单纯,程序指针(PC)指到哪里就执行哪里,默认情况下顺序往下执行,而逻辑关键字作用就是改变PC指针的指向,这个最基本的思想就构成了程序里各种逻辑设计(条件、选择、跳转、循环)。

- 条件关键字:if-else
- 选择关键字:switch-case-default
- 循环关键字:for、while、do-while
- 跳转关键字:continue、break、return、goto
运算符
程序的本质是逻辑和数据。狭义上运算是指是数学运算,广义的运算是数据处理。在C语言中运算符分为:算数运算、逻辑运算、位运算、赋值更新、内存访问。这里提一个理念:对于运算符的优先级问题,不建议死记硬背,用()万能钥匙来人为定义优先级。不要吝啬用(),()会让你的程序变得清晰可读。
算术运算
加减乘除大家从小学就开始学了,所以非常容易理解。这里提一个观念就是:对于CPU来讲,加减法的运行速率比乘除要快。这是因为CPU进行运算主要依赖于算术逻辑单元(ALU)和浮点运算单元(FPU),而ALU的本质是累加器,所以加减法在硬件处理就很快,乘除法有时候编译器会转换成动加减法或者移位操作。随着计算机的发展,现在已经有了"乘法器"的硬件支持,速度上也已经很快。
我们感受一下编译器对乘除法的优化:
-
直接调用"乘法器"进行运算
1
2
3
4
5
6
7
8
9
int main()
{
int a = 10;
int b = a * 34;
printf("b =%d \n",b);
return 0;
}1
2
3
4movl $16,-8(%rbp)
movl -8(%rbp),%eax
imull $34,%eax,%eax //这里调用乘法器
movl %eax,-4(%rbp) -
将乘法转换为左移位和加法操作
==mod运算==:表示取余数
一般可用于
-
循环队列的索引更新
1
2
3
4
5
6
7
8
9
int buff[LEN];
int index = 0;
void data_process(int data)
{
index = index % LEN;
buff[index] = data;
index++;
} -
生成[L,R]区间内的随机数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int* generate_array(int n,int L,int R)
{
int*array = (int*)malloc(sizeof(int) * n);
srand(time(NULL));
for (int i = 0;i < n;i++){
array[i] = rand()%(R - L + 1) + L;
}
return array;
}
void print_array(int* data,int n)
{
for (int i = 0;i < n;i++)
{
printf("%d "data[i]);
}
printf("\n");
}
int main()
{
int *arr = generate_array(10,0,10);
print_array(arr,10);
return 0;
}
//运行结果
//04708611035 -
%还可以做占位符,在格式转换时用
1
2
3
4
5
6
7
int main()
{
int a = 10;
printf("a %d %x %o \n",a,a,a);
return 0;
}
位运算
-
左移
<<:在没有溢出之前,每移动一位等价于乘于2,且移位后右边补0 -
右移
>>:每移动一位等价于除以2,signed类型和unsigned在右移的时候有差别:- 如果是负数,移位后补1,所以负数通过右移永远不会等于0
- 正数补0
-
与或非
1
2
3|:1|1=1; 1|0=1; 0|1=1; 0|0=0 ->俗称置位器(set)
&:1&1=1; 1&0=0; 0&1=0; 0&0=0 ->俗称清零器(c1r)
~:~(0x80) = 0x7F -> 按位取反 -
位异或
^
逻辑运算
-
条件运算:&&(与)||(或)
A && B //只要A为假,B就不会被执行
A || B //只要B为真,B就不会被执行
-
大小判断:==、<、>、>=、<=、?:
[!tip]
建议在使用
==的时候,可以将数字放在==的左边,变量写在右边,这样如果写成=编译器就会报错拦截出来 -
条件取反:!
使用取反可以实现二值化处理
1
2
3int a = xx;
unsigned int b = 0;
b = !!a; //将非零的值a归零化到1 -
赋值更新:=、+=、-=、&=、|=、++、–
赋值更新的本质是:通过标签(变量名)改变内存中的值
内存操作
- 函数访问
():用于访问函数名圈定的地址 - 取值操作:
[]*->.
- 取址操作
& - 内存打包
{}:结构体、联合体、枚举、函数就是典型的通过{}进行打包
函数视角
希望大家养成一个好的编程"洁癖":Don’t Repeat Yourself.,不要写重复的代码。所以当别人问你函数有什么用时,你就可以这么回答他。C语言是一个面向过程的语言,即面向方法(函数)编程(C++/门ava是面向对象编程),所以理解和应用函数的重要性不言而喻。
函数的世界


函数的三大属性
将上面的栗子进行抽象,就可以得到函数的三大属性:输入参数、返回值、函数名
- 函数名(地址)
- 输入参数(可多个)
- 返回值(至多一个)
1 | //输出:函数名:输入 |
[!tip]
编译器只要看到有这三大属性,就判定为函数(即使函数体是空的)
函数名本质上是一个==地址标签==,如果知道函数的地址,就可以直接用()调过去
函数的参数传递
参数传递的本质
调用函数时,需要传入和返回参数。传入和返回的参数过程本质上是:==内存拷贝==。既然是拷贝,那一定存在两个对象:目的地(dst),源(src)。在C语言中,传入参数时,目的地叫==形参==、源叫==实参==:返回参数时,目的地和源都叫返回值
1 |
|
[!note]
在执行函数的时候实际上是一个内存拷贝的过程,我们知道每次调用一个函数,在内存中都会为这个函数分配一部分内存空间作为函数栈
- 当main调用show的时候,内存中会申请一个栈,用于存函数中的变量,比如这里的num中的10086就会拷贝到show函数的形参a中
- 当对a进行自增完之后,返回的值同样会返回到main函数的栈中
- 对show函数的空间进行销毁
block-beta columns 5 space main space show space num data1["10086"] space data2["10086"] a data1 --> data2 ret data3["10087"] space data4["10087"] return data4 --> data3
值传递
==对数据进行隔离和保护==
因为存在拷贝的机制,值传递的时,不会对调用者的源数据进行破坏,所以值传递对数据起到保护和隔离的作用。体会一下下面的例子:
1 |
|
另外,大家要养成一个思维习惯:当函数结束时,函数里的局部变量(形参也是局部变量)都会被”销毁",即使内存中原来的值还存在,但已经不受到系统保护,数据随时可能被覆盖、改写等等。所以当我们要返回一个局部变量的指针时,需要考虑指针指向的内存的生命周期。
[!tip]
恢复栈中的局部变量:在分析crash问题的时候,通常需要分析调用栈,可以通过保存之前的栈针地址,查看原来内存的值。可参考:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static unsigned long addr = 0;
void show(void)
{
int a = 10086;
int b = 10010;
asm volatile("movq %rbp,addr(%rip)");
}
void main(void)
{
show();
int *p = (int*)addr;
printf("show bp 0x%1x,a =%d,b =%d In",addr,p[-1],p[-2]);
}
//程序运行结果:
//show bp 0x7fffff2cacbe,a 10010,b 10086
地址传递
地址传递本质上跟值传递一样,只不过这个值有特殊的含义:代表了一个地址编号。地址传递一般用于返回结果和连续空间传递。
1 |
|
C与面向对象
C语言是面向过程的语言,但我们可以借助C++/JAVA面向对象思想,实现类似"继承"、“多态”、“封装”、"重载”的功能。从而服务于架构设计。
C与继承
继承是使用已存在的类作为基础建立新的类的技术。新的类可以增加新的数据类型或者方法,也可以使用父类的功能,但不能选择性的继承父类。通过继承可以很好的复用以前的代码,提高开发效率。
1 | struct parent_class |
C与封装
封装的概念是指:将抽象的数据和行为(成者方法)相结合,形成一个有机的整体。是面向对象思想的核心,目的是增强安全性和简化编程,使用者不必理解具体的实现细节,通过调用接口使用数据成员。
上节代码中的函数指针就是就做到了封装的概念,通过函数来改变一个对象的值
C与多态
多态的概念是指:通过父类的指针来调用子类中的方法。作用是把不同的子类对象当做父类来看,屏蔽不同子类之间的差异,用于适应需求的不断变化,本质上是面向抽象编程,在C++中,多态的实现是通过虚函数来实现。体会一下以下这个例子:
1 |
|
C与重载
重载的概念是指:在同一个范围中声明几个同名函数,但同名函数的形参不同(个数、类型、顺序),一般是用来处理功能类似但是数据类型不同的问题。严格意义上C的编译器不支持重载,但我们有一些方法达到相同的目的。
[!tip]
printf函数就是典型的具有重载效果的函数
可变参数函数
可变参数函数的格式如下:
1 | void va_func(强制参数,可变参数) |
- 强制参数有至少有一个,代表以一种规则,由函数定义者自行定义和解析。
- 可变参数可以有多个,函数定义者和调用者自行决定,实际上可变参数
实际上,可变参数函数的实现依赖的是四个主键:
-
va_list:实际上是一个结构体数组,但是结构体中只有一个元素
1
2
3
4
5
6
7va_list = struct _va_list_tag
{
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
}[1] -
va_start:定位到可变参数的地址
-
va_arg:遍历可变参数
1
type va_arg(va_list ap, type);
-
va_end:终止va_list遍历
1 |
|
[!important]
注意:这里使用可变函数列表的时候,需要引入
#include <stdarg.h>的头文件
回调函数
1 |
|
弱连接函数
在驱动设计时经常用到。weak修饰的函数属于弱连接函数,==当前工程中如果有定义跟其相同的函数时,weak修饰的函数会被编译覆盖==,达到overridel的效果。
==1.c==
1 | //1.c |
==2.c==
1 |
|
最终执行的效果为:
1 | config one |
Solid设计原则
solid设计是面向对象的原则,是由Gang of Four(四人帮),即Erich Gamma,Richard Helm,Ralph Johnson&John Vlissides四人的《设计模式》
(1995年出版)是第一次将设计模式提升到理论高度,并将之规范化。
solid有五个设计原则:
-
S:单一职责原则:一个类只负责一件事情。在C语言中,就是一个结构体在设计和封装的时候只负责一个事情。 -
O:开闭原则:软件中的对象(类、模块、函数等)应该满足对于拓展是开放的,对修改是封闭的。 -
L:里氏替换原则:简单来讲就是子类可以去拓展父类的功能,但不能改变父类原有的功能。技术上,所有引用基类的地方必须能够透明的使用其子类的对象。价值:继承一定程度上破坏了封装,此原则侧用来包含封装性。 -
I:接口隔离原则定义:不应该强迫用户端实现一个它用不上的接口。具体两个的例子:
-
抽象了面积和体积的两个接口,客户瑞在使用的这个接口的时候会发现,对于正方形来说设有体积,所以实现体积的函数对它来说就是冗余的。
-
抽象了器件初始化和读取数据的接口。对于LED灯来说,一般都是控制型,没有必要实现一个虬ED数据的接口,所以对LED来说,读取数据就是冗余的。

上图中的红色部分就是冗余设计
-
-
``依赖倒置原则
高层次模块不应该依赖低层次的模块,实际上是面向抽象编程思想,这样可以降低代码间的耦合度,使其看扩展、易维护。举一个驱动的
列子:荏这个架构设计中,IMU有很多公共的业务逻辑:初始化、读取数据、数据校准等等,这些业务逻辑不应该耦合具体驱动的接口,而是
依赖于抽象接口:同时驱动与驱动之间也不能互相调用,而是调用抽象接口。后续的器件增加拓展,只需要修改驱动部分,抽象层都不会改
动。
内存视角
前面在学习数据类型关键字的时候我们了解到,定义一个变量的本质是在内存中圈定一块特定大小的内存。本章将会对内存的位置、生命
周期、操作权限等做更多的深讨。

内存分布
内存分布分成静态区和动态区,静态区在编译时已经决定了内存分配大小,动态区是运行时分配。

1 |
|
[!tip]
函数名实际上是一个指针常量,所以可以通过函数名来打印地址
堆和栈的生长空间是相反的
代码段
代码段只能读不可以写,写会触发Segmentation fault(core dumped),俗称"段错误"。在整个程序生命周期内合法有效。
1 |
|
只读数据段
只读数据段存放字符传常量,地址比代码段更高。权限是:只能读不可以写,写会触发Segmentation fault(core dumped),在整个程序生
命周期内合法有效。
1 |
|
全局数据段
全局变量和局部static修饰的变量会放在全局数据段(data/bbs),允许读写,在整个程序生命周期内合法有效。
1 |
|
堆空间
堆空间在运行时由程序员来分配(malloc)和释放(free),可读可写,生命周期是程序员来决定。值得注意的是,如果在一个函数中分配
堆内存,函数执行结束后māoc空间不会被释放,如果不手动进行释放的话,会造成内存泄漏。
1 |
|
栈空间
栈空间指的是在函数运行时的上下文分配的空间,可读可写,生命周期在函数执行结束后结束。看下面的一个例子:buff和s都是局部变量,系统会给buff分配sizeof(“hello world”)栈空间,而s的栈空间是4byte,它指向只读数据段中的字符串"hello world"的地址编号。所以这里返回buff就会有问题,返回指针没有问题。
1 |
|

内存溢出
内存溢出指的程序运行过程中,访问超过其分配空间范围的内存区域。
栈溢出
-
递归函数:递归函数如果没有正常的退出条件,最后会把整个栈空间消耗殆尽,最后出现段错误。
1
2
3
4
5
6
7
8
9
10
void try_overflow(void)
{
int a = 10;
try_overflow();
}
void main(void)
{
try_overflow();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14#运行结果:
Segmentation fault (core dumped)
#汇编
try_overflow:
.LFBO:
endbr64
pushq %rbp
movq %rsp,%rbp
subq $16,%rsp #分配栈空间的内存
movl $10,-4(%rbp)
call try_overf1ow #反复调用
nop
leave
ret -
栈缓冲区溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main(void)
{
char arr[N];
char arr1[] = "hello world";
strcpy(arr,arr1); //工程上建议不要用strcpy,用strncpy,因为原理上是使用\0作为结束符的
//strncpy(arr,arr1,N);
printf("arr:%s \n",arr);
printf("arr1:%s \n",arr1);
}
//strcpy.运行结果
//arr:hello world
//arr1:d
//strncpy.运行结果
//arr:he1 1o wor1hel1 o world/原因是arr长度不够,copy N个字符中缺少结束符'\0'
//arr1:hello world堆溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
void main(void)
{
char *str = (char*)malloc(sizeof(char)*N);
char* strl = (char*)malloc(sizeof(char)*N);
strcpy(str1,"hello world");
strcpy(str,"hello world");
printf("str:%s \n",str);
printf("str1:%s \n",strl);
}
指针
前面在介绍指针类型时,已经知道指针变量圈定的内存大小跟编译器有关或者说跟CPU的地址总线有关。在32位的系统中,指针类型占用4byte,在64位的系统中,指针类型占用8byte。指针变量的值有特殊意义,代表了一个地址编号,本章讲重点讲指针访问内存的规则和技巧。另外,希望大家对这两个概念:指针和指针变量,有清晰的区分。我们一般说指针,代表就是这个指针变量的值,即内存地址;指针变量就是一个变量,用于存放内存地址。

指针访问内存
[!note]
要访问指针指向的地址,那就必然需要回答两个问题:
- 内存的可读可写性是什么?
- 内存的访问规则是什么?
指针变量初始化
大家要养成思维习惯:每当看到一个指针变量,要对其值的合法性保持敬畏。
1 |
|
空指针和野指针
空指针很好理解,就是值为0(NULL)的指针变量,如果访问了0地址就会出现非法访问的错误。野指针是指值为非法地址的指针变量。野
指针在实际工程中有很大的危害,会造成代码的稳定问题(不可预知的bug)。要形成free后,把指针变量置空(NULL)的习惯。
空指针的例子:
1 |
|
野指针的例子:
1 |
|
[!tip]
NULL位于stddef.h文件中
指针访问内存规则
标准数据类型指针
我们前面学过几种指针访问内存的方式:* p、p[x]、p->X。那每次访问的大小是多少呢?我们知道指针变量的定义的形式是:数据类型*p;所以每次访问的大小由前面修饰的数据类型决定。
1 |
|
运行结果为:
1 | *p=0x12345678 |
[!tip]
不管大端还是小端,指针p和p1都是指向低地址。
连续空间类型指针
1 |
|
可以通过结构体成员的地址从而获取结构体的地址,如下代码,假设现在有一个结构体abc,先已经知道c的地址,求这个结构体的地址?
首先先设这个结构体的体的偏移地址是x,那么则有:
1 | &(struct abc*)x->c - x = sizeof(struct abc) |
1 | //Linux container_of宏的思想 |
函数类型的指针
1 |
|
[!note]
建议使用
typedef来重命名函数,使代码通俗易懂
指针运算
算术运算
指针变量本质也是一个变量,所以进行算数运算语法上是可以的。但是实际的工程应用中,指针变量的乘除法没有意义,更多的是加减法。总的来说,+运算符用于指针的算术运算,允许将指针移动到任意偏移位置,而++运算符是+运算符的特例,它递增指针指向的位置,移动一个对象的大小。看一个例子:
1 |
|
逻辑运算
逻辑运算中,判断两个指针是否相等比较常用,其他的不常用。另外,只有两个相同类型的指针比较才有意义,一般编译也会包警告,如果类型不同。
1 |
|
多级指针
多级指针本质上也是一个指针,也需要一个指针变量来存放。这个指针变量的内存大小跟一级指针一样,跟系统有关。
1 | int **p |
*p第一*决定变量p是一个指针变量**p第二*决定指针p访问内存规则是以==指针类型==进行访问,所以*p的值还是一个指针(地址)- int决定指针(
*p)访问内存规则是以int类型进行访问,所以*(*p)是int类型

1 |
|
指针的地址传值
1 |
|
无序变有序
多级指针用于物理无序映射到逻辑有序的数据结构设计:
1 |
|

编译器指令
pragma
在 C语言 中,#pragma 是用于为编译器提供特定指令的预处理指令。具体的使用和支持的 #pragma 类型取决于 编译器(如 GCC、IAR、Keil、MSVC 等)。
基本语法
1 |
如果编译器遇到它不支持的 #pragma 指令,通常会忽略它,而不会产生错误。这使得 #pragma 在处理平台相关编译选项时非常灵活。
常见使用场景
once
防止头文件重复包含,是一种常见的替代 include guards 的方式。
1 |
|
- 作用:确保头文件在一次编译过程中只会被包含一次。
- 支持:大部分现代编译器(如 GCC、Clang、MSVC、IAR)。
pack
用于结构体对齐控制。
1 |
|
- 作用:确保结构体字段按照指定的字节数对齐。
push和pop用于保存和恢复对齐状态。 - 应用:减少内存浪费,或在与硬件设备通信时保持字节一致性。
- 支持:GCC、MSVC、IAR 等大部分编译器。
location (IAR 特有)
用于指定变量或函数的内存地址。
1 |
|
- 作用:将变量或函数放置在特定的内存地址中。这在嵌入式开发中很常见,比如初始化存储在特定 RAM/ROM 地址的配置数据。
- 支持:IAR 编译器。
warning
控制编译器警告的显示和抑制。适用于 MSVC 或 GCC。
1 |
|
- 作用:用于临时禁用某些特定的警告。
- 支持:MSVC,GCC 使用
#pragma GCC diagnostic。
diag_suppress / diag_default / diag_error(IAR)
用于控制 IAR 编译器的警告或错误。
1 |
|
optimize(GCC/MSVC)
用于控制代码优化级别。
1 |
|
- 作用:临时启用或禁用特定区域的优化。
- 支持:GCC、MSVC。
section(IAR)
用于将代码或数据放入指定的段中,常用于嵌入式系统的内存管理。
1 |
|
- 作用:将数据或代码放入特定段,方便嵌入式开发中的内存分区。
- 支持:IAR 等嵌入式编译器。
attribute
在 C 语言中,__attribute__ 是 GCC 编译器提供的一种扩展,用于指定函数或变量的特性。它可以帮助编译器优化代码或改变函数的行为。
函数属性
noreturn
- 作用:告知编译器该函数不会返回,通常用于
exit()、abort()等函数。 - 用法:
1
2
3
4
5
6void my_exit() __attribute__((noreturn));
void my_exit() {
// 终止程序
exit(0);
}
pure
- 作用:表示函数是纯函数,不会产生副作用,返回值只依赖于输入参数。
- 用法:
1
2
3
4
5int square(int x) __attribute__((pure));
int square(int x) {
return x * x;
}
malloc
- 作用:指示返回的指针指向未初始化的内存,告诉编译器该函数会分配内存。
- 用法:
1
2
3
4
5void* my_malloc(size_t size) __attribute__((malloc));
void* my_malloc(size_t size) {
return malloc(size);
}
变量属性
section
- 作用:将变量放置在特定的内存段中,常用于嵌入式开发。
- 用法:
1
int my_var __attribute__((section(".my_section")));
aligned
- 作用:设置变量的对齐方式,确保变量在特定的内存边界上。
- 用法:
1
int my_array[10] __attribute__((aligned(16)));
参数属性
unused
- 作用:防止编译器对未使用的参数发出警告。
- 用法:
1
2
3void my_function(int unused_param __attribute__((unused))) {
// 该参数可以不使用
}
对齐属性
结构体对齐
- 作用:确保结构体的某些成员或整个结构体的对齐方式。
- 用法:
1
2
3
4struct my_struct {
char c;
int i __attribute__((aligned(4))); // 确保 i 在 4 字节边界上
};
性能优化
hot 和 cold
- 作用:用于提示编译器对频繁执行的代码进行优化(
hot)或对不常执行的代码进行优化(cold)。 - 用法:
1
2
3
4
5
6
7
8
9
10void my_hot_function() __attribute__((hot));
void my_cold_function() __attribute__((cold));
void my_hot_function() {
// 频繁调用的代码
}
void my_cold_function() {
// 不常调用的代码
}