前言
我们说C和C++,因为C++和C其实是一家的,只是C++是C的拓展而已。
因此C++兼容C的大部分语法内容,并且新增了许多新功能。因此这个C++全流程学习博客建立在你已经学会了C语言的基础上,探讨C++新增的内容。
学海无涯,求知当以勤勉为舟。愿你在求知的路上持之以恒,不断精进。

1. C++简介与发展历程
C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不 同的地方可能有差异)在贝尔实验室从事计算机科学和软件⼯程的研究⼯作。⾯对项目中复杂的软件开 发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语言(如C语言)在表达能⼒、可维护性 和可扩展性方面的不⾜。
1983年,BjarneStroustrup在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形, 此时的C++已经有了类、封装、继承等核⼼概念,为后来的⾯向对象编程奠定了基础。这⼀年该语⾔被 正式命名为C++。
1.1 C++发展历程
时间 | 版本 | 主要特性 |
---|---|---|
1998 | C++98 | 第一个官方标准,引入STL |
2003 | C++03 | 修复错误,提高稳定性 |
2011 | C++11 | 革命性更新:lambda、智能指针、移动语义等 |
2014 | C++14 | 对C++11的扩展和改进 |
2017 | C++17 | 引入折叠表达式、结构化绑定等 |
2020 | C++20 | 重要里程碑:概念、协程、模块等 |
2023 | C++23 | 进一步完善现有特性 |
1.2. 为什么学习C++?
C++的重要性
-
性能卓越:接近硬件层,执行效率高
-
应用广泛:操作系统、游戏引擎、嵌入式系统、金融服务等
-
承上启下:理解计算机系统的绝佳语言
-
不可替代:在性能敏感领域无可替代
C++在工作领域中的应用
C++的应⽤领域服务器端、游戏(引擎)、机器学习引擎、⾳视频处理、嵌⼊式软件、电信设备、⾦融
应⽤、基础库、操作系统、编译器、基础架构、基础⼯具、硬件交互等很多方面都有。
- ⼤型系统软件开发。如编译器、数据库、操作系统、浏览器等等
- ⾳视频处理。常⻅的⾳视频开源库和⽅案有FFmpeg、WebRTC、Mediasoup、ijkplayer,⾳视频
开发最主要的技术栈就是C++。 - PC客⼾端开发。⼀般是开发Windows上的桌⾯软件,⽐如WPS之类的,技术栈的话⼀般是C++和
QT,QT 是⼀个跨平台的 C++图形⽤⼾界⾯(Graphical User Interface,GUI)程序。 - 服务端开发。各种⼤型应⽤⽹络连接的⾼并发后台服务。这块Java也⽐较多,C++主要⽤于⼀些对
性能要求⽐较⾼的地⽅。如:游戏服务、流媒体服务、量化⾼频交易服务等 - 游戏引擎开发。很多游戏引擎就都是使⽤C++开发的,游戏开发要掌握C++基础和数据结构,学习
图形学知识,掌握游戏引擎和框架,了解引擎实现,引擎源代码可以学习UE4、Cocos2d-x等开源
引擎实现 - 嵌⼊式开发。嵌⼊式把具有计算能⼒的主控板嵌⼊到机器装置或者电⼦装置的内部,通过软件能够
控制这些装置。⽐如:智能⼿环、摄像头、扫地机器⼈、智能⾳响、⻔禁系统、⻋载系统等等,粗
略⼀点,嵌⼊式开发主要分为嵌⼊式应⽤和嵌⼊式驱动开发。 - 机器学习引擎。机器学习底层的很多算法都是⽤C++实现的,上层⽤python封装起来。如果你只想
准备数据训练模型,那么学会Python基本上就够了,如果你想做机器学习系统的开发,那么需要学
会C++。 - 测试开发/测试。每个公司研发团队,有研发就有测试,测试主要分为测试开发和功能测试,测试
开发⼀般是使⽤⼀些测试⼯具(selenium、Jmeter等),设计测试⽤例,然后写⼀些脚本进⾏⾃动化
测试,性能测试等,有些还需要⾃⾏开发⼀些测试⽤具。功能测试主要是根据产品的功能,设计测
试⽤例,然后⼿动的⽅式进⾏测试。
2. 命名空间
命名空间的概念
在C语言中有自己的库函数和语法中的关键字,我们无法使用这些库函数来作为自己变量的名字,例如无法使用main,int,rand,strlen来作为我们创建的变量的名字。
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: "rand": 重定义;以前的定义是"函数"
printf("%d\n", rand);
return 0;
}
C++中便解决了这一个问题。我们引入命名空间-namespace
• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中 即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下 面的rand和main不在冲突了。
cpp
namespace test {
int rand = 10;
int Add(int x = 1, int y = 2)
{
return x + y;
}
int main = 20;
}
当然命名空间内部也可以嵌套
cpp
namespace test {
namespace test2 {
int strlen = 5;
}
int rand = 10;
int Add(int x = 1, int y = 2)
{
return x + y;
}
int main = 20;
}
我们定义了一个命名空间,现在我们就要使用命名空间中的变量了
如果我们要访问命名空间中的变量,那么就要使用 **::**操作符
用命名空间名字::变量名字来访问
cpp
int main()
{
test::rand += 1;
cout << test::rand << endl;
cout << test::test2::strlen << endl;
return 0;
}

