【操作系统】C语言预处理命令与内存模型
C 语言预处理命令
在编译之前对源文件进行简单加工的过程称为预处理。编译器会将预处理的结果保存到和源文件同名的 .i 文件中,例如 main.c 的预处理结果在 main.i 中。C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。
宏定义
无参宏定义
-
格式:
#define 宏 替换体
。 -
宏定义是简单粗暴的替换,不做语法检查,不分配内存。
-
宏定义可以嵌套。
-
代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替。
-
宏定义与
typedef
:- 宏定义是字符串替换,由预处理器来处理。
typedef
给原有的数据类型起一个新的名字,将它作为一种新的数据类型,由编译器处理。
带参宏定义
- 格式:
#define 宏(宏参数) 替换体
。 - 带参宏定义中,不为形参分配内存,不必指明数据类型;宏调用中,实参必须要指明数据类型。
- 为防止无限制递归展开,当宏调用自身时,不再继续展开。
- 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
- 注意括号的使用、自增运算符、运算顺序。
- 带参宏定义与函数:
- 宏只占编译时间,不占运行时间;宏展开使源程序变长。
- 函数调用占运行时间;函数调用不会使源程序变长。
终止宏定义的作用域
- 格式:
#undef 宏
。 #undef
指令用于取消已定义的#define
指令。- 即使原来没有
#define
,#undef
指令仍然有效。
预定义宏
__FILE__
:正在编译的文件的文件名。__LINE__
:正在编译的文件的行号。__DATE__
:编译时刻的日期字符串。__TIME__
:编译时刻的时间字符串。__STDC__
:判断该文件是不是标准 C 程序。
C99 标准提供一个名为 __func__
的预定义标识符,它展开为一个代表函数名的字符串。__func__
具有函数作用域,并不是预定义宏。
例——__func__
、__LINE__
:
void why_me()
{
printf("This is function %s.\n", __func__);
printf("This is line %d.\n", __LINE__);
}
// 输出
// This is function why_me.
// This is line 4.
可变参数宏
- 格式:
#define 宏(宏参数, ...) 替换体(__VA__ARGS__)
。 - 把宏参数列表中最后的参数写成
...
,__VA__ARGS__
用在替换部分,表明省略号代表什么。
例——__VA__ARGS__
:
#include <stdio.h>
#define PR(...) printf(__VA_ARGS__)
int main()
{
PR("ans1 = %d, ans2 = %d\n", 100, 200);
// 相当于 printf("ans1 = %d, ans2 = %d\n", 100, 200);
return 0;
}
字符串化操作符与宏参数连接
字符串化操作符 #
:
#
的功能是将其后面的宏参数进行字符串化操作(Stringfication)。- 例如,如果
x
是一个宏形参,传入实参时,#x
将转换为"实参"
。 #
运算符将修改宏实参中空白符、双引号,以及反斜线- 忽略传入参数名前面和后面的空格;传入参数名间存在空格时,编译器会自动连接各个子字符串,每个子字符串间只以一个空格连接。
"
将改为\"
。\
将改为\\
。
例——#
:
#include <stdio.h>
#define PR(instr) printf("The input string is: %s.\n", #instr)
int main()
{
PR(hhh);
return 0;
}
// 输出
// The input string is: hhh.
宏参数连接 ##
:
##
运算符把两个记号组合成一个记号。- 当用
##
连接形参时,##
前后的空格可有可无。 - 连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。
- 字符串化运算符和记号粘贴运算符并没有固定的运算次序。如果需要采取特定的运算次序,可以将一个宏分解为多个宏。
例——##
:
#include <stdio.h>
#define TEXT_A "Hello, world!"
#define msg(x) puts(TEXT_ ## x)
int main()
{
msg(A);
return 0;
}
// 输出
// Hello, world!
Argument Prescan:
-
凡是宏定义里有用
#
或##
的地方,宏参数是不会再展开。 -
展开宏的过程是层次化展开队列,本质上是广度优先搜索的过程。
-
宏的展开可用以下三步来简单描述(该步骤与 gcc 摘录稍有不同,但更易操作):
- 用实参替换形参,将实参代入宏文本中;
- 若实参也是宏,则展开实参;
- 继续处理宏替换后的宏文本,若宏文本也包含宏则继续展开,否则完成展开。
其中第一步将实参代入宏文本后,若实参前遇到字符
#
或##
,即使实参是宏也不再展开实参,而当作文本处理。
例——Argument Prescan:
#include <stdio.h>
#include <limits.h>
#define STR(s) #s
int main()
{
printf("int max: %s.\n", STR(INT_MAX));
return 0;
}
// 输出
// int max: INT_MAX.
#include <stdio.h>
#include <limits.h>
#define _STR(s) #s
#define STR(s) _STR(s)
int main()
{
printf("int max: %s.\n", STR(INT_MAX));
// 第一步:printf("int max: %s.\n", _STR(2147483647));
// 第二步:printf("int max: %s.\n", "2147483647");
return 0;
}
// 输出
// int max: 2147483647.
泛型宏
C11 中新增了一种表达式,泛型选择表达式,可以根据表达式的类型选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中,它常用做 #define
宏定义的一部分。
例——_Generic
:
#define log10(x) _Generic((x), \
long double: log10l, \
float: log10f, \
default: log10 \
)(x)
文件包含
#include
指令:当预处理起发现 #include
指令时,会查看后面的文件名并把文件的内容包含到当前文件中。
- 使用尖括号
< >
,编译器会在标准系统目录下查找头文件; - 使用双引号
" "
,编译器首先在当前目录下查找头文件,如果未找到查找标准系统目录。
头文件 xxx.h 通常包含:
#define
指令,明示常量、宏函数;- 结构体声明;
typedef
类型定义;- 函数原型(不包含函数定义);
#ifndef
和#define
等防止包含多重头文件。
例——main.c、my.c、my.h:
// main.c
#include <stdio.h>
#include "my.h"
int main() {
printf("%d\n", sum(1, 100));
return 0;
}
// my.c
#include <stdio.h>
#include "my.h"
int sum(int m, int n) {
return m + n;
}
// my.h
int sum(int m, int n);
例——在 #include
命令中使用宏:
#ifdef _DEBUG_
#define SEL_HEADER "myprj_dbg.h"
#else
#define SEL_HEADER "myprj.h"
#endif
#include SEL_HEADER
条件编译
#ifdef 标志符 ... #else ... #endif
#ifndef 标志符 ... #else ... #endif
#if 常量表达式... #elif ... #else ... #endif
- 不能在
#if
与#elif
中使用类型转换运算符。 - 可以使用预处理运算符
defined
。 - 表达式中所有带符号值都具有
intmax_t
类型, 无符号值都具有uintmax_t
类型。
- 不能在
defined 宏
运算符- 若宏已定义,则值为 1;否则,值为 0。
其他指令
#line
指令:重置__LINE__
和__FILE__
。#error
指令:让预处理器发出一条错误消息。#pragma
指令:向编译器提供额外信息的标准方法。_Pragma
运算符:把字符串转化成普通的编译指令,“解字符串化”。
C 语言内存模型
C 语言内存模型
.text
段:用来存放程序执行代码。.rodata
段:用来存放一般的常量、字符串常量。.data
段:用来存放程序中已初始化的全局变量。.bss
段:用来存放程序中未初始化的全局变量。- 栈:用来存放程序中的局部变量;在函数被调用时,栈用来传递参数和返回值。
- 堆:用来存放进程运行中被动态分配的内存段。
C 语言存储类别
- 被存储的每个值都占用一定的物理内存,C 语言把这样的一块内存成为对象。
- 标识符是一个名称,用来指定特定对象的内容。
- 用存储期(storage duration)描述对象,用作用域(scope)和链接(linkage)描述标识符。
- 存储期指对象在内存中保留了多长时间。
- 标识符的作用域和链接表明了程序的哪些部分可以使用它。
- 翻译单元与文件:每个翻译单元均对应一个源代码文件和他所包含的文件。
作用域
- 块作用域。
- 函数作用域:仅用于
goto
语句的标签。 - 函数原型作用域:用于函数原型中的形参名,如变长数组。
- 文件作用域(全局变量):描述一个具有文件作用域的变量时,他的实际可见范围是整个翻译单元。
例——作用域:
#include <stdio.h>
int func1(int a);
void func2(int m, int n, int ar[m][n]); // 函数原型作用域: m, n
int ans = 1; // 文件作用域: ans
int func1(int a) // 块作用域: a, b
{
int b = a + 1;
return b;
}
void func2(int m, int n, int ar[m][n])
{
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
printf("%d ", ar[i][j]);
printf("\n");
}
}
int main()
{
for (int i = 0; i < 10; i++) // 块作用域: i
printf("%d ", i);
printf("\n");
int m = 2, n = 3;
int ar[2][3] = {1, 2, 3, 4, 5, 6};
func2(m, n, ar);
return 0;
}
链接
- 外部链接:可延伸至其它翻译单元(的文件作用域)。
- 内部链接:仅限一个翻译单元(的文件作用域),使用
static
声明。 - 无链接:具有块作用域、函数作用域、函数原型作用域的变量都是无链接的。
例——链接:
#include <stdio.h>
int a = 1; // 文件作用域,外部链接
static int b = 2; // 文件作用域,内部链接
int main()
{
return 0;
}
存储期
- 静态存储期:文件作用域变量、
static
声明的块作用域变量。 - 线程存储期:咕咕咕~
- 自动存储期:未用
static
声明的块作用域变量。 - 动态分配存储期。
关于
static
:对于文件作用域变量,static
表明其链接属性,而非存储期。
例——块作用域的静态变量:
#include <stdio.h>
int sum(int n)
{
static int result = 0; // 块作用域,静态存储期
result += n;
printf("%d\t", result);
printf("%x\n", &result);
return result;
}
int main ()
{
int result = 0;
result = sum(1);
result = sum(2);
printf("%d\t", result);
printf("%x\n", &result);
return 0;
}
// 输出
// 1 1018
// 3 1018
// 3 efbff548
注:
sum
函数中的result
是块作用域的静态变量。它从程序被载入到程序结束期间都存在,只有在执行sum
函数时,才能通过result
访问它指定的对象(其它时间,可以通过指针等方式间接访问)。- 静态数据区的变量只能初始化一次,第一次调用
sum
时已经对result
进行了初始化,所以再次调用时就不会初始化了,也就是说第二次调用时static int result = 0;
语句无效,result
的值为 \(1+2=3\)。 sum
中的result
与main
中的result
不冲突,除了变量名一样,没有任何关系。
五种存储类别
自动变量:
- 默认情况下声明在块或函数中的任何变量都属于自动变量。
- 可以使用
auto
显式声明。【注意:auto
在 C++ 中的用法完全不同】
寄存器变量:
- 使用
register
声明。 - 可声明为
register
的数据类型有限。 - 声明
register
类别更像是一种请求,编译期根据寄存器或最快可用内存的数量衡量你的请求,可能不会如你所愿。在这种情况下寄存器变量变成自动变量,但是仍然不能对该变量使用地址运算符。
块作用域的静态变量:
- 见本文“存储期”部分。
外部链接的静态变量:
-
如果一个源代码文件使用的一个外部变量定义在另一个源代码文件中,则必须使用
extern
在该文件中显式声明该变量。 -
指出函数使用了外部变量可以在函数中用
extern
再次声明,如果在函数中省略了extern
相当于创建了一个同名自动变量。#include <stdio.h> int a = 1; int main () { printf("%d\t%x\n", a, &a); int a = 2; printf("%d\t%x\n", a, &a); return 0; } // 输出 // 1 1018 // 2 efbff548
-
只能用常量表达式初始化文件作用域变量。
-
外部变量只能初始化一次,且必须在定义该变量时进行。
-
定义和声明:
#include <stdio.h> int a = 1; // 第一次声明,定义式声明 // int b = 2 * a; // 错误 int main () { extern int a; // 第二次声明,引用式声明 // extern int a = 2; // 错误 return 0; }
内部链接的静态变量:
- 见本文“链接”部分。
函数的存储类别
- 外部函数(默认):可以被其它文件的函数访问。
- 静态函数:只能用于其定义所在的文件。
- 内联函数(C99 新增):咕咕咕~
存储类别与内存模型
- 栈:局部变量。
- 堆:动态分配内存,
malloc
等。 - 已初始化数据段、已初始化数据段的常量区、未初始化数据段:全局变量、静态变量、常量。
#include <stdio.h>
int a = 1;
int b;
static int c;
const int d = 5;
char e[5];
char f[5] = {'0', '1', '2', '3', '4'};
void func()
{
static int g = 3;
printf("&g:\t%X\n", (unsigned int)&g);
return;
}
int main()
{
int h = 4;
char i[] = "hello";
char * j = "world";
printf("-----栈-----\n");
printf("&h:\t%X\n", (unsigned int)&h);
printf("&i:\t%X\n", (unsigned int)&i);
printf("&j:\t%X\n", (unsigned int)&j);
printf("-----未初始化数据段-----\n");
printf("&b:\t%X\n", (unsigned int)&b);
printf("&c:\t%X\n", (unsigned int)&c);
printf("&e:\t%X\n", (unsigned int)&e);
printf("-----已初始化数据段-----\n");
func();
printf("&f:\t%X\n", (unsigned int)&f);
printf("&a:\t%X\n", (unsigned int)&a);
printf("-----已初始化数据段的常量区-----\n");
printf("j:\t%X\n", (unsigned int)j);
printf("&d:\t%X\n", (unsigned int)&d);
return 0;
}
// 输出
// -----栈-----
// &h: EFBFF548
// &i: EFBFF542
// &j: EFBFF538
// -----未初始化数据段-----
// &b: 1028
// &c: 1034
// &e: 102C
// -----已初始化数据段-----
// &g: 1024
// &f: 101C
// &a: 1018
// -----已初始化数据段的常量区-----
// j: ED6
// &d: EC4
参考
C 语言预处理命令
- C 语言中文网:C语言预处理命令(宏定义和条件编译)
- C 语言中文网:C语言宏的定义和宏的使用方法(#define)
- 博客园:C语言预处理命令详解
- Coekjan's Blog:「C程序设计」 预处理命令
- 《C Prime Plus》(第六版)第十六章
C 语言内存模型
- 博客园:(C语言内存九)Linux下C语言程序的内存布局(内存模型)
- 博客园:(C语言内存二十一)C语言变量的存储类别和生存期
- Coekjan's Blog:「C程序设计」 内存空间与分配
- 《UNIX环境高级编程》(第三版)第七章第六节
- 《深入理解计算机系统》(第三版)第七章第四节
- 《C Prime Plus》(第六版)第十二章