【C++学习笔记】类和对象中篇

目录

  • 流插入符号 << 的运算符重载
  • 流提取符号 >> 的运算符重载
  • const 成员函数
  • 取地址及const取地址操作符重载

流插入符号 << 的运算符重载

我们知道 c++ 中可以使用 cout 进行输出打印,因为 << 也是一个运算符,有了前面介绍的运算符重载的概念我们知道 cout << 对象,就是一个运算符重载。在 c++ 中打印函数可以自动识别类型,这是为什么呢?

int main()
{
	int i = 0;
	double j = 1.11;
	cout << i ;
	cout << j;
	return 0;
}

因为 cout 是一个 ostream 类型的全局对象,因为我们在写代码时需要包一个 iostream 的头文件,而 c++ 库里面定义的东西,都在 std 的命名空间,所以这些定义会被展开。

【C++学习笔记】类和对象中篇_第1张图片

由于 ostream 存在于库里面,本质就是库里面写好的一个函数调用,在库里面默认对内置类型进行了支持,所以可以直接去使用。

【C++学习笔记】类和对象中篇_第2张图片

有了以上的概念,我们知道在 cout << i; 的时候,就会转换成调用一个 ostream 类型的成员函数,即 cout.operator<<(i); 这些函数由库里面写好,并因为函数重载可以自动识别类型。

int main()
{
	int i = 0;
	double j = 1.11;
	cout << i ;//调用 cout.operator<<(i);int版本
	cout << j;//调用 cout.operator<<(j);double版本
	return 0;
}

以上就是为什么 cout 支持内置类型的打印,那么自定义类型想用运算符 << 就需要进行运算符重载?

因为 ostream 类是在库里面,我们不能随便改,所以这里在类外进行重载。看如下代码


class Date
{
public:
	void operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
	int main()
{
	// 流插入
	Date d1(2023, 2, 4);
	cout << d1;
	//d1.operator<<(cout);
}

注意以上代码的调用逻辑是先调用自定义类型的 << 运算符重载,将创建出的 ostream 类的对象 cout 用引用的方式传递给 Date 类里面的运算符重载成员函数 void operator<<(ostream& out); 其形参 out 就是 cout,然后里面的 out << _year 语句中,_year 是整型,也就是内置类型,又会去调用库里面的 << 运算符重载。
那么逻辑我们梳理完进行代码编译的时候发现程序报错了,这是为什么呢?

在这里插入图片描述

我们再来梳理一下逻辑,想调用 << 运算符重载,我们使用显示调用的方式是 d1.operator<<(cout),因为运算符重载的规定就是第一个参数是左操作数,第二个参数是右操作数,将其转换成隐式调用就是 d1 << cout,而库里面的调用是 cout.operator<<(i),转换成隐式调用是 cout << i; 这里日期类对象抢占了第一个参数,所以在运算符的左边,而 cout 在运算符的右边。

注意:

这里并不是使用 this 指针传递(调用 d1.operator<<(cout) <===> operator<<(&d1, cout)this 帮助完成了这一转换),因为如果使用 this 指针传递,需要写在 ostream 类里面,而事实上我们不能将其写到库函数里面,所以这里不是 this 指针传递,这里是写到日期类里面。

int main()
{
	// 流插入
	Date d1(2023, 2, 4);
	cout << d1;
	d1.operator<<(cout);
	//d1 << cout; 
}

将代码运行发现没有报错且正常运行

在这里插入图片描述
虽然结果正确,但是看起来十分别扭,由于上述写法写到了 Date 类里面做了成员函数,当然可以写成全局函数,写成全局函数就可以避免上述问题,但是全局函数访问不了私有,可以使用函数接口,也可以使用友元的方式,友元这里先不具体介绍,只需记住添加友元后,类外函数可以访问类内私有成员即可。下面是全局函数的写法:

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}
int main()
{
	// 流插入
	Date d1(2023, 2, 4);
	operator<<(cout,d1);
	cout << d1;
	//d1 << cout; 
}

当然这里还需要有 ostream& 返回值,用来支持连续输出的情况。那么再来思考一个问题,为什么 c++ 要使用流插入打印而不用 printf ?

因为自定义类型无法打印,由于 C语言 结构体访问不受限制,而 c++ 不能访问对象私有成员,所以才引出流插入运算符打印自定义类型。

流提取符号 >> 的运算符重载

类似于流插入运算符,写出流提取运算符的重载,注意流提取第二个参数不能加 const,因为加 const 就说明不能改变,而实际上我们输入的值是可以随意改变的,所以这里不用加 const 修饰。

istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

当然利用我们之前学到过的内联函数的知识,可以将其做出以下修改,而一个成员函数定义在类里面,默认就是内联函数,内联函数的声明和定义不能分离。

1、程序在汇编过程中会形成符号表,因为在头文件中如果只有函数的声明,就没有函数的实际地址,所以需要在链接的过程中去符号表中找函数的实际地址

2、如果函数在头文件中有定义,就说明此函数有实际地址,那么就不需要在链接过程找地址了

3、由于内联函数没有地址,即不存在于符号表里面,所以链接过程中就不会在去找它的实际地址,导致程序报错,处理办法就是需要在头文件中展开。

//头文件
class Date
{
inline ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

inline istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

注:因为 inline 函数在编译的时候就被插入到调用处,编译器不会单独为一个 inline 函数生成汇编代码,而是在调用的地方直接生成汇编代码插入到调用处,这个是属于编译阶段的事情而不是链接阶段的事情,所以在编译的代码生成阶段就需要拿到 inline 函数的定义。如果编译器在编译的代码生成阶段没有拿到 inline 函数的定义,则将对其的调用推迟到链接时,但是由于对于 inline 函数的定义处,编译器并未生成汇编代码,所以会链接失败。

const 成员函数

我们看如下代码

class A
{
public:
	void Print() 
	{
		cout << _a << endl;
	}
	private:
	int _a = 10;
	};
int  main()
{
	const A aa;
	aa.Print();
	return 0;
}

运行发现改代码会报错,因为出现了权限的放大问题,传参时,aa 的地址 &aa 会被传递给 Print ( ) 作为隐藏参数 this 指针,所以传递的类型是 const A*,因为加了 const 修饰,所以 &aa 指向的内容都不能改变,而 Print 参数为 A this*,明显发生了权限的放大。所以需要将 this 指针的类型变成 const A*,即采用下面的写法:

```cpp
class A
{
public:
	// const 修饰 *this
	// this的类型变成 const A*
	// 内部不改变成员变量的成员函数
	// 最好加上const,const对象和普通对象都可以调用
	void Print() const
	{
		cout << _a << endl;
	}
	private:
	int _a = 10;
	};
int  main()
{
	const A aa;
	aa.Print();
	return 0;
}

上面的写法语法规定就是 const 成员函数,这个 const 修饰 this 指针,此时 this 指针的类型变成 const A*。

注意:
1、哪个对象调用,修饰的就是哪个对象,所以 const 是修饰 *this
2、内部不改变成员变量的成员函数,最好加上 const,这样 const 对象和普通对象都可以调用。

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成,如果是自己写,就如下列代码:

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
}
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!但是没有人会无聊到做这样的事情。。。

你可能感兴趣的:(C++,c++,学习,开发语言)