尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++11核心特性解析:智能指针、移动语义与多线程编程实战

C++11核心特性解析:智能指针、移动语义与多线程编程实战 1. 项目概述为什么C11是C的“重生”如果你是一位从C98/03时代走过来的老程序员第一次接触C11时那种感觉就像给一台老爷车换上了全新的涡轮增压发动机和智能驾驶系统。它还是那门你熟悉的C但内核和体验已经天差地别。C11不是一次简单的版本迭代而是一次彻底的现代化革新它让C这门“古老”的语言重新焕发活力能够优雅、高效地应对现代软件开发中并发、泛型、资源管理等复杂挑战。我至今还记得第一次用auto关键字省去一长串迭代器类型声明时的畅快以及用lambda表达式将一小段逻辑内联实现时的便捷。C11引入的数十项新特性从根本上改变了我们编写C代码的思维方式和习惯。它不仅仅是语法糖更是一套全新的工具箱旨在让代码更安全、更高效、更简洁。无论是智能指针对内存管理的革命性简化还是右值引用和移动语义带来的性能飞跃亦或是多线程库为并发编程奠定的坚实基础每一项都直指C开发中的痛点。理解C11对于任何希望写出现代、健壮C代码的程序员来说已经不是“加分项”而是“必修课”。接下来我将结合自己多年的踩坑与实战经验为你系统性地拆解C11的核心新特性不仅告诉你“是什么”更重点剖析“为什么”以及“怎么用”。2. 核心特性深度解析与设计哲学C11的特性繁多但并非杂乱无章。其设计核心围绕几个关键目标展开提升类型安全、简化资源管理、增强泛型编程能力、支持现代并发模型以及改善开发体验。理解这些设计哲学能帮助我们更好地掌握和运用这些特性。2.1 类型推导让编译器为你打工在C98中编写模板代码或使用STL容器时类型声明往往冗长且容易出错。C11的auto和decltype关键字将程序员从繁琐的类型书写中解放出来。auto关键字它的核心作用是让编译器在编译期根据初始化表达式自动推导出变量的类型。这大大简化了代码尤其是面对复杂类型时。// C98 冗长的迭代器声明 std::vectorstd::mapstd::string, int::iterator it myVec.begin(); // C11 使用 auto auto it myVec.begin(); // 编译器推导 it 的类型 auto i 42; // int auto d 3.14; // double auto s hello; // const char*注意auto是一个占位符而非一种类型。使用auto声明的变量必须初始化因为编译器需要根据初始化器来推导类型。它遵循模板参数推导的规则会忽略顶层const和引用除非你显式声明为auto或const auto。decltype关键字如果说auto是根据初始化器推导类型那么decltype则是根据一个给定的表达式而非值来推导其类型。它返回该表达式的声明类型包括const和引用限定符。int i 0; const int cr i; decltype(cr) y i; // y 的类型是 const int decltype(i) j; // j 的类型是 int (可以不用初始化因为decltype只关心类型) // 常用于模板编程中声明与某个表达式类型相同的变量 templatetypename T, typename U auto add(T t, U u) - decltype(t u) { // 尾置返回类型推导返回值类型 return t u; }实战心得auto在90%的日常场景中都能完美工作特别是在循环和容器操作中。但对于函数返回类型或需要精确控制类型细节如保留引用和const时decltype是更强大的工具。两者结合使用如尾置返回类型能写出既简洁又类型安全的泛型代码。2.2 智能指针告别手动new/delete的梦魇内存管理是C程序员永恒的课题也是Bug的主要来源之一。C11引入的智能指针通过RAII资源获取即初始化机制将内存的生命周期与对象的作用域绑定实现了自动化的资源管理。std::unique_ptr独占所有权的智能指针。同一时刻一个unique_ptr独占一个资源。它不可复制只可移动Move。当unique_ptr离开作用域时它所管理的资源会被自动释放。这是替代原始指针进行对象生命期管理的首选。{ std::unique_ptrMyClass p1(new MyClass()); // 传统构造 auto p2 std::make_uniqueMyClass(); // C14引入更安全高效推荐 // p1 p2; // 错误不可复制 auto p3 std::move(p1); // 正确所有权转移现在p3管理资源p1为空 } // 作用域结束p3或p2管理的对象自动调用deletestd::shared_ptr共享所有权的智能指针。多个shared_ptr可以指向同一个对象并通过引用计数来管理资源。当最后一个shared_ptr被销毁时资源才会被释放。auto sp1 std::make_sharedMyClass(); // 引用计数为1 { auto sp2 sp1; // 复制引用计数1变为2 // sp1和sp2共享同一个对象 } // sp2析构引用计数-1变为1 // sp1析构引用计数变为0对象被销毁std::weak_ptr弱引用指针。它指向一个由shared_ptr管理的对象但不会增加其引用计数。主要用于解决shared_ptr的循环引用问题。class B; class A { public: std::shared_ptrB b_ptr; ~A() { std::cout A destroyed\n; } }; class B { public: // std::shared_ptrA a_ptr; // 如果用它会导致循环引用内存泄漏 std::weak_ptrA a_ptr; // 使用weak_ptr打破循环 ~B() { std::cout B destroyed\n; } }; auto a std::make_sharedA(); auto b std::make_sharedB(); a-b_ptr b; b-a_ptr a; // weak_ptr不会增加引用计数 // 离开作用域后a和b都能被正确销毁重要提示优先使用std::make_uniqueC14和std::make_shared。它们将内存分配和对象构造合为一步不仅代码更简洁而且更安全避免了因异常导致的内存泄漏和高效对于shared_ptr可以一次性分配引用计数和对象的内存。2.3 右值引用与移动语义榨干性能的最后一滴油这是C11中最重要也最难理解的概念之一但它带来的性能提升是革命性的。要理解它首先要明白左值lvalue和右值rvalue。左值可以取地址、有名字的表达式。例如变量、函数返回的引用。右值不能取地址、临时性的表达式。例如字面量、临时对象、函数返回的非引用类型。右值引用用表示例如T。它只能绑定到右值。其核心目的是延长临时对象的生命周期并允许“窃取”其资源。移动语义通过移动构造函数和移动赋值运算符实现。它们接受一个右值引用参数从中“窃取”资源如动态内存、文件句柄而不是进行深拷贝然后将源对象置于一个可安全析构的状态通常是空状态。class MyString { private: char* data; public: // 移动构造函数 MyString(MyString other) noexcept : data(other.data) { other.data nullptr; // 将源对象置空防止其析构时释放资源 } // 移动赋值运算符 MyString operator(MyString other) noexcept { if (this ! other) { delete[] data; // 释放已有资源 data other.data; other.data nullptr; } return *this; } // ... 拷贝构造、拷贝赋值、析构等 }; MyString func() { return MyString(Hello); } // 返回一个临时对象右值 MyString s1(World); MyString s2 s1; // 调用拷贝构造函数深拷贝 MyString s3 func(); // 调用移动构造函数资源窃取高效 s1 std::move(s2); // 使用std::move将左值s2转换为右值引用调用移动赋值运算符std::move它本身并不移动任何东西只是一个强制类型转换工具将一个左值无条件地转换为右值引用从而允许移动操作发生。完美转发通过std::forward实现用于在模板函数中将参数按照其原始的值类别左值或右值转发给另一个函数。这是实现泛型包装器如make_unique的关键。templatetypename T, typename... Args std::unique_ptrT make_unique(Args... args) { // 通用引用 return std::unique_ptrT(new T(std::forwardArgs(args)...)); // 完美转发 }实操心得移动语义的威力在容器操作中体现得淋漓尽致。例如std::vector::push_back现在有重载版本接受右值引用。当你向容器插入一个临时对象或使用std::move的对象时会触发移动构造避免不必要的拷贝极大提升了性能。编写类时如果管理了资源如动态内存务必考虑实现移动构造和移动赋值并将它们声明为noexcept这有助于标准库容器进行优化。3. 语言核心增强与现代化语法除了上述重磅特性C11还提供了大量语法糖和核心增强让代码编写更加现代和舒适。3.1 基于范围的for循环告别繁琐的迭代器以更简洁、更不易出错的方式遍历容器。std::vectorint vec {1, 2, 3, 4, 5}; // C98 for (std::vectorint::iterator it vec.begin(); it ! vec.end(); it) { std::cout *it std::endl; } // C11 for (int val : vec) { // 拷贝元素 std::cout val std::endl; } for (const int val : vec) { // 常引用避免拷贝 std::cout val std::endl; } for (auto val : vec) { // 使用auto通用且方便修改元素 val * 2; }它的原理是编译器将其转换为基于迭代器的传统循环。支持所有提供了begin()和end()成员函数或自由函数的容器包括原生数组。3.2 Lambda表达式匿名函数对象Lambda允许你在需要函数对象的地方内联地定义匿名函数极大地增强了STL算法的表达能力。// 基本语法[捕获列表] (参数列表) - 返回类型 { 函数体 } std::vectorint nums {5, 2, 8, 1, 9}; // 排序使用lambda代替传统的函数对象或函数指针 std::sort(nums.begin(), nums.end(), [](int a, int b) { return a b; }); // 查找找到第一个大于5的数 auto it std::find_if(nums.begin(), nums.end(), [](int n) { return n 5; }); int threshold 5; // 捕获外部变量按值捕获threshold auto count std::count_if(nums.begin(), nums.end(), [threshold](int n) { return n threshold; }); // 捕获外部变量按引用捕获threshold可修改外部变量 int sum 0; std::for_each(nums.begin(), nums.end(), [sum](int n) { sum n; });捕获列表详解[]不捕获任何外部变量。[]按值捕获所有外部变量默认不可修改除非声明mutable。[]按引用捕获所有外部变量。[var]按值捕获特定变量var。[var]按引用捕获特定变量var。[, var]默认按值捕获但var按引用捕获。[, var]默认按引用捕获但var按值捕获。注意默认情况下按值捕获的变量在lambda体内是const的不能修改。如果需要修改需要在参数列表后加上mutable关键字。但修改的只是其副本不影响外部变量。按引用捕获则需注意生命周期问题确保lambda被调用时所引用的变量仍然有效。3.3 初始化列表与统一初始化C11引入了std::initializer_list和用花括号{}进行的统一初始化语法解决了C98中多种初始化方式不一致的问题。// 统一初始化语法 int x{5}; // 直接初始化 int y {6}; // 拷贝初始化 std::vectorint v{1, 2, 3, 4, 5}; // 列表初始化容器 std::mapstd::string, int m{{apple, 1}, {banana, 2}}; // 防止窄化转换 int a 3.14; // C98/03允许a3丢失精度 // int b{3.14}; // C11错误窄化转换编译报错 // 类成员初始化 class Widget { int data{100}; // C11允许在类内对非静态成员进行初始化 std::vectorint vec{1,2,3}; public: Widget() default; Widget(int x) : data{x} {} // 构造函数初始化列表仍可使用 };统一初始化语法更清晰、更一致并且能避免“最令人烦恼的解析”问题例如Widget w();会被解析为函数声明而Widget w{};则是对象定义。在自定义类中可以通过提供接受std::initializer_list参数的构造函数来支持列表初始化。3.4 空指针常量nullptr告别模糊的NULL宏通常是0使用类型安全的nullptr。nullptr是std::nullptr_t类型的纯右值可以隐式转换为任何指针类型但不会转换为整型。void foo(int); void foo(char*); foo(NULL); // 可能调用foo(int)不符合预期 foo(nullptr); // 明确调用foo(char*)始终使用nullptr来表示空指针能使代码意图更清晰避免重载决议的歧义。3.5 强类型枚举enum class传统的C风格枚举存在枚举常量污染外层作用域、隐式转换为整型等问题。enum class或enum struct解决了这些问题。// 传统枚举 enum Color { Red, Green, Blue }; enum TrafficLight { Red, Yellow, Green }; // 错误Red, Green 重定义 // 强类型枚举 enum class Color { Red, Green, Blue }; enum class TrafficLight { Red, Yellow, Green }; // 正确作用域独立 Color c Color::Red; // 必须使用作用域运算符 // int i c; // 错误不能隐式转换为整型 int i static_castint(c); // 需要显式转换 // 可以指定底层类型 enum class Byte : unsigned char { B1 0x01, B2 0x02 };enum class的枚举常量位于枚举类型的作用域内不会泄露到外层且不能隐式转换大大增强了类型安全。4. 多线程支持告别平台相关的并发代码C11最大的里程碑之一是在语言标准库中正式引入了多线程支持使得编写可移植的并发程序成为可能。thread,mutex,condition_variable,future等头文件提供了完整的线程管理、同步和通信机制。4.1 线程管理 (std::thread)创建和运行一个线程变得非常简单。#include iostream #include thread void hello() { std::cout Hello from thread! Thread ID: std::this_thread::get_id() std::endl; } int main() { std::thread t(hello); // 创建线程并启动执行hello函数 std::cout Hello from main! Main Thread ID: std::this_thread::get_id() std::endl; t.join(); // 等待线程t结束 return 0; }关键操作join()阻塞当前线程直到被join的线程执行完毕。必须对每个可结合的joinable线程调用join()或detach()否则线程析构时会调用std::terminate。detach()将线程与std::thread对象分离允许线程独立运行。分离后的线程无法再被join。std::this_thread::sleep_for让当前线程休眠一段时间。std::this_thread::yield建议调度器让出当前线程的时间片。重要避坑点线程对象的生命周期管理。std::thread对象本身是可移动但不可拷贝的。确保在std::thread对象析构前已经对其调用了join()或detach()。一种常见的RAII做法是创建一个ThreadGuard类在析构函数中自动join。4.2 互斥量与锁 (std::mutex,std::lock_guard,std::unique_lock)互斥量用于保护共享数据防止数据竞争。#include thread #include mutex #include vector std::mutex g_mutex; int shared_data 0; void safe_increment() { for (int i 0; i 100000; i) { std::lock_guardstd::mutex lock(g_mutex); // 构造时加锁析构时自动解锁 shared_data; } // lock_guard析构自动释放锁 } int main() { std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back(safe_increment); } for (auto t : threads) { t.join(); } std::cout Final value: shared_data std::endl; // 应为 1000000 }std::lock_guard简单的RAII锁管理器构造时加锁析构时解锁。适用于简单的临界区保护。std::unique_lock功能更丰富的锁管理器。除了具备lock_guard的功能外还支持延迟加锁、尝试加锁、手动加解锁、转移所有权等。常用于条件变量。std::lock用于一次性锁住多个互斥量避免死锁采用特定的算法如std::try_lock。死锁预防始终以固定的全局顺序获取多个锁或者使用std::lock一次性锁定所有需要的互斥量。// 错误可能死锁 // thread1: lock(m1); lock(m2); // thread2: lock(m2); lock(m1); // 正确使用std::lock std::lock(m1, m2); // 同时锁住m1和m2避免死锁 std::lock_guardstd::mutex lock1(m1, std::adopt_lock); // 接管已锁定的m1 std::lock_guardstd::mutex lock2(m2, std::adopt_lock); // 接管已锁定的m24.3 条件变量 (std::condition_variable)用于线程间的同步允许一个或多个线程等待某个条件成立。#include thread #include mutex #include condition_variable #include queue std::queueint data_queue; std::mutex queue_mutex; std::condition_variable data_cond; void data_preparation_thread() { for (int i 0; i 10; i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); { std::lock_guardstd::mutex lock(queue_mutex); data_queue.push(i); std::cout Prepared data: i std::endl; } data_cond.notify_one(); // 通知一个等待的线程 } } void data_processing_thread() { while (true) { std::unique_lockstd::mutex lock(queue_mutex); // 等待条件成立队列非空。wait会原子地解锁锁并阻塞线程。 // 被唤醒后会重新获取锁并检查条件。 data_cond.wait(lock, []{ return !data_queue.empty(); }); int data data_queue.front(); data_queue.pop(); lock.unlock(); // 处理数据前可以提前释放锁 std::cout Processed data: data std::endl; if (data 9) break; // 结束条件 } } int main() { std::thread t1(data_preparation_thread); std::thread t2(data_processing_thread); t1.join(); t2.join(); }关键点wait函数接受一个std::unique_lock和一个谓词lambda或函数。它会循环检查谓词如果为false则释放锁并使线程阻塞当被notify_one()或notify_all()唤醒时重新获取锁并再次检查谓词。这种“循环检查”模式是为了防止虚假唤醒spurious wakeup——即线程可能在没有收到通知的情况下被唤醒。4.4 异步操作与Future (std::async,std::future,std::promise)提供了一种更高层次的异步编程抽象用于获取异步任务的结果。std::async异步启动一个任务返回一个std::future对象。std::future提供访问异步操作结果的机制。可以通过get()获取结果会阻塞直到结果就绪或wait()等待完成。std::promise用于在线程间传递结果。生产者线程通过promise.set_value()设置值消费者线程通过与之关联的future.get()获取值。#include iostream #include future #include chrono int compute_something_heavy() { std::this_thread::sleep_for(std::chrono::seconds(2)); return 42; } int main() { // 使用 std::async 启动异步任务 // std::launch::async 指定策略在新线程中执行 // std::launch::deferred 延迟执行直到调用future.get()时才在当前线程执行 std::futureint result_future std::async(std::launch::async, compute_something_heavy); std::cout Doing other work in main thread...\n; // ... 主线程可以做其他事情 int result result_future.get(); // 阻塞直到异步任务完成并获取结果 std::cout The answer is: result std::endl; // 使用 std::promise 和 std::future std::promiseint prom; std::futureint fut prom.get_future(); std::thread worker_thread([prom]{ std::this_thread::sleep_for(std::chrono::seconds(1)); prom.set_value(100); // 设置承诺的值 }); std::cout Waiting for the promise...\n; std::cout Value from promise: fut.get() std::endl; // 获取值 worker_thread.join(); return 0; }实战建议对于简单的异步计算任务优先使用std::async它让编译器/运行时库决定是在新线程还是当前线程执行代码更简洁。对于更复杂的线程间通信和控制则需要使用std::promise/std::future对或者结合std::packaged_task将可调用对象包装成可以异步执行的任务。5. 其他重要特性与标准库增强C11的革新是全方位的除了上述核心特性还有许多其他重要改进。5.1 常量表达式 (constexpr)constexpr用于声明编译期常量或能在编译期求值的函数。这为元编程和性能优化打开了新的大门。// constexpr 变量 constexpr int size 10; // 编译期常量 int arr[size]; // 可以用作数组大小 // constexpr 函数 constexpr int factorial(int n) { return n 1 ? 1 : n * factorial(n - 1); } constexpr int fact5 factorial(5); // 编译期计算结果为120 // C11中constexpr函数体通常只能包含一条return语句C14放宽了限制constexpr函数如果传入编译期常量参数则会在编译期计算如果传入运行时变量则退化为普通函数。这允许你编写既能用于编译期计算又能用于运行时计算的通用函数。5.2 委托构造函数与继承构造函数委托构造函数允许一个构造函数调用同一个类的另一个构造函数避免了代码重复。class Widget { int x, y, z; public: Widget(int a, int b, int c) : x(a), y(b), z(c) { // 复杂的初始化 } Widget() : Widget(0, 0, 0) {} // 委托给三参数构造函数 Widget(int a) : Widget(a, 0, 0) {} // 委托给三参数构造函数 };继承构造函数使用using声明让派生类继承基类的构造函数。class Base { public: Base(int) {} Base(int, double) {} }; class Derived : public Base { public: using Base::Base; // 继承Base的所有构造函数 // Derived现在拥有 Derived(int) 和 Derived(int, double) 构造函数 };这简化了派生类的编写特别是当派生类没有新增成员变量时。5.3 右尖括号 ()在C98中嵌套模板的结束符会被解析为右移运算符需要空格分隔。C11修正了这一点。std::vectorstd::listint list_of_vectors; // C11正确C98/03需要写成 5.4 静态断言 (static_assert)编译期断言用于在编译时检查条件是否满足。如果条件为false则编译失败并输出指定错误信息。static_assert(sizeof(int) 4, int must be 4 bytes on this platform.); static_assert(sizeof(void*) 4, Pointer size must be at least 4 bytes.);这对于编写平台无关或依赖特定条件的代码非常有用。5.5 标准库的现代化扩充C11为标准库添加了大量新容器和算法无序容器std::unordered_set,std::unordered_map,std::unordered_multiset,std::unordered_multimap。基于哈希表实现提供平均O(1)复杂度的查找但元素无序。元组std::tuple可以存储任意数量、任意类型的元素。正则表达式regex库提供了完整的正则表达式支持。随机数库random库提供了比C库函数rand()更强大、更灵活的随机数生成器。时间库chrono库提供了类型安全的时间点和时长操作。std::array固定大小的数组容器比原生数组更安全知道自己的大小支持迭代器等。std::forward_list单向链表比std::list内存开销更小。6. 常见问题与避坑指南在实际项目中应用C11我积累了一些宝贵的经验和常见的“坑”。6.1auto推导出的类型可能不是你想的那样auto遵循模板推导规则会忽略顶层const和引用。const int ci 10; auto a ci; // a 的类型是 int而不是 const int auto b ci; // b 的类型是 const int正确保留了const和引用 std::vectorbool vec_bool; auto bool_item vec_bool[0]; // 注意类型是 std::vectorbool::reference不是bool // 因为vectorbool进行了特化operator[]返回的是一个代理对象。 // 正确的做法使用显式类型或static_cast bool real_bool vec_bool[0];对于代理迭代器或类似vectorbool的情况使用auto要格外小心。6.2 移动语义的误用与陷阱不要移动局部变量在函数返回局部变量时编译器会自动进行RVO返回值优化或移动不需要也不应该使用std::move。Widget createWidget() { Widget w; // ... 初始化 w return w; // 正确编译器可能会进行RVO或移动 // return std::move(w); // 错误这会抑制RVO }移动后对象状态有效但未指定被移动后的源对象处于“有效但未指定”的状态。这意味着你可以对其执行无副作用的操作如析构、赋值但不能假设其内容是什么。最佳实践是立即赋予其一个新值或不再使用。std::string str1 Hello; std::string str2 std::move(str1); // 此时str1是有效但未指定的状态。可能是空串也可能是其他。 str1.clear(); // 安全操作 // std::cout str1; // 不安全内容未定义 str1 New Value; // 安全赋予新值noexcept的重要性移动构造函数和移动赋值运算符应尽可能标记为noexcept。标准库容器如std::vector在扩容等操作中如果元素的移动操作是noexcept的则会使用移动而非拷贝从而保证强异常安全。6.3 多线程编程的典型错误忘记join或detach这是新手最常见的错误。确保每个std::thread对象在析构前都被处理过。数据竞争与锁粒度锁的粒度太粗影响性能太细容易死锁。尽量只锁住真正需要保护的共享数据并在持有锁的时间内完成最小必要操作。条件变量的虚假唤醒务必在wait调用中使用循环检查条件谓词不能使用if。std::async的启动策略默认策略std::launch::async | std::launch::deferred由实现定义可能不会立即创建新线程。如果需要异步执行应显式指定std::launch::async。std::future的get()只能调用一次get()调用后future的状态变为无效再次调用会导致std::future_error异常。6.4 Lambda捕获的生命周期问题按引用捕获局部变量时必须确保lambda被调用时该变量仍然存在。std::functionvoid() create_lambda() { int local_var 42; return [local_var]() { std::cout local_var; }; // 危险返回lambda但local_var即将销毁 } // local_var 被销毁 auto bad_lambda create_lambda(); bad_lambda(); // 未定义行为访问已销毁的局部变量解决方案如果lambda的生命周期可能超过局部变量则按值捕获[]或[local_var]或者确保局部变量的生命周期足够长例如使用std::shared_ptr管理。6.5 智能指针的循环引用与性能循环引用如前所述使用std::weak_ptr打破std::shared_ptr的循环引用。性能开销std::shared_ptr的引用计数操作是原子操作有性能开销。在单线程环境下如果不需要共享所有权优先使用std::unique_ptr。std::make_shared能一次性分配对象和控制块的内存通常比直接new更高效。避免用原始指针初始化多个独立的shared_ptr这会导致多个控制块最终重复释放。int* raw_ptr new int(10); std::shared_ptrint sp1(raw_ptr); // std::shared_ptrint sp2(raw_ptr); // 灾难两个独立的shared_ptr管理同一个资源 std::shared_ptrint sp3 sp1; // 正确共享所有权C11的旅程远不止于此它奠定了现代C的基础。从C11开始C进入了每三年一个周期的稳定更新节奏C14, C17, C20, C23但C11无疑是变革最大、影响最深远的一版。掌握它就等于拿到了编写现代C代码的钥匙。在实际项目中我建议逐步引入这些特性从auto、范围for、智能指针开始再到lambda、移动语义最后深入多线程。每一步都会让你的代码变得更简洁、更安全、更高效。
返回列表