只要生成一个类 ,那么这个类中就会默认创建6个成员函数。对于一个空类,看似类中什么都没有,但系统也会给它创建出来这6个成员函数。
回顾前边我们函数的使用
例如一个日期类:
可以看出,我们在定义类对象的时候都是先进行对象的定义,然后在进行初始化函数的调用操作,若同时定义多个对象,每一次定义对象之后都调用初始化函数会比较麻烦,因此我们想要在创建对象的时候同时进行一个初始化的赋值操作。
例如,我们在 C 中对变量的定义:
int main()
{
//先定义后赋值
int a;
a = 10;
//在定义同时初始化
int b = 10;
int c(10);
return 0;
}
为了解决在定义类对象的同时进行对象的一个初始化操作,我们引出了构造函数的概念:
构造函数是一个特殊的成员函数,名字与类名相同,它是在创建类类型对象时由编译器来自动进行调用的,从而保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只被调用一次。
(1)函数名与类名相同
例如,一个日期类中:
class Date {
public:
//创建构造函数
Date(int year, int month, int day) //带参构造
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
(2)无返回值
(3)在创建对象时由编译器自动调用
也可以查看反汇编:
(4)构造函数可以重载
在定义类对象之后,会自动调用相应的构造函数:
class Date {
public:
//创建构造函数
Date(int year, int month, int day){
_year = year;
_month = month;
_day = day;
cout << "Date(int, int, int)" << this << endl;
}
//无参拷贝构造
Date(){
cout << "Date()" << endl;
}
void Print(){
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用带参构造
Date d1(2022, 11, 15);
//调用无参构造
Date d2;
return 0;
}
我们可以打印看看结果:
(5)若类中没有显式定义构造函数(用户没定义),则 C++ 编译器会自动生成一个默认的无参构造函数,一旦用户显式定义了则编译器就不会生成
class Date {
public:
//当前代码中并没有定义构造函数
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
由于当前定义的 Date 类中对象成员变量都是 int 类型,编译器会判断有没有必要进行创建构造函数,此时没有资源进行管理,因此并没有创建出来相应的构造函数,因此在汇编代码中看到没有调用函数的 call 指令:
但若我们在 Date 类中添加一个自定义类型的对象,并且该自定义类型对象是具有显式定义的构造函数存在:
class Time {
public:
Time(int hour = 1,int minute = 1,int second = 1) //带参构造
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
public:
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; //自定义类型的变量
};
此时在进行定义 Date 类对象时,调用了 Date 默认的构造函数(由系统自动生成的),这是由于 Date 类此时存在一个自定义类型 Time 类的对象,而 Time 类中包含有相应的全缺省构造,因此编译器在创建 Date 类对象会生成一个默认的无参构造函数,从而在默认构造函数内来调用 Time 类的构造函数。
(6)无参构造、全缺省构造----都称为默认构造函数,两者只能存在一个
class Date {
public:
//全缺省构造
Date(int year = 2022, int month = 11, int day = 15)
{
_year = year;
_month = month;
_day = day;
}
Date() //无参构造
{}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
(7)编译器自动生成的默认构造函数在创建对象时调用,对象初始化值依旧为随即值,则默认的无参构造有什么用??
编译器自动生成的默认构造函数在创建对象时调用,对象初始化值依旧为随即值,但并不是说默认构造函数没用。
(1)假如我们在定义成员变量时就赋予了初始值:
此时调用的是系统自动生成的无参构造
(2)又如上边我们所举的例子,若在 Date 类中添加 Time 类的成员对象作为成员变量,则在编译时会自动生成默认的无参 Date 构造函数,因为 Time 类的构造函数是通过 Date 类的构造来进行调用的。
我们在C语言阶段所学习到的栈、队列等的基本操作中,在定义栈结构时候会进行栈的初始化操作,并且在调用结束之后也会释放相应的栈空间,否则容易造成资源的泄露。
析构函数与构造函数的功能相反,但析构函数并不是完成对象的销毁,局部对象的销毁工作是由编译器自动来完成的。析构函数是在对象调用结束时自动由编译器来进行调用,完成类对象的一些资源清理工作。
(1)析构函数名是在类名前加~
class Date {
public:
Date(int year, int month, int day) //带参构造
{
_year = year;
_month = month;
_day = day;
}
~Date() //对于当前的日期类来说,并没有资源需要进行清理
{}
private:
int _year;
int _month;
int _day;
};
(2)无参数无返回值
在日期类中的析构函数:
~Date()
{}
(3)一个类有且仅有一个析构函数
(4)对象生命周期结束时,C++编译器会自动调用析构函数
假如我们给定一个栈结构:
class Stack {
public:
Stack() { //无参构造
_arr = (int*)malloc(sizeof(int) * 10);
_capacity = 10;
_top = 0;
}
void Push(int data) {
//checkcapacity(); //不考虑扩容
_arr[_top++] = data;
}
void Pop() {
_top--; //不考虑空
}
int Size() {
return _top;
}
int Top() {
return _arr[_top - 1];
}
bool Empty(){
return 0 == _top;
}
~Stack()
{
if (_arr) {
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}
}
private:
int* _arr;
int _capacity;
int _top;
};
void TestStack()
{
Stack s;
}
int main()
{
TestStack();
return 0;
}
(5)若用户未显式定义析构函数,则编译器会生成一个默认的析构函数
如果对象中没有涉及到任何资源管理时,该类的析构函数可以不用给出(因此在用户没有定义析构函数时,系统判断没有资源需要进行释放,因此不会生成默认的析构函数)
class Date {
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Show()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022, 11, 15);
d1.Show();
}
int main()
{
Test(); //生命周期结束不会调用析构
return 0;
}
但若当前程序存在需要释放的类资源时,则会调用相应的析构函数(系统默认生成)来进行资源清理:
class Stack {
public:
Stack() { //无参构造
_arr = (int*)malloc(sizeof(int) * 10);
_capacity = 10;
_top = 0;
}
void Push(int data) {
//checkcapacity();
_arr[_top++] = data;
}
void Pop() {
_top--;
}
int Size() {
return _top;
}
int Top() {
return _arr[_top - 1];
}
bool Empty(){
return 0 == _top;
}
~Stack()
{
if (_arr) {
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}
}
private:
int* _arr;
int _capacity;
int _top;
};
class MyQueue {
//模拟栈实现队列
public:
MyQueue() {}
void Push() {}
void Pop() {}
int Front() {}
int Back() {}
bool Empty() {}
private:
//需要借助两个栈结构
Stack s1;
Stack s2;
};
void TestStack()
{
MyQueue q;
}
int main()
{
TestStack();
return 0;
}
由于此时创建了两个栈结构,因此在生命周期结束之后会调用两次 ~Stack
(6)析构函数的析构顺序与构造顺序相反---------先构造的后析构(释放)
先进行构造的对象会先进行析构。
class Stack {
public:
Stack() { //无参构造
_arr = (int*)malloc(sizeof(int) * 10);
_capacity = 10;
_top = 0;
cout << "构造 Stack()" << this << endl;
}
void Push(int data) {
//checkcapacity();
_arr[_top++] = data;
}
void Pop() {
_top--;
}
int Size() {
return _top;
}
int Top() {
return _arr[_top - 1];
}
bool Empty() {
return 0 == _top;
}
~Stack()
{
if (_arr) {
free(_arr);
_arr = nullptr;
_capacity = _top = 0;
}
cout << "析构 ~Stack() " << this << endl;
}
private:
int* _arr;
int _capacity;
int _top;
};
void Test()
{
Stack s1;
Stack s2;
}
int main()
{
Test();
return 0;
}
练习题********
在创建一个对象时候能否创建一个一样的对象出来呢?
在 C 语言阶段,我们可以用一个已有的对象来创建一个一样的新对象出来
因此,我们思考在创建一个类对象时,能否采用相同的方法来实现新对象的创建?
可以看出来,在创建类对象时候也是可以通过已有对象来实现新对象的创建,但它的底层是如何实现的呢?因此引入拷贝构造函数的基本概念。
拷贝构造函数:
用一个已有的对象来创建一个新的对象,并且新对象的内容与已有对象内容相同
(1)拷贝构造函数是构造函数的一个重载形式
拷贝构造函数名与类名相同,没有返回值,且它的参数只有一个(用已有对象创建新对象):
class Date {
public:
Date(int year, int month, int day) //构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date()
{}
private:
int _year;
int _month;
int _day;
};
拷贝构造函数名与构造函数名相同,且两者都没有返回值类型,函数参数列表不同,因此构成了函数的重载。
(2)拷贝构造函数的参数只有一个并且必须使用引用参数,因为使用传值方式会造成无穷递归调用
Date(const Date& d) //拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
拷贝构造实现的是用一个已有的对象来创建一个新的对象,因此原对象在函数内是不能够进行修改的,故采用 const 进行修饰。
引用传参一方面是效率更高,另一方面是为了防止传值造成的无穷递归调用的产生
由于引用相当于是一个变量的别名,代表的依旧是原对象,因此通过传引用会大大提升函数运行效率,同时避免了无穷递归调用。
(3)若未显式定义拷贝构造函数,则编译器会自动生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储字节完成拷贝---------浅拷贝(值拷贝)
但是编译器在使用已有对象创建新对象的同时会考虑是否有必要生成拷贝构造函数,若不存在资源拷贝过程,编译器就不会生成默认的拷贝构造函数,但依旧会完成了值拷贝的过程。
1)Date 类中显式定义拷贝构造函数时,编译器会自动调用拷贝构造函数来实现:
我们可以通过反汇编来查看:
2)Date 类中用户没有显式定义构造函数时,由于 Date 类中成员变量类型都是 int ,不存在资源的拷贝,因此编译器认为没有必要生成一份默认的拷贝构造函数,但是依旧会完成内容的拷贝
(4)既然在用户没有显式定义拷贝构造函数时候,编译器会默认生成一个拷贝构造函数,那么我们是否还需要显式来定义??
对于编译器默认生成的拷贝构造函数,它只是实现的内容的拷贝,也就是浅拷贝-----将原对象的内容原封不动的拷贝给新对象,但是这种拷贝就一定完全正确吗?
当我们的类中存在资源内容时,来看看拷贝构造函数的实现是否合适:
class String {
public:
String(const char *str = "hello") //带参构造函数
{
_str = (char*)malloc(sizeof(char)*(strlen(str) + 1));
strcpy(_str, str);
}
~String()
{ //此时存在资源需要进行清理
if (_str) {
free(_str);
_str = nullptr;
}
}
private:
char* _str;
};
int main()
{
String s1("hello world!\n"); //调用构造函数
String s2(s1); //会调用默认生成的拷贝构造函数
return 0;
}
可以看出此时编译器确实生成了一个默认的拷贝构造函数,并且实现了内容的拷贝,但是在运行最后却发生了报错,这是为什么?
我们可以查看监视窗口来寻找问题所在:
由此此时 String 类中存在资源需要清理,故在调用结束之后会自动调用析构函数,先构造的后析构,因此先析构 s2 ,后析构 s1 发生报错,这是为什么?
解答:
在通过 s1 对象来创建 s2 对象的时候,调用的是编译器默认生成的拷贝构造函数 ,确实完成了相应的内容拷贝过程,但是我们发现创建出来的两个对象的地址居然是一样的,这就导致了在程序运行结束时候析构函数进行析构的时候出现问题(两次析构清理的是同一份空间)----------------------- 由此可见,编译器默认生成的拷贝构造函数仅仅是浅拷贝,只是将原对象的内容原封不动的拷贝给新对象,这是不可取的,因此我们应该显式的定义拷贝构造函数来完成拷贝工作。
回归 C 语言当中,我们在定义变量时候:
那么在创建类对象时候,能够采用相同的思路来进行?
class Date {
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 11, 27);
//下列创建是否正确?
Date d2(d1);
Date d3 = d1;
Date d4;
d4 = d1;
return 0;
}
运行程序发生了报错这是为什么?
在前边我们谈到了拷贝构造函数,它是用已有的对象来创建新对象,因此此处的 d2,d3 对象的创建显然是调用拷贝构造函数的
而 d4 对象是先进行的创建,然后再通过 d1 对象来进行赋值操作,程序报错是因为此时的对象并不简简单单是某一中数据类型(int 、char、double…),此时的对象是类类型的对象,因此需要进行赋值运算符的重载操作才能够进程赋值。
C++中为了提升代码的可读性引入了运算符重载。
运算符重载是具有特殊函数名的函数,具有返回值类型,函数名以及参数列表,其返回值类型与参数列表与普通函数类似。
返回值类型 operator操作符(参数列表)
赋值运算符的重载:
Date& operator=(const Date& d)
{
if (&d != this) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
注意
(1)返回值类型为 Date&
在进行赋值操作时,有可能存在连续赋值的情况存在,因此返回值类型应该与对象类型相同为 Date ,采用 &引用减少的值拷贝
若返回值类型为 void ,则在进行连续赋值时会报错,因为首先完成了 d2=d1,d1给 d2 的赋值,由于没有返回值,因此 d3 的赋值无法正常进行:
(2)参数列表只有一个,且为 const
因为此时运算符重载函数是位于类内部,属于类内成员函数,而成员函数具有一个默认的参数 *this 作为第一个参数,实际参数 Date& d 为第二个参数,由于调用赋值运算符重载函数是用第二个参数给第一个参数*this进行赋值,所以此处的 d 是不应该被修改的,因此我们采用 const 来进行修饰
(3)&d!=this
*this 指向的是当前操作的对象,为了防止产生自己给自己赋值的情况,因此我们进行一个检测
(4)return *this
返回值应该返回的是被赋值的对象,此时是 第二个参数 d 给 第一个默认的参数*this 进行赋值操作,因此返回的是 *this
(1)不能通过链接其他符号来创建新的操作符:operator@
(2)重载操作符必须有一个类类型或是枚举类型的操作数
(3)用于内置类型的操作符,其含义是不能进行改变的
例如,内置类型的算数 + ,表示加法操作,在进行重载时不能改变其加法含义
(4)用作类成员的重载函数时,其形参看起来比操作数数目少一(因为类中成员函数具有一个默认的 *this 指针,指向当前操作的对象)
(5)不能进行重载的运算符: .* 、 :: 、sizeof、 ?:(三目运算)、.
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
//等价表示
return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
Date& operator=(const Date& d) //存在默认的第一个参数 *this
{
if (&d != this) { //防止自己给自己赋值
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
前置++
int a=0;
int b=++a; // a 先进行自增,然后将修改后的值赋给 b
Date d1(2022, 11, 27);
Date d2 = ++d1; //d1 先进行自增,后将修改后的值赋给 d2
在调用运算符重载函数时,返回的是自增之后的值:
Date& operator++() //前置++
{
_day += 1;
return *this;
}
注意
(1)返回值 Date&
由于返回的是自增之后的值,因此直接返回修改之后的 *this,采用 & 减少了值拷贝过程
(2)参数列表为空
由于前置++,是在原对象的基础上进行自增并反正自增之后的结果,从始至终仅对一个对象的内容进行操作,因此不需要传递参数,直接操作默认的第一个参数 *this 即可
(3)return *this
操作的是 *this ,返回的也是被操作的对象 *this
前置–
原理同前置++
后置++
int a=0;
int ret = a++; //前置++,表示先使用 a 的值,后对 a 自增
Date d1(2022, 11, 27);
Date d2 = d1++; //会先将 d1 的内容赋值给 d2,然后 d1 自增
因此在调用运算符重载函数时,返回的是自增之前的值:
Date operator++(int) //后置++ d1++
{
Date tmp(*this); //保存修改之前的值
_day += 1;
return tmp; //由于 tmp 是一个临时变量,因此返回值应该为值类型-------具有一次值拷贝过程
}
注意
(1)返回值类型
由于返回的 tmp 是一个临时变量,因此返回值应该为值返回,具有一次值拷贝过程
(2)参数 int
由于前置++ 与后置++ 所对应的运算符重载函数只有返回值类型不同,不能构成函数的重载,因此给后置 ++ 添加一个虚参数 int ,使它能够与前置++形成重载
后置–
原理同后置++
类中成员函数的第一个默认参数 *this 的类型为 const 类类型 * this,表明 *this
所指向的内容不能进行修改
//const 成员函数,表明成员变量不能被修改,const 修饰成员函数 实际上是在修饰 this指针
void Print()const
//此时 this 指针的类型 : const Date* const 指向和内容都不能被修改
{
cout << _year << "/" << _month << "/" << _day << endl;
}
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date()
{}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date* operator&() //取地址运算符重载:普通变量的取地址
{
return this; //this 指针中保存的是当前变量的地址,因此返回值用 * 来接收
}
const Date* operator&()const //取地址运算符重载:const 变量的取地址,表明指向和值都不能修改
{
return this; //this 指针中保存的是当前变量的地址
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2022, 11, 20);
Date*p = &d; //调用普通类型的取地址函数
const Date d1(2022, 11, 21); //const 类型表明值内容不能进行修改
const Date* p1 = &d1; //调用 const 变量的取地址函数
return 0;
}
注意
//const 对象不能调用非 const 成员函数-----------防止函数内部修改数据内容
//非 const 对象可以调用 const 成员函数
//const 成员函数内部不可以调用其他的非 const 成员函数
//非 const 成员函数内可以调用其他的 const 成员函数
ps:
欢迎评论思路哦~~