IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 《More Effictive C++》学习笔记 — 技术(二) -> 正文阅读

[C++知识库]《More Effictive C++》学习笔记 — 技术(二)

条款27 — 要求(或禁止)对象产生于堆之中

有的时候,我们可能会希望对象自杀,通过调用delete this。这样的操作要求对象必须分配与堆中。有的时候,我们希望对象一定分配在栈上,这样他们不会发生内存泄露。那么我们有没有可能要求(或禁止)对象产生于堆之中呢?
至于为什么对象会想要自杀,我在网上看了看,说的比较多的就是外部不容易控制对象的生命周期,因此需要自杀,例如在状态模式的状态转换过程中(待学习到状态模式我们再研究)。

1、要求对象产生于堆之中

最直接的方式就是让构造函数和析构函数都成为private。我们并没有理由和必要让他们都成为私有方法。一个好的选择是将析构函数声明为private。如果想要释放析构此函数,使用一个伪析构函数:

class CLS_Test
{
public:
	void destroy()
	{
		delete this;
	}

private:
	~CLS_Test() = default;
};

int main()
{
	CLS_Test* pHeapObj = new CLS_Test;
	pHeapObj->destroy();
	CLS_Test stackObj; // invalid
	delete p; // invalid
}

之所以不选择将构造函数声明为私有,是因为一个函数有可能有很多构造函数(普通,拷贝,移动等)。

2、判断某个对象是否位于堆中

如果我们希望某个类对象及其派生类的基类成分都产生于堆之中,可能吗?首先,为了让前面的基类的派生类有默认构造函数,我们需要将其析构函数声明为protected。因为《C++ Primer》中提到过当基类或类成员的析构函数为私有或删除时,由编译器合成的派生类或包含类的默认构造函数为删除的

(1)使用 operator new

首先,我们会想到欲创建对象于堆中,必然要调用operator new。我们可以利用重载的new确定对象是否创建于堆中:

#include <iostream>
using namespace std;

class CLS_Test
{
public:
	CLS_Test()
	{
		if (!isOnTheHeap)
		{
			throw logic_error("can only allocate object on the heap");
		}

		isOnTheHeap = false;
	}

	void destroy()
	{
		delete this;
	}

	static void* operator new(size_t size)
	{
		isOnTheHeap = true;
		return ::operator new(size);
	}

protected:
	static bool isOnTheHeap;

	~CLS_Test() = default;
};

bool CLS_Test::isOnTheHeap = false;

class CLS_Derived : public CLS_Test {};

int main()
{
	try
	{
		CLS_Derived* pDerivedObj = new CLS_Derived;
	}
	catch (logic_error& e)
	{
		cout << "1 " << e.what() << endl;
	}

	try
	{
		CLS_Derived derivedObj;
	}
	catch (logic_error& e)
	{
		cout << "2 " << e.what() << endl;
	}
}

在这里插入图片描述
看起来结果还不错。但是,这样做有几个问题:
第一个问题是,该操作的主要依据是new操作符的调用和构造函数的调用需要一一对应,每次new时对静态变量置位,而构造时判断该标志。然而当我们想要创建一个对象数组时,二者就不是对应的了。只有一次new操作符的调用,而构造函数调用的次数取决于数组元素个数。如果将代码改为:

CLS_Derived* pDerivedObj = new CLS_Derived[10];

在这里插入图片描述
第二个问题是,即使我们用new操作符来创建对象,也没法保证一句话中包含多处new的对象构建顺序:

CLS_Derived* pDerivedObj = new CLS_Derived(*new CLS_Derived);

先忽略内存泄漏。这里,我们希望的编译器执行顺序是:
a.*new CLS_Derived
b.CLS_Derived::CLS_Derived
c.*new CLS_Derived
d.CLS_Derived::CLS_Derived
然而,在一个完整的语句中,C++标准不强制要求子语句的执行顺序。因此,在某些编译器中,其执行顺序可能是:
a.*new CLS_Derived
b.*new CLS_Derived
c.CLS_Derived::CLS_Derived
d.CLS_Derived::CLS_Derived

(2)不具移植性的解决方案

利用我们对程序内存空间的分布,我们也许可以想出第二个解决方案:根据程序空间的组织顺序,确定堆内存地址的增长方向,进而判断内存是否位于堆上。例如,在某些系统(如某些版本linux)中,其程序的内存组织如图:
在这里插入图片描述
因此,我们可以写出下面的判断代码:

bool isOnHeap(void* ptr)
{
	char ch;
	return ptr < &ch;
}

