如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下六个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
如果我们不写默认的成员函数,编译器会自己生成一个。如果我们写了,编译器就不会生成。换句话说就是,有些类的默认成员函数需要我们自己写,而另一些类,编译器默认生成的就够用了。那这六个默认成员函数究竟是什么呢?我们现在来学一学。
#include
using namespace std;
class Date
{
public:
void Init(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;
};
int main()
{
Date d1;
d1.Init(2022, 11, 5);
d1.Print();
Date d2;
d2.Init(2022, 11, 6);
d2.Print();
return 0;
}
对于上面的日期类Date
,可以通过Init
共有函数对对象设置日期,但如果每次创建对象时都要通过该函数设置信息,未免有点麻烦。那能否在创建对象时,就将信息设置进去呢?C++ 之父也想到了这一点,那么大佬设计的构造函数就登场了来做这一件事。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特性如下:
注:构造函数的无返回值就是函数名前连 void 都不能带。构造函数可以重载的意思就是可以写多个构造函数,提供多种初始化方式。
那我们来看一下日期类的构造函数。
#include
using namespace std;
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 11, 5);
d1.Print();
Date d2(2022, 11, 6);
d2.Print();
Date d3;
d3.Print();
// 无参的不要想下面这样写,因为分不清是定义对象还是函数声明
//Date d4();
return 0;
}
注意:如果通过无参构造函数初始化对象时,对象后面不用跟括号,否则就成了函数声明。
上面的构造函数还是有一点小 BUG 的,就是会将非法日期也看成合法日期。那日期类的构造函数就要检查一下日期的合法性了。
//...
// 获取每个月的天数
int GetMonthDay(int year, int month)
{
// static修饰数组避免频繁创建
static int monthDayArray[13] = { 0, 31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
// 检查日期是否合法
if(!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
//...
上面的日期类的构造函数也还挺简单的,我们来学习一下栈的构造函数。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
// 扩容
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
有时候,我们会经常忘记自定义类型的初始化。那么有了构造函数,我们再也不用担心对象的初始化了。
在上面讲到了,如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
那如果我们不自己给日期类和栈写构造函数,会发生什么呢?我们来看一下:
我们可以看到,使用编译器生成构造函数给对象初始化,却还是随机值。为什么这样呢?我们来看一下下面的结论。
结论:
关于编译器生成的默认成员函数,很多小伙伴会有疑惑:
不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d1 对象调用了编译器生成的默认构造函数,但是 d1 中的数据依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++ 把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char
。自定义类型就是我们使用class、struct、union
等自己定义的类型。对于内置类型,编译器生成的默认构造函数不会对数据进行处理,而对于自定义类型会调用该自定义类型的默认构造函数。
注意:构造函数和默认构造函数不是同一个概念,千万不要混淆。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认构造函数。
我们现在就通过一个例子来验证一下上面的结论。
#include
using namespace std;
class A
{
public:
A()
{
_a = 0;
cout << "A()构造函数" << endl;
}
private:
int _a;
};
class Date
{
public:
// 不自己写构造函数,看看编译器自动生成的构造函数
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
// 内置类型
int _year;
int _month;
int _day;
// 自定义类型
A _aa;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
我们在日期类里加上了个自定义类型class A
,可以发现:如果没有写构造函数,对于自定义类型,编译器会调用该自定义类型的默认构造函数。为了明显的看到默认构造函数对于自定义类型的好处了,我们看一个用栈实现队列的例子。
类MyQueue
没有写构造函数,所以会去调用栈Stack
的默认构造函数来完成初始化。所以,有些类的构造函数需要自己写,有些类不需要自己写。
那什么类的构造函数才需要自己写呢?我给出的意见是:关注需求,如果编译器默认生成的构造函数满足需求了,就不需要自己写;如果不满足需求,就需要自己写。比如:日期类Date
和栈Stack
需要写构造函数,而队列MyQueue
不需要写构造函数。
但是编译器生成的默认构造函数对于内置类型,不会进行处理。其实如果对内置类型进行处理的话,内置类型初始化成 0 肯定会更好一点。为了弥补上这个不足,C++ 之父又做了一下这件事。C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即内置类型成员变量在类中声明时可以给缺省值。
注意:如果日期类有全缺省的构造函数,就会用该构造函数来给成员变量初始化,就不会用成员变量声明时的缺省值来给成员变量初始化。缺省值也可以是 malloc 函数申请来的空间。
在上面,我们已经提到了默认构造函数。无参的构造函数、全缺省的构造函数和编译器默认生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个。通俗来说,不需要传参数就可以调用的构造函数,就称为默认构造函数。 现在我们来看一下默认构造函数需要注意点什么问题。
默认构造函数只能有一个
无默认构造函数
因为我们写了构造函数,所以编译器就不会生成默认构造函数了。但是我们写的这个构造函数又不是默认构造函数,所以我们写出Date d1;
这样的语句就无法通过编译,会提示我们没有合法的默认构造函数可用。
以上大概是构造函数的百分之八十的内容了,还有初始化列表的内容没有讲解。这个知识点将会在类和对象(下)里讲解,现在我们来学习析构函数。
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作, 和我们之前学的销毁栈的函数Destroy
功能相似。
析构函数是特殊的成员函数,其特性如下:
~
。以下为栈的析构函数,有了析构函数,我们就不怕忘记调用Destroy
函数来归还申请的空间了。因为析构函数会帮自动帮我们清理对象的资源。
//...
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//...
注意:并不是所有类都需要写析构函数。比如:日期类就不需要写构造函数。因为日期类并没有向堆区申请空间。
如果我们没有写析构函数,那么编译器就会自动生成一个默认析构函数。默认析构函数对于内置类型不做处理,对于自定义类型会调用该自定义类型的析构函数。我们还是通过用栈实现队列的例子来观察。通过下图,我们可以看到MyQueue
没有写析构函数,但它会调用Stack
的析构函数。
并不是所有的类都需要写析构函数,如果编译器默认生成的析构函数满足我们的需求,我们就不需要写了;如果不满足需求,就需要我们自己写了。
以上就是析构函数的内容,接着我们来学习拷贝构造函数。
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?这是可以的,这就要借助拷贝构造函数了。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const
修饰),在用已存在的类对象创建新对象时由编译器自动调用。拷贝构造函数也是构造函数的一种。
拷贝构造函数也是特殊的成员函数,其特性如下:
那我们以日期类为例,来学拷贝构造函数。
注意:拷贝构造函数的参数一定是类对象的引用。如果参数是类对象的话,会造成无限递归调用拷贝构造函数。因为形参是实参的拷贝,引用是实参的别名,传值传参要调用拷贝构造函数,那么就会无限调用下去。
这个无限递归调用拷贝构造函数有点难理解,那么我们就写两个函数来帮助大家来理解。
我们可以明显地看到,传值的函数会调用拷贝构造函数。所以如果拷贝构造函数的参数为类对象,而不是类对象的引用,就会导致无限递归调用拷贝构造函数的问题。
注:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
日期类的拷贝构造函数
//...
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
// 形参加const修饰,防止写反了,下面的问题就可以检查出来
//d._day = _day;
}
//...
注:将拷贝构造函数的参数换成指针也能实现,但是这个函数就不再是拷贝构造函数了,而是构造函数。拷贝构造函数的引用要用const
关键字修饰,防止写反了两个参数。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
可以看到,如果我们不写日期类Date
的拷贝构造函数,编译器生成的默认拷贝构造函数也够用了。那如果栈Stack
不写拷贝构造函数,编译器生成的默认拷贝构造函数是否够用呢?我们来看一下。
如果我们没有写栈Stack
的拷贝构造函数,编译器就会生成默认拷贝构造函数完成对象的浅拷贝。那么此时 st1 和 st2 就指向了同一块空间,那么就会导致一个问题:调用析构函数是会对同一块空间析构两次,那么程序就会崩溃掉。
注:对象的析构顺序符合先定义后析构的原则。但是要注意 static 对象的存在, 因为 static 改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以。一旦涉及到资源申请时,则拷贝构造函数是一定要写的,且构造拷贝函数就是深拷贝,不能是浅拷贝。
那我们来写一下栈Stack
的拷贝构造函数。
//...
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
//...
这时候,调式起来就可以看到 st1 和 st2 不再指向同一块空间了, st1 和 st2 的修改不会互相影响,也不会造成同一块空间释放多次的问题。
我们已经写好了栈Stack
的拷贝构造函数,那我想问大家一个问题,什么样的类需要写拷贝构造函数呢?下面我给大家总结了个规律。
规律:
编译器默认生成的拷贝构造函数对于内置类型会进行浅拷贝,而对于自定义类型会调用该类型的拷贝构造函数。那现在我们以MyQueue
为例,来看一看是不是酱紫的。
通过上图可以发现,确实是这样子的。因为MyQueue
不需要写析构函数,所以它也就不需要写构造函数了。
C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator 后面接需要重载的运算符符号。
函数原型:返回值类型 operator 操作符(参数列表)
注意:
+
,不能改变其含义。.* :: sizeof ?: .
注意以上5个运算符不能重载,这个经常在笔试选择题中出现。运算符重载是非常有意义的。想要比较内置类型的大小,是相当简单的。那么对于自定义类型的话,我们就需要借助函数来完成这个工作,这个函数就是运算符重载。
注:运算符重载和函数重载没有必然的联系,运算符重载知识为了自定义类型对象能够使用运算符,增强代码的可读性。
接下来的运算符重载的讲解,我都以日期类为例。因为日期类相对来说比较简单,也是相当地经典。
注: == 的优先级比 << 的优先级低。
上图的operator==
函数就是比较两个日期是否相等的函数。如果该函数不在类中定义的话,那么就需要将成员变量改成公有public
。如果这样子做的话,那封装的意义就不存在了。
如果我们既想要运算符重载,又想成员变量为私有private
。那如何解决呢?这时候我们可以借助友元(类和对象下的内容)或者在类中定义一个辅助的函数(Java 经常使用这种方式),还可以将运算符重载定义在类中了。在这里,我们采用将运算符重载定义在类中这种方式。那operator==
如何定义呢?见下图代码:
//...
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//...
看到上面的代码,可能就会有细心的小伙伴发现,怎么operator==
的参数只有一个,是不是写错了呀。其实并没有写错,因为每个成员函数会有一个隐藏参数this
,该参数占据成员函数的第一个参数的位置。
编译器是非常的智能,如果我们运算符重载在类中定义了,编译器就不会去全局中找;如果运算符重载没有在类中定义,那么编译器就会去全局中找。
有时候,我们需要比较两个日期的大小,那我们来看一下operator>
的代码。
bool 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;
}
return false;
}
比较一个日期 d1 是否大于或等于另外一个日期 d2,那么就可以复用上面的operator==
和operator>
。
//...
bool operator>=(const Date& d)
{
return *this == d || *this > d;
}
//...
注:*this
就是日期 d1。
operator<=
重载运算符也可以复用operator>
运算符,因为operator<=
是operator>
的反面,那我们一起来看一下代码。
//..
bool operator<=(const Date& d)
{
return !(*this > d);
}
//...
因为operator<
是operator>=
的反面,所以可以复用operator>=
。
//...
bool operator<(const Date& d)
{
return !(*this >= d);
}
//...
因为operator!=
是operator==
的反面,所以可以复用operator==
。
//...
bool operator!=(const Date & d)
{
return !(*this == d);
}
//...
如果我们想算一个某一天的 N 天后是哪一天,这时候就需要借助operator+=
或者operator+
。注意,operator+=
和operator+
的返回值为Date
。日期的加法是比较复杂的,因为每个月有多少天是没有规律的。
因为每个月的天数是没有规律的,所以我们就写一个函数来得到每个月的天数,然后再进行日期的加法。
获取每个月的天数
//...
// 获取每个月的天数
int GetMonthDay(int year, int month)
{
// static修饰数组避免频繁创建
static int monthDayArray[13] = { 0, 31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
//...
operator+=
//...
Date& operator+=(int day)
{
// 处理 day < 0的情况
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
//...
注:+=
运算符会修改变量的值,而且出了函数的作用域,+=
后的对象还存在,所以operator+=
的函数返回值为Date&
。注:如果是值返回也会存在拷贝。
operator+
因为+
运算符不会影响变量的值,所以我们借助拷贝构造来创建一个对象ret
,然后赋用operator+=
重载运算符让ret += day
,最后将ret
返回。注意:出了函数作用域,ret就不存在了,所以operator+
的返回值为Date
,不能是Date&
。
//...
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
//...
operator-=
//...
Date& operator-=(int day)
{
// 处理 day < 0的情况
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//...
operator-
Date operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确的函数重载。C++ 规定:后置++重载时多增加一个 int 类型的参数来区分前置 ++ 和后置 ++,但调用函数时该参数不用传递,编译器自动传递。
前置 ++:返回 +1 之后的结果。注意:this
指向的对象函数结束后不会销毁,故以引用方式返回提高效率。
//...
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
//...
后置++
注意:后置 ++ 是先使用后 +1,因此需要返回 +1 之前的旧值,故需在实现时需要先将*this
保存一份,然后给*this
+1。而 tmp 是临时对象,因此只能以值的方式返回,不能返回引用。
//...
// 后置++
Date operator++()
{
Date tmp(*this);
*this += 1;
return tmp;
}
//...
注:对于内置类型,使用前置++ 或后置 ++ 的区别不大。但对于自定义类型需要 ++时,建议使用前置 ++,因为使用后置 ++ 会多两次拷贝构造。
前置 - -
//...
// 前置--
Date operator--()
{
*this -= 1;
return *this;
}
//...
后置 - -
//...
// 后置--
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
//...
//...
// 日期 - 日期
int operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
// 注意:不要写出 d > *this
// 这样子写涉及引用权限的放大和缩小
// 后面的内容会讲解这个知识点
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
//...
现在日期类的功能已经实现了差不多了,现在就还差cin >>
和cout <<
的功能了,我们现在把这两个功能实现一下。
学习这个之前,我们需要知道一些前置知识。cin
是头文件istream
里的对象,而cout
是头文件ostream
里的对象。>>
是流提取运算符,而<<
是流插入运算符。istream
和ostream
也是类。
知道了这些,我想问大家一个问题:为什么cin
和cout
能够自动识别类型呢?其实这背后的原理就是函数重载和运算符重载。如下图所示:
operator<<
cin
和cout
默认就支持内置类型的函数重载和运算符重载,而不支持自定义类型的函数重载和运算符重载。这时候,就要发挥我们智慧的大脑了,自己动手丰衣足食。
//...
// d1 << cout
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
//...
上面operator<<
虽然可以实现输出日期的功能,但是和我们的使用习惯相反且不具有可读性。所以一般情况下,流提取重载和流插入重载都不会定义在类中。那怎么解决这个问题呢?我们可以将operator<<
定义成全局函数。这样又会带来应该问题,就是封装的问题。如果我们将operator<<
定义成全局函数,就需要将成员变量的属性改成公有public
。那我们先试试先吧,实现出来再看看还有没有更好的办法。
//...
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
//...
这个operator<<
的写法还有可以优化的地方。因为这个写法不能输出多个日期,那么将函数的返回改成ostream&
就可以了。
//...
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//...
虽然operator<<
这样子写解决了可读性和使用习惯的问题,但是又带来了更大的问题——封装的问题。那怎么解决呢?接下来,我们的友元就要上场表演了。
注:如果不借助友元,operator>>
也会出现上述的问题。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。 通俗来讲,就是声明这个函数是友好的,不会直接修改成员变量的值。
注:友元函数将会在类和对象下详细讲解。友元声明可以在类中的任意位置。
//...
class Date
{
//友元声明
friend ostream& operator<<(ostream& out, const Date& d);
//...
}
// cout << d1 operator<<(cout, d1)
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
有了友元函数,就算类的成员变量的属性为私有private
,也可以在类外访问类的成员变量了。
注:operator<<
运算符重载很有可能会经常被调用,那么我们可以将它改成内联函数。友元声明时不需要加上inline
,定义的时候需要加上inline
。
有了operator<<
运算符重载,那么成员函数Print
也就可以退休了。关于operator<<
运算符重载的知识点就这些了,我们现在来学习一下operator>>
运算符重载。
operator>>
//...
Date
{
//友元声明
friend istream& operator>>(istream& in, Date& d);
//...
}
// cin >> d1 operator>>(cin, d1)
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
注:有关operator>>
的知识点和operator<<
的知识点相似。
operator=
运算符为赋值运算符重载,那我们来看一下赋值运算符重载的格式。
赋值运算符重载格式
*this
:要复合连续赋值的含义注:赋值运算符重载既是默认成员函数,又是运算符重载。
//...
Date& operator=(const Date& d)
{
if(this != &d) // 避免 d1 = d1 的情况
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 返回左操作数
}
//...
注:赋值运算符重载的参数也可以是类对象,但传参的时候需要调用拷贝构造函数。返回值为类对象的引用,既可以提高返回的效率(值返回时会调用拷贝构造函数),又可以实现连续赋值。赋值运算符重载的引用返回一般不用 const 修饰,因为有可能该对象还要修改。
赋值运算符重载是一个默认成员函数,如果自己不写编译器会自动生成。那我们现在就不写赋值运算符重载,看看会有什么情况发生。
将赋值运算符重载屏蔽后,再将程序运行起来,我们可以发现还是可以完成赋值的。那为什么呢?其实是,如果我们不写赋值运算符重载,对于内置类型会完成值拷贝,对于自定义类型会调用该自定义类型的运算符重载。 所以如果我们不写日期类的赋值运算符重载,也能完成拷贝。
那如果栈Stack
和队列MyQueue
不写赋值运算符重载,又会发生什么呢?
上面的栈Stack
还没有写赋值运算符重载,然后运行起来就崩溃了。因为栈Stack
的成员都是内置类型,那么编译器生成的赋值运算符重载会完成值拷贝。那么赋值过后,st1 和 st2就都执行了同一块空间。到了析构的时候,就会对同一块空间析构多次,然后程序就崩溃了。而且还会带来一个很严重的问题就是内存泄漏。
对于栈Stack
来说,编译器默认生成的赋值运算符重载不能用,那么就需要我们自己写了。因为 st1 和 st2 的空间大小情况不清楚,所以我们先把 st1 原来的空间先释放掉,再申请一块和 st2 一样大的空间,然后再把数据拷贝过去。
//...
Stack& operator=(const Stack& st)
{
if(this != &st) // 避免 st1 = st1 的情况
{
free(_a);
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
//...
日期类不需要自己写赋值运算符重载,栈Stack
需要自己写赋值运算符重载,那队列MyQueue
需不需要自己写呢?我们一起来看一下。
可以看到,队列MyQueue
也不需要写赋值运算符重载。那我们来总结一下什么类需要写赋值运算符重载。像拷贝构造函数一样,如果该类需要写析构函数,那么就需要写赋值运算符重载;如果该类不需要写析构函数,那么就不需要写赋值运算符重载。
赋值运算符只能重载成类的成员函数不能重载成全局函数原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
将const
修饰的成员函数称之为const
成员函数,const
修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。const
修饰成员函数时,只会修饰 this 指针,并不会修饰成员函数的其它参数。
知道了const
修饰成员函数,我们现在来看一个例子:
可以看到,当用 const
修饰一个日期时,该日期就不能再修改了。当 d2 调用Print
函数时, 编译器会将 d2 的地址转换成 this 指针,该 this 指针的类型为Date* const
,相当于 this 不能被修改,但是 this 指针指向的空间里的内容可以修改。又因为 d2 用了const
修饰,&d2 的类型是const Date*
,所以这就涉及指针权限的权限放大和缩小。
那如何解决呢?就是在Print
函数后面加上个const
关键字修饰。
类中的成员函数的参数很多都需要用const
修饰,也不会修改 this 指针指向的内容,所以很多成员函数都需要用const
来修饰。那么什么成员函数要用const
修饰呢?不会修改 this 指针指向的内容的成员函数就需要用const
来修饰。大家可以给以上写的成员函数加上const
。
小小的问题
不可以。const 对象被视为只读对象,意味着它们不会修改成员变量的值。因此,const 对象只能调用 const 成员函数,以确保不会对对象的状态进行修改。
可以。非 const 对象可以调用 const 成员函数。这是因为 const 成员函数承诺不会修改对象的状态,所以对于非 const 对象来说,调用 const 成员函数是安全的。
不可以。const 成员函数被设计为不修改对象的状态,因此在 const 成员函数内部不能调用非 const 成员函数。因为非 const 成员函数可能会修改对象的状态,这可能会违反了 const 成员函数的承诺。
可以。非 const 成员函数可以调用 const 成员函数。这是因为非 const 成员函数可以修改对象的状态,但在其内部调用 const 成员函数是安全的,因为 const 成员函数保证不会修改对象的状态。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。不过,我们也把这两个函数实现一下。
//...
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
//...
如果这两个函数不写也没有什么问题,编译器生成也够用。如果你不想让别人拿到类对象的地址就可以像下面这样写。
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return nullptr;
}
//...
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!