第八课 多态
多态的表现形式
- OO程序设计 - 虚函数:是运行时的多态,是动态绑定的,具体引用只能在运行时确定
- 函数重载:一名多用,是静态的多态,在编译时就能确定
- 类属多态:通过泛型编程
template
,传入不同类型,复用同一段代码
重载
函数重载
- 首先进行名空间的匹配
- 再进程参数匹配
- 所以不会进行返回值的匹配
- 由
Compiler
和Linker
共同决定
歧义控制
- 顺序
- 更好匹配
- 窄转换
- 参数的个数和被调函数相等
- 参数类型可以匹配、隐式转换
- 如果没有一模一样的函数签名:找到最佳匹配函数。
- 参数double、float,传入的是int
有一个实参的匹配比其他参数更好,其他实参不比其他参数差
- 完全匹配
- 整形提升:bool转化为int,是更好的
- 标准转化之前没有优劣比较,char 到unsigned char 和char 到double的匹配,一视同仁
窄转换:
操作符重载
操作符重载就是函数重载
在成员函数中,不需要强调自定义类型,因为this
代表的就是自定义的类型
不可重载
.
成员访问操作符.*
成员指针访问运算符
防止访问成员的功能被改变::
域操作符?:
条件操作符
存在跳转的功能,本身是控制流程的作用,操作符重载是函数调用,进行函数调用的过程中,所有的代码都要被执行,无法实现跳转。如果重载后,和语义有偏差,因此不能重载sizeof
参数是类型,不是变量或表达式,因此不能重载
双目操作符
类成员函数重载
第一个参数默认为this
,必须为自定义类型
全局函数
=
赋值运算符()
函数调用运算符
[]
下标运算符->
间接访问操作符:有特定的顺序,先对对象进行操作,再根据对象操作参数。但是全局函数重载无法保证顺序,所以不能全局函数重载- 操作符的第一个参数不是自定义类型本身
- 遵守交换律
- 单目操作符没有意义使用全局函数,单目操作符都可以作为类的成员函数进行重置
全局函数/成员函数的选择
- 第一个参数是自定义对象:使用成员函数
- 需要支持交换律:使用全局函数进行补充
- 单目运算符+只能声明为成员函数的(其他的都用全局函数):作为类的成员函数
- 双目运算符:作为类的友元函数
- 类型转换函数、操作符修改状态时:定义为成员函数
- 第一个参数有隐式类型转换:定义为友元函数,因为成员函数需要精准匹配
1
2
3
4
5
6
7class CL{
int count;
public:
CL(int i){...}//可以进行隐式类型转换
friend CL operator+(int i,CL&a);
friend CL operator+(CL&a,int i);
}
永远不要重载 && 和 ||
第一行if
借用了短路,如果重载,就是函数调用,如第3行和第4行if
,两个参数都会运算,改变了短路行为,会出错
不要过度追求效率??
直接返回对象的拷贝。
- / 直接返回对象的拷贝,效率不是很高,可以对于需要拷贝的进行返回值优化。不要一开始创建tmp对象,最后在
**return**
时创建tmp对象,直接返回,避免拷贝,优化代码
- / 直接返回对象的拷贝,效率不是很高,可以对于需要拷贝的进行返回值优化。不要一开始创建tmp对象,最后在
单目操作符
a++
返回的是左值a = a + 1 ++a
返回的是右值 a+1
如何确定重载的是a++还是++aint
作用
- 区分函数:签名不同,函数名相同才可以重载
int
是哑元,不在函数中参与运行
return``*this
返回当前对象
示例
<<
两个参数:双目操作符的重载
为什么要return ostream
的对象?支持链式调用cout <<d1<<
特殊操作符的重载
操作符 = 的重载
- 默认赋值操作符重载函数
- 逐个成员赋值
- 对含有对象成员的类,该定义是递归的
- 赋值操作符的重载不可以被继承:因为拷贝构造,派生出来的类有一些新的部分
- 返回引用类型:返回*this的引用,支持链式赋值
- this引用应该是非常量引用,返回出来的是作为右值进行计算
- a = b = c:不要求非常量引用
- (a = b).f():要求非常量引用
- 例一:区分:判断被赋值的对象a有没有被构造过
1
2
3
4
5
6
7
8class A;
A b;
A a = b;//对a调用拷贝构造函数(更重要的是构造,在构造对象时候调用,再进行赋值)
A a(b); // 第2 3 行代码等价
// 先调用构造函数再调用赋值操作符函数,效率比直接调用拷贝构造函数低
A a,b; // 对a调用构造函数,因为调用过构造函数了,所以不能再调用拷贝构造函数了
a = b;// 对a调用赋值操作符重载函数
- 已经被构造过,调用赋值操作符重载函数
- 未被构造过,调用拷贝构造函数
1
2
3
4
5
6A 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
30class 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;
//自我赋值可以吗?可以,换了一块内存空间,没有内存泄露
- 注意:避免自我赋值(因为是相同的内存地址)
- Sample: class string
- s = s
class {... A void f(A& a);...}
void f (A& a1, A& a2)
int f2(Derived &rd,Base& rb);
额外检测:证同测试
1 | if(this == &a) |
- Object identity
- Content
- Same memory location
- Object identifier
1
2
3
4
5
6class A{
public:
ObjectID identity() const;
};
A *p1,*p2;
p1-> identity() == p2-> identity()
操作符[]的重载
1 | class string { |
多维数组 class Array2D
1 | class Array2D{ |
data[1][3]
- 使用二级指针:浪费空间,多次寻址
- 先解析第一个
[]
:告诉量的偏移应该是如何的。再解析第二个[]
int *operator[](int i)
是有默认定义的
1 | class Array2D{ |
explicit
不能执行隐式类型转化,只能显式调用
操作符()的重载
函数调用
类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class 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 | class Rational { |
=
不能全局重载,由于优先在类内匹配,首先匹配到的是编译器提供的默认版本[]
不能全局重载- 类型转化不能全局重载
1
2
3
4//48min
ostream f("abc.txt");
if (f)
//重载 数值型:如 int
-> 指针间接引用操作符重载
箭头操作符一般声明为const成员函数
1 | A a; |
1 | class CPen { |
- 名称是不能重载的RAII:资源和对象绑定
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
34class 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 资源获取及初始化
//函数返回,销毁局部指针的时候会直接删除
- 是可控的管理:申请资源和释放资源的时间点都是确定的
- 让资源和栈上的对象同生命周期,确保资源释放且只被释放一次
重载指针可以让对象p看起来和裸指针一样,不需要管是对象还是指针了
局限性:封装的资源必须和函数同生命周期了
new/delete 的重载
重载 new
void *operator new (size_t size, s...)
- 名:
operator new
- 返回类型:void *
- 第一个参数:
size_t(unsigned int)
- 系统自动计算对象的大小,并传值给size
- 其他参数:可有可无
A *p = new (...) A
,表示传给new的
- new的重载可以有多种
- 如果重载一个new,那么通过new动态创建该类的对象时将不再调用内置的(预定义的)new
- 允许进行全局重载,但是不推荐使用全局重载
![F)5I%]Q@IEKC~F%{M_KG12A.png](https://blog-bed0.oss-cn-hangzhou.aliyuncs.com/1670483573949-3fd16832-b4ce-4941-b674-d9abe3ab2a36.png)
1 | if(size != sizeof(base)) |
定位new placement new
:控制了内存的分配malloc
重载 delete
返回值总结
- 返回值是右值的按值传递
- 返回值是左值的按引用传递