目录
程序环境和预处理
任何的C语言的代码都会存在二种环境:
- 翻译环境,这个环境是把代码转换成机器可以执行的指令
- 执行环境,它用于实际执行代码
程序的翻译环境
一个源文件最后的可执行程序一共有三步:
- 首先是每个源文件会单独的经过编译器处理生成一个目标文件
(在Windows环境的目标文件的后缀是.obj
,在Linux环境下目标文件的后缀为.0
) - 链接器会把所有的目标文件捆绑到一起,同时链接器也引用
标准C函数库
中被该程序所引用的函数
同时它也可以搜索程序员个人创建的程序库,将其需要的函数也链接到程序中(链接库操作
) - 最后形成一个单一而完整的可执行程序 (.exe 文件)
程序的执行环境
程序的实际执行值执行环境中进行,程序执行的具体过程分三步:
- 程序必须先被载入内存中。在有操作系统的环境中,此过程一般由操作系统完成;在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 接下来程序开始执行,调用main函数。
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),用于存储函数的局部变量和返回地址;程序同时也可以使用静态(static)内存,存储于静态内存中的变量其值在程序的整个执行过程将会被一直保留。 - 终止程序。正常终止main函数,也有可能是意外终止。
编译的具体过程
这里是比较笼统的写写,感兴趣的可以看看《编译原理》,里面有详细的讲解
一·个源文件的编译还可以分成三步:预处理、编译、汇编
预处理
预处理也叫预编译,程序在预处理阶段会完成如下操作:
#define 定义的全局变量会在预处理的时候把文件中出现的全局变量替换成定义的数值
- 注释的删除
注释是给程序员自己看的,所以会直接删除掉
当然后面还有很多操作,这里就不介绍了
总结:在预处理中都是文本操作
编译
在编译时会生成一个 文件名.s
,是把C语言代码转换成汇编代码
而转换成汇编代码会进行 语法分析
、词法分析
、符号分析
、语义分析
- 这里主要讲
符号分析
:
符号汇总会将我们代码中的全局的符号全部汇总起来
比如全局变量名、函数名;符号汇总不会将局部的变量名汇总进来
经过编译,我们的C语言代码会被转化为汇编代码
汇编,
在汇编式会生成一个 文件名.o
文件
在进行汇编时,会把汇编代码转换成二进制代码(二进制指令),还会形成符号表
符号表就是会给这些全局变量一个地址,并且通过符号表查找变量
如果在符号表找不到对应的变量,那么代表这个变量不存在
链接
程序在链接期间会有二个操作:
- 合并链表
编译器会把之前在汇编中生成多个目标文件相同格式的数据合并成一个可执行程序 .exe
- 符号表的的合并
符号表的合并是指在汇编阶段生成的多个符号表合并成一个符号表,并且重新定位
重新定位是指符号表出现相同的符号时,编译器会选取和符号相关的地址,舍弃另外一个
链接过程符号表的合并和重定向的实际意义是非常大的,因为它保证了符号表中的每一个符号都和有效的地址相关联,我们可以通过该地址找到对应符号,可以让我们跨文件的调用函数,使得我们链接生成的 .exe 文件能够被正常执行。
如果程序在合并符号表时,发现某一符号是无效地址时,程序会发生链接性错误
例如:
上面就是简略的介绍C程序的执行过程,实际上编译器在编译、汇编、链接时还有很多操作
如果详细的介绍的话,那就要深入的去学习编译原理等相关知识
如果很感兴趣可以看看《程序员的自我修养》这本书,里面细致的介绍了程序执行的过程
运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。 - 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。 - 终止程序。正常终止main函数;也有可能是意外终止。
预处理详解
接下来是详细的学习预处理操作的相关的操作
预定义符号
C语言中有许多内置的预定义符号,我们可以在程序中直接使用:
__FILE__ | 进行编译的源文件的路径 |
---|---|
__LINE__ | 文件当前的行号 |
__DATE__ | 文件被编译的日期 |
__TIME__ | 文件被编译的时间 |
__STDC__ | 如果编译器遵循ANSI C,其值为1,否则未定义 |
例如:
int main()
{
int i = 0;
for (i = 1; i <= 10; i++)
{
printf("%s %d %s %s \n%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
return 0;
}
结果:
#define 定义标识符
用 #define
定义的除了可以定义常量,还可以定义字符串、关键字等等
例如:
#define MAX 100 //用 MAX 来代替100这个数值
#define STR "Hello China"
#define print printf("blaue\n")
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠 \ (续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,\
__DATE__,__TIME__ )
重点注意事项:
- #define 定义的标识符会在程序的预处理阶段被全部替换掉。
- 用 #define 定义标识符的时候,不要在末尾加上分号,避免造成程序逻辑的错误。
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的声明方式如下:
# define name(x) x*x
name 是宏的名字
x 是宏的参数
x*x 是宏的内容
注意:
- 参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,宏的参数列表就会被解析成宏的内容。
宏的注意事项:
- 对数值表达式进行求值的宏定义都应该加上括号
避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
比如:
#define name(x) x*x
int main()
{
int ret = name(5+1);
printf("%d", ret);
return 0;
}
注意:宏是完成替换的
- 上面的代码可能是这样想得,5+1 = 6 6*6=36
- 实际上结果并不是36
思路解析:
正确的使用宏:
#define name(x) ((x)*(x))
比如:
这样不管我们以何种方式来使用宏,答案都不会出错
#define 替换规则
在程序中#define定义符号和宏时,需要涉及如下几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号;如果是,它们首先被替换
- 替换文本随后被插入到程序中原来文本的位置;对于宏来说,参数名被他们的值所替换
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号;如果是,就重复上述处理过程
需要注意的是:
- 宏参数和 #define 定义中可以出现其他 #define 定义的符号;但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
# 和 ##
# 是把宏对应的参数转换成对应的字符串,插入到字符串中
例如:
我想把下面的代码,写成一个函数或者宏
int main()
{
int a = 10;
printf("The value of a is %d\n", a);
int b = 20;
printf("The value of b is %d\n", b);
return 0;
}
- 首先函数是不可能实现的,因为函数在函数里内容都是固定的我们只能改变打印的值得大小
所以就用宏来实现,用到 # 符号
#define PRINT(n) printf("The value of "#n" is %d\n", n)
int main()
{
int a = 10;
PRINT(a);
int b = 20;
PRINT(b);
return 0;
}
- 上面就是 # 的应用,把宏对应的参数转换成对应的字符串,插入到字符串中
而 ## 的作用是把二边的符号合并成一个符号
例如:
#define CLA(class,n) class##n
int main()
{
int class108 = 100;
printf("%d\n", CLA(class, 108));
}
结果:
- # 和 ## 在我们日常代码中基本不用,大家在遇到这样的代码时知道它的意思就行了
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数本身带有副作用,那么我们在使用这个宏的时候就可能出现危险,导致不可预测的后果;副作用就是表达式求值的时候出现的永久性效果
比如:
- 想要一个数加 1
x+1 x++
以上二种都实现了我想要的效果,但是第二种如果出现在宏的参数里就会出现副作用
例如:
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 5, b = 4;
int n = MAX(a++, b++);
printf("%d\n", n);
printf("%d %d", a, b);
return 0;
}
结果:
- 宏是完成替换的,如果宏传的参数是带有副作用的,那
a 和 b 的值
也会被改变
而且也不能得到我们想要的结果
宏和函数对比
宏的优点
- 如果写一函数或者宏来进行运算,函数需要传参,接收返回值需要更多的时间
而宏是直接完成替换,所以宏比函数在程序的规模和速度方面更胜一筹 - 进行运算函数必须声明类型,而宏是不用的,一个宏可以完成不同类型的计算任务
只要保证宏传的参数是正确的加没问题
宏的缺点
- 每次使用宏的时候,一份宏定义的代码将插入到程序中;除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的,因为在预处理阶段宏就会被全部替换掉。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
- 宏不能递归
总结:
宏
通常被应用于执行简单的运算
复杂、代码量大
的运算通常由函数
来完成。
而且宏有时候可以做函数做不到的事情
比如:
用宏来开辟动态内存空间
#define MALLOC(num,type) (type*)malloc((num) * sizeof(type))
int main()
{
// 正常开辟动态内存
int* pd = (int*)malloc(10 * sizeof(int));
// 以宏的方式
int* pd = MALLOC(10, int);
}
宏与函数的详细对比优缺点
宏和函数对比的参数 | #define定义的宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 宏是完成替换的,执行的速度会更快 | 存在函数的调用和返回的额外开销,所以相对慢一些。 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测、 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。 |
调试 | 宏不能调试 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | -函数是可以递归的 |
宏和函数命名约定
由于函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者
为了区分宏和函数,我们平时的一个习惯是:
- 宏名全部大写
- 函数名用小写或者不要全部大写
#undef 取消定义的宏
#under 是用来取消定义宏
例如:
条件编译
条件编译就是在满足某些条件下进行编译,不满足则略过
比如:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d ", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
- 以上代码里
#ifdef __DEBUG__
是判断是否定义有__DEBUG__
这个封号
如果有则执行下面一条语句,没有就跳过
常见的条件编译指令:
第一种
int main()
{
#if 3 > 2
printf(“haha\n”);
#endif
}
- 以上是判断
#if
的条件如果为真,就执行#if
到#endif
的语句,否则跳过
- 用#define 来定义
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
- 多个分支的条件编译(类似于
if 和 else
)
#define M 3
#if M < 5
printf("小弟\n");
#elif M == 5
printf("头目\n"):
#else
printf("将领\n");
#endif
4.判断是否被定义
#define MAX 10
#if defined(MAX) #ifdef(MAX)
#endif #endif
----------------------------- 或者
#if !defined(MAX) #ifndef(MAX)
#endif #endif
- 第一种是用来判断是否定义了
MAX
,定义了就执行#if
到#endif
的语句,否则跳过 - 第二种是判断是否定义了
MAX
,没有定义就执 #if 到 #endif 的语句
,否则跳过
- 嵌套指令
#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 <filename>
查找方式:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。 -
本地文件包含
包含形式:#include “filename”
查找方式:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件;如果找不到就提示编译错误。
所以实际上对于库文件也可以使用 “ ”
的形式包含,但是这样做查找的效率就低一些,并且也不容易区分是库文件还是本地文件了
嵌套文件包含
有些时候我们的程序中会出现同一个头文件被重复包含的情况
比如一个stdio.h
头文件被包含多次,这时我们可以使用条件编译来避免头文件被重复包含:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
我们也可以在程序中加入这句话来防止重复包含:
#pragma once
- 避免文件的重复定义