其依据是:如果ptr是一个栈对象指针,那么由于栈是向下生长的,其指针一定高于我们新声明的char栈对象的地址;与之相反,所有堆上分配的对象地址都应该比该对象小。然而,在大多数情况下,即使这样的函数也不能完全生效。因为在使用这种内存布局的系统中,其static对象的存储位置在heap之下:
在这里插入图片描述
因此,我们没法区分堆内存和静态对象内存。

3、安全的delete

上面我们讨论了两种方式,得出的结论是并没有一个好用的方式让我们判断对象是否在堆上。那么让我们回到我们的初衷上,即保证delete this是安全的。

(1)全局 new 和 delete

void* operator new(size_t size)
{
	void* p = getMemory(size);
	//add p to the collection of allocated addresses
	return p;
}

void operator delete(void* ptr)
{
	releaseMemory(ptr);
	// remove ptr from the collection of allocated addresses
}

bool isSafeToDelete(const void* address)
{
	return // whether address is in collection of allocated addresses
}

这个方案对于全局使用new构建和使用delete释放的对象都其作用。然而,有几个原因让我们对它敬而远之:
第一,我们不愿意污染全局空间,尤其是修改程序缺省行为。这不仅会影响到我们的程序,还会影响到使用我们程序的应用以及我们调用的库。
第二,我们并没有对全局对象有这种需求,特别是这种行为会影响全局对象的创建和释放效率(集合中的增、删、查)。
第三,isSafeToDelete并没有我们想象的那么好实现,尤其是当涉及到多态时,我们无法保证被转换成继承体系中某个类的地址和我们保存的指针相同。

(2)提供 abstract mixin base class

所谓mixin class是指定义一组完好的能力,能够与其派生类所可能提供的其他任何能力兼容。如此的类几乎总是抽象的。我们可以提供这样一个类用于有此需求的类继承。其实现如下:

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

class CLS_HeapTracked
{
public:
	virtual ~CLS_HeapTracked() = 0;
	static void* operator new(size_t size)
	{
		void* ptr = ::operator new(size);
		addresses.push_back(ptr);
		return ptr;
	}

	static void operator delete(void* ptr)
	{
		auto iter = find(addresses.begin(), addresses.end(), ptr);
		if (iter != addresses.end())
		{
			::operator delete(ptr);
		}
		else
		{
			throw invalid_argument(string("can only delete object constructed on the heap"));
		}
	}
	
	bool isOnHeap()
	{
		const void* ptr = dynamic_cast<const void*>(this);
		auto iter = find(addresses.begin(), addresses.end(), ptr);
		return iter != addresses.end();
	}

private:
	static list<void*> addresses;
};

list<void*> CLS_HeapTracked::addresses;
CLS_HeapTracked::~CLS_HeapTracked()
{
}

这里需要注意,我们提供了isOnHeap函数要求用户在释放之前显式调用。我们并没有在delete函数显式调用dynamic_cast去转换要释放的指针。这是因为该转换只能用于有虚函数的类对象指针。这里isOnHeap的实现也并不完全适用于isSafeToDelete。毕竟isSafeToDelete还需要判断非多态情况下的指针,这是dynamic_cast转换之外的领域了。

这个类可以这样使用:

class CLS_Test : public CLS_HeapTracked
{
public:
	~CLS_Test() {};

	void changeStatus()
	{
		if (isOnHeap())
		{
			cout << "delete this" << endl;
			delete this;
		}
	}
};

int main()
{
	CLS_Test test;
	test.changeStatus();

	CLS_Test* pTest = new CLS_Test;
	pTest->changeStatus();
}

在这里插入图片描述
这里我们可以选择将基类设置为模板基类,以防止所有的派生类都将堆上保存的对象指针都保存在相同的容器中,降低搜索效率。虽然这个基类不能用于内置类型,但内置类型也不会有delete this的需求。

条款28 — Smart Pointers

本条款主要讨论的是智能指针的实现细节及与如何与原生指针保持相同的特性。

1、拷贝、赋值

书中以auto_ptr为原型分析智能指针的拷贝和赋值,因此其结果为拥有权的转移。然而,这样是很不安全的。一方面,我们无法确定在转移拥有权后,原指针是否还会被使用;另一方面,程序员可能无意间转移了指针的所有权(如函数的值传递)。

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

template<class T>
void test(auto_ptr<T> ptr){}

int main()
{
	auto_ptr<int> a(new int(2));
	cout << "before test " << a.get() << endl;
	test(a);
	cout << "after test " << a.get() << endl;
}

在这里插入图片描述
这也是为什么unique_ptr已经不再支持左值引用的拷贝和赋值了。

