Lecture 11 C++特殊操作符的重载
C++特殊操作符
-
赋值操作符
=
-
访问数组元素操作符
[]
-
动态对象创建与撤销操作符
new
与delete
-
函数调用操作符
()
-
类成员访问操作符
->
-
类型转换操作符
赋值操作符“=”的重载
同一个类的两个对象如何赋值?
A a,b;
......
a = b;
C++编译程序会为每个类定义一个隐式的赋值操作,其行为是:逐个成员进行赋值操作。
- 对于普通成员,它采用常规的赋值操作。
- 对于成员对象,则调用该成员对象类的赋值操作进行成员对象的赋值操作。
隐式的赋值操作有时不能满足要求!
例如,对下面String类的对象进行赋值操作时,系统提供的隐式赋值操作存在问题:
class String
{ int len;
char *str;
public:
String(char *s)
{ len = strlen(s);
str = new char[len+1];
strcpy(str,s);
}
~String() { delete []str; str=NULL; }
};
......
String s1("xyz"),s2("abcdefg");
.......
s1 = s2;
//赋值后,s1.str原来所指向的空间成了“孤儿”
//s1和s2互相干扰
//s1和s2消亡时,"abcdefg"所在的空间将会被释放两次!
解决上面问题的办法是自己定义赋值操作符重载函数:
class String
{ ......
String& operator = (const String& s)
{ if (&s == this) return *this; //防止自身赋值。
delete []str;
str = new char[s.len+1];
strcpy(str,s.str);
len = s.len;
return *this;
}
};
上面的返回值类型为什么是String的引用?
- 为了允许下面的操作:
(a=b)=c
自定义的赋值操作符重载函数不会自动地去进行成员对象的赋值操作,必须要在自定义的赋值操作符重载函数中显式地指出,例如:
class A { .......};
class B
{ A a;
int x,y;
public:
......
B& operator = (const B& b)
{ a = b.a;//调用A类的赋值操作符重载函数来实现成员对象的赋值。
x = b.x;
y = b.y;
return *this;
}
};
-
赋值操作符只能作为非静态的成员函数来重载。不能被继承。
-
一般来讲,需要自定义拷贝构造函数的类通常也需要自定义赋值操作符重载函数。
-
注意:要区别下面两个“=”的不同含义。
A a;
A b=a; //初始化,等价于:A b(a);,调用拷贝构造函数。
......
b = a; //赋值,调用赋值操作符=重载函数。
访问数组元素操作符[]
的重载
对于由具有线性关系的元素所构成的对象,可通过重载下标操作符“[]”来实现对其元素的访问。例如:
class String
{ int len;
char *str;
public:
......
char &operator [](int i) { return str[i]; }
};
......
String s("abcd");
... s[2] ... //访问s中的'c'
再例如:
class Vector //向量类
{ int *p_data;
int num;
public:
......
int &operator[](int i) //访问向量第i个元素。
{ return p_data[i];
}
};
Vector v(10);
......
... v[2] ... //访问向量第三个元素
对于常量对象的场合要加上另一个重载函数:
int operator[](int i) const
{ return p_data[i];
}
操作符new与delete的重载
操作符new和delete用于创建和撤销动态对象:
A *p=new A;
......
delete p;
-
系统提供的new和delete操作所涉及的空间分配和释放是通过系统的堆区管理系统来进行的,效率常常不高。
-
可以对操作符new和delete进行重载,使得程序能以自己的方式来实现动态对象空间的分配和释放功能。
操作符new有两个功能:
- 为动态对象分配空间
- 调用对象类的构造函数
操作符delete也有两个功能:
- 调用对象类的析构函数
- 释放动态对象的空间
重载操作符new和delete时,重载的是它们的分配空间和释放空间的功能,不影响对构造函数和析构函数的调用。
重载操作符new
操作符new必须作为静态的成员函数来重载(static说明可以不写),其格式为:
void *operator new(size_t size);
-
返回类型必须为
void *
-
参数size表示对象所需空间的大小,其类型为
size_t(unsigned int)
例:把动态对象初始化为全‘0’
#include <cstring>
class A
{ int x,y;
public:
void *operator new(size_t size)
{ void *p = malloc(size); //调用系统堆空间分配操作。
memset(p,0,size); //把申请到的堆空间初始化为全“0”。
return p;
}
......
};
- 为一个没有定义任何构造函数的类的动态对象提供初始化。
重载的new操作符的使用格式与系统提供的基本相同。例如(假设A中重载了new):
A *p=new A;
-
自动计算A的大小,把它作为参数(size)去调用new的重载函数。
-
调用A类默认构造函数。
A *q=new A(...);
- 自动计算A的大小,把它作为参数(size)去调用new的重载函数。
- 调用A类带参数(…)的构造函数。
重载new时,除了对象空间大小参数以外,也可以带有其它参数,
void *operator new(size_t size,…);
如果重载的new包含其它参数,其使用格式为:
p = new (<1>) A(<2>);
-
<1> 表示提供给new重载函数的其它参数
-
<2> 表示提供给A类构造函数的参数
例:在非“堆区”为动态对象分配空间
#include <cstring>
class A
{ ......
public:
A(int i) { ... }
void *operator new(size_t size,void *p)
{ return p;
}
};
char buf[sizeof(A)];
A *p = new (buf) A(0);//动态对象的空间分配为buf
......
p->~A(); //使得p所指向的对象消亡。不能用系统的delete
重载delete
一般来说,如果对某个类重载了操作符new,则相应地也要重载操作符delete。
操作符delete也必须作为静态的成员函数来重载(static说明可以不写),其格式为:
void operator delete(void *p, size_t size);
-
返回类型必须为void。
-
第一个参数类型为void *,指向对象的内存空间。
-
第二个参数可有可无,如果有,则必须是size_t类型。
重载后,操作符delete的使用格式与未重载的相同。
例:重载操作符new与delete来管理程序中某类动态对象的堆空间
就某一个类的动态对象而言,系统的堆空间管理效率不高:
-
系统要考虑程序中各种大小的堆空间申请和释放问题。
-
系统要处理“碎片”问题。
针对某个类的动态对象,程序可以自己管理该类对象的空间分配和释放,以提高效率。例如,
-
重载new
- 第一次创建该类的动态对象时,先从系统管理的堆区中申请一块大的空间;
- 把上述大空间分成若干小块,每个小块的大小为该类一个对象的大小,然后用链表来管理这些小块;
- 在该链表上为该类对象分配空间。
-
重载delete
- 该类的一个对象消亡时,该对象的空间归还到new操作中申请到的大空间(链表)中,而不是归还到系统的堆区中。
#include <cstring>
class A
{ ...... //类A的已有成员说明。
public:
static void *operator new(size_t size);
static void operator delete(void *p);
private:
A *next; //用于组织A类对象自由空间结点的链表。
static A *p_free; //用于指向A类对象的自由空间链表头。
};
A *A::p_free=NULL;
const int NUM=32;
void *A::operator new(size_t size)
{ A *p;
if (p_free == NULL)
{ //申请NUM个A类对象的大空间。
p_free = (A *)malloc(size*NUM);
//在大空间上建立自由结点链表。
for (p=p_free; p!=p_free+NUM-1; p++)
p->next = p+1;
p->next = NULL;
}
//从链表中给当前对象分配空间
p = p_free;
p_free = p_free->next;
memset(p,0,size);
return p;
}
void A::operator delete(void *p)
{ ((A *)p)->next = p_free;
p_free = (A *)p;
}
A *q1=new A;
A *q2=new A;
delete q1;
在申请空间时,一块用完了,将会申请第二块。
动态对象数组的创建与撤销
A *p=new A[10];
... p[0]、p[1]、...、p[9] ...
delete []p; //“[]”不能漏掉!
上述操作将调用下面隐式的重载函数:
void *operator new[](size_t size);
void operator delete[](void *p);
可以自定义上面的重载函数完成特殊的操作。
注意:传给函数new[]的参数size的实际值可能比对象数组需要的空间多4个字节,用于系统管理!
重载函数调用操作符“()” --函数对象
-
在C++中,把函数调用也作为一种操作符来看待:
- 操作数为函数名以及各个实参!
-
重载函数调用操作符之后,相应类的对象就可当作函数来使用了。例如:
class A
{ int value;
public:
A() { value = 0; }
A(int i) { value = i; }
int operator () (int x) //函数调用操作符()的重载函数
{ return x+value;
}
};
......
A a(3);
cout << a(10) << endl; //a(10)将会去调用A中的函数调用操
//作符重载函数,10作为实参
函数调用操作符重载主要用于只有一个操作的对象(函数对象,functor),该对象除了具有一般函数的行为外,它还可以拥有状态。例如,下面是一个能产生随机数的对象:
class Random
{ unsigned int seed; //状态
public:
Random(unsigned int i) { seed = i; }
unsigned int operator ()() //函数调用操作符重载
{ seed = (25173*seed+13849)%65536;
return seed;
}
};
......
Random random(1); //创建一个函数对象
...random()... //利用函数对象random产生一个随机数
n在C++中,λ表达式是通过函数对象来实现的。
重载间接类成员访问操作符“->” --智能指针
“->”为一个双目操作符,其第一个操作数为一个指向类或结构的指针,第二个操作数为第一个操作数所指向的类或结构的成员。
A a,*p=&a; p->f();
通过对“->”进行重载,可以实现一种智能指针(smart pointers):
-
一个具有指针功能的对象,通过该对象能访问所“指向”的另一个对象。
-
通过该指针对象去访问所指向的对象的成员前能做一些额外的事情。
B b(&a); b->f(); //b是个智能指针对象,指向对象a
例:在程序执行的某个时刻获得某个对象被访问的次数
class A
{ int x,y;
public:
void f();
void g();
};
void func(A *p)
{ ...... p->f(); ...... p->g(); ......
}
......
A a;
func(&a);
...... //调用完func后,如何知道在func中访问了a多少次?
一种解决方案:
class A
{ int x,y;
int count;
public:
A() { count = 0; ... }
void f() { count++; ... }
void g() { count++; ... }
int num_of_access() const { return count; }
};
void func(A *p) { ...... p->f(); ...... p->g(); ...... }
......
A a;
func(&a);
... a.num_of_access() ... //获得对a的访问次数
问题:
-
要修改类A,每个成员函数都要加上:
count++;
-
如果类A中有外界可访问的数据成员,怎么处理?
另一种解决方案:
class B //智能指针类
{ A *p_a;
int count;
public:
B(A *p)
{ p_a = p; count = 0;
}
A *operator ->() //操作符“->”的重载函数,按单目操作符重载!
{ count++; return p_a;
}
int num_of_a_access() const
{ return count;
}
};
void func(B &p) //注意:p是个B类对象!
{ ...... p->f(); ...... p->g(); ......
}
//p->f(); //等价于:p.operator->()->f();
A a;
B b(&a); //b为一个智能指针对象,它指向了a
func(b);
... b.num_of_a_access() ... //获得对a的访问次数
为了完全模拟普通的指针功能,针对智能指针类,还可以重载*
(对象间接访问)、[]
、+
、-
、++
、--
、…等操作符:
class B //智能指针类
{ A *p_a;
......
A& operator *()
{ return *p_a;
}
A& operator [](int i)
{ return p_a[i];
}
......
};
自定义类型转换操作符
类中带一个参数的构造函数可以用作从其它类型到该类的转换。 例如:
class Complex
{ double real, imag;
public:
Complex() { real = 0; imag = 0; }
Complex(double r) { real = r; imag = 0; }
Complex(double r, double i) { real = r; imag = i; }
......
friend Complex operator + (const Complex& x, const Complex& y);
};
......
Complex c1(1,2),c2,c3;
c2 = c1 + 1.7; //1.7隐式转换成一个复数对象Complex(1.7)
c3 = 2.5 + c2; //2.5隐式转换成一个复数对象Complex(2.5)
可自定义类型转换,从一个类转换成其它类型,例如:
class A
{ int x,y;
public:
......
operator int() { return x+y; } //类型转换操作符int的重载函数
};
...…
A a;
int i=1;
int z = i + a; //将调用类型转换操作符int的重载函数把对象a隐式转换成int型数据。
歧义问题
class A
{ int x,y;
public:
A() { x = 0; y = 0; }
A(int i) { x = i; y = 0; }
A(int i,int j) { x = i; y = j; }
operator int() { return x+y; }
friend A operator +(const A &a1, const A &a2);
};
......
A a;
int i=1,z;
z = a + i; //是a转换成int呢,还是i转换成A?
对上面的问题,可以用显式类型转换来解决:
z = (int)a + i;
//或
z = a + (A)i;
也可以通过给A类的构造函数A(int i)加上一个修饰符explicit,禁止把它当作隐式类型转换符来用:
class A
{ ......
explicit A(int i) //禁止把它当作隐式类型转换符来用。
{ x = i; y = 0;
}
operator int() { return x+y; }
......
};
A a;
int i=1,z;
z = a + i; //a转换成int