C++学习笔记29:友元函数和输入输出运算符重载

目录

一、为什么需要友元函数?

二、友元函数的写法

三、友元函数的特点

[四、为什么 cout 不能直接输出 Date 对象?](#四、为什么 cout 不能直接输出 Date 对象?)

[五、operator<< 为什么通常写成全局函数?](#五、operator<< 为什么通常写成全局函数?)

[六、operator<< 为什么要声明为友元?](#六、operator<< 为什么要声明为友元?)

[七、operator<< 为什么返回 ostream&?](#七、operator<< 为什么返回 ostream&?)

[八、operator>> 运算符重载](#八、operator>> 运算符重载)

[九、cout 为什么会识别不同类型?](#九、cout 为什么会识别不同类型?)

十、小结


一、为什么需要友元函数?

在C++中,类的私有成员只能在类内部访问。

例如:

cpp 复制代码
class Date {
private:
    int _year;
    int _month;
    int _day;
};

如果在另外一个类写普通函数:

cpp 复制代码
void PrintDate(const Date& d) {
    cout << d._year << endl;    // 错误
}

这段代码会报错,因为 _year 是私有成员,类外不能直接访问。

如果确实需要让某个类外函数访问私有成员,可以使用 友元函数


二、友元函数的写法

友元函数使用 friend 关键字声明。

cpp 复制代码
class Date {
	public:
	    Date(int year = 2024, int month = 1, int day = 1)
	        : _year(year)
	        , _month(month)
	        , _day(day)
	    {
	    }
	
	    friend void PrintDate(const Date& d);
	
	private:
	    int _year;
	    int _month;
	    int _day;
}; 

类外实现:

cpp 复制代码
void PrintDate(const Date& d) {
	cout << d._year << "-" << d._month << "-" << d._day << endl;
}

这样 PrintDate 就可以直接访问 Date 类中的私有成员了。

需要注意:

友元函数不是类的成员函数,只是被允许访问这个类的私有成员。

所以调用仍然是普通调用:

cpp 复制代码
Date d(2024, 5, 1);
PrintDate(d);

而不是:

cpp 复制代码
d.PrintDate();    //错误

三、友元函数的特点

友元函数有几个特点:

cpp 复制代码
1. 友元函数不只是类中的函数;
2. 友元函数可以直接访问类的私有成员;
3. friend声明可以写在 public、private、protected 任意位置;
4. 友元函数只对当前类有效;
5. 友元函数会突破封装,不能滥用。

一般情况下能写成成员函数,就优先写成成员函数。

只有实在不适合写成成员函数时,再考虑友元。


四、为什么 cout 不能直接输出 Date 对象?

对于内置类型, cout 可以直接输出:

cpp 复制代码
int a = 10;
double d = 3.14;

cout << a << d << endl;

但是对于自定义类型:

cpp 复制代码
Date d(2024, 5, 1);
cout << d << endl;

编译器不知道一个 Date 对象应该怎么输出。

也不知道输出格式是:

cpp 复制代码
2024-5-1

还是:

cpp 复制代码
2024/5/1

所以我们要重载自己的输出运算符 operator<< 。


五、operator<< 为什么通常写成全局函数?

如果把 operator<< 写成成员函数,左操作数默认就是 this。

例如成员函数时形式会变成:

cpp 复制代码
d.operator<<(cout);

这样调用时可能要写成:

cpp 复制代码
d << cout;

这显然不符合我们的使用习惯。

我们想要的是:

cpp 复制代码
cout << d;

左边是 cout ,右边是 Date 对象。

所以 operator<< 通常写成全局函数:

cpp 复制代码
ostream& operator<<(ostream& out, const Date& d);

其中:

cpp 复制代码
out 表示输出流对象,例如 cout
d 表示要输出的 Date 对象

六、operator<< 为什么要声明为友元?

operator<< 写成全局函数后,他就不是 Date 类的成员函数了。

但是它它需要访问 Date 的私有成员:

cpp 复制代码
d._year
d._month
d._day

所以需要在类中把他声明为友元函数:

cpp 复制代码
friend ostream& operator(ostream& out, const Date& d);

完整代码:

cpp 复制代码
#include <iostream>
using namespace std;

class Date {
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Date(int year, int month, int day) 
		: _year(year)
		, _month(month)
		, _day(day)
	{
	}
	
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const Date& d) {
	out << d._year << "-" << d._month << "-" << d._day << endl;
}

int main() {
	Date d1(2024, 5, 1);
	cout << d1;

	return 0;
} 

运行结果:

cpp 复制代码
2024-5-1

七、operator<< 为什么返回 ostream&?

如果只想输入一次,似乎可以不返回。

但我们平常这样写比较多:

cpp 复制代码
cout << d << endl;

这个表达形式本质上是连续调用:

cpp 复制代码
operator<<(cout, d) << endl;

如果 operator<< 不返回 osteram&,后面就不能继续接 << endl。

所以输出运算符重载要写成:

cpp 复制代码
ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

返回 out 的引用,才能支持连续输出。


八、operator>> 运算符重载

输入运算符重载和输出运算符重载类似,但是有差别。

首先是在返回值类型上,输入运算符重载是:

cpp 复制代码
istream&

还要注意的是第二个参数是:

cpp 复制代码
Date& d

因为输入时要修改 d 的成员变量。

完整代码:

cpp 复制代码
#include <iostream>
using namespace std;

class Date {
	friend istream& operator>>(istream& in, Date& d);
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Date(int year, int month, int day) 
		: _year(year)
		, _month(month)
		, _day(day)
	{
	}
	
private:
	int _year;
	int _month;
	int _day;
};

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

ostream& operator<<(ostream& out, const Date& d) {
	out << d._year << "-" << d._month << "-" << d._day << endl;
}

int main() {
	Date d1(2024, 5, 1);
	cin >> d1;
	cout << d1;
	return 0;
}

输入:

cpp 复制代码
2024 5 1

输出:

cpp 复制代码
2024-5-1

九、cout 为什么会识别不同类型?

cout 能输出 int、double、char等不同类型,本质上也是函数重载。

例如可以简单理解标准库中已经写好的很多版本:

cpp 复制代码
ostream& operator<<(ostream& out, int x);
ostream& operator<<(ostream& out, double x);
ostream& operator<<(ostream& out, char ch);
ostream& operator<<(ostream& out, const char* str);

当我们写:

cpp 复制代码
cout << 10;
cout << 3.14;
cout << 'A';

编译器会根据右操作数的类型,自动匹配对应的 operator<< 版本。

但是标准库不知道我们的 Date 类型应该怎么输出,所以自定义类型需要自己重载。


十、小结

本篇主要学习了友元函数和输入输出运算符重载。

需要记住:

  1. 友元函数用 friend 声明;
  2. 友元函数不是类的成员函数;
  3. 友元函数可以访问类的私有成员;
  4. 友元会破坏类的封装,不能滥用;
  5. operator<< 通常写成全局函数;
  6. operator<< 第一个参数是 ostream&
  7. operator<< 第二个参数一般是 const Date&
  8. operator<< 返回 ostream&,用于支持连续输出;
  9. operator>> 第一个参数是 istream&
  10. operator>> 第二个参数是 Date&,因为输入会修改对象;
  11. cout 的类型自动识别,本质上依赖函数重载。

有云函数最经典的应用场景之一,就是配合输入和输出运算符重载,让自定义类型也能像内置类型一样使用 cin 和 cout。

相关推荐
爱学习的程序媛13 小时前
C 语言全景指南:从底层原理到工业级实战
c++·c#·c
十五年专注C++开发13 小时前
C++ 序列化 Protocol Buffers:高效数据交换
开发语言·c++·序列化·反序列化·protobuf
神仙别闹13 小时前
基于QT(C++)+SQL Server 2008 实现相机租赁系统
开发语言·c++·数码相机
xier_ran13 小时前
【C++】堆(Heap)与栈(Stack)内存详解
java·开发语言·c++
黄小白的进阶之路13 小时前
C++提高编程---3.7 STL-常用容器-list 容器【P215~P222】
c++
星轨初途14 小时前
【C++ 进阶】list 核心机制解析及 vector 巅峰对决
开发语言·数据结构·c++·经验分享·笔记·list
小侯不躺平.14 小时前
C++ Boost库【6】时间戳整体综合
开发语言·c++·算法
小肝一下14 小时前
STL——list
开发语言·c++·stl·list·伊雷娜
Python+9914 小时前
C++ 注解(注释)完整讲解
java·开发语言·c++