封装、继承与多态究极详解

简介: 本文详细介绍了面向对象编程中的三大核心特性:封装、继承和多态。封装通过隐藏数据和提供接口,确保对象的安全性和一致性;继承通过类之间的“is-a”关系实现代码复用和扩展;多态则允许不同类的对象通过相同的接口执行不同的操作,增强程序的灵活性和可扩展性。文章还探讨了这些特性的底层实现机制,如虚函数表和内存布局,并提供了具体的代码示例。

个人理解

封装是类自带的固有属性,就像一个盒子天然就可以分装东西

继承是类与类之间的一种关系表现,我们知道除了继承,类之间的关系还可以有关联、依赖、实现、聚合、组合,为什么只强调继承?私以为实现是继承的特例,而其他四种关系都属于将类放在不同位置的灵活使用,且C中的结构体本身也具有这些特性,它并不是C++新创造出来的,但继承不一样,继承是新的需要提前约定的规则。

继承之后资源分配的规则就是多态

类的固有属性(封装),与它类新关系(继承)以及继承衍生出来的资源分配规则(多态)就是c++之于c多出来的可以从面向过程转为面向对象的内容。

其他的像模版方法、

前言

这个系列我们将就封装、继承、多态概念来展开,尽可能详尽且底层的将他们的原理性的东西展示出来! 话题内容包括但不限于:

  • 封装只有类能做吗?结构体如何封装?名空间能实现封装吗?
  • 封装有哪些好处?
  • 继承的特殊情况说明,比如多继承带来的菱形继承问题……
  • 继承时如何合理细分类的职责?
  • 多态的具体规则,引入指针之后的资源分配本质……
  • 多态的虚函数表和虚函数指针具体是什么?创建时机是什么?
  • 多态的静态绑定和动态绑定是什么?有什么区别?
  • 继承一定好吗?组合优于继承这句话的依据是什么?什么条件下适用?

封装是什么?

面向对象(OOP)与面向过程编程(POP)相比,封装是其中的一个核心特性。封装不仅仅是将数据和行为捆绑在一起,更是通过隐藏实现细节、限制对数据的直接访问来提供一个更安全、易管理的代码结构。为了理解封装,我们需要逐步深入到它的本质,并在代码层面和理论上解释它。

1. 面向过程编程(POP) vs 面向对象编程(OOP)

  • 面向过程编程(Procedural-Oriented Programming,POP):是一种依赖于函数调用和过程的编程范式。在POP中,程序通过执行一系列步骤(函数调用)来达到目标。数据和操作这些数据的功能是分开的。程序的核心是通过操作全局数据来进行的。
  • 面向对象编程(Object-Oriented Programming,OOP):将数据和操作这些数据的功能封装在一起,构成一个“对象”。面向对象的程序是由对象组成的,这些对象通过消息(方法调用)与其他对象交互。

在OOP中,封装是将数据和方法绑定到一个对象中,并通过控制数据的访问来保证对象内部的一致性和安全性。

2. 封装的核心概念

封装的基本思想是隐藏内部实现细节暴露必要的接口。封装有两个主要方面:

  • 数据隐藏:只允许通过公开的接口(方法)访问和修改数据。这样可以避免外部代码直接修改对象的内部状态,减少错误的发生。
  • 接口与实现分离:对象暴露的是一组操作数据的接口,而不是数据本身。外部只关心如何使用这个对象提供的功能,而不需要了解它的内部实现。

3. 如何实现封装

在C++中,封装是通过和访问修饰符(如publicprivateprotected)来实现的。

3.1. 类与对象

  • :是一个模板或蓝图,它定义了数据和方法。数据通常称为“成员变量”,方法称为“成员函数”。
  • 对象:类的实例。每个对象有自己的数据,并可以使用类中的方法。

3.2. 访问修饰符

  • public:类的公共部分,外部可以访问和修改。
  • private:类的私有部分,外部无法直接访问,只能通过类提供的公有方法来间接访问。
  • protected:类似于private,但允许派生类(子类)访问。

3.3. 封装的实现示例

