C++ 对象模型
- 对象的内存布局
- 非虚函数类对象
- 对于不包含虚函数的类,对象的内存布局相对简单,其成员变量按照声明的顺序依次存储。例如:class SimpleClass { private: int num; double d; public: SimpleClass(int n, double dd) : num(n), d(dd) {} };
- 在SimpleClass对象的内存中,首先存储int类型的num,然后存储double类型的d。假设int占4字节,double占8字节,那么一个SimpleClass对象的大小至少为12字节(考虑内存对齐等因素可能会有所增加)。
 
- 对于不包含虚函数的类,对象的内存布局相对简单,其成员变量按照声明的顺序依次存储。例如:
- 包含虚函数的类对象
- 如果一个类包含虚函数,对象的内存布局会包含一个虚函数表指针(vptr)。这个指针通常位于对象内存的开头(不同编译器可能有不同的实现方式)。例如:class VirtualClass { private: int num; double d; public: VirtualClass(int n, double dd) : num(n), d(dd) {} virtual void someVirtualFunction() {} };
- 在VirtualClass对象的内存中,首先是虚函数表指针(通常在32位系统中占4字节,64位系统中占8字节),然后是成员变量num和d。虚函数表是一个函数指针数组,存储了类的虚函数地址。当调用虚函数时,通过这个虚函数表指针找到虚函数表,再根据表中的指针调用相应的函数。
 
- 如果一个类包含虚函数,对象的内存布局会包含一个虚函数表指针(vptr)。这个指针通常位于对象内存的开头(不同编译器可能有不同的实现方式)。例如:
 
- 非虚函数类对象
- 继承关系中的对象模型
- 单继承
- 在单继承关系中,派生类对象的内存布局是基类部分在前,派生类部分在后。例如:class Base { private: int baseNum; public: Base(int n) : baseNum(n) {} }; class Derived : public Base { private: double derivedNum; public: Derived(int n, double d) : Base(n), derivedNum(d) {} };
- 在Derived对象的内存中,首先是Base类的baseNum,然后是Derived类的derivedNum。如果Base类包含虚函数,那么Derived对象也会包含虚函数表指针(并且这个虚函数表指针会被正确初始化以反映Derived类的虚函数情况)。
 
- 在单继承关系中,派生类对象的内存布局是基类部分在前,派生类部分在后。例如:
- 多继承
- 多继承的对象模型较为复杂。例如,有类Base1、Base2和派生类Derived:class Base1 { private: int num1; public: Base1(int n) : num1(n) {} }; class Base2 { private: double num2; public: Base2(double n) : num2(n) {} }; class Derived : public Base1, public Base2 { private: char ch; public: Derived(int n, double d, char c) : Base1(n), Base2(d), ch(c) {} };
- 在Derived对象的内存中,首先是Base1的部分(num1),然后是Base2的部分(num2),最后是Derived类特有的部分(ch)。如果基类包含虚函数,处理虚函数表指针会更加复杂,不同编译器可能采用不同的策略来确保正确的虚函数调用。
 
- 多继承的对象模型较为复杂。例如,有类
 
- 单继承
- 对象的构造与析构
- 构造函数顺序
- 在对象创建时,首先调用基类的构造函数,然后按照继承顺序依次调用派生类的构造函数。在构造函数内部,成员变量的初始化按照声明的顺序进行。例如,对于前面提到的Derived类:Derived(int n, double d, char c) : Base1(n), Base2(d), ch(c) {}
- 首先调用Base1的构造函数初始化Base1部分,然后调用Base2的构造函数初始化Base2部分,最后初始化Derived类特有的ch成员。
 
- 在对象创建时,首先调用基类的构造函数,然后按照继承顺序依次调用派生类的构造函数。在构造函数内部,成员变量的初始化按照声明的顺序进行。例如,对于前面提到的
- 析构函数顺序
- 析构函数的调用顺序与构造函数相反。当Derived类对象被销毁时,首先调用Derived类的析构函数,然后按照与继承顺序相反的顺序依次调用基类的析构函数。并且,如果基类的析构函数是虚函数,那么通过基类指针删除派生类对象时,可以确保正确调用派生类的析构函数。例如:Base1* ptr = new Derived(1, 2.0, 'a'); delete ptr;
- 如果Base1的析构函数不是虚函数,只会调用Base1的析构函数,可能导致Derived类资源无法正确释放;如果Base1的析构函数是虚函数,则会先调用Derived类的析构函数,再调用Base1的析构函数,正确释放资源。
 
- 析构函数的调用顺序与构造函数相反。当
 
- 构造函数顺序
- 函数调用机制(特别是虚函数调用)
- 非虚函数调用
- 对于非虚函数,函数调用在编译时就确定了。编译器根据函数名和作用域直接生成调用代码。例如:class NonVirtualClass { public: void nonVirtualFunction() {} }; int main() { NonVirtualClass obj; obj.nonVirtualFunction(); return 0; }
- 编译器直接知道obj.nonVirtualFunction()应该调用NonVirtualClass类中的nonVirtualFunction函数,不需要在运行时进行额外的查找。
 
- 对于非虚函数,函数调用在编译时就确定了。编译器根据函数名和作用域直接生成调用代码。例如:
- 虚函数调用
- 虚函数的调用是在运行时确定的。当通过基类指针或引用调用虚函数时,会根据对象实际的类型来决定调用哪个函数。例如:class Base { public: virtual void virtualFunction() { std::cout << "Base::virtualFunction" << std::endl; } }; class Derived : public Base { public: void virtualFunction() override { std::cout << "Derived::virtualFunction" << std::endl; } }; int main() { Base* ptr = new Derived(); ptr->virtualFunction(); return 0; }
- 这里ptr是Base类的指针,但实际指向Derived类对象。当调用ptr->virtualFunction()时,由于virtualFunction是虚函数,会在运行时根据ptr实际指向的对象类型(即Derived类)来调用Derived类中的virtualFunction函数。这种机制实现了多态性,使得代码更加灵活和可扩展。
 
- 虚函数的调用是在运行时确定的。当通过基类指针或引用调用虚函数时,会根据对象实际的类型来决定调用哪个函数。例如:
 
- 非虚函数调用
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号