Skip to content

继承

继承就是允许一个类(派生类或子类)使用另一个类(基类或父类)的成员变量和成员函数。

举个例子写一个父类动物Animal,再写一个子类狗Dog继承Animal,继承的基本语法格式为

class 派生类:继承方式 基类

  • 构造函数和析构函数不被子类继承
  • 子类自动接收父类中除了构造函数和析构函数外,所有的非静态成员

继承方式

  • 子类对所继承成员的访问权限由继承方式决定
  • 如果不写继承方式,默认是私有private继承
子类/父类private成员protected成员public成员
private继承子类不能访问子类可以访问
变成子类的私有成员
子类可以访问
变成子类的私有成员
protected继承子类不能访问子类可以访问
外部访问权限不变
子类可以访问
变成子类的私有成员
public继承子类不能访问子类可以访问
外部访问权限不变
子类可以访问
外部访问权限不变

子类的构造函数

  • 子类的构造函数需要完成父类的成员和子类新增成员的初始化

子类构造函数的基本格式

子类名(参数列表):父类名(参数列表){其他初始化操作}

cpp
#include <iostream>
using namespace std;
class Animal{
	protected:
		string name;  // 被子类继承
	public:
		Animal(string name):name(name){
			cout<<"Animal("<<name<<")"<<endl;
		}
		void show(){ // 被子类继承
			cout<<name<<endl;
		}
};
class Dog:public Animal{
	public:
		Dog(string name):Animal(name){ // 调用父类的构造函数
			cout<<"Dog("<<name<<")"<<endl;
		}
};
int main() {
	Dog a=Dog("lala");
	a.show();
	return 0;
}

子类对象创建的过程

  • 首先创建父类对象,调用父类的构造函数对成员变量进行初始化
  • 然后创建子类对象,调用子类的构造函数对子类成员变量进行初始化
  • 子类对象中会包裹一个父类对象
[ 子类对象 ] 
	└── [ 父类对象 ] 
			├── name 
			└── show() ← 箭头指向

对象的销毁过程

  • 在子类和父类的析构函数中增加一行输出,观察销毁过程
  • 先销毁子类对象,再销毁父类对象
cpp
#include <iostream>
using namespace std;
class Animal{
	protected:
		string name;  // 被子类继承
	public:
		Animal(string name):name(name){
			cout<<"Animal("<<name<<")"<<endl;
		}
		void show(){ // 被子类继承
			cout<<name<<endl;
		}
		~Animal(){
			cout<<"~Animal"<<endl;
		}
};
class Dog:public Animal{
	public:
		Dog(string name):Animal(name){ // 调用父类的构造函数
			cout<<"Dog("<<name<<")"<<endl;
		}
		~Dog(){
			cout<<"~Dog"<<endl;
		}
};

添加和修改成员

  • 子类中可以增加新的成员变量和成员函数
  • 重新定义和父类相同的成员函数(同名同参数),覆盖父类中的函数。慎用
cpp
#include <iostream>
using namespace std;
class Animal {
	protected:
		string name;  // 被子类继承
	public:
		Animal(string name):name(name) {
			cout<<"Animal("<<name<<")"<<endl;
		}
		void show() { // 被子类继承
			cout<<name<<endl;
		}
};
class Dog:public Animal {
		int age; // 增加成员变量
	public:
		Dog(string name):Animal(name) {}
		Dog(int age, string name):Animal(name),age(age) {}
		void look() { // 增加成员函数
			cout<<"look after house"<<endl;
		}
		void show() { // 重新定义成员函数
			cout<<name<<":"<<age<<endl;
		}
};
int main() {
	Dog a=Dog(5, "lala");
	a.show();
	a.look();
	return 0;
}

单继承和多继承

  • 子类只有一个父类称为单继承
  • C++支持多继承,即一个子类可以有多个父类
# 单继承 
		父类 
	/    |    \ 
子类1   子类2 子类3 

