c语言内存池的实现原理,c语言内存布局

一、为什么需要使用内存池

在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。

一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;

另一方面,频繁的分配和释放小块内存会导致大量的内存碎片的产生,当碎片积累到一定的量之后,将无法分配到连续的内存空间,系统不得不进行碎片整理来满足分配到连续的空间,这样不仅会导致系统性能损耗,而且会导致程序对内存的利用率低下。

当然,如果我们的程序不需要频繁的分配和释放小块内存,那就没有使用内存池的必要,直接使用malloc,free或new,delete函数即可。

二、内存池的实现方案

内存池的实现原理大致如下:

提前申请一块大内存由内存池自己管理,并分成小片供给程序使用。程序使用完之后将内存归还到内存池中(并没有真正的从系统释放),当程序再次从内存池中请求内存时,内存池将池子中的可用内存片返回给程序使用。

我们在设计内存池的实现方案时,需要考虑到以下问题:

内存池是否可以自动增长?

如果内存池的最大空间是固定的(也就是非自动增长),那么当内存池中的内存被请求完之后,程序就无法再次从内存池请求到内存。所以需要根据程序对内存的实际使用情况来确定是否需要自动增长。

内存池的总内存占用是否只增不减?

如果内存池是自动增长的,就涉及到了“内存池的总内存占用是否是只增不减”这个问题了。试想,程序从一个自动增长的内存池中请求了1000个大小为100KB的内存片,并在使用完之后全部归还给了内存池,而且假设程序之后的逻辑最多之后请求10个100KB的内存片,那么该内存池中的900个100KB的内存片就一直处于闲置状态,程序的内存占用就一直不会降下来。对内存占用大小有要求的程序需要考虑到这一点。

内存池中内存片的大小是否固定?

如果每次从内存池中的请求的内存片的大小如果不固定,那么内存池中的每个可用内存片的大小就不一致,程序再次请求内存片的时候,内存池就需要在“匹配最佳大小的内存片”和“匹配操作时间”上作出衡量。“最佳大小的内存片”虽然可以减少内存的浪费,但可能会导致“匹配时间”变长。

内存池是否是线程安全的?

是否允许在多个线程中同时从同一个内存池中请求和归还内存片?这个线程安全可以由内存池来实现,也可以由使用者来保证。

内存片分配出去之前和归还到内存池之后,其中的内容是否需要被清除?

程序可能出现将内存片归还给内存池之后,仍然使用内存片的地址指针进行内存读写操作,这样就会导致不可预期的结果。将内容清零只能尽量的(也不一定能)将问题抛出来,但并不能解决任何问题,而且将内容清零会消耗一定的CPU时间。所以,最终最好还是需要由内存池的使用者来保证这种安全性。

是否兼容std::allocator?

STL标准库中的大多类都支持用户提供一个自定义的内存分配器,默认使用的是std::allocator,如std::string:

typedef basic_string<char, char_traits<char>, allocator<char> > string;

如果我们的内存池兼容std::allocator,那么我们就可以使用我们自己的内存池来替换默认的std::allocator分配器,如:

typedef basic_string<char, char_traits<char>, MemoryPoll<char> > mystring;

更多Linux内核视频教程资料文档私信【内核大礼包】自行获取。

 

三、内存池的具体实现

计划实现一个内存池管理的类MemoryPool,它具有如下特性:

  1. 内存池的总大小自动增长。
  2. 内存池中内存片的大小固定。
  3. 支持线程安全。
  4. 在内存片被归还之后,清除其中的内容。
  5. 兼容std::allocator。

因为内存池的内存片的大小是固定的,不涉及到需要匹配最合适大小的内存片,由于会频繁的进行插入、移除的操作,但查找比较少,故选用链表数据结构来管理内存池中的内存片。

