指针、引用、const、类的浅显理解

指针、引用、const、类的浅显理解

一、引用

参考C++ primer 5th中文版 Page 45.
定义引用时程序把引用和它的初始值bind在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因此无法令引用重新绑定在另一个对象上。
引用为对象起了一个另外的一个名字
注意:
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字
引用必须初始化

1.1 理解与实验 和const

初始化后不可以重新绑定对象,相当于一个 const pointer to Object, Object是不是const看是否有const的修饰

1.1.1 实验一

reference is a const pointer to Object

#include <iostream>
#include <assert.h>
using namespace std;
int main(int argc, char **argv)
{
    int init_a = 2;
    int init_b = 3;
    int &a= init_a;
    int &b=init_b;
    a=6;
    std::cout<<"init_a="<<init_a<<" a= "<<a<<std::endl;//init_a=6 a= 6
    a=b;//不能理解为引用的重新绑定对象 而是通过b修改了a和init_b的值
    std::cout<<"init_a="<<init_a<<" a= "<<a<<std::endl;//init_a=3 a= 3

    int init_c =10;
    //&a=init_c;//编译错误
    return 0;
}

类型什么的肯定是要严格匹配的,如int对int;double对double

1.1.1 实验二

Object是不是const看是否有const的修饰

#include <iostream>
using namespace std;
int main(int argc,char**argv)
{
    int init_a = 0;
    int init_b = 1;
    const int & a=init_a;
    init_a = 10 ;
    cout<<"init_a = "<<init_a<<" a = "<<a<<endl;//init_a = 10 a = 10
    //a = 100;//编译错误 error: assignment of read-only reference ‘a’
    const int ci=1024;
    const int &r1 = ci; //正确
    //r1 = 42;//error: assignment of read-only reference ‘r1’
    //int &r2 = ci;//error: binding reference of type ‘int&’ to ‘const int’ discards qualifiers 试图将一个非常量引用指向一个常量对象
    return 0;
}

看出const int & a=init_a;
表示的是一个const pointer to const Object表示不可以通过a去修改,但由于init_a不是定义为const所以 =10赋值修改可行

C++ Primer 5th 中文版 Page 56:
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
下面还有一句Tips
试试这样想吧:所谓指向常量的指针和引用,不过是指针或引用“自以为是”罢了,他们觉得自己指向了常量,所以自觉地不去改变所指对象的值

二、指针

指针的本质
例如

int a = 1;
int * p =&a;

请添加图片描述
指针是指向另外一种类型的复合类型

指针是一个对象,允许对其赋值和拷贝,在指针的生命周期可以先后指向不同的对象(对指针对象本身可以取得地址)
指针无需在定义时赋予初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

在C++ Primer 5th 中文版 Page 49建议初始化所有的指针

空悬指针等

指向一个对象
指向紧邻对象所占空间的下一个位置
空指针
无效指针上述情况的其他值

三、指针和引用的区别

编程指北注:其第七条有误
别名

#include <iostream>
using namespace std;
int main()
{
    int a = 10;
    int &b = a;
    b++;
    std::cout << "a= " << a << ",b=" << b << std::endl;
    //a= 11,b=11
}

指针和引用的自增(++)和自减含义不同,指针是指针运算, 而引用是代表所指向的对象对象执行++或–

参考C++ primer 5th中文版 Page 45.
定义引用时程序把引用和它的初始值bind在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因此无法令引用重新绑定在另一个对象上。
引用为对象起了一个另外的一个名字
注意:
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字
引用必须初始化

四、const(const和pointer)

pointer分两种const或者not const,对于const修饰的指针和引用见1.1.1 实验二

4.1 pointer to const

指向常量指针(pointer to const)不能用pointer改变其所属对象的值。要想存放常量对象的地址,只能使用指向常量的指针
同样有些注意见1.1.1 实验二:

#include <iostream>
#include <vector>
using namespace std;
int main(int argc,char**argv)
{
    const double pi=3.14;
    //double *ptr = &pi;//error: invalid conversion from ‘const double*’ to ‘double*’ [-fpermissive]
    const double *cptr = &pi;
    //*cptr = 42;//error: assignment of read-only location ‘* cptr’
    return 0;
} 

4.2 const pointer to Object

const pointer指指针本身是常量,它必须初始化,即里面存储的地址就不能再改变了 (即不变的是指针本身的值而并非指向的那个值)

#include <iostream>
#include <vector>
using namespace std;
int main(int argc,char**argv)
{
    int errNumber = 0;
    int * const curErr = &errNumber;//curErr将一直指向errNumb
    const double pi=3.1415926;
    const double *const pip = &pi;//pip是一个指向常量对象的const pointer
    return 0;
}

4.3 顶层const和底层const

顶层const:指针本身是一个常量
底层const:指针所指的对象是一个常量
分清:从右向左读
const int p;

const int* p;

int const* p;

int * const p;

const int * const p;

int const * const p;

4.3.1 实验

