继承
继承是面向对象三大特性之一
在C++中,继承是面向对象编程的重要特性之一。通过使用继承,一个类可以继承另一个类的属性和方法,从而构建出更加灵活和可重用的代码结构。
继承的基本语法
继承的语法
class 子类 : 继承方式 父类
示例
class BaseClass {
// BaseClass的定义
};
class DerivedClass : public BaseClass {
// DerivedClass从BaseClass继承而来
};
继承方式
也是访问修饰符,可以是public、protected或private,用于指定派生类对基类成员的访问权限。
继承方式一共有三种:
公有继承(public inheritance)
使用关键字
public
进行继承,基类中的公有成员在派生类中仍为公有成员,保护成员在派生类中变为保护成员,私有成员在派生类中不可访问。
保护继承(protected inheritance)
使用关键字
protected
进行继承,基类中的公有和保护成员在派生类中变为保护成员,私有成员在派生类中不可访问。
私有继承(private inheritance)
使用关键字
private
进行继承,基类中的所有成员(包括公有、保护和私有成员)都在派生类中变为私有成员,不可在派生类外部访问。
总结
public继承表示基类的公有成员在派生类中仍为公有成员。
protected继承表示基类的公有和保护成员在派生类中变为保护成员。
private继承表示基类的公有和保护成员在派生类中变为私有成员。
任何派生类都无法访问父类的私有属性
继承中的对象模型
父类中私有成员也被子类继承下去了,只是由编译器给隐藏后访问不到,由于私有成员在类内部是不可访问的,因此在子类中无法直接访问或继承私有成员。
子类只能通过继承来获取父类的接口和实现,但无法直接访问父类的私有成员。私有成员对于子类来说是不可见和不可访问的。
这种隐藏和无法访问的特性是C++封装机制的一部分,其目的是保护类的实现细节和数据的安全性。通过将成员设为私有,可以限制外部访问并控制数据的修改。子类只能通过公有和受保护的成员函数间接地访问和操作基类的私有成员。
继承中构造和析构顺序
结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
构造顺序
首先,基类的构造函数会被调用,在派生类的构造函数体之前执行。
如果存在多级继承,构造函数的调用顺序按照从上到下、从左到右的顺序进行。
派生类的构造函数体会在基类构造函数执行完成后执行。
析构顺序
析构顺序与构造顺序相反
首先,派生类的析构函数会被调用,在基类的析构函数之前执行。
如果存在多级继承,析构函数的调用顺序按照从下到上、从右到左的顺序进行。
基类的析构函数会在派生类析构函数执行完成后执行。
示例
#include <iostream>
class BaseClass {
public:
BaseClass() {
std::cout << "BaseClass 构造函数" << std::endl;
}
~BaseClass() {
std::cout << "BaseClass 析构函数" << std::endl;
}
};
class DerivedClass : public BaseClass {
public:
DerivedClass() {
std::cout << "DerivedClass 构造函数" << std::endl;
}
~DerivedClass() {
std::cout << "DerivedClass 析构函数" << std::endl;
}
};
int main() {
DerivedClass obj;
return 0;
}
输出结果:
BaseClass 构造函数
DerivedClass 构造函数
DerivedClass 析构函数
BaseClass 析构函数
原理
这是因为在对象创建时,需要先构造基类部分,然后才能构造派生类部分。而在对象销毁时,先析构派生类部分,然后才能析构基类部分。这种顺序保证了继承关系中对象的正确构造和析构。
总结
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继承同名成员处理方式
访问方式
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域
当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
隐藏成员
如果派生类定义了与基类同名的成员函数或成员变量,在派生类内部该成员将会隐藏基类的同名成员。在派生类内使用成员名称时,默认情况下将引用派生类的成员,而不会访问基类的同名成员。如果需要访问基类的同名成员,可以使用作用域解析运算符::来指定基类的成员。
覆盖成员函数
如果派生类定义了与基类同名的成员函数,并且它们具有相同的参数列表,那么派生类的成员函数将覆盖(override)基类的同名成员函数。在调用同名函数时,将会调用派生类的函数而不是基类的函数。
使用using声明
在派生类中可以通过using声明来引入基类的同名成员,使其在派生类中可见。这样可以实现对基类成员的重载和扩展,而不是简单地隐藏基类的同名成员。例如,可以使用using声明来将基类的同名成员引入派生类的作用域,然后在派生类中对该成员进行重载。
举例
#include <iostream>
class BaseClass {
public:
void myFunction() {
std::cout << "BaseClass 的函数" << std::endl;
}
};
class DerivedClass : public BaseClass {
public:
void myFunction() {
std::cout << "DerivedClass 的函数" << std::endl;
}
};
int main() {
DerivedClass obj;
// 1. 隐藏成员
obj.myFunction(); // 输出: "DerivedClass 的函数"
// 2. 覆盖成员函数
obj.BaseClass::myFunction(); // 输出: "BaseClass 的函数"
// 3. 使用using声明
using BaseClass::myFunction;
obj.myFunction(); // 输出: "BaseClass 的函数"
return 0;
}
在上面的示例中,派生类DerivedClass继承了基类BaseClass的成员函数myFunction()。我们通过三种不同的方式处理同名成员:
隐藏成员:派生类DerivedClass定义了与基类BaseClass同名的成员函数myFunction()。在派生类内部,默认情况下引用的是派生类的同名成员函数。因此,在调用obj.myFunction()时,会输出"DerivedClass 的函数"。
覆盖成员函数:如果我们想要调用基类BaseClass中的同名成员函数myFunction(),可以使用作用域解析运算符::来指定基类的成员。例如,obj.BaseClass::myFunction()将会调用基类的函数,输出"BaseClass 的函数"。
使用using声明:通过使用using BaseClass::myFunction;的声明,我们将基类中的同名成员引入了派生类的作用域。这样,可以在派生类中对该成员进行重载和扩展。在这种情况下,obj.myFunction()将会调用基类的函数,输出"BaseClass 的函数"。
覆盖和隐藏的区别
覆盖(override):当派生类定义了一个与基类同名的成员函数时,且它们具有相同的参数列表,派生类的成员函数将覆盖(override)基类的同名成员函数。在调用同名函数时,将会调用派生类的函数而不是基类的函数。这种行为也被称为动态多态性(dynamic polymorphism),因为在运行时决定调用哪个函数。
隐藏(hide):当派生类定义了一个与基类同名的成员函数或成员变量时,在派生类内部该成员将会隐藏基类的同名成员。在派生类内使用成员名称时,默认情况下将引用派生类的成员,而不会访问基类的同名成员。如果需要访问基类的同名成员,可以使用作用域解析运算符::来指定基类的成员。隐藏可以看作是一种静态的行为,因为在编译时已经确定了成员的访问。
区别如下:
覆盖是在运行时决定调用哪个函数,是动态的行为;而隐藏是在编译时确定成员的访问,是静态的行为。
覆盖是指派生类重写了基类的同名成员函数,重新定义了其行为;而隐藏是指派生类定义了一个与基类同名的成员,将基类的同名成员隐藏起来。
在覆盖中,通过基类指针或引用调用同名函数时,根据实际指向的对象类型决定调用的函数;而在隐藏中,不论使用什么方式进行调用,都会引用派生类的同名成员。
需要注意的是,覆盖只适用于虚函数(在基类中使用virtual关键字声明的函数),而隐藏适用于任何成员函数或成员变量。
(虚函数相关知识请看 Day18 多态)
继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致,
只不过有两种访问的方式(通过对象 和 通过类名)
(俩种方式见 Day 14 对象的初始化和清理)
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
多继承语法
定义
多继承是一种C++中的面向对象编程特性,允许一个派生类从多个基类派生而来。
语法
class 子类 :继承方式 父类1 , 继承方式 父类2...
注意
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
多继承也可能引发菱形继承问题和命名冲突问题,需要注意和处理。
示例
class BaseClass1 {
// BaseClass1 的成员声明和定义
};
class BaseClass2 {
// BaseClass2 的成员声明和定义
};
class DerivedClass : public BaseClass1, public BaseClass2 {
// DerivedClass 的成员声明和定义
};
在上面的示例中,DerivedClass 是从 BaseClass1 和 BaseClass2 两个基类派生而来的派生类。
多继承的语法使用逗号分隔了多个基类,每个基类的访问权限可以通过 public、protected 或 private 关键字来声明,默认情况下是 private 访问权限。这里的示例中使用了 public 访问权限,这表示派生类可以访问基类的公有成员。
在派生类中,可以直接访问基类的成员变量和成员函数,例如:
DerivedClass obj;
obj.baseVariable1 = 10; // 访问 BaseClass1 的成员变量
obj.baseFunction1(); // 调用 BaseClass1 的成员函数
obj.baseVariable2 = 20; // 访问 BaseClass2 的成员变量
obj.baseFunction2(); // 调用 BaseClass2 的成员函数
菱形继承
定义
菱形继承(Diamond Inheritance,也称为钻石继承)是多继承中可能遇到的一个问题,指的是某个派生类同时继承了两个基类,而这两个基类又共同继承自同一个基类,形成了一个菱形的继承结构。这种继承结构可能导致一些问题,其中最主要的问题是二义性。
二义性(Ambiguity)
通常指的是在派生类中访问基类成员时,存在多个可能的匹配,导致编译器无法确定使用哪个成员的情况。
出现二义性的主要原因是多重继承,即一个派生类同时继承自多个基类,并且这些基类中存在相同名称的成员。当派生类通过对象或指针访问这个名称时,编译器无法确定应该使用哪个基类的成员,从而导致二义性。为了解决这个问题,我们可以使用作用域解析符明确指定使用哪个基类的成员,如A::foo()
和B::foo()
。
问题
class Grandparent {
public:
void greet() {
std::cout << "Hello from Grandparent!" << std::endl;
}
};
class Parent1 : public Grandparent { };
class Parent2 : public Grandparent { };
class Child : public Parent1, public Parent2 { };
在上面的示例中,Child 类从 Parent1 和 Parent2 两个基类派生而来,而 Parent1 和 Parent2 都继承自 Grandparent 基类,形成了一个菱形继承结构。
问题出现在当我们尝试在 Child 类中调用 greet() 函数时,编译器无法确定应该使用哪个 greet() 函数,因为 greet() 函数在 Parent1 和 Parent2 中都存在,而且它们都继承自 Grandparent。这导致了二义性问题。
解决方法
加修饰限定:使用作用域解析符明确指定使用哪个基类的成员
Child child;
child.Parent1::greet(); // 显式调用 Parent1 的 greet() 函数
child.Parent2::greet(); // 显式调用 Parent2 的 greet() 函数
虚继承:在继承方式前加上virtual
class Parent1 : virtual public Grandparent { };
class Parent2 : virtual public Grandparent { };
虚继承(virtual inheritance)是C++中用于解决菱形继承问题的技术。虚继承的原理是通过在派生类中使用虚基类来共享基类的成员。当一个派生类通过多条路径间接继承同一个基类时,如果不使用虚继承,可能会导致基类在派生类中存在多个实例,从而引发二义性和资源浪费的问题。通过使用虚继承,我们可以避免菱形继承带来的二义性和资源浪费问题,确保只有一个实例存在。但是需要注意,虚继承会带来一些额外的开销,包括内存空间和访问成员时的间接性。因此,在使用虚继承时需要权衡利弊,并根据具体情况进行选择。