首页 > 系统相关 >C++17 多态内存管理 pmr

C++17 多态内存管理 pmr

时间:2024-11-10 18:46:00浏览次数:3  
标签:std resource 17 pmr 多态 内存 pool string

C++17 多态内存管理 pmr

概念

C++17开始,增加特性 Polymorphic Memory Resources 多态内存资源,缩写 PMR。

提供新的内存分配策略,更灵活地控制内存的分配与回收——适用于嵌入式和高并发服务器场景。

  • 内存资源的抽象

    抽象基类std::pmr::memory_resource 定义了用于内存的分配和回收的接口

  • 运行时灵活性

    • 能够在运行时更改容器内存分配策略
      • 允许选择 / 实现自定义的std::pmr::memory_resource 内存资源管理类
      • 可将memory_resource作为参数传递给基于polymorpical_allocator的特化容器使用
    • 与标准的 std::allocator 不同,能够在不同类型的容器实例之间传递
  • 性能和功能上的改进

    • 合适的内存分配策略,可以减少系统调用 —— 例如 内存池的思想,避免malloc每次分配时的系统调用。

    • 存在方法以确保分配的内存连续,有利于使用到CPU缓存

    • 重定向堆内存调用,从而复用栈上分配的内存

    • 提供了实现其它功能的自由度(缓冲区管理、内存池、共享内存场景…)

      例如:把容器和元素放在可以被多个进程访问的共享内存

      使用特殊内存区域(如 NUMA 节点)、跟踪和分析内存使用、提供预分配的内存池、实现垃圾回收机制、提供线程本地存储以优化并发

在没有pmr的时代,使用STL容器有时会出现内存分配效率低的问题。例如:std::list 每个节点都会指向下一个节点,每次推入新元素都需要malloc一次。

C++17 为了便捷的支持预定义的和用户定义的内存分配方式,引入了名称空间std::pmr ,其包含一系列使用PMR的容器。现在标准库的所有容器都可以设置分配的内存池,并且具有友好的语法糖。

例如std::pmr::unordered_mapstd::unordered_map 的一个特化版本

  • 使用了多态分配器 (std::pmr::polymorphic_allocator)

  • 可以使用外部提供的任意 std::pmr::memory_resource 实例。

pmr基本使用

不使用 pmr 容器

#include <iostream>
#include <string>
#include <vector>
#include "../lang/tracknew.hpp"
/* 不使用 pmr,需要 1018 次分配堆内存 */
// - vector扩容是非常耗资源的事情
// - 每一个没有使用短字符串优化的 string还要分配一次
// 共需要 1018 次分配堆内存
TrackNew::reset();
std::vector<std::string> coll;
for (int i = 0; i < 1000; ++i) {
    coll.emplace_back("just a non‐SSO string");
}
TrackNew::status();

vector resize:为内存(重新)分配在一些环境中需要很长时间(例如嵌入式系统),完全在堆上分配内存可能会导致性能问题。

  • 事先知道要存储的元素的数量 —— 最最理想
  • 重新分配内存是不可避免的 —— 大多数情况
  • 在避免重新分配和不想浪费太多内存之间权衡

使用pmr容器

当缓冲区没有足够的内存时还会在堆上分配新的内存。

非pmr元素
/* 使用 PMR - monotopic_buffer_resource */
// vector的 18 次内存分配将不会再发生在堆上,而是在初始化的 buf里发生
// 共需要 1000 次分配堆内存
TrackNew::reset();
// - 在栈上分配一些内存, 用作pmr容器的初始内存池
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
// - 使用 std::pmr 容器 std::pmr::vector
// 实际类型 std::vector<std::string, std::pmr::polymorphic_allocator <std::string>>
std::pmr::vector<std::string> coll{&pool};
for (int i = 0; i < 1000; ++i) {
    coll.emplace_back("just a non‐SSO string");
}
TrackNew::status();
pmr元素

pmr vector 会尝试把它的分配器传播给它的元素

  1. 当元素使用多态分配器时(例如std::pmr::string类型),传播不会失败。
  2. 当元素并不使用多态分配器(std::string),SFINE 匹配失败,无事发生。

std::pmr::string类型 和 std::string类型之间有显式的转换,但没有隐式的转换

