一. 类的6个默认成员函数
在我们前面学习的类中,我们会定义成员变量和成员函数,这些我们自己定义的函数都是普通的成员函数,但是如若我们定义的类里什么也没有呢?是真的里面啥也没吗?如下
class Date {};
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数。
注意:这里的“默认”和“缺省”的意思差不多,也就是你不写这6个函数,编译器会自动生成,你若是写了,则编译器就不生成了。
1.1 为什么会出现默认成员函数
我们在日常写代码和刷题的时候,肯定会有过忘记初始化,或者忘记销毁造成了内存的泄漏,这些细节很容易被大家忽略,而C++祖师爷
忘记写初始化:输出随机值,结果会错误,且编译不通过
忘记写销毁:时间久了便会造成内存泄漏,如果是一次大型的泄露还容易被察觉,但是小型的泄露可能会导致后期出现大问题
二 .构造函数
2.1 构造函数的概念
如下的日期类:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "今日日期输出:" << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024,4,14);
d1.Print();
return 0;
}
2.2 构造函数的特性
一. 函数名与类名相同
二. 无返回值,也不需要写void
如下即为构造函数:
class Date
{
Date(int year = 0, int month = 1, int day = 1)// 全缺省的构造函数
{
_year = year;
_month = month;
_day = day;
}
}
三. 对象实例化时编译器自动调用对应的构造函数
当我们在实例化一个对象后,它会自动调用这个构造函数,就自动完成了初始化
四. 构造函数可以重载
这意味着你可以有多种初始化对象的方式,编译器会根据你所传递的参数去调用对应的构造函数。
class Date
{
Date()
{
_year = 1;
_month = 0;
_day = 0;
}
Date(int year, int month, int day) //构造函数的重载
{
_year = year;
_month = month;
_day = day;
}
}
int main()
{
Date s1;
Date s2(2024, 4, 14); //参数写在实例化对象后面进行传参
return 0;
}
注意:
- 没有参数时在调用的时候不能加上括号(),切忌!!构造函数尤为特殊
- 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
- 构造函数的重载我们推荐写成全缺省的样子:
//普通的构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
普通的构造函数和全缺省的构造函数在不调用的情况下可以同时存在,编译也没有错误。但是在实际调用的过程中,会存在歧义。如下的调用:
由此可见:它们俩在语法上可以同时存在,但是使用上不能同时存在,因为会存在调用的歧义,不知道调用的是谁,所以一般情况下,更推荐直接写个全缺省版的构造函数,因为是否传参数可由你决定。传参数数量也是由你决定。
五. 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成
但是不是说好我不自己写构造函数,编译器会默认生成吗?为什么到这又是随机值了?这个随机值算是初始化吗?我们先来理解一下什么是默认构造函数
默认构造函数:
- 我们不写,编译器自动生成的构造函数。
- 无参构造函数也可以叫默认构造函数
- 全缺省的也可以叫默认构造函数
总结:可以不传参数就调用构造,都可以叫默认构造
所以上述的随机值算是初始化,也是自动的调用了第一种类型(编译器自动生成的构造函数也属于默认构造函数)默认构造函数。
C++把变量分成两种:
- 内置类型/基本类型:int、char、double、指针……
- 自定义类型:class、struct去定义的类型对象
C++默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型的成员变量才会处理,这也就能很好的说明了为什么刚才没有对年月日进行处理(初始化),因为它们是内置类型(int类型的变量)
那么自定义类型的变量编译器该如何进行处理呢?
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
首先,这是一个名为Time的类,有成员变量_hour,_minute,_second,并且还有一个无参的构造函数Time,对_hour,_minute,_second均初始化为0。接着:
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << endl;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
通过运行结果以及调试,也正验证了默认构造函数对自定义类型才会处理。这也就告诉我们,当出现内置类型时,就需要我们自己写构造函数了。
如下情况的时候使用默认构造函数会更凸显出其价值
class Stack
{
public:
Stack()
{
_a = nullptr;
_top = _capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
//默认生成的构造函数就可以用了
void push(int x)
{}
int pop()
{}
private:
Stack _S1;
Stack _s2;
};
此时我队列里自定义类型_s1和_s2就不需要单独写初始化了,直接用默认的。但是如果栈里没有写构造函数,那么其输出的还是随机的,因为栈里的也是内置类型。就是一层套一层,下一层生效的前提是上一层地基打稳了。
总结:
- 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
- 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。
三. 析构函数
3.1 析构函数概念
3.2 析构函数特性
1. 析构函数的函数名是在类名前加上字符‘~’
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,自动调用析构函数
~Date()
{
cout << "~Date()" << endl;
}
带入示例再看看:
class Date
{
public:
Date(int year = 2024, int month = 4, int day = 14)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
图解
首先,我实例化出的d1会调用它的默认构造函数进行初始化,其次调用Print函数进行打印,出了作用域后又调用其析构函数,这也就是为什么输出结果会是~Date()
我们知道当一个类对象销毁时,其中的局部变量也会随着该对象的销毁而销毁,例如,我们用日期类创建了一个对象d1,当d1被销毁时,对象d1当中的局部变量_year/_month/_day也会被编译器销毁。
但是这并不意味着析构函数没有什么意义。像栈(Stack)这样的类对象,malloc、new、fopen出的空间,当该对象被销毁时,其中动态开辟的栈并不会随之被销毁,需要我们对其进行空间释放,这时析构函数的意义就体现了。
5. 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
// 程序运行结束后输出:~Time()
return 0;
}
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
int main()
{
Stack st1;
Stack st2;
}
答案:st2先析构,st1后析构
解析:这里st1和st2是在栈上的,建立栈帧,其性质和之前一样,后进先出,st2后压栈,那么它肯定是最先析构的。所以栈里面定义对象,析构顺序和构造顺序是反的。
若自己没有定义析构函数,虽说系统会自动生成默认析构函数,不过也是有要求的,和构造函数一样,内置类型不处理,自定义类型会去调用它的析构函数,如下: