C++语言(03)——对象的构造
(1)从程序设计的角度来看,对象只是变量,定义对象就是定义变量,所以:
在栈上创建对象时,成员变量初始值为随机值
在堆上创建对象时,成员变量初始值为随机值
在静态数据区上创建对象时,成员变量初始值为0
(2)全局变量和static修饰的局部变量存储在静态数据区,没有显式初始化其值为0(bss/ZI段)
/**从程序设计的角度来看,对象只是变量,定义对象就是定义变量,所以: 在栈上创建对象时,成员变量初始值为随机值 在堆上创建对象时,成员变量初始值为随机值 在静态数据区上创建对象时,成员变量初始值为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 t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0;}
对象的初始化
(1)生活中的对象都是初始化之后上市的,如手机,电脑等,我们希望程序中的对象也可以初始化为固定值
initialize函数(1)在类中提供一个public的initialize函数
(2)函数中手工对类的成员进行显式初始化
(3)对象创建后需要立即调用initialize函数进行初始化
(4)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 t1; //t1.initialize(); printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); t1.initialize(); Test* pt = new Test; pt->initialize(); printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0;}
构造函数
(1)C++中可以定义与类同名的构造函数
(2)构造函数:与类同名、没有任何的返回值类型、在对象定义时会被自动调用
#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 t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0;}
对象的构造(中)构造函数重载
(1)构造函数可以根据需要定义参数
(2)一个类中可以存在多个重载的构造函数构造函数的重载遵循C++重载的规则
/**(1)构造函数可以根据需要定义参数(2)一个类中可以存在多个重载的构造函数构造函数的重载遵循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 t; // 调用 Test() Test t1(1); // 调用 Test(int v) //注意此处的Test t1(1),因为1是int类型数据,所以是要告诉编译器这个对象初始化时调用的构造函数 //参数为int而且只有一个参数 Test t2 = 2; // 调用 Test(int v) //C++中支持这样初始化 int i(100); printf("i = %d\n", i); return 0;}
(3)构造函数在对象定义时会被自动调用,此外我们可以手工调用构造函数
#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 ta[3] = {Test(), Test(1), Test(2)}; //我们可以手工调用构造函数 for(int i=0; i<3; i++) { printf("ta[%d].getValue() = %d\n", i , ta[i].getValue()); }/* int i(100); //C++中是可以这样初始化的,等价于int i = 100; printf("i = %d.\n", i); Test t = Test(100); printf("t.getValue() = %d\n", t.getValue()); */ return 0;}
注意:
对象的声明和定义不同,对象定义:申请对象的空间并调用构造函数
对象声明:告诉编译器有这样一个对象
(1)就是没有参数的构造函数
(2)当类中没有定义构造函数时(拷贝构造函数也是构造函数),编译器会默认提供一个无参构造函数,其函数体为空
(1)参数为const class_name&的构造函数
(2)当类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,简单的进行成员变量的复制(浅拷贝)
(3)拷贝构造函数的意义:兼容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; } int getPP() { return *p; } /*Test(const Test& t) //编译器提供的默认拷贝构造函数 { i = t.i; j = t.j; } Test() //编译器提供的默认无参构造函数 { //函数体为空 }*/ Test(int v) { i = 1; j = 2; p = new int; *p = v; } void free() { delete p; }};int main(){ Test t1(1); 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(), t1.getP()); //test printf("t1.*p = %d.\n", t1.getPP()); printf("t2.*p = %d.\n", t2.getPP()); //结果表明t1(1),把1作为参数,传给了构造函数 t1.free(); //t2.free(); // double free or corruption (fasttop): 0x09441008 return 0;}
浅拷贝与深拷贝
(1)浅拷贝:拷贝后的物理状态相同
(2)深拷贝:拷贝后的逻辑状态相同
(3)编译器默认提供的拷贝构造函数只进行浅拷贝
什么时候需要深拷贝?
对象中有成员使用了系统资源(成员指向了动态内存空间、成员打开了外存中的文件、成员使用了系统中的网络端口)
注意:
(1)调用浅拷贝构造函数进行初始化,初始化的两个变量不但参数相同,而且共用同一块内存,在两次释放内存时就会出错
(2)工程中自定义拷贝构造函数时,必然要实现深拷贝(为新的对象重新分配资源)
#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(const Test& t) //自定义构造函数,深拷贝 { i = t.i; j = t.j; p = new int; //必须有这一步,重新分配资源 *p = *t.p; //注意这里是*t.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 = %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;}
初始化列表的使用类中的const成员
(1)在类中可以定义const成员,const成员会被分配空间,存储位置取决于其对象定义在哪里
(2)类中的const是只读变量
(3)在类中不能直接对const成员进行初始化,只能在初始化列表中指定初始值
//在类中不能直接对const成员进行初始化,只能在初始化列表中指定初始值#include <stdio.h>class Test{private: const int ci; //只读变量public: Test() : ci(1) { // ci = 10; } int getCI() { return ci; }};int main(){ Test t; printf("t.ci = %d\n", t.getCI()); return 0;}
初始化列表
(1)C++中提供了初始化列表对成员进行初始化
(2)语法规则
ClassName::ClassName() //构造函数
: m1(v1),m2(v2, v3),m3(v1) //初始化列表
{
//构造函数函数体
}
(3)注意事项:
成员的初始化顺序与成员的声明顺序相同,与初始化列表中的顺序无关
初始化列表先于构造函数的函数体执行
/*C++中提供了初始化列表对成员进行初始化成员的初始化顺序与成员的声明顺序相同,与初始化列表中的顺序无关初始化列表先于构造函数的函数体执行*/#include <stdio.h>class Value{private: int mi;public: Value(int i) { printf("i = %d\n", i); mi = i; } int getI() { return mi; }};class Test{private: Value m2; Value m3; Value m1;public: Test() : m1(1), m2(2), m3(3) //成员的初始化顺序与成员的声明顺序相同,与初始化列表中的顺序无关 { printf("Test::Test()\n"); }};int main(){ Test t; //初始化列表先于构造函数的函数体执行 return 0;}
对象的构造顺序
(1)局部变量的构造顺序依赖于程序的执行流,所以开发中要避免使用goto语句)(破坏程序的执行流)
(2)堆对象的构造顺序依赖于new的使用顺序
(3)全局对象的构造顺序是不确定的,不同的编译器使用不同的规则确定构造顺序,所以要尽量避免全局对象
//局部变量的构造顺序依赖于程序的执行流,所以开发中要避免使用goto语句(破坏程序的执行流)#include <stdio.h>class Test{private: int mi;public: Test(int i) { mi = i; printf("Test(int i): %d\n", mi); } Test(const Test& obj) { mi = obj.mi; printf("Test(const Test& obj): %d\n", mi); } int getMi() { return mi; }};int main(){ int i = 0; Test a1 = i; // Test(int i): 0 while( i < 3 ) { Test a2 = ++i; // Test(int i): 1, 2, 3 }goto End; Test a(100); // crosses initialization of ‘Test a’编译报错,但是在vc10中编译时ok的End: printf("a.mi = %d\n", a.getMi()); //此处的访问必然导致bug return 0;}
对象的销毁
(1)生活中对象都是初始化后才上市的,对象被销毁前会做一些清理工作
程序中如何销毁一个对象方案1:
提供一个public的free函数,
(1)当对象不再需要时立即调用free函数进行清理
(2)free只是一个普通的函数,必须显示的调用
(3)对象销毁之前没有做清理,很可能造成资源泄漏
方案2:
析构函数
(1)C++中可以定义一个特殊的清理函数,析构函数,功能和构造函数相反
(2)析构函数在对象销毁时被自动调用
(3)析构函数没有返回值也没有参数(表明析构函数在一个类中是唯一的,不可能重载)
(4)语法:
~ClassName()
(5)一般当类中自定义了构造函数,并且函数中使用了系统资源,则需要定义析构函数,释放系统资源,防止内存泄漏
#include <stdio.h>class Test{ int mi;public: Test(int i) { mi = i; printf("Test(): %d\n", mi); } ~Test() { printf("~Test(): %d\n", mi); }};int main(){ Test t(1); Test* pt = new Test(2); delete pt; return 0;}
神秘的临时对象
(1)直接调用构造函数将产生一个临时对象,临时对象的生命周期只有一条语句的时间,临时对象的作用域只在一条语句中
(2)临时对象是C++中值得警惕的灰色地带,是性能的瓶颈,也是bug的来源之一
/**直接调用构造函数将产生一个临时对象,临时对象的生命周期只有一条语句的时间,临时对象的作用域只在一条语句中**/#include <stdio.h>class Test { int mi;public: Test(int i) { mi = i; } Test() { Test(0); //直接调用构造函数将产生一个临时对象,临时对象的作用域只有一行代码。 // 所以此处就相当于空 } void print() { printf("mi = %d\n", mi); }};int main(){ Test t; t.print(); return 0;}
(3)实际工程开发中需要人为的避开临时对象
(4)现代C++编译器会尽力避开临时对象
思考:如何解决构造函数的代码复用问题?
方案是提供一个private的init函数,然后在构造函数中去调用它.
/**思考如何解决构造函数的代码复用问题? 方案是提供一个private的init函数,然后在构造函数中去调用它**/#include <stdio.h>class Test { int mi; void init(int i) //提供一个private的init函数,然后在构造函数中去调用它 { mi = i; }public: Test(int i) { init(i); } Test() { init(0); } void print() { printf("mi = %d\n", mi); }};int main(){ Test t; t.print(); return 0;}
二阶构造模式
回顾构造函数的特点:
与类同名,没有返回值,在对象创建时被动调用,用于对象的初始化
1、如何判断构造函数的执行结果?
一般来说无法判断。但是我们可以人为的类中定义一个用于表明构造函数执行结果的变量,并在构造函数结束的地方给该变量赋值,最后通过读取该变量的值来得知构造函数的执行结果
2、在构造函数中执行return语句会发生什么?
首先在构造函数中指向他return是合法的,执行return语句后构造函数立即结束
3、构造函数执行结束是否意味着对象构造成功?
构造函数只提供自动初始化成员变量的机会,不能保证初始化逻辑一定成功。构造函数决定的是对象的初始化状态,而不是对象的诞生。
也就是说构造函数初始化操作的失败不影响对象的诞生
初始化操作不能按照预期完成而得到的对象
是C++中的合法对象,也是bug的来源
#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\n", t1.getI()); printf("t1.mj = %d\n", t1.getJ()); } return 0;}
二阶构造
工程开发中的构造过程可分为:
第一阶段构造:(真正的构造函数)
资源无关的初始化操作,不可能出现异常的操作
第二阶段构造:(返回值表示初始化状态的普通函数)
需要时用系统资源的操做,可能出现异常情况(内存申请,访问文件)
如图所示:(27-2)
#include <stdio.h>class TwoPhaseCons {private: TwoPhaseCons() // 第一阶段构造函数 { } bool construct() // 第二阶段构造函数(普通函数,返回值表示系统资源初始化状态) { return true; }public: TwoPhaseCons* NewInstance() // 对象创建函数 { TwoPhaseCons* ret = new TwoPhaseCons(); // 若第二阶段构造失败,返回 NULL if( !(ret && ret->construct()) ) { delete ret; ret = NULL; } return ret; }};//TwoPhaseCons* TwoPhaseCons::NewInstance() int main(){ TwoPhaseCons* obj = TwoPhaseCons::NewInstance(); printf("obj = %p\n", obj); delete obj; return 0;}
总结:
(1)二阶构造认为的将初始化分为两部分,能够确保创建的对象都是完整的
(2)二阶构造的构造函数都是私有的,并提供了一个用于创建对象的静态函数指针???(通过类名直接访问,然后创建对象),所以最终的对象分配在堆区
(3)实际工程中需要初始化的数据都是比较多的,所以对象创建在堆区是合理的
使用二阶构造完善之前的数组类
析构函数的调用顺序
析构函数与对应的构造函数的调用顺序相反,所以我们之只要知道构造函数的调用顺序就可以知道析构的顺序
(1)单个函数创建时构造函数的调用顺序(先父母,后他人,再自己)
1、调用父类的构造过程
2、调用成员变量的构造函数
3、调用类自身的构造函数
(2)对于栈对象和全局对象,类似于入栈和出栈的顺序,最先构造的对象最后被析构
(3)堆对象的析构发生在使用delete的时候,与delete的使用顺序相关
/**析构函数与对应的构造函数的调用顺序相反,所以我们之只要知道构造函数的调用顺序就可以知道析构的顺序(1)单个函数创建时构造函数的调用顺序 1、调用父类的构造过程 2、调用成员变量的构造函数 3、调用类自身的构造函数**/#include <stdio.h>class Member{ const char* ms;public: Member(const char* s) { printf("Member(const char* s): %s\n", s); ms = s; } ~Member() { printf("~Member(): %s\n", ms); }};class Test{ Member mA; //调用成员变量的构造函数 Member mB;public: Test() : mB("mB"), mA("mA") //初始化列表对成员进行初始化,与对象的的构造顺序无关 { printf("Test()\n"); } ~Test() { printf("~Test()\n"); }};Member gA("gA");int main(){ Test t; //调用类自身的构造函数 //对象的构造函数掉顺序: //gA, mA, mB, Test() //对象的析构顺序与构造顺序相反 return 0;}
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。