05 C++ 继承、多态和虚函数

继承

继承是 \(OOP\) 程序设计中很重要的一个特性,继承可以扩充现有类以满足新的应用

将已有的类称之为父类,也称基类,将新产生的类称为子类,也称为派生类

派生类不做任何改变地继承了基类中的所有变量和函数(构造函数和析构函数除外),并且可增加新的数据成员和函数,从而使派生类比基类更为特殊化

子类中可以访问父类的公有成员,但父类对象或父类中的某个函数不能调用子类中的函数

声明方式:

class Grade
{
    ...
public:
    ...
};
class Test : public Grade //  继承
{
    ...
public:
    ...
};

保护成员和类的访问

三种访问权限:

private 只能在类内部访问

protected 能在该类和子类中访问

public 均能访问

继承后的访问规则

父类成员访问权限 private 继承后 protected 继承后 public 继承后
private: x x:不可访问 x:不可访问 x:不可访问
protected: y private: y protected: y protected: y
public: z private: z protected: z public: z

如果省略继承修饰符,默认为私有继承

构造函数和析构函数

继承情况下的调用次序:基类构造函数,子类构造函数;子类析构函数,基类析构函数

class   BaseDemo {
public:
	BaseDemo( )
	{ cout << "In BaseDemo constructor.\n";  }
	~BaseDemo( )
	{ cout << "In BaseDemo destructor.\n";    }
};
class DerivedDemo : public BaseDemo {
public:
	 DerivedDemo( )
	{ cout << "In DerivedDemo constructor.\n";  }
	~ DerivedDemo( )
	{ cout << "In DerivedDemo destructor.\n";   }
};
// output
In BaseDemo constructor.
In DerivedDemo constructor.
In DerivedDemo destructor.
In BaseDemo destructor.

初始化列表

用于在对象创建时直接初始化成员变量,写在构造函数定义之后、函数体之前,用冒号 : 引出

class MyClass
{
    int x;int y;
public:
    MyClass(int a, int b) : x(a), y(b) 
    { /*构造函数体*/ }
// 一般写法
    MyClass(int a,int b)
    { x=a;y=b; }
};

成员对象的初始化放在初始化列表中,效率较高,放在构造函数中进行赋值,效率较低

基本类型变量的初始化在效率上没区别

class Base {
      Base(  );
      Base(const Base &other);
};
class Derived {
       Base  B_Member;
  public:
       Derived(const Base &a);
};
// 构造函数的实现
Derived::Derived(const Base & b )
          : B_Member(b)
{    /* … */     }
// 也可这样实现,但效率较低 
Derived::Derived(const Base &b)
{
    B_Member = b;
}
  1. 类中的 const 常量只能在初始化列表中进行初始化,而不能在函数体内用赋值的方式来初始化
class Base
{
	const  int  SIZE ;
	Base(int size) : SIZE(size)
    {    /*...*/    }
};
Base  one(100);
  1. 引用类型成员(如 int& )必须使用初始化列表,因为引用必须在声明时绑定
class Demo
{
    int& ref;
public:
    Demo(int& r) : ref(r) { }
};
  1. 继承中,若基类无默认构造函数,必须手动调用基类构造函数

向基类构造函数传参

如果基类和子类都有缺省的构造函数(无参数),它们的调用是自动完成的,这是一种隐式调用

如果基类的构造函数带有参数,必须让子类的构造函数显式调用基类的构造函数,并向基类构造函数传参

class Base {
    Base( int  x );
};	
class Derived : public Base {
    Derived(int x, int y) : Base(x)
    {   /*...*/   }	
};

覆盖基类的函数成员

函数覆盖的特点:

覆盖出现在具有继承关系的基类和子类之间

覆盖除了要求函数名完全相同,还要求相应的参数个数和类型完全相同

当进行函数调用时,子类对象所调用的是子类中定义的函数

覆盖是C++多态性的部分体现

class MileDist
{
protected: float miles;
public:
    void setDist(float d) { miles = d; }
    float getDist() { return miles; }
};
class FeetDist : public MileDist
{
protected: float feet;
public:
    void setDist(float); // 重载父类 setDist
    float getDist() { return feet; } // 重载父类 getDist
    float getMiles() { return miles; }
};
void FeetDist::setDist(float ft)
{
    feet = ft;
    MileDist::setDist(feet / 5280); // 调用基类函数
}
void main()
{
    FeetDist feet;
    float ft;
    cout << "A distance in feet and convert it to miles:";
    cin >> ft;
    feet.setDist(ft);
    cout << feet.getDist() << " feet equals ";
    cout << feet.getMiles() << " miles.\n";
}

