C++11/14/17/20新特性

1、智能指针

智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。

std::unique_ptr

std::unique_ptr是通过指针占有并管理另一对象,并在unique_ptr离开作用域时释放该对象的智能指针,在下列两者之一发生时用关联的删除器释放对象:

  • 销毁了管理的 unique_ptr 对象

  • 通过 operator= 或 reset() 赋值另一指针给管理的 unique_ptr 对象。

std::unique_ptr方法

void reset():减少一个引用计数

std::unique_ptr get():将会返回std::unique_ptr的指针

std::unique_ptr release():返回std::unique_ptr的指针,并且将std::unique_ptr设置为nullptr,但是注意release并不会释放其占用的内存空间

std::move(std::unique_ptr<T> A):指针因为具有唯一性,因此不能被复制,但是可以转移。转移后,原先的智能指针会被设置为nullptr。

std::shared_ptr

std::shared_ptr 是通过指针保持对象共享所有权的智能指针。多个 shared_ptr 对象可占有同一对象。下列情况之一出现时销毁对象并释放其内存:

  • 最后剩下的占有对象的 shared_ptr 被销毁;

  • 最后剩下的占有对象的 shared_ptr 被通过 operator= 或 reset() 赋值为另一指针。

std::shared_ptr方法

std::shared_ptr get():获取指针,存储的指针指向 shared_ptr 对象解引用的对象,通常与其拥有的指针相同。头文件为#include <memory>

void reset():减少一个引用计数

void swap (shared_ptr& x):交换内存

long int use_count():获取引用计数值,共享指针为空返回0

bool unique:检测是否唯一

shared_ptr make_shared (Args&&… args):创建共享指针

shared_ptr static_pointer_cast (const shared_ptr& sp):转换指针类型

std::weak_ptr

std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。

2、动态内存分配

std::unique_ptr<T> std::make_unique<T> 函数用于创建动态分配内存的对象,并以unique_ptr类型智能指针管理分配内存。std::make_unique函数会调用对象的构造方法来创建unique_ptr对象。

std::shared_ptr<T> std::make_shared<T> 函数是C++11中引入的一个模板函数,用于创建指定类型的std::shared_ptr智能指针。它可以在堆内存上创建对象,并返回一个指向该对象的std::shared_ptr指针。由于是通过shared_ptr管理内存,因此一种安全分配和使用动态内存的方法。std::make_shared函数会调用构造方法来创建一个shared_ptr。

std::unique_ptr<T> std::make_unique<T>与std::shared_ptr<T> std::make_shared<T>的区别:

1:分配除的指针,前者是不能赋值,只能move,后者可以赋值。

2:前者使用智能指针数组,后者不行(因为后者分配出来的指针具有单一性)

3、移动和完美转移

std::move 的作用是把左值引用强转为右值引用,它只是进行了左右值的转换,也就是类型的转换,并没有改变对象本身。

std::forward 的情况和std::move类似,但是和std::move无条件地将它的参数转化为rvalue不同,std::forward在特定的条件下才会执行转化。

右值引用可以理解为:把本来即将消亡的临时变量继续使用,右值引用的作用是在某些场景下降低开销。

请注意,用std::move转换过的变量不要再使用了。

#include <iostream>
#include <string>
 
 
void main(void){
    std::string strA = "Hello";
    std::cout << "HHHA:0====>strA=\"" << strA << "\"\n";
 
    std::string strB = std::move(strA);
    std::cout << "HHHA:1====>strA=\"" << strA << "\"\n";
    std::cout << "HHHA:2====>strB=\"" << strB << "\"\n";
}

输出:

语句std::move(strA) 把变量strA强制转成了右值,strA所指向的实体将随着赋值语句的结束而消亡。对strB而言,等效于:

std::string strB = "Hello";

4、Lambda 表达式

Lambda 表达式是现代 C++ 中最重要的特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。这样的场景其实有很多很多,所以匿名函数几乎是现代编程语言的标配。

Lambda 表达式的基本语法如下:

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {

// 函数体

}

上面的语法规则除了 [捕获列表] 内的东西外,其他部分都很好理解,只是一般函数的函数名被略去,返回值使用了一个 -> 的形式进行

所谓捕获列表,其实可以理解为参数的一种类型,Lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:

