前言
终于,经过我们的不懈努力,我们已经通关了C语言以及数据结构的学习,那么接下来我们就要开始C++的学习。C++相较与C,需要花费很长一段时间去学习,总之要成为一名C++工程师道阻且长,我们还有很长的路要走,望诸君共勉。
一、C++的起源
1979年,Bjarne Stroustrup在贝尔实验室工作。当时他参与一个叫"分布式系统"的项目,用的是C语言。C语言很快、很底层、很强大,但有一个致命问题:当项目规模超过某个阈值后,代码的可维护性会断崖式下跌。
因为C语言只有函数和结构体,没有"类"和"封装"。数据和操作数据的函数是分离的,一旦需求变化,你可能要改几十个文件,还容易漏掉某个函数。
Stroustrup的想法很简单:在C语言的基础上,加上类似Simula语言的面向对象特性(类、继承、虚函数等),同时保持C的性能和跨平台能力。
1983年,这个"带类的C"被正式命名为C++。++是C语言的自增运算符,寓意"比C更进一步"。
关键洞察:C++从诞生起就不是一门"纯设计"的语言,而是一种"工程妥协"的产物------它既要兼容C的生态(因此继承了C的很多缺陷),又要提供高级抽象。这种双重性格,既是C++的成功之道,也是它被诟病"复杂"的根源。
二、C++的标准化之路
1989年,C++开始标准化工作。这个过程比所有人预想的都要漫长。
1998年:C++98,第一个官方标准
-
确立了C++的核心语法:模板、异常、RTTI(运行时类型识别)
-
引入了STL(标准模板库),这是Alexander Stepanov的杰作。STL将"泛型编程"推到了前所未有的高度------容器、迭代器、算法、函数对象,一套优雅的体系。
-
注意:STL原本不是C++标准的一部分,是在标准化草案通过后,委员会投票决定"强行塞进去"的。这个决定推迟了标准的发布,但也让C++脱胎换骨。
2003年:C++03,修修补补
主要修复C++98中的缺陷和歧义,没有引入重大新特性。很多人称C++98/03为"经典C++"。
2011年:C++11,姗姗来迟的革命
从2003到2011,整整8年。这期间Java、C#等语言飞速发展,C++被嘲讽为"恐龙语言"。但C++11发布后,所有人都闭嘴了:
-
auto类型推导 :
auto it = vec.begin(); -
lambda表达式 :
[](int x){ return x>0; } -
右值引用和移动语义:彻底解决了临时对象拷贝的性能问题
-
智能指针 :
unique_ptr,shared_ptr,告别手动delete -
并发库 :
std::thread,std::atomic,终于有了跨平台的多线程 -
范围for :
for(int x : arr)
C++11被公认为是C++的"第二次诞生"。从此,现代C++(Modern C++)的概念深入人心。
之后的小步快跑
-
C++14:对C++11的小修小补,比如泛型lambda、二进制字面量
-
C++17 :
if constexpr、结构化绑定、文件系统库(终于可以跨平台操作目录了) -
C++20:概念(Concepts)、协程(Coroutines)、模块(Modules)------这三个特性被称作"三大件",改变了编写模板、异步代码和组织头文件的方式
-
C++23:小版本更新,但有一个"宫斗"故事(见下文)
-
C++26:制定中,预计会加入网络库(希望如此)

三、C++的地位:它为什么依然不可替代?
下图是TIOBE排行榜:

我们发现C++排第三,仅次于Python和C。但你可能会问:Python这么火,为什么还要学C++?
答案是:性能和控制的极致平衡。
-
Python:开发快,但运行慢(因为解释型+动态类型)。你可以用Python写机器学习模型,但底层的TensorFlow/PyTorch内核是用C++写的。
-
C语言:最快,但抽象能力弱。写一个安全的链表都要小心翼翼。
-
Java/C#:有垃圾回收,适合大型业务系统,但无法做到实时控制和极致性能(比如高频交易、游戏引擎、嵌入式系统)。
C++的应用场景(文档列举得很全,我挑几个重点):
-
游戏引擎:Unreal Engine 4/5 核心是C++,Unity底层也是C++
-
数据库:MySQL、PostgreSQL、LevelDB、RocksDB
-
浏览器:Chromium(Blink引擎)、Firefox(Gecko引擎)
-
操作系统:Windows内核部分、Mac/iOS内核、Linux部分驱动
-
高性能计算:数值模拟、量化金融、自动驾驶
-
音视频:FFmpeg、WebRTC、编解码器
个人补充:如果你对"底层"感兴趣------比如想理解计算机是如何运行你的代码的,想手动管理内存,想编写极致高效的算法------C++几乎是必经之路。Python屏蔽了太多细节,会让你变成"调包侠"。
四、命名空间
为什么引入命名空间?
C++中为了避免命名冲突或则文字污染,引入了命名空间的概念。
什么是命名冲突?如下:

补充说明:C++是兼容C语言的,C语言的代码用C++的编译器同样可以实现。
为了避免这种情况,我们可以将rand写到一个命名空间中,如下:

你看是不是完美解决了,接下来我们详细说说命名空间的使用。
命名空间的使用
定义命名空间
需要使用到namespace关键字,后面跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
cpp
#include<stdio.h>
namespace ccc
{
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
printf("%d", ccc::Add(9,1));
return 0;
}

namespace本质是定义出⼀个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不在冲突了。
C++中域有函数局部域,全局域,命名空间域,类域(以后会学到);域影响的是编译时语法查找⼀个变量/函数/ 类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响 编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
namespace只能定义在全局,当然他还可以嵌套定义。
在局部域中就不能进行定义:

嵌套定义展示:
cpp
#include<iostream>
namespace wxuc
{
namespace stu
{
int a = 18;
}
int b = 19;
}
int main()
{
std::cout << wxuc::b << std::endl << wxuc::stu::a << std::endl;
return 0;
}

项目工程中多文件中定义的同名namespace会认为是⼀个namespace,不会冲突。
比如你在a.h中写namespace my { int x; },在a.cpp中写namespace my { int y; },它们会合并成一个命名空间。这为大型项目的模块化提供了便利。
C++标准库都放在⼀个叫std(standard)的命名空间中。
在项目中不能把std的内容暴露出去,但是在我们的日常练习中没关系。
使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以我们要使⽤命名空间中定义的变量/函数,有三种方式:
指定命名空间访问,项⽬中推荐这种⽅式。
cpp
ccc::a=18
using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
cpp
#include<iostream>
namespace wxuc
{
namespace stu
{
int a = 18;
}
int b = 19;
}
using wxuc::b;//这里就是只展开b
int main()
{
std::cout << wxuc::b << std::endl << b<< std::endl;
return 0;
}
展开命名空间中全部成员,项目不推荐,冲突风险很⼤,日常小练习程序为了方便推荐使用。
五、C++中的输入与输出
<iostream> 是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输⼊、输出对象。
std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输 ⼊流。
std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
std::endl 是⼀个函数,流插入输出时,相当于插入⼀个换行字符加刷新缓冲区。
<<是流插⼊运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
一个程序展示:
cpp
#include<iostream>
using namespace std;
int main()
{
cout << "Hello C++ World!" << endl;
int a, b;
cout << "请输入a的值" << endl;
cin >> a;
printf("请输入b的值\n");
scanf("%d", &b);
cout << "a: "<<a << " b: " << b << endl;
printf("a: %d b: %d", a, b);
return 0;
}

