C++面试必备知识点-啊A爱爱
RAII
C++支持将对象存储在栈上,但是很多情况下不可以,比如:
- 对象很大;
- 对象的大小在编译时不确定;
- 对象是函数的返回值,但是由于特殊原因,不应使用对象的值返回
常见的情况之一,在工厂方法或其他面向对象编程的情况下,返回值类型是基类。代码示例:
enum class shape_type{
circle,
triangle,
rectangele,
};
class shape{};
class circle:public shape{...};
class triangle:public shape{...};
class rectangle:public shape{...};
shape* create_shape(shape_type type){
...
switch(type){
case shape_type::cirle:
return new circle(...);
case shape_type::triangle:
return new triangle();
case shape_type::rectangle():
return new rectangle();
}
} 这个create_shape函数会返回一个shape对象,对象的实际类型是某个shape的子类,圆、三角形、矩形等。这种情况下,函数的返回值只能是指针或者其变形体。如果返回类型是shape,实际返回的是circle,编译器不会报错,但是结果多半是错的,这种现象叫做对象切片(object slicing),是C++中特有的一种编码错误,属于对象复制相关的语义错误。
那么应该如何做,才能确保create_shape的返回值不会发生内存泄漏呢?
答案就在析构函数和他的栈展开行为上。我们只需要将这个返回值放在一个本地变量里,并确保其析构函数会删除该对象即可。
简单的实现如下:
class shape_wrapper{
public:
explicit shape_wrapper(shape* ptr=nullptr): ptr_(ptr){}
~shape_wrapper(){delete ptr_;}
shape* get()const{ return ptr_;}
private:
shapr* ptr_;
};
void foo(){
...
shape_wrapper ptr_wrapper(create_shape(...));
...
} delete一个空指针是一个合法的空操作,new一个对象和delete一个指针时编译器都需要干不少活,大致可以翻译为如下操作
// new circle(...)
{
void* temp=operator_new(sizeof(circle));
try{
circle* ptr =static_cast<circle*>(temp);
ptr->circle(...);
return ptr;
}
catch(...){
operator_delete(ptr);
throw;
}
} if(ptr!=nullptr){
ptr->~shape();
operator_delete(ptr);
} new的时候先分配内存(失败时整个操作失败并向外抛出异常,通常是bad_alloc),然后在这个结果指针上构造对象;构造成功则new操作整体完成,否则释放刚分配的内存并继续向外抛构造函数产生的异常。delete时则判断指针是否为空,在指针不为空时调用析构函数并释放之前分配的内存。
回到shape_wrapper和它的析构行为。在析构函数中做些必要的清理工作,这就是RAII的基本用法。这种清理不限于释放内存,也可以是:
- 关闭文件
- 释放同步锁
- 释放其他重要的系统资源

