C++的面向对象
C++面向对象
面向对象的本质
具体的一个事物就是对象,多个同类对象聚合成一个类,类包含数据和动作(成员函数)
三大特征:
- 封装:将具体的实现过程和数据封装成一个函数,只能通过接口访问,降低耦合性。
- 继承:子类继承父类的特征和行为,子类有父类的private方法,成员变量,子类可以重写父类的方法。但是如果用final就不能继承,不能重写修改。
- 多态:子类同一消息不同反应,基类指针呈现不同的表现方式,一般用虚函数来实现。
1 | virtual void makeSound() const = 0; //然后多个子类继承它的时候 |
重载,重写,隐藏的区别
- 重载:函数重载的意思就是,一个类里面有不同多个同名函数,但是入参个数和类型不一样,根据参数列表决定调用哪个函数。不关注函数返回类型。
- 隐藏:只要是和基类同名的成员函数,不管参数列表如何,都是会被子类隐藏起来,除非 son.Father::func(x); 显示进行调用。
- 重写:是子类对父类的一些virtual修饰的函数,进行函数体的重写,但是必须函数名,参数列表,和返回值类型都保持一致才可以了。
PS:隐藏是发生在编译时,重写是发生在运行时。
继承
继承的三种方式
有总共三种继承方式,public, private, protected。
- public继承:public的成员会保持public,protected的成员保持protected,private不可以访问。但是可以通过基类的方法去访问他们。
- procted继承:public的成员变成protected,protected成员不变,private还是一样。
- private继承:public和procted降级为private。
虚继承是什么
虚继承(virtual inheritance)是一种解决多重继承中的“菱形继承”问题的技术。在多重继承中,多个派生类继承自同一个基类时,可能会导致最派生类包含多个基类实例的问题。虚继承通过确保基类在派生类中只存在一个实例,解决了这个问题。
1 | class A { |
这种情况下,D内存空间, 有B和C,而B和C又有各一份A,所以最后很容易出现二义性问题。
虚继承的底层是,是通过一个虚基类指针机制,B,C虚继承A,并不是深拷贝A到自己内存布局里面,而是有一个虚基类指针指向A。
1 | #include <iostream> |
这个时候可以看到运行结果是:
1 | A's constructor |
也就是说A只有一份。
如果把上文的virtual public去掉则是,运行结果则是:
1 | A's constructor |
也就是A有两份。
多重继承的常见问题及避免方法
菱形继承问题:
对于多重继承而言,base1 -> base2, 同时 base1 -> base3,那么有一个子类 base4 同时继承了base2,base3,当他给base1的成员赋值的时候会出现问题。也就是变量名冲突的问题。
解决办法:
这个冲突变量名,给明确写出,比如 给这个base1的变量设置为 base2::var1
或者使用虚拟继承的方式
构造函数问题:
多重继承可能会导致构造函数和析构函数的调用顺序变得复杂,尤其是在存在虚基类时。
1 | class D : public B, public C { |
明确调用顺序
析构函数问题:
需要将基类的析构函数声明为虚函数,不然无法调用到多层析构函数。
那是为什么呢?
首先要明确一个问题?为什么大部分用法都是
1 | Base* B=new Derived() |
因为这里涉及到了一个多态的用法,假如有一个
1 | print(Base* ptr){ |
由于base类下有各种子类,每个子类实现printInfo的方式不同,这样的话就可以运行时动态绑定,让ptr打印不同的子类的printInfo函数,前提是base类这个函数是虚函数,且每个子类对该函数重写。
接着在这种用法下,是怎么实现动态绑定的,和调用不同的虚函数的?
在每个子类中都有虚函数表,printInfo()函数这个时候指代Derived::PrintInfo()函数,通过这个映射关系就会实现动态绑定,如果实在要调用基类函数,可以ptr->Base::PrintInfo()显式的强制的调用!
接着在这种用法下,如果不把析构函数声明为virtual,会怎么办?
回到问题,如果B指针的析构函数不是虚函数,当delete的时候,delete操作是直接静态地默认绑定了基类的析构函数。只有当B指针的析构函数是虚函数的时候,delete操作会通过虚函数表先一步一步析构子类,最后再析构基类。
代码维护和可读性:
组合优于继承:在很多情况下,使用组合(has-a 关系)而非继承(is-a 关系)来实现代码的复用和模块化。
1 | class Engine { |
单继承和多继承的虚函数表结构
无论是单继承,还是多继承,对于子类而言,他有多少基类就有多少个vptr指向不同的虚函数表。且不说单继承的场景,说说多继承吧。
对于一个继承Base1, Base2的Derived子类
他会有两个vptr指向两个虚函数表格,如果无论是基类还是子类都有print函数。
- 那么对于子类的虚函数表格1,他的print函数会覆盖原有的print() -> Base1::print() 成为 print() -> Derived::print()
- 对于虚函数表格2,他的print函数会覆盖原有的print() -> Base2::print() 成为 print() -> Derived::print()。
虚函数
纯虚函数和虚函数区别
他们可以出现在同一个类了,含纯虚函数的叫抽象基类。
虚函数可以直接使用,但是纯虚函数只能在子类实现了才能够用。
1 | virtual void func(int x) = 0; |
析构函数的场景最好都是虚函数,也可以定义为纯虚函数,但是这样的基类是无法实例化了。
虚函数实现机制
- 虚函数表存放的内容:类的虚函数的地址。
- 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
- 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
- 虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针 vptr,来指向类的虚函数表 vtable。虚函数表vtable只有一个,且在编译期间创建
虚函数的作用
是为了运行时多态:运行时多态也称动态绑定,在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
而运行时多态通过提供一个通用的接口来简化代码、减少重复、支持开闭原则以及实现动态绑定,从而使程序设计更加灵活和可扩展。这些优点使得运行时多态在面向对象编程中非常重要和有用。
构造函数和析构函数是否定义成虚函数
析构函数最好还是定义为虚函数,因为基类指针引用绑定子类的对象空间的时候,如果调用的是基类的析构函数,则只能释放掉基类成员的空间,子类的成员则内存泄漏。
构造函数的话,因为虚函数表是在对象空间里的,构造函数是创建该空间的,则是有点前后矛盾了。所以一般没有这样做。
多态的实现方法
1 | #include <iostream> |
其实调用的是子类的虚函数,这种情况是虚函数表起了很大的作用。
存在虚函数的类,都有一个虚函数表,由一个vptr指向,该表存在于对象内存空间里。
所以基类指针,指向的是子类的对象空间,所以遍历的是子类对象空间里的虚函数表。
但是如果基类指针指向的子类对象空间,是用的基类的方法,因为看齐的是指针类型。
子类指针不能逆向指向父类的空间对象。
拷贝与构造
类对象的初始化顺序
这是一个十分冗长的过程
- 首先要调用的还是其继承的父类的构造函数。
- 按照继承列表,来逐个调用基类的构造函数。
- 但是如果有虚继承,则优先调用构造函数。
- 然后按照类里面成员,根据成员变量声明顺序,依次调用成员变量的构造函数如果有的。
- 最后执行子类自身的构造函数。
析构函数则是完全相反的顺序进行。
如果有静态变量则在main函数执行之前他们就是已经初始化好了。
当然如果构造函数内部进行初始化就和代码逻辑有关了。
如何禁止对象的拷贝
声明一个基类,然后将拷贝构造函数和赋值运算符重载设置为private。然后使用子类去继承该基类,那么子类的成员函数和友元函数是无法进行拷贝操作。 或者直接使用 = delete 删除拷贝构造函数,和赋值运算符重载。
初始化列表为什么更高效
如果在构造函数进行赋值,会首先调用默认的构造函数初始化,然后再进行赋值,相当于两次操作。但是初始化列表则只有一次操作,对象初始化时成员一并初始化,且方便编译器对其进行优化。这里有个点在于,写了个构造函数并不妨碍对象先调用默认构造函数。
浅拷贝和深拷贝的区别
- 深拷贝,即新拷贝出的对象和前者内容虽然一样,但是内存空间不一样。
- 浅拷贝,即新拷贝出的对象和前者内容一样,其实空间也一样。
浅拷贝有个坏处,坏处在于如果前者被删除,内存空间被释放,会影响到另一个对象。
编译默认拷贝函数大部分都是浅拷贝,浅拷贝会有很多隐形问题。
如何禁止构造函数的使用
1 | calss A { |
这样就可以禁止了构造函数,即使对其使用私有,private,类内部成员和友元还是可以访问,无法彻底禁止。
什么是类的默认构造函数
什么时候编译器不会提供一个默认的构造函数呢?
如果显示了声明构造函数了。
大部分情况下编译器都会为类合成一个默认构造函数。
但是如果类内部有复合的数据结构,则最好别依赖编译器提供的默认构造函数。
1 | #include <iostream> |