C++程序编译的过程:预处理 -> 编译(优化、汇编)-> 链接
目录
1.预处理
一、包含头文件
二、宏定义指令
三、条件编译
2、编译和链接
一、源代码的组织
二、编译预处理
三、编译
四、链接
五、更多细节
3、命名空间
一、语法
二、使用命名空间
三、注意事项
四、代码示例
预处理指令主要有以下三种:
#include包含头文件有两种方式:
#include也包含其它的文件,如:*.h、*.cpp或其它的文件。
C++98标准后的头文件:
注意:用户自定义的头文件还是用.h为后缀。
无参数的宏:#define 宏名 宏内容
有参数的宏:#define MAX(x,y) ((x)>(y) ? (x) : (y)) MAX(3,5) ((3)>(5) ? (3) : (5))
编译的时候,编译器把程序中的宏名用宏内容替换(直接做对应字符串替换),是为宏展开(宏替换)。
宏可以只有宏名,没有宏内容。
在C++中,内联函数(inline)可代替有参数的宏,效果更好。
C++中常用的宏:
最常用的两种:#ifdef、#ifndef if #define if not #define
#ifdef 宏名
程序段一
#else
程序段二
#endif
含义:如果#ifdef后面的宏名已存在,则使用程序段一,否则使用程序段二。
#ifndef 宏名
程序段一
#else
程序段二
#endif
含义:如果#ifndef后面的宏名不存在,则使用程序段一,否则使用序段二。
在C/C++中,在使用预编译指令#include的时候,为了防止头文件被重复包含,有两种方式。
第一种:用#ifndef指令。
#ifndef _GIRL_
#define _GIRL_
//代码内容。
#endif
第二种:把#pragma once指令放在文件的开头。
#ifndef方式受C/C++语言标准的支持,不受编译器的任何限制;而#pragma once方式有些编译器不支持。
#ifndef可以针对文件中的部分代码;而#pragma once只能针对整个文件。
#ifndef更加灵活,兼容性好;#pragma once操作简单,效率高。
头文件(*.h):#include头文件、函数的声明、结构体的声明、类的声明、模板的声明、内联函数、#define和const定义的常量等。
源文件(*.cpp):函数的定义、类的定义、模板具体化的定义。
主程序(main函数所在的程序):主程序负责实现框架和核心流程,把需要用到的头文件用#include包含进来。
预处理的包括以下方面:
1)处理#include头文件包含指令。
2)处理#ifdef #else #endif、#ifndef #else #endif条件编译指令。
3)处理#define宏定义。
4)为代码添加行号、文件名和函数名。
5)删除注释。
6)保留部分#pragma编译指令(编译的时候会用到)。
将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文件(二进制文件.obj)。
将编译后的目标文件,以及它们所需要的库文件(系统库文件,如iostream)链接在一起,形成一个可执行的.exe程序(Windows系统上)。
1)分开编译的好处:每次只编译修改过的源文件,然后再链接,效率最高。
2)编译单个*.cpp文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误。(直接和间接包含头文件都可以)
3)编译单个*.cpp文件的时候,编译器只需要知道名称的存在,不会把它们的定义一起编译。
4)如果函数和类的定义不存在,编译不会报错,但链接会出现无法解析的外部命令。
5)链接的时候,变量、函数和类的定义只能有一个,否则会出现重定义的错误。(如果把变量、函数和类的定义放在*.h文件中,*.h会被多次包含,链接前可能存在多个副本;如果放在*.cpp文件中,*.cpp文件不会被包含,只会被编译一次,链接前只存在一个版本)
6)把变量、函数和类的定义放在*.h中是不规范的做法,如果*.h被多个*.cpp包含,会出现重定义。
7)用#include包含*.cpp也是不规范的做法,原理同上。
8)尽可能不使用全局变量,如果一定要用,要在*.h文件中声明(需要加extern关键字),在*.cpp文件中定义。
9)全局的const常量在头文件中定义(const常量仅在单个文件内有效)。
10)*.h文件重复包含的处理方法只对单个的*.cpp文件有效,不是整个项目。
11)函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在*.h文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在*.cpp文件中。
12)Linux下C++编译和链接的原理与VS一样。
在实际开发中,较大型的项目会使用大量的全局名字,如类、函数、模板、变量等,很容易出现名字冲突的情况。
命名空间分割了全局空间,每个命名空间是一个作用域,防止名字冲突。
创建命名空间:
namespace 命名空间的名字
{
// 类、函数、模板、变量的声明和定义。
}
创建命名空间的别名:
namespace 别名=原名;
在同一命名空间内的名字可以直接访问,该命名空间之外的代码则必须明确指出命名空间。
1)运算符::
语法:命名空间::名字
简单明了,且不会造成任何冲突,但使用起来比较繁琐。
2)using声明
语法:using 命名空间::名字
用using声明名后,就可以进行直接使用名称。
如果该声明区域有相同的名字,则会报错。
3)using编译指令
语法:using namespace命名空间
using编译指令将使整个命名空间中的名字可用。如果声明区域有相同的名字,局部版本将隐藏命名空间中的名字,不过,可以使用域名解析符使用命名空间中的名称。
1)命名空间是全局的,可以分布在多个文件中。
2)命名空间可以嵌套。
3)在命名空间中声明全局变量,而不是使用外部全局变量和静态变量。
4)对于using声明,首选将其作用域设置为局部而不是全局。
5)不要在头文件中使用using编译指令,如果非要使用,应将它放在所有的#include之后。
6)匿名的命名空间,从创建的位置到文件结束有效。
demo01.cpp
#include // 包含头文件。
#include "public1.h"
#include "public2.h"
using namespace std; // 指定缺省的命名空间。
int main()
{
using namespace aa;
using namespace bb;
using bb::ab;
cout << "aa::ab=" << aa::ab << endl;
aa::func1();
aa::A1 a;
a.show();
cout << "bb::ab=" << bb::ab << endl;
}
public2.cpp
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
#include "public2.h"
namespace aa
{
int ab = 1; // 全局变量。
}
namespace bb
{
int ab = 2; // 全局变量。
void func1() { // 全局函数的定义。
cout << "调用了bb::func1()函数。\n";
}
void A1::show() { // 类成员函数的类外实现。
cout << "调用了bb::A1::show()函数。\n";
}
}
public1.cpp
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
#include "public1.h"
namespace aa
{
void func1() { // 全局函数的定义。
cout << "调用了aa::func1()函数。\n";
}
void A1::show() { // 类成员函数的类外实现。
cout << "调用了aa::A1::show()函数。\n";
}
}
public2.h
#pragma once
namespace aa
{
extern int ab; // 全局变量。
}
namespace bb
{
extern int ab ; // 全局变量。
void func1(); // 全局函数的声明。
class A1 // 类。
{
public:
void show(); // 类的成员函数。
};
}
public1.h
#pragma once
namespace aa
{
void func1(); // 全局函数的声明。
class A1 // 类。
{
public:
void show(); // 类的成员函数。
};
}