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;
}
- 类中的
const常量只能在初始化列表中进行初始化,而不能在函数体内用赋值的方式来初始化
class Base
{
const int SIZE ;
Base(int size) : SIZE(size)
{ /*...*/ }
};
Base one(100);
- 引用类型成员(如
int&)必须使用初始化列表,因为引用必须在声明时绑定
class Demo
{
int& ref;
public:
Demo(int& r) : ref(r) { }
};
- 继承中,若基类无默认构造函数,必须手动调用基类构造函数
向基类构造函数传参
如果基类和子类都有缺省的构造函数(无参数),它们的调用是自动完成的,这是一种隐式调用
如果基类的构造函数带有参数,必须让子类的构造函数显式调用基类的构造函数,并向基类构造函数传参
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!
}
注意:
- 必须用指针或引用才会发生多态
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);
- 若使用基类指针或引用,析构函数应该定义为虚函数(防止内存泄漏)
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 期望指向对象里有子类的所有成员,但是基类成员数量小于等于子类成员数量
多重继承
继承链
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();
}
多继承
**一个子类派生类同时继承多个父类 **
构造函数调用顺序:按照它们在子类类声明第一行中的顺序调用
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"; }
};

浙公网安备 33010602011771号