#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char **argv)
{
    /**
     * @brief 从右向左读
     * 
     */

    //1. const int p;常量
    //const int p=10;


    // //2. p is a pointer to const:一个指向常量的指针
    // const int *p;
    // const int a = 1;
    // const int b = 10;
    // int c = 100;
    // p=&a;
    // //*p=100;//error: assignment of read-only location ‘* p’ 即指向的对象不可更改
    // p=&b;
    // std::cout<<*p<<std::endl;//10-->说明指向可更改(即指针对象里面存的指向的对象地址可以更改)
    // p=&c;//c虽然不是常量但是也成立因为指针认为只管自己 C++ Primer 5th 中文版 Page 56


    //3. p is a pointer to const:一个指向常量的指针  //同上
    // int const *p;
    // const int a = 1;
    // const int b = 10;
    // int c = 100;
    // p=&a;
    // *p=0;//错误 不能通过指针去更改指向的对象的值
    // p=&b;
    // p=&c;


    // 4. p is a const pionter to int//一个const pointer指向int
    //int *const p;//error-->必须初始化
    //int a = 1;
    //int b = 10;
    //int const c = 100; 
    //int *const p=&a;
    //p=&b;//error: assignment of read-only variable ‘p’ p的值作为常量不可更改(即指向的对象不可更改)
    //*p = 10;//正确,因为int 不被const修饰
    //int *const p2=&c;//类型不对 a value of type "const int *" cannot be used to initialize an entity of type "int *const"C/C++(144)invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]


    // 5. p is a const pointer to const int// 一个 const pointer指向const int
    //const int *const p;//error-->必须初始化;
    // const int a = 1;
    // const int b = 10;
    // const int *const p = &a;
    // //*p=100;//error: assignment of read-only location ‘*(const int*)p’ 指向的对象是常量
    // //p=&b;//error: assignment of read-only variable ‘p’ 指针本身是常量(指针对象存的地址不可更改)-->指针指向不可更改
    // int c = 100;
    // const int *const p2 = &c;//c虽然不是常量但是也成立因为指针认为只管自己 C++ Primer 5th 中文版 Page 56
    // //*p2=1000;//error: assignment of read-only location ‘*(const int*)p2’
    // c=10000;


    // 6. p is a const pointer to const int // 同上 //一个 const pointer指向const int
    // int const *const p;
    return 0;
}

五、空悬指针、野指针、无效指针

空悬指针(dangling pointer):指向一块曾经保存数据对象但现在已经无效的内存的指针
野指针:不确定指向的指针,常常来自未初始化的指针(野指针可能对正常数据产生影响),所以指针使用时一定建议初始化
无效指针:

class A;
A* p = new A();
A* q = p;
delete p;
//pointer q is invalid now.

六、拷贝(深拷贝、浅拷贝)

6.1 浅拷贝

在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数-----浅拷贝函数—一一赋值(operator()=)
即直接A=B
没有重新分配新内存

class Person{
public:
    Person(int _age):age(_age)
    {

    }
    int age;

    int *ptr = new int;
    ~Person()
    {
    	delete ptr;
    }
};

上述代码调用拷贝构造时会double free问题
因为浅拷贝按值赋值两个 对象的ptr成员指向同一块内存 当一个对象(p2)析构时,销毁 delete p2.ptr 指向的内存
由于指向同一块内存的指针 再次delete会导致double free

6.2 深拷贝

不止直接A=B的直接赋值

class Person
{
public:
    int age;
    int *ptr = new int;
    Person(int _age) : age(_age)
    {
    }
    Person(Person const &p)
    {
        this->age = p.age;
        this->ptr = new int(*p.ptr);
    }
    Person &operator=(Person const &p)
    {
        this->age = p.age;
        this->ptr =new int(*p.ptr);
        return *this;
        
    }
    ~Person()
    {
        delete ptr;
    }
};
int main(int argc, char **argv)
{
    Person p1(10);
    Person p2(20);
    p1 = p2;
    std::cout << p1.age << std::endl;
    std::cout << *p1.ptr << std::endl;
    p1.age = 40;
    p2.age = 50;
    std::cout << "p1 age= " << p1.age << " p2 age =" << p2.age << std::endl;
    return 0;
}

上述写的有点问题this->ptr原来指向的对象没有被释放

#include <iostream>
using namespace std;
class Person
{
public:
    int age;
    int *ptr = new int;
    Person(int _age) : age(_age)
    {
    }
    Person(Person const &p)
    {
        this->~Person();
        this->age = p.age;
        this->ptr = new int(*p.ptr);
    }
    Person &operator=(Person const &p)
    {
        this->~Person();
        this->age = p.age;
        this->ptr =new int(*p.ptr);
        return *this;
    }
    ~Person()
    {
        delete ptr;
    }


};
int main(int argc, char **argv)
{
    Person p1(10);
    Person p2(20);
    p1 = p2;
    std::cout << p1.age << std::endl;
    std::cout << *p1.ptr << std::endl;
    p1.age = 40;
    p2.age = 50;
    std::cout << "p1 age= " << p1.age << " p2 age =" << p2.age << std::endl;
    return 0;
}

上述进行深拷贝就避免了double free ---->虽然还没完美符合三五法则

6.3 一些注意

c++里面的大多数实现的对象都是进行的拷贝是深拷贝的如vector等(这里值得是编译器提供好的对象)(里面不存放指针)
其实就是按=(赋值)直接拷贝
但是shared_ptr和weak_ptr实现的是浅拷贝
unique_ptr禁止拷贝

6.4 三五法则(*)

图片来自b站小彭老师:RAII与智能指针
在这里插入图片描述
请添加图片描述

请添加图片描述

七、vector等容器存放指针数据的释放问题

std::vector<Object*> //该类型虽然是STL容器,但存储了不安全的原始指针

7.1 释放时出现double free

#include <iostream>
#include <map>
#include <vector>
#include <deque>
#include <assert.h>
#include <memory>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};

int main()
{
    std::vector<Person*> people;
    auto p_shared =new  Person(2000);
    people.push_back(new Person(10));
    people.push_back(new Person(20));
    people.push_back(new Person(30));
    people.push_back(p_shared);
    people.push_back(p_shared);
    std::cout << people.size() << std::endl;

    for(auto p:people)
    {
        std::cout <<"p is "<<p<<std::endl;
    }
    for(auto iter= people.begin();iter!=people.end();++iter)
    {
        if(*iter!=nullptr)
        {
            delete (*iter); 
            *iter =nullptr;//最后一个出现了问题 free(): double free detected in tcache 2// TODO
        }
    }
    people.clear();
    return 0;
}

数值里面的数据打印出来是

p is 0x55555556ced0
p is 0x55555556cf10
p is 0x55555556cef0
p is 0x55555556ceb0
p is 0x55555556ceb0

最后两个一样当第四个进行释放 第五个再次进行释放的时候 该指针指向的地址已经释放造成double free

7.2 先考虑people[0]的释放

