C++ 面向对象

两种头文件引入方式的区别

<>只会在编译器配置的头文件路径和系统路径中寻找,而""会先在当前目录寻找,然后去找配置路径或系统路径

引用

为什么引用不可变?因为实际上是对引用的对象进行赋值。引用和原对象的地址和大小相同(假象),传值和传引用不能区分重载,而const可以

const标记成员函数的必要性

如果用户定义了一个常量对象,那就只能调用标记为const的成员函数。换句话说,如果忘记了标记,那就会导致编译出错。

const为什么可以区分函数重载

一般情况下,对于非常量对象,成员函数加不加const都可以调用。但是如果同时存在constnon-const的函数,C++规定non-const-object只会调用non-const版本的函数。

但是函数参数里的const只能区分引用和指针的重载,而且优先调用与实参匹配的版本

1
2
3
4
5
6
7
8
9
void f(int& a){}
void f(const int& a){} // OK

void f(int* a){}
void f(const int* a){} // OK
void f(int* const a){} // error C2084: function 'void f(int *)' already has a body

void f(int a){}
void f(const int a){} // error C2084: function 'void f(int)' already has a body

关于传值和传引用的重载

定义的时候不会报错,直到调用时才会发现冲突

1
2
3
4
5
6
void f(int a){}
void f(int& a){} // OK
...
f(0); // OK
int a = 0;
f(a); // error C2668: 'f': ambiguous call to overloaded function

什么时候同时需要const和non-const版本的函数

通常情况下各种get函数都需要写两个版本的,尤其是返回指针和引用的时候,这就决定了外部是否可以用这个返回的指针或引用修改对象状态。

STL里的basic_string对于常量字符串进行了优化,采用写时复制策略(Copy-On-Write),相同的字符串会引用同一片内存区域,直到需要改变字符串内容时才会开辟新内存空间以供修改。重载的operator []就需要有constnon-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,且只需要一个实参)则也可以尝试转换。如果两种转换并存就会冲突(把此类转换成其他类还是把其他类转换成此类),除非给这样的ctorexplicit标记。

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函数,从而父类可以用这些实例创建子类

    原型模式

    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
    enum class ObjectType {
    Objec1
    };

    class ObjectBase {
    public:
    static ObjectBase* GetObject(ObjectType type); // 遍历_prototypes数组,调用GetType()比较type,然后调用其Clone

    virtual ObjectType GetType() const = 0;
    virtual void Func() = 0;
    virtual ~ObjectBase() {}

    protected:
    static void AddPrototype(ObjectBase*); // 添加实例到_prototypes数组

    virtual ObjectBase* Clone() = 0; // 子类需要返回自身的实例

    private:
    static ObjectBase* _prototypes; // 这只是简单示例,实际可以用其它数据结构保存,比如map
    };
    ObjectBase* ObjectBase::_prototypes; // static要在类外定义

    class Object1 : ObjectBase{
    public:
    ObjectType GetType() const override { return ObjectType::Objec1; }
    void Func() override; // 子类实现自己的功能
    ~Object1();

    protected:
    Object1(char dummy); // 子类为Clone用的ctor
    ObjectBase* Clone() { return new Object1(0); }

    private:
    static Object1 _prototype;
    Object1() { AddPrototype(this); } // 子类为静态实例准备的ctor,上传实例到父类
    };
    Object1 Object1::_prototype; // static要在类外定义

成员模板

模板类有T,成员函数又声明typename U,调用相应成员函数的时候才会特化