PKU C++程序设计实习 学习笔记4 运算符重载
2015-05-26 20:25
218 查看
第四章 运算符重载
• 调用类的成员函数—>操作它的对象
类的成员函数—>操作对象时,很不方便
• 在数学上,两个复数可以直接进行+/-等运算 Vs. 在C++中,直接将+或-用于复数是不允许的
• 程序更简洁
• 代码更容易理解
运算符重载
• 对已有的运算符赋予多重的含义
• 使同一运算符作用于不同类型的数据时—>不同类型的行为
目的
• 扩展C++中提供的运算符的适用范围,以用于类所表示的抽象数据类型
同一个运算符,对不同类型的操作数,所发生的行为不同
• (5,10i) + (4,8i) = (9,18i)
• 5 + 4 = 9
运算符重载的实质是函数重载
返回值类型 operator 运算符(形参表)
{
……
}
在程序编译时:
• 把含 运算符的表达式 —> 对 运算符函数 的调用
• 把 运算符的操作数 —> 运算符函数的 参数
• 运算符被多次重载时, 根据 实参的类型 决定调用哪个运算符函数
• 运算符可以被重载成普通函数
• 也可以被重载成类的成员函数
• 把一个 int类型变量 赋值给一个 Complex对象
• 把一个 char * 类型的字符串 赋值给一个 字符串对象
需要 重载赋值运算符 ‘=’
赋值运算符 “=” 只能重载为 成员函数
编写一个长度可变的字符串类String
• 包含一个char * 类型的成员变量
—> 指向动态分配的存储空间
• 该存储空间用于存放 ‘\0’ 结尾的字符串
浅复制/浅拷贝
执行逐个字节的复制工作
S1和S2指向了同一块动态分配内存区域,那么当S1和S2同时消亡的时候,这一块内存空间就会被先后释放两次。这样就会导致严重的内存错误,甚至可能引发程序意外的中止。
所以我们看到,这样的一个浅拷贝,或者浅复制的工作本身并不能实现我们所希望实现的像S2中间的这个str字串复制给S1本身指向的那一块空间。
深复制/深拷贝
将一个对象中指针变量指向的内容 —> 复制到另一个对象中指针成员对象指向的地方
在 class MyString 里添加成员函数:
MyString s; s = “Hello”; s = s;
正确写法:
考虑: a = b = c;
//等价于a.operator=(b.operator=(c));
String 好不好?为什么是 String &
运算符重载时, 好的风格 -- 尽量保留运算符原本的特性
考虑: (a=b)=c; //会修改a的值
分别等价于:(a.operator=(b)).operator=(c);
重载为友元函数的情况:
• 成员函数不能满足使用要求
• 普通函数,又不能访问类的私有成员
Complex c ;
c = c + 5; //有定义,相当于 c = c.operator +(5);
但是:
c = 5 + c; //编译出错
为了使得上述表达式能成立, 需要将+重载为普通函数
因为我们不写赋值构造函数的话,编译器自动生成的那个赋值构造函数会执行赋值的功能。这里所说的赋值的工作,只是把a1的成员变量赋值到a2里面去。那a1的成员变量ptr
被赋值到a2里面去,那自然a2.ptr就等于a1.ptr。那也就是说,a2的ptr和a1的ptr都指向了同一片存储空间。
cout是什么?“<<” 为什么能用在 cout上?
“<<” 能用在cout 上是因为,在iostream里对 “<<” 进行了重载。
考虑,怎么重载才能使得cout << 5; 和 cout << “this”都能成立?
有可能按以下方式重载成 ostream类的成员函数:
因为ostream已经封装好了,不可能把这个重载写成成员函数,所以写成全局函数。
cout << 5 ; 即 cout.operator<<(5);
cout << “this”; 即 cout.operator<<( “this” );
怎么重载才能使得cout << 5 << “this” ;成立?
cout.operator<<(5).operator<<(“this”);
假定下面程序输出为 5hello, 该补写些什么
前置运算符作为一元运算符重载
• 重载为成员函数:
T & operator++();
T & operator--();
• 重载为全局函数:
T & operator++(T &);
T & operator—(T &);
++obj, obj.operator++(), operator++(obj) 都调用上述函数
后置运算符作为二元运算符重载
• 多写一个参数, 具体无意义
• 重载为成员函数:
T operator++(int);
T operator--(int);
• 重载为全局函数:
T operator++(T &, int);
T operator--(T &, int);
obj++, obj.operator++(0), operator++(obj,0) 都调用上述函数
int 作为一个类型强制转换运算符被重载,
Demo s;
(int) s ; //等效于 s.int();
类型强制转换运算符重载时,
• 不能写返回值类型
• 实际上其返回值类型 -- 类型强制转换运算符代表的类型
重载后运算符的含义应该符合日常习惯
• complex_a + complex_b
• word_a > word_b
• date_b = date_a + n
运算符重载不改变运算符的优先级
以下运算符不能被重载: “.”, “.*”, “::”, “?:”, sizeof
重载运算符(), [ ], ->或者赋值运算符=时, 重载函数必须声明为类的成员函数
4.1 运算符重载的基本概念
1. 运算符
2. 自定义数据类型与运算符重载
C++提供了数据抽象的手段:用户自己定义数据类型 -- 类• 调用类的成员函数—>操作它的对象
类的成员函数—>操作对象时,很不方便
• 在数学上,两个复数可以直接进行+/-等运算 Vs. 在C++中,直接将+或-用于复数是不允许的
3. 运算符重载
对抽象数据类型也能够直接使用C++提供的运算符• 程序更简洁
• 代码更容易理解
运算符重载
• 对已有的运算符赋予多重的含义
• 使同一运算符作用于不同类型的数据时—>不同类型的行为
目的
• 扩展C++中提供的运算符的适用范围,以用于类所表示的抽象数据类型
同一个运算符,对不同类型的操作数,所发生的行为不同
• (5,10i) + (4,8i) = (9,18i)
• 5 + 4 = 9
运算符重载的实质是函数重载
返回值类型 operator 运算符(形参表)
{
……
}
在程序编译时:
• 把含 运算符的表达式 —> 对 运算符函数 的调用
• 把 运算符的操作数 —> 运算符函数的 参数
• 运算符被多次重载时, 根据 实参的类型 决定调用哪个运算符函数
• 运算符可以被重载成普通函数
• 也可以被重载成类的成员函数
4. 运算符重载为普通函数
class Complex { public: Complex( double r = 0.0, double i= 0.0 ){ real = r; imaginary = i; } double real; // real part double imaginary; // imaginary part }; Complex operator+ (const Complex & a, const Complex & b) { return Complex( a.real+b.real, a.imaginary+b.imaginary); } // “类名(参数表)” 就代表一个对象 Complex a(1,2), b(2,3), c; c = a + b;// 相当于什么? operator+(a,b)重载为普通函数时,参数个数为运算符目数
5. 运算符重载为成员函数
class Complex { public: Complex( double r= 0.0, double m = 0.0 ):real(r), imaginary(m) { } // constructor Complex operator+ ( const Complex & ); // addition Complex operator- ( const Complex & ); // subtraction private: double real; // real part double imaginary; // imaginary part }; // Overloaded addition operator Complex Complex::operator+(const Complex & operand2) { return Complex( real + operand2.real,imaginary + operand2.imaginary ); } // Overloaded subtraction operator Complex Complex::operator- (const Complex & operand2){ return Complex( real - operand2.real,imaginary - operand2.imaginary ); } int main(){ Complex x, y(4.3, 8.2), z(3.3, 1.1); x = y + z;// 相当于什么? y.operator+(z) x = y - z;// 相当于什么? y.operator-(z) return 0; }重载为成员函数时,参数个数为运算符目数减一
4.2 赋值运算符的重载
1. 赋值运算符 ‘=’ 重载
赋值运算符 两边的类型 可以 不匹配• 把一个 int类型变量 赋值给一个 Complex对象
• 把一个 char * 类型的字符串 赋值给一个 字符串对象
需要 重载赋值运算符 ‘=’
赋值运算符 “=” 只能重载为 成员函数
编写一个长度可变的字符串类String
• 包含一个char * 类型的成员变量
—> 指向动态分配的存储空间
• 该存储空间用于存放 ‘\0’ 结尾的字符串
class String { private: char * str; public: String () : str(NULL) { } //构造函数, 初始化str为NULL const char * c_str() { return str; } //返回值为const类型,保证str不会被修改。比如char* p=str.c_str();则编译器会报错,类型不匹配。 char * operator = (const char * s); ~String( );//需要考虑String对象是否指向了动态分配的存储空间 }; //重载‘=’使得obj = “hello”能够成立 char * String::operator = (const char * s){ if(str) delete [] str; if(s) { //s不为NULL才会执行拷贝 str = new char[strlen(s)+1]; strcpy(str, s); } else str = NULL; return str; } String::~String( ) { if(str) delete [] str; }; int main(){ String s; s = “Good Luck,” ; cout << s.c_str() << endl; // String s2 = “hello!”; //这条语句要是不注释掉就会出错 s = "Shenzhou 8!"; cout << s.c_str() << endl; return 0; }
2. 重载赋值运算符的意义 – 浅复制和深复制
S1 = S2;浅复制/浅拷贝
执行逐个字节的复制工作
S1和S2指向了同一块动态分配内存区域,那么当S1和S2同时消亡的时候,这一块内存空间就会被先后释放两次。这样就会导致严重的内存错误,甚至可能引发程序意外的中止。
所以我们看到,这样的一个浅拷贝,或者浅复制的工作本身并不能实现我们所希望实现的像S2中间的这个str字串复制给S1本身指向的那一块空间。
深复制/深拷贝
将一个对象中指针变量指向的内容 —> 复制到另一个对象中指针成员对象指向的地方
在 class MyString 里添加成员函数:
String & operator = (const String & s) { if(str) delete [] str; str = new char[strlen(s.str)+1]; strcpy(str, s.str); return * this; }
3. 思考
考虑下面语句,是否会有问题?MyString s; s = “Hello”; s = s;
正确写法:
String & String::operator = (const String & s){ if(str == s.str) return * this;//增加此行 if(str) delete [] str; if(s.str) { //s.str不为NULL才会执行拷贝 str = new char[strlen(s.str)+1]; strcpy( str,s.str); } else str = NULL; return * this; }
4. 对 operator = 返回值类型的讨论
void 好不好?考虑: a = b = c;
//等价于a.operator=(b.operator=(c));
String 好不好?为什么是 String &
运算符重载时, 好的风格 -- 尽量保留运算符原本的特性
考虑: (a=b)=c; //会修改a的值
分别等价于:(a.operator=(b)).operator=(c);
5. 上面的String类是否就没有问题了?
为 String类编写 复制构造函数 时,会面临和 ‘=’ 同样的问题,用同样的方法处理String::String(String & s) { if(s.str) { str = new char[strlen(s.str)+1]; strcpy(str, s.str); } else str = NULL; }
4.3 运算符重载为友元函数
1. 运算符重载为友元
通常,将运算符重载为类的成员函数重载为友元函数的情况:
• 成员函数不能满足使用要求
• 普通函数,又不能访问类的私有成员
class Complex{ double real, imag; public: Complex(double r, double i):real(r), imag(i){ }; Complex operator+(double r); }; Complex Complex::operator+(double r){ //能解释 c+5 return Complex(real + r, imag); }经过上述重载后:
Complex c ;
c = c + 5; //有定义,相当于 c = c.operator +(5);
但是:
c = 5 + c; //编译出错
为了使得上述表达式能成立, 需要将+重载为普通函数
Complex operator+ (double r, const Complex & c) { //能解释 5+c return Complex( c.real + r, c.imag); }普通函数不能访问私有成员 —> 将运算符+重载为友元函数
class Complex { double real, imag; public: Complex( double r, double i):real(r),imag(i){ }; Complex operator+( double r ); friend Complex operator + (double r, const Complex & c); };
4.4 实例 – 长度可变的整型数组类(可变长整型数组)
class CArray { int size; //数组元素的个数 int *ptr; //指向动态分配的数组 public: CArray(int s = 0); //s代表数组元素的个数 CArray(CArray & a); ~CArray(); void push_back(int v); //用于在数组尾部添加一个元素v CArray & operator=( const CArray & a); //用于数组对象间的赋值 int length() { return size; } //返回数组元素个数 int & CArray::operator[](inti) //返回值为 int 不行!不支持 a[i] = 4 {//用以支持根据下标访问数组元素, //如n = a[i]和a[i] = 4; 这样的语句 return ptr[i]; } }; CArray::CArray(int s):size(s) { if( s == 0) ptr = NULL; else ptr = new int[s]; } CArray::CArray(CArray & a) { if( !a.ptr) { ptr = NULL; size = 0; return; } ptr = new int[a.size]; memcpy( ptr, a.ptr, sizeof(int ) * a.size); size = a.size; }
因为我们不写赋值构造函数的话,编译器自动生成的那个赋值构造函数会执行赋值的功能。这里所说的赋值的工作,只是把a1的成员变量赋值到a2里面去。那a1的成员变量ptr
被赋值到a2里面去,那自然a2.ptr就等于a1.ptr。那也就是说,a2的ptr和a1的ptr都指向了同一片存储空间。
CArray::~CArray() { if( ptr) delete [] ptr; } CArray & CArray::operator=( const CArray & a) { //赋值号的作用是使“=”左边对象里存放的数组,大小和内容都和右边的对象一样 if( ptr == a.ptr) //防止a=a这样的赋值导致出错 return * this; if( a.ptr == NULL) { //如果a里面的数组是空的 if( ptr ) delete [] ptr; ptr = NULL; size = 0; return * this; } if( size < a.size) { //如果原有空间够大,就不用分配新的空间 if(ptr) delete [] ptr; ptr = new int[a.size]; } memcpy( ptr,a.ptr,sizeof(int)*a.size); size = a.size; return * this; } // CArray & CArray::operator=( const CArray & a) void CArray::push_back(int v) { //在数组尾部添加一个元素 if( ptr) { int * tmpPtr = new int[size+1]; //重新分配空间 memcpy(tmpPtr,ptr,sizeof(int)*size); //拷贝原数组内容 delete [] ptr; ptr = tmpPtr; } else //数组本来是空的 ptr = new int[1]; ptr[size++] = v; //加入新的数组元素 }
4.5 流插入运算符和流提取运算符的重载
1. 问题
cout << 5 << “this”;为什么能够成立?cout是什么?“<<” 为什么能用在 cout上?
2. 流插入运算符的重载
cout 是在 iostream 中定义的,ostream 类的对象。“<<” 能用在cout 上是因为,在iostream里对 “<<” 进行了重载。
考虑,怎么重载才能使得cout << 5; 和 cout << “this”都能成立?
有可能按以下方式重载成 ostream类的成员函数:
void ostream::operator<<(int n) { …… //输出n的代码 return; }
因为ostream已经封装好了,不可能把这个重载写成成员函数,所以写成全局函数。
cout << 5 ; 即 cout.operator<<(5);
cout << “this”; 即 cout.operator<<( “this” );
怎么重载才能使得cout << 5 << “this” ;成立?
ostream & ostream::operator<<(int n) { …… //输出n的代码 return * this; } ostream & ostream::operator<<( const char * s ) { …… //输出s的代码 return * this; }cout << 5 << “this”;本质上的函数调用的形式是什么?
cout.operator<<(5).operator<<(“this”);
假定下面程序输出为 5hello, 该补写些什么
class CStudent{ public: int nAge; }; int main(){ CStudent s ; s.nAge = 5; cout << s <<"hello"; return 0; }
ostream & operator<<( ostream & o,const CStudent & s){ o << s.nAge ; return o; }
3. 例题
(教材P218)例子。可略了。4.6 自加/自减运算符的重载
1. 自加/自减运算符的重载
自加 ++/自减 -- 运算符有 前置/后置 之分前置运算符作为一元运算符重载
• 重载为成员函数:
T & operator++();
T & operator--();
• 重载为全局函数:
T & operator++(T &);
T & operator—(T &);
++obj, obj.operator++(), operator++(obj) 都调用上述函数
后置运算符作为二元运算符重载
• 多写一个参数, 具体无意义
• 重载为成员函数:
T operator++(int);
T operator--(int);
• 重载为全局函数:
T operator++(T &, int);
T operator--(T &, int);
obj++, obj.operator++(0), operator++(obj,0) 都调用上述函数
int main(){ CDemo d(5); cout << (d++) << ","; //等价于 d.operator++(0); cout << d << ","; cout << (++d) << ","; //等价于 d.operator++(); cout << d << endl; cout << (d--) << ","; //等价于 operator--(d,0); cout << d << ","; cout << (--d) << ","; //等价于 operator--(d); cout << d << endl; return 0; } 程序输出结果: 5,6,7,7 7,6,5,5 如何编写 CDemo?
class CDemo { private : int n; public: CDemo(int i=0):n(i) { } CDemo & operator++(); //用于前置++形式 CDemo operator++(int); //用于后置++形式 operator int ( ) { return n; } friend CDemo & operator--(CDemo &); //用于前置--形式 friend CDemo operator--(CDemo &, int); //用于后置--形式 }; CDemo & CDemo::operator++() { //前置 ++ n++; return * this; } CDemo CDemo::operator++(int k) { //后置 ++ CDemo tmp(*this); //记录修改前的对象 n++; return tmp; //返回修改前的对象 } CDemo& operator--(CDemo & d) { //前置-- d.n--; return d; } CDemo operator--(CDemo & d, int) { //后置-- CDemo tmp(d); d.n --; return tmp; }operator int ( ) { return n; }
int 作为一个类型强制转换运算符被重载,
Demo s;
(int) s ; //等效于 s.int();
类型强制转换运算符重载时,
• 不能写返回值类型
• 实际上其返回值类型 -- 类型强制转换运算符代表的类型
2. 运算符重载的注意事项
C++不允许定义新的运算符重载后运算符的含义应该符合日常习惯
• complex_a + complex_b
• word_a > word_b
• date_b = date_a + n
运算符重载不改变运算符的优先级
以下运算符不能被重载: “.”, “.*”, “::”, “?:”, sizeof
重载运算符(), [ ], ->或者赋值运算符=时, 重载函数必须声明为类的成员函数
相关文章推荐
- PKU C++程序设计实习 学习笔记2 继承与派生
- PKU C++程序设计实习 学习笔记6 标准模板库STL
- PKU C++程序设计实习 学习笔记1
- PKU C++程序设计实习 学习笔记3 多态与虚函数
- PKU C++程序设计实习 学习笔记5 文件操作和模板
- 【day 17】python编程:从入门到实践学习笔记-基于Django框架的Web开发-设计样式和部署(一)
- 步步为营 .NET 设计模式学习笔记 十八、Template(模板模式)
- 模拟CMOS集成电路设计 学习笔记(一)
- [知了堂学习笔记]设计模式之装饰者模式
- 设计模式学习笔记——组合模式
- HeadFirst 设计模式学习笔记8--模板方法模式
- 一些面向对象的设计法则 学习笔记
- 设计模式学习笔记--简单工厂模式(Simple Factory Pattern)【创建型模式】
- 设计模式学习笔记--组合模式
- 四、机器学习系统设计笔记之主体模型
- Android(OPhone) 学习笔记 - 界面设计工具
- [机器学习笔记]Note9--机器学习系统设计
- spring ioc容器的学习笔记2---ioc容器系列的设计与实现
- 《第一行代码Java》DAO设计模式部分学习笔记与代码
- 学习Android中设计模式的笔记和总结(二)