IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C/C++进阶知识点(C++11新特性) -> 正文阅读

[C++知识库]C/C++进阶知识点(C++11新特性)

手撕虚函数、拿捏C++11新特性,硬核干货持续更新>>>>>
码字不易,给个支持>>>>>

在这里插入图片描述

一.C

1.memset与memcpy

1.1 memset

memset:
memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。通常用于对刚申请到的内存进行初始化。

void *memset(void *s, int c, size_t n); 
  • s指向要填充的内存块。
  • c是要被设置的值。
  • n是要被设置该值的字符数。
  • 返回类型是一个指向存储区s的指针。

注意:
一、不能任意赋值(通常初始化为0)
memset函数是按照字节对内存块进行初始化,所以不能用它将int数组出初始化为0和-1(全1)之外的其他值(除非该值高字节和低字节相同)。
因为memset函数只能取c的后八位给所输入范围的每个字节,因此其实c的实际范围应该在0~255。也就是说无论c多大只有后八位二进制是有效的。
例如:对于int a[4];
memset(a, -1, sizeof(a)) 与 memset(a, 511, sizeof(a)) 所赋值的结果一样都为-1:
因为 -1 的二进制码为(11111111 11111111 11111111 11111111);511 的二进制码为(00000000 00000000 00000001 11111111);
后八位均为(11111111),所以数组中的每个字节都被赋值为(11111111)。
注意int占四个字节,例如a[0]的四个字节都被赋值为(11111111),那么a[0](11111111 11111111 11111111 11111111),即a[0] = -1。
二、注意所要赋值的数组的元素类型

int main(){
    int a[4];
    memset(a,1,4);
    for(int i=0; i<4; i++){
        cout<<a[i]<<" ";
    }
    return 0;
}
//输出16843009  0  0  0

数组a是整型的,整型占据的内存大小为4Byte,而memset函数还是按照字节为单位进行赋值,将1(00000001)赋给每一个字节。那么对于a[0]来说,其值为(00000001 00000001 00000001 00000001),即十进制的16843009。

1.2 memcpy

void *memcpy(void *dest, const void *src, size_t n);

功能:从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中;
返回值:src和dest所指内存区域不能重叠,函数返回指向dest的指针。

//将 s的字符串复制到数组d中  
char *s="Golden Global View";  
char d[20];   
memcpy(d,s,(strlen(s)+1));  
printf("%s\n",d);  

//从第14个字符(V)开始复制,连续复制4个字符(View)
memcpy(d,s+14*sizeof(char),4*sizeof(char));  
d[4]='\0';   //这个语句若不加,输出的字符中有未初化的字符,显示乱码。  
printf("%s\n",d);  

1.3 memcpy与strcpy的区别:(重点)

  1. 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、结构体、类等。
  2. 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
  3. 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。(src空间大小大于dst,又完全复制时也会造成溢出,但是相对strcpy更易于把控)

2.柔性数组char data[0]

struct Msg
{
    ...         // 其它成员
    int nLen;   // 一般char data[0]的前面会加一个长度nLen表示data的大小
    char data[0];   // char[0]或char[1]必须放在最后
//char data[0] 这个数组是没有元素的,它的地址紧跟着nLen后的地址,
//如果分配的内存大于结构体的实际大小,那么大出来的那部分就是data的内容。
};

介绍:
在结构体最后加char[0]或char[1]的用法是GNU C的扩展,在ISO/IEC 9899-1999里面,这么写是非法的。这种用法在C99中叫做 柔性数组。柔性数组成员前面必须至少有一个其它类型成员。包含柔性数组成员的结构要用malloc进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
目的:
主要是为了方便管理内存缓冲区,如果你直接使用指针而不使用数组,那么,你在分配内存缓冲区时,就必须分配结构体一次,然后再分配结构体内的指针一次,(而此时分配的内存已经与结构体的内存不连续了,所以要分别管理即申请和释放)而如果使用数组,那么只需要一次就可以全部分配出来,反过来,释放时也是一样,使用数组,一次释放,使用指针,得先释放结构体内的指针,再释放结构体。还不能颠倒次序。其实就是分配一段连续的的内存,减少内存的碎片化。
使用方法:

int dataBytes = 10;             // 此处指定data的数据大小
struct Msg *p = (struct Msg *)malloc(sizeof(struct Msg) + dataBytes);   // 动态分配
p->nLen = dataBytes;      // 把长度赋值给nLen,以方便其它部分使用此结构体
//p->data 操作data[0]

优点:

  1. 只需分配释放一次,速度稍快。
  2. 减少内存碎片。

