前言
接着讲类和对象的知识,今天主要来了解下,我们的 const成员函数,,重点还是一种新的初始化方式。
回顾:
上期我们讲到了拷贝构造函数的使用,当我们使用拷贝构造函数的时候,其运行的原理是将一个已存在的对象作为模版创造一个新的对象,但是默认生成的拷贝构造函数只能对内置类型进行拷贝,而自定义类型的数据是我们需要自己写构造函数的,而我们自己写拷贝构造函数的时候需要注意的是,拷贝构造函数的实现是需要使用引用的,具体的原因见类和对象——中(2)-CSDN博客
当然拷贝构造的使用中还有一个小小的坑,在我们使用拷贝构造的时候,对象中如果有指针的数据,在进行拷贝的时候是不会发生什么问题的,但是在析构函数调用的时候就会发生二次析构的问题,这里大家需要注意。
运算符重载这方面其实并没有什么过多的点需要注意,大家只要记住运算符重载是给予运算符更多的选择,也就是可以匹配更多的参数,当然还需要注意有少数的运算符并不能进行运算符重载。
一、流运算符
在c++中我们经常使用的运算符<< 、>>流运算符的使用在c++中默认的只能对内置类型进行操作,那么怎么对自定义类型进行操作呢?
下面我们来进行详细的讲解:
#include<iostream>
using namespace std;
class A
{
public:
const ostream& operator<<(ostream& out)
{
out << year << "/" << mouth << "/" << day << endl;
return out;
}
A()
{
year = 2023;
mouth = 11;
day = 4;
}
private:
int year;
int mouth;
int day;
};
int main()
{
A s1;
cout << s1;
s1 << cout;
}
来看看,这里为什么显示的是s1流入cout错误,而cout流入s1却是正确的?这里涉及到了操作符重载的一个小知识,操作符重载规定了,第一个传入的参数是左值,而第二个是右值,当然,可能有细心的人已经隐隐猜到了这个规则,因为如果没有这个规则的存在,编译器是根本无法知道运算顺序的,这里我们将流运算符的重载放到了类中,这样就导致了this指针将第一个参数位置占据,这也是错误的原因,这里我们只需要将运算符重载放到全局即可。
#include<iostream>
using namespace std;
class A
{
public:
A()
{
year = 2023;
mouth = 11;
day = 4;
}
private:
int year;
int mouth;
int day;
};
const ostream& operator<<(ostream& out , A a)
{
out << a.year << "/" << a.mouth << "/" << a.day << endl;
return out;
}
int main()
{
A s1;
cout << s1;
}
那么我们如果将流运算符的重载放到全局中去的话,这里又有一个问题,这里为了类的数据的封装,我们将数据定为私有,但是全局无法访问,怎么办?
这里给大家讲个新知识,友元函数,友元函数的存在能够帮助我们访问类中的私有数据,友元的定义是在函数前面加上一个friend即可,这样表示了,这个函数是当前文件类的友元函数,而友元函数可以访问该类中的私有数据。但是一定要记住,友元函数的声明一定是在类中声明。
#include<iostream>
using namespace std;
class A
{
public:
A()
{
year = 2023;
mouth = 11;
day = 4;
}
friend const ostream& operator<<(ostream& out, A a);
private:
int year;
int mouth;
int day;
};
const ostream& operator<<(ostream& out , A a)
{
out << a.year << "/" << a.mouth << "/" << a.day << endl;
return out;
}
int main()
{
A s1;
cout << s1;
}
当然,这里可能有人会有疑问,为什么流运算符会有返回值,那么,大家注意到了没,流运算符是可以连续使用的,所以这里直接返回cout,来进行下一步的流运算。
还有一点需要注意!这里我们实现的是自定义类型的流运算,但是有人可能会自己又去搞一个+的运算符重载,一定要注意,这里规定了传入参数一定要有一个自定义类型的对象,不要自己搞两个内置类型参数,这会和std库已经实现的发生冲突。
二、const成员函数
1.const对象
代码如下(示例):
猜猜程序运行的结果是什么?
#include<iostream>
using namespace std;
class A
{
public:
void Funk(A a)
{
year = 0;
cout << "Funk" << endl;
}
A()
{
year = 2023;
mouth = 11;
day = 4;
}
private:
int year;
int mouth;
int day;
};
int main()
{
const A s1;
s1.Funk(s1);
}
在上面我们看见了运行报错,这是为什么呢?看好,我们使用了const来修饰s1,但是我们调用Funk函数,当我们将const s1传入Funk函数的时候,这里发生了权限的变化,注意看好,Funk函数的形参是A a 也就是这里的形参是没有const的,也就是说,这里的形参是可以改变的,这里就发生了权限放大。
三.初始化列表
在我们学习了这么多知识后呢,我们的初始化方式也有了不同,这里来给大家讲个新的初始化方法。
初始化列表使用方法:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int mouth, int day)
:_year(year) //以冒号开始
, _mouth(mouth) //以逗号分隔
, _day(day)
{
}
private:
int _year;
int _mouth;
int _day;
};
那么,为什么我们需要初始化列表呢?其他的初始化方法不行吗?既然存在初始化列表,就有它存在的意义,在我们c++中,声明引用的同时必须初始化,同样,const修饰的变量也是一样的。但是有了初始化列表,我们就可以干一些别的事情,
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int mouth, int day)
:_year(year)
, _mouth(mouth)
, _day(day)
{
}
private:
const int _year;
int _mouth;
int _day;
};
我们有了初始化列表之后,我们就可以将const和引用的声明和初始化分离来写了,这就是初始化列表的作用。
但是初始化列表这么有用,为什么我们不全使用它呢?
初始化列表的缺点也有一些,当我们初始化一些 像指针一样的数据时,我们malloc一个空间,将其地址交给初始化列表,但是我们并不能判断它是不是空指针,所以我们还是需要函数体来处理一些事情。
总结
初始化列表帮助我们解决了一些像const,引用声明和初始化无法分离的问题,我们也从const函数的使用上面,更加全面的了解了权限的变化的问题,关于初始化的问题,我们最好还是多使用初始化列表来进行初始化操作。