【C++】继承与多态的常见问题解析


在面向对象编程中,C++的继承和多态是非常重要的概念,它们为软件开发提供了灵活性、可扩展性和代码重用性。然而,在使用继承和多态时,也会遇到一些常见问题和需要注意的事项。本文将从菱形继承、多态、虚函数、抽象类等方面进行详细解析,希望能够帮助读者更好地理解和应用这些概念。

继承

1.什么是菱形继承?菱形继承的问题是什么?

菱形继承:菱形继承是多继承的一种特殊情况。
在这里插入图片描述
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
Assistant的对象中Person成员会有两份。

class Person
{
public:
	string _name; //姓名
};
class Student : public Person
{
protected:
	int _num; // 学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	//  a._name = "peter";

	// 需要指明访问哪个父类成员可以解决二义性问题, 但是数据冗余问题无法解决
	a.Student::_name == "xxx";
	a.Teacher::_name == "xxx";
}

在这里插入图片描述

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在StudentTeacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

class Student : virtual public Person
{
	// ...
};
class Teacher : virtual public Person
{
	// ...
};


通过将 Person 声明为虚基类,可以确保在 Assistant 类中只有一份 Person类的数据成员,避免了数据冗余;同时,在访问 Person类的成员时,由于只有一份实例,消除了二义性问题。

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承和组合是面向对象编程中两种不同的代码重用机制,它们有着各自的优势和适用场景。

1.继承(Inheritance):

  • 继承是一种“是什么”的关系,子类继承父类的属性和方法。
  • 适合用于表达"is-a"的关系,即子类是父类的一种特殊类型。
  • 可以通过继承实现代码复用和多态性。
  • 继承会导致子类与父类之间的耦合度增加,子类的实现依赖于父类的实现。
  • 适合用于共享通用行为和属性的情况。

2. 组合(Composition):

  • 组合是一种“有什么”的关系,一个类包含另一个类作为其成员变量。
  • 适合用于表示“has-a”关系,即一个类包含另一个类的实例作为其一部分。
  • 可以更灵活地构建对象的结构,减少耦合度。
  • 组合可以在运行时动态地改变对象的行为,提高了代码的灵活性和可维护性。
  • 适合用于构建对象之间的整体-部分关系。

在选择使用继承还是组合时,可以考虑以下几点:

  • 当新类是现有类的一种特殊类型,并且需要继承现有类的行为时,可以使用继承。
  • 当新类需要复用现有类的功能,但并非是现有类的特殊类型时,可以考虑使用组合。
  • 当希望通过改变对象的部分来改变对象的行为时,可以使用组合。
  • 当面临多层次的嵌套关系或者存在菱形继承等问题时,可以考虑使用组合来替代继承。

总的来说,继承适合用于表达"is-a"关系,组合适合用于表达"has-a"关系。在设计时根据具体情况选

在 C++ 中使用继承和组合:

#include <iostream>
using namespace std;

// 基类
class Shape {
public:
    void setWidth(int w) {
        width = w;
    }
    void setHeight(int h) {
        height = h;
    }

protected:
    int width;
    int height;
};

// 派生类,使用继承
class Rectangle : public Shape {
public:
    int getArea() {
        return (width * height);
    }
};

// 另一个派生类,使用组合
class Square {
private:
    Shape shape; // 包含一个 Shape 对象作为成员

public:
    void setSide(int s) {
        shape.setWidth(s);
        shape.setHeight(s);
    }

    int getArea() {
        return shape.getWidth() * shape.getHeight();
    }
};

int main() {
    Rectangle rect;
    rect.setWidth(5);
    rect.setHeight(7);
    cout << "Rectangle Area: " << rect.getArea() << endl;

    Square square;
    square.setSide(5);
    cout << "Square Area: " << square.getArea() << endl;

    return 0;
}

多态

1. 什么是多态?

在C++中,多态(polymorphism)是面向对象编程中一个重要的概念,它允许不同类的对象对同一消息做出响应。多态性可以分为静态多态(静态多态绑定)和动态多态(动态多态绑定)两种类型。

1. 静态多态(静态多态绑定):

静态多态是在编译时发生的多态,也称为早期绑定或编译时多态。静态多态通常通过函数重载和运算符重载实现。在静态多态中,编译器在编译时就决定了调用哪个函数或操作符。例如:

使用函数重载的示例:

#include <iostream>
using namespace std;

void print(int num) {
    cout << "Print integer: " << num << endl;
}

void print(double num) {
    cout << "Print double: " << num << endl;
}

int main() {
    int num1 = 10;
    double num2 = 3.14;

    print(num1); // 调用 print(int) 函数
    print(num2); // 调用 print(double) 函数

    return 0;
}

使用运算符重载的示例:

#include <iostream>
using namespace std;

class Vector {
private:
    int x;
    int y;

public:
    Vector(int x, int y) {
        this->x = x;
        this->y = y;
    }

