Skip to content

封装、继承和多态

1.封装

数据封装(Data Encapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。

TIP

封装也就是将一个类中的某些数据设置为private或者protected,使得在类外不能访问这些数据,然后通过设置public权限setget函数实现在类外的数据访问与修改

1.1 访问器和修改器

为了访问类内的私有数据,我们可以在类内定义相应的访问函数,格式为getXXX()的函数被称为访问器,格式为setXXX()的函数被称为修改器,函数的命名一般为set、get后加上私有数据的大驼峰

  • set()函数为修改数据函数
  • get()为访问数据函数
c++
#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不可能为几千岁或者是一个复数,所以我们可以在赋值的时候加以限制

c++
void setAge(int age){
    if(age < 0 || age > 120){
        cout <<"别扯淡了"<<endl; 
        return;
    }
     this->age = age;
}

1.2 数据封装的优点

  • 数据隐藏: 通过将数据成员声明为私有,防止外部代码直接访问这些数据
  • 提高代码可维护性: 提供公共方法来访问和修改数据,这使得可以在不影响外部代码的情况下修改类的内部实现。
  • 增强安全性: 防止不合法的数据输入和不当的修改操作。
  • 实现抽象: 提供了一种机制,使得用户不需要了解类的内部实现细节,只需要了解如何使用类的公共接口即可。

2.继承

一个类可以继承另一个类,比如猫狗可以继承动物类,并且自动继承动物类的一些属性行为

  • 继承的类称为子类,或者派生类
  • 被继承的类称为父类,或者基类

2.1 语法

修饰符可以是public、protectedprivate,若不指定修饰符,则默认为private

c++
class 子类 :修饰符 父类{
    // ...
}

例如

c++
// 基类
class Animal {
public:
    // eat() 函数
    // sleep() 函数
};


//派生类
class Dog : public Animal {
    // bark() 函数		
};

2.2 数据的权限控制

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private

访问位置publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono

注意

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

2.2 继承类型

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。若不指定修饰符,则默认为private

我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

2.3 单继承和多继承

单继承一个类可以被多个类继承,比如猫狗鸡鸭鹅都继承动物类,但这只是单继承

多继承一个类可以继承多个类:比如人可以继承妈妈和爸爸的基因

2.3.1 单继承

c++
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 多继承

不同的父类用逗号,隔开

c++
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 多继承的二义性

如果多个父类中存在相同名字的成员,子类在继承到之后,无法确定到底该访问哪一个

c++
#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;
}

此时若需要调用指定父类的成员,需要显示调用

c++
#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 菱形继承

当两个类继承了同一个类,然后又有一个类继承者这两个类,就形参了菱形继承,又叫做钻石继承

image-20240723150415033

菱形继承导致了一个问题:B、C从A中继承的属性,同事继承到了D中,这样对于D来说,两个父类就有了相同的数据,就形参了多继承的二义性

此时需要显示访问不同父类中相同名称的数据

c++
#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关键字写在继承修饰符的后面

c++
#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 隐式调用

子类创建对象时,需要先默认调用父类无参构造函数,来初始化从父类继承到的东西

c++
#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;
}

image-20240723111732728

若父类没有无参构造函数,子类则会受影响

image-20240723111957427

此时我们有两个选择:

  • 给父类添加无参构造函数
  • 在子类构造函数中,显式调用父类构造函数

2.4.2 显示调用

c++
#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;
}

多继承中也可以调用多个父类的构造函数

c++
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 继承中的析构函数

子类对象在销毁的时候,先调用自已的析构函数再调用父类的析构函数

c++
#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;
}

image-20240723142122595

2.6 父子同名成员

当子类和父类中定义了相同的成员时,子类会将从父类继承到的成员隐藏起来,子类对象直接访问,访问的是子类中的成员。

c++
#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;
}

如果想要访问父类中继承下来的成员,需要显式指定。

c++
#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 对象转型

对象转型是多态的前提,父类的引用/指针指向子类对象

C++
#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关键字,这样我们在多态中就可以使用子类本身的函数

c++
#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,表示在基类中声明但没有实现的虚函数。
  • 它表示基类不提供函数的实现,而是由派生类来实现该函数,从而实现多态性和动态绑定

抽象类:

  • 如果一个类中包含了纯虚函数,这个类就是抽象类,抽象类不能创建对象
  • 如果类继承自一个抽象类,此时这个类中必须重写实现父类中所有的纯虚函数。否则这个类也是一个抽象类,无法创建对象。
c++
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++中没有接口的概念,但是可以通过纯虚函数实现接口。

接口类中只有函数原型定义,没有任何数据定义。

多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。注意:除了析构函数外,其他声明都是纯虚函数。

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 虚析构函数

当父类的引用指向子类时,子类的析构函数并不会被调用,从而会造成内存泄漏

c++
#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;
}

// 只打印了   父类析构函数被调用

我们可以把使用虚析构函数

c++
#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;
}


// 子类析构函数被调用
// 父类析构函数被调用

MIT Licensed