c++高级特性

C++高级特性

对象的优化

对象的应用优化

  • c++对对象构造的优化:临时对象生命周期只存在当前语句,用临时对象生成新对象的时候,临时对象不会产生,直接构造新对象
    • Test t4 = Test(20);这里的Test(20)就是一个临时对象,实际上这个临时对象不会产生
    • t4 = Test(30);这里的临时对象是会生成的,因为这里是为了给t4赋值,而不是构造,同义的还有t4 = (Test)30;显式生成临时对象;t4 = 30;隐式生成临时对象
  • 用指针指向临时对象是不安全的,比如Test* p1 = &Test(30);因为出了这条语句,临时对象就析构了
  • 用引用指向临时对象是可以的,因为引用相当于别名,使用引用后临时对象的生存周期变成了引用的生存周期,比如const Test &ref = Test(30);

函数的应用优化

  • 函数调用把实参给形参,是初始化(因为此时形参并没有产生),调用拷贝构造函数
  • 当函数结束时,先是析构掉形参变量,再是析构掉函数里的临时对象
  • 在函数中return tmp,由于tmp出了函数之后就没了,所以为了在main函数中用t2接收它,必须在main函数的栈帧中构造一个临时对象,把tmp给到这个临时对象

当这个对象成员有一个指针,该指针指向一片其他内存时,以上函数就会有两个耗费性能的地方

  1. 函数中的对象tmp在main函数栈帧上拷贝构造一个临时对象,在这个过程中临时对象会将tmp指向的内存一个一个拷贝到自己的内存,然后析构掉tmp:为啥不直接让临时对象指向tmp指向的内存?
  2. main函数中的赋值操作会先产生一个临时对象,然后让t2一个一个把临时对象的内存拷贝到自己指向的那片内存,然后把临时对象析构掉:为啥不直接让t2的指针指向临时对象的内存?

解决方法:增加右值引用的拷贝构造和赋值函数

右值:没名字、没内存; 左值:有名字、有内存

一个临时对象就是右值,所以用带右值引用的拷贝构造和赋值函数就能接收它,然后执行不同于普通拷贝构造和赋值函数的操作,比如:

c++ 复制代码
CMsytring(CMystring &&str){
	//直接让自己的指针指向临时对象所指向的资源
	mptr = str.mptr;
	//然后将临时对象的指针置null
	str.mptr = nullptr;
}

新的问题:如果使用到了空间配置器,由于右值引用本身是个左值,所以匹配到的依旧是空间配置器里左值引用的版本,必须使用move语义将左值类型转换为右值,比如:

scss 复制代码
void construct(T *p, T &&val){
	new(p) T(std::move(val));
}

void push_back(T &&val){
	if(full()) expand();
	
	_allocator.construct(_last, std::move(val));
	_last++;
}

有没有什么更简单的优化版本方法?:

  • 函数模板的类型推演 + 引用折叠(也就是&&会抵消,即& + && = &;&& + && = &&)+ forward类型的完美转发

    c++ 复制代码
    template<typename Ty>
    //这样一个函数就能实现既接收左值又接收右值
    void push_back(Ty &&val){
    	if(full()) expand();
    	
    	//而move(左值):是移动语义,只能得到右值类型
    	//能够识别左值或者右值类型,类型的完美转发
    	_allocator.construct(_last, std::forward<Ty>(val));
    	_last++;
    }
    
    template<typename Ty>
    void construct(T *p, Ty &&val){
    	new(p) T(std::forward<Ty>(val));
    }

三条对象优化的规则

  1. 函数参数传递过程中,优先按引用传递
  2. 在函数返回对象时直接返回临时对象,而不要返回一个定义过的对象,这样就是用临时对象构造一个新对象,编译器可以优化,比如return Test(val);
  3. 接收返回值是对象的函数调用时,优先按初始化的方式接收,而不要按赋值的方式接收

智能指针

智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中自动释放资源;

所以智能指针是栈上的,利用栈上的对象出作用域自动析构的特点保证资源不泄露(不可能是堆上的!因为堆上的需要自己手动delete)

还需要解决智能指针的浅拷贝问题:如果CSmartPtr<int> ptr1(new int); CSmartPtr<int> ptr2(ptr1);,由于默认的拷贝构造函数是浅拷贝,所以这两个ptr指向的资源是同一份,可能会导致资源多次释放,delete野指针导致崩溃