1. 值捕获 与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 Lambda

表达式被创建时拷贝,而非调用时才拷贝:

void lambda_value_capture() {
    int value = 1;
    auto copy_value = [value] {
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 1, 而 value == 100.
    // 因为 copy_value 在创建时就保存了一份 value 的拷贝
}

2. 引用捕获 与引用传参类似,引用捕获保存的是引用,值会发生变化。

void lambda_reference_capture() {
    int value = 1;
    auto copy_value = [&value] {
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 100, value == 100.
    // 因为 copy_value 保存的是引用
}

3. 隐式捕获

手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获.

总结一下,捕获提供了 Lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:

• [] 空捕获列表

• [name1, name2, . . . ] 捕获一系列变量

• [&] 引用捕获, 让编译器自行推导引用列表

• [=] 值捕获, 让编译器自行推导值捕获列表

4. 表达式捕获

这部分内容需要了解后面马上要提到的右值引用以及智能指针上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。

C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获,被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto 本质上是相同的:

#include <iostream>
#include <utility>

int main() {
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
        return x+y+v1+(*v2);
    };
    std::cout << add(3,4) << std::endl;
    return 0;
}

在上面的代码中,important 是一个独占指针,是不能够被捕获到的,这时候我们需要将其转移为右值,在表达式中初始化。

泛型 Lambda

Lambda 函数的形式参数可以使用 auto关键字来产生意义上的泛型:

auto add = [](auto x, auto y) {
    return x+y;
};
add(1, 2);
add(1.1, 2.2);

5、函数对象包装器

std::function是C++11标准库中提供的一种可调用对象的通用类型,它可以存储任意可调用对象,如函数指针,函数对象,成员函数指针和lambda表达式。std::function类模板是一个类似于函数指针的类型,但它是可以处理任意可调用对象的,并且可以检查调用对象是否为空。

基本语法:

std::function<return_type(parameter_types)> var_name;

        其中,return_type是函数返回值类型,parameter_types是函数参数类型。

int func(int x, int y) { return x + y; }
std::function<int(int, int)> f = func;
 
class A {
public:
    int mem_func(int x) { return x * x; }
};
std::function<int(A*, int)> f2 = &A::mem_func;

6、多线程与并发

std::thread 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含<thread>头文件,它提供了很多基本的线程操作,例如 get_id() 来获取所创建线程的线程 ID,使用join() 来加入一个线程等等,例如:

#include <iostream>
#include <thread>
int main() {
    std::thread t([](){
        std::cout << "hello world." << std::endl;
    });
    t.join();
    return 0;
}

std::mutex 是 C++11 中最基本的 mutex 类,通过实例化 std::mutex 可以创建互斥量,而通过其成员函数 lock() 可以进行上锁,unlock() 可以进行解锁。但是在在实际编写代码的过程中,最好不去直接调用成员函数,因为调用成员函数就需要在每个临界区的出口处调用 unlock(),当然,还包括异常。这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类 std::lock_guard。RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。

在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分,例如:

#include <iostream>
#include <thread>

int v = 1;
void critical_section(int change_v) {
    static std::mutex mtx;
    std::lock_guard lock(mtx);
    // 执行竞争操作
    v = change_v;
    // 离开此作用域后 mtx 会被释放
}


int main() {
    std::thread t1(critical_section, 2), t2(critical_section, 3);
    t1.join();
    t2.join();
    std::cout << v << std::endl;
    return 0;
}

由于 C++ 保证了所有栈对象在声明周期结束时会被销毁,所以这样的代码也是异常安全的。无论critical_section() 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 unlock()。

而 std::unique_lock 则相对于 std::lock_guard 出现的,std::unique_lock 更加灵活,std::unique_lock 的对象会以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex对象的所有权)的方式管理 mutex 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用std::unique_lock。

std::lock_guard 不能显式的调用 lock 和 unlock,而 std::unique_lock 可以在声明后的任意位置调用,可以缩小锁的作用范围,提供更高的并发度。

如果你用到了条件变量 std::condition_variable::wait 则必须使用 std::unique_lock 作为参数。

#include <iostream>
#include <thread>

