第5章 提高类型安全

5.1 强类型枚举

声明强类型枚举非常简单,只需要在enum后加上关键字class(也可以是struct)
示例代码:

enum class Color { RED, GREEN, BLUE };
enum class Fruit { APPLE, ORANGE, BANANA };
int main() {
    Color c = Color::RED;
    Fruit f = Fruit::APPLE;
    if (c == f) { // 编译错误!无法直接比较不同的强类型枚举
        // ...
    }
    if (c == Color::RED) { // 正确!使用作用域限定的枚举值
        // ...
    }
    // 遍历强类型枚举的所有值
    for (Color color = Color::RED; color <= Color::BLUE; color = static_cast<Color>(static_cast<int>(color) + 1)) {
        // ...
    }
    return 0;
}

注意:同一个命名空间中的不同枚举类型的枚举值不能相同

#include <iostream>
#include <type_traits>
namespace T{
    enum Type{
        General,Light,Medium,Heavy
    };
}
namespace {
    enum Category{
        General,Pistal, MachineGun
    };
}
int main(){
    T::Type t = T::General;
    if (t == General){  // 实际上不同 General 为匿名的namespace中的
        printf("some");
    }
}

上面,Category在一个匿名的namespace 中,所以所有的枚举成员名都默认进行全局名字空间。一旦程序员在检查t的值时忘记使用namespace T了,就会出现错误的结果

enum class与enum区别

    1. 避免枚举成员重定义

示例1

#include <iostream>
using namespace std;
enum Sex{
    Girl,  
    Boy
};
enum Student{
    Girl,  
    Boy
};
int main(){
	Sex a = Girl;    	//错误:“Girl”与以前的声明冲突
	Student b = Girl;   //错误:“Girl”与以前的声明冲突
	return 0;
}

示例2

#include <iostream>
using namespace std;
enum class Sex{
    Girl,  
    Boy
};
enum class Student{
    Girl,  
    Boy
};
int main(){
	Sex a = Sex::Girl;    		//正确
	Student b = Student::Girl;	//正确
	return 0;
}
    1. 避免隐式转换
      不限范围的枚举类enum是可以发生隐式转换的,限定作用域的枚举类型enum class不允许任何隐式转化,可以显示或使用static_cast进行强制转换:
enum class Sex{
     Girl,  
     Boy
};

int main(){
    Sex a=Sex::Girl;
    int b =a; 					// 错误,无法从“Girl”隐式转换为“int”。
    int c = int(a); 			// 正确,显示将enum class转换为整数
    int d = static_cast<int>(a);//正确,进行强制转换
    return 0;
}
  1. 声明前置
    在C++11标准中,enum class的枚举类型可以提前声明,因为枚举成员可以使用默认成员类型int,也可以按编程需要修改默认成员类型。而enum未指定枚举成员默认大小,所以必须指定成员类型:
enum class Color;//正确,声明前置,枚举成员默认类型int
enum class Corlor:std:uint32_t;//正确,声明前置,枚举成员默认类型为uint32_t
enum Color; //错误
enum Color:std:uint8_t; //正确,提前指定枚举成员默认类型,可以声明前置

总结:

    1. enum class 是限定作用域枚举类型,枚举成员的作用域限定在枚举类型所声明的作用域中
    1. enum class 不允许隐式转换
    1. enum class 可以前置声明默认枚举成员类型

5.2 堆内存管理:智能指针与垃圾回收

5.2.0 垃圾回收

三种基本的垃圾收集算法及其改进算法

  1. 引用计数法:用不到根节点的GC算法。每个对象加一个计数器,计数器为0释放这个对象。
  2. Mark & Sweep算法:分配内存有标记。标记阶段标记出根节点所有可达节点,清除阶段释放每个未被标记的已分配块。
  3. 节点复制算法:从根节点开始,被引用的对象复制到一个新的存储区域,剩下的对象视为垃圾。

分代回收

思路:程序中存在大量的对象,分配出去之后很快就被回收,也有一部分对象,分配出去相当长的时间没有被回收。对刚分配出去的对象重点扫描,这样可以回收大部分的垃圾。据此将刚分配的内存称为新生代,存在长的称为老生代。

C++ 垃圾回收机制

C语言本身没有提供GC机制,而C++ 0x则提供了基于引用计数算法的智能指针进行内存管理。也有一些不作为C++标准的垃圾回收库,如著名的Boehm库。借助其他的算法也可以实现C/C++的GC机制,如前面所说的标记清除算法。

5.2.1:基本概念

利用对象的生命周期来控制资源。对象创建时 获取资源,整个生命周期内 对资源的访问有效,对象析构时 释放资源。相当于把管理资源的任务 托管给对象。
优势/好处:
1) 不需要显示的释放资源
2)对象所需资源在生命周期内始终有效

5.2.2:智能指针基本概念

智能指针的本质是 类模版 ,可以创建任意类型的指针对象,智能指针对象使用完毕之后,对象自动调用析构函数释放指针指向的资源。

1. 智能指针的基本框架

