面试题答案
一键面试1. 对象创建和销毁过程中保证类型安全性的一般原则
- 构造函数调用顺序:在创建派生类对象时,首先调用基类的构造函数,然后调用派生类的构造函数。这确保了基类部分首先被正确初始化,因为派生类对象包含基类的子对象。
- 析构函数调用顺序:在销毁派生类对象时,顺序与构造函数相反,先调用派生类的析构函数,然后调用基类的析构函数。这保证了派生类特定的资源先被清理,然后再清理基类的资源。
- 虚函数与类型安全性:虚函数通过动态绑定机制确保在运行时根据对象的实际类型调用正确的函数版本。这对于确保类型安全非常重要,尤其是当通过基类指针或引用调用函数时。
- 指针和引用类型成员变量:在构造函数中初始化指针和引用类型成员变量,确保它们指向有效的对象。在析构函数中,正确释放指针指向的资源,避免内存泄漏。
2. 实际代码示例
#include <iostream>
#include <memory>
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
virtual ~Base() { std::cout << "Base destructor" << std::endl; }
virtual void print() { std::cout << "Base print" << std::endl; }
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
memberPtr = std::make_unique<int>(42);
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
void print() override {
std::cout << "Derived print, value: " << *memberPtr << std::endl;
}
private:
std::unique_ptr<int> memberPtr;
};
int main() {
// 通过基类指针指向派生类对象
Base* basePtr = new Derived();
basePtr->print();
// 正确释放内存
delete basePtr;
// 通过基类引用指向派生类对象
Derived derivedObj;
Base& baseRef = derivedObj;
baseRef.print();
return 0;
}
3. 可能出现类型安全问题的场景及解决方案
场景1:基类析构函数不是虚函数
- 问题:如果基类的析构函数不是虚函数,当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类的资源泄漏。
- 解决方案:将基类的析构函数声明为虚函数,如上述代码中的
Base
类。
场景2:悬空指针
- 问题:如果在对象销毁后,指针没有被设置为
nullptr
,该指针就成为悬空指针。后续对悬空指针的解引用操作会导致未定义行为。 - 解决方案:使用智能指针(如
std::unique_ptr
、std::shared_ptr
),它们会自动管理对象的生命周期,避免悬空指针问题。如上述代码中Derived
类使用std::unique_ptr<int>
来管理memberPtr
。
场景3:类型转换错误
- 问题:在进行类型转换(如
static_cast
、dynamic_cast
)时,如果转换不当,可能会导致运行时错误。例如,将一个指向Base
对象的指针错误地static_cast
为指向Derived
对象的指针,而实际上该对象并非Derived
类型。 - 解决方案:在需要进行类型转换时,使用
dynamic_cast
(仅适用于多态类型),它会在运行时检查转换是否合法。如果转换失败,dynamic_cast
会返回nullptr
(对于指针类型)或抛出std::bad_cast
异常(对于引用类型)。