Appearance
继承
继承就是允许一个类(派生类或子类)使用另一个类(基类或父类)的成员变量和成员函数。
举个例子写一个父类动物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自己能访问protected:Base和它的派生类能访问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()→ 虚函数 → 看实际对象 →Dogshow()→ 非虚函数 → 看指针类型 →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 会遇到的坑: 假如你手滑写错一个字母:新建了一个函数