C语言——一些进阶知识(二)
C语言——数据结构相关
1. 高级宏定义
1.1 不带参数的宏定义
1 |
-
为了和普通变量区分,宏的名字通常约定是全部由大写字母组成
-
宏定义只是进行简单的替换,编译器不会对宏定义检查语法错误,检查出来也是替换之后的代码报错
-
宏定义的作用域是从定义的位置开始到整个程序结束
-
可以用
#undef
来终止宏定义的作用域1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void) {
int r;
float s;
scanf("%d", &r);
s = PI * r * r;
printf("S = %.2f\n", s);
return 0;
} -
宏定义允许嵌套
1
2
3
4
5
6
7
8
9
10
11
int main(void) {
printf("地球的体积大约是:%.2f\n", V);
return 0;
}
1.2 带参数的宏定义
1 |
1 |
|
之所以加那么多括号是为了防止宏定义无脑替换后,执行结果出现意想不到的错误。下面给出一个替换后计算错误的例子:
1 |
|
为了避免出错,一定不要吝啬你的括号:
1 |
1.3 宏定义的特殊应用
-
#
和##
是两个预处理运算符。 -
在带参数的宏定义中,
#
运算符后面应该跟一个参数,预处理器会把这个参数转换为一个字符串。1
2
3
4
5
6
7
8
9
int main(void) {
printf("%s\n", STR(Awellfrog));
return 0;
}1
2
3
4
5
6
7
8
9
int main(void) {
printf(STR(Hello %s num = %d), STR(Awellfrog), 520); // 中间很长的空格会被合并为 1 个
return 0;
} -
##
运算符被称为记号连接运算符,可以使用##
运算符连接两个参数1
2
3
4
5
6
7
8
9
int main(void) {
printf("%d\n", TOGETHER(5, 20));
return 0;
}
可变参数
-
之前在进阶知识(一)中学习了如何让函数支持可变参数,带参数的宏定义也是使用可变参数的:
1
-
其中
...
表示使用可变参数,__VA_ARGS__
在预处理中被实际的参数集所替换。1
2
3
4
5
6
7
8
9
int main(void) {
SHOWLIST(Awellfrog, 520, 3.14\n);
return 0;
}1
2
3
4
5
6
7
8
9
10
int main(void) {
PRINT(num = %d\n, 520);
PRINT(Hello Awellfrog!\n); // ## 之后可以为空
return 0;
}
2. 内联函数
即在函数之前加 inline
,该函数将被在调用出展开。
先看一个带参数的宏定义:
1 |
|
可以看到结果是错误的,由于展开之后是 ((i++) * (i++))
在打印结果之前 i++
了两次,于是打印时 i = 3
,且计算过程是 (1) * (2)
。由此观之,宏定义很容易出现错误,于是 inline
内联函数就有了用武之地。
直接使用函数,需要在用到时分配空间,效率较低。
如果使用带参数的宏定义,则直接替换至代码段中,函数调用效率提高了。
但是带参数的宏定义很容易出错,所以使用内联函数将函数直接替换至调用处,解决函数调用的效率问题。不过编译时间有所加长。
- 内联函数虽然节省了函数调用的时间消耗,但由于每一个函数出现的地方都要进行替换,因此增加了代码编译的时间。另外,并不是所有的函数都能够变成内联函数。
- 现在的编译器也很聪明,就算你不写
inline
,它也会自动将一些函数优化成内联函数。 - 总结:编译器比你更了解哪些函数应该内联哪些不能内联,所以这个知识点你只需要知道就好…
3. 结构体
- 为了区分结构体和常量、变量,结构体名称通常第一个字母大写。
- 结构体被声明时并不占用内存空间,只有定义一个结构体变量时才会分配内存空间。
初始化结构体变量的两种方法(假设结构体已经申明好了):
方法一:直接按顺序赋值
1 | struct Student stu = { |
方法二:用对每个成员赋值
1 | struct Student stu = { |
算一算
算一算下面的程序执行结果是多少?
1 |
|
按理说两个 char
一个 int
一共是 6 个字节,但是结果却显示 12 个字节。这是由于编译器将结构体内的数据进行了对齐操作,使得取数更快。
上面这种情况下,按照 int 的 4 字节对齐。
如果调整初始化的位置,将得到不一样的结果:
1 |
|
结构体嵌套
1 |
|
4. 结构体数组和结构体指针
- 结构体数组
结构体数组有两种定义方式:
-
声明结构体的时候定义
1
2
3struct 结构体名称 {
结构体成员;
} 数组名[长度]; -
先声明一个结构体类型(比如上面的 Book),在用此类型定义一个结构体数组
1
2
3
4struct 结构体名称 {
结构体成员;
};
struct 结构体名称 数组名[长度];
- 结构体指针
1 | struct Book *pt; |
通过结构体指针访问结构体成员有两种方法:
-
(*结构体指针).成员名
1
2
3
4
5
6
7
8
9
10
11int main(void) {
struct Book *pt;
pt = &book;
printf("书名:%s\n", (*pt).title);
printf("作者:%s\n", (*pt).author);
printf("售价:%.2f\n", (*pt).price);
printf("出版日期:%d-%d-%d\n", (*pt).date.year, (*pt).date.month, (*pt).date.day);
return 0;
} -
结构体指针->成员名
1
2
3
4
5
6
7
8
9
10
11int main(void) {
struct Book *pt;
pt = &book;
printf("书名:%s\n", pt->title);
printf("作者:%s\n", pt->author);
printf("售价:%.2f\n", pt->price);
printf("出版日期:%d-%d-%d\n", pt->date.year, pt->date.month, pt->date.day);
return 0;
}
两个结构体变量之间是可以直接赋值的
1 |
|
传递结构体参数
1 |
|
由于第一个日期输入没有按照格式 2017-1-1
输入,而是输入为 2017.1.1
所以 b1 中日期 2017 被接收,月和日随机生成,而 .1.1
被当作下一个书名的输入。b2 按照 2023-2-25
输入,正常执行。
直接传递结构体,传输速度慢,改用指针传递结构体参数,执行效率更高。
1 |
|
动态申请结构体
使用 malloc
函数为结构体分配存储空间
1 |
|
5. 内存碎片
如下图,从连续内存中分别申请两块内存,如果 A 被释放,第三次申请的内存空间比 12kb 大,所以原来 A 所在的内存空间就变成了内存碎片,或者说内存垃圾。
程序中调用 malloc
函数需要从应用层切换到内核层,再利用 windows 分配内存,分配完之后再返回应用层,时间开销较大。可以使用建立内存池的方法来节省内存分配时间的开销,并将内存碎片利用起来。
内存池
当用户申请内存块时,优先在内存池查看有没有可用内存垃圾(碎片),如果有直接使用该部分内存即可,不用再调用malloc
去内存中分配。
当用户释放空间时,先查看内存池是否用空闲空间,如果有就将该释放的内存块放入内存池中称为垃圾,供下一次分配使用。
内存池的简单实现:
- 使用单链表来维护一个简单地内存池
- 只需要将没有用的内存空间地址一次用一个单链表记录下来,当再次需要的时候,从这个单链表中获取即可。
6. typedef
6.1 基础部分
- 可以用作起别名,作用类似宏定义
1 |
|
可以将一个名称定义为多个名称:
1 | typedef int INTEGER, *PTRINT; |
运行结果和上面的代码相同。
但是如果使用宏定义,则可能出现问题:
1 | typedef int INTEGER; |
报错了,原因是 PTRINT b, c;
被宏定义替换后是 int * b, c;
b 是指针,c 是整形。
宏定义只是单纯的替换,而 typedef
是一种对类型的封装。
-
用在结构体上,可以省略反复写 struct 的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct Data {
int year;
int month;
int day;
} DATE, *PDATE;
int main(void) {
PDATE date; // 结构体指针
date = (PDATE)malloc(sizeof(DATE)); // 给结构体指针分配内存空间
if (date == NULL) {
printf("内存分配失败!\n");
exit(1);
}
date->year = 2023;
date->month = 2;
date->day = 25;
return 0;
}
6.2 较为恐怖的申明(恐怖,但是常用)
-
指向大小为 3 的数组的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef int (*PTR_TO_ARRAY)[3];
int main(void) {
int array[3] = {1, 2, 3};
PTR_TO_ARRAY ptr_to_array = &array;
int i;
for (i = 0; i < 3; i++) {
printf("%d\n", (*ptr_to_array)[i]);
}
return 0;
} -
指向函数的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef int (*PTR_TO_FUNC)(void); // 指向参数为空,返回类型为 int 的函数指针
int func(void) {
return 520;
}
int main(void) {
PTR_TO_FUNC ptr_to_func = &func;
printf("%d\n", (*ptr_to_func)());
return 0;
} -
一个指针数组,数组的每个值都指向一个参数为 int 返回值为 int 的函数
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
typedef int *(*PTR_TO_FUNC)(int);
int *funA(int num) {
printf("%d\t", num);
return # // 此处执行完后,num 的内存被释放,该地址没有意义,只是为了测试程序
}
int *funB(int num) {
printf("%d\t", num);
return # // 此处执行完后,num 的内存被释放,该地址没有意义,只是为了测试程序
}
int *funC(int num) {
printf("%d\t", num);
return # // 此处执行完后,num 的内存被释放,该地址没有意义,只是为了测试程序
}
int main(void) {
PTR_TO_FUNC array[3] = {&funA, &funB, &funC};
int i;
for (i = 0; i < 3; i++) {
printf("addr of num: %p\n", (*array[i])(i));
}
return 0;
}可以看到由于返回局部变量地址,被编译器警告。同时可以看到三个函数地址是相同的,这证明了每次调用后前一个函数的内存释放,并分配给下一个函数。
-
函数指针的嵌套
1
2int calc(int (*)(int, int), int, int);
int (*select(char))(int, int);可以利用
typedef
简写为:1
2
3
4typedef int (*PTR_TO_FUN)(int, int);
int calc(PTR_TO_FUN, int, int);
TPR_TO_FUN select(char);
7. 共用体
所有成员共享同一个内存地址!
1 | union 共用体名称 { |
定义方式
和结构体定义方式类似:
1 | union data { |
初始化方式
1 | union data { |
根据下面的例子来分析共用体的特点:
1 |
|
- 可以看到,共用体的三个成员共享一个内存地址
- 共用体只有最后一个赋值的成员结果正确,其他被覆盖
- 公用体的大小并不单纯按照最大成员内存来分配(str[10] 最大,占用 10 个字节,但是 test 实际占用 16 个字节。),还和内存的对齐方式有关。
8. 枚举类型
使用枚举类型,便于一些使用连续值作为判据的分支:
1 |
|
枚举类型都是前一个值 +1 :
1 |
|
如果从中间赋值:(赋值的量为所赋值,其余均为前一个 +1 ):
1 |
|
枚举常量定义后,其值不能修改,否则报错。
9. 位域
单片机(Microcontrollers)是一种集成电路芯片,是采用超大规模集成电路技术把具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、多种 I/O 口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在工业控制领域广泛应用。
在单片机这种内存寸土寸金的设备上, 如果用一个 int 来存放 bool 值,就显得有些浪费了,可以使用位域,将一个字节拆开来用。
位域,又称位段、位字段
位域的使用方法:
使用位域的做法是在结构体定义时,在结构体成员后面使用冒号(:)和数字来表示该成员所占的位数。
1 |
|
- 由于 test.c 的位域为 2 bit,所以可以表示 2(10),如果位域设为 1 bit 则仅能存储低位:
1 | struct Test { |
可以看到编译器提示赋值超过位域可表示的范围了。
- 如果不使用位域,则该结构体占用 12 个字节
- 域的长度必须小于其类型的比特大小
超过 32 bit 则报错,并且结构体的大小根据位域的大小不同而变化。
1 | struct Test { |
内存的基本单位是字节,而位域是字节的一部分,所以位域不能取址。