accelerated 第十一章笔记

第十一章 定义抽象数据类型

因为我们在前面经常用到向量类,所以在这里我们要创建一个类似于向量的类来加深我们对如何设计和实现一个类的理解。

11.1 Vec类

在开始设计一个类的时候,一般来说要先确定在类中要提供什么样接口。正确的确定接口的一种途径是研究一下类的使用者们将会用我们的类来编写什么样的程序。既然我们在Vec类中想要实现的是与标准库中的向量类相同的功能,下面就让我们从研究过去使用向量类的一个例子开始:

//构造一个vector
vector<Student_info> vs;//一个空的vector
vector<double> v(100); //一个有100个元素的vector
//获得vector使用的类型的名字
vector<Student_info>::const_iterator b,e;
vector<Student_info>::size_type i = 0;
//用size函数与索引值查看vector中的元素
for(i = 0;i!=vs.size();++i)
    cout << vs[i].name();
//返回指向第一个元素的迭代器与指向最后一个元素后面的那个元素的迭代器
b = vs.begin();e = vs.end();

11.2 实现Vec类

我们决定先写一个模板类

template<class T> class Vec{
    public:
        //接口
    private:
        //实现
};

像其他类的定义一样,模板类的定义可以分为公共和私有两部分,用于分别代表它的接口和实现部分。

用动态建立的数组来容纳Vec中的元素。

我们决定只保存数组的首元素及末元素的地址指针,然后计算出数组的大小来。

template<class T> class Vec{
public:
    //接口
private:
    T* data;    //Vec中的首元素
    T* limit;   //Vec中的末元素
};

Vec<int> T;既然在data和limit的声明中我们用了类型参数T,它们的实际类型将决定于Vec实际封装的对象的类型。

11.2.1 内存分配

new T[n] 不仅分配内存空间,还会运行T的构造函数来为元素进行默认初始化。如果我们打算使用new T[n],那么必须满足一个要求:只有在T具有默认的构造函数时才能创建一个Vec。而标准的向量类没有这样的限制。因为我们要写的类是在模仿标准库中的向量类,所以我们希望在Vec类中也没有这个限制。

库函数中提供了一个内存分配类,在内存分配的方面提供了更为详尽的操作。如果用这个类可以代替new和delete,那它就可以充分满足我们的要求。通过这个类我们可以申请分配到未被初始化的内存空间,然后通过一个特殊的步骤在那片内存中生成对象。

11.2.2 构造函数

Vec<student_info> vs;   //默认构造函数
Vec<double> vs(100);    //带一个大小参数的构造函数

标准的向量类还提供一个相关的第三种构造函数,除了一个大小参数,还带有一个初始值的参数,用来把向量中的元素全部初始化为该值的一个复件。

如果用户仅给出了一个大小参数,那么我们将调用T的默认构造函数来设置元素的初始值。

template<class T>class Vec{
public:
    Vec(){create();}
    explicit Vec(size_type n,const T& val = T())
    {create(n,val);}
    //其他保留接口
private:
    T *data;
    T* limit;
};

默认构造函数不带任何参数,它首先清空Vec类的对象,这是通过调用create成员函数来实现的,create函数也是我们必须要编写代码实现的函数之一。调用create函数返回后,data和limit的值都被设置为零。

explicit这个关键字只在定义带一个参数的构造函数的时候才有意义。如果声明一个构造函数是explicit的,那么编译器只有在用户显式的使用构造函数时才会调用它,否则就不调用。

Vec<int> vi(100);    //正确,显示的调用Vec的构造函数,以一个int类型数据做参数
Vec<int> vi = 100;  //错误,隐式的调用Vec的构造函数

11.2.3 类型定义

迭代器是这样一种对象,它用来定位容器中的不同对象,并提供一种检验对象值的途径。通常迭代器本身也是自定义的类型。

我们可以使用普通指针作为Vec迭代器类型,所有的指针都指向内部的data数组。指针支持所有的随机存取迭代器操作。通过使用一个指针作为内部的迭代器类型,就可以提供完全的随机存取操作,这与标准的向量类是一致的。

