编译预处理
包含头文件
头文件包含了函数声明、宏定义等。使用#include
指令可以在编译阶段把头文件的内容包含到源文件中。#include
的本质就是把指向文件中的全部内容原样复制到#include
指令所在的地方。
其中#include
有两种用法:#include <文件名>
和 #include "文件名"
。#include <文件名>
是从编译器自带的库目录中查找头文件,而 #include "文件名"
是先从源文件所在的目录查找头文件,如果找不到,再从编译器自带的库目录查找。通常,我们在编写自己的头文件时,使用 “.h” 作为后缀。
宏定义
#define
用于宏定义,宏定义可以在源文件中定义常量或函数。宏定义在预处理阶段被替换。
C++中常用的宏:
- 当前源代码文件名:FILE
- 当前源代码函数名:FUNCTION
- 当前源代码行号:LINE
- 编译的日期:DATE
- 编译的时间:TIME
- 编译的时间戳:TIMESTAMP
- 当用C++编译程序时,宏__cplusplus就会被定义。
如果一个宏没有参数,那么它的定义形式是 #define 宏名 宏内容
。
如果一个宏有参数,那么它的定义形式是 #define MAX(x,y) ((x)>(y) ? (x) : (y))
。
条件编译
条件编译的常用预处理器指令有 #if
,#ifdef
,#ifndef
,#else
,#elif
和 #endif
。其中 #ifdef
和 #ifndef
的使用非常广泛。
#ifdef
用于检查某个宏是否已经被定义,如果定义了,那么它后面的代码块会被编译器编译。例如:
#define DEBUG
// ...
#ifdef DEBUG
std::cout << "Debug mode is enabled." << std::endl;
#endif
//在这个例子中,如果 DEBUG 被定义了,那么 "Debug mode is enabled." 就会被打印出来。
类似地,#ifndef
用于检查某个宏是否未被定义,如果未定义,那么它后面的代码块会被编译器编译。例如:
#ifndef DEBUG
std::cout << "Debug mode is not enabled." << std::endl;
#endif
//如果 DEBUG 没有被定义,那么 "Debug mode is not enabled." 就会被打印出来。
防止头文件重复包含
在变成中,如果我们需要多个源文件同时引用同一个头文件,这可能导致头文件的内容被重复包含,进而引起编译错误。为了防止这种情况我们通常会采取指令来避免头文件的重复包含。
- 使用
#ifndef
,#define
和#endif
组合,像下面这样:
// girl.h
#ifndef GIRL_H
#define GIRL_H
class Girl {
// ...
};
#endif // GIRL_H
在例子中,当当编译器首次遇到 girl.h
文件时,GIRL_H
尚未定义,因此 #ifndef GIRL_H
的条件为真,编译器会继续编译该头文件的内容,并在编译完类 Girl
的定义后,通过 #define GIRL_H
来定义 GIRL_H
这个宏。如果后续再次遇到 girl.h
文件,由于 GIRL_H
已经定义过了,所以 #ifndef GIRL_H
的条件为假,编译器就会跳过 #endif
后面的所有内容,从而避免了重复包含。
- 另一种方式是使用
#pragma once
指令,这是一种更简单、更现代的方式,但并非所有的编译器都支持。例如:
// girl.h
#pragma once
class Girl {
// ...
};
在这个例子中,#pragma once
告诉编译器,如果这个头文件已经被包含过一次,那么在后续的包含操作中就会被忽略,从而避免了重复包含。
编译和链接
-
C++编译和链接的概述
1. 源代码的组织
在C++编程中,源代码通常被组织在两种类型的文件中:
-
头文件(*.h或*.hpp):这些文件主要包含函数、结构体、类和模板的声明。此外,它们也包括
#include
预处理器指令(用于引入其他头文件)、#define
预处理器指令和const
关键字定义的常量等。// example.h #ifndef EXAMPLE_H #define EXAMPLE_H void functionExample(); #endif
-
源文件(*.cpp):这些文件包含函数、类和模板的定义(实现)。它们负责提供声明的实现。
// example.cpp #include "example.h" void functionExample() { // implementation... }
-
主程序:通常包含
main
函数的文件,作为程序的入口点,负责组织和执行核心流程。// main.cpp #include "example.h" int main() { functionExample(); return 0; }
2. 预处理
预处理是编译的第一阶段,主要进行以下操作:
- 处理#include:编译器将查找指定的头文件,并将其内容直接插入到源代码中。
- 处理条件编译指令:像
#ifdef
,#else
,#endif
,#ifndef
这样的预处理器指令,根据定义的宏决定是否编译某段代码。 - 处理宏定义:
#define
预处理器指令定义的宏会被替换为具体的值或表达式。 - 添加行号和文件名:帮助在出错时定位错误源。
- 删除注释:在编译的实际过程开始前,预处理器会删除所有的注释。
3. 编译
编译阶段将预处理后的源代码转换为目标代码(一般是二进制格式)。编译过程包括词法分析、语法分析、语义分析和优化等步骤。如果源代码中某个符号(如函数或类)的声明存在,但未找到相应的定义,编译阶段不会报错。
4. 链接
链接阶段负责将编译阶段生成的所有目标文件(二进制文件)以及他们依赖的库文件链接在一起,生成可执行文件。在这个过程中,链接器需要解析所有的未解决符号(即在编译阶段未找到定义的符号)。如果链接器无法找到某个符号的定义,或者发现多个相同符号的定义,就会抛出错误。
5. 更多的注意点
- 分开编译:可以提高编译效率,每次修改代码后只需要重新编译修改的部分,然后再进行链接。
- 头文件守护:为了避免头文件被重复包含,通常会使用预处理器指令
#ifndef
,#define
,#endif
来防止头文件内容在同一编译单元内被重复包含。 - 符号声明和定义:在头文件中通常只包含符号的声明,而在源文件中提供符号的定义。每个符号在整个程序中只能有一个定义,否则会导致链接错误。
- 全局变量和常量:如果必须使用全局变量,应该在头文件中使用
extern
关键字声明,在一个源文件中定义。全局常量const
应在其被使用的文件中定义。 - 模板的处理:函数模板和类模板的声明和定义都应该在头文件中进行,因为模板的实例化在编译期进行。
-
C++命名空间
在大型项目中,由于代码有多个开发者或者团队编写,或者引用了多个库,常常会出现相同名称的函数、类或变量,这就导致了名称冲突。命名空间能够有效地解决这个问题。它将一组相关的功能组合在一起,每个命名空间都是唯一的。如果两个库都有一个名为sort
的函数,它们被放置在不同的命名空间中,如lib1::sort()
和lib2::sort()
,从而避免了名称冲突。
创建和使用命名空间
创建命名空间的语法如下:
namespace MyNamespace {
// 类、函数、模板、变量的声明和定义。
}
在创建命名空间后,你可以定义一个别名来简化其访问:
namespace Alias = MyNamespace;
要在命名空间外部访问其内部的名称,有以下三种方式:
- 使用命名空间运算符(:😃
MyNamespace::MyFunction();
- 使用
using
声明
using MyNamespace::MyFunction;
MyFunction();
- 使用
using namespace
指令
using namespace MyNamespace;
MyFunction();
注意事项
- 命名空间是全局的:你可以在多个不同的文件中定义同一个命名空间。
- 命名空间可以嵌套:你可以在一个命名空间内定义另一个命名空间。
- 在命名空间中声明全局变量:而不是使用外部全局变量和静态变量。
- 对于
using
声明,优先将其作用域设置为局部而不是全局:这样可以避免名称冲突。 - 不要在头文件中使用
using namespace
指令:如果你需要在源文件中使用,应将它放在所有的#include
之后。 - 匿名的命名空间:匿名的命名空间只在定义它的文件中有效。这可以作为定义只在一个文件中可见的全局变量或函数的一种方法。
namespace {
// 类、函数、模板、变量的声明和定义。
}
C++类型转换static_cast
C风格的类型转换很容易理解:(目标类型)表达式
或目标类型(表达式)
C++认为C风格的类型转换过于松散,可能带来隐患,所以C++提供了新的类型转换来替代C风格的类型转换,采取更严格的语法检查,降低使用风险。
C++提供的四种类型转换运算符:static_cast
,const_cast
,reinterpret_cast
,和dynamic_cast
。每种类型转换运算符都有特定的用途和限制。
另外C++的类型转换知识语法上的解释,本质上和C风格的类型转换没什么不同,C做不到事情的C++也做不到。
它是最常用的类型转换。static_cast
可以在任何数据类型之间进行转换(包括指针到整形的转换),只要转换是明确的。在编译期间,编译器会检查static_cast
的转换是否合法。
用于对不同内置类型之间的转换,包括整数,浮点数,字符,和布尔值。
#include <iostream>
int main() {
double d = 5.5;
int i = static_cast<int>(d);
std::cout << i << std::endl; // 输出5
return 0;
}
常用在,在类的继承体系中,我们可以使用static_cast
将基类指针转换成派生类指针,或者将派生类指针转换成基类指针。
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "This is Base class." << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "This is Derived class." << std::endl;
}
void specialFunction() {
std::cout << "Function only exists in Derived class." << std::endl;
}
};
int main() {
Base* base_ptr = new Derived();
base_ptr->print(); // Outputs "This is Derived class."
// Use static_cast to convert base class pointer to derived class pointer
Derived* derived_ptr = static_cast<Derived*>(base_ptr);
derived_ptr->print(); // Outputs "This is Derived class."
derived_ptr->specialFunction(); // Outputs "Function only exists in Derived class."
// Use static_cast to convert derived class pointer back to base class pointer
Base* base_ptr2 = static_cast<Base*>(derived_ptr);
base_ptr2->print(); // Outputs "This is Derived class."
delete base_ptr;
return 0;
}
static_cast
不能丢掉指针(引用)的const
和volitile
属性,const_cast
可以。
如,你不能这样做:
const int a = 10;
int* pa = static_cast<int*>(&a); // 错误,不能移除const
这个时候就可以使用const_cast
移除变量的const
或volatile
限定符:
#include <iostream>
void printValue(int* pi) {
std::cout << "*pi: " << *pi << std::endl;
}
void printConstValue(const int* pci) {
std::cout << "*pci: " << *pci << std::endl;
}
int main() {
int a = 10;
const int* pci = &a;
printConstValue(pci); // 这是OK的
// 我们不能直接把pci传递给printValue,因为pci是一个const指针
// printValue(pci); // 错误,不能把const int*转换为int*
// 但是我们可以使用const_cast来移除pci的const限定符
printValue(const_cast<int*>(pci)); // OK,移除了const限定符
return 0;
}
在C++中,reinterpret_cast
是一种比较特殊的类型转换,它可以将任何类型的指针转换成任何其他类型的指针,或者任何类型的整数和指针之间进行转换。由于这种转换基本上只是简单的重新解释位模式,因此可能导致一些未定义的行为,所以在编程中使用时需要非常谨慎。
下面是一个例子,演示了将一个整数转换成一个指针,并且再讲这个指针转换回整数。
#include <iostream>
using namespace std;
int main() {
// 声明一个整数变量
int i = 42;
// 将整数转换为指针
void* p = reinterpret_cast<void*>(i);
// 再将指针转换回整数
int j = reinterpret_cast<int>(p);
cout << "j: " << j << endl; // 输出:j: 42
return 0;
}