C++ 面向对象

面向对象及其三大特性★★★★★

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类。类中包含数据(成员变量)和动作(成员方法)。

面向对象的三大特性:

  1. 封装:将数据(成员变量)以及对数据的操作(成员方法)封装在一个类中,通过访问修饰符控制对外界的可见性,只能通过特定的接口进行访问,增强了安全性并降低了模块间的耦合性。
  2. 继承:子类继承父类的特征和行为,即子类可以拥有父类的非 private 方法或成员变量,并且可以根据需要重写父类的方法。当父类中的成员变量、成员函数或者类本身被 final关键字修饰时,该类不能被继承,或其成员不能被重写或修改。
  3. 多态:多态指的是不同继承关系的类的对象在接收同一消息时能够表现出不同的行为。这通常是通过基类的指针或引用来指向派生类的对象实现的,使得基类指针/引用呈现不同的表现方式。在C++ 中,多态一般是使用虚函数来实现的。例如,使用基类指针调用函数方法时:
    • 如果该指针指向的是一个基类的对象,则调用的是基类的虚函数;
    • 如果该指针指向的是一个派生类的对象,则调用的是派生类的虚函数。

重载、重写、隐藏的区别★★★★★

函数重载:重载是指在同一可访问区内被声明几个具有不同参数列表(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

class A
{
   
public:
    void fun(int tmp);
    void fun(float tmp);       
    void fun(int tmp, float tmp1); 
    void fun(float tmp, int tmp1);
    int fun(int tmp);           
};

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

#include <iostream>
using namespace std;
class Base
{
   
public:
    void fun(int tmp, float tmp1) {
    cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
   
public:
    void fun(int tmp) {
    cout << "Derive::fun(int tmp)" << endl; }
};
int main()
{
   
    Derive ex;
    ex.fun(1);       // Derive::fun(int tmp)
    // ex.fun(1, 0.01);  error
    return 0;
}

函数重写(覆盖):函数重写是指派生类中存在重新定义的函数。函数名、参数列表必须同基类中被重写的函数一致,返回值类型也需要兼容。派生类调用时会调用派生类的重写函数,不会调用被重写的函数。重写的基类中被重写的函数必须有virtual 修饰。

#include <iostream>
using namespace std;
class Base
{
   
public:
    virtual void fun(int tmp) {
    cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
   
public:
    virtual void fun(int tmp) {
    cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
   
    Base *p = new Derived();
    p->fun(3); // 输出: Derived::fun(int tmp) : 3
    return 0;
}

重写和重载的区别

  • 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
  • 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有virtual 修饰。
  • virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。

隐藏和重写的区别

  • 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。 利用重写可以实现多态,而隐藏不可以。如果使用基类指针 p指向派生类对象,利用这个指针调用函数时,对于隐藏的函数,会根据指针的类型去调用函数;对于重写的函数,会根据指针所指对象的类型去调用函数。重写必须使用virtual 关键字,此时会更改派生类虚函数表的表项。
  • 时间区别:隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口。

多态及其实现方法★★★★★

编译时多态(静态多态):通过函数重载和模板来实现的。对于函数重载,编译器根据参数的数量、类型以及调用方式选择最合适的方法。对于模板,则是在编译期生成特定类型的函数或类的具体版本。
运行时多态(动态多态):通过虚函数和纯虚函数来实现的。基类的指针或引用指向派生类对象,并 且调用的是一个被声明为虚函数 virtual 的方法时,程序会在运行时确定调用哪个版本的方法。

编译时多态和运行时多态的区别:

  • 时期不同:编译时多态在程序编译阶段决定,而运行时多态则是在程序运行期间实现;
  • 实现方式不同:编译时多态主要是通过函数重载和模板技术来实现,而运行时多态则是借助于虚函数表(vtable)和虚函数机制来完成,程序会根据 实际指向的对象类型,查询该对象的虚函数表调用对应的函数。
#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;
}

多态的实现原理:多态是通过虚函数实现的。虚函数的地址保存在虚函数表(vtable)中,而虚函数表的地址则保存在含有虚函数的类的实例对象的内存空间中。

  • 在类中用 virtual 关键字声明的函数叫做虚函数;
  • 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(vptr)(虚函数表和类对应,虚表指针是和对象对应的);
  • 当基类指针指向派生类对象,基类指针调用虚函数时,该基类指针所指的虚表指针实际上指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数然后调用执行。

多态的总结:根据上述结论,我们可以知道虚函数的调用是在运行时决定的,是由本身所指向的对象所决定的。

  • 如果使用虚函数,基类指针指向派生类对象并调用对象方法时,使用的是子类的方法;
  • 如果未使用虚函数,则是普通的隐藏,则基类指针指向派生类对象时,使用的是基类的方法(与指针类型看齐)。
    注意:基类指针能指向派生类对象,但是派生类指针不能直接指向基类对象,除非进行显式的类型转换。

虚函数和纯虚函数详解★★★★★

虚函数:被 virtual 关键字修饰的成员函数,C++ 的虚函数在运行时动态绑定,从而实现多态。

纯虚函数:

  • 纯虚函数在类中声明时,用 virtual 关键字修饰且加上 =0,且没有函数的具体实现;
  • 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口定义,没有具体的实现方法;
  • 抽象类只能作为基类来使用,继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型; 可以声明抽象类指针,可以声明抽象类的引用,如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

纯虚函数的作用:含有纯虚函数的基类要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。定义纯虚函数是为了实现统一的接口属性,用来规范派生类的接口属性,也即强制要求继承这个类的程序员必须实现这个函数。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以要求实现纯虚函数的属性。


虚函数和纯虚函数的区别★★★★★

虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类(含有纯虚函数的类称为抽象基类)。

  • 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
  • 定义形式不同:虚函数在定义时在普通成员函数的基础上加上virtual 关键字,纯虚函数定义时除了加上 virtual 关键字还需要加上 “=0”;
  • 虚函数必须提供实现,否则编译器会报错;
  • 纯虚函数不需要在声明它的类中给出实现(也可以提供默认实现,但通常不推荐这样做);
  • 对于实现了纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数。虚函数和纯虚函数都可以在派生类中重写;
  • 析构函数最好定义为虚函数,特别是对于含有继承关系的类。这样可以确保当通过基类指针删除派生类对象时,派生类的析构函数能被正确调用。析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。
// 示例1: 虚函数
#include <iostream>
using namespace std;
class Base
{
   
public:
    virtual void show() // 虚函数
    {
   
        cout << "Base::show()" << endl;
    }
};
class Derived : public Base
{
   
public:
    void show() override // 重写虚函数
    {
   
        cout << "Derived::show()" << endl;
    }
};
// 示例2: 纯虚函数
#include <iostream>
using namespace std;
class AbstractClass
{
   
public:
    virtual void pure_virtual_function() = 0; // 纯虚函数
    virtual ~AbstractClass() {
   } // 虚析构函数
};
class ConcreteClass : public AbstractClass
{
   
public:
    void pure_virtual_function() override // 实现纯虚函数
    {
   
        cout << "ConcreteClass::pure_virtual_function()" << endl;
    }
};
int main()
{
   
    Base* basePtr = new Derived();// 使用虚函数
    basePtr->show(); // 输出: Derived::show()
    delete basePtr;
    AbstractClass* absPtr = new ConcreteClass();// 使用纯虚函数
    absPtr->pure_virtual_function(); // 输出: ConcreteClass::pure_virtual_function()
    delete absPtr;
    return 0;
}

虚函数的实现机制★★★★★

  • 虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
  • 每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚函数表。该表是编译器在编译时设置的静态数组,一般我们称为 vtable。虚函数表包含可由该类调用的虚函数,此表中的每个条目是一个函数指针,指向该类可访问的虚函数。
  • 每个对象在创建时,编译器会为对象生成一个指向该类的虚函数表的指针,我们称之为 vptr。vptr 在创建类实例时自动设置,以便指向该类的虚拟表。如果对象(或者父类)中含有虚函数,则编译器一定会为其分配一个vptr;如果对象不包含(父类也不含有),此时编译器则不会为其分配 vptr。与 this 指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr 是一个真正的指针。

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

#include <iostream>
using namespace std;
class Base
{
   
public:
    virtual void B_fun1() {
    cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() {
    cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() {
    cout << "Base::B_fun3()" << endl; }
};
class Derive : public Base
{
   
public:
    virtual void D_fun1() {
    cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() {
    cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() {
    cout << "Derive::D_fun3()" << endl; }
};
int main()
{
   
    Base *p = new Derive();
    p->B_fun1(); // Base::B_fun1()
    return 0;
}

我们通过对象内存的开头处取出 vptr,并遍历对象虚函数表。

#include <iostream>
#include <memory>
using namespace std;
typedef void (*func)(void);
class A {
   
public:
	void f() {
    cout << "A::f" << endl; }
	void g() {
    cout << "A::g" << endl; }
	void h() {
    cout << "A::h" << endl; }
};
class Base {
   
public:
	virtual void f() {
    cout << "Base::f" << endl; }
	virtual void g() {
    cout << "Base::g" << endl; }
	virtual void h() {
    cout << "Base::h" << endl; }
};
class Derive: public Base {
   
public:
	void f() {
    cout << "Derive::f" << endl; }
    void g() {
    cout << "Derive::g" << endl; }
	void h() {
    cout << "Derive::h" << endl; }
};
int main() 
{
   
	Base base;
    Derive derive;
	unsigned long* vPtr = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值