前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >封装、继承与多态究极详解(面试必问)

封装、继承与多态究极详解(面试必问)

作者头像
码事漫谈
发布2024-12-20 12:52:50
发布2024-12-20 12:52:50
25300
代码可运行
举报
文章被收录于专栏:设计模式设计模式
运行总次数:0
代码可运行

个人理解

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

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

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

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

其他的像模版方法、

前言

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

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

封装是什么?

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

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

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

2. 封装的核心概念

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

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

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

3.1. 类与对象
  • :是一个模板或蓝图,它定义了数据和方法。数据通常称为“成员变量”,方法称为“成员函数”。
  • 对象:类的实例。每个对象有自己的数据,并可以使用类中的方法。
3.2. 访问修饰符
  • public:类的公共部分,外部可以访问和修改。
  • private:类的私有部分,外部无法直接访问,只能通过类提供的公有方法来间接访问。
  • protected:类似于private,但允许派生类(子类)访问。
3.3. 封装的实现示例

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

代码语言:javascript
代码运行次数:0
运行
复制
#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继承:子类继承父类的公有成员和保护成员,但父类的公有方法和属性在子类中变为私有。
示例代码:
代码语言:javascript
代码运行次数:0
运行
复制
#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. 继承的类型

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

  • 单继承:子类只继承一个父类。
  • 多重继承:子类可以继承多个父类。
  • 多级继承:子类继承自父类,孙类继承自子类等。
示例:多重继承
代码语言:javascript
代码运行次数:0
运行
复制
#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. 多态的实现方式
  • 父类指针或引用指向子类对象:当父类指针或引用指向子类对象时,调用虚函数会动态绑定到子类的实现上,而不是父类的实现。这样就实现了多态。
示例代码:
代码语言:javascript
代码运行次数:0
运行
复制
#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)

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

代码语言:javascript
代码运行次数:0
运行
复制
#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++允许我们为自定义类型重载运算符,这也是一种编译时多态的表现。

代码语言:javascript
代码运行次数:0
运行
复制
#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. 总结

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

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

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

问题拓展

其他文章的链接还在编译中……

欢迎关注、点赞、收藏!更多系列内容可以点击专栏目录订阅,感谢支持,再次祝大家祉猷并茂,顺遂无虞

若将文章用作它处,请一定注明出处,商用请私信联系我!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 个人理解
  • 前言
  • 封装是什么?
    • 1. 面向过程编程(Procedural-Oriented Programming,POP) vs 面向对象编程(Object-Oriented Programming,OOP)
    • 2. 封装的核心概念
    • 3. 如何实现封装
    • 4. 封装的底层实现
    • 5. 总结
  • 继承是什么
    • 1. 继承的基本概念
    • 2. 继承的主要作用
    • 3. 如何实现继承
    • 4. 继承的底层实现
    • 5. 继承的类型
    • 6. 继承的优缺点
    • 7. 总结
  • 多态是什么
    • 1. 多态的基本概念
    • 2. 运行时多态与虚函数
    • 3. 编译时多态与函数重载
    • 4. 多态的底层实现
    • 5. 总结
  • 问题拓展
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档