image.png

多态的表现形式

  1. OO程序设计 - 虚函数:是运行时的多态,是动态绑定的,具体引用只能在运行时确定
  2. 函数重载:一名多用,是静态的多态,在编译时就能确定
  3. 类属多态:通过泛型编程template,传入不同类型,复用同一段代码

重载

函数重载

image.png

  1. 首先进行名空间的匹配
  2. 再进程参数匹配
  3. 所以不会进行返回值的匹配
  4. CompilerLinker共同决定

歧义控制

  • 顺序
  • 更好匹配
  • 窄转换
  1. 参数的个数和被调函数相等
  2. 参数类型可以匹配、隐式转换
  3. 如果没有一模一样的函数签名:找到最佳匹配函数。
    1. 参数double、float,传入的是int

有一个实参的匹配比其他参数更好,其他实参不比其他参数差

  • 完全匹配
  • 整形提升:bool转化为int,是更好的
  • 标准转化之前没有优劣比较,char 到unsigned char 和char 到double的匹配,一视同仁

窄转换:

操作符重载

image.png
操作符重载就是函数重载
image.png
image.png
在成员函数中,不需要强调自定义类型,因为this代表的就是自定义的类型
image.png

不可重载

.成员访问操作符
.*成员指针访问运算符
防止访问成员的功能被改变
::域操作符
?:条件操作符
存在跳转的功能,本身是控制流程的作用,操作符重载是函数调用,进行函数调用的过程中,所有的代码都要被执行,无法实现跳转。如果重载后,和语义有偏差,因此不能重载
sizeof参数是类型,不是变量或表达式,因此不能重载

双目操作符

类成员函数重载

image.png
第一个参数默认为this,必须为自定义类型

全局函数

image.png
=赋值运算符()函数调用运算符

  • []下标运算符->间接访问操作符:有特定的顺序,先对对象进行操作,再根据对象操作参数。但是全局函数重载无法保证顺序,所以不能全局函数重载
  • 操作符的第一个参数不是自定义类型本身
  • 遵守交换律
  • 单目操作符没有意义使用全局函数,单目操作符都可以作为类的成员函数进行重置

全局函数/成员函数的选择

image.png

  • 第一个参数是自定义对象:使用成员函数
  • 需要支持交换律:使用全局函数进行补充
  1. 单目运算符+只能声明为成员函数的(其他的都用全局函数):作为类的成员函数
  2. 双目运算符:作为类的友元函数
  3. 类型转换函数、操作符修改状态时:定义为成员函数
  4. 第一个参数有隐式类型转换:定义为友元函数,因为成员函数需要精准匹配
    1
    2
    3
    4
    5
    6
    7
    class CL{
    int count;
    public:
    CL(int i){...}//可以进行隐式类型转换
    friend CL operator+(int i,CL&a);
    friend CL operator+(CL&a,int i);
    }

永远不要重载 && 和 ||

image.png
第一行if借用了短路,如果重载,就是函数调用,如第3行和第4行if,两个参数都会运算,改变了短路行为,会出错

不要过度追求效率??

直接返回对象的拷贝。

        • / 直接返回对象的拷贝,效率不是很高,可以对于需要拷贝的进行返回值优化。不要一开始创建tmp对象,最后在**return**时创建tmp对象,直接返回,避免拷贝,优化代码

image.png

单目操作符

image.png
a++返回的是左值a = a + 1
++a返回的是右值 a+1
如何确定重载的是a++还是++a
image.png
int作用

  1. 区分函数:签名不同,函数名相同才可以重载
  2. int是哑元,不在函数中参与运行

return``*this返回当前对象

示例

image.png
<<两个参数:双目操作符的重载
为什么要return ostream的对象?支持链式调用cout <<d1<<

特殊操作符的重载