使用指针与char[0]的区别:

  1. 结构体中使用指针:创建时,系统先为结构体分配内存,再分配指针指向的data的内存。两块内存不连续。释放的时候,先释放指针指向的内存,再释放结构体内存。
  2. 结构体中使用char[0]:创建时,系统一起为其分配结构体的内存和data的内存,两块内存是连续的(更确切的说是一块内存)。释放的时候,一次性释放

对齐对柔性数组的影响:
当结构体没有被明确要求对齐的时候会出现填充的情况,即编译器为了对齐结构体往里面填无效的内容。

struct node
{
    int  xxx;//4字节
    char yyy;//1字节
    char //填充;//char data[0];//零字节数组
    char //填充;
    char //填充;
}

很遗憾现在data[0]指向的是填充部位。而我们期望的是它要指向node之后。

这样会造成如下错误:

node* p = new char[sizeof(node)+3];//申请三个字节紧跟着node结构体
memset((char*)p,0,sizeof(node)+3);//在结构体里填零,方便对比
p->xxx =1;
p->yyy =2;
p->data[0]=1;
p->data[1]=2;
p->data[2]=3;
//你的空间是申请到了,但你赋值都赋到了对齐的废字节上了,内存结构:
1//xxx
2//yyy
1//实际的data[0]
2//实际的data[1]
3//实际的data[2]
0//你以为的data[0]
0//你以为的data[1]
0//你以为的data[2]

在实际工作中你会发现你的数据结构后面始终有几个字节是0,而你通过sizeof(结构体)的偏移去读你的char[0]会因为偏移错误而读到错误的数据。
解决方案:
1.强制进行1字节对齐:

#pragma pack(1)
struct node
{
    int  xxx;//4字节
    char yyy;//1字节
    char data[0];//零字节数组
};
#pragma pack()

2.强制不进行字节对齐:

struct node
{
    int a;
    char data[0];
}__attribute ((packed));  //__attribute ((packed))是强制不进行字节对齐

二.C++

1.虚函数与虚表

在一个类中如果有虚函数,那么此类的实例中就有一个虚表指针指向虚表,这个虚表是一块儿专门存放类的虚函数地址的内存。
在这里插入图片描述
参考代码:

//https://blog.csdn.net/u012218838/article/details/79443655
//获取虚表地址和虚函数地址
#include<iostream>
using namespace std;

class Base {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    void h() { cout << "Base::h" << endl; }
};

typedef void(*Fun)(void);  //函数指针
int main()
{
    Base b;
    //  这里指针操作比较混乱,在此稍微解析下:(intptr_t在不同的平台是不一样的,始终与地址位数相同)

    //  *****printf("虚表地址:%p\n", *(intptr_t *)&b); 解析*****:
    //  1.&b代表对象b的起始地址
    //  2.(intptr_t *)&b 强转成intptr_t *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针(64位编译器指针为8字节)
    //  3.*(intptr_t *)&b 取前四个字节,即vptr虚表地址
    //

    //  *****printf("第一个虚函数地址:%p\n", *(intptr_t *)*(intptr_t *)&b);*****:
    //  根据上面的解析我们知道*(intptr_t *)&b是vptr,即虚表指针.并且虚表是存放虚函数指针的
    //  所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(intptr_t *)*(intptr_t *)&b
    //  这样强转后为了后面的取四个字节.所以*(intptr_t *)*(intptr_t *)&b就是虚表的第一个元素.
    //  即f()的地址.
    //  那么接下来的取第二个虚函数地址也就依次类推.  始终记着vptr指向的是一块内存,
    //  这块内存存放着虚函数地址,这块内存就是我们所说的虚表.
    //
    printf("虚表地址:%p\n", *(intptr_t *)&b);
    printf("第一个虚函数地址:%p\n", *(intptr_t *)*(intptr_t *)&b);
    printf("第二个虚函数地址:%p\n", *((intptr_t *)*(intptr_t *)(&b) + 1));

    Fun pfun = (Fun)*((intptr_t *)*(intptr_t *)(&b));  //vitural f();
    printf("f():%p\n", pfun);
    pfun();

    pfun = (Fun)(*((intptr_t *)*(intptr_t *)(&b) + 1));  //vitural g();
    printf("g():%p\n", pfun);
    pfun();
}

虚表的继承:
原文链接:https://blog.csdn.net/Jacksqh/article/details/105326852

当父类定义了虚函数时,在子类进行继承的时候会将父类的虚函数表也给继承下来所以那一些虚函数在子类中也是virtual类型的,如果要对父类中的虚函数进行重写时或添加虚函数,顺序是:
①先将父类的虚函数列表复制过来;
②重写虚函数时是把从父类继承过来的虚函数表中对应的虚函数进行相应的替换;
③如果子类自己要添加自己的虚函数,则是把添加的虚函数加到从父类继承过来虚函数表的尾部。

