秋招C++八股--封装、继承、多态(持续更新)

1 C++中struct和class的区别

相同点:

两者都可以用来定义用户自定义数据类型(UDT)。 两者都可以包含成员变量和成员函数。

不同点:

在默认情况下,C++中的class成员默认为私有(private),而struct成员默认为公有(public)。

C++中的class可以实现封装和数据隐藏,可以使用访问修饰符指定成员的访问权限(公有、私有或保护),而struct默认公开其成员。

C++中的class支持继承和多态性,而struct不支持。

在C语言中,struct只能包含变量,不能包含函数,而在C++中,struct可以包含成员函数。 引申:

在C++中,struct可以被视为class的一种特例,除了默认访问权限和继承方式不同之外,它们几乎具有相同的特性和语法。

C++中的struct可以实现与class相同的功能,但在设计上用于表示更简单的数据结构。 在C语言中,struct主要用于定义数据结构,没有封装和继承的概念,仅仅是一个简单的数据集合。

在C++中,struct被扩展为支持更多的特性,使其能够拥有成员函数、封装性和继承等特性,更加类似于class。

2 深拷贝和浅拷贝

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原 来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也 不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝 的

#include <iostream>  
#include <string.h>
using namespace std;
class Student
{
private:
	int num;
	char* name;
public:
	Student() {
		name = new char(20);
		cout << "Student" << endl;
	};
	~Student() {
		cout << "~Student " << &name << endl;
		delete name;
		name = NULL;
	};
	Student(const Student& s) {//拷贝构造函数
		//浅拷贝,当对象的name和传入对象的name指向相同的地址
		//name = s.name;
		//深拷贝
		name = new char(20);
		memcpy(name, s.name, strlen(s.name));
		cout << "copy Student" << endl;
	};
};
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}

//浅拷贝执行结果:

//Student

//copy Student

//~Student 0x7fffed0c3ec0

//~Student 0x7fffed0c3ed0

//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop):

//0x0000000001c82c20 * **

//深拷贝执行结果:

//Student

//copy Student

//~Student 0x7fffebca9fb0

//~Student 0x7fffebca9fc0

3 什么是类型安全?

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。

“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候 也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。

类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语 言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安 全的程序。绝对类型安全的编程语言暂时还没有。

(1) C的类型安全

C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编 译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。以下是两个十分常 见的例子:

malloc是C中进行内存分配的函数,它的返回类型是void即空类型指针,常常有这样的用法

char* pStr=(char*)malloc(100*sizeof(char)),这里明显做了显式的类型转换。

类型匹配尚且没有问题,但是一旦出现

int* pInt=(int*)malloc(100*sizeof(char))

就很可能带来一些问题,而这样的转换C并不会提示错误。

以下是关于C++类型安全的要点总结:

  1. 操作符new返回的指针类型严格与对象匹配,而不是void*:
    • 在C++中,使用new操作符动态分配内存时,返回的指针类型与对象类型严格匹配,不是通用的void*指针。
    • 这提供了类型安全性,因为我们可以直接将返回的指针赋给对应的指针类型,避免了类型转换的风险。
  2. C++模板函数和类型检查:
    • C++中的模板函数可以根据参数类型进行类型检查和实例化,提供了类型安全性。
    • 相比于C语言中使用void*作为参数的函数,C++模板函数支持更严格的类型检查和类型匹配。
  3. 使用const关键字替代宏定义的常量:
    • 在C++中,推荐使用const关键字来定义常量,而不是使用预处理器的宏定义。
    • const关键字提供了类型和作用域,具有更好的类型安全性和可维护性,而宏定义只是简单的文本替换。
  4. 将宏定义改写为inline函数或模板函数:
    • 一些宏定义可以改写为C++中的inline函数或模板函数,以提供更好的类型安全性。
    • inline函数在编译时进行函数展开,支持类型检查,并能够根据参数类型选择正确的实现。
  5. 使用dynamic_cast进行类型转换:
    • C++提供了dynamic_cast关键字,用于在运行时进行安全的类型转换。
    • dynamic_cast相比于static_cast具有更多的类型检查,可以在类型安全的前提下进行转换。
#include <iostream>
using namespace std;