#include <iostream>
#include <map>
#include <vector>
#include <deque>
#include <assert.h>
#include <memory>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};
int main()
{
    std::vector<Person*> people;
    auto p_shared =new  Person(2000);
    people.push_back(new Person(10));
    people.push_back(new Person(20));
    people.push_back(new Person(30));
    people.push_back(p_shared);
    people.push_back(p_shared);

    delete people[0];

    std::cout << "people's size is "<<people.size() << std::endl;
    for(auto p:people)
    {
        std::cout <<"p is "<<p<<std::endl;
    }
    std::cout<<people[0]<<std::endl;
    std::cout<<people[0]->age<<std::endl;//没错但对象已经回收了内存回收了 1431752496 指向一个混乱的数据 空悬指针
    people[0]=nullptr;
    std::cout<<people[0]<<std::endl;
    std::cout<<people[0]->age<<std::endl;//Segmentation fault 空指针的非法操作
    return 0;

打印为

people's size is 5
p is 0x55555556ced0
p is 0x55555556cf10
p is 0x55555556cef0
p is 0x55555556ceb0
p is 0x55555556ceb0
0x55555556ced0
1431752496
0 //people[0]=nullptr;

所以

if(people[0]!=nullptr)
{
	//一些操作
}

若果调用people.clear()虽然不报错,但并不是真正的释放内存

/*
Erases all the elements. Note that this function only erases the
elements, and that if the elements themselves are pointers, the
pointed-to memory is not touched in any way. Managing the pointer is
the user's responsibility.
*/

这种情况下vector

    people.clear();

    for(auto p:people)
    {
        std::cout <<"p is "<<p<<std::endl;
    }
    std::cout<<"after clear "<<endl;
    std::cout<<people.size()<<std::endl;//输出的是0
    return 0;
    std::cout<<people.capacity()<<std::endl;//8
    people.clear();
    std::cout<<people.capacity()<<std::endl;//8

ab
解释 这些指针指向的对象不会被销毁
并且还有4和5指向同一块内存的东西

7.3 利用智能指针

#include <iostream>
#include <map>
#include <vector>
#include <deque>
#include <assert.h>
#include <memory>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};

int main()
{
    std::vector<std::shared_ptr<Person>> people;
    auto p_shared = std::make_shared<Person>(Person(2000));
    people.push_back(make_shared<Person>(Person(20)));
    people.push_back(make_shared<Person>(Person(20)));
    people.push_back(make_shared<Person>(Person(20)));
    people.push_back(p_shared);
    people.push_back(p_shared);
    std::cout << people.size() << std::endl;

    for (auto p : people)
    {
        std::cout << "p is " << p << std::endl;
    }
    for (auto iter = people.begin(); iter != people.end(); ++iter)
    {
        if (*iter != nullptr)
        {
            (*iter).reset();
        }
    }
    for (auto p : people)
    {
        std::cout << "p is " << p << std::endl;
    }
    assert(people[0]==nullptr);
    std::cout<<"end "<<std::endl;
    std::cout<<people[0]->age<<std::endl;//Segmentation fault 因为是nullptr
    people.clear();
    return 0;
}
/*
5
p is 0x55555556fee0
p is 0x55555556ff20
p is 0x55555556ff00
p is 0x55555556fec0
p is 0x55555556fec0
p is 0
p is 0
p is 0
p is 0
p is 0
end
*/

引用计数为0,能够安全的释放

7.4 两个vector里面存放指针(进行a=b的拷贝操作)

7.4.1 普通指针

代码
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};
int main(int argc, char **argv)
{
    std::vector<Person *> people_a;
    people_a.push_back(new Person(10));
    people_a.push_back(new Person(20));
    people_a.push_back(new Person(30));
    people_a.push_back(new Person(40));

    auto people_b = people_a;

    std::cout << "people_a    is......" << std::endl;
    for (auto &p : people_a)
    {
        std::cout << "p is " << p << std::endl;
    }

    std::cout << "people_b      is......" << std::endl;
    for (auto &p : people_b)
    {
        std::cout << "p is " << p << std::endl;
    }
    for (auto &p : people_a)
    {
        if (p != nullptr)
        {
            delete p;
            p = nullptr;
        }
    }

    for (auto &p : people_b)
    {
        if (p != nullptr)
        {
            delete p;
            p = nullptr;
        }
    }
    return 0;
}
/*
输出
people_a    is......
p is 0x55555556ceb0
p is 0x55555556cef0
p is 0x55555556ced0
p is 0x55555556cf10
people_b      is......
p is 0x55555556ceb0
p is 0x55555556cef0
p is 0x55555556ced0
p is 0x55555556cf10
//这样会造成double free的---->free(): double free detected in tcache 2
*/

std::vector的深拷贝:数组重新开辟内存,里面的元素直接复制(直接复制指针)delete时另一个会造成double free的

7.4.2 智能指针

#include <iostream>
#include <vector>
#include <memory>
#include <assert.h>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};
int main(int argc, char **argv)
{
    std::vector<shared_ptr<Person>> people_a;
    people_a.push_back(make_shared<Person>(Person(10)));
    people_a.push_back(make_shared<Person>(Person(20)));
    auto p_shared = make_shared<Person>(Person(40));
    people_a.push_back(p_shared);
    people_a.push_back(p_shared);

    auto people_b = people_a;

    std::cout << "people_a    is......" << std::endl;
    for (auto &p : people_a)
    {
        std::cout << "p is " << p << std::endl;
    }

    std::cout << "people_b      is......" << std::endl;
    for (auto &p : people_b)
    {
        std::cout << "p is " << p << std::endl;
    }
    for (auto &p : people_a)
    {
        if (p != nullptr)
        {
            p.reset();
            p = nullptr;
        }
    }
    std::cout<<people_a[0]<<std::endl;
    assert(people_a[0]==nullptr);
    std::cout<<people_a[0]->age<<std::endl;//Segmentation fault
    for(auto &p:people_b)
    {
        std::cout<<p->age<<"  ";
        std::cout<<"p's reference num is "<<p.use_count()<<std::endl;
    }
    std::cout<<people_b.back().use_count()<<std::endl;
    for (auto &p : people_b)
    {
        if (p != nullptr)
        {
            p.reset();
            p = nullptr;
        }
    }
    return 0;
}
/*
people_a    is......
p is 0x55555556fec0
p is 0x55555556ff00
p is 0x55555556fee0
p is 0x55555556fee0
people_b      is......
p is 0x55555556fec0
p is 0x55555556ff00
p is 0x55555556fee0
p is 0x55555556fee0
0      
10  p's reference num is 1
20  p's reference num is 1
40  p's reference num is 3
40  p's reference num is 3   //前面有个auto p_shared
3
*/