值得一提的是,上述代码的cout<< <<endl为C++特有的输入代码,和C语言中的printf功能几乎一样,都代表标准输入,即往控制台中输入数据,末尾的endl代表自动换行并刷新缓冲区
命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以 下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
cpp
namespace test{
int a = 3;
}
using test::a;//展开test中的a
int main()
{
printf("%d", a);//展开后无需使用::操作符访问
}
当然也可以展开一整个命名空间
cpp
namespace test{
int a = 3;
int b = 5;
}
using namespace test;//展开test
int main()
{
printf("%d", a);
printf("%d", b);
}
3. 缺省函数
缺省函数的使用
C语言中我们声明的函数中只能用几个形参来接受实参的数据,但无法初始化形参的值
但在C++中我们可以初始化形参的值
cpp
int Add(int x, int y)//C语言
{
return x + y;
}
int Add(int x = 1, int y = 2)//C++
{
return x + y;
}
在C++调用函数时,如果没有给函数传递参数,那么就会使用默认值
cpp
int main()
{
cout << Add(1, 2) << endl;
cout << Add(3) << endl;
return 0;
}

全缺省函数和半缺省函数
全缺省函数指所有的参数都有默认值,而半缺省函数的参数从左到右中,左边的参数没有默认值,而右边的参数有默认值
cpp
//
全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//
半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
4. 函数重载
我们在C语言中无法创造两个函数名一样的函数,即使他们的参数和返回值都不同
而我们在C++却可以创造函数名 相同但参数一定不同的几种函数,这些函数称为重载函数
函数重载的使用
cpp
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
注意:重载的函数要求是函数名相同而参数不同,不考虑返回值,无论返回值相同还是不同都不在重载函数的判断范围内
在使用重载函数时,系统会自动判断传递的参数类型。如果是整型就调用传递整型的函数,如果是浮点数就调用传递浮点数的函数
cpp
int Add(int x = 1, int y = 2)
{
return x + y;
}
double Add(double x = 1, double y = 2.2)
{
return x + y;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(3.3) << endl;
return 0;
}

