Chinaunix首页 | 论坛 | 博客
  • 博客访问: 84313
  • 博文数量: 21
  • 博客积分: 371
  • 博客等级: 一等列兵
  • 技术积分: 225
  • 用 户 组: 普通用户
  • 注册时间: 2012-11-15 21:32
文章分类

全部博文(21)

文章存档

2013年(5)

2012年(16)

我的朋友

分类: C/C++

2013-02-03 23:08:27

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

2.      不管是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数

3.      复制构造函数可用于:

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

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

●初始化顺序容器中的元素

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

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

        所谓逐个成员指的是编译器将现有对象的每个非static成员,一次复制到正创建的对象.只有数组复制是个例外.如果类具有数组成员,则合成复制构造函数将复制数组.复制数组时合成复制构造函数将复制数组的每一个元素.

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

5.下面两种情况必须定义复制构造函数:

        ●有些类必须对复制对象时发生的事情加以控制;这样的类经常有一个数据成员是指针.或者有成员表示在构造函数中分配的其他资源.

        ●某些类在创建新对象时必须做一些特定工作

        定义复制构造函数名与类同名,无返回值,可以(而且应该使用构造函数初始化列表初始化新创建的成员,可以在函数体中做任何其他必要的工作.

6.为了防止复制,类必须显示声明其复构造函数为private,如果想要连友元和成员中的复制也禁止,就可以声明一个复制构造函数但不对其定义.

        大多数类应定义复制构造函数和默认构造函数,如果定义了复制构造函数,也必须定义默认构造函数.

7.赋值操作符接受单个形参,且该形参是同一类型的对象.右操作数一般作为const引用传递.

(2011-12-30加上)而且赋值操作符返回的类型一般都是左操作数类型的的引用.(书上说内置内型是返回右操作数的引用.我觉得不对,而且它给出的例子也是返回左操作数的引用)

举例:

struct A
{

string strName;

int     iID;

A& operator=(constA& a )

{

strName = a.strName;

iID= a.iID;

return *this;

}

};

8.当对象的引用或者超出作用域时,不会运行析构函数.只有删除指向动态分配对象的指针或者实际对象(而不是对象的引用)超出作用域时,才会运行析构函数.并且元素总是逆序撤销

        通常类需要析构函数,则它也需要赋值操作符和复制构造函数.这是一个有用的经验法则

        与复制构造函数或者赋值操作符不同的是,编译器总是会为我们合成一个析构函数

9.析构函数与复制构造函数或者赋值操作符之间的一个重要区别是:即使我们编写了自己的析构函数,合成析构函数仍然运行.

10.编写自己的复制构造函数时,必须显式复制需要复制的任意成员.显式定义复制构造函数不会进行任何自动复制

11.即使对象赋值给自己,赋值操作符的正确工作也非常重要.保证这个行为的通用方法是显示检查对自身的赋值.赋值操作符通常要做复制构造函数和析构函数也要完成的工作.在这种情况下,通用工作应放在private使用函数中.

(2011-12-30 通常如果我们需要定义析构函数,那么我们也应该定义复制构造函数与赋值操作符)

如果想要测试构造函数的调用情况.我们可以使用下面类似的方法来查看.

  1. #include   
  2. #include   
  3.   
  4. using std::string;  
  5. using namespace std;  
  6. class A  
  7. {  
  8. private:  
  9.     string strName;  
  10.     int     iID;  
  11. public:  
  12.     A()  
  13.     {  
  14.         cout<<"A的构造函数运行...\n";  
  15.     }  
  16.   
  17.     A& operator=( const A& a );  
  18.     A( const A& a )  
  19.     {  
  20.         cout<<"A的复制函数正在运行...\n";  
  21.     }  
  22. };  
  23. A& A::operator=( const A& a )  
  24. {  
  25.     cout<<"A的赋值操作符运行..\n";  
  26.     strName = a.strName;  
  27.     iID     = a.iID;  
  28.     return *this;  
  29. }  
  30. void main()  
  31. {  
  32.     A a;  
  33. //  A b = a;  
  34.     a =b;  
  35.   
  36.     system( "pause" );  
  37.     return;  
  38. }  

运行结果如下:

12.包含指针的类需要特别注意复制控制,原因是赋值指针时只是复制指针中的地址,而不会赋值指针指向的对象

13.大多数C++类采用以下三种方法之一管理指针成员:

        ●指针成员采取常规指针型行为.这样的类具有指针的所有缺陷但无需特殊的复制控制

●类可以实现所谓的智能指针行为,指针所指向的对象是共享的,但类能防止垂悬指针

●类采取值型行为.指针所指向的对象是唯一的,有每个类对象独立管理

具有指针成员且使用默认合成复制构造函数的类具有普通指针的所有缺陷.尤其是,类本身无法避免垂悬指针.

14.引入智能指针时使用计数,将一个计数器与类指向的对象相关联(这类似于COM技术中的引用计数技术).复制构造函数复制指针并增加与之相应的使用技术.对一个对象进行赋值时,赋值操作符减少左操作数所指向的使用计数的值(如果减少至0,则删除对象),并增加右操作数所指对象的引用计数的值.


复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。

    复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。


    析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。不管类是否定义了自己的析构函数,编译器都会自动为类中非 static 数据成员执行析构函数。

 

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

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

            ~myclass(){}; // 析构函数
      myclass& operator=(const myclass &obj){}; // 赋值操作符
   private:
           int age;
           string name;
}

13.1 复制构造函数

    编译器合成的复制控制函数是非常精练的——它们只做必需的工作。但对某些类而言,依赖于默认定义会导致灾难。实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。

 

    复制构造函数在下列情况下会被调用:


myclass obj1;
myclass obj2 = obj1; // 根据另一个同类型的对象显式或隐式初始化一个对象