继承多个含有虚函数的类时,子类中含有多个虚表指针:
在这里插入图片描述

2.构造函数中的虚函数

分析以下程序:

//原文链接:https://blog.csdn.net/songchuwang1868/article/details/96481853
#include <iostream>
using namespace std;
class A {
public:
	A() {
		this->fun();
	}
	virtual void fun() {
		cout << "A fun" << endl;
	}
};
class B :public A {
public:
	B() {
		this->fun();
	}
	virtual void fun() {
		cout << "B fun" << endl;
	}
};
int main() {
	B b;
	return 0;
}

/*
运行结果:
A fun
B fun
*/

所以在构造函数中完全可以调用虚函数,只是调用的语义不符预期,在A()中的this类型是A *指针,我们期望调用的是B::fun(),但是实际上调用的是A::fun()。

原因分析:(构造函数的执行流程):

  1. 调用B的构造函数,先调用A的构造函数,调用A的构造函数,先按照A对象的内存布局进行初始化,因为虚表指针是放在顶部的,初始化虚表指针,指向虚表(虚表在编译期就生成),之后按照声明顺序初始化成员变量。最后调用构造函数{ }中的代码。
  2. 由此可见调用this->fun( )时已经设定好虚表指针,所以调用不会有任何问题。之后B将虚表指针指向自己的虚表,初始化自己的成员变量,最后调用B(){ }中的代码this->fun( ),这个时候对象顶部的虚表指针指向B的虚表,调到的自然是B::fun( )。(析构函数完全相反,先调用析构函数{ }中的代码。)

但是:

  1. 派生类与基类的构造函数初始化时,虚函数表是不一样的,意味着构造函数中调用虚函数是当前类的虚函数,无法多态调用
  2. 即使允许多态调用,如果在基类中调用派生类的虚函数,由于派生类还未开始初始化,如果访问了还未初始化的数据,那就有很大的问题了(操作了未初始化的数据,可能会导致无法预料的后果)。

3.析构函数中抛出异常

析构函数中不能抛出异常:

  1. 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
  2. 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

如何处理析构函数中的异常:

  1. 析构函数内部消化异常
~ClassName()
{
  try{
      do_something();
  }
  catch(){  
      //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。
   }
}
  1. 主动关闭程序
~ClassName()
{
  try{
      do_something();
  }
  catch(){  
      std::abort();
   }
}

通过std::abort()函数来主动关闭程序,而不是任由程序在某个随机时刻突然崩溃,这样能减少潜在的用户风险。对于某些比较严重的异常,就可以使用这个方法。并且我们可以结合使用上面的方法,把能处理的异常消化掉。

三.C++11新特性

1.智能指针

参考文章:https://blog.csdn.net/qq_56673429/article/details/124837626
引入智能指针:

  1. C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
  2. 我们知道,一个对象过期时,会自动调用它的析构函数。如果一个常规的指针也有一个析构函数,该析构函数在指针过期时自动释放内存,我们就不用担心忘记释放内存的问题了。智能指针模板定义了类似指针的对象,可以将new获得的地址赋给这种对象,当智能指针过期时,其析构函数将使用delete来释放内存。
  • 智能指针是行为行为类似指针的类对象
  • 智能指针类是一种模板类

四种智能指针:

四种智能指针模板:auto_ptr , unique_ptr , shared_ptr , weak_ptr

#include <memory>
using namespace std;
//示例:
#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Report
{
private:
    string str;
public:
    Report(const string s):str(s){cout<<"Object Create!"<<"\n";}
    ~Report(){cout<<"Object Delete!"<<endl;}
    void comment() {cout<<str<<endl;}
};

int main()
{
    {
        Report* p = new Report("using auto_ptr");
                                     //auto_ptr,shared_ptr,unique_ptr有一个explicit构造函数,该构造函数将指针作为参数
                                     //因此不能自动将指针转换为智能指针对象(隐式转换)
        //auto_ptr<Report> p1 = p;   //not allowed implicit conversion 不允许隐式转换
        auto_ptr<Report> p1(p);      //allowed explicit conversion 允许显示转换
        p1->comment();
    }
    cout<<"------------------"<<endl;
    {
        unique_ptr<Report> p2(new Report("using unique_ptr"));
        p2->comment();
    }
    cout<<"------------------"<<endl;
    {
        shared_ptr<Report> p3(new Report("using shared_ptr"));
        p3->comment();
    }

    return 0;
}

1.1 auto_ptr

auto_ptr是C++98为管理内存提供的解决方案,C++11已将其摒弃,并提供了其他三种解决方案。但是auto_ptr已经使用了很多年,如果编译器不支持其他智能指针模板,auto_ptr仍是我们有限的选择。

atuo_ptr的弊端:
来看下面一段代码:

