C++11QT复习 (四)
Day6-1 输入输出流运算符重载(2025.03.25)
1. 拷贝构造函数的调用时机
2. 友元
2.1 友元函数
3. 输入输出流运算符重载
3.1 关键知识点
3.2 代码
3.3 关键问题
3.4 完整代码
4. 下标访问运算符 `operator[]`
4.1 关键知识点
4.2 代码
5. 函数调用运算符 `operator()`
5.1 关键知识点
5.2 代码
5.3 示例
5.4 完整代码
6. 总结
1. 回顾
1.1 拷贝构造函数的调用时机
拷贝构造函数在以下情况会被调用:
-
对象初始化
- 当用一个已经存在的对象去初始化一个刚刚创建的对象时,调用拷贝构造函数。
- 例:
Complex c1(1, 2); Complex c2 = c1; // 调用拷贝构造函数
-
函数参数传递
- 当形参与实参都是对象时,在函数调用时会调用拷贝构造函数。
- 例:
void func(Complex c) { } func(c1); // 形参与实参结合时调用拷贝构造函数
-
函数返回对象
- 当函数返回一个对象时,可能调用拷贝构造函数(但现代 C++ 编译器会尝试优化此过程,如返回值优化 RVO)。
- 例:
Complex func() { Complex c(3, 4); return c; // 可能调用拷贝构造函数 }
2. 友元
2.1 友元函数
- 友元函数可以访问类的私有成员。
- 友元声明可以出现在类的
public
、protected
或private
部分,不影响其权限。 - 友元关系是单向的、不可传递的:
- 单向:如果
A
是B
的友元,B
并不会自动成为A
的友元。 - 不可传递:如果
A
是B
的友元,B
是C
的友元,A
并不会自动成为C
的友元。
- 单向:如果
3. 输入输出流运算符重载
3.1 关键知识点
-
operator<<
必须是友元函数- 由于
cout << c1;
左操作数是std::ostream
,不能修改std::ostream
,所以operator<<
不能是Complex
的成员函数。
- 由于
-
operator>>
不能是const
成员函数- 因为
operator>>
需要修改对象的值,因此不能加const
。
- 因为
3.2 代码
std::ostream& operator<<(std::ostream& os, const Complex& rhs) {
if (rhs._imag > 0) {
os << rhs._real << " + " << rhs._imag << "i";
} else if (rhs._imag == 0) {
os << rhs._real;
} else {
os << rhs._real << " - " << -rhs._imag << "i";
}
return os;
}
std::istream& operator>>(std::istream& is, Complex& rhs) {
std::cout << "请输入复数的实部和虚部:" << std::endl;
is >> rhs._real >> rhs._imag;
return is;
}
3.3 关键问题
-
为什么
operator<<
和operator>>
的返回值是std::ostream&
和std::istream&
?- 这样可以实现连续输入输出:
cout << c1 << c2 << endl; // 连续输出 cin >> c1 >> c2; // 连续输入
- 这样可以实现连续输入输出:
-
为什么
operator<<
的ostream&
参数不能去掉&
?- 因为
ostream
的拷贝构造函数已被delete
,不能复制ostream
对象。
- 因为
3.4 完整代码
#include <iostream>
#include <limits.h>
#include <ostream>
using namespace std;
//复数
class Complex
{
friend Complex operator*(const Complex& lhs, const Complex& rhs);
//Complex operator/(const Complex& rhs) const;
public:
Complex(double r = 0, double i = 0)
: _real(r), _imag(i)
{
cout << "Complex(double r = 0, double i = 0)" << endl;
}
~Complex()
{
cout << "~Complex()" << endl;
}
void display() const
{
if (_imag > 0)
{
cout << _real << " + " << _imag << "i" << endl;
}
else if (_imag == 0)
{
cout << _real << endl;
}
else
{
cout << _real << " - " << -_imag << "i" << endl;
}
}
//成员函数,operator输出运算符重载
//cout << c1 << endl;
//第一个参数cout ,第二个参数c1
//std::ostream& operator<<(std::ostream& os, const Complex& rhs); //error!隐含this指针
//对于输出流运算符函数而言,不能写成成员函数的形式,因为违背了运算符重载的原则,不能改变操作数的顺序
//std::ostream& operator<<(std::ostream& os);//error!this指针在参数列表的第一个位置
/*友元函数可以放在类的 任何位置(public / protected / private),不影响其功能。
从代码规范角度,建议统一放在类定义的开头或结尾,以提高可读性。
友元关系是单向的,且不具备传递性(即类 A 的友元函数不会自动成为类 B 的友元)。
*/
friend std::ostream& operator<<(std::ostream& os, const Complex& rhs);
//输入流运算符重载
friend std::istream& operator>>(std::istream& is, Complex& rhs);
private:
double _real;
double _imag;
};
//问题1 :参数列表中ostrream的引用符号&能不能去掉?
//解答1 :不能去掉因为形参"os"和实参"cout"结合的时候会满足拷贝构造函数的调用时机,但是ostream中的拷贝构造函数已被delete
//问题2 : 函数返回类型中的引用符号&能不能删除?
//解答2 : 不能取掉,因为return os,返回类型满足拷贝构造函数的调用时机3
//注释:basic_ifstream( const basic_ifstream& rhs ) = delete; (7) (since C++11)
// basic_ofstream( const basic_ofstream& rhs ) = delete; (7) (since C++11)
//ifstram 和 ofstream 的拷贝构造函数已经从C++11开始删除了
std::ostream& operator<<(std::ostream& os, const Complex& rhs)
{
cout << "std::ostream& operator<<(std::ostream& os, const Complex& rhs)" << endl;
if (rhs._imag > 0)
{
os << rhs._real << " + " << rhs._imag << "i" << endl;
}
else if (rhs._imag == 0)
{
os << rhs._real << endl;
}
else
{
os << rhs._real << " - " << -rhs._imag << "i" << endl;
}
return os;
}
void readDouble(std::istream& is, double& rhs)
{
while (is >> rhs, !is.eof())
{
if (is.bad())
{
std::cerr << "istream is bad" << endl;
return;
}
else if (is.fail())
{
is.clear();//重置流的状态
is.ignore(std::numeric_limits<::std::streamsize>::max(), '\n');//清空缓冲区
cout << "请注意:需要输入double类型的数据!";
}
else
{
cout << "rhs = " << rhs << endl;
break;
}
}
}
//输入流运算符重载
std::istream& operator>>(std::istream& is, Complex& rhs)//因为要修改rhs 的值,所以不能加const
{
cout << "std::istream& operator>>(std::istream& is, Complex& rhs)" << endl;
cout << "请分别输入复数的实部和虚部:" << endl;
//is >> rhs._real >> rhs._imag;
readDouble(is, rhs._real);
readDouble(is, rhs._imag);
return is;
}
void testOutputOperator()
{
Complex c1(1, 2);
cout << "c1 = ";
c1.display();
cout << endl << endl;
//cout << "c1 = " << c1.display();
//为什么没有做输出流运算符重载之前上面的写法 不可行呢? 解答:二元“<<”: 没有找到接受“void”类型的右操作数的运算符(或没有可接受的转换)
cout << "c1 = " << c1 << endl;
cout << endl;
Complex c2;
cin >> c2;
cout << "c2 = " << c2 << endl;
}
int main(int argc, char* argv[])
{
testOutputOperator();
return 0;
}
4. 下标访问运算符 operator[]
4.1 关键知识点
operator[]
主要用于自定义数组类型,使得obj[idx]
访问数组元素。- 返回值应为
T&
,以保证能够修改数组内容。 - 必须进行越界检查,以防止访问非法内存。
4.2 代码
char& CharArrar::operator[](size_t idx) {
if (idx < _size) {
return _data[idx];
} else {
static char charNull = '\0';
return charNull;
}
}
5. 函数调用运算符 operator()
5.1 关键知识点
- 使对象像函数一样调用,称为仿函数(函数对象)。
- 可存储状态,例如调用次数。
5.2 代码
class FunctionObject {
public:
int operator()(int x, int y) {
++_cnt;
return x + y;
}
int operator()(int x, int y, int z) {
++_cnt;
return x * y * z;
}
private:
int _cnt = 0;
};
5.3 示例
FunctionObject fo;
cout << "fo(3, 4) = " << fo(3, 4) << endl;
cout << "fo(3, 4, 5) = " << fo(3, 4, 5) << endl;
5.4 完整代码
//bracket.h
#pragma once
#include <iostream>
#include <string.h>
using namespace std;
class CharArrar
{
public:
CharArrar(size_t sz = 10)
:_size(sz)
, _data(new char[_size])
{
cout << " CharArrar(size_t sz = 10)" << endl;
}
~CharArrar()
{
cout << "~CharArrar()" << endl;
if (_data)
{
delete _data;
_data = nullptr;
}
}
size_t size() const
{
return _size;
}
//下标访问运算符的重载
//int arr[10] = { 1,2,3,4,5 };
//arr.operator[](idx);
char& operator[](size_t idx);
private:
size_t _size;
char* _data;
};
//bracket.cpp
#include "bracket.h"
/*
要修复错误 C2106: “=”: 左操作数必须为左值,需要确保 CharArrar 类的
下标运算符 operator[] 返回一个可修改的左值。当前的 operator[] 声明
返回的是一个 char,这不是一个可修改的左值。我们需要将其修改为返回一个 char&。
*/
char& CharArrar::operator[](size_t idx)
{
if (idx < _size)
{
return _data[idx];
}
else
{
static char charNUll = '\0';//静态变量延长生命周期
return charNUll; //使得返回的实体生命周期比函数的生命周期长
}
}
//C++中优势:重载的下标访问运算符考虑了越界的问题
//引用什么时候需要加上?
//1.如果返回类型是类型的时候,可以减少拷贝构造函数的执行
//2.有可能需要一个左值(来调用右操作数),而不是拷贝后的右值、
//3.cout << "111" << c1 << endl << 10 << 1 << endl,像这种情况下连续使用的时候可以加上引用
//parenthese.h
#pragma once
#include <iostream>
using namespace std;
class FunctionObject
{
public:
int operator()(int x, int y);
int operator()(int x, int y, int z);
private:
int _cnt;//记录被调用的次数(函数对象状态)
};
parenthese.cpp
#include <iostream>
#include "parenthese.h"
using namespace std;
int FunctionObject::operator()(int x, int y)
{
++_cnt;
cout << "int operator()(int x, int y)" << endl;
return x + y;
}
int FunctionObject::operator()(int x, int y, int z)
{
cout << "int operator()(int x, int y,int z)" << endl;
++_cnt;
return x * y * z;
}
//main.cpp
#include <iostream>
#include "bracket.h"
#include "parenthese.h"
using namespace std;
int add(int x, int y)
{
cout << "int add(int x,int y)" << endl;
static int cnt = 0;
++cnt;
return x + y;
}
void testFunctionObject()
{
FunctionObject fo;
int a = 3;
int b = 4;
int c = 5;
//fo本质上是一个对象,但是他的使用
cout << "fo(a,b) = " << fo(a, b) << endl;
cout << "fo(a, b ,c) = " << fo(a, b, c) << endl;
cout << endl;
//正经的函数
cout << "add(a, b) = " << add(a, b) << endl;
}
void testCharArrar()
{
//把字符串中的内容拷贝到CharArray
const char* pstr = "hello cpp";
CharArrar ca(strlen(pstr) + 1);
for(size_t idx = 0; idx != ca.size(); ++idx)
{
//ca[idx] = pstr[idx];
//上面和下面两条代码等价
ca.operator[](idx) = pstr[idx];
}
for (size_t idx = 0; idx != ca.size(); ++idx)
{
cout << ca[idx] << " ";
}
cout << endl;
}
int main(int argc, char** argv)
{
testFunctionObject();
testCharArrar();
return 0;
}