【C++】多态|原理|override|final|抽象类|多继承虚函数表|对象模型|虚表打印|(万字详解版)

news2024/11/24 12:22:24

目录

​编辑

一.多态的概念

二.多态的构建

虚函数

重写

虚函数重写的例外 

协变

隐藏

析构函数的重写 

三.重载、重写(覆盖)、隐藏(重定义)的对比  

四.C++11新增的 override 和 final 

override

final

五.抽象类 

 六.多态的原理

虚函数表

总结:

引用和指针如何实现多态  

虚函数表存放位置 

七.单继承和多继承关系的虚函数表 

 单继承中的虚函数表

打印虚表 

总结:

多继承中的虚函数表 

打印虚表 

打印第二章虚表

多继承多个虚表 

​编辑 总结:

八.菱形继承和菱形虚拟继承 

菱形继承

菱形虚拟继承

区别和联系: 

九.例题


一.多态的概念

多态是面向对象编程中的一个核心概念,它允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。在C++中,多态主要通过虚函数来实现。

二.多态的构建

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
// 声明一个基类Person,其中包含一个虚函数BuyTicket
class Person
{
public:
    // 虚函数声明,使用virtual关键字,使得通过基类指针或引用调用该函数时能够根据实际对象类型动态绑定
    virtual void BuyTicket()		//虚函数,是建立多态的条件
    {
        cout << "全票" << endl;	// Person类中BuyTicket函数的行为是打印“全票”
    }
};

// Student类是从Person类公有继承而来
class Student : public Person
{
public:
    // 重写了基类中的虚函数BuyTicket,这是多态的表现
    virtual void BuyTicket()		// 重写基类的虚函数
    {
        cout << "半票" << endl;	// Student类中BuyTicket函数的行为是打印“半票”
    }
};

// 函数func接受一个Person类的引用作为参数
void func(Person& p)
{
    p.BuyTicket();	// 调用传入对象的BuyTicket函数,由于BuyTicket是虚函数,这里会根据实际对象类型执行对应版本的函数
}

int main()
{
    Person p;			// 创建Person类的对象p
    Student s;			// 创建Student类的对象s

    // 将基类和派生类的对象分别传递给func函数
    func(p);			// 输出“全票”,因为p是Person类型的对象
    func(s);			// 输出“半票”,尽管传入的是Student对象的引用,但通过基类引用调用,由于虚函数机制,会正确调用到Student的BuyTicket

    return 0;
}

注:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(

因为派生类继承的是虚函数的声明),但是这种写法不规范,不建议这样使用 。

虚函数

virtual修饰的类成员函数称为虚函数。

注:和虚继承没有任何关系,只是一个关键词两用。

    virtual void BuyTicket()	//携带关键字 virtual的函数
    {
        cout << "虚函数" << endl;	
    }

重写

派生类中有个跟基类相同的虚函数,则称为重写

        相同指 三同:函数名相同,返回值相同,参数列表相同

class Base {
public:
    virtual void print() 
    { // 虚函数声明
        cout << "Base Class" <<endl;
    }
};