value_type的类型显然必须是T。size_t变量足够大,它可以装下任何一个数组能容纳的元素个数。既然可以用数组来存储Vec的元素,我们可以用size_t作为Vec::size_type的基础类型。

template<class T>class Vec{
public:
    //新增部分
    typedef T* iterator;
    typedef const T* const_iterator;
    typedef size_t size_type;
    typedef T value_type;
    Vec(){create();}
    explicit Vec(size_type n,const T& val = T())
    {create(n,val);}
    //其他保留接口
private:
    //更改部分
    iterator data;
    iterator limit;
};

除了加入合适的typedef,我们还在类中使用了新的类型。在类中使用同样的名字(在typedef声明中被定义)可以使程序代码更具可读性,另外,如果在今后改变了某个地方的类型,其他地方也会自动的发生同步变化。

11.2.4 索引与大小

运算符的种类(是一个一元运算符还是一个二元运算符)一定程度上决定了该函数要带多少个参数。

如果运算符是一个函数而不是一个成员,那么函数的参数个数与运算符的操作数一样多。第一个参数一定是左操作数;第二个参数是右操作数。

如果运算符被定义成一个成员函数,那么它的左操作数必须是调用该运算符的对象。

可见成员运算符函数比简单的运算符函数要少带一个参数。

一般来说,运算符函数既可以是成员函数,也可以是非成员函数。但是索引运算符必须是成员函数。

template<class T>class Vec{
public:
    typedef T* iterator;
    typedef const T* const_iterator;
    typedef size_t size_type;
    typedef T value_type;
    Vec(){create();}
    explicit Vec(size_type n,const T& val = T())
    {create(n,val);}
    //新增操作:大小与索引
    size_type size() const {return limit - data;}
    T& operator[](size_type i){return data[i];}
    const T& operator[](size_type i)const {return data[i];}
private:
    iterator data;
    iterator limit;
};

两个指针相减生成一个ptrdiff_t类型的值,表示两个指针的距离

因为Vec类的size函数不能改变Vec类型对象本身,所以在此把size声明为const成员函数。

索引运算符在数组中定位到正确的元素位置并返回该元素的一个引用。通过这个返回的引用我们可以修改Vec类型对象中所储存的数据。我们可以直接对元素进行写操作,这意味着我们需要写出Vec类的两个版本:一个用于常量(const)Vec对象,另一个是用于非常量(nonconst)Vec对象。注意,const版本返回一个指向const的引用,这样做是为确保用户只能用索引来读Vec的数据,而不能改变它的值。我们返回一个引用而不是返回一个值,如果这仅仅是为了与标准的向量类保持一致性的话那就没有任何意义了。这样做的真正原因其实是为了避免当容器中的对象很大时对它进行复制,那样既浪费空间有影响运行速度。

我们还可以对索引运算符进行重载,这一点看起来令人惊讶,因为看上去它们的参数列表是完全一样的;它们的参数都是一个size_type类型的变量。但是,类中的每一个成员函数,包括这些运算符函数,**都必须带一个隐式的参数作为作用对象。**因为操作的对象可能是常量,也可能不是常量,所以我们可以对运算符进行重载。

11.2.5 返回迭代器的操作

template<class T>class Vec{
public:
    typedef T* iterator;
    typedef const T* const_iterator;
    typedef size_t size_type;
    typedef T value_type;
    Vec(){create();}
    explicit Vec(size_type n,const T& val = T())
    {create(n,val);}
    size_type size() const {return limit - data;}
    T& operator[](size_type i){return data[i];}
    const T& operator[](size_type i)const {return data[i];}
    //新增返回迭代器的函数
    iterator begin(){return data;}
    const_iterator begin() const {return data;}
    iterator end(){return limit;}
    const_iterator end() const {return limit;}
private:
    iterator data;
    iterator limit;
};

11.3 复制控制

我们已经介绍了如何创建对象,但还没有谈到当对象被复制、赋值以及销毁时,会有什么样的情况发生。我们将会得知,如果我们忘记了定义这些操作,那么编译器将会在必要的时为我们合成它们。有时候这些被合成出的操作正是我们所期望的。其他情况下,这些默认操作可能会导致莫名其妙的结果,甚至产生实时错误。