下面是一个简单的C++示例,展示了如何通过封装保护数据和提供接口。

#include <iostream>
using namespace std;

class BankAccount {
private:
   double balance;  // 余额是私有的,外部不能直接访问

public:
   // 构造函数,初始化账户余额
   BankAccount(double initialBalance) {
       if (initialBalance < 0) {
           balance = 0;
       } else {
           balance = initialBalance;
       }
   }

   // 提供一个公有方法来访问余额
   double getBalance() const {
       return balance;
   }

   // 提供一个公有方法来修改余额
   void deposit(double amount) {
       if (amount > 0) {
           balance += amount;
       } else {
           cout << "Deposit amount must be positive." << endl;
       }
   }

   void withdraw(double amount) {
       if (amount > 0 && amount <= balance) {
           balance -= amount;
       } else {
           cout << "Invalid withdrawal amount." << endl;
       }
   }
};

int main() {
   BankAccount account(1000); // 创建一个初始余额为1000的账户

   cout << "Initial balance: $" << account.getBalance() << endl;

   account.deposit(500);  // 存款500
   cout << "After deposit: $" << account.getBalance() << endl;

   account.withdraw(200); // 提款200
   cout << "After withdrawal: $" << account.getBalance() << endl;

   // 尝试直接访问balance(会出错)
   // cout << "Direct balance access: $" << account.balance << endl;  // 编译错误

   return 0;
}

代码解释:

  • BankAccount 类:这个类包含了一个私有成员变量 balance,它存储账户余额。外部代码无法直接访问或修改这个余额。
  • deposit 和 withdraw 方法:这些是公有的接口方法,允许外部代码在合法的条件下(如存款金额为正,提款金额不超过余额)修改账户余额。
  • getBalance 方法:提供了一个公有的方法来获取余额,确保外部代码不能直接修改余额,但可以查询。

为什么使用封装?

  1. 数据保护:封装隐藏了数据的实现,外部无法直接改变对象的内部状态,防止了误操作或非法操作。
  2. 提高代码可维护性:通过暴露清晰的接口和隐藏复杂的内部实现,程序更加模块化。如果需要改变实现细节,只需要修改类的内部代码,不会影响到其他依赖这个类的代码。
  3. 提高安全性:封装可以确保对象的一致性和有效性。比如,withdraw方法中检查提款金额是否合理,确保余额不被非法提取。

4. 封装的底层实现

从底层的角度看,封装的实现通常依赖于内存布局和访问控制机制。在C++中,类的成员变量通常会在对象实例化时分配内存。通过访问控制(privatepublic)和getset方法,编译器帮助开发者实现了对数据访问的精细控制。

  • 内存分配:每个对象都有独立的内存区域来存储成员变量。当对象被创建时,内存会分配给它的所有成员变量。privatepublic 只是影响这些成员在外部代码中的访问方式,实际的内存布局不会变化。
  • 访问控制privatepublicprotected 是由编译器支持的访问权限控制机制,确保类的私有数据只能通过特定的公有方法来修改。编译器会在编译时检查是否有非法访问的代码,防止程序出现不可预期的行为。

5. 总结

封装是面向对象编程的基础,它通过将数据和行为捆绑在一起,并限制外部对数据的访问,来保护对象的内部状态,提供更安全、灵活和易维护的代码结构。通过控制数据的访问和修改,我们能够保证数据的完整性和一致性,同时也能隐藏复杂的实现细节,简化外部接口的使用。

继承是什么

面向对象编程中的继承(Inheritance)是一个非常重要的概念,它允许一个类(子类)继承另一个类(父类)的方法和属性,从而避免代码重复,提高代码的复用性。继承是OOP的三大特性之一,另外两个特性是封装和多态。

1. 继承的基本概念

继承是一种“is-a”(是一个)关系。例如,假设你有一个基类Animal,然后你创建一个类Dog继承自Animal,那么Dog就可以看作是Animal的一个特例,继承了Animal的一些属性和方法。

  • 父类(基类):提供共通的属性和方法。
  • 子类:从父类继承属性和方法,子类可以添加新的属性和方法,或重写(覆盖)父类的方法。