class Derived : public Base {
public:
    void print() 
    {
        // 使用override重写父类的虚函数
       cout << "Derived Class" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();      // 基类指针指向子类对象
    basePtr->print();                // 输出 "Derived Class",展示了多态性
    delete basePtr; //释放
    return 0;
}

虚函数重写的例外 

协变和析构是例外

协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。

基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时,称为协变,简单来说,就是基类的返回值和派生类的返回值是同样的继承关系

class A {};
class B : public A {};  //继承关系

class Base {
public:
    virtual A* f()  //基类虚函数返回值是 另外一个基类的指针/引用
    {
        return new A;
    }
};
class Derive : public Base
{
public:
    virtual B* f()  //派生类重写了基类的虚函数,返回派生类对象的指针/引用
    { 
        return new B;
    }
};
隐藏

在了解析构函数的重写前,先了解隐藏

指在派生类中定义了一个与基类中同名的成员(可以是函数、变量等),但并没有使用virtual关键字进行虚函数的重写。这种情况下,基类的成员在派生类的上下文中被隐藏了,而不是被重写。这意味着,如果你有一个派生类的对象,并尝试通过派生类的引用或指针访问这个同名成员,你将访问到派生类的版本;但是,如果通过基类的引用或指针访问,则仍然只能访问到基类的成员,即使该对象实际上是派生类类型的实例。 

详情点击【C++】继承|切片|菱形继承|虚继承-CSDN博客中有关于 隐藏/重定义的详解。

析构函数的重写 

首先了解到,编译器会将析构函数统一处理成destructor,而C++中又有隐藏这一处理。

先看,如果没有加virtual构成隐藏

class Base {
public:
    ~Base()
    {
        cout << "~Base" << endl;
    }
};
class Derive : public Base
{
public:
    ~Derive()
    {
        cout << "~Derive" << endl;
    }
};

int main()
{
    //Base b;
    //Derive d;

    Base* ptrb1 = new Base;
    Base* ptrb2 = new Derive;
    delete ptrb1;
    delete ptrb2;
    return 0;
}

 发生隐藏后,调用的时候只看自身的类型,是Base就调用Base的函数,是Derive就调用Derive的函数,不构成多态,且派生类析构没有调用,内存泄漏

加上virtual的析构函数 ,就能正常析构了

注:析构函数加virtual是在new场景下才需要, 其他环境下可以不用

解释如下:

  1. 对象d(类型为Derive)首先被销毁,调用其析构函数,输出~Derive
  2. 紧接着,因为d的析构完成后,会自动调用其基类Base的析构函数,输出~Base
  3. 最后,对象b(类型为Base)被销毁,调用其析构函数,再次输出~Base

三.重载、重写(覆盖)、隐藏(重定义)的对比  

四.C++11新增的 override 和 final 

C++11 引入了overridefinal 两个关键字,它们增强了面向对象编程中的继承和多态特性,提高了代码的安全性和可读性。

override

关键字用于指示一个虚函数是其基类中虚函数的重写版本。

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

在派生类中定义虚函数时,在函数声明后紧跟 override关键字。

final

用途:

当用于类声明时,它表示该类不能被其他类继承,即禁止继承。

当用于虚函数声明时,它表示该虚函数在其派生类中不能被进一步重写,即锁定重写。

语法:

对于类,将 final 放在类声明的末尾;对于虚函数,在其声明后紧跟 final 关键字。

五.抽象类 

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类);

        抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

 六.多态的原理

虚函数表

  1. 每个包含虚函数的类都会有一个虚函数表。
  2. 每个对象的内存布局中,第一个位置通常是一个指向其类虚函数表的指针。
  3. 当通过基类指针或引用调用虚函数时,程序会先访问对象的虚函数表指针,然后根据虚函数在表中的索引找到正确的函数指针,并调用该函数。
  4. 在多重继承的情况下,每个类都有自己的虚函数表,对象的内存布局中可能包含多个虚函数表指针
class A
{
public:
    int i;
    virtual void Print() { } // 虚函数
};

int main()
{
    A a;
    cout << sizeof(a) <<endl;        //输出结果是8  当然根据不同X86/X64来决定指针大小
    return 0;
}

 因为该类有虚函数,所以至少有一个虚函数表指针:虚函数的地址会放在这个虚函数表里面。

 

而如果有派生类,派生类会继承基类的虚函数表和成员变量。

class A
{
public:
    virtual void Print() { } // 虚函数
    virtual void Print1() { } // 虚函数
    virtual void Print2() { } // 虚函数
    virtual void Print3() { } // 虚函数
    void func() { }
protected:
    int _a = 1;
};

class B :public A
{
public:
    virtual void Print() {}     //重写
    virtual void func() {}       //派生类自己特有的虚函数
protected:
    int _b = 0;
};

int main()
{
    A a;
    B b;
    cout << sizeof(a)<<","<<sizeof(b) << endl;  //8,12
    return 0;
}

 类A:  包含一个虚函数表指针(通常占用4或8字节,具体取决于平台),因为类A中至少有一个虚函数。成员变量_a是一个 int 通常占用4字节。

