在ANSI C(国际标准C)中的任何一种实现中,存在两个不同的环境
这里创建两个源文件,编译后,观察项目文件中是否生成了目标文件:
object简写obj,也就是目标的意思,这就是目标文件,因此可以说明,每个源文件经过编译处理,都会生成目标文件。
而所有的目标文件加上链接库经过链接器的处理,最后链接生文件后缀为.exe的可执行程序。
链接库是除了自己实现的一些功能以外,编译器所提供的其它功能。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
生成可执行程序的步骤总的来看,其实也就两种情况:编译和链接。
如果细分的话,编译又分成三个步骤:
在第一个步骤预处理阶段要做的事:
在预处理阶段做的事情都叫文本操作,然后生成test.i的文件
在第二个步骤编译阶段要做的事: 笼统的说是把C语言代码转换为汇编代码,实际上以下操作:
第三个步骤汇编做的事:是把汇编指令转换成二进制指令,生成目标文件,并且形成符号表(符号表是在编译的过程中的符号汇总处理,会把每个文件中的全局符号汇总出来,比如说全局的变量名,函数名等等,然后给每个符号带上一个地址,符号+地址就形成了符号表),接下来在链接期间会使用符号表,同时把目标文件链接生成可执行程序。
链接也会细分为两个步骤:
第一个步骤合并段表:是把相同段式的文件进行合并。
第二个步骤符号表的合并和重定位:在编译期间不同的目标文件会形成不同的符号表,此时把这些符号表进行合并,最终的可执行程序中只能有一个符号表。
执行的过程中,如果一个文件中放着函数的声明,另一个文件中放着函数的定义,那么这时符号表有两个相同的符号,那么用哪个呢?
实际上函数声明的符号地址是没有意义的,只是声明实际有没有并不知道,而函数定义的符号地址是有意义的,因此合并的时候就会选择这个有效的符号表进行合并并且重定位,如果不这么做,链接期间就没法使用该函数,因为地址可能无效,这也就会导致链接错误。
汇编阶段形成符号表,链接阶段合并符号表和重定位就是为了在链接期间能够跨文件找到函数。
程序运行的过程:
__ FILE __ //进行编译的源文件
__ LINE __ //文件当前的行号
__ DATE __ //文件被编译的日期
__ TIME __ //文件被编译的时间
__ STDC __ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的。
举个例子:
int main()
{
for (int i = 0; i < 10; ++i)
{
printf("file: %s line: %d date: %s time: %s i = %d\n",
__FILE__, __LINE__, __DATE__, __TIME__, i);
}
return 0;
}
#define MAX 1000
int main()
{
int a = MAX;
printf("%d\n", a);
return 0;
}
#define定义标识符,本质上是文本替换(在预编译过程中),上述代码就是把MAX出现的地方全部替换为1000,预编译后代码就是下面这样子:
int main()
{
int a = 1000;
printf("%d\n", a);
return 0;
}
define不光可以定义整形,绝大多数类型都可以,比如:
#define STR "hello world"
int main()
{
printf("%s\n", STR);
return 0;
}
注:不要在define定义的后面加分号
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的声明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中.
注意:
参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
比如:
#define SQUARE(x) x*x
int main()
{
int a = SQUARE(5);
printf("%d\n", a);//25
return 0;
}
这里替换后实际上就是算5的平方,那么这么定义宏有没有问题呢?
#define SQUARE(x) x*x
int main()
{
int a = SQUARE(5+1);
printf("%d\n", a);//?
return 0;
}
这里的结果为11,并不是36, 这是因为宏的参数不是计算进去的,是直接进行替换,所以上面的代码就可以替换为:int a = 5+1*5+1
,把5+1当作一个整体x后进行替换,因此结果是11。
如果宏的参数带有运算符且与宏体的运算符的优先级不同时,就容易出现问题。所以为了避免这种情况,就需要用括号括起来,把参数当成一个整体,再把整个宏体括起来:
#define SQUARE(x) ((x)*(x))
int main()
{
int a = SQUARE(5+1);
printf("%d\n", a);
return 0;
}
此时就可以把代码替换为:int a = ((5+1) * (5+1))
;这时的结果就是36了。
在定义宏体的时候不要吝啬括号,如果不带,就容易出现意料之外的错误。
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
注意:
如何把参数插入到字符串中?
printf("hello bit\n");
printf("hello"" bit\n");
上面两段代码输出的内容相同,其实两个字符串可以合并成一个字符串。
看下面代码:
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", a);
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;
}
最开始说printf函数里多个字符串会合并成一个字符串,所以#N的作用就是把宏的参数改成字符串的形式而不是N本身。
这里的宏就可以替换为:
#define PRINT(N) printf("the value of "#N" is %d\n", N)
int a = 10;
printf("the value of ""a"" is %d\n", a);
int b = 20;
printf("the value of ""b"" is %d\n", b);
//
这就是#号的作用,类似与插入字符串。
## 的作用
//##会把两边的符号合并成一个符号
#define CLS(Class, Num) Class##Num
int main()
{
int Class123 = 10;
printf("%d\n", CLS(Class, 123));//10
//这句代码在预处理之后就会替换为以下语句:
printf("%d\n", Class123);//10
return 0;
}
什么是副作用,例如:
int a = 10;
int b = 0;
//想让b为11有两种方法
b = a + 1;
//或者
b = ++a;
//这两种方法有什么区别呢
//首先a+1不会改变a的值,执行完后a还是10
//而++a会让a永久自增1,执行完后a为11
//所以说++a这种方式在某些场景下会产生副作用
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 5;
int b = 4;
int m = MAX(a++, b++);
printf("%d %d %d", m, a, b);
return 0;
}
三个数分别是多少?
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 5;
int b = 4;
int m = MAX(a++, b++);
//可以替换为:
int m = ((a++) > (b++) ? (a++) : (b++));
// 5++ > 4++ ? 6++ : 5++;
//表达式为真整个表达式的结果为表达式1的结果
//m = 6,此时a已经自增为7,而b自增为5,表达式二没计算
printf("%d %d %d", m, a, b);
//6 7 5
return 0;
}
所以像上面带有副作用的宏参数,在使用的时候会产生一些无法预测的后果,进而出现一定的危险。
到这里,也许会发发现宏和函数在不少情况下还是非常相似的,就比如上面计算两个数的较大值。
那么和函数的形式相比,哪个比较好些?
其实是宏好一些,原因如下:
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏则可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
宏是类型无关的.
宏的缺点,当然和函数相比宏也有劣势的地方:
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
int main()
{
//参数带类型是函数做不到的
int* ret = (int*)malloc(10 * int);
return 0;
}
但是宏就可以:
#define MALLOC(num,type) (type*)malloc((num) * sizeof(type))
int main()
{
int* ret = MALLOC(10, int);
//就可以替换为:
int* ret = (type*)malloc((num) * szieof(type));
//即:
int* ret = (int*)malloc(10 * sizeof(int));
return 0;
}
总结一下宏和函数的区别:
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
这条指令用于移除一个宏定义。
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
例如:当根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
这时,许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
顾名思义,条件满足就编译,否则不编译。
在编译一个程序的时候如果要将一条语句(一组语句)编译或者放弃是很方便的。因为有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#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\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
这里#ifdef的意思是,如果定义了__DEBUG__则运行该条语句,如果不想打印只需要把上面define定义的__DEBUG__注释或者删掉,该语句就不会执行了。
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
比如:
//如果表达式为真就执行这条语句
//否则不执行,在预处理阶段就被干掉了
#if 1
printf("666\n");
#endif
//注意这两个是一对,都要写
//下面这种方法也可以
#define __DEBUG__ 1
#if __DEBUG__
//...
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
比如:
#define M 3
int main()
{
#if M<5
printf("1");
#elif M==5
printf("2");
#else
printf("2");
#endif
return 0;
}
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
比如:
//如果定义了的情况执行打印
#define M 1000
int main()
{
#if defined(M)
printf("已定义");
//这种写法等价于
//#ifdef M
#endif
return 0;
}
-----
//如果没定义了的情况执行打印
int main()
{
#if !defined(M)
printf("未定义");
//这种写法等价于
//#ifndef M
#endif
return 0;
}
如果一个头文件被重复包含多次,那么它的内容也会被重复包含多次,这样就会造成代码非常的冗余,那么有没有一种方法能让文件只能被包含一次呢?
有这么两种办法:
//比如说test.h是头文件
#ifndef __TEST_H__
#define __TEST_H__
//代码块...
#endif
//首先第一次包含,先判断是否定义该符号
//没有定义表达式为真,执行下面代码
//先定义__TEST_H__,然后执行代码
//当再次包含该头文件时,判断是否定义了符号
//发现已经定义了,那么表达式额为假,下面就不执行了
//所以后面再次包含头文件都不会产生效果了
//这段代码的作用就是防止头文件被多次包含
另一种方法:
#pragma once
//代码块....
//这条指令的作用也是让头文件只能被包含一次
包含头文件的方式有两种:
//包含库文件
#include
//包含自己的头文件
#include "test.h"
一个是尖括号,一个是双引号,那么有什么区别呢?
如果是包含库目录的头文件一律用尖括号(用双引号也可以,但是效率会慢,不推荐),包含自己的头文件用双引号。
本篇完。