【more effective c++读书笔记】【第5章】技术(3)——要求(或禁止)对象产生于heap之中

一、要求对象产生于heap之中

方法:让析构函数成为private,构造函数为public。然后导入一个伪的析构函数,来调用真正的析构函数。但是它也妨碍了继承和内含。

例子:

//UPNumber.h
#ifndef UPNUMBER_H
#define UPNUMBER_H

#include<iostream>
class UPNumber{
public:
	UPNumber() :value(0) { std::cout << "UPNumber()" << std::endl; }
	UPNumber(int initValue) :value(initValue) { std::cout << "UPNumber(int initValue)" << std::endl; }
	UPNumber(const UPNumber& rhs) { this->value = rhs.value; }
	//伪构造函数
	void destroy() const { delete this; }
private:
	~UPNumber() { std::cout << "~UPNumber()" << std::endl; }
	int value;
};

#endif
//main.cpp
#include "UPNumber.h"
using namespace std;

int main(){
	//UPNumber n;//错误,析构函数是private的
	UPNumber* p = new UPNumber;
	//delete p;//错误,析构函数是private的
	p->destroy();

	system("pause");
	return 0;
}

另一个方法就是将所有的构造函数都声明为private。这个方法的缺点是类常常有多个构造函数,类的作者必须将它们每一个都声明为private。所以比较容易的方法还是让析构函数成为private,构造函数为public,因为一个类只有一个析构函数。

上述方法说到妨碍了继承,可以用如下方法解决:令UPNumber的析构函数成为protected。也说到妨碍了内含UPNumber对象,可以用如下方法解决:内含一个UPNumber类的指针,指向UPNumber对象。

二、禁止对象产生于heap之中

方法:直接将对象产生于heap之中,总是以new产生出来的。new operator总是调用operator new,可以将operator new声明为private。

例子:

//UPNumber.h
#ifndef UPNUMBER_H
#define UPNUMBER_H

#include<iostream>
class UPNumber{
public:
	UPNumber() :value(0) { std::cout << "UPNumber()" << std::endl; }
	UPNumber(int initValue) :value(initValue) { std::cout << "UPNumber(int initValue)" << std::endl; }
	UPNumber(const UPNumber& rhs) { this->value = rhs.value; }
	~UPNumber() { std::cout << "~UPNumber()" << std::endl; }
private:
	static void* operator new(size_t size);   //禁止UPNumber对象位于heap内
	static void* operator new[](size_t size) ;//禁止UPNumber对象组成的数组位于heap内
	static void operator delete(void* ptr);   //禁止UPNumber对象位于heap内
	static void operator delete[](void* ptr); //禁止UPNumber对象组成的数组位于heap内

	int value;
};

#endif
//main.cpp
#include "UPNumber.h"
using namespace std;

int main(){
	UPNumber n1;//可以
	static UPNumber n2;//也可以
	//UPNumber* p = new UPNumber;//错误,operator new是private的
	
	system("pause");
	return 0;
}

将operator new声明为private,往往也会妨碍UPNumber对象被实例化为heap-based继承类对象的基类成分。因为operator new和operator delete都会被继承,所以如果这些函数不在继承类内声明为public,继承类继承的便是基类声明的private版本。

当UPNumber作为其他类的内嵌对象,UPNumber的operator new为private没有什么影响。

三、判读某个对象是否位于heap内

方法有以下几种:

1、在类中增加静态变量指示是否调用operator new来分配内存;如果不是使用operator new来构造对象,就在构造函数中抛出异常。

例子:

//UPNumber.h
#ifndef UPNUMBER_H
#define UPNUMBER_H

#include<iostream>
class UPNumber{
public:
	class HeapConstrainViolation{};
	static void* operator new(size_t size);
	UPNumber();
	UPNumber(int initValue);
	UPNumber(const UPNumber& rhs);
private:
	int value;
	static bool onTheHeap;//用来在构造函数内指示正构造中的对象是否位于heap
};
bool UPNumber::onTheHeap = false;
void* UPNumber::operator new(size_t size){
	onTheHeap = true;
	return ::operator new(size);
}
UPNumber::UPNumber() :value(0) {
	if (!onTheHeap)
		throw HeapConstrainViolation();
	std::cout << "UPNumber()" << std::endl; 
}
UPNumber::UPNumber(int initValue) : value(initValue) { 
	if (!onTheHeap)
		throw HeapConstrainViolation();
	std::cout << "UPNumber(int initValue)" << std::endl; 
}
UPNumber::UPNumber(const UPNumber& rhs) {
	this->value = rhs.value; 
}

