程序的翻译环境和运行环境

.c -> .exe翻译环境
翻译分为编译(编译器)和链接(链接器)两个过程

每个源文件都会经过编译器的 单独处理 生成目标文件(.obj文件object)
所有目标文件和链接库经过链接器生成可执行文件
链接器可以搜索所需的标准C库文件和个人的程序库

编译分为预编译,编译,汇编三个过程

以下在Linux下演示
创建test.c

预编译:

gcc -E预编译命令
gcc -E test.c > test.i将预编译产生的内容重定向到test.i中
Linux中头文件存放在/usr/include
预编译过程中:

  1. 头文件展开
  2. 去除注释(使用空格替换注释)
  3. 完成预处理指令,例如对#define定义的值进行替换
    例如:
1
2
3
4
5
6
7
8
#define MAX 100

int main(void)
{
int i = MAX;
return 0;
}

替换为:

1
2
3
4
5
6
int main(void)
{
int i = 100;
return 0;
}

总结:文本操作

编译:

gcc -S test.i生成test.s文件
把C代码翻译成汇编代码
具体操作有:语法分析,词法分析,语义分析,符号汇总
符号汇总:函数名,全局变量等
涉及编译原理
《程序员的自我修养》

汇编:

gcc -c test.s生成test.o
Windows下是.obj
操作:把汇编代码转换成二进制代码(指令),形成符号表
符号表:一个表中放置符号(函数名,全局变量等)和符号的地址

链接过程

  1. 合并段表
    每个.o文件都分为相同格式的段(段里面的内容不同)(elf文件格式)
    链接时不同文件的相同段合并到一起,最终生成的可执行文件的格式也是elf格式
  2. 符号表的合并和重定位
    不同文件的符号表合并,表中遇到相同的符号,选地址有效的那个
    例如:
    main.c中引用外部函数add, add.c中定义了add函数的实现,main.o的符号表中有add,地址是无效地址,add.o的符号表中也有add,表中的地址有效,链接时选取add.o符号表中add的地址

运行.exe运行环境

  1. 程序载入内存中:
    在有操作系统的环境中一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行:
    从main函数开始
  3. 开始执行程序代码:
    程序使用一个运行时程序堆栈,存储局部变量和返回地址,也可以在堆空间里开辟动态内存,在静态内存中存放静态变量
  4. 终止程序:
    可以是正常终止,也可以是意外终止

预处理

预定义符号

  1. FILE
    获取当前文件名称和路径
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("%s\\n", __FILE__);
return 0;
}

输出:D:/Study/StudyNotes-C/Pretreatment/main.c

  1. LINE
    获取当前行号
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("%d\\n", __LINE__);
return 0;
}

输出:4

  1. DATE
    获取当前日期
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("%s\\n", __DATE__);
return 0;
}

输出:Mar 11 2023

  1. TIME
    获取当前时间
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("%s\\n", __TIME__);
return 0;
}

输出:20:51:56

  1. FUNCTION
    获取当前位置所在的函数名称
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("%s\\n", __FUNCTION__);
return 0;
}

输出:main

  1. STDC
    如果编译器遵循ANSI C,其值为1,否则未定义
    用来检测是否遵循ANSI C
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("%d\\n", __STDC__);
return 0;
}

输出:1
如果是MSVC那么会报错

用处:写日志文件
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main(void)
{
int i = 0;
int arr[10] = { 0 };
FILE* pf = fopen("log.txt", "w");
for (i = 0; i < 10; i++)
{
arr[i] = i;
fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\\n",
__FILE__, __LINE__, __DATE__, __TIME__, i);
}
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}

输出:0 1 2 3 4 5 6 7 8 9

1
2
3
4
5
6
7
8
9
10
11
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=0
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=1
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=2
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=3
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=4
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=5
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=6
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=7
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=8
file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=9

预处理指令

#开头的指令都是预处理指令#

  1. #define#
  2. #include#
  3. #pragma#
  4. #if#
  5. #endif#
  6. #ifdef#
  7. #line#

#define#

#define定义标识符#

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define MAX 100

int main(void)
{
int max = MAX;
printf("%d\\n", max);
return 0;
}

1
2
3
4
5
6
7
8
#include <stdio.h>
#define STR "hello"
int main(void)
{
printf("%s\\n", STR);
return 0;
}

#define reg regist#define do_forever for(;;)#define CASE break;case
如果如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)

1
2
3
4
5
#define DEBUG_PRINT printf("file:%s\\tline:%d\\t \\
date:%s\\ttime:%s\\n" , \\
__FILE__,__LINE__ ,   \\
__DATE__,__TIME__ )

define语句不要加分号,相当于定义的内容就有分号

#define定义宏#

#define# 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff 其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意: 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define SQUARE(X) X*X

int main(void)
{
int ret = SQUARE(5);
printf("%d\\n", ret);
return 0;
}

输出为25

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define SQUARE(X) X*X

int main(void)
{
int ret = SQUARE(5 + 1);
printf("%d\\n", ret);
return 0;
}

输出为11
宏不是传参而是替换,例如:int ret = SQUARE(5 + 1)被替换成
int ret = 5 + 1 * 5 + 1所以说是11
如果改成

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define SQUARE(X) (X)*(X)

int main(void)
{
int ret = SQUARE(5 + 1);
printf("%d\\n", ret);
return 0;
}

或者

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define SQUARE(X) X*X

int main(void)
{
int ret = SQUARE((5 + 1));
printf("%d\\n", ret);
return 0;
}

那么输出就是36

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#define DOUBLE(X) X+X

int main(void)
{
int a = 5;
int ret = 10 * DOUBLE(a);
printf("%d\\n", ret);
return 0;
}

