C++ 构造函数与析构函数学习与理解与解析

一、构造函数的介绍
    1. 构造函数的作用
        构造函数主要用来在创建对象时完成对对象属性的一些初始化等操作, 当创建对象时, 对象会自动调用它的构造函数。一般来说, 构造函数有以下三个方面的作用:
            ■ 给创建的对象建立一个标识符;
            ■ 为对象数据成员开辟内存空间;
            ■ 完成对象数据成员的初始化。
        
    2. 默认构造函数
        当用户没有显式的去定义构造函数时, 编译器会为类生成一个默认的构造函数, 称为 "默认构造函数", 默认构造函数不能完成对象数据成员的初始化, 只能给对象创建一标识符, 并为对象中的数据成员开辟一定的内存空间。
        
    3. 构造函数的特点
        无论是用户自定义的构造函数还是默认构造函数都主要有以下特点:
            ①. 在对象被创建时自动执行;
            ②. 构造函数的函数名与类名相同;
            ③. 没有返回值类型、也没有返回值;
            ④. 构造函数不能被显式调用。
            

        #给Python程序员的注释: C++中的构造函数类似于Python中的 __init__ 方法.


        
        
        
        
二、构造函数的显式定义
    由于在大多数情况下我们希望在对象创建时就完成一些对成员属性的初始化等工作, 而默认构造函数无法满足我们的要求, 所以我们需要显式定义一个构造函数来覆盖掉默认构造函数以便来完成必要的初始化工作, 当用户自定义构造函数后编译器就不会再为对象生成默认构造函数。
    
    在构造函数的特点中我们看到, 构造函数的名称必须与类名相同, 并且没有返回值类型和返回值, 看一个构造函数的定义:

复制代码
 1     #include <iostream>
 2 
 3     using namespace std;
 4 
 5     class Point
 6     {
 7         public:
 8             Point()     //声明并定义构造函数
 9             {
10                 cout<<"自定义的构造函数被调用...\n";
11                 xPos = 100;         //利用构造函数对数据成员 xPos, yPos进行初始化
12                 yPos = 100;
13             }
14             void printPoint()
15             {
16                 cout<<"xPos = " << xPos <<endl;
17                 cout<<"yPos = " << yPos <<endl;
18             }
19 
20         private:
21             int xPos;
22             int yPos;
23     };
24 
25     int main()
26     {
27         Point M;    //创建对象M
28         M.printPoint();
29 
30         return 0;
31     }
复制代码

 
    编译运行的结果:

复制代码
        自定义的构造函数被调用...
        xPos = 100
        yPos = 100

        Process returned 0 (0x0)   execution time : 0.453 s
        Press any key to continue.
复制代码

 


    代码说明:
        在Point类的 public 成员中我们定义了一个构造函数 Point() , 可以看到这个Point构造函数并不像 printPoint 函数有个void类型的返回值, 这正是构造函数的一特点。在构造函数中, 我们输出了一句提示信息, "自定义的构造函数被调用...", 并且将对象中的数据成员xPos和yPos初始化为100。
        
        在 main 函数中, 使用 Point 类创建了一个对象 M, 并调用M对象的方法 printPoint 输出M的属性信息, 根据输出结果看到, 自定义的构造函数被调用了, 所以 xPos和yPos 的值此时都是100, 而不是一个随机值。
        
        需要提示一下的是, 构造函数的定义也可放在类外进行。
        
        
        


        
三、有参数的构造函数
    在上个示例中实在构造函数的函数体内直接对数据成员进行赋值以达到初始化的目的, 但是有时候在创建时每个对象的属性有可能是不同的, 这种直接赋值的方式显然不合适。不过构造函数是支持向函数中传入参数的, 所以可以使用带参数的构造函数来解决该问题。
    

