- ♂️ 作者:海码007
- 专栏:C++专栏
- 相关文章:C++多态详解
- 标题:【C++ 】面向对象三大特性:封装、继承、多态 详解
- ❣️ 寄语:人生的意义或许是可以发挥自己全部的潜力,所以加油吧!
- 最后:文章作者技术和水平有限,如果文中出现错误,希望大家能指正
封装、继承、多态是面向对象的三大特性。接着本篇博客大家一起回顾一下
封装(Encapsulation)是面向对象编程中的一种重要概念,指将数据和操作封装在一个单元中,通过对外提供公共接口来访问和操作数据,同时隐藏内部的实现细节。
在C++中,封装通过类来实现。类将数据成员和成员函数组合在一起,形成一个封闭的单元。数据成员可以是私有的(private),只能在类的内部访问;成员函数可以是公共的(public),被外部代码调用以访问和修改数据。这种封装机制提供了数据的安全性和灵活性。
其实封装最显著的特点就是将复杂的实现过程隐藏起来,不暴露给外界。(例如开汽车,我们不需要知道复杂的汽车是如何实现的,只需要通过一些简单的操作就能让汽车启动起来)
封装的优点:
- 数据隐藏和安全性:通过将数据成员设为私有,封装可以防止外部代码直接访问和修改对象的数据,只允许通过公共接口进行操作。这样可以有效地保护数据的完整性和安全性。
- 接口统一和简化:封装可以将相关的操作封装在一起,形成一个简洁的公共接口。外部代码只需要调用接口函数,而无需了解具体的实现细节,使代码使用更加方便和易懂。
- 代码模块化和维护性:封装促进了代码的模块化,使得各个模块之间的耦合度降低。当需要修改实现时,只需修改封装类内部的代码,而不会影响到外部代码,提高了代码的维护性和可复用性。
- 增强了代码的可靠性:封装可以通过公共接口对数据进行有效的验证和控制,避免了错误的数据访问和操作。这样可以减少bug的产生,提高代码的可靠性。
封装的缺点:
- 间接性和性能开销:封装导致代码的间接性增加,因为需要通过函数调用来访问和操作数据。这会引入一定的性能开销,特别是对于频繁调用的函数。
- 不利于对数据的直接访问:封装限制了对数据的直接访问,可能导致某些特定场景下的效率问题。在某些情况下,直接访问数据可能比通过函数调用更有效。
综上所述,封装是面向对象编程的重要特征,它通过将数据和操作封装在一起,提供了数据的隐藏和安全性,简化了接口,增强了代码的可维护性和可靠性。尽管存在一些缺点,但在大多数情况下,封装的优点远远超过了其缺点,使得代码更加可靠、易用和可维护。
它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程,从抽象到具体,例如从动物类到猫类。
C++ 继承是面向对象编程中的一种重要概念,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和行为。继承可以通过创建一个新的类,该类从一个或多个现有类派生而来,以实现代码的重用和扩展。
在C++中,继承通过关键字class后面的冒号来声明。派生类在其类定义中指定基类的名称,如下所示:
class DerivedClass : access-specifier BaseClass
{
// class body
};
其中,DerivedClass 是派生类的名称,BaseClass 是基类的名称,access-specifier 可以是public、protected或private,用于指定派生类对基类成员的访问权限。
- 继承方式限定了基类成员在派生类中的访问权限,包括public(公有的)、private(私有的)和protected(受保护的)。此项是可选项,如果不写,默认为private(成员变量和成员函数默认也是private)。
- public成员可以通过对象来访问,private成员不能通过对象访问,protected成员和private成员类似,也不能通过对象访问。但是当存在继承关系时,protected和private就不一样了:基类中的protected成员可以在派生类中使用,而基类中的private成员不能在派生类中使用。(这里要注意一个混淆点:基类中的protected成员,假如被派生类以private的方式继承,那么其protected成员就变成private成员了,也就不能在派生类中直接访问了)
C++中的继承方式
- 公有继承(public inheritance):使用public关键字声明派生类对基类的继承,基类中的公有成员在派生类中仍然为公有成员,保持其访问权限不变。
- 保护继承(protected inheritance):使用protected关键字声明派生类对基类的继承,基类中的公有成员在派生类中变为保护成员,不能被外部访问,只能在派生类的成员函数中访问。
- 私有继承(private inheritance):使用private关键字声明派生类对基类的继承,基类中的公有成员在派生类中变为私有成员,不能被外部访问,只能在派生类的成员函数中访问。
通过上述分析可以得到以下几点结论:
- 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。(例如当继承方式为protected时,那么基类成员在派生类中的访问权限最高也为protected,高于protected的会降级为protected,但低于protected不会升级。再如,当继承方式为public时,那么基类成员在派生类中的访问权限将保持不变。也就是说,继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的。)
- 不管继承方式如何,基类中的private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
- 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public或protected;只有那些不希望在派生类中使用的成员才声明为private。
- 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为protected。
注意:我们这里说的是基类的private成员不能在派生类中使用,并没有说基类的private成员不能被继承。实际上,基类的private成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。
名字遮蔽(Name Hiding):当派生类中定义了和基类成员名称相同的成员时,派生类成员会遮蔽(隐藏)基类成员。这意味着在派生类中无法直接访问被遮蔽的基类成员。如果需要访问被遮蔽的基类成员,可以使用作用域解析运算符::来指定基类的名称。
class BaseClass {
public:
int x;
};
class DerivedClass : public BaseClass {
public:
int x; // 遮蔽了基类的成员x
void print() {
cout << "DerivedClass x: " << x << endl; // 访问派生类的成员x
cout << "BaseClass x: " << BaseClass::x << endl; // 访问基类的成员x
}
};
在派生类的成员函数print()中,可以使用DerivedClass::x来访问派生类的成员x,使用BaseClass::x来访问基类的成员x。
在继承中,我们经常会遇到名字遮蔽(Name Hiding)、函数重写(Function Overriding)和函数重载(Function Overloading)这三个概念很容易弄混淆。
(1)名字遮蔽(Name Hiding)
名字遮蔽指的是在派生类中定义了与基类中的成员名称相同的成员。当派生类中存在与基类相同名称的成员时,基类的成员会被遮蔽,即无法直接访问基类中被遮蔽的成员。
class BaseClass {
public:
void method() {
// base class method
}
};
class DerivedClass : public BaseClass {
public:
void method() {
// derived class method, hides the base class method
}
};
在上述例子中,派生类DerivedClass定义了一个名为method的成员函数,该函数与基类BaseClass的成员函数method名称相同。
由于名字遮蔽的存在,派生类的method会隐藏基类的method,在派生类中无法直接访问基类的method。
(2)函数重写(Function Overriding)
函数重写指的是在派生类中定义一个与基类中相同名称、参数列表和返回类型的成员函数,用来覆盖(override)基类中的同名函数。与名字遮蔽不同,基类中的同名函数是虚函数,被vitural关键字修饰
class BaseClass {
public:
virtual void method() {
// base class method
}
};
class DerivedClass : public BaseClass {
public:
void method() {
// derived class method, overrides the base class method
}
};
在上述例子中,DerivedClass重写了BaseClass中的方法method。
通过在基类函数声明中添加virtual关键字,可以实现动态多态性,确保在运行时根据对象的实际类型调用适当的函数版本。
(3)函数重载(Function Overloading)
函数重载指的是在同一个作用域中定义多个具有相同名称但不同参数列表的函数。函数重载允许使用相同的函数名称来执行不同的操作,提供了更加灵活的函数调用方式。(同一作用域是重载,不同作用域是遮蔽)
class MyClass {
public:
void method(int x) {
// method with int parameter
}
void method(double x) {
// method with double parameter
}
};
在上述例子中,MyClass定义了两个名字相同但参数类型不同的成员函数method,分别接受int和double类型的参数。
这样,根据所传递的参数类型,编译器可以选择调用合适的函数版本。
总结:
- 名字遮蔽(Name Hiding)发生在派生类中定义了与基类成员名称相同的成员,并导致基类成员在派生类中无法直接访问。
- 函数重写(Function Overriding)是指在派生类中定义与基类中函数名称、参数列表和返回类型相同的成员函数,用于覆盖基类函数并实现对基类行为的修改或扩展。
- 函数重载(Function Overloading)允许在同一作用域中定义具有相同名称但参数列表不同的多个函数,通过不同的参数类型或参数个数来实现不同的操作。
这些概念在C++继承中具有不同的作用和行为,可以根据需要灵活地使用。
- 无遮蔽时的对象模型
派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,所有成员函数仍在另一个区域(代码区),由所有对象共享。- 有遮蔽时的对象模型
基类中被遮蔽的成员变量,仍然会在派生类对象的内存中。
在C++对象模型中,一个类的对象通常由两部分组成:对象成员和虚函数表(对于含有虚函数的类)。
以下是C++对象模型的几个重要要素:
- 非静态数据成员:
类中的非静态数据成员(包括基类的成员和派生类自己的成员)被继承时,会按照它们声明的顺序依次排列在对象中。这样就实现了对成员的内存布局。- 虚函数表(vtable):
虚函数表是C++实现多态性的关键机制。当一个类声明了虚函数,编译器会为该类创建一个虚函数表。虚函数表是一个函数指针数组,包含了该类所有虚函数的地址。每个对象都有一个指向其类的虚函数表的指针(通常被称为虚指针),用于调用适当的虚函数。- 虚指针(vptr):
虚指针是一个指向虚函数表的指针,位于对象或类的开头。虚指针的存在使得C++的运行时多态性成为可能。通过虚指针,编译器可以在运行时根据对象的实际类型来调用正确的虚函数。- 虚基类:
当一个类被多个派生类继承时,可能会出现多个派生类共享同一个基类的实例(称为共享对象)。为了避免创建多个共享对象,C++引入了虚基类的概念。在对象模型中,虚基类的子对象只会在派生类的继承层次结构中存在一次,并且被所有派生类共享。- 对象大小和内存对齐:
C++对象的大小由其成员变量的总大小决定。为了高效访问对象成员,编译器通常会对对象进行内存对齐。对齐规则要么由编译器的默认规则决定,要么可以通过对齐属性指定。