C++的面向对象

C++面向对象

面向对象的本质

具体的一个事物就是对象,多个同类对象聚合成一个类,类包含数据和动作(成员函数)

三大特征:

  • 封装:将具体的实现过程和数据封装成一个函数,只能通过接口访问,降低耦合性。
  • 继承:子类继承父类的特征和行为,子类有父类的private方法,成员变量,子类可以重写父类的方法。但是如果用final就不能继承,不能重写修改。
  • 多态:子类同一消息不同反应,基类指针呈现不同的表现方式,一般用虚函数来实现。
1
2
virtual void makeSound() const = 0; //然后多个子类继承它的时候
void makeSound() const override {} //自行决定自己的反应。

重载,重写,隐藏的区别

  • 重载:函数重载的意思就是,一个类里面有不同多个同名函数,但是入参个数和类型不一样,根据参数列表决定调用哪个函数。不关注函数返回类型。
  • 隐藏:只要是和基类同名的成员函数,不管参数列表如何,都是会被子类隐藏起来,除非 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
2
3
4
5
6
7
8
class A {
public:
    int value;
    void show() { std::cout << "A" << std::endl; }
};
class B : public A {};
class C : public A {};
class D : public B, public C {};

这种情况下,D内存空间, 有B和C,而B和C又有各一份A,所以最后很容易出现二义性问题。
虚继承的底层是,是通过一个虚基类指针机制,B,C虚继承A,并不是深拷贝A到自己内存布局里面,而是有一个虚基类指针指向A。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
class A {
public:
    int value;
    void show() { std::cout << "A" << std::endl; }
};
class B : virtual public A {
public:
    void setBValue(int v) { value = v; }
};
class C : virtual public A {
public:
    void setCValue(int v) { value = v; }
};
class D : public B, public C {
public:
    void setDValue(int v) { value = v; }
    void showDValue() { std::cout << value << std::endl; }
};
int main() {
D d;
}

这个时候可以看到运行结果是:

1
2
3
4
A's constructor
B's constructor
C's constructor
D's constructor

也就是说A只有一份。
如果把上文的virtual public去掉则是,运行结果则是:

1
2
3
4
5
A's constructor
B's constructor
A's constructor
C's constructor
D's constructor

也就是A有两份。

多重继承的常见问题及避免方法

菱形继承问题:

对于多重继承而言,base1 -> base2, 同时 base1 -> base3,那么有一个子类 base4 同时继承了base2,base3,当他给base1的成员赋值的时候会出现问题。也就是变量名冲突的问题。

解决办法:

这个冲突变量名,给明确写出,比如 给这个base1的变量设置为 base2::var1

或者使用虚拟继承的方式

构造函数问题:

多重继承可能会导致构造函数和析构函数的调用顺序变得复杂,尤其是在存在虚基类时。

1
2
3
4
class D : public B, public C {
public:
    D(int x) : A(x), B(x), C(x) { std::cout << "D's constructor" << std::endl; }
};

明确调用顺序

析构函数问题:

需要将基类的析构函数声明为虚函数,不然无法调用到多层析构函数。

那是为什么呢?

首先要明确一个问题?为什么大部分用法都是

1
2
Base* B=new Derived()
Derived* D=new Derived() //而不是

因为这里涉及到了一个多态的用法,假如有一个

1
2
3
print(Base* ptr){
ptr->printInfo();
}

由于base类下有各种子类,每个子类实现printInfo的方式不同,这样的话就可以运行时动态绑定,让ptr打印不同的子类的printInfo函数,前提是base类这个函数是虚函数,且每个子类对该函数重写。
接着在这种用法下,是怎么实现动态绑定的,和调用不同的虚函数的?

在每个子类中都有虚函数表,printInfo()函数这个时候指代Derived::PrintInfo()函数,通过这个映射关系就会实现动态绑定,如果实在要调用基类函数,可以ptr->Base::PrintInfo()显式的强制的调用!

接着在这种用法下,如果不把析构函数声明为virtual,会怎么办?

回到问题,如果B指针的析构函数不是虚函数,当delete的时候,delete操作是直接静态地默认绑定了基类的析构函数。只有当B指针的析构函数是虚函数的时候,delete操作会通过虚函数表先一步一步析构子类,最后再析构基类。

代码维护和可读性:

组合优于继承:在很多情况下,使用组合(has-a 关系)而非继承(is-a 关系)来实现代码的复用和模块化。

1
2
3
4
5
6
7
8
9
10
class Engine {
public:
    void start() { std::cout << "Engine start" << std::endl; }
};
class Car {
private:
    Engine engine; // Composition
public:
    void start() { engine.start(); }
};

单继承和多继承的虚函数表结构

无论是单继承,还是多继承,对于子类而言,他有多少基类就有多少个vptr指向不同的虚函数表。且不说单继承的场景,说说多继承吧。

对于一个继承Base1, Base2的Derived子类