c++是唯一允许编程者对对象进行如此层次的控制并被广泛使用的语言。毫无疑问,正确使用这些操作对于构建有用的数据类型至关重要。

11.3.1 复制构造函数

//下面这个例子把一个对象的值作为参数传递给函数,或者从函数通过传值返回一个对象,这就是在对对象进行隐式的复制操作
vector<int> vi;
double d;
d= median(vi);  //把vi作为参数传递给median函数

string line;
vector<string> words = split(line); //把split函数的返回值赋给words

//类似的,我们可以通过使用一个对象来初始化另外一个都对象,从而显示的复制对象
vector<Student_info> vs;
vector<Student_info> v2 = vs;   //将vs复制给v2

无论是显示还是隐式的复制,都由一个名为copy constructor(复制构造函数)的特殊的构造函数进行。

因为我们定义了复制所带来的效果,包括产生函数参数的复件,所以当参数时引用类型时就有了问题。此外,由于复制对象不会改变原对象的值,所以复制构造函数的参数使用了一个常量引用类型:

template<class T>class Vec{
public:
Vec(const Vec& v);  //复制构造函数
    //其他部分与前面相同
}

在我们的Vec类中进行复制操作时,也应该开辟新的内存空间并把源对象的内存复制进新的内存块里。

template<class T>class Vec{
public:
Vec(const Vec& v){create(v.begin(),v.end());}
//其他部分与前面相同
};

11.3.2 赋值运算符

一个类中可以定义几种不同的赋值运算符(习惯上通过不同的参数进行重载),其中以一个指向类自身的常量引用作为参数的版本比较特殊。

像索引运算符一样,赋值运算符也必须是类的一个成员函数。

像所有的运算符一样,赋值运算符必须有一个返回值,所以要对其定义一个返回类型。为了与c++自带的赋值算符一致,我们让它返回左操作数的引用:

template<class T>class Vec{
    public:
    Vec& operator=(const Vec&);
    //其他部分与前面相同
};

赋值总是把一个已经存在的值擦去,然后代之以一个新的值。

复制时,我们先创建一个新的对象,所以不需要对对一个已经存在的对象进行删除操作。

自我赋值 有时候用户可能会把一个对象赋给它本身。

template<class T>
Vec<T>& Vec<T>::operator=(const Vec& rhs)
{
    //判断是否进行自我赋值
    if(&rhs!=this){
        //删除运算符左侧的数组
        uncreate();
        //从右侧复制元素到左侧
        create(rhs.begin(),rhs.end());
    }
    return *this;
}

注意到这里我们定义返回类型的时,用的是Vec&。对比在头文件中定义返回类型的语法,在头文件中定义用的是Vec&类型,不需要显示的声明返回类型名称。因为在模板文件的范围内,c++允许我们忽略其具体类型名称。在头文件里,因为模板参数是隐式的,所以不需要重复写。而在头文件外面,我们必须声明返回类型,所以在必要的地方显示的写出模板参数。类似的,函数名是Vec<T>::operator=,而不能简写成Vec::operator。不过一旦在前面我们的定义是一个Vec类型的成员函数,后面就不需要重复使用这个定语了。因此,在参数列表中我们把参数写成const Vec&的形式,而不必写成const Vec&这样的复杂形式。

this关键词只在成员函数内部才有效,代表指向函数操作的对象的指针。对于一个二元操作,例如赋值操作等,this总是指向左操作数。一般来说,我们在需要指向对象本身的时候用this关键字。

需要写出另一个实用函数uncreate,我们要用它来删除Vec中的元素,释放其占用的内存空间。一旦调用uncreate函数,抹去原来的值后,就可以用create函数来把右操作数的值赋给左操作数。

去掉判断后,程序总会调用uncreate函数,把左操作数对象的元素删除并释放其占用的内存。注意,如果左右操作数指向同一个对象,那么右操作数同时被删除。这时候如果还用右操作数的元素来为左操作数生成新的数组元素,那将会带来一场灾难:**在释放左操作数对象的内存空间的时候,我们已经把右操作数对象的空间也释放了。**当create函数试图复制rhs的元素时,这些元素实际上已经被删除,其内存早已被释放回系统中。

如果将一个引用返回给一个局域量对象将会引发灾难:被引用参照的对象在函数返回时会被删除,将导致一个错误的引用。在赋值操作的例子中,我们返回一个指向表达式左操作数对象的一个引用调用,那个对象的生存周期大于赋值操作,保证了在函数返回的时候不被删除。

11.3.3 赋值不是初始化

在很多编程语言中,包括c语言,这两者没有显著的差别。

在使用“=”为一个变量赋一个初始值的时候,程序自动调用复制构造函数。而在赋值表达式中,程序调用operator=赋值操作函数。设计类时类作者必须要注意它们的区别,以实现正确的语义操作。

赋值和初始化有两个主要地区别:赋值总是删除一个旧的值;而初始化则没有这步操作。确切的说,初始化包括创建一个新的对象并同时给它一个初始的值。

在下面的时候会发生初始化:

  • 声明一个变量的时候
  • 在一个函数的入口处用到函数参数的时候
  • 函数返回中使用函数返回值的时候
  • 在构造初始化的时候

赋值只在表达式中使用=运算符的时候调用。

//初始化
string url_ch = "~,/?:@=&$-.+!*'():";
string spaces(url_ch.size(),' ');
string y
//赋值
y = url_ch;
vector<string> split(const string&);//函数声明
vector<string> v;   //初始化
v= split(words);    //在split的函数入口处初始化words
                    //在出口处则既要对返回值进行初始化
                    //又将返回值赋给变量v

将一个函数返回的类型对象的返回值进行赋值分为两步操作:第一步,运行复制构造函数,并在这个地方生成一个临时变量,构造函数对该临时变量进行初始化;第二步,运行赋值运算符函数,把这个临时变量的值赋给左操作数。

初始化操作与赋值操作的区别是很重要的,因为其中一个操作的执行可能导致另一个操作的执行,二者是紧密相关的,但要注意:

  • 构造函数始终只控制初始化操作。
  • operator=成员函数只控制赋值操作。

11.3.4 析构函数

一个在局域范围里被创建的队形在它的生存范围以外时就会被自动删除;而一个动态分配内存的对象则只有在我们使用delete来删除它时才会被删除。

vector<string> split(const stirng& str)
{
    vector<string> ret;
    //将str分离成不同的单词 并将这些单词存储在ret变量中
    return ret;
}

当我们从split函数返回时,局域变量ret超出其生存范围,被自动删除。

析构函数不带任何参数,而且没有返回值。

构造函数的任务是做好一个对象被删除时的一切大扫除工作。这一大扫除工作一般指的是释放资源,例如释放在构造函数中为对象分配的内存资源等。

template<class T> class Vec{
    public:
    ~Vec(){uncreate();}
    //同上
}

11.3.5 默认操作

没有显式的定义一个copy构造函数,也没有显式的定义一个赋值运算符函数或者定义一个析构函数。那么在创建这些类的对象时,对它们进行赋值操作或者删除这类型对象时,逻辑上应该进行什么操作呢:在编写类时如果没有显式的定义这些操作,编译器将自动为类生成相应的默认版本的函数,进行一些默认的操作。

这些默认的函数被定义成一系列的递归操作——对每个成员数据按照它们相应的类型规则进行复制、赋值或者是删除。

如果成员变量是类的对象实例,那么对它们进行复制,赋值与删除时会调用相应的类的构造函数,赋值运算符函数或析构函数等。

如果成员变量是c++自带的变量类型,那么在对它们进行复制或者赋值时将对它们的值进行复制或者赋值,而这类变量在删除时不需要做任何额外的工作,即使其变量类型是指针也不例外。

特别值得指出的是,在通过默认析构函数删除一个指针变量时不会释放该指针指向的对象占用的内存空间。

如果类中没有定义任何构造函数,那么编译器将自动生成一个默认的不带任何参数的构造函数。这一自动生成的构造函数通过一种对象自身在初始化时采取的方式,对其成员数据进行递归初始化。如果上下文要求进行默认初始化,那么它将对数据成员进行默认初始化;如果上下文要求进行值初始化,那么它就对数据成员进行值初始化。

