Skip to content

Latest commit

 

History

History
640 lines (531 loc) · 16.2 KB

l_12.md

File metadata and controls

640 lines (531 loc) · 16.2 KB

Lesson12 类:委托构造&重载(静态多态)



1. 委托(代理)构造函数 (delegating constructor)

顾名思义,委托其他构造函数帮忙构造。

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的

class A {
  public:
   A()
       : A(0) {}  // 委托构造
   A(int i)
       : A(i, 0) {}  // 委托构造
   A(int j, int i) {}
};

构造函数调用顺序为:

A() ----> A(int) ----> A(int, int)

委托构造的写法

Constructor1(param) : Constructor2(param) {statement}

举个详细的例子

class Base {
  public:
   int _value1;
   int _value2;
   Base() {               // 目标构造函数
      _value1 = 1;
   }
   Base(int value)        // 委托构造函数
       : Base() {
      _value2 = value;
   }
};

int main() {
  Base b(2);
}

会调用Base(int value) : Base(),进入Base(),先给_value1赋值,然后进入Base(int value),给_value2赋值

委托环 (delegation cycle)

即委托构造函数形成闭环,无限递归,程序中应避免出现此现象

class A {
 public:
   A() : A(0) {}
   A(int i) : A(i, 0) {}
   A(int i, int j) : A() {}
};

int main() {
   A a;
}

构造函数调用顺序为

A() ----> A(int) ----> A(int, int) ----> A()

2. 运算符重载

C++将运算符重载扩展到自定义的数据类型,它可以让对象操作更美观,优雅

语法:

ret_type operator character(param) {}
  • 运算符重载函数的返回值类型要与运算符本身的含义一致。
  • 非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同;
  • 成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象;
  • 如果同时重载了非成员函数和成员函数版本,会出现二义性。

注意

  1. 返回自定义数据类型的引用可以让多个运算符表达式串联起来。(不要返回局部变量的引用)
  2. 重载函数参数列表中的顺序决定了操作数的位置。
  3. 重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
  4. 如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
  5. 重载函数不能违背运算符原来的含义和优先级。
  6. 不能创建新的运算符。
  7. 以下运算符不可重载:
符号 含义
sizeof sizeof运算符
. 成员运算符
.* 成员指针运算符
:: 作用域解析运算符
?: 条件运算符
typeid 一个RTTI运算符
const_cast 强制类型转换运算符
dynamic_cast 强制类型转换运算符
reinterpret_cast 强制类型转换运算符
static_cast 强制类型转换运算符
  1. 以下运算符只能通过成员函数进行重载:
符号 含义
= 赋值运算符
() 函数调用运算符
[] 下标运算符
-> 通过指针访问类成员的运算符

重载算术运算符

这里仅以类内 +,类外 - 作为演示,其他算术运算符同理

class A {
 public:
   int _a;
   int _b;
   A(int a, int b) 
     : _a(a), _b(b) {}

   A& operator+(A& a) {
      this->_a += a._a;
      return *this;
   }
};

A& operator-(A& a1, A& a2) {
   a1._b -= a2._b;
   return a1;
}

int main() {
   A a1(1, 3);
   A a2(2, 3);
   a1 = a1 + a2;
   a1 = a1 - a2;
   std::cout << a1._a << std::endl;
   std::cout << a1._b << std::endl;
}

output

3
0

重载关系运算符

仅以 > 作为演示,其他关系运算符同理

class A {
  public:
   int _a;
   int _b;
   A(int a, int b)
       : _a(a), _b(b) {}

   bool operator>(A& a) {
      return this->_a > a._a;
   }
};

int main() {
   A a1(1, 3);
   A a2(2, 3);
   if (a1 > a2) {
      std::cout << "a1 > a2" << std::endl;
   } else {
      std::cout << "a1 < a2" << std::endl;
   }
}

output

a1 < a2

重载左移运算符

class A {
   friend std::ostream& operator<<(std::ostream& cout, const A& a);

  public:
   A(int a, int b)
       : _a(a), _b(b) {}

  private:
   int _a;
   int _b;
};

std::ostream& operator<<(std::ostream& cout, const A& a) {
   cout << "_a = " << a._a << " _b = " << a._b;
   return cout;
}

int main() {
   A a1(1, 3);
   std::cout << a1 << std::endl;
}

output

_a = 1 _b = 3

重载下标运算符

class A {
  private:
   int arr[3];

  public:
   int& operator[](int i) {
      return arr[i];
   }
};

int main() {
   A a1;
   a1[0] = 0;
   a1[1] = 1;
   a1[2] = 2;
   std::cout << a1[0] << a1[1] << a1[2] << std::endl;
}

output

012

重载赋值运算符

一般用于实现深拷贝

class A {
  private:
   int* _a;

  public:
   A() { _a = new int(1); }
   ~A() { delete _a; }

   A& operator=(A& a) {
      if (this->_a == _a)
         return *this;
      a._a = new int(*_a);
      return *this;
   }
};

int main() {
   A a1, a2;
   a1 = a2;
}

重载 new & delete 运算符

在C++中,使用new时,编译器做了两件事情:

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

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

  1. 调用析构函数;
  2. 调用标准库函数operator delete()释放内存。

构造函数和析构函数由编译器调用,我们无法控制。

但是,可以重载内存分配函数operator new()和释放函数operator delete()

  • 重载内存分配函数的语法
void* operator new(size_t size);   

参数必须是size_t,返回值必须是void*

  • 重载内存释放函数的语法
void operator delete(void* ptr)  

参数必须是void*(指向由operator new()分配的内存),返回值必须是void

注意

  • 重载的newdelete可以是全局函数,也可以是类的成员函数。
  • 为一个类重载newdelete时,尽管不必显式地使用static,但实际上仍在创建static成员函数。
  • 编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()
  • new[]delete[]也可以重载。

内存池

class A {
  private:
   int _a;
   int _b;
   static char* _pool;  // 内存池的起始地址

  public:
   // 初始化内存池
   static bool initpool() {
      _pool = (char*)malloc(18);    // 向系统申请18字节的内存
      if (_pool == 0)
         return false;              // 如果申请内存失败,返回false
      memset(_pool, 0, 18);         // 把内存池中的内容初始化为0
      std::cout << "内存池的起始地址是: " << (void*)_pool << std::endl;
      return true;
   }

   // 释放内存池
   static void freepool() {
      if (_pool == 0)
         return;                    // 如果内存池为空,不需要释放,直接返回
      free(_pool);                  // 把内存池归还给系统
      std::cout << "内存池已释放" << std::endl;
   }

   A(int a, int b) {
      _a = a, _b = b;
      std::cout << "调用了构造函数A()" << std::endl;
   }
   ~A() {
      std::cout << "调用了析构函数~A()" << std::endl;
   }

   void* operator new(size_t size) {  // 参数必须是size_t(unsigned long long),返回值必须是void*
      if (_pool[0] == 0)              // 判断第一个位置是否空闲
      {
         std::cout << "分配了第一块内存:" << (void*)(_pool + 1) << std::endl;
         _pool[0] = 1;                 // 把第一个位置标记为已分配
         return _pool + 1;             // 返回第一个用于存放对象的址
      }
      if (_pool[9] == 0) {             // 判断第二个位置是否空闲
         std::cout << "分配了第二块内存:" << (void*)(_pool + 9) << std::endl;
         _pool[9] = 1;                 // 把第二个位置标记为已分配
         return _pool + 9;             // 返回第二个用于存放对象的址
      }

      // 如果以上两个位置都不可用,那就直接系统申请内存
      void* ptr = malloc(size);        // 申请内存
      std::cout << "申请到的内存address: " << ptr << std::endl;
      return ptr;
   }

   void operator delete(void* ptr) {   // 参数必须是void *,返回值必须是void
      if (ptr == 0)
         return;                       // 如果传进来的地址为空,直接返回

      if (ptr == _pool + 1) {          // 如果传进来的地址是内存池的第一个位置
         std::cout << "释放了第一块内存" << std::endl;
         _pool[0] = 0;                 // 把第一个位置标记为空闲
         return;
      }

      if (ptr == _pool + 9) {          // 如果传进来的地址是内存池的第二个位置
         std::cout << "释放了第二块内存" << std::endl;
         _pool[9] = 0;                 // 把第二个位置标记为空闲
         return;
      }

      // 如果传进来的地址不属于内存池,把它归还给系统
      free(ptr);                       // 释放内存
   }
};

char* A::_pool = 0;                    // 初始化内存池的指针

int main() {
   if (A::initpool() == false) {       // 初始化内存池
      std::cout << "初始化内存池失败。" << std::endl;
      return -1;
   }

   A* p1 = new A(3, 8);                // 将使用内存池的第一个位置
   std::cout << "p1 address: " << p1 << std::endl;

   A* p2 = new A(4, 7);                // 将使用内存池的第二个位置
   std::cout << "p2 address: " << p2 << std::endl;

   A* p3 = new A(6, 9);                // 将使用系统的内存
   std::cout << "p3 address: " << p3 << std::endl;

   delete p1;                          // 将释放内存池的第一个位置

   A* p4 = new A(5, 3);                // 将使用内存池的第一个位置
   std::cout << "p4 address: " << p4 << std::endl;

   delete p2;                          // 将释放内存池的第二个位置
   delete p3;                          // 将释放系统的内存
   delete p4;                          // 将释放内存池的第一个位置

   A::freepool();                      // 释放内存池
}

