1. C++的发展历程
C++的诞生可以追溯到1979年,由Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)在贝尔实验室初步构想。当时,他深感现有语言(如C语言)在处理复杂软件开发项目,尤其是模拟和操作系统开发方面的局限性。
1983年,Stroustrup在C语言的基础上融入了面向对象编程的特性,设计出了C++的雏形,引入了类、封装、继承等核心概念,并正式命名为C++。
此后,C++在学术界和工业界迅速普及,其标准库和模板等特性也得到持续发展。C++的标准化工作始于1989年,由ANSI和ISO(国际标准化组织)联合委员会负责。1994年,委员会发布了首个标准化草案。随后,由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室开发的STL(标准模板库)也被纳入C++标准。尽管这扩展了C++的原始定义并延迟了标准化进程,但最终在1997年11月14日通过了最终草案,并于1998年正式投入使用。
1.1 C++版本迭代
C++经历了多次版本更新,主要版本及其特性如下表所示:
时间 | 阶段 | 内容 |
---|---|---|
1998年 | C++98 | C++官方第一个版本,绝大多数编译器支持,获得了ISO认可,以模板方式重写C++标准库,引入了STL。 |
2003年 | C++03 | 主要修订C++98标准中的错误和漏洞,引入了tr1库等新特性和功能。 |
2011年 | C++11 | 革命性更新,增加了大量新特性,如lambda表达式、范围for循环、右值引用和移动语义、智能指针、标准线程库等。 |
2014年 | C++14 | 对C++11的扩展和改进,例如泛型的lambda表达式、函数返回值类型推导、二进制字面常量等。 |
2017年 | C++17 | 进一步增强功能和表达能力,引入if constexpr、折叠表达式等语法特性,改进了string、filesystem等标准库组件。 |
2020年 | C++20 | 又一个重要里程碑,引入协程(Coroutines)、概念(Concepts)、模块化(Modules)等。 |
2023年 | C++23 | 小版本更新,完善现有特性,增加了if consteval、flat_map, import std导入标准库等。 |
2026年 | C++26 | 制定中。 |
1.2 关于C++23网络库的小插曲
C++23原计划引入网络库(networking),但最终发布时并未包含,引发了社区的广泛讨论。据称,Asio库的标准化进程受到了一些阻碍。尽管如此,C++标准委员会(WG21)对网络功能的重视程度依然很高,未来版本仍值得期待。
2. C++参考文档
学习C++时,可以参考以下文档:
- https://legacy.cplusplus.com/reference/:内容相对易懂,但标准更新到C++11。
- https://zh.cppreference.com/w/cpp:C++官方文档中文版,信息全面,更新及时。
- https://en.cppreference.com/w/:C++官方文档英文版,信息全面,更新及时。
建议结合使用这些文档,各有优势。
3. C++的重要性
3.1 编程语言排行榜
根据TIOBE排行榜(2024年6月数据),C++在热门编程语言中位居第二。TIOBE排行榜反映了编程语言的受欢迎程度,基于程序员数量、课程和第三方厂商数量,并结合搜索引擎数据统计得出。
3.2 C++在工作领域中的应用
C++的应用领域非常广泛,包括但不限于:
- 大型系统软件开发:如编译器、数据库、操作系统、浏览器等。
- 音视频处理:许多主流的音视频开源库(如FFmpeg、WebRTC)主要技术栈为C++。
- PC客户端开发:例如使用C++和QT开发Windows桌面应用。
- 服务端开发:用于对性能要求较高的场景,如游戏服务、流媒体服务、量化高频交易服务等。
- 游戏引擎开发:许多知名的游戏引擎(如UE4、Cocos2d-x)均使用C++开发。
- 嵌入式开发:应用于智能手环、摄像头、车载系统等设备的软件控制。
- 机器学习引擎:机器学习底层的许多算法使用C++实现,上层用Python封装。
- 测试开发/测试:编写自动化测试脚本、性能测试以及开发测试工具等。
4. C++学习建议和书籍推荐
4.1 C++学习难度
C++是一门相对较难学习和精通的语言,学习曲线较为陡峭。这既有历史包袱的原因,也与语言本身的设计和发展历程有关。
4.2 C++学习建议
- 课后务必练习课堂上讲过的所有样例,深入理解相关知识点。
- 如果时间允许,最好每节课后总结博客或笔记;时间紧张时,至少要整理重点章节的笔记。
4.3 学习书籍推荐
- 《C++ Primer》:经典的C++语法书籍,内容详尽,适合初学预习及后续查阅。
- 《STL源码剖析》:从底层实现角度深入剖析STL,有助于学习高效简洁的数据结构和算法代码实现及泛型封装。建议在课程学习过半后阅读。
- 《Effective C++》:总结了55个编写高效、正确C++代码的实用条款。建议在中后期学习,并在工作1-2年后重读,会有新的收获。
5. C++的第一个程序
C++兼容C语言的大部分语法,因此C语言的 “hello world” 程序在C++中依然可以运行,只需将文件后缀改为 .cpp。在Linux环境下,需要使用 g++ 编译器进行编译。
C语言风格的 “hello world”:
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
更规范的C++版本 “hello world”:
// test.cpp
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
6. 命名空间 (Namespace)
6.1 namespace的价值
在C/C++中,大量的变量、函数和类名都存在于全局作用域,容易导致命名冲突或名字污染。namespace 关键字的出现就是为了解决这个问题,它通过对标识符的名称进行本地化来避免命名冲突。例如,C语言中 rand 函数与同名全局变量的冲突可以通过命名空间解决。
6.2 namespace的定义
- 定义命名空间使用 namespace 关键字,后跟命名空间名称,再接一对花括号 {},括号内即为命名空间的成员(变量、函数、类型等)。
- namespace 本质上是定义一个独立的域,与全局域隔离,不同域中可以定义同名成员。
- C++中的域包括函数局部域、全局域、命名空间域和类域。域主要影响编译时对变量/函数/类型出处的查找逻辑,从而解决名字冲突。局部域和全局域还会影响变量的生命周期,而命名空间域和类域不影响生命周期。
- namespace 可以嵌套定义。项目中多个文件定义的同名 namespace 会被视为同一个 namespace,不会冲突。
- C++标准库的所有内容都放在名为 std (standard) 的命名空间中。
示例:
#include <stdio.h>
#include <stdlib.h>
namespace bit
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
printf("%p\n", rand); // 访问全局的rand函数指针
printf("%d\n", bit::rand); // 指定访问bit命名空间中的rand
return 0;
}
嵌套命名空间示例:
namespace bit
{
namespace pg
{
int rand = 1;
// ...
}
namespace hg
{
int rand = 2;
// ...
}
}
int main()
{
printf("%d\n", bit::pg::rand);
printf("%d\n", bit::hg::rand);
return 0;
}
6.3 命名空间使用
编译器默认只在局部或全局查找变量的声明/定义,不会进入命名空间查找。因此,要使用命名空间中定义的成员,有三种方式:
- 指定命名空间访问:使用作用域解析运算符 ::,例如 namespace_name::member_name。项目中推荐此方式。
- using 将命名空间中某个成员展开:例如 using namespace_name::member_name;。项目中对于经常访问且不存在冲突的成员推荐此方式。
- 展开命名空间中全部成员:例如 using namespace namespace_name;。项目中不推荐,因为冲突风险大,但日常小型练习程序为了方便可以使用。
7. C++ 输入&输出
<iostream>
是C++标准的输入输出流库,定义了标准输入输出对象。std::cin
是istream
类的对象,用于标准输入流(窄字符)。std::cout
是ostream
类的对象,用于标准输出流(窄字符)。std::endl
是一个函数,在流插入输出时,相当于插入一个换行符并刷新缓冲区。<<
是流插入运算符,>>
是流提取运算符。- C++的输入输出比C语言的
printf
/scanf
更方便,能自动识别变量类型(通过函数重载实现),并且更好地支持自定义类型对象的输入输出。 - 日常练习中可以使用
using namespace std;
,但在实际项目中不建议。
示例:
#include <iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
cout << a << " " << b << " " << c << endl;
cin >> a >> b >> c;
cout << a << " " << b << endl;
return 0;
}
提高IO效率的优化代码:
ios_base::sync_with_stdio(false); // 关闭与C标准库的同步
cin.tie(nullptr); // 解除cin与cout的绑定
cout.tie(nullptr);
8. 缺省参数
缺省参数(也叫默认参数)是在声明或定义函数时为函数的参数指定一个默认值。调用该函数时,如果未指定实参,则采用该形参的默认值;否则,使用指定的实参。
- 全缺省参数:函数的所有形参都带有默认值。
- 半缺省参数:函数的部分形参带有默认值。C++规定半缺省参数必须从右往左依次连续给出,不能跳跃。
- 调用带缺省参数的函数时,实参必须从左到右依次给出,不能跳跃。
- 当函数声明和定义分离时,缺省参数不能同时出现在声明和定义中,规定必须在函数声明中给出默认值。
示例:
#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 = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func1(); // 输出 10, 20, 30
Func1(1); // 输出 1, 20, 30
Func2(100); // 输出 100, 10, 20
Func2(100, 200); // 输出 100, 200, 20
return 0;
}
9. 函数重载
C++支持在同一作用域中出现同名函数,但要求这些同名函数的形参列表不同(参数个数、类型或顺序不同)。这使得函数调用表现出多态行为,使用更灵活。
- 参数类型不同
- 参数个数不同
- 参数类型顺序不同
注意:函数的返回值类型不同不能作为函数重载的条件,因为调用时无法区分。如果两个函数仅因缺省参数而可能导致调用歧义,编译器会报错。
示例:
#include<iostream>
using namespace std;
// 参数类型不同
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;
}
// 参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
Add(10, 20); // 调用int版本
Add(10.1, 20.2);// 调用double版本
f(); // 调用无参版本
f(10); // 调用有参版本
return 0;
}
10. 引用 (Reference)
10.1 引用的概念和定义
引用不是新定义一个变量,而是给已存在的变量取一个别名。编译器不会为引用变量开辟内存空间,它和它引用的变量共享同一块内存空间。
示例:
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a; // b是a的别名
int& c = a; // c也是a的别名
++b;
cout << a << endl; // 输出1
return 0;
}
10.2 引用的特性
- 引用在定义时必须初始化。
- 一个变量可以有多个引用。
- 引用一旦引用一个实体,就不能再引用其他实体。对引用赋值,实际上是修改其引用的实体的值。
10.3 引用的使用
引用主要用于:
- 引用传参:功能类似指针传参,但使用更方便,可以减少拷贝,提高效率,并能修改传入的参数。
- 引用做返回值:场景相对复杂,可以减少拷贝,提高效率,有时可以直接修改函数返回的变量。
示例 - 引用传参:
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
10.4 const引用
- 可以引用一个 const 对象,但必须使用 const 引用。
- const 引用也可以引用普通对象,此时引用的访问权限被缩小(不能通过该引用修改对象)。
- 当引用一个表达式的结果或发生类型转换时产生的临时对象时,必须使用 const 引用。
示例:
const int a = 10;
const int& ra = a; // 正确
int b = 20;
const int& rb = b; // 正确,权限缩小
11. inline 内联函数
- 用
inline
修饰的函数称为内联函数。编译器在调用内联函数的地方会尝试将其展开,避免函数调用的栈帧开销,从而提高效率。 - 通常适用于频繁调用且短小的函数。递归函数或代码量较大的函数即使加了
inline
也可能被忽略。 inline
函数不建议声明和定义分离到不同文件,否则可能导致链接错误。
示例:
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
cout << Add(1, 2) << endl;
return 0;
}
12. nullptr
- C++11引入了
nullptr
关键字,它是一种特殊类型的字面量,可以转换成任意其他类型的指针类型,但不能被隐式转换为整数类型。 - 使用
nullptr
定义空指针可以避免因NULL
的宏定义带来的类型转换问题。
示例:
#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(int x)
f(nullptr); // 调用 f(int* ptr)
return 0;
}