一:声明
-
auto:
auto
是 C++11 引入的一个关键字,用于自动推断变量的类型。通过使用 auto
,编译器可以根据变量的初始化表达式推断其类型,从而减少代码中的重复冗长的类型声明。
简化模板声明:
for(auto p = vec.begin();p!=vec.end();p++)
-
decltype
decltype
是 C++11 引入的一个关键字,用于获取表达式的类型,而不进行实际的表达式求值。它的作用是让编译器在编译时推断表达式的类型,并将其作为编译器生成的类型声明的一部分。
获取变量的类型:
int x = 5;
decltype(x) y; // y 的类型为 int,与 x 的类型相同
获取表达式的类型:
int a = 10;
double b = 20.5;
decltype(a + b) c; // c 的类型为 double,因为 a + b 的类型为 double
结合 auto
和 decltype
:
int x = 5;
auto y = x; // y 的类型为 int,auto 推断为 int
decltype(auto) z = x; // z 的类型为 int&,decltype(auto) 保留了 x 的引用类型
获取函数返回值类型:
int foo();
decltype(foo()) result; // result 的类型为 foo() 函数返回值的类型
-
auto
:- 简化模板代码,减少模板参数的复杂性。
- 适用于需要简化类型声明,提高代码可读性的场合。
-
decltype
:- 在需要编写泛型代码时,用于捕获表达式的确切类型。
- 与模板结合,用于处理复杂的类型推导,例如迭代器或容器中的类型。
- 在需要保留变量引用类型的场合。
int arr[5] = {1, 2, 3, 4, 5};
decltype(arr) arr_copy; // arr_copy 的类型是 int[5]
auto arr_ref = arr; // arr_ref 的类型是 int*
当我们声明 int arr[5] = {1, 2, 3, 4, 5};
时,我们创建了一个名为 arr
的数组,其中包含了5个整数元素。
-
这行代码中,decltype(arr) arr_copy
:decltype(arr)
将返回arr
的类型,即int[5]
,表示一个包含5个整数元素的数组。因此,arr_copy
的类型也是int[5]
,它是一个未初始化的包含5个整数元素的数组。 -
这里使用auto arr_ref = arr
:auto
推导arr_ref
的类型。因为arr
是一个数组,当数组名被用作表达式时,它会自动退化为指向数组首元素的指针(即int*
类型)。因此,arr_ref
的类型被推导为int*
,表示一个指向整数的指针,指向数组arr
的首元素。
所以,arr_copy
是一个未初始化的包含5个整数元素的数组,而 arr_ref
是一个指向数组 arr
的首元素的指针。
-
返回类型后置
在C++11之前,返回类型通常在函数签名的前面指定,例如:
int add(int a, int b) {return a + b;
}
然而,随着C++11的引入,一种新的语法允许在函数签名之后指定返回类型,这被称为返回类型后置(Trailing Return Type)。这种语法对于需要从参数中推导返回类型的情况特别有用,并且在与 decltype
结合时非常方便。
#include <iostream>// 使用返回类型后置来定义一个函数,返回两个输入参数的和。
auto add(int a, int b) -> int {return a + b;
}// 使用返回类型后置和 decltype 来推导返回类型。
template <typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {return a * b;
}int main() {std::cout << "Add: " << add(3, 4) << std::endl; // 返回类型是 intstd::cout << "Multiply: " << multiply(3, 4.5) << std::endl; // 返回类型由 decltype 推导return 0;
}
-
模板别名:using=
在C++11中,引入了 using
关键字用于定义模板别名。using
关键字提供了一种更简洁、易读的方式来定义类型别名,特别是在使用模板时。
using alias_name = type;
alias_name
是你为类型定义的别名,而 type
是你要为其创建别名的类型。using
语句可以用于定义任何类型的别名,包括模板类型。
typedef
可以创建基本类型、指针类型、复合类型和函数指针类型的别名。
using
在 C++11 引入之后,成为了更通用的替代方案,可以用于创建类型别名、模板别名以及模板别名模板。
差别在于,新语法可用于模板部分具体化,但typedef不能
template<typename T>using arr12 = std::arry<T,12>
对于如下声明:
std::array<double,12> a1;
std::array<std::string,12> a2;
可以使用上述具体化模板声明:
arr12<double> a1;
arr12(std::string) a2;
-
nullptr
nullptr
是 C++11 中引入的空指针常量,用于代表空指针。它是一个特殊的字面值,可以被赋值给指针类型,而且不会与整数进行混淆。nullptr
用于替代传统的 NULL
宏,NULL
通常被定义为 0
或者 (void*)0
。因为 NULL
的定义可能是 0
或者指针类型的零值,所以在某些情况下,使用 NULL
可能会引起歧义,特别是在函数重载时。
nullptr
的引入解决了这个问题,它是一个明确的指针值,可以用于初始化任何指针类型,而不会与整数进行混淆。
二:智能指针
智能指针是 C++ 中用于管理动态内存的一种工具,它们可以自动管理内存的生命周期,从而减少内存泄漏和悬挂指针等问题。智能指针在 C++11 引入标准库之后变得非常流行。
-
std::unique_ptr:
std::unique_ptr
是一种独占所有权的智能指针,它确保在其生命周期结束时自动释放所管理的对象。- 每个
std::unique_ptr
拥有对其所指向对象的唯一所有权,当std::unique_ptr
被销毁时,它会自动释放其所管理的对象。 - 不能进行拷贝操作,但可以进行移动操作。
-
std::shared_ptr:
std::shared_ptr
是一种共享所有权的智能指针,它允许多个指针共享对同一对象的所有权。std::shared_ptr
使用引用计数来跟踪有多少个std::shared_ptr
共享同一对象。当引用计数为零时,对象会被自动释放。- 拷贝
std::shared_ptr
会增加引用计数,而销毁或者重置std::shared_ptr
则会减少引用计数。
-
std::weak_ptr:
std::weak_ptr
是一种弱引用智能指针,它不会增加对象的引用计数,也不会拥有对象的所有权。- 通常与
std::shared_ptr
一起使用,用于解决std::shared_ptr
的循环引用问题。 - 可以通过
std::weak_ptr
创建std::shared_ptr
,但需要检查std::weak_ptr
是否过期(即底层对象是否已被释放)。
三:类的修改
-
explicit
explicit
关键字用于防止隐式转换和复制初始化。它可以应用于单参数构造函数和转换函数。
#include <iostream>class MyClass {
public:explicit MyClass(int x) : data(x) {}int getData() const { return data; }private:int data;
};void process(const MyClass& obj) {std::cout << "Data: " << obj.getData() << std::endl;
}int main() {MyClass obj1(42); // 直接初始化,正常process(obj1); // 调用 process 函数,正常// MyClass obj2 = 42; // 错误!explicit 构造函数禁止复制初始化MyClass obj2 = MyClass(42); // 正确,使用直接初始化process(obj2); // 调用 process 函数,正常return 0;
}
explicit
阻止了 MyClass
的单参数构造函数被用于隐式转换,从而增强了代码的清晰度和安全性。
- 类内成员初始化
类内成员初始化是在 C++11 引入的特性,允许在类定义中直接初始化成员变量。这种方式可以确保成员变量在对象创建时就被初始化,提高了代码的可读性和可维护性。
#include <iostream>class MyClass {
public:// 类内初始化成员变量int data = 0;double value = 3.14;// 构造函数MyClass(int d) : data(d) {} // 对于没有在类内初始化的成员变量,可以在构造函数中进行初始化
};int main() {MyClass obj(42);std::cout << "Data: " << obj.data << std::endl; // 输出:Data: 42std::cout << "Value: " << obj.value << std::endl; // 输出:Value: 3.14return 0;
}
四:模板和STL的修改
为改善模板和标准模板库的可用性。
-
for循环修改
对于内置数组以及包含方法begin()和end()的类(如std::string)和STL容器,基于范围的for循环可简化编写循环的工作。
如要修改可以使用引用类型。
#include <iostream>
#include <vector>
#include <string>int main() {// 修改内置数组int arr[] = {1, 2, 3, 4, 5};for (int &x : arr) {x *= 2; // 将数组中的每个元素乘以 2}// 修改 std::stringstd::string str = "hello";for (char &c : str) {c = toupper(c); // 将字符串中的每个字符转换为大写形式}// 修改 STL 容器(例如 std::vector)std::vector<int> vec = {1, 2, 3, 4, 5};for (auto &num : vec) {num *= 2; // 将容器中的每个元素乘以 2}// 输出修改后的结果for (const auto &x : arr) {std::cout << x << " ";}std::cout << std::endl;std::cout << str << std::endl;for (const auto &num : vec) {std::cout << num << " ";}std::cout << std::endl;return 0;
}
-
新的STL容器
C++ 新的 STL(标准模板库)容器包括一些在 C++11 标准中引入的以及后续标准中新增的容器。
-
std::array
:- 固定大小的数组,与内置数组类似,但提供了更多的功能和安全性。
- 它具有固定的大小,在编译时就确定了,因此不支持动态大小调整。
-
std::forward_list
:- 单向链表,与
std::list
不同,它只能从头到尾进行迭代,无法逆向迭代。 std::forward_list
在某些情况下比std::list
更加高效,尤其是对于大量的插入和删除操作。
- 单向链表,与
-
std::unordered_set
和std::unordered_map
:- 哈希集合和哈希映射,分别对应于
std::set
和std::map
的无序版本。 - 它们使用哈希表来实现,具有 O(1) 的平均插入、查找和删除时间复杂度,但不保证元素的顺序。
- 哈希集合和哈希映射,分别对应于
-
std::unordered_multiset
和std::unordered_multimap
:- 允许重复键的哈希集合和哈希映射,分别对应于
std::multiset
和std::multimap
的无序版本。 - 允许插入相同键的多个副本,不保证元素的顺序。
- 允许重复键的哈希集合和哈希映射,分别对应于
-
std::tuple
:- 元组,可以存储多个不同类型的值,并且可以在编译时或运行时访问这些值。
- 元组的大小和类型在编译时确定,提供了一种方便的方式来处理多个值。
-
std::array_view
和std::span
(C++20):- 提供了对连续内存区域的非拥有式访问,允许安全地查看数组或容器的一部分,而不复制数据。
std::span
在 C++20 中引入,提供了更多的功能和灵活性。
-
新的STL方法
cbegin()
和 cend()
返回的是常量迭代器,而 begin()
和 end()
返回的是普通迭代器。主要区别在于:
-
cbegin() 和 cend():
- 返回常量迭代器。
- 用于遍历容器中的元素,但不能修改这些元素。
cbegin()
返回指向容器第一个元素的常量迭代器。cend()
返回指向容器尾后位置的常量迭代器。
-
begin() 和 end():
- 返回普通迭代器。
- 可以用于遍历容器中的元素,并且可以修改这些元素。
begin()
返回指向容器第一个元素的迭代器。end()
返回指向容器尾后位置的迭代器。
-
crbegin() 和 crend():
- 返回常量逆向迭代器。
- 用于逆序遍历容器中的元素,并且不能修改这些元素。
crbegin()
返回指向容器最后一个元素的常量逆向迭代器。crend()
返回指向容器起始位置的常量逆向迭代器。
-
rbegin() 和 rend():
- 返回普通逆向迭代器。
- 用于逆序遍历容器中的元素,并且可以修改这些元素。
rbegin()
返回指向容器最后一个元素的逆向迭代器。rend()
返回指向容器起始位置的逆向迭代器。
#include <iostream>
#include <vector>int main() {// 创建一个vectorstd::vector<int> vec = {1, 2, 3, 4, 5};// 使用 cbegin() 和 cend() 遍历容器中的元素(不修改元素)std::cout << "Using cbegin() and cend(): ";for (auto it = vec.cbegin(); it != vec.cend(); ++it) {std::cout << *it << " ";}std::cout << std::endl;// 使用 begin() 和 end() 遍历容器中的元素(可以修改元素)std::cout << "Using begin() and end(): ";for (auto it = vec.begin(); it != vec.end(); ++it) {*it *= 2; // 修改元素的值std::cout << *it << " ";}std::cout << std::endl;// 使用 crbegin() 和 crend() 逆序遍历容器中的元素(不修改元素)std::cout << "Using crbegin() and crend(): ";for (auto it = vec.crbegin(); it != vec.crend(); ++it) {std::cout << *it << " ";}std::cout << std::endl;// 使用 rbegin() 和 rend() 逆序遍历容器中的元素(可以修改元素)std::cout << "Using rbegin() and rend(): ";for (auto it = vec.rbegin(); it != vec.rend(); ++it) {*it *= 2; // 修改元素的值std::cout << *it << " ";}std::cout << std::endl;return 0;
}
-
>>
为避免与运算符>>混淆,C++11要求在声明嵌套模板时使用空格将尖括号分开
std::vector<std::pair<int, std::string> > myVector; // 正确
-
左值引用
传统C++引用(现称左值引用)使得标识符关联到左值,
左值引用的声明
int x = 5;
int& refX = x; // refX 是对 x 的左值引用
左值引用作为函数参数
左值引用经常用作函数的参数,可以实现按引用传递,避免不必要的复制。
void increment(int& num) {num++; // 修改传入的参数
}int main() {int value = 10;increment(value); // 传入 value 的引用std::cout << value << std::endl; // 输出 11,因为 value 已被增加return 0;
}
左值引用和赋值
int x = 5;
int y = 10;
int& ref = x; // ref 引用 x
ref = y; // 将 x 的值改为 y 的值,即 x 变为 10
虽然引用和取地址都可以用于访问对象,但它们的实现方式和用途不同:
- 引用是对象的别名,提供了对对象的另一种访问方式,更方便、更安全地操作对象。
- 取地址是获取对象在内存中的地址,返回的是指向目标对象的指针,可以通过指针来直接访问或修改对象的值,但需要注意指针的正确使用和管理。
引用示例:
#include <iostream>int main() {int x = 10;int& ref = x; // 定义引用 ref,绑定到变量 xstd::cout << "x 的值为:" << x << std::endl;std::cout << "ref 的值为:" << ref << std::endl;ref = 20; // 通过引用修改 x 的值std::cout << "修改后,x 的值为:" << x << std::endl;std::cout << "修改后,ref 的值为:" << ref << std::endl;return 0;
}
取地址示例:
#include <iostream>int main() {int x = 10;int* ptr = &x; // 取得变量 x 的地址,并将其保存到指针 ptr 中std::cout << "x 的值为:" << x << std::endl;std::cout << "ptr 指向的值为:" << *ptr << std::endl;*ptr = 20; // 通过指针修改 x 的值std::cout << "修改后,x 的值为:" << x << std::endl;std::cout << "修改后,ptr 指向的值为:" << *ptr << std::endl;return 0;
}
- 左值引用必须绑定到一个具有持久性的对象,因此不能绑定到临时对象(右值)。
- 左值引用一般用于实现按引用传递,可以在函数内部修改传入的参数,而不是复制参数的副本。
-
右值引用
右值引用是 C++11 引入的新特性,用于实现移动语义和完美转发,主要用于优化对象的拷贝和移动操作。
右值引用的声明方式是在类型后面加上 &&
,例如 int&&
表示一个右值引用类型。
int&& rvalue_ref = 5; // 右值引用绑定到临时对象 5
右值引用主要用于绑定到临时对象(右值),通常用于移动语义和完美转发。
int&& rvalue_ref = 5; // 右值引用绑定到临时对象 5int x = 10;
int&& rvalue_ref2 = std::move(x); // std::move 将左值转换为右值引用
std::move
是一个 C++ 中的函数模板,主要用于将对象转换为右值引用,从而支持移动语义。它的主要作用有两个:
-
标识对象为右值引用:
std::move
将对象转换为右值引用,即使原本是左值,这样就可以在移动构造函数和移动赋值运算符中使用。这使得我们可以使用移动语义,将资源从一个对象转移到另一个对象,而不是进行深拷贝。这样可以提高程序的性能和效率。 -
避免不必要的拷贝: 通过将对象转换为右值引用,
std::move
告诉编译器该对象可以被移动而不是复制,从而避免不必要的拷贝操作。这对于大型对象或者资源管理类(如动态分配的内存、文件句柄等)尤其有用,因为移动操作通常比复制操作更加高效。
右值引用可以用于传递临时对象或者转移对象的所有权。
void process(int&& data) {// 处理右值引用绑定的临时对象
}int main() {process(10); // 传递临时对象int x = 20;process(std::move(x)); // 转移对象的所有权return 0;
}
移动语义
传统的拷贝操作会导致对象的深拷贝,即复制对象的所有内容,包括动态分配的内存资源,这在处理大型对象时可能效率低下。而移动语义允许在资源管理类中,将资源的所有权从一个对象转移到另一个对象,而不需要进行深拷贝,从而提高了效率。
移动语义的关键在于利用右值引用来识别临时对象(右值),然后通过移动构造函数或者移动赋值运算符来“窃取”这些临时对象的资源,而不是像拷贝构造函数那样创建新的资源副本。
移动语义的实现通常使用 std::move
来将对象转换为右值引用,以便移动构造函数和移动赋值运算符能够正确地被调用。通过移动语义,可以有效地避免不必要的资源复制,提高程序的性能。
完美转发
完美转发是一种技术,允许我们在函数中将参数以相同的方式传递给其他函数,保持参数的值类型不变。它通过右值引用和模板来实现。std::forward
是实现完美转发的关键,它能够在保持参数类型不变的同时,将参数转发给其他函数。
简单的参数传递:如果你只是简单地将参数传递给其他函数,而不需要保留其值类别(即不需要完美转发),那么你可以直接使用传递给你的参数。在这种情况下,不使用 std::forward
是可以的。
template<typename T>
void foo(T arg) {bar(arg); // 没有保留参数的值类别的必要性
}
复杂的参数传递:但是,在需要将参数完美转发给其他函数的情况下,特别是在涉及到重载或泛型编程时,使用 std::forward
更为安全。这样可以确保参数的原始值类别被保留,从而正确地调用相关函数。使用 std::forward
可以避免意外地触发拷贝构造函数或移动构造函数,从而提高了代码的效率和安全性。
template<typename T>
void foo(T&& arg) {bar(std::forward<T>(arg)); // 保留参数的值类别
}
#include <iostream>
#include <utility> // for std::move, std::forwardtemplate<typename T>
class MoveOnlyVector {
private:T* data;size_t capacity;size_t size;public:// 默认构造函数MoveOnlyVector() : data(nullptr), capacity(0), size(0) {}// 析构函数~MoveOnlyVector() {delete[] data;}// 移动构造函数MoveOnlyVector(MoveOnlyVector&& other) noexcept : data(std::exchange(other.data, nullptr)), capacity(std::exchange(other.capacity, 0)), size(std::exchange(other.size, 0)) {}// 移动赋值运算符MoveOnlyVector& operator=(MoveOnlyVector&& other) noexcept {if (this != &other) {delete[] data;data = std::exchange(other.data, nullptr);capacity = std::exchange(other.capacity, 0);size = std::exchange(other.size, 0);}return *this;}// 添加元素template<typename U>void push_back(U&& value) {if (size >= capacity) {// 扩展容量size_t new_capacity = (capacity == 0) ? 1 : 2 * capacity;T* new_data = new T[new_capacity];for (size_t i = 0; i < size; ++i) {new_data[i] = std::move(data[i]);}delete[] data;data = new_data;capacity = new_capacity;}data[size++] = std::forward<U>(value);}
};int main() {MoveOnlyVector<std::string> vec;// 添加元素vec.push_back("hello");vec.push_back(std::string("world"));return 0;
}
五:绑定器和函数对象
-
function
std::function
是C++11中的一个模板类,用于封装任意可调用对象,包括函数指针、函数对象、成员函数指针、Lambda表达式等。它提供了一种统一的方式来处理不同类型的可调用对象,并可以在运行时确定其类型。
#include <functional>// 定义一个函数
int add(int x, int y) {return x + y;
}// 使用 std::function 封装一个可调用对象
std::function<int(int, int)> func = add;// 调用封装的函数
int result = func(3, 4); // result = 7
std::function
的模板参数是函数的签名,它可以用来存储具有相同参数和返回类型的任意可调用对象。
-
bind
std::bind
是C++11中的一个函数模板,用于部分应用函数参数或重新排序函数参数。它允许您在调用函数时固定某些参数的值,从而创建一个新的可调用对象。绑定器(对STL中bind1st和bind2nd的升级,结合二元函数对象-》一元函数对象)
#include <functional>// 定义一个函数
int add(int x, int y) {return x + y;
}auto add_five = std::bind(add, std::placeholders::_1, 5);
int result = add_five(3); // result = 3 + 5 = 8
在这个例子中,std::bind
将 add
函数的第一个参数绑定为占位符 std::placeholders::_1
,并将第二个参数固定为 5。返回的 add_five
可调用对象只有一个参数,当调用它时,它会将传递给它的参数与之前绑定的参数一起传递给 add
函数。
-
Lambda表达式
Lambda表达式是C++11中引入的一种匿名函数语法,它允许您在需要函数对象的地方内联定义函数。Lambda表达式可以捕获外部变量,并具有非常灵活的语法。
#include <iostream>int main() {int x = 3;int y = 4;// 使用Lambda表达式定义一个函数对象auto func = [x, y](int a, int b) {return a * x + b * y;};int result = func(1, 2); // result = 1 * 3 + 2 * 4 = 11std::cout << "Result: " << result << std::endl;return 0;
}
六:C++语言级别支持的多线程编程
C++的标准库在C++11版本中引入了多线程支持,这为跨平台多线程编程提供了一个统一的接口。这意味着你可以使用C++标准库中的多线程API来编写跨平台的多线程应用程序,而无需依赖于特定的操作系统API(如Windows的CreateThread
、Linux的pthread_create
或clone
)。
1. std::thread
:
std::thread
是C++11中提供的类,它封装了操作系统特定的线程创建和管理方式。这使得你可以在不同的操作系统上使用相同的代码创建和管理线程。
#include <iostream>
#include <thread>// 一个简单的线程函数
void threadFunction() {std::cout << "Thread is running" << std::endl;
}int main() {// 创建一个新线程并运行 threadFunctionstd::thread myThread(threadFunction);// 等待线程结束myThread.join();return 0;
}
在这个例子中,创建了一个线程,执行threadFunction
函数,然后等待该线程结束。std::thread
类允许你创建、启动、加入、分离线程等。
2. std::mutex
和 线程同步:
多线程编程通常需要考虑同步和共享资源的访问。C++标准库提供了各种同步机制,如互斥锁、条件变量等。
#include <iostream>
#include <thread>
#include <mutex>// 共享资源
int counter = 0;
std::mutex mtx;void incrementCounter() {std::lock_guard<std::mutex> lock(mtx);counter++;
}int main() {std::thread t1(incrementCounter);std::thread t2(incrementCounter);t1.join();t2.join();std::cout << "Counter: " << counter << std::endl;return 0;
}
使用std::mutex
来保护共享资源counter
,确保在多线程环境下不会发生数据竞争
3. 其他同步机制:
除了std::mutex
外,C++标准库还提供了std::condition_variable
、std::future
、std::promise
等,用于实现更复杂的线程同步和通信。
假设正在开发一个网络服务器,需要处理来自多个客户端的请求。我们将使用多线程来同时处理这些请求,以提高服务器的性能和并发能力。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <queue>
#include <chrono>// 定义一个任务结构体,用于模拟客户端请求
struct Task {int id;Task(int _id) : id(_id) {}
};// 定义一个线程安全的队列,用于存储待处理的任务
template<typename T>
class SafeQueue {
private:std::queue<T> queue_;std::mutex mutex_;
public:void push(const T& item) {std::lock_guard<std::mutex> lock(mutex_);queue_.push(item);}bool try_pop(T& item) {std::lock_guard<std::mutex> lock(mutex_);if (queue_.empty()) {return false;}item = queue_.front();queue_.pop();return true;}
};// 定义一个处理请求的函数
void processRequest(SafeQueue<Task>& tasks, int threadId) {while (true) {Task task(0);if (tasks.try_pop(task)) {// 模拟处理请求的过程std::cout << "Thread " << threadId << " processing task " << task.id << std::endl;// 模拟请求处理时间std::this_thread::sleep_for(std::chrono::seconds(1));} else {// 如果队列为空,则线程等待新的任务到来std::this_thread::yield();}}
}int main() {const int numThreads = 4;SafeQueue<Task> taskQueue;// 创建多个线程来处理请求std::vector<std::thread> threads;for (int i = 0; i < numThreads; ++i) {threads.emplace_back(processRequest, std::ref(taskQueue), i);}// 模拟生成一些任务并将其放入队列中for (int i = 1; i <= 10; ++i) {taskQueue.push(Task(i));std::this_thread::sleep_for(std::chrono::milliseconds(200));}// 等待所有线程完成任务for (auto& thread : threads) {thread.join();}return 0;
}