只要在类中显式的定义了任何一个构造函数(包括复制构造函数),编译器将不会为类自动生成默认的构造函数。

为了对每个数据成员进行默认初始化,要求成员数据的类型都有相应的默认构造函数。

所以我们应该养成一个良好的编程风格,就是记住为每一个类定义一个默认构造函数,既可以显式的定义,也可以隐式的定义。

11.3.6 rule of three

如果我们没有定义析构函数,那么默认析构函数将被调用。这个默认析构函数仅仅删除对象的指针,而删除一个指针不会释放指针指向对象占用的内存空间。最终结果导致内存泄漏:Vec类型对象使用的内存空间再也不能被收回使用。

如果我们只想提供一个析构函数来修正这个内存泄露的错误,仍然不显示写出复制构造函数与赋值运算符函数,那么情况将会变得更加一团糟。如果这样的化,有可能两个Vec类型对象共享同一块内存空间。当其中一个对象被删除以后,析构函数将释放那片共享的内存空间。接下来对这片已经释放的内存任何一个引用都导致不可预见的后果。

如果类需要一个析构函数,那么它就几乎一定需要一个复制构造函数和一个赋值运算符成员函数。对一个动态分配资源的类型对象进行复制或者赋值操作一般都要重新分配内存。

为了控制T类型对象的每一个复件,你需要

T::T()  //一个或几个构造函数,可能会带参数
T::~T() //析构函数
T::T(const T&)  //复制构造函数
T::operator+(const T&)  //赋值运算符函数

注意,类型对象有可能会被隐式的创建、复制或者删除。无论是隐式的还是显式的操作,编译器都会激活相应的函数。

复制构造函数、析构函数和赋值运算符函数相互之间关系十分密切,它们之间构成了一个“三位一体”规则rule of three:如果类需要一个析构函数,那么它同时可能也需要一个复制构造函数与一个赋值运算符函数。

11.4 动态的Vec类型对象

幸运的是,只有push_back和其他类似的内存管理函数(这些函数现在尚未写出来)才需要知道这些新的元素。更为幸运的是,push_back函数本身十分简单的;它把复杂的、困难的工作推给了另外两个内存管理函数:grow函数和unchecked_append函数,而这两个函数根本不需要我们亲自动手编写。

template<class T> class Vec{
public:
    void push_back(const T& val){
        if(avil==limit) //获取需要的空间
            grow();
        unchecked_append(val);  //将新元素加入到对象末端
    }
private:
    iterator data;  //如前,指针指向Vec的第一个元素
    iterator avail; //指针指向构造元素末元素后面的一个元素
    iterator limit; //现在指向最后一个可获得元素后面的一个元素
    //类的其余接口与实现同前
} 

11.5 灵活的内存管理

我们不打算使用c++内建的new运算符和delete运算符来管理内存。c++内建的new运算符要做许多工作:既要分配新的内存空间,又要对新内存进行初始化。在为一个类型为T的数组分配空间时,它需要去调用T的默认构造函数。

用new实际上要进行两次初始化,一次是new自动进行的,另一次是在把用户提供的数值赋给Vec类型对象的元素时进行的。更为糟糕的是,push_back函数中采用的方法通过分配我们实际需要的内存的两倍空间来获得更多的内存空间,我们没有理由要为这些额外的元素进行初始化。这些空间只会再把一个数据放进一个元素空间时才会被push_back函数使用。而如果使用new来为数组分配内存空间的话,不管我们需不需要使用这些额外的空间,都无一例外的对它们进行初始化。

除了使用自带的new和的了delete运算符来管理内存外,我们还可以使用c++专门设计以支持灵活的内存管理的一些类来管理内存。

标准函数库并不需要支持所有的内存。内存管理者们一般不乐意亲自动手写内存管理函数,C++的标准库中提供了管理内存的功能,但是只是为内存管理者们提供了一个统一的内存管理接口。和前面所说的把输入/输出作为标准库的一部分而不是语言的特性相同,内存管理功能也是库的一部分,这一特性为我们方便的管理各种不同种类的内存提供了很大的弹性。