class MyClass {
public:
    int value;
    MyClass(int val) : value(val) {}
};

int main() {
    MyClass obj(10);
    void* ptr = &obj;  // 将MyClass对象的地址赋给void*指针

    // 将void*指针转换回MyClass*指针
    MyClass* newObj = static_cast<MyClass*>(ptr);

    // 尝试访问转换后的指针指向的对象的成员
    cout << newObj->value << endl;  // 这里访问成员是不安全的

    return 0;
}

#include<iostream>
using namespace std;

class Parent{};

class Child1 : public Parent {
public:
    int i;
    Child1(int e) : i(e) {}
};

class Child2 : public Parent {
public:
    double d;
    Child2(double e) : d(e) {}
};

int main() {
    Child1 c1(5);
    Child2 c2(4.1);
    Parent* pp;
    Child1* pc1;
    
    pp = &c1;
    pc1 = (Child1*)pp;  // 类型向下转换 强制转换,由于类型仍然为Child1*,不造成错误
    cout << pc1->i << endl; // 输出:5
    
    pp = &c2;
    pc1 = (Child1*)pp;  // 强制转换,且类型发生变化,将造成错误
    cout << pc1->i << endl; // 输出:1717986918

    return 0;
}

4 C++ 虚函数如何实现多态?

(1) 编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维 数组,虚表里保存了虚函数的入口地址

(2) 编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构 造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找 到正确的函数

(3) 所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表 并对虚表初始化。 在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为 父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表 -->(虚表指针何时产生)

(4) 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类 的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚 表中将此虚函数地址添加在后面

这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现 多态性。

#include <iostream>

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Animal makes a sound" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Cat();
    Animal* animal2 = new Dog();

    animal1->makeSound();  // 输出:Cat meows
    animal2->makeSound();  // 输出:Dog barks

    delete animal1;
    delete animal2;

    return 0;
}

5 基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

  1. 虚函数表是全局共享的元素,编译时构造完成,全局只有一个虚函数表
  2. 虚函数表类似于一个数组,在类对象中通过vptr指针指向虚函数表。虚函数表不是函数、不是程序代码,因此不存储在代码段
  3. 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针。在编译时期可以确定类中虚函数的个数,所以虚函数表的大小也可以确定。虚函数表的大小是在编译时期确定的,不需要动态分配内存空间,因此不在堆中
  4. 虚函数表类似于类中的静态成员变量,也是全局共享的。由于静态成员变量是全局的,大小确定,虚函数表也具有这些特点,因此最可能存在于全局数据区
  5. 根据测试结果,在Linux/Unix中,虚函数表(vtable)存放在可执行文件的只读数据段中(rodata)。与微软的编译器将虚函数表存放在常量段存在一些差别。

综上所述,虚函数表是一个全局共享的数组,在类对象中通过vptr指针指向虚函数表。它存储着虚函数的地址,大小在编译时确定,可能存在于全局数据区。在C++内存模型中,虚函数表位于只读数据段(.rodata),而虚函数位于代码段(.text)。

