第27课二阶构造模式(上)---------出现的背景

构造函数的回顾
关于构造函数
——类的构造函数用于对象的初始化
——构造函数与类同名并且没有返回值
——构造函数在对象定义时自动被调用

问题:
1. 如何判断构造函数的执行结果?
   目前来说,没有办法来判断构造函数的执行结果
2. 在构造函数中执行return语句会发生什么?
    在构造函数中可以存在return语句,return之后下面的代码就无法执行,会影响对象的初始状态
3. 构造函数执行结束是否意味着对象构造成功?
   对象的诞生与构造函数的执行结果是没有任何关系的。如果构造函数没有执行成功,只会影响它的初始状态,并不影响对象的创建。

异常的构造函数

#include <stdio.h>

class Test
{
    int mi;
    int mj;public:
    Test(int i, int j)
    {
        mi = i;
        
        return;
        
        mj = j;
        
    }
    int getI()
    {
        return mi;
    }
    int getJ()
    {
        return mj;
    }
    
};

int main()
{  
    Test t1(1, 2);
    
    printf("t1.mi = %d
", t1.getI());
    printf("t1.mj = %d
", t1.getJ());
    return 0;
}

在构造函数中执行return语句:对象会被创建成功,但是对象的初始状态会发生异常。例如在这里的本意是将对象的成员mj赋值为2,但是得到的结果却是随机值。

下面可以这样做:

#include <stdio.h>

class Test
{
    int mi;
    int mj;
    bool mStatus;
public:
    Test(int i, int j) : mStatus(false)
    {
        mi = i;
        
        return;
        
        mj = j;
        
        mStatus = true;
    }
    int getI()
    {
        return mi;
    }
    int getJ()
    {
        return mj;
    }
    int status()
    {
        return mStatus;
    }
};

int main()
{  
    Test t1(1, 2);
    
    if( t1.status() )
    {
        printf("t1.mi = %d
", t1.getI());
        printf("t1.mj = %d
", t1.getJ());
    
    }
    
    return 0;
}

这种解决方案确实可以,就是强行的让构造函数有一个返回值。并且手工的调用status来得到构造函数的返回值。这个似乎可以解决问题,但是不够优美。

通过上面的实验,可以得到:
构造函数
——只提供自动初始化成员变量的机会
——不能保证初始化逻辑一定成功
——执行return语句后构造函数立即结束。

构造函数能决定的只是对象的初始状态,而不是对象的诞生。

半成品对象的概念
——初始化操作不能按照预期完成而得到的对象
——半成品对象是合法的C++对象,也是Bug的重要来源

办成品对象的危害

还是以之前创建的那个数组类为例,进行分析。

#include "IntArray.h"

IntArray::IntArray(int len)
{
    m_pointer = new int[len];  
    
    for(int i=0; i<len; i++)
    {
        m_pointer[i] = 0;
    }
    
    m_length = len;
}

IntArray::IntArray(const IntArray& obj)
{
    m_length = obj.m_length;
    
    m_pointer = new int[obj.m_length];
    
    for(int i=0; i<obj.m_length; i++)
    {
        m_pointer[i] = obj.m_pointer[i];
    }
}

int IntArray::length()
{
    return m_length;
}

bool IntArray::get(int index, int& value)
{
    bool ret = (0 <= index) && (index < length());
    
    if( ret )
    {
        value = m_pointer[index];
    }
    
    return ret;
}

bool IntArray::set(int index, int value)
{
    bool ret = (0 <= index) && (index < length());
    
    if( ret )
    {
        m_pointer[index] = value;
    }
    
    return ret;
}

IntArray::~IntArray()
{
    delete[]m_pointer;
}
分析下面这段代码:
IntArray::IntArray(int len) { m_pointer = new int[len]; //申请堆空间 for(int i=0; i<len; i++) { m_pointer[i] = 0; } m_length = len; }
问题:每次申请堆空间时,你能保证每次都申请成功吗?也许1万次了,出现几次不成功。看上去概率很低,但是我们也不能容忍,你可能会这样去做。
IntArray::IntArray(int len)
{
    m_pointer = new int[len];   //申请堆空间
    if(m_pointer)
    {  for(int i=0; i<len; i++)
      {
          m_pointer[i] = 0;
      }
    }
    m_length = len;
}
这样当申请失败了,就不需要执行花括号里面的代码了。看似没有什么问题,也合情合理。
注意:前面已经说过,构造函数内执行是否成功,与对象是否创建没有什么关系。上面这个例子中构造函数体内没有执行成功,肯定会影响对象的初始状态,但是用户并不知道这个对象的成员有异常。当用户拿到这个类时,它并不知道你花括号里面的内容没有执行,该怎么用就怎么用。
看下面的使用:
int main()
{
  IntArray a(5);    
  a.set(0,1);
}
编译没有问题,运行时悲剧就产生了。段错误。出现的原因就是在构造函数中
m_pointer没有申请成功,但是下面又使用了它。