(五)C++的类继承、多态、组合
在C++中,类继承(Class Inheritance)是面向对象编程(OOP)的核心概念之一。它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性(数据成员)和行为(成员函数)。通过继承,可以实现代码的重用、建立类之间的层次结构以及实现多态性。以下是对C++类继承的详细介绍,包括其定义、语法、类型、实现方式、访问控制以及多态性。
一.继承
定义
类继承是指一个类(派生类)从另一个类(基类)派生出来,从而获得基类的成员(数据成员和成员函数)。派生类可以扩展或修改基类的功能。
语法
class BaseClass {
// 基类的成员
};
class DerivedClass : accessSpecifier BaseClass {
// 派生类的成员
};
BaseClass:基类的名称。
DerivedClass:派生类的名称。
accessSpecifier:继承的访问控制符,可以是public、protected或private。
继承的类型
a. 公有继承(Public Inheritance)
class Derived : public Base {
// ...
};
访问权限:
public成员在派生类中仍然是public。
protected成员在派生类中仍然是protected。
private成员在派生类中不可访问。
示例:
class Base {
public:
void publicFunc() {}
protected:
void protectedFunc() {}
private:
void privateFunc() {}
};
class Derived : public Base {
public:
void test() {
publicFunc(); // 合法
protectedFunc(); // 合法
// privateFunc(); // 非法,不可访问
}
};
b. 受保护继承(Protected Inheritance)
class Derived : protected Base {
// ...
};
访问权限:
public和protected成员在派生类中都是protected。
private成员在派生类中不可访问。
示例:
class Derived : protected Base {
public:
void test() {
publicFunc(); // 合法,变为 protected
protectedFunc(); // 合法
// privateFunc(); // 非法
}
};
c. 私有继承(Private Inheritance)
class Derived : private Base {
// ...
};
访问权限:
public和protected成员在派生类中都是private。
private成员在派生类中不可访问。
示例:
class Derived : private Base {
public:
void test() {
publicFunc(); // 合法,变为 private
protectedFunc(); // 合法,变为 private
// privateFunc(); // 非法
}
};
实现方式
a. 单继承
一个派生类继承自一个基类。
示例:
class Animal {
public:
void eat() {}
};
class Dog : public Animal {
public:
void bark() {}
};
b. 多继承
一个派生类继承自多个基类。
示例:
class Person {
public:
void speak() {}
};
class Employee {
public:
void work() {}
};
class Manager : public Person, public Employee {
public:
void manage() {}
};
c. 虚继承(Virtual Inheritance)
用于解决多继承中的菱形继承问题,确保基类在派生类中只有一份实例。
示例:
class A {
public:
void func() {}
};
class B : virtual public A {
// ...
};
class C : virtual public A {
// ...
};
class D : public B, public C {
// ...
};
访问控制
公有继承(public):基类的public和protected成员在派生类中保持原样,private成员不可访问。
受保护继承(protected):基类的public和protected成员在派生类中都变为protected,private成员不可访问。
私有继承(private):基类的public和protected成员在派生类中都变为private,private成员不可访问
多态性
通过继承和虚函数,可以实现多态性,即同一个函数调用可以根据对象的实际类型执行不同的实现
示例:
#include <iostream>
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing Shape" << std::endl;
}
};
class Circle : public Shape {
public:
void draw() const override {//重写基类方法draw()
std::cout << "Drawing Circle" << std::endl;
}
};
void render(const Shape& shape) {
shape.draw();
}
int main() {
Circle circle;
render(circle); // 输出: Drawing Circle
return 0;
}
注意事项
构造函数和析构函数:派生类的构造函数会先调用基类的构造函数,析构函数则相反,先调用派生类的析构函数,再调用基类的析构函数。
重写(Override):在派生类中重写基类的虚函数时,使用override关键字可以提高代码的可读性和安全性。
防止继承:使用final关键字可以防止类被继承。
访问控制:合理使用访问控制符,确保类的封装性和安全性。
二.多态
多态(Polymorphism)是面向对象编程(OOP)的核心概念之一,它允许不同类的对象以统一的方式进行处理。多态性使得程序能够根据对象的实际类型,在运行时调用相应的方法,从而实现灵活和可扩展的代码。在C++中,多态主要通过虚函数(Virtual Functions)和基类指针或引用来实现。以下是对多态的详细介绍,包括其定义、类型、实现方式、注意事项以及示例。
多态是指同一个操作可以作用于不同类型的对象上,并产生不同的行为。换句话说,多态允许程序在运行时根据对象的实际类型调用相应的方法,而不是编译时绑定的类型。
类型
a. 编译时多态(静态多态)
定义:在编译时确定调用哪个函数。
实现方式:
函数重载:同一个函数名有不同的参数列表。
模板(Templates):使用泛型编程实现不同类型的操作。
示例:
#include <iostream>
// 函数重载
void print(int x) {
std::cout << "Integer: " << x << std::endl;
}
void print(double x) {
std::cout << "Double: " << x << std::endl;
}
int main() {
print(5); // 调用 void print(int)
print(3.14); // 调用 void print(double)
return 0;
}
b. 运行时多态(动态多态)
定义:在运行时确定调用哪个函数。
实现方式
*虚函数(Virtual Functions)
声明:在基类中使用virtual关键字声明函数。
重写:在派生类中使用override关键字重写虚函数。
动态绑定:通过基类指针或引用调用虚函数时,根据实际对象的类型执行相应的实现。
示例:
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing Shape" << std::endl;
}
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};
纯虚函数(Pure Virtual Functions)和抽象类(Abstract Classes)
纯虚函数:在基类中声明为virtual且没有实现的函数,通常用于定义接口。
抽象类:包含至少一个纯虚函数的类,不能实例化,只能被继承。
示例:
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};
注意事项
虚函数表(vtable):C++通过虚函数表实现动态绑定,每个包含虚函数的类都有一个虚函数表,每个对象都有一个指向虚函数表的指针。这会增加一定的内存开销和函数调用开销。
性能影响:动态绑定相比于静态绑定有一定的性能开销,但在大多数情况下,这种开销是可以接受的。
避免过度使用:虽然多态性提供了强大的功能,但过度使用可能导致代码难以理解和维护。应根据实际需求合理使用多态性。
虚析构函数:如果类中包含虚函数,建议将析构函数也声明为virtual,以确保在删除基类指针指向的派生类对象时,能够正确调用派生类的析构函数。
示例:
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* obj = new Derived();
delete obj; // 输出:
// Derived destructor
// Base destructor
return 0;
}
三.菱形继承问题
在C++中,菱形继承问题(Diamond Problem) 是多重继承(Multiple Inheritance)带来的一种常见问题。
当一个类继承自两个或多个基类,而这些基类又共同继承自同一个基类时,就会出现菱形继承问题。以下是对菱形继承问题的详细解释:
1. 菱形继承问题的定义
在这种继承结构中,类D通过类B和类C间接继承了类A两次,这导致了菱形继承问题。
A 基类
/ \
B C 单继承
\ /
D 多重继承
菱形继承问题的影响
由于类D通过两条路径继承了类A的成员(通过类B和类C),这会导致以下问题:
命名冲突:类D中可能存在来自类A的重复成员(变量或函数),导致命名冲突。
数据冗余:类D中可能存在多个来自类A的实例,导致数据冗余。
二义性:在类D中访问类A的成员时,会产生二义性,因为编译器不知道应该使用哪条继承路径。
示例:
#include <iostream>
class A {
public:
void show() {
std::cout << "A::show()" << std::endl;
}
};
class B : public A {
// B 继承自 A
};
class C : public A {
// C 继承自 A
};
class D : public B, public C {
// D 多重继承自 B 和 C
};
int main() {
D d;
d.show(); // 编译错误:二义性
return 0;
}
d.show() 会导致编译错误,因为编译器不知道调用的是 B::A::show() 还是 C::A::show()。
解决方案
a.使用虚拟继承(Virtual Inheritance)
虚拟继承可以解决菱形继承问题,使得最终派生类只继承基类的一份成员。
示例:
#include <iostream>
class A {
public:
void show() {
std::cout << "A::show()" << std::endl;
}
};
class B : virtual public A {
// B 虚拟继承自 A
};
class C : virtual public A {
// C 虚拟继承自 A
};
class D : public B, public C {
// D 多重继承自 B 和 C
};
int main() {
D d;
d.show(); // 正确调用 A::show()
return 0;
}
b.显式指定基类
如果不使用虚拟继承,可以通过显式指定基类来消除二义性。
示例:
d.B::show(); // 调用 B::A::show()
d.C::show(); // 调用 C::A::show()
注意事项
虚拟继承的开销:虚拟继承会增加一些额外的开销,因为需要维护共享基类的实例。
设计复杂性:虚拟继承增加了继承结构的复杂性,可能使代码难以理解和维护。
慎用多重继承:在设计类层次结构时,尽量避免不必要的多重继承,使用接口(抽象类)或组合(Composition)来替代多重继承,可以减少菱形继承问题的发生。
四.组合
在C++中,**组合(Composition)**是一种面向对象编程的设计原则,它通过在一个类中包含另一个类的对象来实现代码的复用和模块化。组合强调的是“有一个”的关系,即一个类由一个或多个其他类的对象组成。与继承相比,组合通常提供了更大的灵活性和更低的耦合度。
组合是指一个类包含一个或多个其他类的对象作为其成员。这种关系表示“整体-部分”的关系,即一个类(整体)由其他类(部分)组成。
组合与继承区别:
组合 | 继承 |
---|---|
关系类型 “有一个” | 关系类型 “是一个” |
耦合度低 | 耦合度高 |
灵活性高 | 灵活性低 |
代码复用方式 通过包含对象 | 代码复用方式通过扩展类 |
不直接支持多态性 | 支持多态性 |
适用场景需要复用代码但不需要多态性时 | 适用场景需要实现多态性或扩展类功能时 |
组合的实现
通过在类中声明其他类的对象作为成员变量来实现组合
#include <iostream>
#include <string>
// 引擎类
class Engine {
public:
void start() {
std::cout << "Engine started." << std::endl;
}
void stop() {
std::cout << "Engine stopped." << std::endl;
}
};
// 车轮类
class Wheel {
public:
void inflate(int psi) {
std::cout << "Wheel inflated to " << psi << " psi." << std::endl;
}
};
// 汽车类(组合)
class Car {
public:
Car() : engine(), wheels() {
std::cout << "Car created." << std::endl;
}
void start() {
engine.start();
}
void stop() {
engine.stop();
}
void inflateWheels(int psi) {
for (int i = 0; i < 4; ++i) {
wheels[i].inflate(psi);
}
}
private:
Engine engine; // 汽车包含一个引擎
Wheel wheels[4]; // 汽车包含四个车轮
};
int main() {
Car myCar;
myCar.start(); // 输出: Engine started.
myCar.inflateWheels(32); // 输出: Wheel inflated to 32 psi. (四次)
myCar.stop(); // 输出: Engine stopped.
return 0;
}
组合与继承的选择
使用组合:
当你只需要复用功能而不需要多态性时。
当你希望降低类之间的耦合度时。
当你希望更灵活地改变组合关系时。
使用继承:
当你需要实现多态性时。
当你希望扩展类的功能或行为时。
当“是一个”关系更合适时。
五.动态联编和静态联编
在C++中,**动态联编(Dynamic Binding)和静态联编(Static Binding)**是两种不同的函数调用机制,它们决定了在程序运行时如何解析函数调用。理解这两种联编方式对于掌握多态性、虚函数以及面向对象编程至关重要。
静态联编
静态联编是指在编译阶段就确定了函数调用的具体实现。这种绑定方式基于对象的静态类型(即声明类型),而不是对象的实际类型。
实现方式
普通函数调用:非虚函数的调用是静态联编的。
函数重载:编译器根据参数类型和数量在编译时确定调用哪个重载版本。
模板实例化:编译器在编译时生成具体的函数实例。
特点
效率高:由于在编译时已经确定了调用哪个函数,运行时没有额外的开销。
灵活性低:无法根据对象的实际类型动态改变函数的行为。
示例
#include <iostream>
class Base {
public:
void display() const {
std::cout << "Base display" << std::endl;
}
};
class Derived : public Base {
public:
void display() const {
std::cout << "Derived display" << std::endl;
}
};
void show(const Base& obj) {
obj.display(); // 静态联编,调用 Base::display()
}
int main() {
Derived d;
show(d); // 输出: Base display
return 0;
}
尽管show函数接收的是一个Derived对象的引用,但由于display函数不是虚函数,调用的是Base::display(),这是静态联编的结果。
动态联编
动态联编是指在运行时根据对象的实际类型确定函数调用的具体实现。这种绑定方式允许程序在运行时根据对象的实际类型动态改变函数的行为。
实现方式
虚函数(Virtual Functions):在基类中声明为virtual的函数,在派生类中重写。
基类指针或引用:通过基类指针或引用调用虚函数时,调用的是实际对象的重写版本。
特点
灵活性高:可以根据对象的实际类型动态改变函数的行为。
性能开销:由于需要在运行时查找虚函数表,存在一定的性能开销。
内存开销:每个包含虚函数的类都有一个虚函数表,每个对象都有一个指向虚函数表的指针。
示例
#include <iostream>
class Base {
public:
virtual void display() const {
std::cout << "Base display" << std::endl;
}
};
class Derived : public Base {
public:
void display() const override {
std::cout << "Derived display" << std::endl;
}
};
void show(const Base& obj) {
obj.display(); // 动态联编,调用实际对象的重写版本
}
int main() {
Derived d;
show(d); // 输出: Derived display
return 0;
}
示例对比
#include <iostream>
class Base {
public:
void func() const {
std::cout << "Base func (static)" << std::endl;
}
virtual void vfunc() const {
std::cout << "Base vfunc (dynamic)" << std::endl;
}
};
class Derived : public Base {
public:
void func() const {
std::cout << "Derived func (static)" << std::endl;
}
void vfunc() const override {
std::cout << "Derived vfunc (dynamic)" << std::endl;
}
};
void callFunc(const Base& obj) {
obj.func(); // 静态联编,调用 Base::func()
obj.vfunc(); // 动态联编,调用 Derived::vfunc()
}
int main() {
Derived d;
callFunc(d);
return 0;
}
Base func (static)
Derived vfunc (dynamic)
在上述示例中,func是普通函数,调用的是Base::func(),这是静态联编的结果。而vfunc是虚函数,调用的是Derived::vfunc(),这是动态联编的结果。