在头文件中提供了一个名为allocator的类,它可以分配一块预备用来储存T类型对象但是尚未被初始化的内存块,并返回一个指向这块内存块的头元素的指针。这样的指针是很危险的,因为指针的类型表明它们指向类型的对象,但实际上这些内存块却并没有储存实际的对象。

标准库提供了一种途径来为这种内存块进行初始化,也提供了删除对象的方法——仅仅是删除对象,而没有释放内存空间。

由程序员们来使用allocator类得到这些被指定用来存放类型对象但实际上没有被初始化的内存空间地址。

template<class T>class allocator{
public:
    T* allocate(size_t);
    void deallocate(T*,size_t);
    void construct(T*,T);
    void destrcy(T*);
};
void uninitialized_fill(T*,T*,const T&);
T* uninitialized_copy(T*,T*,T*);

allocate成员函数用来分配一块被指定了类型但却未被初始化的内存块,我们可以通过使用一个T*类型的指针来得到它的地址。

deallocate成员函数则是用来释放未被初始化的内存,两个参数:一个是allocate函数返回的指针,另一个是该指针指向的内存块的大小。

construct成员函数用来在allocate申请分配但尚未初始化的内存区域上进行初始化,生成单个的对象。两个参数:allocate函数返回的指针,另一个是用来复制到指针指向的内存块的对象值。

destory成员函数用来删除这个对象,调用析构函数,删除它的参数所指对象的元素。

uninitialized_fill函数向内存块中填充一个指定的值。在函数调用结束后,前两个参数指针指向的内存区间中的元素都被初始化成第三个参数所指对象的内容。

uninitialized_copy函数用来把前两个参数指针所指向的内存区间中的值复制到第三个参数指针所指向的目标内存块中。

像uninitialized_fill函数一样,它假定目标内存块尚未被初始化而不是已经储存着一个实际对象的值,它将在目标内存块中构造新的对象。

11.5.1 最后Vec类

template<class T>class Vec{
public:
    typedef T* iterator;
    typedef const T* const_iterator;
    typedef size_t size_type;
    typedef T value_type;

    Vec(){create();}
    explicit Vec(size_type n,const T& t = T())
    {create(n,t);}

    Vec(const Vec&v){create(v.begin(),v.end());}
    Vex& operator=(const Vec&);
    ~Vec(){uncreate();}
    T& operator[](size_type i){return data[i];}
    const T& operator[](size_type i) const
    {return data[i];}

    void push_back(const T&t){
        if(avail == limit)
            grow();
        unchecked_append(t);
    }
    size_type size() const {return avail - data;}

    iterator begin(){return data;}
    const_iterator begin() const {return data;}

    iterator end(){return avail;}
    const_iterator end() const {return avail;}
private:
    iterator data;  //Vec中的首元素
    iterator avail; //Vec中末元素后面一个元素
    iterator limit; //新分配内存中末元素后面一个元素

    //内存分配工具
    allocator<T> alloc;  //控制内存分配的对象

    //为底层的数组分配空间并对它进行初始化
    void create();
    void create(size_type,const T&);
    void create(const_iteraor,const_iterator);

    //删除数组中的元素并释放其占用的内存
    void uncreate();

    //支持push_back的函数
    void grow();
    void unchecked_append(const T&);
}

类不变式(class invariant)

  1. 如果对象中有数据元素的话,data指向对象数组的首元素,否则为零。
  2. data<=avail<=limit。
  3. 在[data,avail)区间内的元素被初始化。
  4. 在[avail,limit)区间内的元素不会被初始化。

类中的公有成员函数都不能打破这一不变式。因为打破它的办法是改变data、avail或者limit的值,而这些公有成员函数都不能做到这一点。

template<class T>void Vec<T>::create()
{
    data = avail = limit = 0;
}

template<class T>void Vec<T>::create(size_type n,const T& val)
{
    data = alloc.allocate(n);
    limit = avail = data+n;
    uninitialized_fill(data,limit,val);
}