2. 继承的主要作用

  • 代码复用:子类无需重新定义父类已经实现的方法和属性,可以直接使用它们。
  • 扩展性:子类可以在继承的基础上扩展功能,添加特有的行为。
  • 层次化设计:继承允许程序员通过类层次结构来组织和简化代码。例如,DogCat都可以继承自Animal,然后你可以根据需要为DogCat添加各自的特殊行为。

3. 如何实现继承

在C++中,继承通过classpublicprotectedprivate修饰符来实现。

  • public继承:子类继承父类的公有成员和保护成员,父类的公有方法和属性在子类中保持可访问。
  • protected继承:子类继承父类的公有成员和保护成员,但父类的公有方法和属性在子类中变为受保护。
  • private继承:子类继承父类的公有成员和保护成员,但父类的公有方法和属性在子类中变为私有。

示例代码:

#include <iostream>
using namespace std;

// 基类(父类)
class Animal {
public:
   void speak() {
       cout << "Animal speaks!" << endl;
   }

   void move() {
       cout << "Animal moves!" << endl;
   }
};

// 派生类(子类)
class Dog : public Animal {
public:
   void bark() {
       cout << "Dog barks!" << endl;
   }

   // 重写父类方法
   void speak() {
       cout << "Dog barks loudly!" << endl;
   }
};

int main() {
   Animal animal;
   animal.speak();  // 调用基类方法
   animal.move();   // 调用基类方法

   Dog dog;
   dog.speak();     // 调用子类重写的方法
   dog.move();      // 调用继承的父类方法
   dog.bark();      // 调用子类自己的方法

   return 0;
}

代码解释:

  1. 基类 Animal:定义了两个方法,speakmove,表示动物的行为。
  2. 派生类 Dog:继承自 Animal,除了继承 Animalspeakmove 方法外,Dog 还定义了一个新的方法 bark,表示狗的行为。
  3. 方法重写Dog 中重写了 speak 方法,使得狗发出的声音与其他动物不同。

4. 继承的底层实现

在底层,继承通过对象布局指针偏移来实现。每个对象都有一个虚函数表(vtable),用于支持多态(如果使用了虚函数)。当你创建一个子类对象时,它不仅包含自己的数据成员,还会包含父类的数据成员(如果父类有数据成员的话)。

内存布局:

  • 对象的内存布局包含了父类部分子类部分。父类的成员变量和成员函数会先存储在内存中,子类会在父类的基础上添加额外的成员。
  • 如果有虚函数,编译器会为类创建一个虚函数表,虚函数表包含所有虚函数的指针,确保子类能够重写(覆盖)父类的虚函数。

示例内存布局:

假设有以下类继承关系:

  • A 是基类,B 是从 A 继承的子类,C 是从 B 继承的子类。
内存布局 说明
A 类的成员 基类 A 中的成员数据存储在内存中
B 类的成员 子类 B 扩展的成员数据存储在内存中
C 类的成员 子类 C 扩展的成员数据存储在内存中

5. 继承的类型

继承可以分为不同类型,常见的包括:

  • 单继承:子类只继承一个父类。
  • 多重继承:子类可以继承多个父类。
  • 多级继承:子类继承自父类,孙类继承自子类等。

示例:多重继承

#include <iostream>
using namespace std;

// 基类1
class Animal {
public:
   void move() {
       cout << "Animal moves!" << endl;
   }
};

// 基类2
class Mammal {
public:
   void nurse() {
       cout << "Mammal nurses!" << endl;
   }
};

// 派生类
class Dog : public Animal, public Mammal {
public:
   void bark() {
       cout << "Dog barks!" << endl;
   }
};

int main() {
   Dog dog;
   dog.move();    // 来自 Animal
   dog.nurse();   // 来自 Mammal
   dog.bark();    // 来自 Dog

   return 0;
}

6. 继承的优缺点

优点:

  • 代码重用:子类继承父类的行为,可以减少代码重复,提升代码复用性。
  • 模块化设计:通过继承可以构建层次结构,使得代码更具组织性。
  • 扩展性:子类可以继承父类的功能,并在此基础上扩展或重写,满足更多需求。

