目录
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
#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;
}
-
- 避免隐式转换
不限范围的枚举类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;
}
- 声明前置
在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; //正确,提前指定枚举成员默认类型,可以声明前置
总结:
-
- enum class 是限定作用域枚举类型,枚举成员的作用域限定在枚举类型所声明的作用域中
-
- enum class 不允许隐式转换
-
- enum class 可以前置声明默认枚举成员类型
5.2 堆内存管理:智能指针与垃圾回收
5.2.0 垃圾回收
三种基本的垃圾收集算法及其改进算法
- 引用计数法:用不到根节点的GC算法。每个对象加一个计数器,计数器为0释放这个对象。
- Mark & Sweep算法:分配内存有标记。标记阶段标记出根节点所有可达节点,清除阶段释放每个未被标记的已分配块。
- 节点复制算法:从根节点开始,被引用的对象复制到一个新的存储区域,剩下的对象视为垃圾。
分代回收
思路:程序中存在大量的对象,分配出去之后很快就被回收,也有一部分对象,分配出去相当长的时间没有被回收。对刚分配出去的对象重点扫描,这样可以回收大部分的垃圾。据此将刚分配的内存称为新生代,存在长的称为老生代。
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;
}
- 问题:当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)则会使程序崩溃。
智能指针常用的三个函数
- 获取智能指针托管的指针地址
_NODISCARD _Ty * get() const noexcept
{ // return wrapped pointer
return ptr;
}
- 取消智能指针对动态内存的托管
// 定义智能指针
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);
}
- 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
// 定义智能指针
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:智能指针相关面试题
- 智能指针的实现原理
智能指针的出现主要是为了解决C++中内存泄漏的问题,用对象创建指针,对象生命周期结束后调用析构函数来释放指针指向的资源。
智能指针主要包括
auto_ptr:管理权转移思想,由于赋值之后原对象的使用可能会引起野指针访问,故而c++11 的时候弃用
unique_ptr: 是独享指针,已经禁用了拷贝构造函数和=运算符重载。但是可以通过std::move() 进行显示的转移控制权
share_ptr:共享指针,通过计数来决定何时释放指针
weak_ptr:解决share_ptr 循环依赖的问题,在双向链表和二叉树等数据结构中可以用weak_ptr定义指针
- 智能指针和指针的区别
智能指针利用对象的生命周期来完成对 指针指向资源的管理
普通指针则需要程序员自己申请和释放资源
- 智能指针怎么知道自己的生命周期结束的
auto_ptr 和unique_ptr 由于是独占式管理,对象生命周期到期之后就自动释放管理的指针指向的资源
share_ptr 是共享式的管理,利用内部的引用计数来计算当前指向指针资源的个数,当计数为0时候释放指针指向的资源
weak_ptr 是为了配合share_ptr 指针使用,可以指向share_ptr 但是不增加share_ptr内引用计数
- 智能指针一定不会造成内存泄漏吗?使用的时候要注意什么?
share_ptr 循环引用的情况下,会造成内存泄漏。需要配合weak_ptr使用。
- shared_ptr是线程安全的吗?
不是线程安全的,虽然share_ptr 内部的引用计数是加锁且线程安全的,但是指向的内容在操作写的时候并不是线程安全的。
- 为什么要尽量使用 make_shared()?
节省一次分配内存
shared_ptr x(new Foo); 需要为 Foo 和 ref_count 各分配一次内存
make_shared() 的话,可以一次分配一块足够大的内存,供 Foo 和 ref_count 对象容身