7.5 引用并不会增加shared_ptr的计数

#include <iostream>
#include <memory>
using namespace std;
int main(int argc, char **argv)
{
    auto p = make_shared<int>(10);
    std::cout << "p's reference num is " << p.use_count() << std::endl;
    auto &a = p;
    std::cout << "p's reference num is " << p.use_count() << std::endl;
    return 0;
}
/*
p's reference num is 1
p's reference num is 1
*/

7.6 赋值的时候注意

7.6.1 引用导致出错

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
int main(int argc, char **argv)
{
    std::vector<shared_ptr<int>> nums_ptr;
    nums_ptr.push_back(make_shared<int>(10));
    auto &temp_ptr = nums_ptr.back();
    std::cout << "temp_ptr use_count = " << temp_ptr.use_count() << std::endl;
    std::cout << "nums_ptr[0] use_count=" << nums_ptr[0].use_count() << std::endl;
    nums_ptr.push_back(temp_ptr);
    nums_ptr.push_back(temp_ptr);
    std::cout << "nums_ptr size is " << nums_ptr.size() << std::endl;
    std::cout << "[0] use_count=" << nums_ptr[0].use_count() << std::endl;
    std::cout << "[1] use_count=" << nums_ptr[1].use_count() << std::endl;
    std::cout << "[2] use_count=" << nums_ptr[2].use_count() << std::endl;
    return 0;
}
/*
temp_ptr use_count = 1
nums_ptr[0] use_count=1
nums_ptr size is 3
[0] use_count=2
[1] use_count=2
[2] use_count=1
Segmentation fault
*/
2
    std::vector<shared_ptr<int>> nums_ptr;
    nums_ptr.push_back(make_shared<int>(10));
    auto &temp_ptr = nums_ptr.back();
    std::cout << "temp_ptr use_count = " << temp_ptr.use_count() << std::endl;
    std::cout << "nums_ptr[0] use_count=" << nums_ptr[0].use_count() << std::endl;
    nums_ptr.push_back(temp_ptr);
    nums_ptr.push_back(temp_ptr);
    nums_ptr.push_back(temp_ptr);
    std::cout << "nums_ptr size is " << nums_ptr.size() << std::endl;
    std::cout << "[0] use_count=" << nums_ptr[0].use_count() << std::endl;
    std::cout << "[1] use_count=" << nums_ptr[1].use_count() << std::endl;
    std::cout << "[2] use_count=" << nums_ptr[2].use_count() << std::endl;
    std::cout << "[3] use_count=" << nums_ptr[3].use_count() << std::endl;
    return 0;

输出

temp_ptr use_count = 1
nums_ptr[0] use_count=1
nums_ptr size is 4
[0] use_count=2
[1] use_count=2
[2] use_count=2
[3] use_count=2
Segmentation fault

Segmentation fault发生在
请添加图片描述

上述会导致Segmentation fault析构的时候出现问题
_M_dispose()释放对象出现了问题
从引用计数来看一个temp引用的一个数组存的,先到引用计数为零,释放指向内存,后面释放就出问题了
emplace_back也是同样的问题

7.6.2 非引用

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
int main(int argc, char **argv)
{
    std::vector<shared_ptr<int>> nums_ptr;
    nums_ptr.push_back(make_shared<int>(10));
    auto temp_ptr = nums_ptr.back();
    nums_ptr.push_back(temp_ptr);
    nums_ptr.push_back(temp_ptr);
    std::cout << nums_ptr.size() << std::endl;

    std::cout << nums_ptr[0].use_count() << std::endl;
    temp_ptr.reset();
    std::cout << nums_ptr[0].use_count() << std::endl;
    return 0;
}
/*
3
4
3
*/

综合以上,对于指针指针慎重和引用一起使用(不要写入就行)
对于智能指针的判断:进行reset后进行置空,使用nullptr进行判断
不要用use_count对于那么多数据也不知道什么时候计数器清空
auto temp_ptr使用然后再消亡即可

7.6.3 attention

7.6.2和7.6.3都是对一个数组操作,取出某位数组然后存入
两个数组的时候
正常不会出现Segmentation fault

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
int main(int argc, char **argv)
{
    std::vector<shared_ptr<int>> nums_ptr;
    nums_ptr.push_back(make_shared<int>(10));

    vector<shared_ptr<int>> new_nums;
    auto &temp_ptr = nums_ptr.back();
    std::cout << "temp_ptr count =" << temp_ptr.use_count() << std::endl;
    std::cout << "nums_ptr[0] count =" << nums_ptr[0].use_count() << std::endl;
    std::cout << "=============================" << std::endl;
    new_nums.push_back(temp_ptr);
    new_nums.push_back(temp_ptr);
    std::cout << "temp_ptr count =" << temp_ptr.use_count() << std::endl;
    std::cout << "nums_ptr[0] count =" << nums_ptr[0].use_count() << std::endl;
    std::cout << "new_nums[0] count = " << new_nums[0].use_count() << std::endl;
    std::cout << "new_nums[1] count = " << new_nums[1].use_count() << std::endl;
    return 0;
}

输出结果

temp_ptr count =1
nums_ptr[0] count =1
=============================
temp_ptr count =3
nums_ptr[0] count =3
new_nums[0] count = 3
new_nums[1] count = 3

显然对智能指针的应用并不增加智能指针的引用计数

7.6.3 注意

因为引用不可更改对象,
问题出现在对存放智能指针的同一个数组 对里面数组的进行取出(引用),再放入同一数组。
引用计数并没有按照预想的增加
你对
for(auto& p:people){}
这种对单个对象(读)操作还好(基本上都这么用),但是放进了数组中
放进一个位置,只绑定了一个对象,不出错(难保以后不出错)
避免用同一个数组引用智能指针存取

