本文主要是介绍类和对象(中)【类的6个默认成员函数】 【零散知识点】 (万字),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
类和对象(中)
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2.构造函数
2.1概念
对于以下Date类:
# include<iostream>
using namespace std;class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this) 编译后{cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1,d2;d1.Init(2024, 5, 26);// d1.Init(&d1, 2024, 5, 26); // 编译后d2.Init(2077, 5, 26);// d2.Init(&d2, 2024, 5, 26);d1.Print(); // 2024-5-26 // d1.Print(&d1)d2.Print(); // 2077-5-26 // d2.Print(&d2);return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
-
函数名与类名相同。
-
无返回值。
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数可以重载。
我们来看例子:
# include<iostream>
using namespace std;class Date
{
public:// 构造函数——在对象构建时调用的函数,这个函数完成初始化工作// [要注意这个函数只完成实例对象的初始化,不参与对象的构造,那是编译器的工作]Date(int year, int month, int day) // 构造函数的名和类名相同{_year = year;_month = month;_day = day;}// 构造函数不要返回值// 构造函数也支持重载Date() // 无参构造函数{_year = 0;_month = 0;_day = 0;}// 这个函数按需使用void Init(int year, int month, int day)// void Init(Date* this, int year, int month, int day){_year = year; // 如果我们设置的成员变量名称不加_或者m 这里就不好理解_month = month;_day = day;}void Print() // void Print(Date* this) 编译后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 26); // 直接给实例对象传参,由自动调用的构造函数完成初始化。Date d2(2077, 5, 26);Date d3; // 通过函数重载实现,调用无参构造函数// 注意这里不能Date d3();d1.Print(); // 2024-5-26 // d1.Print(&d1);d2.Print(); // 2077-5-26 // d2.Print(&d2);// 是通过隐含的this指针实现的d3.Print(); // 0 - 0 - 0return 0;
}
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
// 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
# include<iostream>
using namespace std;class Time
{
public:Time(){_hour = 0;_min = 0;_second = 0;cout << "Time()" << endl;}private:int _hour;int _min;int _second;
};class Date
{
public:// 这里我们没有显示定义构造函数,编译器会生成无参默认构造函数// 一旦用户定义了显式构造函数。那么编译器将不会生成无参构造函数// 即使只定义了带参构造函数,编译器也不会生成无参构造函数// 想要无参构造函数,要自己再定义一个//Date(int year, int month, int day) //{// _year = year;// _month = month;// _day = day;//}void Init(int year, int month, int day){_year = year; _month = month;_day = day;}void Print() // void Print(Date* this) 编译后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;Time _t;
};int main()
{Date d1; // 调用编译器生成的无参默认构造函数,d1.Print();// -858993460--858993460--858993460// 我们发现,即使d1调用了编译器的默认构造函数,打印出来还是随机值,看起来这个构造函数什么都没做一样// 但是实际上,这个无参默认构造函数是有做事情的// 我们给Date一个自定义的成员变量,_t// d1.Print();的结果如下://Time()//-858993460--858993460--858993460// 这个说明了一个现象// 默认生成的无参构造函数(语法坑:双标)// 1. 针对内置类型的成员变量不会做处理// 2. 针对自定义类型的成员变量,调用它的构造函数初始化return 0;
}
- 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
总结:
默认生成的无参构造函数(语法坑:双标)
-
针对内置类型的成员变量不会做处理
-
针对自定义类型的成员变量,调用它的构造函数初始化
-
一旦用户定义了显式构造函数。那么编译器将不会生成无参构造函数
即使只定义了带参构造函数,编译器也不会生成无参构造函数
想要无参构造函数,要自己再定义一个
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
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 = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}
但是这样也不够好,我们可以用之前学习的缺省参数——全缺省参数
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
# include<iostream>
using namespace std;
class Date
{
public: 带参构造函数//Date(int year, int month, int day) //{// _year = year;// _month = month;// _day = day;//}// 构造函数不能有返回值 构造函数也支持重载//Date() // 无参构造函数//{// _year = 0;// _month = 0;// _day = 0;//}// 更好的方式// 构造函数——全缺省Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 注意,有了全缺省的构造函数,不能再有无参构造函数,编译的时候会产生歧义//Date() // 无参构造函数//{// _year = 0;// _month = 0;// _day = 0;//}void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this) 编译后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1; // 调用默认构造函数// 1. 自己实现的无参构造函数// 2. 自己实现的全缺省构造函数// 3. 编译器自动生成的默认构造函数// 这三个的特点都是无参,不传参数// 因此这三个只能存在一个Date d2(2024, 5, 27);d1.Print();// 0-0-0d2.Print();// 2024-5-27return 0;
}
总结:
默认构造函数一共有三种:
- 自己实现的无参构造函数
- 自己实现的全缺省构造函数
- 编译器自动生成的默认构造函数
这三种默认构造函数只能存在一种
3.析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值类型。
-
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
-
对象生命周期结束时,C++编译系统系统自动调用析构函数
来看一段代码:
// 析构函数——在实例对象的生命周期结束的时候,会自动调用析构函数
# include<iostream>
using namespace std;
class Date
{
public:// 构造函数——全缺省 [一种默认构造函数]Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this) 编译后{cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "析构函数" << endl;}private:int _year;int _month;int _day;};int main()
{// 析构函数,会在这两个对象生命周期结束之后自动调用,完成清理工作,不是完成对其的销毁Date d1; // 调用默认构造函数Date d2(2024, 5, 27);// 执行代码,会执行两次析构函数//析构函数//析构函数//注意这个析构,先析构d2 在析构d1,因为是在栈区的变量return 0;
}
在上述代码中要注意:析构,先析构d2 在析构d1,因为是在栈区的变量
而有了析构函数和构造函数的存在,我们来感受一下它们的使用。
这里我们来写一个Stack类,这个Stack类,不在需要初始化和销毁的接口,由构造函数和析构函数代替了。
// 实现Stack类——使用构造和析构函数
# include<iostream>
using namespace std;class Stack
{
public:// 构造函数Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl; _size = 0;_capacity = n;}// ....... 栈的接口// 析构函数~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{Stack s1; Stack s2; // 注意先析构s2 在析构s1。因为两个局部变量在栈区上 ,后进先出//malloc:000001FC0C62BEF0//malloc : 000001FC0C636160//free : 000001FC0C636160//free : 000001FC0C62BEF0return 0;
}
- 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
# include<iostream>
using namespace std;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类生成的默认析构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
总结:
默认生成的析构函数(语法坑:双标)
-
针对内置类型的成员变量不会做处理
-
针对自定义类型的成员变量,调用它的析构函数
-
一旦用户定义了显式析构函数。那么编译器将不会生成默认析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
4.拷贝构造函数
4.1概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
4.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须是类 类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
我们来看一个例子:
// 拷贝构造函数
# include<iostream>
using namespace std;class Date
{
public:// 构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}// 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去Date(Date d) // 构造函数的重载函数{_year = d._year;_month = d._month;_day = d._day;}// 析构函数~Date(){cout << "析构函数" << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);// 如果我们想创建一个跟d1一模一样的对象,我们可以怎么做呢?//Date d2(2024, 5, 27);// 这样可以,但是肯定不好,因为d1一变,我们还要手动更改d2// 这个时候我们就可以用到拷贝构造Date d2(d1); // 传d1进去,// 这里会报错return 0;
}
为什么会造成无穷递归呢?
// 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去Date(Date d) // 构造函数的重载函数{_year = d._year;_month = d._month;_day = d._day;}
int main()
{// Date d2(d1);
}
因为我们这里是传值给拷贝构造函数,我们知道,传值调用的形参就是实参的一个临时拷贝,既然是拷贝,我们就要创建一个跟实参一个类型的值,那实参是什么类型的值呢,是Date,相当于要把实参d1 拷贝给 形参d,那d的构造又需要调用拷贝构造函数,那又要传参,又要拷贝实参,又要构造形参,又要调用拷贝构造函数。无限循环下去。
如图所示:
解决方法是什么呢?
其实就是让形参是实参的别名就好了。
// 拷贝构造函数
# include<iostream>
using namespace std;class Date
{
public:// 构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去//Date(Date d)//{// _year = d._year;// _month = d._month;// _day = d._day;//}// 我们使用别名来解决这个问题Date(const Date& d) // 传参的过程相当于 Date& d = d1;{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}// 析构函数~Date(){cout << "析构函数" << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);// 如果我们想创建一个跟d1一模一样的对象,我们可以怎么做呢?//Date d2(2024, 5, 27);// 这样可以,但是肯定不好,因为d1一变,我们还要手动更改d2// 这个时候我们就可以用到拷贝构造Date d2(d1); // 传d1进去Date d3 = d1; // 这个也是拷贝构造。d2.Print();d3.Print();return 0;
}
上面这个代码的运行结果
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2024;int _month = 5;int _day = 28;// 自定义类型Time _t;
};int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}
- 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 涉及内存资源管理的Stack的拷贝构造
# include<iostream>
using namespace std;class Stack
{
public:// 构造函数Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl;_size = 0;_capacity = n;}// ....... 栈的接口// 析构函数~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{// 浅拷贝问题Stack s1;Stack s2(s1);return 0;
}
上面我们的Stack类中,没有显式实现的拷贝构造,因此我们在main函数中的拷贝构造 的使用,用的都是编译器生成的默认的函数。这里用的是浅拷贝。因此造成了c++中比较经典的一个问题——浅拷贝问题
我们来分析一下代码为何会崩溃。
- 首先就是我们在调用拷贝构造的时候,我们是将s1对象一个一个字节拷贝到s2中的,这就意味着我们两个对象所存储的数组指针_a是一样的,也就是说指向的数组都是同一个空间的数组
- 这个时候我们的代码还不会崩溃,并且也确实成功完成了拷贝的任务
- 但是问题会出现在析构函数上,我们知道在析构函数中,我们要完成对栈这个类对象的资源清理工作,我们要在析构函数中释放掉数组的空间,由于s1 s2存储在栈区,我们先释放掉s2的数组空间,但是紧接着我们要释放掉s1的数组空间,但是s1和s2指向的数组是同一个数组,这就会造成会同一个空间的重复释放,这就会导致代码崩溃
因此对于这种涉及到内存资源管理的类,我们需要自己实现深拷贝的拷贝构造
但是这里有人会说,那我不自己实现析构函数不就不会报错了吗?
因为我们的析构函数中存在对数组空间的释放,但是不释放就会造成内存泄漏的问题,释放了就会造成浅拷贝问题。
- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d) // 调用拷贝构造函数
{Date temp(d);return temp; // 调用拷贝构造函数
}
int main()
{Date d1(2022, 1, 13); // 调用拷贝构造函数Test(d1);return 0;
}
5.赋值运算符重载
5.1 运算符重载
为什么要搞运算符重载呢?
其实很简单,就是因为内置类型,我们在使用运算符的时候。编译器知道怎么去比较,但是如果我们自己定义了一个类,在用内置的运算符,编译器就不知道怎么比较了,因此,为了能够方便的比较我们自己定义的类对象,我们需要对运算符重载
在之前我们是通过自定义函数,来解决这个问题的,但是这个解决方式也没有运算符重载好。因为可读性不是很好。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
-
不能通过连接其他符号来创建新的操作符:比如operator@
-
重载操作符必须有一个类类型参数
-
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针形参
-
.* :: sizeof ?: .
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。注意是 **.**不能重载,不是 (解引用)
我们来看一段代码:
// 运算符重载
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}Date(const Date& d) // 拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}//private: // 我们的成员变量不能是私有的,不然运算符重载函数内无法访问int _year;int _month;int _day;};// 运算符有几个操作数, operator重载的函数就有几个参数
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1(2024, 5, 27);Date d2(d1);// 如果我们相对Date类的 对象 d1 d2 进行比较// 我们可以通过定义函数实现//IsDateEqual(d1, d2); // 可读性不好// 为了让可读性更好,c++推出了运算符重载d1 == d2;// 编译过后会变成, operator==(d1, d2)// 如果不对== 进行重载,那么这里就会编译出错operator==(d1, d2); // 等价d1 == d2 ,但是不推荐这样写,这样可读性又下降了if (d1 == d2)cout << "相等" << endl;return 0;
}
上述的写法不好,因为我们为了实现运算符重载,我们牺牲了Date类中成员变量的私有性。把成员变量变成公有的,才能让运算符重载函数访问到其成员变量。
要如何解决这个问题呢?
这里其实可以用友元解决,这个后面学习
我们还可以通过把运算符重载函数写进类中来解决
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}Date(const Date& d) // 拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}// 运算符重载// 我们知道成员函数会自带this指针形参,因此这里要修改函数的形参个数。// 我们知道谁调用这个运算符重载函数,那么this指针就指向谁// d1 == d2;// d1.operator==(d2);bool operator==(const Date& d) // bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 > d2bool operator>(const Date& d){if (_year > d._year){return true;}else if (_year == d._year && _month > d._month){return true;}else if (_year == d._year && _month == d._month && _day > d._day){return true;}elsereturn false;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);Date d2(d1);d1 == d2; // 编译后变成 d1.operator==(d2)if (d1 > d2)cout << "大于" << endl;elsecout << "其他" << endl;return 0;
}
5.2赋值运算符重载
- 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
来看代码:
// 赋值运算符重载
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 运算符重载// d1 == d2 -> d1.operator==(d2);bool operator==(const Date& d) // bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// 赋值运算符重载//d2 = d1 -> d2.operator(&d2, d1)Date& operator=(const Date& d) // 其实这里不用引用传参也不会报错,但是使用引用传参效率更高// void operator(Date* this, const Date d){// 自己给自己赋值是没有意义的if (this == &d) // d是引用,d的地址和this相同就说明是自己给自己赋值return *this; _year = d._year;_month = d._month;_day = d._day;return *this;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);Date d2 = d1; //注意这里这个=不是赋值运算符,而是拷贝构造Date d3;d3 = d1;d3.Print();// 2024-5-27d3 = d3; // 自己赋值给自己//我们的=除了要实现单个的赋值,还要能实现连续的赋值d2 = d3 = d1;// 先执行d3 = d1,也就是d3.operator=(d1), 其返回值得是d3 才可以d2.Print();return 0;
}
在来看看拷贝构造和 赋值运算符重载的区别:
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}// 编译失败:
// error C2801: “operator =”必须是非静态成员
编译出错的原因是:
赋值运算符如果不显式实现,编译器会生成默认的赋值运算符重载。此时我们类外边在定义一个赋值运算符重载,就会和编译器的产生冲突。因此,赋值运算符重载只能是类的成员函数
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
// 如果没有显式实现 拷贝构造 和 赋值运算符重载,编译器会生成默认的
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 拷贝构造函数//Date(const Date& d)//{// _year = d._year;// _month = d._month;// _day = d._day;//} 赋值运算符重载d2 = d1 -> d2.operator(&d2, d1)//Date& operator=(const Date& d) // 其实这里不用引用传参也不会报错,但是使用引用传参效率更高// // void operator(Date* this, const Date d)//{// // 自己给自己赋值是没有意义的// if (this == &d) // d是引用,d的地址和this相同就说明是自己给自己赋值// return *this;// _year = d._year;// _month = d._month;// _day = d._day;// return *this;//}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 10);Date d2(2024, 5, 28);d1 = d2; // 即使我们类中屏蔽了赋值运算符重载,这里不会报错// 因为我们不实现的时候,编译器会生成拷贝构造和 operator= // 会完成按字节的值拷贝(浅拷贝)。// 也就是说有些类,比如这个日期类,我们不需要去实现拷贝构造和赋值运算符重载。// 但是涉及到 内存管理 的类 我们就需要去实现拷贝构造和赋值运算符重载d1.Print();// 2024-5-28d2.Print();// 2024-5-28Date d3(d1); // 屏蔽了拷贝构造函数,也不会报错,这里调用了编译器生成的默认拷贝构造函数Date d4 = d3;d3.Print();// 2024-5-28d4.Print();// 2024-5-28return 0;
}
- 值拷贝(浅拷贝):将对象按照一个字节一个字节的传过去。
注意: 只有六个成员函数我们不显式实现,编译器会自动生成。
拷贝构造函数和 赋值运算符函数就是其中之二,其他运算符编译器在没有显式实现的时候不会默认生成
但是这样就产生了一个问题:
- 我们还需要去自己实现拷贝构造函数和赋值运算符重载吗?编译器不是已经帮我们实现了吗?
答案当时是需要的,因为除了我们的日期类,还有许多类我们会涉及到**(内存的资源管理)**。
我们来看一段代码来感受一下:
// 涉及内存资源管理的Stack的拷贝构造和赋值运算符重载
# include<iostream>
using namespace std;class Stack
{
public:// 构造函数Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl;_size = 0;_capacity = n;}// ....... 栈的接口// 析构函数~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{// 浅拷贝问题Stack s1;Stack s3(30);s1 = s3;return 0;
}
上面我们的Stack类中,没有显式实现的赋值运算符重载,因此我们再main函数中的赋值运算符的使用,用的都是编译器生成的默认的函数。这里用的是浅拷贝。造成了浅拷贝问题
前面我们已经知道了拷贝构造函数造成浅拷贝的分析和原理了
对于赋值运算符也是同样的道理,我们将s3赋值给s1用的是浅拷贝,s1和s3的_a数组指针,指向的都是同一个数组空间,在调用析构函数的时候,也会造成对同一个空间的重复释放,代码崩溃.
我们来看图片来更好的理解一下:
因此对于这种涉及到内存资源管理的类,我们需要自己实现深拷贝的赋值运算符重载
但是这里有人会说,那我不自己实现析构函数不就不会报错了吗?
因为我们的析构函数中存在对数组空间的释放,但是不释放就会造成内存泄漏的问题,释放了就会造成浅拷贝问题。
5.3前置++和后置++的重载
直接来看代码:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2022, 1, 13);d = d1++; // d: 2022,1,13 d1:2022,1,14d = ++d1; // d: 2022,1,15 d1:2022,1,15return 0;
}
6.日期类的实现
实际上完整的完善的日期类,我们应该让声明和定义分离,并且还要优化代码。这里为了方便,我们就不让声明和定义分离了。
// 实现一个完善的日期类
# include<iostream>
using namespace std;class Date
{
public:int GetMonthDay(int year, int month){// 给13 的原因是为了刚好让下标对上月份static int monthday[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 给static的原因是 每次访问的数组都不变并且都是这个数组,那每次访问都要开辟,不如直接放静态区去// 闰年的2月份 是 29天if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)monthday[2] = 29;elsemonthday[2] = 28;int day = monthday[month];return day;}// 默认构造函数Date(int year = 0, int month = 0, int day = 0){// 对传进来的年 月 日 进行判断,是否合法,合法才构造if (year >= 0 && month > 0 && month < 13 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" << endl;}}// 拷贝构造函数 (其实日期类这种不涉及内存资源管理的类,不需要我们显式实现拷贝构造,但是实现了也没问题)// Date(Date* this, const Date& d) Date(const Date& d) // 一定要有别名 & ,不然会无限递归{_year = d._year;_month = d._month;_day = d._day;}// 析构函数~Date() // 其实类似日期类这种类,是不需要我们去编写显式析构函数的{cout << " 析构函数" << endl;}// 运算符重载//d1 < d2 编译器处理后 d1.operator<(&d1, d2)bool operator<(const Date& d)// bool operator(Date* this, const Date& d){if (_year < d._year)return true;else if (_year == d._year && _month < d._month)return true;else if (_year == d._year && _month == d._month && _day < d._day)return true;elsereturn false;}// d1 == d2 编译器处理后 d1.operator==(&d1, d2)bool operator==(const Date& d)// bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 <= d2 编译器处理 d1.operator<=(&d1, d2)bool operator<=(const Date& d) // bool operator<=(Date* this, const Date& d){// 这里我们采用复用上面的代码来实现,以减少代码重复return *this < d || *this == d; // *this 解引用就是 d1// (*this).operator<(&(*this), d) || (*this).operator==(&(*this), d);}// d1 > d2 编译器处理后 d1.operator>(&d1, d2)bool operator>(const Date& d){// 不再采取之前那种代码,代码重复性太强,并且一旦成员变量改变,代码也要跟着大量修改// 我们采取函数复用return !(*this <= d); // 只要*this 不<= d 那就是> d// 编译器处理后 !((*this).operator<=(&(*this), d))}// d1 >= d2 编译器处理 d1.operator>=(&d1, d2)bool operator>=(const Date& d)// bool operator>=(Date* this, const Date& d){// 也采用函数复用。复用前面已经实现的 > 和 == 的运算符重载函数return *this > d || *this == d;}// d1 != d2 -> d1.operator!=(&d1, d2)bool operator!=(const Date& d)// bool operator!=(Date* this, const Date& d){// 函数复用return !(*this == d);} d1 + 10(天数) -> d1.operator+(&d1, 10)//Date operator+(int day) // Date operator+(Date* this, int day) 这里不能使用引用返回,因为拷贝构造的ret在函数结束之后就会销毁//{// Date ret = *this; // 拷贝构造一个d1 ,这里等价于 ret(*this)// ret._day += day;// // 对day进行判断是否合法,合法就输出,不合法要进位// while (ret._day > GetMonthDay(ret._year, ret._month))// 有可能不止进一次位,所以给一个循环// {// ret._day -= GetMonthDay(ret._year, ret._month);// ret._month++;// // 要注意月份是否合法// if (ret._month == 13)// {// ret._year++;// ret._month = 1;// }// }// return ret;//}//d1 += 10 -> d1.operator+=(&d1, 10)Date& operator+=(int day)// 这里可以使用引用返回,因为this指向的本来就是外面的d1,这个函数结束之后,d1不会销毁{// 如果day是负数。要处理if (day < 0){return *this -= -day; // 加负数 相当于 减正数}// += 和 + 的区别是, + 不改变d1本身,但是+=要改变d1本身_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;// 判断月份是否合法if (_month == 13){_year++;_month = 1;}}return *this; // 返回的是d1本身}// 上面我们对+的重载和 对+=的重载的代码重复性很高,那我们就可以考虑采用复用// d1 + 10Date operator+(int day){Date ret(*this); // 拷贝构造d1ret += day; // 复用我们实现的+=的重载// ret.operator+=(day)return ret;} d1 - 10(天数) -> d1.operator-(&d1, 10)//Date operator-(int day)//{// Date ret = *this; // 拷贝构造一个d1// ret._day -= day;// // 判断-=day之后的 day是否合法,合法就返回,不合法就要退位// while (ret._day <= 0)// {// ret._month--; // 先让月份退到上一个月// // 判断月份是否合法// if (ret._month == 0)// {// ret._month = 12; // ret._year--;// }// ret._day += GetMonthDay(ret._year, ret._month); // + 上一个月份的天数,看看是否_day是否合法// }// return ret;//}// d1 -= 10 -> d1.operator(&d1, 10)Date& operator-=(int day) // Date& operator(Date* this, int day){// 判断day是否是负数if (day < 0){return *this += -day; // 减负数 相当于 加正数}// -= 改变的是d1自己,也就是this指针指向的对象_day -= day;// 判断_day是否合法,不合法要退位,直至合法while (_day <= 0){// 先让月份退一位_month--;// 判断月份是否合法if (_month == 0){_month = 12;_year--;}_day += GetMonthDay(_year, _month); // += 上一个月份的天数,看看是否_day是否合法}return *this; // 返回自身}// 对-的重载采用代码复用,减少对代码的重复性,提升维护性// d1 - 10Date operator-(int day){Date ret(*this); // 拷贝构造d1ret -= day; // 函数复用return ret; // 返回ret的时候要创建一个临时变量, 会调用一次拷贝构造}// ++d1 -> d1.operator++(&d1)Date& operator++() // Date& operator(Date* this){// ++也可以像之前一样,每次调用+1天,+完之后判断day是否需要进位,月是否需要进位,年是否需要进位// 但是为了提升类的维护性,和减少代码重复度,我们采取复用*this += 1;// ++d1就是让自己去 + 1return *this;}// d1++ -> d1.operator++(&d1, 0)Date operator++(int) // 加int是为了代表是后置++,如果不加就是默认前置++{// 前置++和后置++的区别就是 前置++返回+之后的 后置++返回+之前的Date tmp = *this;*this += 1;return tmp; // 返回+之前的}//--d1Date& operator--(){*this -= 1; // 函数复用return *this;}//d1--Date operator--(int){// 前置-- 要返回--之后的 后置-- 要返回-之前的Date tmp(*this);*this -= 1;return tmp;}// 赋值运算符重载 operator=// d1 = d2 -> d1.operator(&d1, d2)Date& operator=(const Date& d){// 防止自己给自己赋值if (this != &d) // this指向的地址 d是别名,d的地址和this指针相等就说明是自己给自己赋值{_year = d._year;_month = d._month;_day = d._day;}return *this;}// 日期 - 日期 d1 - d2// 我们让小的日期++ 直至 == 大的日期, + 了多少次,就差了多少天int operator-(const Date& d){int flag = 1;// 默认大的是 this指针指向的 小的是 dDate max = *this; // 拷贝构造Date min = d; // 拷贝构造if (max < min){// 走到这里说明 默认的情况是错的min = *this;max = d;flag = -1; // 说明是小的 - 大的 最后应该是个负数}int n = 0;while (max > min) // 让小日期一直加 直至 == 大日期{++min;++n;}// 加了多少次,n就是多少,n就是相差的天数return n * flag;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 28);Date d2 = d1; // 等价于 d2(d1)cout << (d1 < d2) << endl; // 0cout << (d1 == d2) << endl;// 1cout << (d1 > d2) << endl;// 0cout << (d1 != d2) << endl;// 0cout << (d1 <= d2) << endl;// 1cout << (d1 >= d2) << endl;// 1// 是否要重载一个运算符,看的是这个运算符是否对该类的对象有意义// 比如日期 + 日期没有意义, 但是日期 - 日期有意义,是两日期相隔的天数// 比如 日期 + 天数 有意义,是多少天之后, 日期 - 天数有意义 是多少天之前// 而有+ 就有 += , 有-就有-= , 并且日期 * 日期 和 日期 / 日期是没有意义的Date d3 = d1 + 10; // 将d1 + 10后的日期 拷贝构造到d3对象d3.Print();// 2024-6-7Date d4(d1 + 100);d4.Print();// 2024-9-5Date d5(d1 + 1000);d5.Print();// 2027-2-22Date d6(d1 - 1000);d6.Print();// 2021-9-1Date d7(d1 -= 1000);d7.Print();// 2021-9-1Date d8(d1++);d8.Print(); // 2021-9-1Date d9(++d1);d9.Print();//2021-9-3Date d10(d1--);d10.Print();// 2021-9-3Date d11(--d1);d11.Print();//2021-9-1cout << d3 - d1 << endl; // 1010 cout << d1 - d3 << endl;// -1010return 0;
}
中间两次调用析构函数是因为,在日期 - 日期函数中,我们创建了两个Date类的局部变量,函数栈帧结束后要销毁变量。调用了析构函数
7.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
我们来看一段代码:
// const成员
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 由于this指针是隐含的 所以不能这样加 void Print(const Date* this)// 因此我们使用const修饰成员函数, 实际上修饰的是 *this, 也就是this指针指向的对象void Print() const // 编译后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;};void f(const Date& d)
{d.Print();// 编译无法通过 编译后 -> d.Print(&d)// 因为这里的&d 是const Date*类型, 但是Print函数的this形参是 Date* 类型// 涉及到了权限放大的问题,就无法编译通过
}int main()
{Date d1;f(d1);return 0;
}
这里可以在回忆一下const修饰的用法
-
const Date p1;*
-
Date const p2;*
-
Date const p3;*
第一第二种const都在*的左边,修饰的都是指针指向的对象,也就是不能修改指针指向的对象。
第三种 const在* 的右边,修饰的是指针本身,也就是不能让修改指针本身指向其他地方
上面代码属于是对象调用成员函数,那我们再来看看成员函数之间的相互调用,是否也有const之间的关系
来看代码:
// 成员函数之间调用
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}void f1()// void f1(Date* this){f2(); // 编译后 this->f2(this);// 这里的*this是可读可写的,传进去的*this要变成const Date类型,只能读// 属于权限缩小,不会报错}void f2() const // void f2(const Date* this){}void f3()// void f3(Date* this){}void f4() const // void f4(const Date* this){f3(); // this->f3(this) 会报错// 这里的*this 只能读不能改, 但是传给f3之后会变成可读可写的// 属于权限放大,因此会报错}void Print() const // 编译后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;};int main()
{return 0;
}
看了上面两段代码之后,请思考下面的几个问题:
-
const对象可以调用非const成员函数吗?【不可以】
-
非const对象可以调用const成员函数吗?【可以】
-
const成员函数内可以调用其它的非const成员函数吗?【不可以】
-
非const成员函数内可以调用其它的const成员函数吗?【可以】
注意:
成员函数之间调用,实际上就是this指针指向的对象在调用。
总结:
只要成员函数中不需要修改成员变量,最好都加上const修饰成const成员函数
不然有const类型的对象传进来就会报错,因为权限缩小了。上面我们也有讲。
如图所示:
8.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
//取地址及const取地址操作符重载
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 取地址操作符重载Date* operator&(){cout << "取地址操作符重载" << endl;return this;}// 取地址操作符重载const Date* operator&() const{cout << "const取地址操作符重载" << endl;return this;}void Print() const // 编译后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1, d2;const Date d3, d4;cout << &d1 << endl;//取地址操作符重载//00000008B33CF528cout << &d2 << endl;//取地址操作符重载//00000008B33CF558//cout << &d3 << endl; // 这里不会调用我们实现的&重载函数,因为d3是const Date类 权限放大了。 仍然获得了d3的地址是因为调用了编译器默认生成的const取地址操作符重载函数000000A4BA3AF568// const取地址操作符重载函数我们也可以自己实现cout << &d4 << endl; // 这里调用的就是我们自己实现的//const取地址操作符重载//00000008B33CF5B8return 0;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
这篇关于类和对象(中)【类的6个默认成员函数】 【零散知识点】 (万字)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!