在之前的场景里面深拷贝是实现自己的拷贝构造函数,把底层资源复制一份出来;但在指针场景下,CSmartPtr<int> ptr1(new int); CSmartPtr<int> ptr2(ptr1);之后,用户会认为ptr1和ptr2管理的是同一份资源,按照之前的思路不能满足这个需求

  • 解决浅拷贝问题:不带引用计数的智能指针

    • auto_ptr:当ptr2拷贝构造ptr1的时候,将ptr1指向底部资源的指针置null,永远只让最后一个ptr指向资源,前面的所有ptr都是null,不推荐使用这个auto_ptr

    • scoped_ptr:直接把拷贝构造和赋值函数给delete掉,直接不能拷贝构造和赋值,也不太推荐

    • unique_ptr:首先,它也像scoped_ptr把拷贝构造和赋值函数也delete了;但是它支持unique_ptr<int> p2(std::move(ptr1));,因为它提供了带右值引用的拷贝构造和赋值运算符,所以它可以支持在函数中的传参和返回,比如:

      arduino 复制代码
      template<typename T>
      unique_ptr<T> getSmartPtr(){
      	unique_ptr<T> ptr(new T());
      	return ptr;
      }
      
      unique_ptr<int> ptr1 = getSmartPtr<int>();

      如果是scoped_ptr是不行的,因为返回值是先在main函数栈帧上通过拷贝构造生成一个临时对象后再给ptr1的,而拷贝构造函数已经被删了

      在这里用户的用意是很明显的,unique_ptr<int> p2(std::move(ptr1));很明显就是要把ptr1的内容转移给ptr2,这是它跟auto_ptr的区别

  • 解决浅拷贝问题:带引用计数的智能指针,多个智能指针可以管理同一个资源,给每个对象资源匹配一个引用计数,直到引用计数为0的时候才析构

    • shared_ptr:强智能指针,可以改变资源的引用计数

    • weak_ptr:弱智能指针,不会改变资源的引用计数,没有提供*和->,只是一个观察者,不能改变资源

      通过弱智能指针操作强智能指针,再操作资源

      那它有什么用?需要使用提升方法把它转成shared_ptr,才能用*和->,适用于线程安全的地方:当使用指针访问资源的时候,如果资源已经释放会提升失败,资源还在才提升成功,解决多线程访问共享对象的线程安全问题

      C++ 复制代码
      shared_ptr<A> ps = _ptra.lock();
      if(ps != nullptr) ps->testA();
    • 为什么要有强弱两种指针------强智能指针循环引用问题

      • 问题场景:A和B两个智能指针分别指向classA和classB,而classA里又有一个智能指针指向B,classB里有一个智能指针指向A,导致new出来的资源无法释放,资源泄露
      • 解决:定义对象的地方用强智能指针,引用对象的地方用弱智能指针

解决智能指针自动释放所有类型的资源:自定义删除器deletor,因为不是所有类型的资源都是用delete的

  • 方式一:不推荐,因为这个class可能只用一次,但却要特地定义出来

    c++ 复制代码
    template<typename T>
    class Mydeletor{
    public:
    	void opeartor()(T* ptr)const {
    		fclose(ptr);
    	}
    }
    unique_ptr<FILE, mydeletor<FILE>> ptr2(fopen("data.txt","w"));
  • 方式二:lambda表达式,推荐

    php 复制代码
    unique_ptr<FILE, function<void(FILE*)>> ptr(fopen("data.txt","w"),
    	[](FILE *p)->void {
    		fclose(p);
    	}
    );

function和绑定器

function

  • 作用:留下绑定器、函数对象、lambda表达式的类型,以便在多条语句中使用

  • function<void()> func1 = hello; func1(); :使用函数类型实例化function,注意返回值+参数列表是函数类型,而void(*)()是函数指针类型

  • function<int(int,int)> func2 = [](int a,int b)->int {return a + b;}

  • function<void(Test*, string)> func3 = &Test::hello;

  • 应用场景:

    • 当需要根据choice的值使用switch调用不同的函数时:使用switch-case,不好,因为不满足开闭原则,当有需求更改的时候这块switch-case代码也要更改

    • 解决:使用function+map的方法

      scss 复制代码
      map<int,void()> actionmp;
      mp.insert({1, func1});
      mp.insert({2, func2});
      //然后调用的时候
      auto it = actionmp.find(1);
      it->second();
  • 实现原理

    把传入的函数对象,保存到了成员变量里,即保存了传入的函数对象类型

    同时模板处使用typename... A的意思是可变参的类型参数,说明那里是一组参数,可以应对传入不同形参个数的函数对象

函数对象