    Vector operator+(const Vector& other) {
        int newX = this->x + other.x;
        int newY = this->y + other.y;
        return Vector(newX, newY);
    }

    void print() {
        cout << "Vector(" << x << ", " << y << ")" << endl;
    }
};

int main() {
    Vector v1(1, 2);
    Vector v2(3, 4);

    Vector v3 = v1 + v2; // 调用重载的 + 运算符

    v3.print();

    return 0;
}

2.动态多态(动态多态绑定):

动态多态是在运行时发生的多态,也称为晚期绑定或运行时多态。动态多态通过虚函数和纯虚函数实现。在动态多态中,函数的调用是在运行时确定的,根据指针或引用指向的实际对象类型来调用相应的函数。例如:

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() {
        cout << "Drawing shape..." << endl;
    }
};

class Circle : public Shape {
public:
    void draw() {
        cout << "Drawing circle..." << endl;
    }
};

int main() {
    Shape* s = new Shape();
    Shape* c = new Circle();

    s->draw(); // 调用 Shape 类中的 draw 函数
    c->draw(); // 调用 Circle 类中的 draw 函数

    delete s;
    delete c;

    return 0;
}

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

重载(Overloading):指在同一个作用域内,函数名相同但参数列表不同的现象。通过函数重载,可以让同名函数根据参数的不同来执行不同的操作。编译器会根据函数的参数列表来区分不同的重载函数。重载不考虑函数的返回类型,只考虑函数的参数列表。

重写(Override):指子类重新定义(覆盖)了父类中的虚函数。在继承关系中,子类可以通过重写父类的虚函数来改变或扩展父类的行为。通过重写,子类可以在多态调用中动态地调用到自己定义的函数。

重定义(Hide):也称为隐藏,指子类定义了与父类同名的非虚函数。当子类中定义了一个与父类同名但参数列表不同的函数时,父类中的同名函数将被隐藏而不是被重写。这种情况下,在使用基类指针或引用调用该函数时,会调用基类中的函数,而不是子类中定义的函数。这种行为被称为函数的重定义或隐藏。

总结:

  • 重载是指在同一作用域内,函数名相同但参数列表不同;
  • 重写是指子类重新定义父类中的虚函数,实现多态性;
  • 重定义是指子类定义了与父类同名但参数列表不同的函数,导致父类函数被隐藏。

重写与隐藏的对比:

#include <iostream>

class Base {
public:
    virtual void print() {
        std::cout << "Base class print()" << std::endl;
    }

    void display() {
        std::cout << "Base class display()" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived class print()" << std::endl;
    }

    void display(int num) {
        std::cout << "Derived class display(int)" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived;

    basePtr->print();    // 调用重写的虚函数
    basePtr->display();  // 调用基类中的非虚函数

    delete basePtr;

    return 0;
}

在上面的示例中,Base 类中有一个虚函数 print() 和一个非虚函数 display()Derived 类继承自 Base 类,并重写了 print() 函数,并在 Derived 类中定义了一个与 display() 参数列表不同的函数 display(int)

当我们通过基类指针 basePtr 调用函数时:

basePtr->print() 调用的是被重写的虚函数,会动态调用 Derived 类中的 print() 函数,输出 “Derived class print()”
basePtr->display() 调用的是基类中的非虚函数,因此输出 “Base class display()”
这就展示了重写和重定义(隐藏)在实际调用时的区别:重写会根据实际对象类型动态调用适当的函数,而重定义则会调用基类中的函数而不是派生类中定义的函数。

3. 多态的实现原理?

多态是面向对象编程中的一个重要概念,通过多态性可以实现不同对象对同一消息作出不同响应的能力。在C++中,多态性主要通过虚函数和指针/引用的动态绑定来实现。

具体来说,多态的实现原理如下:

  1. 虚函数(Virtual Function):在基类中使用 virtual 关键字声明的成员函数即为虚函数。派生类可以通过重写(Override)虚函数来改变或扩展基类的行为。
  2. 动态绑定(Dynamic Binding):当通过基类的指针或引用调用虚函数时,程序会根据指针或引用所指向的对象的实际类型来确定调用哪个版本的虚函数,这种机制称为动态绑定。
  3. 虚函数表(Virtual Table,VTable):编译器会为每个包含虚函数的类生成一个虚函数表,其中存储了各个虚函数的地址。每个对象都包含一个指向虚函数表的指针,当调用虚函数时,程序会根据该指针找到对应的虚函数并执行。
  4. 虚函数调用过程:当通过基类指针或引用调用虚函数时,程序会首先根据指针或引用的静态类型(基类类型)找到对应的虚函数表,然后根据表中的偏移量找到实际需要调用的函数,并执行。
  5. 实现多态性:通过虚函数和动态绑定的机制,不同类型的对象可以表现出不同的行为,从而实现了多态性,即同一类型的指针或引用在不同情况下表现出不同的行为。

总之,多态性通过虚函数和动态绑定机制实现,使得程序可以根据对象的实际类型来动态选择调用适当的函数,从而实现了灵活的对象行为表现。
指向父类,在父类的虚函数表中找到虚函数。指向子类会把子类中的切片切出来,然后在中间找找到的是覆盖重写后的虚函数。

在C++中,通过指向父类对象的指针或引用调用虚函数时,程序会在父类的虚函数表中找到对应的虚函数。而当指向子类对象的指针或引用调用虚函数时,会首先将子类对象切片(Slicing),然后在切片后的对象中找到被覆盖重写后的虚函数。

具体来说,当通过指向父类对象的指针或引用调用虚函数时,程序会在父类的虚函数表中找到对应的虚函数并执行;而通过指向子类对象的指针或引用调用虚函数时,会首先进行对象切片,即只保留子类对象中与父类相同部分的内容,然后在切片后的对象中找到被子类覆盖重写后的虚函数并执行。

这种动态绑定的机制使得程序能够根据对象的实际类型来调用适当的函数,实现了多态性,即同一类型的指针或引用在不同情况下表现出不同的行为。因此,在面向对象编程中,多态性是一种强大的特性,能够提高代码的灵活性和可扩展性。

4. inline函数可以是虚函数吗?

可以,不过编译器在多态调用的时候就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。而在普通调用的时候依然有inline属性。

5. 静态成员可以是虚函数吗?

,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

析构函数可以被声明为虚函数。在C++中,将基类的析构函数声明为虚函数是一种良好的编程实践,特别是当使用指向派生类对象的基类指针时,通过将析构函数声明为虚函数可以确保在删除基类指针时正确调用派生类的析构函数,从而避免内存泄漏和未定义行为。

在以下场景下,将析构函数声明为虚函数是非常有必要的:

  1. 当基类指针指向派生类对象,并且在删除这个指针时需要调用派生类的析构函数时,应该将基类的析构函数声明为虚函数。
  2. 当使用多态性(Polymorphism)时,即通过基类指针或引用调用虚函数来实现动态绑定时,为了确保在销毁对象时调用正确的析构函数,应该将基类的析构函数声明为虚函数。
  3. 当基类中存在至少一个虚函数时,通常建议将析构函数也声明为虚函数,以避免潜在的问题。

总之,将析构函数声明为虚函数可以确保在多态情况下正确调用对象的析构函数,从而避免内存泄漏和其他问题。因此,在设计具有继承关系的类时,通常建议将析构函数声明为虚函数。

8. 对象访问普通函数快还是虚函数更快?

首先如果是普通对象(普通调用),是一样快的。如果是指针对象或者是引用对象(多态调用),则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?

虚函数表(vtable)是在编译阶段生成的,主要存在于程序的数据段(data segment)(常量区)中。在C++中,每个类(包括含有虚函数的类)都会在编译时生成一个虚函数表,用于存储该类的虚函数地址。当一个类包含虚函数时,编译器会为该类生成一个虚函数表,并在该类的对象中添加一个指向虚函数表的指针(通常称为虚函数指针)。

虚函数表中存储了该类的虚函数的地址,每个虚函数在虚函数表中占据一个固定的位置,通过虚函数指针可以根据偏移量找到对应的虚函数地址。当调用一个虚函数时,程序会通过虚函数指针找到对象所属类的虚函数表,并根据函数在虚函数表中的位置找到实际要调用的函数地址,从而实现动态绑定(Dynamic Binding)。

虚函数表的存在使得C++能够实现多态性(Polymorphism),即同一类型的指针或引用在不同情况下表现出不同的行为,提高了代码的灵活性和可维护性。虚函数表的生成是C++实现多态性的关键机制之一,也是面向对象编程的重要特征之一。

10. C++菱形继承的问题?虚继承的原理?

菱形虚拟继承(Diamond Inheritance)的原理涉及到虚基表(Virtual Base Table)。在菱形虚拟继承中,如果一个类同时从两个不同的路径继承同一个虚基类,那么为了避免出现两份相同的虚基类子对象,需要使用虚基类来解决这个问题。

在菱形虚拟继承中,每个派生类会包含一个指向虚基表的指针(通常称为虚基指针),这样可以在运行时准确定位虚基类的位置和偏移量。虚基表中存储了虚基类在派生类对象中的偏移量,以及虚基类的构造函数和析构函数的地址,通过虚基表,程序可以正确地访问共享的虚基类。

因此,虚基表是C++语言中用于实现菱形虚拟继承的关键机制,它能够确保在菱形继承情况下,共享的虚基类只会被构造一次,避免了资源浪费和访问冲突的问题。

11. 什么是抽象类?抽象类的作用?

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

接口继承和实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q_hd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值