约定对象创建限制

如何限制一个类对象只在堆/栈上分配空间?

为什么需要限制一个类对象只在堆/栈上分配空间?

more effective cpp :条款27

为什么要限制对象创建?

可能你是嵌入式设备,堆空间有限,你希望某类对象不会泄漏内存(不允许在堆上创建,就绝不会内存泄漏,😊)。

可能你想要某些对象有自杀”delete this“的能力,如此安排,对象必须创建在堆上(否则将会被析构两次,调用自杀函数一次,编译器调用一次)。

1
2
3
run.out(8482,0x7ff853a44700) malloc: *** error for object 0x7ff7bf21bf98: pointer being freed was not allocated
run.out(8482,0x7ff853a44700) malloc: *** set a breakpoint in malloc_error_break to debug
[1] 8482 abort ./run.out

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;

  • 静态建立类对象:由编译器为对象在栈空间中分配内存。通过直接移动栈顶指针,在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,将直接调用类的构造函数。

  • 动态建立类对象:使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步调用构造函数构造对象,初始化这片内存空间。这种方法,将间接调用类的构造函数。

只在堆上建立

Non-heap object会在定义点自动构造,在其声明周期结束时自动析构。因此,只需要将那些隐式调用构造函数或析构函数动作非法化,就能限制对象在栈上的建立。

直截了当的方式

将构造函数与析构函数声明为private。

原理:在栈上构建对象时,编译器负责对象的生命周期,自动调用对象的析构函数。如果析构函数为私有,编译器就不能调用对象的析构函数,类对象就无法静态建立。

稍好点的方式

将构造函数声明为public,然后将析构函数声明为private,再导入一个伪析构函数,用于释放对象。

为什么不是将构造函数声明为private?

因为一个类有许多构造函数,析构函数只有一个。编译器添加的构造函数总是public的。

1
2
3
4
5
6
7
class A  {  
public:
A(){}
void destory(){delete this;}
private:
~A(){}
};

但这种方式(将析构函数声明为private)妨碍了继承与包含:派生类无法访问基类的析构函数;包含类无法访问被包含类的析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
class B : public A{
};

class C{
private:
A a;
};

int main(){
B* pb = new B(); // 编译错误
C* pc = new C(); // 编译错误
}

解决方式:将基类的析构函数声明为protected可解决继承问题;将成员变量使用方式改为指针,可解决包含问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A  {  
public:
A(){}
void destory(){delete this;}
protected:
~A(){}
};

class C{
// C的析构函数注意释放A
private:
A *a;
};

// 这下ok了
int main(){
B* pb = new B();
B b;
C* pc = new C();
C c;
}

更严厉的限制

上述方式,并没有限制派生类在栈上创建。如果更严厉点,要求其派生类也必须在堆上建立,该如何解决?

很遗憾,不能。

more effective cpp 介绍了两种方法:

  • 重载operator new,设置onHeap标志,构造函数判断onHeap标志。
  • 通过对象地址判断是否位于堆内

遗憾的是,前者无法应对数组(内存一次分配,多次构造);后者无法区分堆对象与静态对象。

只在栈上建立

方法:将operator new() 设为私有

Operator new[]可能也需要设置为private。

1
2
3
4
5
6
7
8
class A  {  
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};

但是这种方法无法面对继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class B : A{
public:
B(int m):mem(m){}
// static void* operator new(size_t size){
// return ::operator new(size);
// }
// static void operator delete(void* pointer){
// delete pointer;
// }
private:
int mem;
};

B* pb = new B(1); // 错误

包含并无大碍:

1
2
3
4
5
6
7
8
9
10
11
class C{
public:
C(int m):mem(m){}

private:
A a;
int mem;
};

C* pc = new C(1); //ok
C c(1); //ok

更严厉的制约同样无法达到。

作者

Desirer

发布于

2025-05-25

更新于

2025-05-25

许可协议