auto_ptr<string> ptr1(new string("hello"));
auto_ptr<string> ptr2;
ptr2 = ptr1;

我们可能会产生这样的疑问,ptr1、ptr2先后过期时,会把new string(“hello”)释放两次吗?这是不能接受的。要避免这种问题,有很多种方法:

  • 定义赋值运算符,使之执行深拷贝;
  • 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可以拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权,这就是用于auto_ptr和unique_ptr的策略,但unique更加严格。当ptr1转让所有权后,再次引用*ptr1时,程序将在运行阶段崩溃。但是使用unique_ptr时,程序不会等到运行阶段崩溃。总之,程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器将禁止这样做。
  • 创建智能更好的指针,跟踪引用特定对象的智能指针数,这称为引用计数(reference counting)。例如赋值时,计数加1,指针过期时,计数j减1。仅当最后一个指针过期时才调用delete,这是shared_ptr所采用的策略。

1.2 unique_ptr

  1. 初始化:
    std::unique_ptr是一个独占式的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
    可以通过构造函数初始化,移动构造初始化,reset函数初始化。

  2. 删除器:
    unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器

#include <iostream>
using namespace std;
#include <string>
#include <memory>
#include <functional>
 
class Test
{
public:
    Test() : m_num(0)
    {
        cout << "construct Test..." << endl;
    }
 
    Test(int x) : m_num(1)
    {
        cout << "construct Test, x = " << x << endl;
    }
 
    Test(string str) : m_num(2)
    {
        cout << "construct Test, str = " << str << endl;
    }
 
    ~Test()
    {
        cout << "destruct Test..." << endl;
    }
 
    void setValue(int v)
    {
        this->m_num = v;
    }
 
    void print()
    {
        cout << "m_num: " << this->m_num << endl;
    }
 
private:
    int m_num;
};
 
int main()
{
    /*--------------------------  一,初始化智能指针unique_ptr  ------------------------------*/
    //1.通过构造函数初始化
    unique_ptr<int> ptr1(new int(3));
 
    //2.通过移动函数初始化
    unique_ptr<int> ptr2 = move(ptr1);
 
    //.通过reset初始化
    ptr2.reset(new int(7));
 
    /*--------------------------  二,unique_ptr的使用  ------------------------------*/
    //1.方法一
    unique_ptr<Test> ptr3(new Test(666));
    Test* pt = ptr3.get();
    pt->setValue(6);
    pt->print();
 
    //2.方法二
    ptr3->setValue(777);
    ptr3->print();
 
    /*------------------------------------  三,指定删除器  -----------------------------------*/
    //1.函数指针类型
    //using ptrFunc = void(*)(Test*);
    //unique_ptr<Test, ptrFunc> ptr4(new Test("hello"), [](Test* t) {
    //    cout << "-----------------------" << endl;
    //    delete t;
    //    });
 
    //2.仿函数类型(利用可调用对象包装器)
    unique_ptr<Test, function<void(Test*)>> ptr4(new Test("hello"), [](Test* t) {
        cout << "-----------------------" << endl;
        delete t;
        });
 
    /*---------- 四,独占(共享)的智能指针可以管理数组类型的地址,能够自动释放 ---------*/
    unique_ptr<Test[]> ptr5(new Test[3]);
 
    //在c++11中shared_ptr不支持下面的写法,c++11以后才支持的
    shared_ptr<Test[]> ptr6(new Test[3]);
 
    return 0;
}

1.3 shared_ptr

  1. shared_ptr的初始化:
    共享式智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count();
  2. 获取原始指针:
    对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的get()方法得到原始地址;
  3. 指定删除器:
    当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
#include <iostream>
using namespace std;
#include <string>
#include <memory>

class Test
{
public:
    Test() : m_num(0)
    {
        cout << "construct Test..." << endl;
    }

    Test(int x) : m_num(0)
    {
        cout << "construct Test, x = " << x << endl;
    }

    Test(string str) : m_num(0)
    {
        cout << "construct Test, str = " << str << endl;
    }

    ~Test()
    {
        cout << "destruct Test..." << endl;
    }

    void setValue(int v)
    {
        this->m_num = v;
    }

    void print()
    {
        cout << "m_num: " << this->m_num << endl;
    }

private:
    int m_num;
};