若想使用子类调用父类成员函数,可以通过作用域强制调用

d.MileDist::getDist();

虚函数

函数覆盖并不能称为真正的多态性

#include <iostream>
using namespace std;
class Animal
{
public:
    void speak()
    { cout << "Animal makes a sound" << endl; }
};
class Cat : public Animal
{
public:
    void speak()
    { cout << "Meow~" << endl; }
};
void makeAnimalSpeak(Animal a)
{
    a.speak();
}
int main()
{
    Cat c;
    makeAnimalSpeak(c);
}
// output
Animal makes a sound

原因:

虽然 Cat 也有 speak(),但由于传参是 Animal不会自动调用子类的版本

makeAnimalSpeak(c) 实际是把 Cat 对象当作 Animal 处理,调用的仍是 Animal::speak()Cat 的 speak() 被隐藏了

也就是说,C++编译器在缺省情况下,对函数成员的调用实施的是静态链接(也称静态绑定)

静态绑定:根据前面对象(指针)类型,决定调用的函数

动态绑定:根据实际指向的对象类型,决定调用的函数(在运行时决定)

声明方式:

在基类函数前加 virtual

virtual void speak();

作用:

实现运行时多态(动态绑定),即父类中定义一个统一的接口,让所有子类都可以有不同的实现,并且通过基类指针或引用来调用这些子类的行为

#include <iostream>
using namespace std;
class Animal
{
public:
    virtual void speak()
    { cout << "Animal speaks" << endl; }
};
class Dog : public Animal
{
public:
    void speak()
    { cout << "Woof!" << endl; }
};
class Cat : public Animal
{
public:
    void speak()
    { cout << "Meow!" << endl; }
};
void makeSpeak(Animal &a)
{
    a.speak();
}
int main()
{
    Dog d;
    Cat c;
    makeSpeak(d); // 输出:Woof!
    makeSpeak(c); // 输出:Meow!
}

注意:

  1. 必须用指针或引用才会发生多态
Animal a;
Dog d;
a = d; // 对象赋值不会保留子类信息!
a.speak(); // wrong! 调用的是 Animal::speak
Animal* p = new Dog();
p->speak();  // right! 输出 Dog 的 speak()
Animal& x=d; // 使用引用
Animal* y=&d;
makeSpeak(x);
makeSpeak(*y);
  1. 若使用基类指针或引用,析构函数应该定义为虚函数(防止内存泄漏)
class Base
{
public:
    virtual ~Base() { cout << "Base destroyed\n"; }
};
class Derived : public Base
{
public:
    ~Derived() { cout << "Derived destroyed\n"; }
};
int main() {
    Base* b = new Derived();
    delete b;
}

如果析构函数不是虚的,只会调用 Base::~Base(),而 Derived::~Derived() 不会调用,造成资源泄露

纯虚函数和抽象类

纯虚函数是在基类中声明的、没有函数体的虚函数,继承基类的子类必须覆盖它,否则不能定义对象

带有纯虚函数的类称为抽象类,不能定义抽象类的对象

派生类可根据自身需要实现的功能,分别覆盖它,从而实现真正意义上的多态性

格式:

 virtual void showInfo( ) = 0;

基类指针指向子类对象

基类指针可以指向其子类的对象

基类指针指向子类对象,访问非虚函数时,调用的是基类的成员函数

基类指针指向子类对象,访问虚函数时,调用的是子类的成员函数

能用基类指针指向派生类,不能用子类指针指向基类(赋值兼容),引用同理(本质就是指针)

Derived f;
Base* q = &f; // right!
Base s;
Derived* p = &s; // wrong!
// p 期望指向对象里有子类的所有成员,但是基类成员数量小于等于子类成员数量

多重继承

继承链

graph LR A(class C)-->B(class B)-->C(class A)
class A
{
public:
    void funcA() { cout << "A\n"; }
};
class B : public A
{
public:
    void funcB() { cout << "B\n"; }
};
class C : public B
{
public:
    void funcC() { cout << "C\n"; }
};
int main()
{
    C classc;
    classc.funcB();
    classc.funcA();
}

多继承

**一个子类派生类同时继承多个父类 **

构造函数调用顺序:按照它们在子类类声明第一行中的顺序调用

graph LR A(class C)-->B(class A) A-->C(class B)
class A
{
public:
    void funcA() { cout << "A\n"; }
};
class B
{
public:
    void funcB() { cout << "B\n"; }
};
class C : public A, public B
{  // 多继承
public:
    void funcC() { cout << "C\n"; }
};
posted @ 2025-04-22 18:32  YamadaRyou  阅读(29)  评论(0)    收藏  举报