int v = 1;
void critical_section(int change_v) {
    static std::mutex mtx;
    std::unique_lock lock(mtx);
    // 执行竞争操作
    v = change_v;
    std::cout << v << std::endl;
    // 将锁进行释放
    lock.unlock();
    // 在此期间,任何人都可以抢夺 v 的持有权
    // 开始另一组竞争操作,再次加锁
    lock.lock();
    v += 1;
    std::cout << v << std::endl;
}


int main() {
    std::thread t1(critical_section, 2), t2(critical_section, 3);
    t1.join();
    t2.join();
    return 0;
}

7、容器

emplace

C++11中,针对顺序容器(如vector、deque、list),新标准引入了三个新成员:emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。

emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。

其它容器中,std::forward_list中的emplace_after、emplace_front函数,std::map/std::multimap中的emplace、emplace_hint函数,std::set/std::multiset中的emplace、emplace_hint,std::stack中的emplace函数,等emplace相似函数操作也均是构造而不是拷贝元素。

emplace相关函数可以减少内存拷贝和移动。当插入rvalue,它节约了一次move构造,当插入lvalue,它节约了一次copy构造。

8、bind对象、函数对象

std::bind

std::bind可以看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。

std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:

将可调用对象和其参数绑定成一个仿函数;

只绑定部分参数,减少可调用对象传入的参数。

调用bind的一般形式:

auto newCallable = bind(callable, arg_list);

该形式表达的意思是:当调用newCallable时,会调用callable,并传给它arg_list中的参数。

需要注意的是:arg_list中的参数可能包含形如_n的名字。其中n是一个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

直接文字可能不那么生动,不如看代码:

#include <iostream>
#include <functional>


class A {
public:
    void fun_3(int k,int m) {
        std::cout << "print: k = "<< k << ", m = " << m << std::endl;
    }
};


void fun_1(int x,int y,int z) {
    std::cout << "print: x = " << x << ", y = " << y << ", z = " << z << std::endl;
}


void fun_2(int &a,int &b) {
    ++a;
    ++b;
    std::cout << "print: a = " << a << ", b = " << b << std::endl;
}


int main(int argc, char * argv[]) {
    //f1的类型为 function<void(int, int, int)>
    auto f1 = std::bind(fun_1, 1, 2, 3); 					//表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
    f1(); 													//print: x=1,y=2,z=3


    auto f2 = std::bind(fun_1, std::placeholders::_1, std::placeholders::_2, 3);
    //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定
    f2(1, 2);												//print: x=1,y=2,z=3
 
    auto f3 = std::bind(fun_1, std::placeholders::_2, std::placeholders::_1, 3);
    //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定
    //注意: f2  和  f3 的区别。
    f3(1, 2);												//print: x=2,y=1,z=3


    int m = 2;
    int n = 3;
    auto f4 = std::bind(fun_2, std::placeholders::_1, n); //表示绑定fun_2的第一个参数为n, fun_2的第二个参数由调用f4的第一个参数(_1)指定。
    f4(m); 													//print: a=3,b=4
    std::cout << "m = " << m << std::endl;					//m=3  说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的,如m
    std::cout << "n = " << n << std::endl;					//n=3  说明:bind对于预先绑定的函数参数是通过值传递的,如n
    
    A a;
    //f5的类型为 function<void(int, int)>
    auto f5 = std::bind(&A::fun_3, &a, std::placeholders::_1, std::placeholders::_2); //使用auto关键字
    f5(10, 20);												//调用a.fun_3(10,20),print: k=10,m=20


    std::function<void(int,int)> fc = std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2);
    fc(10, 20);   											//调用a.fun_3(10,20) print: k=10,m=20 


    return 0; 
}

std::function

std::function是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。

定义function的一般形式:

std::function<函数类型>

例子:

# include <iostream>
# include <functional>


typedef std::function<int(int, int)> comfun;


// 普通函数
int add(int a, int b) { return a + b; }


// lambda表达式
auto mod = [](int a, int b){ return a % b; };


// 函数对象类
struct divide{
    int operator()(int denominator, int divisor){
        return denominator/divisor;
    }
};


int main(){
	comfun a = add;
	comfun b = mod;
	comfun c = divide();
    std::cout << a(5, 3) << std::endl;
    std::cout << b(5, 3) << std::endl;
    std::cout << c(5, 3) << std::endl;
}

std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。

故而,std::function的作用可以归结于:

std::function对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的std::function对象,简化调用;

