对象的构造(十四)

        咱们在 C 语言中,每一个变量都有其初始值。那么问题来了,对象中成员变量的初始值是多少呢?从设计的角度来看,对象只是变量,所以:在栈上建立对象时,成员变量初始为随机值;在堆上建立对象时,成员变量初始为随机值;在静态存储区建立对象时,成员变量初识为 0 值。数组

        下来咱们以代码为例进行验证,代码以下安全

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI() { return i; }
    int getJ() { return j; }
};

Test gt;

int main()
{
    printf("gt.i = %d\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test at;
    
    printf("at.i = %d\n", at.getI());
    printf("at.j = %d\n", at.getJ());
    
    Test* pt = new Test;
    
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());
    
    return 0;
}

        gt 对象是在静态存储区建立的,因此 gt.i 和 gt.j 应该都为 0,;at 对象是在栈上建立的,因此 at.i 和 at.j 应该都为随机值;pt 对象是在堆上建立的,因此 pt->i 和 pt->j 应该也为随机值。咱们来编译下,看看是否如咱们所分析的那样呢?网络

图片.png

        咱们看到前面两个如咱们所分析的那样,最后一个不同。咱们再来看看BCC编译器呢ide

图片.png

        咱们看到BCC编译器是如咱们所分析的那样。因此咱们不能依赖于某种编译器的特性。函数

        在生活中的对象都是在初始化后上市的,初识状态(出厂设置)是对象广泛存在的一个状态。那么程序中如何对一个对象进行初始化呢?通常而言,对象都须要一个肯定的初识状态。解决方案即是在类中提供一个 public 的 initialize 函数,对象建立后当即调用 initialize 函数进行初始化。下来咱们以代码为例进行分析,在上面代码基础上加上 initialize 函数
学习

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI() { return i; }
    int getJ() { return j; }
    void initialize()
    {
        i = 1;
        j = 2;
    }
};

Test gt;

int main()
{
    gt.initialize();
    
    printf("gt.i = %d\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test at;
    at.initialize();
    
    printf("at.i = %d\n", at.getI());
    printf("at.j = %d\n", at.getJ());
    
    Test* pt = new Test;
    pt->initialize();
    
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());
    
    return 0;
}

        咱们编译,看看结果是否初始化好呢优化

图片.png

        咱们看到已经所有初始化为按照咱们所想要的状态了。可是这个就存在一个问题了,initialize 只是一个普通的函数,必须显示调用才行。若是为调用 initialize 函数的话,结果是不肯定的。若是咱们忘记在 at 对象中调用 initialize 函数,编译结果以下spa

图片.png

        那么这时问题来了,咱们该如何解决这个问题呢?在 C++ 中介意定义与类名相同的特殊成员函数,这种特殊的成员函数叫作构造函数。注意:构造函数没有返回类型的声明;构造函数在对象定义时自动被调用。那么这时咱们就能够将上面的程序改成这样设计

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI() { return i; }
    int getJ() { return j; }
    Test()
    {
        printf("Test() Begin\n");
        
        i = 1;
        j = 2;
        
        printf("Test() End\n");
    }
};

Test gt;

int main()
{
    printf("gt.i = %d\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test at;
    
    printf("at.i = %d\n", at.getI());
    printf("at.j = %d\n", at.getJ());
    
    Test* pt = new Test;
    
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());
    
    return 0;
}

        咱们编译后结果以下对象

图片.png

        咱们这样是否是就方便不少呢?那确定了。咱们能够明显看到定义了三个对象后,调用了三次构造函数。那么咱们既然知道了有构造函数这一类的函数,它是否能像通常函数那样进行带参数呢?构造函数能够根据须要定义参数;一个类中能够存在多个重载的构造函数;构造函数的重载遵循 C++ 重载的规则。咱们以前说过定义和声明不一样,在对象这块也一样适用。对象定义和对象声明时不一样的:对象定义 -- 申请对象的空间并调用构造函数;对象声明 -- 告诉编译器存在这样一个对象。下来咱们以代码为例进行分析

#include <stdio.h>

class Test
{
public:
    Test()
    {
        printf("Test()\n");
    }
    Test(int v)
    {
        printf("Test(int v), v = %d\n", v);
    }
};