#endif
//main.cpp
#include "UPNumber.h"
using namespace std;

int main(){
	//UPNumber n;//发生异常
	UPNumber* p = new UPNumber;
	delete p;
	
	system("pause");
	return 0;
}

这种方法有以下几个问题:

a、考虑如下代码:

UPNumber* numberArray = new UPNumber[100];

出现第一个问题是数组由operator new[]分配,这个仍可以自己写一个operator new[]的版本。第二个问题numberArray有100个元素,应该有100次构造函数调用,但整个程序只有一次内存分配,所以在100次构造函数中,只有第一次onTheHeap的值为true,后面调用构造函数时有exception抛出。

b、考虑如下代码:

UPNumber* pn = new UPNumber(*new UPNumber);

我们通常希望函数调用顺序如下:

1)调用第一个对象的operator new

2)调用第一个对象的constructor

3)调用第二个对象的operator new

4)调用第二个对象的constructor

但C++不保证这么做,有可能是这样的顺序

1)调用第一个对象的operator new

2)调用第二个对象的operator new

3)调用第一个对象的constructor

4)调用第二个对象的constructor

因在步骤1)和2)中设立的onTheHeap的值在步骤3)中被清除了,造成步骤4)的对象认为它不处于heap之中。

2、根据stack和heap的增长方向来判断内存是否在heap中分配。

依据的事实是程序的地址空间以线性序列组织而成,其中栈从高地址往低地址生长,堆从低地址往高地址生长。这种方法存在两个问题,一是不具有可移植性;二是区分不了heap和static。

3、维护一个集合,operator new负责把对象的地址加入到一个由动态分配所形成的集合中,operator delete负责把该对象的地址移除,判断一个对象是否在heap中,就看这个对象的地址是否在集合中。对于全局的operator new和operator delete会有以下三个问题:第一,改变原有的operator new和operator delete的语义;第二,效率低,需要承担沉重的簿记工作;第三,当对象涉及多重继承或虚继承的基类时,会拥有多个地址。

4、采用抽象abstract mixin base class(抽象混合式基类)。

抽象基类是一个不能被实例化的基类,至少含有一个纯虚函数;混合类则提供一组定义完好的能力,能够与派生类所提供的其他任何功能兼容。我们可以形成一个抽象混合式基类,用来为继承类提供判读某指针是否以operator new分配出来的能力。

例子:

//HeapTracked.h
#ifndef HEAPTRACKED_H
#define HEAPTRACKED_H

#include<list>
#include<iostream>

class HeapTracked{
public:
	class MissingAddress {};//异常类

	virtual ~HeapTracked() = 0;
	static void* operator new(size_t size);
	static void operator delete(void* ptr);
	bool isOnHeap() const;
private:
	static std::list<const void*> addresses;
};

std::list<const void*> HeapTracked::addresses;

HeapTracked::~HeapTracked() {}

void* HeapTracked::operator new(size_t size){
	void* memPtr = ::operator new(size);//取得内存
	addresses.push_back(memPtr);//将其地址置于list内
	return memPtr;
}

void HeapTracked::operator delete(void* ptr){
	std::list<void const*>::iterator iter = find(addresses.begin(), addresses.end(), ptr);
	if (iter != addresses.end()){//如果找到符合条件的元素,移除之并释放内存
		addresses.erase(iter);
		::operator delete(ptr);
	}
	else{//否则表示ptr不是operator new所分配,抛出一个异常
		throw MissingAddress();
	}
}

bool HeapTracked::isOnHeap() const{
	//取得一个指针,指向*this所占内存的起始处
	const void* rawAddress = dynamic_cast<const void*>(this);
	std::list<const void*>::iterator iter = find(addresses.begin(), addresses.end(), rawAddress);
	return iter != addresses.end();//返回找到与否的消息
}

#endif 
//main.cpp
#include "HeapTracked.h"
using namespace std;

class Asset :public HeapTracked{
public:
	Asset(int initValue) {}
};

int main(){
	Asset s(10);
	if (s.isOnHeap())
		cout << "is on heap" << endl;
	else
		cout << "is not on heap" << endl;

	Asset* p = new Asset(1);
	if (p->isOnHeap())
		cout << "is on heap" << endl;
	else
		cout << "is not on heap" << endl;
	delete p;

	system("pause");
	return 0;
}

上述Heaptracked这样的mixin类有个缺点,就是不能够使用于内建类型身上,因为像int和char这种内建类型并不继承自任何东西。