int main()
{
    /*--------------------------  一,初始化智能指针shared_ptr  ------------------------------*/
    //1.通过构造函数初始化
    shared_ptr<int> ptr1(new int(3));
    cout << "ptr1.use_count(): " << ptr1.use_count() << endl;

    //2.通过移动和拷贝构造函数初始化
    shared_ptr<int> ptr2 = move(ptr1);
    cout << "ptr1.use_count(): " << ptr1.use_count() << endl;
    cout << "ptr2.use_count(): " << ptr2.use_count() << endl;

    shared_ptr<int> ptr3 = ptr2;
    cout << "ptr2.use_count(): " << ptr2.use_count() << endl;
    cout << "ptr3.use_count(): " << ptr3.use_count() << endl;

    //3.通过 std::make_shared初始化
    shared_ptr<int> ptr4 = make_shared<int>(8);
    shared_ptr<Test> ptr5 = make_shared<Test>(7);
    shared_ptr<Test> ptr6 = make_shared<Test>("GOOD LUCKLY!");

    //4.通过reset初始化
    ptr6.reset(); //重置ptr6, ptr6的引用基数为0
    cout << "ptr6.use_count(): " << ptr6.use_count() << endl;

    ptr5.reset(new Test("hello"));
    cout << "ptr5.use_count(): " << ptr5.use_count() << endl;

    cout << endl;
    cout << endl;

    /*--------------------------  二,共享智能指针shared_ptr的使用  ------------------------------*/
    //1.方法一
    Test* t = ptr5.get();
    t->setValue(1000);
    t->print();

    //2.方法二
    ptr5->setValue(7777);
    ptr5->print();

    printf("\n\n");
    /*------------------------------------  三,指定删除器  -----------------------------------*/
     //1.简单举例
    shared_ptr<Test> ppp(new Test(100), [](Test* t) {
        //释放内存
        cout << "Deleted Test......." << endl;
        delete t;
        });
    printf("----------------------------------------------------------------------\n");

    //2.如果是数组类型的地址,就需要自己写指定删除器,否则内存无法全部释放
    //shared_ptr<Test> p1(new Test[5], [](Test* t) {
    //    delete[]t;
    //    });

//    //3.也可以使用c++给我们提供的 默认删除器函数(函数模板)
//    shared_ptr<Test> p2(new Test[3], default_delete<Test[]>());

//    //4.c++11以后可以这样写 也可以自动释放内存
//    shared_ptr<Test[]> p3(new Test[3]);

    return 0;
}

另外,我们还可以自己封装一个函数模板make_shared_array方法来让shared_ptr支持数组,代码如下:

#include <iostream>
#include <memory>
#include <string>
using namespace std;
 
//有了这个函数模板,我们就不用自己去释放数组类型的地址了
template <typename T>
shared_ptr<T> make_share_array(size_t size) 
{
	//返回匿名对象
	return shared_ptr<T>(new T[size], default_delete<T[]>());
}
 
int main()
{
	shared_ptr<int> ptr1 = make_share_array<int>(10);
	cout << ptr1.use_count() << endl;
 
	shared_ptr<string> ptr2 = make_share_array<string>(7);
	cout << ptr2.use_count() << endl;
		
	return 0;
}

1.4 weak_ptr

  • weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
  • weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
  • 使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数 expired() 的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
  • weak_ptr可以使用一个非常重要的成员函数 lock() 从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr;
#include <iostream>
#include <memory>
using namespace std;
 
int main() 
{
    shared_ptr<int> sp(new int);
 
    weak_ptr<int> wp1;			//构造了一个空weak_ptr对象
    weak_ptr<int> wp2(wp1);		//通过一个空weak_ptr对象构造了另一个空weak_ptr对象
    weak_ptr<int> wp3(sp);		//通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象
    weak_ptr<int> wp4;
    wp4 = sp;					//通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象(这是一个隐式类型转换)
    weak_ptr<int> wp5;
    wp5 = wp3;					//通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象
    
    return 0;
}

常用函数:

  1. 通过调用std::weak_ptr类提供的expired()方法来判断观测的资源是否已经被释放
  2. 通过调用std::weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象
  3. 通过调用std::weak_ptr类提供的reset()方法来清空对象,使其不监测任何资源

weak_ptr解决循环引用问题:
参考文章:https://blog.csdn.net/qq_36553387/article/details/115105137

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        if (this->ChildPtr.use_count()) {

        }
    }
    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl;  // 0
    std::cout << wpc.use_count() << std::endl;  // 0
    std::cout << wpp.expired() << std::endl;    // 无观测资源返回true
    std::cout << wpc.expired() << std::endl;    
    return 0;
}

2.函数对象(函数符)

很多STL算法都使用函数对象——也叫函数符(functor)。函数符是可以以函数方式与 () 结合使用的任意对象。这包括:

  1. 函数名;
  2. 指向函数的指针;
  3. 重载了()运算符的类对象,即定义了函数 operator()() 的类。