std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(如:函数指针这类可调用实体,是类型不安全的)。

9、模板

std::reference_wrapper

在C++中,std::reference_wrapper(标准引用包装器)是一个模板类,它的主要目的是在需要"引用语义"而不是"值语义"的地方提供一个可复制的引用。换句话说,它是一个对象,该对象包含一个指向特定对象的引用,并且可以像普通对象一样进行复制和赋值。

在日常英语交流中,我们可以这样描述std::reference_wrapper: “The std::reference_wrapper in C++ is a template class that provides a copyable reference when a reference semantic is needed instead of a value semantic.” (C++中的std::reference_wrapper是一个模板类,当需要引用语义而不是值语义时,它提供了一个可复制的引用。)

在这个句子中,“The std::reference_wrapper in C++ is a template class”(C++中的std::reference_wrapper是一个模板类)是主句,描述了std::reference_wrapper的基本属性。“that provides a copyable reference when a reference semantic is needed instead of a value semantic”(当需要引用语义而不是值语义时,它提供了一个可复制的引用)是一个定语从句,用来进一步描述std::reference_wrapper的特性和用途。

在C++编程实践中,std::reference_wrapper的使用场景非常广泛,例如在容器中存储引用,在std::bind和std::function中使用引用,以及在泛型编程中使用引用。

下面是一个简单的std::reference_wrapper的使用示例:

#include <iostream>
#include <functional>


void print(int& x) {
    std::cout << "x: " << x << std::endl;
}


int main() {
    int a = 5;
    std::reference_wrapper<int> ref_a = a;
    print(ref_a);  // 输出:x: 5
    return 0;
}

在这个示例中,我们创建了一个std::reference_wrapper对象ref_a,它包含对整数a的引用。然后,我们将ref_a传递给print函数,该函数接受一个整数引用作为参数。这个例子展示了如何使用std::reference_wrapper来传递引用。

在C++中,引用本身并不是一个对象,因此不能直接存储。但指针是一个对象,可以存储和复制。因此,std::reference_wrapper 使用指针来间接地表示引用。

考虑以下简化的 std::reference_wrapper 的实现:

template <typename T>
class reference_wrapper {
private:
    T* ptr;  // 指向引用对象的指针


public:
    // 构造函数,接受一个引用,并存储其地址
    reference_wrapper(T& ref) : ptr(&ref) {}


    // 返回存储的引用
    T& get() const {
        return *ptr;
    }


    // 重载函数调用操作符,使其可以像引用一样使用
    operator T& () const {
        return *ptr;
    }
};

在上面的代码中,reference_wrapper 的构造函数接受一个引用,并存储其地址。这样,即使引用本身不能被存储,我们也可以通过存储引用的地址(即指针)来间接地表示引用。

这种设计允许 std::reference_wrapper 在容器中存储引用,并在需要时返回原始引用。

例如:

int x = 10;
std::reference_wrapper<int> ref(x);


int& y = ref.get();  // y 是 x 的引用
y = 20;  // 修改 y 也会修改 x


std::cout << x;  // 输出 20

这就是 std::reference_wrapper 如何使用指针来间接地表示引用的原因。

std::reference_wrapper 包含以下成员函数:

reference_wrapper(T&) noexcept:这是一个构造函数,用于创建一个新的 std::reference_wrapper 对象。这个对象会保存对 T 类型对象的引用。

reference_wrapper(T&&) = delete:这个构造函数已被删除,防止临时变量的引用。

operator T& () const noexcept:这个成员函数用于将 std::reference_wrapper 对象转换为它包装的引用。

T& get() const noexcept:这个成员函数用于获取到 std::reference_wrapper 包装的引用。

reference_wrapper(const reference_wrapper&) noexcept = default:这是一个复制构造函数,用于创建新的 std::reference_wrapper 对象,这个新对象会包含与原对象同样的引用。

reference_wrapper& operator=(const reference_wrapper& x) noexcept = default:这是一个赋值操作符,允许一个 std::reference_wrapper 对象赋值给另一个对象。

以上成员函数的目的都是提供一个能够重新赋值的引用的包装,这是因为在 C++ 中,常规的引用一旦初始化后就不能改变。通过使用 std::reference_wrapper,开发者可以实现引用的重新赋值。

  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值