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_ptrshared_ptrweak_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_ptrunique_ptr 更加安全和高效。

2.1 特点
  • 独占所有权unique_ptr 独占所管理的资源,不能被拷贝,但可以通过移动语义(move semantics)转移所有权。
  • 轻量高效:相比于 shared_ptrunique_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_ptrunique_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 的使用,使得我们可以将函数接收到的参数以最优的方式传递给其他函数。

完美转发依赖于以下几个概念:

  1. 右值引用(Rvalue Reference)
    • 右值引用通过 T&& 语法定义,用来接受右值对象(临时对象),并允许开发者高效地使用和转移这些对象的资源。
    • 对于模板参数中的 T&&,它既能接受右值引用,也能接受左值引用,这是完美转发的基础。
  2. 万能引用(Universal Reference)
    • T&& 作为模板参数出现时,它被称为万能引用,可以同时接受左值引用和右值引用。编译器根据传递的参数类型推导 T 的类型,从而决定是左值引用还是右值引用。
  3. 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. 完美转发的注意事项

  1. 引用折叠
    • 在完美转发中,C++11 引入了引用折叠规则,帮助解决模板参数推导过程中多重引用的问题。具体规则如下:
      • T& &T& &&T&& & 都会折叠为 T&(左值引用)。
      • 只有 T&& && 会折叠为 T&&(右值引用)。
  2. 性能问题
    • 完美转发的设计目的是优化性能,减少不必要的拷贝和对象构造。然而,完美转发可能引入额外的复杂性,尤其是在处理移动语义时。
  3. std::forwardstd::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::threadstd::mutexstd::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++ 在现代开发中更加高效和实用。