—————— 程序从写好到运行所经历的过程

  1. 编写源代码:程序员使用文本编辑器或集成开发环境(IDE)编写C++源代码文件,其中包含程序的逻辑和功能实现。
  2. 预处理(Preprocessing):编译器在编译过程之前进行预处理。预处理器根据源代码中的预处理指令(以 # 开头的指令,如#include#define 等)对源代码进行处理,例如展开宏定义、包含头文件等。预处理的结果是一个被修改过的源代码文件。
  3. 编译(Compilation):**编译器接收预处理后的源代码文件,将其转换为汇编语言代码。**在编译阶段,编译器会进行词法分析、语法分析、语义分析等操作,生成相应的中间代码或汇编语言代码。
  4. 汇编(Assembly):汇编器将编译阶段生成的汇编语言代码转换为机器语言的目标文件。目标文件是机器代码的二进制表示形式
  5. 链接(Linking):链接器将目标文件与所需的库文件进行链接,生成可执行文件。链接器将目标文件中的符号引用解析为实际地址,并解决不同目标文件之间的引用关系,生成最终可执行文件。
  6. 执行(Execution):最终生成的**可执行文件被操作系统加载到内存中,并在计算机上执行。**程序的执行过程涉及操作系统的资源管理、指令执行、内存访问等操作,最终实现程序的功能和逻辑。

计算机内存(Memory)是指计算机系统中用于存储数据和指令的物理设备。它提供了临时存储数据的空间,供计算机进行读取、写入和处理操作。

内存在计算机系统中扮演着重要的角色,它被用于存储程序、数据和运算中间结果,是计算机系统进行数据交换和处理的关键组成部分。

计算机内存通常以字节(Byte)为最小的存储单元,每个字节都有一个唯一的地址。这些地址可以被用来定位和访问内存中的数据。

内存被分为不同的层级和类型,包括:

  1. 寄存器(Registers):**位于CPU内部,用于存储指令、数据和中间结果。**寄存器是计算机系统中最快速的存储设备,但容量非常有限。
  2. 缓存(Cache):位于CPU和主存(主内存)之间,用于存储最常用的数据和指令。缓存的访问速度比主存快,但容量较小。
  3. 主存(Main Memory):也称为内存或随机存取存储器(Random Access Memory,RAM),是计算机中用于存储程序和数据的主要存储设备。主存的访问速度比较快,容量通常比寄存器和缓存大。
  4. 虚拟内存(Virtual Memory):是一种扩展主存的技术,它允许将部分数据和程序存储在硬盘上,以提供更大的可用内存空间。虚拟内存通过将数据从磁盘交换到内存中进行访问,并根据需要进行调度。

6 涉及到虚函数,对象的内存如何布局?虚函数表和谁绑定?

虚函数表(Virtual Function Table,VTable)是一种用于实现动态多态的机制,在C++的对象模型中起到关键作用。虚函数表是针对类而生成的,而不是针对对象。

每个包含虚函数的类都会在内存中生成一个虚函数表,虚函数表是一个特殊的数据结构,其中存储了该类的虚函数的地址。

虚函数表是类的静态成员,它在编译时就已经生成,并且对于该类的所有对象都是共享的。

当一个类被定义为包含虚函数时,编译器会自动创建一个虚函数表,并将类中的每个虚函数的地址按顺序存储在虚函数表中的相应位置。

在对象被创建时,编译器会在对象的内存布局中添加一个指向所属类的虚函数表的指针,通常称为虚函数表指针(vptr)。

通过虚函数表指针,对象能够在运行时动态地访问虚函数表,并根据对象的实际类型调用正确的虚函数。当调用一个虚函数时,实际上是通过对象的虚函数表指针找到对应的虚函数的地址,然后进行调用。

结合以下Derive1对象的内存布局会理解的更清楚

一、单继承且本身不存在虚函数的派生类内存布局 alt

class Base1 {
public:
    int base1_1, base1_2;

    virtual void base1_fun1() {}  //  定义虚函数
    virtual void base1_fun2() {}
};
 
class Derive1 : public Base1 { // Derive1 中不存在虚函数
public:
    int derive1_1, derive1_2;
};

二、单继承且存在基类虚函数覆盖的派生类内存布局

alt

class Base1 {
public:
    int base1_1, base1_2;
 
    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};
 
class Derive1 : public Base1 {
public:
    int derive1_1, derive1_2;

    virtual void base1_fun1() {} // 派生类函数覆盖基类中同名函数
};

三、单继承且派生类存在属于自己的虚函数 alt

class Base1 {
public:
    int base1_1, base1_2;
 
    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};
 
class Derive1 : public Base1 {
public:
    int derive1_1, derive1_2;
 
    virtual void derive1_fun1() {} // 派生类存在属于自己的虚函数
};

四、多继承且存在虚函数覆盖同时又存在自身定义的虚函数的派生类对象布局

多继承场景下,派生类 Derive1 对象的内存中存在两个虚表指针,分别指向两个虚函数表。 alt

class Base1 {
public:
    int base1_1, base1_2;
 
    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};
 
class Base2 {
public:
    int base2_1, base2_2;
 
    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};
 
class Derive1 : public Base1, public Base2 { // Derive 1 分别从 Base 1 和 Base2 继承过来
public:
    int derive1_1, derive1_2;
 
    virtual void base1_fun1() {}
    virtual void base2_fun2() {}
 
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

Sum

当定义一个指向派生类的基类指针,用指针访问基类中的某个函数时,

如果该函数不是虚函数, 直接调用基类中的这个函数;

如果是虚函数, 则查找虚函数表, 并进行后续的调用。

编译器在编译时给类创建一个虚函数表,同一个类的所有实例共用同一份虚函数表,但不同的类实例中各自保存一个虚表指针,编译器会在构造函数中插入一段代码,这段代码用来给虚表指针赋值,虚表指针里存放的就是虚函数表的地址,这个虚表指针的内容在程序运行期间才能决定,也就是所谓的“动态绑定”。

简单理解,就是在编译时对象的所属类不能确定,那么编译器就无法采用静态编联,所以就只有通过动态编联的方式,在运行时去绑定调用指针和被调用地址。

虚函数表位于只读数据段(.rodata),即:C++内存模型中的常量区。虚函数表在编译期就已经生成,函数执行之前查表。

7 public,protected和private访问和继承权限

访问权限:

访问权限 类内可访问 类外可访问 派生类可访问
public Yes Yes Yes
private Yes No No
protected Yes No Yes
继承方式 基类成员在派生类中的访问权限
public 保持不变
private 变为私有
protected 变为保护

public继承

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有 成员任然是私有的,不能被这个派生类的子类所访问

protected继承

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的

private继承

私有继承的特点是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类所访问,基类的成员只能由自己派生类访问,无法再往下继承

8 多态的定义、种类、实现 ?

多态是面向对象编程中的一个重要概念,它允许以统一的方式处理不同类型的对象,并根据对象的实际类型来执行相应的操作

多态性使得程序能够根据对象的实际类型,在运行时动态地选择合适的函数或方法。

种类:

静态多态(编译时多态):

通过函数重载和运算符重载实现。在编译时根据函数参数的类型或运算符的操作数类型来确定调用的具体函数或操作。

动态多态(运行时多态):

通过继承和虚函数实现。在运行时根据对象的实际类型来确定调用的具体函数或方法。

实现动态多态的关键是使用虚函数(Virtual Function):

在基类中声明虚函数:在基类中使用 virtual 关键字声明一个函数为虚函数,表示该函数可以在派生类中被重写。

在派生类中重写虚函数:在派生类中重新定义(重写)基类的虚函数,并使用 override 关键字进行标注,确保函数签名与基类中的虚函数一致。

使用基类指针或引用调用虚函数:可以通过基类指针或引用指向派生类对象,并调用虚函数。编译器会根据对象的实际类型选择正确的函数实现,实现动态绑定。

9 什么是纯虚函数?什么时候使用?和虚函数的区别?

1

纯虚函数是在基类中声明但没有实现的虚函数。它通过在函数声明后面加上 "= 0" 来指定。纯虚函数在基类中没有具体的实现,它的目的是为了让派生类必须重写该函数以提供具体的实现。

虚函数是一个可以在派生类中被重写的函数,它允许在基类中定义一个函数的框架,然后在派生类中根据需要进行特定的实现。通过使用虚函数,可以实现运行时多态性,即在运行时根据对象的实际类型调用正确的函数实现。

2

虚函数和纯虚函数的主要区别在于:

虚函数可以在基类中有默认的实现,而纯虚函数在基类中没有具体的实现。

虚函数可以被派生类重写,但不是必须重写,而纯虚函数必须在派生类中被重写以提供具体的实现。

包含纯虚函数的类被称为抽象类,抽象类不能直接实例化对象,只能被用作基类,而包含虚函数的类可以直接实例化对象。

3

纯虚函数的存在确实可以满足不同的继承需求,具体可以归纳为以下情况:

只继承函数接口:派生类只需要继承基类的函数接口,而不需要继承函数的实现。通过将函数声明为纯虚函数,基类可以定义一组接口供派生类实现,派生类必须重写这些纯虚函数来提供具体的实现。这种情况下,基类成为了一个接口定义的角色,派生类根据需要提供不同的实现。

继承函数接口和实现:派生类既需要继承基类的函数接口,又需要继承函数的实现,同时还可以在派生类中重写这些函数以实现多态。这种情况下,基类可以将函数定义为虚函数而不是纯虚函数,派生类可以继承基类的实现,并根据需要进行重写。

禁止默认实现:有时候,我们希望派生类在继承函数接口和实现的情况下,不能使用基类的默认实现,而是强制要求派生类提供自己的实现。通过将函数声明为纯虚函数,可以使包含纯虚函数的类成为抽象类,抽象类不能实例化,而派生类必须提供该函数的实现才能被实例化。

#include <iostream>

// 抽象基类
class Animal {
public:
    // 纯虚函数,只继承函数接口
    virtual void makeSound() = 0;

    // 普通虚函数,可继承函数接口和实现
    virtual void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};

// 派生类
class Dog : public Animal {
public:
    // 实现纯虚函数
    void makeSound() override {
        std::cout << "Dog is barking." << std::endl;
    }

    // 重写虚函数
    void eat() override {
        std::cout << "Dog is eating." << std::endl;
    }
};

int main() {
    // 使用指针调用函数接口
    Animal* animal = new Dog();
    animal->makeSound();  // 调用派生类的纯虚函数
    animal->eat();        // 调用派生类的虚函数

    delete animal;
    return 0;
}

10 子类重写虚函数表会有什么影响?

当子类重写(覆盖)了虚函数时,它会重新定义该虚函数的实现,从而在虚函数表中替换基类虚函数的入口地址。虚函数表是一个存储了虚函数指针的表格,用于实现动态多态性。

在基类中,虚函数表中的相应槽位存储了指向基类的虚函数的地址。当派生类重写了虚函数时,它会在自己的虚函数表中存储指向派生类虚函数的地址。这样,在使用派生类对象调用该虚函数时,程序会通过派生类对象的虚函数表找到正确的函数地址,并调用派生类的实现。

这意味着,在继承关系中,每个派生类都有自己的虚函数表,并且该表中的函数指针会根据派生类的实现进行更新。当通过基类指针或引用调用虚函数时,根据指针或引用的动态类型,程序会在相应的虚函数表中查找并调用正确的函数。

11 什么是菱形继承?如何用虚拟继承解决二义性?内存布局如何?

1 菱形继承(Diamond Inheritance)是指在继承关系中存在两个派生类分别继承同一个基类,并且另一个派生类同时继承这两个派生类的情况。这样的继承关系形成了一个菱形的结构,因此得名菱形继承。

#include <iostream>

class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};

class Mammal : public Animal {
public:
    void move() {
        std::cout << "Mammal is moving." << std::endl;
    }
};

class Bird : public Animal {
public:
    void fly() {
        std::cout << "Bird is flying." << std::endl;
    }
};

class Bat : public Mammal, public Bird {
public:
    void feed() {
        std::cout << "Bat is feeding." << std::endl;
    }
};

int main() {
    Bat bat;
    bat.eat(); // Ambiguous function call
    bat.move(); // Mammal is moving.
    bat.fly(); // Bird is flying.
    bat.feed(); // Bat is feeding.
    
    return 0;
}

上述代码存在二义性,解决如下:


#include <iostream>

class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};