MemoryPool中有2个链表,它们都是双向链表(设计成双向链表主要是为了在移除指定元素时,能够快速定位该元素的前后元素,从而在该元素被移除后,将其前后元素连接起来,保证链表的完整性):

  1. data_element_ 记录以及分配出去的内存片。
  2. free_element_ 记录未被分配出去的内存片。

MemoryPool实现代码

代码中使用了std::mutex等C++11才支持的特性,所以需要编译器最低支持C++11:

#ifndef PPX_BASE_MEMORY_POOL_H_

#define PPX_BASE_MEMORY_POOL_H_



#include <climits>

#include <cstddef>

#include <mutex>



namespace ppx {

    namespace base {

        template <typename T, size_t BlockSize = 4096, bool ZeroOnDeallocate = true>

        class MemoryPool {

        public:

            /* Member types */

            typedef T               value_type;

            typedef T*              pointer;

            typedef T&              reference;

            typedef const T*        const_pointer;

            typedef const T&        const_reference;

            typedef size_t          size_type;

            typedef ptrdiff_t       difference_type;

            typedef std::false_type propagate_on_container_copy_assignment;

            typedef std::true_type  propagate_on_container_move_assignment;

            typedef std::true_type  propagate_on_container_swap;



            template <typename U> struct rebind {

                typedef MemoryPool<U> other;

            };



            /* Member functions */

            MemoryPool() noexcept;

            MemoryPool(const MemoryPool& memoryPool) noexcept;

            MemoryPool(MemoryPool&& memoryPool) noexcept;

            template <class U> MemoryPool(const MemoryPool<U>& memoryPool) noexcept;



            ~MemoryPool() noexcept;



            MemoryPool& operator=(const MemoryPool& memoryPool) = delete;

            MemoryPool& operator=(MemoryPool&& memoryPool) noexcept;



            pointer address(reference x) const noexcept;

            const_pointer address(const_reference x) const noexcept;



            // Can only allocate one object at a time. n and hint are ignored

            pointer allocate(size_type n = 1, const_pointer hint = 0);

            void deallocate(pointer p, size_type n = 1);



            size_type max_size() const noexcept;



            template <class U, class... Args> void construct(U* p, Args&&... args);

            template <class U> void destroy(U* p);



            template <class... Args> pointer newElement(Args&&... args);

            void deleteElement(pointer p);



        private:

            struct Element_ {

                Element_* pre;

                Element_* next;

            };



            typedef char* data_pointer;

            typedef Element_ element_type;

            typedef Element_* element_pointer;



            element_pointer data_element_;

            element_pointer free_element_;



            std::recursive_mutex m_;



            size_type padPointer(data_pointer p, size_type align) const noexcept;

            void allocateBlock();



            static_assert(BlockSize >= 2 * sizeof(element_type), "BlockSize too small.");

        };





        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::size_type

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::padPointer(data_pointer p, size_type align)

            const noexcept {

            uintptr_t result = reinterpret_cast<uintptr_t>(p);

            return ((align - result) % align);

        }







        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool()