7.7 直接删除数组的某个元素

#include <iostream>
#include <deque>
#include <memory>
#include <vector>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};
int main(int argc, char **argv)
{
    std::vector<shared_ptr<Person>> people_a;
    people_a.push_back(make_shared<Person>(Person(10)));
    people_a.push_back(make_shared<Person>(Person(20)));
    auto p_shared = make_shared<Person>(Person(40));
    people_a.push_back(p_shared);
    people_a.push_back(p_shared);
    for (int i = 0; i < people_a.size(); ++i)
    {
        std::cout << people_a[i].use_count() << std::endl;
    }
    std::cout << "people_a's size is " << people_a.size() << std::endl;
    people_a.pop_back();
    for (int i = 0; i < people_a.size(); ++i)
    {
        std::cout << people_a[i].use_count() << std::endl;
    }
    std::cout << "people_a's size is " << people_a.size() << std::endl;
    return 0;
}
/*
1
1
3
3
people_a's size is 4
1
1
2
people_a's size is 3
*/
/*
有多一个的原因是前面 auto p_shared = make_shared<Person>(Person(40));有一个引用计数
*/

引用计数减一 数组的size()减一:这样销毁一个指针,引用计数减一

7.8 erase

#include <iostream>
#include <deque>
#include <memory>
#include <vector>
using namespace std;
class Person
{
public:
    Person(int _age) : age(_age)
    {
    }
    int age;
};
int main(int argc, char **argv)
{
    std::vector<shared_ptr<Person>> people_a;
    people_a.push_back(make_shared<Person>(Person(10)));
    people_a.push_back(make_shared<Person>(Person(20)));
    auto p_shared = make_shared<Person>(Person(40));
    people_a.push_back(p_shared);
    people_a.push_back(p_shared);
    for (int i = 0; i < people_a.size(); ++i)
    {
        std::cout << people_a[i].use_count() << std::endl;
    }
    std::cout << "people_a's size is " << people_a.size() << std::endl;
    //people_a.pop_back();//(1)

    // auto it = people_a.end();(2)
    // it--;
    // people_a.erase(it);

    // auto it = people_a.begin();//(3)
    // it+=2;
    // people_a.erase(it);

    for (int i = 0; i < people_a.size(); ++i)
    {
        std::cout << people_a[i].use_count() << std::endl;
    }
    std::cout << "people_a's size is " << people_a.size() << std::endl;
    return 0;
}
/*(1)
1
1
3
3
people_a's size is 4
1
1
2
people_a's size is 3
*/
/*
有多一个的原因是前面 auto p_shared = make_shared<Person>(Person(40));有一个引用计数
*/

/*(2)
1
1
3
3
people_a's size is 4
1
1
2
people_a's size is 3
*/

/*(3)
1
1
3
3
people_a's size is 4
1
1
2
people_a's size is 3

*/

八、智能指针管理类

有的类会去删除拷贝构造函数,然后让智能指针管理这个类,这样这个类的拷贝就变成智能指针的浅拷贝
RAII与智能指针
1:53:48

8.1 示例说明

mention
// Person *p1 = new Person(10, 120);
// Person *p2(p1);
// Person *p3 = p1;
上述直接进行的是指针的赋值,不会下面的共享内存拷贝

#include <iostream>
#include <memory>
using namespace std;
class Person
{
public:
    Person(int _age, int _height) : age(_age)
    {
        height = make_shared<int>(_height);
    }
    int age;
    shared_ptr<int> height;
};
int main()
{
    // Person *p1 = new Person(10, 120);
    // Person *p2(p1);
    // Person *p3 = p1;
    // std::cout<<p1->height<<std::endl;//0x55555556cee0
    // std::cout<<p2->height<<std::endl;//0x55555556cee0
    // std::cout<<p3->height<<std::endl;//0x55555556cee0
    // std::cout<<p3->height.use_count()<<std::endl;//1
    // p3->height.reset();
    // std::cout<<*(p2->height);//Segmentation fault
    // std::cout<<"----------------"<<std::endl;


    Person p4(10, 120);
    Person p5(p4);
    Person p6 = p4;
    std::cout << p4.height << std::endl;//0x55555556cec0
    std::cout << p5.height << std::endl;//0x55555556cec0
    std::cout << p6.height << std::endl;//0x55555556cec0
    std::cout << p4.height.use_count() << std::endl;//3
    p4.height.reset();
    std::cout << *(p5.height)<<std::endl;//120
    std::cout << p4.height.use_count() << std::endl;//0
    std::cout << p5.height.use_count() << std::endl;//2
    std::cout << "----------------" << std::endl;
    return 0;

}

8.2 含有vector<ptr>的拷贝

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Person
{
public:
    Person(int _age, int _height) : age(_age)
    {
        height = make_shared<int>(_height);
        measure_buf.push_back(make_shared<int>(_height));
    }
    int age;
    vector<shared_ptr<int>> measure_buf;
    shared_ptr<int> height;
};
int main()
{
    Person p1(10, 120);
    Person p2(p1);
    Person p3(p1);
    std::cout << p1.measure_buf[0] << std::endl;
    std::cout << p2.measure_buf[0] << std::endl;
    std::cout << p3.measure_buf[0] << std::endl;
    std::cout << p1.measure_buf[0].use_count() << std::endl;
    std::cout << "==================" << std::endl;
    vector<shared_ptr<int>> a_buf;
    a_buf.push_back(make_shared<int>(100));
    auto b_buf = a_buf;
    std::cout << "a_buf和b_buf的地址" << std::endl;//两者的地址是不一样的,可以说明是深拷贝
    std::cout << &a_buf[0] << std::endl;//0x555555570380
    std::cout << &b_buf[0] << std::endl;//0x5555555703a0
    std::cout << "里面数据" << std::endl;
    std::cout << a_buf[0] << std::endl;//0x555555570370
    std::cout << b_buf[0] << std::endl;//0x555555570370
    std::cout << a_buf[0].use_count() << std::endl;//2
    std::cout << b_buf[0].use_count() << std::endl;//2
    std::cout << "after reset" << std::endl;
    a_buf[0].reset();
    std::cout << a_buf[0] << std::endl;//0->nullptr
    std::cout << b_buf[0] << std::endl;//0x555555570370
    std::cout << a_buf[0].use_count() << std::endl;//0
    std::cout << b_buf[0].use_count() << std::endl;//1
    std::cout << *(b_buf[0]) << std::endl;//100
    return 0;
}