他会有两个vptr指向两个虚函数表格,如果无论是基类还是子类都有print函数。

  • 那么对于子类的虚函数表格1,他的print函数会覆盖原有的print() -> Base1::print() 成为 print() -> Derived::print()
  • 对于虚函数表格2,他的print函数会覆盖原有的print() -> Base2::print() 成为 print() -> Derived::print()。

虚函数

纯虚函数和虚函数区别

他们可以出现在同一个类了,含纯虚函数的叫抽象基类。

虚函数可以直接使用,但是纯虚函数只能在子类实现了才能够用。

1
2
virtual void func(int x) = 0; 
//这才是纯虚函数。而且必须实现不然报错给你看。

析构函数的场景最好都是虚函数,也可以定义为纯虚函数,但是这样的基类是无法实例化了。

虚函数实现机制

  • 虚函数表存放的内容:类的虚函数的地址。
  • 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
  • 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
  • 虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针 vptr,来指向类的虚函数表 vtable。虚函数表vtable只有一个,且在编译期间创建

虚函数的作用

是为了运行时多态:运行时多态也称动态绑定,在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。

而运行时多态通过提供一个通用的接口来简化代码、减少重复、支持开闭原则以及实现动态绑定,从而使程序设计更加灵活和可扩展。这些优点使得运行时多态在面向对象编程中非常重要和有用。

构造函数和析构函数是否定义成虚函数

析构函数最好还是定义为虚函数,因为基类指针引用绑定子类的对象空间的时候,如果调用的是基类的析构函数,则只能释放掉基类成员的空间,子类的成员则内存泄漏。

构造函数的话,因为虚函数表是在对象空间里的,构造函数是创建该空间的,则是有点前后矛盾了。所以一般没有这样做。

多态的实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl; }
virtual void fun1() { cout << "Base::fun1()" << endl; }
virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()" << endl; }
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
};
int main()
{
Base *p = new Derive();
p->fun(); // Derive::fun() 调用派生类中的虚函数
return 0;
}

其实调用的是子类的虚函数,这种情况是虚函数表起了很大的作用。
存在虚函数的类,都有一个虚函数表,由一个vptr指向,该表存在于对象内存空间里。

所以基类指针,指向的是子类的对象空间,所以遍历的是子类对象空间里的虚函数表。

但是如果基类指针指向的子类对象空间,是用的基类的方法,因为看齐的是指针类型。

子类指针不能逆向指向父类的空间对象。

拷贝与构造

类对象的初始化顺序

这是一个十分冗长的过程

  • 首先要调用的还是其继承的父类的构造函数。
  • 按照继承列表,来逐个调用基类的构造函数。
  • 但是如果有虚继承,则优先调用构造函数。
  • 然后按照类里面成员,根据成员变量声明顺序,依次调用成员变量的构造函数如果有的。
  • 最后执行子类自身的构造函数。
    析构函数则是完全相反的顺序进行。

如果有静态变量则在main函数执行之前他们就是已经初始化好了。

当然如果构造函数内部进行初始化就和代码逻辑有关了。

如何禁止对象的拷贝

声明一个基类,然后将拷贝构造函数和赋值运算符重载设置为private。然后使用子类去继承该基类,那么子类的成员函数和友元函数是无法进行拷贝操作。 或者直接使用 = delete 删除拷贝构造函数,和赋值运算符重载。

初始化列表为什么更高效

如果在构造函数进行赋值,会首先调用默认的构造函数初始化,然后再进行赋值,相当于两次操作。但是初始化列表则只有一次操作,对象初始化时成员一并初始化,且方便编译器对其进行优化。这里有个点在于,写了个构造函数并不妨碍对象先调用默认构造函数。

浅拷贝和深拷贝的区别

  • 深拷贝,即新拷贝出的对象和前者内容虽然一样,但是内存空间不一样。
  • 浅拷贝,即新拷贝出的对象和前者内容一样,其实空间也一样。
    浅拷贝有个坏处,坏处在于如果前者被删除,内存空间被释放,会影响到另一个对象。

编译默认拷贝函数大部分都是浅拷贝,浅拷贝会有很多隐形问题。

如何禁止构造函数的使用

1
2
3
4
5
calss A {
public:
int v1, v2;
A(int t1, int t2) = delete;
};

这样就可以禁止了构造函数,即使对其使用私有,private,类内部成员和友元还是可以访问,无法彻底禁止。

什么是类的默认构造函数

什么时候编译器不会提供一个默认的构造函数呢?

如果显示了声明构造函数了。

大部分情况下编译器都会为类合成一个默认构造函数。

但是如果类内部有复合的数据结构,则最好别依赖编译器提供的默认构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
class A
{
public:
    int var;
    char c;
};
int main()
{
    A ex;
    cout << ex.c << endl << ex.var << endl;
    return 0;
}
/*
运行结果:
0
*/