2、将智能指针转换为原生指针

如果我们希望将智能指针传入需要原生指针的函数中,需要为智能指针实现一个隐式转换函数。

template<class T>
class SmartPointer
{
public:
	operator T*()
	{
		return pointer;
	}
private:
	T* pointer;
};

不过这样的转换违背了我们的初衷:

SmartPointer<int> p(new int(1));
delete p;

这将导致保存的堆上分配的内存释放多次,进而出现崩溃。
即使我们提供了操作符,智能指针也没法完全像原生指针一样工作:

class CLS_Test
{
public:
	CLS_Test(int* _iPara){}
};

void test(CLS_Test obj) {}

int main()
{
	test(new int(1)); // 1
	SmartPointer<int> p; 
	test(p); // 2
}

如上,如果一个函数的参数可以通过原生指针转换过来,那么第1种调用就是合理的;然而,通过智能指针进行调用是不能转换的,编译器是不能执行一个以上的隐式转化的。
因此,尽量不要提供智能指针向原生指针的转换。在C++标准库中,提供了get方法用以获取原生指针。

3、智能指针和继承

原生指针还有一个特性就是多态。然而,对于智能指针,模板为基类的智能指针和模板为派生类的智能指针之间并没有任何关系。因此,我们无法将保存派生类指针的智能指针对象传递给使用保存基类指针的智能指针对象:

class CLS_Base {};
class CLS_Derived : CLS_Base {};

void test(SmartPointer<CLS_Base>& p) {}

int main()
{
	SmartPointer<CLS_Derived> p(new CLS_Derived);
	test(p);
}

这样的问题该怎么解决呢?首先我们就会想到使用类型转换。:

class CLS_Base {};
class CLS_Derived : public CLS_Base {};
class CLS_Derived2nd : public CLS_Derived {};

template<>
class SmartPointer<CLS_Derived>
{
public:
	operator SmartPointer<CLS_Base>();
};

template<>
class SmartPointer<CLS_Derived2nd>
{
public:
	operator SmartPointer<CLS_Base>();
	operator SmartPointer<CLS_Derived>();
};

这种实现方案有两个缺点:一方面,针对使用不同类型实例化的智能指针,用户需要特化智能指针类模板以提供转化函数;另一方面,需要的转换函数个数和类型可能也不同,这取决于类在继承体系中的位置。
那么如何为智能指针类提供一个泛化的转化函数呢?是的,泛化的函数。我们想到可以将其声明为模板函数:

template<class T>
class SmartPointer
{
public:
	template<class U>
	operator SmartPointer<U>()
	{
		return SmartPointer<U>(pointer);
	}
	...
};

这样就可以使得前面的测试代码(test)编译通过了。其原理在于:我们调用test函数,希望的参数类型为 SmartPointer<CLS_Base>,而实际得到的类型为 SmartPointer<CLS_Derived>。那么编译器就要尝试寻找二者的转换函数了。这涉及到函数匹配的顺序。编译器先寻找满足要求的普通函数,发现没有;然后寻找可以满足条件的模板函数,定位到了我们新提供的模板转换函数。接下来,编译器会尝试实例化这个函数,将模板类型参数推导为 CLS_Base。最后编译器会进行语法分析,看看这里我们是否能使用 T* 类型(即 CLS_Derived* 类型)的指构造 U*(即CLS_Base*) 为参数的智能指针对象。二者为继承关系,因此上述转换显然是成立的。事实上,这种方式转换模板函数可以用于任何一种可以隐式转换为CLS_Base*类型的指针类型

上述转换函数基本能使得智能指针的行为与原生指针保持一致。当然,它还是有一个小瑕疵:

void test(SmartPointer<CLS_Base> p) {}
void test(SmartPointer<CLS_Derived> p) {}

void test(CLS_Base* p) {}
void test(CLS_Derived* p) {}

int main()
{
	SmartPointer<CLS_Derived2nd> p(new CLS_Derived2nd);
	test(p); // ambiguous

	test(new CLS_Derived2nd); // void test(CLS_Derived* p) {}
}

如果是原生指针,在函数重载解析时,对于向基类的隐式转换来说,向一个more derived类的转化的优先级更高。然而对于智能指针,重载解析时它们都找到了模板转换函数,C++的转换函数优先级都是相同的。因此这里会出现二义性的错误。

4、智能指针和const

回顾原生指针和const的结合,有两种方式,顶层const和底层const。因此,我们很自然地希望智能指针也有这样的区分。不幸的是,智能指针作为类,只能实现顶层const

const SmartPointer<CLS_Derived> p = new CLS_Derived;
p = new CLS_Derived; // invalid