支持显式转换是因为所有的字符串都可以隐式转换为 std::string_view字符串视图,std::string_view又可以显式转换为任何字符串类型。

/* 使用 PMR - monotopic_buffer_resource */
TrackNew::reset();
// - 在栈上分配一些内存, 用作pmr容器的初始内存池
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
// 元素类型改为 std::pmr::string来避免任何内存分配
std::pmr::vector<std::pmr::string> coll{&pool};
for (int i = 0; i < 1000; ++i) {
    coll.emplace_back("just a non‐SSO string");
}
TrackNew::status();

标准内存资源

内存资源都以指针的形式传递。必须保证指针指向的资源对象直到最后释放内存之前都一直有效。

如果到处 move对象并且内存资源可互换,可能导致最后释放内存的时机比预期的更晚。

总览

为了支持多态分配器,C++标准库提供了如下内存资源:

三个 pmr 分配器

  • 都是类,继承自抽象基类std::pmr::memory_resource

  • 必须 1.创建分配器对象,2.把分配器对象指针传递给目标pmr对象

memory_resource派生类效率线程安全性内存
monotopic_buffer_resource效率最高线程不安全“只进不出”(从不释放、可传递进可选的缓冲区)
unsynchronized_pool_resource效率较高(内部不需要上锁)线程不安全(各线程所私有)更少碎片化
synchronized_pool_resource效率低(内部需要上锁)线程安全(允许多线程共享)更少碎片化

两个返回指向单例全局内存资源的指针的函数

返回指向单例全局内存资源的指针特点
new_delete_resouece()函数默认的内存资源(转发给传统 new/delete)
null_memory_resource()函数“永远拒绝”
/* 3个 memory_resource派生类 */
// 1. std::pmr::monotonic_buffer_resource
// - 创建不释放内存、按块分配内存、指定地址和大小 的内存池
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool1{buf.data(), buf.size()};
// - 创建不释放内存、按块分配内存、未指定初始大小 的内存池
std::pmr::monotonic_buffer_resource pool1;
// - 创建不释放内存、按块分配内存、初始大小为10k 的内存池
std::pmr::monotonic_buffer_resource pool1{10000};
std::pmr::string s1{"my string", &pool1};

// 2. std::pmr::synchronized_pool_resource
// - 创建以很小的内存碎片分配内存、线程安全 的内存池
std::pmr::synchronized_pool_resource pool2;
// - 创建以很小的内存碎片分配内存、线程安全、不释放内存、按块分配内存、初始大小为10k 的内存池
std::pmr::synchronized_pool_resource pool2{&pool1};
std::pmr::string s2{"my string", &pool2};

// 2. std::pmr::unsynchronized_pool_resource
// - 创建以很小的内存碎片分配内存、线程不安全 的内存池
std::pmr::unsynchronized_pool_resource pool3;
std::pmr::string s2{"my string", &pool3};

/* 3个 返回指向单例全局内存资源的指针的函数 */
std::pmr::string s1{"my string", std::pmr::new_delete_resource()};
std::pmr::string s2{"my string", std::pmr::null_memory_resourcenew_delete_resouece()};

monotonic_buffer_resource

monotonic_buffer_resource 可以减少分配和释放操作,从而提高性能。

往往是栈上临时缓冲区,短生命周期

使用情景:当有大量的对象需要分配,且预知它们具有相似的生命周期时。

原理:在一个单调增长的内存块上进行内存分配,一直向上分配内存。当有分配内存的请求时,它会返回下一块剩余的内存,直到所有的内存都被消耗光。

  • 分配:

    • 传递一个缓冲区用作内存 —— 特别的,可被用来在栈上分配内存
    • 再从该固定大小的缓冲区分配内存
  • 不是线程安全的

    • 需要确保对它的访问是同步的。

    • 在使用线程池或并发场景时,要为每个线程分配一个单独的 monotonic_buffer_resource,或者使用锁或其他同步机制来保证线程安全。

  • 回收:内存资源从来不会释放直到整个资源作为整体被一起释放。

    • 不能单独释放单个对象,只有一次性释放所有资源。

    • 存在超出内存池的可能性。超出作用域时,所有通过它分配的内存都会被释放

      • 内存池足够时

        • 不需要任何额外的内存分配
        • 内存池销毁后,内存(栈上或堆上)可被重新使用
      • 内存超限时

        • 会在堆里分配额外的内存
        • 内存池销毁后,内存(栈上或堆上)也被释放
    • 非常快速,因为释放操作实际上什么也不做,不需要为了复用而追踪被释放的内存

  • 可用于让所有的内存资源跳过释放操作(我非常不建议这么使用)

    • 存在 无初始内存起始地址,无内存大小 的重载版本

      使用默认构造时会作用于默认内存资源(即new_delete_resource()的返回值)

    • 存在 无初始内存起始地址,有内存大小 的重载版本

      允许传递一个初始的大小,被用作第一次内存分配的最小值

      使用默认构造时会作用于默认内存资源(即new_delete_resource()的返回值)

