C++学习笔记--构造函数

来源:互联网 发布:软件项目进度报告 编辑:程序博客网 时间:2024/05/19 19:32

当我们创建了一个类并定义了一个对象后并不显式初始化,我们知道对象成员变量的初始值是多少吗?是随机值还是0?答案是两者都有可能,在堆和栈上创建的对象成员初始值为随机值,在全局数据区创建的对象成员初始值为0。但是我们并不想它一会儿为随机值一会儿为0,想让它初始值为多少就为多少,又该怎么办?方法肯定是有的,在类里我们可以定义一个成员函数对成员变量进行赋值然后每次在创建对象后紧接着就调用初始化函数这样是不是就可以让对象的成员变量变成自己想要的任何值了,但是万一忘了调用初始化函数那么运行结果不就还是不确定了吗?幸运的是现实并不是这么残酷,C++中给我们提供了一个特殊的成员函数让我们能够随意的初始化对象成员变量。为什么可以这么肆无忌惮呢?因为这个特殊的成员函数是自动被调用的,那么它就是今天的主题-----构造函数。

构造函数有什么样的存在意义呢?它可以在对象定义时初始化它的成员变量。构造函数确实是一个特殊的成员函数,因为它的函数名和类名相同,没有返回值,并且在对象定义时自动被调用,这样在任何存储区创建的对象都可以初始化成你想要的初始值了,注意,每个对象在使用之前都应该初始化。下面给出一个带构造函数的类:

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

前面说到我们可以定义构造函数并在函数内部实现对象成员变量的初始化,那么要是我们想给同一个类创建的不同对象初始化成不同的初始值该怎么弄,按照前面的类的实现

来看好像并不是可行的,那么该怎么修改程序实现想要的功能呢?

那么就是带参构造函数出场的时候了,是的,构造函数可以带有参数,我们可以根据需要来定义带参构造函数,那是不是又说明构造函数可以实现重载了?当然了,一个类中

可以在多个重载的构造函数并且遵循C++函数重载的规则。比如:

class Test{private:    int i;    int j;public:    int getI() { return i; }    int getJ() { return j; }    Test()    {        i = 1;        j = 2;    }    Test(int m)    {        i = m;        j = 3;    }    Test(int m,int n)    {        i = m;        j = n;    }};
接下来就该说怎么调用构造函数了,虽然说是自动调用,但是现在有三个构造函数,应该调用哪一个呢?在说明之前先友情提醒一下,对象定义和对象声明是不同的,对象

定义(Test t;)会申请对象的空间并调用构造函数,对象声明(extern Test t;)只是提醒编译器存在这么一个对象。说到构造函数的自动调用其实也简单,就跟普通函数重载调用没多

大区别,就是由你给出的初始化参数列表来确定调用哪个构造函数,反正对象只会调用一个构造函数。

    Test t;      // 调用 Test()    Test t1(1);  // 调用 Test(int m)    Test t2 = 2; // 调用 Test(int m)    Test t3(3,4);// 调用 Test(int m,int n)
上面代码展示的是不是很详细了,你给出了什么样的初始化值列表就会调用对应的重载构造函数。

一般来说构造函数在对象定义时被自动调用,但是一些特殊情况下我们也需要手工调用构造函数。如何手工调用构造函数?

    Test t4 = Test(100);//定义并初始化,手动调用构造函数    Test t5 = Test(100,200);//定义并初始化,手动调用构造函数
我们在定义对象时将构造函数手动调用,那么我们该如何定义并初始化一个对象数组

Test tarr[3] = {Test(), Test(1), Test(2,3)};   //手动调用构造函数
在构造函数中存在两个特殊的构造函数。不是说构造函数本身就比较特殊了吗?当然了,就是在特殊之上再特殊点,它们就是无参构造函数和拷贝构造函数。无参构造函数就是没有参数的构造函数,拷贝构造函数就是参数为 const classname& 类型的构造函数。当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且函数体为空,所以有时候就会被问到一个空类里是不是什么都没有的问题,这时就需要注意了,即便类里什么也没有,当定义对象时编译器也会默认提供一个无参构造函数;当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,它简单的进行成员变量的值复制

看到拷贝构造函数后我们会想到它究竟有什么作用?

还记得在定义变量时可以被另一个已初始化的变量初始化吧,同样的,一个类对象在定义时也可以被其他已存在的类对象初始化,这就是拷贝构造函数存在的意义了,利用一个已存在的类对象创建一个一模一样的新对象,它是为了兼容C语言的初始化方式,并且使初始化行为能够符合预期的逻辑,预期的逻辑就是两个对象的状态完全一样。

当执行下面两条语句后会发生什么呢?

    Test t1;    Test t2 = t1;
实际上,这时编译器就会默认提供一个拷贝构造函数并调用了。如下所示:

Test(const Test& t)    {        i = t.i;        j = t.j;    }

但是它提供的拷贝构造函数属于浅拷贝,就是拷贝后对象的物理状态相同,即单纯的进行值复制,这样就会导致对象之间占据的内存的每个字节是相同的,看一个例子:

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;    }    void free()    {        delete p;    }};int main(){    Test t1(3);    Test t2(t1);    printf("t1.i = %d, t1.j = %d, t1.p = %p\n", *t1.getI(), *t1.getJ(), t1.getP());    printf("t2.i = %d, t2.j = %d, t2.p = %p\n", *t2.getI(), *t2.getJ(), t2.getP());    //t1.free();    //t2.free();    return 0;}
输出结果:

t1.i = 1, t1.j = 2, t1.p = 00113768
t2.i = 1, t2.j = 2, t2.p = 00113768
我们在构造函数里申请了一个堆空间的变量,当使用t1初始化t2时编译器发现类里面没有自定义的拷贝构造函数,所以编译器会提供一个浅拷贝的拷贝构造函数,导致两个类对象的指针变量都指向了同一个空间(00113768),若是在最后两次释放同一个内存就会发生内存错误,这就是浅拷贝存在的缺点。所以我们就应该手工提供一个拷贝构造函数:

    Test(const Test& t)    {        i = t.i;        j = t.j;        p = new int;        *p = *t.p;    }
再次看看输出结果:

t1.i = 1, t1.j = 2, t1.p = 00113768
t2.i = 1, t2.j = 2, t2.p = 00113798

原理就是重新申请一个空间接收初始化的数据,打印出来的数据虽然看起来导致了两个对象状态不同,实际上我们需要的状态仅仅是内存空间里保存的值一样,所以这就满足了逻辑状态,实现了深拷贝。
那什么时候需要进行深拷贝?

对象中有成员指代了系统的资源,如:成员指向了动态内存空间,成员打开了硬盘中的文件,成员使用了系统中的网络端口

所以前面发生的现象就是因为类成员使用了堆空间的变量。

对象构造顺序

局部对象的构造顺序依赖于程序执行流。

堆对象的构造顺序依赖于new的执行顺序。

C中的全局变量的初始化顺序因编译器的不同而不同,C++中全局对象的构造顺序因编译器的不同而不同。goto语句导致程序执行流顺序改变,可能会导致对象的构造顺序发生变化。












原创粉丝点击