构造函数与析构函数

目录

  • 前言
  • 1.构造函数
    • 1.1构造函数特征
      • 1️⃣构造函数的引入
      • 2️⃣带参数的构造函数
      • 3️⃣构造函数的重载
      • 4️⃣默认构造函数
      • 2.析构函数
        • 2.1析构函数的引入
        • 2.2析构函数的调用情况
        • 3.析构函数与构造函数的调用顺序
          • 3.1构造函数与析构函数特性补充
          • 3.2二者调用顺序
          • Ending

            前言

            在上一篇内容中,自定义的类里面是存在成员函数的;而这些成员函数都是我们显式定义出来的,而一般情况下,类中还存在几个默认的成员函数:构造函数、析构函数、拷贝构造函数、赋值重载以及取地址重载等,而本节则主要论述前两个函数,构造函数和析构函数!

            关于类和对象这部分内容,是有些晦涩的,不易理解的,主要是其中要点过于琐碎,还是应该回归于书本!

            1.构造函数

            在之前我们都是利用Init函数对对象中的数据进行初始化的,而C++中提供了一种较为高效的初始化方式——构造函数。

            需要注意的是,构造函数的主要任务是对对象的数据进行初始化操作!

            1.1构造函数特征

            1️⃣构造函数的引入

            构造函数的名字必须与类名一致,且不能任意命名,以便编译系统识别,且构造函数不具有任何类型不具有返回值!

            下面给出一个例子:

            class Date
            {private:
            	int _year;
            	int _month;
            	int _day;
            public:
            	Date()
            	{_year = 1;
            		_month = 1;
            		_day = 1;
            	}
            	void Display();
            };
            void Date :: Display()
            {cout << _year << ":" << _month << ":" << _day << endl;
            }
            int main()
            {Date d1;
            	d1.Display();
            	return 0;
            }
            

            其中Date就是一个简易的构造函数!因此对象d1就的数据就被初始化成了1:1:1!

            在上述调用过程中,存在以下几点需要注意:

            1.在建立类的对象时,类的构造函数会自动调用。在建立对象时系统会为该对象分配存储单元,此时先执行构造函数,就把指定的初值送到有关的数据成员的存储单元中去了。

            2.构造函数没有返回值,不需要类型,是定义对象时自动调用不需要人为手动调用!

            3.如果用户未定义构造函数,则C++系统会为其生成一个构造函数,只是这个构造函数是空的,也没有参数!

            关于第3点,我们不妨将先前的构造函数注释掉,此时则可以很好的观察出这一特性!

            所以当我们没有显式定义构造函数的时候,编译器会自动生成1个无参的默认构造函数,显式定义的话,编译器就不再生成了!

            2️⃣带参数的构造函数

            Date(int year,int month,int day)
            {_year = year;
            	_month = month;
            	_day = day;
            }
            

            将构造函数改成这个样子,此时在使用该类去定义对象的时候就需要传递参数了!

            int main()
            {Date d1(2022, 10, 4);
            	d1.Display();
            	return 0;
            }
            

            如果我们将该构造函数改为缺省参数的话,此时传几个参数我们就可以自己决定了,既可以全传也可以不穿,或者传递部分参数。

            以下我写一个全缺省的参数的构造函数:

            Date(int year=1,int month=1,int day=1)
            {_year = year;
            	_month = month;
            	_day = day;
            }
            

            3️⃣构造函数的重载

            构造函数支持重载!

            我们知道,函数重载需要保证函数名一致,但是函数的参数类型顺序都要存在差异才可以算的上是函数重载,因为此条性质比较简单,就不进行演示了!

            class Date
            {private:
            	int _year;
            	int _month;
            	int _day;
            public:
            	Date()
            	{_year = 1;
            		_month = 1;
            		_day = 1;
            	}
            	Date(int year=1,int month=1,int day=1)
            	{_year = year;
            		_month = month;
            		_day = day;
            	}
            	void Display();
            };
            

            此时这种重载是不正确的,编译器会报错,因为当定义对象自动调用构造函数时,编译器并不知道调用哪一个构造函数!因此就存在了二义性!

            Box(int a=10,int b=10,int c=10);
            Box();
            Box(int a,int b);
            //.......
            当存在以下调用时:
            Box box1;
            //此时是调用第一个函数还是第二个呢?
            Box box2(10,20);
            //此时调用第一个函数还是第三个呢?
            

            所以存在默认参数的构造函数时需要注意调用传参时是否存在错误!

            4️⃣默认构造函数

            默认构造函数:在建立对象的时候不必给出实参的构造函数都被称为默认构造函数!

            因此,有了上述概念,我们重新观察日期类:

            class Date
            {private:
            	int _year;
            	int _month;
            	int _day;
            public:
            	Date()
            	{_year = 1;
            		_month = 1;
            		_day = 1;
            	}
            	Date(int year=1,int month=1,int day=1)
            	{_year = year;
            		_month = month;
            		_day = day;
            	}
            	void Display();
            };
            

            前面提到过,当我们未显式定义构造函数的时候,此时编译器会给我们提供一个构造函数,这个构造函数也被称为默认构造函数,这种构造函数的函数体是空的,参数也没有;所以用户在创建对象的时候如果需要数据成员存在初值,就必须自己定义构造函数!

            所以上述代码中两个构造函数都属于默认构造函数!

            而一个类中只能存在一个构造函数!

            2.析构函数

            2.1析构函数的引入

            析构函数:是一个特殊的成员函数,他的作用与构造函数相反,它的命名前需要加上“~”,也是没有返回值与类型的!

            析构函数的主要作用并不是删除对象,而是在撤销对象占用的内存之前完成的一些清理工作,也就是说当对象要被销毁时或者生命周期结束时会自动调用析构函数!析构函数在一定程度上与构造函数一致,同样是没有函数类型、没有返回值,并且最重要的是没有函数参数的!

            class Test
            {private:
            	int* arr;
            	int num;
            public:
            	Test(int n);
            	void Display(int n);
            	~Test();
            };
            Test::Test(int n)
            {arr = (int*)calloc(n,sizeof(int));
            	if (arr == NULL)
            	{perror("malloc fail");
            		return;
            	}
            	num = 100;
            }
            Test::~Test()
            {free(arr);
            	num = 0;
            }
            void Test::Display(int n)
            {for (int i = 0; i < n; i++)
            	{cout << arr[i] << "  ";
            	}
            	cout << endl;
            }
            int main()
            {Test t1(10);
            	t1.Display(10);
            }
            

            测试以上代码,可以得出以下结论:

            1. 由于析构函数没有参数所以不能重载只能存在1个
            2. 析构函数在没有显式定义的时候,编译器会自动生成1个析构函数,但它只是虚有其表只是有虚构函数的名称与形式,实际上什么操作都不会进行!
            3. 自定义类型会调用它的析构函数
            4. 当然,有些类是需要析构函数的,但是有些类是不需要析构函数的,所以当存在资源申请的时候就需要写析构函数,否则会造成资源泄漏!

            2.2析构函数的调用情况

            1. 如果一个函数定义了一个对象(局部对象),当这个函数调用结束时,对象应该被释放,在对象释放前会自动执行析构函数!
            2. 静态(static)局部对象在函数调用结束对象并不会被释放,当main函数结束或者调用exit函数结束程序时,会调用析构函数!
            3. 如果定义了一个全局的变量,则在程序的流程离开其作用域时,调用该全局对象的析构函数!
            4. 当用new建立对象时,当用delete运算符释放该对象时先调用析构函数!

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

            3.1构造函数与析构函数特性补充

            1. 构造函数中对于内置类型是不做处理的,对于自定义类型会调用其构造函数!
            2. 对于析构函数对于内置类型在销毁时系统会自动回收内存,而对于自定义类型会调用其析构函数!

            3.2二者调用顺序

            先构造的后析构,后构造的先析构

            class Time
            {public:
            	Time()
            	{cout << "Time()" << endl;
            	}
            	~Time()
            	{cout << "~Time()" << endl;
            		_hour = 0;
            		_minute = 0;
            		_second = 0;
            	}
            private:
            	int _hour;
            	int _minute;
            	int _second;
            };
            class Date
            {private:
            	// 基本类型(内置类型)
            	int _year = 1970;
            	int _month = 1;
            	int _day = 1;
            	// 自定义类型
            	Time _t;
            };
            int main()
            {Date d1;
            	Date d2;
            	return 0;
            }
            

            调试这个程序就可以很轻易的看出这一特性!而这一特性也完全与栈的原理一样!

            注:

            • 如果在全局范围定义对象,那么它的构造函数在本文件模块中的所有函数执行之前调用,如果一个程序包含多个文件,那么它的构造函数的调用顺序是不确定的,当main函数结束或者调用exit结束程序时,调用析构函数!
            • 如果定义局部对象,那么定义时自动调用构造函数,函数调用结束时,调用析构函数!
            • 如果定义静态函数,则只在程序调用的第一次调用构造函数,当main函数结束时或者调用exit函数时调用析构函数!

              Ending

              关于构造函数和析构函数也就结束了,而构造函数中还有一种特殊的构造函数,叫做拷贝构造函数,这个在下一篇博客中会介绍!🙋‍♂️