从a_buf=b_buf得出的首地址不一致可以看出新开辟了内存
vector内数据使用结构体的话是深拷贝,vector内的数据会拷贝一份保存,vector内数据不会丢失。如果vector内数据是指针的话是进行浅拷贝,数据超出作用域后会自动析构,vector内所指向的数据会被更改和丢失,所以vector如果作为全局变量,不应该使用指针。
其实就是按照Values拷贝,非指针新开辟内存,指针直接赋值

从a_buf=b_buf得出的首地址不一致是vector的深拷贝,里面的是按值拷贝

九、右值引用

C++ Primer 5th 中文版 Page 471
为了支持移动操作,新标准引入了一种新的引用类型----右值引用
右值引用—>必须绑定到右值的引用:通过&&来获得右值引用
右值引用的性质–>只能绑定到将要销毁的对象上。因此可以自由地将一个右值引用的资源“移动”到另一个对象上

一般而言,一个左值表达式表示的是一个对象的身份,而右值表达式表示的是对象的值
C++ Primer 5th 中文版 Page 121:当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)

int i=42;
int &r = i;//正确,左值引用
int &&rr = i;//错误,不能将一个右值引用绑定到一个左值上
int &r2 = i*42;//错误i*42是一个右值
const int &r3 = i*42;//正确,可以将一个const的引用绑定到一个右值上
int &&rr2 = i*42;//正确,将rr2绑定到乘法结果上

9.1 左值持久,右值短暂

由于右值引用只能绑定到临时对象上,所以

所引用的对象将要被销毁
该对象没有其他用户

所以使用右值引用的代码可以自由的接管所引用的对象的资源
右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态

十、虚函数、纯虚函数、虚函数指针、虚函数表

10.1 虚函数、纯虚函数

子类继承父类并重写父类的虚函数实现多态
定义纯虚函数的类是抽象类,不可以被实例化,如果子类继承该类但没有重写纯虚函数,子类将也是抽象类不可被实例化。

#include <iostream>
using namespace std;
class Base
{
public:
    virtual void speak()
    {
        std::cout << "base" << std::endl;
    }
};
class Base2
{
public:
    virtual void speak() = 0;
};
int main()
{
    Base base; //只有当虚函数变成纯虚函数时候,才可以变成抽象类,不可实例化对象
    Base2 base;//object of abstract class type "Base2" is not allowed 纯虚函数不可被实例化
    base.speak();
    return 0;
}

10.1.2 需全部重写否则仍然为抽象类

#include <iostream>
using namespace std;
class Base
{
public:
    virtual void speak()
    {
        std::cout << "base speak" << std::endl;
    }
    virtual void cry()
    {
        std::cout << "base cry" << std::endl;
    }
};

class Base2
{
public:
    virtual void speak() = 0;
    virtual void cry() = 0;
};

class Son2 : public Base2
{
public:
    virtual void speak()
    {
        std::cout << "Son2 speak" << std::endl;
    }
    virtual void cry()
    {
        std::cout << "Son2 cry" << std::endl;
    }
};
void a()
{
}
int main()
{
    Base base1; //只有当虚函数变成纯虚函数时候,变成抽象类,不可实例化对象
    // Base2 base2;//object of abstract class type "Base2" is not allowed 纯虚函数不可被实例化

    Son2 son2;
    base1.speak();
    // printf("%p\n",&a);
    std::cout << &a << std::endl;
    return 0;
}

10.2 C++对象模型、虚函数指针、虚函数表

深度探索C++对象模型 Page 37:
Stroustrup当初设计(⽬前仍占有优势)的C++对象模型是从简单对象模型派⽣⽽来的,并对内存空间和存取时间做了优化。在此模型中,Nonstatic data members被配置于每⼀个class object之内,static data members则被存放在个别的class object之外。Static和nonstatic function members也被放在个别的class object之外。Virtual functions则以两个步骤⽀持之:

注:成员和成员函数存储是不同的

1.每⼀个 class产⽣出⼀堆指向 virtual functions 的指针,放在表格之中。这个表格被称为 virtual table(vtbl )。
2 .每⼀个 class object被安插⼀个指针,指向相关的 virtual table。通常这个指针被称为 vptr 。vptr 的设定(setting)和重置(resetting)都由每⼀个 class的 constructor、destructor和 copy assignment运算符⾃动完成(我将在第5章讨论这个问题)。每⼀个 class所关联的 type_info object(⽤以⽀持 runtime type identification,RTTI)也经由 virtual table被指出来,通常放在表格的第⼀个 slot。
图片来自黑马
请添加图片描述
取地址

十一、移动构造、移动赋值

参考小彭老师
三五法则:

如果一个类定义了解构函数,那么必须同时定义或删除拷贝构造函数拷贝赋值函数,否则出错。
如果一个类定义了拷贝构造函数,那么必须同时定义或删除拷贝赋值函数,否则出错,删除可导致低效。
如果一个类定义了移动构造函数,那么必须同时定义或删除移动赋值函数,否则出错,删除可导致低效。
如果一个类定义了拷贝构造函数拷贝赋值函数,那么必须最好同时定义移动构造函数移动赋值函数,否则低效。

11.1 移动

有时需要把一个对象v2移动到v1上,不需要涉及实际数据的拷贝
时间复杂度:移动是O(1),拷贝是O(n),
使用std::move()实现移动
v2被移动到v1后,原来v2会被清空因此仅当v2再也用不到的时候采用移动

11.1.1 交换