int main()
{
    Test t1;         // 调用 Test()
    Test t2(1);      // 调用 Test(int v)
    Test t3 = 2;     // 调用 Test(int v)
    
    int i(10);
    
    printf("i = %d\n", i);
    
    return 0;
}

        咱们看到第 18 行的 t1 对象的构造函数确定调用了 Test(),第 19 和 20 行则是调用了 Test(int v);在 C 语言中还有 int i(10) 这种写法,咱们看看编译是否会经过?

图片.png

        咱们看到编译经过,而且如咱们所分析的那样。那么构造函数的调用是否有什么规则呢?在通常状况下,构造函数在对象定义时被自动调用,一些特殊状况下,须要手工调用构造函数。咱们如何利用构造函数来建立一个数组呢?

#include <stdio.h>

class Test
{
private:
    int m_value;
public:
    Test()
    {
        printf("Test()\n");
        
        m_value = 0;
    }
    Test(int v)
    {
        printf("Test(int v), v = %d\n", v);
        
        m_value = v;
    }
    
    int getValue()
    {
        return m_value;
    }
};

int main()
{
    Test ta[3] = {Test(), Test(1), Test(2)};
    
    for(int i=0; i<3; i++)
    {
        printf("ta[%d].getValue() = %d\n", i, ta[i].getValue());
    }
    
    Test t = Test(10);
    
    printf("t.getValue() = %d\n", t.getValue());
    
    return 0;
}

        咱们首先来分析下,数组第一个成员调用的构造函数应该是 Test(),后面两个成员调用的是 Test(int v) 函数,并打印出相应的值。最后定义的对象 t,它会打印出构造函数和获得的值都为 10,咱们来看看编译结果

图片.png

        下来咱们来开发一个数组类解决原生数组的安全性问题:提供函数获取数组长度;提供函数获取数组元素;提供函数设置数组元素。咱们来看看它是怎么实现的


IntArray.h 源码

#ifndef _INTARRAY_H_
#define _INTARRAY_H_

class IntArray
{
private:
    int m_length;
    int* m_pointer;
public:
    IntArray(int len);
    int length();
    bool get(int index, int& value);
    bool set(int index, int value);
    void free();
};

#endif


IntArray.cpp 源码

#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;
}

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;
}

void IntArray::free()
{
    delete[] m_pointer;
}


test.cpp 源码

#include <stdio.h>
#include "IntArray.h"

int main()
{
    IntArray a(5);
    
    for(int i=0; i<a.length(); i++)
    {
        a.set(i, i+1);
    }
    
    for(int i=0; i<a.length(); i++)
    {
        int value = 0;
        
        if( a.get(i, value) )
        {
            printf("a[%d] = %d\n", i, value);
        }
    }
    
    a.free();
    
    return 0;
}

        咱们编译后获得以下结果

图片.png

        下来咱们来看看特殊的构造函数:无参构造函数和拷贝构造函数。无参构造函数顾名思义就是没有参数的构造函数,而拷贝构造函数则是参数为 const class_name& 的构造函数。那么这两类构造函数有什么区别呢?无参构造函函数是当类中没有定义构造函数时,编译器默认提供一个无参构造函数,而且其函数体为空;拷贝构造函数是当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制。下来咱们以代码为例进行分析

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI()
    {
        return i;
    }
    
    int getJ()
    {
        return j;
    }
/*    
    Test()
    {
        printf("Test()\n");
    }
    
    Test(const Test& t)
    {
        printf("Test(const Test& t)\n");
        i = t.i;
        j = t.j;
    }
*/
};

int main()
{
    Test t1;
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ());
    printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ());
    
    return 0;
}

        咱们先将本身提供的无参构造函数和拷贝构造函数注释掉,编译下,看编译器是否提供默认的构造函数,是否能够经过

图片.png

        咱们看到编译是经过的,也就是说,编译器经过了默认的构造函数。咱们再来本身提供呢,看看是否会发生冲突

