C++11 引入了许多新特性,以改进语言的语法、功能和标准库。以下是一些主要的新特性:
一、语法的改进
(1) 统一的初始化方法
C++11 引入了统一的初始化语法 {}
,可以用于变量初始化、数组初始化、类成员初始化等。
1 2 int x{5 };std::vector<int > vec{1 , 2 , 3 , 4 , 5 };
(2) 成员变量默认初始化
类成员变量可以在声明时直接初始化。
1 2 3 4 class MyClass { int x = 0 ; double y = 1.0 ; };
(3) auto 关键字
auto
允许编译器自动推断变量的类型,减少类型声明的冗余。
1 2 auto x = 5 ; auto y = 3.14 ;
(4) decltype 关键字
decltype
用于推断表达式的类型。
1 2 int x = 0 ;decltype (x) y = 1 ;
(5) 智能指针
C++11 引入了智能指针 std::shared_ptr
和 std::unique_ptr
,用于自动管理动态分配的对象的生命周期。
1 2 3 4 #include <memory> std::shared_ptr<int > p1 = std::make_shared <int >(10 ); std::unique_ptr<int > p2 = std::make_unique <int >(20 );
(6) 空指针 nullptr
C++11 引入了 nullptr
作为空指针的明确表示,替代传统的 NULL
。
(7) 基于范围的 for 循环
简化遍历容器元素的语法。
1 2 3 4 std::vector<int > vec = {1 , 2 , 3 , 4 , 5 }; for (int n : vec) { std::cout << n << std::endl; }
(8) 右值引用和移动语义
引入右值引用和移动构造函数,允许高效地将资源从一个对象移动到另一个对象,提高性能。
1 2 std::vector<int > v1 = {1 , 2 , 3 }; std::vector<int > v2 = std::move (v1);
二、标准库扩充
(9) 无序容器
C++11 引入了无序容器(如 std::unordered_map
和 std::unordered_set
),基于哈希表实现。
1 2 3 4 5 #include <unordered_map> std::unordered_map<int , std::string> umap; umap[1 ] = "one" ; umap[2 ] = "two" ;
(10) 正则表达式
标准库新增了正则表达式支持,提供了灵活的字符串模式匹配功能。
1 2 3 4 5 6 7 8 #include <regex> std::regex pattern ("\\d+" ) ;std::smatch matches; std::string text = "There are 123 numbers here" ; if (std::regex_search (text, matches, pattern)) { std::cout << "Found a match: " << matches[0 ] << std::endl; }
(11) Lambda 表达式
允许在代码中定义匿名函数,简化函数对象的创建。
1 2 auto add = [](int a, int b) { return a + b; };std::cout << add (2 , 3 ) << std::endl;
三、智能指针
智能指针(Smart Pointer)是一种自动管理动态内存的机制,可以避免内存泄漏 和悬空指针 (Dangling Pointer)等问题。
0. 实现原理
智能指针的实现原理基于RAII(Resource Acquisition Is Initialization)原则,通过在构造函数 中获取资源,在析构函数 中释放资源,确保对象在生命周期结束时自动释放其占用的资源。不同类型的智能指针实现方式略有不同,但基本原理类似,下面详细介绍std::unique_ptr
、std::shared_ptr
和std::weak_ptr
的实现原理。
1. std::unique_ptr的实现原理
std::unique_ptr
是一种独占所有权的智能指针,其实现非常简单,主要依赖于C++的移动语义。
核心概念:
独占所有权:同一时间只能有一个std::unique_ptr
对象管理某个资源。
移动语义:通过移动构造函数和移动赋值运算符来转移所有权。
主要成员函数:
构造函数:初始化时获取资源。
析构函数:销毁时释放资源。
移动构造函数和移动赋值运算符:转移资源所有权。
示例实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 template <typename T>class unique_ptr {private : T* ptr; public : explicit unique_ptr (T* p = nullptr ) : ptr(p) { } ~unique_ptr () { delete ptr; } unique_ptr (const unique_ptr&) = delete ; unique_ptr& operator =(const unique_ptr&) = delete ; unique_ptr (unique_ptr&& other) noexcept : ptr (other.ptr) { other.ptr = nullptr ; } unique_ptr& operator =(unique_ptr&& other) noexcept { if (this != &other) { delete ptr; ptr = other.ptr; other.ptr = nullptr ; } return *this ; } T& operator *() const { return *ptr; } T* operator ->() const { return ptr; } T* get () const { return ptr; } T* release () { T* temp = ptr; ptr = nullptr ; return temp; } void reset (T* p = nullptr ) { delete ptr; ptr = p; } };
2. std::shared_ptr的实现原理
std::shared_ptr
是一种共享所有权的智能指针,通过引用计数 来管理资源的生命周期 。
核心概念:
引用计数:记录有多少std::shared_ptr
对象共享同一个资源,当引用计数归零时释放资源。
控制块(Control Block):存储资源指针和引用计数。
主要成员函数:
构造函数:初始化时创建控制块并增加引用计数。
析构函数:销毁时减少引用计数,当引用计数为0时释放资源和控制块。
拷贝构造函数和拷贝赋值运算符:增加引用计数。
示例实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 template <typename T>class shared_ptr {private : T* ptr; std::size_t * ref_count; void release () { if (ref_count && --(*ref_count) == 0 ) { delete ptr; delete ref_count; } } public : explicit shared_ptr (T* p = nullptr ) : ptr(p), ref_count(p ? new std::size_t(1 ) : nullptr) { } ~shared_ptr () { release (); } shared_ptr (const shared_ptr& other) : ptr (other.ptr), ref_count (other.ref_count) { if (ref_count) { ++(*ref_count); } } shared_ptr& operator =(const shared_ptr& other) { if (this != &other) { release (); ptr = other.ptr; ref_count = other.ref_count; if (ref_count) { ++(*ref_count); } } return *this ; } T& operator *() const { return *ptr; } T* operator ->() const { return ptr; } T* get () const { return ptr; } };
3. std::weak_ptr的实现原理
std::weak_ptr
是一种弱引用智能指针,不会影响资源的生命周期。它用于解决std::shared_ptr
的循环引用问题。循环引用问题(Circular Reference Problem)是指两个或多个对象之间相互引用,导致这些对象的引用计数无法归零,从而造成内存泄漏。
核心概念:
弱引用:不增加引用计数,不管理资源生命周期。
需要与std::shared_ptr
配合使用,通过lock
函数获取std::shared_ptr
。
主要成员函数:
构造函数:初始化时从std::shared_ptr
获取控制块。
提升函数lock
:尝试获取共享所有权,如果资源已释放则返回空的std::shared_ptr
。
示例实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename T>class weak_ptr {private : T* ptr; std::size_t * ref_count; public : weak_ptr () : ptr (nullptr ), ref_count (nullptr ) {} weak_ptr (const shared_ptr<T>& sp) : ptr (sp.ptr), ref_count (sp.ref_count) {} shared_ptr<T> lock () const { return (ref_count && *ref_count > 0 ) ? shared_ptr <T>(*this ) : shared_ptr <T>(); } };
这个weak_ptr
类模板展示了如何实现一个简化版的std::weak_ptr
,其核心功能是:
不增加对象的引用计数,从而避免循环引用。
通过lock
函数安全地访问对象,只有在对象仍然存在时,才返回有效的shared_ptr
。
1. shared_ptr
std::shared_ptr
是C++11中引入的一个共享所有权的智能指针。多个shared_ptr
可以指向同一个对象,并且当最后一个拥有该对象的shared_ptr
被销毁时,该对象才会被删除。这通过引用计数实现。
std::shared_ptr
适用于需要多个拥有者共同使用资源的场景,如容器、动态数组、链表等。它提供了共享所有权的语义,并通过引用计数来管理资源的生命周期。当引用计数变为零时,资源将被自动释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include <memory> void functionA (std::shared_ptr<MyClass> ptr) { ptr->sayHello (); } void functionB (std::shared_ptr<MyClass> ptr) { ptr->sayHello (); } int main () { std::shared_ptr<MyClass> ptr (new MyClass()) ; functionA (ptr); functionB (ptr); return 0 ; }
线程安全
读操作是线程安全的。
写操作是线程不安全的,需要额外的同步措施。
共享引用计数的不同 shared_ptr
被多个线程写是安全的。
2. unique_ptr
std::unique_ptr
是C++11中引入的一个独占所有权的智能指针。它“拥有”它所指向的对象,并在离开其作用域时自动删除它。在任意给定的时刻,一个对象只能由一个unique_ptr
拥有。unique_ptr
不允许复制构造或复制赋值,但允许移动构造和移动赋值。
std::unique_ptr
适用于需要确保资源有且仅有一个拥有者的场景,如函数返回值、局部变量、类成员等。它提供了独占所有权的语义,并通过移动语义支持所有权的转移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> #include <memory> class MyClass { public : MyClass () { std::cout << "MyClass Constructor\n" ; } ~MyClass () { std::cout << "MyClass Destructor\n" ; } void sayHello () { std::cout << "Hello from MyClass\n" ; } }; int main () { std::unique_ptr<MyClass> ptr1 (new MyClass()) ; ptr1->sayHello (); std::unique_ptr<MyClass> ptr2 = std::move (ptr1); if (!ptr1) { std::cout << "ptr1 is now empty\n" ; } ptr2->sayHello (); return 0 ; }
3. weak_ptr
std::weak_ptr
是为了解决std::shared_ptr
可能导致的循环引用问题 而引入的。它是对对象的弱引用,不会增加对象的引用计数。weak_ptr
可以用来“观察”一个对象,但不会影响其生命周期。当需要访问对象时,必须将其转换为shared_ptr
(这可能会失败,如果对象已经被删除)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> class B ; class A {public : std::shared_ptr<B> b_ptr; ~A () { std::cout << "A destroyed\n" ; } }; class B {public : std::weak_ptr<A> a_ptr; ~B () { std::cout << "B destroyed\n" ; } }; int main () { std::shared_ptr<A> a = std::make_shared <A>(); std::shared_ptr<B> b = std::make_shared <B>(); a->b_ptr = b; b->a_ptr = a; if (auto sp = b->a_ptr.lock ()) { std::cout << "A is still alive\n" ; } else { std::cout << "A has been destroyed\n" ; } return 0 ; }
四、类型推导
1. auto
auto
关键字用于自动类型推导,让编译器根据初始化表达式来推断变量的类型。
必须初始化 :使用auto
定义的变量在声明时必须立即初始化,否则编译器无法推断其类型。
一行定义多个变量 :虽然可以在一行中使用auto
定义多个变量,但每个变量都必须能够被单独初始化,并且这些初始化不能产生类型推导的二义性。
不能用作函数参数 :函数参数的类型在编译前必须明确,因此不能使用auto
。
非静态成员变量 :在类的定义中,auto
不能用作非静态成员变量的类型,因为非静态成员变量在类的定义时就需要知道其类型。
定义数组 :auto
不能直接用于定义数组的大小,但你可以通过初始化一个std::initializer_list
或std::array
来间接定义。
模板参数 :auto
不能用作模板参数的类型,因为模板参数在实例化之前必须已知。
引用和cv限定符 :当auto
用于非引用类型时,它会忽略等号右边的引用和cv(const和volatile)限定符。但是,如果auto
与&
或&&
结合使用(即声明为引用类型),则会保留这些属性。
1 2 3 4 auto x = 10 ; auto & y = x; const auto z = 20 ; const auto & w = z;
auto
必须立即初始化,否则无法推导类型。
auto
不能用作函数参数。
2. decltype
decltype
关键字用于在编译时确定表达式的类型。与auto
不同,decltype
不会计算表达式的值,只是分析表达式的类型。
保留引用和cv属性 :decltype
会保留表达式的引用和常量性(constness)或易变性(volatility)。
函数调用 :如果exp
是一个函数调用,decltype(exp)
将是函数返回值的类型。
左值 :如果exp
是一个左值(例如变量、数组元素、结构体的成员等),decltype(exp)
将是该左值类型的左值引用。如果exp
是一个非常量左值,但你希望得到的类型是常量引用,可以使用decltype((exp))
(注意额外的括号)。
1 2 3 4 int x = 10 ; decltype (x) y = 20 ; decltype ((x)) z = x; decltype (x + 5 ) sum = 30 ;
auto 和 decltype 的配合使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T, typename U>auto add (T t, U u) -> decltype (t + u) { return t + u; } template <typename T, typename U> auto add (T t, U u) -> decltype (auto ) { return t + u; } template <typename T, typename U> auto add (T t, U u) { return t + u; }
这种方式允许函数返回类型根据参数类型自动推导。
五、右值引用:
右值引用(Rvalue References)是C++11引入的一种新类型引用,用于支持移动语义和实现完美转发。右值引用通过两个&&
符号表示,例如T&&
。它主要用于绑定右值(临时对象)和将资源从一个对象“移动”到另一个对象
1. 左值和右值
左值(Lvalue):
定义: 可以放在等号左边进行赋值操作的表达式。
性质: 有名字,可以取地址,通常表示一个具体的对象或变量。
右值(Rvalue):
定义: 不能放在等号左边进行赋值操作的表达式。
性质: 通常是临时的、无名的值,不可以取地址。
示例:
1 2 3 4 5 6 7 int a = 10 ;int & ref = a; int b = 20 ;int && rref = 20 ; int && another_rref = std::move (b);
2. 将亡值(Rvalue Reference):
将亡值是右值的一种特殊类型,指即将被销毁的对象,可以通过移动语义来“盗取”其资源。典型的将亡值包括函数返回的右值引用、std::move
返回值等。
3. 左值引用和右值引用
左值引用是对左值进行引用的类型,用 &
表示。左值引用可以修改被引用的对象,但不能绑定到右值。
1 2 int a = 42 ;int & lvalueRef = a;
右值引用是对右值进行引用的类型,用 &&
表示。右值引用允许绑定到右值,并且在移动语义中用于资源的转移。
左值引用
左值引用主要用于以下场景:
别名 :为变量创建别名,以便在函数内部或其他作用域中使用相同的对象。
传递参数 :通过传递引用参数,避免函数参数的拷贝,提高效率。
返回值 :函数可以返回左值引用,以便修改返回的对象。
右值引用
右值引用主要用于以下场景:
移动语义 :
移动语义通过右值引用实现资源的转移,而不是复制资源,从而提高程序的性能,特别是在处理大型数据结构时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> #include <vector> class MyClass {public : MyClass (std::vector<int >&& vec) : data (std::move (vec)) { std::cout << "Move Constructor" << std::endl; } private : std::vector<int > data; }; int main () { std::vector<int > vec = {1 , 2 , 3 }; MyClass obj (std::move(vec)) ; return 0 ; }
在这个例子中,通过std::move
将vec
转换为右值引用,从而调用MyClass
的移动构造函数,将vec
的资源转移给对象obj
。
完美转发 :
完美转发允许函数模板将其参数完全按照传递给它们的方式传递给另一个函数。使用右值引用和std::forward
,可以在函数模板中保留参数的左值或右值性质。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> #include <utility> 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 forwardToProcess (T&& x) { process (std::forward<T>(x)); } int main () { int a = 10 ; forwardToProcess(a); forwardToProcess(20 ); forwardToProcess(std::move (a)); return 0 ; }
1 2 3 4 template <typename T>void forwardToProcess (T&& x) { process (std::forward<T>(x)); }
在这个例子中,forwardToProcess
函数模板通过std::forward
将参数x
完美转发给process
函数,从而保留参数的左值或右值性质。
左值引用和右值引用的转换
通过std::move
,可以将左值显式转换为右值,从而绑定到右值引用。这是实现移动语义的关键。
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> #include <utility> void process (int && x) { std::cout << "Processing rvalue: " << x << std::endl; } int main () { int a = 10 ; process (std::move (a)); return 0 ; }
特性
左值引用 (Lvalue References)
右值引用 (Rvalue References)
表示方式
T&
T&&
绑定对象类型
左值
右值
用途
别名、传递参数、返回值
移动语义、完美转发
典型场景
函数参数、变量别名
移动构造函数、移动赋值运算符
4. 移动语义:
移动语义(Move Semantics)是C++11引入的一项重要特性,旨在提高程序的性能,尤其是在涉及大量对象复制的场景中。移动语义通过“移动”资源而不是复制资源,减少了不必要的性能开销。下面是移动语义的作用和原理的详细解释:
减少不必要的复制 :在传统的复制语义下,当对象被传递、返回或赋值时,往往会进行深拷贝操作,导致性能开销和资源浪费。移动语义通过移动资源,避免了这些不必要的复制操作。
提高程序性能 :移动操作比复制操作更高效,因为它只是简单地转移指针或资源的所有权,而不需要分配新的内存或复制数据。特别是在处理大数据结构(如大型数组、容器)时,性能提升显著。
优化资源管理 :移动语义可以与智能指针(如std::unique_ptr
)结合使用,优化资源的管理和生命周期,避免内存泄漏。
移动语义的原理
移动语义(Move Semantics)通过移动而不是复制资源,提高了程序的性能,特别是在涉及大数据结构的情况下。移动语义的核心概念包括右值引用(Rvalue References)、移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator)。它们共同作用,允许对象的资源所有权从一个对象转移到另一个对象,而不需要昂贵的深拷贝操作。
移动语义的主要原理
右值引用 :用于绑定临时对象或将要被销毁的对象。
移动构造函数 :从一个右值引用构造新的对象,通过转移资源来避免深拷贝。
移动赋值运算符 :从一个右值引用赋值给已有对象,通过转移资源来避免深拷贝。
例子
以下是一个详细的例子,展示了如何实现和使用移动语义。
定义一个支持移动语义的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> #include <utility> class MyClass {public : MyClass (int size) : data (new int [size]), size (size) { std::cout << "Constructor" << std::endl; } ~MyClass () { delete [] data; std::cout << "Destructor" << std::endl; } MyClass (MyClass&& other) noexcept : data (other.data), size (other.size) { other.data = nullptr ; other.size = 0 ; std::cout << "Move Constructor" << std::endl; } MyClass& operator =(MyClass&& other) noexcept { if (this != &other) { delete [] data; data = other.data; size = other.size; other.data = nullptr ; other.size = 0 ; std::cout << "Move Assignment" << std::endl; } return *this ; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; private : int * data; int size; }; int main () { MyClass a (10 ) ; MyClass b = std::move (a); MyClass c (20 ) ; c = std::move (b); return 0 ; }
运行结果解释
1 2 3 4 5 6 7 Constructor Move Constructor Constructor Move Assignment Destructor Destructor Destructor
解释:
MyClass a(10);
:调用构造函数,分配内存并初始化a
的资源。
MyClass b = std::move(a);
:调用移动构造函数,转移a
的资源到b
,并将a
的资源指针置空。这样,b
接管了a
的资源,避免了深拷贝。
MyClass c(20);
:调用构造函数,分配内存并初始化c
的资源。
c = std::move(b);
:调用移动赋值运算符,释放c
的原有资源,然后将b
的资源转移到c
,并将b
的资源指针置空。
对象a
、b
和c
在作用域结束时析构,调用析构函数。由于a
和b
的资源指针已经置空,所以它们的析构函数不会重复释放资源。
移动语义的优势
性能提升 :通过移动语义,可以避免不必要的深拷贝操作,大大提高程序的性能,尤其是在处理大型数据结构时。
资源管理 :移动语义有助于更高效地管理资源,避免内存泄漏和重复释放资源的问题。
小结
移动语义利用右值引用、移动构造函数和移动赋值运算符,通过转移资源所有权而不是复制资源,实现高效的资源管理和性能优化。在现代C++编程中,充分利用移动语义可以显著提高代码的性能和可靠性。
六、nullptr
在C++中,nullptr
是用于表示空指针的关键字。它是在C++11中引入的,用来代替传统的 NULL
。以下是一些关于 nullptr
的关键点:
1. 区别于 NULL
:
NULL
通常被定义为 0
或 ((void*)0)
。在某些情况下,这会导致编译器混淆 0
和空指针。例如,char *ch = NULL
会被解释为 char *ch = 0
。
nullptr
是一个类型为 nullptr_t
的字面量,专门用于表示空指针。它不会与整数 0
混淆。
2. 避免混淆 :
当函数重载时,使用 NULL
可能会导致错误的函数被调用。例如,假设有两个重载函数 void foo(char*);
和 void foo(int);
。调用 foo(NULL)
时,如果 NULL
被定义为 0
,则编译器可能会选择调用 foo(int)
。
使用 nullptr
可以避免这种混淆,因为 nullptr
明确表示为空指针。
3. 例子
1 2 3 4 5 void foo (char *) ; void foo (int ) ; foo (NULL ); foo (nullptr );
七、范围 for 循环
C++11 引入了基于范围的 for
循环,用于简化遍历容器或其他集合的语法。
1. 遍历 string 对象的每个字符:
1 2 3 4 std::string str ("some thing" ) ;for (char c : str) { std::cout << c << std::endl; }
2. 遍历 vector 中的元素:
1 2 3 4 5 6 7 8 9 10 11 std::vector<int > arr (5 , 100 ) ;for (std::vector<int >::iterator i = arr.begin (); i != arr.end (); ++i) { std::cout << *i << std::endl; } for (auto &i : arr) { std::cout << i << std::endl; }
八、列表初始化
C++11 引入了列表初始化,可以使用花括号 {}
进行初始化。这种方式对于内置类型和用户定义类型都适用。
例子
初始化 int
变量:
1 2 3 4 int x = 0 ; int x = {0 }; int x{0 }; int x (0 ) ;
防止信息丢失:
1 2 3 long double d = 3.1415926536 ;int a = {d}; int a = d;
九、Lambda 表达式
Lambda 表达式是C++11引入的一种匿名函数,用于编写内联的、无命名的可调用代码单元。
语法Lambda 表达式的语法:
1 [capture list] (parameter list) -> return type {function body}
捕获列表 (capture list
): 用于捕获外部变量。
参数列表 (parameter list
): 与普通函数的参数列表相似。
返回类型 (return type
): 指定返回值的类型,可以省略。
函数体 (function body
): 匿名函数的实现。
变量捕获: Lambda 的独特之处在于其能够捕获外部变量。
[]
不捕获任何变量。
[&]
以引用方式捕获所有变量。
[=]
以值的方式捕获所有变量。
[=, &foo]
以引用捕获变量 foo
,其余变量值捕获。
[&, foo]
以值捕获变量 foo
,其余变量引用捕获。
[bar]
以值方式捕获变量 bar
。
[this]
捕获所在类的 this
指针。
1 2 3 int a = 1 , b = 2 , c = 3 ;auto lam2 = [&, a]() { b = 5 ; c = 6 ; cout << a << b << c << endl; };lam2 ();
基本 lambda 表达式:
1 2 3 4 5 6 auto lam = []() -> int { std::cout << "Hello, World!" ; return 88 ; }; auto ret = lam ();std::cout << ret << std::endl;
变量捕获:
1 2 3 4 5 6 7 8 9 10 int a = 1 , b = 2 , c = 3 ;auto lam2 = [&, a]() { b = 5 ; c = 6 ; std::cout << a << b << c << std::endl; }; lam2 ();
值捕获和引用捕获:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void fcn_value_capture () { size_t v1 = 42 ; auto f = [v1] { return v1; }; v1 = 0 ; auto j = f (); } void fcn_mutable_lambda () { size_t v1 = 42 ; auto f = [v1]() mutable { return ++v1; }; v1 = 0 ; auto j = f (); } void fcn_reference_capture () { size_t v1 = 42 ; auto f = [&v1]() { return ++v1; }; v1 = 0 ; auto j = f (); }
使用 lambda 表达式进行数组排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 std::vector<int > vec = {3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 , 5 }; std::sort (vec.begin (), vec.end (), [](int a, int b) { return a < b; }); int arr[] = {6 , 4 , 3 , 2 , 1 , 5 };bool compare = [](const int &a, const int &b) { return a > b; };std::sort (arr, arr + 6 , compare); std::sort (arr, arr + 6 , [](const int & a, const int & b){return a > b;}); std::for_each(begin (arr), end (arr), [](const int & e){cout << "After:" << e << endl;});
十、深拷贝和浅拷贝:
浅拷贝: 多个对象共享同一块内存,修改一个对象的数据会影响其他对象。
1 2 int * arr1 = new int [5 ];int * arr2 = arr1;
深拷贝: 每个对象拥有自己的一份数据,修改一个对象的数据不会影响其他对象。
1 2 3 int * arr1 = new int [5 ];int * arr2 = new int [5 ];std::copy (arr1, arr1 + 5 , arr2);
十一、完美转发:
完美转发是 C++11 中引入的技术,用于在模板中保留参数的值类别。通过 std::forward
实现,在实现泛型函数时确保参数的类型信息不丢失。
1 2 3 4 template <typename T>void forwarder (T&& arg) { target (std::forward<T>(arg)); }
std::forward
用于在泛型函数中将参数完美地转发到目标函数,保持参数的值类别。这在实现通用容器、智能指针等类时非常有用。
十二、多线程同步机制
在C++中,多线程同步机制是为了防止数据竞争 和确保多个线程能够安全地访问共享资源 。以下是几种常见的同步机制及其用法:
1. 互斥锁(Mutex)
互斥锁是最常用的同步机制,用于保护共享资源,确保在任一时刻只有一个线程可以访问被锁定的资源。
std::mutex :基础的互斥锁。
std::lock_guard :RAII风格的锁管理,自动管理锁的生命周期。
std::unique_lock :更灵活的锁管理,可以手动锁定和解锁。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <thread> #include <mutex> std::mutex mtx; void print_thread_id (int id) { std::lock_guard<std::mutex> lock (mtx) ; std::cout << "Thread #" << id << std::endl; } int main () { std::thread threads[10 ]; for (int i = 0 ; i < 10 ; ++i) { threads[i] = std::thread (print_thread_id, i + 1 ); } for (auto &th : threads) { th.join (); } return 0 ; }
2. 读写锁(Shared Mutex)
读写锁允许多个线程同时读取,但只允许一个线程写入。C++17引入了std::shared_mutex
来实现这种锁。
std::shared_mutex :允许多个线程共享读访问,但独占写访问。
std::shared_lock :用于读锁定。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <thread> #include <shared_mutex> #include <vector> std::shared_mutex mtx; std::vector<int > data; void read_data (int id) { std::shared_lock<std::shared_mutex> lock (mtx) ; std::cout << "Thread #" << id << " reads data size: " << data.size () << std::endl; } void write_data (int id) { std::unique_lock<std::shared_mutex> lock (mtx) ; data.push_back (id); std::cout << "Thread #" << id << " writes data" << std::endl; } int main () { std::thread readers[5 ]; std::thread writers[5 ]; for (int i = 0 ; i < 5 ; ++i) { readers[i] = std::thread (read_data, i + 1 ); writers[i] = std::thread (write_data, i + 1 ); } for (int i = 0 ; i < 5 ; ++i) { readers[i].join (); writers[i].join (); } return 0 ; }
3. 条件变量(Condition Variable)
条件变量用于线程间的通知机制,使一个线程可以等待另一个线程的特定条件。
std::condition_variable :一般与std::mutex
结合使用。
std::condition_variable_any :可以与任何锁类型结合使用。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false ;void print_id (int id) { std::unique_lock<std::mutex> lock (mtx) ; cv.wait (lock, []{ return ready; }); std::cout << "Thread #" << id << std::endl; } void set_ready () { std::this_thread::sleep_for (std::chrono::seconds (1 )); { std::lock_guard<std::mutex> lock (mtx) ; ready = true ; } cv.notify_all (); } int main () { std::thread threads[10 ]; for (int i = 0 ; i < 10 ; ++i) { threads[i] = std::thread (print_id, i + 1 ); } std::thread notifier (set_ready) ; for (auto &th : threads) { th.join (); } notifier.join (); return 0 ; }
4. 信号量(Semaphore)
C++20引入了信号量,用于限制对资源的访问数量。
std::counting_semaphore :一个计数信号量,允许多个线程访问。
std::binary_semaphore :一个二进制信号量,相当于一个互斥锁。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <thread> #include <semaphore> std::counting_semaphore<3> sem (3 ) ; void worker (int id) { sem.acquire (); std::cout << "Thread #" << id << " is working" << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); std::cout << "Thread #" << id << " is done" << std::endl; sem.release (); } int main () { std::thread threads[10 ]; for (int i = 0 ; i < 10 ; ++i) { threads[i] = std::thread (worker, i + 1 ); } for (auto &th : threads) { th.join (); } return 0 ; }
5. 原子操作(Atomic Operations)
原子操作用于无锁编程,保证对共享数据的操作是原子的,不会被中断。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> #include <thread> #include <atomic> std::atomic<int > counter (0 ) ;void increment () { for (int i = 0 ; i < 1000 ; ++i) { counter++; } } int main () { std::thread threads[10 ]; for (int i = 0 ; i < 10 ; ++i) { threads[i] = std::thread (increment); } for (auto &th : threads) { th.join (); } std::cout << "Final counter value: " << counter << std::endl; return 0 ; }
总结
C++提供了多种同步机制来管理多线程程序中的共享资源。这些机制包括互斥锁、读写锁、条件变量、信号量和原子操作。选择合适的同步机制取决于具体的应用场景和需求。通过正确使用这些同步机制,可以有效地防止数据竞争,提高程序的可靠性和性能。
并发编程是指同时执行多个任务的能力。C++标准库提供了多种工具来实现并发编程,其中最重要的包括std::thread
、std::lock_guard
和std::unique_lock
。
十三、如何在C++中创建和管理线程?
1. 创建线程
使用 std::thread
类来创建和管理线程。std::thread
的构造函数接受一个可调用对象(如函数、函数对象或 lambda 表达式)和可选的参数,这些参数会被传递给线程执行的函数。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <thread> void print_message (const std::string& message) { std::cout << message << std::endl; } int main () { std::thread t (print_message, "Hello from the thread!" ) ; t.join (); return 0 ; }
解释
std::thread t(print_message, "Hello from the thread!");
:创建了一个新线程 t
,它将执行 print_message
函数,并传递 "Hello from the thread!"
作为参数。
t.join();
:主线程等待 t
线程完成。如果没有调用 join()
或 detach()
,主线程在结束时会终止所有子线程,这可能导致未定义的行为。
2. 线程函数
线程函数可以是普通函数、成员函数、lambda 表达式或函数对象。需要注意的是,如果使用成员函数作为线程函数,则需要将对象实例作为参数传递。
普通函数
1 2 3 void print_message (const std::string& message) { std::cout << message << std::endl; }
成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <thread> class Printer {public : void print_message (const std::string& message) { std::cout << message << std::endl; } }; int main () { Printer p; std::thread t (&Printer::print_message, &p, "Hello from the member function!" ) ; t.join (); return 0 ; }
Lambda 表达式
1 2 3 4 5 6 7 8 9 10 #include <iostream> #include <thread> int main () { std::thread t ([]() { std::cout << "Hello from the lambda!" << std::endl; }) ; t.join (); return 0 ; }
3. 线程同步
线程同步机制可以用来避免数据竞争和确保线程安全。
互斥锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <thread> #include <mutex> std::mutex mtx; void print_message (const std::string& message) { std::lock_guard<std::mutex> lock (mtx) ; std::cout << message << std::endl; } int main () { std::thread t1 (print_message, "Hello from thread 1!" ) ; std::thread t2 (print_message, "Hello from thread 2!" ) ; t1. join (); t2. join (); return 0 ; }
条件变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false ;void wait_for_ready () { std::unique_lock<std::mutex> lock (mtx) ; cv.wait (lock, []{ return ready; }); std::cout << "Thread is proceeding" << std::endl; } void set_ready () { std::this_thread::sleep_for (std::chrono::seconds (1 )); { std::lock_guard<std::mutex> lock (mtx) ; ready = true ; } cv.notify_all (); } int main () { std::thread t1 (wait_for_ready) ; std::thread t2 (set_ready) ; t1. join (); t2. join (); return 0 ; }
4. 分离线程
线程可以被分离,使其在后台运行,不需要等待它完成(注意:这会使得主线程无法直接获取该线程的执行结果)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <thread> void print_message () { std::cout << "Hello from a detached thread!" << std::endl; } int main () { std::thread t (print_message) ; t.detach (); std::this_thread::sleep_for (std::chrono::seconds (1 )); return 0 ; }
5. 线程管理
std::thread::join()
:主线程等待子线程结束。
std::thread::detach()
:将线程分离,让它在后台运行,不需要等待其结束。
总结
在C++中创建和管理线程可以通过标准库提供的 std::thread
类来实现。你可以创建线程、传递参数、同步线程操作(使用互斥锁、条件变量等),并管理线程的生命周期(使用 join()
或 detach()
)。掌握这些基础操作是进行高效多线程编程的关键。