【C++】C++11新特性


一、常量及变量的优化

1.1 nullptr的引入

nullptr 出现的目的是为了替代 NULL
传统C++会将NULL、0视为同一东西,取决于编译器如何定义NULL。有些编译器会将 NULL定义为 ((void*)0),有些则会直接将其定义为 0。
C++ 不允许直接将 void * 隐式转换到其他类型
对于这句代码:char* ch = NULL;,没有了 void* 隐式转换的C++ 只好将NULL定义为0,此时对于C++就会发生重载歧义:

void foo(char*);
void foo(int);

foo(NULL);	//此语句会调用void foo(int),违法正常思路

C++11 引入了 nullptr 关键字,专门用来区分空指针、0
nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

void foo(char *) {
    std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {
    std::cout << "foo(int) is called" << std::endl;
int main() {

    foo(0);          // 调用 foo(int)
    // foo(NULL);    // 该行不能通过编译
    foo(nullptr);    // 调用 foo(char*)
    return 0;
}

输出:
foo(int) is called
foo(char*) is called
从输出中我们可以看出,NULL 不同于 0 与 nullptr。

1.2 constexpr

C++11为了提高代码执行效率做了一些改善。这种改善之一就是:生成常量表达式,允许程序利用编译时的计算能力

#define LEN 10

int len_foo() {
    int i = 2;
    return i;
}
constexpr int len_foo_constexpr() {
    return 5;
}

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

int main() {
    char arr_1[10];                      // 合法
    char arr_2[LEN];                     // 合法

    int len = 10;
    // char arr_3[len];                  // 非法
	
    const int len_2 = len + 1;
    constexpr int len_2_constexpr = 1 + 2 + 3;
    // char arr_4[len_2];                // 非法
    //数组的长度必须是一个常量表达式,而对于len_2而言,这是一个const常数
    char arr_4[len_2_constexpr];         // 合法

    // char arr_5[len_foo()+5];          // 非法
    //编译器无法得知len_foo()在运行期实际上是返回一个常数,这也就导致了非法的产生。
    
    char arr_6[len_foo_constexpr() + 1]; // 合法
	
    std::cout << fibonacci(10) << std::endl;
    // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
    std::cout << fibonacci(10) << std::endl;
    return 0;
}

constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。

1.3 类型推导

1.3.1 auto

C++11新标准引入了auto 类型说明符,让编译器去分析表达式的类型。由于,需要编译器推断变量或表达式的类型,所以,auto定义的变量必须初始化。

//编译器会根据初始化的值来推断val的数据类型为flaot,但要注意如果去掉f则编译器会认为val为double型变量
auto val = 5.2f;     
//x初始化为y和z相加的结果,由y和z的数据类型推断x的数据类型
auto x = y + z;  
 //但如果在C++中出现这样的语句,会编译报错,提示“类型包含“auto符号”必须具有初始值设定项”
auto num;               

注意:

  • auto 不能用于函数传参
  • auto不能用于推导数组类型

1.3.2 decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。decltype 仅仅“查询”表达式的类型,并不会对表达式进行“求值”

int i = 4;
decltype(i) a; //推导结果为int。a的类型为int。

decltype还可以用于追踪函数的返回值类型
C++11 引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

template <typename T>
auto multiply(T x, T y)->decltype(x*y)
{
    return x*y;
}

1.4 区间for迭代

int main()
{
	std::vector<int> vec = {1, 2, 3, 4};
	for(auto element : vec)
	{
		std::cout << element << std::endl; 
	}
}

1.5 面向对象

1.5.1 委托构造

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() { // 委托 Base() 构造函数
        value2 = value;
    }
};

1.5.2 继承构造

在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 using 引入了继承构造函数的概念:

#include <iostream>
class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() { // 委托 Base() 构造函数
        value2 = value;
    }
};
class Subclass : public Base {
public:
    using Base::Base; // 继承构造
};

1.5.3 显示虚函数重载

在传统 C++ 中,经常容易发生意外重写虚函数的事情。例如:

struct Base {
    virtual void foo(int a);
};
struct SubClass: Base {
    void foo(char a);
};

