对象的构造(十四)
我们在 C 语言中,每个变量都有其初始值。那么问题来了,对象中成员变量的初始值是多少呢?从设计的角度来看,对象只是变量,因此:在栈上创建对象时,成员变量初始为随机值;在堆上创建对象时,成员变量初始为随机值;在静态存储区创建对象时,成员变量初识为 0 值。
下来我们以代码为例进行验证,代码如下
#include<stdio.h>classTest{private:inti;intj;public:intgetI(){returni;}intgetJ(){returnj;}};Testgt;intmain(){printf("gt.i=%d\n",gt.getI());printf("gt.j=%d\n",gt.getJ());Testat;printf("at.i=%d\n",at.getI());printf("at.j=%d\n",at.getJ());Test*pt=newTest;printf("pt->i=%d\n",pt->getI());printf("pt->j=%d\n",pt->getJ());return0;}
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>classTest{private:inti;intj;public:intgetI(){returni;}intgetJ(){returnj;}voidinitialize(){i=1;j=2;}};Testgt;intmain(){gt.initialize();printf("gt.i=%d\n",gt.getI());printf("gt.j=%d\n",gt.getJ());Testat;at.initialize();printf("at.i=%d\n",at.getI());printf("at.j=%d\n",at.getJ());Test*pt=newTest;pt->initialize();printf("pt->i=%d\n",pt->getI());printf("pt->j=%d\n",pt->getJ());return0;}
我们编译,看看结果是否初始化好呢
我们看到已经全部初始化为按照我们所想要的状态了。但是这个就存在一个问题了,initialize 只是一个普通的函数,必须显示调用才行。如果为调用 initialize 函数的话,结果是不确定的。如果我们忘记在 at 对象中调用 initialize 函数,编译结果如下
那么这时问题来了,我们该如何解决这个问题呢?在 C++ 中介意定义与类名相同的特殊成员函数,这种特殊的成员函数叫做构造函数。注意:构造函数没有返回类型的声明;构造函数在对象定义时自动被调用。那么这时我们就可以将上面的程序改为这样
#include<stdio.h>classTest{private:inti;intj;public:intgetI(){returni;}intgetJ(){returnj;}Test(){printf("Test()Begin\n");i=1;j=2;printf("Test()End\n");}};Testgt;intmain(){printf("gt.i=%d\n",gt.getI());printf("gt.j=%d\n",gt.getJ());Testat;printf("at.i=%d\n",at.getI());printf("at.j=%d\n",at.getJ());Test*pt=newTest;printf("pt->i=%d\n",pt->getI());printf("pt->j=%d\n",pt->getJ());return0;}
我们编译后结果如下
我们这样是不是就方便很多呢?那肯定了。我们可以明显看到定义了三个对象后,调用了三次构造函数。那么我们既然知道了有构造函数这一类的函数,它是否能像一般函数那样进行带参数呢?构造函数可以根据需要定义参数;一个类中可以存在多个重载的构造函数;构造函数的重载遵循 C++ 重载的规则。我们之前说过定义和声明不同,在对象这块也同样适用。对象定义和对象声明时不同的:对象定义 -- 申请对象的空间并调用构造函数;对象声明 -- 告诉编译器存在这样一个对象。下来我们以代码为例进行分析
#include<stdio.h>classTest{public:Test(){printf("Test()\n");}Test(intv){printf("Test(intv),v=%d\n",v);}};intmain(){Testt1;//调用Test()Testt2(1);//调用Test(intv)Testt3=2;//调用Test(intv)inti(10);printf("i=%d\n",i);return0;}
我们看到第 18 行的 t1 对象的构造函数肯定调用了 Test(),第 19 和 20 行则是调用了 Test(int v);在 C 语言中还有 int i(10) 这种写法,我们看看编译是否会通过?
我们看到编译通过,并且如我们所分析的那样。那么构造函数的调用是否有什么规则呢?在一般情况下,构造函数在对象定义时被自动调用,一些特殊情况下,需要手工调用构造函数。我们如何利用构造函数来创建一个数组呢?
#include<stdio.h>classTest{private:intm_value;public:Test(){printf("Test()\n");m_value=0;}Test(intv){printf("Test(intv),v=%d\n",v);m_value=v;}intgetValue(){returnm_value;}};intmain(){Testta[3]={Test(),Test(1),Test(2)};for(inti=0;i<3;i++){printf("ta[%d].getValue()=%d\n",i,ta[i].getValue());}Testt=Test(10);printf("t.getValue()=%d\n",t.getValue());return0;}
我们首先来分析下,数组第一个成员调用的构造函数应该是 Test(),后面两个成员调用的是 Test(int v) 函数,并打印出相应的值。最后定义的对象 t,它会打印出构造函数和得到的值都为 10,我们来看看编译结果
下来我们来开发一个数组类解决原生数组的安全性问题:提供函数获取数组长度;提供函数获取数组元素;提供函数设置数组元素。我们来看看它是怎么实现的
IntArray.h 源码
#ifndef_INTARRAY_H_#define_INTARRAY_H_classIntArray{private:intm_length;int*m_pointer;public:IntArray(intlen);intlength();boolget(intindex,int&value);boolset(intindex,intvalue);voidfree();};#endif
IntArray.cpp 源码
#include"IntArray.h"IntArray::IntArray(intlen){m_pointer=newint[len];for(inti=0;i<len;i++){m_pointer[i]=0;}m_length=len;}intIntArray::length(){returnm_length;}boolIntArray::get(intindex,int&value){boolret=(0<=index)&&(index<=length());if(ret){value=m_pointer[index];}returnret;}boolIntArray::set(intindex,intvalue){boolret=(0<=index)&&(index<=length());if(ret){m_pointer[index]=value;}returnret;}voidIntArray::free(){delete[]m_pointer;}
test.cpp 源码
#include<stdio.h>#include"IntArray.h"intmain(){IntArraya(5);for(inti=0;i<a.length();i++){a.set(i,i+1);}for(inti=0;i<a.length();i++){intvalue=0;if(a.get(i,value)){printf("a[%d]=%d\n",i,value);}}a.free();return0;}
我们编译后得到如下结果
下来我们来看看特殊的构造函数:无参构造函数和拷贝构造函数。无参构造函数顾名思义就是没有参数的构造函数,而拷贝构造函数则是参数为 const class_name& 的构造函数。那么这两类构造函数有什么区别呢?无参构造函函数是当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空;拷贝构造函数是当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制。下来我们以代码为例进行分析
#include<stdio.h>classTest{private:inti;intj;public:intgetI(){returni;}intgetJ(){returnj;}/*Test(){printf("Test()\n");}Test(constTest&t){printf("Test(constTest&t)\n");i=t.i;j=t.j;}*/};intmain(){Testt1;Testt2=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());return0;}
我们先将自己提供的无参构造函数和拷贝构造函数注释掉,编译下,看编译器是否提供默认的构造函数,是否可以通过
我们看到编译是通过的,也就是说,编译器通过了默认的构造函数。我们再来自己提供呢,看看是否会发生冲突
我们看到打印出了自己定义的语句,证明它是调用了我们自己写的构造函数。那么这个拷贝构造函数的意义在哪呢?一是兼容 C 语言的初始化方式,二是初始化行为能够符合预期的逻辑。那么这块就牵扯到是浅拷贝还是深拷贝。浅拷贝是拷贝后对象的物理状态相同,深拷贝是拷贝后对象的逻辑状态相同。注意:编译器提供的拷贝构造函数只进行浅拷贝!
下来我们以实例代码看看对象的初始化是怎样进行的
#include<stdio.h>classTest{private:inti;intj;int*p;public:intgetI(){returni;}intgetJ(){returnj;}int*getP(){returnp;}Test(intv){i=1;j=2;p=newint;*p=v;}Test(constTest&t){i=t.i;j=t.j;p=newint;*p=*t.p;}voidfree(){deletep;}};intmain(){Testt1(3);Testt2(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();return0;}
我们看看 t1 应该进行的是浅拷贝,t2 应该进行的是深拷贝。我们看看编译结果
我们如果只有浅拷贝,没有深拷贝的话,看看结果会是怎样的,将第 34 - 41 行的代码注释掉,将第 54 和 55 行的打印 *p 的值改为打印 p 的地址。
我们看到它运行的时候报段错误了,t1.p 和 t2.p 指向了同一个地址。我们看看它是怎样进行的
我们看到将同一个地址释放两次肯定是会出问题的,这时我们就需要进行深拷贝了。那么我们就要考虑到底什么时候需要进行深拷贝?当对象中有成员指代了系统中的资源时,如:成员指向了动态内存空间,成员打开了外存中的文件,成员使用了系统中的网络端口...
我们在实现拷贝构造函数这块有个一般性的原则,自定义拷贝构造函数时,必须要实现深拷贝。那么我们再来优化下之前的数组类
IntArray.h 源码
#ifndef_INTARRAY_H_#define_INTARRAY_H_classIntArray{private:intm_length;int*m_pointer;public:IntArray(intlen);IntArray(constIntArray&obj);intlength();boolget(intindex,int&value);boolset(intindex,intvalue);voidfree();};#endif
IntArray.cpp 源码
#include"IntArray.h"IntArray::IntArray(intlen){m_pointer=newint[len];for(inti=0;i<len;i++){m_pointer[i]=0;}m_length=len;}IntArray::IntArray(constIntArray&obj){m_length=obj.m_length;m_pointer=newint[obj.m_length];for(inti=0;i<obj.m_length;i++){m_pointer[i]=obj.m_pointer[i];}}intIntArray::length(){returnm_length;}boolIntArray::get(intindex,int&value){boolret=(0<=index)&&(index<=length());if(ret){value=m_pointer[index];}returnret;}boolIntArray::set(intindex,intvalue){boolret=(0<=index)&&(index<=length());if(ret){m_pointer[index]=value;}returnret;}voidIntArray::free(){delete[]m_pointer;}
test.cpp 源码
#include<stdio.h>#include"IntArray.h"intmain(){IntArraya(5);for(inti=0;i<5;i++){a.set(i,i+1);}for(inti=0;i<a.length();i++){intvalue=0;if(a.get(i,value)){printf("a[%d]=%d\n",i,value);}}printf("\n");IntArrayb=a;for(inti=0;i<b.length();i++){intvalue=0;if(b.get(i,value)){printf("b[%d]=%d\n",i,value);}}a.free();b.free();return0;}
我们看看编译结果是否如我们代码所写的那样,创建数组并初始化。用数组 a 初始化数组 b。
通过对对象的构造的学习,总结如下:1、每个对象在使用之前都应该初始化;2、类的构造函数用于对象的初始化,构造函数与类同名并且没有返回值;3、构造函数在对象定义时被自动调用,构造函数可以根据需要定义参数;4、构造函数之间可以存在重载关系,并且构造函数遵循 C++ 中重载函数的规则;5、对象定义时会触发构造函数的调用,在一些情况下可以手动调用构造函数;6、C++ 编译器会默认提供构造函数;7、无参构造函数用于定义对象的默认初识状态,拷贝构造函数在创建对象时拷贝对象的状态;8、对象的拷贝有浅拷贝和深拷贝两种方式:浅拷贝使得对象的物理状态相同,而深拷贝则使得对象的逻辑状态相同。
欢迎大家一起来学习 C++ 语言,可以加我QQ:243343083。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。