c++11新特性总结

原始字面量

#include <iostream>
using namespace std;
int main (){
    string str = R"(\t今天你学习了吗\n\n)";
    string str1 = "(\t\n今天你学习了吗\n)";
    string str2 = "(\\t\\n今天你学习了吗\\n)";
    cout <<str<<endl;
    cout<< str1<<endl;
    cout << str2<<endl;
    return 0;
}
/*在使用特殊字符时,c++str本身是会对其进行转义的,还要再加入一个反斜杠('\'),影响我们的使用体验,所以使用原始字面量可以便利一些*/

指针空值类型

在c中NULL做为空值,是这样被定义的: (void*)0 。

c++中NULL是这样定义的: 0。

是由于 C++ 中,void * 类型无法隐式转换为其他类型的指针,此时使用 0 代替 ((void *)0)。这使得我们在编译c的时候,要显示的加上强转类型,不然就会报错。

同时void*类型的值不能用于初始化其他类型的实体,如:

void * ptr = NULL;
int *ptr1 = ptr ;
/*这是错误案例
正确如下:*/
int *ptr1 = (int *) ptr;

c++11新特性总结

在c++11中,设计了新的初始化——nullptr 。它可以被编译器隐式转换成被赋值变量的类型。同时在函数参数中也是如此,它可以被转换成哪种类型的参数,它就做为哪种类型的空值进行传递。

auto的使用场景

auto 并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto 并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。

使用语法如下:
auto 变量名 = 变量值;

1、用于容器的遍历:

之前

vector<int>  :: iterator it = vc.begin();

现在:

auto  it = vc.begin();

2、用于泛型编程:

#include <iostream>
#include <string>
using namespace std;

class T1
{
public:
    static int get()
    {
        return 15;
    }
};

class T2
{
public:
    static string get()
    {
        return "helloworld";
    }
};

template <class T>
void func(void)
{
    auto val = T::get();   //用auto来接收val的类型。
    cout << "val: " << val << endl;
}

int main()
{
    func<T1>();
    func<T2>();
    return 0;
}

decltype

在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用 C++11 提供的 decltype 关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:

decltype (表达式)

decltype 是 “declare type” 的缩写,意思是 “声明类型”。decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:

int a = 10;
decltype(a) b = 99;                 // b -> int
decltype(a+3.14) c = 52.13;         // c -> double
decltype(a+b*c) d = 520.1314;       // d -> double

可以看到 decltype 推导的表达式可简单可复杂,在这一点上 auto 是做不到的,auto 只能推导已初始化的变量类型。

第一种:表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的

第二种:表达式是函数调用,使用 decltype 推导出的类型和函数返回值一致。

第三种:表达式是一个左值,或者被括号 ( ) 包围,使用 decltype 推导出的是表达式类型的引用(如果有 const、volatile 限定符不能忽略)

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

class Test
{
public:
    int num;
};

int main() {
    const Test obj;
    //带有括号的表达式
    decltype(obj.num) a = 0;
    decltype((obj.num)) b = a;
    //加法表达式
    int n = 0, m = 0;
    decltype(n + m) c = 0;
    decltype(n = n + m) d = n;
    return 0;
}


obj.num 为类的成员访问表达式,符合场景 1,因此 a 的类型为 int
obj.num 带有括号,符合场景 3,因此 b 的类型为 const int&。
n+m 得到一个右值,符合场景 1,因此 c 的类型为 int
n=n+m 得到一个左值 n,符合场景 3,因此 d 的类型为 int&

final

C++ 中增加了 final 关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和 Jave 的 final 关键字的功能是类似的。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。

1.1 修饰函数
如果使用 final 修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了:

1.2 修饰类
使用 final 关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类。

override

override 关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和 final 一样这个关键字要写到方法的后面。

class Base
{
public:
    virtual void test()
    {
        cout << "Base class...";
    }
};

class Child : public Base
{
public:
    void test() override
    {
        cout << "Child class...";
    }
};


使用了 override 关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误,提高了程序的正确性,降低了出错的概率。

委托构造函数

委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化:

class Test
{
public:
    Test() {};
    Test(int max)
    {
        this->m_max = max > 0 ? max : 100;
    }

    Test(int max, int min):Test(max)
    {
        //this->m_max = max > 0 ? max : 100;             冗余代码
        this->m_min = min > 0 && min < max ? min : 1;
    }