类B: B会继承类A的所有成员,包括虚函数表指针和成员变量,自身也有成员变量,所以是4/8字节的虚函数表指针,4字节的类A成员变量,4字节自己的成员变量;

详解:

        类B还重写了虚函数Print(),并且新增了一个虚函数func(),

但是重写不会增加虚函数表的大小,新增的虚函数会使得虚函数表中增加一项,这不会直接影响类的大小,因为虚函数表本身只有一份,但是每个对象需要一个指向该表的指针。

 

总结:

  • 虚函数自身自带一个_vfptr的虚函数表
  • 子类虚函数继承父类虚函数时,不仅继承父类虚函数表,也可以对虚函数进行重写。
  • 子类虚函数继承父类虚函数时,非虚函数不会存放在_vfptr
  • 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
  • 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

引用和指针如何实现多态  

为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?

传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数

传递子类,首先会进行切割 ;

将子类的内容切割掉,父类再去接受这个数据了,一样有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。

这样就完成了多态。

且记:是运行时才知道调用的是基类还是派生类的,这就是运行时多态;

同类对象的虚表一样。

如果子类没有重写父类的虚函数,那么他们的虚函数表指针不同,但里面的内容相同

虚函数表存放位置 

我们通过代码来打印各个区地地址,可以判断虚函数表存放位置

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

void func()
{}

int main()
{
	Base b1;
	Base b2;
	static int a = 0;
	int b = 0;
	int* p1 = new int;
	const char* p2 = "hello world";
	printf("静态区:%p\n", &a);
	printf("栈:%p\n", &b);
	printf("堆:%p\n", p1);
	printf("代码段:%p\n", p2);
	printf("虚表:%p\n", *((int*)&b1));
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数:%p\n", func);
}

注意打印虚表这里,vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节

b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址

七.单继承和多继承关系的虚函数表 

 单继承中的虚函数表

// 基类Base,包含一个虚函数func1和func2,以及一个私有成员变量a
class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; } // 虚函数func1的实现
    virtual void func2() { cout << "Base::func2" << endl; } // 虚函数func2的实现
private:
    int a; // 私有成员变量a
};

// 派生类Derive,公有继承类Base
class Derive : public Base {
public:
    // 重写基类的虚函数func1
    virtual void func1() { cout << "Derive::func1" << endl; }

    // 添加新的虚函数func3
    virtual void func3() { cout << "Derive::func3" << endl; }

    // 添加另一个新的虚函数func4
    virtual void func4() { cout << "Derive::func4" << endl; }
private:
    int b; // 私有成员变量b
};

// 从Derive类进一步派生出的类MM
class MM : public Derive {
public:
    // 重写从Derive继承来的虚函数func3
    virtual void func3() { cout << "MM::func3" << endl; }
};

int main()
{
    Base b; // 创建Base类的对象b
    Derive d; // 创建Derive类的对象d
    MM m; // 创建MM类的对象m
    return 0;
}

调试窗口有时候不可信,这时候要去看内存;

打印虚表 

虚函数表指针,其实就是一个函数数组指针,这个数组中的每个元素都是一个函数指针,指向类的虚函数实现

// 基类Base,包含虚函数func1和func2,以及一个私有成员变量a
class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; } // 虚函数func1的实现
    virtual void func2() { cout << "Base::func2" << endl; } // 虚函数func2的实现
private:
    int a; // 私有成员变量a
};

// 派生类Derive,公有继承自Base
class Derive : public Base {
public:
    // 重写基类的虚函数func1
    virtual void func1() { cout << "Derive::func1" << endl; }
    // 添加新的虚函数func3
    virtual void func3() { cout << "Derive::func3" << endl; }
    // 添加另一个新的虚函数func4
    virtual void func4() { cout << "Derive::func4" << endl; }
private:
    int b; // 私有成员变量b
};

