默认成员函数
在c++中,类在创建对象的时候,默认加入的函数,也就是你即使在类中没有写这6个函数,编译器也会自动生成这些函数
1. 构造函数
1.1 构造函数的使用
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但是构造函数的主要任务并不是开辟空间创建对象,而是初始化对象
先说说这个函数是为了解决什么问题而出现的:
- 在我们实现栈的时候,可能忘记初始化,或者每次栈,队列,各种都需要在开头进行初始化,好麻烦,所以引入了构造函数的概念。
- 构造函数的特点:
- 函数名和类名相同。
- 无返回值(函数定义前不需要写void,int,或者自定义类型)。
- 对象实例化时,编译器自动调用对应函数。
- 构造函数可以重载(可以写多个构造函数,提供多种初始化的方式)
#include<iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int x,int y,int z)
{
_year = x;
_month = y;
_day = z;
}
void Print()
{
cout << this->_year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date a;
a.Print();
Date b(2023, 10, 11);
b.Print();
return 0;
}
我们上面提供了两种初始化的方式
第一种是无数据传入,默认全部成员变量初始化为 1 。
第二种是传入三个数据,成员变量被输入数据赋值。
这样,在定义好对象的时候,对象会根据输入的不同,而自动调用不同的构造函数。
- 与前面的缺省参数结合起来,我们可以在输入的时候更加随意。
Date(int x=1,int y=1,int z=1)
{
_year=x;
_month=y;
_day=z;
}
这样的构造函数会更灵活。
1.2. 注意事项
还是上面Date类型
1 .传入需注意
如果定义的构造函数有参数传入需要写成
Date a(1,2,3);
但是下面这种情况要特别注意
在没有参数传入的情况下,后面不能加“()”。如:
Date a;//正确
Date a();//错误
Date Func();//函数声明
编译器不允许下面的写法----容易和声明函数冲突
可以看出,错误的和函数声明的写法一致,这么写会出现歧义。
传入参数的情况下:
Date b(2023, 10, 11);
Date f(int x,int y,int z);
函数声明和传参一眼就能分辨出来,不存在歧义。
- 全缺省和无参函数
Date(int x=1,int y=1,int z=1)
{
_year=x;
_month=y;
_day=z;
}
Date()
{
_year=1;
_month=1;
_day=1;
}
无参的函数和全缺省的函数语法上构成重载,但是实际上我们使用的时候无法调用无参数的函数。
所以我们一般写 全缺省的函数* 更加灵活
下面的是我们后面会经常出现的老朋友,栈
#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
Stack()
{
a=nullptr;
top=capacity=0;
}
void StackPush(int x)
{
if (top == capacity)
{
int newcapacity = (capacity == 0 ? 4 : 2 * capacity);
int* tmp = (int*)realloc(a,sizeof(int) * newcapacity);
if (tmp == NULL)
{
perror("malloc failed");
exit(-1);
}
if (a == tmp)
{
cout <<capacity<<" " << "same" << endl;
}
else
{
cout << capacity << " " << "not same" << endl;
}
a = tmp;
capacity = newcapacity;
}
a[top++] = x;
}
void StackPop()
{
assert(top > 0);
a[top--] = 0;
}
void StackDestory()
{
free(a);
top = capacity = 0;
}
void StackPrint()
{
for (int i=0;i<top;i++)
{
cout << a[i] << " " ;
}
}
bool Empty()
{
return top == 0;
}
int Top()
{
assert(top > 0);
return a[top - 1];
}
private:
int* a;
int top = 1;//声明时的缺省值,补坑
int capacity = 1;
}
如果我们知道我们要传入600个数据,Push里面的扩容的顺序 4 8 16,32 64,128,256,512,1024,最后会多出狠多空间,所以这里使用传入参数更好,直接在初始化中开辟好空间。
但是如果我们不知道需要传入多少参数时,不传入参数更好。
所以,这里我们就可以使用缺省参数解决上面的问题
初始化:
Stack()
{
if (n == 0)
{
a = nullptr;
capacity = top = 0;
}
else
{
a = (int*)malloc(sizeof(int) * n);
if (a == nullptr)
{
perror("malloc failed");
exit(-1);
}
capacity = n;
top = 0;
}
}
如果不传入参数,默认初始化栈
传入参数,就会根据传入参数的大小开辟空间。
- 有了上面的构造函数,我们想初始化什么样都可以。
1.3. 默认的含义
- 构造函数,也是默认成员函数,我们不写,编译器会自动生成。
1. 没有,编译器会自动生成
不写才编译器会自动生成一个,写了编译器就不会自动生成。
class Date
{
public:
Date(int year,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Print();
return 0;
}
这里由于我们写了构造函数,编译器不会自动生成构造函数。但是我们写的构造函数不满足我们定义对象的条件(必须至少传入一个值),它会没有构造函数可以自动调用,因此会显示“不存在默认构造函数”。
但是这里系统自动生成的构造函数,是,没有参数传入的,是为了让所有函数都可以调用该函数。
2. 不会处理内置类型
内置类型的成员不会处理,如编译器本来就含有的 int char 等类型(在c++11中,支持给缺省值----成员变量初始值)
class Date
{
public:
Date(int year,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year=1; //声明给的缺省值,默认生成的构造函数就会使用缺省值初始化
int _month=1;
int _day=1;
};
ps:这里的成员变量的缺省值只是补 不能给内置类型初始化的坑。
3. 自定义类型的成员,会调用成员的构造函数
如:
下面要实现栈实现队列:
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
int main()
{
MyQueue mq;
return 0;
}
在 MyQueue 这个定义的类型中,我们声明的成员变量都是 Stack 类型,也就是我们上面栈的类型。
对内部成员是自定义类型的,在创建对象,系统自动创建的MyQueue构造函数,会自动调用成员的构造函数。
- 一般情况下,都需要我们自己写构造函数,决定初始化成员变量的方式。
- 成员变量都是自定义类型的,可以考虑不写其构造函数。
4. 不需要传值调用的–默认构造函数
无参,全缺省,系统自动生成,的构造函数,都可以认为是默认构造函数。
这三种默认构造函数只能存在其中一个,存在多个会存在歧义。
2. 析构函数
与构造函数相反,析构函数不是完成对对象本身的销毁,局部对象的。
析构函数:与构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的。
子啊对象销毁时,会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,特性:
- 析构函数名是在类名前加上字符 ~
- 无参数无返回值类型
- 一个类只能有一个析构函数。若我们没有定义,系统会自动生成一个默认的析构函数,注:析构函数不能重载。
- 对象生命周期结束时,c++编译系统会自动调用析构函数。
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
对于上面 Date 类型的析构函数,我们并没有严格要求,因为内部 Date 内部成员变量都是局部变量,如果主函数结束,这些局部变量也就会自己销毁。
析构函数的主要价值在于 对栈,队列等这些结构上。
栈每次在程序结束之前,都需要我们手动销毁,但是有了析构函数,在每次对象销毁的时候,就会自动调用析构函数,帮我们进行销毁。
- 我们在c++中一个功能比较完善的栈就差不多写好了:
class Stack
{
public:
Stack(int n = 4)
{
if (n == 0)
{
a = nullptr;
capacity = top = 0;
}
else
{
a = (int*)malloc(sizeof(int) * n);
if (a == nullptr)
{
perror("malloc failed");
exit(-1);
}
capacity = n;
top = 0;
}
}
void StackPush(int x)
{
if (top == capacity)
{
int newcapacity = (capacity == 0 ? 4 : 2 * capacity);
int* tmp = (int*)realloc(a,sizeof(int) * newcapacity);
if (tmp == NULL)
{
perror("malloc failed");
exit(-1);
}
if (a == tmp)
{
cout <<capacity<<" " << "原地扩容" << endl;
}
else
{
cout << capacity << " " << "异地扩容" << endl;
}
a = tmp;
capacity = newcapacity;
}
a[top++] = x;
}
void StackPop()
{
assert(top > 0);
a[top--] = 0;
}
void StackPrint()
{
for (int i=0;i<top;i++)
{
cout << a[i] << " " ;
}
}
bool Empty()
{
return top == 0;
}
int Top()
{
assert(top > 0);
return a[top - 1];
}
~ Stack()
{
cout << "~ Stack()" << endl;
free(a);
top = capacity = 0;
}
private:
int* a;
int top = 1;//声明时的缺省值,补坑
int capacity = 1;
};
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
int main()
{
Stack st;
st.StackPush(1);
st.StackPush(2);
st.StackPush(3);
st.StackPush(4);
st.StackPush(5);
return 0;
}
我们不需要自己手动初始化,不需要自己手动销毁,这些构造函数和析构函数都帮我们处理好了,我们只需要进行我们想进行的操作即可。
如果我们定义了许多对象
int main()
{
Date d1;
Date d2;
Stack st1;
Stack st2;
return 0;
}
调用析构函数时,是按照函数栈帧的顺序调用,最开始d1,d2,st1,st2 分别入栈,然后销毁的时候,st2先调用析构,st1调用,d2调用,d1调用。
后定义的先析构。
默认的析构函数:
- 内置类型成员不会处理
- 自定义类型会调用这个成员的析构函数
如:
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
int main()
{
MyQueue mq;
return 0;
}
只需要定义一个MyQueue对象,他就会自动生成构造和析构函数,自动帮我们初始化和销毁,这不爽歪歪。
构造和析构最大的特性:自动调用
3. 拷贝构造函数
3.1. 浅拷贝
拷贝,也就是复制一份新的内容
我们先认识一下浅拷贝
首先,还是使用上面 Date类
void func(Date d)
{
}
int main()
{
Date d1(2013,10,20);
func1(d1);
return 0;
}
如果我们想要拷贝Date类,我们调用func函数,在函数内,局部变量 d 会复制一份传入参数 d1 的值。
在这两份空间,我们对拷贝的 d 进行修改,修改结束后,不会对原数据 d1产生影响。
我们对栈进行上面对Date类的操作,我们看看会发生什么。
void Func(Stack st2)
{
st2.StackPrint();
}
int main()
{
Stack st1;
Func(st1);
return 0;
}
我们发现它经历了两个析构函数,还崩了
这就是浅拷贝的问题,我们简单分析一下:
虽然我们对 st1 复制了一份给 st2 ,但是 st1的a 存储的是 动态数组的空间首地址,复制给 st2 之后,是将 st1.a保存的地址复制一份给了 st2 的 a,但是开辟的数组空间仍然只有一份。
所以在 st2 进行销毁的时候,析构函数会自动销毁开辟的空间,st1此时指向的是数组的地址,st2销毁后,st1会变为野指针,在后面 st2 销毁的后,会对已经销毁的空间再次进行销毁,因此发生错误。
st1,st2 指向空间相同,st1,st2是是两个变量,他们地址不同。
如果想要解决两次析构的问题,很简单,传入引用即可
void Func(Stack& st2)
{
st2.StackPrint();
}
传入引用的话,由于都是使用 st1 没有产生第二个变量,当函数结束时,不会对 st1 进行销毁, 只有当程序结束的时候 st1 才会销毁,只销毁一次。
但是我们的最终目的是拷贝这个 st1,但是引用传入的就是st1,如果我们在func函数内修改数据,主函数内的st1 数据也会改变,这不是我们想要的拷贝。
3.2. 深拷贝(拷贝构造函数)
深拷贝:也就是对需要拷贝的内容整体做一份拷贝,如栈,原栈开辟了多少空间,新拷贝的也要开辟多少空间,且和原空间不存在重合,这样我们对新拷贝空间的使用,就不会影响原空间。
拷贝构造函数:
拷贝构造函数也是特殊的成员函数,特征:
- 拷贝构造函数时构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器直接报错,会引发无穷递归调用。
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Stack st1;
Date a;
Date a1(a);
//Date (a);
return 0;
}
注意:参考上面第一个函数,构造函数。
我们调用构造函数的方式
Date d;
如果我们按照上面 Date(Date d)的方式定义,我们看看会发生什么
会导致无穷递归,但是因为编译器检查严格,我们这里无法观察,我们可以推一下
因为每一次在传入参数位置,我们都用的是 Stack s,也就是在每次传入参数的位置,会将传入参数判断为新的拷贝构造,然后再次往下判断。,这样会导致无穷递归。
- 所以这里我们就不能进行值传参,需要引用传参。
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Stack(Stack& s)
{
cout << "Stack(Stack& s)" << endl;
a = (int*)malloc(sizeof(int) * s.capacity);
if (NULL == a)
{
perror("malloc failed");
exit(-1);
}
memcpy(a, s.a,(sizeof(int) * s.top));
top = s. top;
capacity = s.capacity;
}
引用是一种别名,传入后,传入参数为 Date& d ,传参的时候就不会进入下一层的拷贝构造函数,也就不会出现上面的错误。
怎么使用拷贝构造函数
void Func(Stack st2)
{
st2.StackPrint();
st2.StackPush(5);
st2.StackPush(6);
st2.StackPush(7);
st2.StackPush(8);
st2.StackPrint();
}
int main()
{
Stack st1;
st1.StackPush(1);
st1.StackPush(2);
st1.StackPush(3);
st1.StackPush(4);
st1.StackPrint();
Func(st1);
st1.StackPrint();
return 0;
}
这里,我们传入的参数的时候,会先进入Func函数的参数传入,这里会被认为是调用拷贝构造函数,然后调用,再接收拷贝后的st1,然后用拷贝的部分进行函数内的操作。
指针也可以实现上面的拷贝构造函数,但是与引用还是存在差别。
使用指针的传参:
Date d2(&d1);
func1(&d1);
写起来没有引用的方便。
下面两种写法等价
Date d3=d1;
Date d2(d1);
拷贝构造函数特性
- 拷贝构造和前面两张特性不太一样
- 内置类型,会进行值拷贝。
- 自定义类型,调用他的构造函数。
日期类,不需要我们去实现拷贝构造,默认生成的就可以用。
但是 栈 这种,仅仅是对内部成员的值拷贝,不满足我们使用的需要(如:两个指针,复制完后都指向同一块空间,对一个指针指向的空间进行修改,会影响原指针指向的空间),对这种类型,我们不能使用浅拷贝,必须使用深拷贝。
和上面两个函数类似,内部函数为自定义类型的不要写默认拷贝构造函数
class MyQueue
{
public:
Stack _pushst;
Stack _popst;
};
这里,我们只是简单定义了qu,然后拷贝了一份qu,他就会自动调用内部成员的 默认构造函数,拷贝函数,析构函数。
拷贝构造函数的注意
const引用
Date( Date& d)
{
d._year = _year;
d.month = _month;
d._day = _day;
}
如果我们拷贝构造函数内部写反了,系统不会报错,但是这样运行出来后,原来有的数据会被覆盖。
所以如果我们传入的引用,只是为了赋值,我们就可以使用const 对其修饰,这里属于权限的缩小(从Date类型变为const Date类型),不会产生问题,如果函数内我们写反了,就会报错,有效防止错误。
4. 赋值运算符重载
4.1. 运算符重载的认识
如果想要比较两个 Date 类的大小
int main()
{
Date d1(2023,10,18);
Date d2(2022,8,20);
d1<d2;
return 0;
}
如果是两个整形类型,我们可以这样比较,但是这是两个自定义类型,我们不能直接进行比较。
一般想要比较自定义类型大小的话,可以使用函数来操作
bool DateLess(const Date& x1,const Date& x2)
{
if(x1._year<x2._year)
{
return true;
}
else if(x1._year == x2._year&&x1._month<x2._month)
{
return true;
}
else if(x1._year == x2._year&&x1._month==x2._month&&x1._day<x2._day)
{
return true;
}
else
{
return false;
}
}
int main()
{
Date d1(2023,10,18);
Date d2(2022,8,20);
cout<<DateLess(d1,d2)<<endl;
return 0;
}
上面的函数能输出 d1<d2 的结果,但是代码的可读性不是很高,每次需要看函数名来判断这个函数的意义。
在c++中,有一种新的方式,运算符重载。
在类外:
bool operator<const Date& x1,const Date& x1)
{
if(x1._year<x2._year)
{
return true;
}
else if(x1._year == x2._year&&x1._month<x2._month)
{
return true;
}
else if(x1._year == x2._year&&x1._month==x2._month&&x1._day<x2._day)
{
return true;
}
else
{
return false;
}
int main()
{
Date d1(2023,10,18);
Date d2(2022,8,20);
cout<<(d1<d2)<<endl;
return 0;
}
c++规定,operator+‘<’ <会被当做函数名
调用的时候
cout<<(d1<d2)<<endl;
cout<<(operator<(d1,d2)<<endl;
如果使用运算符重载,可读性更强,只需要看中间的符号就知道是什么含义,不再需要根据那些函数名来判断。
但是上面在类外学的函数,访问了成员变量,按照上面的写法,我们不得不将成员变量的访问权限改为公有。
所以我们可以想办法将该函数写进类内部
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;
}
}
上面写入类中,我们只需要传入一个参数即可,因为另一个参数是this指针的形式传入。
我们想要调用可以按照下面的方式:
cout<<(d1<d2)<<endl;
cout<<d1.operator<(d2)<<endl;
ps:上面 (d1<d2) 的括号是必须加上的,因为(d1<d2) 在编译器看来就是第二种写法,加上括号表示调用
4.2. 运算符重载的规定
c++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,也是具有返回值类型,函数名以及参数列表,其返回值类型与普通函数类似。
函数名:关键字operator后面需要重载的运算符符号
函数类型:返回值类型operator操作符。
注意:
- 不能通过连接其他符号来创建新的操作符:比如:operator@,使用原有的操作符对自定义类型使用。
- 重载运算符必须有一个类的类型参数(自定义类型)
- 用于内置类型的运算符,其含义不变,如:内置的类型 +,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数少1个,因为成员函数的第一个参数为隐藏的this。
- .*(点星),:: ,sizeof , ?:(三目) ,.(点) 注意以上5个运算符不能重载。
简单来说,我们重载后的操作符要配合我们的使用习惯,创建运算符重载的目的是为了增强程序的可读性,不是为了整花活。
4.3. 函数的复用
#include<iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int x,int y,int z)
{
_year = x;
_month = y;
_day = z;
}
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;
}
}
void Print()
{
cout << this->_year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
上面我们简单的实现了 基础的日期类的小于
但是如果我们需要判断 日期相等,日期大于,日期大于等于,日期小于等于呢。
实现日期等于:
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
有了上面的 < 和 == ,我们实现后面的,只需要对前面函数复用即可。
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool operator>(const Date& d)
{
return !(*this <= d);
}
bool operator>=(const Date& d)
{
return !(*this < d);
}
bool operator!=(const Date& d)
{
return !(*this==d);
}
扩充:日期计算的问题
对于日期类,如果我们给一个日期加上天数,它应该怎么变,这牵扯到日,月,年的变化,同时有些月的时间都不相同,所以,这种日期的加减法还是有必要的。
- 根据输入的日期,计算是这一年的第几天。
保证年份为4位数且日期合法。
进阶:时间复杂度:O(n)\O(n) ,空间复杂度:O(1)\O(1)
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
int GetMonthDay(int year,int month)
{
static int arr[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 arr[month];
}
int dayofyear()
{
int sum=0;
for(int i=1;i<_month;i++)
{
sum+=GetMonthDay(_year,i);
}
sum+=_day;
return sum;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int year=0,month=0,day=0;
cin>>year>>month>>day;
Date d(year,month,day);
int sum=d.dayofyear();
cout<<sum;
return 0;
}
4.4. const问题
const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员成员函数隐含的this指针,表明在该成员函数中,不能对类的任何成员进行修改。
- 后面加const
void testDate()
{
const Date d1(2023,10,24);
d1.Print();
}
上面这段代码,d1 是由const 修饰的,但是 Print 函数传递的 this指针 没有经过 const 修饰,如果传进去,会导致权限放大,所以这里直接调用Print函数会出现问题。
但是在定义 Print函数的时候,this 指针是默认 传进去的类型是自定义类型的地址,我们没办法直接修改。
为了避免上面的问题,c++中的新写法
void Print() const ;
这样能直接将默认的 this 指针定位为 const 类型,
这样写,在编译器看来:
void Print( const Date* this )
因为权限只能缩小和平移,所以后面传入的const 和非const 都没有影响。
2. 前面加const
const int func()
{
int ret;
return ret;
}
int func()
{
int ret;
return ret;
}
前面的const修饰返回值,后面的const修饰this指针。
两个函数,都返回的是ret,但是返回值的类型没有区别。
第二个函数返回值没有const,但是在返回ret的时候,会产生新的临时变量接收这个值,而新产生的临时变量具有常性,不可以修改,所以从返回值来说,上下没有区别。
- 后置const和没有const修饰的同时存在
void Print()const;
void Print();
看似没区别,实际上这两个函数构成重载,传入参数类型不同,上面传入的是 const Date* this,下面的传入的是Date* this。
这种情况下,如果我们传入const Date的类型,就是调用第一个函数,如果传入 Date 类型,就会调用第二个函数。
(如果没有第一个用const修饰的函数,编译器会进入最接近的第二个,也可以运行(权限缩小))
- 上面的例子意义不大
4.5. [ ]运算符重载
#include<iostream>
using namespace std;
class Seqlist
{
public:
void PushBack(int x)
{
_a[size++]=x;
}
size_t size()
{
return _size;
}
int operator[](size_t i)
{
assert(i<_size);
return _a[i];
}
private:
int* _a = (int*)malloc(sizeof(int)*10);
int _size =0;
int _capacity = 10;
};
int main()
{
Seqlist s;
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
s.PushBack(4);
for (size_t i = 0; i < s.size(); i++)
{
cout << s[i] << " ";
cout << s.operator[](i) <<" ";
}
cout << endl;
return 0;
}
只要我们使用 s[i] ,他就会自动返回 s._a[i] 的数据,这样我们就能把对象当成数组一样使用,方便了很多。
4.6. const的意义
如果我们在类外面写 Print 函数
void Print(const Seqlist& sl)
{
for(size_t i = 0 ;i < sl.size();i++)
{
cout<sl[i]<<" ";
}
cout<<endl;
}
这样写,编译器会报错,因为我们Print函数的参数是const 修饰的对象,但是 [] 运算符重载函数,接收的是非const修饰的对象,也就是说我们把const的对象,传入了非const的函数,所以会报错。
消除错误:定义的函数用const修饰
size_t size()const
{
return _size;
}
int& operator[](size_t i)const
{
assert(i < _size);
return _a[i];
}
这样就没有问题了。
- 但是如果我们在Print函数内进行下面操作
void Pirnt(const Seqlist& s1)
{
for(size_t i =0; i <sl.size();i++)
{
sl[i]++;//对对数组内元素++
//这里的sl[i]++在Print函数内,我们更希望这句代码会报错
cout<<sl[i]<<" ";
}
cout<<endl;
}
我们会发现,数组内元素发生了改变,我们的数组全程是const保护过的,但是为什么上面的++操作依旧有效?
我们先分清楚const保护了什么,const保护的是 传入对象内容不被改变,也就是对象的成员变量,_a,_size,_capacity,const保护了这三个,但是_a是个数组,数组的内容不是传入对象的成员变量,不受const保护,所以会出现这种情况。
- 解决方法:
对 [] 运算符重载函数的返回值进行修改
const int& operator[](size_t i)const
{
assert( i < _size);
return _a[i];
}
这样写的话,我们返回的值会受到const的保护,这样在Print函数内我,我们就无法修改数字内的数据。
但是这样写,如果我们在其他地方想要改变数组中的值
int main()
{
Seqlist s;
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
s.PushBack(4);
for (size_t i = 0; i < s.size(); i++)
{
s[i]++;
}
cout << endl;
return 0;
}
这里会报错,因为上面函数返回来的是const保护过的数据,这样++是对const修饰的内容++,所以会报错。
因此这里需要一个没有const修饰返回值的函数。
- 这个时候函数重载就用上了
我们可以写两个函数。
int& operator[](size_t i)
{
assert( i <_size)
return _a[i];
}
const int& operator[](size_t i)const
{
assert( i < _size);
return _a[i];
}
上面的函数构成重载。
如果我们在Print函数内使用,传入的是const修饰的参数,会进入第二个函数,最后返回的是const保护的数据,s[i]++会报错。
如果我们在主函数想要修改数组的数据,传入的是没有const修饰的参数,会进入第一个函数,返回来的数据也是数组数据的引用,可以直接修改。
const修饰的,只支持读
非const修饰的,支持写入
完美的解决了上面的问题。
- 这个方法在c++的库函数中就在使用
vector中的 [] 也是使用了重载的使用方法。 - 这里要注意const的使用
非const修饰的变量/对象传入const修饰过的变量/对象,权限都会缩小。
const修饰过的变量/对象传入非const修饰过的变量/对象,权限会放大。
- 权限放大不可以,权限缩小可以。
- const函数,const函数可以调用(权限平移),非const函数也可以调用(权限缩小)
- 只读函数可以加const,内部不涉及修改生成。
5. 取地址运算符的重载
取地址运算符重载和const取地址运算符重载近似,而且因为编译器自动生成的够用,平时用的比较少,这里两个合起来说
本质还是运算符的重载
Date* operator&()
{
return *this;
}
取地址运算符,作用还是取对象地址,但是平时编译器会自动生成,自动生成的足够使用。
如果我们的对象是const类型,或者只需要读取,不需要修改,和上面一样,用const修饰即可
const Date* operator&()const
{
return this;
}
如果仅仅是传对象地址,就没什么说的,但是哦们可以自己写这两个函数,我们返回的就不一定只是对象地址了。
- 如果不想让别人获取我这个类的对象的地址
我们可以自己实现一个取地址运算符重载
Date* operator&()
{
return nullptr;
}
const Date* operator&()const
{
return this;
}
这样的话,最后返回来的地址,非const的不能取到地址,const修饰的可以取到地址。
如果想整活,也可以返回野指针,看个人
Date* operator&()
{
return (Date*)0x01202;//强制将后面的数据转换为(Date*)类型,可以返回的类型
}
这种情况很危险,一般不要这样写。