// 限定一个作用域
{ // 使用默认的内存资源但是在池销毁之前跳过释放操作
    // - 创建不释放内存、按块分配内存、未指定初始大小 的内存池
    std::pmr::monotonic_buffer_resource pool2;
    // - 创建不释放内存、按块分配内存、初始大小为10k 的内存池
    // std::pmr::monotonic_buffer_resource pool3{10000};
    // - 创建不释放内存、按块分配内存、初始大小为10k 、以很小的内存碎片分配内存 的内存池
    // std::pmr::synchronized_pool_resource pool4{&pool3};
    std::pmr::vector<std::pmr::string> coll{&pool};
    for (int j = 0; j < 100; ++j) {
        std::pmr::vector<std::pmr::string> coll{&pool};
        for (int i = 0; i < 100; ++i) {
            coll.emplace_back("just a non‐SSO string");
        }
        coll.clear(); // 销毁元素但不会释放内存
    } // 底层的池收到了释放操作,但不会释放内存
    // 到此为止,没有释放任何内存
} // 释放所有分配的内存

(un)synchronized_poo_resource

synchronized_pool_resource和 unsynchronized_pool_resource

  • 更少碎片化

    • (un)synchronized_poo_resource会尝试在相邻位置分配所有内存的内存资源类,可以尽可能的减小内存碎片

    • 当池被销毁时它们会释放所有内存

    • 一个应用方向:

      • 可以保证以节点为单位的容器里的元素能相邻排列

      • 这也许能显著提高容器的性能——缓存命中

        实际的性能依赖于内存资源的实现。例如,如果内存资源使用了互斥量来同步内存访问,性能可能会变得非常差。

  • 线程安全性(区别)

    • synchronized_pool_resource 线程安全(会影响性能)
    • unsynchronized_pool_resource 线程不安全
  • 封装默认的内存资源

    • 这两个类从底层来看,实际的分配和释放操作都使用了默认的内存资源
    • 它们只是保证内存分配更加密集的包装。
/* 这两个类使用默认的内存资源进行实际的分配和释放操作 */
std::pmr::synchronized_pool_resource myPool_sync;
// 等价于
std::pmr::synchronized_pool_resource myPool_sync{std::pmr::get_default_resource()};

std::pmr::unsynchronized_pool_resource myPool_unsync;
// 等价于
std::pmr::unsynchronized_pool_resource myPool_unsync{std::pmr::get_default_resource()};

new_delete_resource()

new_delete_resource() 的返回值

  • 是默认的内存资源

    不做任何修改的情况下get_default_resource()的返回值就是new_delete_resource()

  • 处理内存分配的方式和普通的分配器相同(转发给传统 new/delete)

    • 每次分配内存会调用 new
    • 每次释放内存会调用 delete

持有这种内存资源的多态分配器不能和默认的分配器互换,因为它们的类型不同。

std::string s{"my string with some value"};
// 不会发生 move(直接把 s分配的内存转让给 ps)
// 而是把 s的内存拷贝到 ps内部用 new分配的新的内存中
std::pmr::string ps{std::move(s), std::pmr::new_delete_resource()};

null_memory_resource()

null_memory_resource(),对分配操作进行处理,让每一次分配都抛出 bad_alloc异常。

最主要的应用:对于在栈上分配的内存的池

  • 确保其不会突然在堆上分配额外的内存
  • 任何尝试分配更多堆内存的行为都会抛出异常

详情见下一章节 常用情景 - 避免误用堆内存

常用情景

