面向对象基础概念
简述
在 C 语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。我们把这种程序方法称为程序性的(procedural),由一组“分布在各个以功能为导向的函数中”的算法所驱动,它们处理的是共同的外部数据。主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令,这是一种模块化编程思想。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反,面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。
面向过程性能高,缺点是不易维护;面向对象易维护、易扩展、易复用,因为面向对象需要实例化类,开销大,性能比面向过程低。
目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。OOP 的开发是为了提高源代码的可重用性和可维护性。
C++中的class
class的分类:
- Value Classes(数据类)
- RAII (Resource Acquisition Is Initialization) Classes(资源类)
- Polymorphic Classes(多态类)
Class = Data + Operations,每个成员都有一个访问级别,即
- private -
- public +
- protected #
对象是类的实例化,一个对象封装了数据和函数,对象分为属性和操作,属性决定不同的对象之间的差别,由类的数据成员定义,而操作是每个对象可以执行什么行为,由类的函数成员定义
面向对象特点
- Inheritance(继承)
- Polymorphism(多态)
- Encapsulation(封装)
- Abstraction(抽象)
Inheritance
概述
方法
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
数据
- 公有继承(public):(Is-A)关系
is-a关系表示某个对象是另一个对象的一个特例,公有继承通常用于表示 “Is-A” 的关系。也就是说,子类是父类的一种特殊类型。例如,Dog是Animal的一种特殊类型,Dog继承Animal类,具有Animal的特征。- 当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员被派生类访问
- 保护继承(protected):部分与整体关系,
保护继承通常用于需要限制对父类接口的访问的场景。子类依然需要访问父类的成员,但不希望这些成员暴露给外部。比如,某些派生类可能希望拥有父类的实现细节,但不希望外部代码直接使用这些细节。- 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):(Has-A)关系
has-a关系表示一个对象 拥有 另一个对象,通常通过 组合 或 聚合 来实现。它表示的是一种 实现继承,即子类是通过父类的实现来“实现”某些功能,但不直接暴露给外部。- 当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
这里我们主要过一下几种基础的继承关系
- Single inheritance
- Multiple inheritance
- Hierarchical inheritance
- Multilevel inheritance
- Hybrid inheritance
访问属性
| 访问 | public | protected | private |
| 同一个类 | yes | yes | yes |
| 派生类 | yes | yes | no |
| 外部的类 | yes | no | no |
virtual
- virtual用于基类声明此虚函数需要重写
- 纯虚函数所在的类被称为抽象类,只能用来被继承而不能实例化。一个虚拟函数只是提供了一个可被子类型改写的接口,派生类必须实现每一个纯虚函数。在基类中实现纯虚函数的方法是在函数原型后加 =0,
virtual void funtion1()=0 - 虚继承就是为了解决菱形继承中,B,C都继承了A,D继承了B,C,那么D关于 A的引用只有一次,而不是 普通继承的对于A引用了两次,格式:可以采用public、protected、private三种不同的继承关键字进行修饰,只要确保包含virtual就可以了。
override final
override用于派生类声明此虚函数需要重写覆盖,用法Tick read(PacketPtr pkt) override;
final声明当前派生类此虚函数为最后一次覆盖
Polymorphism
重载函数是通过匹配参数的类型和数量来调用的。这些信息在编译时就可以得到,因此,编译器会在编译时选择合适的函数。这是通过函数重载和运算符重载实现的,这也被称为静态绑定或早期绑定。
现在,我们考虑一下函数名称和原型相同的情况,函数的原型在基类和派生类中都是一样的,因此,静态绑定不能被应用。应该在运行时选择合适的函数,这就是所谓的运行时多态性。
运行时多态性是在对象的方法在运行时而不是在编译时被调用时实现的。它是通过方法重写实现的,这也被称为动态绑定或晚期绑定。
| 编译时多态 | 运行时多态 |
| 要调用的函数在编译时是已知的 | 要调用的函数在运行时是已知的 |
| 它也被称为重载、早期绑定和静态绑定 | 它也被称为覆盖、动态绑定和后期绑定 |
| 重载是一种编译时的多态性,即一个以上的方法具有相同的名称,但参数数量或参数的类型不同 | 覆盖是一种运行时的多态性,在这种多态性中,有多个方法具有相同的名称、参数数量和参数的类型 |
| 它是通过函数重载和运算符重载实现的 | 它是通过虚拟函数和指针实现的 |
| 它提供了快速的执行,因为它在编译时就已经知道了 | 它提供了缓慢的执行,因为它在运行时是已知的 |
| 它的灵活性较差,因为主要是在编译时执行所有的东西 | 它更加灵活,因为所有的东西都在运行时执行 |
Overloading
函数重载
函数重载的定义是:在C++中,拥有两个或多个名称相同但参数不同的函数的过程被称为函数重载。在函数重载中,通过使用不同类型的参数或不同数量的参数,对函数进行重新定义。只有通过这些差异,编译器才能区分不同的函数
函数重载的原因:
- 类型转换
- 带有默认参数的函数
- 带有通过引用传递的函数
运算符重载
操作符重载是一种编译时的多态性,其中操作符被重载,为用户定义的数据类型提供特殊含义。操作符重载用于重载或重新定义C++中的大多数操作符。它被用来对用户定义的数据类型进行操作。例如,C++提供了将用户定义的数据类型的变量添加到内置数据类型的能力。
操作符重载的优点是对同一操作数进行不同的操作
不能重载的运算符如下:
- Scope operator (::)
- Sizeof
- member selector(.)
- member pointer selector(*)
- ternary operator(?:)
函数覆盖
如果派生类定义了与基类相同(一模一样)的函数,这在C++中被称为函数覆盖。它被用来实现运行时多态性。它使你能够提供基类已经提供的函数的具体实现。
| 派生类\基类 | 非虚函数 | 虚函数 | 纯虚函数 |
| 非虚函数 | 派生类的非虚函数会隐藏基类的非虚函数。在派生类中调用时,只会调用派生类的版本。但这不会影响基类的版本,基类的非虚函数在基类对象中仍然有效。 | 派生类的非虚函数会隐藏基类的虚函数,但虚函数机制不受影响。通过基类的指针或引用调用时,仍然可以调用基类的虚函数版本。 | 派生类提供具体的非虚函数实现,这使得派生类不再是抽象类,但该实现不支持多态性。 |
| 虚函数 | 派生类的非虚函数会隐藏基类的虚函数。即使基类的虚函数支持动态绑定,通过基类指针或引用调用时也只会调用派生类的非虚函数版本,这种情况下动态绑定机制失效。 | 派生类覆盖基类的虚函数,支持多态性。在动态绑定时,调用的是派生类的虚函数版本。 | 派生类提供具体的虚函数实现,支持多态性,派生类不再是抽象类。 |
| 纯虚函数 | 错误 | 派生类可以将基类的虚函数重新声明为纯虚函数,这会使派生类本身变成抽象类,需要在进一步派生类中提供具体实现。 | 派生类保留纯虚函数时,仍然是抽象类,不能被实例化。派生类可以选择提供该函数的实现,也可以保持它为纯虚函数。 |
Encapsulation & Abstraction
封装是包含信息的过程或方法。封装是一种将数据隐藏在单一实体或单元中的方法,同时也是一种保护信息不受外界影响的方法。这种方法将数据和功能一起封装在一个类中,这也导致了数据的抽象化。
在OOPs中,抽象是一种获取信息的方法,需要的信息将以最简单的方式被提取出来,只有需要的部分被提取出来,而那些被认为不太重要的部分则没有被注意到。抽象的概念只向用户展示必要的信息。它通过隐藏程序的实施复杂性来降低程序的复杂性。
抽象与封装的不同
- 抽象是获得信息的过程或方法,而封装是包含信息的过程或方法。
- 在抽象中,问题是在设计或接口层面解决的,而在封装中,问题是在实现层面解决的。
- 抽象是隐藏不需要的信息的方法。而封装是将数据隐藏在一个实体或单元中的方法,同时也是保护信息不受外界影响的方法。
- 我们可以使用抽象类和接口来实现抽象,而封装可以通过访问修改器来实现,即私有、保护和公共。
- 在抽象中,使用抽象类和接口来隐藏实现的复杂性,而在封装中,使用getters和setters的方法来隐藏数据。
- 有助于执行抽象的对象是被封装的,而导致封装的对象不需要被抽象化
抽象
数据抽象是一种设计模式,其中数据仅对语义相关的功能可见,以防止误用。数据抽象的成功通常是在面向对象和纯函数式编程中经常将数据隐藏作为设计原则。如果一个类不允许调用代码访问内部对象数据并且只允许通过方法访问,这就是一种信息隐藏形式,称为抽象。
我们可以在C++中使用类来实现抽象化。类可以帮助我们使用可用的访问指定器来分组数据成员和成员函数。一个类可以决定哪些数据成员将对外界可见,哪些不可见
在C++中还有一种抽象类型可以是头文件。例如,考虑math.h头文件中的pow()方法。每当我们需要计算一个数字的幂,我们只需调用math.h头文件中的函数pow(),并将数字作为参数传递,而不知道该函数实际上是根据什么算法来计算数字的幂
访问限定符是C++中实现抽象的主要支柱,可以使用访问限定符来对类成员进行限制。
抽象的好处
帮助用户避免编写低级别的代码
避免了代码的重复,增加了可重用性。
可以独立改变类的内部实现而不影响用户。
有助于提高应用程序或程序的安全性,因为只向用户提供重要的细节。
它减少了代码的复杂性和冗余度,因此提高了可读性
封装
封装防止外部代码关注对象的内部工作。这有助于代码重构,例如,允许类的作者更改该类的对象在内部表示其数据的方式,而无需更改任何外部代码。它还鼓励程序员将与特定数据集有关的所有代码放在同一个类中,这样组织起来便于其他程序员理解。封装是一种鼓励解耦的技术。
在面向对象的编程中,在类中制作使用该类成员变量的函数,那么我们就称之为封装,封装需要将数据和操作数据的函数绑定在一起。
设计模式
常见的编程问题和解决这些问题的模式。
- 创建型模式、工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
- 结构型模式、适配器模式、桥接模式、复合模式、装饰器模式、门面模式、享元模式、代理模式
- 行为模式、责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式
C++不属于对象的类型和概念
- 基本数据类型(Primitive types):
- 基本数据类型(如
int、char、double、float等)并不属于对象。在 C++ 中,基本数据类型是值类型,不具有成员函数、构造函数或析构函数。 - 例如:
int x = 10;这里x只是一个普通的基本类型变量,而不是 C++ 中的对象。
- 基本数据类型(如
- 指针(Pointers):
- 指针本身不是一个对象,它是一个变量,用来存储一个内存地址。尽管指针可以指向对象(例如
int* p = &x;),指针本身并不是对象。 - 例如:
int* p;,p是一个指针,它可以指向一个对象,但指针p本身并不被视为对象。
- 指针本身不是一个对象,它是一个变量,用来存储一个内存地址。尽管指针可以指向对象(例如
- 函数(Functions):
- 函数也是 C++ 中的一种实体,但它们不是对象。虽然函数有地址(指向函数的指针),并且可以像对象一样传递或存储,但函数本身并不具备对象的特性,如构造函数、成员函数等。
- 例如:
int add(int a, int b) { return a + b; },add是一个函数,而不是一个对象。
- 常量(Constants):
- 常量,如
const变量、字面常量(例如数字常量5或字符串常量"Hello"),也不被视为对象。常量通常是值类型,不具备对象的结构和方法。 - 例如:
const int a = 10;,a是常量,不是对象。
- 常量,如
- 数组(Arrays):
- 数组 是一个元素集合,通常在栈上分配内存。虽然数组在内存中是连续的,但它们与对象有本质的区别,数组类型不支持成员函数。
- 例如:
int arr[10];,arr是一个数组,它不是一个对象。
- 枚举(Enums):
- 枚举类型定义了一组整数常量,它们不是对象,尽管它们是类型安全的。枚举的成员只是常量,而非完整的对象实例。
- 例如:
enum Color { Red, Green, Blue };,Color是枚举类型,Red、Green、Blue是枚举值,而不是对象。
- 类型(Types):
- 类型本身(如
int、float、MyClass)也不是对象,它们是对数据的描述,而不是存储具体数据的实体。 - 例如:
int x;这里int是类型,而x是变量,只有x才是对象。
- 类型本身(如
-
void类型:-
void类型表示无类型,它不是一个对象类型。在 C++ 中,void用于表示不返回任何值的函数,或者指针类型表示指向未知类型的内存。 - 例如:
void* ptr;这里void*是指针类型,而void本身并不是对象。
-
- Lambda 表达式:
- Lambda 表达式本质上是匿名的函数对象,它虽然可以被当作函数对象使用,但不完全等同于常规的对象,它的生命周期和行为也比较特殊。
- 例如:
auto lambda = [](int x) { return x * 2; };,这里的lambda是一个函数对象,但它本质上是一个匿名对象。
- 临时对象:
- 临时对象是 C++ 中在表达式求值时临时创建的对象。它们通常是返回值、类型转换或者从右值引用创建的对象。这些临时对象在某些条件下(如用于初始化变量或传递给函数时)会被创建,但它们并不持久化存在。
- 例如:
MyClass foo = MyClass();,MyClass()是一个临时对象,但它不会像常规对象一样有明确的生命周期。
OOP思想的应用
- 网络协议—分布式数据管理架构(DDM)
- Linux内核