浅谈C++编译过程
在c++程序中,一般只包含2种文件,.h头文件和.cpp源文件,暂时不考虑和C混编.c这种。头文件主要包含类的声明、函数的原型和常量的定义等内容,而源文件包含实际的函数实现和变量定义等代码。以后我们要记得这样一句话,头文件作声明的,源文件作定义的。
头文件是不需要直接被编译的,而源文件才是需要编译的。这一点从g++编译命令就可以看出(后面会讲g++ 编译,链接的命令)。通常在源文件中通过#include指令包含头文件,当编译源文件时,会把#include指令时,会将头文件的内容复制到源文件中,形成一个完整的代码文件。现在明白了吧,头文件申明的内容只是会被拷贝到源文件,这样做的目的是为了避免重复定义。然后对源文件进行编译,将其转换为目标文件.o。 然后,编译器会对目标文件进行链接,将所有的目标文件和库文件链接在一起,生成最终的可执行文件。链接的过程会解析头文件中的函数和变量引用,并将其与实际的函数和变量定义进行关联。
g++编译和链接
在使用g++编译C++程序时,通常需要进行编译和链接两个步骤。编译将源代码转换为目标文件,而链接将目标文件和库文件组合成可执行文件。下面是g++编译和链接的基本命令:
编译命令:
g++ -c main.cpp -o main.o
链接命令:
g++ main.o -o main
这将生成一个名为main的可执行文件,可以通过运行./main命令来执行它。
独立编译
java出身的同学要注意理解一点,C++的编译方式和Java的不一样。C++是支持独立编译的。也就是说将一个程序分成多个源文件,每个源文件独立编译成目标文件,最后将所有的目标文件链接在一起形成可执行文件的过程。
头文件
头文件的作用
C++设计头文件的主要目的是为了实现模块化编程和代码重用。
- 模块化编程:头文件可以将一个模块的接口和实现分离开来。头文件中通常包含类的声明、函数的原型、常量和宏定义等。通过将接口放在头文件中,其他源文件可以通过包含头文件来访问模块的功能,而无需了解具体的实现细节。这样可以提高代码的可读性和可维护性,同时也方便了多人协作开发。
- 代码重用:头文件可以定义类、函数和变量等,其他源文件可以通过包含头文件来重用这些定义。这样可以避免重复编写相同的代码,提高了代码的复用性和开发效率。同时,头文件也可以用来引入外部库的定义,方便使用这些库的功能。
- 此外,头文件还可以用于解决循环依赖的问题。当多个源文件相互引用时,如果没有头文件,编译器会出现无法解析的错误。通过将相互引用的类的声明放在头文件中,可以解决这个问题。
- 总结来说,C++设计头文件的目的是为了实现模块化编程、代码重用和解决循环依赖的问题。头文件将接口和实现分离开来,提高了代码的可读性、可维护性和复用性。
头文件的实际应用
#ifndef CPPSTUDY_FILE3_H
#define CPPSTUDY_FILE3_H
//常量的定义
const int globalVar1 = 10;
static int globalVar2 = 20;
//全局变量的声明
extern int globalVar3;
// int file3Count4 ; //多个cpp文件包含这个头文件就会报错,重复定义变量,链接多个.o文件出错
int add(int x,int y); //函数声明
#endif //CPPSTUDY_FILE3_H
头文件申明类
#ifndef CPPSTUDY_STU_H
#define CPPSTUDY_STU_H
#include <string>
class Student{
public:
Student(); //无参构造函数
Student(std::string name,int age); //有参构造函数
Student(const Student& s); //拷贝构造函数
~Student(); //析构函数
void study(); //成员函数
private:
std::string _name;
int _age;
};
#endif //CPPSTUDY_STU_H
在需要使用的地方,通过#innclude "xxx.h"就可以了。
#include
#include 是一个预处理指令,用于包含头文件。它是在预编译的时候就会起作用,会把头文件的内容全部拷贝到当前文件。
比如上面在头文件定义的类,在main.cpp使用
#include "stu.h"
int main() {
Student stu("lisi", 18);
stu.study();
return 0;
}
头文件的保护措施 #define
C++头文件中的保护 #define 是用来防止头文件被多次包含的机制。当一个头文件被多个源文件包含时,如果没有保护机制,可能会导致重复定义的错误。
在头文件中,我们通常会看到编译器自动生成的范式代码:
#ifndef EXAMPLE_H
#define EXAMPLE_H
// 头文件的内容
#endif
这段代码的作用是:
#ifndef检查指令,如果EXAMPLE_H这个宏没有被定义过,则继续执行下面的代码。如果已经定义过了,则跳过这段代码。#define定义指令,将EXAMPLE_H宏定义为一个非零值。- 头文件的内容,可以包含类的声明、函数的原型、常量和宏定义等。
#endif结束指令,表示头文件的内容结束。 这样,当多个源文件包含同一个头文件时,第一个源文件会成功包含头文件并定义EXAMPLE_H宏,而后续的源文件在检查到EXAMPLE_H已经定义时,会跳过头文件的内容,避免重复定义的错误。- 通过使用这种保护机制,可以确保头文件只被包含一次,避免了重复定义的问题,提高了代码的可靠性和可维护性。
源文件
源文件实现头文件中的声明
这种方式也叫定义。前面说过头文件是做声明的,而源文件提供了它们的具体实现。源文件的作用是实现程序的逻辑和功能。它包含了程序的具体实现细节。
还是之前的例子:
#ifndef CPPSTUDY_FILE_H
#define CPPSTUDY_FILE_H
//常量和静态变量可以在头文件中声明及定义
const int globalVar1 = 10;
static int globalVar2 = 20;
//这个全局变量需要在cpp定义(实现)
extern int globalVar3;
// int file3Count4 ; //多个cpp文件包含这个头文件就会报错,重复定义变量,链接多个.o文件出错
//这个全局函数需要在cpp定义(实现)
int add(int x,int y); //函数声明
#endif //CPPSTUDY_FILE_H
在源文件中的实现:
#include "file3.h" //必须要包含对应的头文件
int globalVar3 = 30; //变量的定义
//也是定义,也就是具体的实现
int add(int x, int y) {
return x + y;
}
stu.cpp实现:
#include <iostream>
#include "stu.h"
using namespace std;
Student::Student() {
cout << "Student() " << endl;
}
//通过初始化成员列表的方式初始化成员变量
Student::Student(std::string name, int age) : _name(name), _age(age) {
cout << "Student(std::string name, int age) " << endl;
}
Student::Student(const Student &s) {
cout << "Student(const Student &s) " << endl;
}
Student::~Student() {
cout << "~Student() " << endl;
}
void Student::study() {
cout << "study() " << endl;
}
在源文件中要实现头文件的声明,需要在源文件中通过#include包含对应的头文件。
源文件根据 #include 来关联头文件
上面讲了在源文件中通过#include来做功能的实现,也叫做定义。对于要使用相关功能的地方,需要#include包含头文件之后,才能使用。
1、系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。
2、用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,
源文件如何关联头文件
在上面已经讲了,如果要在main.cpp中使用stu.cpp的功能,只需要在当前文件通过 #include "stu.h"这样包含头文件即可使用。那么 main.cpp是怎样找到 stu.cpp 中的实现呢?
编译的时候,并不会去找stu.cpp文件中的函数实现,只有在链接 (link) 的时候才进行这个工作。在main.cpp 中用 #include "stu.h" 实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o 或 .obj 文件),目标文件中,这些函数和变量就视作一个个符号。在 link 的时候,需要在 makefile 里面说明需要连接哪个 .o 文件(在这里是 stu.cpp 生成的 .o文件),此时,链接器会去这个 .o文件中找在 stu.cpp 中实现的函数,再把他们 build 到 makefile 中指定的那个可以执行文件中。