一.函数重载
- 函数重载的本质为相互独立的不同函数
- C++中通过函数名和函数参数确定函数调用
- 无法直接通过函数名得到重载函数的入口地址
- 函数重载必然发生在同一个作用域中
二.类中的重载
类中的成员函数可以进行重载
- 构造函数的重载
- 普通成员函数的重载
- 静态成员函数的重载
问题:
全局函数,普通成员函数以及静态成员函数之间是否可以构成重载? 可以
小结:
类的成员函数之间可以进行重载;
重载必须发生在同一个作用域中;
全局函数和成员函数不能构成重载关系;//不在一个作用域
重载的意义在于扩展已经存在的功能;
重载的特点:
- 重载函数的本质为多个不同的函数
- 函数名和参数列表是唯一的标识
- 函数重载必须发生在同一个作用域中
类的重载示例:
#include <stdio.h>
class Test
{
int i;
public:
Test()
{
printf("Test::Test()\n");
this->i = 0;
}
Test(int i)
{
printf("Test::Test(int i)\n");
this->i = i;
}
Test(const Test& obj)
{
printf("Test(const Test& obj)\n");
this->i = obj.i;
}
static void func()
{
printf("void Test::func()\n");
}
void func(int i)
{
printf("void Test::func(int i), i = %d\n", i);
}
int getI()
{
return i;
}
};
void func()
{
printf("void func()\n");
}
void func(int i)
{
printf("void func(int i), i = %d\n", i);
}
int main()
{
func();
func(1);
Test t; // Test::Test()
Test t1(1); // Test::Test(int i)
Test t2(t1); // Test(const Test& obj)
func(); // void func()
Test::func(); // void Test::func()
func(2); // void func(int i), i = 2;
t1.func(2); // void Test::func(int i), i = 2
t1.func(); // void Test::func()
return 0;
}
重载深度的意义:
- 通过函数名对函数功能进行提示
- 通过参数列表对函数用法进行提示
- 扩展系统中已经存在的函数功能
示例:
#include <stdio.h>
#include <string.h>
char* strcpy(char* buf, const char* str, unsigned int n)
{
return strncpy(buf, str, n);
}
int main()
{
const char* s = "D.T.Software";
char buf[8] = {0}; //buf大小不够的时候会越界
//strcpy(buf, s);
strcpy(buf, s, sizeof(buf)-1);
printf("%s\n", buf);
return 0;
}
问题思考:
重载能够扩展系统中已经存在的函数功能,
那么重载是否也能够扩展其它更多的功能?
下面的复数解决方案是否可行?
class Comples
{
public:
int a;
int b;
};
int main()
{
Complex c1 = {1, 2};
Complex c2 = {3, 4};
Complex c3 = c1 + c2;
return 0;
}
直接操作两个对象相加会报错,按照以前的方法,重新定义一个函数来操作
#include <stdio.h>
class Complex
{
int a;//hide variable
int b;
public:
Complex(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
}
int getA()
{
return a;
}
int getB()
{
return b;
}
friend Complex Add(const Complex& p1, const Complex& p2); //友元
};
Complex Add(const Complex& p1, const Complex& p2)
{
Complex ret;
ret.a = p1.a + p2.a;
ret.b = p1.b + p2.b;
return ret;
}
int main()
{
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = Add(c1, c2); // c1 + c2 完美的方案
printf("c3.a = %d, c3.b = %d\n", c3.getA(), c3.getB());
return 0;
}
Add函数可以解决Complex对象相加的问题,但是Complex是现实世界中确实存在的复数,并且复数在数学中的地位和普通的实数相同。
三. 操作符重载
C++中的重载能够扩展操作符的功能
操作符的重载以函数的方式进行
本质:
用特殊形式的函数扩展操作符的功能
通过operator关键字可以定义特殊的函数
operator的本质是通过函数重载操作符
语法:
Type operator Sign(const Type p1, const Type p2)
{
Type ret;
return ret;
}
Sign为系统中预定义的操作符,如:+ ,- ,* , / 等等
改写之前的ADD函数,全局函数+友元的示例版本:
#include <stdio.h>
class Complex
{
int a;
int b;
public:
Complex(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
}
int getA()
{
return a;
}
int getB()
{
return b;
}
friend Complex operator + (const Complex& p1, const Complex& p2);//友元函数
};
Complex operator + (const Complex& p1, const Complex& p2)
{
Complex ret;
ret.a = p1.a + p2.a;
ret.b = p1.b + p2.b;
return ret;
}
int main()
{
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1 + c2; // operator + (c1, c2)
printf("c3.a = %d, c3.b = %d\n", c3.getA(), c3.getB());
return 0;
}
以上使用了全局友元函数,可以将操作符重载函数定义为类的成员函数:
- 比全局操作符重载函数少一个参数(左操作数)this充当左操作数
- 不需要依赖友元就可以完成操作符重载
- 编译器优先在成员函数中寻找操作符重载函数
语法:
class Type
{
public:
Type operator Sign(const Type& p)
{
Type ret;
return ret;
}
};
修改后的示例, 成员函数版本的操作符重载:
#include <stdio.h>
class Complex
{
int a;
int b;
public:
Complex(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
}
int getA()
{
return a;
}
int getB()
{
return b;
}
Complex operator + (const Complex& p)//成员函数版本---优先调用
{
Complex ret;
printf("Complex operator + (const Complex& p)\n");
ret.a = this->a + p.a;
ret.b = this->b + p.b;
return ret;
}
//两种版本都实现,优先调用成员函数版本
friend Complex operator + (const Complex& p1, const Complex& p2);
};
//两种版本都实现,优先调用成员函数版本
Complex operator + (const Complex& p1, const Complex& p2)//全局函数版本
{
Complex ret;
printf("Complex operator + (const Complex& p1, const Complex& p2)\n");
ret.a = p1.a + p2.a;
ret.b = p1.b + p2.b;
return ret;
}
int main()
{
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1 + c2; // c1.operator + (c2)
printf("c3.a = %d, c3.b = %d\n", c3.getA(), c3.getB());
return 0;
}
编译结果:
Complex operator + (const Complex& p)
c3.a = 4, c3.b = 6
小结:
操作符重载是C++的强大特性之一
操作符重载的本质是通过函数扩展操作符的功能
operator关键字是实现操作符重载的关键
操作符重载遵循相同的函数重载规则
全局函数和成员函数都可以实现对操作符的重载
四.完善的复数类:
复数类应该具有的操作:
- 运算: + , - ,* , /
- 比较: == , !=
- 赋值: =
- 求模: modules
利用操作符重载:
统一复数与实数的运算方式
统一复数与实数的比较方式
Complex operator + (const Complex& c);
Complex operator - (const Complex& c);
Complex operator * (const Complex& c);
Complex operator / (const Complex& c);
bool operator == (const Complex& c);
bool operator != (const Complex& c);
Complex& operator = (const Complex& c);
Complex.h
#ifndef _COMPLEX_H_ //定义预处理宏
#define _COMPLEX_H_
class Complex
{
double a; // 实部
double b; // 虚部
public:
//定义功能函数
Complex(double a = 0, double b = 0); // 函数申明的时候指定默认值
double getA(); //获取实部
double getB(); //获取虚部
double getModulus();
//定义操作符重载函数
Complex operator + (const Complex& c);
Complex operator - (const Complex& c);
Complex operator * (const Complex& c);
Complex operator / (const Complex& c);
//比较运算
bool operator == (const Complex& c);
bool operator != (const Complex& c);
//赋值操作符的重载,特殊之处在于只能当做成员函数实现
Complex& operator = (const Complex& c);
};
#endif
Complex.cpp
#include "Complex.h"
#include "math.h"
Complex::Complex(double a, double b)
{
this->a = a;
this->b = b;
}
double Complex::getA()
{
return a;
}
double Complex::getB()
{
return b;
}
double Complex::getModulus()
{
return sqrt(a * a + b * b);
}
Complex Complex::operator + (const Complex& c)
{
double na = a + c.a;
double nb = b + c.b;
Complex ret(na, nb);
return ret;
}
Complex Complex::operator - (const Complex& c)
{
double na = a - c.a;
double nb = b - c.b;
Complex ret(na, nb);
return ret;
}
Complex Complex::operator * (const Complex& c)
{
double na = a * c.a - b * c.b;
double nb = a * c.b + b * c.a;
Complex ret(na, nb);
return ret;
}
Complex Complex::operator / (const Complex& c)
{
double cm = c.a * c.a + c.b * c.b;
double na = (a * c.a + b * c.b) / cm;
double nb = (b * c.a - a * c.b) / cm;
Complex ret(na, nb);
return ret;
}
bool Complex::operator == (const Complex& c)
{
return (a == c.a) && (b == c.b);
}
bool Complex::operator != (const Complex& c)
{
return !(*this == c);//特殊的计算
}
Complex& Complex::operator = (const Complex& c)
{
if( this != &c )//查看是否为同一个复数
{
a = c.a;
b = c.b;
}
return *this;
}
main.c
#include <stdio.h>
#include "Complex.h"
int main()
{
Complex c1(1, 2);
Complex c2(3, 6);
Complex c3 = c2 - c1;
Complex c4 = c1 * c3;
Complex c5 = c2 / c1;
printf("c3.a = %f, c3.b = %f\n", c3.getA(), c3.getB());
printf("c4.a = %f, c4.b = %f\n", c4.getA(), c4.getB());
printf("c5.a = %f, c5.b = %f\n", c5.getA(), c5.getB());
Complex c6(2, 4);
printf("c3 == c6 : %d\n", c3 == c6);
printf("c3 != c4 : %d\n", c3 != c4);
(c3 = c2) = c1;
printf("c1.a = %f, c1.b = %f\n", c1.getA(), c1.getB());
printf("c2.a = %f, c2.b = %f\n", c2.getA(), c2.getB());
printf("c3.a = %f, c3.b = %f\n", c3.getA(), c3.getB());
return 0;
}
运行结果:
c3.a = 2.000000, c3.b = 4.000000
c4.a = -6.000000, c4.b = 8.000000
c5.a = 3.000000, c5.b = 0.000000
c3 == c6 : 1
c3 != c4 : 1
c1.a = 1.000000, c1.b = 2.000000
c2.a = 3.000000, c2.b = 6.000000
c3.a = 1.000000, c3.b = 2.000000
注意事项:
C++规定赋值操作符(=)只能重载为成员函数
操作符重载不能改变原操作符的优先级
操作符重载不能改变操作数的个数
操作符重载不能改变操作符的原有语义
小结:
复数的概念可以通过自定义类实现
复数中的运算操作可以通过操作符重载实现
赋值操作符只能通过成员函数实现
操作符重载的本质为函数定义—扩展原有的功能
五.数组操作符的重载
String类最大限度的考虑了C字符串的兼容性,可以按照使用C字符串的方式使用string对象,也就是可以通过数组下标访问String对象的单个字符。
示例:
用C语言的方式使用string类
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
string s = "a1b2c3d4e5f6g7";
int n = 0;
for(int i=0;i<s.length();i++)
{
if(isalpha(s[i]))
{
n++;
}
}
cout<<n<<endl;
}
编译结果:
7
类的对象怎么支持数组的下标访问?
函数重载
数组访问符是C/C++中的内置操作符
数组访问符的原生意义是数组访问和指针运算
a[n] — *(a + n) — *(n + a) — n[a]
以上是数组访问操作符的原生意义。
数组访问操作符( [] ) 的重载:
- 只能通过类的成员函数重载
- 重载函数能且仅能使用一个参数
- 可以定义不同参数的多个重载函数
示例:重载数组访问操作符
#include <iostream>
#include <string>
using namespace std;
class Test
{
int a[5];
public:
int& operator [] (int i)
{
return a[i];
}
int& operator [] (const string& s)
{ //通过字符串访问数组
if( s == "1st" )
{
return a[0];
}
else if( s == "2nd" )
{
return a[1];
}
else if( s == "3rd" )
{
return a[2];
}
else if( s == "4th" )
{
return a[3];
}
else if( s == "5th" )
{
return a[4];
}
return a[0];
}
int length()
{
return 5;
}
};
int main()
{
Test t;
for(int i=0; i<t.length(); i++)
{
t[i] = i; //函数调用的形式 t.operator[](i) = i
//想将i作为函数调用的返回值返回给a[i], 使用引用,可以出现在赋值符号的左边
}
for(int i=0; i<t.length(); i++)
{
cout << t[i] << endl;
}
cout << t["5th"] << endl;
cout << t["4th"] << endl;
cout << t["3rd"] << endl;
cout << t["2nd"] << endl;
cout << t["1st"] << endl;
return 0;
}
编译结果:
0
1
2
3
4
4
3
2
1
0
六.赋值操作符的重载
编译器为每个类默认重载了赋值操作符
默认的赋值操作符仅完成浅拷贝
当需要进行深拷贝时必须重载赋值操作符
赋值操作符与拷贝构造函数有相同的存在意义
示例:
#include <iostream>
#include <string>
using namespace std;
class Test
{
int* m_pointer;
public:
Test()
{
m_pointer = NULL;
}
Test(int i)
{
m_pointer = new int(i); //指向堆空间中的数据
}
Test(const Test& obj)//自定义拷贝构造函数,实现深拷贝
{
/*
堆空间中申请,代表int空间的值
将参数对象的point指针所指向的堆空间的整型值,
将整型值取出并且复制到新申请的一片内存空间中去
*/
m_pointer = new int(*obj.m_pointer);
}
//没有这个函数的话t2=t1,触发析构函数,将释放同一片堆空间,被释放两次将报错
Test& operator = (const Test& obj)
{ //返回值一定是引用,便于连续赋值,参数为const的引用类型
if( this != &obj ) //当前this指针的地址和参数的地址不同
{
delete m_pointer;
m_pointer = new int(*obj.m_pointer);
}
return *this;
}
void print()
{
cout << "m_pointer = " << hex << m_pointer << endl;
}
~Test()
{
delete m_pointer;
}
};
int main()
{
Test t1 = 1; //m_pointer = 0x707070 指向堆空间中的内存,并设置为1
Test t2; //m_pointer = 0 内部的空间为空
t2=t1;
t1.print(); //m_pointer = 0x7e7777
t2.print(); //m_pointer = 0x7e7777
//调用结束,t1和t2指向的是同一处堆空间,释放的时候释放了两次
//重载了赋值操作符,有深拷贝,地址不同,不会崩溃
return 0;
}
一般性原则:
重载赋值操作符,必然需要实现深拷贝。
七. 逻辑操作符重载
逻辑运算符的原生语义:
操作数只有两种值(true和false)
逻辑表达式不用完全计算就能确定最终值—短路规则
最终结果只能是true或者false
逻辑操作符示例:
#include <iostream>
#include <string>
using namespace std;
int func(int i)
{
cout << "int func(int i) : i = " << i << endl;
return i;
}
int main()
{
if( func(0) && func(1) )
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}
cout << endl;
if( func(0) || func(1) )
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}
return 0;
}
重载逻辑操作符:
#include <iostream>
#include <string>
using namespace std;
class Test
{
int mValue;
public:
Test(int v)
{
mValue = v;
}
int value() const
{
return mValue;
}
};
bool operator && (const Test& l, const Test& r)
{
return l.value() && r.value();
}
bool operator || (const Test& l, const Test& r)
{
return l.value() || r.value();
}
Test func(Test i)
{
cout << "Test func(Test i) : i.value() = " << i.value() << endl;
return i;
}
int main()
{
Test t0(0);
Test t1(1);
if( func(t0) && func(t1) )
//if( operator && (func(t0), func(t1)) )
//等价于上面的定义,进入函数体之前,参数需要确定
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}
cout << endl;
if( func(1) || func(0) )
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}
return 0;
}
编译结果:
Test func(Test i) : i.value() = 1
Test func(Test i) : i.value() = 0
Result is false!
Test func(Test i) : i.value() = 0
Test func(Test i) : i.value() = 1
Result is true!
显示并没有完全按照逻辑操作符的原生语义输出,从右到左都调用了fun函数
问题的本质分析:
- C++通过函数调用扩展操作符的功能
- 进入函数体前必须完成所有参数的计算
- 函数参数的计算次序是不定的
- 短路法则完全失效
在语法上对逻辑运算符的重载没有任何问题,但是我们不推荐重载逻辑与和逻辑或,因为我们无法通过函数调用的方式去满足短路规则,也就是我们无法通过操作符重载完全实现逻辑与和逻辑或的原生语义。
工程上的建议:
- 实际工程开发中避免重载逻辑操作符
- 通过重载比较操作符代替逻辑操作符重载
- 直接使用成员函数代替逻辑操作符重载
- 使用全局函数对逻辑操作符进行重载
小结
C++从语法上支持逻辑操作符重载
重载后的逻辑操作符不满足短路原则
工程开发中不要重载逻辑操作符
通过重载比较操作符代替逻辑操作符重载
通过专用成员函数替换逻辑操作符重载
八.逗号操作符重载
逗号操作符( ,)可以构成逗号表达式:
- 逗号表达式用于将多个子表达式连接为一个表达式
- 逗号表达式的值为最后一个子表达式的值
- 逗号表达式中的前N - 1个子表达式可以没有返回值
- 逗号表达式按照从左到右的顺序计算每个子表达式的值
exp1, exp2, exp3, … , expN
逗号操作符示例:
#include<iostream>
#include<string>
using namespace std;
void func(int i)
{
cout<<"func():i="<<i<<endl;
}
int main()
{
int a[3][3]={
(0,1,2),
(3,4,5),
(6,7,8)
};
int i=0;
int j=0;
while(i<5)//while语句没有大括号的话,它的作用域只有紧跟它的一句语句。
func(i), //逗号,连接的为逗号表达式
i++;
/*上面三行等价于:
while(i<5)
{
func(i);
i++;
}
*/
for(i=0;i<3;i++)
{
for(j=0;j<3;j++)
{
cout<<a[i][j]<<endl;
}
//结果输出为2 5 8 0 0 0 0 0 0的原因是二维数组里面用的是括号
}
(i,j)=6;
cout<<"i="<<i<<endl;
cout<<"j="<<j<<endl;
return 0;
}
编译结果:
func():i=0
func():i=1
func():i=2
func():i=3
func():i=4
2
5
8
0
0
0
0
0
0
i=3
j=6
逗号操作符的重载:
- 在C++中重载逗号操作符是合法的
- 使用全局函数对逗号操作符进行重载
- 重载函数的参数必须有一个是类类型
- 重载函数的返回值类型必须是引用
Class& operator ,(const Class& a, const Class& b)
{
retrun const_cast<Class&>(b);
}
重载逗号操作符:
#include <iostream>
#include <string>
using namespace std;
class Test
{
int mValue;
public:
Test(int i)
{
mValue = i;
}
int value()
{
return mValue;
}
};
Test& operator , (const Test& a, const Test& b)
{
return const_cast<Test&>(b);
}
Test func(Test& i)
{
cout << "func() : i = " << i.value() << endl;
return i;
}
int main()
{
Test t0(0);
Test t1(1);
Test tt = (func(t0), func(t1)); // Test tt = func(t1);
cout << tt.value() << endl; // 1
return 0;
}
func() : i = 1
func() : i = 0
1
结果表明:
Test tt = (func(t0), func(t1));
Test tt = func(t1);并没有从左向右逐个调用,输出结果正确但是中间过程出错了。
逗号表达式重载时,中间过程会发生变化,这是不允许的,这违背了逗号表达式的本质。
问题的本质分析:
- C++通过函数调用扩展操作符的功能
- 进入函数体前必须完成所有参数的计算
- 函数参数的计算次序是不定的
- 重载后无法严格从左向右计算表达式
小结:
逗号表达式从左向右顺序计算每个子表达式的值
逗号表达式的值为最后一个子表达式的值
操作符重载无法完全实现逗号操作符的原生意义
工程开发中不要重载逗号操作符。
九. 前置操作符和后置操作符
原生语义:
i++; // i的值作为返回值,i自增1
++i; // i自增1,i的值作为返回值
前置++和后置++真正的区别:
(1)对于基础类型的变量
前置++的效率与后置++的效率基本相同
根据项目组编码规范进行选择
(2)对于类类型的对象
前置++的效率高于后置++
尽量使用前置++操作符提高程序效率
从汇编代码来分析:
现代编译器产品会 对代码进行优化
优化使得最终的二进制程序更加高效
优化后的二进制程序丢失了 C/C++ 的原生语义
不可能从编译后的二进制程序还原C/C++ 程序
++操作符可以被重载:
- 全局函数和成员函数均可进行重载
- 重载前置++操作符不需要额外的参数
- 重载后置++操作符需要一个int类型的占位参数
重载++操作符示例:
#include <string>
#include <iostream>
using namespace std;
class Test
{
int mValue;
public:
Test(int i)
{
mValue = i;
}
int value()
{
return mValue;
}
/*前置++效率更高,没有生成额外的对象,不需要过多的内存*/
Test& operator ++ ()
{
++mValue;
return *this;
}
/*后置++返回值跟前置++不同,不是引用。根据C语言中的定义,后置++先将当前操作数的值
保存下来在一个临时对象中,在实现操作符重载函数的时候,必须要考虑如何将当前对象的值
保存下来,借助局部的对象*/
Test operator ++ (int)
{
Test ret(mValue);
mValue++;
return ret;
}
};
int main()
{
Test t(0);
++t;
cout<<t.value()<<endl;
Test tt = t++;
cout<<tt.value()<<endl;
cout<<t.value()<<endl;
return 0;
}
编译结果:
1
1
2
小结:
编译优化使得最终的可执行程序更加高效
前置 ++ 操作符和后置 ++ 操作符都可以被重载
++ 操作符的重载必须符合其原生语义
对于基础类型,前置 ++ 与后置 ++ 的效率几乎相同
对于类类型, 前置 ++ 的效率高于后置 ++