修改默认内存资源

若未给pmr容器 传递内存资源,会使用多态分配器的默认内存资源

默认内存资源的操作行为
std::pmr::get_default_resource()返回指向当前默认内存资源的指针
std::pmr::set_default_resource()设置默认内存资源,返回之前的内存资源的指针
// std::pmr::get_default_resource() 获取当前的默认资源
// 向默认资源传递多态分配器,从而进行初始化
//  - 1.初始化内存资源
static std::pmr::synchronized_pool_resource myPool;
// std::pmr::set_default_resource() 设置默认内存资源
//  - 2.设置新的默认内存资源
// 之前的默认内存资源可能仍然会被使用,即使它已经被替换掉。
std::pmr::memory_resource* old = std::pmr::set_default_resource(&myPool);
//  - 3.恢复旧的默认内存资源
std::pmr::set_default_resource(old)

打算在程序中设置了自定义内存资源并且把它用作默认资源

  • 直接在 main()中首先将它创建为 static对象
int main() {
    static std::pmr::synchronized_pool_resource myPool;
    // ...
}
  • 提供返回静态资源的全局函数
memory_resource* myResource() {
    static std::pmr::synchronized_pool_resource myPool;
    return &myPool;
}

嵌套内存池

pmr 分配器之间可以设置“上游内存池”,实现级连内存池。

// 限定一个作用域
{ // 在池销毁之前跳过释放操作
    // - 创建不释放内存、按块分配内存、初始大小为10k 的内存池
    std::pmr::monotonic_buffer_resource keepAllocatedPool{10000};
    // 创建嵌套内存池
    // - 级联不释放内存、按块分配内存、初始大小为10k 的内存池
    // - 以很小的内存碎片分配内存、线程安全(会影响性能) 的内存池
    std::pmr::synchronized_pool_resource pool{&keepAllocatedPool};
    std::pmr::vector<std::pmr::string> coll{&pool};
    for (int j = 0; j < 100; ++j) {
        std::pmr::vector<std::pmr::string> coll{&pool};
        for (int i = 0; i < 100; ++i) {
            coll.emplace_back("just a non‐SSO string");
        }
    }
    // 到此为止,没有释放任何内存
} // 释放所有分配的内存

复用内存池

通过复用std::pmr::monotonic_buffer_resource内存池,可以只分配一次内存(在栈上或堆上)然后在每一个新任务里(服务请求、事件、处理数据文件等等)都复用这块内存。

但有个问题,内存超限时会额外申请堆内存,这可能会导致内存泄漏。解决方法见下一节 常用情景 - 避免误用堆内存

内存超限时,会在堆里分配额外的内存,内存池销毁后,内存(栈上或堆上)也被释放

// - 在栈上分配一些内存, 用作pmr容器的初始内存池
std::array<std::byte, 200000> buf;
// - 再次提醒:当缓冲区没有足够的内存时还会在堆上分配新的内存
for (int num : {1000, 2000, 500, 2000, 3000, 50000, 1000}) {
    std::cout << "‐‐ check with " << num << " elements:\n";
    TrackNew::reset();
    std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
    std::pmr::vector<std::pmr::string> coll{&pool};
    for (int i = 0; i < num; ++i) {
        coll.empalce_back("just a non‐SSO string");
    }
    TrackNew::status();
}

避免误用堆内存

std::pmr::null_memory_resource()最主要的应用:对于在栈上分配的内存的池

  • 确保其不会突然在堆上分配额外的内存
  • 任何尝试分配更多堆内存的行为都会抛出异常
int main()
{
    // 创建内存池:
    //  - 使用栈上的内存
    //  - 不用堆作为备选项(备选内存资源)
    std::array<std::byte, 200000> buf;
    std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size(),
                                             std::pmr::null_memory_resource()};
    // 故意申请过量的堆内存
    sdt::pmr::unordered_map <long, std::pmr::string> coll{&poll};
    try {
        for (int i = 0; i < buf.size(); ++i) {
            std::string s{"Customer" + std::to_string(i)};
            coll.emplace(i, s);
        }
    }
    catch (const std::bad_alloc& e) {
        std::cerr << "BAD ALLOC EXCEPTION: " << e.what() << '\n';
    }
    std::cout << "size: " << coll.size() << '\n';
}

