目录
接上一篇C++类与对象(1)—初步认识
一、类的6个默认成员函数
如果一个类中没有定义任何成员,我们可以称之为空类。
然而,实际上空类并不是真的什么都没有。当我们没有在类中显式定义任何成员时,编译器会自动生成以下6个默认成员函数:
这些默认成员函数在需要时会被编译器自动生成,以提供类的基本功能。尽管我们没有显式定义任何成员,但这些默认成员函数确保了类的完整性和正确性。
本次主要讲解前两个默认成员函数。
二、构造函数
之前学习数据结构中的 “栈” 时,对其进行初始化的函数如下:
class Stack
{
public:
void Init(int n)
{
a = (int*)malloc(sizeof(int) * n);
if (a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_size = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
对于Stack类,可以通过 Init 公有方法给对象进行初始化,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
这时,构造函数登场。
1、定义
- 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
- 构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
2、特征
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。—多种构造方式
下面通过例子一一解释 :
构造函数可以重载。(本质可以写多个构造函数,提供多种初始化方式)
class Stack
{
public:
// 1.无参构造函数
Stack()
{
_a = nullptr;
_size = _capacity = 0;
}
// 2.带参构造函数
Stack(int n)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_size = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
两个Stack函数都为构造函数,有无参数均可以,他们共同的特点是函数名与类名相等,无返回值,其余部分跟正常函数一样。
接下来看如何在主函数调用构造函数初始化对象。
当我们声明类的对象后,程序运行到这段代码后,编译器会自动调用无参数的构造函数进行初始化。
int main()
{
Stack st;
return 0;
}
在下图中,我们对Stack st;打断点,在调试中可以看到按F11之后直接跳转到无参数的Stack()进行初始化。
也可以选择选择带参数的构造函数进行初始化,只需在后面加上参数即可。
Stack st(4);
我们再看一个日期类:
class Date
{
public:
//Date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
//
//Date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//缺省构造函数代替上述函数重载
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 2, 3);
d1.Print();
d2.Print();
return 0;
}
除了上述的两种构造函数,我们还可以结合缺省参数使其更完善,只需这一个构造函数就可满足各种情况,一般来说很多时候都喜欢使用全缺省或半缺省的构造函数。
注意:这种缺省构造函数不能与无参的构造函数同时存在,编译能通过但有警告,会产生歧义,程序不知道调用哪个。
三、析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
1、定义
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
2、特征
其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
下面代码中的~Stack()即为析构函数:
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = nullptr;
_size = _capacity = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st;
//Stack st(4);
//st.Destroy();由析构函数代替
return 0;
}
我们使用调试看一下程序如何调用 ~Stack()函数:
在return 0;位置打断点,按F11可以看到直接进入~Stack()函数
程序直接跳转~Stack()函数
四、默认生成构造&析构
1、定义
如果类中没有显式定义构造或析构函数,则C++编译器会自动生成一个无参的默认构造或析构函数,一旦用户显式定义编译器将不再生成。同理析构函数也一样。
2、内置类型
我们来看下面的例子:
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year ;
int _month ;
int _day ;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
通过结果可知,实际上并没有对成员变量自动进行初始化。
如果我们加上自己定义的构造函数呢?
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
结果可以正常初始化。
关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?对象调用了编译器生成的默认构造函数,但是d1对象_year/_month/_day,依旧是随机值。
也就说在这里编译器生成的默认构造函数并没有什么用??
- C++规定:默认生成构造和析构函数对内置类型成员不做处理,内置类型就是语言提供的数据类型,如:int/char...。
- 对自定义类型的成员,会去调用它的默认构造(不用传参数的构造),自定义类型就是我们使用class/struct/union等自己定义的类型。
3、自定义类型
下面是用栈实现队列的程序,使用两个栈模拟实现队列,编译器生成默认的构造函数会对自定类型成员MyQueue调用的它的默认成员函数。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = nullptr;
_size = _capacity = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class MyQueue {
public:
void push(int x) {}
//....
Stack _pushST;
Stack _popST;
};
int main()
{
MyQueue q;
return 0;
}
输出结果如下:(使用两个栈模拟实现队列,构造和析构函数各调用两次)
我们通过下面代码看一下自定义类型的默认析构函数:
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;
return 0;
}
程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:
- main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
- 而 _t 是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
- 但是,main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
- main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。
- 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
结论:
- 默认生成构造函数,对自定义类型成员,会调用他的默认构造函数。
- 默认生成析构函数,对自定义类型成员,会调用他的析构函数。
4、声明处给默认值
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
如下图所示:
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
成功初始化:
我们对MyQueue函数中新增一个整型变量size。
class MyQueue {
public:
void push(int x) {}
//....
Stack _pushST;
Stack _popST;
int _size;
};
int main()
{
MyQueue q;
return 0;
}
在调试中可以看到size未初始化,
这时可以在声明处给默认值0。
int _size = 0;
可以看到size变量成功初始化。
5、总结
- 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数,并且默认构造函数只能有一个。
- 一般情况下,都需要我们自己写构造函数,决定初始化方式;成员变量全是自定义类型,可以考虑不写构造函数。
五、构造析构顺序
构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构。
在函数F中,本地变量a和b的构造函数(constructor)和析构函数(destructor)的调用顺序是: a构造 b构造 b析构 a析构
Class A;
Class B;
void F() {
A a;
B b;
}
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
- 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
- 全局对象先于局部对象进行构造
- 局部对象按照出现的顺序进行构造,无论是否为static
- 所以构造的顺序为 c a b d
- 析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构
- 因此析构顺序为:B A D C