// 从Derive类进一步派生出的类X
class MM : public Derive {
public:
    // 重写从Derive继承来的虚函数func3
    virtual void func3() { cout << "MM::func3" << endl; }
};

// 函数指针类型定义,用于指向无参数无返回值的函数
typedef void(*VFTPTR)();

// 打印虚函数表的函数
void PrintVFPtr(VFTPTR a[])
{
    for (size_t i = 0; a[i] != 0; i++) // 遍历虚函数表,直到遇到空指针为止
    {
        printf("a[%d]:%p\n", i, a[i]); // 打印当前索引的函数指针地址
    }
    cout << endl;
}

int main()
{
    Base b; // 创建Base类的对象b
    Derive d; // 创建Derive类的对象d
    MM m; // 创建MM类的对象m

    // 使用指针技巧获取虚函数表地址并打印其内容
    //  指针和指针之间可以进行强转
    //  整型家族可以跟指针进行强转
    PrintVFPtr((VFTPTR*)*(int*)&b); // 打印Base对象b的虚函数表
    PrintVFPtr((VFTPTR*)*(int*)&d); // 打印Derive对象d的虚函数表
    PrintVFPtr((VFTPTR*)*(int*)&m); // 打印MM对象m的虚函数表

    return 0;
}
  1. &b获取对象b的地址,它是一个指向Base对象的指针。
  2. (int*)&b将这个地址强制转换为一个指向 int 的指针。大部分编译器在对象的内存布局中,虚函数指针位于对象的起始位置,并且其大小与int相同。
  3. *(int*)&b解引用这个int指针,实际上得到的是对象b虚函数表指针的值
  4. (VFTPTR*)... 再次进行类型转换,这次是将前面得到的值(虚函数表的地址)转换为指向函数指针的指针,即(VFTPTR*)。这一步我们可以将这个地址传递给PrintVFPtr函数,进而访问虚函数表。

我们可以将代码修改一下,得到该地址具体是什么

因为a[i]里面存放的就是函数指针,因此我们可以选择直接调用。 

// 函数声明,接受一个VFTPTR类型的指针数组,VFTPTR是一个 无返回值无参数函数指针
void PrintVFPtr(VFTPTR a[]) {       //VFTPTR a[] 是一个函数指针数组,元素类型为 VFTPTR 
   
    for (size_t i = 0;
        a[i] != 0;    // 循环条件是当前指针不为空(通常虚函数表以NULL(00000000)结尾)
        i++)            // 每次循环迭代,i递增,指向下一个虚函数
    {
        // 打印当前虚函数指针在数组中的索引及其地址
        printf("a[%d]:%p->", i, a[i]);

        // 创建函数指针变量    临时保存当前索引处的虚函数指针 就是指向的虚函数地址
        VFTPTR p = a[i];

        // 调用当前索引处的虚函数(通过函数指针调用)
        /*调用 p() 使用函数指针时,通过在指针后面加上括号 () 就可以实现对指针所指向函数的调用。
        这与直接调用一个函数的方式相似,例如 func(),区别在于这里是通过一个指向该函数地址的指针来实现调用。*/
        p();    
    }

详解:VFTPTR p = a[i];

                        p();

  • p:是一个函数指针,来自数组 a 中索引为 i 的函数地址赋给了它。这个地址指向某个具体的函数实现。
  • p() :意味着“调用 p 所指向的函数”。编译器会解析 p 指针所存储的地址,然后跳转到该地址执行函数代码。

 

总结:

每个类都有一个自己的虚函数表,其中包含了该类所有虚函数的地址。在多重继承或更复杂的继承结构中,虚函数表的布局和内容可能会更加复杂,但基本原理相同。 

多继承中的虚函数表 

两个基类,都有各自的func1,func2。派生类继承了类Base1,类Base2,且重写了func1,增加了自己的func3

// 定义一个函数指针类型,用于指向无参数无返回值的函数
typedef void(*VFTPTR)();

// 打印虚函数表的函数,接受一个函数指针数组作为参数
void PrintVFPtr(VFTPTR a[]) {
    // 遍历数组直到遇到空指针
    for (size_t i = 0; a[i] != 0; i++) {
        // 打印当前函数指针的索引和地址
        printf("a[%d]:%p->", i, a[i]);
        // 调用当前函数指针指向的函数
        VFTPTR p = a[i];
        p();
    }
    cout << endl; // 换行
}

// 定义基础类Base1,包含两个虚函数
class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; } // 虚函数func1
    virtual void func2() { cout << "Base1::func2" << endl; } // 虚函数func2