#include <iostream>
using namespace std;
class Test
{
private:
    double a;
    double b;
public:
    Test(double _a=0,double _b=1):a(_a),b(_b){}
    double operator () (double x)
    {
        return a*x+b;
    }
};
int main()
{
    Test t1;
    Test t2(2.5,10.0);
    double y1 = t1(1.5);  //t1.operator()(1.5)  0*1.5+1 = 1
    double y2 = t2(0.4);  //2.5*0.4+10.0 = 11
                          //表达式 a*x+b中,a、b来自对象的构造函数,x来自operator()()的参数
    cout<<"y1:"<<y1<<endl
        <<"y2:"<<y2<<endl;
    return 0;
}

3. Lambda表达式

在C++中引出lambda的主要目的是,某些函数的参数需要接受函数指针或函数符,程序员能够将类似于函数的表达式用作这种函数的参数。因此,典型的lambda表达式是测试表达式或者比较表达式(可编写为一条返回语句)。这使得lambda简洁而利于理解,而且可自动推断返回类型。

3.1比较函数指针、函数符、和Lambda函数

示例:生成一个随即列表,判断其中有多少整数可以被3整除,多少可以被13整除
函数指针:

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;


bool f3(int x) {return x%3 == 0;}
bool f13(int x) {return x%13 == 0;}
int main()
{
    vector<int> numbers(1000);
    /*
        generate()接受一个区间(前两个参数)并将每个元素设置为第三个参数的返回值,
        而第三个参数是一个不接受任何参数的函数对象
    */
    generate(numbers.begin(),numbers.end(),rand); //给容器中的元素设置为随机数
    /*
        count_if()计算使得第三个参数函数对象返回true的元素数;
        前两个参数指定区间,第三个参数是一个返回true 或 false的函数对象
    */
    int count3 = count_if(numbers.begin(),numbers.end(),f3);
    cout<<"Count of numbers divisible by 3: "<<count3<<endl;  //324

    int count13 = count_if(numbers.begin(),numbers.end(),f13);
    cout<<"Count of numbers divisible by 13: "<<count13<<endl;	//68

    return 0;
}

函数符:

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;

class F_mod
{
private:
    int dv;
public:
    F_mod(int _dv = 1):dv(_dv){}
    bool operator()(int x){return x%dv == 0;} //重载()
};

int main()
{
    vector<int> numbers(1000);
    generate(numbers.begin(),numbers.end(),rand); //给容器中的元素设置为随机数

    int count3 = count_if(numbers.begin(),numbers.end(),F_mod(3));
                                                  //F_mod(3)创建了一个对象,count_if()调用该对象重载的()运算符
    cout<<"Count of numbers divisible by 3: "<<count3<<endl;      // 324

    int count13 = count_if(numbers.begin(),numbers.end(),F_mod(13));
    cout<<"Count of numbers divisible by 13: "<<count13<<endl;    // 68

    return 0;
}

Lambda表达式:
名称Lambda来自于lambda calculus(λ演算)——一种定义、应用函数的数学系统,这个系统让您能够使用匿名函数,即无需给函数命名。在C++11中。对于接受函数指针或函数符的函数,可以使用匿名函数定义(lambda)作为其参数。与前述函数 f3() 对应的lambda如下:
bool f3(int x) {return x%3 == 0;}
[] (int x) {return x%3 == 0;}
差别:

  1. 使用[]替代了函数名(匿名);
  2. 没有声明函数返回类型。返回类型相当于使用decltype(类型推断)根据返回值推断得到的,这里为bool。如果lambda不包含返回语句,推断出的返回类型将为void。

仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用,否则,需要使用新增的返回类型后置语法:
[](double x)->double(int y = x;return x-y;)返回值类型为double

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
int main()
{
    vector<int> numbers(1000);
    generate(numbers.begin(),numbers.end(),rand); //给容器中的元素设置为随机数

    int count3 = count_if(numbers.begin(),numbers.end(),[] (int x) {return x%3 == 0;});

    cout<<"Count of numbers divisible by 3: "<<count3<<endl;      // 324

    int count13 = count_if(numbers.begin(),numbers.end(),[] (int x) {return x%13 == 0;});
    cout<<"Count of numbers divisible by 13: "<<count13<<endl;    // 68

    return 0;
}

3.2 lambda表达式的优点

  1. 距离近:
    定义尽可能在使用的地方附近,这样无需翻阅多页源代码,以了解函数具体内容;另外,如果需要修改代码,涉及的内容都将在附近;lambda是理想的选择,因为其定义和使用是在同一个地方进行的;而函数是最糟糕的选择,因为不能在函数内部定义其他函数,因此函数的定义可能离使用的地方很远。函数符是不错的选择,因为可在函数内部定义类(包含函数符类),因此定义离使用地点可以很近。
  2. 较简洁:
    从简洁的角度看,函数符代码比函数和lambda表达式代码更加繁琐。也可以在函数内部定义有名称的lambda。
  3. 效率高:
    由于函数地址的概念意味着非内联函数,因此函数指针方法阻止了内联,而lambda通常不会阻止内联。
  4. 功能多:
    下一节详细介绍。

