C++11核心特性解析:智能指针、移动语义与Lambda表达式实战指南
1. 从“新特性”到“新思维”:C++11的范式转变
如果你是从C++98/03时代一路走来的开发者,第一次接触C++11时,那种感觉可能不仅仅是“多了几个新功能”,而更像是一次认知上的刷新。过去,我们写C++,脑子里想的是“对象生命周期”、“手动管理资源”、“复杂的模板元编程”。而C++11之后,很多代码的写法、思考问题的角度都变了。它不仅仅是语法糖的堆砌,更是一次旨在让C++更安全、更高效、更具表达力的范式升级。很多人把学习重点放在
auto
、
lambda
这些显眼的特性上,这没错,但今天我想和你深入聊聊C++11中那些真正重塑我们编程习惯的“第二梯队”核心特性,它们可能不那么炫酷,却实实在在地解决了我们日常开发中的诸多痛点,是写出现代、健壮C++代码的基石。
我们将聚焦于几个关键领域:如何告别裸指针的噩梦,拥抱智能指针进行资源管理;如何理解移动语义,从根本上提升程序性能;以及如何利用
lambda
表达式和函数对象,让代码既简洁又强大。这些内容,是连接C++古典时代与现代编程的桥梁,理解了它们,你才能说真正踏入了现代C++的大门。
2. 资源管理的革命:从
new/delete
到智能指针
在C++11之前,资源管理,尤其是动态内存管理,是程序员肩上最重的担子之一。每一个
new
都必须对应一个
delete
,在复杂的逻辑分支和异常处理中,确保这一点极其困难,内存泄漏几乎是大型C++项目的“标配”问题。C++11引入的智能指针,其核心思想是RAII(Resource Acquisition Is Initialization),即资源获取即初始化。对象的构造函数获取资源,析构函数释放资源。智能指针就是这种思想的典型载体,它管理的是动态分配对象的生命周期。
2.1
std::unique_ptr
: 独占所有权的轻量级卫士
std::unique_ptr
如其名,表示对所指对象的独占所有权。一个对象在任何时刻只能被一个
unique_ptr
拥有。这种设计带来了两个巨大好处:一是所有权清晰,避免了多个指针指向同一对象时“谁该负责删除”的混乱;二是极高的效率,因为它几乎没有额外的开销,几乎等同于裸指针。
核心用法与原理:
#include <memory>
#include <iostream>
class Widget {
public:
Widget() { std::cout << "Widget constructed\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
void doSomething() { std::cout << "Widget working\n"; }
};
void useUniquePtr() {
// 1. 创建独占指针
std::unique_ptr<Widget> up1(new Widget()); // 传统方式
auto up2 = std::make_unique<Widget>(); // C++14起推荐方式,更安全高效
// 2. 像普通指针一样使用
up1->doSomething();
(*up2).doSomething();
// 3. 所有权的转移(移动语义)
std::unique_ptr<Widget> up3 = std::move(up1); // up1所有权转移给up3,up1变为nullptr
// 此时 up1.get() == nullptr, up3 拥有对象
// 4. 重置或释放
up3.reset(); // 显式释放所管理对象,up3变为nullptr。对象在此处被销毁。
// up2 在作用域结束时自动析构,释放其管理的对象
}
std::make_unique
(C++14)是创建
unique_ptr
的首选方式。它有两个关键优势:一是异常安全。考虑
foo(std::unique_ptr<T>(new T), std::unique_ptr<U>(new U))
,如果
new T
成功而
new U
抛出异常,那么
T
对象就会泄漏,因为
unique_ptr<T>
的构造函数还没来得及接管。而
make_unique
将对象的构造和智能指针的构造合为一步原子操作,避免了这个问题。二是代码更简洁,不需要重复写类型
T
。
一个常见的坑:
unique_ptr
禁止拷贝,只允许移动。这意味着你不能将它放入需要拷贝操作的旧式容器算法中(除非是移动)。但这也正是其保证所有权唯一的机制。
注意:
std::auto_ptr在C++11中已被废弃,正是因为它尝试用拷贝语法实现移动语义,导致语义混乱和潜在错误。unique_ptr是它的正确替代品。
2.2
std::shared_ptr
与
std::weak_ptr
: 共享所有权与打破循环引用
当需要多个实体共享同一个对象的所有权时,
std::shared_ptr
登场了。它通过引用计数来管理对象的生命周期。每多一个
shared_ptr
指向该对象,引用计数加1;每有一个
shared_ptr
被销毁或重置,引用计数减1。当计数变为0时,管理的内存被自动释放。
共享指针的基本使用:
void useSharedPtr() {
auto sp1 = std::make_shared<Widget>(); // 引用计数 = 1
{
auto sp2 = sp1; // 拷贝构造,引用计数 = 2
sp2->doSomething();
} // sp2 析构,引用计数 = 1
// sp1 仍然有效
sp1->doSomething();
} // sp1 析构,引用计数 = 0,Widget对象被销毁
std::make_shared
同样是推荐创建方式,它通常比直接
new
更高效,因为它可以将对象本身和控制块(存储引用计数等元数据)分配在连续的内存块中。
循环引用问题与
weak_ptr
:
shared_ptr
最大的陷阱是循环引用。如果两个对象各自持有一个指向对方的
shared_ptr
,那么它们的引用计数永远无法降到0,导致内存泄漏。
struct Node {
std::shared_ptr<Node> next;
~Node() { std::cout << "Node destroyed\n"; }
};
void circularReference() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1; // 循环引用形成!
} // 作用域结束,node1和node2的引用计数仍为1,对象永不销毁,内存泄漏。
为了解决这个问题,C++11引入了
std::weak_ptr
。
weak_ptr
是一种“弱引用”,它指向一个由
shared_ptr
管理的对象,但不会增加该对象的引用计数。这意味着它不拥有对象的所有权,因此不会阻止对象的销毁。你需要通过
lock()
成员函数来尝试获取一个可用的
shared_ptr
。
使用
weak_ptr
打破循环引用:
struct SafeNode {
std::weak_ptr<SafeNode> next; // 使用 weak_ptr 而非 shared_ptr
~SafeNode() { std::cout << "SafeNode destroyed\n"; }
};
void safeReference() {
auto node1 = std::make_shared<SafeNode>();
auto node2 = std::make_shared<SafeNode>();
node1->next = node2;
node2->next = node1; // 循环引用被打破,因为 weak_ptr 不增加计数
// 使用 weak_ptr
if (auto sp = node1->next.lock()) { // 尝试提升为 shared_ptr
sp->doSomething(); // 对象还存在,安全使用
} else {
std::cout << "The object no longer exists.\n";
}
} // 作用域结束,node1和node2引用计数归零,对象被正确销毁。
经验之谈:
在设计对象关系时,如果关系不是严格的“拥有”(ownership),而是“观察”(observation)或“临时使用”,就应该优先考虑使用
weak_ptr
。例如,在观察者模式中,主题(Subject)持有观察者(Observer)的
weak_ptr
列表,这样即使观察者被销毁,也不会影响主题,主题在通知前只需检查
weak_ptr
是否有效即可。
3. 性能飞跃的关键:理解移动语义与右值引用
这是C++11最深刻、也最难理解的变化之一,但它带来的性能提升是颠覆性的。要理解移动语义,必须先理解左值(lvalue)和右值(rvalue)。
简单区分:
-
左值
:有持久身份、可以取地址的表达式。例如变量、函数返回的引用、解引用指针等。
int a = 5;中的a是左值。 -
右值
:临时对象、字面量(除了字符串字面量)、返回非引用类型的函数调用等。例如
10,x + y的结果,std::string(“hello”)。它们即将消亡,无法取地址(临时对象有地址但很快失效)。
右值引用(
T&&
)
:就是绑定到右值的引用。它的出现,让我们能够识别出那些“即将消亡”的临时对象。
3.1 移动构造函数与移动赋值运算符
传统拷贝是“深拷贝”:为新对象分配一块新内存,然后把旧对象的数据逐个复制过来。对于持有大量资源的对象(如
std::vector
,
std::string
),这很昂贵。
移动语义的思想是:既然那个旧对象(右值)马上就要被销毁了,我们何不“偷”走它的资源(比如内部指针),直接交给新对象呢?这样新对象就免去了分配和复制的大量开销,而旧对象被“掏空”后处于有效但未定义的状态(通常可安全析构)。
一个简单的自定义类示例:
class Buffer {
private:
char* data_;
size_t size_;
public:
// 传统拷贝构造函数(深拷贝)
Buffer(const Buffer& other) : size_(other.size_) {
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copied buffer, size = " << size_ << "\n";
}
// 移动构造函数(C++11新增)
Buffer(Buffer&& other) noexcept // noexcept 很重要,标准库容器会利用它优化
: data_(other.data_), size_(other.size_) { // “窃取”资源
other.data_ = nullptr; // 将源对象置于有效但空的状态
other.size_ = 0;
std::cout << "Moved buffer, size = " << size_ << "\n";
}
// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放当前资源
data_ = other.data_; // 窃取资源
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
std::cout << "Move-assigned buffer\n";
}
return *this;
}
~Buffer() { delete[] data_; }
// ... 其他成员函数
};
void testMoveSemantics() {
Buffer buf1(100); // 假设有构造函数分配100字节
Buffer buf2 = std::move(buf1); // 调用移动构造函数,高效!
// 此时 buf1 的 data_ 为 nullptr,不能再使用其内容
Buffer buf3(200);
buf3 = std::move(buf2); // 调用移动赋值运算符
}
std::move
本身并不移动任何东西,它只是一个简单的类型转换工具,将其参数无条件转换为右值引用。它告诉编译器:“这个对象我愿意被移动,请优先调用移动语义的函数”。真正的移动操作发生在移动构造函数或移动赋值运算符中。
为什么移动构造函数要加
noexcept
?
标准库容器(如
std::vector
)在重新分配内存(
realloc
)时,为了提供强异常安全保证,会判断元素的移动构造函数是否
noexcept
。如果是,它就用移动(更快);如果不是,它就用拷贝(更安全)。为你的移动操作标记
noexcept
能带来显著的性能提升。
3.2 移动语义如何提升实际代码性能
最直接的受益者是标准库容器和字符串。当你向容器中插入一个临时对象,或者从函数返回一个局部容器时,移动语义会悄无声息地发挥作用,避免不必要的深拷贝。
std::vector<std::string> createAndProcess() {
std::vector<std::string> vec;
vec.reserve(10);
for (int i = 0; i < 10; ++i) {
std::string tempStr = "String_" + std::to_string(i);
// 在C++98中,这里会发生拷贝,将tempStr的内容复制到vec中。
// 在C++11中,因为tempStr是左值,理论上也会拷贝。但我们可以优化:
vec.push_back(std::move(tempStr)); // 明确移动,避免拷贝。
// 或者更常见的是,直接插入右值:
vec.push_back("String_" + std::to_string(i)); // 这个临时字符串是右值,会自动触发移动构造。
}
// 返回局部变量vec。在C++98中,这里需要拷贝(或者依赖NRVO编译器优化)。
// 在C++11中,编译器会优先尝试移动构造返回值。
return vec; // 很可能发生移动,高效。
}
int main() {
auto myVec = createAndProcess(); // 高效的移动构造或RVO
// ...
}
实操心得:
在编写自己的资源管理类(如管理文件句柄、网络连接、GPU内存)时,务必实现移动构造函数和移动赋值运算符。这不仅能让你自己的类更高效,也能让它在标准库容器中表现更好。同时,对于函数参数,如果函数需要接管参数的所有权(即“沉没”参数),应该按值传递(利用移动)或者使用右值引用参数
(T&&)
,而不是常量左值引用
(const T&)
。
4. 匿名函数的艺术:Lambda表达式与函数对象
在C++11之前,如果我们想传递一段可调用的逻辑,要么使用函数指针(功能受限),要么定义一个完整的函数对象(Functor,即重载了
operator()
的类),这通常很繁琐。
lambda
表达式的出现,让我们能够就地定义匿名函数,极大地提升了代码的简洁性和表现力,尤其是在算法库(
<algorithm>
)的使用中。
4.1 Lambda表达式的基本语法与捕获
一个lambda表达式的基本形式是:
[capture-list] (parameters) -> return-type { body }
其中,
-> return-type
和
parameters
在某些情况下可以省略。
核心在于捕获列表
[capture-list]
:它决定了lambda体内部如何访问外部作用域的变量。
-
[]:不捕获任何外部变量。 -
[=]:以值的方式捕获所有外部变量(在lambda创建时拷贝)。 -
[&]:以引用的方式捕获所有外部变量。 -
[var]:以值的方式捕获特定变量var。 -
[&var]:以引用的方式捕获特定变量var。 -
[=, &var]:默认以值捕获,但变量var以引用捕获。 -
[&, var]:默认以引用捕获,但变量var以值捕获。 -
[this]:捕获当前类对象的this指针,从而可以访问成员变量和函数。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
void lambdaDemo() {
int base = 100;
std::vector<int> nums = {1, 2, 3, 4, 5};
// 1. 简单lambda,不捕获
auto print = [] (int x) { std::cout << x << " "; };
std::for_each(nums.begin(), nums.end(), print);
std::cout << "\n";
// 2. 以值捕获外部变量
auto addBaseByValue = [base] (int x) { return x + base; };
std::transform(nums.begin(), nums.end(), nums.begin(), addBaseByValue);
// nums 现在是 {101, 102, 103, 104, 105}
// 注意:base的拷贝在lambda创建时发生,后续修改base不影响lambda内的值。
// 3. 以引用捕获外部变量
int sum = 0;
std::for_each(nums.begin(), nums.end(), [&sum] (int x) { sum += x; });
std::cout << "Sum = " << sum << "\n"; // 输出 515
// 直接操作外部sum,无需返回值。
// 4. 通用lambda (C++14),使用auto参数
auto genericAdd = [] (auto a, auto b) { return a + b; };
std::cout << genericAdd(1, 2) << ", " << genericAdd(1.5, 2.3) << "\n";
// 5. mutable lambda:允许修改以值方式捕获的变量
int counter = 0;
auto incrementer = [counter] () mutable { // 没有mutable,不能修改counter的拷贝
++counter; // 修改的是lambda内部捕获的副本
std::cout << "Internal counter: " << counter << "\n";
};
incrementer(); // 输出 1
incrementer(); // 输出 2
std::cout << "External counter: " << counter << "\n"; // 输出 0,外部变量未变
}
4.2 Lambda的本质与
std::function
Lambda表达式在编译器看来,是一个匿名类的对象,这个类重载了
operator()
。所以,每个lambda表达式都有自己独特的、编译器生成的类型。这意味着你不能直接用两个不同的lambda表达式给同一个具体的lambda类型变量赋值,但你可以用
auto
来接收。
当我们需要存储或传递一个可调用对象,并且其类型可能多变(普通函数、lambda、函数对象)时,就需要
std::function
。它是一个通用的、类型擦除的可调用对象包装器。
#include <functional>
#include <iostream>
void useStdFunction() {
// 存储一个无参无返回值的lambda
std::function<void()> task = []() { std::cout << "Task done.\n"; };
task();
// 存储一个接收int返回bool的lambda
std::function<bool(int)> isEven = [](int x) { return x % 2 == 0; };
std::cout << isEven(4) << "\n"; // 输出 1 (true)
// 存储一个通过捕获列表持有状态的lambda
int threshold = 50;
std::function<bool(int)> isAboveThreshold = [threshold](int x) { return x > threshold; };
// 即使threshold离开了作用域,isAboveThreshold内部仍保存着其拷贝的值。
// std::function 也可以绑定普通函数、成员函数等。
}
性能提示:
std::function
由于类型擦除和动态分配,会带来一定的开销(通常很小)。在性能极度敏感的循环内部,直接使用具体类型的lambda对象(用
auto
)或函数指针可能更快。但在需要灵活性(如回调函数列表、事件系统)的场合,
std::function
是不可或缺的工具。
一个结合算法库的实战例子:
// 传统方式:需要先定义一个函数或函数对象
bool oldWayCompare(int a, int b) { return a > b; }
std::sort(vec.begin(), vec.end(), oldWayCompare);
// C++11 lambda方式:就地定义,意图更清晰
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });
// 更复杂的场景:根据对象的某个成员排序
struct Person { std::string name; int age; };
std::vector<Person> people = { /* ... */ };
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) { return a.age < b.age; });
Lambda让代码的意图和实现紧贴在一起,极大地提高了可读性和编写效率。
5. 类型推导的进化:
auto
与
decltype
auto
关键字在C++11中被赋予了新的含义:自动类型推导。它让编译器根据初始化表达式来推导变量的类型。这并非动态类型,推导发生在编译期,类型一旦确定就不再改变。
5.1
auto
的使用场景与限制
主要优势:
-
简化冗长类型名
:特别是迭代器和模板类型。
std::vector<std::map<std::string, std::list<int>>> complexContainer; // 不用 auto std::vector<std::map<std::string, std::list<int>>>::iterator it = complexContainer.begin(); // 用 auto auto it = complexContainer.begin(); // 清晰简洁 -
避免类型截断
:确保使用正确的类型。
std::vector<int> vec; // 错误:size() 返回 size_t (无符号),与 int 可能不同,且比较有符号/无符号警告 for (int i = 0; i < vec.size(); ++i) { /* ... */ } // 正确:使用 auto for (auto i = 0; i < vec.size(); ++i) { /* ... */ } // i 被推导为 int,仍有警告 // 最佳:直接使用 size_t 或 auto for (size_t i = 0; i < vec.size(); ++i) { /* ... */ } for (auto i = vec.size(); i-- > 0; ) { /* ... */ } // 另一种倒序循环 -
配合Lambda
:Lambda类型是唯一的,只能用
auto或std::function存储。auto lambda = [](int x) { return x * x; };
注意事项与限制:
-
auto必须要有初始化表达式。 -
auto会忽略顶层const和引用。如果需要,需手动加上。const int ci = 10; auto a = ci; // a 是 int (const被忽略) const auto b = ci; // b 是 const int int& ri = i; auto c = ri; // c 是 int (引用被忽略) auto& d = ri; // d 是 int& -
在函数参数、返回值(C++14起支持
auto返回类型)和类成员变量(C++17起支持)中,auto的使用有特定规则和场景。
5.2
decltype
:获取表达式的声明类型
decltype
用于查询表达式的类型,它返回的是该表达式的“声明类型”,包括顶层
const
和引用。
常见用途:
-
声明与某个表达式类型相同的变量
。
int i = 0; const int& cr = i; decltype(cr) y = i; // y 的类型是 const int& -
在泛型编程中,依赖模板参数推导返回类型
(常与
auto和尾置返回类型结合,在C++11中用于推导函数模板返回类型)。template<typename T, typename U> auto add(T t, U u) -> decltype(t + u) { // 尾置返回类型,decltype推导t+u的类型 return t + u; } -
decltype(auto)(C++14) :它用decltype的规则来推导auto,主要用于完美转发函数返回值。template<typename Func, typename... Args> decltype(auto) call(Func f, Args&&... args) { // 完美保持返回值的类型(包括引用) return f(std::forward<Args>(args)...); }
auto
和
decltype
的对比:
-
auto根据初始化器推导类型,会忽略顶层const和引用。 -
decltype根据给定的表达式推导类型,保留顶层const和引用。 -
decltype((variable))(注意双括号)会产生引用类型,而decltype(variable)不会(除非variable本身就是引用)。这是一个需要小心的细节。
6. 统一初始化与列表初始化
C++11引入了花括号
{}
初始化语法,旨在提供一种统一、安全的初始化方式。
6.1 花括号初始化的优势
-
统一语法
:可以用于初始化几乎所有类型。
// 内置类型 int x{5}; int y = {5}; // 等号可选 // 数组 int arr[]{1, 2, 3}; // 结构体/类(如果所有成员都是public) struct Point { int x; int y; }; Point p{10, 20}; // 标准库容器 std::vector<int> vec{1, 2, 3, 4, 5}; std::map<std::string, int> dict{{"apple", 1}, {"banana", 2}}; -
防止窄化转换
:花括号初始化禁止可能导致数据丢失的隐式转换。
int a = 3.14; // 警告,但允许,a=3 int b{3.14}; // 错误!从 double 到 int 需要窄化转换 char c{999}; // 错误!999超出char范围(假设char是8位) -
避免“最令人烦恼的解析”
:
class Timer { /* ... */ }; // 这行代码的本意是定义一个Timer对象,但编译器会解析为一个返回Timer的函数声明! Timer t1(); // 函数声明,名为t1,无参,返回Timer Timer t2{}; // 正确!使用花括号初始化,定义了一个Timer对象t2。
6.2
std::initializer_list
与构造函数重载
当使用花括号初始化一个类对象时,编译器会优先匹配接收
std::initializer_list
作为参数的构造函数。这有时会导致令人意外的行为。
#include <iostream>
#include <vector>
class Widget {
public:
Widget(int a, int b) { std::cout << "Called Widget(int, int)\n"; }
Widget(std::initializer_list<int> list) {
std::cout << "Called Widget(std::initializer_list<int>), size = " << list.size() << "\n";
}
};
int main() {
Widget w1(10, 20); // 调用 Widget(int, int)
Widget w2{10, 20}; // 调用 Widget(std::initializer_list<int>)!可能不是你想要的结果。
Widget w3({10, 20}); // 明确传递initializer_list
Widget w4{10}; // 调用 Widget(std::initializer_list<int>),size=1
// 如果想用花括号调用第一个构造函数,需要额外注意
Widget w5({10, 20}); // 或者像w3一样
}
经验之谈:
在设计自己的类时,如果提供了
std::initializer_list
构造函数,需要意识到它会被花括号初始化“强烈偏好”。这可能会影响其他构造函数的调用。在标准库中,
std::vector
就是一个典型例子:
std::vector<int> v(5, 10);
创建5个元素,每个都是10。而
std::vector<int> v{5, 10};
创建2个元素:5和10。这是一个容易混淆的地方,使用时需格外小心。
7. 其他不容忽视的实用特性
除了上述重量级特性,C++11还包含许多提升开发效率和代码质量的小特性。
7.1 范围
for
循环
提供了一种遍历容器或序列的简洁语法。
std::vector<int> vec = {1, 2, 3, 4, 5};
// 传统迭代器方式
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// 范围for循环 (C++11)
for (int value : vec) { // 按值访问,会拷贝
std::cout << value << " ";
}
for (const int& value : vec) { // 按常量引用访问,无拷贝,推荐
std::cout << value << " ";
}
for (auto& value : vec) { // 使用auto,按引用访问,可修改元素
value *= 2;
}
它的原理是编译器将其转换为基于迭代器的普通循环。要求遍历的对象有
begin()
和
end()
成员函数,或者可以通过ADL找到对应的
begin
和
end
自由函数。
7.2
nullptr
字面量
用于替代宏
NULL
(通常是0)。
nullptr
具有明确的指针类型
std::nullptr_t
,可以隐式转换为任何指针类型,但不会转换为整数类型。这解决了函数重载时
NULL
可能被当作整数0导致的二义性问题。
void foo(int);
void foo(char*);
foo(NULL); // 调用哪个?可能调用foo(int),不符合直觉。
foo(nullptr); // 明确调用foo(char*)
7.3 强类型枚举
enum class
传统C++枚举存在枚举常量污染外层作用域、隐式转换为整数等问题。
// 传统枚举
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // 错误!Red和Green重定义
int i = Red; // 隐式转换,OK
// 强类型枚举 (C++11)
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green }; // OK,作用域独立
Color c = Color::Red;
// int i = Color::Red; // 错误!不能隐式转换
int j = static_cast<int>(Color::Red); // 需要显式转换
enum class
的枚举常量位于枚举类的作用域内,不会污染外部,且不能隐式转换为整数,更加安全。
7.4 静态断言
static_assert
编译期断言。如果断言条件为假,编译器会报错并显示指定的错误信息。常用于检查模板参数、平台特性等。
static_assert(sizeof(int) == 4, "int must be 4 bytes on this platform.");
template<typename T>
class Widget {
static_assert(std::is_arithmetic<T>::value, "T must be arithmetic type.");
// ...
};
7.5
=default
与
=delete
控制默认函数的生成。
-
=default:显式要求编译器生成默认版本的函数(如默认构造函数、析构函数、拷贝控制成员)。即使你提供了其他构造函数,也可以用这个来保留默认版本。 -
=delete:禁止编译器生成某个函数,或禁止某个函数被调用。常用于禁止拷贝(实现不可拷贝的类),或删除某些不希望的函数重载。
class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 禁止拷贝
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 允许移动
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
void foo(int);
void foo(double) = delete; // 禁止使用double参数调用foo
// foo(3.14); // 错误!该重载已被删除
C++11的这些特性,从资源管理、性能优化到语法便利性,全方位地革新了C++的编程体验。掌握它们,意味着你从“古典C++”程序员,进化成了“现代C++”的实践者。这不仅仅是语法的更新,更是一种编程思维方式的转变:更多地依赖语言机制来保证安全与效率,而非完全依赖程序员的自觉。在实际项目中,尤其是新启动的项目中,积极、合理地运用这些特性,能显著提升代码的质量和可维护性。当然,迁移旧代码时需要谨慎评估,但为新代码选择现代C++,无疑是更明智的选择。
更多推荐
所有评论(0)