C++面向对象编程继承与多态
C++ 面向对象编程之继承
继承的基本概念
在 C++ 中,继承是一种重要的面向对象编程特性,它允许我们基于现有的类创建新的类。新创建的类称为派生类(或子类),而现有的类称为基类(或父类)。派生类继承了基类的成员(包括数据成员和成员函数),并且可以在此基础上添加新的成员或重写基类的成员。
继承的主要优点在于代码复用。通过继承,我们可以避免在多个类中重复编写相同的代码,从而提高开发效率和代码的可维护性。例如,假设我们有一个 Animal
类,包含一些通用的属性和行为,如 name
和 eat
方法。如果我们要创建 Dog
和 Cat
类,它们都具有 Animal
类的共性,那么可以让 Dog
和 Cat
类继承自 Animal
类,而不需要在 Dog
和 Cat
类中重新实现这些共性。
继承的语法
在 C++ 中,定义派生类的语法如下:
class 派生类名 : 继承方式 基类名 {
// 派生类成员声明
};
其中,继承方式可以是 public
、private
或 protected
。下面分别介绍这三种继承方式:
public
继承:在public
继承中,基类的public
成员在派生类中仍然是public
的,基类的protected
成员在派生类中仍然是protected
的,而基类的private
成员在派生类中是不可访问的。
示例代码:
class Animal {
public:
std::string name;
void eat() {
std::cout << name << " is eating." << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << name << " is barking." << std::endl;
}
};
在上述代码中,Dog
类通过 public
继承自 Animal
类。因此,Dog
类可以访问 Animal
类的 public
成员 name
和 eat
方法。
private
继承:在private
继承中,基类的public
和protected
成员在派生类中都变为private
成员,而基类的private
成员在派生类中仍然是不可访问的。
示例代码:
class Animal {
public:
std::string name;
void eat() {
std::cout << name << " is eating." << std::endl;
}
};
class Dog : private Animal {
public:
void bark() {
eat(); // 可以调用,因为在 private 继承下,eat 方法在派生类中变为 private 方法
std::cout << name << " is barking." << std::endl;
}
};
在这个例子中,Dog
类通过 private
继承自 Animal
类。虽然 Dog
类可以访问 Animal
类的 public
成员 name
和 eat
方法,但这些成员在 Dog
类中变为 private
成员,外部代码无法直接访问。
protected
继承:在protected
继承中,基类的public
成员在派生类中变为protected
成员,基类的protected
成员在派生类中仍然是protected
成员,而基类的private
成员在派生类中是不可访问的。
示例代码:
class Animal {
public:
std::string name;
void eat() {
std::cout << name << " is eating." << std::endl;
}
};
class Dog : protected Animal {
public:
void bark() {
eat(); // 可以调用,因为在 protected 继承下,eat 方法在派生类中变为 protected 方法
std::cout << name << " is barking." << std::endl;
}
};
在该示例中,Dog
类通过 protected
继承自 Animal
类。Animal
类的 public
成员 name
和 eat
方法在 Dog
类中变为 protected
成员,派生类的成员函数可以访问这些成员,但外部代码无法直接访问。
基类和派生类的构造函数与析构函数
- 构造函数:当创建派生类对象时,首先会调用基类的构造函数,然后再调用派生类的构造函数。这是因为派生类对象包含了基类的部分,需要先初始化基类部分。
示例代码:
class Animal {
public:
Animal(const std::string& n) : name(n) {
std::cout << "Animal constructor called." << std::endl;
}
std::string name;
void eat() {
std::cout << name << " is eating." << std::endl;
}
};
class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {
std::cout << "Dog constructor called." << std::endl;
}
void bark() {
std::cout << name << " is barking." << std::endl;
}
};
在上述代码中,Dog
类的构造函数通过 Animal(n)
调用了 Animal
类的构造函数,先初始化 Animal
类的 name
成员,然后再执行 Dog
类构造函数的其他部分。
- 析构函数:与构造函数相反,当销毁派生类对象时,首先会调用派生类的析构函数,然后再调用基类的析构函数。这是为了确保在释放派生类特有的资源后,再释放基类的资源。
示例代码:
class Animal {
public:
~Animal() {
std::cout << "Animal destructor called." << std::endl;
}
};
class Dog : public Animal {
public:
~Dog() {
std::cout << "Dog destructor called." << std::endl;
}
};
在这个例子中,当 Dog
对象被销毁时,会先调用 Dog
类的析构函数,输出 “Dog destructor called.”,然后再调用 Animal
类的析构函数,输出 “Animal destructor called.”。
继承中的访问控制
-
访问基类的成员:在派生类中,根据继承方式的不同,可以访问基类的不同成员。如前面所述,
public
继承下可以访问基类的public
和protected
成员,private
继承下只能在派生类内部访问基类的public
和protected
成员(它们在派生类中变为private
),protected
继承下派生类及其派生类可以访问基类的public
和protected
成员(它们在派生类中变为protected
)。 -
隐藏基类成员:如果派生类中定义了与基类成员同名的成员(函数或变量),那么基类中的同名成员会被隐藏。即使它们的参数列表不同,基类的同名成员也会被隐藏。
示例代码:
class Animal {
public:
void eat() {
std::cout << "Animal eats." << std::endl;
}
};
class Dog : public Animal {
public:
void eat(int amount) {
std::cout << "Dog eats " << amount << " units." << std::endl;
}
};
在上述代码中,Dog
类定义了一个与 Animal
类中 eat
方法同名但参数列表不同的 eat
方法。此时,Animal
类的 eat
方法在 Dog
类中被隐藏。如果我们创建一个 Dog
对象并调用 eat
方法,只有 Dog
类的 eat(int amount)
方法会被调用。如果要调用 Animal
类的 eat
方法,可以使用作用域解析运算符 ::
,如 dog.Animal::eat();
。
C++ 面向对象编程之多态
多态的基本概念
多态是面向对象编程的另一个重要特性,它允许我们以统一的方式处理不同类型的对象。多态通过基类指针或引用调用虚函数来实现。当我们使用基类指针或引用调用虚函数时,实际调用的是派生类中重写的虚函数,而不是基类中的虚函数,这使得程序能够根据对象的实际类型来决定执行哪个函数版本,从而实现动态绑定。
多态性提高了程序的灵活性和可扩展性。例如,我们有一个 Shape
基类,包含一个 draw
虚函数。然后我们有 Circle
、Rectangle
等派生类,它们重写了 draw
函数以实现各自的绘制逻辑。我们可以使用 Shape
指针或引用的数组来存储不同类型的 Shape
对象,并通过调用 draw
函数来绘制这些对象,而无需关心它们的具体类型。
虚函数和纯虚函数
- 虚函数:在基类中使用
virtual
关键字声明的函数称为虚函数。派生类可以重写这些虚函数以提供特定的实现。
示例代码:
class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
在上述代码中,Shape
类的 draw
函数被声明为虚函数。Circle
和 Rectangle
类重写了 draw
函数,并使用 override
关键字来显式表明这是对基类虚函数的重写。override
关键字是 C++11 引入的,它有助于编译器检测重写错误,如果派生类中声明的函数并非真正重写基类的虚函数,编译器会报错。
- 纯虚函数:纯虚函数是一种特殊的虚函数,它在基类中没有具体的实现,其声明格式为在函数声明后加上
= 0
。包含纯虚函数的类称为抽象类,抽象类不能直接实例化对象,只能作为基类被其他类继承。
示例代码:
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
在这个例子中,Shape
类的 draw
函数是纯虚函数,因此 Shape
类是抽象类。Circle
和 Rectangle
类必须重写 draw
函数,否则它们也会成为抽象类。
动态绑定和静态绑定
- 静态绑定:静态绑定是指在编译时就确定函数调用的绑定方式。对于非虚函数,函数调用在编译时根据对象的类型来确定。例如:
class Animal {
public:
void eat() {
std::cout << "Animal eats." << std::endl;
}
};
class Dog : public Animal {
public:
void eat() {
std::cout << "Dog eats." << std::endl;
}
};
int main() {
Dog dog;
Animal* animalPtr = &dog;
animalPtr->eat(); // 调用 Animal::eat(),因为 eat 不是虚函数,静态绑定
return 0;
}
在上述代码中,虽然 animalPtr
指向一个 Dog
对象,但由于 eat
方法不是虚函数,所以调用的是 Animal
类的 eat
方法,这就是静态绑定。
- 动态绑定:动态绑定是指在运行时根据对象的实际类型来确定函数调用的绑定方式。对于虚函数,通过基类指针或引用调用时,会在运行时根据对象的实际类型来决定调用哪个派生类的虚函数版本。
示例代码:
class Animal {
public:
virtual void eat() {
std::cout << "Animal eats." << std::endl;
}
};
class Dog : public Animal {
public:
void eat() override {
std::cout << "Dog eats." << std::endl;
}
};
int main() {
Dog dog;
Animal* animalPtr = &dog;
animalPtr->eat(); // 调用 Dog::eat(),因为 eat 是虚函数,动态绑定
return 0;
}
在这个例子中,Animal
类的 eat
方法是虚函数,通过 animalPtr
调用 eat
方法时,会根据 animalPtr
实际指向的对象类型(这里是 Dog
对象)来调用 Dog
类的 eat
方法,这就是动态绑定。
多态的实现机制
在 C++ 中,多态的实现主要依赖于虚函数表(vtable)和虚函数表指针(vptr)。
-
虚函数表:每个包含虚函数的类都有一个虚函数表。虚函数表是一个数组,其中存储了类中虚函数的地址。当一个类继承自另一个包含虚函数的类时,派生类的虚函数表会继承基类的虚函数表,并根据需要重写虚函数的地址。
-
虚函数表指针:每个对象都有一个虚函数表指针,它指向该对象所属类的虚函数表。当通过基类指针或引用调用虚函数时,程序会首先通过虚函数表指针找到虚函数表,然后根据虚函数在表中的索引找到实际要调用的虚函数地址,并执行该函数。
示例代码:
class Base {
public:
virtual void func1() {
std::cout << "Base::func1" << std::endl;
}
virtual void func2() {
std::cout << "Base::func2" << std::endl;
}
};
class Derived : public Base {
public:
void func1() override {
std::cout << "Derived::func1" << std::endl;
}
};
在上述代码中,Base
类有两个虚函数 func1
和 func2
,它会有一个虚函数表,表中存储了 func1
和 func2
的地址。Derived
类继承自 Base
类,它的虚函数表继承了 Base
类的虚函数表,并将 func1
的地址替换为 Derived::func1
的地址。当创建 Derived
对象时,对象的虚函数表指针指向 Derived
类的虚函数表。当通过 Base
指针或引用调用 func1
时,会根据虚函数表指针找到 Derived
类的虚函数表,并调用 Derived::func1
。
多态的应用场景
- 图形绘制系统:如前面提到的
Shape
类及其派生类Circle
、Rectangle
等。通过多态,可以方便地管理和绘制不同类型的图形,提高代码的可维护性和扩展性。例如:
#include <vector>
#include <iostream>
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Rectangle());
for (Shape* shape : shapes) {
shape->draw();
}
for (Shape* shape : shapes) {
delete shape;
}
return 0;
}
在这个例子中,我们使用 Shape
指针的向量来存储不同类型的 Shape
对象,并通过调用 draw
函数来绘制它们,无需关心对象的具体类型。
- 游戏开发中的角色系统:假设有一个
Character
基类,包含attack
虚函数。然后有Warrior
、Mage
等派生类,它们重写attack
函数以实现不同的攻击方式。通过多态,可以方便地管理不同角色的攻击行为。
示例代码:
class Character {
public:
virtual void attack() = 0;
};
class Warrior : public Character {
public:
void attack() override {
std::cout << "Warrior attacks with sword." << std::endl;
}
};
class Mage : public Character {
public:
void attack() override {
std::cout << "Mage casts a spell." << std::endl;
}
};
在游戏中,可以根据角色的实际类型调用相应的 attack
函数,实现多样化的游戏玩法。
- 插件系统:在一些大型软件中,插件系统可以利用多态来实现动态加载不同的插件。例如,有一个
Plugin
基类,包含execute
纯虚函数。每个插件类继承自Plugin
类并实现execute
函数。主程序可以通过Plugin
指针来调用不同插件的execute
函数,实现插件的动态加载和执行。
示例代码:
class Plugin {
public:
virtual void execute() = 0;
virtual ~Plugin() {}
};
class Plugin1 : public Plugin {
public:
void execute() override {
std::cout << "Plugin1 is executing." << std::endl;
}
};
class Plugin2 : public Plugin {
public:
void execute() override {
std::cout << "Plugin2 is executing." << std::endl;
}
};
在主程序中,可以根据配置文件或用户选择动态创建相应的插件对象,并通过 Plugin
指针调用 execute
函数,实现插件的灵活加载和运行。
多态与指针和引用
- 通过指针实现多态:通过基类指针指向派生类对象,并调用虚函数,就可以实现多态。例如:
class Base {
public:
virtual void print() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived" << std::endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->print(); // 调用 Derived::print()
delete basePtr;
return 0;
}
在上述代码中,basePtr
是 Base
类型的指针,但指向了 Derived
对象,调用 print
虚函数时,实际执行的是 Derived
类的 print
函数。
- 通过引用实现多态:与指针类似,通过基类引用绑定派生类对象,并调用虚函数也能实现多态。
示例代码:
class Base {
public:
virtual void print() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived" << std::endl;
}
};
int main() {
Derived derivedObj;
Base& baseRef = derivedObj;
baseRef.print(); // 调用 Derived::print()
return 0;
}
在这个例子中,baseRef
是 Base
类型的引用,绑定到 Derived
对象,调用 print
虚函数时,同样执行的是 Derived
类的 print
函数。
需要注意的是,如果直接通过派生类对象调用虚函数,不会体现多态性,因为此时函数调用在编译时就已经确定。例如:
class Base {
public:
virtual void print() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived" << std::endl;
}
};
int main() {
Derived derivedObj;
derivedObj.print(); // 调用 Derived::print(),但不是通过多态实现
return 0;
}
在这种情况下,由于 derivedObj
是 Derived
类型的对象,编译器在编译时就知道要调用 Derived
类的 print
函数,而不是通过运行时的动态绑定。
多态与函数重载和隐藏
- 多态与函数重载的区别:函数重载是指在同一个类中定义多个同名但参数列表不同的函数,函数重载是在编译时根据参数列表来确定调用哪个函数,属于静态绑定。而多态是通过基类指针或引用调用虚函数,在运行时根据对象的实际类型来确定调用哪个函数版本,属于动态绑定。
示例代码:
class Math {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
};
class Base {
public:
virtual void print() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived" << std::endl;
}
};
在上述代码中,Math
类的 add
函数是函数重载的例子,而 Base
和 Derived
类的 print
函数体现了多态。
- 多态与函数隐藏的关系:当派生类定义了与基类同名但参数列表不同的函数时,基类的同名函数会被隐藏,这与多态不同。多态要求函数签名(包括返回类型、函数名和参数列表)完全相同,并且通过基类指针或引用调用虚函数来实现动态绑定。如果派生类函数与基类函数同名但参数列表不同,即使基类函数是虚函数,也不会构成多态,而是函数隐藏。
示例代码:
class Base {
public:
virtual void func(int a) {
std::cout << "Base::func(int)" << std::endl;
}
};
class Derived : public Base {
public:
void func(double b) {
std::cout << "Derived::func(double)" << std::endl;
}
};
在这个例子中,Derived
类的 func(double b)
函数隐藏了 Base
类的 func(int a)
函数,通过 Derived
对象调用 func
函数时,只会调用 Derived
类的 func(double b)
函数,而不会体现多态。如果要调用 Base
类的 func(int a)
函数,需要使用作用域解析运算符 ::
,如 derivedObj.Base::func(10);
。
通过深入理解继承与多态这两个 C++ 面向对象编程的重要特性,开发者可以编写出更加灵活、可维护和可扩展的代码,在处理复杂的软件系统时能够更加高效地组织和管理代码结构。无论是构建大型应用程序、游戏开发还是其他领域,继承与多态都发挥着至关重要的作用。同时,在实际应用中,需要注意继承方式、访问控制、虚函数的正确使用以及多态实现过程中的细节,以避免出现潜在的错误和问题。