????????C++11作为一个重要的版本,引入了很多新的特性,解决了C++语言本身很多遗留的内存泄露问题,并且提供了很多比较灵活的用法。引入的auto,智能指针、线程机制都使得C++语言的灵活性、安全性、并发性有了很大的提升。
? ? ? ? 本文会比较详细的介绍C++11引入的新特性,以及各自引入的原因和基本的用法。
目录
? ? ? ? C++11引入的新特性基本上可以分为并发编程支持、泛型编程支持和简化使用三部分。
如下图是详细的分类:
C++11内存模型是一种抽象,它定义了程序中各种变量(如指针、数组、对象等)的访问方式,以及在多线程环境下的并发操作行为。内存模型解决了由于编译器优化、指令重排等原因导致的可见性问题,从而使得程序员可以更好地控制程序的执行顺序,避免出现竞态条件等问题。
C++11内存模型的主要特性包括:
原子操作:C++11标准对原子操作进行了定义,并引入了一些新的原子类型和函数,以支持无锁编程。原子操作就是对一个内存上变量(或者叫左值)的读取-变更-存储(load-add-store)作为一个整体一次完成。
同步原语:C++11提供了一些同步原语,如std::mutex、std::condition_variable等,用于实现线程间的同步和互斥。
内存模型:C++11从各种不同的平台上抽象出了一个软件的内存模型,并以内存顺序进行描述,以使得想进一步挖掘并行系统性能的程序员有足够简单的手段来完成以往只能通过底层编程技术实现的功能。
禁止重排序:C++11内存模型规定了某些特定的内存操作不能被重排序。
内存屏障:C++11内存模型引入了内存屏障(memory barrier)的概念,用于控制内存访问的顺序,以避免编译器对代码的重排导致的问题。
内存序:C++11标准中提供了6种memory order,来描述内存模型。
例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <vector>
std::mutex mtx; // 定义互斥量
std::atomic<int> x(0); // 定义原子变量
std::vector<int> data; // 定义数据容器
void reader() {
mtx.lock(); // 获取锁
data.push_back(x.load(std::memory_order_relaxed)); // 读取原子变量的值
mtx.unlock(); // 释放锁
}
void writer() {
mtx.lock(); // 获取锁
x.store(10, std::memory_order_release); // 修改原子变量的值
mtx.unlock(); // 释放锁
}
int main() {
std::thread t1(reader); // 创建读线程t1
std::thread t2(writer); // 创建写线程t2
t1.join(); // 等待读线程结束
t2.join(); // 等待写线程结束
return 0;
}
????????C++11还提供了一些同步机制,例如互斥锁、条件变量、自旋锁、读写锁等。其中,条件变量是一种同步原语,用于阻塞一个或多个线程,直到接收到另一个线程的通知信号,或暂停信号,或伪唤醒信号。自旋锁是一种忙等待锁,它在等待锁的过程中不会使线程进入睡眠状态。读写锁允许多个线程同时读取共享资源,但在写入时只允许一个线程访问。
? mutex——系统的互斥锁,支持 lock()、unlock() 和保证 unlock() 的 RAII 方式
? condition_variable——系统中线程间进行事件通信的条件变量
? thread_local——线程本地存储
线程和锁模型需要使用某种形式的同步来避免竞争条件。C++11 为此提供了标准 的 mutex(互斥锁)。
????????在C++11中,期值(future)是一种用于异步计算的对象,它代表了一个尚未完成但将来会得到的值。期值的主要用途是实现并发编程中的异步操作,例如在一个线程中执行某个耗时的操作,然后在另一个线程中等待该操作的结果。
C++11主要提供的API
future——一个句柄,通过它你可以从一个共享的单对象缓冲区中 get() 一个值,可能需要等待某个 promise 将该值放入缓冲区。
? promise——一个句柄,通过它你可以将一个值 put() 到一个共享的单对象缓冲区,可能会唤醒某个等待 future 的 thread。
? packaged_task——一个类,它使得设置一个函数在线程上异步执行变得容易,由 future 来接受 promise 返回的结果。
? async()——一个函数,可以启动一个任务并在另一个 thread 上执行。
可以通过std::future来获取期值,其基本用法如下:
#include <iostream>
#include <thread>
#include <future>
int main() {
// 创建一个异步任务
std::future<int> fut = std::async(std::launch::async, [](){
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
return 42; // 返回结果
});
// 在另一个线程中等待结果
std::cout << "Result: " << fut.get() << std::endl; // 输出结果
return 0;
}
一个例子:
#include <iostream>
#include <future>
int main() {
std::future<int> result;
// 启动一个异步任务来计算两个数之和
result = std::async(std::launch::async, [](int a, int b) {
return a + b;
}, 3, 4);
// 在主线程中等待异步任务完成并获取结果
int sum = result.get();
std::cout << "两数之和为:" << sum << std::endl;
return 0;
}
C++11引入了lambda表达式,它是一种匿名函数,可以在代码中直接定义和使用。lambda表达式的语法如下:
[capture](parameters) -> return_type { function_body }
其中:
下面是一个简单的lambda表达式示例:
#include <iostream>
int main() {
auto add = [](int a, int b) -> int { return a + b; };
int sum = add(3, 4);
std::cout << "Sum: " << sum << std::endl;
return 0;
}
捕获列表的几种形式:
1.不捕获任何外部变量:
[]{ /* code */ }
2.按值捕获所有外部变量:
[=]{ /* code */ }
3.按引用捕获所有外部变量:
[&]{ /* code */ }
4.按值捕获特定变量:
[x]{ /* code */ }
5.按引用捕获特定变量:
[&x]{ /* code */ }
6.混合捕获模式:
[x, &y]{ /* code */ }
7.隐式捕获某些变量,显式捕获其他变量:
[=, &x]{ /* code */ }
[&x, y]{ /* code */ }
例子:
1.不捕获任何外部变量:
[]{ return 23; }
一个简单的 lambda,不使用任何外部变量,返回 23。
2.按值捕获外部变量:
int x = 2;
auto l = [=]{ return x + 1; };
这个 lambda 按值捕获 x,返回 x + 1 的结果。
3.按引用捕获外部变量并修改:
int x = 1;
auto l = [&x]{ ++x; };
按引用捕获 x 并在 lambda 内部对其进行修改。
4.具有参数的 lambda:
auto l = [](int a, int b){ return a + b; };
一个接受两个整数参数并返回它们的和的 lambda。
5.可变 lambda(修改按值捕获的变量):
int x = 1;
auto l = [x]() mutable { return ++x; };
mutable 关键字允许 lambda 修改按值捕获的变量。
6.带有显式返回类型的 lambda:
auto l = [](int a, int b) -> double { return (a + b) / 2.0; };
一个计算平均值的 lambda,显式指定返回类型为 double。
?
C++11中的变参模板允许函数或类模板接受可变数量的参数。在函数模板中,可以使用省略号(...)表示可变数量的参数;在类模板中,可以使用参数包(parameter pack)表示可变数量的类型参数。
下面分别举一个变参函数模板和一个变参类模板的例子:
template<typename T, typename... Args>
void foo(T t, Args... args) {
// ...
}
在上面的例子中,foo
函数模板可以接受任意数量的参数,其中第一个参数的类型为T
,后面的参数类型为Args
。这些参数可以是任何类型,包括基本类型、类类型和其它模板类型。
在函数体内,可以使用递归展开(recursive expansion)来访问这些参数。例如:
template<typename T, typename... Args>
void bar(T t, Args... args) {
foo(t); // 处理第一个参数
bar(args...); // 递归调用自身,处理剩余参数
}
在上面的例子中,bar
函数首先调用foo
函数处理第一个参数,然后使用递归展开的方式调用自身来处理剩余的参数。这样,我们就可以实现类似于printf函数的功能,接受任意数量的参数并进行处理。
template<typename T, typename... Args>
class MyClass {
public:
MyClass(T t, Args... args) {
// ...
}
};
在上面的例子中,MyClass
类模板可以接受任意数量的类型参数。在构造函数中,可以使用参数包展开(parameter pack expansion)来初始化成员变量。例如:
template<typename T, typename... Args>
void bar(T t, Args... args) {
MyClass<T, Args...> obj(t, args...); // 创建对象并初始化成员变量
}
在上面的例子中,我们使用MyClass<T, Args...>
来创建一个对象,并将传入的参数传递给构造函数进行初始化。这样,我们就可以实现类似于printf函数的功能,接受任意数量的参数并进行处理。
一个简单的例子:
#include <iostream>
#include <string>
#include <sstream>
template<typename T>
std::string toString(const T& value) {
std::ostringstream oss;
oss << value;
return oss.str();
}
template<typename T, typename... Args>
std::string toString(const T& value, const Args&... args) {
std::ostringstream oss;
oss << value;
return oss.str() + " " + toString(args...);
}
template<typename... Args>
void myPrintf(const char* format, const Args&... args) {
std::cout << toString(format, args...) << std::endl;
}
int main() {
myPrintf( "Alice", 30,23.8);
return 0;
}
? ? ? ? 运行结果:
Alice 30 23.8
在C++11中,引入了类型别名(Type Alias)的新特性,允许开发者为现有的数据类型创建一个新的名称。这个功能有助于提高代码的可读性、可维护性和可重用性。类型别名可以通过typedef
关键字或using
关键字来定义。
以下两种typedef和using的使用方式:
// 别名
typedef int myInt;
myInt kp;
using IntVector = std::vector<int>;
IntVector ki;
ki.push_back(23);
for (auto i : ki)
{
std::cout << i << std::endl;
}
using MYint = int;
MYint lo = 4;
std::cout << lo << std::endl;
这两种方法在语义上是等效的。另外,值得一提的是,使用using
关键字定义的模板别名相比typedef
更具优势,它更简洁,并且可以完全像新的类模板一样使用。
C++11中的tuple是一个模板类,它可以存储任意数量和类型的数据。它的主要优点是可以方便地访问元组中的元素,并且支持元组的复制、移动和比较等操作。
使用tuple的基本语法如下:
std::tuple<type1, type2, ...> tuple_name(value1, value2, ...);
其中,type1、type2等是元组中元素的类型,value1、value2等是对应的元素值。
例如,我们可以定义一个包含两个int类型和一个string类型的tuple:
#include <iostream>
#include <tuple>
#include <string>
int main() {
std::tuple<int, int, std::string> myTuple(1, 2, "hello");
return 0;
}
要访问tuple中的元素,可以使用std::get函数。例如,如果我们想访问上述定义的tuple中的第一个元素(一个int),可以使用std::get<0>(myTuple)
来获取。需要注意的是,元组的索引是从0开始的。
此外,tuple还支持一些其他操作,例如创建元组的副本、移动元组等。例如,我们可以使用std::make_tuple函数来创建一个包含两个int类型和一个string类型的tuple:
std::tuple<int, int, std::string> anotherTuple = std::make_tuple(3, 4, "world");
我们还可以使用std::tie函数将tuple中的元素解包到多个变量中:
int a, b;
std::string c;
std::tie(a, b, c) = myTuple;
最后,我们可以使用std::tuple_cat函数将两个或多个tuple连接起来:
std::tuple<int, int, std::string> combinedTuple = std::tuple_cat(myTuple, anotherTuple);
结构化绑定:
// tuple
std::tuple<int, int, std::string> myTuple(1, 2, "hello");
auto [value1, value2, value3] = myTuple;
// 结构化绑定
std::cout << value1 << " " << value2 << " " << value3 << " " << std::endl;
C++11中用auto关键字来支持自动类型推导。用decltype推导表达式类型。头文件:#include<typeinfo>
auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。(前提是定义一个变量时对其进行初始化)
auto a = 10; // 10是int型,可以自动推导出a是int
decltype:用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。(decltypde是不需要推导变量初始化的,根据的是表达式对变量的类型就可以推导。)
auto varname = value;
decltype(exp) varname = value;
decltype(10.8) x; //x 被推导成了 double
两者区别:
1,auto用于变量的类型推导,根据初始化表达式的类型来推导变量的类型,常用于简化代码和处理复杂类型。而decltype则用于获取表达式的类型,保留修饰符,并且可以进行表达式求值。
2,auto在初始化时进行类型推导,而decltype直接查询表达式的类型,可以用于任何表达式,包括没有初始化的变量。
3,auto在编译期间确定类型,并且无法更改。而decltype在运行时才确定表达式的类型。
4,auto适用于简单的类型推导,而decltype适用于复杂的类型推导和获取表达式的结果类型。
范围for是C++11中引入的一种新的循环结构,用于遍历容器或数组中的元素。它的基本语法如下:
for (declaration : range) {
// 执行语句
}
其中,declaration表示声明一个变量来存储当前迭代的值,range表示要遍历的范围,可以是容器、数组、字符串等。
例如,遍历一个vector中的元素可以使用以下代码:
#include <iostream>
#include <vector>
int main() {
std::vector<int> myVector = {1, 2, 3, 4, 5};
for (int num : myVector) {
std::cout << num << std::endl;
}
for (auto num : myVector) {
std::cout << num << std::endl;
}
return 0;
}
在这个例子中,我们使用了一个int类型的变量num来存储当前迭代的值,然后通过std::cout输出到控制台。每次循环时,num会自动更新为myVector中的下一个元素。当myVector中的所有元素都被遍历完后,循环结束。这里也可以使用auto来推导。
在C++11中,引入了新的智能指针类,用于更安全和方便地管理动态分配的资源,避免内存泄漏和悬空指针等问题。以下是C++11中的三种主要智能指针:
std::unique_ptr
:
1,std::unique_ptr
?是一种独占式智能指针,用于管理唯一的对象,确保只有一个指针可以访问该对象。
2,使用?std::unique_ptr
?可以自动释放动态分配的内存,当指针超出作用域或被重置时,它会自动删除所管理的对象。
3,通过?std::make_unique
?函数可以创建?std::unique_ptr
?对象,如:std::unique_ptr<int> ptr = std::make_unique<int>(42);
std: :shared_ptr:
1,std::shared_ptr
?是一种共享式智能指针,多个指针可以同时共享对同一对象的拥有权。
2,std::shared_ptr
?使用引用计数技术追踪所管理对象的引用数量,当引用计数变为零时,自动销毁所管理的对象。
3,通过?std::make_shared
?函数可以创建?std::shared_ptr
?对象,如:std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::weak_ptr
:
1,std::weak_ptr
?是一种弱引用智能指针,它可以解决?std::shared_ptr
?的循环引用问题。
2,std::weak_ptr
?指向?std::shared_ptr
?管理的对象,但不会增加引用计数。因此,当所有?std::shared_ptr
?对象超出作用域后,即使还有?std::weak_ptr
?对象存在,所管理的对象也会被销毁。
3,通过?std::shared_ptr
?的?std::weak_ptr
?构造函数可以创建?std::weak_ptr
?对象,如:std::weak_ptr<int> weakPtr = sharedPtr;
C++11引入了统一初始化(Uniform Initialization)的概念,它允许在构造函数参数列表中使用花括号{}来初始化对象。这种初始化方式可以用于创建数组、结构体、类等对象。
例如,对于数组的初始化:
int arr[] = {1, 2, 3}; // 使用花括号{}初始化数组
对于结构体的初始化:
struct MyStruct {
int a;
double b;
char c;
};
MyStruct ms = {1, 2.0, 'c'}; // 使用花括号{}初始化结构体
对于类的初始化:
class MyClass {
public:
int a;
double b;
char c;
};
MyClass mc = {1, 2.0, 'c'}; // 使用花括号{}初始化类
统一初始化还可以用于lambda表达式和捕获变量的初始化:
auto func = [](int x, int y) -> int { return x + y; }; // 使用花括号{}初始化lambda表达式
int a = 10;
int b = 20;
[a, b]() mutable { // 使用花括号{}初始化捕获变量
std::cout << "a: " << a << ", b: " << b << std::endl;
}();
C++11引入了一个新的关键字nullptr,用于表示空指针。它与NULL和0等传统空指针表示方法不同,是一个类型安全的空指针常量。
nullptr的类型是std::nullptr_t,它是一个特殊的整数类型,用于表示空指针。在C++11中,可以使用nullptr来初始化或赋值给任何指针类型,包括原生指针、智能指针和成员指针等。
int* ptr = nullptr; // 使用nullptr初始化原生指针
std::shared_ptr<int> sp = nullptr; // 使用nullptr初始化智能指针
MyClass obj;
obj.setPointer(nullptr); // 使用nullptr赋值给成员指针
需要注意的是,在使用nullptr时,应该尽量避免将其与NULL或0混淆。因为NULL和0在某些情况下可能会被编译器识别为整型字面量,从而导致意外的结果。
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
常量表达式(const experssion)是指:
(1)值不会改变
(2)在编译过程就能得到计算结果的表达式。
constexpr和const的区别:
两者都代表可读,
const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,
而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。
#include<iostream>
using namespace std;
constexpr int func(int i) {
return i + 1;
}
int main() {
int i = 2;
func(i);// 普通函数
func(2);// 编译期间就会被计算出来
}
在C++11中,explicit关键字用于修饰类的构造函数,表示该构造函数是显式的,不能进行隐式类型转换。
class MyClass {
public:
explicit MyClass(int a) {} // 显式构造函数
};
// MyClass obj = 10; // 错误,不能进行隐式类型转换
MyClass obj2(10); // 正确,需要显示调用构造函数
使用explicit关键字可以防止因编译器自动进行类型转换而导致的错误或不必要的代码生成。同时,它也可以强制程序员显式地指定参数类型,提高代码的可读性和安全性。
在C++11中,final和override是两个关键字,用于修饰类的成员函数。
final关键字表示该成员函数不能被继承或覆盖。当一个基类的虚函数被声明为final时,派生类不能再重写该函数。这可以防止派生类无意中修改基类的行为。
override关键字用于显式地指示派生类中的虚函数是覆盖基类中的虚函数。如果没有使用override关键字,编译器可能会因为函数签名不匹配而报错。使用override关键字可以避免这种情况的发生,提高代码的可读性和安全性。
class Base {
public:
virtual void func() final {} // 基类中的虚函数被声明为final,不能被覆盖
};
class Derived : public Base {
public:
void func() override {} // 派生类中的虚函数覆盖了基类中的虚函数,并使用了override关键字
};
C++ 中的右值引用(Rvalue reference)是一种引用类型,它用于绑定到临时对象或将要被移动的对象(右值)。通过右值引用,我们可以对右值进行有效的操作,如移动语义和完美转发。
右值引用的语法是在类型后面加上?&&
,例如?int&&
?表示一个右值引用到 int 类型的对象。右值引用只能绑定到右值,不能绑定到左值。
右值引用主要有两个重要的应用场景:移动语义和完美转发。
【1】移动语义: 右值引用使得我们可以实现高效的资源管理,尤其是在处理动态分配的内存或大型对象时。通过移动语义,我们可以将资源从一个对象转移到另一个对象,避免了不必要的拷贝开销。
通过定义移动构造函数和移动赋值运算符,并使用右值引用参数,可以实现对资源的高效转移。移动构造函数用于在构造对象时从临时或将要被销毁的对象中“窃取”资源,移动赋值运算符用于在对象已存在时将资源从右值赋值给对象。这样,在资源转移完成后,原始对象就不再拥有该资源,而新对象拥有该资源,避免了多余的内存分配和拷贝操作。
【2】完美转发: 完美转发是指在函数模板中保持参数的值类别(左值或右值)并将其转发到其他函数,以实现泛型编程中的通用参数传递。通过使用右值引用和模板参数推导,可以实现参数类型的自动推导和类型保持。
在函数模板中使用右值引用参数可以接收右值和左值,并保持参数的原始类型。结合?std::forward
?可以实现完美转发,将参数以原始类型转发到其他函数。这样,在调用模板函数时,参数的值类别被保留,从而选择正确的函数进行处理。
右值引用在 C++11 中引入,它的出现在很大程度上优化了资源管理和提升了代码的性能。它为移动语义和完美转发提供了重要的基础,并在现代 C++ 开发中广泛应用。
C++11 引入了移动语义(Move Semantics)的概念,旨在提高对象的性能和效率。移动语义通过转移资源所有权,避免不必要的拷贝操作,从而更高效地管理对象。
在传统的拷贝语义中,当我们将一个对象赋值给另一个对象或者作为函数参数传递时,会进行对象的拷贝操作,这包括复制所有成员变量的值、分配内存等。在某些情况下,这种拷贝操作是非常昂贵的,特别是对于大型对象或者资源密集型的操作。
移动语义通过引入右值引用(Rvalue Reference)来解决这个问题。右值引用使用?&&
?语法进行声明,表示一个临时对象或者即将销毁的对象。在移动语义中,我们可以将资源的所有权从一个对象转移到另一个对象,而不需要进行昂贵的拷贝操作。
在使用移动语义时,可以借助?std::move
?函数将左值转换为右值引用,以便进行移动操作。下面是一个简单的示例:
#include <iostream>
#include <vector>
class Person {
public:
Person() {
std::cout << "默认构造函数" << std::endl;
// 假设需要分配大量内存或进行其他资源密集型操作
}
Person(const Person& other) {
std::cout << "拷贝构造函数" << std::endl;
// 实现对象的拷贝操作
}
Person(Person&& other) {
std::cout << "移动语义构造函数" << std::endl;
// 实现对象的移动操作
}
};
int main() {
Person person1; // 调用默认构造函数
Person person2(person1); // 调用拷贝构造函数,拷贝 person1 的值到 person2
Person person3(std::move(person1)); // 调用移动构造函数,将 person1 的值转移到 person3
return 0;
}
通过移动语义,我们可以避免不必要的拷贝操作,提高代码的性能和效率。特别是对于容器类(如?std::vector
、std::string
)或动态分配的资源,利用移动语义可以显著降低内存分配和复制的开销。
需要注意的是,移动构造函数的实现通常是将源对象指针设置为 nullptr,以确保在析构时不会释放已经被转移的资源。此外,移动构造函数和拷贝构造函数应该遵循特定的语义规范,以确保正确、可预期的行为。
完美转发(perfect forwarding)是 C++ 中用于保持传递参数类型和转发函数调用的机制。它通常与模板和右值引用一起使用,以实现泛型编程中的参数传递。
在传统的函数调用中,如果我们想要将一个函数的参数传递给另一个函数,通常可以直接通过值传递或引用传递来实现。但是问题在于,当我们希望将参数以原始类型(值类型或引用类型)传递给另一个函数时,需要显式指定参数的类型,而无法自动推导。
完美转发解决了这个问题,它允许我们在一层函数中接收参数,并将其转发到另一层函数,同时保持参数的原始类型。这样就可以实现泛型编程中的通用参数传递,不再需要手动指定参数类型。
完美转发的核心是使用了两种类型:通用引用和?std::forward
。
auto&&
?或模板参数推导结合引用折叠规则的引用类型。通用引用可以绑定到左值或右值,并保持参数的原始类型。std::forward
?是一个条件转发的工具函数,用于根据参数的原始类型,选择性地将参数转发为左值引用或右值引用。它的使用场景通常是在模板函数或模板类中,用于将参数转发到另一个函数。一个例子:
#include <iostream>
#include "common.h"
using namespace std;
template<typename F ,typename T, typename U>
void testFun(F f, T && t1, U && t2)
{
f(std::forward<T>(t1),std::forward<U>(t2));
}
void gu_y(int && t1, int & t2) // 接收左值和右值
{
cout << t1+t2 << endl;
}
int main(int argc, char *argv[])
{
{
__LOG__("完美转发");
int per1 = 23;
int per2 = 34;
cout << "左右: ";
testFun(gu_y,23,per2);// 传入右值和左值
}
return 0;
}
? ? ? ? 运行结果:
对于T && t1 ,t1始终是变量是左值,因此在传入函数gu_y()时,需要将一个左值转换成右值,又由于这是一个模板函数,因此不可以使用移动语义将其直接固定为右值,所以提出了完美转发。