1. 包含文件
#include
编译指令可以将外部文件包含在源文件中,在预编译时,预编译器会使用外部文件的内容替换掉#include
指令
C++
中,如果需要使用其它文件中的函数,甚至是编译好的库、系统调用函数,需要提供函数声明以告诉编译器函数的形式,并等到链接时再从外部查找函数的内容,而一个库往往包含大量的函数声明、复合类型、符号常量的定义等,这些定义错综复杂,为了方便所有程序使用这些库,开发者们往往把所有这些调用库需要的信息放在一个文件中,称为头文件,常用扩展名.h
或.hpp
头文件也是一个合法的C++
源文件,只是里面一般只包含函数声明、复合类型的定义、符号常量等需要在文件之间共享的内容,而不包含具体实现,在链接时需要链接含有具体实现的目标文件
而#include
指令就可以用于引入头文件,这样一来,只用引入头文件就可以使用头文件中所有的内容了
#include<file>
用于引入系统头文件,编译器会在系统环境变量配置的搜索路径中搜索头文件,这类头文件由系统或第三方软件包提供,其实现一般放在系统库路径下的链接库中,编译器在链接时会自动链接这些代码,一般情况下不用手动链接
可以在编译时使用-l
指令导入库,相当于导入对应的头文件,也可以用-L
指令添加一个搜索路径
NOTE
C
中系统头文件也使用.h
拓展名,而C++
更改了这一设定,C++
中的头文件都是没有拓展名的,如iostream
,当然,C++
中可以使用C
的头文件,不过推荐不使用拓展名而在前面加一个字母c
区分,如在C++
中#include<stdio.h>
和#include<cstdio>
是一样的效果,都指向stdio.h
#include"file"
用于引入项目中的头文件,填写头文件相对该文件的相对路径即可,需要自己实现头文件中的内容,并在链接时链接上,编译时也要加入这个头文件
例如
//hello.h
int hello();
该头文件告诉编译器有一个函数int hello()
,接下来只要引入这个头文件就可以使用这个函数,而不用关心函数实现
//main.cpp
#include "hello.h"
int main()
{
hello();
return 0;
}
使用如下命令可以正常编译这个程序为目标代码
g++ -o main.o -c main.cpp hello.h
但是如果试图将其编译为可执行程序会报错,因为我们并没有提供int hello()
的实现,链接器找不到这个函数
我们需要实现一下这个函数
//hello.cpp
#include <iostream> //引入系统头文件提供标准IO
#include "hello.h"
int hello()
{
std::cout << "hello, world";
return 0;
}
接下来,我们使用一样的方法编译该程序
g++ -o hello.o hello.cpp hello.h
然后将两个目标代码链接在一起
g++ -o ./hello hello.o main.o
这样链接器就可以找到int hello()
的实现,然后正常编译出可执行程序了
当然,也可以直接编译三个文件
g++ -o ./hello hello.h hello.cpp main.cpp
IMPORTANT引入大量头文件会极大增加名称冲突的可能,也会大大增加链接器查找的时间,大大降低编译速度,故开发中不建议引入大量无关头文件,尤其大项目中禁止使用“万能头”之类的头文件
IMPORTANT头文件中的代码会在每个文件中被重复运行,故请不要在头文件中包含任何变量声明(可以声明常量)、函数实现(内联函数、虚函数除外)
2. 宏定义
#define
指令会定义一个宏,可以用作标识,也可以用作文本替换
2.1. 符号常量
#define name value
会搜索源文件中所有name
并替换为value
,我们把这种方式定义的常量称为符号常量
符号常量在正式编译前就被替换,不会占用内存空间,也不存在类型,只是简单的文本替换,但是也因为这样可能会产生潜在的问题,C++
不建议使用这种方式定义常量,但C
只能这样定义常量,如果考虑兼容C
,则必须使用符号常量
头文件中常常使用符号常量来定义一些静态常量,如climits
中对各数据类型最大值的定义
当然,这种宏也可以用来替换部分代码,你甚至可以用#define + -
来把所有加法变成减法,不过应该不会有人这么干吧
2.2. 带参宏定义
宏定义可以携带参数,产生类似于函数的用法,如#define ADD(x,y) (x+y)
会在编译前把复合条件的表达式替换掉,如ADD(1,2)
替换为(1+2)
但是相比函数,这种定义虽然效率高,但是没有类型检查,就算ADD("1",2)
也会被替换为"1"+2
,无法通过编译
可以使用#
来将变量视为字符串替换,如#define TEST(x) #x
,会把TEST(1+2)
替换为"1+2"
,而不是1+2
可以使用##
来替换部分元素,即拼接被替换部分与前面的元素,如#define TEST(x) a##x
,会吧TEST(1)
替换为a1
2.3. 条件编译
在使用头文件时,头文件可能又包含其它头文件,项目变得复杂后,可能会导致重复包含头文件,导致重复定义使编译失败
为了避免这个问题,我们往往会在头文件开头使用条件编译#ifndef TEST_H
,之后在末尾定义宏#define TEST_H
,再使用条件编译#endif
,这样在预编译过程中只有没有定义过TEST_H
宏时才会保留ifndef
和endif
中间的内容,而解析过一次该内容后就会定义宏,最终只会保留一份了
常用的指令还有
ifdef
:定义过该宏才会保留这些内容undef
:取消宏定义else
:与ifdef
或ifndef
一同使用,放在endif
前,当不满足条件时保留else
和endif
中间的内容,反之保留ifdef/ifndef
和else
中间的内容
为了区分不同的目标平台,编译器也会定义一些告诉程序目标平台的宏,利用这些宏和条件编译指令可以实现针对不同平台编译不同代码,常用于Windows
和POSIX
系统使用同一套代码时
3. 类型别名
#typedef type alia
可以为type
类型设置别名alia
#define
也可以实现这样的功能,但#define
只是在预处理时进行文本替换,而#typedef
会在编译时交给编译器处理,比#define
安全很多
使用#typedef
定义的类型通常带有_t
后缀,前面数据类型一节提到的int16_t
、size_t
等正是用这种方法定义的,这个定义由编译器完成,我们不用关心到底是哪种数据类型,只要知道它是当前编译器中特定长度的类型就可以了
在实际应用中#typedef
也是这样的功能,不用关心到底是哪种类型,只要确定用途就行,需要时可以快速更改
4. 名称空间
名称空间是C++
中独有的概念
当程序变得复杂,尤其是使用了很多库之后,可能有很多重名的变量,为了区分这些变量,C++
引入了名称空间的概念,可以使用namespace{}
规定一块名称空间,名称空间之外的程序要访问内部定义的内存就需要使用作用域解析运算符::
namespace alice{
int a = 1;
}
namespace bob{
int a = 2;
}
std::cout << a;//incorrect
std::cout << alice::a;//1
std::cout << bob::a;//2
类的成员也会自动带上类名称空间,名称空间可以嵌套,C++
标准库中的名称往往定义在std
名称空间下
而每次都写一串名称空间都很麻烦,于是我们有了using
命令,可以明确某一个量的名称空间,如上述例子中,若使用using alice::a;
,则后面使用的所有a
都默认为alice::a
(仍然可以使用bob::a
,不冲突)
但是如果量很多,还是很麻烦,这样就出现了using
编译指令
如using namespace std;
会把所有用到的量(而非定义的)量视为std
名称空间下的量,免去每次都写std
的麻烦,同时需要使用其他名称空间的量时也可以指定其他的名称空间来覆盖掉
using
指令和编译指令的作用范围都仅限当前作用域,即当前大括号内,using
编译指令也可以嵌套
由于using
编译指令会引入大量用不到的量,可能产生名称冲突,带来不必要的麻烦,故不推荐使用using
编译指令