# 多继承
 父类1 父类2 父类3 
   \     |    / 
    \    |   / 
	    子类

多继承的语法格式

class 派生类 : 继承方式 基类1, 继承方式 基类2......

cpp
class A{
};

class B{
    ......
};

class C:public A,public B{ //  ← 类C继承自类A和类B
};

真题

(2024年3月)继承是将已有类的属性和方法引入新类的过程。

答案:正确

(样题)如果一个对象具有另一个对象的性质,那么它们之间就是继承关系

答案:错误

(2024年9月) 如下列代码所示的基类 (base) 及其派生类 (derived),则生成一个派生类的对象时,只调用派生类的构造函数

cpp
#include <iostream>
using namespace std;
class base {
	public:
		base() {
			cout << "base constructor" << endl;
		}
		~base() {
			cout << "base destructor" << endl;
		}
};
class derived : public base {
	public:
		derived() {
			cout << "derived constructor" << endl;
		}
		~derived() {
			cout << "derived destructor" << endl;
		}
};

答案:错误

(样例)下面关于C++类的说法中,正确的是( )

A. 派生类不能和基类有同名成员函数,因为会产生歧义。 B. 派生类可以和基类有同名成员函数,派生类覆盖基类的同名成员函数。 C. 派生类可以和基类有同名成员函数,但是否覆盖同名成员函数需要取决于函数参数是否一致。 D. C++中派生类不继承基类的任何成员函数。

答案:C

cpp
class Base {
	public:
		void fun() {
			cout<<"Base";
		}
};

class Derived:public Base {
	public:
		void fun() {
			cout<<"Derived";
		}
};

这里子类和父类:

  • 函数名相同:fun
  • 参数列表相同:()

那么子类的 fun()覆盖(重写/隐藏父类同名函数)

调用:

cpp
Derived d;
d.fun();

输出:Derived

所以 B 的前半句没问题,但它说:

派生类覆盖基类的同名成员函数

再看第二种:

cpp
class Base{
public:
    void fun(int x){
        cout<<"Base";
    }
};

class Derived:public Base{
public:
    void fun(){
        cout<<"Derived";
    }
};

此时:

  • 名字相同:fun
  • 参数不同:() 和 (int)

很多人会以为这是重载,其实不是普通重载

在继承中:

cpp
Derived d;
d.fun(5);

会报错。

因为子类中的 fun() 把父类所有同名 fun 都隐藏了。

也就是说:Base::fun(int)被隐藏了。想用必须写:d.Base::fun(5);

或者:

cpp
class Derived:public Base{
public:
    using Base::fun; // 引入父类fun
    void fun(){
        cout<<"Derived";
    }
};

这样才会形成重载:

cpp
d.fun();      // Derived
d.fun(5);     // Base

同名同参叫覆盖;同名不同参先隐藏;using再重载。

(2024年9月)关于以下C++代码,( )行代码会引起编译错误。

cpp
#include <iostream>
using namespace std;
class Base {
private:
    int a;
protected:
    int b;
public:
    int c;
    Base() : a(1), b(2), c(3) {}
};
class Derived : public Base {
public:
    void show() {
        cout << a << endl; //Line 1
        cout << b << endl; //Line 2
        cout << c << endl; //Line 3
    };
};

A. Line 1 B. Line 2 C. Line 3 D. 没有编译错误

答案:A

三个访问权限的区别是:

  • private:只有 Base 自己能访问
  • protectedBase 和它的派生类能访问
  • public:任何地方都能访问

(样题)关于下面C++代码说法错误的是( )

cpp
#include <iostream>
using namespace std;
class Pet {
public:
	string kind;
	int age;
	Pet(string_kind,int_age):kind(kind),age(age){}
};
class Dog:public Pet{
public:
	string color;
	Dog(string_kind,int_age,string_color):Pet(kind,age),color(color){}
};
int main(){
	auto dog=Dog("dog",3,"white");
	cout<<"kind: "<<dog.kind<<endl;
	cout<<"age: "<<dog.age<<endl;
	cout<<"color: "<<dog.color<<endl;//输出行A
	return 0;
}

