哪些网站可以做爬虫实验,酒店如何做网站,网站模版,灵犀科技 网站开发为什么需要对运算符进行重载
C预定义中的运算符的操作对象只局限于基本的内置数据类型#xff0c;但是对于我们自定义的类型是没有办法操作的。但是大多时候我们需要对我们定义的类型进行类似的运算#xff0c;这个时候就需要我们对这么运算符进行重新定义#xff0c;赋予其…为什么需要对运算符进行重载
C预定义中的运算符的操作对象只局限于基本的内置数据类型但是对于我们自定义的类型是没有办法操作的。但是大多时候我们需要对我们定义的类型进行类似的运算这个时候就需要我们对这么运算符进行重新定义赋予其新的功能以满足自身的需求。比如:
class Complex
{
public:Complex(double real 0, double image 0): _real(real), _image(image){}
private:double _real;double _image;
};
void test()
{Complex c1(1, 2), c2(3, 4);Complex c3 c1 c2;//编译出错
}为了使对用户自定义数据类型的数据的操作与内置数据类型的数据的操作形式一致C提供了运算符的重载通过把C中预定义的运算符重载为类的成员函数或者友元函数使得对用户的自定义数据类型的数据(对象)的操作形式与C内部定义的类型的数据一致。
运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的 C 多态。目的在于让人能够用同名的函数来完成不同的基本操作。要重载运算符需要使用被称为运算符函数的特殊函数形式运算符函数形式
返回类型 operator 运算符(参数表)
{//...
}运算符重载的规则
运算符是一种通俗、直观的函数比如int x 2 3; 语句中的 “” 操作符系统本身就提供了很多个重载版本
int operator(int, int);
double operator(double, double);但并不是所有的运算符都可以重载。可以重载的运算符有 运算符重载还具有以下规则
1、为了防止用户对标准类型进行运算符重载C规定重载的运算符的操作对象必须至少有一个是自定义类型或枚举类型
2、重载运算符之后其优先级和结合性还是固定不变的。
3、重载不会改变运算符的用法原来有几个操作数、操作数在左边还是在右边这些都不会改变。
4、重载运算符函数不能有默认参数否则就改变了运算符操作数的个数。
5、重载逻辑运算符,||后不再具备短路求值特性。
6、不能臆造一个并不存在的运算符如、$等
运算符重载的形式
运算符重载的形式有三种
1、采用普通函数的重载形式
2、采用成员函数的重载形式
3、采用友元函数的重载形式
以普通函数形式重载
在上面的例子中Complex对象无法执行加法操作接下来我们重载运算符。由于之前的定义中Complex的成员都设置成了private成员所以不能访问我们需要在类中添加2个get函数获取其值。
class Complex
{
public:Complex(double real 0, double image 0): _real(real), _image(image){}public:double getReal() const{return _real;}double getImage() const{return _image;}private:double _real;double _image;
};Complex operator(const Complex lhs, const Complex rhs)
{return Complex(lhs.getReal() rhs.getReal(), lhs.getImage() rhs.getImage());
}void test()
{Complex c1(1, 2), c2(3, 4);Complex c3 c1 c2;//此时编译通过
}以成员函数形式重载
成员函数形式的运算符声明和实现与成员函数类似首先应当在类定义中声明该运算符声明的具体形式为
返回类型 operator 运算符参数列表;既可以在类定义的同时定义运算符函数使其成为 inline 型也可以在类定义之外定义运算符函数但要使用作用域限定符::类外定义的基本格式为
返回类型 类名::operator 运算符参数列表
{//...
}注意用成员函数重载双目运算符时左操作数无须用参数输入而是通过隐含的 this 指针传入。回到 Complex 的例子如果以成员函数形式进行重载则不需要定义get函数
class Complex
{
public://...Complex operator(const Complex rhs){return Complex(_real rhs._real, _image rhs._image);}
};以友元函数形式重载
如果以友元函数形式进行重载同样不需要定义get函数
class Complex{//...friend Complex operator(const Complex lhs, const Complex rhs);
};Complex operator(const Complex lhs, const Complex rhs){return Complex(lhs._real rhs._real, lhs._image rhs._image);
}运算符重载可以改变运算符内置的语义如以友元函数形式定义的加操作符
Complex operator(const Complex lhs,const Complex rhs){return complex(lhs._real - rhs._real, lhs._image - rhs._image);
}明明是加操作符但函数内却进行的是减法运算这是合乎语法规则的不过却有悖于人们的直觉思维会引起不必要的混乱。因此除非有特别的理由尽量使重载的运算符与其内置的、广为接受的语义保持一致。
特殊运算符的重载
复合赋值运算符
复合赋值运算符推荐以成员函数的形式进行重载包括这些(-*%^|)因为对象本身会发生变化。
class Complex
{
public://对于复合赋值运算符对象本身发生了改变推荐使用成员函数形式Complex operator(const Complex rhs){cout Complex operator(const Complex ) endl;_dreal rhs._dreal;_dimag rhs._dimag;return *this;}Complex operator-(const Complex rhs){cout Complex operator-(const Complex ) endl;_dreal - rhs._dreal;_dimag - rhs._dimag;return *this;}
};自增自减运算符
自增运算符和自减运算符–推荐以成员函数形式重载分别包含两个版本即运算符前置形式(如 x)和运算符后置形式(如 x)这两者进行的操作是不一样的。因此当我们在对这两个运算符进行重载时就必须区分前置和后置形式。
C根据参数的个数来区分前置和后置形式。如果按照通常的方法成员函数不带参数来重载/–运算符那么重载的就是前置版本。要对后置形式进行重载就必须为重载函数再增加一个int类型的参数该参数仅仅用来告诉编译器这是一个运算符后置形式在实际调用时不需要传递实参。
class Complex
{
public://...//前置形式Complex operator(){_real;_image;return *this;}//后置形式Complex operator(int) //int作为标记并不传递参数{Complex tmp(*this);_real;_image;return tmp;}
};
void test()
{int a 3;int b 4;(a);//表达式的值与a的值需要进行区分对于重载前置与后置是有一定参考价值的(a)
}赋值运算符
对于赋值运算符只能以成员函数形式进行重载我们已经在类和对象中讲过了就不再赘述大家可以翻看前面的内容。
函数调用运算符
我们知道普通函数执行时有一个特点就是无记忆性一个普通函数执行完毕它所在的函数栈空间就会被销毁所以普通函数执行时的状态信息是无法保存下来的这就让它无法应用在那些需要对每次的执行状态信息进行维护的场景。大家知道我们学习了成员函数以后有了对象的存在对象执行某些操作之后只要对象没有销毁其状态就是可以保留下来的但在函数作为参数传递时会有障碍。为了解决这个问题C引入了函数调用运算符。函数调用运算符的重载形式只能是成员函数形式其形式为:
返回类型 类名::operator()(参数列表){//...
}在定义 () 运算符的语句中第一对小括号总是空的因为它代表着我们定义的运算符名第二对小括号就是函数参数列表了它与普通函数的参数列表完全相同。对于其他能够重载的运算符而言操作数个数都是固定的但函数调用运算符不同它的参数是根据需要来确定的 并不固定。
接下来我们来看一个例子:
class FunctionObject
{
public:FunctionObject(int count 0) : _count(count){}void operator()(int x){_count;cout x x endl;}int operator()(int x, int y){_count;return x y;}int _count;//函数对象的状态
};
void test()
{FunctionObject fo;int a 3, b 4;fo(a);cout fo(a, b) endl;
}从例子可以看出一个类如果重载了函数调用operator()就可以将该类对象作为一个函数使用。对于这种重载了函数调用运算符的类创建的对象我们称为函数对象Function Object。函数也是一种对象这是泛型思考问题的方式。
下标访问运算符
下标访问运算符 [] 通常用于访问数组元素它是一个二元运算符如 arr[ idx ] 可以理解成 arr 是左操作数idx 是右操作数。对下标访问运算符进行重载时只能以成员函数形式进行如果从函数的观点来看语句 arr[idx] ;可以解释为 arr.operator[] (idx) ;因此下标访问运算符的重载形式如下
返回类型 类名::operator[](参数类型);
返回类型 类名::operator[](参数类型) const;下标运算符的重载函数只能有一个参数不过该参数并没有类型限制任何类型都可以。如果类中未重载下标访问运算符编译器将会给出其缺省定义在其表达对象数组时使用。
class CharArray
{
public:CharArray(size_t size 10): _size(size), _array(new char[_size]()){}char operator[](size_t idx){if (idx _size){return _array[idx];}else{static char nullchar \0;return nullchar;}}const char operator[](int idx) const//针对的是const对象{if (idx _size){return _array[idx];}else{static char nullchar \0;return nullchar;}}~CharArray(){delete[] _array;}
private:size_t _size;char* _array;
};我们之前使用过的std::string同样也重载了下标访问运算符这也是为什么它能像数组一样去访问元素的原因。
成员访问运算符
成员访问运算符包括箭头访问运算符-和解引用运算符*我们先来看箭头运算符-.
箭头运算符只能以成员函数的形式重载其返回值必须是一个指针或者重载了箭头运算符的对象。来看下例子
class Data
{
public:int getData() const{return _data;}
private:int _data;
};class MiddleLayer
{
public:MiddleLayer(Data* pdata): _pdata(pdata){}//返回值是一个指针Data* operator-(){return _pdata;}Data operator*(){return *_pdata;}~MiddleLayer(){delete _data;}
private:Data* _pdata;
};class ThirdLayer
{
public:ThirdLayer(MiddleLayer* ml): _ml(ml){}//返回一个重载了箭头运算符的对象MiddleLayer operator-(){return *_ml;}~ThirdLayer(){delete _ml;}
private:MiddleLayer* _ml;
};void test()
{MiddleLayer ml(new Data());cout ml-getData() endl;cout (ml.operator-())-getData() endl;cout (*ml).getData() endl;ThirdLayer tl(new MiddleLayer(new Data()));cout tl-getData() endl;cout ((tl.operator-()).operator-())-getData() endl;
}输入输出流运算符
在之前的例子中我们如果想打印一个对象时常用的方法是通过定义一个 print 成员函数来完成但使用起来不太方便。我们希望打印一个对象与打印一个整型数据在形式上没有差别(如下例子)那就必须要重载 运算符。
void test()
{int a 1, b 2;cout a b endl;Point pt1(1, 2), pt2(3, 4);cout pt1 pt2 endl;
}从上面的形式能看出cout 是左操作数a 或者 pt1 是右操作数那输入输出流能重载为成员函数形式吗我们假设是可以的由于非静态成员函数的第一个参数是隐含的 this 指针代表当前对象本身这与其要求是冲突的因此 和 不能重载为成员函数只能是非成员函数如果涉及到要对类中私有成员进行访问还得将非成员函数设置为类的友元函数。
class Point
{
public://...friend ostream operator(ostream os, const Point rhs);friend istream operator(istream is, Point rhs);
private:int _ix;int _iy;
};ostream operator(ostream os, const Point rhs)
{os ( rhs._ix , rhs._iy );return os;
}istream operator(istream is, Point rhs)
{is rhs._ix;is rhs._iy;return is;
}通常来说重载输出流运算符用得更多一些。同样的输入流运算符也可以进行重载如上。
总结
对于运算符重载时采用的形式的建议
1、所有的一元运算符建议以成员函数重载
2、运算符 () [] - * 必须以成员函数重载
3、运算符 - / * % ^ ! 建议以成员函数形式重载
4、其它二元运算符建议以非成员函数重载
类型转换
前面介绍过对普通变量的类型转换比如说 int 型转换为 long 型double 型转换为 int 型接下来我们要讨论下类对象与其他类型的转换。转换的方向有:
1、由其他类型向自定义类型转换
2、由自定义类型向其他类型转换
由其它类型向自定义类型转换
由其他类型向定义类型转换是由构造函数来实现的只有当类中定义了合适的构造函数时转换才能通过。这种转换一般称为隐式转换。下面我们通过一个例子进行说明:
class Point
{
public:Point(int ix 0, int iy 0): _ix(ix), _iy(iy){}//...friend ostream operator(ostream os, const Point rhs);
private:int _ix;int _iy;
};
ostream operator(ostream os, const Point rhs)
{os ( rhs._ix , rhs._iy );return os;
}
void test()
{Point pt 1;//隐式转换cout pt pt endl;
}这种隐式转换有时候用起来是挺好的比如我们以前学过的std::string当执行
std::string s1 hello,world;该语句时这里其实是有隐式转换的但该隐式转换的执行很自然很和谐。而上面把一个 int 型数据直接赋值给一个 Point 对象看起来就是比较诡异的难以接受所以这里我们是不希望发生这样的隐式转换的。那怎么禁止隐式转换呢比较简单只需要在相应构造函数前面加上 explicit 关键字就能解决。
由自定义类型向其它类型转换
由自定义类型向其他类型的转换是由类型转换函数完成的这是一个特殊的成员函数。它的形式如下
operator 目标类型()
{//...
}类型转换函数具有以下的特征
1、必须是成员函数
2、参数列表中没有参数
3、没有返回值但在函数体内必须以return语句返回一个目标类型的变量。
我们来看一个例子:
class Fraction
{
public:Fraction(double numerator, double denominator): _numerator(numerator), _denominator(denominator){}operator double(){return _numerator / _denominator;}operator Point(){return Point(_numerator, _denominator);}
private:double _numerator;double _denominator;
};
void test()
{Fraction f(2, 4);cout f f endl;double x f 1.11;cout x x endl;double y f;
}补充类作用域
作用域可以分为类作用域、类名的作用域以及对象的作用域几部分内容。在类中定义的成员变量和成员函数的作用域是整个类这些名称只有在类中包含类的定义部分和类外函数实现部分是可见的在类外是不可见的因此可以在不同类中使用相同的成员名。另外类作用域意味着不能从外部直接访问类的任何成员即使该成员的访问权限是 public 也要通过对象名来调用对于 static 成员函数要指定类名来调用。
如果发生 “屏蔽” 现象类成员的可见域将小于作用域但此时可借助 this 指针或 “类名::” 形式指明所访问的是类成员这有些类似于使用 :: 访问全局变量。例如:
#include iostream
using std::cout;
using std::endl;int num 1;namespace test
{int num 20;class Example{public:void print(int num) const{cout 形参num num endl;cout 数据成员num this-num endl;cout 数据成员num Example::num endl;cout 命名空间中num test::num endl;cout 全局变量num ::num endl;}private:int num;};
}//end of namespace test和函数一样类的定义没有生存期的概念但类定义有作用域和可见域。使用类名创建对象时首要的前提是类名可见类名是否可见取决于类定义的可见域该可见域同样包含在其作用域中类本身可被定义在3种作用域内这也是类定义的作用域。
全局作用域
在函数和其他类定义的外部定义的类称为全局类绝大多数的 C 类是定义在该作用域中我们在前面定义的所有类都是在全局作用域中全局类具有全局作用域。
类作用域
一个类可以定义在另一类的定义中这是所谓嵌套类或者内部类举例来说如果类 A 定义在类 B 中如果 A 的访问权限是 public 则 A 的作用域可认为和 B 的作用域相同不同之处在于必须使用 B::A 的形式访问 A 的类名。当然如果 A 的访问权限是 private 则只能在类内使用类名创建该类的对象无法在外部创建 A 类的对象。
class Line
{
public:Line(int x1, int y1, int x2, int y2);void printLine() const;
private:class Point{public:Point(int x 0, int y 0): _x(x), _y(y){}void print() const;private:int _x;int _y;};
private:Point _pt1;Point _pt2;
};
Line::Line(int x1, int y1, int x2, int y2): _pt1(x1, y1), _pt2(x2, y2)
{
}
void Line::printLine() const
{_pt1.print();cout --- ;_pt2.print();cout endl;
}
void Line::Point::print() const
{cout ( _x , _y );
}