std::swap(v1,v2)
std::swap()定义在 /usr/include/c++/9/bits---->std::move底层也应该是利用std::move()实现的
可以参考一下c++ std::swap()和STL提供的swap()成员函数的不同
vector<T>::swap() (a.swap(b))只交换了指向的地址

11.1.2 触发移动的情况

//这些情况下编译器会调用移动:
return v2                                                    // v2 作返回值
v1 = std::vector<int>(200)                         // 就地构造的 v2
v1 = std::move(v2)                                    // 显式地移动
//这些情况下编译器会调用拷贝:
return std::as_const(v2)                            // 显式地拷贝
v1 = v2                                                      // 默认拷贝
//注意,以下语句没有任何作用:
std::move(v2)                                            // 不会清空 v2,需要清空可以用 v2 = {} 或 v2.clear()
std::as_const(v2)                                      // 不会拷贝 v2,需要拷贝可以用 { auto _ = v2; }
//这两个函数只是负责转换类型,实际产生移动/拷贝效果的是在类的构造/赋值函数里。
std::move(t)       相当于 (T &&)t
std::as_const(t)   相当于 (T const &)t

11.1.2 移动构造:缺省实现

小彭老师PPT
在这里插入图片描述

同样,如果对降低时间复杂度不感兴趣:
移动构造≈拷贝构造+他解构(other)+他默认构造(this)
移动赋值≈拷贝赋值+他解构(other)+他默认构造(this)
只要不定义移动构造和移动赋值,编译器会自动这样做。虽然低效,但至少可以保证不出错。
若自定义了移动构造,对提高性能不感兴趣:
移动赋值≈解构+移动构造(使用了std::move)

11.1.3 小技巧:如果有移动赋值函数,可以删除拷贝赋值函数

小彭老师PPT
在这里插入图片描述

其实:如果你的类已经实现了移动赋值函数,那么为了省力你可以删除拷贝赋值函数。
这样当用户调用: v2 = v1时,
因为拷贝赋值被删除,编译器会尝试: v2 = List(v1)
从而先调用拷贝构造函数,然后因为 List(v1)
相当于就地构造的对象,从而变成了移动语义,从而进一步调用移动赋值函数

十二、智能指针

std::shared_ptr 共享 引用计数器 浅拷贝
std::unique_ptr 独享 应用计数器 禁止拷贝
std::weak_ptr 弱应用(与std::shared_ptr使用不会增加共享指针的计数器) 浅拷贝

12.1 std::shared_ptr

//创建方式1
std::shared_ptr<int> p = std::make_shared<int>(42);
//创建方式2 如果不初始化一个智能指针,它会被初始化一个空指针
std::shared_ptr<int> p1;
std::shared_ptr<int> p2(new int(1024));

12.1.1 不要使用get初始化另一个指针或为智能指针赋值

来自C++ Primer 5th 中文版 Page 414 代码结果有点不符合预期?

std::shared_ptr<int> p(new int(42));//引用计数为1
int *q = p.get();//正确:但是不要让q管理的内存被释放
{
	//新程序块
	//未定义:两个独立的shared_ptr指向相同的内存
	std::shared_ptr<int>(q);
}//程序块结束,q被销毁,指向的内存被释放
int foo = *p;//未定义;p指向的内存已经被释放

见12.1.1.1 实验一的结果直接正常打印输出了
上述更正一下见下面分析

12.1.1.1 实验一
#include <iostream>
#include <memory>
using namespace std;
int main()
{
    std::shared_ptr<int> p(new int(42)); //引用计数为1
    int *q = p.get();//正确:但是不要让q管理的内存被释放
    {                    
        //新程序块
        std::shared_ptr<int>(q); //就地,右值即将消亡不会运行后,p的引用计数仍然有     
    }             
    int foo = *p; 
    std::cout<<"foo="<<foo<<std::endl;//42
    return 0;
}

正常输出

12.1.1.2 实验二
#include <iostream>
#include <memory>
using namespace std;
int main()
{
    std::shared_ptr<int> p(new int(42)); 
    int *q = p.get();
    {
                            
        auto other = std::shared_ptr<int>(q);

    }             
    int foo = *p; 
    std::cout<<"foo="<<foo<<std::endl;
    return 0;
}
foo=0
free(): double free detected in tcache 2

智能指针other离开作用域 引用计数变为零销毁指向的内存,智能指针指向的对象消失,输出了0
智能指针析构导致double free
指针q在main()作用域下
debug模式发生在 shared_ptr_base.h的第376-377行:智能指针double free了

      _M_dispose() noexcept
      { delete _M_ptr; }
12.1.1.3 实验3
#include <iostream>
#include <memory>
using namespace std;
int main()
{
    std::shared_ptr<int> p(new int(42));

    {
        int *q = p.get();
        std::shared_ptr<int>(q);
    }
    int foo = *p;
    std::cout << "foo=" << foo << std::endl;
    return 0;
}
/*
编译不通过
error: conflicting declaration ‘std::shared_ptr<int> q’
   10 |         std::shared_ptr<int>(q);
note: previous declaration as ‘int* q’
    9 |         int *q = p.get();   
*/

十三、多线程死锁与线程安全

参考小彭老师
在这里插入图片描述

由于同时执行的两个线程,他们中发生的指令不一定是同步的,因此有可能出现这种情况:
t1 执行 mtx1.lock()。
t2 执行 mtx2.lock()。
t1 执行 mtx2.lock():失败,陷入等待
t2 执行 mtx1.lock():失败,陷入等待
双方都在等着对方释放锁,但是因为等待而无法释放锁,从而要无限制等下去。
这种现象称为死锁(dead-lock)。

两个线程持有锁,都在等待对方的锁释放
左目图像和右目图像传入的话用两个锁要考虑锁的顺序

与此同时,同一个线程重复调用lock()也会造成死锁

13.1 一个简单的单生产者单消费者无锁队列

