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 }
View Code

结果:

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 }
View Code

结果:

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++的类型转换函数可以将一个类的对象转换为一个指定类型的数据

类型转换函数的一般形式为 :
        operator 类型名()
        {实现转换的语句}
类型转换函数在函数名前不能指定函数类型,也没有参数。返回类型是类型名决定的,只能作为成员函数,因为转换的类型是本类的对象,不能作为友元函数或者普通函数。
  • 赋值操作符
 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 }
View Code

如果对象在申明的同时马上进行的初始化操作,则称之为拷贝运算。例如:
        class1 A("af"); class1 B=A;
此时其实际调用的是B(A)这样的浅拷贝操作。
如果对象在申明之后,再进行的赋值运算,我们称之为赋值运算。例如:
        class1 A("af"); class1 B;
        B=A;

赋值操作符与拷贝构造函数和析构函数结对而行。

posted @ 2016-12-05 10:23  etcjd  阅读(288)  评论(0)    收藏  举报