类似于c语言中的函数指针,区别如下

  1. 通过函数对象调用operator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够inline内联使用,因为在编译阶段看到那个函数指针根本不知道是调用哪个函数,只有在运行时去取地址才知道)效率更高
  2. 因为函数对象使用类生成的,可以添加相关的成员变量,用来记录函数对象使用时的更多的信息

在前文的set和map都可以通过传入不同的函数对象,来实现从大到小排序还是从小到大排序,比如set<int, greater<int>> set1:设定从大到小,(默认是从小到大)

绑定器

bind1st,bind2nd,将operator()的第一个或第二个形参变量绑定成一个确定的值

auto it = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 65));

底层原理,绑定器本质上还是函数对象的一个应用,只不过是传入了一个值给二元函数对象,实质上还是那个二元函数对象在起作用,但bind1st和bind2nd都只能用于二元函数对象,所以c++11推出了更强大的bind

C++11的bind绑定器:返回的结果还是一个函数对象

  • 可以使用它的小括号重载函数,比如:bind(hello, "hello")();
  • 参数占位符,bind(hello, placeholders::_1)("hello");参数占位符,告诉编译器参数被绑定了但还没传,需要在调用的时候由用户进行传递

可以综合使用function和bind进行绑定器的复用

function<void(string)> func1 = bind(hello, placeholders::_1);

lambda表达式

捕获外部变量\] (形参列表) -\>返回值{操作代码}; ` auto func1 = []()->void{cout<<"hello world";}` 其中的\[捕获外部变量\]就是代表等价的函数对象构造函数里需要传入的参数 * \[\],说明不传入任何变量给函数对象 * \[=\],以传值的方式捕获外部的所有变量 > 类似于的函数对象 > > ```arduino > template > class testlambda > { > public: > testlambda(int a, int b): ma(a), mb(b){} > void operator()const{ > .... > } > private: > int ma; > int mb; > } > ``` * \[\&\],以传引用的方式捕获外部的所有变量 * \[this\],捕获外部的this指针 * \[=, \&a\],以传值的方式捕获外部所有变量,但对a以传引用的方式捕获 * \[a, b\],以值传递的方式捕获外部变量a和b 应用场景 * `sort(vec.begin(), vec.end(), [](int a, int b)->bool{ return a > b;}` * `auto it = find_if(vec.begin(), vec.end(), [](int a, int b)->bool{return a < 65});`:找到第一个小于65的数字的迭代器位置 * `for_each(vec.begin(), vec.end(), [](int a){ if(a%2==0) cout<, function> queue([](Data &d1, Data &d2)->bool{ return d1.ma > d2.ma});` 用什么类型表示lambda表达式,从而跨语句使用之前定义好的lambda表达式:function,比如: ```c map> caculatemap; caculatemap[1] = [](int a,int b)->int(return a + b); cout< lock(mtx);`,出了作用域自动释放这把锁,scoped_ptr,不支持拷贝构造和赋值重载,只能在简单的地方使用 * `unique_lock lck(mtx); lck(lock()); lck(unlock());`:可以使用在函数调用、函数返回过程中,适用于线程通信间 atomic原子类型:用CAS的无锁操作来保证简单的++ --的临界区代码互斥 * `std::atomic_int count = 0;` * `volatile std::atomic_int count = 0;`不允许多线程对共享变量进行缓存,更保证代码的正确性 线程间的同步通信:以生产者-消费者模型为例 * `std::condition_variable cv;`定义条件变量 * `unique_lock lck(mtx); cv.wait(lck);`进入等待状态,并释放锁 * `cv.notify_all();`通知其他所有线程 * `cv.notify_one();`

相关推荐
Livingbody2 小时前
基于【ERNIE-4.5-VL-28B-A3B】模型的图片内容分析系统
后端
你的人类朋友3 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
十秒耿直拆包选手3 小时前
Qt:主窗体(QMainwindow)初始化注意事项
c++·qt
追逐时光者4 小时前
面试第一步,先准备一份简洁、优雅的简历模板!
后端·面试
慕木兮人可4 小时前
Docker部署MySQL镜像
spring boot·后端·mysql·docker·ecs服务器
发粪的屎壳郎4 小时前
ASP.NET Core 8 轻松配置Serilog日志
后端·asp.net·serilog
霖005 小时前
C++学习笔记三
运维·开发语言·c++·笔记·学习·fpga开发
mit6.8245 小时前
[shad-PS4] Vulkan渲染器 | 着色器_重新编译器 | SPIR-V 格式
c++·游戏引擎·ps4
倔强青铜三5 小时前
苦练Python第4天:Python变量与数据类型入门
前端·后端·python
倔强青铜三5 小时前
苦练Python第3天:Hello, World! + input()
前端·后端·python