C++ Primer 学习笔记_54_类与数据抽象 --复制构造函数、赋值操作符

复制控制

--复制构造函数、赋值操作符

引言:

当定义一个新类型时,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么– 复制构造函数、赋值操作符和析构函数的作用!

     复制构造函数:具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式的使用复制构造函数;当将该类型的对象传递给函数或者从函数返回该类型的对象时,将隐式使用复制构造函数。

    析构函数:作为构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。

    赋值操作符:与构造函数一样,复制操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,则编译器将为我们合成一个。

【小心地雷】

通常,编译器为我们合成的复制构造函数函数是非常精炼的---它们只做必须的工作,但对于类而言,依赖于默认定义有时会导致灾难!

一、复制构造函数

只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。复制构造函数可用于:

1)根据另一个同类型的对象显式或隐式初始化一个对象。

2)复制一个对象,将它作为实参传给一个函数。

3)从函数返回时复制一个对象。

4)初始化顺序容器中的元素。

5)根据元素初始化式列表初始化数组元素。

1、对象的定义形式

对于类类型,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化式总是调用复制构造函数。[复制初始化首先使用指定构造函数创建一个临时对象,然后复制构造函数将那个临时对象复制到正在创建的对象!]

    //理解下列语句的区别
    string null_book = "9-99999-999-9";
    string dots(10,‘.‘);
    string empty_copy = string();
    string empty_direct;

对于类类型对象,只有指定单个实参或显式创建一个临时对象用于复制时,才使用复制初始化!

支持初始化的复制形式主要是为了与C的用法兼容,当情况许可时,可以允许编译器跳过复制构造函数函数直接创建对象,但是编译器没有义务这样做!

通常直接初始化和复制初始化仅在低级别上存在差异。然而,对于不支持复制的类型,或者使用非explicit构造函数的时候,它们有本质区别:

    ifstream file1("filename"); //OK
    ifstream file2 = "filename";//Error,由于IO类型的对象不能复制

    Sales_item item = string("9-99999-999-9");

2、形参与返回值

当形参为非引用类型的时候,将复制实参的值,类似的,以非引用类型作返回值时,将返回return语句中值的副本。

//当形参/返回值为类类型时,由复制构造函数进行复制。
//然而该函数的形参是const的引用,因此不会复制
string make_plural(size_t,const string &,const string &);

3、初始化容器元素

//首先使用string默认构造函数创建一个临时值来初始化svec
//然后使用复制构造函数将临时值复制到svec的每个元素
vector<string> svec(5);

【推荐】

作为一般规则,除非你想使用容器元素的默认初始值,更有效的办法是,分配一个空容器并将已知元素的值加入容器。

4、构造函数与数组元素

如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:

    Sales_item arrItem[] = {string("0-201-16487-6"),
                            string("0-201-54848-6"),
                            string("0-201-82470-6"),
                            Sales_item()
                           };

合成的复制构造函数

合成复制构造函数的行为是:执行逐个(非static)成员初始化,将新对象初始化为原对象的副本!

合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。

逐个成员初始化最简单的概念模型是,将合成复制构造函数看作这样一个构造函数:其中每个数据成员在构造函数初始化列表中进行初始化。

class Sales_item
{
public:
    Sales_item(const Sales_item &);
private:
    std::string isbn;
    int units_sold;
    double revenue;
};

Sales_item::Sales_item(const Sales_item &orig):
    isbn(orig.isbn),units_sold(orig.units_sold),revenue(orig.revenue) {}

定义自己的复制构造函数

class Foo
{
public:
    Foo();
    Foo(const Foo &);   //复制构造函数
};

复制构造函数的形参通常是一个const引用;因为由于向函数传递对象和从函数返回对象,该构造函数一般不应设置为explicit!

对许多类而言,合成复制构造函数只完成必要的工作。只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。

然而,有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针或者有成员表示在构造函数中分配的其他资源,而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制复制构造函数!

通常,定义复制构造函数最困难的部分在于认识到需要复制构造函数O(∩_∩)O~。只要能认识到需要复制构造函数,定义构造函数一般非常简单。复制构造函数的定义与其他构造函数一样:它与类同名,没有返回值,可以(而且应该)使用构造函数初始化列表初始化新创建对象的成员,可以在函数体中做任何其他必要工作

