构造函数 | 析构函数

目录

构造函数

定义

特征

默认构造函数

特性

1.默认构造函数只能有一个。

2.初始化->随机值?

问题

多个非默认构造函数,可以同时存在吗?

析构函数

 简述

特性


构造函数

定义

  设想这样两个场景:a.我们在创建变量以后,常常会因为忘记初始化使结果出现随即值…… b.我们一下子创建了100个变量,为了一一初始化它们,得调用100次Init函数…… 是不是想想头都大了?对此,C++的解决方式是:用构造函数来自动初始化。

  构造函数是特殊的成员函数(怎么特殊我们一会再说)。它的名字具有误导性,实际上构造函数并不是用来开空间创建对象的,而是初始化对象的。

特征

  1. 构造函数的函数名与类名相同。

  2. 无返回值。注:不要写void,void代表空返回值。无返回值就是没有返回类型,函数的定义为“函数名+()+{}“

  3. 对象实例化时编辑器自动调用对应的构造函数。

    下面的例子通过打印成员变量的值,来说明实例化时,的确调用了构造函数。

    #include
    class Date
    {
    public:
        Date()
        {
            _year = 2000;
            _month = 1;
            _day = 1;
        }
        void print()
        {
            printf("%d %d %d", _year, _month, _day);
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    int main()
    {
        Date d1;         //实例化出对象d1
        d1.print();      //打印出d1中的成员变量,观察是否被初始化
        putchar('\n');
        return 0;
    }

    运行结果:可见实例化出d1时,编辑器自动调用了Date()函数。

    构造函数 | 析构函数_第1张图片

  4. 构造函数可以重载。

    即允许存在多个具有相同名称但参数列表不同的构造函数。

    构造函数的重载可以通过以下几种方式实现:

    1.参数个数不同:可以定义多个构造函数,每个构造函数具有不同数量的参数。例如:

    class Date {
    public:
        Date() {
            // 无参构造函数
        }
        
        Date(int num) {
            // 带一个整型参数的构造函数
        }
        
        Date(int num1, int num2) {
            // 带两个整型参数的构造函数
        }
    };

    注意:在调用无参构造函数时,千万不要在对象后面加括号,不然就成了函数声明!

    正确的调用方式为:Date d1;

    错误的调用方式为: Date d1(); //这样的含义是:函数d1的声明。该函数无参,返回类型为Date类型

    //我们说过,构造函数是特殊的!你不能像对待一般函数那样使用它,这里就是它的特殊点之一

    2.参数类型不同:可以定义多个构造函数,每个构造函数具有不同类型的参数。例如:

    class Date {
    public:
        Date() {
            // 无参构造函数
        }
        
        Date(int num) {
            // 带一个整型参数的构造函数
        }
        
        Date(double num) {
            // 带一个双精度浮点型参数的构造函数
        }
    };

    3.参数顺序不同:可以定义多个构造函数,每个构造函数具有相同类型的参数,但顺序不同。例如:

    class Date {
    public:
        Date() {
            // 无参构造函数
        }
        
        Date(int num1, int num2) {
            // 带两个整型参数的构造函数(参数顺序为num1, num2)
        }
        
        Date(int num2, int num1) {
            // 带两个整型参数的构造函数(参数顺序为num2, num1)
        }
    };

  5. 如果类中没有显式定义构造函数,那么C++编译器会自动生成一个无参的默认构造函数。一旦用户显式定义了,编译器便不再自动生成。下面的例子说明这一点:

    #include
    class Date
    {
    public:
        /*Date(int year, int month, int day)       //这个构造函数是我们显示定义出来的
        {                               //如果注释这段代码,那就没有显式定义的函数了,结果会如何呢?                    
            _year = year;                       //如果放开这段代码,那一定会报错,为什么呢?
            _month = month;
            _day = day;
        }*/
        void print()
        {
            printf("%d %d %d", _year, _month, _day);
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    int main()
    {
        Date d1;
        d1.print();
        putchar('\n');
        return 0;
    }

    被注释掉的那部分如果不放开:

    构造函数 | 析构函数_第2张图片

     这里的结果表明:调用了编译器自动生成的无参默认构造函数。至于为什么是随机值?我们一会讲到。

注释掉的那部分如果放开:

构造函数 | 析构函数_第3张图片

报错:不存在默认构造函数!

      这是因为:我们实例化对象时,并没有传三个参数过去,这就和类中的构造函数的形参列表不匹配,所以不会调用它。然而编译器也无法自动生成默认构造函数,因为类中已经存在显式构造函数了。所以,d1可以说是两边都不讨好,无法被初始化了。

默认构造函数

默认构造函数有三种:无参的构造函数、全缺省的构造函数、(我们没写时)编译器自动生成的构造函数。

特性

1.默认构造函数只能有一个。

我们用这段代码来展示”默认构造函数只能有一个“:

#include
class Date
{
public:
    /*Date()                   //当注释掉这段代码,就只有一个默认构造函数,可以编译通过。
    {                          //如果放开,就有两个默认构造函数了。能编译通过吗?
        _year = 2000;
        _month = 1;
        _day = 1;
    }*/
    Date(int year=0, int month=0, int day=0)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        printf("%d %d %d", _year, _month, _day);
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    d1.print();
    putchar('\n');
    return 0;
}

 如果注释掉代码:

 构造函数 | 析构函数_第4张图片

此时调用的是全缺省的构造函数,即在场唯一的默认构造函数。

如果放开代码:

 

报错:包含多个默认函数!可见,只能由一个默认构造函数。

2.初始化->随机值?

我们刚刚留了个疑问,为什么这段代码调用了构造函数,结果还是随机值:

#include
class Date
{
public:
    void print()
    {
        printf("%d %d %d", _year, _month, _day);
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    d1.print();
    putchar('\n');
    return 0;
}

运行结果:

构造函数 | 析构函数_第5张图片

   我们知道,在没有显示构造函数时,编译器会自动生成无参的默认构造函数。那为什么还是随机值?这个构造函数看起来并没有什么作用啊……

构造函数 | 析构函数_第6张图片

  实际上,的确没什么作用。。。

  C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。编译器生成的默认构造函数会对自定义类型调用它的默认成员函数,而内置类型不做处理。简言之,只有自定义类型能被初始化,内置类型不能!

看下面的程序,如果控制台输出了"Time()",就证明自定义类型Time的默认构造函数被调用了:

#include
using namespace std;
class Time
{
public:
    Time()
    {
        cout << "Time()" << endl;    //只要调用了默认构造函数Time(),就会输出"Time()"
        _hour = 0;                   
        _minute = 0;
        _second = 0;
    }
private:
    int _hour;
    int _minute;
    int _second;
};
​
class Date
{
private:
    int _year;
    int _month;
    int _day;
    
    Time _time;
};
int main()
{
    Date d1;
    return 0;
}

运行结果:Time的默认构造函数的确被调用了。

构造函数 | 析构函数_第7张图片

 在监视中可以看到,只有自定义类型_time被初始化,内置类型仍为随机值:

构造函数 | 析构函数_第8张图片

  不过,C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

这里的代码和上一个一样,只不过我们给内置类型赋上了默认值:

#include
using namespace std;
class Time
{
public:
    Time()
    {
        cout << "Time()" << endl;
        _hour = 0;
        _minute = 0;
        _second = 0;
    }
private:
    int _hour;
    int _minute;
    int _second;
};
​
class Date
{
private:
    int _year=2000;    //给内置类型赋上默认值
    int _month=0;
    int _day=0;
    
    Time _time;
};
int main()
{
    Date d1;
    return 0;
}

打开监视:发现内置类型也可以被初始化了。

构造函数 | 析构函数_第9张图片

问题

多个非默认构造函数,可以同时存在吗?

除去我们说到的那三个默认的,其他的,如半缺省的构造函数、没设缺省的构造函数,都不是默认构造函数。它们可以多个同时存在吗?

答案:如果能满足函数重载的话,就能!构造函数的重载允许我们定义多个具有不同参数列表的构造函数。

但注意:如果形参列表相同,单单缺省参数不同,是不能同时存在的。

例:

构造函数 | 析构函数_第10张图片

(问题待补充……)

析构函数

 简述

  析构函数的功能与构造函数相反,后者负责初始化,前者负责清理(对象占用的)内存。析构函数不是用来Destroy对象的,出了函数栈帧对象会被编译器销毁。它是负责对象中的资源清理工作,在程序中任职“清洁工”。

构造函数 | 析构函数_第11张图片

  析构函数并非能清理一切,对于内置类型,它是不做处理的。它主要用于释放自定义类型所占的空间、释放动态分配的内存、关闭文件等。

为什么不处理内置类型?

因为int,char这些内置类型,当超出作用域是会被自动销毁的,不需要再由析构函数去释放了。

  析构函数作为特殊的成员函数,是C++中非常重要的概念。它可以避免内存泄漏的问题,从此你不需要再挨个手动free了。

特性

1.析构函数的函数名是在类名前加上~

2.无参数无返回类型。注:无返回类型就是不写。不要写void !

命名规则与构造函数相同,即以“~类名( )”的格式。

3.一个类只能有一个析构函数。析构函数分两种,一种是程序员自己写的,一种是系统自动生成的。若未显示定义,那么系统会自动生成默认的析构函数。析构函数不能重载。

4.对象生命周期结束时,系统会自动调用析构函数。

5.系统自动生成的默认析构函数,对内置类型不做处理。对于自定义类型,则会调用析构函数。但是!默认析构函数对自定义类型的处理也没有那么省心:它是不会主动free掉动态内存空间的,它只能帮你调用成员变量的析构函数,至于开辟的动态内存,还得你自己去释放掉。这种情况,就要自己写显示的析构函数了。

下面这个例子,展示了“程序自动调用析构函数,来处理自定义类型”:

#include
using namespace std;
class Time
{
public:                     //如果调用了Time的析构函数
	~Time()                  //会输出“~Time()”
	{                        //会调用吗?
		cout << "~Time()" << endl;
	}
private:
	int _hour;
};
class Date         
{                           //Date的析构函数会被调用吗?
private:
	int _year;  //内置类型
	int _month;
	int _day;

	Time _time;  //自定义类型
};
int main()
{
	Date d1;
	return 0;
}

结果:

疑惑:我们实例化的是Date类型的对象,Time类型压根没实例化,那为什么会调用Time的析构函数呢?

answer:在销毁 Date前,会调用Date的默认析构函数,目的是销毁Date中的自定义变量Time。而销毁Time,则又会调用Time的析构函数。(由于这里显示写了~Time(),那么编译器会直接调用,而不默认生成)

Tip:这里我们把~time()显示写出来,是为了打印出来方便理解。如果这里不写也是可以的,编译器会生成默认析构,仍会自动调用,销毁Time。

补充:我们厘清一个问题:到底什么时候写析构函数,什么时候不写?

构造函数 | 析构函数_第12张图片

  

构造函数与析构函数的调用顺序

  当构造的多个对象时需要被析构时,它们是按照什么顺序被释放的呢?这篇旨在帮助我们学习:析构函数的调用顺序。

  先说结论:先构造的后析构,后构造的先析构。其实构造和析构都遵循栈的数据结构,即后进先出。这是因为对象的初始化和清理是被定义在(构造/析构)函数里面的,调用函数时会建立栈帧,函数建立时会依次压入栈帧,函数销毁时又会依次弹出栈。

构造函数 | 析构函数_第13张图片

  一般情况下,对象的析构顺序和它们的创建顺序相反。而当局部对象、静态局部对象、全局对象同时存在时,析构的顺序并不是简单的倒过来了。

下面这个程序可以让我们直观地看到三种对象的构造/析构顺序:

#include
using namespace std;
class A
{
public:
    A(int num)
    {
        _a = num;
        cout << "A()->" << _a << endl;   //当调用构造函数时,会打印出来
    }
    ~A()
    {
        cout << "~A()->" << _a << endl;  //当调用析构函数时,会打印出来
    }
private:
    int _a;
};
A a0(0);         //我们依次构造了全局的a0,局部的a1、a2,静态的a3
int main()
{
    A a1(1);
    A a2(2);
    static A a3(3);
    return 0;
}

运行结果:

构造函数 | 析构函数_第14张图片

结果表明,局部对象先按出栈顺序析构,然后是静态对象,最后是全局对象。

解释

  全局对象的作用域是全局作用域,这个作用域相当于一个程序最外层的容器,全局对象在一个工程里的不同文件内都可以使用。因此,全局对象在程序结束时析构。

  局部对象的作用域在程序块中,它在程序块结束时析构。

  被static修饰的静态局部对象,生命周期被延长至(它所定义在的)整个文件,当文件结束时,它被析构。

  因此,局部对象先进行析构,析构的顺序为出栈的顺序;然后文件结束时,静态局部对象被析构,如果是多个对象,依然按照出栈顺序;最后,是全局变量。

tips:

"局部对象,在退出程序块时析构

静态对象,在定义所在文件结束时析构

全局对象,在程序结束时析构

继承对象,先析构派生类,再析构父类

对象成员,先析构类对象,再析构对象成员"

                                                                    (tips from:@南方以北,http://t.csdn.cn/bTeud

OK,我们来练习一下,试着写出这个程序的打印结果:

#include
using namespace std;
class A
{
public:
    A(int num)
    {
        _a = num;
        cout << "A()->" << _a << endl;    //当调用构造函数时,会打印出来
    }
    ~A()
    {
        cout << "~A()->" << _a << endl;    //当调用析构函数时,会打印出来
    }
private:
    int _a;
};
A a0(0);
void f()
{
    A a1(1);
    A a2(2);
    static A a3(3);
}
int main()
{
    f();           //注意:调用了两次f()
    f();
    return 0;
}

运行结果:

构造函数 | 析构函数_第15张图片  

  这里需要注意的点在于:第一次调用已经在静态区创建了全局对象和静态对象,它们在f( )结束时,并不会释放。只有局部对象释放了。第二次调用f( )时,因为静态局部对象a3已经存在,所以不会再构造了,只会开辟a1和a2的栈帧。

  


  关于构造函数和析构函数的知识点就分享到这里,后续会不断更新这篇博客,扩充这篇的知识容量。如有错误,还望指正(^ - ^)

你可能感兴趣的:(c++)