            noexcept {

            data_element_ = nullptr;

            free_element_ = nullptr;

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(const MemoryPool& memoryPool)

            noexcept :

            MemoryPool() {

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(MemoryPool&& memoryPool)

            noexcept {

            std::lock_guard<std::recursive_mutex> lock(m_);



            data_element_ = memoryPool.data_element_;

            memoryPool.data_element_ = nullptr;

            free_element_ = memoryPool.free_element_;

            memoryPool.free_element_ = nullptr;

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        template<class U>

        MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(const MemoryPool<U>& memoryPool)

            noexcept :

            MemoryPool() {

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        MemoryPool<T, BlockSize, ZeroOnDeallocate>&

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::operator=(MemoryPool&& memoryPool)

            noexcept {

            std::lock_guard<std::recursive_mutex> lock(m_);



            if (this != &memoryPool) {

                std::swap(data_element_, memoryPool.data_element_);

                std::swap(free_element_, memoryPool.free_element_);

            }

            return *this;

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        MemoryPool<T, BlockSize, ZeroOnDeallocate>::~MemoryPool()

            noexcept {

            std::lock_guard<std::recursive_mutex> lock(m_);



            element_pointer curr = data_element_;

            while (curr != nullptr) {

                element_pointer prev = curr->next;

                operator delete(reinterpret_cast<void*>(curr));

                curr = prev;

            }



            curr = free_element_;

            while (curr != nullptr) {

                element_pointer prev = curr->next;

                operator delete(reinterpret_cast<void*>(curr));

                curr = prev;

            }

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::address(reference x)

            const noexcept {

            return &x;

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::const_pointer

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::address(const_reference x)

            const noexcept {

            return &x;

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        void

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::allocateBlock() {

            // Allocate space for the new block and store a pointer to the previous one

            data_pointer new_block = reinterpret_cast<data_pointer> (operator new(BlockSize));

            element_pointer new_ele_pointer = reinterpret_cast<element_pointer>(new_block);

            new_ele_pointer->pre = nullptr;

            new_ele_pointer->next = nullptr;



            if (data_element_) {

                data_element_->pre = new_ele_pointer;

            }



            new_ele_pointer->next = data_element_;

            data_element_ = new_ele_pointer;

        }



        template <typename T, size_t BlockSize,  bool ZeroOnDeallocate>

        inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::allocate(size_type n, const_pointer hint) {

            std::lock_guard<std::recursive_mutex> lock(m_);



            if (free_element_ != nullptr) {

                data_pointer body =

                    reinterpret_cast<data_pointer>(reinterpret_cast<data_pointer>(free_element_) + sizeof(element_type));



                size_type bodyPadding = padPointer(body, alignof(element_type));



                pointer result = reinterpret_cast<pointer>(reinterpret_cast<data_pointer>(body + bodyPadding));



                element_pointer tmp = free_element_;



                free_element_ = free_element_->next;



                if (free_element_)

                    free_element_->pre = nullptr;



                tmp->next = data_element_;

                if (data_element_)

                    data_element_->pre = tmp;

                tmp->pre = nullptr;

                data_element_ = tmp;



                return result;

            }

            else {

                allocateBlock();



                data_pointer body =

                    reinterpret_cast<data_pointer>(reinterpret_cast<data_pointer>(data_element_) + sizeof(element_type));



                size_type bodyPadding = padPointer(body, alignof(element_type));



                pointer result = reinterpret_cast<pointer>(reinterpret_cast<data_pointer>(body + bodyPadding));



                return result;

            }

        }



        template <typename T, size_t BlockSize, bool ZeroOnDeallocate>

        inline void

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::deallocate(pointer p, size_type n) {

            std::lock_guard<std::recursive_mutex> lock(m_);



            if (p != nullptr) {

                element_pointer ele_p =

                    reinterpret_cast<element_pointer>(reinterpret_cast<data_pointer>(p) - sizeof(element_type));



                if (ZeroOnDeallocate) {

                    memset(reinterpret_cast<data_pointer>(p), 0, BlockSize - sizeof(element_type));

                }



                if (ele_p->pre) {

                    ele_p->pre->next = ele_p->next;

                }



                if (ele_p->next) {

                    ele_p->next->pre = ele_p->pre;

                }



                if (ele_p->pre == nullptr) {

                    data_element_ = ele_p->next;

                }



                ele_p->pre = nullptr;

                if (free_element_) {

                    ele_p->next = free_element_;

                    free_element_->pre = ele_p;

                }

                else {

                    ele_p->next = nullptr;

                }

                free_element_ = ele_p;

            }

        }



        template <typename T, size_t BlockSize, bool ZeroOnDeallocate>

        inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::size_type

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::max_size()

            const noexcept {

            size_type maxBlocks = -1 / BlockSize;

            return (BlockSize - sizeof(data_pointer)) / sizeof(element_type) * maxBlocks;

        }



        template <typename T, size_t BlockSize, bool ZeroOnDeallocate>

        template <class U, class... Args>

        inline void

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::construct(U* p, Args&&... args) {

            new (p) U(std::forward<Args>(args)...);

        }



        template <typename T, size_t BlockSize, bool ZeroOnDeallocate>

        template <class U>

        inline void

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::destroy(U* p) {

            p->~U();

        }



        template <typename T, size_t BlockSize, bool ZeroOnDeallocate>

        template <class... Args>

        inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::newElement(Args&&... args) {

            std::lock_guard<std::recursive_mutex> lock(m_);

            pointer result = allocate();

            construct<value_type>(result, std::forward<Args>(args)...);

            return result;

        }



        template <typename T, size_t BlockSize, bool ZeroOnDeallocate>

        inline void

            MemoryPool<T, BlockSize, ZeroOnDeallocate>::deleteElement(pointer p) {

            std::lock_guard<std::recursive_mutex> lock(m_);

            if (p != nullptr) {

                p->~value_type();

                deallocate(p);

            }

        }

    }

}



#endif // PPX_BASE_MEMORY_POOL_H_



使用示例:

#include <iostream>

#include <thread>

using namespace std;







class Apple {

public:

    Apple() {

        id_ = 0;

        cout << "Apple()" << endl;

    }



    Apple(int id) {

        id_ = id;

        cout << "Apple(" << id_ << ")" << endl;

    }



    ~Apple() {

        cout << "~Apple()" << endl;

    }



    void SetId(int id) {

        id_ = id;

    }



    int GetId() {

        return id_;

    }

private:

    int id_;

};







void ThreadProc(ppx::base::MemoryPool<char> *mp) {

    int i = 0;

    while (i++ < 100000) {

        char* p0 = (char*)mp->allocate();



        char* p1 = (char*)mp->allocate();



        mp->deallocate(p0);



        char* p2 = (char*)mp->allocate();



        mp->deallocate(p1);

        

        mp->deallocate(p2);



    }

}



int main()

{

    ppx::base::MemoryPool<char> mp;

    int i = 0;

    while (i++ < 100000) {

        char* p0 = (char*)mp.allocate();



        char* p1 = (char*)mp.allocate();



        mp.deallocate(p0);



        char* p2 = (char*)mp.allocate();



        mp.deallocate(p1);



        mp.deallocate(p2);



    }



    std::thread th0(ThreadProc, &mp);

    std::thread th1(ThreadProc, &mp);

    std::thread th2(ThreadProc, &mp);



    th0.join();

    th1.join();

    th2.join();



    Apple *apple = nullptr;

    {

        ppx::base::MemoryPool<Apple> mp2;

        apple = mp2.newElement(10);

        int a = apple->GetId();

        apple->SetId(10);

        a = apple->GetId();



        mp2.deleteElement(apple);

    }



    apple->SetId(12);

    int b = -4 % 4;



    int *a = nullptr;

    {

        ppx::base::MemoryPool<int, 18> mp3;

        a =  mp3.allocate();

        *a = 100;

        //mp3.deallocate(a);



        int *b =  mp3.allocate();

        *b = 200;

        //mp3.deallocate(b);



        mp3.deallocate(a);

        mp3.deallocate(b);



        int *c = mp3.allocate();

        *c = 300;

    }



    getchar();

    return 0;

}



本文来自投稿,不代表展天博客立场,如若转载,请注明出处:https://www.me900.com/326675.html

(0)

相关推荐

  • 服务器的ddos防御,ddos攻击可以防御吗

    当您的网站或网络设备遭到 DDOS 攻击时,可能会感到非常恐慌和无助。然而,采取一些正确的措施可以帮助您应对这种情况,并最大程度地减少攻击对您网站或网络设备的影响。 首先,请确保您的网络设备和网站的日志功能已经启用,并且您能够访问这些日志。这将有助于您了解攻击的来源和规模。 其次,请尽快联系您的网络运营商或云服务提供商,请他们采取防御措施来阻止攻击流量到达您…

    2023-07-09
  • 演员赵薇最近出了什么事(演员赵薇事件最新消息)

    演员赵薇最近出了什么事(演员赵薇事件最新消息)演员赵薇最近出了什么事(演员赵薇事件最新消息)演员赵薇最近出了什么事(演员赵薇事件最新消息)演员赵薇最近出了什么事(演员赵薇事件最新消息)

    赵薇被全网除名,其实是意料之内的事情,只是这样突然间戏在人不在,有点猝不及防!   8月26日晚,有网友发现,某视频平台的《还珠格格1》和《还珠格格2》作品简介里,已经把赵薇的名字去掉了,除此之外,赵薇还被多部作品删除掉了主演名字,其中就有《情深深雨濛濛》《京华烟云》《画皮》《虎妈猫爸》《一个女人的史诗》等……   众所周知,能享受到这种…

    2022-03-14 投稿
  • 高德淘金一个月能赚多少,最赚钱的地图采集软件

    高德地图,作为如今生活工作旅行多数人必备的导航软件,为大家提供了出行的便利,使人们出门远行,不用必备纸质地图,不需要再提前规划路线,但是大家知道高德地图的店铺数据,和公交数据,导航数据是怎样来的吗?我想多数人应该是不知道的,其实在高德地图的主页里,有几款不起眼的应用,高德地图的数据就是从这几款应用里面来的,这几款软件就是高德地图采集数据的APP,大家都可以利…

    2022-03-24
  • 河北保定疫情最新情况(河北保定疫情最新情况属于高风险)

    2022年1月1日 河北省新型冠状病毒肺炎疫情情况 2022年1月1日0—24时,河北省无新增新型冠状病毒肺炎确诊病例,无新增无症状感染者。 截至1月1日24时,河北省现有确诊病例0例;尚在医学观察无症状感染者0例。 事关“加强针” 石家庄市疾控中心最新提醒 打“加强针”啦 接种新冠病毒疫苗后,体内的抗体水平会随着时间的推移出现不同程度的下降,预防病毒感染的…

    2022-01-02
  • 钓鱼线组怎么绑的(台钓放风筝钓法解答)

    钓鱼线组怎么绑的(台钓放风筝钓法解答)钓鱼线组怎么绑的(台钓放风筝钓法解答)钓鱼线组怎么绑的(台钓放风筝钓法解答)钓鱼线组怎么绑的(台钓放风筝钓法解答)

    放风筝钓法是台钓中的一个小技巧,又叫做拖钓,这个拖钓跟传统钓的拖钓完全不是一回事,这点需要注意下。放风筝钓法在黑坑中使用较多,尤其是在限竿的混养塘或者大物塘经常很多钓友会使用,这个钓法的最大优势在于可以钓的更远,正所谓一寸长一寸强,在混养或者大物塘中钓的更远,那钓到更多鱼的机会就会增加,毕竟凡是大鱼都喜欢躲在深远的位置。 一、放风筝钓法原理 要说明这种钓法,…

    2022-05-09 投稿
  • 交通银行最新积分规则(交通银行信用卡最新政策)

    部分银行的协商政策有所变化,比如民生的之前的规定还不是必须逾期三期以上才可以办理,但是今年新规就是没有逾期三个月以上是不可以申请办理的。还有浦发,去年一部分不好谈,需要分期费用,期数也不好谈,但是今年政策利好,期数相较于去年也比较完美,而且大多数不需要分期费用。也有一些好的政策,例如招商,今年最高60期,也可以申请协商个性化还款。前面可以少还,后面多还的方式…

    2022-01-05
  • 大运中心体育场,大运中心体育场活动信息及入场方式

    今起,这些大运场馆 免费或低收费开放 赶快来get“游玩攻略” 户外足篮球场 场地位置: 东安湖体育公园-户外足篮球场 开放时间: 周一-周五:10:00-17:00 周末、节假日:10:00-19:30 开放场地: 1-8号户外篮球场 2-3号户外足球场(5人制) 人数限制: 篮球场每片场地不超过30人 足球场每片场地不超过20人 注:每场时长2小时 市内…

    2023-04-25
  • 阿里p8级别待遇(阿里p8是什么级别待遇)

    阿里p8级别待遇(阿里p8是什么级别待遇)阿里p8级别待遇(阿里p8是什么级别待遇)阿里p8级别待遇(阿里p8是什么级别待遇)阿里p8级别待遇(阿里p8是什么级别待遇)

    一:腾讯 1.**划分: 腾讯内部设五大职能岗位,分别是 T(技术) P(产品) S(销售) M(市场) D(设计) 其中技术、产品、设计的薪资水平相差不大,销售和市场的薪资相对较低。 在**上,之前主要从T1-T6分六级。   之后腾讯宣布调整职级,取消了原有的 6 级 18 等(1.1-6.3 级)的职级体系设计,将专业职级体系优化为 14 级…

    2021-12-16 投稿
  • 在农村怎么赚钱(农村变废为宝项目)

    来源:人民网-海南频道 人民网海口7月30日电 (符武平)7月30日,海口市演丰镇易腐废弃物堆肥点项目启动仪式在瑶城村举行。项目建成后,将有效推进演丰镇垃圾分类工作开展,提高易腐废弃物资源化利用水平,实现垃圾分类投放、分类收集、分类运输及分类处理工作的有效衔接。 据悉,演丰镇易腐废弃物堆肥点设计日处理量为1吨/日,占地面积约960平方米,主要包含菌种发酵区、…

    2022-01-22
  • 武林风李响的现状(武林风李响图片)

    武林风打造出了不少拳手,“死神”方便与“武僧”一龙是最成功的两个。一龙赢得了“KO”之王的名声,被拳迷们成为“第一武僧”。纵然方便没有一龙这么多的名头,但他却是赢得更多认可的那个。这是因为一龙曾在武林风的舞台上被暴揍过,而方便至今仍保持着全胜战绩。此外,方便所遇到的对方实力更强,而为人也更低调。 一龙在武林风的舞台上经常KO对手,其中不乏很多号称“拳王”的人…

    2022-01-14
  • 现在三级士官安排工作吗,海警三级士官退役安排工作吗

    近日,一位服役满16年选择自主就业回地方的退役士官向军路反映问题,称自己在退役时并不知道回地方后,还可以选择不要工作再领取地方补助,当时部队也没有告知,问现在能不能向有关部门反映,重新给自己安排工作。 退役士兵在退役时可以根据自身实际及符合的条件慎重选择哪种安置方式,在决定好安置方式后,已经实质展开后,无法再变更安置方式。今天,军路就带大家了解一下,《退役士…

    2022-03-21
  • 黄圣依婆婆,黄圣依的婆家是干什么的

    黄圣依婆婆,黄圣依的婆家是干什么的黄圣依婆婆,黄圣依的婆家是干什么的黄圣依婆婆,黄圣依的婆家是干什么的黄圣依婆婆,黄圣依的婆家是干什么的

    近日,黄圣依婆婆刘丽芹的孙女晒出一则外婆乘坐豪车回村的视频。视频中黄圣依86岁的婆婆身穿白色羽绒服看似十分低调,但紫帽与紫色布包做点缀又增添一丝潮流,脖戴珍珠项链凸显些许精致。 画面中只见她被孙女细心搀扶着从豪车中走出,嘴里说着想老家的词语,随后一名手抱小孩的年轻女子笑容满脸的上前尊称黄圣依婆婆为“太太”,语气温柔对其很是敬重,可见黄圣依婆婆在老家的地位非同…

    2022-04-29 投稿