C++中的面向对象到底是什么
对象嘛,就和大家都有的对象一样,两只眼睛、一个嘴巴、两条腿……
对不起跑题了,C++的面向对象中的对象可不是显示中的对象哦,但是有一些相似之处,有对象的同学可以参考着去学习C++面向对象的概念,没有对象的同学……那就先new一个出来(手动滑稽
面向对象的概念
对象是对某一类事物的抽象表示,同时具有属性和方法。属性就是这个对象拥有的一些特征,方法就是这个对象可以进行的一些行为。
例如我们大学都必会的“学生管理系统”,用C++面向对象的思想就是,我们把学生看作对象,这个对象拥有身高、姓名、学号、班级、成绩等属性,同时还有修改班级、设置成绩等方法。
再例如人也是一个对象,拥有性别、姓名、户籍地等属性,拥有吃饭、睡觉、唱歌等方法。
在C++中,万物皆可对象,也就是我们在开发之前进行系统设计的时候,要从面向对象的角度出发,先考虑如何通过对象的角度去解决问题,规划好各个对象之间的逻辑关系(例如老师可以管理学生,领导可以管理老师等等)。
在C++中,对象是使用类来实现,例如下面的Person类
class Person
{
public:
void eat();
void sleep();
private:
int age;
string name;
string sex;
float hight;
}
对象和实例
对象是对某一类事物的抽象表示,也就是总称;实例就是这个对象的一个具体的例子,同时也是对象,比如人是对象,小明就是对象的一个实例,但是小明也是人。
例如,女朋友是对象,拥有身高、体重、性格、饭量等属性,还有帮你洗衣服、给你做饭、想你撒娇等方法,女朋友A是对象的一个实例,女朋友B也是对象的一个实例,她们的属性数据不同。
我们平常看到的“对象的实例化”、“实例化一个对象”其实都是一个意思,就是由抽象的对象去创建一个个具体的“对象”,因为对象实例化产生的实例也是对象。
在C++中,对象的实例化有下面几种方式
默认构造函数实例化(栈上)
这是最常见的方式,使用类的默认构造函数创建对象。默认构造函数没有参数,它会在对象创建时被调用。下面这种方式是在栈上实例化。
class MyClass {
public:
MyClass() {
// 构造函数的实现
}
};
int main() {
MyClass obj; // 使用默认构造函数实例化对象
return 0;
}
带参数的构造函数实例化(栈上)
如果类定义了带参数的构造函数,你可以在实例化对象时传递参数给构造函数,下面这种方式在栈上实例化
class MyClass {
public:
MyClass(int value) {
// 构造函数的实现
}
};
int main() {
MyClass obj(42); // 使用带参数的构造函数实例化对象
return 0;
}
无参数动态内存分配实例化(堆上)
使用 new 运算符可以在堆上动态地分配内存,并调用构造函数来实例化对象,需要使用delete手动删除实例化的对象。
class MyClass {
public:
MyClass() {
// 构造函数的实现
}
};
int main() {
MyClass* obj = new MyClass(); // 使用动态内存分配实例化对象
// 使用对象
delete obj; // 释放内存
return 0;
}
有参数动态内存分配实例化(堆上)
使用 new 运算符可以在堆上动态地分配内存,并调用构造函数来实例化对象,需要使用delete手动删除实例化的对象。
class MyClass {
public:
MyClass(int value) {
// 构造函数的实现
}
};
int main() {
MyClass* obj = new MyClass(10); // 使用动态内存分配实例化对象
// 使用对象
delete obj; // 释放内存
return 0;
}
在栈上实例化和在堆上实例化的区别
内存管理:
- 堆上实例化:使用 new 运算符在堆上动态分配内存,对象的生存期由程序员手动控制。需要显式使用 delete 运算符来释放内存,否则可能导致内存泄漏。
- 栈上实例化:对象在函数的栈帧上自动分配内存,对象的生存期与其所在的作用域相对应。当对象离开作用域时,内存会自动被释放,无需手动管理。
对象访问:
- 堆上实例化:通过指针来访问对象,需要使用箭头运算符(->)来访问对象的成员。
- 栈上实例化:通过对象名来直接访问对象的成员。
生命周期:
- 堆上实例化:对象可以在程序的任意位置创建和销毁,生存期由程序员控制。对象可以在多个作用域中共享。
- 栈上实例化:对象的生存期与其所在的作用域相对应,当对象离开作用域时,自动被销毁。
对象的大小:
- 堆上实例化:对象的大小可以是动态的,取决于对象的成员和继承关系。对象的大小可能会影响堆的碎片化。
- 栈上实例化:对象的大小在编译时确定,通常是固定的。
对象初始化:
- 堆上实例化:在堆上实例化对象时,可以调用带参数的构造函数来初始化对象。
- 栈上实例化:在栈上实例化对象时,会自动调用构造函数来初始化对象。
面向对象的三大特性
封装
将一些属性和相关方法“集成”在一个对象中,对外隐藏内部具体的实现细节,外界只需要根据内部提供的接口去使用就可以。
上面的类就体现了封装的概念,实例化一个对象之后,只需要调用它的方法即可,不需要关心内部的实现细节。
继承
我们先明确几个术语:
- 基类(父类)(Base Class):被继承的类。
- 派生类(子类)(Derived Class):从基类继承得到的类。
- 进一步派生的类(孙子类)(Further Derived Class):从派生类再次派生出的类,可以视为“孙”类或更远的后代。
继承就是把父类(也称为基类)的属性和方法继承过来,继承也分为三种,分别是公有继承(public)、保护继承(protected)和私有继承(private)
- 通过公有继承,基类的公有成员和保护成员在派生类中保持其原有的访问权限(公有成员仍为公有,保护成员仍为保护),而基类的私有成员不能直接被派生类访问,但可以通过基类提供的公有或保护方法来访问。
- 通过保护继承,基类的公有和保护成员在派生类中都成为保护成员。私有成员仍然保持私有,不可直接访问。
- 通过私有继承,基类的公有和保护成员在派生类中都成为私有成员。基类的私有成员仍然保持私有,不可直接访问。私有继承通常被用于实现细节的隐藏和复用基类的实现而不是其接口。
详细说下保护继承:
在保护继承中,基类的公有和保护成员在派生类中都成为保护成员,意味着这些成员在派生类内部是可访问的,但是不能被派生类的对象直接访问。也就是说,你不能通过派生类的对象直接访问这些从基类继承来的成员,因为它们在派生类中的访问级别是保护的,而不是公有的。
当我们继续从这样的派生类再派生出新的类时,这些保护成员依然保持保护状态,这意味着在这个新的、进一步的派生类中,这些成员仍然是可访问的。因为保护成员的特性就是允许在类的内部以及这个类的派生类中访问,但不允许通过类的对象进行访问。
假设有三个类:Base、Derived(从Base通过保护继承得来)和FurtherDerived(从Derived类继承得来)。
Base类有一个公有成员和一个保护成员。
由于是保护继承,Derived类中这些成员都成为保护成员,Derived类可以访问这些成员,但Derived类的对象不能直接访问这些成员。
FurtherDerived类,作为Derived的派生类,同样可以访问这些从Base继承来的保护成员,因为保护成员在整个继承链中都保持保护状态,使得它们可以在任何派生类中被访问,但不可以被类的对象直接访问。
这种机制允许在类的内部和其派生类之间共享成员,同时阻止外部访问,从而在一定程度上封装和保护了这些成员。
多态
多多态是面向对象编程中的一个核心概念,它指的是不同类的对象对同一消息的响应方式不同,或者说,同一个接口可以被不同的对象以不同的方式实现。在C++中,多态主要通过虚函数来实现,它允许使用指向基类的指针或引用来调用派生类的方法。
多态主要分为两种类型:静态多态和动态多态。
- 静态多态(编译时多态):通过函数重载和运算符重载实现。它在编译时决定了使用哪个函数,依赖于参数的数量和类型。
- 动态多态(运行时多态):主要通过虚函数实现。在运行时根据对象的实际类型来调用相应的方法,实现了接口的统一和行为的多样。
动态多态的实现依赖于以下几个关键点:
- 虚函数(Virtual Function):在基类中用virtual关键字声明的函数。派生类可以重写(Override)这个函数以提供特定的实现。
- 纯虚函数(Pure Virtual Function):形式为virtual ReturnType FunctionName() = 0;。含有纯虚函数的类称为抽象类(Abstract Class),不能直接实例化。
- 基类指针或引用:可以指向派生类的对象,并通过这个指针或引用调用虚函数,实现动态绑定(Dynamic Binding),即在运行时决定调用哪个版本的虚函数。
多态内容较多,我在后面的博客会详细说名这部分内容。
访问权限修饰符
解释一下几种称呼:
- 派生类:继承自父类的子类;
- 类外部:除了类内部定义和派生类之外的任何地方。在类外部只能访问该类对象的public成员;
- 类内部:指的是在类的定义内部,你可以访问所有public、private和protected成员。
public
公有成员可以在类的内部、派生类以及类的外部访问。使用public修饰的成员在类的接口中非常常见,因为它们是类与外界交互的部分。
protected
保护成员可以在类的内部和派生类中访问,但不能在类的外部直接访问。保护访问修饰符在基类中定义成员时特别有用,这些成员需要对派生类可见,但对其他外部类或函数不可见。
protected成员不能被类的对象访问,因为类的对象属于类外部
private
私有成员只能在类的内部访问。即使是该类的派生类也无法访问私有成员。私有访问限制是类封装的一个重要方面,它防止了外部对类内部实现细节的直接访问。
访问权限总结
访问修饰符 | 类内部 | 类外部 | 派生类 |
---|---|---|---|
public | 可访问 | 可访问 | 可访问 |
protected | 可访问 | 不可访问 | 可访问 |
private | 可访问 | 不可访问 | 不可访问 |
为什么要面向对象编程
面向对象编程,真的是为开发者带来了极大的便利。
封装我们通过将一类事物封装成一个对象,隐藏内部的实现细节,我们只需要为这个对象设计一组接口,开发者可以直接调用此接口去进行其他操作,而不必思考这个接口内部的实现细节。这种信息隐藏的特性有助于降低系统复杂性,提高模块的独立性,从而使得软件易于理解、修改和扩展。
继承允许新创建的类(派生类)继承现有类(基类)的属性和方法。这促进了代码的重用,允许新的类重写或扩展基类的功能。通过继承,可以创建出具有层次结构的类体系,有助于组织和管理复杂的代码。
多态可以使得一个接口可以用不同的方式来实现,增加了代码的灵活性和可扩展性。这也是实现插件架构和依赖注入等设计模式的关键。
此外,封装、继承和多态也支持了代码的低耦合性,这使得修改和维护现有代码更加容易。
另外,类的抽象性允许开发者专注于高层次的操作,而不是底层的实现细节。通过定义具有通用功能的类,可以创建抽象的表示,这减少了代码的复杂性并提高了代码的可理解性。并且面向对象的概念支持软件工程的基本原则,如模块性、封装性和复用性。这些原则是构建大型系统和复杂应用程序的基石,有助于降低软件开发和维护的复杂性。