SubClass::foo 可能并不是程序员尝试重写虚函数,只是恰好加入了一个具有相同名字的函数。另一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重写该虚函数并摇身一变成为了一个普通的类方法,这将造成灾难性的后果。

  • override
    当重载虚函数时,引入 override 关键字将显式的告知编译器进行重写,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译:
    struct Base {
        virtual void foo(int);
    };
    struct SubClass: Base {
        virtual void foo(int) override;   // 合法
        virtual void foo(float) override; // 非法, 父类没有此虚函数
    };
  • final
    final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。第一,它阻止了从类继承;第二,阻止一个虚函数的重载。
    struct Base {
        virtual void foo() final;
    };
    struct SubClass1 final: Base {
    }; // 合法
    
    struct SubClass2 : SubClass1 {
    }; // 非法, SubClass1 已 final
    
    struct SubClass3: Base {
        void foo(); // 非法, foo 已 final
    };

1.5.4 显示禁用默认函数

在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。
这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。
并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。

class Magic {
    public:
    Magic() = default; // 显式声明使用编译器生成的构造
    Magic(int magic_number);
    
    Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成默认拷贝赋值函数
    
}

1.5.5 强类型枚举

在传统 C++中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较,甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果。
C++11 引入了枚举类(enumeration class),并使用 enum class 的语法进行声明:

enum class new_enum : unsigned int {
    value1,
    value2,
    value3 = 100,
    value4 = 100
};

这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较, 更不可能对不同的枚举类型的枚举值进行比较。
但相同枚举值之间如果指定的值相同,那么可以进行比较:

if (new_enum::value3 == new_enum::value4) {
    // 会输出
    std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}

二、语言运行期的强化

2.1 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. 隐式捕获
    可以在捕获列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获
  • [] 空捕获列表
  • [name1, name2, …] 捕获一系列变量
  • [&] 引用捕获, 让编译器自行推导引用列表
  • [=] 值捕获, 让编译器自行推导值捕获列表
  1. 表达式捕获
    上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。

    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;
    }
  2. lambda表达式底层实现
    参考lambda表达式实现
    底层实现为:创建了一个类,这个类重载了(),让我们可以像调用函数一样使用。
    对于一个lambda表达式[int x, int y](int & a){a = x + y;},它的底层:

    class _SomeCompilerGenerateName_
    {
    public:
    	_SomeCompilerGenerateName_(int x, int y):_x(x),_y(y){}
    	void operator() (int & a)const { a = _x + _y};
    private:
    	int _x;
    	int _y;
    }

2.2 函数对象包装器

2.2.1 std::function

Lambda 表达式的本质是一个和函数对象类型相似的类类型(称为闭包类型)的对象(称为闭包对象), 当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递,例如:

using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
// 定义在参数列表中的函数类型foo被视为退化后的函数指针类型foo*
void functional(foo f) { 
    f(1); // 通过函数指针调用函数
}

int main() {
    auto f = [](int value) {
        std::cout << value << std::endl;
    };
    functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
    f(1); // lambda 表达式调用
    return 0;
}

上面的代码给出了两种不同的调用形式,一种是将 Lambda 作为函数类型传递进行调用, 而另一种则是直接调用 Lambda 表达式。在 C++11 中,统一了这些概念,将能够被调用的对象的类型, 统一称之为可调用类型。而这种类型,便是通过 std::function 引入的。
std::function 是一种通用、多态的函数封装, 它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作, 它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的), 换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理。 例如:

#include <functional>

int foo(int para) {
    return para;
}

