我们知道 c++ 中可以使用 cout 进行输出打印,因为 << 也是一个运算符,有了前面介绍的运算符重载的概念我们知道 cout << 对象,就是一个运算符重载。在 c++ 中打印函数可以自动识别类型,这是为什么呢?
int main()
{
int i = 0;
double j = 1.11;
cout << i ;
cout << j;
return 0;
}
因为 cout 是一个 ostream 类型的全局对象,因为我们在写代码时需要包一个 iostream 的头文件,而 c++ 库里面定义的东西,都在 std 的命名空间,所以这些定义会被展开。
由于 ostream 存在于库里面,本质就是库里面写好的一个函数调用,在库里面默认对内置类型进行了支持,所以可以直接去使用。
有了以上的概念,我们知道在 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 函数的定义处,编译器并未生成汇编代码,所以会链接失败。
我们看如下代码
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 对象和普通对象都可以调用。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成,如果是自己写,就如下列代码:
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!但是没有人会无聊到做这样的事情。。。