使⽤C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输⼊输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要 通过命名空间的使⽤方式去用他们。
⼀般日常练习中我们可以using namespace std,实际项目开发中不建议using namespace std。 这⾥我们没有包含<stdio.h>,也可以使用printf和scanf,在包含<iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
局限性:
cout不能指定格式进行输出,当我们需要对输出内容有一定格式要求时,我们还是使用printf;
cin以及cout因为不需要手动指定格式,所以速度会比printf以及scanf慢,如果在一些高要求的竞赛中,我们可以使用下面的代码来及取消同步流,这样可以加快速度;
cpp
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
六、缺省参数
缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调用该函数时,如果没有指定实参 则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把 缺省参数也叫默认参数) 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。
C++规定半缺省参数必须从右往左 依次连续缺省,不能间隔跳跃给缺省值。
带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
代码展示:
cpp
#include <iostream>
#include <assert.h>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使⽤参数的默认值
Func(10); // 传参时,使⽤指定的实参
return 0;
}
cpp
#include<iostream>
using namespace std;
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 = 20, int c = 30)
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl << endl;
}
int main()
{
Func1();
Func1(40);
Func1(40, 50);
Func1(40, 50, 60);
//只能从左到右依次进行赋值,不能跳跃进行赋值
Func2(10);
Func2(40,50,60);
return 0;
}
七、函数重载
C++⽀持在同⼀作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更加灵活。C语言是不支持同⼀作用域中出现同名函数的。
比如我们在C语言中要实现不同数据类型的加法,我们需要进行如下操作:
cpp
#include<stdio.h>
int Addi(int a, int b)
{
return a + b;
}
double Addd(double a, double b)
{
return a + b;
}
int main()
{
printf("%d\n", Addi(1,2));
printf("%f\n", Addd(1.2, 3.4));
return 0;
}
我们就需要创建不同名的函数进行区分,但是其实代码的核心思路是没变的,都是实现两个数的相加,这样其实加重了我们的记忆负担,那C++就引入了函数重载的概念,可以为我们减轻记忆负担,C++中函数重载实现上面功能代码展示:
cpp
#include<iostream>
using namespace std;
double Add(double x, double y)
{
return x + y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
cout << Add(1.2, 3.4) << endl;
cout << Add(1, 2) << endl;
return 0;
}
注意点:
如果只有返回类型不同,函数是不构成重载的,必须要函数中的参数不同,可以是个数不同、数据类型不同,但是只是变量名不同同样无法构成函数重载。
八、引用
什么是引用?
引⽤不是新定义⼀个变量,而是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头;
类型& 引用别名 = 引用对象;
C++中为了避免引入太多的运算符,会复用C语言的⼀些符号,比如前⾯的<< 和 >>,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。(吐槽⼀下,这个问题其实挺坑的,个⼈觉得⽤更多符号反⽽更好,不容易混淆)

cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& pa = a;
cout << a <<' '<< pa << endl;
pa = 12;
cout << a << ' ' << pa << endl;
return 0;
}

引用的特性
1.引用在定义时必须初始化
2.⼀个变量可以有多个引用
3.引用⼀旦引用⼀个实体,再不能引用其他实体
代码展示:
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
// 编译报错:"ra": 必须初始化引⽤
//int& ra;
int& b = a;
int c = 20;
// 这⾥并非让b引⽤c,因为C++引⽤不能改变指向,
// 这⾥是⼀个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
引用的使用
在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被
引用对象。
引用传参跟指针传参功能是类似的,引用传参相对更方便⼀些。
引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。
cpp
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
const 引用
可以引用⼀个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。

不需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
const int& ra = 30;
// 编译报错: "初始化": ⽆法从"int"转换为"int &"
int& rb = a * 3;
const int& rb = a * 3;
double d = 12.34;
// 编译报错:"初始化": ⽆法从"double"转换为"int &"
int& rd = d;
const int& rd = d;
return 0;
}

我们可以看到,编译器会已经发现错误用红线标出了。
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,
C++中把这个未命名对象叫做临时对象。
指针与引用的关系
指针和引⽤的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
语法概念上引用是⼀个变名不开空间,指针是存储⼀个变量地址,要开空间。
引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
引用可以直接访问指向对象,指针需要解引用才是访问指向对象。 sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
九、inline
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。
inline对于编译器而言只是⼀个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展 开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline使用于频繁 调用的短小函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
cpp
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
// 可以通过汇编观察程序是否展开
// 有call Add语句就是没有展开,没有就是展开了
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}
C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调 试,C++设计了inline⽬的就是替代C的宏函数。
vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下 以下两个地方。

inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地 址,链接时会出现报错。
十、nullptr
NULL实际是⼀个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种
定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的
f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);
调用会报错。
C++11中引入nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换
成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被
隐式地转换为指针类型,而不能被转换为整数类型。
cpp
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
// 编译报错:error C2665: "f": 2 个重载中没有⼀个可以转换所有参数类型
// f((void*)NULL);
f(nullptr);
return 0;
}