如果想要产生底层const,我们貌似可以修改模板参数:

CLS_Derived obj;
SmartPointer<const CLS_Derived> p = &obj;

这样,我们可以产生与原生指针相同的,四种使用指针与const的结合方式:

CLS_Derived obj;
SmartPointer<CLS_Derived> p1 = &obj;
SmartPointer<const CLS_Derived> p2 = &obj;
const SmartPointer<CLS_Derived> p3 = &obj;
const SmartPointer<const CLS_Derived> p4 = &obj;

这与原生指针很接近了。然而,这上面有个小bug。对与原生指针我们可以使用non-const指针作为const指针的初值,也可以使用 指向non-const对象 之指针作为 指向const 之指针的初值。例如:

CLS_Derived *pObj = new CLS_Derived;
const CLS_Derived* cpObj = pObj;

然而,对于智能指针:

SmartPointer<CLS_Derived> pObj = new CLS_Derived;
SmartPointer<const CLS_Derived> cpObj = pObj;

这样的转换是不合法的。因为 CLS_Derivedconst CLS_Derived 是不同的类型。因此其对应的智能指针无法相互转换。这里,我们同样可以使用前面的模板转换函数实现相应的功能。

书中提到了另一种解决方案。考虑 const 指针和 non-const 指针、pointer-to-constpointer-to-non-const 的关系:能够应用于 const 的行为一定能应用于 non-const。对于non-const,我们还可以实现特定行为
这种特质听起来很像继承关系,因此书中提出使用 const 版本派生 non-const 版本:

template <class _Myvec>
class _Vector_const_iterator : public _Iterator_base
{
	...
};

template<class T>
class SmartPtrToConst
{
	...
protected:
	union
	{
		const T* constPointer;
		T* pointer;
	}
};

template<class T>
class SmartPtr : public SmartPtrToConst<T>
{
	...
};

这里我们使用联合体去保存真正的指针。因为内部保存的指针既可能是const,也可能是non-const。为了避免空间浪费,我们使用联合体实现这种语义。

虽然这种方法并没有真正应用于C++标准库,但是借由const类版本实现non-const却在C++标准库中有很多体现。例如在迭代器相关的实现中:

template <class _Myvec>
class _Vector_iterator : public _Vector_const_iterator<_Myvec>
{
	...
};

5、智能指针、继承和const引用

讨论完前面指针和继承以及const的使用,我自然而然地想到一个问题:当一个函数需要引用类型时,正常情况下是不会产生临时对象的。但是这有一个例外,就是函数类型为const&,而实参和参数类型之间存在隐式类型转换。这时会产生一个临时对象出来。考虑到我们这里智能指针的拥有权,如果在函数调用过程中,发生了隐式转换,这会导致指针被释放两次:

void test(const SmartPointer<CLS_Base>& p) {}

int main()
{
	SmartPointer<CLS_Derived> pObj = new CLS_Derived;
	test(pObj);
}

程序最后崩溃在operator delete中。

那么,我们应该解决这个问题呢?首先分析问题原因,我们不难发现,这个问题的根本原因在于作为实参的pObj在传入函数,被拷贝之后,还会继续被使用。因此在其析构时会重复释放指针。那么如果该对象不再被使用,我们是不是可以在其发生拷贝时将内部指针置空?这样就只会在函数内部发生指针释放了。那么什么样的对象不再会被使用呢?右值。然而,转换函数只能控制目标类型而不能修改源类型。因此,我们想到转而使用转换构造函数:

class SmartPointer
{
public:
	template <class U>
	SmartPointer(SmartPointer<U>&& _Right)
	{
		pointer = static_cast<T*>(_Right.release());
	}

	T* release()
	{
		auto oldPointer = pointer;
		pointer = nullptr;
		return oldPointer;
	}
	...
};

这样我们就可以限制只有右值才可以进行类型转换。在转换后改右值对象将失去指针的拥有权。查看C++标准库unique_ptr,会发现其实现方式正是这样:

template <class _Ty2, class _Dx2, enable_if_t<conjunction_v<negation<is_array<_Ty2>>, 
is_convertible<typename unique_ptr<_Ty2, _Dx2>::pointer, pointer>, conditional_t<is_reference_v<_Dx>, 
is_same<_Dx2, _Dx>, is_convertible<_Dx2, _Dx>>>, int> = 0>
unique_ptr(unique_ptr<_Ty2, _Dx2>&& _Right) noexcept
    : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx2>(_Right.get_deleter()), _Right.release()) {}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-09 10:03:37  更:2021-08-09 10:05:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 1:46:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码