C++ Primer 学习笔记 — 复制控制

复制构造函数、赋值操作符和析构函数总称为复制控制。

一.复制构造函数

1.复制构造函数用途

只有单个形参,而且该形参是对本类类型对象的引用(通常是const修饰)的构造函数,称为复制构造函数,它通常可用于:

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

string  str1 = "100000";
string  str2("9999");
string  str3(str2);
区别在于第一种形式先调用一个接受C风格字符串的string构造函数,创建一个临时对象,然后调用string复制构造函数将str1初始化为临时对象的副本;第二种则是调用构造函数在生成对象str2的同时即刻进行了初始化"9999"。效率上来说当然后者更高,也是我们提倡的初始化方式。第三种则显式调用了复制构造函数将str2复制给str3 。

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

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

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

当函数的形参或者返回值为类类型时,由复制构造函数进行复制。如:

//复制构造函数用于复制返回值,而这里的参数为引用,不会调用复制构造函数
string  make_plural(size_t,  const string&,  const string&);  

4.初始化顺序容器

//默认string构造函数和5个string复制构造函数被调用
vector<string> sevc(5); 

先调用string默认构造函数创建一个临时值来初始化svec,然后使用复制构造函数将临时值复制到sevc的每个元素

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

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

2.合成复制构造函数

      如果我们没有定义复制构造函数,编译器就会为我们合成一个。与合成的默认构造函数不同,即使我们定义了其他构造函数,也会合成复制构造函数。合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。

       所谓“逐个成员”,指的是编译器将现在对象的每个非 static 成员,依次复制到正创建的对象。只有一个例外,每个成员搂类型决定了复制该成员的含义。合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。

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

3.自定义的构造函数

复制构造函数就是接受单个类类型引用形参(通常用 const 修饰)的构造函数:

class Foo {
     public:
        Foo();           // default constructor
        Foo(const Foo&); // copy constructor
        // ...
     };

  • 形参并不限制为const,但必须是一个引用。
  • 只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。
  • 类中有一个数据成员是指针或者有成员表示在构造函数中分配的其他资源;而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制构造函数。

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

4.禁止复制

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

      为了防止复制,类必须显式声明其复制构造函数为 private。

      如果复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。然而,类的友元和成员仍可以进行复制如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义

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


二.赋值操作符

1.重载赋值介绍

  •  重载操作符是一些函数,其名字为 operator 后跟着所定义的操作符的符号。因此,通过定义名为 operator= 的函数,我们可以对赋值进行定义。像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式 this 形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。
  •  大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到 this 指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为 const 引用传递
  •  赋值操作符的返回类型应该与内置类型赋值运算返回的类型相同。内置类型的赋值运算返回对左操作数的引用,因此,赋值操作符也返回对同一类类型的引用

2.合成赋值操作符

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

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

  • 可以使用合成复制构造函数的类通常也可以使用合成赋值操作符。
  • 类也可以定义自己的赋值操作符。一般而言,如果类需要复制构造函数,它也会需要赋值操作符。

三.析构函数

析构函数的一个用途是自动回收资源。一般来说撤销类对象时会自动调用析构函数来释放类的成员,但是对于在程序运行过程中动态分配的对象,则只有指向该对象的指针被删除时才可以撤销,由此容易导致内存泄漏问题。因此应将动态对象的释放写入到析构函数中去。

1、何时编写析构函数

       析构函数通常用于释放在构造函数或在对象生命期内获取的资源。

       如果类需要析构函数,则它也需要赋值操作符和复制构造函数

2、合成析构函数

       与复制构造函数和赋值运算符不同,编译器总是为我们合成一个析构函数,合成析构函数按对象创建时的逆序撤销每个非static成员

3、如何编写析构函数

      (1)析构函数是一个成员函数,无返回值,无形参

      (2)析构函数不可以重载

      (3)一个类可以定义多个构造函数,但是只能定义一个析构函数

      (4)析构函数与复制构造函数和赋值运算符之间的一个重要区别是:即使编写了自己的析构函数,合成析构函数仍然运行(先运行自己的析构函数,再运行合成 的析构函数)

四.例子

理解复制控制成员和构造函数的一个方法就是定义一个简单类,下面程序有助于我们理解复制控制中的基本概念,帮助我们理解何时执行哪个构造函数和复制控制成员,

#include <iostream>
#include <vector>

using namespace std;

class Exmpl
{
	public:
		Exmpl()
	    {
		   cout << "Exmpl()" << endl;
	    }

		Exmpl(const Exmpl&)
	    {
		   cout << "Exmpl(const Exmpl&)" << endl;
	    }

		Exmpl& operator = (const Exmpl& rhs)
	    {
		   cout << "operator = (const Exmpl&)" << endl;
		   return *this;
	    }

		~Exmpl()
	    {
		   cout << "~Exmpl()" << endl;
	    }
};

void fun1(Exmpl obj)
{

}

void fun2(Exmpl& obj)
{

}

Exmpl fun3()
{
	Exmpl obj;
	return obj;
}

int main()
{
   Exmpl eobj;//①
   cout << endl;
   fun1(eobj);//②
   cout << endl;
   fun2(eobj);//③
   cout << endl;
   eobj = fun3();//④
   cout << endl;
   Exmpl *p = new Exmpl;//⑤
   cout << endl;
   vector<Exmpl> evec(3);//⑥
   cout << endl;
   delete p;//⑦
   cout << endl;

   return 0;//⑧
}
上述程序在vc6.0上运行结果如下图:


在VS2010中,其运行结果为,并对其进行分析:


从上述结果可以看出来:不同的编译器对顺序容器的初始化采用不同的方式,在VC6.0中,先调用默认构造函数创建一个临时值对象Exmpl,然后3次调用复制构造函数,将临时值对象复制到vector中的每个元素中去,最后调用析构函数撤销临时对象Exmpl。在VS2010中,可参考上述注释。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值