文章目录
类的六个默认成员函数
每当一个新的类被创建时,它就会自动生成6个默认的成员函数,分别为:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、普通对象取地址运算符重载、const对象取地址运算符重载。
下面分别对这六个默认成员函数进行分析。
1. 构造函数
1.1 概念
以前,我们创建一个类对象的时候,若想要给它进行初始化赋值,必须通过调用一个自定义的成员函数Init来实现,这多少有些麻烦,而构造函数的存在,就是为了避免这种麻烦的。
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(2024,1,4); return 0; }
构造函数:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
以后我们就可以这么写:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1(2024,1,4); return 0; }
1.2 构造函数的特性
-
函数名与类名相同。(参考上面的代码)
-
无返回值。(可以传参,实参在创建对象的时给)
-
对象实例化时,编译器自动调用对应的构造函数。
-
构造函数可以重载。
class Date { public: // 1.无参构造函数 Date() {} // 2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数 Date d3(); // 错误!编译器无法区分d3是对象还是函数名! }
注意:调用无参构造函数时,类对象后面不要加括号,否则就变成函数的声明了!
-
C++编译器会自动生成一个无参的默认构造函数。
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 (就是我们不写它就自己生成,我们写它就不自己生成)自己生成的默认构造函数,有两个功能:
- 对内置类型不做处理
如int、char等等 - 对自定义类型,会调用它们自己的默认构造函数
如我们写的一些类等等
有些编译器会把内置类型也做处理,但这不是C++的性质,是编译器自己干的,别的编译器就不一定了。
- 对内置类型不做处理
-
默认构造函数只能有一个。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且==默认构造函数只能有一个==。注意:无参构造函数、全缺省构造函数、我们没写从而编译器默认生成的构造函数,这三者都可以认为是默认构造函数。(不传参的都是默认构造函数。)一个无参构造函数和全缺省的构造函数,是能构成函数重载,但是编译器无法区分,所以不能把它们一起写。
-
可以给内置类型成员加缺省值。
一开始默认构造函数对内置类型不做处理,然后c++11的补丁,可以给内置类型成员加缺省值(不是初始化!!)缺省值是给初始化列表使用的。class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; };
比如上面的基本类型,都给了缺省值!(注意,不是对成员变量进行初始化!只有实例化之后才叫成员变量的初始化)
1.3 注意点
一般情况下,构造函数都需要我们自己写,尤其是有内置类型成员,更不能让编译器自己生成了。
a. 除非内置类型成员都有缺省值,且给的值都符合我们的要求。且自定义类型有我们写了的默认构造函数,就可以考虑让编译器自己生成。
b. 全部是自定义类型成员时,且它们都有我们写了的默认构造函数(我们写的不传参的构造函数),就可以考虑让编译器自己生成。
上面只是常规情况,以后复杂的情况要更复杂的处理或者随机应变。
2. 析构函数
2.1 概念
析构函数:与构造函数功能相反,析构函数不是完成对象本身的销毁,局部对象的销毁工作是由编译器完成的,而析构函数是完成对象中资源的清理工作,它是对象在销毁时自动调用的。(它不是销毁对象本身,而是销毁对象的一系列操作过程中所开辟的空间,这些空间无法由编译器自动销毁,需要析构函数来执行销毁的工作)
2.2 特性
-
析构函数名是在类名前面加上
~
。 -
无参数、无返回值类型。
-
一个类只能有一个析构函数。若未显式定义(我们没有手动定义),编译器会自动生成默认析构函数。
-
对象生命周期结束时,C++编译器会自动调用析构函数。
typedef int DataType; class Stack { public: Stack(size_t capacity = 4) //构造函数 { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } // 其他... ~Stack() // 析构函数 { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: DataType* _array; int _capacity; int _size; }; void TestStack() { Stack s; s.Push(1); s.Push(2); }
-
C++编译器会自动生成默认析构函数。
和默认构造函数一样,若我们没有手动定义析构函数,编译器会自动生成一个默认析构函数。
默认析构函数有以下两种功能:- 内置类型成员变量不做处理。
如int、char等 - 自定义类型会调用它自己的析构函数。
如我们定义的类
- 内置类型成员变量不做处理。
-
什么时候可以不写析构函数?
当类中没有申请资源时,比如Date类,可以直接用编译器默认生成的默认析构函数。
当类中有申请资源时,比如Stack类,一定要自己写析构函数,否则会造成资源泄露。
2.3 注意点
- 析构函数和构造函数类似,但是不能重载。
- 一般情况下,有动态申请资源,就需要我们自己写析构函数释放资源;没有动态申请资源时,一般可以不用写。
- 需要释放资源的成员都是自定义类型,如果某个自定义类型的析构函数也符合第2点,那么这个自定义类型也可以不手动写析构函数。
- 上面的特性只是常规情况,若有复杂情况也是正常的,具体的处理方法需要针对具体的情况而随机应变。
3. 拷贝构造函数
3.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
3.2 特性
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个,且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
C++规定,传参、赋值时,内置类型直接拷贝,自定义类型必须调用它的拷贝构造函数来完成拷贝。
上图中,d1作为参数,因为是传值的方式,
const Date date
中的date
是一个临时变量,d1会赋值给date(传参时若传值,形参是一个临时变量,实参把自己的值拷贝给形参)。而d1赋值给date,又会调用拷贝构造函数,变成Date date(d1)
,后面就会无限循环下去。
如果d1传参是传引用的话,就不需要创建一个临时变量并把值拷贝给它了,也就不会再调用拷贝构造函数,不会无限循环了。 -
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。(仅仅是把值拷贝过来,申请的空间资源不会拷贝过来)
默认拷贝构造函数的两个功能:- 内置类型成员完成值拷贝/浅拷贝
- 自定义类型成员调用它自己的拷贝构造函数
浅拷贝的问题:
- 如果有个数组,浅拷贝仅仅是创建两个指针,但是都指向同一块空间,可能会析构两次。
- 在上面这种情况下, 修改一个可能会影响另一个。
因此,有申请资源的类一般都要深拷贝。
-
拷贝构造函数典型的调用场景:
- 使用已存在对象创建新的对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
-
示例:
class Date { public: Date(int year, int month, 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; }; int main() { Date d1(2024,1,4); Date d2(d1); return 0; }
4. 赋值运算符重载
4.1 运算符重载概念
要说赋值运算符重载,我们得先知道什么是运算符重载,其实它和之前谈到的函数重载有很多相似之处。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数的名字为:关键字
operator
+ 需要重载的运算符符号函数原型:返回类型
operator
操作符(参数列表)
注意点:
-
不能通过连接其他符号来创建新的操作符,比如:
operator@
。只能重载平时常见的操作符:+
-
*
/
%
=
+=
-=
*=
/=
%=
++(前置或后置)
--(前置或后置)
等等。 -
重载操作符(重载函数的参数)必须有一个类类型参数。
-
用于内置类型的运算符,其含义不能改变,比如:内置类型整型的
+
,不能改变其含义。 -
运算符重载作为类的成员函数时,其形参看起来要比操作数数量少1个,因为类成员函数的第一个参数一定是隐藏的this指针。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator<(const Date& x1, const Date& x2) { // ... } private: int _year; int _month; int _day; }; int main() { Date d1(2024,1,6); Date d2(2024,1,7); if(d1 < d2) cout << "d1 < d2" << endl; else cout << "d1 >= d2" << endl; return 0; }
上面的代码中运算符
<
的重载即是错误的写法!
该函数应该这么写:// ... bool operator<(const Date& x) { // ... } // ...
分析:代码中的
d1 < d2
,被编译器转换成了这样:operator<(d1, d2)
,也就是直接去调用重载函数了。而因为这个运算符重载函数是类成员函数,所以编译器把运算符左边的那个对象的地址变成this指针,&d1
就是隐藏的this指针,右边的对象就是我们写的x
。所以当一个运算符重载需要两个操作数时,我们编写运算符重载函数的时候只需写一个形参即可,另一个是隐藏的this指针。如果我们多写了,那么编译器会报错,因为形参数量和实参数量不匹配! -
以下5个运算符不能重载,笔试选择题中经常出现:
.*
::
sizeof
?:
.
-
当代码中遇到运算符时,内置类型就转换成对应指令,自定义类型就转换成调用运算符重载函数。
-
后置
++
或--
与它们的前置构成重载,那么怎么区分前置和后置呢?Date& operator++(); // 前置++ Date& operator++(int); // 后置++
编译器为了区分前置和后置,默认正常写的是前置(
++
或--
),而对于后置,编译器会自动传递一个整型为参数,来与前置构成重载。所以我们写函数的时候,在括号里写上一个int即可。 -
流插入(cin >> d1)运算符重载作为成员函数,d1在运算符的右边,而this指针从来都只是运算符左边那个,也就是说
&d1
不是this指针,&cin
变成this指针了,这样是不行的,cin并不是类的类型,不能与默认this指针匹配,因此我们不能把流插入运算符重载写成类的成员函数,必须要写在类外面。然后通过友元函数来访问类的内部。
4.2 赋值运算符重载
- 概念
赋值运算符重载:已经存在的两个对象之间的复制拷贝(是6个默认成员函数之一)。
与拷贝构造区分:用一个已经存在的对象初始化另一个对象。
2. 有返回值,返回*this
,且返回要用引用
3. 要检查是否自己给自己赋值
4. 赋值运算符重载只能重载成类的成员函数,不能在全局域中重载
因为:赋值运算符重载是默认成员函数,当我们不写的时候会默认生成,如果在全局中重载赋值运算符,那么它就会与类中默认生成的产生冲突。
5. 当用户没有显式实现时,编译器会自动生成一个默认运算符重载函数
默认生成的赋值运算符重载有两种功能:
1. 对于内置类型,直接赋值(浅拷贝)。
2. 对于自定义类型,会调用它的赋值运算符重载函数。
5. 取地址操作符重载和const取地址操作符重载
对于这两个默认成员函数,我们一般不需要自己手动定义,使用编译器默认生成的即可。
只有在一些特殊的情况下才需要我们重新定义,比如想让别人获取到指定的内容。
Date* operator&() // 普通对象 { return this; } const Date* operator&() const // const对象 { return this; }
当我们不想让别人取到this指针的地址时,就可以自己手动定义。
6. const成员
我们之前已经谈及const成员函数,就是在成员函数的后面加上一个const,其修饰的是隐藏的this指针,使Date* const this
变成const Date* const this
,从原来的不能改变this指向,只能改变this指向的内容到既不能改变this指向,也不能改变this指向的内容。
在下方实现的日期类中就有很多个const成员函数。
const我们并不陌生,不过需要注意的是:const修饰会使得变量的权限变小,对于const修饰的变量,我们不能将其权限变大。
比如:
- const对象不能调用非const的成员函数
非const的成员函数可能会用到一些const对象不允许的范围,传递this指针时权限被放大 - 非const对象可以调用const的成员函数
传递this指针时权限被缩小 - const的成员函数不可以调用其他的非const的成员函数
传递this指针时权限被放大 - 非const的成员函数可以调用其他的const的成员函数
传递this指针时权限被缩小
不过也不是所有的成员都可以加上const,对于那些我们想要修改this指向的内容的成员,是不能加const的。
7. 实现日期类
1. Date.h
#include<iostream> using namespace std; class Date { friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); public: // 全缺省构造 Date(int year = 2024, 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 Getdate() //{ // cout << _year << ' ' << _month << // ' ' << _day << endl; //} // 析构 ~Date(){} // Date的运算符重载 static int GetMonthDays(int year, int month); // 获取该月天数 bool operator<(const Date& x) const; // 日期小于 bool operator==(const Date& x) const; // 日期等于 bool operator>(const Date& x) const; // 日期大于 bool operator<=(const Date& x) const; // 日期小于等于 bool operator>=(const Date& x) const; // 日期大于等于 bool operator!=(const Date& x) const; // 日期不等于 Date operator+(int day) const; // 加天数 Date& operator+=(int day); // 加等于天数 Date& operator++(); // 加一天(前置) Date operator++(int); // 加一天(后置) Date& operator-=(int day); // 减等于天数 Date operator-(int day) const; // 减天数 Date& operator--(); // 减一天(前置) Date operator--(int); // 减一天(后置) int operator-(const Date& d) const; // 日期相减 private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d); // 流插入 istream& operator>>(istream& in, Date& d); // 流提取
2.Date.cpp
#include"Date.h" // 获取该月天数 int Date::GetMonthDays(int year, int month) { char Days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; int ret = 0; if (((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) && month == 2) ret++; ret += Days[month]; return ret; } // 日期小于 bool Date::operator<(const Date& x) const { // 记住,要比的是 *this < x if (_year > x._year) return false; else if (_year == x._year && _month > x._month) return false; else if (_year == x._year && _month == x._month && _day >= x._day) return false; else return true; } // 日期等于 bool Date::operator==(const Date& x) const { if (_year == x._year && _month == x._month && _day == x._day) return true; else return false; } // 日期大于 bool Date::operator>(const Date& x) const { return !(*this < x || *this == x); } // 日期小于等于 bool Date::operator<=(const Date& x) const { return !(*this > x); } // 日期大于等于 bool Date::operator>=(const Date& x) const { return !(*this < x); } // 日期不等于 bool Date::operator!=(const Date& x) const { return !(*this == x); } // 加等于天数 Date& Date::operator+=(int day) { _day += day; int days_tmonth = GetMonthDays(_year, _month); while (_day > days_tmonth) { _day -= days_tmonth; _month++; if (_month > 12) { _year++; _month = 1; } days_tmonth = GetMonthDays(_year, _month); } return *this; } // 加天数 Date Date::operator+(int day) const { Date d(*this); d += day; return d; } // 前置++ Date& Date::operator++() { *this += 1; return *this; } // 后置++ Date Date::operator++(int) { Date d = *this; *this += 1; return d; } // 减天数 Date Date::operator-(int day) const { Date d = *this; d -= day; return d; } // 减等天数 Date& Date::operator-=(int day) { _day -= day; while (_day < 1) { _month--; if (_month < 1) { _year--; _month = 12; } _day += GetMonthDays(_year, _month); } return *this; } // 减一天(前置) Date& Date::operator--() { *this -= 1; return *this; } // 减一天(后置) Date Date::operator--(int) { Date d = *this; *this -= 1; return d; } // 日期相减 int Date::operator-(const Date& d) const { int count = 0; Date large = *this; Date small = d; int sign = 1; if (*this == d) return 0; if (*this < d) { large = d; small = *this; sign = -1; } while (small != large) { ++small; count++; } return count * sign; } // 流插入 ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } // 流提取 istream& operator>>(istream& in, Date& d) { int year, month, day; in >> year >> month >> day; // 检查输入的日期是否规范 while (month < 1 || month > 12 || day < 1 || day > Date::GetMonthDays(year, month)) { cout << "日期不规范!重新输入" << endl; in >> year >> month >> day; } d._year = year; d._month = month; d._day = day; return in; }
3. Test.cpp
#include"Date.h" int main() { //Date d1(2025,2,2); //Date d2; //d2 = d1; //Date d1(2024, 1, 8); //Date d2 = (d1 + 2); //Date d3; //d3 += 2; //cout << (d1 < d2) << endl; Date d1(2024, 12, 23); Date d2(2024, 1, 8); //cout << (d1 != d2) << endl; //d2 = d1 + 1; //d1 += 1; //d2 = d1 - 1; //d1 -= 1; //d2 = d1++; //d2 = d1--; //d2 = --d1; //d2 += 52; //d2.Getdate(); //cout << (d1 - d2) << endl; cout << d1; cin >> d2; cout << d2; return 0; }