基本概念
本文介绍 C++ 的一些基本概念。
编译器¶
编译器 (Compiler) 是将人类可读的 C++ 源代码(例如 .cpp
和 .h
文件)转换成计算机可以执行的机器码(可执行文件)的程序。常见的 C++ 编译器如下表所示:
编译器 | 全称 | 支持的平台 | 备注 |
---|---|---|---|
GCC | GNU Compiler Collection | Linux, macOS, Windows (MinGW/Cygwin/WSL) | 开源世界的标准 |
Clang | / | Linux, macOS, Windows | 作为底层虚拟机 (Low Level Virtual Machine, LLVM) 项目的一部分,以其快速的编译速度、模块化设计和友好的错误提示而闻名 |
MSVC | Microsoft Visual C++ | Windows | 集成在 Visual Studio 中 |
安装¶
直接找到对应的官网按照文档下载安装即可,可以参考 一文搞懂 C/C++ 常用编译器 这篇博客。也可以直接安装 Visual Studio 和 CLion 等集成开发环境。
与 C++ 标准关系¶
C++ 标准会不断改进,这意味着编译器也需要升级以支持最新的 C++ 标准,因此才会看到诸如 C++14
、C++23
等形式的 C++ 标准,后面的数字表示标注发布的年份。
编译器的型号也就会不断改变,例如 GCC 的 gcc 12.2
、gcc 13.4
等版本。
例如我的 Windows 笔记本下载的 CLion 对应的编译器就是 GCC 的 Windows 适配版 MinGW。在终端输入 gcc --version
后打印的版本为 gcc 13.1.0
。
命名空间¶
在 C++ 中,代码的组织结构如下所示:
graph TB
n1(命名空间1)
n2(命名空间2)
c1(类1)
c2(类2)
c3(类3)
f1(函数1)
f2(函数2)
f3(函数3)
n1 --> c1 & c2 & f2
c1 --> f1
n2 --> c3 & f3
类涉及到面向对象编程,函数就是 C 语言中最基本的函数,这里简单介绍一下命名空间的概念。
每一个命名空间下都可以自定义各种类与函数。如果不声明命名空间,则自定义的类与函数就归属到全局命名空间。标准库的命名空间为 std
,可以通过使用命名空间域作用符 ::
使用。
std¶
下面的程序展示了标准库 (Standard Library) 命名空间的声明规范:
自定义命名空间¶
也可以自定义命名空间:
甚至可以嵌套命名空间:
- 如果需要使用第一层命名空间中的类或函数,语法如;
anotherLibrary::who myClass;
- 如果需要使用嵌套的命名空间中的类或函数,语法如:
anotherLibrary::secondLibrary::write(1, 2);
using¶
我们可以通过 using
关键字将指定的命名空间引入 当前作用域 中,从而可以省略 ::
的使用:
上述程序中,std
命名空间被显式的在 fun
函数作用域中标出了,所以可以直接使用 vector
而不用写成 std::vector
。但是在 main
函数作用域中没有显式的标出 std 故还是需要使用 std::
才能使用该命名空间中定义的类或方法,此处为输入输出类的 std::cout
与 std::endl
。
模块化开发¶
当项目规模变大时,将所有代码放在一个文件中是不可行的,此时就可以通过模块化的方式提高代码的可读性、复用性和可维护性。
头文件 .h
¶
- 用于存放 声明,如:函数原型、类定义、宏定义、全局变量的
extern
声明等; - 通过
#include
指令被源文件或其他头文件包含。 - 通常使用
#pragma once
或#ifndef...#define...#endif
来防止头文件被重复包含。
源文件 .cpp
¶
- 用于存放 定义,如:函数的具体实现、类成员函数的实现、全局变量的定义和初始化等;
- 每个源文件通常会
#include
其对应的头文件以及其他需要的头文件。
示例¶
demo.h
文件:
demo.cpp
文件:
main.cpp
文件:
控制台正常输出:
我们知道在 python 中,导入一个模块后除了可以使用模块的类,还可以直接调用模块文件中的函数,那么在 C++ 文件中也可以这样吗?答案是可以的。
我们在上述 demo 类的下面继续添加函数:
编译报错:multiple definition of globalFun()
。
即重定义错误。解决方法有两个:
- 将函数定义写在
.cpp
文件中,.h
文件只包含函数声明; - 将函数写成内联函数的形式(直接在函数返回值前面加上
inline
关键字)。
为什么内联函数不会产生函数重定义问题?
- 非内联函数会引发重定义错误是因为引用
.h
文件时,如果一个函数被定义,那么引用该.h
的文件中都会包含这个函数的定义,由于每个函数(包括全局变量)只能有一个定义,那么很显然在编译链接时就会报重定义的错; - 当函数被声明为内联时,编译器虽然也会将其插入到每个调用该函数的地方,但链接器知道这是内联函数,可以合并成同一个函数实体,不会当作“重定义”。
为什么 .h
文件可以直接定义类的成员函数而不会出现重定义问题?C++ 中的类成员函数默认是内联的。但是不推荐这种直接定义成员函数的方法,因为这会导致编译时代码膨胀(每个调用的地方都会被插入函数代码),故多文件编程的规范就是:无论是类的成员函数还是全局函数,函数的声明都写在 .h
文件中,而对于这些函数声明的定义都写在相同文件名的 .cpp
文件中。
库¶
我们知道 C++ 编译器在下载时会自带一些功能代码,我们将其称为标准库 (Standard Library),但毕竟功能有限。为了实现特定功能(如图形渲染、网络通信、数学计算等)从而提高开发效率,各种 C++ 第三方库被开发了出来。第三方库指的就是预先编写好的、可重用的代码集合。
库的种类¶
分两种,分别是静态库 (Static Library) 和动态库 (Dynamic/Shared Library)。具体情况如下表对比:
文件后缀 | 链接方式 | 优点 | 缺点 | |
---|---|---|---|---|
静态库 | Windows 是 .lib ,Linux/macOS 是 .a |
在链接阶段,库中被用到的代码会被完整地复制到最终生成的可执行文件中 | 部署简单,因为可执行文件是自包含的,不依赖外部库文件 | 可执行文件体积较大;如果库更新了,整个项目需要重新编译链接 |
动态库 | Windows 是 .dll ,Linux 是 .so ,macOS 是 .dylib |
在链接阶段,只记录库的引用信息。当程序运行时,操作系统会负责将动态库加载到内存中,并与程序建立连接 | 节省空间,因为多个程序可以共享同一个动态库的内存副本;更新方便,因为更新动态库文件无需重新编译所有使用它的程序(只要接口保持兼容) | 部署时需要确保可执行文件能找到所需的 .dll 或 .so 文件 |
库的使用¶
流程如下: