运算符重载
目录
- 1.运算符重载基础
- 2. 重载关系运算符==
- 3.重载左移运算符<<
- 4. 重载下标运算符[]
- 5.重载赋值运算符
- 6.重载new & delete运算符
- 7. 实现简单的内存池
- 8. 重载括号运算符()
- 9.重载一元运算符 ++
1.运算符重载基础
C++将运算符重载扩展到自定义的数据类型(类和对象)
运算符重载本质还是函数调用,但C++编译器做了特殊的处理,在程序中,函数的实参书写在运算符+的两边,代替了函数名和括号的传统书写方式。
好处
- 代码的书写更简单
- 代码的可读性更好
语法:返回值 operator运算符(参数列表);
运算符重载函数的返回值类型要与运算符本身的含义一致
成员运算符重载和非成员运算符重载
- 非成员的运算符重载函数,形参个数与运算符的操作个数相同
- 成员函数版本的运算符重载:形参个数比运算符操作数个数少一个,有一个操作数隐式的调用了运算符重载函数
- 如果同时重载了非成员函数版本和成员函数版本,就会出现二义性
注意事项
- 返回自定义数据类型的引用可以让多个运算符表达式串联起来。(不要返回局部变量的引用→局部变量在函数返回之后会被销毁,这样程序会进入一种无所知的状态)
- 重载函数参数列表中的顺序决定了操作数的位置。
- 重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
- 如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
- 重载函数不能违背运算符原来的含义和优先级。
- 不能创建新的运算符。
- 只能通过成员函数进行重载的4个符号: = () [] →
2. 重载关系运算符==
关系运算符:== ≠ > ≥ < ≤
有成员函数和非成员函数两个版本,建议使用成员函数版本
例子
bool operator==(const CGirl& g1) {
if ((m_yz + m_sc) == (g1.m_yz + g1.m_sc)) return true;
return false;
}
3.重载左移运算符<<
重载左移运算符(<<)用于输出自定义对象的成员变量,在调试和日志中很有价值
只能使用非成员函数版本:原因是使用成员函数版本实现的话,需要类成员在运算符左侧,与实际使用习惯不符合
如果要输出对象的私有成员,可以配合友元一起使用
class CGirl
{
friend ostream& operator<<(ostream& cout, const CGril& g);
string m_name;
int m_xw;
int m_score;
}
ostream& operator<<(ostream& cout, const CGril& g) {
cout << "姓名" << g.m_name << "评分" << g.m_score;
return cout;
}
C++中cout
可以输出多种数据类型,也是因为iostream
头文件中内置了cout
操作符的多种重载方法
4. 重载下标运算符[]
如果对象中有数组,重载下标运算符[],操作对象中的数组将像操作普通数组一样方便
必须以成员函数的形式来重载
语法
返回值类型& operator[](参数)
或者
const 返回值类型& operator[](参数) const
提供两种重载方法的原因是因为,有可能声明常对象,而常对象只能调用常成员函数
- 使用第一种声明方式,[]不仅可以访问数组元素,还可以修改数组元素
- 使用第二种声明方式,[]只能访问而不能修改数组元素
在重载函数中,可以对下标做合法性检查,防止数组越界,但是一般不做这个工作,如果越界的话,要返回什么呢?
class CGirl
{
private:
string m_boys[3];
public:
string& operator[](int ii) {
return m_boys[ii];
}
const string& operator[](int ii) const {
return m_boys[ii];
}
}
int main() {
CGril g; // 创建对象
g[1] = "王麻子";
cout << "第一任男朋友" << g[1] << endl;
const CGril g1 = g;
cout << "第一任男朋友" << g1[1] << endl;
}
5.重载赋值运算符
C++编译器可能会给类添加四个函数
- 默认构造函数,空实现。
- 默认析构函数,空实现。
- 默认拷贝构造函数,对成员变量进行浅拷贝。
- 默认赋值函数, 对成员变量进行浅拷贝。
拷贝构造函数和赋值函数的区别
- 拷贝构造是指原来的对象不存在,用已存在的对象进行构造;赋值运算是指已经存在了两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量。
语法
类名& operator=(const 类名& 源对象);
注意
- 编译器默认提供的拷贝构造函数,是浅拷贝
- 如果对象中不存在堆区内存空间,默认赋值函数就可以满足要求
class CGril{
int* m_ptr;
public:
CGril() {m_ptr = nullptr};
~CGril() {
if (m_ptr != nullptr {
delete m_ptr;
m_ptr = nullptr;
}
}
CGril& operator=(const CGril& g) {
// 如果源对象是自己,返回this指针的解引用
if (this = &g) return *this
if (g.m_ptr == nullptr) {
// 源对象是nullptr
if (m_ptr != nullptr {
delete m_ptr;
m_ptr = nullptr;
}
} else {
// 先分配内存
if (m_ptr == nullptr)
m_ptr = new int;
// 然后将源对象内存中的数据复制到目标对象的内存中
memset(m_ptr, g.m_ptr, sizeof(int));
}
}
};
6.重载new & delete运算符
重载new和delete运算符的目是为了自定义内存分配的细节。(内存池:快速分配和归还,无碎片)
需要用到malloc()
和free()
在C++中,使用new时,编译器做了两件事情
- 调用标准库函数 operator new()分配内存
- 调用构造函数初始化内存
使用delete时,也做了两件事情:
- 调用析构函数;
- 调用标准库函数operator delete()释放内存。
构造函数和析构函数由编译器调用,我们无法控制。但是,可以重载内存分配函数operator new()和释放函数operator delete()。
语法(固定的)
// size_t 是unsigned long long
void* operator new(size_t size)
void operator delete(void* ptr)
重载的new和delete可以是全局函数,也可以是类的成员函数,new 和 delete是全局函数的话,将接管全部的new和delete动态创建和销毁内存的工作。如果为类重载new和delete函数,作用范围将会是类,而不是全局的,不管有没有写static关键字,都是static函数,不能访问非静态成员函数。
编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()。
new[]和delete[]也可以重载,但在实际开发中,类用作数组的情况不多见,需要自定义动态分配和回收的场景基本没有,所以就不多讲了。
在C++中,delete空指针是安全的,不会造成程序的崩溃
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。
{
cout << "调用了全局重载的new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。
{
cout << "调用了全局重载的delete。\n";
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
}
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
int m_xw; // 胸围。
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。
{
cout << "调用了类的重载的new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。
{
cout << "调用了类的重载的delete。\n";
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
}
};
int main()
{
int* p1 = new int(3);
cout << "p1=" << (void *)p1 <<",*p1=" <<*p1<< endl;
delete p1;
CGirl* p2 = new CGirl(3, 8);
cout << "p2的地址是:" << p2 << "编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;
delete p2;
}
7. 实现简单的内存池
7.1 内存池的概念
内存池是预先分配的内存空间,如果没有内存池,我们直接向系统借;如果有内存池,就向内存池借。程序如果内存太小了,就会动态扩充,但是有一个原则,就是每次都会向系统申请一大块连续的内存空间。
使用内存池的目的
- 提升分配和归还的速度
- 减少内存碎片
如果内存池用完了一般有三种实现方法
- 扩展内存池
- 直接向系统申请内存
- 返回空地址
内存池的概念比较简单,但是实现起来比较复杂,涉及到的数据结构比较多,这里实现一个demo
7.2 单内存池代码示例
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
int m_xw;
static char* m_pool; // 内存池的起始地址。
static bool initpool() // 个初始化内存池的函数。
{
m_pool = (char*)malloc(18); // 向系统申请18字节的内存。
if (m_pool == 0) return false; // 如果申请内存失败,返回false。
memset(m_pool, 0, 18); // 把内存池中的内容初始化为0。
cout << "内存池的起始地址是:" << (void*)m_pool << endl;
return true;
}
static void freepool() // 释放内存池。
{
if (m_pool == 0) return; // 如果内存池为空,不需要释放,直接返回。
free(m_pool); // 把内存池归还给系统。
cout << "内存池已释放。\n";
}
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。
{
if (m_pool[0] == 0) // 判断第一个位置是否空闲。
{
cout << "分配了第一块内存:" << (void*)(m_pool + 1) << endl;
m_pool[0] = 1; // 把第一个位置标记为已分配。
return m_pool + 1; // 返回第一个用于存放对象的址。
}
if (m_pool[9] == 0) // 判断第二个位置是否空闲。
{
cout << "分配了第二块内存:" << (void*)(m_pool + 9) << endl;
m_pool[9] = 1; // 把第二个位置标记为已分配。
return m_pool + 9; // 返回第二个用于存放对象的址。
}
// 如果以上两个位置都不可用,那就直接系统申请内存。
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。
{
if (ptr == 0) return; // 如果传进来的地址为空,直接返回。
if (ptr == m_pool + 1) // 如果传进来的地址是内存池的第一个位置。
{
cout << "释放了第一块内存。\n";
m_pool[0] = 0; // 把第一个位置标记为空闲。
return;
}
if (ptr == m_pool + 9) // 如果传进来的地址是内存池的第二个位置。
{
cout << "释放了第二块内存。\n";
m_pool[9] = 0; // 把第二个位置标记为空闲。
return;
}
// 如果传进来的地址不属于内存池,把它归还给系统。
free(ptr); // 释放内存。
}
};
char* CGirl::m_pool = 0; // 初始化内存池的指针。
int main()
{
// 初始化内存池。
if (CGirl::initpool()==false) { cout << "初始化内存池失败。\n"; return -1; }
CGirl* p1 = new CGirl(3, 8); // 将使用内存池的第一个位置。
cout << "p1的地址是:" << p1 << ",编号:" << p1->m_bh << ",数据:" << p1->m_xw << endl;
CGirl* p2 = new CGirl(4, 7); // 将使用内存池的第二个位置。
cout << "p2的地址是:" << p2 << ",编号:" << p2->m_bh << ",数据:" << p2->m_xw << endl;
CGirl* p3 = new CGirl(6, 9); // 将使用系统的内存。
cout << "p3的地址是:" << p3 << ",编号:" << p3->m_bh << ",数据:" << p3->m_xw << endl;
delete p1; // 将释放内存池的第一个位置。
CGirl* p4 = new CGirl(5, 3); // 将使用内存池的第一个位置。
cout << "p4的地址是:" << p4 << ",编号:" << p4->m_bh << ",数据:" << p4->m_xw << endl;
delete p2; // 将释放内存池的第二个位置。
delete p3; // 将释放系统的内存。
delete p4; // 将释放内存池的第一个位置。
CGirl::freepool(); // 释放内存池。
}
8. 重载括号运算符()
括号运算符()也可以重载,对象名可以当成函数来使用(函数对象、仿函数),lambda表达式实际上也是生成一个仿函数。
括号运算符重载函数的语法:
返回值类型 operator()(参数列表);
注意:
- 括号运算符必须以成员函数的形式进行重载。
- 括号运算符重载函数具备普通函数全部的特征。
- 如果函数对象与全局函数同名,按作用域规则选择调用的函数。
函数对象的用途:
- 表面像函数,部分场景中可以代替函数,在STL中得到广泛的应用;
- 函数对象本质是类,可以用成员变量存放更多的信息;
- 函数对象有自己的数据类型;
- 可以提供继承体系。
void show(string str) // 向超女表白的函数。
{
cout << "普通函数:" << str << endl;
}
class CGirl
{
public:
void operator()(string str)
{
cout << "重载函数:" << str << endl;
}
};
int main()
{
CGirl show;
// 这里加了::会优先调用普通函数,否则应该优先调用重载函数
::show("我是一只傻傻鸟。");
show("我是一只傻傻鸟。");
}
9.重载一元运算符 ++
1)++ 自增 2)-- 自减 3)! 逻辑非 4)& 取地址
5)~ 二进制反码 6)* 解引用 7)+ 一元加 8) - 一元求反
一元运算符通常出现在它们所操作的对象的左边。
但是,自增运算符++和自减运算符–有前置和后置之分。
C++ 规定,重载++或–时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数。
语法
成员函数版:CGirl &operator++(); // ++前置
成员函数版:CGirl operator++(int); // 后置++
非成员函数版:CGirl &operator++(CGirl &); // ++前置
非成员函数版:CGirl operator++(CGirl &,int); // 后置++
在表达式中,前置的自增可以嵌套,但是后置的自增不可以嵌套
CGirl & operator++() // ++前置的重载函数。
{
// 先++,再返回
m_ranking++;
return *this;
}
CGirl operator++(int) // ++后置的重载函数。
{
// 先返回,再++
// 这里不能返回临时对象的引用
CGirl tmp = *this;
m_ranking++;
return tmp;
}