private:
    int b1; // 私有成员变量
};

// 定义另一个基础类Base2,同样包含两个虚函数
class Base2 {
public:
    virtual void func1() { cout << "Base2::func1" << endl; } // 虚函数func1
    virtual void func2() { cout << "Base2::func2" << endl; } // 虚函数func2
private:
    int b2; // 私有成员变量
};

// 派生类Derive从Base1和Base2继承,添加自己的虚函数和成员变量
class Derive : public Base1, public Base2 {
public:
    virtual void func1() { cout << "Derive::func1" << endl; } // 重写func1
    virtual void func3() { cout << "Derive::func3" << endl; } // 添加新的虚函数func3
private:
    int d1; // 私有成员变量
};

int main() {
    // 创建Derive类的对象d
    Derive d;

    // 通过强制类型转换和指针偏移访问Derive对象的虚函数表
    PrintVFPtr((VFTPTR*)(*(int*)&d));

    // 打印Derive对象的大小,用于观察多继承对对象大小的影响
   // cout << sizeof(d) << endl;
    return 0; // 程序正常结束
}

 可以看到有两个虚表,但是自身特有的虚函数放在哪张表呢?

打印虚表 

当一个类通过多重继承从多个基类继承时,派生类可能会有多个虚函数表(vtable)和虚基类表。

每个非虚基类的虚函数表

  • 如果派生类从每个非虚基类继承了虚函数,那么派生类将为每个基类拥有一个独立的虚函数表。

虚基类的虚基类表

  • 如果派生类通过继承引入了虚基类,那么将为每个虚基类有一个虚基类表,用于管理虚基类的布局信息。

派生类自己的虚函数表

  • 如果派生类本身声明了新的虚函数或重写了继承的虚函数,它将拥有自己的虚函数表。

 

打印出来的结果只有第一张的虚表,且自身的func3虚函数在该表中。可以推论出该对象模型; 

打印第二章虚表

 由上可见,

方法1:

&d强转为Base1*,这样+1就会跳过整个Base1,就刚好到达了Base2类的开始,再进行之前的强转便可以打印了。

方法2:

直接将&d赋值给Base2* ptr;这样Base2会进行切片操作,于是ptr就直接指向了Base2的虚函数表,依然就行之前的强转操作便可以打印了。

int main()
{
    Derive d; // 创建一个Derive类的对象d

    // 直接通过Derive对象d访问虚函数表
    PrintVFPtr((VFTPTR*)(*(int*)&d));
    // 这里首先取d的地址(&d),然后强制转换为int*,接着解引用(*)得到虚函数表指针的地址,
    // 最后再次强制转换为VFTPTR*来打印Derive类的虚函数表。

    // 方法1: 通过将d转换为Base1指针并偏移1来尝试访问虚函数表
    PrintVFPtr((VFTPTR*)(*(int*)((Base1*)&d +1)));
    // 这里首先将d的地址强制转换为Base1*,为了访问Base1的虚函数表,
    // (Base1*)&d +1操作是访问紧随Base1之后的内存区域,

    // 方法2: 通过Base2指针访问Derive对象的虚函数表
    Base2* ptr = &d; // 将Derive对象d的地址赋给Base2指针ptr  切片
    PrintVFPtr((VFTPTR*)(*(int*)ptr));
    // 首先将ptr转换为int*,然后解引用得到Base2的虚函数表指针地址,
    // 再次转换为VFTPTR*以打印虚函数表。但是,会打印Base2的虚函数表,
                                 // 而非Derive的,因为ptr是指向Derive对象中Base2子对象的首地址。
    return 0; 
}

