1. 对象的内存布局
    • 非虚函数类对象
      • 对于不包含虚函数的类,对象的内存布局相对简单,其成员变量按照声明的顺序依次存储。例如:
        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字节),然后是成员变量numd。虚函数表是一个函数指针数组,存储了类的虚函数地址。当调用虚函数时,通过这个虚函数表指针找到虚函数表,再根据表中的指针调用相应的函数。
  2. 继承关系中的对象模型
    • 单继承
      • 在单继承关系中,派生类对象的内存布局是基类部分在前,派生类部分在后。例如:
        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类的虚函数情况)。
    • 多继承
      • 多继承的对象模型较为复杂。例如,有类Base1Base2和派生类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)。如果基类包含虚函数,处理虚函数表指针会更加复杂,不同编译器可能采用不同的策略来确保正确的虚函数调用。
  3. 对象的构造与析构
    • 构造函数顺序
      • 在对象创建时,首先调用基类的构造函数,然后按照继承顺序依次调用派生类的构造函数。在构造函数内部,成员变量的初始化按照声明的顺序进行。例如,对于前面提到的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的析构函数,正确释放资源。
  4. 函数调用机制(特别是虚函数调用)
    • 非虚函数调用
      • 对于非虚函数,函数调用在编译时就确定了。编译器根据函数名和作用域直接生成调用代码。例如:
        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;
        }
      • 这里ptrBase类的指针,但实际指向Derived类对象。当调用ptr->virtualFunction()时,由于virtualFunction是虚函数,会在运行时根据ptr实际指向的对象类型(即Derived类)来调用Derived类中的virtualFunction函数。这种机制实现了多态性,使得代码更加灵活和可扩展。
posted @ 2024-10-06 21:33 西北野狼 阅读(16) 评论(0) 推荐(0) 编辑
摘要: 概念 在C++中,dynamic_cast是一种运行时类型转换操作符。它主要用于在类的层次结构中进行安全的向下转换(将基类指针或引用转换为派生类指针或引用)。这种转换基于对象的实际类型进行检查,以确保转换的安全性。 使用条件 为了使用dynamic_cast,类层次结构中必须包含虚函数。这是因为dy 阅读全文
posted @ 2024-10-05 21:20 西北野狼 阅读(66) 评论(0) 推荐(0) 编辑
摘要: 概念 在C++中,常类型转换主要涉及到const_cast操作符,用于在特定情况下对const(常量)限定符进行处理。const关键字在C++中有重要意义,它表示被修饰的对象是常量,不能被修改。但在某些特殊情况下,需要在不破坏常量性语义的前提下,进行与常量相关的操作转换。 const_cast的使用 阅读全文
posted @ 2024-10-05 21:18 西北野狼 阅读(5) 评论(0) 推荐(0) 编辑
摘要: 概念 在C++中,reinterpret_cast被称为重新解释类型转换。它是一种强制类型转换操作符,用于将一种数据类型转换为另一种几乎完全不相关的数据类型。这种转换不进行任何数据的重新格式化或转换操作,只是简单地将数据的二进制表示重新解释为新的类型。 语法 语法形式为:reinterpret_ca 阅读全文
posted @ 2024-10-05 21:16 西北野狼 阅读(31) 评论(0) 推荐(0) 编辑
摘要: 静态类型转换(static_cast) 概念 static_cast是C++中的一种类型转换操作符,用于在编译时进行类型转换。它主要用于具有明确的、编译器可以在编译阶段确定的类型转换关系的情况。这种转换通常在相关类型之间进行,例如基本数据类型之间的转换,或者在类层次结构中的向上转换(将派生类指针或引 阅读全文
posted @ 2024-10-05 21:13 西北野狼 阅读(70) 评论(0) 推荐(0) 编辑
摘要: static_cast 基本概念 static_cast主要用于在相关类型之间进行转换,这些类型之间存在某种隐式转换关系。它在编译时进行检查,是一种比较安全的类型转换方式。 适用场景 基本数据类型转换:例如将int转换为double,或者double转换为int(会截断小数部分)。int numIn 阅读全文
posted @ 2024-10-05 20:59 西北野狼 阅读(9) 评论(0) 推荐(0) 编辑
摘要: 概念 在C++中,命名空间(namespace)是一种将代码中的标识符(如变量名、函数名、类名等)进行分组和隔离的机制。它可以避免不同代码模块之间的命名冲突,提高代码的可维护性和可移植性。 命名空间的定义 基本语法为:namespace 命名空间名称 { // 在这里定义变量、函数、类等 } 例如: 阅读全文
posted @ 2024-10-05 13:53 西北野狼 阅读(42) 评论(0) 推荐(0) 编辑
摘要: C++对C的扩展主要体现在以下几个方面: 语法增强: 变量声明位置更灵活:在C语言中,函数内的所有局部变量必须在函数开头的块内进行声明。而C++放松了这一限制,可以在函数内的任何位置声明变量,只要在使用该变量之前进行声明即可。例如: void someFunction() { // C++中可以在循 阅读全文
posted @ 2024-10-03 22:46 西北野狼 阅读(30) 评论(0) 推荐(0) 编辑
摘要: 概念 在C语言中,函数指针是一种特殊的指针类型,它指向的是函数而不是普通的数据变量。函数在内存中有其入口地址,函数指针就是用来存储这个地址的变量。 函数指针的定义 函数指针的定义形式如下: 返回值类型 (*指针变量名)(参数类型列表); 例如,定义一个指向返回值为int,参数为int和int的函数的 阅读全文
posted @ 2024-10-02 22:44 西北野狼 阅读(117) 评论(0) 推荐(0) 编辑
摘要: 概念 在C语言中,共用体(Union)是一种特殊的数据类型。它可以在不同的时刻存储不同类型的数据,但所有成员共享同一块内存空间。这与结构体不同,结构体的每个成员都有自己独立的内存空间。 定义和声明 定义 共用体的定义形式与结构体相似,使用关键字union。例如:union Data { int nu 阅读全文
posted @ 2024-10-02 11:27 西北野狼 阅读(141) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示