class Mammal : virtual public Animal { // 虚拟继承
public:
    void move() {
        std::cout << "Mammal is moving." << std::endl;
    }
};

class Bird : virtual public Animal { // 虚拟继承
public:
    void fly() {
        std::cout << "Bird is flying." << std::endl;
    }
};

class Bat : public Mammal, public Bird {
public:
    void feed() {
        std::cout << "Bat is feeding." << std::endl;
    }
};

int main() {
    Bat bat;
    bat.eat(); // Animal is eating.
    bat.move(); // Mammal is moving.
    bat.fly(); // Bird is flying.
    bat.feed(); // Bat is feeding.
    
    return 0;
}


在上述代码中,通过在 Mammal 和 Bird 对 Animal 的继承声明中使用 virtual 关键字,将它们的继承关系设置为虚拟继承。这样,在派生类 Bat 中只会存在一份共享的 Animal 子对象。

通过使用虚拟继承,派生类 Bat 可以正常调用 eat() 函数,而不会出现二义性问题。此时,eat() 函数只有一份实现,并被派生类 Bat 继承和调用。

3 详解虚继承

虚继承(Virtual Inheritance)是C++中用于解决多重继承带来的二义性和冗余存储的机制。虚继承的底层实现原理与具体的编译器相关,但通常通过虚基类指针(vbptr)和虚基类表(virtual base table)来实现。