复制代码
 1     #include <iostream>
 2 
 3     using namespace std;
 4 
 5     class Point
 6     {
 7         public:
 8             Point(int x = 0, int y = 0)     //带有默认参数的构造函数
 9             {
10                 cout<<"自定义的构造函数被调用...\n";
11                 xPos = x;         //利用传入的参数值对成员属性进行初始化
12                 yPos = y;
13             }
14             void printPoint()
15             {
16                 cout<<"xPos = " << xPos <<endl;
17                 cout<<"yPos = " << yPos <<endl;
18             }
19 
20         private:
21             int xPos;
22             int yPos;
23     };
24 
25     int main()
26     {
27         Point M(10, 20);    //创建对象M并初始化xPos,yPos为10和20
28         M.printPoint();
29 
30         Point N(200);       //创建对象N并初始化xPos为200, yPos使用参数y的默认值0
31         N.printPoint();
32 
33         Point P;            //创建对象P使用构造函数的默认参数
34         P.printPoint();
35 
36         return 0;
37     }
复制代码

 
    编译运行的结果:

复制代码
        自定义的构造函数被调用...
        xPos = 10
        yPos = 20
        自定义的构造函数被调用...
        xPos = 200
        yPos = 0
        自定义的构造函数被调用...
        xPos = 0
        yPos = 0

        Process returned 0 (0x0)   execution time : 0.297 s
        Press any key to continue.
复制代码

 


    代码说明:
        在这个示例中的构造函数 Point(int x = 0, int y = 0) 使用了参数列表并且对参数进行了默认参数设置为0。在 main 函数中共创建了三个对象 M, N, P。
            M对象不使用默认参数将M的坐标属性初始化10和20;
            N对象使用一个默认参数y, xPos属性初始化为200;
            P对象完全使用默认参数将xPos和yPos初始化为0。

            
            


            
三、构造函数的重载
    构造函数也毕竟是函数, 与普通函数相同, 构造函数也支持重载, 需要注意的是, 在进行构造函数的重载时要注意重载和参数默认的关系要处理好, 避免产生代码的二义性导致编译出错, 例如以下具有二义性的重载:
    

复制代码
        Point(int x = 0, int y = 0)     //默认参数的构造函数
        {
            xPos = x;
            yPos = y;
        }

        Point()         //重载一个无参构造函数
        {
            xPos = 0;
            yPos = 0;
        }
复制代码

        
    在上面的重载中, 当尝试用 Point 类重载一个无参数传入的对象 M 时, Point M; 这时编译器就报一条 error: call of overloaded 'Point()' is ambiguous 的错误信息来告诉我们说 Point 函数具有二义性, 这是因为 Point(int x = 0, int y = 0) 全部使用了默认参数, 即使我们不传入参数也不会出现错误, 但是在重载时又重载了一个不需要传入参数了构造函数 Point(), 这样就造成了当创建对象都不传入参数时编译器就不知道到底该使用哪个构造函数了, 就造成了二义性。
    
    
    

 


四、初始化表达式
    对象中的一些数据成员除了在构造函数体中进行初始化外还可以通过调用初始化表来进行完成, 要使用初始化表来对数据成员进行初始化时使用 : 号进行调出, 示例如下:
    

        Point(int x = 0, int y = 0):xPos(x), yPos(y)  //使用初始化表
        {
            cout<<"调用初始化表对数据成员进行初始化!\n";
        }

        
    在 Point 构造函数头的后面, 通过单个冒号 : 引出的就是初始化表, 初始化的内容为 Point 类中int型的 xPos 成员和 yPos成员, 其效果和 xPos = x; yPos = y; 是相同的。
    
    与在构造函数体内进行初始化不同的是, 使用初始化表进行初始化是在构造函数被调用以前就完成的。每个成员在初始化表中只能出现一次, 并且初始化的顺序不是取决于数据成员在初始化表中出现的顺序, 而是取决于在类中声明的顺序。
    
    此外, 一些通过构造函数无法进行初始化的数据类型可以使用初始化表进行初始化, 如: 常量成员和引用成员, 这部分内容将在后面进行详细说明。使用初始化表对对象成员进行初始化的完整示例:
    