注意事项
cpp
// 错误示例:返回值不同不能构成重载
// int GetValue() { return 1; }
// double GetValue() { return 1.0; } // 编译错误!
// 有歧义的重载
void Func(int a = 10) {
cout << "Func(int a)" << endl;
}
void Func() {
cout << "Func()" << endl;
}
int main() {
// Func(); // 错误:有歧义,不知道调用哪个版本
Func(5); // 正确:明确调用带参数的版本
return 0;
}
5. 引用
引⽤不是新定义⼀个变量,而是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同⼀块内存空间。比如:水壶传中李逵,宋江叫"铁牛",江湖上⼈称"黑旋风";林冲,外号豹子头;
cpp
#include <iostream>
using namespace std;
int main() {
int a = 10;
// 引用:变量的别名
int& ref = a; // ref是a的别名
int& ref2 = ref; // ref2也是a的别名
cout << "a = " << a << endl; // 10
cout << "ref = " << ref << endl; // 10
cout << "ref2 = " << ref2 << endl; // 10
ref = 20;
cout << "修改后 a = " << a << endl; // 20
// 地址相同
cout << "&a = " << &a << endl;
cout << "&ref = " << &ref << endl;
cout << "&ref2 = " << &ref2 << endl;
return 0;
}
在上面,其实就可以可以把ref看作是a,ref2看作是ref,那么ref2其实就是a。就跟鲁迅和周树人的关系,ref是鲁迅,a是周树人。所以如果ref变了a也会变
引用的特性
引用在定义时必须初始化
⼀个变量可以有多个引用
引用⼀旦引用⼀个实体,再不能引用其他实体
cpp
#include <iostream>
using namespace std;
int main() {
int a = 10;
// 1. 引用必须初始化
// int& ref; // 错误:必须初始化
int& ref = a; // 正确
// 2. 引用不能改变指向
int b = 20;
ref = b; // 这是赋值,不是改变引用指向!
cout << "a = " << a << endl; // 20(值被改变)
cout << "&ref = " << &ref << endl; // 仍然是a的地址
return 0;
}
引用的使用
• 引用在实践中主要是于引用传参和引用做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被 引用对象。
• 引用传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
• 引用返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节 中会继续深⼊讲解。
• 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他 语⾔的引用(如Java)是有很⼤的区别的,除了用法,最⼤的点,C++引⽤定义后不能改变指向, Java的引用可以改变指向。
• ⼀些主要用C代码实现版本数据结构教材中,使⽤C++引用替代指针传参,⽬的是简化程序,避开 复杂的指针,但是很多同学没学过引用,导致⼀头雾⽔。
cpp
#include <iostream>
using namespace std;
// 引用传参:避免拷贝,可以修改实参
void Swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 引用返回:避免拷贝返回值
int& GetMax(int& a, int& b) {
return a > b ? a : b;
}
int main() {
int x = 5, y = 10;
cout << "交换前: x=" << x << ", y=" << y << endl;
Swap(x, y);
cout << "交换后: x=" << x << ", y=" << y << endl;
// 引用返回的应用
GetMax(x, y) = 100; // 修改较大的值
cout << "修改后: x=" << x << ", y=" << y << endl;
return 0;
}
const引用
我们也可以把具有常属性的值创造一个引用,如3.14,a+b这些。然而具有常属性变量的值必须得使用const引用,因为const修饰的引用也具有常属性,无法被更改,根常变量相吻合。如果不用const修饰那么就代表可以更改,与常属性相悖
cpp
#include <iostream>
using namespace std;
int main() {
const int a = 10;
// const引用
const int& ref1 = a; // 正确
// int& ref2 = a; // 错误:权限放大
int b = 20;
const int& ref3 = b; // 正确:权限缩小
// 临时对象的引用必须是const
const int& ref4 = 30; // 正确
const int& ref5 = b * 2; // 正确
// int& ref6 = 30; // 错误
double d = 3.14;
const int& ref7 = d; // 正确:类型转换产生临时对象
return 0;
}
指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功 能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。 • 引用可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引用结果为引用类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
6. 内联函数
在C语言定义的宏虽然好用,但往往坑较多
cpp
#define Add(x,y) x+y;
这个宏有什么错误?那错误可多了,首先后面的分号就不可取,不然会直接copy到目标代码行中
其次如果目标代码行为Add(1,2)*4,那么就会替换为1+2*4,与我们的预想不一样
为了完美地替代宏,C++就引入了内联函数inline
• inline对于编译器而言只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展 开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁 调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数。
• vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下 以下两个地⽅。
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地 址,链接时会出现报错。
cpp
#include <iostream>
using namespace std;
// 内联函数:建议编译器在调用处展开
inline int Add(int a, int b) {
return a + b;
}
// 对比宏函数
#define ADD_MACRO(a, b) ((a) + (b))
int main() {
int x = 5, y = 3;
cout << "内联函数: " << Add(x, y) << endl;
cout << "宏函数: " << ADD_MACRO(x, y) << endl;
// 查看汇编代码可以发现内联函数可能被展开
int result = Add(x, y);
return 0;
}
内联函数只是一个建议,建议编译器把这个函数当作内联函数处理,但如果你的函数的代码行过多,甚至含有大量的递归,那么编译器就不会使用内联函数而是当作正常函数使用
cpp
// 内联函数适合短小的频繁调用的函数
// 不适合复杂函数或递归函数
// 正确:短小简单
inline int Max(int a, int b) {
return a > b ? a : b;
}
// 可能不会被内联:函数体较大
inline void ProcessData(int* data, int size) {
// 复杂的处理逻辑...
for (int i = 0; i < size; ++i) {
// 很多操作...
}
}
7. nullptr
在C++中NULL不再代表空指针,而是0,取而代之的是nullptr
cpp
#include <iostream>
using namespace std;
void Func(int x) {
cout << "调用整型版本: " << x << endl;
}
void Func(int* ptr) {
if (ptr == nullptr) {
cout << "调用指针版本: 空指针" << endl;
} else {
cout << "调用指针版本: " << *ptr << endl;
}
}
int main() {
int value = 10;
Func(0); // 调用整型版本
Func(NULL); // 可能调用整型版本(取决于NULL的定义)
Func(&value); // 调用指针版本
Func(nullptr); // 明确调用指针版本
return 0;
}
nullptr的优势
cpp
#include <iostream>
using namespace std;
int main() {
// 传统NULL的问题
int* p1 = NULL; // NULL可能是0或(void*)0
// C++11的nullptr
int* p2 = nullptr; // 明确的空指针
// 类型安全
// int x = nullptr; // 错误:不能将nullptr转换为int
if (p2 == nullptr) {
cout << "p2是空指针" << endl;
}
// 与所有指针类型兼容
double* pd = nullptr;
char* pc = nullptr;
return 0;
}
8. 综合示例
简单的学生管理系统
cpp
#include <iostream>
#include <string>
using namespace std;
namespace StudentSystem {
struct Student {
string name;
int age;
double score;
};
// 使用引用避免拷贝
void PrintStudent(const Student& stu) {
cout << "姓名: " << stu.name
<< ", 年龄: " << stu.age
<< ", 分数: " << stu.score << endl;
}
// 函数重载
void UpdateScore(Student& stu, double newScore) {
stu.score = newScore;
}
void UpdateScore(Student& stu, double newScore, string comment) {
stu.score = newScore;
cout << "更新说明: " << comment << endl;
}
// 返回引用以便链式调用
Student& InitStudent(Student& stu,
string name = "未知",
int age = 0,
double score = 0.0) {
stu.name = name;
stu.age = age;
stu.score = score;
return stu;
}
}
int main() {
using namespace StudentSystem;
Student s1, s2;
// 使用缺省参数
InitStudent(s1);
InitStudent(s2, "张三", 20, 85.5);
PrintStudent(s1);
PrintStudent(s2);
// 函数重载
UpdateScore(s1, 90.0);
UpdateScore(s2, 95.0, "期末考试优秀");
PrintStudent(s1);
PrintStudent(s2);
return 0;
}