从函数重载说起
函数重载是在一定的作用域内,多个名称相同,但是参数不同的函数重载,在编译时由编译器根据给的参数类型来决定调用哪一个函数,这个过程被称为“重载决策”,重载的本质还是多个独立的函数,函数重载发生在编译过程,与运行无关,函数重载的意义就是避免我们给函数乱起名,也是为了方便编写类库覆盖各种操作,函数重载也是一种语法糖。
什么是运算符重载
我们平时用到的算数运算符如+、-、×、÷和关系运算符>、 =、 < 就是典型的可以运算符重载的符号,然而并不是所有的运算符都可以被重载,我们在一些普通的变量进行“预定义”的运算时大家都可以理解,然而我们要实现自定义的类型变量的运算,如一个对象与另外一个对象进行运算,就不易操作,这就是运算符重载要解决的问题。
重载示例
废话不多说,先上个示例来理解一下运算符重载,如下:
class coordinate //定义一个类 coordinate
{
public:
int x;
coordinate operator+(coordinate aa);//运算符重载函数
};
coordinate coordinate::operator+(coordinate aa)//重载函数实体
{
return (this->x + aa.x);
}
int main(void)
{
coordinate a,b,c; //定义一个对象abc
c=a+b; //这句话的实指是c=b.operator+(a);
}
上例中,operator是运算符重载的关键字,在operator后面跟什么符号,就是对什么符号进行重定义(重载),要注意的是operator与跟在后面的符号不能有空格。
如c=a+b是对“+”的意义进行了重定义。 “+”这个符号映射到类中的operator+这个函数,完全可以写成c=a.operator(b),运算符重载其实就是重定义对象运算符对应的函数。
运算符重载是c++的语法特性,只有c++支持得比较好,java没有运算符重载,而pyton仅保留了一些简单的,主要是因为这东西太灵活,学习难度偏大,且易出事。其实没有运算符同样可以实现目标的,直接调用函数即可达到目的。运算符重载是一种语法糖,复杂的实现都隐藏在了内部。正因如此,所以是类库编写者工作变复杂了,而类库使用者的工作变简单了。运算符重载使得同样的符号,意义不同,体现了c++的多态性,
在上例中,a+b不一定就等于b+a,这主要需要重载函数是怎么做的运算,算是运算符重载延伸的风险点。
this指针
编译器会为每个自定义的class提供一个默认的“赋值=”运算符重载,当然我们可以重写来覆盖,所以在c++中同类型的class对象可以相互直接赋值,这是c中没有的。
如下示例,a=b对“=”号进行重载,解析后a就是this,b就是形参。
a=b;//实际上是a.operator(b)
coordinate coordinate::operator=(coordinate aa)
{
x = aa.x;
y = aa.y;
return *this; //返回当前对象的指针。
}
需要注意的是,上例中return *this是一个值传递,如果this返回的是普通变量就直接复制,如果是对象,就会调用对象的拷贝构造函数。
运算符重载总则
运算符左边是this,右边是other(函数形参),整个表达式的返回值就是就是函数的返回值。
运算符重载函数的细节
赋值运算符重载 & 拷贝构造函数
如下示例,class的初始化赋值与非初始化赋值的解析方法是不同的,初始化赋值背后调用的是“拷贝构造函数”。非初始化赋值会调用“运算符重载函数,如果函数内有return *this 还会再次调用拷贝构造函数。
class person a=b;//调用拷贝构造函数
a=c; //调用“=”运算符重载函数
自赋值引起错误
如class a;a=a;这种做法是没有意义的,轻则浪费效率,重则导致内存丢失,因为会调用到拷贝函数,如果是深拷贝,意味着会去申请内存。为了避免这个严重的问题,通常在赋值运算符重载时会添加一个判断,示例如下:
coordinate coordinate::operator=(const coordinate &aa)
{
if(this != &aa) //检查不是自赋值
{
x = aa.x;
}
return *this;
}
返回值内存不容修改
原本课程讲解的是运算符重载函数返回值引用问题,然而经过我的思考和试验发现本节的内容并不是运算符重载引起的,而是一个c++语言的基本特性,如下示例,
int a=30,b=40;
int c= div ( and (a,b) );//实现c=(a+b)/10
int and(int a,int b)//求和
{
return (a+b);
}
int div(const int &a) //对值取余,引用时必须有const
{
return (a /10);
}
定义了变量a、b、c三个变量,and函数计算a+b的结果后返回值以“传址”传参给div函数,div函数的传参中使用了“引用”也就是直接使用了栈上临时分配的地址,必须添加cosnt编译器才不报错,即不能修改,我分析有两点:
- 栈上临时分配的地址未与任何变量绑定,我们没办法二次访问到该地址,所以对其修改是没有任何意义。
- 返回值赋值完成后该地址会被分配给其它函数,如果此时被修改,会照成灾难性错误。
所以要求要引用该地址时必须添加const来保证不修改,平时我们一般不会这么用,但是在运算符重载时可能会这么用。所以使用运算符重载的时候要注意加const。
重载函数返回引用
赋值运算符重载函数返回值应优先使用引用,虽然返回对象也可以,但是效率比较低,返回一个对象时,c++会为我们临时建立一个对象,再调用拷贝构造函数来对值进行拷贝,拷贝完成后释放临时建立的对象,如果直接使用返回一个引用,啥事都没有了,所以返回一个引用效率更高,如下示例:
coordinate& coordinate::operator=(const coordinate &aa)
{
if(this != &aa)
{
x = aa.x;
y = aa.y;
}
return *this;
}
引用换指针的问题
如下示例,通过测试实际上将引用换成指针,执行没有任何问题,也就是说编译器允许我们这么干,由于函数的形参是一个指针,那么我们要将对象传给指针,应当使用&取地址,这样才符合c的语法风格,一旦加上了&,那么我们一直致力于想实现的a=b直接赋值的效果就达不到了(实测不写&,也一样),综合来看还是写引用才是最优选方案,传说引用就是在这里被发明出来的。
coordinate* coordinate::operator=(const coordinate *aa)
a=&b;
sting赋值运算符重载
sting运算符重载与普通的类重载基本相同,只是sting赋值时要频繁使用到内存分配和释放,一旦涉及到内存就要特别小心,如下示例:
mysting :: mysting(const char *p) //mystring构造函数
{
str_leng=0; //类中的一个用来记录字符串长度的变量
while(*(p+str_leng) != '\0') str_leng++; //计算输入字符大小
str_leng++; //加1
pc = new char[str_leng]; //申请内存
cnt=0;
while(*p != '\0') //将输入的字符串拷贝到申请的内存中
{
*(pc+cnt) = *p++;
cnt++;
}
*(pc+cnt) = '\0'; //补写结束标记
}
该构造函数每次都会根据初始化时传入的字符串数量大小来申请内存,并将值拷贝到内存中,当我们定义了2个大小不同的内存,在进程运算符重载时就会导致内存越界,如下示例:
mysting a("hudaizhou"); //定义一个aa对象并赋初值(构造函数申请9个字节)
mysting b("hu"); //定义一个aa对象并赋初值(构造函数申请3个字节)
b=a; // 9个长度赋值到3个,内存越界。
为了防止拷贝造成内存越界,所以通常会将原来在构造函数中申请的内存释放,再根据要拷贝的字符串大小重新申请内存,如下示例:
mysting& mysting::operator=(const mysting& aa)//赋值运算符重载
{
delete[ ] pc; //释放构造函数申请的内存
pc = new char[aa.str_leng]; //根据aa中的字符大小申请内存
cnt=0;
while(*(aa.pc + cnt) != '\0')//拷贝到申请的内存中
{
*(pc+cnt) = *(aa.pc+cnt);
cnt++;
}
*(pc+cnt) = '\0';
return *this;
}
++与--前置与后置
我们在c中学习的a++与++a是两种快捷的运算,两者的结果不一样,在c++中这两个的运算符重载函数是一特例。
a++
a++对应type operator++(int x);// this是a,int x是编译器强制格式,如下示例:
number number::operator++(int a)//a++
{
number temp;
temp.x=x;
temp.y=y;
x=x+1;
y=y+1;
cout<<"b=a++"<<endl;
return temp;
}
++a
++a 对应type operator++(void);//this是a,传参是空,示例如下:
number number::operator++(void)//++a
{
x=x+1;
y=y+1;
cout<<"b=++a"<<endl;
return *this;
}
独立运算符重载函数
前面我们研究的运算符重载函数都是在类中,实际上在c++中还支持在类外的运算符重载函数,只是运算符重载必须在类中声明为友元(friend)才能访问类中的私有成员,如下:
friend number operator+(number& a,number& b);//在类中声明友元函数
number operator+(number& a,number& b)//独立的+运算符重载
{
number temp;
temp.x=a.x+b.x;
temp.y=a.y+b.y;
return temp;
}
在类中的运算符重载函数和在类外的运算符重载函数,功能上相同,效率也相同,两个都可以,两个都有人使用,但是推荐使用在类外的方法,因为这种方法没有this,直接将要运算的两个对象传参,例如:c=a+b;就可以看成c=operator+(a,b);这种方式更符合c的编程习惯,而且a和b可以任意交换位置。而在类中的交换后就有问题,如下:
c=a+2;//c=a.operator+(2)
c=2+a; //c=2.operator+(a)
不支持重载的运算符
(因为没必要,所以不支持):
. | 成员访问运算符 |
.* ->* | 成员指针访问运算符 |
:: | 域作用符 |
sizeof | 长度运算符 |
? : | 条件运算符 |
# | 预处理符号 |