    Test(int max, int min, int mid):Test(max, min)
    {
        //this->m_max = max > 0 ? max : 100;            冗余代码
        //this->m_min = min > 0 && min < max ? min : 1; 冗余代码
        this->m_middle = mid < max && mid > min ? mid : 50;
    }

    int m_min;
    int m_max;
    int m_middle;
};
  • 这种链式的构造函数调用不能形成一个闭环(死循环),否则会在运行期抛异常。
  • 如果要进行多层构造函数的链式调用,建议将构造函数的调用的写在初始列表中而不是函数体内部,否则编译器会提示形参的重复定义。
  • 在初始化列表中调用了代理构造函数初始化某个类成员变量之后,就不能在初始化列表中再次初始化这个变量了。
// 错误, 使用了委托构造函数就不能再次m_max初始化了
Test(int max, int min) : Test(max), m_max(max)
{
    this->m_min = min > 0 && min < max ? min : 1;
}
//初始化min也不行

继承构造函数

C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。

#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i), m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

    int m_i;
    double m_j;
    string m_k;
};

class Child : public Base
{
public:
    using Base::Base;
};

在修改之后的子类中,没有添加任何构造函数,而是添加了 using Base::Base; 这样就可以在子类中直接继承父类的所有的构造函数,通过他们去构造子类对象了。

另外如果在子类中隐藏了父类中的同名函数即(父类中没有用到virtual,同时子类定义新的同名函数),也可以通过 using 的方式在子类中使用基类中的这些父类函数:

class Base
{
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i), m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

    void func(int i)
    {
        cout << "base class: i = " << i << endl;
    }
    
    void func(int i, string str)
    {
        cout << "base class: i = " << i << ", str = " << str << endl;
    }

    int m_i;
    double m_j;
    string m_k;
};

class Child : public Base
{
public:
    using Base::Base;
    using Base::func;
    void func()
    {
        cout << "child class: i'am luffy!!!" << endl;
    }
};

子类中的 func() 函数隐藏了基类中的两个 func() 因此默认情况下通过子类对象只能调用无参的 func(),在上面的子类代码中添加了 using Base::func; 之后,就可以通过子类对象直接调用父类中被隐藏的带参 func() 函数了。

std::initializer_list

在 C++ 的 STL 容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。

先来介绍一下这个类模板的一些特点:

  • 它是一个轻量级的容器类型,内部定义了迭代器 iterator 等容器必须的概念,遍历时得到的迭代器是只读的。

  • 对于 std::initializer_list 而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型 T

  • 在 std::initializer_list 内部有三个成员接口:size(), begin(), end()。

  • std::initializer_list 对象只能被整体初始化或者赋值。

1 作为普通函数参数
如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为 std::initializer_list,使用初始化列表 { } 作为实参进行数据传递即可。

#include <iostream>
#include <string>
using namespace std;

void traversal(std::initializer_list<int> a)
{
    for (auto it = a.begin(); it != a.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
}
int main() 
{	
    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
  
    return 0;
}
/*输出
current list size: 10
1 2 3 4 5 6 7 8 9 0*/

基于范围的for循环

在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。expression 是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。

使用基于范围的 for 循环遍历容器,示例代码如下:

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

int main(void)
{
    vector<int> t{ 1,2,3,4,5,6 };
    for (auto value : t)
    {
        cout << value << " ";
    }
    cout << endl;

    return 0;
}
/*1 2 3 4 5 6 */

在上面的例子中,是将容器中遍历的当前元素拷贝到了声明的变量 value 中,因此无法对容器中的元素进行写操作,如果需要在遍历过程中修改元素的值,需要使用引用。

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

int main(void)
{
    vector<int> t{ 1,2,3,4,5,6 };
    cout << "遍历修改之前的容器: ";
    for (auto &value : t)
    {
        cout << value++ << " ";
    }
    cout << endl << "遍历修改之后的容器: ";

    for (auto &value : t)
    {
        cout << value << " ";
    }
    cout << endl;

    return 0;
}
/*
遍历修改之前的容器: 1 2 3 4 5 6
遍历修改之后的容器: 2 3 4 5 6 7  */

对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用 const 定义保存元素数据的变量,在定义的时候建议使用 const auto &,这样相对于 const auto 效率要更高一些。

尽量用引用,不要用拷贝元素值

右值引用

左值: lvalue :指的是locator value 右值:rvalue 指的是read value 通俗上理解为,可以取地址的,有名字的,非临时的是左值,不能取地址的,没名字的,临时的是右值。

左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了

传统的左值引用。

int a = 10;
int &b = a;  // 定义一个左值引用变量
b = 20;      // 通过左值引用修改引用内存的值

左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。

int &var = 10;

上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:

const int &var = 10;

使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:

const int temp = 10; 
const int &var = temp;

右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

关于右值引用的使用,参考代码如下:

#include <iostream>
using namespace std;

int&& value = 520;
class Test
{
public:
    Test()
    {
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a)
    {
        cout << "copy construct: my name is tom" << endl;
    }
};

Test getObj()
{
    return Test();
}

int main()
{
    int a1;
    int &&a2 = a1;        // error
    Test& t = getObj();   // error
    Test && t = getObj();
    const Test& t = getObj();
    return 0;
}
  • 在上面的例子中 int&& value = 520; 里面 520 是纯右值,value 是对字面量 520 这个右值的引用。

  • 在 int &&a2 = a1; 中 a1 虽然写在了 = 右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。

  • 在 Test& t = getObj() 这句代码中语法是错误的,右值不能给普通的左值引用赋值。

  • 在 Test && t = getObj(); 中 getObj() 返回的临时对象被称之为将亡值,t 是这个将亡值的右值引用。

  • const Test& t = getObj() 这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。 但是常量左值引用只能用做读不能用做写,所以我们用右值引用。

性能优化

在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }
Test(const Test& a) : m_num(new int(*a.m_num))
{
    cout << "copy construct: my name is tom" << endl;
}