#include <iostream>
#include <queue>
#include <thread>
//#include <mutex>
using namespace std;
int num = 0;
//std::mutex mtx;
queue<int> qu;
class Producer
{
public:
    void write(std::queue<int> &qu)
    {
        while (1)
        {
            //std::unique_lock<mutex> ul(mtx);
            qu.push(num);
            num++;
            if(num>10000) //不加这一行会有double free or corruption (!prev)
            {
                num=0;
            }
            //std::cout<<"qu size is "<<qu.size()<<std::endl;//不加肯会出现 Segmentation fault (core dumped)
            //加了上一行 qu size is 6519121 还没出错(很明显时间比上面的时间要长的多)
            //分析qu size 一直在增长,但是加了一句打印显然要时间拖慢了Segmentation fault (core dumped)应该是qu所占内存越来越大把内存干崩了
            //std::cout<<"qu size is "<<qu.size()<<std::endl;
            printf("qu size is %d",(int)qu.size());
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    }
};
/*
std::this_thread::sleep_for(std::chrono::milliseconds(100));
交互会有问题
qu size is 0

241
qu size is 0

242
qu size is 0

243
qu size is 0
qu size is 
244
1
qu size is 
245
1
qu size is 
246
1

247
qu size is 0
qu size is 
248
1

qu size is 249
1

250qu size is 
1
qu size is 
251
1

252
qu size is 0

qu size is 253
1
qu size is 1

254
qu size is 
255
1
^C
https://blog.csdn.net/ctthuangcheng/article/details/9177885
(1)printf:在对标准输出作任何处理前先加锁。

(2)std::cout:在实际向标准输出打印时方才加锁。
换成printf
165
qu size is 1
166
qu size is 1
167
qu size is 1
168
qu size is 1qu size is 1
169

170
qu size is 1
171
qu size is 1
172
qu size is 1
问题小很多
*/
class Consumer
{
public:
    void read(std::queue<int> &qu)
    {
        while (1)
        {
            if (!qu.empty())
            {
                //std::unique_lock<mutex> ul(mtx);
                int temp = qu.front();
                printf("\n%d\n",temp);
                //std::cout <<std::endl<<temp<<std::endl;
                qu.pop();
                //return temp;//退出去读写的循环就被破坏了  如果想获取数据就用引用传值
            }
        }
    }
};
int main(int argc, char **argv)
{
    Producer producer;
    Consumer consumer;
    std::thread t1(&Producer::write, &producer, std::ref(qu));
    std::thread t2(&Consumer::read, &consumer, std::ref(qu));
    t1.join();
    t2.join();
    return 0;
}

13.2 std::thread构造函数传参和std::ref

13.2.1 代码

#include <iostream>
#include <queue>
#include <thread>
class ThreadPool
{
    std::vector<std::thread> m_pool;
public:
    void push_back(std::thread thr)
    {
        m_pool.push_back(std::move(thr));
    }
    ~ThreadPool()
    {
        for(auto &t:m_pool)
        {
            t.join();
        }
    }
};
using namespace std;
int num = 0;
queue<int> qu;
class Producer
{
public:
    void write(std::queue<int> &qu)
    {
        while (1)
        {
            qu.push(num);
            num++;
            if(num>10000) 
            {
                num=0;
            }
            printf("qu size is %d",(int)qu.size());
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    }
};

class Consumer
{
public:
    void read(std::queue<int> &qu, int &num_out )
    {
        while (1)
        {
            if (!qu.empty())
            {
                int temp = qu.front();
                num_out = temp;
                printf("\n%d\n",temp);
                qu.pop();
            }
        }
    }
};
ThreadPool tpool;
int main(int argc, char **argv)
{
    Producer producer;
    Consumer consumer;
    int a=0;
    std::thread t1(&Producer::write, &producer, std::ref(qu));
    std::thread t2(&Consumer::read, &consumer, std::ref(qu),std::ref(a));
    tpool.push_back(std::move(t1));
    tpool.push_back(std::move(t2));
    std::this_thread::sleep_for(std::chrono::milliseconds(422));
    std::cout<<"a ============== "<<a<<std::endl;//a ============== 4    
    return 0;
}

上述代码是类的成员函数地址,调用的对象,参数传递列表(引用是std::ref,值传递就是std::thread t2(f1, n + 1); ),对上就行

13.2.2 std::ref

用来构建一个reference_wrapper对象并返回,该对象拥有传入的elem变量的引用。如果参数本身是一个reference_wrapper类型的对象,则创建该对象的一个副本,并返回。
thread的方法传递引用的时候,必须用ref来进行引用传递,否则就是浅拷贝。
std::ref只是尝试模拟引用传递,并不能真正变成引用,在非模板情况下,std::ref根本没法实现引用传递,只有模板自动推导类型或类型隐式转换时,ref能用包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型。
std::ref和std::reference_wrapper
std::ref详解

13.3.3 std::thread构造

std::thread 构造方法
(1)默认构造函数 thread() noexcept;
(2)初始化构造函数 template <class Fn, class… Args>
(3)拷贝构造函数 thread (const thread&) = delete;
(4)move构造函数 thread (thread&& x) noexcept;

(1)默认构造函数:创建一个空thread对象,该对象非joinable
(2)初始化构造函数:创建一个thread对象,该对象会调用Fn函数,Fn函数的参数由args指定,该对象是joinable的
(3)拷贝构造函数:被禁用,意味着thread对象不可拷贝构造
(4)move构造函数:移动构造,执行成功之后x失效,即x的执行信息被移动到新产生的thread对象,该对象非joinable

值传递使用值传递,引用使用std::ref
调用对象的成员函数就是 成员函数地址,调用的对象地址,参数列表

13.3 无锁队列的实现学习(TODO)

1
2
3
4

13.4 数据结构安全

STL容器不是线程安全的

13.4.1 mutable

vector<>::size()是const的mutux::lock()不是const的利用这去保护vector的访问(size时会出错)
逻辑上是const而部分成员非const:采用mutable

mutable std::mutex m_mtx;
/**
size() 在逻辑上仍是 const 的。因此,为了让 this 为 const 时仅仅给 m_mtx 开后门,
可以用 mutable 关键字修饰他,从而所有成员里只有他不是 const 的。
*/

13.4.2 读写锁

13.5 条件变量

13.6 原子操作

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值