C/C++程序文件包括 .h .c .hpp .cpp,其中源文件(.c .cpp)是基本的编译单元,头文件(.h .hpp)不会被编译器编译。c++
C/C++项目构建(build)过程,分为如下几个步骤 预处理 → 编译 → 连接。程序员
预编译的过程能够理解为编译器(其实是预处理器,这里统称为编译器就能够了)在正式编译以前处理C/C++文件中的预处理命令,即#开头的代码。函数
经常使用的几个预处理命令以下:优化
#include ......ui
#ifdef ...... #else......#endifspa
#define ......code
#pragma ......blog
举个例子,下面是个很简单的类定义:ip
MyClass.h作用域
#define DEFAULT_VALUE 0 class MyClass { public: void Fun(); public: int value = DEFAULT_VALUE; };
MyClass.cpp
#include "MyClass.h" void MyClass::Fun() { // Do someting return; }
预编译完成后的样子:
class MyClass { public: void Fun(); public: int value = 0; }; void MyClass::Fun() { // Do someting return; }
能够看到编译器把.h文件替换到了.cpp文件中的#include 位置上,把DEFAULT_VALUE定义的值也替换到了相应的位置。
预编译以后,编译器会编译每一个源文件(.c .cpp),若是编译成功,会生成对应的目标文件,Linux为.o文件,Windows平台下为.obj文件。
以Linux平台为例,上面的MyClass.cpp编译完成后会生成MyClass.o文件
使用objdump能够看到目标文件MyClass.o的内容
$$ objdump -x MyClass.o
......
0000000000000000 g F .text 0000000000000015 _ZN7MyClass3FunEv
......
编译器会把MyClass::Fun()的名字改为_ZN7MyClass3FunEv,这个过程叫Mangle,因为C++支持重载,覆盖等特性,因此编译器必须把函数用一个惟一的标识表示。这个字符串就是编译器生成的惟一标识。
这里还要单独说一下头文件,头文件的既然不是编译单元,那么它的做用是什么?
头文件就是负责”声明“,编译器在编译MyClass.cpp的时候,对于MyClass这个类以及Fun()这个成员函数,编译器必须找到它的声明,这个函数才能被正确编译。
若是有其余cpp须要使用MyClass这个类的时候,也须要它的的声明。例如
main.cpp
#include "MyClass.h" int main(int argc, char** argv) { MyClass tmp; tmp.Fun(); return 0; }
加上#include "MyClass.h" 编译器在编译main.cpp的时候才知道怎么编译MyClass这个类。MyClass.h里声明是不会真正被编译到main.o中,.h文件中的内容在目标文件中只是以列表的形式存在,这个表在后面连接时会用到。
固然,头文件不只能够用来声明,还能够定义(定义全局变量,全局函数等),在头文件中的定义要当心,可能会引发连接错误。
连接就是将一堆目标文件加静态库文件装配成可执行文件的过程。(或者是装配成静态/动态库的过程)
上面两个cpp分别被编译成了MyClass.o, 和main.o,咱们要生成可执行程序的话,就必须通过连接的过程,把两个目标文件合成一个可执行文件。
main.o中,main函数会构造MyClass, 而且调用Fun()函数,那么main就根据MyClass.h生成的表,找到MyClass.o中的函数,这个就是连接器要作的工做。
构建c/c++工程的时候,最多见的就是两种错误:
-- 编译错误,在编译过程当中产生的错误,一般是语法错误,没有声明,重复声明致使编译目标文件错误
其中没有声明一般是因为没有#include相应的头文件,或者头文件缺乏相应的声明。
而重复声明一般是#include了相同的头文件,好比 B.h 和 C.h 都包含 A.h,而后 main.h 包含了 B.h 和 C.h,这就致使A.h 在main中被包含了两次。
解决这个问题的方法是能够在全部.h文件的第一行加上
#pragma once
或者,使用#ifndef ... #define ... #endif 语句块
#ifndef NEWCLASS_H #define NEWCLASS_H ...... #endif /* NEWCLASS_H */
-- 连接错误,常见的错误也是两种,没有定义和重复定义,和上面的没有声明,重复声明相似。(这里定义指的就是函数实现)
一般是由于函数有声明,并且被使用了,可是没有被定义。好比上面MyClass.cpp中,若是Fun()没有被实现的话,MyClass.cpp和main.cpp编译时都不会报错,可是连接时会报告找不到Fun()。
固然,若是Fun()没被main.cpp调用的话,即便不实现它,整个构建过程也不会出错,由于连接器根本不会去找这个函数的定义。
指的一份相同的定义在两个目标文件中都存在,连接的时候连接器不知道时用哪一个了。这种问题一般因为全局函数,和全局变量定义在了头文件中。致使多个目标文件包含相同的全局函数和全局变量的定义。
解决方法就是在头文件中声明,定义放到cpp文件中,或者为定义加上const 或 static这样的修饰符,连接时会对这些带有const和修饰符的变量特殊处理的。
const只适用于定义常量变量,static定义的是静态全局变量,只在当前cpp有效,因此连接它也不会被别的目标文件连接,就不会有重复定义的问题了。
总之在头文件中定义变量和函数要特别主意,可能会致使连接错误。
固然也不是全部定义都不能放到头文件中,好比刚才说的const常量,static全局变量就是例外,还有内联函数,能够定义在.h文件中,由于内联函数会被拷贝到每一个目标文件中,也不会参与连接的过程。
还有模板类必须放在头文件中定义,这个下面会讨论这个。
模板类模板函数必须声明和定义在头文件中,缘由是什么,举个例子,假设MyClass是模板类
MyClass.h
template <typename T> class MyClass { public: void Fun(); public: T value; };
MyClass.cpp
#include "MyClass.h" template <typename T> void MyClass<T>::Fun() { // Do someting return; }
main.cpp
int main(int argc, char** argv) { MyClass<int> tmp; tmp.Fun(); return 0; }
编译的时候没有问题,可是连接时会报错,main.cpp找不到MyClass<int>::Fun(),以下图
MyClass虽然定义了Fun函数,可是MyClass.o中存在MyClass<T>::Fun(),而根据MyClass.h文件,main.o中须要找到MyClass<int>::Fun()的定义
结果连接器哪都找不到,只好报错了。(实际上经过objdump查看MyClass.o,编译器都没有生成MyClass<T>::Fun(),由于编译器认为这个函数没人使用,就直接优化掉了)
若是非得在cpp中定义模板类的成员函数呢,有一种方法就是要显示的在cpp文件中声明,好比
MyClass.cpp
#include "MyClass.h" template <typename T> void MyClass<T>::Fun() { // Do someting return; } template void MyClass<int>::Fun();
加上下面这行就不会有问题了,可是缺点就是开发MyClass的程序员无从知道其余类是怎么使用这个模板的,不可能把全部可能的模板参数全都一一的列在这里。
因此模板类的定义仍是要写在.h文件中,
那么若是main.cpp使用到了MyClass<int>, 另一个cpp也使用到了MyClass<int>,会不会产生重复代码致使重复定义呢,不会,编译器会处理好模板类的。
下面是静态成员变量,为何静态成员变量的定义要放在cpp里,(模板类的静态成员变量除外)
静态成员变量和静态全局成员变量不一样。
静态成员变量的做用域能够是整个工程,而静态全局变量的做用域只是当前的cpp。因此静态成员变量定义在.h中就会发生重定义错误。