int main() {
    // std::function 包装了一个返回值为 int, 参数为 int 的函数
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
        return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

2.3 右值引用

2.3.1 左值、右值的纯右值、将亡值、右值

**左值(lvalue) **,顾名思义就是赋值符号左边的值。准确来说, 左值是表达式后依然存在的持久对象。(不一定是赋值表达式)
右值(rvalue) ,右边的值,是指表达式结束后就不再存在的临时对象。
而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
纯右值(prvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
(字符串字面量只有在类中才是右值,当其位于普通函数中是左值)

将亡值(xvalue),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

class Foo
{
    const char *&&right = "this is a rvalue"; // 此处字符串字面量为右值
    const char *&right = "hello world";    // error
public:
    void bar()
    {
        right = "still rvalue"; // 此处字符串字面量为右值
    }
};
int main()
{
    const char *const &left = "this is an lvalue"; // 此处字符串字面量为左值
    left = "123"; // error
}

2.3.2 左值引用和右值引用

要拿到一个将亡值,就需要用到右值引用:T &&,其中 T 是类型。
右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,例如:

void reference(std::string& str) {
    std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
    std::cout << "右值" << std::endl;
}

int main()
{
    std::string lv1 = "string,"; // lv1 是一个左值
    std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    
    std::string&& rv1 = std::move(lv1); //合法,std::move可以将左值转移为右值
    std::cout << rv1 << std::endl; // string,

    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    // lv2 += "Test"; // 非法, 常量引用无法被修改
    std::cout << lv2 << std::endl; // string,string

    std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    //rv2是具名引用。因此运算符左侧的右值引用作为左值
    
    rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
    std::cout << rv2 << std::endl; // string,string,string,Test

    reference(rv2); // 输出"lvalue ref"
	// rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。
    // 也就是说,T&& Doesn’t Always Mean “Rvalue Reference”, 它既可以绑定左值,也能绑定右值
    return 0;
}

不允许非常量引用绑定到右值
允许常量引用绑定到右值

2.3.3 移动语义

传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作, 调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。

#include <iostream>
class A {
public:
    int *pointer;
    //移动构造
    A():pointer(new int(1)) { 
        std::cout << "构造" << pointer << std::endl; 
    }
    // 无意义的对象拷贝
    A(A& a):pointer(new int(*a.pointer)) { 
        std::cout << "拷贝" << pointer << std::endl; 
    } 
    //移动拷贝
    A(A&& a):pointer(a.pointer) { 
        a.pointer = nullptr;
        std::cout << "移动" << pointer << std::endl; 
    }
    ~A(){ 
        std::cout << "析构" << pointer << std::endl; 
        delete pointer; 
    }
};
// 防止编译器优化
//在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出
A return_rvalue(bool test) {
    A a,b;
    if(test) return a; // 等价于 static_cast<A&&>(a);
    else return b;     // 等价于 static_cast<A&&>(b);
}
int main() {
	//函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,	   并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防       止了这块内存区域被销毁。
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}
#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string

int main() {

std::string str = "Hello world.";
std::vector<std::string> v;

    // 将使用 push_back(const T&), 即产生拷贝行为
    v.push_back(str);
    // 将输出 "str: Hello world."
    std::cout << "str: " << str << std::endl;

    // 将使用 push_back(const T&&), 不会出现拷贝行为
    //而整个字符串会被移动到vector中,所以有时候std::move会用来减少拷贝出现的开销
    // 这步操作后, str 中的值会变为空
    v.push_back(std::move(str));
    // 将输出 "str: "
    std::cout << "str: " << str << std::endl;

    return 0;
}

2.3.4 完美转发(Perfect Forwarding)

一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题:

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T t){ 
    cout<<"f1"<<endl;
    f2(t);  
    //如果t是右值,我们希望传入f2也是右值;如果t是左值,我们希望传入f2也是左值
}   
//在main函数里:
int a = 2;
f1(3); //传入右值
f1(a); //传入左值

当我们从 f1 调用 f2 的时候,不管传入 f1 的是右值还是左值,因为 t 是一个变量名,传入 f2 的时候都变成了左值
所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T&& t) {    //这是通用引用,而不是右值引用
    cout<"f1"<<endl;
    f2(std::forward<T>(t));  //std::forward<T>(t)用来把t转发为左值或右值,决定于T
}

std::forwardstd::move一样,没有做任何事情,std::move单纯的将左值转化为右值, std::forward也只是单纯的将参数做了一个类型的转换,从现象上来看, std::forward<T>(v)static_cast<T&&>(v)是完全一样的。


三、智能指针

3.1 RAII与引用计数

引用计数这种计数是为了防止内存泄露而产生的: 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
RAII( 资源获取即初始化技术):在传统 C++ 中,对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间。
引用计数不是垃圾回收,引用计数能够尽快收回不再被使用的对象,同时在回收的过程中也不会造成长时间的等待, 更能够清晰明确的表明资源的生命周期。

3.2 std::shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