3.3 lambda表达式访问作用域内的变量

  1. [a] a为变量名,lambda表达式内可以按值访问 a。
  2. [&a] 可以按引用访问变量a
  3. [=] 可以按值访问作用域内的所有动态变量
  4. [&] 可以按引用的方式访问作用域内所有动态变量

可以混合使用以上方式,例如 [&a,=] 可以按引用方式访问a,按值方式访问所有作用域内的变量

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;

int main()
{
    vector<int> numbers(1000);
    generate(numbers.begin(),numbers.end(),rand); //给容器中的元素设置为随机数

    int count3 = 0;
    int count13 = 0;
    for_each(numbers.begin(),numbers.end(),
             [&](int x){count3 += x%3==0;count13 += x%13==0;});

    cout<<"Count of numbers divisible by 3: "<<count3<<endl;      // 324
    cout<<"Count of numbers divisible by 13: "<<count13<<endl;    // 68

    return 0;
}

4.移动语义和右值引用

4.1 右值引用

  1. 左值(locator value)的概念:左值是一个表示数据的表达式,如变量名名或解除引用的指针,程序可获取其地址。
  2. 右值(real value)的概念:右值是可以出现在赋值表达式右边,但不能对其应用地址运算符的值。
    右值包括字面常量(c风格字符串除外,它表示地址),诸如x+y等表达式以及函数返回值(返回引用除外)。

传统的C++引用(现在成为左值引用)使得标识符关联到左值。C++11x新增了右值引用,使用&&表示,右值引用可以关联到右值。

int x = 10;
int y = 23;
int &&r1 = 13;
int &&r2 = x+y;
double &&r3 = std::sqrt(2.0);

注意,r2关联到的是当时计算x+y得到的结果33,即使以后修改了x或y,也不会影响到r2;有趣的是,将右值引用关联到右值,导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将取地址运算符&用于13,但可以将其用于r1。通过将数据与特定的地址相关联,使得可以通过右值引用来访问该数据。
示例:

#include <iostream>
using namespace std;
inline double fun(double tf) {return 5.0*tf;}
int main()
{
    double tc = 2.5;
    double && rd1 = 7.0;
    double && rd2 = 1.0*tc + 2.0;
    double && rd3 = fun(rd2);
    cout<<"tc value and address: "<<tc<<" "<<&tc<<endl;
    cout<<"rd1 value and address: "<<rd1<<" "<<&rd1<<endl;
    cout<<"rd2 value and address: "<<rd2<<" "<<&rd2<<endl;
    cout<<"rd3 value and address: "<<rd3<<" "<<&rd3<<endl;

    return 0;
}

4.2 移动语义

vector<string> allcaps(const vector<string> &vs)
{
    vector<string> temp;
    //省略代码:vs中元素全部转大写后保存到temp中
    return temp;
}

vector<string> vstr1(1000,"abcdefghi");
vector<string> vstr2(allcaps(vstr1)); 

我们发现,allcaps()创建了对象temp,该对象管理着10000个字符,vector和string的拷贝构造函数创建这10000个字符的副本,然后程序删除allcaps()返回的临时对象(迟钝的编译器甚至可能将temp复制给一个临时返回对象,删除temp,再删除临时返回对象)。这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr2 不是更好吗? 也就是说,不将10000个字符复制到新地方再删除原来的字符,而将字符留在原来的地方,并将vstr2与之关联。 这类似与计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录这种方法被称为移动语义(move semantics)。

移动语义实际上避免了移动原始数据,而只是修改了记录。

示例:

#include <iostream>
using namespace std;

class Useless
{
private:
    int n;              //元素数量
    char* pc;           //数据指针
    static int ct;      //对象数量
    void ShowObject() const;
public:
    Useless();
    explicit Useless(int k);
    Useless(int k,char ch);
    Useless(const Useless &f);  //传统的拷贝构造
    Useless(Useless &&f);       //移动构造
    ~Useless();
    Useless operator + (const Useless &f) const;
    void showData() const;
};

int Useless::ct = 0;

int main()
{
    Useless one(10,'x');
    Useless two = one;       //calls copy constructor
    Useless three(20,'o');
    Useless four(one+three); //calls operator +() ,move constructor

    cout<<"object one: ";
    one.showData();
    cout<<"object two: ";
    two.showData();
    cout<<"object three: ";
    three.showData();
    cout<<"object four: ";
    four.showData();

    return 0;
}


void Useless::ShowObject() const
{
    cout<<"number of elements: "<<n<<"    "
        <<"data address:  "<<(void *)pc<<endl;
}