~Test()
{
    delete m_num;
}

int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

测试代码执行的结果为:
construct: my name is jerry
copy construct: my name is tom
t.m_num: 100
通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t,在 getObj() 函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }
Test(const Test& a) : m_num(new int(*a.m_num))
{
    cout << "copy construct: my name is tom" << endl;
}

// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num)
{
    a.m_num = nullptr;
    cout << "move construct: my name is sunny" << endl;
}

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

int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};
测试代码执行的结果如下:
construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...

通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。

如果不使用移动构造,在执行 Test t = getObj() 的时候也是进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存也就被析构了,对象 t 也就无法访问这块内存地址了。

在测试程序中 getObj() 的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。

对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。

转载自https://subingwen.cn/cpp/rvalue-reference/#2-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96

转移与完美转发

move

在 C++11 添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助 std::move () 函数,使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。

使用 move 几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用 move () 就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。

forward

右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward () 函数,该函数实现的功能称之为完美转发。

精简后:std::forward(t);

当T为左值引用类型时,t将被转换为T类型的左值
当T不是左值引用类型时,t将被转换为T类型的右值

智能指针

头文件

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

智能指针有三种:分别是:共享的智能指针shared_ptr 独占的智能指针unique_ptr 弱引用的智能指针weak_ptr

其中

  • 共享智能指针是指多个智能指针可以同时管理同一块有效的内存
  • 独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个 unique_ptr 赋值给另一个 unique_ptr。
  • 弱引用的智能指针,它不共享指针,不能操作资源,是用来监视 shared_ptr 的。

shared_ptr

shared_ptr初始化

shared_ptr的初始化方式有4种:

  • 通过构造函数初始化

shared_ptr 智能指针名字(创建堆内存);

  • 通过移动构造函数和拷贝构造函数初始化

拷贝构造: shared_ptr ptr2(ptr1);

移动构造:shared_ptr ptr2(move(ptr1));

move将ptr1变成右值,就可以调用移动构造函数

  • 通过std:make_shared初始化

shared_ptr make_shared( Args&&... args );

使用 std::make_shared() 模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。

  • 通过reset初始化

reset方法是放弃原来监管的内存资源。

例如:shared_ptrptr1 = make shared (99) ; ptr1.reset() ; 在调用完reset后,ptr1所指向的内存的引用次数就由1变成了0。

reset的同时还可以指定一块新的内存资源。如:ptr1.reset(new int(999)); 这样它就完成了新的初始化,同时引用次数又成了1 。

查看引用次数的方法可以通过use_count()方法来查询

————————————————————————————————————————————————————————————

获得原始指针

对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的 get () 方法得到原始地址。

自定义删除器

不具备析构函数的类,使用智能指针的时候应该提供一个删除器。

当智能指针管理的内存对应的引用计数变为 0 的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

c++11的版本中是shared_ptr是不支持删除数组资源的,但是后续版本就支持了

自定义的方法:

// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p)
{
    delete p;
}
shared_ptr<int> ptr(new int(250), deleteIntPtr);

删除器函数也可以是lambda表达式:

   shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });

lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址函数体内部就可以完成删除操作。

