c++类重载的输入输出运算符为什么不是类的成员函数
我们知道自定义的类可以重载输入输出运算符(<<和>>),以实现便捷的输入输出操作。
通常我们是通过友元函数而不是成员函数来实现输入输出运算符的重载的。
#include <iostream> #include <string> using namespace std; class CA{ public: CA(int _i):i(_i){} friend ostream& operator<<(ostream& os, const CA& a){ os << a.i; return os; } private: int i; }; int main() { CA a(12), b(34); cout << a << endl << b; return 0; }
那么,能否通过成员函数的方式实现输入输出运算符的重载呢?语法上可行。
#include <iostream> #include <string> using namespace std; class CA{ public: CA(int _i):i(_i){} ostream& operator<<(ostream& os){ os << i; return os; } private: int i; }; int main() { CA a(12), b(34); //a << b << cout; a << (b << cout << endl); return 0; }
但是请留意一下这个成员函数版本的输出运算符的用法。
- 对象需要出现在输出运算符<<的左侧(a<<cout),实际上是执行了a.operator<<(cout)。
- 连续输出同类型对象的写法非常别扭(a<<(b<<cout)),必须用括号扩住前一次的输出。中间如果插入其他类型的变量,需要思考一下该怎么使用,开发效率很低下。
所以,设计与实现类时,需要站在用户的角度考虑,提高代码的易用性。
侯捷-C++面向对象程序设计-学习笔记-07-c++三大函数-拷贝构造函数、拷贝赋值函数和析构函数
首先明确两个概念。
- 拷贝构造:通过已有对象来【构造】一个新的对象,这个是在对象的构造过程中发生的,会调用拷贝构造函数
- 拷贝赋值:用另一个对象给已存在的对象赋予新的值,这是为对象整体赋予新值时发生的,会调用拷贝赋值函数
- 拷贝赋值函数就是类中重载了赋值运算符(=)的那个函数
每个类都拥有拷贝构造函数和拷贝赋值函数,如果我们没有对这两个函数进行重载,编译器会生成默认的函数,其拷贝操作就是简单的【按位拷贝】(也称为【浅拷贝】),把源对象中成员变量的数据原封不动地拷贝到目标对象中。
侯捷老师把c++类分为带指针成员变量的类和不带指针成员变量的类。
对于不含有指针类型成员变量的类,使用编译器提供的默认拷贝函数是没有问题的。
而对于含有指针类型成员变量的类,按位拷贝会造成两个对象拥有相同指针指向的同一块内存空间。如果两者同时操作(读取、写入、释放)这块内存,很可能会造成脏数据、内存泄露等问题。
因此,针对包含指针类型成员变量的类,我们需要自己来实现以下三个函数:拷贝构造函数、拷贝赋值函数和析构函数。这三个函数也称为c++类的Big Three。
字符串类是一个经典的包含指针的类,我们看一下字符串类的构造函数和Big Three函数是如何实现的。
1,String类的声明
class String{ public: String(const char* cstr=0); String(const String& str); String& operator=(const String& str); ~String(); private: char* m_data; };
String类有一个m_data指针,用于指向存储字符串的内存空间。
2,构造函数
inline String::String(const char* cstr) { //判断是否为空指针 if(cstr){ m_data = new char[strlen(cstr)+1];//多分配一个字节 strcpy(m_data, cstr); } else{ m_data = new char[1]; *m_data = '\0'; } }
构造函数用于从一个c形式的字符串构造一个String对象。对于指针类型的形参,首要的工作就是判断是否为空指针。
为String的m_data分配内存时需要额外分配一个字节,用于存储结束字符。
3,拷贝构造函数
String::String(const String& str){ m_data = new char[strlen(str.m_data) + 1]; strcpy(m_data, str.m_data); }
拷贝构造函数接受一个String类型的对象,为m_data分配内存,并复制源字符串str.m_data所指内存中的数据。这种拷贝称为【深拷贝】。
有两种形式创建String对象时会调用拷贝构造函数。
String s1('Hello'); String s2(s1); String s3 = s1;
s2和s3都是通过s1来【初始化】,因此都会调用拷贝构造函数。
【还有一些场景会隐式调用拷贝构造函数。比如:
以值的方式传递对象参数;以值的方式返回对象;向容器中存放对象】
4,拷贝赋值(运算符)函数
String& operator=(const String& str){ //检测自身赋值 if(this == &str){ return *this; } //释放原有内存 delete []m_data; //执行深拷贝 m_data = new char[strlen(str.m_dadta) + 1]; strcpy(m_data, str.m_data); return *this; }
赋值运算符的重载有几点细节:
- 入参为const类型的引用,提高效率
- 需要判断是否自身赋值,即s=s。这种情况没必要进行后续的内存操作,直接返回即可
- 需要释放目标对象已有的内存,避免内存泄露
- 需要重新给m_data分配内存空间,并copy源str.m_data所指内存存放的内容,这里也是【深拷贝】。
- 返回一个目标对象的引用,这样可以使得赋值运算符应用在连续赋值的场景,即s1=s2=s3
拷贝赋值函数是在已存在的对象之间赋值时才调用的,一定要和构造函数的使用场景区分开来。
String s1("hello"), s2('World'); s2 = s1;
5,析构函数
inline String::~String(){ delete []m_data; }
我们看到构造函数、拷贝构造函数和拷贝赋值函数都为m_data分配了内存,那么在对象销毁时,要记得释放这块内存。
这项工作一般在析构函数中实现。编译器会在对象销毁时自动调用析构函数,确保内存得到了释放。
————————-笔记分割线——————–
【侯捷老师推荐的书籍】
侯捷-C++面向对象程序设计-学习笔记-06-无指针成员变量的类的设计过程
以Complex类为例,描述如何设计一个不含指针成员变量的类。
1,头文件使用防卫式声明
#ifndef __COMPLEX__ #define __COMPLEX__ #endif
2,首先考虑类中需要包含哪些数据(成员变量),数据是第一位的。
- 将这些数据声明为private
3,类需要提供哪些功能,如何设计对外的接口,以哪种形式提供这些接口(成员函数或者全局函数)
- 对于函数参数和返回值的设计,一定要考虑效率问题,优先使用引用类型的参数
- 成员函数是否会修改成员变量,如果不修改,将其声明为const
- 构造函数:通过初始化列表初始成员变量
- 运算符是否需要重载
————————-笔记分割线——————–
【侯捷老师推荐的书籍】
侯捷-C++面向对象程序设计-学习笔记-05-操作符重载和临时对象
1,c++操作符重载
【操作符也即运算符。为什么要重载运算符?这其实是为了保证我们自定义的类型和c++内置的数据类型对使用者来说有着一致的操作性,用户可以像使用内置类型一样使用我们定义的类型;同时也要保证相关操作的正确性,如果不重载,编译器要么明确提示不支持该操作,要么给出的实现和我们预期的不一致】
运算符重载有两种实现方式:
- 以成员函数的形式重载
-
class Complex{ public: inline Complex& operator +=(const Complex& r) { this->re += r.re; this->im += r.im; return *this; } private: double re, im; };
-
- 以全局函数的形式重载
-
class Complex{ public: double& real() {return re;} double& imag(){return im;} private: double re, im; }; inline Complex& operator +=(Complex& l, const Complex& r) { l.real() += r.real(); l.imag() += r.imag(); return l; }
-
2,对于返回值类型的考量
- 要不要返回值?返回对象的值还是返回对象的引用?
- 【这里其实还是以运算符的使用场景作为设计原则。以输出运算符(<<)为例。】
-
class Complex{ public: Complex(double r, double i):re(r), im(i) {} double real() const {return re;} double imag() const {return im;} private: double re, im; }; inline ostream& operator <<(ostream& os, const Complex& c) { os << c.real() << "," << c.imag(); return os; } int main() { Complex c1(1,2), c2(3,4); cout << c1 << endl << c2; return 0; }
- 如果仅仅是输出单个对象,就不需要返回值:cout << c1;
- 如果希望可以连续输出多个对象,输出运算符就得返回一个ostream对象:cout << c1 << c2;
- 对于返回对象的场景,优先考虑返回非局部对象的引用。
3,临时对象
- 直接使用typename()形式就创建了一个临时对象:Complex(1,2)
————————-笔记分割线——————–
【侯捷老师推荐的书籍】
侯捷-C++面向对象程序设计-学习笔记-04-常量成员函数-参数传递与返回值
1,c++类中的常量(const)成员函数
类似下边这种在函数圆括号之后附加const关键词的函数,称为常量成员函数。
class A{ public: void fun() const {} };
其作用是,确保类的成员变量不被修改。
我们可以使用普通的类对象来调用const成员函数,也可以使用const类对象来调用const成员函数。
A a; const A c; a.fun(); c.fun();
然而,const类对象只能调用const成员函数。这点由编译器来保证,如果使用不当,编译器会报错。
如果成员函数不改变类中的数据,则应该将其定义为常量成员函数。
2,参数传递方式
- 传值(pass by value)
- void foo(A a);
- 将实参原原本本地复制一份给形参
- 如果实参size较大,效率会很低
- 传引用(pass by reference)
- void bar(A& a);
- 通过形参直接访问实参,效率高。建议所有的参数都以引用的方式传递。
- 如果不希望函数改变实参,可以传递const类型的引用:void bar2(const A& a);
- 【这里,侯捷老师提到,引用在底层也是通过指针实现的,之前没有深究过。这还是比较容易理解的,c++是基于c实现的,c中最高效的数据传递方式就是指针了,用指针来实现引用可以说一脉相承。那为什么有了指针,还需要单独设计引用呢?显然这是为了规避指针在使用环节上的缺点(空指针、野指针、内存泄露等),提高语言的易用性。关于指针和引用的异同,可以参考这里。】
3,函数返回值方式
- 返回对象的值(return by value)
- 返回对象的引用(return by reference)
- 这也是为了提高效率,避免对象复制操作
- 但是需要注意:不要返回局部变量的引用!局部变量在函数返回后会被销毁,其引用也就没有意义了。
- 【这里的引用指左值引用,c++11中增加了右值引用,是可以返回局部变量的右值引用的。这里不深究。】
4,c++类的友元函数
- 类中通过friend声明的一个外部定义的函数,可以直接访问类的private成员变量
- 打破了类的封装性
- 同一类的对象互为友元。
-
class A{ public: int func(const A& a){return <span style="color: #0000ff;">a.i</span>;} private: int i; }; A a,b; a.func(b);
- 【对于这一点,是不是也可以从作用域的角度来理解】
-
————————-笔记分割线——————–
【侯捷老师推荐的书籍】
侯捷-c++面向对象程序设计-学习笔记
最近重学c++,看的是侯捷老师的《c++面向对象程序设计》视频,有一些新的学习心得。
本文为学习笔记目录,笔记中方括号(【】)内为我的一些看法,如有错误,还望指正。
c++博大精深,常看常新!
7,c++类中的三大函数:拷贝构造函数、拷贝赋值函数和析构函数
10,c++类的static成员、namespace和模板简介
————————-目录分割线——————–
【侯捷老师推荐的书籍】