总结:Derive类对象的虚函数会放在多继承中继承的第一个类的虚函数表里(即Base1类虚函数表)  

多继承多个虚表 

如果没有多个虚表,那么p1去调用func1(),p2也去调用func1(),

如若d没有重写func1(),那么这个多态就会紊乱,调用的都是那一个func1()了,而不是p1调用Base1的func1(),p2调用Base2的func1()了。因此多继承就需要多个虚表才不会出现紊乱的问题

 

 总结:

在多继承的情况下,如果基类中有同名的虚函数(如func1()),每个基类都会维护自己的虚函数表,即使这些函数在名字和签名上完全相同。

        即使Base1和Base2中都有名为func1()的虚函数,它们在各自类的虚函数表中会有不同的地址,因为它们实际上是两个不同的函数实现。 当一个派生类(如Derive)继承了这些基类,它会继承这些虚函数表。

        如果派生类重写了这些同名的虚函数,则派生类的虚函数表中会包含这个重写后的函数地址。如果没有重写,派生类对象在调用这些函数时,会根据调用上下文(即通过哪个基类的指针或引用调用)来决定使用哪一个基类的函数实现。 

        在多继承体系中,每个基类的虚函数表都是独立的,即使它们包含同名函数,这些函数也是基类定义范围内的独立实现。这保证了多态性,即派生类对象可以正确响应通过基类指针或引用调用的虚函数,无论这些函数在多少个基类中被重载。

八.菱形继承和菱形虚拟继承 

两种不同的继承结构,它们都涉及到基类和派生类之间的关系

菱形继承

菱形继承是一种继承结构,其中两个或多个派生类继承自同一个基类,然后一个更深层次的派生类继承自这两个派生类。这种结构在类图上形成了一个菱形的样子;

class Base {
public:
    int value;
};

class Derive1 : public Base {
};

class Derive2 : public Base {
};

class MostDerived : public Derive1, public Derive2 {
    // MostDerived 有两个 Base 的拷贝,一个来自 Derive1,一个来自 Derive2
};

 

菱形虚拟继承

 是解决菱形继承问题的一种方法。通过将基类的继承方式改为虚继承(在继承时使用 virtual 关键字),可以确保所有派生类共享同一个基类子对象的拷贝。

优点

  • 避免了内存浪费,因为所有派生类共享同一个基类实例。
  • 保持了数据一致性,因为所有对基类成员的修改都作用于同一个实例。
class Base {
public:
    int value;
};

class Derive1 : virtual public Base {
};

class Derive2 : virtual public Base {
};

class MostDerived : public Derive1, public Derive2 {
    // MostDerived 只有一个 Base 的拷贝,所有 Derived 共享
};

区别和联系: 

  • 菱形继承不使用 virtual 关键字,导致基类在派生类中被多次实例化。
  • 菱形虚拟继承使用 virtual 关键字来避免重复实例化基类,确保所有派生类共享同一个基类实例。
  • 菱形虚拟继承解决了菱形继承中的数据一致性和内存浪费问题。

九.例题

class A
{
public:
    virtual void func1(int val = 1)
    {
        cout << "A::func1->val = " <<val<< endl;
    }
    virtual void test()
    {
        func1();
    }
};
class B:public A
{
public:
    virtual void func1(int val = 0) 
    { 
        cout << "A::func1->val = " <<val<< endl; 
    }
};
int main()
{
    B* b = new B;
    b->test();
    return 0;
}

 

 

是不是结果很匪夷所思

B类型的对象p去调用test();

test()是B类继承的,但是里面默认存放的this指针依然是A*,B类里面的func()是重写了A类的func()  (A类func()为虚函数,B类重写了可以不写virtual)。

重写的关键点:

        仅仅是重写了基类的实现,函数的声明,依然是A类的声明,因此给到的val默认值是1,调用了B类的函数实现!!! 所以输出B->1

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1830407.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CMake多行注释以及通过Message打印不同级别日志