自定义

内存资源

提供自定义内存资源,需要

  1. 从 std::pmr::memory_resource派生

  2. 实现下列私有函数:

    • 分配内存

      void* do_allocate(size_t bytes, size_t alignment)

    • 释放内存

      void do_deallocate(void* ptr, size_t bytes, size_t alignment)

    • 什么情况下、何时,两个内存资源可以交换分配的内存

      (即一个多态内存资源对象是否以及何时可以释放另一个多态内存资源对象分配的内存)

      bool do_is_equal(const std::pmr::memory_resource& other)

#include <iostream>
#include <string>
#include <memory_resource >
class Tracker : public std::pmr::memory_resource
{
private:
    std::pmr::memory_resource *upstream; // 被包装的内存资源
    std::string prefix{};
public:
    // 包装 传入的/默认的 资源:
    explicit Tracker(std::pmr::memory_resource *us
                     = std::pmr::get_default_resource()) : upstream{us} {
    }
    explicit Tracker(std::string p, std::pmr::memory_resource *us
                     = std::pmr::get_default_resource()) : upstream{us}, prefix{std::move(p)} {
    }
private:
    // 分配内存
    void* do_allocate(size_t bytes, size_t alignment) override {
        std::cout << prefix << "allocate " << bytes << " Bytes\n";
        void* ret = upstream‐>allocate(bytes, alignment);
        return ret;
    }
    // 释放内存
    void do_deallocate(void* ptr, size_t bytes, size_t alignment) override {
        std::cout << prefix << "deallocate " << bytes << " Bytes\n";
        upstream‐>deallocate(ptr, bytes, alignment);
    }
    // 何时该类型可与其他内存资源对象交换分配的内存
    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        // 判断是否是同一个memory_resource对象
        if (this == &other)
            return true;
        // 判断是否是相同的类型并且prefix和upstream都相等
        auto op = dynamic_cast<const Tracker*>(&other);
        return op != nullptr && op‐>prefix == prefix && upstream‐>is_equal(other);
    }
};

自定义PMR类型

为自定义类型提供内存资源支持

怎么保证自己的自定义类型支持多态分配器,从而保证它们能像 pmr::string一样作为一个 pmr容器的元素?

  1. 指明这个类型支持多态分配器——提供类型 allocator_type的声明

    定义一个 public成员 allocator_type作为一个多态分配器

  2. 为所有构造函数添加一个接受分配器作为额外参数的重载(包括拷贝和移动构造函数)

  3. 给初始化用的构造函数的分配器参数添加一个默认的 allocator_type类型的值(不包括拷贝和移动构造函数)

#include <string>
#include <memeory_resource >
// 支持多态分配器的顾客类型
// 分配器存储在字符串成员中
class PmrCustomer
{
private:
    std::pmr::string name; // 可以用来存储分配器
public:
    using allocator_type = std::pmr::polymorphic_allocator <char>;
    // 初始化构造函数
    PmrCustomer(std::pmr::string n, allocator_type alloc = {}) : name{std::move(n), alloc} {
    }
    // 带有分配器的移动构造函数
    PmrCustomer(const PmrCustomer& c, allocator_type alloc) : name{c.name, alloc} {
    }
    PmrCustomer(PmrCustomer&& c, allocator_type alloc) : name{std::move(c.name), alloc} {
    }
    // setter/getter:
    void setName(std::pmr::string s) {
        name = std::move(s);
    }
    std::pmr::string getName() const {
        return name;
    }
    std::string getNameAsString() const {
        return std::string{name};
    }
};

使用PMR类型(定义PMR容器)

#include "pmrcustomer.hpp"
#include "tracker.hpp"
#include <vector>
int main()
{
    Tracker tracker;
    std::pmr::vector<PmrCustomer > coll(&tracker);
    coll.reserve(100); // 使用tracker分 配
    PmrCustomer c1{"Peter, Paul & Mary"}; // 使用get_default_resource()分配
    coll.push_back(c1); // 使用vector的分配器(tracker)分配
    coll.push_back(std::move(c1)); // 拷贝(分配器不可交换)
    for (const auto& cust : coll) {
        std::cout << cust.getName() << '\n';
    }
}