View Code

 



    


    
五、析构函数
    与构造函数相反, 析构函数是在对象被撤销时被自动调用, 用于对成员撤销时的一些清理工作, 例如在前面提到的手动释放使用 new 或 malloc 进行申请的内存空间。析构函数具有以下特点:
        ■ 析构函数函数名与类名相同, 紧贴在名称前面用波浪号 ~ 与构造函数进行区分, 例如: ~Point();
        ■ 构造函数没有返回类型, 也不能指定参数, 因此析构函数只能有一个, 不能被重载;
        ■ 当对象被撤销时析构函数被自动调用, 与构造函数不同的是, 析构函数可以被显式的调用, 以释放对象中动态申请的内存。

 

    #给Python程序员的注释: C++中的析构函数类似于Python中的 __del__ 方法.

 


    当用户没有显式定义析构函数时, 编译器同样会为对象生成一个默认的析构函数, 但默认生成的析构函数只能释放类的普通数据成员所占用的空间, 无法释放通过 new 或 malloc 进行申请的空间, 因此有时我们需要自己显式的定义析构函数对这些申请的空间进行释放, 避免造成内存泄露。

复制代码
 1 #include <iostream>
 2      #include <cstring>
 3 
 4      using namespace std;
 5 
 6      class Book
 7      {
 8          public:
 9              Book( const char *name )      //构造函数
10              {
11                  bookName = new char[strlen(name)+1];
12                  strcpy(bookName, name);
13              }
14              ~Book()                 //析构函数
15              {
16                  cout<<"析构函数被调用...\n";
17                  delete []bookName;  //释放通过new申请的空间
18              }
19              void showName() { cout<<"Book name: "<< bookName <<endl; }
20 
21          private:
22              char *bookName;
23      };
24 
25      int main()
26      {
27          Book CPP("C++ Primer");
28          CPP.showName();
29 
30          return 0;
31 
32      }
复制代码


    编译运行的结果:

        Book name: C++ Primer
        析构函数被调用...

        Process returned 0 (0x0)   execution time : 0.266 s
        Press any key to continue.


    代码说明:
        代码中创建了一个 Book 类, 类的数据成员只有一个字符指针型的 bookName, 在创建对象时系统会为该指针变量分配它所需内存, 但是此时该指针并没有被初始化所以不会再为其分配其他多余的内存单元。在构造函数中, 我们使用 new 申请了一块 strlen(name)+1 大小的空间, 也就是比传入进来的字符串长度多1的空间, 目的是让字符指针 bookName 指向它, 这样才能正常保存传入的字符串。
        
        在 main 函数中使用 Book 类创建了一个对象 CPP, 初始化 bookName 属性为 "C++ Primer"。从运行结果可以看到, 析构函数被调用了, 这时使用 new 所申请的空间就会被正常释放。
        
        自然状态下对象何时将被销毁取决于对象的生存周期, 例如全局对象是在程序运行结束时被销毁, 自动对象是在离开其作用域时被销毁。 
        
    如果需要显式调用析构函数来释放对象中动态申请的空间只需要使用 对象名.析构函数名(); 即可, 例如上例中要显式调用析构函数来释放 bookName 所指向的空间只要:

        CPP.~Book();










1.C++规定,每个类必须有默认的构造函数,没有构造函数就不能创建对象。 

  2.若没有提供任何构造函数,那么c++提供自动提供一个默认的构造函数,该默认构造函数是一个没有参数的构造函数,它仅仅负责创建对象而不做任何赋值操作。 

  3.只要类中提供了任意一个构造函数,那么c++就不在自动提供默认构造函数。 

  4.类对象的定义和变量的定义类似,使用默认构造函数创建对象的时候,如果创建的是静态或者是全局对象,则对象的位模式全部为0,否则将会是随即的。 

  我们来看下面的代码:

#include <iostream>  
using namespace std;    
class Student  
{  
    public:  
    Student()//无参数构造函数  
    {  
        number = 1;  
        score = 100;  
    }  
    void show();  
  
    protected:  
    int number;  
    int score;  
  
};  
  
void Student::show()  
{  
    cout<<number<<endl<<score<<endl;  
}  
  
void main()  
{  
    Student a;  
    a.show();  
    cin.get();  
}

执行结果:1

        100

  在类中的定义的和类名相同,并且没有任何返回类型的Student()就是构造函数,这是一个无参数的构造函数,他在对象创建的时候自动调用,如果去掉Student()函数体内的代码那么它和c++的默认提供的构造函数等价的。

  构造函数可以带任意多个的形式参数,这一点和普通函数的特性是一样的! 

下面我们来看一个带参数的构造函数是如何进行对象的始化操作的。 

  代码如下:

#include <iostream>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher(char *input_name)//有参数的构造函数  
    {  
        name=new char[10];  
        //name=input_name;//这样赋值是错误的  
        strcpy(name,input_name);  
    }  
    void show();  
  
    protected:  
    char *name;  
  
};  
  
void Teacher::show()  
{  
    cout<<name<<endl;  
}  
  
void main()  
{  
         //Teacher a;//这里是错误的,因为没有无参数的构造函数  
    Teacher a("test");  
    a.show();  
    cin.get();  
}

执行结果:test

   我们创建了一个带有字符指针的带有形参的Teacher(char *input_name)的构造函数,调用它创建对象的使用类名加对象名称加扩号和扩号内参数的方式调用,这和调用函数有点类似,但意义也有所不同,因为 构造函数是为创建对象而设立的,这里的意义不单纯是调用函数,而是创建一个类对象。 

  一旦类中有了一个带参数的构造函数而又没无参数构造函数的时候系统将无法创建不带参数的对象,所以上面的代码

Teacher a;

  就是错误的!!! 

  这里还有一处也要注意

//name=input_name;//这样赋值是错误的

  因为name指是指向内存堆区的,如果使用name=input_name;会造成指针指向改变不是指向堆区而是指向栈区,导致在后面调用析构函数delete释放堆空间出错!(析构函数的内容我们后面将要介绍) 

  如果需要调用能够执行就需要再添加一个没有参数的构造函数 

  对上面的代码改造如下:

#include <iostream>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher(char *input_name)  
    {  
        name=new char[10];  
        //name=input_name;//这样赋值是错误的  
        strcpy(name,input_name);  
    }  
    Teacher()//无参数构造函数,进行函数重载  
    {  
      
    }  
    void show();  
  
    protected:  
    char *name;  
  
};  
  
void Teacher::show()  
{  
    cout<<name<<endl;  
}  
  
void main()  
{  
    Teacher test;  
    Teacher a("test");  
    a.show();  
    cin.get();  
}

执行结果:test

  创建一个无阐述的同名的Teacher()无参数函数,一重载方式区分调用,由于构造函数和普通函数一样具有重载特性所以编写程序的人可以给一个类添加任意多个构造函数,来使用不同的参数来进行初始话对象。

现在我们来说一下,一个类对象是另外一类的数据成员的情况,如果有点觉得饶人那么可以简单理解成:类成员的定义可以相互嵌套定义,一个类的成员可以用另一个类进行定义声明。

  c++规定如果一个类对象是另外一类的数据成员,那么在创建对象的时候系统将自动调用那个类的构造函数。 

  下面我们看一个例子。 

  代码如下:

#include <iostream>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher()  
    {  
        director = new char[10];  
        strcpy(director,"王大力");  
    }  
    char *show();  
    protected:  
    char *director;  
};  
char *Teacher::show()  
{  
    return director;  
}  
class Student  
{  
    public:  
    Student()  
    {  
        number = 1;  
        score = 100;  
    }  
    void show();  
  
    protected:  
    int number;  
    int score;  
    Teacher teacher;//这个类的成员teacher是用Teacher类进行创建并初始化的  
  
};  
  
void Student::show()  
{  
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;  
}  
  
void main()  
{  
    Student a;  
    a.show();  
    Student b[3];  
    for(int i=0; i<sizeof(b)/sizeof(Student); i++)  
    {  
        b[i].show();  
    }  
    cin.get();  
}

执行结果:王大力

        1

        100

        王大力

        1

        100

        王大力

        1

        100

        王大力

        1

        100

  上面代码中的Student类成员中teacher成员是的定义是用类Teacher进行定义创建的,那么系统碰到创建代码的时候就会自动调用Teacher类中的Teacher()构造函数对对象进行初始化工作! 

  这个例子说明类的分工很明确,只有碰到自己的对象的创建的时候才自己调用自己的构造函数。

  一个类可能需要在构造函数内动态分配资源,那么这些动态开辟的资源就需要在对象不复存在之前被销毁掉,那么c++类的析构函数就提供了这个方便。 

  析构函数的定义:析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载,只有在类对象的生命期结束的时候,由系统自动调用。 

  析构函数与构造函数最主要大不同就是在于调用期不同,构造函数可以有参数可以重载! 

   我们前面例子中的Teacher类中就使用new操作符进行了动态堆内存的开辟,由于上面的代码缺少析构函数,所以在程序结束后,动态开辟的内存空间并 没有随着程序的结束而小时,如果没有析构函数在程序结束的时候逐一清除被占用的动态堆空间那么就会造成内存泄露,使系统内存不断减少系统效率将大大降低!

那么我们将如何编写类的析构函数呢? 

  析构函数可以的特性是在程序结束的时候逐一调用,那么正好与构造函数的情况是相反,属于互逆特性,所以定义析构函数因使用"~"符号(逻辑非运算符),表示它为腻构造函数,加上类名称来定义。 

  看如下代码:

#include <iostream>  
#include <string>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher()  
    {  
        director = new char[10];  
        strcpy(director,"王大力");  
        //director = new string;  
        // *director="王大力";//string情况赋值  
    }  
    ~Teacher()  
    {  
        cout<<"释放堆区director内存空间1次";  
        delete[] director;  
        cin.get();  
    }  
    char *show();  
    protected:  
    char *director;  
    //string *director;  
};  
char *Teacher::show()  
{  
    return director;  
}  
class Student  
{  
    public:  
    Student()  
    {  
        number = 1;  
        score = 100;  
    }  
    void show();  
  
    protected:  
    int number;  
    int score;  
    Teacher teacher;  
  
};  
  
void Student::show()  
{  
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;  
}  
void main()  
{  
    Student a;  
    a.show();  
    Student b[3];  
    for(int i=0; i<sizeof(b)/sizeof(Student); i++)  
    {  
        b[i].show();  
    }  
    cin.get();  
}

执行结果:王大力

        1

        100

        王大力

        1

        100

        王大力

        1

        100

        王大力

        1

        100

        释放堆区director内存空间1次

        释放堆区director内存空间1次

        释放堆区director内存空间1次

        释放堆区director内存空间1次

  上面的代码中我们为Teacher类添加了一个名为~Teacher()的析构函数用于清空堆内存。 

  建议大家编译运行代码观察调用情况,程序将在结束前也就是对象生命周期结束的时候自动调用~Teacher() 

  ~Teache()中的delete[] director;就是清除堆内存的代码,这与我们前面一开始提到的。 

name=input_name;//这样赋值是错误的 

  有直接的关系,因为delete操作符只能清空堆空间而不能清楚桟空间,如果强行清除栈空间内存的话将导致程序崩溃!

前面我们已经简单的说了类的构造函数和析构函数,我们知道一个类的成员可以是另外一个类的对象,构造函数允许带参数,那么我们可能会想到上面的程序 我们可以在类中把Student类中的teacher成员用带参数的形式调用Student类的构造函数,不必要再在Teacher类中进行操作,由于这 一点构想我们把程序修改成如下形式:

#include <iostream>    
#include <string>    
using namespace std;      
class Teacher    
{    
    public:    
    Teacher(char *temp)    
    {    
        director = new char[10];    
        strcpy(director,temp);  
    }  
    ~Teacher()    
    {    
        cout<<"释放堆区director内存空间1次";    
        delete[] director;    
        cin.get();    
    }    
    char *show();    
    protected:    
    char *director;    
};    
char *Teacher::show()    
{    
    return director;    
}    
class Student    
{    
    public:    
    Student()    
    {    
        number = 1;    
        score = 100;    
    }    
    void show();    
    
    protected:    
    int number;    
    int score;    
    Teacher teacher("王大力");//错误,一个类的成员如果是另外一个类的对象的话,不能在类中使用带参数的构造函数进行初始化  
    
};    
    
void Student::show()    
{    
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;    
}    
void main()    
{    
    Student a;    
    a.show();    
    Student b[3];    
    for(int i=0; i<sizeof(b)/sizeof(Student); i++)    
    {    
        b[i].show();    
    }    
    cin.get();    
}

  可是很遗憾,程序不能够被编译成功,为什么呢? 

  因为:类是一个抽象的概念,并不是一个实体,并不能包含属性值(这里来说也就是构造函数的参数了),只有对象才占有一定的内存空间,含有明确的属性值! 

  这一个问题是类成员初始化比较尴尬的一个问题,是不是就没有办法解决了呢?呵呵。。。。。。 

  c++为了解决此问题,有一个很独特的方法,下一小节我们将介绍。

  对于上面的那个"尴尬"问题,我们可以在构造函数头的后面加上:号并指定调用哪那个类成员的构造函数来解决! 

  教程写到这里的时候对比了很多书籍,发现几乎所有的书都把这一章节叫做构造类成员,笔者在此觉得有所不妥,因为从读音上容易混淆概念,所以把这一小节的名称改为构造类的成员比较合适!


  代码如下:

#include <iostream>    
using namespace std;      
class Teacher    
{    
    public:    
    Teacher(char *temp)    
    {    
        director = new char[10];    
        strcpy(director,temp);    
    }  
    ~Teacher()    
    {    
        cout<<"释放堆区director内存空间1次";    
        delete[] director;    
        cin.get();    
    }    
    char *show();    
    protected:    
    char *director;    
};    
char *Teacher::show()    
{    
    return director;    
}    
class Student    
{    
    public:    
    Student(char *temp):teacher(temp)  
    {    
        number = 1;    
        score = 100;    
    }    
    void show();    
    
    protected:    
    int number;    
    int score;    
    Teacher teacher;    
    
};    
    
void Student::show()    
{    
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;    
}    
void main()    
{    
    Student a("王大力");    
    a.show();    
    //Student b[3]("王大力");  //这里这么用是不对的,数组不能够使用带参数的构造函数,以后我们将详细介绍vector类型  
    // for(int i=0; i<sizeof(b)/sizeof(Student); i++)    
    //{    
    //    b[i].show();    
    //}    
    cin.get();    
}

执行结果:王大力

        1

        100

        释放堆区director内存空间1次

  大家可以发现最明显的改变在这里 

Student(char *temp):teacher(temp) 

   冒号后的teacher就是告诉调用Student类的构造函数的时候把参数传递给成员teacher的Teacher类的构造函数,这样一来我们就成 功的在类体外对teacher成员进行了初始化,既方便也高效,这种冒号后指定调用某成员构造函数的方式,可以同时制定多个成员,这一特性使用逗号方式, 例如: 

Student(char *temp):teacher(temp),abc(temp),def(temp) 

  由冒号后可指定调用哪那个类成员的构造函数的特性,使得我们可以给类的常量和引用成员进行初始化成为可能。 

  我们修改上面的程序,得到如下代码:

#include <iostream>    
#include <string>    
using namespace std;      
class Teacher    
{    
    public:    
    Teacher(char *temp)    
    {    
        director = new char[10];    
        strcpy(director,temp);    
    }  
    ~Teacher()    
    {    
        cout<<"释放堆区director内存空间1次";    
        delete[] director;    
        cin.get();  
    }    
    char *show();    
    protected:    
    char *director;    
};    
char *Teacher::show()    
{    
    return director;    
}    
class Student    
{    
    public:    
    Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10)  
    {    
        number = 1;    
        score = 100;  
    }    
    void show();    
    
    protected:    
    int number;    
    int score;    
    Teacher teacher;  
    int &pk;  
    const int ps;  
    
};    
    
void Student::show()    
{    
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl<<pk<<endl<<ps<<endl;    
}    
void main()    
{    
    char *t_name="王大力";  
    int b=99;  
    Student a(t_name,b);  
    a.show();  
    cin.get();  
}

执行结果:王大力

        1

        100

        99

        10

        释放堆区director内存空间1次

  改变之处最重要的在这里Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10) 

  调用的时候我们使用 

Student a(t_name,b); 

  我们将b的地址传递给了int &pk这个引用,使得Student类的引用成员pk和常量成员ps进行了成功的初始化。 

但是细心的人会发现,我们在这里使用的初始化方式并不是在构造函数内进行的,而是在外部进行初始化的,的确,在冒号后和在构造函数括号内的效果是一样的, 但和teacher(temp)所不同的是,pk(pk)的括号不是调用函数的意思,而是赋值的意思,我想有些读者可能不清楚新标准的c++对变量的初始 化是允许使用括号方式的,int a=10和int a(10)的等价的,但冒号后是不允许使用=方式只允许()括号方式,所以这里只能使用pk(pk)而不能是pk=pk了。

  这一小节的内容是说对象构造的顺序的,对象构造的顺序直接关系程序的运行结果,有时候我们写的程序不错,但运行出来的结果却超乎我们的想象,了解c++对对象的构造顺序有助于解决这些问题。 

  c++规定,所有的全局对象和全局变量一样都在主函数main()之前被构造,函数体内的静态对象则只构造一次,也就是说只在首次进入这个函数的时候进行构造! 

  代码如下:

#include <iostream>    
#include <string>    
using namespace std;      
  
class Test  
{  
public:  
    Test(int a)  
    {  
        kk=a;  
        cout<<"构造参数a:"<<a<<endl;  
    }  
public:  
    int kk;  
};  
  
void fun_t(int n)  
{  
    static Test a(n);  
    //static Test a=n;//这么写也是对的  
    cout<<"函数传入参数n:"<<n<<endl;  
    cout<<"对象a的属性kk的值:"<<a.kk<<endl;  
}  
Test m(100);  
void main()  
{  
    fun_t(20);  
    fun_t(30);  
    cin.get();  
}

执行结果:构造参数a:100

        构造参数a:20

        函数传入参数n:20

        对象a的属性kk的值:20

        函数传入参数n:30

        对象a的属性kk的值:20

  下面我们来看一下,类成员的构造顺序的问题。 

  先看下面的代码:

#include <iostream>    
using namespace std;      
  
class Test  
{  
public:  
    Test(int j):pb(j),pa(pb+5)  
    {  
          
    }  
public:  
    int pa;  
    int pb;  
};  
void main()  
{  
    Test a(10);  
    cout<<a.pa<<endl;  
    cout<<a.pb<<endl;  
    cin.get();  
}

执行结果:-858993455

        10

  上面的程序在代码上是没有任何问题的,但运行结果可能并不如人意。 

  pa并没有得到我们所希望的15而是一个随机的任意地址的值。 

  这又是为什么呢? 

  类成员的构造是按照在类中定义的顺序进行的,而不是按照构造函数说明后的冒号顺序进行构造的,这一点需要记住!






创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。


注意,类的数据成员是不能在声明类时初始化的。如果一个类中所有的成员都是公用的,则可以在定义对象时对数据成员进行初始化。如:


      
      
  1. class Time
  2. {
  3. public : //声明为公用成员
  4. hour;
  5. minute;
  6. sec;
  7. };
  8. Time t1={14,56,30}; //将t1初始化为14:56:30
这种情况和结构体变量的初始化是差不多的,在一个花括号内顺序列出各公用数据成员的值,两个值之间用逗号分隔。但是,如果数据成员是私有的,或者类中有private或protected的成员,就不能用这种方法初始化。

这里的几个例子(C++面向对象程序设计举例)是用成员函数来对对象中的数据成员赋初值的(例如例8.3中的set_time函数)。从例8.3中可以看到,用户在主函数中调用set_time函数来为数据成员赋值。如果对一个类定义了多个对象,而且类中的数据成员比较多,那么,程序就显得非常臃肿烦琐。

构造函数的作用

为了解决这个问题,C++提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。

构造函数的名字必须与类名同名,而不能由用户任意命名,以便编译系统能识别它并把它作为构造函数处理。它不具有任何类型,不返回任何值。构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数。

【例9.1】在例8.3基础上定义构造成员函数。

      
      
  1. #include <iostream>
  2. using namespace std;
  3. class Time
  4. {
  5. public :
  6. Time( )
  7. {
  8. hour=0;
  9. minute=0;
  10. sec=0;
  11. }
  12. void set_time( );
  13. void show_time( );
  14. private :
  15. int hour;
  16. int minute;
  17. int sec;
  18. };
  19. void Time::set_time( )
  20. {
  21. cin>>hour;
  22. cin>>minute;
  23. cin>>sec;
  24. }
  25. void Time::show_time( )
  26. {
  27. cout<<hour<<":"<<minute<<":"<<sec<<endl;
  28. }
  29. int main( )
  30. {
  31. Time t1;
  32. t1.set_time( );
  33. t1.show_time( );
  34. Time t2;
  35. t2.show_time( );
  36. return 0;
  37. }
程序运行的情况为:
10 25 54↙  (从键盘输入新值赋给t1的数据成员)
10:25:54    (输出t1的时、分、秒值) 
0:0:0   (输出t2的时、分、秒值)

在类中定义了构造函数Time,它和所在的类同名。在建立对象时自动执行构造函数,它的作用是对该对象中的数据成员赋初值0。请不要误认为是在声明类时直接对程序数据成员陚初值(那是不允许的),赋值语句是写在构造函数函数体中的,只有在调用构造函数时才执行这些赋值语句,对当前的对象中的数据成员赋值。

上面是在类内定义构造函数的,也可以只在类内对构造函数进行声明而在类外定义构造函数。将程序中的第4~7行改为下面一行:
    Time( ); //对构造函数进行声明
在类外定义构造函数:

      
      
  1. Time::Time( ) //在类外定义构造成员函数,要加上类名Time和域限定符“::”
  2. {
  3. hour=0;
  4. minute=0;
  5. sec=0;
  6. }

有关构造函数的使用,有以下说明:
  1. 在类对象进入其作用域时调用构造函数。
  2. 构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。
  3. 构造函数不需用户调用,也不能被用户调用。
  4. 在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句。但是一般不提倡在构造函数中加入与初始化无关的内容,以保持程序的清晰。
  5. 如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。

析构函数(destructor)也是一个特殊的成员函数,它的作用与构造函数相反,它的名字是类名的前面加一个“~”符号。


在C++中“~”是位取反运算符,从这点也可以想到,析构函数是与构造函数作用相反的函数。当对象的生命期结束时,会自动执行析构函数。

具体地说如果出现以下几种情况,程序就会执行析构函数:
  1. 如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
  2. static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
  3. 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。
  4. 如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。

析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。程序设计者事先设计好析构函数,以完成所需的功能,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作。

注意:析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。

实际上,析构函数的作用并不仅限于释放资源方面,它还可以被用来执行“用户希望在最后一次使用对象之后所执行的任何操作”,例如输出有关的信息。这里说的用户是指类的设计者,因为,析构函数是在声明类的时候定义的。也就是说,析构函数可以完成类的设计者所指定的任何操作。

一般情况下,类的设计者应当在声明类的同时定义析构函数,以指定如何完成“清理”的工作。如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。

【例9.5】包含构造函数和析构函数的C++程序。

      
      
  1. #include<string>
  2. #include<iostream>
  3. using namespace std;
  4. class Student //声明Student类
  5. {
  6.    public :
  7.    Student(int n,string nam,char s ) //定义构造函数
  8.    {
  9.       num=n;
  10.       name=nam;
  11.       sex=s;
  12.       cout<<"Constructor called."<<endl; //输出有关信息
  13.    }
  14.    ~Student( ) //定义析构函数
  15.    {
  16.       cout<<"Destructor called. The num is "<<num<<"."<<endl;
  17.    } //输出有关信息
  18.    void display( ) //定义成员函数
  19.    {
  20.       cout<<"num: "<<num<<endl;
  21.       cout<<"name: "<<name<<endl;
  22.       cout<<"sex: "<<sex<<endl<<endl;
  23.    }
  24.    private :
  25.    int num;
  26.    string name;
  27.    char sex;
  28. };
  29. int main( )
  30. {
  31.    Student stud1(10010,"Wang_li",'f'); //建立对象stud1
  32.    stud1.display( ); //输出学生1的数据
  33.    Student stud2(10011,"Zhang_fun",'m'); //定义对象stud2
  34.    stud2.display( ); //输出学生2的数据
  35.    return 0;
  36. }
程序运行结果如下:
Constructor called.    (执行stud1的构造函数) 
num: 10010    (执行stud1的display函数)
name:Wang_li
sex: f
Constructor called.    (执行stud2的构造函数)
num: 10011     (执行stud2的display函数)
name:Zhang_fun
sex:m
Destructor called. The num is 10011.    (执行stud2的析构函数)
Destructor called. The num is 10010.    (执行stud1的析构函数)

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值