day36-c++多态

day36-c++多态

一、多态概念

  C++多态是一种程序设计思想,它允许使用一个统一的接口来操作不同的对象。多态的好处是可以提高代码的可扩展性和复用性,减少重复的代码和冗余的判断。C++中实现多态的方式有两种:静态多态和动态多态。

  静态多态是在编译时就确定了函数的调用,它主要依靠函数重载和运算符重载来实现。函数重载是指在同一作用域中,可以有多个同名但参数不同的函数,编译器根据函数的参数类型和个数来选择合适的函数。运算符重载是指可以为自定义的类型重新定义运算符的含义,使得运算符可以用于不同的对象。

  动态多态是在运行时才确定了函数的调用,它主要依靠虚函数和继承来实现。虚函数是在基类中使用关键字virtual声明的函数,在派生类中可以重新定义该函数的行为。继承是指一个类可以从另一个类继承其成员变量和成员函数,形成类之间的层次关系。当基类的指针或引用指向派生类的对象时,通过该指针或引用调用虚函数,会根据对象的实际类型来执行相应的函数,这就是动态多态。




二、虚继承

  C++虚继承是一种特殊的继承方式,它可以解决多重继承中的菱形继承问题。菱形继承是指一个类继承了两个或多个具有相同基类的类,导致最终的派生类拥有多份基类的数据成员和成员函数。这样会造成数据冗余和二义性,影响程序的正确性和效率。

  为了避免菱形继承的问题,C++引入了虚继承的概念。虚继承是指在继承关系中,使用virtual关键字修饰某个或某些基类,使得这些基类在派生类中只存在一份拷贝。这样,最终的派生类就不会出现重复的基类成员,也不会有二义性的调用。

  虚继承的实现原理是通过虚基表和虚基指针来实现的。虚基表是一个存储了虚基类地址的表,每个虚基类都有一个虚基表。虚基指针是一个指向虚基表的指针,每个含有虚基类的派生类都有一个或多个虚基指针。当派生类对象访问虚基类成员时,就会通过虚基指针和虚基表找到对应的虚基类地址,然后再访问其成员。

下面是一个简单的例子,演示了类的虚继承的用法和效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
using namespace std;

// 基类A
class A {
public:
int a;
A(int x) : a(x) {}
void show() {
cout << "a = " << a << endl;
}
};

// 派生类B,虚继承A
class B : virtual public A {
public:
int b;
B(int x, int y) : A(x), b(y) {}
void show() {
cout << "b = " << b << endl;
}
};

// 派生类C,虚继承A
class C : virtual public A {
public:
int c;
C(int x, int y) : A(x), c(y) {}
void show() {
cout << "c = " << c << endl;
}
};

// 派生类D,继承B和C
class D : public B, public C {
public:
int d;
D(int x, int y, int z, int w) : A(x), B(x, y), C(x, z), d(w) {}
void show() {
cout << "d = " << d << endl;
}
};

int main() {
D obj(1, 2, 3, 4); // 创建D对象
obj.show(); // 调用D的show函数
obj.B::show(); // 调用B的show函数
obj.C::show(); // 调用C的show函数
obj.A::show(); // 调用A的show函数
return 0;
}

输出结果:

1
2
3
4
d = 4
b = 2
c = 3
a = 1

可以看到,D对象只有一份A对象的拷贝,而不是两份。这就是虚继承的作用。




三、虚函数

  c++中,多态是通过虚函数来实现的。虚函数是在基类中用virtual关键字声明的成员函数,它可以在派生类中被重写(override)或重载(overload)。

  当我们用一个基类指针或引用指向一个派生类对象时(向上隐式转换,即派生类转换为父类,舍弃虚函数以外的派生类独有成员),如果调用的是虚函数,那么会根据指针或引用所指向的对象的实际类型来决定调用哪个版本的虚函数,这就是多态的表现形式。**也就是说,接口只是一个形式,只有在运行时当把调用了接口的派生类给基类指针或者引用时,接口才有具体的形式。**

