27. C++11/14 新特性:auto、decltype、范围for、nullptr、移动语义、右值引用、完美转发
说实话,C++11/14 这一波更新,是我从业十几年来见过的最猛的一次「版本大爆发」。很多老程序员一开始是抗拒的——「我写 C++ 好好的,为什么要学这些新东西?」但后来大家发现,真香。
我个人习惯把这一章叫做「现代 C++ 的起跑线」。你想想看,如果没有 auto、没有移动语义,写起代码来就像背着沙袋跑步。今天我们就来把这些沙袋一个一个卸掉。
27.1 auto:让编译器替你猜类型
auto 不是「动态类型」,它只是类型推导的语法糖。说白了,就是让编译器根据右边的表达式,自动推断出左边的类型。
auto x = 42; // int
auto y = 3.14; // double
auto z = new MyClass; // MyClass*
我在项目中遇到过最典型的场景:迭代器。以前写 std::vector<std::pair<int, std::string>>::iterator it = vec.begin(); 简直要命。现在一行 auto it = vec.begin(); 搞定。
auto a = 0; 这种,我宁愿写 int a = 0;,因为一眼就能看出是 int。
27.2 decltype:获取表达式的类型
auto 是「推导变量类型」,decltype 是「获取表达式类型」。两者经常配合使用。
int a = 10;
decltype(a) b = 20; // b 是 int
auto func() -> decltype(a + b) {
return a + b;
}
嗯,这里要注意:decltype 不会真的执行表达式,它只做类型分析。所以哪怕表达式里有除零操作,也不会报错。
decltype((x)) 和 decltype(x) 结果可能不同——加括号会推导出引用类型。这个细节在模板元编程里特别容易翻车。
27.3 范围 for:告别手写迭代器
范围 for 循环,说白了就是 Python 的 for x in list 的 C++ 版。写起来特别爽。
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto v : vec) {
std::cout << v << " ";
}
// 如果要修改元素,用引用
for (auto& v : vec) {
v *= 2;
}
为什么推荐用引用?因为如果不加引用,每次循环都会拷贝一个元素。对于 std::string 这种大对象,性能损失很明显。
27.4 nullptr:真正的空指针
在 C++03 里,空指针用 NULL 或 0。但 NULL 其实是个宏,展开后就是 0。这就导致了一个经典问题:
void func(int);
void func(char*);
func(NULL); // 调用的是 func(int),不是 func(char*)
nullptr 是 std::nullptr_t 类型的字面量,可以隐式转换为任何指针类型,但不会和整数混淆。我个人习惯是:所有指针初始化都用 nullptr,再也不写 NULL 了。
27.5 右值引用与移动语义
这是 C++11 最核心的更新之一。我们先搞清楚两个概念:
- 左值: 有地址、可以取地址的表达式。比如变量名、数组元素。
- 右值: 没有地址、不能取地址的表达式。比如字面量、临时对象。
右值引用用 && 表示,它专门绑定到右值上。有什么用?实现移动语义。
class MyString {
public:
// 拷贝构造函数
MyString(const MyString& other) {
data_ = new char[other.size_];
memcpy(data_, other.data_, other.size_);
}
// 移动构造函数
MyString(MyString&& other) noexcept {
data_ = other.data_; // 偷走指针
size_ = other.size_;
other.data_ = nullptr; // 置空原对象
other.size_ = 0;
}
private:
char* data_;
size_t size_;
};
你想想看,拷贝构造函数要重新分配内存、复制数据;移动构造函数只是交换几个指针。对于大对象来说,性能差距可能是几十倍。
std::move 把左值转成右值引用。
27.6 完美转发:std::forward
完美转发解决的问题是:在模板函数里,把参数原封不动地转发给另一个函数,同时保留它的左值/右值属性。
template<typename T>
void wrapper(T&& arg) {
// 希望保持 arg 的左右值属性
target(std::forward<T>(arg));
}
这里 T&& 不是右值引用,而是「转发引用」(也叫万能引用)。它既能绑定左值,也能绑定右值。配合 std::forward,就能实现完美转发。
我在项目中遇到过这样的场景:写一个工厂函数,需要把参数转发给构造函数。如果没有完美转发,就得写一堆重载版本,累死人。
std::forward。记住,每个参数只能 forward 一次,因为 forward 之后它的值类别就变了。
知识体系总览
下面这张图帮你理清这些新特性之间的关系:
总结一下
这一章的内容,说白了就是 C++ 从「老派」走向「现代」的关键一步。auto 和 decltype 让你少写类型,范围 for 让你少写循环,nullptr 让你少写 bug,移动语义和完美转发让你少写性能瓶颈。
我个人建议:从今天开始,把代码里的 NULL 全部换成 nullptr,把能写 auto 的地方都写上,把大对象的传参改成引用或移动。你会发现,代码质量提升不是一点点。
公众号:蓝海资料掘金营,微信deep3321