C++ 面向对象
两种头文件引入方式的区别
<>
只会在编译器配置的头文件路径和系统路径中寻找,而""
会先在当前目录寻找,然后去找配置路径或系统路径
引用
为什么引用不可变?因为实际上是对引用的对象进行赋值。引用和原对象的地址和大小相同(假象),传值和传引用不能区分重载,而const
可以
const标记成员函数的必要性
如果用户定义了一个常量对象
,那就只能调用标记为const
的成员函数。换句话说,如果忘记了标记,那就会导致编译出错。
const为什么可以区分函数重载
一般情况下,对于非常量对象,成员函数加不加const
都可以调用。但是如果同时存在const
和non-const
的函数,C++规定non-const-object
只会调用non-const
版本的函数。
但是函数参数里的const
只能区分引用和指针的重载,而且优先调用与实参匹配的版本
1 | void f(int& a){} |
关于传值和传引用的重载
定义的时候不会报错,直到调用时才会发现冲突
1 | void f(int a){} |
什么时候同时需要const和non-const版本的函数
通常情况下各种get
函数都需要写两个版本的,尤其是返回指针和引用的时候,这就决定了外部是否可以用这个返回的指针或引用修改对象状态。
STL里的basic_string
对于常量字符串进行了优化,采用写时复制策略(Copy-On-Write),相同的字符串会引用同一片内存区域,直到需要改变字符串内容时才会开辟新内存空间以供修改。重载的operator []
就需要有const
和non-const
两个版本,前者就不会COW,后者才会,从而提高了效率。
关于std::string
的细节,其实采用了引用计数,在真正的字符串内存前面多开辟一块内存用来保存引用计数,比如data
是字符串指针,那么引用计数是data[-1]
。在拷贝构造和拷贝赋值时时引用增加,析构和修改时减少,计数为0时释放字符串内存。
具体细节详见:(转)C++——std::string类的引用计数 - HelloCsz - 博客园 (cnblogs.com)
什么时候操作符要重载为全局的
操作符重载可全局也可以为成员函数,但是因为操作符是作用在左操作对象上的,如果左操作对象的类不是我们写的,改不了代码,自然就不能添加成员函数的重载。还有当左操作数只是基本类型如double
之类的,当然也只能使用全局的重载。
operator new
尽量不要重载,除非清楚地知道带来的影响
操作符返回值还是引用
如果考虑连续使用,比如a += b += c
那就需要返回引用
operator=一定要自我检测
拷贝赋值的步骤:1.自我检测 2.释放原内存 3.开辟新空间 4.复制内容
如果不进行检测,将会释放自己的内存,内容丢失,后续步骤也就无法完成。
operator Type()
类型转换没有返回值类型,因为就是要转换为对应的类型。
隐式类型转换的规则:先尝试找运算符重载,没有则找类型转换函数,如果是有non-explicit-one-argument-ctor
(没有指定explicit,且只需要一个实参)则也可以尝试转换。如果两种转换并存就会冲突(把此类转换成其他类还是把其他类转换成此类),除非给这样的ctor
加explicit
标记。
operator ->
返回一个指针,之后还是会调用指针的->
,可以直接返回&(operator *())
。
operator new 和 operator new[]
表达式new
:1.operator new()
2.static_cast
3.ctor
。其中的operator new
调用了malloc
,可以被重载
重载时的第一个参数类型为size_t
,返回void*
,如果还添加其它参数则称为placement new
,可以在new的时候额外传入参数,比如重载为void* operator new(size_t size, char c, float a)
,调用为new('c', 1.1) MyClass
。
重载可以全局,也可以是类成员函数或者静态成员函数,如果都有,可以用::new
来强制调用全局版本的。
operator new[](size_t size)
,其中size
是数组中所有元素的大小之和
operator delete 和 operator delete[]
表达式delete
:1.dtor
2.operator delete()
。其中的operator delete
调用了free
,可以被重载
重载时第一个参数类型为void*
,返回void
,可以添加其他参数。但是表达式delete
并不能在使用时传入额外参数,比如重载void operator delete(void* ptr, char c, float a)
,但是却不能delete('c', 1.1) myObj
,所以只能调用delete
,只有参数为void*
的版本会被调用,有额外参数的重载不会被调用。
那这些重载有什么用呢?
如果重载了placement new
,也就是operator new
加了额外参数,而且在new
的时候传入了对应的额外参数,那么就会调用到这些版本的operator new
,如果在接下来的ctor
中发生了异常,将会调用含有对应参数的operator delete
来进行处理。所以如果没有重载对应参数的operator delete
,编译器会发出警告,但编译不会出错,这时编译器认为编写者放弃处理异常时的内存问题。(不知道为何手动抛出异常也没能成功调用)
重载可以全局,也可以是类成员函数或者静态成员函数,如果都有,可以用::new
来强制调用全局版本的。
为什么在成员函数里可以直接访问其它同类对象的私有数据
这里用friend
来解释:友元类和友元函数都可以直接访问类的私有数据,而同类型的对象之间就是互为友元的。
C++ Big-Three
包括拷贝构造copy-ctor
、拷贝赋值copy-operator=
和析构dtor
。右指针的类一定要自行实现,因为编译器提供的默认版本是浅拷贝,只是简单的按位复制,不仅会因为共享内存而导致结果不正确,还会在析构时重复释放内存。
如果考虑继承,dtor
还要设为virtual
的。
组合与继承时的ctor和dtor调用顺序
构造先后顺序(由内而外):父类-组合成员-自己
析构先后顺序(由外而内):自己-组合成员-父类
private成员访问
父类本来就是
private
的成员会被子类继承,只是子类无法访问父类里原本不是
private
,只是因为private
继承而导致的private
成员可以被子类访问
发生动态绑定的条件
1.是用指针或引用调用 2.是向上转型 3.是虚函数
动态绑定简化代码为(*(pObj->vptr)[n])(pObj)
,对应汇编代码call ptr [idx]
,而静态绑定就是在编译期确定了调用的函数地址,汇编指令为call 函数地址
。
如果在构造函数和析构函数里调用了虚函数,其实也是静态绑定,为什么呢?子类转换成父类时,函数表怎么转化的?C++幕后故事(四)-- 虚函数的魅力 - 知乎 (zhihu.com)
常用的继承与组合
Adapter:利用已有的类,改装接口后就实现了另一个类
Delegation:用指针组合,与Adapter类似
TemplateMethod:父类实现函数主要框架,只在函数的关键地方提一个虚函数出来让子类自定义
Observer:Delegation(Subject有Observer的指针)+Inheritance(Observer可被继承)
Prototype:Delegation+Inheritance。子类有静态实例,在私有
ctor
中上传此实例给父类(因为只上传一次,所以还需要其它的ctor
),统一提供clone
函数,从而父类可以用这些实例创建子类
成员模板
模板类有T
,成员函数又声明typename U
,调用相应成员函数的时候才会特化