在虚继承中,每个虚继承的子类都会有一个虚基类指针(vbptr),该指针占用一个指针大小的存储空间(通常是4字节或8字节,取决于平台的位数)。

同时,虚继承的子类还会有一个虚基类表,该表存储了虚基类相对于直接继承类的偏移地址。

当一个类通过虚继承派生出子类时,子类会继承父类的虚基类指针。这样,子类可以通过虚基类指针来访问共享的虚基类成员,而不会产生冗余的拷贝。虚继承的主要目的是确保在多重继承中共享的虚基类只有一份,避免二义性和冗余存储。

虚继承中,子类对象的布局与普通继承不同。子类对象会多出一个指向中间层父类对象的虚基类指针(vbptr)。该虚基类指针指向一个虚基类表,其中存储了虚基类相对于直接继承类的偏移地址。通过偏移地址,可以在运行时动态查找并访问虚基类的成员。

使用虚继承可以避免多重继承中的二义性问题,同时节省了存储空间,因为共享的虚基类只有一份拷贝。虚继承在设计中需要谨慎使用,通常用于解决特定的继承问题。 alt

alt

#include <iostream>
#include <string>

class Father {
public:
    Father() {
        std::cout << "Father constructed !" << std::endl;
    }
    virtual void Func() {}
    virtual void Func2() {}
    void Func3() {}
};

