# Lecture12 - 多态
& 引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 & 引用名 = 目标变量名;
return *this 返回的是当前对象的克隆或者本身(若返回类型为 A, 则是拷贝, 若返回类型为 A&, 则是本身 )。return this 返回当前对象的地址(指向当前对象的指针)
# 操作符重载
# 函数重载
- 名同、参数不同,返回值不同没有用的:参数顺序、参数类型匹配 (找到最佳匹配)
- 静态绑定
# 歧义控制
最佳匹配:
- 原则一:这个匹配每一个参数不必其他的匹配更差
- 原则二:这个匹配有一个参数更精确匹配
整型提升:标准转化都是一视同仁的(char to unsigned char, char to double)
窄转换:是被允许的,把表数范围大的 赋值给表数范围小的叫做窄转换,特点是 不安全 不是自动的 可能会发生精度丢失 需要进行强制类型转换
至少要包含一个用户自定义的类型
# 可重载的操作符
操作符的重载本质上是函数的调用,不能够改变原来的语法
不可以重载的操作符:
.
(成员访问操作符)、
.*
(成员指针访问运算符,如下)、
::
(域操作符)、
?:
(条件操作符)、
sizeof
也不重载
- 原因:前两个为了防止类访问出现混乱
- :: 后面是名称不是变量
- ?: 条件运算符是跳转和流程控制,重载之后会出现理解的偏差
- 格式:
<ret type>operator #(<arg>)
- this: 隐含,必然是第一个参数
全局函数要有两个参数
?不懂
注意: =
、 ()
、 []
、 ->
不可以作为全局函数重载
- 大体上来讲,C++ 一个类本身对这几个运算符就已经有了相应的解释了。
- 如果将这四种符号进行友元全局重载,则会出现一些冲突
- 下标和箭头运算符为什么?有保留调用顺序,我们希望能保留原来的顺序,而全局不能要求,而成员函数的 this 就可以解决这个问题
- 参考
需要支持一些交换律,全局函数可以作为补充
全局函数作为补充:
- 单目运算符、类型转换函数最好重载为类的成员函数(不存在上图出现的情况)(单目操作不需要补充)
- 双目运算符最好重载为类的友元函数
返回什么值,由应用场景决定,返回右值的就要按值传递,临时变量。
四则运算大多数都是右值。
其他大多数都是左值。
prefix 和 postfix 是不一样的。
prefix & 返回引用
postfix 返回值 dummy argument 传值没有用,仅作为区分
# =
如果程序员没有提供等号的操作符重载,编译器会提供默认的赋值操作符重载函数。
A a,b; | |
a = b; // 赋值操作符 | |
A b; | |
A a = b; // A a (b); 拷贝构造函数 |
必须同时有赋值操作符和拷贝构造函数。
一个对象只能被初始化一次,所以需要有赋值。
拷贝构造函数效率更高,且会被经常使用(函数的传参)。
A & operator = (const A &){ | |
return *this; | |
} |
等号是右结合的
a = b = c
(a = b) = c
可以按左值 -》传递引用
内存可能不够?
这段代码写得不好,应该先申请,再释放。
temp = p; | |
p = new char[strlen(a.p) + 1]; | |
strcpy(p, a.p); | |
delete temp[]; |
上面那个代码如果出现 “自我赋值”,会出现错误,把数据删除了。
解决方法 1:做一个额外的检测。
if(this == &rhs) return this; | |
// rhs right hand side | |
// 加上正同测试 | |
// 以上只是一个很简单的操作 |
解决方法 2:先申请再释放可以解决。
# []
可以有两个重载函数吗?可以。
在前文歧义控制的最佳匹配中,可以解释,为什么 s 的构建会匹配第一个重载函数,cs 则会调用第二个重载函数。因为是” 更精确的匹配 “
string s("aacd"); | |
s[2] = 'b' ; | |
// 第一个重载加上 const 可以使得 const 或者非 const 对象都可以调用 | |
const string cs('const'); | |
cout << cs[0]; | |
const cs[0] = 'D';//const 版本不想被赋值 (返回 const 的),非 const 版本想要被赋值,之后再进行重载的时候就需要同时重载两个 |
用于内存资源、多维数组的管理。
wrapper
隐式转换,explicit 禁用。
# ()
函数调用、类型转换
问题:为什么禁止在类外禁止重载赋值操作符?
- 如果没有类内提供一个赋值操作符,则编译器会默认提供一个类内的复制操作符
- 查找操作符优先查找类内,之后查找全局,所以全局重载赋值操作符不可能被用到
# ->
CPanel 中有一个 CPen,需要修改 pen 的颜色
CPanel c; | |
c.getPen() -> SetColor(16); | |
// 箭头重定义之后 | |
c->setColor(16); |
重载时按照一元操作符重载描述
必须返回指针类型?不是,还可以返回一些能够进行箭头操作的引用。
# prevent memory leak
一定不会出现内存泄漏吗?
void test(){ | |
if() return; //return 的时候就没有办法 delete 了 | |
throw; // 以上是 | |
delete p; | |
} |
多出口程序的处理出现了问题。
用 RAII 进行处理。将
A *p = new A;
写成AWrapper p(new A);
,资源和对象同生命周期,栈上的对象一定会消亡,不需要管理多出口问题,且是可控的。将箭头操作符重载
A *operator -> () { return p;}
使得申请的资源暴露,就像普通的指针一样使用。所有类型都需要写一个 wrapper 吗?
不需要,利用泛型 / 模板解决。
如此用 wrapper 封装,有什么局限性?
资源和对象必须同生命周期。
解决方法:例如,shared_ptr
# new、delete
new 和 delete 的操作步骤
new
- 获得一块内存空间
- 调用构造函数
- 返回一个正确的指针
delete
- 调用析构函数
- 确定指向分配空间的指针
- 归还内存空间
系统自助管理的缺点:系统频繁调用系统的存储管理,影响效率。并且会造成内存碎片。
重载 new 和 delete 之后:程序自身管理内存,提高效率
重载过的 new 和 delete 是静态成员:默认是 static,不能调用非静态函数
返回类型:void * ,不知道 new 什么类型
返回参数:第一个参数是固定的,其他参数可有可无。系统知道要申请的大小,参数 size_t 是给程序员用的。
new 是可以被继承的。
定位 new A *p = new (place) A
可以自己在栈上申请一块内存,反复新建和销毁 A。好处是,效率高(?