目录
类的6个默认成员函数
如果一个类里面什么都没有(没有成员对象及函数),那称其为空类。但是空类也不是真的什么都没有,编译器会默认自动生成6个成员函数(隐式的),如果我们自己在类中实现了这些默认成员函数,编译器就不会生成。
这段话隐含的意思就是:有的类需要我们自己写默认成员函数(因为有些情况编译器自动生成的不方便使用),有的类则不需要,编译器默认生成的就很方便。
第一个默认成员函数———构造函数
声明一下:关于构造函数的初始化列表在类和对象(下)中讲解。
在之前用C语言写栈的时候,我们经常会犯一个错误:忘记调用 初始化函数Init 和 销毁函数Destory。不仅是我们,设计该语言的大佬也会,所以在构架C++的时候,就引入了构造函数和析构函数。
构造函数是在我们创建变量的时候调用的函数,帮助我们自动实现初始化。注意:构造函数不是创建变量,变量还是交给我们自己创建,它只是自动完成变量初始化。
构造函数的特征
1、构造函数名与类名相同。
2、编译器在对象创建时自动调用,上面讲了。
3、在对象生命周期只调用一次,初始化只会有一次。
4、无返回值!注意:这里无返回值不是指void,而是不写返回值!
5、支持函数重载。
还是以日期类为例:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1) //函数名与类名相同
{
_year = year; //没有返回值
_month = month;
_day = day;
}
void Print(/*Date* const this*/)
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1);
Date d2(2022, 1, 1);
return 0;
}
构造函数调用的时候如上所示,参数写在对象后面。(C++规定)
如果是无参的,写成:
Date d1;
不能写成:
Date d1();
下面这种写法,编译器容易认为Date是返回类型,d1是函数名,()里是调用,会报警告
前面说了,如果我们没有写构造函数,那么编译器就会自己实现一个,那它是如何实现的呢?
我们来看一下。
其实这是C++设计的一个失败之处,是一个比较麻烦的坑。
还是以上面的日期类为例,如果不写构造函数,看看编译器默认生成什么样:
我们发现,编译器自动调用的构造函数生成的是随机值。
我们来看看这是什么情况。
C++将类型分为两种,内置类型和自定义类型。
内置类型:int、char、double.......指针(自定义类型指针也算,如:struct node*)
自定义类型:class、struct、stack(栈)、queue(队列)、自定义结构
对于内置类型,C++是不处理的,也就是说初始化为随机值。(像上面例子一样)
对于自定义类型,C++会调用它的默认构造函数。(自定义类型也有自己的构造函数)
class A
{
public:
A()
{
_a = 0;
}
private:
int _a;
};
class Date
{
public:
void Print(/*Date* const this*/)
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d1;
return 0;
}
这里_year , _month, _day 都是内置类型,所以初始化为随机值;而_aa是自定义类型,调用它的构造函数,初始化为0 。
这里再说一个容易被误解的概念:
很多人认为默认构造函数是编译器自动生成的构造函数,其实不是。
有3种构造函数都属于默认构造函数:
1、上面说的编译器自动生成的。
2、无参的。
3、参数全缺省的。(全缺省和无参的不能一起出现,会有二义性错误)
来看下面场景:
这里为什么报错报的是不存在默认构造函数,因为我们自己写了构造函数,编译器不会自动生成;并且没有全缺省和无参的构造函数。
第二个默认成员函数———析构函数
析构函数比较简单,它与构造函数相对,一个初始化,一个完成清理。
析构函数类似于之前学的free,它不是销毁对象,而是清理对象的空间。
与构造函数一样,对于内置类型,编译器自动调用的析构函数也不处理。对于自定义类型,则会调用它的默认析构。
在对象生命周期结束时,编译器会自动调用析构函数。如下这种情况需要注意:
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack( )" << endl;
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack( )" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void StackPush(){}
private:
int _top;
int _capacity;
DataType* _a;
};
void func()
{
Stack n;
}
int main()
{
func();//对象生命周期到了,就调用析构函数
Stack d1;
return 0;
}
.如果将对象创建在func函数内部,随着调用func函数结束,对象生命周期结束,那么编译器就自动调用析构函数。
所以说,什么时候编译器调用析构函数,关键是看对象的生命周期结束。
构造函数与析构函数小结
一、
构造函数和析构函数需不需要我们自己写,取决于具体情况(编译器自动生成的方不方便使用)。
就算类里面都是自定义类型,编译器自动生成的构造函数会处理,有的情况下也需要我们自己写。
具体写不写,是看有没有需求。
二、
之前讲了,C++编译器对内置类型不处理是早期遗留下来的坑,但是因为语言向前兼容的特性,只能选择后期弥补而不好修改以前的东西。
因此,在C++11针对内置类型不处理问题发了一个补丁。
可以在类里面声明成员变量类型的时候,给成员变量一个缺省值(类似函数参数缺省值)
这样在编译器在调用自动生成的构造函数时,就会按照这个缺省值来初始化。
class Date
{
private:
int _year =0;
int _month = 0;
int _day = 0;
};
int main()
{
Date d1;
return 0;
}
注意:这里是给缺省值,而不是直接初始化,不要将概念混淆了。
在自己写了构造函数的情况下,如果有的成员变量没有初始化,那也可以用缺省值初始化。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
//_day = day;
}
private:
int _year =0;
int _month = 0;
int _day = 0;
};
int main()
{
Date d1;
return 0;
}
缺省值是好用,可这里毕竟是声明,不会开空间,它只会初始化,开空间还需要创建对象。
既然如此,那能不能这样:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year =0;
int _month = 0;
int _day = 0;
int* _a = (int*)malloc(sizeof(int));
};
int * _a = (int*) malloc( sizeof (int) ); 能不能这样给缺省值?
有人说,既然是声明,不能开空间,那肯定不能这么写。
恰恰这里是可以的,这确实也是不太好理解的一个点。 但这里实质上还是给缺省值,不是在这个位置开空间。当对象创建后_a变量初始化时才会调用这里的malloc开空间,而没创建对象时只是给一个形式,说明一下给的缺省值的大小。
第三个默认成员函数———拷贝构造函数
构造函数是创建对象时的初始化,而拷贝构造函数可以理解为拷贝一个对象初始化的值。
拷贝构造是构造函数的重载形式,并且它的参数必须是同类类型对象的引用。
以日期类为例:
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,1,1);
Date d2(d1);
return 0;
}
如上是日期类的写法。看一下细节:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date d1(2022,1,1);
Date d2(d1);
创建d1对象后,想拷贝一份给d2,因为d1是自定义类型,调用拷贝构造函数Date,d是d1的别名,直接访问成员变量赋值给d2的成员变量。
这里都好理解,问题是如果不用引用,直接传值为什么不行?
看报错:非法的复制(拷贝)构造函数。 如果不加引用,直接传值的话,会出现无限递归拷贝的情况。
将实参d1传值给形参d,需要拷贝一份d1给临时变量,临时变量再给到d,而由于d1是自定义类型, 拷贝d1又需要拷贝构造,那不成了无限递归了吗?会一直套娃下去。
为什么传值就需要拷贝构造呢?以前写的很多函数都有传值传参啊。
这是因为内置类型(int、char)编译器很熟悉如何拷贝,需要拷贝多少字节。
但是这里是自定义类型,有很多难以直接拷贝的类型(如:链表、树),所以需要用到拷贝构造。
每一次实参传值给形参,都需要拷贝构造一份实参再继续下去,就形成了无限递归,因此编译器禁止这种行为。
用引用则不会产生这种情况,因为引用d相当于是d1的别名,直接操纵d1的成员变量。
此外,在引用前加const是为了防止前后位置写反(d._year = _year的情况)。这样编译器能检查出来。
拷贝构造函数也属于默认成员函数,自然也存在编译器自动生成的情况。
还是分内置类型和自定义类型讲解:
一、内置类型
内置类型是按字节方式拷贝的,类似memcpy函数。
以日期类为例:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1);
Date d2(d1);
return 0;
}
我们没写拷贝构造函数,编译器自动生成的可以使用。
但是下面这种情况,就不行了:
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(DataType x)
{
_a[_top++] = x;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int _top;
int _capacity;
DataType* _a;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(st1);
return 0;
}
栈里面不写拷贝构造,编译器默认生成的就不行。
有的人会说,这里不是完成拷贝了吗,好像也没问题啊?
当程序调试到free的时候,程序会崩溃,这里存在一大问题,拷贝的指针_a和st1的_a地址相同,
这意味着它们指向同一个地方。
当对象生命周期结束,编译器调用析构函数~Stack() 清理空间时,先free的是st2,也就是st2指向的空间,此时还没有问题,
_a确实置空了,free没问题。但是调用~Stack( )清理st1的空间时,因为st1,st2指向同一块空间,已经被清理过了,所以再清理的时候就崩溃了。
日期类的那种拷贝方式,叫做浅拷贝。它只拷贝表层的东西,也就是只将数值完全拷贝过去,而不管两个地方是否存在联系。
而栈这块需要的拷贝叫做深拷贝,它虽然拷贝了数值,但是拷贝者与被拷贝者是独立、互不干扰的,所指向的位置也是不一样的。
下面是深拷贝的实现:
Stack(const Stack& st)
{
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(DataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
深拷贝下,两者地址就不同了,不会产生上述错误。
根据经验总结:需要写析构函数~Stack( )的,需要深拷贝;
不需要写析构函数~Stack( )的,编译器默认的浅拷贝即可。
二、自定义类型
自定义类型调用其自身拷贝构造函数(这里Stack的拷贝构造函数)完成拷贝,是不需要我们自己写的。
用自定义类型验证一下:
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
Stack(const Stack& st)
{
cout << "stack(const stack& st)" << endl;
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = st._top;
_capacity = st._capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int _top;
int _capacity;
DataType* _a;
};
class MyQueue {
public:
void push(int x)
{
_pushST.Push(x);
}
private:
Stack _pushST;
Stack _popST;
size_t _size = 0;
};
int main()
{
MyQueue q1;
MyQueue q2(q1);
return 0;
}
如果没有Stack的拷贝构造函数就会出错。
MyQueue不需要写析构函数,因此也没必要写拷贝构造函数,编译器默认的足够。
所以,是否需要写拷贝构造函数,一是看是否需要写析构函数,而是看对象类型和具体场景。
第四个默认成员函数———赋值运算符重载
在看这个默认成员函数之前,先来理解一下运算符重载。
运算符重载
对于内置类型,运算符可以直接使用(如:int a = 1,b = 2; a+b;)
如果是自定义类型想要直接使用运算符,就需要用到运算符重载(与函数重载没有关系)
以日期类为例,如果想比较创建的两个日期对象的年月日大小,或者计算10天以后的日期,就可以用这一块的运算符重载,不仅高效而且代码阅读性较强。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,1,1);
Date d2(2022,1,1);
return 0;
}
一个熟悉的日期类,先从判断两个日期对象相等开始,d1 == d2,要写出它的运算符重载函数,
要先明确它的性质:
判断是否相等,返回值类型可以用bool,函数名是operator后的运算符,参数必须是类类型的。
代码如下:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
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(2022,1,1);
Date d2(2022,1,1);
d1 == d2;
return 0;
}
为了便于观察结果,可以写成打印的形式:
cout<<(d1 == d2)<<endl;
注意:这里一定要加括号(),如果不加,<<也是运算符,优先级高于 == ,cout会先和d1结合。
这里本质上是将d1 == d2转化成函数 operator==(d1, d2);
cout << operator==(d1, d2) << endl;
所以可以写成显示调用,但是一般不这么写,为了增强代码阅读性,还是写成d1 == d2
运行程序,后会发现一个问题,
成员函数是私有的,无法访问。将成员变量改为公有显然不是一个好方法。
这时我们可以将函数放到类里面,这样就不存在公有私有的问题了,但是此时又报错了,
显示函数的参数多了,我们传参传两个,用两个参数接收怎么会多呢?————因为有隐含的this指针。这里正因为有这个隐含的this指针作了一个参数,所以会显示参数过多。
因此,我们拿掉一个参数,就传一个,this指针相当于d1,d就是d2的别名,代码如下:
bool operator == (const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,1,1);
Date d2(2022,1,1);
cout<<(d1 == d2)<<endl; 转化成 d1.operator==(d2)
cout << d1.operator==(d2) << endl; 显示调用写法
return 0;
}
PS:这里传参要引用传参,因为传值传参会多一个拷贝构造(自定义类型默认),并且比较两个对象不做修改,用const修饰更好。
等于写好了,看一下大于的写法吧(大同小异):
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;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,1,3);
Date d2(2022,1,2);
cout<<(d1 == d2)<<endl;
cout << (d1 > d2) << endl;
return 0;
}
类似的,也可以完成其他运算符重载。
注意:这里有一种简便写法,以大于等于>=为例,既然上面已经分别写好了等于和大于的重载,那么大于等于可以直接复用:
bool operator>=(const Date& d)
{
return *this > d || *this == d;
}
*this 相当于d1,d相当于d2,return d1> d2 || d1 == d2 直接调用了上面两个重载,用operator调用也可以。
比较的运算符重载完成后,我们来看一下 " +=" 是如何操作的。
首先,是函数名:operator+= 参数是int day 返回类型则是Date(目的是得到N天后的日期)
这里情况比较多,要分类讨论了:
大致是这三种:1、只需改变日 2、日满进月 3、日满进月、月满进年
在日满进月的时候有个问题,每个月天数不一样,特别是2月还有平年闰年之分,因此,我们先写一个能得到月份天数的函数:
class Date
{
public:
int GetMonthday(int year, int month)
{
static int monthday[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 monthday[month];
}
这有很多写法,这种还是比较简单的。
接下来就是实现图中的逻辑了,如何进位:
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthday(_year,_month))
{
_day -= GetMonthday(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month -= 12;
}
}
return *this;
}
注意这里是引用返回,因为我们写的是 += ,会改变日期,如果不用引用返回,*this出作用域之前会先拷贝,用引用返回就省了拷贝。
那不改变日期的显然就是 " + " 了,那么就要用到拷贝构造,不需要引用返回了。
还是一样,复用+=的函数:
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
int main()
{
Date d2(2022,1,2);
d2 + 1000;
return 0;
}
拷贝构造一份d2给ret,调用上面的+=重载,返回ret,ret出了作用域就销毁,不改变d2.
其他运算符重载也是一样,重点是活用复用!没必要都写,比如写了 >= 就可以用 ' ! ' 得出 <
此外,因为在类里的成员函数都默认是内联函数,所以我们可以将不频繁调用的以及较长的函数声明和定义分离,以此避免内联。
以下是对一些常用运算符的重载————
Date.h
#pragma once
#include<iostream>
using namespace std;
class Date
{
//友元声明
friend istream& operator>>(istream& in, Date& d);
friend inline ostream& operator<<(ostream& out, const Date& d);
public:
void PrintDate()
{
cout << _year << " " << _month << " " << _day << endl;
}
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;
}
// 赋值运算符重载
// d3 = d2 -> d3.operator=(&d3, d2)
Date& operator=(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// >运算符重载
bool operator>(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
//输入日期
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
//打印日期
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
Date.cpp
#include"Test.h"
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
return *this;
}
bool Date::operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool Date::operator != (const Date& d)
{
//复用
return !(*this == d);
}
bool Date::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;
else
return false;
}
bool Date::operator >= (const Date& d)
{
return *this > d || *this == d;
}
bool Date::operator < (const Date& d)
{
return !(*this > d || *this == d);
}
bool Date::operator <= (const Date& d)
{
return !(*this > d);
}
int Date::GetMonthDay(int year, int month)
{
static int MonthDay[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;
}
return MonthDay[month];
}
Date& Date::operator+=(int day)
{
_day += day;
while(_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month > 12)
{
++_year;
_month -= 12;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
if(_month >= 2)
_day += GetMonthDay(_year, _month-1);
else
{
_day += GetMonthDay(_year-1, 12);
}
--_month;
if (_month <= 0)
{
--_year;
_month = 12;
}
}
return *this;
}
Date Date::operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//日期 - 日期
//方法一 :
int Date::operator-(const Date& d)
{
Date max = d;
Date min = *this;
int flag = 1;
if (max < min)
{
max = *this;
min = d;
flag = -1;
}
int tmon_day = 0;
int subyear = d._year - (*this)._year;
for (int i = min._year; i <= max._year; ++i)
{
if ((((i % 4 == 0) && (i % 100 != 0))
|| (i % 400 == 0)) && flag == 1)
{
tmon_day++;
}
else if ((((i % 4 == 0) && (i % 100 != 0))
|| (i % 400 == 0)) && flag == -1)
{
tmon_day--;
}
}
int total_day = subyear * 365 + tmon_day;
int MonthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
for (int i = 1; i <= (*this)._month; ++i)
{
total_day -= MonthDay[i];
}
if ((*this)._month >= 2 &&
(((*this)._year % 4 == 0) &&
((*this)._year % 100 != 0))||
((*this)._year % 400 == 0))
{
total_day -= 1;
}
total_day -= (*this)._day;
for (int i = 1; i <= d._month; ++i)
{
total_day += MonthDay[i];
}
if (d._month >= 2 &&
((d._year % 4 == 0) &&
(d._year % 100 != 0)) ||
(d._year % 400 == 0))
{
total_day -= 1;
}
total_day += d._day;
int x = total_day * flag;
return x;
}
//方法二 :
int Date::operator-(const Date& d)
{
Date max = d;
Date min = *this;
int flag = 1;
if (max < min)
{
max = *this;
min = d;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * abs(flag);
}
注意:有5个运算符不能重载 : 1 .* 2 :: 3 sizeof 4 ? :(三目操作符) 5 .
特别是第一个 .* 这不是只有一个 * 而是 .* 笔试中可能会问到,不要和解引用符号*混淆了。
还要注意一下前置++与后置++,为了区分它们,规定将其设置为函数重载,后置多给一个int参数,但是不使用,仅做区分。
后置++会比前置多拷贝两次,所以自定义类型应尽量使用前置++,内置类型则不会存在该问题,用两个中哪一个差别不大。
运算符重载讲完其实这个默认成员函数也基本讲完了,因为本质是一样的。
下面说一下赋值重载(d1=d2):
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
return *this;
}
注意这里和拷贝构造的区别:
拷贝构造(Date d2(d1)) 是拷贝已经存在的d1,用d1去初始化d2,也就是说是在d2创建的过程中拷贝; 而赋值重载(d1=d2),d1 和 d2都是原本就存在已经创建好的,用d2的内容覆盖d1,不是初始化。
那 Date d1 =d2是属于哪一种呢?
————是拷贝构造!不能光看符号形式,要理解它的意义更符合哪一种。很明显这里是在初始化的过程(Date d1),所以是拷贝构造。
赋值重载一定要给返回值,不能是void,因为有可能会连续赋值,d1=d2=d3这种,将d2=d3的返回值再作为 = 的两个操作数,所以必须是Date&。
既然赋值重载是默认成员函数,那么也有编译器默认实现的情况,我们不写,会默认生成一个,也和拷贝构造一样,如果没有动态开辟空间,那么默认生成的赋值重载函数就可以使用,但是如果有动态开辟空间,那么就需要自己写了。
还是用栈那块来举例:
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(DataType x)
{
_a[_top++] = x;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int _top;
int _capacity;
DataType* _a;
};
现在我们没有写赋值重载函数,看看编译器自动生成的会怎样吧:
#include"stack.h"
int main()
{
Stack st1;
Stack st2;
st2 = st1;
return 0;
}
这里崩溃了,存在两大问题:
1、内存泄漏 2、free重复清理同一块区域
这里的情况和拷贝构造类似:
首先创建出st1和st2 ,此时它们各自指向各自的空间,st1=st2,赋值重载,st1指向st2,st1自己的空间最后没人释放,st2的空间被自己和st1free了两次。
要解决这个问题,最好的方式是先free st1的空间,在重新按st2的空间大小开辟一块新空间拷贝。
Stack& operator=(const Stack& st)
{
free(_a);
_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = st._capacity;
}
和拷贝构造代码几乎一样,就多了一个free st1的空间。
这样还有一个问题,当自己给自己赋值,st1=st1的时候,会出现拷贝随机值的问题。
因为开头的free会free st1的空间(包括里面的东西),因此拷贝会出现随机值。
总结:与拷贝构造一样,赋值重载函数需要我们自己写的情况是:有开辟动态空间(malloc、free),需要写析构函数的情况下。
可以用编译器默认的情况则相反。
第五、六个成员函数————取地址重载
这两个函数非常简单,用的也很少,并且一般来说编译器自动会生成,很少存在需要我们自己实现的情况,这一块主要结合权限的放大缩小来理解。
日期类中,用print函数打印一个对象时,如果在初始化时加了const修饰,就将对象属性修改成了只读(原本是可读可写的)。
const Date d1;
d1.Print();
这时候传参类型就是 const Date*,而Print函数接收则是Date* this,前者是只读,后者是可读可写,也就是权限扩大了,因此编译器会报错。
此时想修改权限调整this指针,但是this指针规定是隐含的,无法显示操作,所以规定在函数后面加const表示对this指针的修饰。
void PrintDate() const
{
cout << _year << " " << _month << " " << _day << endl;
}
总而言之:权限可以平移和缩小,但是不能扩大。
调用函数的时候,const修饰的可以被const和非const调用,反过来就不行。
凡是内部不改变成员变量的(也就是*this对象数据的),这些成员函数都应该加const
像日期类中的比较类成员函数,+ - == != 都可以加, 要改变成员变量的不能加,如 += , -=
取地址重载函数
这了解上面的基础上,我们来看最后两个默认成员函数,取地址重载。
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
非常简单,其实就是对取地址&运算符重载,上面的是没有加const修饰的对象,下面则是加了const修饰的对象。
当然,一般不需要我们自己实现,编译器会实现的。
如果有特殊场景特别需求(比如不允许返回地址)需要我们自己实现一下。
以上就是关于6个默认成员函数的分享,谢谢大家支持!