任何的C语言的代码都会存在二种环境:
一个源文件最后的可执行程序一共有三步:
.obj
,在Linux环境下目标文件的后缀为.0
)标准C函数库
中被该程序所引用的函数链接库操作
)程序的实际执行值执行环境中进行,程序执行的具体过程分三步:
这里是比较笼统的写写,感兴趣的可以看看《编译原理》,里面有详细的讲解
一·个源文件的编译还可以分成三步:预处理、编译、汇编
预处理也叫预编译,程序在预处理阶段会完成如下操作:
#define 定义的全局变量会在预处理的时候把文件中出现的全局变量替换成定义的数值
当然后面还有很多操作,这里就不介绍了
总结:在预处理中都是文本操作
在编译时会生成一个 文件名.s
,是把C语言代码转换成汇编代码
而转换成汇编代码会进行 语法分析
、词法分析
、符号分析
、语义分析
符号分析
:经过编译,我们的C语言代码会被转化为汇编代码
在汇编式会生成一个 文件名.o
文件
在进行汇编时,会把汇编代码转换成二进制代码(二进制指令),还会形成符号表
符号表就是会给这些全局变量一个地址,并且通过符号表查找变量
如果在符号表找不到对应的变量,那么代表这个变量不存在
程序在链接期间会有二个操作:
可执行程序 .exe
链接过程符号表的合并和重定向的实际意义是非常大的,因为它保证了符号表中的每一个符号都和有效的地址相关联,我们可以通过该地址找到对应符号,可以让我们跨文件的调用函数,使得我们链接生成的 .exe 文件能够被正常执行。
如果程序在合并符号表时,发现某一符号是无效地址时,程序会发生链接性错误
例如:
上面就是简略的介绍C程序的执行过程,实际上编译器在编译、汇编、链接时还有很多操作
如果详细的介绍的话,那就要深入的去学习编译原理等相关知识
如果很感兴趣可以看看《程序员的自我修养》这本书,里面细致的介绍了程序执行的过程
程序执行的过程:
接下来是详细的学习预处理操作的相关的操作
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 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 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的声明方式如下:
# define name(x) x*x
name 是宏的名字
x 是宏的参数
x*x 是宏的内容
注意:
宏的注意事项:
比如:
#define name(x) x*x
int main()
{
int ret = name(5+1);
printf("%d", ret);
return 0;
}
注意:宏是完成替换的
正确的使用宏:
#define name(x) ((x)*(x))
比如:
这样不管我们以何种方式来使用宏,答案都不会出错
在程序中#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));
}
结果:
当宏参数在宏的定义中出现超过一次的时候,如果参数本身带有副作用,那么我们在使用这个宏的时候就可能出现危险,导致不可预测的后果;副作用就是表达式求值的时候出现的永久性效果
比如:
#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定义的宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 宏是完成替换的,执行的速度会更快 | 存在函数的调用和返回的额外开销,所以相对慢一些。 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测、 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。 |
调试 | 宏不能调试 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | -函数是可以递归的 |
由于函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者
为了区分宏和函数,我们平时的一个习惯是:
#under 是用来取消定义宏
例如:
条件编译就是在满足某些条件下进行编译,不满足则略过
比如:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
#include
#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 __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
查找方式:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
本地文件包含
包含形式:#include “filename”
查找方式:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件;如果找不到就提示编译错误。
所以实际上对于库文件也可以使用 “ ”
的形式包含,但是这样做查找的效率就低一些,并且也不容易区分是库文件还是本地文件了
有些时候我们的程序中会出现同一个头文件被重复包含的情况
比如一个stdio.h
头文件被包含多次,这时我们可以使用条件编译来避免头文件被重复包含:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
我们也可以在程序中加入这句话来防止重复包含:
#pragma once