Useless::Useless()
{
    ++ct;
    n = 0;
    pc = nullptr;
    cout<<"default constructor called;number of objects: "<<ct<<endl;
}

Useless::Useless(int k):n(k)
{
    ++ct;
    pc = new char[n];
    cout<<"int constructor called;numberof objects: "<<ct<<endl;
    ShowObject();
}

Useless::Useless(int k, char ch):n(k)
{
    ++ct;
    pc = new char[n];
    for(int i=0;i<n;i++)
        pc[i] = ch;
    cout<<"int char constructor called;numberof objects: "<<ct<<endl;
    ShowObject();
}

Useless::Useless(const Useless &f):n(f.n)  //拷贝构造
{
    ++ct;
    pc = new char[n];
    for(int i=0;i<n;i++)
        pc[i] = f.pc[i];
    cout<<"copy constructor called;numberof objects: "<<ct<<endl;
    ShowObject();
}

Useless::Useless(Useless &&f):n(f.n)       //移动构造
{
    ++ct;
    pc = f.pc;
    f.pc = nullptr;
    f.n = 0;
    cout<<"move constructor called;numberof objects: "<<ct<<endl;
    ShowObject();
}

Useless::~Useless()
{
    cout<<"destructor called;numberof objects: "<<--ct<<endl;
    ShowObject();
    delete[] pc;
}

Useless Useless::operator +(const Useless &f) const
{
    cout<<"entering operator + ()\n";
    Useless temp(n+f.n);
    for(int i=0;i<n;i++)
        temp.pc[i] = pc[i];
    for(int i=n;i<temp.n;i++)
        temp.pc[i] = f.pc[i-n];
    cout<<"temp object:\n";
    cout<<"leaving operator+()\n";
    return temp;
}

void Useless::showData() const
{
    if(n==0) cout<<"object empty";
    else
        for(int i=0;i<n;i++)
            cout<<pc[i];
    cout<<endl;
}

Useless four(one+three);//calls move constructor
表达式one+three调用Useless::operator+(),而右值引用f将关联到该方法返回的临时对象。

通过分析复制构造与移动构造的区别可以发现,移动构造让pc指向现有的数据,以获取这些数据的所有权(窃取 plifering)。此时,pc和f.pc指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用delete[]两次。为了避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete[]没有问题。由于修改了f对象,这要求不能在参数声明中使用const

在这里插入图片描述
two是one的副本,它们显示的数据输出相同,但是显示的数据地址不同(006F4B68和006F4BB0)。另一方面,在方法Useless::operator+()中创建的对象的数据地址与对象four存储的数据地址相同(都是006F4C48),其中对象four是由移动构造创建的,创建对象four后,为临时对象调用了析构函数。

做了优化的编译器编译该程序,输出将不同:
在这里插入图片描述
注意到,没有调用移动构造函数,且之创建了4个对象。创建对象four时,该编译器没有调用移动构造或拷贝构造函数;相反,它推断出对象four是operator+()所做工作的受益人,因此将operator+()创建的对象转让到four的名下。

4.3 移动构造函数解析

要让移动语义的发生,需要两个步骤:

  1. 右值引用:让编译器知道何时使用移动语义
  2. 编写移动构造函数。

机智的编译器可能自动消除额外的复制工作,但通过使用右值引用,程序员可以指出何时该使用移动语义。

4.4 移动赋值运算符

适用于构造函数的移动语义也适用于赋值运算符:

Useless & Useless::operator =(const Useless &f) //copy assignment
{
    cout<<"calls copy assignment\n";
    if(this == &f) return *this;
    delete[] pc;
    n = f.n;
    pc = new char[n];
    for(int i=0;i<n;i++)
        pc[i] = f.pc[i];
    return *this;
}

Useless & Useless::operator =(Useless &&f) //move assignment
{
    cout<<"calls move assignment\n";
    if(this == &f) return *this;
    delete[] pc;        //释放原来内存
    n = f.n;
    pc = f.pc;          //窃取f所有权
    f.n = 0;
    f.pc = nullptr;     //修改f
    return *this;
}

int main()
{
    Useless one(10,'x');
    Useless two;
    two = one;         //calls copy constructor
    Useless three;
    three = move(one); //calls move constructor

    one.showData();
    two.showData();
    three.showData();

    return 0;
}

three = move(one); //calls move constructor
头文件< utility > 中声明的std:move()强制移动将对象one强制转换为右值 Useless &&类型。

在这里插入图片描述

C++11在原有的4个特殊成员函数(默认构造、拷贝构造、拷贝赋值运算符和默认析构函数)基础上新增了两个:
移动构造函数和移动赋值运算符

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-10-08 20:22:11  更:2022-10-08 20:23:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 5:05:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码