//P410 习题13.4
class NoName
{
public:
    NoName():pstring(new std::string),i(0),d(0){}

    NoName(const NoName &temp):i(temp.i),d(temp.d)
    {
        pstring = new std::string;
        *pstring = *(temp.pstring);
    }

private:
    std::string *pstring;
    int i;
    double d;
};

禁止复制

有些类需要完全禁止复制。例如,iostream类就不允许复制。如果想要禁止复制,似乎可以省略复制构造函数,然而,如果不定义复制构造函数,编译器将合成一个。

通过声明但不定义private复制构造函数可以禁止任何复制类类型对象的尝试:用于代码中的复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误!

大多数类应定义复制构造函数和默认构造函数

不定义复制构造函数和/或默认构造函数,会严重局限类的使用:不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。

一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数。

二、赋值操作符

与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个!

1、介绍重载赋值

   重载操作符是一些函数,其名字为operator后跟着所定义的操作符的符号。因此,通过定义名为operator=的函数,我们可以对赋值进行定义。像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式this形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。

   大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以this绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。

赋值操作符也返回对同一类类型的引用。

class Sales_item
{
public:
    Sales_item &operator=(const Sales_item &);
};

2、合成赋值操作符

    合成赋值操作符会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。如:

Sales_item &Sales_item::operator=(const Sales_item &rhs)
{
    isbn = rhs.isbn;
    units_sold = rhs.units_sold;
    revenue = rhs.revenue;

    return *this;
}

3、复制和赋值常一起使用

实际上,应该将复制构造函数和赋值操作符看做一个单元,如果需要其中一个,我们几乎也可以肯定需要另一个!

//P412 习题13.9
NoName &NoName::operator=(const NoName &rhs)
{
    if (pstring)
    {
        delete pstring;
    }
    pstring = new string();
    *pstring = *(rhs.pstring);
    i = rhs.i;
    d = rhs.d;

    return *this;
}

//习题13.10
class Employee
{
public:
    typedef unsigned int num_type;

    Employee(const std::string Name = "NoName"):name(Name),mark(count)
    {
        set();
    }
    Employee(const Employee &rhs):name(rhs.name),mark(count)
    {
        set();
    }
    ~Employee()
    {
        -- count;
    }

    Employee &operator=(const Employee &rhs)
    {
        name = rhs.name;
        return *this;
    }

    ostream &output(ostream &os)
    {
        os << "Name: " << name << "\t\tMark: " << mark << endl;
        return os;
    }

private:
    std::string name;
    num_type mark;
    static num_type count;

    void set()
    {
        ++ count;
    }
};

Employee::num_type Employee::count = 0;

C++ Primer 学习笔记_54_类与数据抽象 --复制构造函数、赋值操作符,布布扣,bubuko.com

时间: 2024-10-03 14:55:46

C++ Primer 学习笔记_54_类与数据抽象 --复制构造函数、赋值操作符的相关文章

C++ Primer 学习笔记_19_类与数据抽象(5)_初始化列表(const和引用成员)、拷贝构造函数

C++ Primer 学习笔记_19_类与数据抽象(5)_初始化列表(const和引用成员).拷贝构造函数  从概念上将,可以认为构造函数分为两个阶段执行: 1)初始化阶段: 2)普通的计算阶段.计算阶段由构造函数函数体中的所有语句组成. 一.构造函数初始化列表 推荐在构造函数初始化列表中进行初始化 1.对象成员及其初始化 <span style="font-size:14px;">#include <iostream> using namespace std;

C++ Primer 学习笔记_56_类与数据抽象 --消息处理示例

复制控制 --消息处理示例 说明: 有些类为了做一些工作需要对复制进行控制.为了给出这样的例子,我们将概略定义两个类,这两个类可用于邮件处理应用程序.Message类和 Folder类分别表示电子邮件(或其他)消息和消息所出现的目录,一个给定消息可以出现在多个目录中.Message上有 save和 remove操作,用于在指定Folder中保存或删除该消息. 数据结构: 对每个Message,我们并不是在每个Folder中都存放一个副本,而是使每个Message保存一个指针集(set),set中

C++ Primer 学习笔记_57_类与数据抽象 --管理指针成员

