C++中的面向对象重点总结

2024-01-07 19:49:12

面向对象的三大特性

封装

封装是将数据和操作这些数据的函数(方法)组合在一个类中的过程。

封装的主要目的是隐藏类的内部实现细节,仅暴露必要的接口给外部,通过控制类成员的访问级别,可以限制对类内部数据的直接访问,确保数据的完整性和安全性。

继承

继承是一个类(派生类)从另一个类(基类)哪里获得其属性和方法的过程。

C++中? ? public继承表示派生类 is-a (是一个)基类。

pricate继承表示派生类 包含(has a)基类。

多态*

定义:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接受时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)

功能:多态允许不同类的对象使用相同的接口名字,但具有不同实现的特性,允许将子类类型的指针赋值给父类类型的指针,

实现多态有两种方法:

1. 覆盖(override):是指子类重新定义父类的虚函数的做法

2. 重载(overload):是指允许存在多个同名函数,而这些函数的参数表不同,或许参数个数不同,或许参数类型不同,或许两者都不同

重载overload

重载是指相同作用域(比如命名空间或者同一个类)内拥有相同的方法名,但具有不同的参数类型和/或参数数量的方法。 重载允许根据所提供的参数不同来调用不同的函数。它主要在以下情况下使用:

方法具有相同的名称。
方法具有不同的参数类型或参数数量。
返回类型可以相同或不同。

同一作用域,比如都是一个类的成员函数,或者都是全局函数。

重写overriding

重写是指在派生类中重新定义基类中的方法。

当派生类需要改变或扩展基类方法的功能时,就需要用到重写。

重写的条件包括:

方法具有相同的名称。
方法具有相同的参数类型和数量。
方法具有相同的返回类型。

重写的基类中被重写的函数必须有virtual修饰。
重写主要在继承关系的类之间发生。

隐藏Hiding

隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

类对象的初始化和析构顺序

初始化顺序

在C++中,类对象的初始化顺序遵循以下规则:

1. 基类初始化顺序:如果当前类继承自一个或多个基类,它们将按照声明顺序进学校初始化,但是在有虚继承和一般继承存在的情况下,优先虚继承。

2. 成员变量初始化顺序:类的成员变量按照它们在类定义中的声明顺序进行初始化

3. 执行构造函数

类的析构顺序

类的析构顺序和构造顺序完全相反

多态的实现方式:虚函数、纯虚函数和模板函数

虚函数、纯虚函数实现多态

虚函数是指在基类中声明的函数,它在派生类中可以被重写。

当我们使用基类指针或引用指向派生类对象时,通过虚函数的机制,可以调用到派生类中重写的函数,从而实现多态。

因此C++的多态必须满足两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数是虚函数,且必须完成对基类虚函数的重写

虚函数表

虚函数是通过一张虚函数表来实现的,在这个表中,存放的是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,确定其调用的函数。

当一个类中的成员函数有虚函数时,这个类的实例内存中都有一个虚函数表的指针(vptr),虚函数表指针指向一个数组,该数组的元素就是各个虚函数的地址,通过函数的索引,我们就能直接访问对应的虚函数。

对于派生类,其虚函数表通常是在基类的虚函数表的基础上扩展而来的,当我们在子类中重写一个父类的虚函数时,在子类的虚函数表中用子类重写的函数地址去取代父类的函数地址。当调用一个虚函数时,编译器会通过对象的虚函数指针查找到该对象所属的类的虚函数表,并根据函数的索引值来找到对应的虚函数地址,然后将控制转移到该地址,实际执行该函数的代码。

所以,当我们用父类的指针来操作有一个子类的时候,程序会根据子类的续函数表调用所指的函数。

静态绑定:当我们使用派生类的指针去访问虚函数时,实际上并未发生多态,因为编译时就能够确定对象类型为派生类型,然后直接生成调用派生类虚函数的代码即可。

动态绑定:通过基类的指针或引用调用虚函数才能构成多态,因为这种情况下运行时才能确定对象的实际类型。

纯续函数

纯虚函数是一种在基类中声明但没有实现的虚函数。它的作用是定义了一种接口,这个接口需要由派生类来实现

包含纯虚函数的类被称为抽象类:抽象类仅仅提供了一些接口,但是没有实现具体的功能,作用就是制定各种接口,通过派生类来实现不同的功能,从而实现代码的复用和可扩展性。

为什么基类析构函数需要是虚函数

析构函数是进行类的清理工作,比如释放内容、关闭文件等。

为了实现多态,C++可以通过基类的指针或引用访问派生类的成员

那么,如果一个基类的析构函数不是虚函数时,则该类的虚函数表中就不会有这个析构函数的地址,当我们通过基类的指针来访问派生类且结束时,派生类的析构函数不会被调用,只会调用基类的析构函数,因为这里并没有使用到多态(析构函数不在虚函数表中),这可能会导致资源泄露。

例如:

Class Base1{
    public:
        ~Base(){ cout << "Base1 destructor" <<endl;}
}

Class derived1 : public Base1{
    public:
        ~Derived(){ cout << "Derived1 destructor" << endl;}
}


Class Base2{
    public:
        virtual ~Base(){ cout << "Base2 destructor" <<endl;}
}

Class derived2 : public Base2{
    public:
        ~Derived(){ cout << "Derived2 destructor" << endl;}
}

int main()
{
    Base1 *ptr1 = new Derived1();
    delete ptr1;

    Base1 *ptr2 = new Derived2();
    delete ptr2;

    return 0;
}

该程序的运行结果为:

Base1 destructor
Derived2 destructor
Base2 destructor

模板函数多态

模板函数可以根据传递参数的不同类型,自动生成相应类型的函数代码,模板函数可以用来实现多态,例如

template <class T>
T max(T a, T b){return (a>b? a:b)}

int main()
{
    int i=1, j = 2;
    char a = 's', b = 'w';
    cout << max(i,j) << max(a,b) << endl;
    return 0;
}

?

为什么C++的成员模板函数不能是virtual

因为C++的编译与链接模型是分离的

1. 从Unix/C开始,一个C/C++程序就可以被分开编译,然后用一个linker链接起来。这种模型有一个问题,就是各个编译单元可能对另一个编译单元一无所知。

2. 一个function template 最后到底会被instantiate为多少个函数,要等整个程序(所有编译单元)全部被编译完成才知。

3. virtual function的实现利用了虚函数表,这种实现中,一个类的内存布局需要在这个类编译完成的时候就被完全确定。

所以当一个虚函数是模板函数时,编译器在编译时无法为其生成一个确定的虚函数表条目,因为模板函数可以有无数个实例。但是编译时无法确定需要调用那个特定的模板实例。因此,C++规定成员函数不能既是模板函数又是虚函数。

参考:为什么C++的成员模板函数不能是 virtual 的 | 编程指北 (csguide.cn)

文章来源:https://blog.csdn.net/m0_53846807/article/details/135232667
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。