A. Pet 类是基类,Dog 类是子类。
B. Dog类的构造函数中,将自动调用Pet 类的构造函数。
C. dog是Dog类的实例。
D. 最后一行(即输出行A)输出代码会 报错,因为Pet类中没有成员变量 color

答案:D

多态

多态

  • 多态,顾名思义就是多种形态
  • 多态的前提是继承,多态在继承的基础实现;
  • 通过父类指针指向子类对象实现

虚函数

  • 使用关键字virtual声明成员函数,该函数就称为虚函数
  • 虚函数可以被子类重写override
cpp
class Animal{
	protected:
		string name;
	public:
		Animal(string n){
			name=n;
		}
		virtual void eat(){  // 虚函数
			cout<<"Animal eat"<<endl;
		}
		void show(){  // 普通成员函数
			cout<<name<<endl;
		}
		
};

virtual 表示这是一个虚函数。 意思是:

将来如果子类写了同名同参数函数,可以在运行时决定调用谁。

然后子类:void eat() override ,这里 override 表示:

我明确告诉编译器:我要重写父类的虚函数。

如果名字或者参数写错:void eat(int x) override 编译器会直接报错:error: marked override but does not override 所以 override 相当于防止手滑。

cpp
#include<bits/stdc++.h>
using namespace std;
class Animal{
	protected:
		string name;
	public:
		Animal(string n){
			name=n;
		}
		virtual void eat(){  // 虚函数
			cout<<"Animal eat"<<endl;
		}
		void show(){  // 普通成员函数
			cout<<name<<endl;
		}
		
};
class Dog:public Animal{
	private:
		int age;
	public:
		Dog(string n, int a): Animal(n){
			age=a;
		}
		void eat() override{  // 重写父类虚函数
			cout<<"Dog eat meat"<<endl;
		}
};
int main(){
	Dog b("lele", 5);
	b.eat();
	b.show();
	Animal *p=&b;
	p->eat();
	p->show();
	return 0;
}

下面:

cpp
Dog b("lele",5);

b.eat();

对象本身调用:b.eat() , 当然直接找 Dog:输出:Dog eat meat

然后:b.show(); show() 没有被重写,所以沿着继承找到父类:lele

重点来了:Animal *p=&b; 这句非常重要: 不是:Animal p=b; 而是:Animal *p=&b;意思:

父类指针

Animal *p

指向

Dog对象

内存大概像这样:

b对象(Dog)

┌─────────┐
│ name    │
│ age     │
│ eat()   │
└─────────┘


Animal *p

然后:p->eat();因为 eat() 是虚函数。

编译器会:

看 p 实际指向谁。

发现:p 指向 Dog对象

所以调用:Dog::eat()

输出:Dog eat meat

这就叫:动态绑定(运行时多态)

再看:p->show(); show() 不是虚函数。

非虚函数遵循:看指针类型,而不是看对象类型。

p 类型是:Animal* 所以调用:Animal::show()

输出:lele,虽然结果一样,但原理不同。

cpp
class Animal{
public:
    virtual void eat(){
        cout<<"Animal"<<endl;
    }

    void show(){
        cout<<"show"<<endl;
    }
};

class Dog:public Animal{
public:
    void eat(){
        cout<<"Dog"<<endl;
    }

    void show(){
        cout<<"DogShow"<<endl;
    }
};

int main(){

    Dog d;
    Animal *p=&d;

    p->eat();
    p->show();
}

输出: Dog show

原因:

  • eat() → 虚函数 → 看实际对象 → Dog
  • show() → 非虚函数 → 看指针类型 → Animal

口诀: 虚函数看对象,普通函数看指针。