1 CMake注释 1.1 单行注释 CMake中单行注释时以 # 开头。 # 指定CMake最低版本cmake_minimum_required(VERSION 3.20)# 这是注释project(myproject)1.2 多行注释 多行注释时&#xff0c;以 #[[ 开头&#xff0c;以 ]] 结尾&#xff0c;中间都可以写注释内容。3.0之前的版本…

MySQL面试重点-1

1. 数据库基础知识&#xff1a; DDL、DML、DQL、DCL的概念与区别&#xff1f; DDL&#xff08;数据定义语言&#xff09;&#xff1a;创建&#xff08;CREATE&#xff09;数据库中的各种对象&#xff1a;表、视图、索引等DML&#xff08;数据操纵语言&#xff09;&#xff1a…

点积和叉积

文章目录 1、向量的点积2、向量的叉积3、矩阵的点积4、矩阵的叉积 1、向量的点积 数量积又称标量积&#xff08;Scalar product&#xff09;、点积&#xff08;Dot product&#xff09;&#xff0c;在欧几里得空间&#xff08;Euclidean space&#xff09;中称为内积&#xff…

为何云原生是未来?企业IT架构的颠覆与重构(上)

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《未来已来&#xff1a;云原生之旅》&#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、什么是云原生 2、云原生的背景和起源 背景 起源 关…

酷开会员丨酷开系统K歌模式,父亲节的家庭欢聚时光

K歌以其独特的魅力&#xff0c;为家庭娱乐带来了无限乐趣。想象一下&#xff0c;父亲节这天&#xff0c;打开电视进入K歌频道&#xff0c;与家人一起嗨唱&#xff0c;客厅里充满了欢声笑语&#xff0c;酷开系统的K歌应用也就成为了连接亲情的桥梁&#xff0c;让爸爸们都能在这个…

CSS-0_1 CSS和层叠(样式优先级、内联样式、选择器 用户代理样式)

CSS 的本质就是声明规则 ——《深入解析CSS》 文章目录 CSS层叠和优先级用户代理样式请和用户代理样式和谐相处 选择器单选择器的优先级选择器组的优先级关于选择器的其他源码顺序尽可能的选择优先级低的选择器 内联样式内联样式和JavaScript !important多个 !important 碎碎念…

Wing FTP Server v7.2.0 解锁版安装教程 (跨平台的专业FTP服务器软件)

前言 Wing FTP Server是一款跨平台的专业FTP服务器软件, 支持可扩展处理器架构采用异步IO处理, 在速度和效率方面领先于其他同类产品. 它在高负载的情况下也能持续地正常运行, 非常适合企业文件传输. 通过基于Web管理端, 何时何地都能轻松管理远程的服务器. 除了基本功能外, 它…

qt笔记之qml和C++的交互系列(二):rootObject

qt笔记之qml和C的交互系列(二)&#xff1a;rootObject code review! —— 2024-06-17 杭州 夜 文章目录 qt笔记之qml和C的交互系列(二)&#xff1a;rootObject一.使用rootObject例程1.运行2.main.cpp3.main.qml3.用于Debug的加长版main.cpp 二.QML文件的根对象详解基本概念常…

蜂鸣器:基础(1)

蜂鸣器&#xff1a;基础&#xff08;1&#xff09; 原文&#xff1a; In this tutorial, we are going to learn how to use the buzzer with Arduino, In detail, we will learn: 在本教程中&#xff0c;我们将学习如何将蜂鸣器与Arduino一起使用&#xff0c;将学习&#xff…

C的I/O操作

目录 引言 一、文件与目录操作 1. 打开与关闭文件 2. 文件读写操作 3. 文件定位与错误处理 二、字符流与字节流 1. 字符流处理 2. 字节流处理 三、序列化与反序列化 1. 序列化 2. 反序列化 四、新的I/O&#xff08;NIO&#xff09; 表格总结 文件与目录操作 字符…

证明 均匀分布 的期望和方差

