C++类自动提供的特殊成员函数

71 阅读7分钟
  1. 默认构造函数:

    • 如果未提供任何构造函数,c++会自动生成默认构造i函数。创建对象时会调用。

    默认样式:

    className()
    {
        
    }//初始化值随机
    
    • 若想创建对象时不显式的对它进行初始化,则必须显式的定义默认构造函数:
    //例如:
    Klunk::Klunk()
    {
        klunk_ct=0;//可以用其设置特定的值
        ...
    }
    
    • 带参的构造函数也可以是默认构造函数,只要所有参数都有默认值:

      Klunk(int n=0)
      {
          klunk_ct=n;
      }
      
      • 只能有一个默认构造函数,否则会引发二义性
    例子:
    String::String()
    {
        len=0;
        str=new char[l];//与下面析构函数相匹配
        str[0]='\0';//default string
    }
    
    • 析构函数中包含如下代码:
    delete [] str;
    
    • delete[]与使⽤new[]初始化的指针和空指针都兼容。
    String::String()
    {
        len=0;
        //str=new char[l];//与下面析构函数相匹配
        //str[0]='\0';
        //更改为
        str=0;
        //default string
    }
    
  2. 默认析构函数:

  3. 复制构造函数:

    • 复制构造函数⽤于将⼀个对象复制到新创建的对象中。⽤于初始化过程中(包括按值传递参数)。

    • 原型:

      Class_name(const Class_name&);
      

      它接受⼀个指向类对象的常量引⽤作为参数。

      例如:StringBad(const StringBad&);

    • 新建⼀个对象并将其初始化为同类现有对象时,复制构造函数都将被调⽤。最常⻅的情况是将新对象显式地 初始化为现有的对象。

      例子:

      StringBad ditto(motto);
      StringBad metto=motto;
      StringBad also=StringBad(motto);
      StringBad * pStringBad=new StringBad(motto);
      
      • 其中中间的2种声明可能会使⽤复制构造函数直接创建metoo和 also,也可能使⽤复制构造函数⽣成⼀个临时对象,然后将临时对象的 内容赋给metoo和also,这取决于具体的实现。
      • 最后⼀种声明使⽤motto 初始化⼀个匿名对象,并将新对象的地址赋给pstring指针。
    • 每当程序⽣成了对象副本时,编译器都将使⽤复制构造函数。

      • 当函数按值传递对象(如程序清单12.3中的callme2())或函数返回对象时,都将使⽤复制构造函数。
      • 按值传递意味着创建原始变量的⼀个副本。编译器⽣成临时对象时,也将使⽤复制构造函数。
    • 由于按值传递对象将调⽤复制构造函数,因此应该按引⽤传递对象。这样可以节省调⽤构造函数的时间以及存储新对象的空间

    复制构造函数的功能:
    • 默认的复制构造函数逐个复制⾮静态成员(成员复制也称为浅复制),复制的是成员的值
    • 静态函数(如num_strings)不受影响,因为它们**属于整个类,**⽽不是各个对象。
    引例:
    • 让程序准确地记录对象计数。解决办法是提供⼀个对计数进⾏更新的显式复制构造函数:

      StringBad::StringBad(const StringBad&)
      {
          num_strings++;
          ...
      }
      
      • 如果类中包含用于记录对象数的静态成员,且其值会在新对象被创建时发生变化,则应提供一个显式复制构造函数来处理计数问题。
    • 隐式复制构造函数是按值进⾏复制的。

      • 隐式复制构造函数的功能相当于:

        sailor.str=sport.str;
        //复制的是指向字符串的指针,而不是字符串本身。
        //当调用析构函数时将产生问题,可能对同一块内存区域进行两次删除,这将导致程序异常终止。
        
      • 解决方案:定义一个显式复制构造函数。

        • 解决类设计中这种问题的⽅法是进⾏深度复制(deep copy)。

        • 复制构造函数应当复制字符串并将副本的地址赋给str成员,⽽不 仅仅是复制字符串地址。

          函数实现:

          StringBad::StringBad(const StringBad& st)
          {
              num_strings++;
              len=st.len;
              str=new char[len+1];
              std::strcpy(str,st.str);
              
              cout<<num_strings<<":\"<<str<<"\"object created"\n";
              ...
          }
          

          必须定义复制构造函数的原因在于,⼀些类成员是使⽤new初始化的、指向数据的指针,⽽不是数据本⾝。

        • 如果类中包含了使⽤new初始化的指针成员,应当定义⼀个复制构造函数,以复制指向的数 据,⽽不是指针,这被称为深度复制。复制的另⼀种形式(成员复制或浅复制)只是复制指针 值。浅复制仅浅浅地复制指针信息,⽽不会深⼊“挖掘”以复制指针引⽤的结构。

  4. 赋值运算符:

    ANSI C允许结构赋值,⽽C++允许类对象赋值,这是通过⾃动为类重载赋值运算符实现的。

    • 原型:

      Class_name & Class_name::operator=(const Class_name &);
      //它接受并返回⼀个指向类对象的引⽤
      //例子:
      StringBad & StringBad::operator=(const StringBad &);
      
    赋值运算符的功能及何时调用:
    • 已有的对象赋给另⼀个对象时,将使⽤重载的赋值运算符:
    StringBad headline1("Celery Stalks at Midnight");
    ...
    StringBad knot;
    knot=headline1;
    
    • 初始化对象时,并不⼀定会使⽤赋值运算符。
    StringBad metoo=knot;
    

    metoo是⼀个新创建的对象,被初始化为knot的值,因此使⽤复制构造函数

    实现时也可能分两步来处理这条语句:

    使⽤复制构造函数创建⼀个临时对象,然后通过赋值将临时对象的值复制到新对象中。

    初始化总是会调⽤复制构造函数, ⽽使⽤=运算符时也可能调⽤赋值运算符。

    • 赋值运算符的隐式实现也对成员进⾏逐个复制浅复制将导致相同地址重复删除,造成数据受损
      • 如果操作结果是不确定的,则执⾏的操作将随编译器⽽异,包括显⽰独⽴声明 (Declaration of Independence)或释放隐藏⽂件占⽤的硬盘空间。当 然,编译器开发⼈员通常不会花时间添加这样的⾏为。
    • 如果成员本⾝就是类对象,则程序将使⽤为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
    解决赋值的问题:

    解决办法是提供赋值运算符(进⾏深度复制)定义

    • 由于⽬标对象可能引⽤了以前分配的数据,所以函数应使⽤**delete[ ]**来释放这些数据。(即程序运行后,该目标对象将不再指向此内存位置,这将导致内存浪费。)
    • 函数应当避免将对象赋给⾃⾝;否则,给对象重新赋值前,释放内 存操作可能删除对象的内容。
    • 函数返回⼀个指向调⽤对象的引⽤
      • 通过返回⼀个对象,函数可以像常规赋值操作那样,连续进⾏赋 值,即如果S0、S1和S2都是StringBad对象,则可以编写这样的代码:
    S0=S1=S2;
    //使用函数表示法时,转换为;
    S0.operator=(S1.operator=(S2));
    //,S1.operator=(S2)的返回值是函数S0.operator=()的参数。
    //返回值是⼀个指向StringBad对象的引⽤,因此参数类型是正确的。
    
    例子:为StringBad类编写赋值运算符:
    StringBad & StringBad::operator=(const StringBad & st)
    {
        if(this==&st)
            return *this;
        delete [] str;
        len=st.len;
        str=new char[len+1];
        std::strcpy(str,st.str);
        return *this;
    }
    
    • 代码⾸先检查⾃我复制,这是通过查看赋值运算符右边的地址 (&s)是否与接收对象(this)的地址相同来完成的。如果相同,程序 将返回*this,然后结束。
    • 如果地址不同,函数将释放str指向的内存,这是因为稍后将把⼀个 新字符串的地址赋给str。如果不⾸先使⽤delete运算符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存被浪费掉。
    • 接下来为新字符串分配⾜够的内存 空间,然后将赋值运算符右边的对象中的字符串复制到新的内存单元中。
    • 程序返回*this并结束。
    • 赋值操作并不创建新的对象,因此不需要调整静态数据成员 num_strings的值。
  5. 地址运算符: