C++面向对象编程入门:构造函数与析构函数

  请注意,这一节内容是c++的重点,要特别注意!

  我们先说一下什么是构造函数。

  上一个教程我们简单说了关于类的一些基本内容,对于类对象成员的初始化我们始终是建立成员函数然后手工调用该函数对成员进行赋值的,那么在c++中对于类来说有没有更方便的方式能够在对象创建的时候就自动初始化成员变量呢,这一点对操作保护成员是至关重要的,答案是肯定的。关于c++类成员的初始化,有专门的构造函数来进行自动操作而无需要手工调用,在正式讲解之前先看看c++对构造函数的一个基本定义。

  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(); 
}

  在类中的定义的和类名相同,并且没有任何返回类型的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(); 
}

  我们创建了一个带有字符指针的带有形参的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(); 
}

  创建一个无阐述的同名的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[5]; 

    for(int i=0; i<sizeof(b)/sizeof(Student); i++) 

    { 
        b[i].show(); 
    } 
    cin.get(); 
}

  上面代码中的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[5]; 
    for(int i=0; i<sizeof(b)/sizeof(Student); i++) 

    { 
        b[i].show(); 
    } 
    cin.get(); 
}

  上面的代码中我们为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[5];   

    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[5]("王大力");  //这里这么用是不对的,数组不能够使用带参数的构造函数,以后我们将详细介绍vector类型 

    // for(int i=0; i<sizeof(b)/sizeof(Student); i++)   

    //{   
    //    b[i].show();   
    //}   

    cin.get();   
}

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

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(); 
}

  改变之处最重要的在这里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(); 
}

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

  先看下面的代码:

#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(); 
}

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

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

  这又是为什么呢?

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

 
 

《挑战30天C++入门极限》C++面向对象编程入门:构造函数与析构函数的更多相关文章

  1. 《挑战30天C++入门极限》C++面向对象编程入门:类(class)

        C++面向对象编程入门:类(class) 上两篇内容我们着重说了结构体相关知识的操作. 以后的内容我们将逐步完全以c++作为主体了,这也意味着我们的教程正式进入面向对象的编程了. 前面的教程我 ...

  2. (二)Javascript面向对象编程:构造函数的继承

    Javascript面向对象编程:构造函数的继承   这个系列的第一部分,主要介绍了如何"封装"数据和方法,以及如何从原型对象生成实例. 今天要介绍的是,对象之间的"继承 ...

  3. 《Java从入门到放弃》JavaSE入门篇:面向对象概念(入门版)

    要知道什么是面向对象,你首先要有个对象吧,所以······没有对象的可以回家洗洗睡了· 好吧,前面是开玩笑,要说明什么是面向对象,我们还是先 例子: 小呆:"小傻,你今天早餐吃的什么?&qu ...

  4. 《挑战30天C++入门极限》C++中利用构造函数与无名对象简化运算符重载函数

        C++中利用构造函数与无名对象简化运算符重载函数 在完整描述思想之前,我们先看一下如下的例子,这个例子中的加运算符重载是以非成员函数的方式出现的: //程序作者:管宁  //站点:www.cn ...

  5. 挑战30天写操作系统-day1-从计算机结构到汇编程序入门

    先动手操作 软盘映像文件制作:先采用二进制编辑器编辑我们所需要的映像文件helloos.img 二进制编辑器下载链接:Bz - c.mos (vcraft.jp) 制作好之后,可以选择写入软盘,通过软 ...

  6. JavaScript面向对象编程入门

    来源极客网 function Person() { var _this = {} //创建一个空的对象,接着我们利用这个"空的对象"承载Person的属性和方法 _this.say ...

  7. C++ 快速入门笔记:面向对象编程

    类 & 对象 类定义 class Box { public: double length; // Length of a box double breadth; // Breadth of a ...

  8. 《Java从入门到放弃》JavaSE入门篇:网络编程(入门版)

    要进行网络编程,首先要搞清楚目的是什么. 网络编程说简单点就是在网络上的计算机进行数据的交互. 既然要进行数据交互,那就需要有一个发送方和一个接受方. 按网络上的说法,就是一个攻一个受· 当然,现阶段 ...

  9. Javascript面向对象编程:构造函数的继承

    今天要介绍的是,对象之间的"继承"的五种方法. 比如,现在有一个"动物"对象的构造函数. function Animal(){ this.species = & ...

随机推荐

  1. 记一次git merge 事故

    最近发生在自己身上的一件矬事儿,一不小心把matser上别人的代码给冲掉了,事后追溯了下原因. 1.准备三个分支,分别从master拉取 realease/v1.0分支 和 realease/bugf ...

  2. ASP.NET MVC+Entity Framework code first 迁移

    再来一张,选择 MVC 模版,其他的没选过,不会用 =_=!! 身份验证用个人用户账户,这个是为了偷懒,话说 ASP.NET Identity  还是很给力的,不用白不用 ^_^~ 点击确定之后,会看 ...

  3. sourceTree 3.X免注册[学习]

    一. 在路径C:\****\AppData\Local\Atlassian\SourceTree下创建accounts.json文件 [ { "$id": "1" ...

  4. 逗号分隔的字符串转成表格参与IN条件查询

    返回值为'1,2,3,4,5,6,7',是一个字符串,如果要用IN 查询的话sql认为这是一个完整的字符串,需要将内容分隔转换变成table 定义函数如下: create Function sysfS ...

  5. MySQL Backup--Xtrabackup备份参数

    Xtrabackup备份参数 参数选项: innobackupex [--compress] [--compress-threads=NUMBER-OF-THREADS] [--compress-ch ...

  6. uwsgi配置文件示例

    uwsgi配置文件参考 相关路径请根据自己项目的实际路径配置 在进行Nginx+uwsgi部署Django项目的时候,需要Nginx的配置中包含uwsgi的配置项,具体请查看另一篇:Nginx配置文件 ...

  7. 使用ImageMagick在Linux系统上截图

    ImageMagick安装指令: sudo apt-get install imagemagick 安装完成后,输入 import screenshot.png 命令就可以开始截图.此时鼠标图标会变成 ...

  8. k8s 笔记

    一. 解决pod间依赖性 1.手动的采用不同顺序启动不同pod 2.定义restart policy(默认为alway,我们可以定义当某条件不满足时就一直重启,当满足条件是才启动容器) 3.如果对于强 ...

  9. convirt集中管理平台搭建

    情况说明: (1)本文采用OEL6.3x64操作系统,需要有KVM安装环境.(2)convirt2.1.1采用源码方式安装,convirt环境分别两部分,一部分是cms,用于管理kvm/xen虚拟主机 ...

  10. XPath知识点【一】

    什么是 XPath? XPath 使用路径表达式在 XML 文档中进行导航 XPath 包含一个标准函数库 XPath 是 XSLT 中的主要元素 XPath 是一个 W3C 标准 XPath 路径表 ...