标签:std,resource,17,pmr,多态,内存,pool,string
From: https://blog.csdn.net/weixin_41733034/article/details/143664598

相关文章

  • [JXOI2017] 加法 题解
    最小值最大,考虑二分答案,问题转为判断最小值是否能\(\gex\)。假如\(a_i\gex\),那我们肯定不管;假如\(a_i<x\),那最好能让选择的区间\(r\)值更大,用优先队列维护即可。区间增幅可以用树状数组维护。时间复杂度\(O(n\log^2n)\)。#include<bits/stdc++.h>#defineintlonglon......
  • 学期2024-2025-1 学号20241317 《计算机基础与程序设计》第七周学习总结
    学期2024-2025-学号20241317《计算机基础与程序设计》第七周学习总结作业信息这个作业属于哪个课程<班级的链接>(如2024-2025-1-计算机基础与程序设计)这个作业要求在哪里<作业要求的链接>(如2024-2025-1计算机基础与程序设计第一周作业)这个作业的目标<写上具体......
  • 3174 含K个3的数( oj )
    描述输入两个正整数m和k。判断m能否被19整除,且恰好含有k个3。如果满足条件,则输出YES;否则,输出NO。例如,输入:438333,满足条件,输出YES。如果输入:393313,尽管有3个3,但不能被19整除,也不满足条件,应输出NO。输入描述m和k的值,中间用单个空格间隔。输出描述满足条件时输出"YES",......
  • 2024-2025-1 20241417 《计算机基础与程序设计》第七周学习总结
    作业信息这个作业属于哪个课程2024-2025-1-计算机基础与程序设计这个作业要求在哪里2024-2025-1计算机基础与程序设计第七周作业这个作业的目标<数组与链表,基于数组和基于链表实现数据结构,无序表与有序表,树,图,子程序与参数>作业正文https://www.cnblogs.com/lry......
  • chapter17
    malloc.py参数中文版第一题问题1.首先运行flag-n10-H0-pBEST-s0来产生一些随机分配和释放。你能预测malloc()/free()会返回什么吗?你可以在每次请求后猜测空闲列表的状态吗?随着时间的推移,你对空闲列表有什么发现?空闲列表不会合并,导致外部碎片越来越多第二题......
  • Unity类银河战士恶魔城学习总结(P117 Ice And Fire Item Effec 制作一把冰火属性的剑)
    【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili教程源地址:https://www.udemy.com/course/2d-rpg-alexdev/制作一把双重属性的剑,我取名为冰与火之歌 IceAndFire_Effect.cs这个脚本的作用是在玩家第三次攻击时生成一个冰火技能预制体,预制体会根据玩家的朝向......
  • 代码随想录算法训练营第十七天| 654.最大二叉树 , 617.合并二叉树 , 700.二叉搜索树中的
    654.最大二叉树文章链接:https://programmercarl.com/0654.最大二叉树.html题目链接:https://leetcode.cn/problems/maximum-binary-tree/description/classSolution{public:TreeNode*traversal(vector<int>&nums,intleft,intright){if(left>=right)ret......
  • 【漏洞复现】通达OA 2013、2016、2017 header.inc.php 任意用户登陆漏洞
    免责声明:        本文旨在提供有关特定漏洞的信息,以帮助用户了解潜在风险。发布此信息旨在促进网络安全意识和技术进步,并非出于恶意。读者应理解,利用本文提到的漏洞或进行相关测试可能违反法律或服务协议。未经授权访问系统、网络或应用程序可能导致法律责任或严......
  • 深入Java多态机制:从原理到实现
    目录1.什么是多态?2.如何在Java中实现多态?2.1方法重写实现多态2.2接口实现多态3.Java接口中方法实现的支持3.1默认方法4.总结多态(Polymorphism)是面向对象编程(OOP)的核心概念之一。多态允许对象在不同的上下文中执行不同的行为,即同一操作可以在不同的对象中产生不......
  • c++学习:封装继承多态
    目录封装封装的定义封装的好处封装的实例继承继承的定义继承的好处继承的实例多态多态的定义多态的好处多态的实例封装封装的定义封装是面向对象编程(OOP)中的一个核心概念,它指的是将数据(属性)和操作这些数据的函数(方法)结合在一起的过程,以此来模拟现实世界中的实......