复制控制 --管理指针成员 引言: 包含指针的类需要特别注意复制控制,原因是复制指针时只是复制了指针中的地址,而不会复制指针指向的对象! 将一个指针复制到另一个指针时,两个指针指向同一对象.当两个指针指向同一对象时,可能使用任一指针改变基础对象.类似地,很可能一个指针删除了一对象时,另一指针的用户还认为基础对象仍然存在.指针成员默认具有与指针对象同样的行为. 大多数C++类采用以下三种方法之一管理指针成员: 1)指针成员采取常规指针型行为:这样的类具有指针的所有缺陷但无需特殊的复制控制! 2)类

C++ Primer 学习笔记_55_类与数据抽象 --析构函数

复制控制 --析构函数 引言: 在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源.析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充. 1.何时调用析构函数 撤销类对象时会自动调用析构函数: Sales_item *p = new Sales_item; { Sales_item item(*p); //调用复制构造函数 delete p; //调用指针p的析构函数 } //调用对象item的析构函数 动态分配的对象只有在指向该对象的指针被删除时才撤销,

C++ Primer 学习笔记_53_类与数据抽象 --友元、static成员

类 --友元.static成员 一.友元 友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类(对未被授权的函数或类,则阻止其访问):友元的声明以关键字friend开始,但是它只能出现在类定义的内部.友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响. [最佳实践] 通常,将友元声明成组的放在类定义的开始或结尾是个好主意! 1.友元关系:一个例子 假设一个窗口管理类Window_Mgr可能需要访问由其管理的Screen对象的内部

C++ Primer 学习笔记_24_类与数据抽象(10)--static 与单例模式、auto_ptr与单例模式、const成员函数、const 对象、mutable修饰符

C++ Primer 学习笔记_24_类与数据抽象(10)--static 与单例模式.auto_ptr与单例模式.const成员函数.const 对象.mutable修饰符 前言 [例]写出面向对象的五个基本原则? 解答:单一职责原则,开放封闭原则,依赖倒置原则,接口隔离原则和里氏替换原则 里氏替换原则:子类型必须能够替换他们的基类型. 设计模式分为三种类型:创建型模式.结构型模式和行为型模式 一.static 与单例模式 1.单例模式 单例模式的意图:保证一个类仅有一个实例,并提供一个访问它

C++ Primer 学习笔记_16_类与数据抽象(2)_隐含的this指针

C++ Primer 学习笔记_16_类与数据抽象(2)_隐含的this指针 1.引言 在前面提到过,成员函数具有一个附加的隐含形参,即指向该类对象的一个指针.这个隐含形参命名为this. 2.返回*this 成员函数有一个隐含的附加形参,即指向该对象的指针,这个隐含的形参叫做this指针(编译器自动传递)使用this指针保证了每个对象可以拥有不同数值的数据成员,但处理这些成员的代码可以被所有对象共享.成员函数是只读的代码,由所有对象共享,并不占对象的存储空间,因为this指针指向当前对象,所以

C++ Primer 学习笔记_17_类与数据抽象(3)_类作用域

C++ Primer 学习笔记_17_类与数据抽象(3)_类作用域 引言: 每个类都定义了自己的新作用域与唯一的类型.即使两个类具有完全相同的成员列表,它们也是不同的类型.每个类的成员不同与任何其他类(或任何其他作用域)的成员. 一.类作用域中的名字查找 1)首先,在使用该名字的块中查找名字的声明.只考虑在该项使用之前声明的名字. 2)如果在1)中找不到,则在包围的作用域中查找. 如果找不到任何声明,则出错. 类的定义实际上是在两个阶段中处理: 1)首先,编译器声明: 2)只有在所有成员出现之后

C++ Primer 学习笔记_15_类与数据抽象(1)_类的定义和声明

C++ Primer 学习笔记_15_类与数据抽象(1)_类的定义和声明 在C++中,用类来定义自己的抽象数据类型.通过定义类型来对应所要解决的问题中的各种概念,可以使我们更容易编写.调试和修改程序.可以使得自己定义的数据类型用起来与内置类型一样容易和直观. 看一下Sales_item类: class Sales_item { private: std::string isbn; unsigned units_sold; double revenue; public: double ave_pr