操作符 = 的重载

  1. 默认赋值操作符重载函数
  2. 逐个成员赋值
  3. 对含有对象成员的类,该定义是递归的
  4. 赋值操作符的重载不可以被继承:因为拷贝构造,派生出来的类有一些新的部分
  5. 返回引用类型:返回*this的引用,支持链式赋值
  6. this引用应该是非常量引用,返回出来的是作为右值进行计算
    1. a = b = c:不要求非常量引用
    2. (a = b).f():要求非常量引用
  7. 例一:
    1
    2
    3
    4
    5
    6
    7
    8
    class A;
    A b;
    A a = b;//对a调用拷贝构造函数(更重要的是构造,在构造对象时候调用,再进行赋值)
    A a(b); // 第2 3 行代码等价
    // 先调用构造函数再调用赋值操作符函数,效率比直接调用拷贝构造函数低

    A a,b; // 对a调用构造函数,因为调用过构造函数了,所以不能再调用拷贝构造函数了
    a = b;// 对a调用赋值操作符重载函数
    区分:判断被赋值的对象a有没有被构造过
  • 已经被构造过,调用赋值操作符重载函数
  • 未被构造过,调用拷贝构造函数
    1
    2
    3
    4
    5
    6
    A operator = (const A&){


    return *this; // 方便链式调用等号
    // 返回值可以做左值,也可以做右值。通过小括号改变优先级。
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class A {
    int x,y ;
    char *p ; // 额外申请的内存块,赋值的时候需要先释放
    public :
    A(int i,int j,char *s):x(i),y(j){
    p = new char[strlen(s)+ 1 ];
    strcpy(p,s);//进行拷贝,最后留一个\0
    }
    virtual ~A(){
    delete[] p;
    }
    A& operator = (A& a) {
    //赋值
    x = a.x;
    y = a.y;
    delete []p; // 防止内存泄漏
    // 1.如果自我赋值,会出错
    p = new char[strlen(a.p)+1]; // 重新开辟新的内存
    // 2.如果资源没有了 new char 会抛出异常,对象构造失败了,但是p已经被
    // 删掉了,对象本身存在,但是p被delete调了。
    // 指针还在,指针的内存已经没了,出现指针悬垂问题
    strcpy(p,a.p);
    return *this;//也会出现悬垂
    }//还有问题,就是赋值自身会出现问题
    };
    A a, b;
    a = b;//调用自己的复制

    //idle pointer,B被析构的时候会将p释放掉,导致p指向已经被释放掉的指针
    //Memory leak,A申请的区域可能没有办法被释放
    资源拷贝:先申请,再删除
    解决方法:
    1
    2
    3
    4
    5
    6
    7
    //更安全的拷贝,先new再delete
    char *pOrig = p;
    p = new char ...
    strcpy();
    delete pOrig;
    return *this;
    //自我赋值可以吗?可以,换了一块内存空间,没有内存泄露
  1. 注意:避免自我赋值(因为是相同的内存地址)
    1. Sample: class string
    2. s = s
      1. class {... A void f(A& a);...}
      2. void f (A& a1, A& a2)
      3. int f2(Derived &rd,Base& rb);

额外检测:证同测试

1
2
3
if(this == &a)
return *this; // 不需要赋值了
delete p;//48min - 50min
  1. Object identity
    1. Content
    2. Same memory location
    3. Object identifier
      1
      2
      3
      4
      5
      6
      class A{
      public:
      ObjectID identity() const;
      };
      A *p1,*p2;
      p1-> identity() == p2-> identity()

操作符[]的重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class string {
char *p;
public :
string(char *p1){
p = new char [strlen(p1)+ 1];
strcpy(p,p1);//#pragma warning(disable:4996)来屏蔽问题
}
// 参数是this、i
char& operator[] (int i){ // 可以声明为常成员函数:兼容常量类型string
return p[i]; //p[i] = *(p+i)
}
// 精确匹配高于类型转化
// 参数是 const this、i
const char operator[](int i) const{
return p[i];
}
//可以用两个重载函数吗?是可以的
virtual ~string() { delete[] p ; }
};
string s("aacd");
s[2] = 'b' ; // 是可以的,只会调用第一个,因为是精确匹配的
//第一个重载加上const可以使得const或者非const对象都可以调用
const string cs('const'); //只能调用常成员函数
cout << cs[0];
cs[0] = 'D'; // 如果没有第二个重载,则无法阻止这一行为
//const 版本不想被赋值(返回const的),
// 非const版本想要被赋值,之后再进行重载的时候就需要同时重载两个

多维数组 class Array2D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Array2D{
int n1, n2;
int *p;
public:
Array2D(int l, int c):n1(l),n2(c){
p = new int[n1*n2];
}
virtual ~Array2D() { delete[] p; }
};

int & Array2D::getElem(int i, int j) { ... }
//上面是实现高维数组
Array2D data(2,3);
data.getElem(1,2) = 0;
//target -> data[1][2]
//想法:化解为两次调用
data.operator[](1)[2];//int *operator[](int i) 一次偏移一行,转化成Array1D
data.operator[](1).operator[](2)

//问题:三维数组重载问题:重载一次降维一次,3D->2D等等,多个依次进行重载,重载之后返回对象
//代理对象:Array1D

class Array1D{
int *q;//一维降低到int *就行
Array1D(int *p){ q = p; }
int& operator[](j){
return q[j];
}
}

data[1][3]

  • 使用二级指针:浪费空间,多次寻址
  • 先解析第一个[]:告诉量的偏移应该是如何的。再解析第二个[]

int *operator[](int i)是有默认定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Array2D{
private:
int *p;
int num1, num2;
public:
class Array1D{//Surrogate 多维,proxy class 访问代理类,对于其他外部类没有作用
public:
Array1D(int *p) { this->p = p; }
// 构造函数中只有一个参数,可以允许该参数类型转化到该类
int& operator[] (int index) { return p[index]; }
const int operator[ ] (int index) const { return p[index]; }
private:
int *p;
};
Array2D(int n1, int n2) {
p = new int[n1 * n2];
num1 = n1;
num2 = n2;
}
virtual ~Array2D() {
delete [] p;
}
Array1D operator[](int index) {
return p + index * num2; //通过偏移量寻找
//return的值和int*相同,构造函数不能声明成显式构造函数。
}
//这里为什么是array1D?通过构造函数进行类型转换
const Array1D operator[] (int index) const {
return p+index*num2;
}
};
  • explicit不能执行隐式类型转化,只能显式调用

操作符()的重载

  • 函数调用

  • 类型转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Func {
    double para;
    int lowerBound , upperBound ;
    public:
    double operator()(double,int,int);
    };
    Func f; // 函数对象:可以作为参数传递到其他函数
    f(2.4, 0, 8);
    class Array2D{
    int n1, n2;
    int *p;
    public:
    Array2D(int l, int c):n1(l),n2(c){
    p = new int[n1*n2];
    }
    virtual ~Array2D() {
    delete[] p;
    }
    int& operator()(int i, int j){
    return (p+i*n2)[j];//优化getElement
    }
    };
  • 函数指针:在没有全局变量下,给定一个参数,返回值是确定的

  • 函数对象:对象是有状态的,可以作为泛型算法的实参

类型转换运算符()重载

  1. 基本数据类型
  2. 自定义类

减少自定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Rational {
public: Rational(int n1, int n2) {
n = n1;
d = n2;
}
// 由类型名称决定返回值类型,从而区分函数调用操作符
// 类型转化不能作为全局函数 -- 主体是本类的对象
operator double() { //类型转换操作符,语法特殊。
// 不能使用哑元,因为函数调用的参数个数是不确定的
return (double)n/d;
}
private:
int n, d;
};
//减少混合计算中需要定义的操作符重载函数的数量
Rational r(1,2);
double x = r;
x = x + r;//避免的double 和 rational 的全局函数重载,会自动全部转换为double
  • =不能全局重载,由于优先在类内匹配,首先匹配到的是编译器提供的默认版本
  • []不能全局重载
  • 类型转化不能全局重载
    1
    2
    3
    4
    //48min
    ostream f("abc.txt");
    if (f)
    //重载 数值型:如 int

-> 指针间接引用操作符重载

箭头操作符一般声明为const成员函数

1
2
3
4
5
6
7
8
A a;
a.f();
a->f();
a.operator->(f)
a.operator->()->f()
//重载时按一元操作符重载描述,这时,
// a.operator->() 返回一个指针(或者是已经重定义过->的对象)
// 返回支持箭头操作符的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class CPen {
int m_color;
int m_width;
public:
void setColor(int c){ m_color = c;}
int getWidth(){ return m_width; }
};
class CPanel {
CPen m_pen;
int m_bkColor;
public:
CPen* getPen(){return &m_pen;}
void setBkColor(int c){ m_bkColor =c;}
};
CPanel c;
c.getPen()->setColor(16); // 按照名称获得成员变量
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
//简单修改,可以返回一个对象内部对象的指针
class CPen {
int m_color;
int m_width;
public:
void setColor(int c){ m_color = c;}
int getWidth(){return m_width; }
};
class CPanel {
CPen m_pen;
int m_bkColor;
public:
CPen* operator ->(){ return &m_pen;}
CPen* getPen(){return &m_pen;}
void setBkColor(int c) { m_bkColor =c;}
};
CPanel c;
c->setColor(16); // 可以当成CPen指针使用
//等价于
//c.operator->()->setColor(16);
//c.m_pen.setColor(16)
c->getWidth();
//等价于
//c.operator->()->getWidth();
//c.m_pen.getWidth()
CPanel *p=&c;
p->setBkColor(10);
  • 名称是不能重载的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class A{
    public:
    void f();
    int g(double);
    void h(char);
    };
    void test(){
    A *p = new A;
    if(...){return ...}
    p->f();//如果出错可能会导致问题
    p->g(1.1);//返回值
    p->h('A');
    delete p; // 不一定会执行到该处,是多出口函数
    }
    // --------------------------------------------------------------------------
    //更好的管理A对象,不用在任何退出的地方写delete p
    void test()
    {
    AWrapper p(new A);
    p->f();//如果出错可能会导致问题
    p->g(1.1);//返回值
    p->h('A');
    delete p;
    // 对象的消亡就是资源的释放,资源和栈上的对象同声明周期了
    }
    //须符合compiler控制的生命周期
    class AWrapper{//不包含逻辑
    A* p;// ? T p; 支持多个类型
    public:
    AWrapper(A *p) { this->p = p;}
    ~AWrapper() { delete p;}
    A*operator->() { return p;} // 不需要管是对象还是指针了
    };//RAII 资源获取及初始化
    //函数返回,销毁局部指针的时候会直接删除
    RAII:资源和对象绑定
  1. 是可控的管理:申请资源和释放资源的时间点都是确定的
  2. 让资源和栈上的对象同生命周期,确保资源释放且只被释放一次