注意事项:

  1. 不要用同一个原始指针初始化多个 shared_ptr,会造成二次销毁。
int *p = new int;
{	std::shared_ptr<int> sp1(p); // ok
}
{	std::shared_ptr<int> sp2(p); //这时由p这个内存资源已经被释放掉了,所以error
} 

2.函数不能返回管理了this的共享智能指针对象

原因和上面相同,也是用了一块原始指针初始化了两个共享智能指针。

#include <iostream>
#include <memory>
using namespace std;

struct Test
{
    shared_ptr<Test> getSharedPtr()
    {
        return shared_ptr<Test>(this);
    }
    
    ~Test()
    {
        cout << "class Test is disstruct ..." << endl;
    }

};

int main() 
{
    shared_ptr<Test> sp1(new Test);
    cout << "use_count: " << sp1.use_count() << endl;
    shared_ptr<Test> sp2 = sp1->getSharedPtr();
    cout << "use_count: " << sp1.use_count() << endl;
    return 0;
}

如上图:用new Test匿名对象初始化了sp1,然后调用了getSharedPtr()方法初始化sp2,此时sp1管理的是this,sp2也管理的是this,相当于1中的用1块原始指针初始化了2个智能指针, 这时程序释放这块堆空间时,就会报错。这个问题可以使用weak_ptr来解决此问题。通过 wek_ptr 返回管理 this 资源的共享智能指针对象 shared_ptr。C++11 中为我们提供了一个模板类叫做std::enable_shared_from_this,这个类中有一个方法叫做 shared_from_this(),通过这个方法可以返回一个共享智能指针,在函数的内部就是使用 weak_ptr 来监测 this 对象,并通过调用 weak_ptr 的 lock() 方法返回一个 shared_ptr 对象。

struct Test : public enable_shared_from_this<Test>
{
    shared_ptr<Test> getSharedPtr()
    {
        return shared_from_this();
    }

    ~Test()
    {
        cout << "class Test is disstruct ..." << endl;
    }

};

通过继承这个模板类,返回其中的方法,这样的引用计数就是2了。同时在析构的时候就不会出现问题。

3.禁止使用指向 shared_ptr 的裸指针,也就是智能指针的指针,这听起来就很奇怪,但开发中我们还需要注意,使用 shared_ptr 的指针指向一个 shared_ptr 时,引用计数并不会加一,操作 shared_ptr 的指针很容易就发生野指针异常。

4.循环引用。A中有B , B中有A时,就会出现这样的问题:A出了作用域后,就要释放B,但是释放B的时候,要先把A的资源释放掉,这样就出现了闭环,这两个对象都不会被销毁,从而发生内存泄露。

unique_ptr

初始化:

通过构造函数初始化

unique_ptr<int> ptr1(new int(10));

因为unique_ptr不容许复制, 所以不能使用拷贝构造函数进行初始化,但是可以通过函数返回给其他的 std::unique_ptr,还可以通过 std::move 来转译给其他的 std::unique_ptr,这样原始指针的所有权就被转移了,这个原始指针还是被独占的。

unique_ptr<int> ptr2 = move(ptr1);   // 通过转移所有权的方式初始化

通过reset进行初始化:

 ptr1.reset(new int(9));

删除器

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

using func_ptr = void(*)(int*);
unique_ptr<int, func_ptr> ptr1(new int(10), [](int*p) {delete p; });

weak_ptr

弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手,它不管理 shared_ptr 内部的指针。std::weak_ptr 没有重载操作符 * 和 ->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在。

初始化

// 默认构造函数
constexpr weak_ptr() noexcept;
// 拷贝构造
weak_ptr (const weak_ptr& x) noexcept;
template <class U> weak_ptr (const weak_ptr<U>& x) noexcept;
// 通过shared_ptr对象构造
template <class U> weak_ptr (const shared_ptr<U>& x) noexcept;

use_count()

通过调用 std::weak_ptr 类提供的 use_count() 方法可以获得当前所观测资源的引用计数.

expired()

通过调用 std::weak_ptr 类提供的 expired() 方法来判断观测的资源是否已经被释放.

lock()

通过调用 std::weak_ptr 类提供的 lock() 方法来获取管理所监测资源的 shared_ptr 对象

reset()

通过调用 std::weak_ptr 类提供的 reset() 方法来清空对象,使其不监测任何资源

参考自:
https://subingwen.cn/categories/C/

上一篇:怎么一次同时把多个服务项目进行打包


下一篇:v-show,v-if,display:none,visibility:hidden 的区别和本质