class Mother {
public:
    Mother() {
        std::cout << "Mother constructed !" << std::endl;
    }
    virtual void Func() {}
};

class Son : public Mother, public Father {
public:
    Son() {
        std::cout << "Son constructed !" << std::endl;
    }
};

int main() {
    Father father;
    std::cout << sizeof(father) << std::endl;
    Mother mother;
    std::cout << sizeof(mother) << std::endl;
    Son son;
    std::cout << sizeof(son) << std::endl;
    std::cin.get();
}


两个父类各自含有虚函数表指针8字节

在多继承场景中,派生类中会存在两个虚表指针,分别从 Father 和 Mother 中继承过来的,所以 Son 对象占 16 字节。派生类的虚表指针都是从基类中继承来的(基类中得存在虚表指针才行),如果派生类中要新增新的虚函数,则在基类虚函数表中新增函数地址,如果派生类中要重写基类中的虚函数,则在基类的虚函数表中覆盖写,把基类中的函数换成自己类中的函数。

12 哪些函数不能是虚函数?

构造函数:构造函数在对象创建时被调用,用于初始化对象的状态。由于虚函数的调用需要依赖于对象的类型,而在构造函数中对象的类型尚未确定,因此构造函数不能是虚函数。

内联函数:内联函数在编译时会被展开替换,而虚函数的调用是在运行时通过虚表进行动态分派。内联函数的展开是根据编译时的类型确定的,而虚函数的调用是基于运行时的对象类型确定的,所以内联函数不能是虚函数。

静态函数:静态函数是属于类而不是对象的函数,它们没有 this 指针,无法通过对象来调用。虚函数的调用是通过对象的虚表指针进行的,因此静态函数不能是虚函数。

友元函数:友元函数不是类的成员函数,它们在访问类的私有成员时具有特殊权限,但它们与类的继承关系无关,因此没有虚函数的概念。

普通函数:普通函数不是类的成员函数,它们与类没有继承关系,所以没有虚函数的概念。

#23届找工作求助阵地##秋招#
C++ 校招面试精解 文章被收录于专栏

适用于 1.C++基础薄弱者 2.想快速上手C++者 3.秋招C++方向查漏补缺者 4.秋招C++短期冲刺者

全部评论
c++中的struct也支持继承和多态的吧,第一个是不是就错了。
点赞
送花
回复
分享
发布于 2023-07-12 20:11 江苏
额,C++的struct也支持继承和多态
点赞
送花
回复
分享
发布于 2023-07-27 00:10 吉林
秋招专场
校招火热招聘中
官网直投

相关推荐

5 30 评论
分享
牛客网
牛客企业服务