均匀分布 均匀分布&#xff08;Uniform Distribution&#xff09;是一种常见的连续型概率分布&#xff0c;其中随机变量在给定区间内的每个值都有相同的概率。假设随机变量 ( X ) 在区间 ([a, b]) 上服从均匀分布&#xff0c;记作 均匀分布的概率密度函数&#xff08;PDF&am…

好用的库函数,qsort函数大详解(干货满满!)(进阶)

前言&#xff1a; 小编在上一篇文章说了这一篇将要写qsort函数的模拟实现&#xff0c;那么废话不多说&#xff0c;现在开始进入今天的代码之旅喽&#xff01; 目录&#xff1a; 1.qsort函数的模拟实现的逻辑和思路 2.qsort函数模拟实现的代码实现 3.代码展示 1.qsort函数的模…

YOLOv9独家提点|加入MobileViT 、SK 、Double Attention Networks、CoTAttention等几十种注意力机制(五)

本文介绍了YOLOv9模型的改进,包括加入多种注意力机制,如SE、CBAM、ECA和SimAM。此外,还探讨了MobileViT轻量级视觉Transformer在移动设备上的应用,以及SelectiveKernelNetworks和A2-Nets的双注意力结构。最后,CoTAttention网络在视觉问答任务中的改进展示了跨模态注意力交…

今日早报 每日精选15条新闻简报 每天一分钟 知晓天下事 6月17日,星期一

每天一分钟&#xff0c;知晓天下事&#xff01; 2024年6月17日 星期一 农历五月十二 1、 今年首个红色山洪灾害预警&#xff1a;17日&#xff0c;浙江西南部、福建北部局地发生山洪灾害可能性很大。 2、 国家医疗保障局重构产科服务价格项目&#xff0c;“分娩镇痛”亲情陪产…

AI大模型在运动项目的深度融合和在穿戴设备的实践及未来运动健康技术发展

文章目录 1. 技术架构2. 模型选择2.1 LSTM&#xff08;长短期记忆网络&#xff09;2.2 CNN&#xff08;卷积神经网络&#xff09;2.3 Transformer 3. 数据处理数据预处理 4. 实时性要求4.1 边缘计算4.2 模型优化 5. 数据隐私与安全6. 深入分析AI大模型在穿戴设备的应用和未来发…

Yum安装LAMP

查看当前80端口是否被占用 ss -tulanp | grep 80查询httpd是否在yum源中 yum info httpd安装httpd yum -y install httpd启动httpd服务&#xff0c;设置开机自启 systemctl enable httpd --now systemctl start httpd查看当前进程 ps aux | grep httpd查看当前IP&#xff…

Google谈出海:品牌「性价比」转向「心价比」

Google Marketing Live中国站活动现场 越来越多的中国全球化品牌基于对全球消费和海外地区的深刻洞察&#xff0c;不断提升产品研发和迭代能力&#xff0c;在海外消费者心中塑造「中国质造」和「中国智造」的新形象。2023年6月15日&#xff0c;凯度与Google合作发布《2023 凯…

AI数据分析:根据Excel表格数据进行时间序列分析

ChatGPT中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个Python脚本编写的任务&#xff0c;具体步骤如下&#xff1a; 读取Excel表格&#xff1a;"F:\AI自媒体内容\AI行业数据分析\toolify月榜\toolify2023年-2024年月排行榜汇总数据.xlsx"…

vite-plugin-pwa 离线安装Vite应用

渐进式Web应用&#xff08;PWA&#xff09;通过结合 Web 和移动应用的特点&#xff0c;为用户带来更加流畅和快速的体验。且PWA支持离线访问能力&#xff08;访问静态资源本地缓存&#xff09;&#xff0c;极大提高了用户交互的流畅性&#xff0c;降低非必要的网络依赖。尤其适…

基于 U-Net 的图像分割

点击下方卡片&#xff0c;关注“小白玩转Python”公众号 图像分割是一种将图像划分为不同区域或对象的过程。它通常在像素级别进行&#xff0c;通过将图像中具有相似特征的区域分组或定义对象的边界来完成。这是一种识别和解析图像中不同对象或特征的方法。 假设一位医学专业人…