例如,假设有一个基类Animal和两个派生类Cat和Dog,它们都有一个虚函数speak(),分别输出“喵喵”和“汪汪”。如果我们定义了一个Animal指针p,并让它分别指向一个Cat对象和一个Dog对象,那么当我们调用p->speak()时,就会根据p所指向的对象的类型来输出不同的声音:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 基类
class Animal {
public:
virtual void speak() { // 虚函数
cout << "Animal speaks" << endl;
}
};

// 派生类
class Cat : public Animal {
public:
virtual void speak() { // 重写虚函数
cout << "Meow" << endl;
}
};

// 派生类
class Dog : public Animal {
public:
virtual void speak() { // 重写虚函数
cout << "Woof" << endl;
}
};

int main() {
Animal* p = nullptr; // 基类指针
Cat c; // Cat对象
Dog d; // Dog对象
p = &c; // 指向Cat对象
p->speak(); // 调用Cat的speak(),输出Meow
p = &d; // 指向Dog对象
p->speak(); // 调用Dog的speak(),输出Woof
return 0;
}

虚函数的限制:

  • 非类的成员函数不能定义为虚函数
  • 虚函数不能是静态函数。静态函数属于类,而不属于对象,它不受对象类型的影响,因此不能被重写。
  • 虚函数不能是构造函数。构造函数用于初始化对象,它在创建对象时就确定了对象的类型,因此不能被重写。但析构函数可以定义为虚函数。
  • 虚函数不能是内联函数。内联函数在编译时就被替换为函数体,它不会产生函数调用的开销,但也失去了多态的特性,因此不能被重写。
  • 虚函数不能是友元函数。友元函数不属于类的成员,它可以访问类的私有和保护成员,但它不受对象类型的影响,因此不能被重写。

析构函数一般要写成虚函数,不写的话当基类指针或引用指向派生类,在销毁时只会销毁基类而不会销毁派生类。当析构函数加上虚函数后销毁时就会都被销毁。




四、覆盖、重载和隐藏

  1. 覆盖

覆盖(override):指子类重新实现了父类中的虚函数,函数名、参数列表、返回类型必须与父类中的虚函数完全相同,覆盖后,在使用基类指针或引用调用虚函数时,将调用子类的实现。

覆盖是为了实现多态,让派生类可以根据自己的特性来修改基类中的虚函数行为,提高代码的灵活性和扩展性。

覆盖(override)的特征:

  • 只能用于虚函数
  • 函数名、参数列表、返回类型必须与父类中的虚函数完全相同
  • 在使用基类指针或引用调用虚函数时,将调用子类的实现
  • 用于实现运行时多态性
  1. 重载

重载(overload):指在同一个作用域内,函数名相同,但是参数列表不同的函数。在使用函数时,根据参数的类型、数量或顺序来选择正确的函数。重载不要求函数是虚函数。

重载是为了让同一个函数名可以根据不同的参数类型或个数来执行不同的操作,提高代码的简洁性和可读性。

重载(overload)的特征:

  • 函数名相同,但是参数列表不同的函数
  • 在同一个作用域内定义
  • 参数的类型、数量或顺序不同
  • 返回类型可以相同也可以不同
  • 不要求函数是虚函数
  • 根据参数的类型、数量或顺序来选择正确的函数
  • 编译器在编译时根据函数的参数类型来确定函数调用
  1. 隐藏

隐藏(hide):指子类中的成员函数和父类中的成员函数同名,但参数列表不同,或者不是虚函数。在使用子类对象调用该函数时,将调用子类中的函数而不是父类中的函数。父类的同名函数被隐藏了,但不是被覆盖。

隐藏是为了避免派生类和基类中的同名函数发生混淆或冲突,提高代码的安全性和稳定性。