#include <memory>
int main() {
    {
        int a = 10;
        std::shared_ptr<int> ptra = std::make_shared<int>(a);
        std::shared_ptr<int> ptra2(ptra); //copy
        std::cout << ptra.use_count() << std::endl;

        int b = 20;
        int *pb = &a;
        //std::shared_ptr<int> ptrb = pb;  //不能将指针直接赋值给一个智能指针
        std::shared_ptr<int> ptrb = std::make_shared<int>(b);
        ptra2 = ptrb; 	  //赋值拷贝
        pb = ptrb.get(); //获取原始指针

        std::cout << ptra.use_count() << std::endl;
        std::cout << ptrb.use_count() << std::endl;
    }
}

注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。

3.2 std::weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。,weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

#include <iostream>
#include <memory>
int main() {
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        //use_count()可以观测资源的引用计数
        std::cout << sh_ptr.use_count() << std::endl;

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;
        
		//expired()的功能等价于use_count()==0
        if(!wp.expired()){
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;
        }
    }
    //delete memory
}

share_ptr依然存在着资源无法释放的问题,两个类互相引用就会发生循环引用问题:

struct A {
    std::shared_ptr<B> pointer;
    ~A() {
        std::cout << "A 被销毁" << std::endl;
    }
};
struct B {
    std::shared_ptr<A> pointer;
    ~B() {
        std::cout << "B 被销毁" << std::endl;
    }
};
int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->pointer = b;
    b->pointer = a;
}

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
解决这个问题的办法就是使用弱引用指针 std::weak_ptr,std::weak_ptr是一种弱引用:

struct A {
    //std::shared_ptr<B> pointer;
    std::weak_ptr<B> pointer;
    ~A() {
        std::cout << "A 被销毁" << std::endl;
    }
};
struct B {
    std::shared_ptr<A> pointer;
    ~B() {
        std::cout << "B 被销毁" << std::endl;
    }
};
int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->pointer = b;
    b->pointer = a;
}

3.3 std::unique_ptr

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:

std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法

由于是独占,所以不可复制,但我们我们可以利用std::move将其转移给其他的 unique_ptr,例如:


#include <memory>
int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能赋值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷贝
        std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
        uptr2.release(); //释放所有权
    }
    //超过uptr的作用域,內存释放
}

实现智能指针
每个shared_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域的指针
(用原始指针构造时,会new一个地址出来作为计数存放的地方,然后用指针指向它,计数加减都通过计数指针间接操作。)

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;

template <typename T>
class Smart_Point{
private:
    int *_count;
    T *_ptr;
    mutex *_pMutex;

public:
	//构造
    Smart_Point(T* ptr = nullptr): _ptr(ptr), _pMutex(new mutex)
    {
        if(ptr)
            _count = new int(1);
        else
            _count = new int(0);
    }
	//拷贝构造
    Smart_Point(const Smart_Point<T>& sptr)
    {
        if(this != &sptr)
        {
            this->_ptr = sptr._ptr;
            this->_count = sptr._count;
            this->_pMutex = sptr._pMutex;
            AddCount();
        }
    }
	//拷贝赋值
    Smart_Point<T>& operator=(const Smart_Point<T>& sptr)
    {
        if(this->_ptr == sptr._ptr)
            return *this;
        
        if(this->_ptr)
        {
            Release();
            this->_ptr = sptr._ptr;
            this->_count = sptr._ptr;
            this->_pMutex = sptr._pMutex;
            AddCount();
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
	//显示计数
    int UseCount() { return *_count; }
    //获取指针
    T *Get() { return _ptr; }
	//计数+1
    void AddCount()
    {
        _pMutex->lock();
        ++(*_count);
        _pMutex->unlock();
    }
	//析构
    ~Smart_Point()
    {
        Release();
    }
private:
	//释放资源
    void Release()
    {
        bool deleteFlag = false;
        _pMutex ->lock();
        if(--(*_count) == 0)
        {
            delete _count;
            delete _ptr;
            deleteFlag = true;
        }
        _pMutex ->unlock();
        if(deleteFlag == true)
        {
            delete _pMutex;
        }
    }
};

int main()
{
    Smart_Point<int> sp(new int(10));
    Smart_Point<int> sp2(sp);

    std::cout << sp.UseCount() << std::endl;
    
    Smart_Point<int> sp3(new int(20));
    sp2 = sp3;

    std::cout << sp.UseCount() << std::endl;
    std::cout << sp3.UseCount() << std::endl;

    system("pause");
    return 0;
}

四、多线程


文章作者: YukinoKyoU
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YukinoKyoU !
评论
  目录