template<class T>
class smartPtr{
public:
	//构造函数
	smartPtr(T*  _ptr):ptr(_ptr);
	//析构函数
	~smartPtr(){
		if (ptr != nullptr){
			delete ptr;
			ptr = nullptr;
		}
	}
	// 运算符* 重载
	T& operator*(){
    	return *prt;
	}
	// 运算符-> 重载
	T* operator->(){
		return ptr;
	}
private:
	T* ptr;// 指针对象
};
int main(){
	smartPtr<int>  ptr1 = new int(1);
	smartPtr<string> ptr2 = new string("hello word");
	cout<<*ptr1<<ptr2->c_str()<<endl;
	return 0;
}
  1. 问题:当ptr1 与 ptr2 同时指向同一块资源时,如果同时释放,则会出现重复释放的问题

5.2.3: C++中智能指针

1 . auto_ptr

C++98 中提供的智能指针,解决上诉问题采用的思想:控制权转移。原对象拷贝给新对象时,原对象会被置为nullptr。

std::auto_ptr ptr1(new int);
std::auto_ptr ptr2(ptr1);
在这里插入图片描述

// 拷贝构造函数 -> ap为原来的指针
auto_ptr(auto_ptr &ap):ptr(ap.ptr){
	ap.ptr = nullptr;
}
// 赋值重载函数 -> ap为原来的指针
auto_ptr operator=(auto_ptr<T> &ap){
	if (ptr != ap.ptr){
		ptr = ap.ptr;
		ap.ptr = nullptr;
	}
	return ptr;
}

有个致命问题: 如果auto指针调用拷贝构造函数或者赋值重载函数之后,如果再去使用原来的指针(指向nullptr)则会使程序崩溃。

智能指针常用的三个函数

  1. 获取智能指针托管的指针地址
_NODISCARD _Ty * get() const noexcept
{    // return wrapped pointer
	return ptr;
}
  1. 取消智能指针对动态内存的托管
// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp2 = test.release();    // 取消智能指针对动态内存的托管
delete tmp2;    // 之前分配的内存需要自己手动释放

_Ty * release() noexcept
{    // return wrapped pointer and give up ownership
	_Ty * _Tmp = _Myptr;
	_Myptr = nullptr;
	return (_Tmp);
}
  1. 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
// 定义智能指针
auto_ptr<Test> test(new Test);
test.reset();            // 释放掉智能指针托管的指针内存,并将其置NULL
test.reset(new Test());    // 释放掉智能指针托管的指针内存,并将参数指针取代之

void reset(_Ty * _Ptr = nullptr)
{    // destroy designated object and store new pointer
	if (_Ptr != _Myptr)
	delete _Myptr;
	_Myptr = _Ptr;
}

2. unique_ptr

直接将 拷贝构造函数和赋值重载函数 禁用,不允许拷贝 和 赋值
示例:

unique_ptr<int> ptr1 = new int(1);
unique_ptr<int> ptr2(ptr1); // 编译错误,不允许拷贝
unique_ptr<int> ptr3(new int);
ptr3 = ptr1; //编译错误,不允许赋值
禁用示例:
template<class T>
class unique_ptr{
public:
	unique_ptr(unique_ptr &)=delete;
	unique_ptr operator=(unique_ptr &)=delete;
private:
	T *ptr;
} 

3. share_ptr

采用的是引用计数原理来实现 多个share_ptr之间的资源共享
原理
1) share_ptr 内部维护一份引用计数,记录资源被几个对象共享
2)当一个share_ptr对象被销毁的时候,引用计数 -1;
3)如果引用计数为0 ,则表示该对象为最后一个对象,必须释放资源
4)如果引用计数大于0,则表示该资源还有其他对象引用,不能释放资源,否则其他对象则成为野指针
创建过程
在这里插入图片描述

销毁过程
在这里插入图片描述

share_ptr 实现
赋值重载的三种情况
ptr1 = prt1; // 赋值给自己,不做处理
ptr1 = ptr2; // ptr1 与 ptr2 指向同一块,不做处理
ptr1 = ptr2; // 两个指针指向不同块,做如下处理

在这里插入图片描述

  注意点: 

_ptrcount 是存放在堆上的变量,多线程修改的时候需要注意线程安全,因此需要加锁。
实现源码

template<class T>
class share_ptr{
public:
	share_ptr(T *_ptr):ptr(_ptr){    //构造函数
		_ptrcount = new int(1);
		mt = new mutex();// 锁 
	}
	void addCount(){
		mt->lock();
		(*_ptrcount)++;
		mt->unlock();
	}
	// 拷贝构造函数
	share_ptr(share_ptr<T> &sp){
		ptr=sp->ptr;
		_ptrcount = sp->_ptrcount;
		mt = sp->mt;
		addCount();
	}
	// 赋值重载
	share_ptr<T>& operator=(share_ptr<T> &sp){
		// 判断是否相等
		if (ptr != sp->ptr){
			release();// 将现有的资源进行释放
			ptr=sp->ptr;
			_ptrcount = sp->_ptrcount;
			mt = sp->mt;
			addCount();// 赋值之后的进行叠加
		}
		return *this;
	}
	//返回引用计数
	int& usecount(){
		return *_ptrcount;
	}
	// 析构函数
	~share_ptr(){
		release();
	}
	// 释放资源
	void release(){
		bool deleteFlag = false;
		mt->lock();
		if (--(*_ptrcount) == 0) {
			deleteFlag = true;
			delete _ptr;
			delete _ptrcount;
			ptr = nullptr;
			_ptrcount = nullptr;
		}
		mt->unlock();
		if (deleteFlag){
			delete mt;
		}
	}
	// 重载*
	T& operator*(){
		return *ptr;
	}
	// 重载->
	T* operator*(){
		return ptr;
	}

private:
	T *_ptr;// 指针
	int* _ptrcount;// 计数
	mutex*  mt;
}

