C++运算符重载详解

运算符重载

目录

1.运算符重载基础

C++将运算符重载扩展到自定义的数据类型(类和对象)

运算符重载本质还是函数调用,但C++编译器做了特殊的处理,在程序中,函数的实参书写在运算符+的两边,代替了函数名和括号的传统书写方式。

好处

  1. 代码的书写更简单
  2. 代码的可读性更好
语法:返回值 operator运算符(参数列表);

运算符重载函数的返回值类型要与运算符本身的含义一致

成员运算符重载和非成员运算符重载

  1. 非成员的运算符重载函数,形参个数与运算符的操作个数相同
  2. 成员函数版本的运算符重载:形参个数比运算符操作数个数少一个,有一个操作数隐式的调用了运算符重载函数
  3. 如果同时重载了非成员函数版本和成员函数版本,就会出现二义性

注意事项

  1. 返回自定义数据类型的引用可以让多个运算符表达式串联起来。(不要返回局部变量的引用→局部变量在函数返回之后会被销毁,这样程序会进入一种无所知的状态
  2. 重载函数参数列表中的顺序决定了操作数的位置。
  3. 重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
  4. 如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
  5. 重载函数不能违背运算符原来的含义和优先级。
  6. 不能创建新的运算符。
  7. 只能通过成员函数进行重载的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时,编译器做了两件事情

  1. 调用标准库函数 operator new()分配内存
  2. 调用构造函数初始化内存

使用delete时,也做了两件事情:

  1. 调用析构函数;
  2. 调用标准库函数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 内存池的概念

内存池是预先分配的内存空间,如果没有内存池,我们直接向系统借;如果有内存池,就向内存池借。程序如果内存太小了,就会动态扩充,但是有一个原则,就是每次都会向系统申请一大块连续的内存空间。

使用内存池的目的

  1. 提升分配和归还的速度
  2. 减少内存碎片

如果内存池用完了一般有三种实现方法

  1. 扩展内存池
  2. 直接向系统申请内存
  3. 返回空地址

内存池的概念比较简单,但是实现起来比较复杂,涉及到的数据结构比较多,这里实现一个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;
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值