缘由
C语言自认学得不错,用得很多,前段时间受到B站up主 ‘“冒-_-泡”‘发布的视频【C/C++】早期程序员怎么写DEBUG_LOG宏? 的启发,想要补一补之前欠缺的一些知识,于是捡起《C primer plus》这本C语言字典查查单词。
第16章 C预处理器和C库
记一些要点:
C函数 宏 不替换
" "
中的部分。ANSI C 字符串的
串联特性
“x” “=” “y” 会被拼接成 “x=y”用宏参数创建字符串:
#
运算符为了替换
" "
中的x ,即将其 字符串化#define P(x) printf(“msg “ #x “ is”, ((x)*(x)) ),
1
2
3
4
5
6
7
8
9
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}输出结果为:
1
2The square of y is 25.
The square of 2 + 4 is 36.调用第1个宏时,用”y”替换#x。调用第2个宏时,用”2 + 4”替换#x。
ANSI C字符串的串联特性将这些字符串与printf()语句的其他字符串组合,生
成最终的字符串。例如,第1次调用变成:
printf(“The square of “ “y” “ is %d.\n”,((y)*(y)));
然后,字符串串联功能将这3个相邻的字符串组合成一个字符串:
“The square of y is %d.\n”预处理器粘合剂:
##
运算符1
xNAME(4) 将转换为 x4
变参宏
...
和_ _ VA_ARGS_ _
1
2
3
4
5
6
7
8
9
10
11
12
13// variadic.c -- 变参宏
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}第1个宏调用,X的值是1,所以#X变成”1”。展开后成为:
print(“Message “ “1” “: “ “x = %g\n”, x);
然后,串联4个字符,把调用简化为:
print(“Message 1: x = %g\n”, x);输出结果为:
1
2Message 1: x = 48
Message 2: x = 48.00, y = 6.92821
2//记住,省略号只能代替最后的宏参数:
#undef
指令为了防止出现宏的不相同的重定义,在使用前可以使用
#undef
撤销#define
#ifdef
、#else
和#endif
指令 用于条件编译1
2
3
4
5
6
7
预定义宏
宏 含义 _ _ DATA _ _ 预处理日期,”Mmm dd yyyy” _ _ FILE _ _ 当前源代码文件名的字符串字面量 _ _ LINE_ _ 当前源代码文件中行号的整型常量 _ _ TIME _ _ 翻译代码的时间 “hh:mm:ss” 1
2
3
4
5
6
7
8
9
10
11
12
13
14printf(…)用void强转表示DBGLOG宏相当于一个void函数,
完全禁止 DBGLOG作为一个表达式 ,则可以使用
do {...} while(0)
多层嵌套的STR宏,是为了正确地将
__LINE__
代表的 行数(整型常量) 转化为字符串,而不是将__LINE__
转化为字符串。原因是,宏替换是一轮一轮进行的,__LINE__
作为某一个宏的参数,外层的宏先转换,内层的后转换。如果 直接 _STR(__LINE__
),那么会转成#__LINE__
,下一轮的宏替换,会将其替换成"__LINE__"
,预定义宏就失效了#pragma
编译指示
内联函数
inline
把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用
1 |
|
第12章 存储类别、链接和内存管理
这一章是C语言中一些较为底层的概念,之前常用但是不怎么注意 或者 已经比较了解 的内容。原文说得很细致,逻辑严密。但是 局限在C语言的视角,有些如果结合实际的内存管理 去讲会更加易于理解。
同样,记录一些要点,熟悉C语言的可以直接读该章节,会有收获的。
12.1 存储类别
有几个前置的关键概念: 作用域、链接和存储期。
作用域
作用域名 | 含义 | 备注 |
---|---|---|
块作用域 |
花括号括起来的代码区域 | if和for循环没有花括号也是 |
函数作用域 |
仅用于goto标签 | 标签在内层块,作用域延伸至整个函数 |
函数原型作用域 |
用于函数原型中的形参名 | 范围是从形参定义处到原型声明结束 |
文件作用域 |
变量定义在函数的外面 | 从定义处到文件末尾均可见 |
1 | //块作用域 |
编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元
。
如果程序由多个源代码文件
组成,那么该程序也将由多个翻译单元
组成。每个翻译单元
均对应一个源代码文件
和它所包含的文件
。
描述一个具有文件作用域
的变量时,它的实际可见范围是整个翻译单元
。
链接
分类 | 描述 |
---|---|
外部链接 | 外部链接变量可在多文件程序中使用 |
内部链接 | 内部链接只能在一个翻译单元中使用 |
无链接 | 具有块作用域,函数作用域和函数原型作用域的变量都是 |
存储期
分类 | 描述 | 备注 |
---|---|---|
静态存储期 | 程序执行期间一直都存在 | .data .bss |
线程存储期 | 被声明时到线程结束都存在 | |
自动存储期 | 块作用域的变量,进入块到退出块 | 栈 |
动态分配存储期 | 堆 |
自动存储期中,变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。
块作用域变量也能具有静态存储期,函数中定义static int a = 0;
有了前面的概念那么引入正题
存储类别
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用 register |
静态外部链接 | 静态 | 多文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 单文件 | 内部 | 所有函数外,使用static |
静态无链接 | 静态 | 块 | 无 | 块内,使用 static |
自动存储类别,可以显示使用关键字auto
,语义和C++不同,不推荐使用。默认情况下,声明在块或者函数头中的任何变量都属于自动存储类别。
寄存器变量使用register
关键字声明,但是仅仅是对编译器的请求,可能不会生效,但无论如何不能对register
声明的变量使用地址运算符。
1 | int traveler = 1; // 外部链接 |
从此处可见C语言类型系统的内在不一致性
12.2 随机函数和静态变量
1 | static unsigned long int next = 1; /* 种子 */ |
此函数不是 线程安全的,不可重入。
12.3 分配内存:malloc()和free()
malloc 和 free 要一一对应,不能少(内存泄露),不能多(重复释放)
12.5 ANSI C类型限定符
const
1 | float * const pfc;//pt 是一个const指针, pf 指向的值不能被改变,而 pt 本身的值可以改变 |
对于所有的限定符有一个形式化的解读方法,将声明中的单类型去掉,剩余的部分从左到右逐一限定。
1 | // 指针p 不可变,解引用一次不可变,解4次不可变,解6次不可变 |
volatile
volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。
涉及编译器的优化,在ANSI C 标准出现后,如果声明中没有volatile关键字,编译器会假定变量的值在使用
过程中不变,然后再尝试优化代码。
volatile和const可以修饰同一个词,表示程序不能更改,仅可以读。
1 | volatile const int loc; |
restrict
restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
举个例子(来自CSAPP,比本书中的更直观)
1 | void twiddle1(long *xp, long *yp) |
函数twiddle1和twiddle2不是等价的,如果xp和yp指向同一个值n,那么前者结果是 n = 4n,后者是n = 2n,因此编译器只能考虑最坏的情况做优化。
restrict 关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但
是无视它后果自负。