4. share_ptr 循环引用

如果定义一个双向链表

struct ListNode{
	share_ptr<ListNode> pre;
	share_ptr<ListNode> next;
	int val;
};
int main(){
	share_ptr<ListNode> ptr1;
	share_ptr<ListNode> ptr2;
	
	ptr1->next = ptr2;
	ptr2->pre = ptr1;
	cout<<"ptr1 的引用计数"<<ptr1->usecount()<<endl;// 数值是2
	cout<<"ptr2 的引用计数"<<ptr2->usecount()<<endl;// 数值是2
}

在这里插入图片描述

这就造成了当两个指针使用完毕之后,_ptrcount = 1 ,ptr1 与ptr2 指向的对象都不能正常释放。

5. weak_ptr 指针

为了解决share_ptr循环依赖的问题,出现了weak_ptr。 可以指向weak_ptr 的对象,但是不改变_ptrcount的计数。

struct ListNode{
weak_ptr<ListNode> pre;
weak_ptr<ListNode> next;
int val;
};
int main(){
share_ptr<ListNode> ptr1;
share_ptr<ListNode> ptr2;

ptr1->next = ptr2;
ptr2->pre = ptr1;
cout<<"ptr1 的引用计数"<<ptr1->usecount()<<endl;// 数值是1
cout<<"ptr2 的引用计数"<<ptr2->usecount()<<endl;// 数值是1
}

在双向链表 或者 二叉树等场景可以用 weak_ptr 来定义结构体,防止循环引用的出现。
代码实现

template<class T>
class weak_ptr{
public:
weak_ptr(share_ptr<T> &sp):ptr(sp->ptr){
}
~weak_ptr();
weak_ptr operator=(share_ptr<T> &sp){
ptr = sp->ptr;
}
T& operator*(){
return *ptr;
}
T* operator->(){
return ptr;
}
private:
T* ptr;
}

6. 定制删除器

int *ptr = new int[10];
delete  ptr; // 错误
delete[] ptr;// 正确

share_ptr<ListNode> ptr1 = new ListNode[10]; //错误

定制删除器
template<class T>
class DelArray{
public:
    void operator(const T* array){
        delete[] array;
    }
}
int main(){
share_ptr<ListNode> ptr1( new ListNode[10], DelArray<ListNode>());
}

5.2.4:智能指针相关面试题

  1. 智能指针的实现原理

智能指针的出现主要是为了解决C++中内存泄漏的问题,用对象创建指针,对象生命周期结束后调用析构函数来释放指针指向的资源。
智能指针主要包括
auto_ptr:管理权转移思想,由于赋值之后原对象的使用可能会引起野指针访问,故而c++11 的时候弃用
unique_ptr: 是独享指针,已经禁用了拷贝构造函数和=运算符重载。但是可以通过std::move() 进行显示的转移控制权
share_ptr:共享指针,通过计数来决定何时释放指针
weak_ptr:解决share_ptr 循环依赖的问题,在双向链表和二叉树等数据结构中可以用weak_ptr定义指针

  1. 智能指针和指针的区别

智能指针利用对象的生命周期来完成对 指针指向资源的管理
普通指针则需要程序员自己申请和释放资源

  1. 智能指针怎么知道自己的生命周期结束的

auto_ptr 和unique_ptr 由于是独占式管理,对象生命周期到期之后就自动释放管理的指针指向的资源
share_ptr 是共享式的管理,利用内部的引用计数来计算当前指向指针资源的个数,当计数为0时候释放指针指向的资源
weak_ptr 是为了配合share_ptr 指针使用,可以指向share_ptr 但是不增加share_ptr内引用计数

  1. 智能指针一定不会造成内存泄漏吗?使用的时候要注意什么?

share_ptr 循环引用的情况下,会造成内存泄漏。需要配合weak_ptr使用。

  1. shared_ptr是线程安全的吗?

不是线程安全的,虽然share_ptr 内部的引用计数是加锁且线程安全的,但是指向的内容在操作写的时候并不是线程安全的。

  1. 为什么要尽量使用 make_shared()?

节省一次分配内存
shared_ptr x(new Foo); 需要为 Foo 和 ref_count 各分配一次内存
make_shared() 的话,可以一次分配一块足够大的内存,供 Foo 和 ref_count 对象容身

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值