输出是55
在写宏时不要少些括号

#define的替换规则#

在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

#define MAX 100

#define DOUBLE(X) ((X)+(X))

int main(void)
{
int a = 5;
int ret = 10 * DOUBLE(MAX);
printf("%d\\n", ret);
return 0;
}

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
    例如printf(MAX = %d\\n, MAX)中第一个MAX不被替换

#和##

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void print(int a)
{
printf("the value of a is %d\n", a);
}
int main(void)
{
int a = 10;
int b = 20;
print(a);
print(b);
return 0;
}

无法做到替换a

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void)
{
printf("hello world\\n");
printf("hello " "world\\n");
return 0;
}

C语言可以自动连接两个字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

#define PRINT(X) printf("the value of " #X " is %d\\n", X)

int main(void)
{
int a = 10;
int b = 20;
PRINT(a);
//printf("the value of " "a" " is %d\\n", X)
PRINT(b);
//printf("the value of " "b" " is %d\\n", X)
return 0;
}

#把一个宏参数直接插入到字符串中#

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define CAT(X, Y) X##Y

int main(void)
{
int a1 = 1;
printf("%d\\n", CAT(a, 1));
return 0;
}

##会把两端的值连接到一起

带有副作用的宏参数

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)
{
int a = 10;
int b = a + 1;//a不变
int b = ++a;//a加一
//a变化了可能会影响后续代码,有副作用
return 0;
}

a+1没有副作用
a++有副作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))

int main(void)
{
int a = 10;
int b = 11;
int max = MAX(a++, b++);
printf("%d\\n", max);
printf("%d\\n", a);
printf("%d\\n", b);
return 0;
}

输出12 11 13MAX(a++, b++)替换为((a++) > (b++) ? (a++) : (b++))

宏和函数的区别

  1. 宏的优势
    • 函数调用的时候会有函数调用和返回的开销,效率较低
      宏在预处理时就完成了替换,没有函数调用和返回的开销,效率高
    • 函数的参数必须声明成特定类型,只能在类型适合的表达式上使用,宏的使用与类型无关
      例如:
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
#include <stdio.h>

#define MAX(X, Y) (X)>(Y)?(X):(Y)

int Max(int x, int y)
{
return x > y ? x : y;
}

int main(void)
{
int a = 0;
int b = 1;
float c = 1.0f;
float d = 2.0f;
int max = Max(a, b);
printf("%d\\n", max);
max = MAX(a, b);
printf("%d\\n", max);
max = Max(c, d);
printf("%d\\n", max);
max = MAX(c, d);
printf("%d\\n", max);
return 0;
}

  1. 宏的劣势
    • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度,函数只有一份
    • 宏是没法调试的
    • 宏由于类型无关,也就不够严谨
    • 宏可能会带来运算符优先级的问题,导致程序容易出现出错
    • 宏不能递归,函数可以递归
      宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define SIZEOF(type) sizeof(type)

int main(void)
{
int ret = SIZEOF(int);
printf("%d\\n", ret);
return 0;
}

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>

#define MALLOC(num, type) (type*)malloc(num*sizeof(type))

int main(void)
{
int* p = MALLOC(10, int);
return 0;
}

命名约定

一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。那么我们平时的一个习惯是:
宏命名全部大写,函数名不要全部大写

#undef#

移除一个宏定义

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define MAX 100

int main(void)
{
printf("MAX = %d\\n", MAX);
#undef MAX
return 0;
}

#命令行定义#

许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main( void)
{
int arr[SZ] = { 0 };
int i = 0;
for(i = 0; i < SZ; i++)
{
arr[i] = i;
}
for(i = 0; i < SZ; i++)
{
printf("%d\\n", arr[i]);
}
return 0;
}

编译时输入gcc main.c -D SZ=10则可以编译成功
在预编译阶段替换

条件编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

#define DEBUG

int main(void)
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[1] = 0;
#ifdef DEBUG
printf("%d ", arr[i]);
#endif//DEBUG
}
return 0;
}

常见的条件编译指令:

  1. #if和#endif
1
2
3
4
#if 常量表达式
//...
#endif

#if判断常量表达式的真假,非零执行,零不执行#

  1. 多分支的条件编译
1
2
3
4
5
6
7
8
9
10
#if 常量表达式
//...
#elif 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(void)
{
#if 1==1
printf("1");
#elif 2==1
printf("2");
#else
printf("3");#endif
return 0;
}

  1. 判断是否被定义
1
2
3
4
5
6
7
8
#if defined(symbol)
//等价于#ifdef symbol

#if !defined(symbol)
//等价于#ifndef symbol

#endif

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

#define DEBUG

int main(void)
{
#ifdef DEBUG
//#if defined(DEBUG)
printf("hello world");
#endif//DEBUG
return 0;
}

  1. 嵌套指令
1
2
3
4
5
6
7
8
9
10
11
12
13
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

文件包含

#include在包含本地文件时用#" ",如果包含库文件时用< >" " 本地文件查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误
< > 库文件查找策略:直接去标准路径下去查找,如果找不到就提示编译错误
库文件也可以用" "包含,但是查找效率低

嵌套文件包含

头文件重复包含导致代码冗余

1
2
3
4
5
6
7
#ifndef __TEST_H__
#define __TEST_H__

//...

#endif

或者
#pragma once

offsetof()的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

#define OFFSETOFF(struct_name, member_name) ((int)&(((struct_name*)0)->member_name))

struct S
{
char c1;
int a;
char c2;
};

int main(void)
{
struct S s;
printf("%d\\n", OFFSETOFF(struct S,c1));
printf("%d\\n", OFFSETOFF(struct S,a));
printf("%d\\n", OFFSETOFF(struct S,c2));
return 0;
}