• 【C++】养很多鱼,只为观察向量的生长


    向量的存储空间根据需要而自动伸缩。当现有的存储空间不足以容纳新增元素时,向量会自动申请新的存储空间,并将旧的元素进行迁移。我们通过分析下述C++程序来理解向量的内存管理行为。

    本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
    叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
    1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
    2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
    3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频

    //Project - FishVector
    #include 
    #include 
    using namespace std;
    
    class Fish{
        string sNumber;
    public:
        Fish(){
            static int i = 0;
            sNumber = std::to_string(i++);
            cout << "Fish constructor: " << sNumber << endl;
        }
    
        Fish(const Fish& r){
            sNumber = r.sNumber + "[Copy]";
            cout << "Fish copy constructor: " << sNumber << endl;
        }
    
        ~Fish(){ cout << "Fish destructor: " << sNumber << endl; }
    };
    
    int main(){
        vector<Fish> f(2);    //0,1号鱼
        printf("f.capacity = %d, f.size = %d\n",f.capacity(),f.size());
        Fish f2;              //2号鱼
        cout << "-------------f.push_back(f2)----------" << endl;
        f.push_back(f2);
        printf("f.capacity = %d, f.size = %d\n",f.capacity(),f.size());
        cout << "vector f is " << (f.empty()?"empty.\n":"not empty.\n");
        cout << "-------------f.pop_back()-------------" << endl;
        f.pop_back();
        cout << "-------------f.resize(1)--------------" << endl;
        f.resize(1);
        cout << "-------------f.clear()----------------" << endl;
        f.clear();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    上述程序的执行结果为:

    Fish constructor: 0
    Fish constructor: 1
    f.capacity = 2, f.size = 2
    Fish constructor: 2
    -------------f.push_back(f2)----------
    Fish copy constructor: 2[Copy]
    Fish copy constructor: 0[Copy]
    Fish copy constructor: 1[Copy]
    Fish destructor: 0
    Fish destructor: 1
    f.capacity = 4, f.size = 3
    vector f is not empty.
    -------------f.pop_back()-------------
    Fish destructor: 2[Copy]
    -------------f.resize(1)--------------
    Fish destructor: 1[Copy]
    -------------f.clear()----------------
    Fish destructor: 0[Copy]
    Fish destructor: 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    说明:不同版本的标准模板库在算法策略上会有差异,在读者的计算机上,上述执行结果很可能与本书存在差异。

    🚩第6 ~ 21行:Fish类型的构造函数通过局部静态变量i为每个新对象提供一个唯一的序号(sNumber)。当Fish对象被拷贝构造时,sNumber序号会增加"[Copy]"字样,以表明拷贝生成的新对象是由哪一个旧对象拷贝而得的。无论是构造函数,拷贝构造函数,还是析构函数,都会向控制台打印包括序号的报告信息,以便于我们观察向量元素增减时的内存管理行为。

    🚩第24行:f向量的构造参数2导致向量自动创建了0号鱼及1号鱼。0,1号鱼的构造函数输出见执行结果的第1 ~ 2行。可以想象,该行代码事实上要求Fish类型必须具备一个“零参数”的构造函数,否则编译器会报错。

    🚩第25行:capacity()成员函数返回向量对象的容量,即向量对象能够容纳的元素个数;size()成员函数返回向量对象当前实际包含的元素个数。执行结果的第3行显示,当前容量(capacity)及尺寸(size)均为2。这提示,如果试图往向量中增加一个元素,将存在向量内存空间不足的问题。

    🚩第26行:定义并构建了2号鱼对象。其构造函数的输出见执行结果的第4行。

    🚩第27 ~ 28行:pushback(f2)将2号鱼加入向量f的末尾。C++并不能把2号鱼从外部变量f2“移入”向量,它只能将外部对象f2“复制”至向量内。但当前向量f的容量和尺寸都为2,没有剩余空间,为了将f2加入向量,pushback()函数做了如下工作。

    • 在堆中申请并分配新的足够的存储空间;
    • 将2号鱼,即f2对象拷贝构造至新空间内的2号位置(从下标0开始数)。相关拷贝构造函数的输出见执行结果的第6行。请注意,向量内的2号鱼的序号已变为2[Copy]。
    • 将向量内原有的0,1号鱼由旧空间拷贝至新空间的对应位置。相关拷贝构造函数的输出见执行结果的第7 ~ 8行。请注意,向量内的0,1号鱼的序号变为0[Copy]及1[Copy]。
    • 释放旧空间,旧空间内的原0,1号鱼的析构函数被执行。相关析构函数的输出见执行结果的第9 ~ 10行。

    可以想象,push_back()函数的这种“复制”行为要求元素类型具有公开的拷贝构造函数(可以是默认的),否则,编译器会报错。

    🚩第29行:再次打印向量f的容量及尺寸。执行结果的第11行可见,容量为4,尺寸为3。这提示,向量f“认为”使用者有可能会再次往向量内增加元素,push_back()函数在分配新空间时作了适当的预留。

    🚩第30行:当向量内容纳的元素个数为0时,empty()成员函数返回真,否则为假。执行结果的第12行显示,向量f非空。

    🚩第31 ~ 32行:pop_back()成员函数将向量的最后一个元素弹出。执行结果的第14行可见,向量内的2号鱼被析构,其序号为2[Copy]。

    🚩第33 ~ 34行:resize( n )成员函数将向量内的元素个数修改为n。如果n大于向量内的当前元素个数,向量将自动新增元素来实现目标;如果n小于向量内的当前元素个数,则末尾方向的多余元素会被移除。第34行执行前,向量f内有2个元素,resize(1)导致1号鱼被移除,相关析构函数输出见执行结果第16行,其序号为1[Copy]。

    🚩第35 ~ 36行:clear()成员函数清空向量内的全部元素。执行结果第18行显示,向量内的最后一个元素,序号为0[Copy]的0号鱼被析构。

    在执行结果的最后一行,我们还看到了局部变量f2的析构输出。

    🎯
    要点当往向量内加入元素时,向量会“酌情”申请新空间,并将旧空间的原有元素及拟加入元素以拷贝构造的形式复制到新空间内。同时,旧空间的原有元素会被释放。如果期望降低这种因内存不足而导致的“搬家开销”,可以通过reserve(n)函数预分配向量的元素存储空间。reserve(n)成员函数会导致向量预分配n个元素的存储空间。请注意,该函数的执行只会导致向量的容量(capacity)发生变化,其实际存储的元素个数(size)不变。

    向量等容器还提供emplace_back()函数【C++ 11】,它们的功能与push_back()类似,但实现机制略有差别。我们结合下述程序进行解释。

    //Project - EmplaceFish
    #include 
    #include 
    using namespace std;
    
    class Fish{
        string sName;
    public:
        Fish(const char* name){
            sName = name;
            cout << "Fish constructor: " << sName << endl;
        }
    
        Fish(const Fish& r){
            sName = r.sName;
            cout << "Fish copy constructor: " << sName << endl;
        }
    };
    
    int main(){
        vector<Fish> v;
        v.reserve(10);  //提前分配10个元素的空间
        v.push_back(Fish("Tom"));
        v.push_back("Dora");
        v.emplace_back("Charlie");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    上述程序的执行结果为:

    Fish constructor: Tom
    Fish copy constructor: Tom
    Fish constructor: Dora
    Fish copy constructor: Dora
    Fish constructor: Charlie
    
    • 1
    • 2
    • 3
    • 4
    • 5

    🚩第22行:执行reserve()函数提前为向量分配10个元素的空间,以避免通过push_back()、emplace_back()函数添加新元素时重新分配内存。

    🚩第23行:程序先执行Fish的构造函数,构造一个临时对象,其输出见执行结果的第1行。push_back()函数则把临时对象Tom鱼通过拷贝构造函数复制到向量内部,相关拷贝构造函数的输出见执行结果的第2行。

    🚩第24行:push_back()函数的参数为一个C风格的字符数组,其类型为const char*。显然,向量v预期存储Fish对象,而不是const char*类型的对象,但这行代码会通过编译并正确执行:

    • 编译器“聪明”地执行Fish的构造函数,以“Dora”为参数,构建了一个用完即弃的临时对象。相关构造输出对应执行结果的第3行。
    • push_back()函数将临时Fish对象拷贝复制到向量内。相关拷贝构造输出对应执行结果的第4行。

    🚩第25行:emplace_back()函数与push_back()函数有如下区别。

    • emplace_back()函数的预期参数并不是Fish对象(结合本例),而是Fish类型的构造参数。其参数的个数及类型取决于Fish的构造函数定义。
    • emplace_back()向向量增加元素的方法并不是先构造、后复制,而是直接在分配好的元素空间上执行Fish类型的构造函数,以"Charlie"为参数(结合本例)。相关构造函数输出对应执行结果第5行。

    理论上,如果待添加的元素不是一个已经存在的Fish对象,emplace_back()函数的执行效率比push_back()高。

    如果需要在向量的特定位置插入或者删除元素,则需要使用到迭代器(iterator),请见本章后续部分。

    为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!

    简洁的C及C++
    由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
    Python编程基础及应用
    由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造

    如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。

    Python编程基础及应用

    Python编程基础及应用实验教程
    在这里插入图片描述

  • 相关阅读:
    图解 LeetCode 算法汇总——二分查找
    大学生HTML CSS动漫主题网页制作——刀剑神域(6页) dreamweaver作业静态HTML网页设计
    springMvc27-get乱码解决
    vue3【echarts 做的词云图】
    HCIP(DSVPN)实验
    LVM_基本操作
    数据库迁移(DBeaver版本)
    STM32MP157F-DK2 使用体验
    四、cadence ic 617 ——添加工艺库文件
    日期调度器:dbi-tech Solutions Schedule .NET v7
  • 原文地址:https://blog.csdn.net/SeaBiscuitUncle/article/details/126552952