template<class T>
void Vec<T>::create(const_iterator i,const_iterator j)
{
    data = alloc.allocate(j-i);
    limit = avail = uninitialized_copy(i,j,data);
}

uncreate成员函数所做的工作与create函数正好相反:它运行析构函数,删除该对象,并释放其占用的内存

template<class T>void Vec<T>::uncreate()
{
    if(data){
        //以相反的顺序删除构造函数生成的元素
        iterator it = avail;
        while(it!=data)
            alloc.destroy(--it);
        
        //返回占用的所有内存空间
        alloc.deallocate(data,limit-data);
    }
    //重置指针以表明Vec类型对象为空
    data = limit = avail = 0;
}

如果data为零,那么将不执行任何操作。如果使用delete函数来释放内存,我们就不需要判断data是否为零,因为delete作用在零指针上是不会产生错误的。与delete不同的是,alloc.deallocate函数需要一个非零指针作为参数(即便它并不准备释放任何内存)。因此,我们必须检查data是否为零。

push_back函数的成员函数

template<class T>void Vec<T>::grow()
{
    //在扩展对象大小的时候,为对象分配实际使用的两倍大小的内存空间
    size_type new_size = max(2*(limit-data),ptrdiff_t(1));
    //分配新的内存空间并将已存在的对象元素内容复制到新内存中
    iterator new_data = alloc.allocate(new_size);
    iterator new_avail = uninitialized_copy(data,avail,new_data);
    //返回原来的内存空间
    uncreate();
    //重置指针,使其指向新分配的内存空间
    data = new_data;
    avail = new_avail;
    limit = data+new_size;
}
    //假设avail指向一片新分配但尚未被初始化的内存空间
    template<class T>void Vec<T>::unchecked_append(const T& val)
    {
        alloc.construct(avail++,val);
    }

11.6 小结

复制控制:在创建和复制对象的时会调用构造函数;在包括赋值的表达式中调用赋值运算符函数;而析构函数则是在对象被显式的删除,或者程序运行到对象的生存范围以外的地方的时候会被调用。

在构造函数中分配资源的类几乎都要定义复制构造函数、复制运算符函数以及析构函数。在写一个赋值运算符函数的时候,一定要注意判断是否是自我赋值。为了与C++自带的赋值运算符一致,最好在函数返回一个对左操作数的引用调用。

自动生成的操作:如果一个类没有定义任何构造函数,编译器将自动生成默认的构造函数。如果在类中没有显式的定义复制构造函数、赋值运算符函数以及析构函数,编译器会自动生成相应的默认成员函数。这些自动生成的操作被定义为递归操作:它会递归的对类中的每个成员数据调用相应的操作。

重载运算符函数:指的是对一个已经被定义过的运算符op进行重复定义,函数名为operator op。该运算符中至少有一个操作数的类型和该类相同。如果某个运算符函数是类的一个成员函数,那它的左操作数或者它的唯一操作数必须是调用它的对象。索引运算符和赋值运算符都是类成员函数。

(avail++,val);
}

## 11.6 小结
复制控制:在创建和复制对象的时会调用构造函数;在包括赋值的表达式中调用赋值运算符函数;而析构函数则是在对象被显式的删除,或者程序运行到对象的生存范围以外的地方的时候会被调用。

在构造函数中分配资源的类几乎都要定义复制构造函数、复制运算符函数以及析构函数。在写一个赋值运算符函数的时候,一定要注意判断是否是自我赋值。为了与C++自带的赋值运算符一致,最好在函数返回一个对左操作数的引用调用。

自动生成的操作:如果一个类没有定义任何构造函数,编译器将自动生成默认的构造函数。如果在类中没有显式的定义复制构造函数、赋值运算符函数以及析构函数,编译器会自动生成相应的默认成员函数。这些自动生成的操作被定义为递归操作:它会递归的对类中的每个成员数据调用相应的操作。

重载运算符函数:指的是对一个已经被定义过的运算符op进行重复定义,函数名为operator op。该运算符中至少有一个操作数的类型和该类相同。如果某个运算符函数是类的一个成员函数,那它的左操作数或者它的唯一操作数必须是调用它的对象。索引运算符和赋值运算符都是类成员函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值