纯虚函数和抽象类

  • 没有实现的虚函数称为纯虚函数,声明的基本格式如下: virtual 返回值类型 函数名(参数列表)=0;
  • 含有纯虚函数(一个或多个)的类称为抽象类
  • 抽象类不能用于创建实例对象
cpp
#include<bits/stdc++.h>
using namespace std;
class Animal{
	protected:
		string name;
	public:
		// 虚函数
		virtual void eat(){
			cout<<"Animal eat"<<endl;
		}
		// 纯虚函数
		virtual void sleep()=0;
};
int main(){
	Animal an = Animal("coco");
	return 0;
}

纯虚函数可以理解成: 父类只规定“必须有这个功能”,但不知道具体怎么实现,所以把实现任务交给子类。

例如动物都会吃饭,但不同动物吃法不同:

cpp
class Animal{
	public:
		virtual void eat()=0; // 纯虚函数
};

这里的意思不是:Animal::eat() 有代码

而是:Animal::eat() 根本没有实现。

纯虚函数的目的是告诉所有子类:你们必须有一个 eat() 函数。

cpp
class Dog: public Animal{
	public:
		void eat() override{
			cout<<"Dog eat meat"<<endl;
		}
};
cpp
class Cat: public Animal{
	public:
		void eat() override{
			cout<<"Cat eat fish"<<endl;
		}
};

如果子类不实现,那么:Dog d; 也会报错。

因为 Dog 仍然包含未实现的纯虚函数。

此时 Dog 也是抽象类。

为什么抽象类不能创建对象?

假设允许:

cpp
Animal a;
a.eat();

eat() 根本没有实现。

编译器不知道该执行什么代码。

所以 C++ 直接规定:

只要类里有纯虚函数,这个类就不能实例化。

注意辨别重写(Override)和重载(Overload)

  • 所有继承自抽象类的子类都要实现父类中的纯虚函数
  • 抽象类相当于提供一种功能框架,具体的实现交给子类
cpp
#include<bits/stdc++.h>
using namespace std;
class Animal {
	protected:
		string name;
	public:
		Animal(string name):name(name) {}
		// 虚函数
		virtual void eat() {
			cout<<"Animal eat"<<endl;
		}
		// 纯虚函数
		virtual void sleep()=0;
};
class Dog:public Animal {
		int age=0;
	public:
		Dog(int age,string name)
			:Animal(name),age(age) {}
		void eat() {
			cout<<"Dog eat meat"<<endl;
		}
		void sleep() {
			cout<<"Dog sleep"<<endl;
		}
};

class Cat: public Animal {
	public:
		Cat(string name):Animal(name) {}
		void eat();
		void sleep();
};
void Cat::eat() {
	cout<<"Cat:eat fish"<<endl;
}
// 实现父类的纯虚函数
void Cat::sleep() {
	cout<<"Cat sleep"<<endl;
}

int main() {
	Dog b(5, "lele");
	b.eat();
	b.sleep();
	Cat c=Cat("mimi");
	c.eat();
	c.sleep();
	return 0;
}
cpp
	//抽象类指针的多态 
	Animal* p = new Dog(5,"lele");
	p->eat();
	p->sleep();

为什么非要父类指针指向子类对象?这和普通调用有什么区别?

Animal* p; 此时编译器只知道:p 是 Animal* 它不知道将来指向谁。

可能是:p = new Dog(); 也可能是:p = new Cat(); 甚至:p = new Pig();

如果没有虚函数:

cpp
class Animal{
public:
    void eat(){
        cout<<"Animal eat"<<endl;
    }
};

Animal* p = new Dog();
p->eat();

编译器只看:p 的类型是 Animal* 于是调用:Animal::eat() 输出:Animal eat Dog 的 eat 根本不会执行。

如果加上虚函数:

cpp
class Animal{
public:
    virtual void eat(){
        cout<<"Animal eat"<<endl;
    }
};

这时候:

cpp
Animal* p = new Dog();
p->eat();