重载指针可以让对象p看起来和裸指针一样,不需要管是对象还是指针了
局限性:封装的资源必须和函数同生命周期了

new/delete 的重载image.png

重载 new

  1. void *operator new (size_t size, s...)
  2. 名:operator new
  3. 返回类型:void *
  4. 第一个参数:size_t(unsigned int)
    • 系统自动计算对象的大小,并传值给size
  5. 其他参数:可有可无
    • A *p = new (...) A,表示传给new的
  6. new的重载可以有多种
  7. 如果重载一个new,那么通过new动态创建该类的对象时将不再调用内置的(预定义的)new
  8. 允许进行全局重载,但是不推荐使用全局重载

![F)5I%]Q@IEKC~F%{M_KG12A.png](https://blog-bed0.oss-cn-hangzhou.aliyuncs.com/1670483573949-3fd16832-b4ce-4941-b674-d9abe3ab2a36.png)

1
2
3
4
5
6
7
8
9
10
11
if(size != sizeof(base))
return ::operator new (size);
//调用全局标准库的new进行size的分配,标准库的new永远是可以使用的
operator new;
new A[10];
operator new [];
void * operator new (size_t size, void*)//是不可以被重载的,标准库版本,指定了对象创建的位置
void * operator new (size_t size, ostream & log);//可以同时写入到日志
void * operator new (size_t size, void * pointer);
//定位new,placement new,被调用的时候,在指针给定的地方的进行new(可能预先已经分配好的),
分配比较快,长时间运行不被打断(不会导致内存不足)

定位new placement new:控制了内存的分配malloc

重载 delete

image.png

返回值总结

  • 返回值是右值的按值传递
  • 返回值是左值的按引用传递