目录
示例 3:使用 std::weak_ptr 解决循环引用问题
2. std::condition_variable(条件变量)
C++11的由来
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节主要讲解实际中比较实用的语法
-
C++11的全部参考文档
命名趣事
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际 标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫 C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也 完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的 时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11
统一的列表初始化
C++11 引入了统一的列表初始化(uniform initialization),这是一种新的初始化语法,它使用花括号
{}
来初始化对象。统一的列表初始化提供了一种更为一致和灵活的初始化方式,不仅适用于自定义类型,也适用于内置类型。
统一的列表初始化的一些关键点和特性:
语法一致性:不论对象类型如何,都可以使用
{}
来进行初始化。灵活性:它可以用来初始化数组、类对象、聚合体(如结构体和联合体),甚至是标准库容器。
避免最窄匹配:在函数重载时,使用列表初始化可以避免最窄匹配的问题,因为列表初始化不会触发类型转换。
初始化器的嵌套:可以嵌套使用初始化器列表,例如,初始化多维数组或包含其他容器的容器。
简单测试代码示例:
示例 1:初始化内置类型和数组
#include <iostream>
#include <vector>
int main() {
// 初始化内置类型
int x = {42}; // 与 int x = 42; 等价
double y{3.14}; // 使用花括号进行初始化
// 初始化数组
int arr1[3] = {1, 2, 3}; // 使用花括号初始化数组
int arr2[] = {4, 5, 6}; // 编译器根据初始化器列表的大小自动确定数组大小
// 输出数组元素
for (int i = 0; i < 3; ++i) {
std::cout << "arr1[" << i << "]: " << arr1[i] << std::endl;
std::cout << "arr2[" << i << "]: " << arr2[i] << std::endl;
}
return 0;
}
示例 2:初始化类和结构体
#include <iostream>
struct Point {
int x, y;
};
class MyClass {
public:
MyClass(int val) : value(val) {}
int value;
};
int main() {
// 初始化结构体
Point p1{1, 2}; // 结构体成员按照声明的顺序初始化
Point p2 = {3, 4}; // 等价于上面的初始化方式
// 初始化类对象
MyClass obj1{10}; // 调用构造函数进行初始化
// 输出值
std::cout << "p1: (" << p1.x << ", " << p1.y << ")" << std::endl;
std::cout << "p2: (" << p2.x << ", " << p2.y << ")" << std::endl;
std::cout << "obj1.value: " << obj1.value << std::endl;
return 0;
}
示例 3:初始化标准库容器
#include <iostream>
#include <vector>
#include <list>
int main() {
// 初始化 vector
std::vector<int> vec{1, 2, 3, 4, 5};
// 初始化 list
std::list<int> lst{5, 4, 3, 2, 1};
// 输出容器元素
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
for (int i : lst) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
在以上示例中,你可以看到统一的列表初始化如何为不同的数据类型提供了一致的初始化语法。这种语法不仅提高了代码的可读性,而且减少了在初始化时可能发生的错误。注意,虽然统一的列表初始化在大多数情况下都很方便,但也有一些情况下它可能并不适用,例如初始化非聚合类对象时,如果该类没有合适的构造函数接受一个初始化器列表,那么就会编译失败。
声明
c++11提供了多种简化声明的方式
auto关键字
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
auto
关键字是 C++11 引入的一个非常有用的特性,它允许编译器自动推断变量的类型。这大大简化了代码编写,特别是在处理复杂类型或模板时。使用auto
可以让程序员不必显式地指定变量的类型,编译器会根据初始化的表达式自动确定变量的类型。
auto 关键字的特点:
自动类型推导:编译器根据初始化表达式自动推断
auto
变量的类型。简化代码:特别是在处理复杂类型时,使用
auto
可以使代码更加简洁。与引用和指针的结合:
auto
可以与引用(&
)和指针(*
)结合使用,自动推导引用或指针的类型。初始化要求:使用
auto
声明的变量必须立即初始化,否则编译会报错。
注意事项:
虽然
auto
可以简化代码,但过度使用可能会降低代码的可读性。因此,在使用auto
时,应确保代码的可读性和可维护性。
auto
不能用于函数参数或返回类型的推导(C++14 引入了auto
类型的函数返回类型,但那是另外的话题)。
测试代码示例:
示例 1:基本类型推导
#include <iostream>
int main() {
auto x = 42; // x 被推导为 int 类型
auto y = 3.14; // y 被推导为 double 类型
std::cout << "x: " << x << ", type: " << typeid(x).name() << std::endl;
std::cout << "y: " << y << ", type: " << typeid(y).name() << std::endl;
return 0;
}
示例 2:复杂类型推导(如 STL 容器)
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin(); // it 被推导为 std::vector<int>::iterator 类型
std::cout << "First element: " << *it << std::endl;
return 0;
}
示例 3:与引用和指针的结合使用
#include <iostream>
#include <vector>
int main() {
int a = 10;
auto& ref = a; // ref 被推导为 int& 类型,即 a 的引用
auto* ptr = &a; // ptr 被推导为 int* 类型,即指向 a 的指针
ref = 20; // 修改 ref,实际上修改了 a
std::cout << "a: " << a << std::endl; // 输出 20
*ptr = 30; // 修改 ptr 指向的值,实际上也修改了 a
std::cout << "a: " << a << std::endl; // 输出 30
return 0;
}
在上述示例中,
auto
关键字使得程序员不必显式声明变量x
、y
、it
、ref
和ptr
的类型,编译器会根据初始化的表达式自动推导出它们的类型。这不仅简化了代码,还提高了代码的可读性和可维护性。当然,在使用auto
时,应确保初始化的表达式是明确的,以便编译器能够准确地推导出变量的类型。
decltype关键字
decltype
是 C++11 引入的一个关键字,它用于在编译时查询表达式的类型。decltype
提供了一种方式,使得程序员能够明确地获取一个表达式或变量的类型,并在需要的时候使用这个类型信息。
decltype
的基本用法:
获取变量的类型:
decltype
可以直接用于变量,以获取其类型。获取表达式的类型:
decltype
也可以用于更复杂的表达式,以获取该表达式的类型。引用和指针的处理:如果表达式的结果是一个左值(例如变量或数组元素),则
decltype
会产生对该类型的引用;如果表达式的结果是一个右值(例如字面量或临时对象),则decltype
会产生该类型的非引用。
decltype
的特点:
类型推导:编译器会根据
decltype
后面的表达式自动推导类型。与
auto
的区别:auto
用于变量声明时的类型推导,而decltype
用于在编译时查询任意表达式的类型。灵活性:
decltype
可以处理复杂的表达式和类型,包括函数指针、成员函数指针等。
注意事项:
decltype
主要用于模板元编程和高级类型操作,对于一般的应用程序开发,可能并不常用。使用
decltype
时,应确保表达式的含义是明确的,以避免类型推导错误。
测试代码示例:
示例 1:获取变量的类型
#include <iostream>
#include <type_traits> // 用于 std::is_same 检查类型是否相同
int main() {
int x = 10;
decltype(x) y = 20; // y 的类型与 x 相同,即 int
std::cout << typeid(y).name() << std::endl; // 输出 y 的类型
return 0;
}
示例 2:获取表达式的类型
#include <iostream>
#include <type_traits>
int main() {
int a = 5, b = 10;
decltype(a + b) sum = a + b; // sum 的类型与 a + b 的结果类型相同,即 int
std::cout << typeid(sum).name() << std::endl; // 输出 sum 的类型
return 0;
}
示例 3:处理引用和指针
#include <iostream>
#include <type_traits>
int main() {
int a = 5;
decltype((a)) ref = a; // ref 是 int&,因为 a 是左值
decltype(10) val = 10; // val 是 int,因为 10 是右值
std::cout << std::boolalpha;
std::cout << "ref is a reference: " << std::is_reference<decltype(ref)>::value << std::endl; // 输出 true
std::cout << "val is a reference: " << std::is_reference<decltype(val)>::value << std::endl; // 输出 false
return 0;
}
在上面的示例中,
decltype
用于推导变量的类型,以及表达式的类型。同时,通过使用std::is_reference
和std::is_same
等类型特性,我们可以检查推导出的类型是否符合预期。这些特性通常定义在<type_traits>
头文件中。
nullptr
nullptr
是 C++11 中引入的一个新关键字,用于表示空指针常量。在 C++11 之前,通常使用NULL
或0
来表示空指针,但是这两个选择都存在一些问题。NULL
通常是定义为0
或(void*)0
的宏,这导致了类型的不一致性,而直接使用0
作为指针类型虽然大多数情况下可以工作,但在模板编程中可能会引发问题。nullptr
解决了这些问题,它是一个类型安全的空指针常量,其类型为std::nullptr_t
。它不能隐式转换为整数类型,只能转换为指针类型,这使得它在模板编程中更为安全和方便。
nullptr
的特点:
类型安全:
nullptr
的类型是std::nullptr_t
,这确保了它只能被赋给指针类型的变量,从而避免了与整数类型的混淆。明确性:
nullptr
的使用比NULL
或0
更明确,它清晰地表示一个空指针。与
NULL
的兼容性:nullptr
可以与接受NULL
的代码兼容,因为nullptr
可以隐式转换为void*
。
测试代码示例:
#include <iostream>
int main() {
int* ptr1 = nullptr; // 使用 nullptr 初始化一个空指针
int* ptr2 = 0; // 使用 0 初始化一个空指针(在 C++11 中仍然有效,但不推荐)
int* ptr3 = NULL; // 使用 NULL 初始化一个空指针(在 C++11 中仍然有效,但不推荐)
if (ptr1 == nullptr) {
std::cout << "ptr1 is nullptr" << std::endl;
}
if (ptr2 == nullptr) {
std::cout << "ptr2 is nullptr" << std::endl;
}
if (ptr3 == nullptr) {
std::cout << "ptr3 is nullptr" << std::endl;
}
// 尝试将 nullptr 赋给非指针类型,这将导致编译错误
// int nonPointer = nullptr; // 错误:不能将 nullptr 赋给非指针类型
return 0;
}
在这个示例中,我们创建了三个指针
ptr1
、ptr2
和ptr3
,并分别使用nullptr
、0
和NULL
来初始化它们。然后,我们检查这些指针是否等于nullptr
,并打印相应的消息。注意,尝试将nullptr
赋给非指针类型的变量会导致编译错误,这展示了nullptr
的类型安全性。
范围for
C++11 引入的范围for循环(Range-based for loop)是一种简化遍历容器(如数组、向量、列表等)或任何支持迭代器的序列的语法。范围for循环通过隐藏迭代器细节,使得遍历容器变得更加直观和简洁。
范围for循环的基本语法:
for (declaration : expression) {
// 循环体
}
declaration
:定义了循环变量,该变量会在每次迭代时接收容器中的一个元素。
expression
:表示一个容器或序列,它必须支持begin()
和end()
成员函数或类似的全局函数,用于获取迭代器。
关键点:
迭代器隐藏:范围for循环内部处理了迭代器的细节,程序员无需显式使用迭代器。
自动类型推导:循环变量
declaration
的类型通常由expression
中的元素类型自动推导得出。效率:范围for循环在性能上与使用迭代器的手动循环相当,没有额外的开销。
示例代码:
示例 1:遍历数组
#include <iostream>
int main() {
int arr[] = {1, 2, 3, 4, 5};
// 使用范围for循环遍历数组
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
示例 2:遍历向量(vector)
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用范围for循环遍历向量
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
示例 3:遍历其他STL容器
#include <iostream>
#include <list>
#include <set>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
std::set<int> st = {5, 4, 3, 2, 1};
// 遍历链表
for (int num : lst) {
std::cout << num << " ";
}
std::cout << std::endl;
// 遍历集合
for (int num : st) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
注意事项:
如果
expression
不支持begin()
和end()
函数,范围for循环将无法使用。在循环体内,循环变量
declaration
是只读的(对于常量表达式),并且每个迭代都会得到一个新的元素副本(对于非引用类型)。如果需要修改容器中的元素,应使用引用类型。
修改容器元素的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用引用遍历向量并修改元素
for (int& num : vec) {
num *= 2; // 修改容器中的元素
}
// 输出修改后的向量
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,我们使用
int&
(整数引用)作为循环变量的类型,以便在循环体内直接修改向量vec
中的元素。每个num
都是向量中元素的引用,所以当我们改变num
的值时,实际上是改变了向量中对应元素的值。
智能指针
C++11 引入了三种智能指针:
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
,它们旨在自动管理动态分配的内存,以避免常见的内存泄漏和悬挂指针问题。这些智能指针通过自动释放它们所指向的对象,简化了内存管理。
1. std::unique_ptr
std::unique_ptr
表示对动态分配对象的独占所有权。同一时间只能有一个unique_ptr
指向一个对象。当unique_ptr
被销毁时(例如超出作用域),它所指向的对象也会被自动删除。
2. std::shared_ptr
std::shared_ptr
实现共享所有权语义。多个shared_ptr
可以指向同一个对象,并且当最后一个指向该对象的shared_ptr
被销毁时,对象才会被删除。这通过引用计数来实现,每个shared_ptr
都持有一个指向控制块的指针,该控制块包含对对象的引用计数。
3. std::weak_ptr
std::weak_ptr
是对shared_ptr
所管理对象的弱引用,不会影响对象的生命周期。它主要是为了解决shared_ptr
之间的循环引用问题。
测试代码示例:
示例 1:使用 std::unique_ptr
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : value_(value) {}
~MyClass() { std::cout << "Destroying MyClass with value " << value_ << std::endl; }
void printValue() const { std::cout << "Value: " << value_ << std::endl; }
private:
int value_;
};
int main() {
std::unique_ptr<MyClass> ptr(new MyClass(42)); // 使用 new 创建对象并用 unique_ptr 管理
ptr->printValue(); // 输出对象的值
// 当 ptr 离开作用域时,它所指向的对象也会被自动删除
return 0;
}
示例 2:使用 std::shared_ptr
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : value_(value) {
std::cout << "Creating MyClass with value " << value_ << std::endl;
}
~MyClass() {
std::cout << "Destroying MyClass with value " << value_ << std::endl;
}
void printValue() const { std::cout << "Value: " << value_ << std::endl; }
private:
int value_;
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42); // 使用 make_shared 创建对象并用 shared_ptr 管理
std::shared_ptr<MyClass> ptr2 = ptr1; // ptr2 现在也指向同一个对象
ptr1->printValue(); // 输出对象的值
ptr2->printValue(); // 再次输出对象的值
// 当 ptr1 和 ptr2 都离开作用域时,对象才会被删除
return 0;
}
示例 3:使用 std::weak_ptr
解决循环引用问题
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "Destroying A\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用
~B() { std::cout << "Destroying B\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;
// 此时,即使 A 和 B 相互引用,也不会导致循环引用问题,因为 B 使用的是 weak_ptr
} // a 和 b 离开作用域,它们指向的对象会被正确销毁
return 0;
}
在上述示例中,
std::unique_ptr
用于管理单个对象的生命周期,确保对象在unique_ptr
销毁时也被销毁。std::shared_ptr
用于共享对象的所有权,当最后一个指向对象的shared_ptr
被销毁时,对象才会被删除。而std::weak_ptr
解决了shared_ptr
之间的循环引用问题,它不会延长对象的生命周期,只是提供了一个观察shared_ptr
所管理对象的手段。
注意事项:
使用智能指针时,通常应避免直接使用
new
和delete
来管理动态内存,因为智能指针会自动处理这些操作。
std::make_shared
是创建shared_ptr
的推荐方式,因为它比直接使用new
和shared_ptr
构造函数更高效,因为它只分配一次内存(同时分配控制块和对象本身)。小心使用
std::shared_ptr
,以避免不必要的共享所有权和潜在的性能开销。在确实需要共享所有权的情况下才使用它。当处理智能指针时,注意避免野指针(dangling pointers)和悬挂指针(dangling references),即不要保留指向已删除对象的智能指针或引用。
总结:
C++11 的智能指针通过自动管理内存,极大地简化了动态内存管理,并减少了内存泄漏和悬挂指针的风险。在实际编程中,应优先使用智能指针来管理动态分配的内存,并谨慎处理智能指针之间的关系,以避免循环引用等问题。
STL中一些变化
C++11 为 STL(Standard Template Library,标准模板库)带来了许多重要的改进和新增功能,这些变化使得 STL 更加灵活、高效和易于使用。下面是一些 C++11 中 STL 的主要变化,以及相应的简单测试代码:
1. 容器初始化
C++11 提供了列表初始化(List Initialization)的方式初始化容器,这使得代码更加简洁。
#include <iostream>
#include <vector>
#include <list>
#include <initializer_list>
int main() {
// 使用初始化列表初始化 vector
std::vector<int> vec = {1, 2, 3, 4, 5};
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
// 使用初始化列表初始化 list
std::list<std::string> lst = {"apple", "banana", "cherry"};
for (const std::string& s : lst) {
std::cout << s << " ";
}
std::cout << std::endl;
return 0;
}
2. 无序容器
C++11 引入了新的无序容器,包括
unordered_map
、unordered_multimap
、unordered_set
和unordered_multiset
,它们基于哈希表实现,提供了常数平均时间复杂度的查找操作。
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> umap;
umap["apple"] = 1;
umap["banana"] = 2;
umap["cherry"] = 3;
for (const auto& pair : umap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
3. range-based for loop
基于范围的 for循环(range-based for loop)使得遍历容器变得更加简单。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用基于范围的for循环遍历 vector
for (const auto& elem : vec) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
4. auto 类型推导
在 C++11 中,
auto
关键字用于自动推导变量类型,这特别适用于迭代器和复杂的 STL 类型。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 auto 推导迭代器类型
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
5. 新的算法
C++11 为 STL 算法库增加了一些新算法,例如
std::copy_n
、std::all_of
、std::any_of
和std::none_of
等。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 std::all_of 检查所有元素是否都大于 0
bool all_positive = std::all_of(vec.begin(), vec.end(), [](int i) { return i > 0; });
std::cout << std::boolalpha << "All elements are positive: " << all_positive << std::endl;
return 0;
}
6. lambda 表达式
C++11 引入了 lambda 表达式,这使得在 STL 算法中使用匿名函数变得更加容易。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 lambda表达式与 `std::for_each` 算法遍历并打印 vector 中的每个元素
std::for_each(vec.begin(), vec.end(), [](int i) {
std::cout << i << " ";
});
std::cout << std::endl;
// 使用 lambda 表达式与 `std::remove_if` 算法移除 vector 中所有的偶数元素
vec.erase(std::remove_if(vec.begin(), vec.end(), [](int i) {
return i % 2 == 0;
}), vec.end());
// 打印修改后的 vector
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
7. 右值引用和移动语义
C++11 引入了右值引用和移动语义,允许资源在对象之间高效地转移,这显著提高了 STL 容器的性能,特别是在插入和删除元素时。
#include <iostream>
#include <vector>
#include <string>
int main() {
std::string str = "Hello, World!";
std::vector<std::string> vec;
// 使用 push_back 和移动语义添加元素到 vector
vec.push_back(std::move(str)); // str 的资源被移动到 vector 中的元素
// 打印 vector 中的元素
for (const auto& s : vec) {
std::cout << s << std::endl;
}
return 0;
}
以上只是 C++11 中 STL 变化的一部分。实际上,C++11 对 STL 进行了许多改进和扩展,包括性能优化、错误处理的改进、新算法和容器的增加等。这些变化使得 C++ 的标准库更加现代、高效和易用。
右值引用和移动语义
C++11 引入了右值引用和移动语义,这两个特性极大地提高了 C++ 的性能,特别是在处理资源密集型对象时,如大数组、大字符串或自定义的复杂数据结构。
右值引用
右值引用是对一个将要被销毁的对象的引用。它使用
&&
符号表示,并且只能绑定到右值。右值通常是临时对象或不再使用的对象。
移动语义
移动语义允许我们从一个对象“窃取”资源(如内存、文件句柄等),而不是复制它们。这通常通过重载移动构造函数和移动赋值运算符来实现。移动操作通常比复制操作更快,因为它避免了资源的复制,只是简单地重新指向原有资源。
移动构造函数
移动构造函数接受一个右值引用参数,并用于初始化对象,同时确保源对象在移动操作后处于有效但未定义的状态。
移动赋值运算符
移动赋值运算符也接受一个右值引用参数,并用于将资源从一个对象移动到另一个已存在的对象。
std::move
std::move
是一个标准库函数,它将其参数转换为右值引用。它实际上并不移动任何东西,但它允许我们将左值当作右值来处理,从而可以调用移动构造函数或移动赋值运算符。
示例代码
#include <iostream>
#include <cstring>
class ArrayWrapper {
private:
int* data;
size_t size;
public:
// 构造函数
ArrayWrapper(size_t s) : size(s) {
data = new int[size];
std::cout << "Allocated " << size << " integers at " << (void*)data << std::endl;
}
// 移动构造函数
ArrayWrapper(ArrayWrapper&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "Moved " << size << " integers from " << (void*)other.data << " to " << (void*)data << std::endl;
}
// 析构函数
~ArrayWrapper() {
if (data) {
delete[] data;
std::cout << "Deallocated " << size << " integers at " << (void*)data << std::endl;
}
}
// 移动赋值运算符
ArrayWrapper& operator=(ArrayWrapper&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
std::cout << "Moved " << size << " integers from " << (void*)other.data << " to " << (void*)data << std::endl;
}
return *this;
}
// 禁用复制构造函数和复制赋值运算符
ArrayWrapper(const ArrayWrapper&) = delete;
ArrayWrapper& operator=(const ArrayWrapper&) = delete;
};
int main() {
{
ArrayWrapper a(5); // 使用构造函数创建一个 ArrayWrapper 对象
ArrayWrapper b = std::move(a); // 使用移动构造函数创建另一个对象,资源从 a移动到b
// 此时a的data指针被置为nullptr,不应该再访问a
} // a和b的析构函数被调用,但由于a的资源已经被移动,它不会尝试删除空指针
ArrayWrapper c(10);
ArrayWrapper d(5);
d = std::move(c); // 使用移动赋值运算符将c的资源移动到d
// 此时c的data指针被置为nullptr,不应该再访问c
return 0;
}
在这个示例中,
ArrayWrapper
类管理一个动态分配的整数数组。它有一个移动构造函数和一个移动赋值运算符,这两个函数都允许我们从一个ArrayWrapper
对象窃取资源并“移动”到另一个对象,而不是复制它们。注意,我们禁用了复制构造函数和复制赋值运算符,以防止不小心进行深拷贝。
std::move
函数用于将左值转换为右值引用,从而允许我们调用移动构造函数或移动赋值运算符。
请注意,在移动操作后,源对象(在这个例子中是
a
和c
)处于有效但未定义的状态,这意味着它们仍然是一个有效的ArrayWrapper
对象,但其内部数据(在这个例子中是data
指针)不再指向有效的资源
可变参数模板
C++11 引入了可变参数模板(variadic templates),它允许模板接受任意数量和类型的参数。这大大增强了模板的灵活性,使得我们可以编写更通用和可复用的代码。可变参数模板使用模板参数包(template parameter packs)来实现,它们是以省略号(...)结尾的模板参数。
可变参数模板的基本结构
template <typename... Args>
class MyClass {
// ...
};
template <typename... Args>
void myFunction(Args... args) {
// ...
}
在上面的代码中,
Args...
是一个模板参数包,它可以代表任意类型和数量的参数。
递归分解模板参数包
处理模板参数包时,通常需要使用递归模板特化或者递归函数来“解开”这个包,对每一个参数进行处理。C++11 提供了一种更简单的方式,即使用
std::initializer_list
和std::forward
与完美转发结合来展开参数包。
完美转发
完美转发(Perfect Forwarding)是 C++11 引入的一个特性,它允许我们将参数以原有的形式(包括左值或右值)转发给另一个函数。这通常与可变参数模板一起使用,以实现对任意数量和类型的参数进行转发。
简单的测试代码
以下是一个简单的示例,演示了如何使用可变参数模板和完美转发来创建一个通用的打印函数,该函数可以接受任意数量和类型的参数,并将它们打印到标准输出。
#include <iostream>
#include <utility> // for std::forward
// 辅助函数,用于递归地打印参数
template <typename Arg, typename... Args>
void print(Arg&& arg, Args&&... args) {
std::cout << std::forward<Arg>(arg) << " ";
print(std::forward<Args>(args)...); // 递归调用
}
// 终止递归的基例
void print() {
std::cout << std::endl;
}
// 封装函数,接受任意数量和类型的参数
template <typename... Args>
void print_all(Args&&... args) {
print(std::forward<Args>(args)...); // 展开参数包
}
int main() {
print_all("Hello", 42, 3.14, 'c'); // 输出: Hello 42 3.14 c
print_all(1, "world", 2.718); // 输出: 1 world 2.718
return 0;
}
在这个例子中,
arg
和任意数量的额外参数args...
。它首先打印arg
,然后递归地调用自身来打印剩余的参数。print_all
函数是一个封装函数,它接受任意数量和类型的参数,并使用std::forward
将它们转发给
std::forward
用于完美转发参数,确保左值保持为左值,右值保持为右值,这样我们可以保持原始参数的类别(lvalue 或 rvalue),这对于性能优化(如移动语义)至关重要。
通过可变参数模板和完美转发,我们可以编写非常通用和灵活的代码,以适应各种不同的使用场景。
lambda表达式
C++11 中的 lambda 表达式是一种创建匿名函数对象的方式,它可以捕获其所在作用域的变量,并且可以在需要函数对象的地方使用。Lambda 表达式提供了一种简洁的方式来定义小型函数,它们经常与 STL 算法一同使用,以提供自定义的行为。
-
Lambda 表达式的基本语法如下:
[capture](parameters) -> return_type {body_of_lambda}
capture
:捕获子句,用于指定哪些外部变量可以在 lambda 体内被访问。捕获可以是按值(=
)或按引用(&
)。
parameters
:参数列表,与常规函数参数列表相似。
return_type
:返回类型。如果 lambda 体中的代码不包含return
语句,或者所有路径都返回同一类型,则编译器可以推导出返回类型,此时-> return_type
是可选的。
body_of_lambda
:lambda 函数的主体,包含要执行的代码。
下面是一些使用 lambda 表达式的简单示例:
示例 1:无捕获和无参数的 lambda
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用无捕获和无参数的 lambda 打印每个元素
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << ' ';
});
std::cout << std::endl;
return 0;
}
示例 2:捕获外部变量的 lambda
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int threshold = 3;
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用按值捕获外部变量 threshold 的 lambda 过滤出大于 threshold 的元素
auto filtered = std::copy_if(numbers.begin(), numbers.end(),
std::ostream_iterator<int>(std::cout, " "),
[threshold](int n) { return n > threshold; });
std::cout << std::endl;
return 0;
}
示例 3:按引用捕获外部变量的 lambda
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int sum = 0;
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用按引用捕获外部变量 sum 的 lambda 累加 vector 中的所有元素
std::for_each(numbers.begin(), numbers.end(), [&sum](int n) {
sum += n;
});
std::cout << "Sum: " << sum << std::endl;
return 0;
}
示例 4:带有返回类型的 lambda
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int max_value = 0;
// 使用带有返回类型的 lambda 找出 vector 中的最大值
auto max_finder = [](int a, int b) -> int { return (a > b) ? a : b; };
max_value = std::accumulate(numbers.begin(), numbers.end(), 0, max_finder);
std::cout << "Max value: " << max_value << std::endl;
return 0;
}
在这些示例中,lambda 表达式被用作算法(如
std::for_each
、std::copy_if
和std::accumulate
)的参数,以提供自定义的行为。Lambda 表达式让代码更加简洁和灵活,因为它们允许在需要的地方直接定义小型函数,而无需创建单独的函数或函数对象。
包装器
function包装器
C++11 引入了
std::function
,它是一个通用的、多态的函数包装器。std::function
可以将任何可调用的目标(函数、lambda 表达式、bind 表达式或其他函数对象)封装成统一的接口。这使得函数可以作为参数传递,也可以赋值给变量,甚至存储在容器中。
std::function
的特点:
类型擦除:
std::function
内部使用类型擦除技术,使得你可以存储不同类型的可调用对象,而无需关心其具体的类型。
通用性:它可以封装任何可调用的对象,包括普通函数、成员函数、lambda 表达式、bind 表达式等。
可调用性:你可以像调用普通函数一样调用
std::function
对象。
使用 std::function
的简单测试代码:
#include <iostream>
#include <functional> // 引入 std::function
// 普通函数
void normal_function(int x) {
std::cout << "Normal function called with: " << x << std::endl;
}
// Lambda 表达式
auto lambda_function = [](int x) {
std::cout << "Lambda function called with: " << x << std::endl;
};
// 使用 std::function 的例子
int main() {
// 创建一个 std::function 对象,它可以存储接受 int 参数且没有返回值的任何可调用对象
std::function<void(int)> func;
// 将普通函数赋值给 std::function 对象
func = normal_function;
func(42); // 输出: Normal function called with: 42
// 将 lambda 表达式赋值给 std::function 对象
func = lambda_function;
func(100); // 输出: Lambda function called with: 100
// 也可以直接将 lambda 表达式构造 std::function 对象
std::function<void(int)> another_func = [](int x) {
std::cout << "Another lambda function called with: " << x << std::endl;
};
another_func(200); // 输出: Another lambda function called with: 200
return 0;
}
在上面的代码中,我们首先定义了一个普通函数
normal_function
和一个 lambda 表达式lambda_function
。然后我们创建了一个std::function
对象func
,它可以接受一个int
类型的参数并且没有返回值。我们首先将normal_function
赋值给func
并调用它,然后将lambda_function
赋值给func
并调用它。最后,我们还展示了如何直接用一个 lambda 表达式来构造std::function
对象。
std::function
的一个常见用途是作为回调函数,因为它允许你传递任何可调用的对象作为参数,增加了代码的灵活性和可复用性。此外,std::function
还可以与std::bind
或 lambda 表达式结合使用,以实现更复杂的函数调用逻辑。
bind函数适配器
C++11 的
std::bind
是一个功能强大的函数适配器,它可以将一个可调用对象(如函数、成员函数、函数对象或 lambda 表达式)与一组参数绑定在一起,生成一个新的可调用对象。这个新的可调用对象可以像普通函数一样被调用,且会调用原始的可调用对象,并传递给它绑定的参数。
std::bind
的特点:
参数绑定:你可以使用
std::bind
来绑定可调用对象的参数,从而在后续调用时无需再次提供这些参数。
占位符:
std::bind
提供了占位符_1
、_2
等,用于表示绑定后的新函数对象参数的位置。
成员函数绑定:你可以使用
std::bind
来绑定类的成员函数,并指定要操作的对象实例。
使用 std::bind
的简单测试代码:
#include <iostream>
#include <functional> // 引入 std::bind
// 普通函数
void print_sum(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
// 类的成员函数
class MyClass {
public:
void print_hello(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
};
int main() {
// 使用 std::bind 绑定普通函数的参数
auto bound_print_sum = std::bind(print_sum, 10, std::placeholders::_1);
bound_print_sum(20); // 输出: Sum: 30
// 使用 std::bind 绑定成员函数的实例和参数
MyClass obj;
auto bound_member_func = std::bind(&MyClass::print_hello, &obj, std::placeholders::_1);
bound_member_func("World"); // 输出: Hello, World!
// 使用 std::bind 绑定 lambda 表达式
auto lambda = [](int x, int y) { return x * y; };
auto bound_lambda = std::bind(lambda, 5, std::placeholders::_1);
int product = bound_lambda(6); // product 现在是 30
std::cout << "Product: " << product << std::endl; // 输出: Product: 30
return 0;
}
在上面的代码中,我们首先定义了一个普通函数
print_sum
和一个包含成员函数print_hello
的类MyClass
。
然后,我们使用
std::bind
来绑定print_sum
函数的第一个参数为10
,生成一个新的可调用对象bound_print_sum
。当我们调用bound_print_sum(20)
时,它实际上会调用print_sum(10, 20)
。
接着,我们使用
std::bind
来绑定MyClass
的实例obj
和print_hello
成员函数,生成一个新的可调用对象bound_member_func
。当我们调用bound_member_func("World")
时,它实际上会调用obj.print_hello("World")
。
最后,我们还展示了如何使用
std::bind
来绑定一个 lambda 表达式,并调用它。
std::bind
的一个常见用途是生成回调函数或者将函数与特定参数绑定以生成新的行为。然而,由于 lambda 表达式的灵活性和易用性,在很多情况下,lambda 表达式已经成为了std::bind
的替代品。不过,std::bind
仍然在某些特定场景下(如绑定成员函数)有其独特的用途。
线程库
C++11 引入了对线程的原生支持,通过
<thread>
头文件提供了一组用于创建和管理线程的类和函数。这使得 C++ 程序员能够更方便地在多核处理器上编写并发程序。
C++11 线程库的主要特性:
线程创建:使用
std::thread
类创建新线程。线程同步:通过互斥锁(
std::mutex
)、条件变量(std::condition_variable
)、原子操作(std::atomic
)等实现线程同步。 C++11 提供了多种工具来实现线程同步,这些工具对于确保多线程环境下的数据完整性和程序正确性至关重要。线程局部存储:使用
thread_local
存储类,为线程提供局部存储。线程安全的数据结构:例如
std::lock_guard
、std::unique_lock
、std::shared_mutex
等,这些工具帮助程序员更安全地管理线程间的共享资源。
线程创建
简单的测试代码:
下面是一个简单的示例,展示了如何使用 C++11 的线程库来创建两个线程,并分别输出不同的信息。
#include <iostream>
#include <thread>
#include <chrono> // 用于休眠
// 线程执行的函数
void thread_function(const std::string& message) {
for (int i = 0; i < 5; ++i) {
std::cout << message << " " << i << std::endl;
// 休眠一段时间,以便观察线程交替执行的情况
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main() {
// 创建两个线程
std::thread thread1(thread_function, "Thread 1");
std::thread thread2(thread_function, "Thread 2");
// 等待两个线程完成
thread1.join();
thread2.join();
return 0;
}
在上面的代码中:
std::thread
类用于创建新线程。构造函数接受一个可调用的对象(例>如函数、函数对象、lambda 表达式等)以及任何所需的参数。
thread_function
是一个简单的函数,它将一个字符串和一个整数作为参数,并输出信息。
std::this_thread::sleep_for
用于使当前线程休眠一段时间。
join
成员函数用于等待线程完成执行。在main
函数中,我们调用join
来确保main
线程会等待thread1
和thread2
完成后才继续执行。
注意事项:
当创建线程时,请确保传递给
std::thread
构造函数的函数或 lambda 表达式是线程安全的,即它们不会访问或修改没有适当同步的共享数据。线程局部存储和原子操作是管理共享数据的重要工具,它们可以确保数据的正确性和线程安全。
在多线程环境中,应当特别注意资源的管理,避免资源泄漏和竞争条件。
C++11 的线程库为 C++ 程序员提供了一个强大的工具集,使得编写高效且安全的并发程序变得更加容易。然而,编写复杂的并发程序仍然需要深入理解和小心处理同步和通信问题。
线程同步
1. std::mutex
(互斥锁)
std::mutex
是一个简单的互斥锁,用于保护共享资源不被多个线程同时访问。当一个线程拥有互斥锁时,其他尝试获取该互斥锁的线程将会被阻塞,直到锁被释放。
测试代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx; // 全局互斥锁
int counter = 0; // 共享计数器
void increment() {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 使用 lock_guard 自动管理锁
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value is " << counter << std::endl;
return 0;
}
在这个例子中,我们使用
std::lock_guard
来自动管理锁的生命周期,确保在离开作用域时锁被释放。increment
函数由两个线程t1
和t2
同时执行,它们共同增加counter
的值。如果没有互斥锁,counter
的值可能小于 2000,因为两个线程可能同时读取和写入counter
。
2. std::condition_variable
(条件变量)
std::condition_variable
用于让线程等待某个条件成立。一个或多个线程可以在条件变量上等待,而另一个线程可以在条件成立时通知这些等待的线程。
测试代码:
#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);
while (!ready) { // 等待条件成立
cv.wait(lock); // 释放锁并等待通知
}
// ... 执行其他任务
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lock(mtx);
ready = true; // 设置条件为 true
cv.notify_all(); // 通知所有等待的线程
}
int main() {
std::thread threads[10];
// 创建并启动 10 个线程
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // 发送通知
for (auto& th : threads) th.join();
return 0;
}
在这个例子中,我们创建了 10 个线程,它们都等待
ready
变量变为true
。主线程调用go
函数,该函数设置ready
为true
并使用cv.notify_all()
通知所有等待的线程。
3. std::atomic
(原子操作)
std::atomic
提供了对基本数据类型的原子操作,这些操作在多线程环境中是安全的。原子操作不可分割,即它们不会在执行过程中被其他线程打断。
测试代码:
#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 t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value is " << counter<< std::endl;
return 0;
}
在这个例子中,我们使用
std::atomic<int>
来定义counter
,它是一个线程安全的整数。两个线程t1
和t2
同时增加counter
的值。由于使用了原子操作,最终counter
的值将准确为 2000,不会出现数据竞争或不一致的情况。
注意事项:
std::mutex
提供了互斥访问共享资源的能力,但过度使用可能导致性能下降和死锁。
std::condition_variable
应与互斥锁一起使用,以安全地等待和通知条件变化。
std::atomic
是轻量级的,适用于简单的原子操作,但可能不支持所有类型的原子操作或复杂的复合操作。
在使用这些同步工具时,应谨慎处理死锁和活锁问题,确保线程能够正确、高效地协作。此外,根据具体的应用场景和性能要求,可能需要结合使用多种同步机制,以达到最佳效果。
线程安全的数据结构
C++11 引入了两种锁保护的数据结构:
std::lock_guard
和std::unique_lock
,它们用于简化互斥量(std::mutex
)的管理,确保资源的正确同步访问。这两种锁类型都提供了 RAII(Resource Acquisition Is Initialization)风格的锁管理,即锁的获取在构造时自动完成,锁的释放则在对象销毁时自动完成。
std::lock_guard
std::lock_guard
是一个简单的锁类型,它在构造时自动锁定互斥量,并在析构时自动解锁。它适用于简单的锁定/解锁场景,其中不需要手动控制锁的获取和释放。
std::unique_lock
std::unique_lock
是一个更灵活的锁类型,它提供了比std::lock_guard
更多的控制选项。你可以延迟锁定、尝试锁定、手动解锁等。它还允许与std::condition_variable
一起使用,以在特定条件下等待。
简单的测试代码:
下面是一个简单的示例,展示了如何使用
std::lock_guard
和std::unique_lock
来保护共享资源。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono> // 用于休眠
// 共享资源
int shared_data = 0;
// 互斥量,用于保护 shared_data
std::mutex mtx;
// 使用 std::lock_guard 的线程函数
void increment_with_lock_guard() {
for (int i = 0; i < 5; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 在构造时自动锁定 mtx
++shared_data;
std::cout << "shared_data incremented to " << shared_data << " by lock_guard\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 休眠一段时间
}
}
// 使用 std::unique_lock 的线程函数
void increment_with_unique_lock() {
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock(mtx); // 在构造时自动锁定 mtx
++shared_data;
std::cout << "shared_data incremented to " << shared_data << " by unique_lock\n";
lock.unlock(); // 手动解锁
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 休眠一段时间
lock.lock(); // 手动重新锁定
}
}
int main() {
// 创建两个线程,分别使用 lock_guard 和 unique_lock
std::thread t1(increment_with_lock_guard);
std::thread t2(increment_with_unique_lock);
// 等待两个线程完成
t1.join();
t2.join();
std::cout << "Final shared_data value: " << shared_data << std::endl;
return 0;
}
在这个例子中:
shared_data
是被多个线程访问的共享资源。
mtx
是一个互斥量,用于保护shared_data
的访问。
increment_with_lock_guard
函数使用std::lock_guard
来自动管理锁。在函数体内部,lock_guard
对象在构造时自动锁定mtx
,并在函数返回时(即lock_guard
对象销毁时)自动解锁。
increment_with_unique_lock
函数使用std::unique_lock
,它提供了更多的灵活性。在这个例子中,我们展示了如何手动解锁和重新锁定互斥量。
注意事项:
当你使用
std::lock_guard
或std::unique_lock
时,你不需要显式调用lock()
或unlock()
方法,除非你有特定的需求。
std::lock_guard
更适合简单的同步需求,而std::unique_lock
更适合需要更精细控制同步的场景。使用锁时,要特别注意避免死锁。死锁通常发生在两个或更多的线程无限期地等待一个资源,而该资源又被另一个线程持有,后者也在等待其他线程释放资源。
在多线程环境中,使用锁来保护共享资源是一种常见的做法,但也要注意锁的使用可能会引入性能瓶颈,因此应该谨慎使用,并考虑其他同步机制,如条件变量、原子操作等。