隐藏(hide)的特征:

  • 子类中的成员函数和父类中的成员函数同名,但参数列表不同,或者不是虚函数
  • 在使用子类对象调用该函数时,将调用子类中的函数而不是父类中的函数
  • 父类的同名函数被隐藏了,但不是被覆盖
  • 在编译时根据调用的对象类型来确定函数调用
  1. 之间的区别
  • 定义方式不同:覆盖是在子类中重新实现父类的虚函数,重载是在同一个作用域中定义多个同名函数,但是参数列表不同,隐藏是在子类中定义与父类同名但参数列表不同的非虚函数。
  • 参数列表不同:覆盖要求完全相同,重载要求函数名相同且参数列表不同,而隐藏则要求函数名相同但参数列表不同。
  • 是否是虚函数:覆盖只能用于虚函数,重载不要求函数是虚函数,而隐藏是在子类中定义非虚函数。
  • 实现机制不同:覆盖是在编译时动态绑定的,而重载和隐藏是在编译时静态绑定的。
  • 调用方式不同:覆盖是在使用基类指针或引用调用虚函数时,将调用子类的实现,而重载和隐藏是在编译时根据调用的对象类型来确定函数调用。

当然,这三种特性也有一些注意事项和限制:

  • 重载时要避免歧义和二义性,即不能让编译器无法判断应该调用哪个重载函数。
  • 覆盖时要遵循里氏替换原则,即不能让派生类中的覆盖函数违反基类中虚函数的契约或预期。
  • 隐藏时要注意作用域和指针类型,即不能让基类指针或引用调用到派生类中隐藏的函数或反之。



五、抽象类

C++ 抽象类是一种不能实例化的类,它包含了一个或多个纯虚函数,用来表示一种通用的概念或接口。纯虚函数是没有实现的虚函数,它们必须在派生类中被重写,否则派生类也不能实例化。抽象类的作用是为其他类提供一个可以继承的基类,从而实现多态性和代码复用。不能创建抽象类的对象。

纯虚函数的定义如下:

1
virtual <返回值> <函数名>() = 0;//有纯虚数的类就是抽象类

下面是一个抽象类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 定义一个抽象类 Shape
class Shape {
public:
// 纯虚函数,用 = 0 表示
virtual double getArea() = 0;
// 普通成员函数
void setWidth(int w) {
width = w;
}
void setHeight(int h) {
height = h;
}
protected:
int width;
int height;
};

// 定义一个派生类 Rectangle,继承自 Shape
class Rectangle: public Shape {
public:
// 重写纯虚函数
double getArea() {
return width * height;
}
};

// 定义一个派生类 Triangle,继承自 Shape
class Triangle: public Shape {
public:
// 重写纯虚函数
double getArea() {
return width * height / 2;
}
};

// 在主函数中使用抽象类和派生类
int main() {
// 不能创建抽象类的对象
// Shape s; // 编译错误
// 可以创建指向抽象类的指针或引用
Shape* p;
// 可以创建派生类的对象
Rectangle r;
Triangle t;
// 可以通过指针或引用调用纯虚函数
p = &r; // 指向 Rectangle 对象
cout << "Rectangle area: " << p->getArea() << endl; // 多态调用
p = &t; // 指向 Triangle 对象
cout << "Triangle area: " << p->getArea() << endl; // 多态调用
return 0;
}



六、限制构造

c++限制构造是一种编程技术,用于防止类的对象被随意创建或复制。c++限制构造的目的是保证类的封装性和一致性,以及避免资源的浪费和泄露。c++限制构造的方法有以下几种:

  • 将构造函数或拷贝构造函数声明为私有或受保护的,这样只有类的成员函数或友元函数才能访问它们。
  • 将构造函数或拷贝构造函数声明为删除的,这样编译器会报错,如果试图调用它们。
  • 将构造函数或拷贝构造函数声明为纯虚的,这样类就变成了抽象类,不能创建对象。
  • 将构造函数或拷贝构造函数定义为内联的,并在类内部抛出异常,这样运行时会发生错误,如果试图调用它们。
Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2020-2024 nakano-mahiro
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信