myclass fun(myclass par)
{
    // ... 

    return par; // 从函数返回时复制一个对象
}

fun(obj1); // 复制一个对象,将它作为实参传给一个函数


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

myclass ls[]{obj1,obj1,obj1,obj1};// 根据对象初始化数组

myclass ls[]{myclass(),myclass(),myclass()}; // 按照书上说是会调用复制构造函数但实际不会调用,据说是做了优化


    如果不提供显示的复制构造函数系统会合成一个。合成的构造函数会在上述情况发生时会赋值对象副本并将对象数据成员逐一初始化成与原对象相同的值。
    有个有趣的现象:数组是不能复制的,但如果对象数据成员是个数组类型却可以复制数组给对象副本的对应成员,合成复制构造函数模型如下

myclass(const myclass &obj):age(obj.age),name(obj.name)
{
    // obj是源对象,用它来复制副本

}


    如果想禁止复制可以显示声明私有的复制构造函数(最好不要这么做否则类只能作为指针或引用传递),复制构造函数属于构造函数,一旦定义复制构造函数应该给类显示同时定义一个默认构造函数。

 

13.2 赋值操作符   

    类的赋值操作符实际上是操作符重载(operator=

    赋值操作结果和拷贝构造函数类似,它会执行逐个成员赋值(复制构造是逐个成员初始化,然后也允许重新赋值)


myclass& operator=(const myclass &obj)
{
    // obj是源对象,用它来为操作符左面对象赋值

    age = obj.age;
    name = obj.name;
    return *this;
}

 

    赋值操作符和复制构造函数几乎可以看做一个整体,如果需要其中一个几乎肯定也需要另外一个。

    关于操作符重载会在后续章节做详细介绍。

 

13.3 析构函数
    析构函数一个用途是对象在销毁之前做一些相关操作,比如清理资源,刷新缓冲区等。析构函数在对象即将销毁前执行


class he
{
    public:
        string name;
        ~he(){cout << name << " is delete!" << endl;};
};

int main()
{
    he cls;
    cls.name = "zhang san";

    he *ls = new he[4];
    ls[0].name = "item0";
    ls[1].name = "item1";
    ls[2].name = "item2";
    ls[3].name = "item3";
    delete [] ls;

    cout << "delete list" << endl;

    he *pr = new he();
    pr->name = "li si"

    cout << "delete li si" << endl;
}

// 输出:

item3 is delete!
item2 is delete!
item1 is delete!
delete list
li si is delete!
delete li si
zhang san is delete!

 

    赋值操作和复制(拷贝)构造函数效果类似,在使用=号操作时有时候会调用赋值有时候会调用复制构造函数,怎么区分调用方式呢?

    复制(拷贝)构造函数,是用一个已知的对象去初始化另一个正在创建的对象;赋值操作,是用一个已经存在的对象去更新另一个已经存在的对象。

    myclass a ;
    myclass b = a ;  // 用一个已知的对象去初始化另一个正在创建的对象,调用复制构造函数
    b = a ;  // 用一个已经存在的对象去更新另一个已经存在的对象,调用赋值操作

   

    赋值操作符可以通过指定不同类型的右操作数而重载,看代码


class myclass
{
   public
      myclass& operator=(const myclass &obj){ name = obj.name; return *this;}; // 赋值操作符

      myclass& operator=(string str){ name = str; return *this;}; // 赋值操作符重载
   private:
     string name;
}

myclass a;
myclass b;
b = a;     // 调用 operator=(const myclass &obj)版

b = "tom"// 调用 operator=(string str)版

 

    本章最后介绍了智能指针的概念。它不是c++具体技术而是解决拷贝对象时指针字段会可能会引发错误的解决方案

// 类数据成员指针类
class myclass
{
    public:
        string name;
        int age;
};

// 智能指针

class curr
{
    // 将具体类设置成智能指针的友元类

    friend class test;
    private:
        curr(myclass &ip):cur(&ip),used(1) {};

        // 最后一个拥有指针成员的对象消亡时会删除智能指针对象,析构函数执行删除真正指向的类对象

        ~curr()
        {
             cout << "已经没有任何指针指向myclass对象!"<< endl;
             delete cur; // 构造函数参数*ip必须是动态创建的对象指针 delete才能正确删除,否则会产生无法预知的运行时错误

         };

        myclass *cur;
        int used;
};

// 具体类

class test
{
    public:
        test(myclass &ip, string stname,int stage): pro(new curr(ip)) ,name(stname) ,age(stage) {}; 

        test(const test &t): pro(t.pro) ,name(t.name) ,age(t.age)
        {
            ++pro->used;
        };

        ~test()
        {
            --pro->used;

            // 最后一个引用对象消失,智能指针计数器等于0,删除智能指针动态对象(智能指针删除时会出发自身的析构函数,析构函数中负责删除类成员)

            if(pro->used == 0)
            {
                cout << "over" << endl;
                delete pro;
             }
         };
        void show(){cout << pro->used << endl;};
    private:
        curr *pro;
        string name;
        int age;
};

int main()
{
    myclass *pr =  new myclass();

    test *t1 = new test(*pr,"tom",21) ;
    t1->show(); // 1


    test *t2 = new test(*t1);
    t1->show(); // 2

    t2->show(); // 2

    delete t1; 

    t2->show(); // 1

    delete t2; // over 已经没有任何指针指向myclass对象!

    智能指针基本思路是用智能指针对象替换数据成员类对象指针,由智能指针维护对象指向。当具体类发生拷贝或删除时更新智能指针维护的计数器。如果计数器==0说明所有具体类都消亡,删除智能指针。智能指针再负责删除数据成员对象。



阅读(1669) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~