C++几个版本的特性
C++11的特性:史诗级变革(现代 C++ 的基石)
nullptr
在 C++ 的世界里,虽然 NULL 和 nullptr 都代表“空指针”,但它们在底层逻辑和类型安全上有本质的区别。简单来说,NULL 是过去(C 语言遗产),而 nullptr 是未来(现代 C++)。
本质区别:整数 vs 指针
- NULL:在底层通常被定义为整数0或者 (void*)0。在 C++ 中,它本质上是一个宏。因为 C++ 允许将0隐式转换为指针,所以 NULL 一直被当作指针用。
- nullptr:是 C++11 引入的关键字。它的类型是 std::nullptr_t,它可以隐式转换为任何指针类型,但不能隐式转换为整数。
nullptr是为了解决NULL的二义性问题而出现的。由于NULL本质上是一个整数,因此在函数重载时会导致意想不到的错误。举个例子,加入你有以下两个函数:
|
|
如果你调用func(NULL):编译器会优先把它看作整数0,从而调用函数A。这通常不是你的本意。如果你调用func(nullptr):编译器明确知道这是一个指针,从而调用函数B。
因此,在编写现代C++(C++11及以上):请永远使用nullptr。它能帮你规避掉很多难以排查的重载Bug。
以下是它们的核心差异对比表:
| 特性 | NULL | nullptr |
|---|---|---|
| 本质 | 宏定义(通常是 0) | 关键字(std::nullptr_t) |
| 类型安全 | 弱(容易与整数混淆) | 强(只能代表指针) |
| 引入版本 | C 语言时代 / C++98 | C++11 及以后 |
| 隐式转换 | 可转为整数或指针 | 仅可转为指针,不可转为整数 |
| 推荐用法 | 现代 C++ 中应避免使用 | 强烈建议在所有场景使用 |
类型推导
在现代 C++(C++11 及以后)中,auto 和 decltype 都是用来进行类型推导的利器。虽然它们看起来都在替你省事,但它们的工作原理和“脑回路”完全不同。
auto:由值推导类型
auto的核心逻辑是:“根据等号右边的初始化值,推断出变量应该是什么类型。”
- 特点:变量必须初始化。
- 规则:它会忽略顶层的 const 属性和引用属性(除非你显式加上 auto& 或 const auto)。
|
|
decltype:由表达式推导类型
decltype(declared type)的核心逻辑是:“查询一个表达式或变量的声明类型,而不实际计算它。”
- 特点:不需要初始化,它只看表达式的“户口本”。
- 规则:它会完整保留所有的 const 和引用属性。
|
|
协作场景:追踪返回类型
在C++11中,如果你写一个模板函数,返回值取决于参数的运算,你可能无法预先知道类型。这时需要结合两者:
|
|
Lambda表达式
Lambda表达式实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
智能指针
智能指针的核心思想是 RAII(资源获取即初始化):它本质上是一个类(封装了原始指针),利用栈上对象的生命周期来管理堆上的内存。当智能指针对象超出作用域被销毁时,它的析构函数会自动帮你 delete 掉底层的内存。
C++ 标准库(<memory>)主要提供了三种智能指针,它们各自对应不同的“所有权”模型:
1.std::unique_ptr:独占所有权(霸道总裁)
这是最常用、也最高效的智能指针。它的原则是:同一时刻,只能有一个unique_ptr指向同一块内存。
- 特点:不允许复制(拷贝构造函数被禁用),只允许移动(转移所有权)。由于没有额外的性能开销,它的性能和裸指针几乎一模一样。
- 适用场景:只要你不需要在多个地方共享这块内存,就无脑用它。
|
|
输出:
|
|
2.std::shared_ptr:共享所有权(合伙人)
当你确实需要多个指针共同管理同一块内存时,就需要用到它。它的原则是:大家一起用,最后一个走的人负责关灯(释放内存)。
-
核心机制:引用计数(Reference Counting)。系统会在内部维护一个计数器,记录有多少个 shared_ptr 指向这块内存。
- 每多一个 shared_ptr 指向它,计数器 +1。
- 每销毁一个 shared_ptr,计数器 -1。
- 当计数器归 0 时,自动 delete 内存。
-
代价:因为要维护引用计数(且计数操作是线程安全的),它的性能开销比 unique_ptr 稍大。
|
|
输出:
|
|
3.std::weak_ptr:弱引用(旁观者)
weak_ptr 是为了解决 shared_ptr 的一个致命缺陷而生的:循环引用。
- 什么是循环引用? 假设对象 A 里面有一个 shared_ptr 指向对象 B,对象 B 里面也有一个 shared_ptr 指向对象 A。这会导致它们的引用计数永远降不到 0,从而造成内存泄漏。代码演示如下:
|
|
输出:
|
|
~ClassA() 和 ~ClassB() 里的“被销毁了!”这两句话根本没有打印出来,这意味着内存泄漏了。所以,为了避免出现上述问题,我们需要引入weak_ptr。只需讲上面代码中的这行代码 // std::weak_ptr
- weak_ptr 的作用:它只能从 shared_ptr 或另一个 weak_ptr 构造。它不增加引用计数,仅仅是“观察”这块内存。当你要使用数据时,必须调用.lock() 方法将它提升为shared_ptr(如果内存还没被释放的话)。该方法会返回一个shared_ptr,但若没有被接收的话,则不会增加所指向对象的引用计数。
|
|
输出:
|
|
基于范围的for循环:for(auto &i:arr)
thread类和mutex类
C++14的特性
泛型 Lambda
Lambda 的参数可以用 auto 推导了([](auto x, auto y) { return x + y; })。
返回值类型推导
普通函数也可以直接写 auto 作为返回值类型,让编译器去猜。
std::make_unique
补齐了 C++11 只有 std::make_shared 的遗憾。
C++17的特性
结构化绑定
可以直接解包结构体或 std::tuple:auto [x, y, z] = get_3d_point();。
if / switch 带初始化
把变量的作用域严格限制在判断语句内,例如:if (auto it = map.find(key); it != map.end()) { … }。
现代化词汇类型:
- std::optional:完美替代以前用指针判空来表示“可能没有值”的场景。
- std::variant:类型安全的 union。
- std::string_view:字符串的“只读视图”,实现了零拷贝传递字符串,极大地提升了性能。
C++20的特性:四大支柱(第二次历史跨越)
C++20 是继 C++11 之后最庞大的一次更新,它引入的“四大支柱”彻底改变了现代 C++ 的编程范式。
Concepts (概念)
给模板编程加上了“类型约束”。以前写模板报错时,编译器会打印几百行天书;现在用 Concepts 约束后,错误信息终于变成了人类可读的文字。
Modules (模块)
正式告别 #include 历史!使用 import 引入模块,不仅解决了宏污染问题,还让大型项目的编译速度有了数量级的提升。
Coroutines (协程)
引入了无栈协程,新增了 co_await, co_yield, co_return 关键字,让异步编程(比如网络请求、高并发)可以用同步的方式写出来。
Ranges (范围库)
结合了函数式编程思想,允许用类似 Linux 管道符 | 的方式处理数据:data | views::filter(…) | views::transform(…),代码极其优雅。
其他亮点
三路比较运算符(<=>,也叫飞船运算符)、std::format(类似 Python 的高级格式化输出)。
C++23 (及以后):标准库的极致完善
C++23 进一步吸收了其他现代语言的优点,让标准库更加“开箱即用”。
std::print / std::println
大家终于可以抛弃老旧的 std::cout « 和不安全的 printf 了,这是一种更现代、更快速的控制台输出方式。
std::expected
引入了类似 Rust Result 的错误处理机制。函数可以返回一个“期望的值”或者“错误原因”,比抛出异常更轻量,比返回错误码更清晰。
显式 this 参数 (Deducing this)
允许在类成员函数中显式把 this 当作参数传递,极大简化了复杂的模板代码(如 CRTP 模式)。
多维数组操作:std::mdspan
对科学计算和高性能计算非常友好,提供了一种访问连续内存的多维视图。