图片.png

        咱们看到打印出了本身定义的语句,证实它是调用了咱们本身写的构造函数。那么这个拷贝构造函数的意义在哪呢?一是兼容 C 语言的初始化方式,二是初始化行为可以符合预期的逻辑。那么这块就牵扯到是浅拷贝仍是深拷贝。浅拷贝是拷贝后对象的物理状态相同,深拷贝是拷贝后对象的逻辑状态相同。注意:编译器提供的拷贝构造函数只进行浅拷贝!

        下来咱们以实例代码看看对象的初始化是怎样进行的

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
    int* p;
public:
    int getI()
    {
        return i;
    }
    
    int getJ()
    {
        return j;
    }
    
    int* getP()
    {
        return p;
    }
    
    Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;
        
        *p = v;
    }
    
    Test(const Test& t)
    {
        i = t.i;
        j = t.j;
        p = new int;
        
        *p = *t.p;
    }
    
    void free()
    {
        delete p;
    }
};

int main()
{
    Test t1(3);
    Test t2(t1);
    
    printf("t1.i = %d, t1.j = %d, *t1.p = %d\n", t1.getI(), t1.getJ(), *t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %d\n", t2.getI(), t2.getJ(), *t2.getP());
    
    t1.free();
    t2.free();
    
    return 0;
}

        咱们看看 t1 应该进行的是浅拷贝,t2 应该进行的是深拷贝。咱们看看编译结果

图片.png

        咱们若是只有浅拷贝,没有深拷贝的话,看看结果会是怎样的,将第 34 - 41 行的代码注释掉,将第 54 和 55 行的打印 *p 的值改成打印 p 的地址。图片.png

        咱们看到它运行的时候报段错误了,t1.p 和 t2.p 指向了同一个地址。咱们看看它是怎样进行的

图片.png

        咱们看到将同一个地址释放两次确定是会出问题的,这时咱们就须要进行深拷贝了。那么咱们就要考虑到底何时须要进行深拷贝?当对象中有成员指代了系统中的资源时,如:成员指向了动态内存空间,成员打开了外存中的文件,成员使用了系统中的网络端口...

        咱们在实现拷贝构造函数这块有个通常性的原则,自定义拷贝构造函数时,必需要实现深拷贝。那么咱们再来优化下以前的数组类


IntArray.h 源码

#ifndef _INTARRAY_H_
#define _INTARRAY_H_

class IntArray
{
private:
    int m_length;
    int* m_pointer;
public:
    IntArray(int len);
    IntArray(const IntArray& obj);
    int length();
    bool get(int index, int& value);
    bool set(int index, int value);
    void free();
};

#endif

IntArray.cpp 源码

#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;
}

void IntArray::free()
{
    delete[] m_pointer;
}


test.cpp 源码

#include <stdio.h>
#include "IntArray.h"

int main()
{
    IntArray a(5);
    
    for(int i=0; i<5; i++)
    {
        a.set(i, i+1);
    }
    
    for(int i=0; i<a.length(); i++)
    {
        int value = 0;
        
        if( a.get(i, value) )
        {
            printf("a[%d] = %d\n", i, value);
        }
    }
    
    printf("\n");
    
    IntArray b = a;
    
    for(int i=0; i<b.length(); i++)
    {
        int value = 0;
        
        if( b.get(i, value) )
        {
            printf("b[%d] = %d\n", i, value);
        }
    }
    
    a.free();
    b.free();
    
    return 0;
}

        咱们看看编译结果是否如咱们代码所写的那样,建立数组并初始化。用数组 a 初始化数组 b。

图片.png

        经过对对象的构造的学习,总结以下:一、每一个对象在使用以前都应该初始化;二、类的构造函数用于对象的初始化,构造函数与类同名而且没有返回值;三、构造函数在对象定义时被自动调用,构造函数能够根据须要定义参数;四、构造函数之间能够存在重载关系,而且构造函数遵循 C++ 中重载函数的规则;五、对象定义时会触发构造函数的调用,在一些状况下能够手动调用构造函数;六、C++ 编译器会默认提供构造函数;七、无参构造函数用于定义对象的默认初识状态,拷贝构造函数在建立对象时拷贝对象的状态;八、对象的拷贝有浅拷贝和深拷贝两种方式:浅拷贝使得对象的物理状态相同,而深拷贝则使得对象的逻辑状态相同。


        欢迎你们一块儿来学习 C++ 语言,能够加我QQ:243343083

相关文章
相关标签/搜索