在上篇我们讲解了类与对象的基础框架,中篇我们将讲解类与对象的基本内容,即类的六个默认成员函数。
如果一个类中什么成员都没有,简称为空类。
问题: 空类中真的什么都没有吗?
答案是: 空类不是什么都没有,空类,编译器会自动生成6个默认成员函数。
tip:
①默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
②显式实现就是自己定义。
③这6个默认成员函数,前四个我们需要重点学习,最后两个了解即可。
括号匹配问题
用C语言实现括号匹配问题,代码示例:
//栈的元素类型
typedef char STDataType;
//栈的类型
typedef struct Stack
{
STDataType* a;//指向堆区开辟的数组
int top;//栈顶指针
int capacity;//栈的容量
}ST;
//栈的初始化
void STInit(ST* pst)
{
//pst一定不为空,断言
assert(pst);
pst->a = (STDataType*)malloc(sizeof(STDataType) * 4);
//判断是否申请空间成功
if(NULL == pst->a)
{
//打印错误信息,并退出
perror("STInit::malloc");
return ;
}
//初始化栈的容量和栈顶指针
pst->top = 0;//指向栈顶位置的下一个
pst->capacity = 4;
}
//栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
//清理栈变量
//1、释放在堆区申请的空间
free(pst->a);
pst->a = NULL;//释放之后并不会改变pst->a,防止野指针生成置为空
pst->top = 0;
pst->capacity = 0;
}
//判断是否为空栈——空栈返回真
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
//获取栈的元素个数
int STSize(ST* pst)
{
assert(pst);
//断言是否为空栈
assert(!STEmpty(pst));
return pst->top;
}
//入栈——即数组尾插
void STPush(ST* pst ,STDataType x)
{
assert(pst);
//先判断是否需要扩容
if(pst->top == pst->capacity)
{
//防止扩容失败,先用一个临时变量保存扩容后的空间地址
//扩容一般按倍数扩
STDataType* tmp = (STDataType*)realloc(pst->a,sizeof(STDataType) * pst->capacity * 2);
//判断扩容是否成功
if(NULL == tmp)
{
//扩容失败打印错误信息,并退出
perror("STPush::realloc");
return;
}
//扩容成功
pst->a = tmp;
pst->capacity *= 2;
}
//入栈——即数组的尾插
pst->a[pst->top] = x;
pst->top++;
}
//出栈——即数组尾删
void STPop(ST* pst)
{
assert(pst);
//断言栈不为空
assert(!STEmpty(pst));
//出栈——即数组尾删
pst->top--;
}
//获取栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
//断言站不为空
assert(!STEmpty(pst));
return pst->a[pst->top - 1];
}
//利用栈先进后出的特点——遇到'(','{','[',进栈,遇到')','}',']'出栈
bool isValid(char* s)
{
//定义栈
ST st;
//初始化栈
STInit(&st);
int i = 0;
//字符串进栈,判断是否匹配
for(i = 0;s[i] != '\0';i++)
{
//判断是否入栈
if(s[i] == '(' || s[i] == '{' || s[i] == '[')
{
//入栈
STPush(&st,s[i]);
}
//不是入栈就是出栈
else
{
//判断栈是否为空
if(STEmpty(&st))
{
//栈为空,不匹配
//1、记得销毁栈
STDestroy(&st);
//2、返回假
return false;
}
//栈不为空
char tmp = STTop(&st);//保存栈顶元素
STPop(&st);//出栈
//判断栈顶元素是否匹配
if(tmp == '(' && s[i] != ')' ||
tmp == '[' && s[i] != ']' ||
tmp == '{' && s[i] != '}'
)
{
//不匹配
//1、记得销毁栈
STDestroy(&st);
//2、返回假
return false;
}
}
}
//字符串匹配结束,
//在释放之前要保存匹配的真假
bool ret = STEmpty(&st);//如果栈为空则全部匹配成功,不为空则匹配失败
//记得销毁栈
STDestroy(&st);
return ret;
}
tip:
①用C语言实现的话,你会发现一个栈类型ST,我们定义了一个栈变量后,并不是就可以直接使用了,要通过STInit函数给栈变量初始化,才能使用。
② 在栈变量出作用域(销毁)时,就要通过STDestroy函数清理栈变量资源。
③但是实际中我们a.初始化和销毁经常忘记(①初始化忘记虽然会报错,但是每创建一个栈对象都要调用STInit函数初始化太麻烦了;②栈对象出了作用域忘记调用STDestroy函数清理资源,就会造成内存泄漏,但是不报错,这就严重了);b.销毁有些地方写起来很繁琐。 如下图所示:
④我们C++祖师爷也遇到了这些问题,对象都要初始化和清理,这时祖师爷就想能不能自动初始化和清理,所以祖师爷引入了两个默认成员函数,构造函数——对象实例化时自动初始化;析构函数——对象出了作用域之前就自动清理对象资源。 有了构造和析构,就不怕忘记写初始化和清理了,也简化了。
构造函数 是一个特殊的成员函数:
①名字与类名相同。
②创建类类型对象时由编译器自动调用, 以保证每个数据成员都有一个合适的初始值。
③在对象整个生命周期内只调用一次。
代码示例:
#include
using namespace std;
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}
//栈对象的初始化
/*void Init(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}*/
// 其他方法...
void Destroy()
{
if (_a)
{
free(_a);
_a = NULL;
_capacity = 0;
_top = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
//实例化对象s1
Stack s1;
s1.Destroy();
//实例化对象s2
Stack s2;
s2.Destroy();
return 0;
}
运行结果:
tip:构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。 因为不管是局部变量,还是全局变量的创建都是系统自动创建的,是系统的事情。例如局部变量在栈帧里面,栈帧创建时变量自动创建,栈帧结束时自动销毁。
tip:不能把构造函数当成普通函数,它是特殊函数。
其特征如下:
①函数名与类名相同。
②无返回值。(注意,也不需要写void)
③对象实例化时编译器自动调用对应的构造函数。
④构造函数可以重载。(重载:构造函数虽然是特殊函数,但是也是函数,所以只要构造函数的形参列表不同即可重载。)
代码示例:
class Stack
{
public:
//全缺省构造函数——默认构造函数
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}
//构造函数:用数组初始化
Stack(int* a, int n)
{
cout << "Stack(int* a, int n)" << endl;
_a = (int*)malloc(sizeof(int) * n);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
//将数组的值拷贝到栈
memcpy(_a, a, sizeof(int) * n);
_capacity = n;
_top = n;
}
// 其他方法...
~Stack()
{
if (_a)
{
free(_a);
_a = NULL;
_capacity = 0;
_top = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
//实例化对象s1
Stack s1;//不传参,调用默认构造函数
//实例化对象s2
int a[] = { 1, 2, 3 };
Stack s2(a, sizeof(a) / sizeof(int));//用数组初始化
return 0;
}
tip: 如上代码,我们发现构造函数的调用与普通函数不一样!
构造函数是实例化对象时自动调用的,a.如果实例化时不给参数,不能在后面添加括号;b.实例化时给参数,才可以添加括号。
⑤如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。
代码示例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;
};
int main()
{
//不传参,调用无参的默认构造函数
Date d1;//报错,没有合适的默认构造函数可用,因为我们显示定义了一个构造函数
return 0;
}
代码示例2:
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;
};
int main()
{
//不传参,调用无参的默认构造函数
Date d1;//正确,当我们将类中的构造函数屏蔽后,没有显示的构造函数,编译器会生成一个无参的默认构造函数
return 0;
}
⑥既然我们不显式定义构造函数,编译会生成一个无参的默认构造函数,那我们还显式定义干嘛?都让编译器做了不更好?
我们先来看一段代码,我们不显式定义构造函数,让编译器生成,观察调用编译器默认生成的构造函数初始化后对象的值。
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用编译器生成无参的默认构造函数
Date d1;
//打印观察,初始化后的对象
d1.Print();
return 0;
}
我们发现调用编译器生成的默认构造函数初始化后对象依旧是随机值,这是为什么呢?
答案是:编译器生成的默认构造函数——a.内置类型不做处理(有些编译器可能会去处理内置类型,但是那是个性化行为,不是所有编译器都会处理,所以建议就算你的编译器会处理内置类型,你也要当成内置类型都不处理。例如VS这个系列的集成开发器,VS2019添加一个自定义类型,其他内置类型也被处理了,VS2013就不会处理。);b.自定义类型会去调用它的默认构造函数(注意:自定义类型如果没有默认构造函数,会报错!)。 这里应该是祖师爷的失误,应该都初始化的。
tip:C++把类型分为内置类型(基本类型)和自定义类型。a.内置类型:语言本身提供的数据类型,例如:int、char、double、指针(包括任意类型的指针)等等;b.自定义类型:用class、struct、union等自己定义的类型。
注意:C++11中针对内置类型成员不初始化的缺陷,打了个补丁,即:内置类型成员变量在类中声明时可以给缺省值。
代码示例:
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//注意是C++11支持,这里不是初始化,因为还没有开空间
//这里的默认的缺省值,是给编译器生成的默认构造函数使用的
int _year = 2001;
int _month = 1;
int _day = 1;
//自定义类型,编译器生成的默认构造函数会去调用它的默认构造函数
Stack _s;
};
int main()
{
//调用编译器生成无参的默认构造函数
Date d1;
//打印观察,初始化后的对象
d1.Print();
return 0;
}
⑦默认构造函数: 无参构造函数、全缺省构造函数、编译器默认生成的构造函数都称为默认构造函数。注意:默认构造函数只能有一个!
代码示例:
class Date
{
public:
//无参的构造函数——默认构造函数
Date()
{
_year = 2001;
_month = 1;
_day = 1;
}
//全缺省的构造函数——默认构造函数
Date(int year = 2001, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 2001;
int _month = 1;
int _day = 1;
};
int main()
{
//不传参调用的就是默认构造函数
Date d1;
d1.Print();
return 0;
}
混淆知识:区分默认成员函数与默认构造函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员;析构函数释放对象使用的资源,销毁对象的非static数据成员。
析构函数:对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。 注意:析构不是完成对对象本身的销毁,对象的销毁工作是由编译器完成的。
①析构函数名是在类名前加上字符~。
②无参数无返回值类型。(因为没有参数,自然析构函数不存在重载。)
③一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
④对象生命周期结束时,C++编译系统会自动调用析构函数。
代码示例:
class Date
{
public:
//默认构造函数
Date(int year = 2001, int month = 1, int day = 1)
{
cout << "Date(int year = 2001, int month = 1, int day = 1)" << endl;
_year = year;
_month = month;
_day = day;
}
//析构函数
~Date()
{
//一般只有在堆区的内存资源需要我们自己清理
//这里我们只是观察对象销毁时,是否会自动调用析构函数
cout << "~Date()" << endl;
}
private:
int _year = 2001;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
return 0;
}
运行结果:实例化对象时自动调用构造函数初始化对象,对象销毁时自动调用析构函数清理对象。
⑤编译器自动生成的析构函数,与自动生成的构造函数一样——a.内置类型成员不做处理;b.自定义类型会去调用它的析构函数。
代码示例:
class Stack
{
public:
//全缺省构造函数——默认构造函数
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
void TestStack()
{
Stack s1;
}
int main()
{
TestStack();
return 0;
}
通过F10调试观察内置类型成员s1.a:
对象销毁前:
对象销毁后:
tip:内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。
总结:
例如当前日期是2023.12.5,我想知道100天之后日期是多少,又不改变当前日期,该怎么办呢?
答案是:创建一个与已存在对象一模一样的新对象——拷贝构造函数。
拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
①拷贝构造函数是构造函数的一个重载形式。
②拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
解析:
③若未显示定义,编译器会生成默认的拷贝构造函数。
代码示例1:Date类只需完成浅拷贝即可,所以不用写拷贝构造函数
class Date
{
public:
//默认构造函数
Date(int year = 2001, int month = 1, int day = 1)
{
cout << "Date(int year = 2001, int month = 1, int day = 1)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,12,6);
Date d2(d1);
return 0;
}
F10调试结果,如下图:
代码示例2:MyQueue类的成员都是自定义类型,编译器生成的默认构造函数会去调用其拷贝构造函数完成拷贝,所以不用写拷贝构造函数。
class MyQueue
{
private:
Stack _popst;
Stack _pushst;
};
int main()
{
MyQueue d1;
MyQueue d2(d1);
return 0;
}
问题:编译器生成的默认拷贝构造对内置类型也会做处理,完成值拷贝,那还需要自己显示实现吗?
代码示例3:Stack类有动态申请的资源
class Stack
{
public:
//全缺省构造函数——默认构造函数
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}
//析构函数——有动态申请的空间,所以需要显示定义析构,自己清理资源
~Stack()
{
cout << "~Stack()" << endl;
if (_a)
{
free(_a);
_a = NULL;
_capacity = 0;
_top = 0;
}
}
void Push(const int& data)
{
// CheckCapacity();
_a[_top] = data;
_top++;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
//调用编译器默认生成的拷贝构造函数——只完成浅拷贝
Stack s2(s1);
return 0;
}
④拷贝构造函数典型调用场景:
代码示例:
#include
using namespace std;
class Date
{
public:
//默认构造函数
Date(int year = 2023, int month = 12, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
//日期类没有申请资源,所以浅拷贝即可,
//这里只是观察拷贝构造函数的调用场景
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2001, 1, 1);
Test(d1);
return 0;
}
运行结果:
tip:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
为什么运算符对内置类型可以直接使用,对自定义类型不可以直接使用呢?
答案是:内置类型是祖师爷定义,所以当然知道该怎么运算。而自定义类型是我们自己定义的,祖师爷不知道该怎么运算,所以需要我们自己定义运算。
例如,要比较两个日期的大小,我们可以定义一个函数实现。
class Date
{
public:
//默认构造函数
Date(int year = 2023, int month = 12, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
//如果d1 > d2,返回真
bool Less(const Date& d1, const Date& d2)
{
if (d1._year > d2._year)
{
return true;
}
else if (d1._year == d2._year && d1._month > d2._month)
{
return true;
}
else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2023, 12, 11);
Date d2(2023, 12, 12);
cout << Less(d1, d2) << endl;
cout << Less(d2, d1) << endl;
int a = 3;
int b = 5;
cout << (a > b) << endl;
cout << (b > a) << endl;
return 0;
}
如上代码日期类的比较我们都要调用Less函数,而内置类型我们使用>运算符就可以比较了,我们发现可读性太差了,有没有好的办法解决呢?
虽然我们可以定义函数实现自定义类型的运算,但是该方式可读性差。所以C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
简单来说:运算符重载就是让自定义类型支持运算符,增强代码可读性。
运算符重载是具有特殊函数名的函数, 也具有其返回值类型,函数名字以及参数列表,其返回类型与参数列表与普通的函数类似。
函数名字为: 关键字operator后面接需要重载的运算符符号。
函数原型: 返回值类型 operator操作符(参数列表)。
tip:一般操作符是几个操作数,就是几个参数。
现在我们可以将Less函数改造为重载运算符>。
代码示例:
class Date
{
public:
//默认构造函数
Date(int year = 2023, int month = 12, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
bool operator>(const Date& d);
private:
int _year;
int _month;
int _day;
};
//如果d1 > d2,返回真
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;
}
return false;
}
int main()
{
Date d1(2023, 12, 11);
Date d2(2023, 12, 12);
d1 > d2;//等价于d1.operator>(d2)
d1.operator>(d2);
return 0;
}
tip:
①因为刚才的Less函数写成全局的,成员变量如果是私有的类外不能访问,改成公有的破坏了类封装性,所以我们为了保证类的封装性。可以将其改为成员函数。
②重载运算符可读性好,因为编译器会自动转换调用重载运算符函数。
①注意: 赋值运算符只能重载成类的成员函数,不能重载成全局函数。
原因: 赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
②分清赋值运算符重载与拷贝构造函数!
int main()
{
Date d1;
//用一个已经存在的对象初始化另一个对象——拷贝构造函数
Date d2(d1);
//已经存在的两个对象之间复制拷贝——复制运算符重载
d2 = d1;
return 0;
}
tip:
错误代码示例1:
class Date
{
public:
//默认构造函数
Date(int year = 2023, int month = 12, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
//赋值运算符重载
void operator=(Date& d);
private:
int _year;
int _month;
int _day;
};
//d1 = d2
void Date::operator=(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1(2023, 12, 12);
Date d2;
d2 = d1;
Date d3;
//连续赋值
d3 = d2 = d1;
return 0;
}
tip:忘记返回值, 赋值运算符应该可以连续赋值的。
错误代码示例2:
//赋值运算符重载
Date& Date::operator=(Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
//d1 = d2,返回赋值之后的d1,d1是左操作数,即是this指针指向d1
return *this;
}
int main()
{
Date d1(2023, 12, 12);
Date d2;
Date d3;
//连续赋值
d3 = d2 = d1;
return 0;
}
我们代码本是想将d1拷贝给d2与d3,但是由上图我们可知程序没有完成工作,反而将d1改变了。
经过调试发现,赋值运算符重载函数内部,将赋值顺序弄反了!
tip:常引用参数——如果函数中只是使用参数,不改变参数的值,建议使用常引用。
正确代码示例:
class Date
{
public:
//默认构造函数
Date(int year = 2023, int month = 12, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
//赋值运算符重载
Date& operator=(const Date& d);
private:
int _year;
int _month;
int _day;
};
//d1 = d2
Date& Date::operator=(const Date& d)
{
//防止自己赋值自己
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//d1 = d2,返回赋值之后的d1,d1是左操作数,即是this指针指向d1
return *this;
}
int main()
{
Date d1(2023, 12, 12);
Date d2;
Date d3;
//连续赋值
d3 = d2 = d1;
return 0;
}
总结:
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
与拷贝构造函数一样,编译器生成的默认赋值运算符重载函数只能完成按字节序的浅拷贝,所以一旦涉及申请资源则必须自己实现赋值运算符重载。
例如:stack类,如下图分析
如图,我们可知:
总结
我们先来看一段代码:
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 8)
{
_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.Print();
//常量对象——const修饰
const Date d2;
d2.Print();
return 0;
}
分析:
在传参时,d2的权限被放大,所以编译报错。(引用过程中权限可以平移或缩小,但是不能放大!)
那怎么解决这个问题呢?
加一个const修饰*this,但是this是隐含的,我们该怎么修饰呢?祖师爷想了很久是在没有办法了,只能将const写在成员函数的最后面表示修饰 *this。
将const修饰的成员函数称为const成员函数。
解析:
成员函数后面加上const后,普通对象和const对象都可以调用了。
const成员函数这么好,那能不能所有成员函数都加上const?
答案是:不是,对于函数体内部需要修改对象的成员函数不能加const。
总结:只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用。
tip:
只有引用与指针涉及权限的放大与缩小,赋值不涉及。如下图:
权限可以平移或缩小,但不能放大!
对象取地址有两类——普通对象与const对象。
代码示例:
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 9)
{
_year = year;
_month = month;
_day = day;
}
//普通对象取地址
Date* operator&()
{
cout << "Date* operator&()" << endl;
return this;
}
//const对象取地址
const Date* operator&()const
{
cout << "const Date* operator&()const" << endl;
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//普通对象
Date d1;
cout << &d1 << endl;
//const对象
const Date d2;
cout << &d2 << endl;
return 0;
}
tip:
注意: 当有特殊情况,例如不想让别人取到对象的地址时,要自己实现取地址重载。
代码示例:
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 9)
{
_year = year;
_month = month;
_day = day;
}
//不想让别人取到对象的地址,返回nullptr
Date* operator&()
{
return nullptr;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//普通对象不能取到地址——需要自己实现
Date d1;
cout << &d1 << endl;
//const对象能取到地址——自己不实现,使用编译器生成即可
const Date d2;
cout << &d2 << endl;
return 0;
}
运行结果:
总结:取地址重载一般情况下我们都不需要实现,使用编译器生成的即可。当然如果开发中有特殊要求,我们也是需要自己实现的。