重载括号运算符

主要用于构建仿函数(函数对象)

注意

  • 括号运算符必须以成员函数的形式进行重载。
  • 括号运算符重载函数具备普通函数全部的特征。
  • 如果函数对象与全局函数同名,按作用域规则选择调用的函数

仿函数的本质是类,可以比函数存放更多信息

class A {
  public:
   int count = 0;

   bool isPrime(const int& n) {
      int x = sqrt(n);
      for (int i = 2; i <= x; i++) {
         if (n % i == 0) {
            return false;
         }
      }
      return true;
   }

   void operator()(int range) {
      for (int i = 2; i <= range; i++) {
         if (isPrime(i)) {
            std::cout << i << " ";
            count++;
         }
      }
      std::cout << std::endl;
   }
};

int main() {
   int num = 100;
   A a;
   a(num);
   std::cout << a.count << std::endl;
}

可以实现统计函数被调用次数
output

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 
25

重载一元运算符

可重载的一元运算符:

符号 含义
++ 自增
-- 自减
! 逻辑非
& 取地址
~ 二进制反码
* 解引用
+ 一元加
- 一元求反

一元运算符通常出现在它们所操作的对象的左边;但是,自增运算符++和自减运算符--有前置和后置之分

C++ 规定,重载++--时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数

  • 成员函数版
A& operator++();             // ++前置
A operator++(int);           // 后置++
  • 非成员函数版
A& operator++(A&);           // ++前置
A operator++(A, int);        // 后置++

仅演示成员函数版

class A {
  public:
   int _a = 0;

   // 前置++
   A& operator++() {
      _a++;
      return *this;
   }

   // 后置++
   const A operator++(int) {
      A tmp = *this;
      _a++;
      return tmp;
   }
};

补充

为什么前置++和后置++的返回值类型不同?

  • 前置++返回对象的引用,是为了实现函数能够改变实参,并且链式编程,如++ ++a;
  • 而后置++返回临时对象,是为了保证实参不被函数体改变,做到先返回原先的对象而后进行自增运算,返回值被const修饰是为了防止使用链式编程(a++的值应是右值),当然,并不阻止你使用链式编程,只是因为这符合编译器的规定(普通变量的后置++的链式编程也是不被允许的)

3. 转换函数

(转换)构造函数只用于从某类型到类类型的转换,如果要进行相反的转换,可以使用特殊的运算符函数——转换函数

语法

operator Type();

注意 转换函数必须是类的成员函数,不能指定返回值类型,不能有参数

class A {
  public:
   int _a;
   double _b;
   std::string _c;

   A() {
     _a = 1;
     _b = 1.2;
     _c = "hello";
   }

   operator int() { return _a; }
   operator double() { return _b; }
   operator std::string() { return _c; }
};

int main() {
  A object;
  int a = object;           // 1
  double b = object;        // 1.2
  std::string c = object;   // hello
}

也可以通过显示类型转换指定转换类型

int a = (int)object;                     // 1
double b = (double)object;               // 1.2
std::string c = (std::string)object;     // hello

如果隐式类型转换出现二义性,则会报错

short s = object;       // int ? double ?

可以使用显示类型转换

short s = (int)object;       // 1

C++11中,explicit可以修饰转换函数,修饰后的函数只能用于显示类型转换

class A {
  public:
   int _a;
   double _b;
   std::string _c;

   A() {
     _a = 1;
     _b = 1.2;
     _c = "hello";
   }

   explicit operator int() { return _a; }
   operator double() { return _b; }
   operator std::string() { return _c; }
};

int main() {
  int a = (int)object;            // (int)1
  short s = object;               // (double) 1.2
}

自定义函数实现显式类型转换

class A {
  public:
   int _a;
   double _b;
   std::string _c;

   A() {
     _a = 1;
     _b = 1.2;
     _c = "hello";
   }

   int to_int() {return _a; }
   double to_double { return _b };
   std::string to_string { return _c; }
};

int main() {
  A object;
  int a = object.to_int();              // 1
  double b = object.to_double();        // 1.2
  std::string c = object.to_string();   // hello
}

谨慎地使用隐式转换函数,最好选择仅被显式调用时才会执行的成员函数


edit Serein