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,调用相应成员函数的时候才会特化