从 C 向 C++ 进阶系列导航
1. 重载赋值运算符
在需要深拷贝时,除了需要定义拷贝构造函数外,还需要自定义重载运算符 “=” 函数,因为类中默认提供的 “=” 为浅拷贝。
在重载运算符 “=” 函数中,必须遵循以下规则:
- 函数返回类型必须为类引用类型。
- 函数形参必须为 const 修饰的类引用类型
- 函数内部必须检查形参对象与函数调用者是否相同,相同是直接返回 *this 或形参对象。
- 函数申请系统资源之前必须对原持有的系统资源进行释放。
- 函数调用正确时的返回值必须为 this 指向的对象。
- 示例:
class Test
{
private:
char* m_p;
public:
Test(char var)
{
m_p = new char(var);
}
Test(const Test& obj)
{
m_p = new char(*obj.m_p);
}
Test& operator = (const Test& obj)
{
if(this == &obj)
{
return *this;
}
delete this->m_p;
this->m_p = new char(*obj.m_p);
return *this;
}
char PrintVar()
{
return *this->m_p;
}
~Test()
{
delete m_p;
m_p = NULL;
}
};
int main(int argc, char *argv[])
{
Test obj_A = 'a'; // 调用 Test(char var)
Test obj_B = obj_A; // 调用 Test(const Test& obj)
Test obj_C = 'b'; // 调用 Test(char var)
obj_A = obj_C; // 调用 Test& operator = (const Test& obj)
cout << "obj_A.PrintVar() = " << "'" << obj_A.PrintVar() << "'" << endl; // obj_A.PrintVar() = 'b'
cout << "obj_B.PrintVar() = " << "'" << obj_B.PrintVar() << "'" << endl; // obj_B.PrintVar() = 'a'
}
需要注意的是,只有在非定义类对象时两对象之间使用 =
才会调用重载的等号运算符函数,在定义类对象时使用 =
将会调用类的拷贝构造函数。
2. 重载动态内存分配运算符
new 与 delete 为 C++ 所扩展的动态内存分配运算符,其支持进行运算符重载。
在程序中为何可能需要重载这两个运算符呢?这是因为使用 new 来申请一段内存空间时,申请成功固然会返回所申请的内存空间的地址,但申请失败后不同编译器所产生的行为是不一致的,在有的编译器下会抛出异常,而有的编译器下会返回空指针。在工程中,当需要设计在不同平台下共用的类库时,在类中进行 new 与 delete 的运算符重载是非常有必要的,这样保证了库行为的一致性。
重载 new 与 delete 时必须遵循以下规则:
- new 重载函数的返回值类型必须为 void*,形参类型必须为 size_t(Linux 下)。
- delete 重载函数的返回值类型必须为 void,形参类型必须为 void*。
- 如果需要在重载函数中禁止抛出异常,可以在函数名称后使用
throw()
表示不能抛出任何异常。
在一个类中重载了 new 与 delete 运算符后,使用 new 与 delete 来管理类对象的动态内存分配有什么变化呢?
其实本质上并无差别。在触发动态内存分配时,编译器会先调用 new 的规则来获取一段内存,然后在该内存上通过构造函数构造一个类对象;在释放时,会先通过析构函数析构该对象,然后再调用 delect 的规则释放对应的内存空间。而由于该类重载了 new 与 delect,则 new 的规则与 delect 的规则将是自定义的重载函数,不再是编译器的所提供的缺省行为。
简单来说,当使用 new 来申请一个类对象时,会先调用类的 new 运算符重载函数,再调用类的构造函数;当使用 delete 来释放该类对象时,会先调用类的析造函数,再调用类的 delete 运算符重载函数。
- 示例:
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
void* operator new (size_t size) throw()
{
cout << "operator new" << endl;
return malloc(size);
}
void operator delete (void* p)
{
cout << "operator delete" << endl;
free(p);
}
};
int main()
{
Test* p = new Test; // operator new
// Test()
delete p; // ~Test()
// operator delete
}
以上程序中,在重载的 new 与 delete 中调用了 C 函数库 malloc() 与 free(),这样在 new 失败时返回一个空指针,而不会抛出异常,保证了库行为的一致性。这是在工程中常见的重载做法。
3. 重载逻辑运算符陷阱
这里的逻辑运算符指的是运算符 “&&” 与 “||”。对于标准的逻辑运算符,是存在短路法则的,即可能只通过检查运算符左边的条件便可得知整个表达式所返回的值。如:
- 1 || xxx :返回 1,表达式 xxx 并没有检查。
- 0 && xxx :返回 0,表达式 xxx 并没有检查。
逻辑运算符是支持运算符重载的,但由于运算符重载后调用运算符实际为函数调用,则逻辑运算符左右两边的条件都必须检查,无法满足短路法则,因此逻辑运算符重载不是完整的运算符重载,应尽量不重载逻辑运算符。
4. 重载逗号运算符陷阱
逗号运算符即运算符 “,”,用于连接多个表达式,在依次顺序执行多个表达式后,返回最后一个表达式的内容。
- 示例:
int main(int argc, char *argv[])
{
int var_A = 1;
int var_B = 2;
int var_C = 0;
var_C = (var_A = 3, var_B = 4);
cout << "var_A = " << var_A << endl; // var_A = 3
cout << "var_B = " << var_B << endl; // var_B = 4
cout << "var_C = " << var_C << endl; // var_C = 4
}
逻辑运算符是支持运算符重载的,但由于运算符重载后调用运算符实际为函数调用,函数调用在进入函数体前需要完成所有参数的计算,而参数的计算次序是不确定的,因此重载逗号操作符后无法保证重载后的逗号操作符为从左向右计算。
同时,重载逗号运算符是最鸡肋的,几乎没有实际价值,所以切勿重载逗号运算符。
5. 重载前、后置操作符
前、后置运算符指的是 “++” 与 “–”,这里以 “++” 为例,"–" 用法同理。
“++” 的用法如下:
- “++” 前置:"++i" 表示 i 自加 1 后返回自加后的结果。
- “++” 后置:“i++” 表示 i 自加 1,并返回 i。
当重载运算符 “++” 是,需要注意前置与后置的重载情况是不一致的,而后置时需要使用一个 int 类型的占位参数,如:Type operator Sign(int)
。
- 实验:
class Test
{
private:
int m_var;
public:
Test(int num)
{
m_var = num;
}
int GetVar()
{
return m_var;
}
/* 前置运算符重载 */
Test& operator ++()
{
++m_var;
return *this;
}
/* 后置运算符重载 */
Test operator ++(int)
{
Test ret = *this;
this->m_var++;
return ret;
}
};
int main(int argc, char *argv[])
{
Test obj_A = 0;
obj_A++;
cout << "obj_A.GetVar() = " << obj_A.GetVar() << endl; // obj_A.GetVar() = 1
++obj_A;
cout << "obj_A.GetVar() = " << obj_A.GetVar() << endl; // obj_A.GetVar() = 2
Test obj_B = obj_A++;
cout << "obj_A.GetVar() = " << obj_A.GetVar() << endl; // obj_A.GetVar() = 3
cout << "obj_B.GetVar() = " << obj_B.GetVar() << endl; // obj_B.GetVar() = 2
Test obj_C = ++obj_A;
cout << "obj_A.GetVar() = " << obj_A.GetVar() << endl; // obj_A.GetVar() = 4
cout << "obj_C.GetVar() = " << obj_C.GetVar() << endl; // obj_C.GetVar() = 4
}
可以看出,在后置运算符重载函数中会创建一个对象用来保存调用对象的初始状态以作返回,其开销是明显大于前置运算符重载函数的。因此,当对象单独使用 “++” 或 “–” 作为一条语句时,应尽量使用运算符前置的方式。