封装、继承和多态
1.封装
数据封装(Data Encapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。
TIP
封装也就是将一个类中的某些数据设置为private或者protected,使得在类外不能访问这些数据,然后通过设置public权限set和get函数实现在类外的数据访问与修改
1.1 访问器和修改器
为了访问类内的私有数据,我们可以在类内定义相应的访问函数,格式为getXXX()的函数被称为访问器,格式为setXXX()的函数被称为修改器,函数的命名一般为set、get后加上私有数据的大驼峰
set()函数为修改数据函数get()为访问数据函数
#include <iostream>
using namespace std;
class Woman {
private:
int age;
public:
void setAge(int age){
this->age = age;
}
int getAge() {
return age;
}
};
int main()
{
Woman w;
w.setAge(18);
cout << w.getAge() << endl; // 18
return 0;
}因为以上set函数是我们自己定义的,所以里面的逻辑不止可以写赋值语句,上面的例子age不可能为几千岁或者是一个复数,所以我们可以在赋值的时候加以限制
void setAge(int age){
if(age < 0 || age > 120){
cout <<"别扯淡了"<<endl;
return;
}
this->age = age;
}1.2 数据封装的优点
- 数据隐藏: 通过将数据成员声明为私有,防止外部代码直接访问这些数据。
- 提高代码可维护性: 提供公共方法来访问和修改数据,这使得可以在不影响外部代码的情况下修改类的内部实现。
- 增强安全性: 防止不合法的数据输入和不当的修改操作。
- 实现抽象: 提供了一种机制,使得用户不需要了解类的内部实现细节,只需要了解如何使用类的公共接口即可。
2.继承
一个类可以继承另一个类,比如猫狗可以继承动物类,并且自动继承动物类的一些属性,行为
- 继承的类称为子类,或者派生类
- 被继承的类称为父类,或者基类
2.1 语法
修饰符可以是public、protected 或 private,若不指定修饰符,则默认为private
class 子类 :修饰符 父类{
// ...
}例如
// 基类
class Animal {
public:
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};2.2 数据的权限控制
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
| 访问位置 | public | protected | private |
|---|---|---|---|
| 同一个类 | yes | yes | yes |
| 派生类 | yes | yes | no |
| 外部的类 | yes | no | no |
注意
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
2.2 继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。若不指定修饰符,则默认为private
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
2.3 单继承和多继承
单继承:一个类可以被多个类继承,比如猫狗鸡鸭鹅都继承动物类,但这只是单继承
多继承:一个类可以继承多个类:比如人可以继承妈妈和爸爸的基因
2.3.1 单继承
class Animal {
public :
string face = "两个眼睛一张嘴";
};
class Dog:Animal {
public:
void printFace() {
cout << face << endl;
}
};
class Cat:Animal {
public:
void printFace() {
cout << face << endl;
}
};2.3.2 多继承
不同的父类用逗号,隔开
class Father {
public :
string eye = "双眼皮";
};
class Mother {
public:
string leg = "大长腿";
};
class Child: public Father, public Mother {
public:
void print() {
cout << eye << "," << leg << endl; //双眼皮,大长腿
}
};2.3.3 多继承的二义性
如果多个父类中存在相同名字的成员,子类在继承到之后,无法确定到底该访问哪一个
#include <iostream>
using namespace std;
class Father {
public:
void play() {
cout << "男单打" << endl;
}
};
class Mother {
public:
void play() {
cout << "女单打" << endl;
}
};
class Child : public Father, public Mother {
public:
};
int main()
{
Child c;
c.play(); // 报错,成员不明确
return 0;
}此时若需要调用指定父类的成员,需要显示调用
#include <iostream>
using namespace std;
class Father {
public:
void play() {
cout << "男单打" << endl;
}
};
class Mother {
public:
void play() {
cout << "女单打" << endl;
}
};
class Child : public Father, public Mother {
public:
};
int main()
{
Child c;
c.Father::play(); // "男单打"
c.Mother::play(); // "女单打"
return 0;
}2.3.4 菱形继承
当两个类继承了同一个类,然后又有一个类继承者这两个类,就形参了菱形继承,又叫做钻石继承

菱形继承导致了一个问题:B、C从A中继承的属性,同事继承到了D中,这样对于D来说,两个父类就有了相同的数据,就形参了多继承的二义性
此时需要显示访问不同父类中相同名称的数据
#include <iostream>
using namespace std;
class Animal {
public:
int age = 8;
};
class Horse:public Animal {
public:
string run = "跑步";
};
class Donkey:public Animal {
public:
string work = "拉磨";
};
class Mule :public Horse, public Donkey {
};
int main()
{
Mule m;
cout << m.run << endl; // 跑步
cout << m.work << endl; //拉磨
cout << m.Horse::age << endl; // 8
cout << m.Donkey::age << endl; // 8
return 0;
}2.3.5 虚继承
虚继承为了解决菱形继承的时候命名冲突的问题,使得派生类中只保留一份相同的间接父类中的成员。
通过virtual关键字写在继承修饰符的后面
#include <iostream>
using namespace std;
class Animal {
public:
int age = 8;
};
class Horse:public virtual Animal {
public:
string run = "跑步";
};
class Donkey:public virtual Animal {
public:
string work = "拉磨";
};
class Mule :public Horse, public Donkey {
};
int main()
{
Mule m;
cout << m.run << endl; // 跑步
cout << m.work << endl; //拉磨
cout << m.age << endl; // 8
return 0;
}2.4 继承中的构造函数
c++中支持多继承,所以没有super()函数。
2.4.1 隐式调用
子类创建对象时,需要先默认调用父类的无参构造函数,来初始化从父类继承到的东西
#include <iostream>
using namespace std;
class Father {
public :
Father() {
cout << "父类无参构造函数" << endl;
}
Father (int age){
cout << "父类有参构造函数:" << age << endl;
}
};
class Child :Father {
public:
Child() {
cout << "子类构造函数" << endl;
}
};
int main()
{
Child c;
return 0;
}
若父类没有无参构造函数,子类则会受影响

此时我们有两个选择:
- 给父类添加无参构造函数
- 在子类构造函数中,显式调用父类构造函数
2.4.2 显示调用
#include <iostream>
using namespace std;
class Father {
public :
Father (int age){
cout << "父类有参构造函数:" << age << endl;
}
};
class Child :Father {
public:
Child():Father(10) {
cout << "子类构造函数" << endl;
}
};
int main()
{
Child c;
return 0;
}多继承中也可以调用多个父类的构造函数
class Father {
public:
int age;
Father(int age):age(age) {}
};
class StepFather {
public:
string money;
StepFather(string money):money(money) {}
};
class Child :Father, StepFather {
public:
Child() :Father(10), StepFather("两个亿") {
cout << age << endl; // 10
cout << money << endl; // 两个亿
}
};2.5 继承中的析构函数
子类对象在销毁的时候,先调用自已的析构函数,再调用父类的析构函数
#include <iostream>
using namespace std;
class Father {
public:
Father() {}
~Father() {
cout << "父类析构函数被调用" << endl;
}
};
class Child :Father {
public:
Child() {}
~Child() {
cout << "子类析构函数被调用" << endl;
}
};
int main()
{
Child c;
return 0;
}
2.6 父子同名成员
当子类和父类中定义了相同的成员时,子类会将从父类继承到的成员隐藏起来,子类对象直接访问,访问的是子类中的成员。
#include <iostream>
using namespace std;
class Father {
public:
int age;
void showAge() {
cout <<"父类的age:"<< age << endl;
}
};
class Child :Father {
public:
int age;
void showAge() {
cout << "子类的age:" << age << endl;
}
};
int main()
{
Child c;
c.age = 18;
c.showAge(); // 子类的age:18
return 0;
}如果想要访问父类中继承下来的成员,需要显式指定。
#include <iostream>
using namespace std;
class Father {
public:
int age;
void showAge() {
cout <<"父类的age:"<< age << endl;
}
};
class Child :public Father {
public:
int age;
void showAge() {
cout << "子类的age:" << age << endl;
}
};
int main()
{
Child c;
c.age = 18;
c.showAge(); // 子类的age:18
c.Father::age=40;
c.Father::showAge(); //父类的age:40
return 0;
}3.多态
3.1 对象转型
对象转型是多态的前提,父类的引用/指针指向子类对象
#include <iostream>
using namespace std;
class Animal {
public:
void bark() {
cout << "Animal Bark" << endl;
}
};
class Dog :public Animal{
public:
int age = 10;
void bark() {
cout << "Dog Bark" << endl;
}
};
int main() {
Dog dog;
Animal& animal = dog;
Dog* dog1 = new Dog();
Animal* animal1 = dog1;
cout << animal.age << endl; // 错误,没有该成员
}但是这样访问会出现问题,于是我们需要用到虚函数
3.2 虚函数与函数重写
虚函数 是在基类中使用关键字 virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
函数重写是在子类中与父类相同的函数后面使用override关键字,这样我们在多态中就可以使用子类本身的函数
#include <iostream>
using namespace std;
class Animal {
public:
virtual void bark() {
cout << "Animal Bark" << endl;
}
};
class Dog :public Animal{
public:
int age = 10;
void bark() override{
cout << "Dog Bark" << endl;
}
};
int main() {
Dog dog;
Animal& animal = dog;
Dog* dog1 = new Dog();
Animal* animal1 = dog1;
animal.bark(); // Dog Bark
}3.3 纯虚函数与抽象类
纯虚函数:
- 纯虚函数是一个虚函数,但是我们直接给这个函数赋值为0,表示在基类中声明但没有实现的虚函数。
- 它表示基类不提供函数的实现,而是由派生类来实现该函数,从而实现多态性和动态绑定
抽象类:
- 如果一个类中包含了纯虚函数,这个类就是抽象类,抽象类不能创建对象
- 如果类继承自一个抽象类,此时这个类中必须重写实现父类中所有的纯虚函数。否则这个类也是一个抽象类,无法创建对象。
class TrafficTool {
public:
virtual void run() = 0;
};
class Bike :public TrafficTool {
public:
void run() override{
cout << "自行车双腿蹬" << endl;
}
};
class Bus :public TrafficTool {
public:
void run() override{
cout << "公交车两块钱投币" << endl;
}
void stop(){
cout << "停车" << endl;
}
};3.4 纯虚函数与多继承
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。
绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,C++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。注意:除了析构函数外,其他声明都是纯虚函数。
class Horse {
public:
virtual void eat() = 0;
virtual void run() = 0;
};
class Donkey {
public:
virtual void eat() = 0;
virtual void work() = 0;
};
class Mule :public Horse, public Donkey {
public:
void eat() override {
cout << "吃饭" << endl;
}
void run() override {
cout << "跑步" << endl;
}
void run() override {
cout << "拉磨" << endl;
}
};3.5 虚析构函数
当父类的引用指向子类时,子类的析构函数并不会被调用,从而会造成内存泄漏
#include <iostream>
using namespace std;
class Animal {
public:
~Animal() {
cout << "父类析构函数被调用" << endl;
}
};
class Dog :public Animal{
public:
int* n;
Dog() {
n = new int(10);
}
~Dog() {
cout << "子类析构函数被调用" << endl;
if (n != nullptr) {
delete n;
n = nullptr;
}
}
};
int main() {
Animal* animal = new Dog();
delete animal;
}
// 只打印了 父类析构函数被调用我们可以把使用虚析构函数
#include <iostream>
using namespace std;
class Animal {
public:
virtual ~Animal() {
cout << "父类析构函数被调用" << endl;
}
};
class Dog :public Animal{
public:
int* n;
Dog() {
n = new int(10);
}
~Dog() override{
cout << "子类析构函数被调用" << endl;
if (n != nullptr) {
delete n;
n = nullptr;
}
}
};
int main() {
Animal* animal = new Dog();
delete animal;
}
// 子类析构函数被调用
// 父类析构函数被调用