C++11\14特性
C++11特性
1. 自动类型推导 (auto
)
C++11 引入了 auto
关键字,允许编译器根据表达式的类型自动推导变量的类型。它可以简化代码,特别是在处理复杂的类型时。
auto x = 5; // 编译器推导 x 为 int
auto y = 3.14; // 编译器推导 y 为 double
auto ptr = new int(10); // 推导为 int*
推导规则看CppLearning.pdf。
2. 范围 for 循环(Range-based for loop)
C++11 引入了范围 for 循环,简化了对容器或数组元素的迭代操作。
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& v : vec) {
std::cout << v << std::endl;
}
3. 智能指针 (shared_ptr
, unique_ptr
, weak_ptr
)
智能指针本质是一个封装了一个原始C++指针的类模板,为了确保动态内存的安全性而产生的。 C++11 提供了新的智能指针,用于更好地管理动态内存:
std::unique_ptr
:独占所有权。std::shared_ptr
:共享所有权,使用引用计数来管理对象的生命周期。std::weak_ptr
:用于打破shared_ptr
之间的循环引用。
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::unique_ptr<int> p2 = std::make_unique<int>(20);
在 C++ 的早期,auto_ptr
是标准库中唯一提供的智能指针,用于自动管理动态分配的内存。然而,auto_ptr
有一些设计缺陷,尤其是所有权的转移行为不直观、不安全,因此在 C++11 引入了新的智能指针类型,如 unique_ptr
、shared_ptr
和 weak_ptr
,并逐渐淘汰了 auto_ptr
。
接下来,我们详细讨论从 auto_ptr
到 C++11 的智能指针演变。
1. auto_ptr
(C++98)
auto_ptr
是 C++98 标准中的智能指针,用来管理动态分配的对象。它的主要目的是在对象离开作用域时自动释放动态分配的内存,避免内存泄漏。
1.1 基本用法:
#include <memory>
int main() {
std::auto_ptr<int> p1(new int(10)); // 动态分配内存并绑定到 auto_ptr
std::auto_ptr<int> p2 = p1; // 转移所有权,p1 不再持有对象
std::cout << *p2 << std::endl; // 输出 10
// p1 现在是空指针,持有 nullptr
}
1.2 auto_ptr
的问题:
- 所有权转移:
auto_ptr
在拷贝和赋值时会转移资源的所有权。例如,auto_ptr p2 = p1;
会将资源从p1
转移到p2
,而p1
被置为空。这种所有权转移是不直观的,容易导致悬空指针或双重释放的问题。 - 不能用于容器:由于
auto_ptr
拷贝时会改变源对象的状态,它无法安全地用于 STL 容器(如std::vector
)。容器通常会在元素的插入和删除过程中进行拷贝操作,使用auto_ptr
会导致意外的行为。 - 已经废弃:由于这些问题,
auto_ptr
在 C++11 被标记为废弃,并在 C++17 中完全移除。
2. unique_ptr
(C++11)
unique_ptr
是 C++11 引入的智能指针,旨在替代 auto_ptr
。它也是一种独占所有权的智能指针,但相比 auto_ptr
,unique_ptr
更加安全和高效。
2.1 特点:
- 独占所有权:
unique_ptr
独占所管理的资源,不能被拷贝,但可以通过移动语义(move semantics)转移所有权。 - 轻量高效:相比于
shared_ptr
,unique_ptr
没有额外的引用计数开销。 - 自定义删除器:
unique_ptr
支持自定义删除器,可以管理复杂资源(如文件句柄、网络连接等)。
2.2 基本用法:
#include <memory>
int main() {
std::unique_ptr<int> p1(new int(20)); // 创建 unique_ptr,管理动态分配的内存
std::unique_ptr<int> p2 = std::move(p1); // 使用 std::move 转移所有权
std::cout << *p2 << std::endl; // 输出 20
// p1 现在是空的,不能再使用它
}
2.3 与 auto_ptr
的区别:
-
不能拷贝:与
auto_ptr
不同,unique_ptr
不能通过复制构造或赋值操作来拷贝。必须使用std::move
将所有权从一个unique_ptr
转移到另一个。这避免了auto_ptr
的潜在所有权混乱问题。 -
自定义删除器:
unique_ptr
可以接受自定义删除器来释放资源,适用于更复杂的资源管理场景。
std::unique_ptr<FILE, decltype(&fclose)> filePtr(fopen("test.txt", "w"), fclose);
2.4 应用场景:
- 用于确保资源在离开作用域时被正确释放,避免内存泄漏。
unique_ptr
适用于独占资源的场景,如管理文件、网络连接或设备句柄等。
3. shared_ptr
(C++11)
shared_ptr
是一种支持共享所有权的智能指针,多个 shared_ptr
可以共同管理同一个对象。对象的生命周期会延长,直到最后一个 shared_ptr
释放它。
3.1 特点:
- 引用计数:
shared_ptr
内部维护一个引用计数,当引用计数为 0 时,所管理的对象会被自动销毁。 - 拷贝安全:
shared_ptr
可以被拷贝,拷贝时引用计数增加。 - 线程安全:C++11 标准中,
shared_ptr
的引用计数是线程安全的。
3.2 基本用法:
#include <memory>
int main() {
std::shared_ptr<int> p1 = std::make_shared<int>(30); // 推荐的创建方式
std::shared_ptr<int> p2 = p1; // 拷贝 p1,引用计数增加
std::cout << *p1 << std::endl; // 输出 30
std::cout << "Use count: " << p1.use_count() << std::endl; // 引用计数输出 2
}
3.3 shared_ptr
与 unique_ptr
的区别:
-
共享所有权:
shared_ptr
的设计目的是让多个智能指针共享同一个对象。当不再需要对象时,引用计数归零,自动销毁对象。相比之下,unique_ptr
独占对象,不能共享。 -
性能开销:由于
shared_ptr
需要维护引用计数,它比unique_ptr
有更大的内存和性能开销。如果没有必要的共享行为,应该优先使用unique_ptr
。
3.4 自定义删除器:
与 unique_ptr
类似,shared_ptr
也支持自定义删除器:
std::shared_ptr<FILE> filePtr(fopen("test.txt", "w"), fclose);
3.5 应用场景:
- 当多个对象需要共享同一个资源时,可以使用
shared_ptr
。例如,一个对象在多个模块中被使用,所有模块都应该控制其生命周期。 - 当对象的强引用计数降为0时,shared_ptr会释放该对象的内存,但控制块不会立即释放,因为仍然有weak_ptr可能在观察它。控制块会在弱引用计数也降为0时释放,这样可以确保weak_ptr可以安全地判断对象是否有效。
4. weak_ptr
(C++11)
weak_ptr
是一种非拥有(non-owning)的智能指针,它与 shared_ptr
配合使用,用于解决共享对象的循环引用问题。weak_ptr
不影响引用计数。
4.1 特点:
- 弱引用:
weak_ptr
只是指向对象,并不影响对象的生命周期,不能直接解引用。 - 循环引用解决:在使用
shared_ptr
时,如果两个对象相互持有对方的shared_ptr
,会导致循环引用,无法释放内存。使用weak_ptr
可以打破这种循环。
4.2 基本用法:
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(40);
std::weak_ptr<int> wp = sp; // 创建 weak_ptr,不增加引用计数
if (auto temp = wp.lock()) { // 使用 lock() 生成 shared_ptr
std::cout << *temp << std::endl; // 输出 40
} else {
std::cout << "Resource no longer available." << std::endl;
}
}
4.3 应用场景:
weak_ptr
通常用于避免循环引用。例如在双向链表、图结构或依赖关系复杂的系统中,shared_ptr
的循环引用问题可以通过weak_ptr
解决。
智能指针中的引用计数是线程安全的,但是智能指针所指向的对象的线程安全问题,智能指针没有做任何保障线程不安全。
4. Lambda 表达式
C++11 引入了 lambda 表达式,使得可以在代码中编写匿名函数,特别适合于函数作为参数的场景,如回调、事件处理等。
auto add = [](int a, int b) { return a + b; };
std::cout << add(1, 2) << std::endl; // 输出 3
捕获外部变量:
int x = 5;
auto func = [x](int a) { return a + x; };
std::cout << func(3) << std::endl; // 输出 8
匿名函数本质上是一个对象,在其定义的过程中会创建出一个栈对象,内部通过重载()符号实现函数调用的外表。
优点:使用匿名函数,可以免去函数的声明和定义。这样匿名函数仅在调用函数的时候才会创建函数对象,而调用结束后立即释放,所以匿名函数比非匿名函数更节省空间。
5. 移动语义与右值引用 (rvalue references
)
C++11 引入了移动语义和右值引用,允许开发者通过转移资源所有权来避免不必要的拷贝,极大地提升了性能,尤其是对于临时对象的处理。
- 右值引用通过
T&&
定义,可以捕获右值,并通过std::move()
转移资源。
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 的资源被转移给 v2,避免了拷贝
完美转发(Perfect Forwarding)是 C++11 引入的一个特性,允许函数将其接收到的参数(包括其值类型、引用类型和右值或左值属性)完美地转发给另一个函数。这种转发可以保留参数的所有特性,从而避免不必要的拷贝或引用失效问题。
完美转发的核心是右值引用(rvalue references)和模板参数推导,特别是结合了 std::forward
的使用,使得我们可以将函数接收到的参数以最优的方式传递给其他函数。
完美转发依赖于以下几个概念:
- 右值引用(Rvalue Reference):
- 右值引用通过
T&&
语法定义,用来接受右值对象(临时对象),并允许开发者高效地使用和转移这些对象的资源。 - 对于模板参数中的
T&&
,它既能接受右值引用,也能接受左值引用,这是完美转发的基础。
- 右值引用通过
- 万能引用(Universal Reference):
- 当
T&&
作为模板参数出现时,它被称为万能引用,可以同时接受左值引用和右值引用。编译器根据传递的参数类型推导T
的类型,从而决定是左值引用还是右值引用。
- 当
std::forward
:std::forward
是一个帮助函数,用来在函数内部将参数以原始的类型(左值或右值)转发给另一个函数。它根据模板参数的类型决定是否执行移动语义(move semantics)。
#include <iostream>
#include <utility> // for std::forward
void process(int& x) {
std::cout << "Lvalue reference: " << x << std::endl;
}
void process(int&& x) {
std::cout << "Rvalue reference: " << x << std::endl;
}
template <typename T>
void forwarder(T&& arg) {
process(std::forward<T>(arg)); // 保持 arg 的值类别
}
int main() {
int a = 42;
forwarder(a); // 转发左值,调用 process(int&)
forwarder(100); // 转发右值,调用 process(int&&)
}
Lvalue reference: 42
Rvalue reference: 100
如果没有使用 std::forward
,完美转发就无法正常工作。例如:
template <typename T>
void forwarder(T&& arg) {
process(arg); // 没有使用 std::forward
}
int main() {
int a = 42;
forwarder(a); // 错误:a 是左值,传递给 process 时应为左值引用
forwarder(100); // 错误:100 是右值,但被转发为左值
}
在这段代码中,process(arg)
传递的是 arg
,无论 arg
是左值还是右值,都会被视为左值传递。结果是,即使你传递的是右值,它在传递过程中也会丧失右值属性。
std::forward
根据模板参数的类型来决定是否转发为右值:
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
- 如果
T
是左值引用类型,std::forward<T>(t)
会返回t
的左值引用。 - 如果
T
是右值引用类型,std::forward<T>(t)
会将t
转换为右值引用。
6. 完美转发的注意事项
- 引用折叠:
- 在完美转发中,C++11 引入了引用折叠规则,帮助解决模板参数推导过程中多重引用的问题。具体规则如下:
T& &
、T& &&
、T&& &
都会折叠为T&
(左值引用)。- 只有
T&& &&
会折叠为T&&
(右值引用)。
- 在完美转发中,C++11 引入了引用折叠规则,帮助解决模板参数推导过程中多重引用的问题。具体规则如下:
- 性能问题:
- 完美转发的设计目的是优化性能,减少不必要的拷贝和对象构造。然而,完美转发可能引入额外的复杂性,尤其是在处理移动语义时。
std::forward
与std::move
的区别:std::move
总是将其参数转换为右值引用,而std::forward
会根据参数类型决定是否保留其值类别。因此,std::forward
更适合用于模板参数转发。
6. nullptr
C++11 引入了 nullptr
,它取代了传统的 NULL
,成为一个类型安全的空指针常量。
int* ptr = nullptr; // 代替 NULL
7. constexpr
C++11 引入了 constexpr
,它允许在编译时计算常量表达式,提高编译期的优化。
constexpr int square(int x) { return x * x; }
constexpr int result = square(5); // 在编译时计算
8. 委托构造函数(Delegating constructors)
C++11 允许一个构造函数调用另一个构造函数,简化了构造函数的实现。
class MyClass {
public:
MyClass(int a) : MyClass(a, 0) {} // 调用另一个构造函数
MyClass(int a, int b) : x(a), y(b) {}
private:
int x, y;
};
9. 显式转换运算符(Explicit conversion operators)
C++11 允许将类型转换运算符标记为 explicit
,避免隐式转换带来的错误。
class MyClass {
public:
explicit operator bool() const { return true; }
};
MyClass obj;
if (obj) { // 只有通过显示的转换,才能将 obj 转换为 bool
// ...
}
10. std::thread
和多线程支持
C++11 引入了标准库的多线程支持,包括 std::thread
、std::mutex
、std::lock_guard
等,可以直接使用标准库进行线程创建和同步。
#include <thread>
void threadFunc() {
std::cout << "Hello from thread" << std::endl;
}
int main() {
std::thread t(threadFunc); // 创建一个线程
t.join(); // 等待线程结束
}
11. 变参模板(Variadic templates)
C++11 支持变参模板,允许模板接受任意数量的模板参数。
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // C++17 fold expression
}
print(1, 2.5, "Hello"); // 输出:1 2.5 Hello
12. std::tuple
C++11 引入了 std::tuple
,它是一个固定大小的异构集合,允许存储不同类型的对象。
#include <tuple>
std::tuple<int, double, std::string> t(1, 2.5, "Hello");
std::cout << std::get<0>(t) << std::endl; // 输出 1
13. enum class
C++11 引入了强类型枚举 enum class
,它比传统的 enum
更安全,因为它不会隐式转换为整型。
enum class Color { Red, Green, Blue };
Color c = Color::Red;
14. std::array
C++11 提供了 std::array
,它是 C++ STL 的静态数组,具有固定大小,并且提供了与 STL 容器相同的接口。
#include <array>
std::array<int, 3> arr = {1, 2, 3};
std::cout << arr[1] << std::endl; // 输出 2
15. 初始化列表(Initializer Lists)
C++11 引入了初始化列表语法,允许通过大括号 {}
直接初始化对象。
std::vector<int> vec = {1, 2, 3, 4}; // 使用初始化列表
16. decltype
C++11 提供了 decltype
,用于推导表达式的类型,类似于 auto
,但 decltype
可以用于复杂表达式的类型推导。
int x = 5;
decltype(x) y = 10; // y 的类型与 x 相同,推导为 int
17. 静态断言(static_assert
)
C++11 引入了 static_assert
,用于在编译时进行条件检查,如果条件为假,编译器会抛出错误。
static_assert(sizeof(int) == 4, "int size is not 4 bytes");
18. 后置返回类型(Trailing Return Types)
C++11 支持在函数定义中使用后置返回类型,特别适用于复杂的返回类型或函数模板。
auto add(int a, int b) -> int {
return a + b;
}
19. noexcept
C++11 引入了 noexcept
,用于标记函数不抛出异常,有助于编译器优化。
void func() noexcept {
// 函数不抛出异常
}
20. 右值引用与移动构造函数
C++11 提供了右值引用和移动语义,用于避免不必要的对象拷贝,提高性能。移动构造函数和移动赋值运算符可以通过转移资源来避免深拷贝。
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 实现移动构造函数
}
MyClass& operator=(MyClass&& other) noexcept {
// 实现移动赋值运算符
}
};
总结
C++11 是 C++ 语言的一次重大更新,带来了许多极大增强语言能力的特性。这些特性不仅简化了编写 C++ 代码的过程,还提供了强大的工具用于提高性能和安全性,使得 C++ 在现代开发中更加高效和实用。