对象的构造(十四)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对象的构造(十四)相关的知识,希望对你有一定的参考价值。

        我们在 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 应该也为随机值。我们来编译下,看看是否如我们所分析的那样呢?

技术分享图片

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

技术分享图片

        我们看到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;
}

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

技术分享图片

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

技术分享图片

        那么这时问题来了,我们该如何解决这个问题呢?在 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;
}

        我们编译后结果如下

技术分享图片

        我们这样是不是就方便很多呢?那肯定了。我们可以明显看到定义了三个对象后,调用了三次构造函数。那么我们既然知道了有构造函数这一类的函数,它是否能像一般函数那样进行带参数呢?构造函数可以根据需要定义参数;一个类中可以存在多个重载的构造函数;构造函数的重载遵循 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) 这种写法,我们看看编译是否会通过?

技术分享图片

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

#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,我们来看看编译结果

技术分享图片

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


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

        我们编译后得到如下结果

技术分享图片

        下来我们来看看特殊的构造函数:无参构造函数和拷贝构造函数。无参构造函数顾名思义就是没有参数的构造函数,而拷贝构造函数则是参数为 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;
}

        我们先将自己提供的无参构造函数和拷贝构造函数注释掉,编译下,看编译器是否提供默认的构造函数,是否可以通过

技术分享图片

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

技术分享图片

        我们看到打印出了自己定义的语句,证明它是调用了我们自己写的构造函数。那么这个拷贝构造函数的意义在哪呢?一是兼容 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 应该进行的是深拷贝。我们看看编译结果

技术分享图片

        我们如果只有浅拷贝,没有深拷贝的话,看看结果会是怎样的,将第 34 - 41 行的代码注释掉,将第 54 和 55 行的打印 *p 的值改为打印 p 的地址。技术分享图片

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

技术分享图片

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

        我们在实现拷贝构造函数这块有个一般性的原则,自定义拷贝构造函数时,必须要实现深拷贝。那么我们再来优化下之前的数组类


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。

技术分享图片

        通过对对象的构造的学习,总结如下:1、每个对象在使用之前都应该初始化;2、类的构造函数用于对象的初始化,构造函数与类同名并且没有返回值;3、构造函数在对象定义时被自动调用,构造函数可以根据需要定义参数;4、构造函数之间可以存在重载关系,并且构造函数遵循 C++ 中重载函数的规则;5、对象定义时会触发构造函数的调用,在一些情况下可以手动调用构造函数;6、C++ 编译器会默认提供构造函数;7、无参构造函数用于定义对象的默认初识状态,拷贝构造函数在创建对象时拷贝对象的状态;8、对象的拷贝有浅拷贝和深拷贝两种方式:浅拷贝使得对象的物理状态相同,而深拷贝则使得对象的逻辑状态相同。


        欢迎大家一起来学习 C++ 语言,可以加我QQ:243343083

以上是关于对象的构造(十四)的主要内容,如果未能解决你的问题,请参考以下文章

第十四章 类型信息1(java的反射)

Java入门十四 构造函数

Java学习总结(十四)——java反射机制,利用反射动态创建对象

Spring详解(二)

在 Visual Studio 中创建构造函数的代码片段或快捷方式

JavaSE 学习笔记之反射技术(二十四)