<think>我们被要求讨论“要求禁止在堆上产生对象”的方法和原理。在面向对象编程中,对象可以在堆(heap)上栈(stack)上分配。通常,栈上分配的对象生命周期与作用域绑定,而堆上分配的对象需要手动管理(如C++由垃圾回收器管理(如Java)。有时,出于性能、安全设计考虑,我们可能希望强制禁止在堆上创建对象。 参考引用: 引用[1]中提到构造器,但没有直接相关的内容。 引用[2]提到volatile关键字和指令重排序,与堆上对象创建无关。 引用[3]是关于CPU部件的,不相关。 引用[4]是关于逻辑运算符,不相关。 引用[5]是关于monitor(锁)的,不直接相关。 因此,我们主要依靠自己的知识来回答。 我们将从以下几个方面讨论: 1. 什么是堆和栈? 2. 为什么需要禁止在堆上创建对象3. 如何禁止在堆上创建对象?(以C++为例,因为Java中所有对象都在堆上) 4. 如何要求在堆上创建对象?(以C++和Java为例) 5. 总结 注意:由于问题没有指定语言,我们以C++和Java为例,因为这两种语言在内存管理上差异明显。 一、堆和栈的区别 - 栈:由编译器自动管理,对象生命周期与作用域相关。当离开作用域时,栈上对象自动释放。分配速度快,但空间有限。 - 堆:由程序员手动管理(C++垃圾回收器管理(Java)。对象生命周期可以超越作用域,直到显式释放(C++垃圾回收(Java)。分配速度相对慢,空间较大。 二、禁止在堆上创建对象的理由 1. 性能:栈上分配更快,且没有释放开销。 2. 避免内存泄漏:堆上对象需要手动释放,如果忘记释放会导致内存泄漏(C++)。 3. 实时性:栈上分配时间确定,而堆上分配时间不确定(可能触发垃圾回收,导致停顿)。 4. 安全性:栈上对象不会产生悬挂指针,而堆上对象如果释放后仍被引用,会导致未定义行为。 三、如何禁止在堆上创建对象(以C++为例) 在C++中,如果我们想禁止在堆上创建对象,只需要禁止使用`new`操作符。可以通过将`operator new`设为私有删除来实现。 示例: ```cpp class HeapProhibited { private: // 将operator new声明为私有,并且不提供实现(使用C++11的delete) static void* operator new(size_t size) = delete; static void* operator new[](size_t size) = delete; }; ``` 这样,任何尝试在堆上创建`HeapProhibited`对象的代码都会编译错误: ```cpp HeapProhibited obj; // 正确,在栈上 HeapProhibited* p = new HeapProhibited(); // 错误,因为operator new被删除 ``` 四、如何要求在堆上创建对象(即禁止在栈上创建对象) 1. C++中:将析构函数设为私有(受保护),并在类内部提供销毁对象的公有方法(如`release`)。因为栈上对象在离开作用域时会自动调用析构函数,如果析构函数不可访问,则栈上对象无法编译。 示例: ```cpp class StackProhibited { public: StackProhibited() {} void release() { delete this; } // 通过成员函数释放对象 private: ~StackProhibited() {} // 析构函数私有 }; // 使用 StackProhibited* p = new StackProhibited(); // 正确 p->release(); // 释放对象 // StackProhibited obj; // 错误:析构函数不可访问 ``` 2. Java中:所有对象都在堆上,栈上只能存储基本类型和对象引用。但是,我们可以通过将构造函数设为私有,然后提供静态工厂方法来创建对象,这样就不能在类外部使用`new`创建对象(从而不能直接创建栈上对象?注意:Java中所有对象都在堆上,所以这里讨论的“禁止在栈上创建对象”没有意义,因为栈上只能放引用。实际上,我们通常讨论的是如何控制对象的创建方式,比如强制使用工厂方法)。 五、总结 禁止要求在堆上创建对象通常是为了控制对象的生命周期和分配方式,以满足性能、安全设计约束。在C++中,我们可以通过控制`operator new`和析构函数的访问权限来实现;在Java中,由于对象都在堆上,我们主要关注如何控制对象的创建(如使用工厂方法)。 注意:在C++11之后,还可以使用`= delete`来禁用函数,这样更方便。 下面生成相关问题: </think>### 要求禁止在堆上产生对象的方法与原理 在系统编程中,控制对象的内存分配位置(堆栈)对性能、安全性和资源管理至关重要。以下是实现要求禁止在堆上分配对象的核心方法和原理,结合内存管理机制和语言特性: --- #### 一、禁止在堆上分配对象 **适用场景**:需要保证对象生命周期严格绑定作用域、避免内存泄漏满足实时性要求(如嵌入式系统)。 ##### 1. **C++实现方法** - **原理**:重载并禁用 `operator new` - 栈对象由编译器自动管理,堆对象需显式调用 `new` - 删除 `operator new` 可阻断堆分配 - **代码示例**: ```cpp class HeapProhibited { private: // 禁用堆分配(C++11以上) static void* operator new(size_t) = delete; static void* operator new[](size_t) = delete; // 禁用堆释放 static void operator delete(void*) = delete; static void operator delete[](void*) = delete; public: HeapProhibited() {} // 构造器仅允许栈分配[^1] }; // 使用示例 HeapProhibited obj; // 合法:栈分配 // HeapProhibited* p = new HeapProhibited(); // 编译错误:operator new被删除 ``` ##### 2. **原理分析** - **内存布局**:栈对象由编译器在函数栈帧预留空间,地址连续且自动回收(通过 `pop` 指令)。 - **性能优势**:避免堆分配的系统调用开销(如 `brk`/`mmap`)和碎片化问题。 - **安全性**:消除 `delete` 遗漏导致的内存泄漏风险。 --- #### 二、要求在堆上分配对象 **适用场景**:需要长生命周期对象、动态大小结构跨作用域共享数据。 ##### 1. **C++实现方法** - **原理**:私有化析构函数 + 工厂模式 - 析构函数私有化使栈对象无法自动销毁 - 工厂方法强制通过 `new` 创建 - **代码示例**: ```cpp class HeapOnly { private: ~HeapOnly() {} // 私有析构函数阻止栈分配 public: static HeapOnly* create() { return new HeapOnly(); // 强制堆分配 } void destroy() { delete this; } // 显式释放 private: HeapOnly() {} // 私有构造器[^1] }; // 使用示例 HeapOnly* obj = HeapOnly::create(); // 合法:堆分配 obj->destroy(); // HeapOnly stackObj; // 编译错误:析构函数不可访问 ``` ##### 2. **Java实现方法** - **原理**:Java 中所有对象默认在堆分配 - 栈分配仅限基本类型和逃逸分析优化的局部对象 - 通过 `new` 关键字显式触发堆分配: ```java // 所有对象均在堆分配 Object heapObj = new Object(); ``` ##### 3. **原理分析** - **堆内存特性**: - 通过 **地址加法器** 计算逻辑地址(如 `段地址寄存器 << 4 + 偏移量`)[^3] - 动态扩展能力(对比栈的大小限制) - **多线程安全**:volatile 修饰指针可避免指令重排序(如 `public static volatile T t;`)[^2] - **垃圾回收**:由 GC 自动管理,避免显式释放(对比 C++)。 --- #### 三、关键机制对比 | **特性** | **栈分配** | **堆分配** | |------------------|-------------------------------|--------------------------------| | **生命周期** | 作用域绑定(自动回收) | 显式控制(需手动/GC回收) | | **速度** | 极快(编译时预留空间) | 较慢(系统调用+可能触发GC) | | **线程安全** | 线程独占(无竞争) | 需同步机制(如 monitor[^5]) | | **内存大小** | 固定且有限(可能溢出) | 动态扩展(仅受系统限制) | | **适用场景** | 短生命周期、小对象、实时系统 | 大对象、共享数据、跨作用域 | --- #### 四、设计注意事项 1. **C++析构函数安全**: - 虚析构函数对多态堆对象必要(避免资源泄漏)[^3] - 构造函数中禁止调用虚函数(未完成初始化)[^3] 2. **Java逃逸分析**: - JVM 可能优化局部对象为栈分配(需关闭 `-XX:-DoEscapeAnalysis` 强制堆分配) 3. **内存屏障**: - 堆分配需考虑可见性(如 volatile 禁止指令重排序[^2]) 4. **资源管理**: - 堆对象需配套 RAII(C++ try-with-resources(Java) --- ### 总结 - **禁止堆分配**:通过禁用 `operator new`(C++)实现,保证对象作用域可控性。 - **要求堆分配**:通过私有化析构函数(C++依赖语言特性(Java)实现,支持动态生命周期。 - **核心权衡**:在性能(栈)与灵活性(堆)间取舍,结合线程安全(如monitor[^5])、内存模型(如段寄存器[^3])和系统约束设计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值