C++学习随笔9 面向对象编程(2)- 对象生灭
- 构造函数
在C++中,对象的实例在编译的时候,就需要为其分配内存大小,因此,系统都是在stack上为其分配内存的。这一点和C#完全不同!千万记住:在C#中,所有类都是reference type,要创建类的实体,必须通过new在heap上为其分配空间,同时返回在stack上指向其地址的reference。
因此,在C++中,只要申明该实例,在程序编译后,就要为其分配相应的内存空间,至于实体内的各个域的值,就由其构造函数决定了。
1. 一次性对象:
创建对象如果不给出对象名,直接以类名调用构造函数,则产生一个无名对象,无名对象经常在参数传递时用到,例如:
cout<<Date(2003, 12, 23);
Date(2003, 12, 23)是一个无名对象,该对象在做了<<操作后便烟消云散了,所以这种对象一般用在创建后不需要反复使用的场合。
2. 无参构造函数:
类机制中总是为无构造函数的类默认地建立一个无参的构造函数,它除了分配对象的实体空间外,其他什么也不做。因此没有定义构造函数的类,可以看做系统给了一个默认的无参构造函数:
1 class A{ 2 //私有成员 3 public: 4 A(){} //无参构造函数 5 //其他公有成员 6 };
如果手工定义了无参的构造函数,或者任何其他的构造函数,则系统不再提供默认的无参构造函数。
1 class A{ 2 //私有成员 3 public: 4 A(int x){} 5 A(string s){} //构造函数重载 6 //其他公有成员 7 }; 8 9 A a(2); 10 A b(3.9); 11 A c; //错,类中没有默认的无参构造函数
- 类成员初始化
在构造函数体中是不能完成对常量成员和引用成员的初始化,只能用构造参数表的方式。
构造参数表:
在构造函数的参数列表右括号后面,花括号前面,可以用冒号引出构造函数的调用表,该调用表可以省略类型名称,但却行创建对象之职。
1 class Silly{ 2 const int ten; 3 int &ra; 4 public: 5 Silly(int x, int& a){ 6 ten = 10; //错,不能在构造函数体中完成对常量成员初始化 7 ra = a; //错,不能在构造函数体中完成对引用成员初始化 8 } 9 }; 10 11 class Silly{ 12 const int ten; 13 int &ra; 14 public: 15 Silly(int x, int& a):ten(10), ra(a){} //构造参数表 16 };
- 构造顺序
1. 局部对象:
C++根据程序运行中定义对象的顺序来决定对象的创建顺序。而且,静态对象只创建一次。
2. 全局对象:
全局对象的创建顺序在标准C++中没有规定,一切视编译器的内在特性而定。应尽量不要设置全局对象,更不要让全局对象之间互相依赖。
3. 成员对象:
成员对象以其在类中声明的顺序构造。
1 class A{ 2 public: 3 A(int x) {cout<<"A:"<<x<<"->";} 4 }; 5 6 class B{ 7 public: 8 B(int x) {cout<<"B:"<<x<<"->";} 9 }; 10 11 class C{ 12 A a; 13 B b; //先声明a,再声明b 14 public: 15 C(int x, int y):b(x), a(y){cout<<"C\n";} //构造参数表顺序与声明顺序相反 16 }; 17 18 int main(){ 19 C c(15, 9); //结果:A:9->B:15->C,构造函数调用顺序与成员对象在类中声明顺序一致,先调用A的构造函数,再调用B的,最后调用C的 20 }
- 拷贝构造函数(浅拷贝、深拷贝)
以下几种情况都会自动调用拷贝构造函数:
1)用一个已有的对象初始化一个新对象的时候
2)将一个对象以值传递的方式传给形参的时候
3)函数返回一个对象的时候
如果类中数据成员需要动态分配存储空间,需要自定义拷贝构造函数;同时还需要自定义非空的析构函数,释放动态申请的内存,防止内存泄露。因为系统不会自动为人为申请的内存做内存释放工作;否则便无需要。
1. 默认拷贝构造函数(浅拷贝):
1 class Person{ 2 char * pName; 3 public: 4 Person(char * pN = "noName"){ 5 cout<<"Constructing "<<pN<<"\n"; 6 pName = new char[strlen(pN)+1]; 7 if(pName) strcpy(pName, pN); 8 } 9 ~Person(){ 10 cout<<"Destructing "<<pName<<"\n"; 11 delete[] pName; 12 } 13 }; 14 15 int main(){ 16 Person p1("Randy"); 17 Person p2(p1); //等价于Person p2 = p1; 18 }
结果:
Constructing Randy
Destructing Randy
Destructing 茸茸茸茸
程序报错
程序开始运行时,创建p1对象,p1对象的构造函数从堆中分配空间并赋给数据成员pName;执行p2=p1时,因为没有定义拷贝构造函数,于是就调用默认拷贝构造函数,使得p2与p1完全一样,并没有新分配堆空间给p2, p1与p2的pName都是同一个值。析构p2时,将存有Randy的堆空间先行释放了; 当析构p1,执行delete[] pName时因为Randy已经不复存在,所以程序报错。
2. 自定义拷贝构造函数(深拷贝):
1 class Person{ 2 char * pName; 3 public: 4 Person(char * pN = "noName"){ 5 cout<<"Constructing "<<pN<<"\n"; 6 pName = new char[strlen(pN)+1]; 7 if(pName) strcpy(pName, pN); 8 } 9 Person(const Person& s){ 10 cout<<"copy Constructing "<<s.pName<<"\n"; 11 pName = new char[strlen(s.pName)+1]; 12 if(pName) strcpy(pName, s.pName); 13 } 14 ~Person(){ 15 cout<<"Destructing "<<pName<<"\n"; 16 delete[] pName; 17 } 18 }; 19 20 int main(){ 21 Person p1("Randy"); 22 Person p2(p1); 23 }
结果:
Constructing Randy
copy Constructing Randy
Destructing Randy
Destructing Randy
程序正常
自定义构造拷贝函数是构造函数的重载,一旦定义了拷贝构造函数,默认的拷贝构造函数就不再起作用了。
拷贝构造函数的参数必须是类对象常量引用:
Person(const Person& s);
- 析构函数
析构函数与拷贝构造函数是成对出现的;因为析构函数没有参数,所以析构函数不能重载;对象构造与析构的关系是栈数据结构中的入栈和出栈的关系,所以对象析构的顺序与对象创建的顺序正好相反。
- 转型构造函数
转型构造函数的作用是将某种类型的数据转换为类的对象,当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转型构造函数。
1 class A 2 { 3 public: 4 int a; 5 A(int a) :a(a) {} //转型构造函数,将int型转换为A类的对象 6 operator int() //类型转换函数,将A类的对象转换为int型 7 { 8 return a; 9 } 10 }; 11 int main() 12 { 13 A a(2); //显示转换 14 //A a = 2; 隐式转换,若在转型构造函数前加explicit则无法进行隐式转换 15 int b = a + 3; 16 A c = a + 4; 17 cout<<b<<"\n"<<c.a<<endl; 18 return 0; 19 }
- 类型转换函数
c++的类型转换函数可以将一个类的对象转换为一个指定类型的数据。
- 赋值操作符
1 class Person{ 2 char * pName; 3 public: 4 //自定义构造函数 5 Person(char * pN = "noName"){ 6 cout<<"Constructing "<<pN<<"\n"; 7 pName = new char[strlen(pN)+1]; 8 if(pName) strcpy(pName, pN); 9 } 10 //自定义拷贝构造函数 11 Person(const Person& s){ 12 cout<<"copy Constructing "<<s.pName<<"\n"; 13 pName = new char[strlen(s.pName)+1]; 14 if(pName) strcpy(pName, s.pName); 15 } 16 //赋值操作符重载 17 Person& operator=(Person& s){ 18 cout<<"Assigning "<<s.pName<<"\n"; 19 if(this == &s) return s; 20 //释放掉原来对象所占有的堆空间 21 delete[] pName; 22 //申请一块新的堆内存 23 pName = new char[strlen(s.pName)+1]; 24 //将源对象的堆内存的值copy给新的堆内存 25 if(pName) strcpy(pName, s.pName); 26 //返回源对象的引用 27 return *this; 28 } 29 //析构函数 30 ~Person(){ 31 cout<<"Destructing "<<pName<<"\n"; 32 delete[] pName; 33 } 34 }; 35 36 int main(){ 37 Person p1("Randy"); 38 Person p2("Jenny"); 39 p2 = p1; //赋值操作 40 Person p3(p2); //拷贝操作 41 Person p4 = p3; //拷贝操作 42 }
如果对象在申明的同时马上进行的初始化操作,则称之为拷贝运算。例如:
class1 A("af"); class1 B=A;
此时其实际调用的是B(A)这样的浅拷贝操作。
如果对象在申明之后,再进行的赋值运算,我们称之为赋值运算。例如:
class1 A("af"); class1 B;
B=A;
赋值操作符与拷贝构造函数和析构函数结对而行。

浙公网安备 33010602011771号