基本概念:
+,-,*…操作只能用于基本的数据类型。(就如整形,实型,逻辑型等)
允许用户定义自己的数据类型——类。
对类的成员函数进行操作时很不方便。如:复数类,在VS中,直接讲+ - 作用在复数类上是不允许的。
运算符重载能对抽象数据类型也能够直接使用C++提供的运算符。这样能让程序更简洁,代码更容易理解。
运算符重载:
- 对已有的运算符赋予多重含义。
- 使同一运算符作用域不同数据类型时产生不同的行为。
- 扩展了C++中运算符的适用范围,以用于类所表示的抽象数据类型。
运算符重载的实质就是函数的重载。
class Complex{
public:
Complex( double r = 0.0, double i = 0.0){
real = r;
imag = i;
}
double real;
double imag;
};
Complex operator+ (const Complex & a, const Complex & b) //设置为常引用,不改变a和b的值。
{
return Complex(a.real+b.real, a.imag + b.imag);
}
Complex a(1,2),b(2,3),c;
c = a + b;
//疑问:这个算调用函数会触发“复制构造函数”吗?
//尝试后是不会触发的。
上面这个是重载为普通函数的情况,这种情况下参数个数为运算符目数。 (就是运算符几个,函数的参数就几个)
而当重载为类的成员函数时,参数的个数就是运算符数目减一。
class Complex{
public:
Complex( double r = 0.0, double m = 0.0): real(r),imag(m){}
Complex operator+ (const Complex &);
Complex operator- (const Complex &);
private:
double real;
double imag;
};
Complex Complex::operator+ ( const Complex operand2){
return Complex(real+operand2.real, imag + operand2.real);
}
Complex Complex::operator- ( const Complex & operand2){
return Complex( real - operand2.real, imag - operand2.imag);
}
int main(){
Complex x, y(4.3, 5.6), z(3.3 , 1.1);
x = y+z; //这个其实就是 x = y.operator+(z),只传入一个参数。
x = y-z;
return 0;
}
这里的 a+b就是调用 a.operator+,只传递后面那个操作数。
赋值运算符 “=” 的重载
“=” 可以直接使用。
可以直接用赋值运算符,将一个对象复制给另一个对象。如 c1 = c2
赋值运算符两边的类型可以不匹配。
- 把一个 int类型的变量赋值给一个Complex对象。
- 把一个char* 类型的字符串赋值给一个字符串对象。
这时候就需要用到重载赋值运算符 “=”
赋值运算符 “=” 只能重载为成员函数。
编写一个长度可变的字符串类String,要求:
- 包含一个 char* 类型的成员变量,能指向动态分配的存储空间。
- 该存储空间用于存放 ‘\0’结尾的字符串。
class String{
private:
char *str; //指向动态分配的存储空间
public:
String():str(NULL){} //构造函数,初始化str为NULL。
const char* c_str(){return str;} //这里返回的时const char*类型,直接用 char* p 去接收,程序会报错。
char *operator= (const char* s); //
~String();
};
char* String:: operator= (const char* s){
if(str) delete []str; //若原有信息,则需要delete掉,即清空。
if(s){
str = new char[strlen(s) + 1]; //开辟一块新的内存空间,大小为传入的参数s字串的大小加上一个\0的结束符。
strcpy(str,s); //copy工作。
}
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"; 这样初始化会出错,初始化语句不会调用operator的函数,而时调用构造函数。我们这个程序没有构造函数(构造函数是创建一个NULL)。
s = "YOYOYO";
cout << s.c_str()<< endl;
return 0;
}
这种复制时逐个字节的复制工作——浅复制。
//接着上面那个String类
String S1,S2;
S1 = "THIS";
S2 = "THAT";
S1 = S2; //S1.str 和 S2.str 完全相同。
//经过这个操作,S1和S2的指针指向同一个值。
复制的过程是将S2的内容逐字节地拷贝给S1,所以导致最后S1的指针指向S2的指针指向的位置。就是两个都指向同一个位置。(虽然上面的operator=是对char*类型的参数,但上代码的操作也可以实现。)
- 造成内存垃圾,就是原先S1的"THIS"空间就浪费了。
- 当S1和S2同时消亡,这块空间会先后释放两次,可能引起意外错误。
深复制
将一个对象中指针变量指向的内容复制到另一个对象中指针成员指向的地方。
实现过程:在类里面添加成员函数:
String & operator= (const String & s){ //在这里的参数变成引用。而不是以char* 为参数。
//还有个问题。这里的operator为什么要用 引用 类型?明天敲一下代码试一下。
//如果去掉引用会出错,应该就是后面视频提到的“只有返回引用类型才能作为等式左值”吧。
if (str) delete []str;
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
return *this;
}
但同样会有点小问题,如下代码:
String s;
s = "Hello";
s = s;
因为上面有一个delete []str;
的操作,所以会删除掉自己的东西,然后再把自己的东西(这时候已经是空的了)赋值给自己就会粗问题。
解决方法:
再原先的delete前加一个判断: if(str == s.str) return *this
还有一个问题:在调用复制构造函数时,回产生同样的问题,就是浅拷贝的问题。
所以需要自己编写复制构造函数:
String::String(String & s)
{
if(s.str){
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
}
else
str = NULL;
}
运算符重载为友元函数:
上面提到,可以重载为 1. 类的成员函数。 2. 普通函数。
但有时候成员函数不能满足使用要求。普通函数又不能访问类的私有成员。
所以就有了将运算符重载为友元函数。
class Complex{
double real,imag;
public:
Complex(double r, double i): real(r),imag(i){};
Complex operator+(double r);
};
Complex Complex::operator+(double r){
return Complex(real+ r,imag);
}
这种重载方式去实现 c = 5 + c
就会出错。(5在加号前面就会出错,只能用 c = c + 5
为了实现c = 5 + c
只能将+重载为普通函数。而且把operator+
声明为友元,这样才能访问对应类的私有成员。
注意:非引用的函数返回值,不能作为左值使用。
实例:长度可变的String:
自己在vs里跑了一下,视频中有几处小错误。
#include <iostream>
using namespace std;
class CArray {
int size; //数组元素的个数
int* ptr;
public:
CArray(int s = 0);
CArray(CArray& a);
~CArray();
void push_back(int v);
CArray& operator= (const CArray& a);
int length() { return size; }
int& operator[](int i);
};
int& CArray::operator[](int i) { //引用类型能完成赋值工作,就是能放在等号左边。
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;
}
CArray::~CArray() {
if (ptr) delete[]ptr;
}
CArray& CArray::operator=(const CArray& a) {
if (ptr == a.ptr)
return *this;
if (a.ptr == NULL) {
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;
}
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;
}
int main() {
CArray a;
for (int i = 0; i < 5; i++) {
a.push_back(i);
}
CArray a2, a3;
a2 = a;
for (int i = 0; i < a.length(); i++)
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a);
for (int i = 0; i < a4.length(); i++) {
cout << a4[i] << " ";
}
return 0;
}
流插入运算符和流提取运算符的重载:
其实 cout << 123 << "qwe"
的 <<
都是左移运算符。
cout是在iostream定义的,是ostream类对象。
在iostream的头文件里,对 <<
进行了重载。
可以按照以下方式重载:
void ostream::operator<< (int n)
{
...
}
如果想要将 cout << 5 << "this";
这个表达式成立,则需要考虑返回值的问题。(返回值至少不能是void
),如果cout << 5
的返回值还是 cout
,那么也就能将后面的这个"this"
输出了。
ostream & ostream::operator<< (int n)
{
....//输出n的代码。
return *this;
} //对cout使用 << 操作之后,返回的还是 cout 的这个对象。
ostremam & ostream::operator<< (const char* s)
{
....//输出s的代码
return *this;
}
所以 cout << 5 << "this";
的本质就是 cout.operator<<(5).operator<<("this");
流插入运算符的重载
class CStudent{
public: int nAge;
};
//因为 << 运算符是ostream类的对象,而ostream类的东西是我们不能改的,在头文件写好了的。所以我们只能把左移运算符定义成全局函数。
ostream & operator << (ostream & o, const CStudent & s){ //全局函数,需要两个参数。 在这个程序中,第一个参数 & o 其实就是 cout 的引用。
o << s.nAge;
return o;
}
int main(){
CStudent s;
s.nAge = 5;
cout << s << "hello";
return 0;
}
读取复数实例:
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex{
double real,imag;
public:
Complex(double r = 0, double i = 0):real(r),imag(i){};
friend ostream & operator<< (ostream &os, const Complex & c);
friend istream & operator>> (istream & is, Complex & c);
};
ostream & operator<< (ostream & os, const Complex & c){
os << c.real << "+" << c.imag << "i";
return os;
}
istream & operator>> (istream & is, Complex & c)
{
string s;
is >> s; //a+bi作为字符串读入。具体的细节还需完善。
int pos = s.find("+",0); //找到“+”的位置
string sTmp = s.substr(0,pos); //把real部分分离出来,后面也是同理分离imag。
c.real = atof(sTmp.c_str());
sTmp = s.substr(pos+1,s.length()-pos-2);
c.imag = atof(sTmp.c_str());
return is;
}
自加,自减运算符的重载:
前置运算符作为一元运算符的重载。
重载为成员函数:
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);
第二个int没有意义,只是为了区别前置运算符。
obj++, obj.operator++(0),operator++(obj,0)都调用上函数。
class CDemo{
private:
int n;
public:
CDemo(int i = 0):n(i){}
CDemo operator++(); //前置
CDemo operator++(int); //后置
operator int(){return n;} //设置了一个int类型的强制类型运算符的重载。为了保证输出。
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;}
类型强制转换运算符重载时,不能写返回值类型,因为返回值的类型就是转换后的类型。
另外:
- C++不允许定义新的运算符。
- 运算符重载不改变运算符的优先级。
"." ".*" "::" "?:" sizeof
等运算符不能被重载。- 重载运算符
() [] -> =
时,必须声明为类的成员函数。