执行时会发生:

p 是 Animal*

检查实际对象是谁

发现是 Dog

调用 Dog::eat()

输出:Dog eat meat 这就是多态。

Animal* p 就像一个遥控器。它只知道:我是动物遥控器
但不知道控制的是: 狗?猫?猪?

直到运行时才发现。

cpp
Animal* p;

p = new Dog();
p->eat();

p = new Cat();
p->eat();

同一句:p->eat();

结果却不同:

cpp
Dog eat meat
Cat eat fish

这就是:

同一个接口(eat),表现出不同的行为。

这就是"多态(Polymorphism)"这个词的本意:

Poly = 多
Morph = 形态

即:

同一个调用
产生多种形态

实际开发喜欢这么干的原因,假设有:

cpp
Dog
Cat
Pig
Tiger
Lion

你想让所有动物吃饭。

不用多态:

cpp
dog.eat();
cat.eat();
pig.eat();
tiger.eat();
lion.eat();

每增加一种动物都得改代码。


有多态:

cpp
vector<Animal*> animals;

里面放:

cpp
animals.push_back(new Dog());
animals.push_back(new Cat());
animals.push_back(new Pig());

然后:

cpp
for(auto p:animals){
    p->eat();
}

同一句:

cpp
p->eat();

自动调用对应子类。

输出:

cpp
Dog eat meat
Cat eat fish
Pig eat everything

程序根本不需要知道具体是什么动物。

继承解决:公共代码复用

虚函数解决:运行时决定调用哪个函数

多态体现为:

cpp
Animal* p = new Dog();
p->eat();

或者

cpp
Animal& r = dog;
r.eat();

即:

父类指针(或引用)指向子类对象,通过虚函数调用时,执行的是子类版本。

核心规则是:

父类指针只能指向父类对象或者其派生类对象。

真题

第4题 运行以下C++代码,屏幕将输出“derived class”。

cpp
 1  #include <iostream>
 2  using namespace std;
 3  
 4  class base {
 5  public:
 6      virtual void show() {
 7          cout << "base class" << endl;
 8      }
 9  };
10  
11  class derived : public base {
12  public:
13      void show() override {
14          cout << "derived class" << endl;
15      }
16  };
17  
18  int main() {
19      base* b;
20      derived d;
21      b = &d;
22  
23      b->show();
24      return 0;
25  }

答案:正确

这就是C++ 多态

基类指针指向派生类对象,调用虚函数时,会执行派生类重写后的函数,而非基类函数。

2406第3题 运行下列代码,屏幕上输出( )。

A. rectangle area: triangle area: B. parent class area: parent class area: C. 运行时报错 D. 编译时报错

cpp
1  #include <iostream>
2  using namespace std;
3  
4  
5  class shape {
6      protected:
7          int width, height;
8      public:
9          shape(int a = 0, int b = 0) {
10             width = a;
11             height = b;
12         }
13         virtual int area() {
14             cout << "parent class area: " <<endl;
15             return 0;
16         }
17  };
18  
19  class rectangle: public shape {
20  public:
21      rectangle(int a = 0, int b = 0) : shape(a, b) {}
22  
23      int area() {
24          cout << "rectangle area: ";
25          return (width * height);
26      }
27  };
28  
29  class triangle: public shape {
30  public:
31      triangle(int a = 0, int b = 0) : shape(a, b) {}
32  
33      int area() {
34          cout << "triangle area: ";
35          return (width * height / 2);
36      }
37  };
38  
39  int main() {
40      shape *pshape;
41      rectangle rec(10, 7);
42      triangle tri(10, 5);
43  
44      pshape = &rec;
45      pshape->area();
46  
47      pshape = &tri;
48      pshape->area();
49      return 0;
50  }

答案: A 隐式重写,编译器会自动识别这是在重写基类的虚函数。

不写 override 会遇到的坑: 假如你手滑写错一个字母:新建了一个函数