1.程序文件分类
(1)头文件
(1)后缀:c和c++头文件都是以.h结尾
(2)作用
(1)存放各种声明
(1)#开头的各种预处理指令的声明,比如宏定义,#include包含其它文件
(2)各种类型的声明,比如结构体,联合体,类类型,全局变量定义
(3)各种函数声明
(2)头文件还可以包含普通函数定义和内联函数定义
(2)源文件
(1)后缀:.c/.cc/.cpp/.cxx都可以
(2)作用
包含需要被编译成机器码的字符形式的程序代码,主要内容就是各种自定义算法函数
2.程序编译链接过程
(1)编译链接的4个过程
不管是c还是c++,编译链接分成了4个过程
(1)预处理(预编译)
(1)实现命令
gcc/g++ -E a.c/a.cpp -o a.i
(2)处理程序:预处理器
(3)作用:处理所有#开头的预处理指令,其中最重要的一项就是就将#include
包含的文件全部展开到源文件文件中。
(4)得到的.i文件
得到后.i文件成为扩展后的源文件,是真正意义上的完整的源文件
,也称为一个编译单元(转换单元)。编译器是以一个.i文件为单
元进行下一步工作的。
(2)编译
(1)实现命令
gcc/g++ -S a.c/a.cpp/a.i -o a.s
(2)处理程序:编译器
当被编译文件是a.c/a.cpp时会默认进行预处理的操作
(3)作用:将字符编码的源C代码编译成为字符码编码的机器汇编
在编译的过程中会将不同的符号标记上链接属性,无连接,
内链接,外链接,留给链接器链接时使用。
(4)得到的.s: 机器汇编
(3)汇编
(1)实现命令
gcc/g++ -c a.c/a.cpp/a.i/a.s -o a.o
(2)处理程序:汇编器
(3)作用:将机器汇编编程二进制的机器码
(4)得到的.o
称为目标文件(object文件),在uix和linux是ELF格式的文件。
(4)链接
(1)实现命令
gcc/g++ *.c/*.cpp/*.i/*.s/*.o -o a.out
(2)处理程序:链接器
(1)如果制定被链接的文件不是.o文件的话,或默认执行
前面的预处理,编译,汇编等操作。
(3)将机器码的多个.o目标文件最后链接成为一个可执行文件,.o文件
被分成了很多的段,不同.o文件的各个端段会被组合在一起。
链接的过程主要做如下两件事:
(1)符号统一:
根据编译时留下的各种符号的链接属性,将强弱符号
合并。程序中符号拥有的不同链接属性在链接时就决
定了不同符号(函数,变量)的作用域的问题。
链接的种类:内链接/外链接/无连接
(2)地址重定位:
因为.o文件中所有的机器码都被定为从0地址开始运行的,
但是实际上链接时需要根据运行环境来决定组合在一起得
到的可执行文件从什么地址开始运行。
(4)默认得到的a.out的可执行文件
默认得到a.out文件,可以通过-o指定为随意的名字,在windows
下,可执行文件往往以.exe结尾。
可执行文件与目标文件一样,也是ELF格式文件。
3. 符号的作用域
在前面讲函数/全局变量和局部变量时,我们已经讲到了有关作用域的问题,作用域
指的就是名称起作用的范围。
(1)名称的隐藏
在c中遇到这种情况时,局部变量会屏蔽(隐藏)全局变量,这时在函数内部
是不能访问全局变量的,如果想访问,只能将它们定义成为不同的名称。
但是在c++中情况有所变化,我们可以通过::访问外部作用域符号,在上面描述的
例子中,我们可以使用::访问全局变量。
例子1
#include <iostream>
#include <string>
using namespace std;
int a = 100;
int main(int argc, char **argv)
{
int a = 200;
cout << "inner a = " << a << endl;
cout << "outer a = " << ::a << endl;
return 0;
}
(2)全局作用域再讨论
(1)什么是全局作用域
定义在全局作用域的符号的作用域。
(2)全局作用域分类
(1)从形式上看,全局作用域由什么决定
(1)定义位置
(2)声明
(3)受到关键字的影响,extern/static
(2)全局作用域分为两种
(1)文件内作用域
符号只在编译单元内有效。
(2)跨文件作用域
除了在本编译单元内有效外,还其它编译单元内也有效,但是需要做相应的声明。
4. 再次讨论只能有一个定义,但是可以有多个声明的规则
(1)强弱符号总结
“定义”为强符号,“声明”为弱符号,链接时,编译器会根据同名强弱符号的链接属性
将它们做合并。
生成为一个可执行文件时,每个符号必须唯一的。但是在强弱符号的定义中,强符号(定义)
只能有一个,弱符号(声明)有很多。
(1)全局变量
(1)强符号(定义):被初始化了的
(2)弱符号(声明):未被初始化的
(2)全局函数
(1)强符号:包含{ }的函数定义
(2)弱符号:函数声明
对于普通函数来说,默认情况下其作用域为跨文件作用域,默认是extern
修饰的,当使用static修饰时,全局作用域范围被限制在了一个转换单元
内部。
注意内联函数是一个特殊的例子,这在讲内联函数时已经提到过,内联函数在
一个转换单元内只能有一个定义,但是在跨文件的全局作用域里面可以有多个定义。
对于结构体类类型定义来说,有着与内联函数形同的情况,在一个转换单元内
只能有一个定义,但是在跨文件的全局作用域里面可以有多个定义。
防止头文件包含的重要性:
通常情况下,像类结构体和宏定义等的声明,以及内联函数的定义都是
放在了头文件中,这些虽然允许在跨文件的全局作用域中重复定义,但是
在一个转换单元内(本文件内)是不允许重复定义的。这些声明往往都是
存放在了头文件中,因此防止头文件包含就是为了防止这些被重复定义。
使用如下条件编译是非常重要的,因为可以有效防止在一个转换单元中
同一个头文件重复包含。
5. 链接属性与符号作用域
(1)链接属性与符号作用域的对应关系
(1)无链接:对应局部作用域,典型的就是局部变量的作用域
(2)内链接:在一个编译单元内有效的全局作用域,典型的就是staic修饰的
为什么需要内链接,或者说为什么需要static修饰函数或者全局变量?
答:防止命名冲突。
(3)外链接:外链接包含内链接,全局作用域为跨文件作用域,典型就是extern修饰的函数和全局变量
,其它文件想要使用时,需要做声明。
(2)符号的作用域是由链接属性决定
g++进行编译链接时,链接符号的过程是这样的。
(1)编译器编译时留下每个符号的链接属性
编译器操作的对象是单个的单元,编译不能将多个文件编译成为一个文件。
编译时:
(1)如果在代码块内找到符号定义,该符号为无链接。
(2)在编译单元内的全局作用域内找到该符号,该符号为内链接
(3)如果在本编译单元内没有找到该符号
(1)如果该符号没有声明,编译报错,表示该符号无法找到
(2)如果发现了该符号的声明,将该符号标记为外链接
(2)链接器根据每个符号的链接属性进行符号合并
链接时:
(1)将所有内链接属性的定义与声明进行符号统一。
(2)到其它编译单元中去寻找标记为外链接属性的符号,如果没有链接报错,否者将强弱符号统一。
我们常常称为的作用域其实就是链接域,只是为了学习的方便我们习惯以作用域进行称呼。
6. 函数全局变量局部变量总结
(1)内存的各种不同的管理方式
这在上一章已经讲解过。
(2)全局变量和局部变量对比分析
这里忽略c++中成员变量的情况。
(1)全局变量
(1)存储位置:静态数据区.bss或者.data中
(2)生命周期:由存储的位置决定,因为存在静态存储区,表明其生命周期为整个程序运行期间都有效
(3)作用域:为全局作用域
(1)本文件作用域:对应内链接域,从定义的位置到本文件末尾,可以被声明扩展
(2)跨文件作用域:对应外链接域(包含内链接域),其它文件需要使用这个全局变量时,必须声明
(4)与全局变量声明和定义相关的关键字
(1)static修饰:表示全局变量只能在本文件有效,其它文件不能使用,换句话说只有内链接,无外链接
(2)extern修饰:默认就是extern的,表示该全局变量的有跨文件作用域,也就是说有外链接,可以被其他文件
访问,但是需要做相应的声明,比如 extern int a;,这个声明可以放在函数外部,也可以
放在某函数内部,放在函数内部时,说明这个a只能在这个函数内有效,其他函数不能访问。
(2)局部变量
所谓局部变量指的是代码块{ }内定义的变量,实际上函数只是代码块
的一种,类,循环,switch都是代码块,里面定义的变量都是局部变量。
所以这里讨论的局部变量不包含结构体成员,类成员,联合体成员的讨论。
因为这些的成员受到包含它们的对象的影响。
(1)自动局部变量
(1)存储位置:函数栈
(2)生命周期:由存储的位置决定,栈决定了它的生命周期为函数运行期间有效,之后释放
(3)作用域:对应无链接,定义的位置到函数结尾,不能通过声明改变作用域
(4)相关关键字
(1)auto:默认就是auto修饰的,表示自动局部表变量
(2)静态局部变量:
(1)存储位置:空间开辟于静态数据区.bss或者.data中
(2)生命周期:存储的位置决定了声明周期为整个程序运行期间都有效
(3)作用域:定义的位置到函数结尾,不能通过声明改变作用域,无链接
(4)相关关键字
(1)static:静态局部变量
(3)总结
这里讨论的变量不包含函数成员变量,函数成员变量的作用域和有效期主要是受到对象的限制。
(1)不管是全局变量还是局部变量,生命周期(有效期)由存储的位置决定
(2)作用域由定义的位置和声明决定(局部变量无声明),声明与定义与static/extern密切相关
(2)static关键字在修饰全局变量/全局函数和局部变量时,作用大不相同
(1)修饰局部变量时,改变的是存储的位置,进而改变的是生命周期。
(2)修饰全局变量时,改变的是链接域,与存储位置无关,这一点与修饰函数同。
(4)讲解过程中直接举例子说明
7. c++中的命名空间
(1)全局命名空间,如果名称在代码块外部,并且没有制定特殊的命名空间的话
为什么c++引入命名空间?
回忆:
(1)哪些只在编译时有效的定义
类定义/结构体定义/宏定义/内联函数定义,这些定义在同一个编译单元中
不能冲突,所以在同一个编译单元中防止同文件重复包含非常重要。
但是可以在不同的编译单元被重复包含,比如a.cpp和b.cpp完全可以包含同
一个头文件c.h。
之所以是这样的原因,因为这些类型定义只在编译和预编译期间起到类型说
明作用,一旦编译变成.o文件后,在进行链接时,这些类型将不复存在。
所有我们知道,对于类定义/结构体定义/宏定义/内联函数定义来说,只要我们
能够防止它们在同一个编译单元被重复定义即可,我们不用担心它们的命名空间
的问题,因为他们只在编译时有效。
(2)哪些在链接时有效的定义
普通函数定义/全局变量定义,这些定义在链接需要和它们的声明进行符号统一,
这些符号在不同文件中都会重复出现,但是定义只能有一个,否者会照成二义性
,因此必须防止定义名字的的冲突。
因此我们知道,命名空间这个概念实际上对于普通函数和全局变量来说时很重要的
,在c中由于命名空间的划分过于简单化,只有全局和局部命名空间,非常容易出
现相同函数和全变量定义重名的情况,c中引入了static进行命名空间的简单限制
,在c++中引入了更加精确的命名空间的概念。
定义函数或者全局变量虽然重名了,但是只要属于不同的命名空间,就没有任何
问题。
从上面讲述中,我们不难看出,于命名空空间密切相关的实际上是普通函数的定义/全局变量的
定义,为类类型/结构体/内联函数/宏定义几乎不需要给其设定命名空间。
(2)c++中的命名空间
为了缓解c语言中命名空间的问题,c++中引入了自定义命名空间。
命名空间的定义和声明
举例:
命名空间定义:
b.cpp
#include <iostream>
#include <string>
namespace bregion
{
extern const int a = 10;
}
注意需要使用extern进行修饰。
命名空间声明
在另一个cpp文件中使用时,比如a.cpp中使用时,需要声明命名空间。
#include <iostream>
#include <string>
namespace bregion
{
extern const int a;
}
int main(int argc, char **argv)
{
std::cout << bregion::a << std::endl;
return 0;
}
(3)使用using声明
在前面的例子中,如果希望使用希望在使用a时,不想加上bregion::
的繁琐前缀的话,我们可以使用using进行声明下。
上例可以改为如下:
#include <iostream>
#include <string>
namespace bregion
{
extern const int a;
}
using bregion::a;
//或者使用using指令
//using namespace bregion;
int main(int argc, char **argv)
{
std::cout << a << std::endl;
return 0;
}
例子说明:
本例子中打印a时,不需要指定域名,因为using bregion::a;做了
声明,同时使用using namespace bregion;这种方式也是可以的,这两者的区别
是,前一种之声明了a,后一种表示声明了bregion中所有的符号,这与如下。
using namespace std::cout;
using namespace std;
是一回事。
(4)函数与命名空间
(1)函数直接定义在命名空间中,比如下面的例子。
b.cpp文件
#include <iostream>
#include <string>
namespace bregion
{
extern const int a = 10;
extern int mymax(int a, int b)
{
return a>b ? a : b;
}
extern int mymin(int a, int b)
{
return a<b ? a : b;
}
}
a.cpp文件
#include <iostream>
#include <string>
namespace bregion
{
extern int mymax(int a, int b);
extern int mymin(int a, int b);
}
using namespace bregion;//using指令
int main(int argc, char **argv)
{
std::cout << mymax(10, 20) << std::endl;
std::cout << mymin(50, 40) << std::endl;
return 0;
}
例子说明:
在本例子中,是直接将函数定义写在了命名空间中,实际上我们完全可以将
函数的定义写在命名空间的外部,而只是将声明写在命名空间里面,而且
很多时候我们都是将命名空间放在了头文件中。
比如可以将上面的例子改成下面的样子。
b.h文件
#ifndef H_B_H
#define H_B_H
namespace bregion
{
extern const int a;
extern int mymax(int a, int b);
extern int mymin(int a, int b);
}
#endif
b.cpp文件
#include <string>
#include "b.h"
extern int bregion::mymax(int a, int b)
{
return a>b ? a : b;
}
extern int bregion::mymin(int a, int b)
{
return a<b ? a : b;
}
a.cpp文件
#include <iostream>
#include <string>
#include "b.h"
using namespace bregion::mymax;//using声明
using namespace bregion::mymin;//using声明
//或者使用using指令
//using namespace bregion;
int main(int argc, char **argv)
{
std::cout << bregion::a << std::endl;
std::cout << mymax(10, 20) << std::endl;
std::cout << mymin(50, 40) << std::endl;
return 0;
}
(5)函数模板与命名空间
这与函数与命名空间其实是一样的,但是在一般情况下,模板会直接定义
在命名空间中,因为分开定义的和声明的话,在模板定义时需要制定export
关键字,但是很多编译器并不兼容该关键字,所以我们就不分开定义了。
a.h文件
#ifndef H_B_H
#define H_B_H
namespace bregion
{
template <class T> T mymax(T a, T b)
{
return a>b ? a : b;
}
template <class T> T mymin(T a, T b)
{
return a<b ? a : b;
}
}
#endif
a.cpp文件
#include <iostream>
#include <string>
#include "a.h"
using namespace bregion;
int main(int argc, char **argv)
{
std::cout << mymax(10, 20) << std::endl;
std::cout << mymin(50, 40) << std::endl;
return 0;
}
(6)命名空间是可以扩展
比如在a.h中
namespace A
{
}
namespace B
{
}
/* 这是对第一个namespace A扩展 */
namespace A
{
}
在多个地方进行命名空间内内容的声明都可以
(7)没有名字的命名空间
(1)定义格式
namespace
{
}
(2)限制
每一个文件(转换单元)只能有一个没有名字的命名空间,在本文件中
如果出现了其它的没有名字的命名空间,认为是对第一个未命名空间的扩展。
(3)意义
c++中没有名字的命名空间表示这个命名空间的名称只在本文件(转换单元)
内有效,其它文件无法使用,这与在c中直接使用static将全局变量和全局
函数进行封锁是一样的效果,只是在c++中即延续了static的做法,又增加了
没有名字命名空间的做法。
(8)给命名空间起别名
(1)给别名的目的
当有些命名空间的名字很长的时候,就可以给一个非常简短的别名,
便于记忆和使用。
(2)例子
a.h文件
#ifndef H_B_H
#define H_B_H
namespace project_model2_plane1
{
template <class T> T mymax(T a, T b)
{
return a>b ? a : b;
}
template <class T> T mymin(T a, T b)
{
return a<b ? a : b;
}
}
/* 注意要放在命名空间的定义后面 */
namespace alias = project_model2_plane1;
#endif
a.cpp文件
#include <iostream>
#include <string>
#include "a.h"
using namespace alias;
int main(int argc, char **argv)
{
std::cout << mymax(10, 20) << std::endl;
std::cout << mymin(50, 40) << std::endl;
return 0;
}
例子说明:
在本例子中给命名空间project_model2_plane1起了一个别名叫alias。
使用命名空间时,使用alias时就可以。
(9)命名空间也可以嵌套包含
比如看下面这个例子:
a.h文件
#ifndef H_B_H
#define H_B_H
namespace outer
{
template <class T> T mymax(T a, T b)
{
return a>b ? a : b;
}
namespace inner
{
template <class T> T mymin(T a, T b)
{
return a<b ? a : b;
}
}
}
#endif
a.cpp文件
#include <iostream>
#include <string>
#include "a.h"
//using namespace outer;
//using namespace outer::inner;
int main(int argc, char **argv)
{
std::cout << outer::mymax(10, 20) << std::endl;
std::cout << outer::inner::mymin(50, 40) << std::endl;
return 0;
}
例子分析:
在本例中,outer是外部命名空间,在里面还有一个叫inner的内部命名空间
,在调用这两个命名空间里面的函数时,需要加outer::和outer::inner::
的前缀。
如果不希望加繁琐的亲啊最也可是使用using声明
using namespace outer::mymax;
using namespace outer::inner::mymin;
或者using指令
using namespace outer;
using namespace outer::inner;
8. 预处理器详解
(1)作用
预处理操作,这在前面已经提到过,是编译链接过程的第一个步骤。
实际上预处理器是编译器的一个部分。
预处理的目的是为了处理所有#的预处理指令,预处理指令之所要加#,主要是
为了和c/c++中的正常指令区分开。
与处理完成后得到**.i文件才是真正完整意义上的c源文件。
(2)预处理指令汇总
以下这些指令在c和c++中的作用完全一致。
(1)#include:文件包含,常见的是头文件包含
(2)条件编译指令
(1)
#if
#elif
#else
#endif
(2)
#ifdef
#ifndef
#if defined
#if !defined
(3)宏定义
(1)#define:定义一个宏
(2)#undef:删除一个宏
(4)#line:重新定义当前行号和文件名
(5)#error:输出编译出错消息,并且终止编译
(6)#pragma:特殊操作指令
9. 预处理之文件包含#include
(1)一般情况下,#include都是用来包含头文件的,但是实际上#include可以
包含任何文件,只要程序能够通过,包含任何文件都可以,比如可以包
含卡它的.c/.cpp文件。
(2)预处理完成后,“#include 文件”语句会被展开后的文件内容替代。
(4)一个#include只能包含一个文件,而且允许嵌套包含(间接包含)
(5)包含头文件时<>和""的异同
<>表示直接到指定目录下查找头文件,""表示先在自己制定的目下查看
找不到才到系统目下查找,具体到系统那个目下查找往往是由编译器决定的。
#include "/aa/dd/aa.h"
表示先在制定的绝对路径下查找,找不到再到系统目录下查找。
#include "aa.h"//同 #include "aa.h"
表示现在当前目录下查找,找不到再到系统目录下查找。
10. 预处理之宏定义
(1)宏定义的作用范围
宏定义的作用范围从定义的位置到本文件的末尾或者遇到#undef符号为止
(2)宏定义规则
(1)宏名惯用大写字母,与变量名区别,小写命名
宏定义也有,少见,比如stdin,stdout,__func__
(2)宏定义不是语句,不需要在末尾加分号
(3)只做替换,不分配空间
(3)宏定义需要注意的问题
(1)宏定义不做类型和正确性检查
经典例子:
#define pi 3.1415q
不会对3.1415q合法性做检查,只会替换,至于该数据合不合理只能由
后面的编译检查。
(2)不能自己嵌套自己,但是可以嵌套其它宏定义
嵌套其它宏定义时,其它宏定义的位置可以在后面,因为宏定义只做替换
(1)嵌套自己是错误的
#define R R*3
(2)嵌套别的宏是可以的
#define R 3.0
#define PI 3.1415926
#define AREAR PI*R*R
下面这种写法也是对的
#define AREAR PI*R*R
#define R 3.0
#define PI 3.1415926
(3)宏定义取消方法
#define R 3.0
#undef R
(4)宏定义到底有哪些好处
(1)对于使用频繁的名称,可以使用宏定义替换,防止大量修改
(2)简化复杂表达式,有利于降低代码复杂度
(3)实现有条件编译,实现跨平台程序的实现
(4)对于常量数使用宏定义来代替,提高代码的可理解性
(5)无参宏定义
(1)只有宏名没有宏体的宏,比如
#define BUG
这样的宏大都用于条件编译中,这在后面讲条件编译的时候会使用。
(2)有宏体宏
#define PI 3.1415926
#define myname "张三"
常常用于定义符号常量.
(6)带参宏定义
(1)带参宏举例
#define max(a, b) ((a)>(b)?(a):(b))
(2)带参宏作用:常常用于简化复杂问题
(3)使用带参数宏需要注意的一些问题
(1)宏名与参数列表之间不要隔空格
(2)带参宏定义中一定要使用括号保证正确性
#define POWER(R,R) R*R
int va = POWER(1+2, 2+3);
宏替换完成后,就变成了
int va = 1+2 * 2+3;
所以宏定义应该改为:
#define POWER ((R)*(R))
(4)带参宏与函数的区别
(1)处理时间
(1)宏的处理时间实在预编译的时候
(2)函数是在运行时被处理的
(2)有关参数
(1)宏定义的参数只是为了实现替换,因此
没有类型,因为没有类型,也就不会分配
空间存放参数值,因为凡是有类型都会涉及
变量或者常量空间。
(2)函数有参数类型,有变量空间,如果传参是引
用的话,实参形参共用变量空间。
(5)是否改变程序长度
(1)宏替换会改变程序长度
(2)函数没有展开的过程,只有调用的过程,不会改变程序长度
(6)是否占用运行时间
(1)宏定义是在被预处理时处理的,不占用运行时间
(2)显然函数是要占用运行时间
(2)带参宏与函数与内联函数
(1)调用频繁的简短函数(2-5代码,没有循环),常常使用带参宏
替换,因为没有函数的调用开销,但是程序的代码量会升高。
所以是否使用带参宏替换函数,要认真权衡效率和内存消耗。
#define MAX(a, b) ((a)>(b)?(a):(b))
可以替换
int max(int a, int b) {
return a>b?a:b;
}
(2)带参宏替代简短函数的功能常被内联函数替代
内联函数兼具带参宏的和函数的共同特点。
(1)内联函数像向宏一样替换展开
(2)内联函数跟普通函数一样会做参数类型检查,但是参数不会分配空间
(3)带参宏都建函数复杂原型时,是内联函数不能替代的
比如讲系统编程遇到的例子
#ifndef ___RECODE_LOCK__
#define ___RECODE_LOCK__
#define read_lock(fd, l_whence, l_start, l_len) \
lock_set(fd, F_SETLK, F_RDLCK, l_whence, l_start, l_len)
#define read_lockw(fd, l_whence, l_start, l_len) \
lock_set(fd, F_SETLKW, F_RDLCK, l_whence, l_start, l_len)
#define write_lock(fd, l_whence, l_start, l_len) \
lock_set(fd, F_SETLK, F_WRLCK, l_whence, l_start, l_len)
#define write_lockw(fd, l_whence, l_start, l_len) \
lock_set(fd, F_SETLKW, F_WRLCK, l_whence, l_start, l_len)
#define unlock(fd, l_whence, l_start, l_len) \
lock_set(fd, F_SETLK, F_UNLCK, l_whence, l_start, l_len)
int lock_set(int fd, int l_ifwset, short l_type, short l_whence, \
off_t l_start, off_t l_len)
{
int ret = -1;
struct flock f_lock;
f_lock.l_type = l_type;
f_lock.l_whence = l_whence;
f_lock.l_start = l_start;
f_lock.l_len = l_len;
return(fcntl(fd, l_ifwset, &f_lock));
}
#endif
(3)带参数宏实现复杂表达式
(1)例子1
#define FUN(a,b) (((a)+(b))*((a)-(b)))/a*b
(2)例子2
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ( { \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) ); } )
11. 预处理之条件编译
(1)条件编译的作用
(1)注销代码
(2)防止文件重复包含
(3)有选择性的编译代码内容,实现综合性的跨平台代码
(2)#if #elif #else #endif
(1)集中组合
(1)组合1
#if 条件
需要条件编译代码
#endif
(2)组合2
#if 条件
需要条件编译代码
#else
需要条件编译代码
#endif
(3)组合3
#if 条件
需要条件编译代码
#elif 条件
需要条件编译代码
#elif 条件
...
#else
需要条件编译代码
#endif
(2)需要注意的地方
(1)如果条件为真,编译该条件下的代码。
(2)如果多个条件为真,只有第一个为真的条件下的代码得到编译。
(3)为真的条件必须是整形常量(非0为真,0为假)
(4)为了增加条件的可读性,可以将常量定义为宏常量
(5)不要将变量作为条件否者直接作为假条件
(6)必须使用#endif结尾
(3)例子
#define COND1 1
#define COND2 3
int a = 10;
int main(int argc, char **argv)
{
#if a
printf("这是满足编译条件1时的代码提示的内容\n");
#elif COND2
printf("这是满足编译条件2时的代码提示的内容\n");
#elif 0
printf("这是满足编译条件3时的代码提示的内容\n");
#else
printf("这是满足编译条件4时的代码提示的内容\n");
#endif
return 0;
}
例子分析:
#if a
这里使用了a作为预编译的判断条件,这是不允许的,出现这样的情况
的时候会直接将条件人为为假。
#elif COND2
这里使用了宏常量作为编译条件,COND2宏值为3,显然为真,主要是为
了提高代码可识别性。
#elif 0
这里直接使用整形数0表示条件,显然0代表条件为假。
#else
当所有条件都不满足时就条件编译#else后面的内容。
(3)#ifdef #else #endif 与 #ifndef #else #endif
(1)组合
(1)#ifdef
#ifdef 条件
需要条件编译代码
#endif
或者
#ifdef 条件
需要条件编译代码
#else
需要条件编译代码
#endif
(2)#ifndef
#ifndef 条件
需要条件编译代码
#endif
或者
#ifndef 条件
需要条件编译代码
#else
需要条件编译代码
#endif
(2)例子
#include <iostream>
#include <string>
//#define MCU51
#define MCUSTM32
int a;
int main(int argc, char **argv)
{
#ifdef MCUSTM32
printf("用于51上的代码\n");
#endif
#ifndef MCU51
printf("用于stm32上的代码\n");
#endif
return 0;
}
例子分析:
#ifdef MCUSTM32
printf("用于51上的代码\n");
#endif
如果定义了MCUSTM32宏,就编译接着的这段代码,显然这个宏是存在的。
#ifndef MCU51
printf("用于stm32上的代码\n");
#endif
#ifndef是以MCU51不存在为真,显然该宏定义不存在。
(3)需要注意的地方
(1)#ifdef与#ifndef都是以判断宏名存不存在作为真假判断,所以
有没有宏体没有任何关系,所以在上面的例子中,宏都是没有
宏体的。
不要使用变量作为判断条件,否者直接人为条件为假。
(2)必须以#endif结尾
(3)#ifdef常用用于构建跨平台的综合性程序
(4)#ifndef常用于防止头文件重复包含。
#ifndef H_XXX_H
#denfine H_XXX_H
头文件的内容
#endif
这样的定义可以有效的防止在一个转换单元中,同一个头文件被多次包含。
(3)#if defined #else #endif 与 #if !defined #else #endif
(1)组合
这个与#ifdef等的情况非常的类似。
(2)例子
#include <iostream>
#include <string>
//#define MCU51
#define MCUSTM32
#define MCUARM
int a;
int main(int argc, char **argv)
{
#if defined MCU51
printf("用于51上的代码\n");
#endif
#if defined MCU51 && MCUARM
printf("用于51和MCUARM上的代码\n");
#endif
#if !defined MCU51
printf("用于stm32上的代码\n");
#endif
return 0;
}
(3)需要注意的地方
(1)同样使用宏名是否存在作为判断真假的条件,所以可以没有宏体
(2)不要使用变量作为条件,否者直接认为条件为假
(3)从例子中我们很容易看出,完全可以替代#ifdef和#ifndef,
但是反过来就不行,因为#if defined和#if !defined的方式是可以实现条
件的&&和||等运算的,但是#ifdef和#ifndef却不行。
12. 预处理之常见标准预处理宏
(1)标注预处理宏清单
(1)__LINE__
当前代码行号,十进制整形数
(2)__FILE__
源文件的名称,字符串常量(字符串字面量)
(3)__func__
当前所处的函数的名称
(4)__DATE__
源文件被处理的日期,是字符串常量,格式为mmm dd yyyy
(5)__TIME__
源文件被编译的时间,字符串常量,格式为hh::mm::ss
(2)例子
#include <iostream>
#include <string>
int main(int argc, char **argv)
{
std::cout << "file:" << __FILE__ << std::endl;
std::cout << "line:" << __LINE__ << std::endl;
std::cout << "func:" << __func__ << std::endl;
std::cout << "date:" << __DATE__ << std::endl;
std::cout << "time:" << __TIME__ << std::endl;
return 0;
}
运行结果:
file:a.cpp
line:9
func:main
date:Aug 3 2016
time:11:48:05
13. 预处理之#error和#pragram预处理指令
(1)#error
打印预处理时导致的错误,并且终止程序编译
(2)例子
比如我想知道在某个头文件中是否有定义某个名称的宏,比较笨的做法就是
打开头文件一行一行的查找,还有一个办法就是#error打印提示,但是需要
和#ifdef结合使用。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#ifdef stdin
#error stdin be defined!!
#else
#error stdin not be defined!!
#endif
int main(int argc, char **argv)
{
return 0;
}
例子分析:
显然这个stdin在stdio.h的头文件中是有定义的,因此#error的输出结果为
#error "stdin be defined!!"。
(2)#pragram
作用非常多,比如实现变量存储空间的手动对其,一般都是自动对齐,这一个的用法略。
14. 断言assert()
(1)为什么使用断言
assert()是一个宏,c和c++都支持这个关键字,往往用于程序非常关键的位置
进行异常提示的,所谓断言就是我断定某件事情在正常的程序逻辑中是不应该
发生的,但是还是发生了,那么这就是非常严重的错误,需要处理,这种严重
的错误就需要提示具体信息,在这种情况下我们常常使用断言来操作。
使用断言需要包含cassert宏。
(2)例子
#include <iostream>
#include <string>
#include <cassert>
int main(int argc, char **argv)
{
int a = 5;
assert(a < 5);
return 0;
}
运行结果:
int main(int, char**): Assertion `a < 5' failed.已放弃
例子分析:
程序中断言a应该小于5的,但是a实际上违反了这个规则,所以断言及时终止
了程序,并提示该处断言被打破。
网友评论