缺点:

  • 紧密耦合:继承会导致类之间的紧密耦合,子类对父类的依赖较强,修改父类可能影响子类的行为。
  • 继承层次复杂:多层继承可能导致类关系复杂,尤其是多重继承时,可能出现二义性(例如“菱形继承问题”)。
  • 不利于灵活性:过度使用继承可能导致代码不易扩展或维护,过度继承会使类层次过于复杂。

7. 总结

继承是OOP的重要特性,能够通过建立类的层次关系实现代码重用和扩展。它允许子类继承父类的行为和属性,并且能够扩展或修改这些行为。理解继承如何在底层实现、如何利用它来构建高效的程序,是掌握OOP的关键。

多态是什么

多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它允许不同类的对象通过相同的接口(方法名)来调用不同的实现。简单来说,多态使得不同类型的对象可以通过相同的接口执行不同的操作。多态性使得程序更加灵活和可扩展。

1. 多态的基本概念

多态来源于两个希腊词根:“poly”(多)和“morph”(形态)。在OOP中,多态指的是同一个操作作用于不同类型的对象时,可以有不同的表现形式。最常见的多态形式是方法重写(overriding),即子类可以重写(覆盖)父类的方法。

多态的两种类型

  1. 编译时多态(静态多态):在编译时决定调用哪个函数,常见的实现方式是方法重载(Overloading)和运算符重载(Operator Overloading)。
  2. 运行时多态(动态多态):在程序运行时决定调用哪个函数,常通过虚函数和继承实现。

2. 运行时多态与虚函数

运行时多态通常通过虚函数来实现。虚函数是基类中声明为 virtual 的函数,子类可以重写这个函数。当通过基类指针或引用调用该函数时,程序会根据对象的实际类型(而不是指针或引用的类型)来决定调用哪个函数实现。

2.1. 虚函数的定义和用法

虚函数是在父类中声明的成员函数,并使用 virtual 关键字修饰,表示这个函数可以在子类中被重写。

2.2. 多态的实现方式

  • 父类指针或引用指向子类对象:当父类指针或引用指向子类对象时,调用虚函数会动态绑定到子类的实现上,而不是父类的实现。这样就实现了多态。

示例代码:

#include <iostream>
using namespace std;

// 基类
class Animal {
public:
   // 虚函数
   virtual void speak() {
       cout << "Animal speaks!" << endl;
   }

   virtual ~Animal() {}  // 虚析构函数,避免内存泄漏
};

// 派生类
class Dog : public Animal {
public:
   void speak() override {   // 重写父类的 speak 方法
       cout << "Dog barks!" << endl;
   }
};

class Cat : public Animal {
public:
   void speak() override {  // 重写父类的 speak 方法
       cout << "Cat meows!" << endl;
   }
};

int main() {
   // 父类指针指向不同的子类对象
   Animal* animal1 = new Dog();
   Animal* animal2 = new Cat();

   // 调用虚函数
   animal1->speak();  // 输出: Dog barks!
   animal2->speak();  // 输出: Cat meows!

   delete animal1;  // 释放内存
   delete animal2;  // 释放内存

   return 0;
}

代码解析:

  • **虚函数 speak**:在 Animal 类中被声明为虚函数,并在 DogCat 类中重写了该函数。
  • 父类指针animal1animal2 是指向 Animal 类型的指针,但它们分别指向 DogCat 类型的对象。
  • 运行时多态:当通过父类指针调用 speak 方法时,C++ 会根据指针实际指向的对象类型来决定调用哪个函数(即 Dog 类的 speakCat 类的 speak),这就是运行时多态

3. 编译时多态与函数重载

编译时多态指的是在编译阶段就可以确定调用哪个函数。编译时多态通常通过函数重载运算符重载实现。

3.1. 函数重载(Function Overloading)

在同一个类中,可以定义多个同名的函数,只要它们的参数类型或参数个数不同。编译器会根据函数调用时传递的参数来决定调用哪个版本的函数。

#include <iostream>
using namespace std;

