前言
个人学习笔记
一、友元
一般来说,类的私有成员只能在类的内部访问,类之外是不能访问它们的。但如果将其他类或函数设置为类的友元(friend),就可以访问了。友元分为友元类和友元函数。目前为止,我们学过的函数形式有两种:成员函数和非成员函数(全局函数、自由函数、普通函数),友元一般有3种形式。友元的存在,使得类的接口扩展更为灵活,使用友元进行运算符重载从概念上也更容易理解一些,而且C++规则已经极力地将友元的使用限制在了一定范围内,它是单向的、不具备传递性、不能被继承,所以,应尽力合理使用友元。注意:友元的声明是不受 public/protected/private 关键字限制的。
友元的第一种形式:友元之普通函数
#include <math.h>
#include <iostream>
using std::cout;
using std::endl;
class Point
{
//友元不受访问权限的控制(public/protected/private)
friend float distance(const Point &lhs, const Point &rhs);
/* friend float distance(const Point lhs, const Point rhs); */
/* friend float distance( Point &lhs, Point &rhs); */
public:
Point(int ix = 0, int iy = 0)
:_ix(ix)
,_iy(iy)
{
cout << "Point(int = 0, int = 0)" << endl;
}
Point(const Point &rhs)
{
cout << "Point(const Point &)" << endl;
_ix = rhs._ix;
_iy = rhs._iy;
}
Point &operator=(const Point &rhs)
{
_ix = rhs._ix;
_iy = rhs._iy;
cout << "operator =(const Point &rhs)" << endl;
return *this;
}
~Point()
{
cout << "~Point()" << endl;
}
void print() const
{
cout << "(" << _ix << "," << _iy << ")" << endl;
}
private:
int _ix;
int _iy;
};
//1、友元的第一种形式:友元之普通函数(全局函数、自由函数)
float distance(const Point &lhs, const Point &rhs)
/* float distance(const Point lhs, const Point rhs) */
{
return hypot(lhs._ix - rhs._ix, lhs._iy - rhs._iy);
}
int main()
{
Point pt1(1, 2);
Point pt2(4, 6);
Point pt3 = Point();
pt3 = pt1;
pt3.print();
cout << "pt1---->pt2的距离:" << distance(pt1, pt2) << endl;
return 0;
}
友元的第二种形式:友元之成员函数
#include <math.h>
#include <iostream>
using std::cout;
using std::endl;
class Point;//类的前向声明
class Line
{
public:
//友元函数是可以重载的,但是其中一个设置为另一个类的友元的时候
//其他重载形式并不是另一个类的友元
//2、友元的第二种形式:友元之成员函数
float distance(const Point &lhs, const Point &rhs);
//不能在类里面实现,因为程序执行的时候是按代码的先后顺序执行,因为前面只有
//Point类的声明,并不知道具体的数据成员,所以只能在Point类定义后定义该函数
/* { */
/* return hypot(lhs._ix - rhs._ix, lhs._iy - rhs._iy); */
/* } */
};
class Point
{
friend float Line::distance(const Point &lhs, const Point &rhs);
public:
Point(int ix = 0, int iy = 0)
:_ix(ix)
,_iy(iy)
{
cout << "Point(int = 0, int = 0)" << endl;
}
Point(const Point &rhs)
{
cout << "Point(const Point &)" << endl;
_ix = rhs._ix;
_iy = rhs._iy;
}
Point &operator=(const Point &rhs)
{
_ix = rhs._ix;
_iy = rhs._iy;
cout << "operator =(const Point &rhs)" << endl;
return *this;
}
~Point()
{
cout << "~Point()" << endl;
}
void print() const
{
cout << "(" << _ix << "," << _iy << ")" << endl;
}
private:
int _ix;
int _iy;
};
float Line::distance(const Point &lhs, const Point &rhs)
{
return hypot(lhs._ix - rhs._ix, lhs._iy - rhs._iy);
}
int main()
{
Point pt1(1, 2);
Point pt2(4, 6);
cout << "pt1---->pt2的距离:" << Line().distance(pt1, pt2) << endl;//Line()是一个临时对象或者说匿名对象,生命周期只在本行
return 0;
}
友元的第三种形式:友元之友元类
#include <math.h>
#include <iostream>
using std::cout;
using std::endl;
class Point;//类的前向声明
//友元是单向的
//友元关系不可以传递(A->B->C,A不是C的友元),
//友元关系不能被继承
class Line
{
public:
float distance(const Point &lhs, const Point &rhs);
void setPoint(Point &lhs, int ix, int iy);
private:
int _iz;
};
class Point
{
/* friend float Line::distance(const Point &lhs, const Point &rhs); */
/* friend void Line::setPoint(Point &lhs, int ix, int iy); */
//3、友元的第三种形式:友元之友元类
/* friend class Line; //下面是简写 */
friend Line;//如果一个类中的所有成员函数都是另一个类的友元函数,那么可以把这个类当成另一个的类的友元类
public:
Point(int ix = 0, int iy = 0)
:_ix(ix)
,_iy(iy)
{
cout << "Point(int = 0, int = 0)" << endl;
}
Point(const Point &rhs)
{
cout << "Point(const Point &)" << endl;
_ix = rhs._ix;
_iy = rhs._iy;
}
Point &operator=(const Point &rhs)
{
_ix = rhs._ix;
_iy = rhs._iy;
cout << "operator =(const Point &rhs)" << endl;
return *this;
}
//Line是Point的友元类,但Point不是Line的友元类,所以在Point类中
//不能访问Line的私有数据成员
/* void setLine(Line &lhs, int iz) */
/* { */
/* lhs._iz = iz; */
/* } */
~Point()
{
cout << "~Point()" << endl;
}
void print() const
{
cout << "(" << _ix << "," << _iy << ")" << endl;
}
private:
int _ix;
int _iy;
};
float Line::distance(const Point &lhs, const Point &rhs)
{
return hypot(lhs._ix - rhs._ix, lhs._iy - rhs._iy);
}
void Line::setPoint(Point &lhs, int ix, int iy)
{
lhs._ix = ix;
lhs._iy = iy;
}
int main()
{
Point pt1(1, 2);
Point pt2(4, 6);
cout << "pt1---->pt2的距离:" << Line().distance(pt1, pt2) << endl;//Line()是一个临时对象或者说匿名对象,生命周期只在本行
cout << endl;
Line line;
line.setPoint(pt1, 5, 8);
pt1.print();
return 0;
}
二、运算符重载
C++预定义中的运算符的操作对象只局限于基本的内置数据类型,但是对于我们自定义的类型是没有办法操作的。但是大多时候我们需要对自定义的类型进行类似的运算,这个时候就需要我们对这么运算符进行重新定义,赋予其新的功能,以满足自身的需求。为了使用户自定义数据类型的数据操作与内置数据类型的数据操作形式一致,C++提供了运算符重载,通过把C++中预定义的运算符重载为类的成员函数或者友元函数,使得用户自定义类型的数据操作形式与C++内置类型的数据操作形式一致。运算符重载的实质就是函数重载或者说是函数多态。其目的在于让人能用同名的函数来完成不同的基本操作,并不是所有运算符都可以重载的,不可以重载的有5个:. .* ?: :: sizeof(sizeof是运算符不是函数)。
运算符重载有以下几条规则需要注意:
- 1、为了防止用户对内置类型进行运算符重载,C++规定重载的运算符操作对象必须至少有一个是自定义类型或枚举类型;
- 2、重载运算符之后,其优先级和结合性还是固定不变的;
- 3、重载不会改变运算符的用法,原来有几个操作数、操作数在左边还是在右边,这些都不会改变;
- 4、重载运算符函数不能有默认参数,否则就改变了运算符操作数的个数;
- 5、重载逻辑运算符(&&,||)后,不再具备短路求值特性;
- 6、不能创造一个并不存在的运算符,如@、$等;
运算符重载有三种:采用普通函数的重载形式、采用成员函数的重载形式、采用友元函数的重载形式。注意一点尽量使重载的运算符与其内置的、广为接受的语义保持一致。
运算符重载之普通函数
#include <iostream>
using std::cout;
using std::endl;
class Complex
{
public:
Complex(double dreal = 0, double dimag = 0)
:_dreal(dreal)
,_dimag(dimag)
{
cout << "Complex(double = 0, double = 0)" << endl;
}
double getReal() const
{
return _dreal;
}
double getImag() const
{
return _dimag;
}
Complex(const Complex &rhs)
{
_dreal = rhs._dreal;
_dimag = rhs._dimag;
cout << "Complex(const Complex &)" << endl;
}
Complex &operator=(const Complex &rhs)
{
_dreal = rhs._dreal;
_dimag = rhs._dimag;
cout << "Complex &operator=(const Complex &)" << endl;
return *this;
}
void print() const
{
/* cout << "(" << _dreal << "," << _dimag << ")" << endl; */
cout << _dreal << " + " << _dimag << "i" << endl;
}
~Complex()
{
cout << "~Complex()" << endl;
}
private:
double _dreal;
double _dimag;
};
//根据前面的知识,引用不能绑定到临时对象上,所以该函数返回值不需要加引用
/* Complex &operator+(const Complex &lhs, const Complex &rhs)//error */
//1、函数重载之普通函数的形式(自由函数、全局函数)
Complex operator+(const Complex &lhs, const Complex &rhs)
{
cout << "Complex operator +(const Complex &, const Complex &)" << endl;
return Complex(lhs.getReal() + rhs.getReal(), lhs.getImag() + rhs.getImag());
}
void test()
{
Complex c1(1, 2);
c1.print();
cout << endl;
Complex c2(3, 5);
c2.print();
cout << endl;
//用临时对象初始化新创建的对象,调用普通构造函数
Complex c3 = c1 + c2;
c3.print();
//用已经创建初始化新创建的对象,会调用拷贝构造函数
Complex c4 = c1;
c4.print();
/* Complex c5 = Complex(1,2); */
}
int main()
{
test();
return 0;
}
运算符重载之成员函数
代码与普通函数的类似,只需要将重载运算符的函数放入类中作为成员函数即可。
//Complex operator+(const Complex &lhs, const Complex &rhs),这种函数参数错误,因为作为成员函数的时候第一个参数有一个隐藏的this指针,
//为了保持和内置类型一致,所以只需要传递一个参数即可,依然不可以返回函数的引用,因为return的是一个临时变量,根据前面所学
//不可以返回一个临时变量的引用
//
class Complex
{
public:
//...
//2、运算符重载之成员函数的形式
//下面这种方式重载与正常需要两个参数的形式相违背,不容易看出来有2个参数
Complex operator+(const Complex &rhs)
{
cout << "Complex operator+(const Complex &)" << endl;
return Complex(_dreal + rhs._dreal, _dimag + rhs._dimag);
}
//...
};
运算符重载之友元函数(加号推荐使用友元形式,很简洁)
class Complex
{
public:
//...
friend Complex operator+(const Complex &lhs, const Complex &rhs);
//...
};
//3、函数重载之友元函数,加号推荐使用友元形式,比较简洁
//尽量使重载的运算符与其内置的、广为接受的语义保持一致。
Complex operator+(const Complex &lhs, const Complex &rhs)
{
cout << "Complex operator +(const Complex &, const Complex &)" << endl;
return Complex(lhs._dreal + rhs._dreal, lhs._dimag + rhs._dimag);
}
三、特殊运算符重载
复合赋值运算符
复合赋值运算符包括(+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=),因为对象本身会发生变化对于复合赋值运算符推荐以成员函数的形式进行重载。
class Complex
{
public:
//对于复合赋值运算符,对象本身发生了改变,对于+=运算符推荐使用成员函数形式
//因为这里返回的不是临时变量,为了提升效率,可以返回函数引用,不加的话return的时候
//会调用拷贝构造函数
Complex &operator+=(const Complex &rhs)
{
cout << "Complex &operator+=(const Complex &)" << endl;
_dreal += rhs._dreal;
_dimag += rhs._dimag;
return *this;
}
};
自增自减运算符
自增自减运算符分别包含两种形式,运算符前置与运算符后置,这两者进行的操作是不一样的,所以重载时要区分前置和后置。C++根据参数的个数来区分前置和后置形式。如果按照通常的方法(成员函数不带参数)来重载 ++/–运算符,那么重载的就是前置版本;要对后置形式进行重载,就必须为重载函数再增加一个int类型的参数,该参数仅仅用来告诉编译器这是一个运算符后置形式,在实际调用时不需要传递实参。
class Complex {
public:
//...
//前置形式,因为要返回对象本身,所以函数可以返回引用类型
//前置++比后置++执行效率高
Complex & operator++()
{
++_real;
++_image;
return *this;
}
//后置形式,返回的是临时对象,所以不需要加引用
Complex operator++(int) //int作为标记,并不传递参数
{
Complex tmp(*this);
++_real;
++_image;
return tmp;
}
//...
};
Complex c;
//(++c);//c先自增,然后作为整个式子的值;返回的是一个对象,是一个左值
//(c++);//c作为整个式子的值,然后再自增;返回的是一个临时对象,是一个右值
输入输出流运算符
cout与cin都只能打印输入内置类型,如果希望打印一个对象,则必须要重载输出流(<<)运算符,为了保持输入输出流的原有形式,>>与<<不能是成员函数,因为成员函数第一个参数是隐含的this指针,所以只能是非成员函数,一般输入输出都涉及到对象的数据成员,所以一般将其设置为类的友元函数
class Complex
{
public:
//...
friend std::ostream & operator<<(std::ostream & os, const Complex& rhs);
friend std::istream & operator>>(std::istream & is, Complex& rhs);
//...
private:
double _dreal;
double _dimag;
};
std::ostream &operator<<(std::ostream &os, const Complex &rhs)
{
if(0 == rhs._dreal && 0 == rhs._dimag)
{
os << 0 << endl;
}
else if(0 == rhs._dreal)
{
os << rhs._dimag << "i" << endl;
}
else
{
os << rhs._dreal;
if(rhs._dimag > 0)
{
os << " + " << rhs._dimag << "i" << endl;
}
else if(rhs._dimag < 0)
{
os << " - " << (-1) * rhs._dimag << "i" << endl;
}
else
{
os << endl;
}
}
return os;
}
void readDouble(std::istream &is, double &number)
{
while(is >> number, !is.eof())
{
if(is.bad())
{
std::cerr << "The istream is bad" << endl;
return;
}
else if(is.fail())
{
is.clear();//重置流的状态
is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
cout << "pls input a double data " << endl;
}
else
{
cout << "number = " << number << endl;
break;
}
}
}
std::istream &operator>>(std::istream &is, Complex &rhs)
{
/* is >> rhs._dreal >> rhs._dimag; */
readDouble(is, rhs._dreal);
readDouble(is, rhs._dimag);
return is;
}
void test()
{
Complex c1(1, 2);
/* Complex c2(1, 2); */
cout << "c1 = " << c1 << endl;//链式编程,与下行的语句等价
operator<<(operator<<(cout, "c1 = "), c1).operator<<(endl);
cout << endl;
Complex c2;
std::cin >> c2;
/* operator>>(std::cin, c2); */
cout << "c2 = " << c2 << endl;
}
函数调用运算符
我们知道,普通函数执行时,有一个特点就是无记忆性,一个普通函数执行完毕,它所在的函数栈空间就会被销毁,所以普通函数执行时的状态信息,是无法保存下来的,这就让它无法应用在那些需要对每次的执行状态信息进行维护的场景。我们学习了成员函数以后,有了对象的存在,对象执行某些操作之后,只要对象没有销毁,其状态就是可以保留下来的,但在函数作为参数传递时,会有障碍。为了解决这个问题,C++引入了函数调用运算符,函数调用运算符的重载形式只能是成员函数形式。
第一对小括号总是空的,因为它代表着我们定义的运算符名,第二对小括号就是函数参数列表了,它与普通函数的参数列表完全相同。对于其他能够重载的运算符而言,操作数个数都是固定的,但函数调用运算符不同,它的参数是根据需要来确定的, 并不固定。函数也是一种对象,这是泛型思考问题的方式。
#include <iostream>
using std::cout;
using std::endl;
//函数对象:重载了函数调用运算符的类所创建的对象,称为函数对象
class FunctionObject
{
public:
FunctionObject()
: _cnt(0)
{
}
int operator()(int x, int y)
{
cout << "int operator()(int, int)" << endl;
++_cnt;
return x + y;
}
int operator()(int x, int y, int z)
{
cout << "int operator()(int, int, int)" << endl;
++_cnt;
return x * y * z;
}
private:
int _cnt;//函数对象的状态
};
int add(int x, int y)
{
cout << "int add(int, int)" << endl;
static int cnt = 0;
++cnt;
return x + y;
}
int main(int argc, char **argv)
{
int a = 3, b = 4, c = 5;
FunctionObject fo;
cout << "fo(a, b) = " << fo(a, b) << endl;//使用形式类似函数
fo.operator()(a, b);
cout << "fo(a, b, c) = " << fo(a, b, c) << endl;
fo.operator()(a, b, c);
cout << endl;
cout << "add(a, b) = " << add(a, b) << endl;
cout << endl;
//typedef 变成一种类型,可以创建多个对象
typedef int (*pFunc)(int, int);//函数指针在使用上与普通函数形式类似
pFunc pf = &add;
cout << "pf(a, b) = " << pf(a, b) << endl;
//内联函数、宏定义、函数对象、函数指针等使用形式都与普通函数类似
return 0;
}
下标访问运算符
下标访问运算符[]通常用于访问数组元素,如果要以这种方式访问自定义类型时,则必须重载下标访问运算符,它是一个二元运算符,对下标访问运算符进行重载时,因为需要通过类创建对象后才能使用,所以该运算符只能以成员函数形式进行重载。下标运算符的重载函数只能有一个参数,不过该参数并没有类型限制,任何类型都可以。如果类中未重载下标访问运算符,编译器将会给出其缺省定义,在其表达对象数组时使用。之前使用过的std::string同样也重载了下标访问运算符,这也是为什么它能像数组一样去访问元素。
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
//int arr[10] = {1, 3, 5, 7, 9};
//arr.operator[](size_t)
//
class CharArray
{
public:
CharArray(size_t sz = 10)
: _sz(sz)
, _data(new char[_sz]())
{
cout << "CharArray(size_t = 10)" << endl;
}
#if 1
//安全性比C中的数组高
//函数加了引用 ,返回值就为左值,返回的就是一个数组,这样ca[idx] = pstr[idx]的时候就不会报错
//不加引用返回的是右值,右值不能给右值赋值,比如3 = 10,这就是右值给右值赋值
char &operator[](size_t idx)
{
if(idx < size())
{
return _data[idx];
}
else
{
static char nullchar = '\0';
return nullchar;
}
}
#endif
~CharArray()
{
cout << "~CharArray()" << endl;
if(_data)
{
delete [] _data;
_data = nullptr;
}
}
size_t size() const
{
return _sz;
}
private:
size_t _sz;
char *_data;
};
void test()
{
const char *pstr = "hello,world";
CharArray ca(strlen(pstr) + 1);
for(size_t idx = 0; idx != ca.size(); ++idx)//size_t 无符号整型,可以跨平台使用
{
/* 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)
{
test();
return 0;
}
成员访问运算符
成员访问运算符包括箭头访问运算符(->)和解引用运算符(*),箭头运算符和解引用运算符都只能以成员函数的形式重载,其返回值必须是一个指针或者重载该运算符的对象。
#include <iostream>
using std::cout;
using std::endl;
class Data
{
public:
Data(int data = 20)
: _data(data)
{
cout << "Data(int = 20)" << endl;
}
int getData() const
{
return _data;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int _data;
};
class SecondLayer
{
public:
SecondLayer(Data *pdata)
: _pdata(pdata)
{
cout << "SecondLayer(Data *)" << endl;
}
Data *operator->()
{
return _pdata;
}
Data &operator*()
{
return *_pdata;
}
~SecondLayer()
{
cout << "~SecondLayer()" << endl;
if(_pdata)
{
delete _pdata;
_pdata = nullptr;
}
}
private:
Data *_pdata;;
};
class ThirdLayer
{
public:
ThirdLayer(SecondLayer *psl)
: _psl(psl)
{
cout << "ThirdLayer(SecondLayer *)" << endl;
}
SecondLayer &operator->()
{
return *_psl;
}
~ThirdLayer()
{
cout << "~ThirdLayer()" << endl;
if(_psl)
{
delete _psl;
_psl = nullptr;
}
}
private:
SecondLayer *_psl;
};
int main(int argc, char **argv)
{
SecondLayer sl(new Data());
cout << sl->getData() << endl;//智能指针的雏形
sl.operator->()->getData();
(*sl).getData();
sl.operator*().getData();
cout << endl;
ThirdLayer tl(new SecondLayer(new Data(10)));
cout << tl->getData() << endl;
cout << tl.operator->().operator->()->getData() << endl;
return 0;
}
四、建议
- 1、所有的一元运算符(自增、自减),建议以成员函数重载;
- 2、运算符=,(),[],->,*等,必须以成员函数重载;
- 3、运算符+=,-=,/=,*=,%=,^=,&=,!=,>>=,<<=建议以成员函数形式重载;
- 4、其它二元运算符(+,-,*,/),建议以非成员函数重载;