class Printer {
public:
   // 函数重载
   void print(int i) {
       cout << "Printing integer: " << i << endl;
   }

   void print(double d) {
       cout << "Printing double: " << d << endl;
   }

   void print(const char* str) {
       cout << "Printing string: " << str << endl;
   }
};

int main() {
   Printer printer;
   printer.print(10);          // 输出: Printing integer: 10
   printer.print(3.14);        // 输出: Printing double: 3.14
   printer.print("Hello!");    // 输出: Printing string: Hello!

   return 0;
}

代码解析:

  • 函数重载:在 Printer 类中,定义了三个同名的 print 函数,但它们的参数类型不同(intdoubleconst char*)。
  • 编译时多态:编译器根据传入的参数类型来决定调用哪个 print 函数,这就是编译时多态

3.2. 运算符重载(Operator Overloading)

C++允许我们为自定义类型重载运算符,这也是一种编译时多态的表现。

#include <iostream>
using namespace std;

class Complex {
public:
   int real;
   int imag;

   Complex(int r, int i) : real(r), imag(i) {}

   // 运算符重载
   Complex operator + (const Complex& other) {
       return Complex(real + other.real, imag + other.imag);
   }

   void print() {
       cout << real << " + " << imag << "i" << endl;
   }
};

int main() {
   Complex c1(1, 2), c2(3, 4);
   Complex c3 = c1 + c2;  // 使用重载的 + 运算符
   c3.print();  // 输出: 4 + 6i

   return 0;
}

代码解析:

  • 运算符重载:我们重载了 + 运算符,使其可以对 Complex 类型的对象进行加法操作。
  • 编译时多态:当我们使用 c1 + c2 时,编译器会调用重载的 operator + 函数来执行加法运算。

4. 多态的底层实现

多态的底层实现依赖于虚函数表(vtable)。每个包含虚函数的类,在编译时会生成一个虚函数表,其中存储着类的所有虚函数指针。当通过父类指针调用虚函数时,程序会查找虚函数表,找到对应的子类实现并调用。

虚函数表的工作原理

  1. 每个类有一个虚函数表,表中存储该类的虚函数的地址。
  2. 当创建一个对象时,虚函数表会绑定到该对象中。
  3. 当调用虚函数时,程序会通过对象的虚函数表找到对应的函数地址,进而实现多态。

5. 总结

多态是面向对象编程的核心特性之一,它通过相同的接口执行不同的实现。多态主要分为两种类型:

  • 编译时多态:通过方法重载和运算符重载等手段实现。
  • 运行时多态:通过虚函数和继承实现,通常通过基类指针或引用调用派生类的重写方法。

多态使得代码更加灵活和可扩展,有助于构建更易于维护和扩展的程序架构。

目录
相关文章
|
6天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
8天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
8003 19
|
11天前
|
Cloud Native Apache 流计算
资料合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
4357 10
资料合集|Flink Forward Asia 2024 上海站
|
19天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
12天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
7天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
|
1月前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
104580 10
|
7天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
637 39
|
5天前
|
弹性计算 运维 监控
阿里云云服务诊断工具:合作伙伴架构师的深度洞察与优化建议
作为阿里云的合作伙伴架构师,我深入体验了其云服务诊断工具,该工具通过实时监控与历史趋势分析,自动化检查并提供详细的诊断报告,极大提升了运维效率和系统稳定性,特别在处理ECS实例资源不可用等问题时表现突出。此外,它支持预防性维护,帮助识别潜在问题,减少业务中断。尽管如此,仍建议增强诊断效能、扩大云产品覆盖范围、提供自定义诊断选项、加强教育与培训资源、集成第三方工具,以进一步提升用户体验。
626 243
|
1天前
|
弹性计算 运维 监控
云服务测评 | 基于云服务诊断全方位监管云产品
本文介绍了阿里云的云服务诊断功能,包括健康状态和诊断两大核心功能。作者通过个人账号体验了该服务,指出其在监控云资源状态和快速排查异常方面的优势,同时也提出了一些改进建议,如增加告警配置入口和扩大诊断范围等。