이 기사에서는 C++의 기본 가상 함수 클래스와 다형성에 대한 관련 정보를 주로 소개합니다. 이 기사에서는 예제 코드를 통해 이를 매우 자세하게 소개합니다. 학습이나 작업이 필요한 모든 사람에게 도움이 되는 학습 가치가 있습니다. 아래 에디터와 함께 배워볼까요?
머리말
이 글은 주로 C++의 상속 및 다형성의 기본 가상 함수 클래스에 대한 관련 내용을 소개하며 참고 및 학습을 위해 공유합니다. 세부적으로 살펴보자.
가상 함수 클래스
우리는 상속에서 가상 상속을 자주 언급합니다. 이제 가상 함수 클래스의 멤버 함수 앞에 virtual 키워드를 추가하면 해당 멤버 함수에 대해 살펴보겠습니다. 가상 함수라고 불리는데, 이 가상 함수를 과소평가하지 마세요. 상속의 까다로운 문제를 해결할 수 있으며, 다형성이 없으면 다형성이 없기 때문에 이 지식도 매우 중요합니다. 나중에 소개하는 가상 함수 테이블처럼 모두 매우 중요하므로 주의 깊게 이해해야 합니다~ 이제 가상 함수의 개념은 부모와 정확히 동일한 가상 함수가 있을 때 또 다른 개념으로 이어집니다. 클래스가 하위 클래스에 정의된 경우 이를 하위 클래스라고 합니다. 클래스의 이 함수는 상위 클래스의 가상 함수를 재정의(재정의라고도 함)합니다. 먼저 가상 함수 테이블에 대해 언급하겠습니다. 나중에 설명하겠지만 재작성이란 하위 클래스에 있는 가상 함수 테이블에 있는 상위 클래스의 재정의된 함수 주소를 모두 하위 클래스 함수의 주소로 변경하는 것입니다.
순수 가상 함수
멤버 함수의 형식 매개변수 뒤에 =0을 쓰면 해당 멤버 함수는 순수 가상 함수입니다. 순수 가상 함수를 포함하는 클래스를 추상 클래스(인터페이스 클래스라고도 함)라고 합니다.
추상 클래스는 객체를 인스턴스화할 수 없습니다. 순수 가상 함수가 파생 클래스에서 재정의된 후에만 파생 클래스가 개체를 인스턴스화할 수 있습니다.
예를 살펴보세요.
class Person { virtual void Display () = 0; // 纯虚函数 protected : string _name ; // 姓名 }; class Student : public Person {};
먼저 개념을 요약해 보겠습니다.
1 파생 클래스는 다형성을 달성하기 위해 기본 클래스의 가상 함수를 재정의하며, 함수 이름, 매개변수 목록 및 반환이 필요합니다. 값이 완전히 동일해야 합니다. (공분산 제외)
2. 가상 함수는 기본 클래스에 정의되어 있으며, 이 함수는 파생 클래스에서 항상 가상 함수의 특성을 유지합니다.
3. 클래스의 멤버 함수만 가상 함수로 정의할 수 있습니다.
4. 정적 멤버 함수는 가상 함수로 정의할 수 없습니다.
5. 클래스 외부에서 가상 함수를 정의하면 함수 선언 시에만 가상을 추가할 수 있습니다. 클래스 외부에서 함수를 정의할 때는 가상을 추가할 수 없습니다.
6. 생성자와 소멸자에서 가상 함수를 호출하지 마세요. 객체가 불완전하고 정의되지 않은 동작이 발생할 수 있습니다.
7. 기본 클래스의 소멸자를 가상 함수로 선언하는 것이 가장 좋습니다. (왜? 게다가 소멸자는 파생 클래스의 소멸자 이름이 기본 클래스의 소멸자 이름과 다르지만 재정의를 구성하기 때문에 특별합니다. 이는 컴파일러에서 특수 처리를 했기 때문입니다.)
8. 생성자는 가상 함수일 수 없습니다. 연산자=는 가상 함수로 정의할 수 있지만 쉽게 사용할 경우 혼동을 일으킬 수 있으므로 가상 함수로 정의하지 않는 것이 가장 좋습니다. 위 개념이 왜 필요한가요? 이 내용에 대한 답은 다음 지식에서 찾을 수 있습니다~ 그럼 오늘의 주인공
가상함수
가상함수테이블이란 무엇일까요? 그리고 모니터링 창을 조정하여 알 수 있습니다.
#include<iostream> #include<windows.h> using namespacestd; class Base { public: virtual void func1() {} virtual void func2() {} private: inta; }; void Test1() { Base b1; } int main() { Test1(); system("pause"); return0; }
이제 b1의 모니터링 창을 클릭합니다
그 안에 _vfptr이 있는데, 이 _vfptr이 가리키는 것이 우리의 주인공인 virtual입니다. 기능 테이블. 단일 상속이든 다중 상속이든 다이아몬드 모양의 상속 가상 함수 테이블도 모양이 다르다는 사실은 누구나 금방 알 수 있습니다.
단일 상속의 메모리 레이아웃을 연구해 보겠습니다.
다음 코드를 주의 깊게 살펴보세요.
#include<iostream> #include<windows.h> using namespace std; class Base { public: virtual void func1() { cout<< "Base::func1"<< endl; } virtual void func2() { cout<< "Base::func2"<< endl; } private: inta; }; class Derive:public Base { public: virtual void func1() { cout<< "Derive::func1"<< endl; } virtual void func3() { cout<< "Derive::func3"<< endl; } virtual void func4() { cout<< "Derive::func4"<< endl; } private: int b; };
. 그러면 우리가 생각하는 것이 맞는지 확인하기 위해 모니터링 창을 띄워볼까요?
我预计应该是看到fun1()
,fun2()
,fun3()
,fun4()
的虚函数表,但是呢这里监视窗口只有两个fun1()
, fun2()
,难道我们错了????
这里并不是这样的,只有自己靠得住,我觉得这里的编译器有问题,那我们就得自己探索一下了。 但是在探索之前我们必须来实现一个可以打印虚函数表的函数。
typedef void(*FUNC)(void); void PrintVTable(int* VTable) { cout<< " 虚表地址"<<VTable<< endl; for(inti = 0;VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout<< endl; } int main() { Derive d1; PrintVTable((int*)(*(int*)(&d1))); system("pause"); return0; }
下图来说一下他的缘由:
我们来使用这个函数,该函数代码如下:
//单继承 class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; typedef void(*FUNC)(void); void PrintVTable(int* VTable) { cout<< " 虚表地址"<<VTable<< endl; for(inti = 0;VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout<< endl; } int main() { Derive d1; PrintVTable((int*)(*(int*)(&d1))); //重点 system("pause"); return0; }
这里我就要讲讲这个传参了,注意这里的传参不好理解,应当细细的"品味".
PrintVTable((int*)(*(int*)(&d1)));
首先我们肯定要拿到d1的首地址,把它强转成int*,让他读取到前4个字节的内容(也就是指向虚表的地址),再然后对那个地址解引用,我们已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int*的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。
最后我们来看看输出结果:
到底打印的对不对呢? 我们验证一下:
这里我们通过&d1的首地址找到虚表的地址,然后访问地址查看虚表的内容,验证我们自己写的这个函数是正确的。(这里VS还有一个bug,当你第一次打印虚表时程序可能会崩溃,不要担心你重新生成解决方案,再运行一次就可以了。因为当你第一次打印是你虚表最后一个地方可能没有放0,所以你就有可能停不下来然后崩溃。)我们可以看到d1的虚表并不是监视器里面打印的那个样子的,所以有时候VS也会有bug,不要太相信别人,还是自己靠得住。哈哈哈,臭美一下~
我们来研究一下多继承的内存格局
探究完了单继承,我们来看看多继承,我们还是通过代码调试的方法来探究对象模型
看如下代码:
class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; typedef void(*FUNC) (); void PrintVTable(int* VTable) { cout << " 虚表地址>" << VTable << endl; for (int i = 0; VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout << endl; } void Test1() { Derive d1; //Base2虚函数表在对象Base1后面 int* VTable = (int*)(*(int*)&d1); PrintVTable(VTable); int* VTable2 = (int *)(*((int*)&d1 + sizeof (Base1) / 4)); PrintVTable(VTable2); } int main() { Test1(); system("pause"); return 0; }
现在我们现在知道会有两个虚函数表,分别是Base1和Base2的虚函数表,但是呢!我们的子类里的fun3()
函数怎么办?它是放在Base1里还是Base2里还是自己开辟一个虚函数表呢?我们先调一下监视窗口:
监视窗口又不靠谱了。。。。完全没有找到fun3().那我们直接看打印出来的虚函数表。
现在很清楚了,fun3()
在Base1的虚函数表中,而Base1是先继承的类,好了现在我们记住这个结论,当涉及多继承时,子类的虚函数会存在先继承的那个类的虚函数表里。记住了!
我们现在来看多继承的对象模型:
现在我们来结束一下上面我列的那么多概念现在我来逐一的解释为什么要这样.
1.为什么静态成员函数不能定义为虚函数?
因为静态成员函数它是一个大家共享的一个资源,但是这个静态成员函数没有this指针,而且虚函数变只有对象才能能调到,但是静态成员函数不需要对象就可以调用,所以这里是有冲突的.
2.为什么不要在构造函数和析构函数里面调用虚函数?
构造函数当中不适合用虚函数的原因是:在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则析构函数当中不适用虚函数的原因是:一般析构函数先析构子类的,当你在父类中调用一个重写的fun()
函数,虚函数表里面就是子类的fun()
函数,这时候已经子类已经析构了,当你调用的时候就会调用不到.
现在我在写最后一个知识点,为什么尽量最好把基类的析构函数声明为虚函数??
现在我们再来写一个例子,我们都知道平时正常的实例化对象然后再释放是没有一点问题的,但是现在我这里举一个特例:
我们都知道父类的指针可以指向子类,现在呢我们我们用一个父类的指针new一个子类的对象。
//多态 析构函数 class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } virtual ~Base() { cout << "~Base" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual ~Derive() { cout << "~Derive"<< endl; } private: int b; }; void Test1() { Base* q = new Derive; delete q; } int main() { Test1(); system("pause"); return 0; }
这里面可能会有下一篇要说的多态,所以可能理解起来会费劲一点。
注意这里我先让父类的析构函数不为虚函数(去掉virtual),我们看看输出结果:
这里它没有调用子类的析构函数,因为他是一个父类类型指针,所以它只能调用父类的析构函数,无权访问子类的析构函数,这种调用方法会导致内存泄漏,所以这里就是有缺陷的,但是C++是不会允许自己有缺陷,他就会想办法解决这个问题,这里就运用到了我们下次要讲的多态。现在我们让加上为父类析构函数加上virtual,让它变回虚函数,我们再运行一次程序的:
诶! 子类的虚函数又被调用了,这里发生了什么呢?? 来我们老方法打开监视窗口。
刚刚这种情况就是多态,多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。这个我们下一个博客专门会总结多态.
当然虚函数的知识点远远没有这么一点,这里可能只是冰山一角,比如说菱形继承的虚函数表是什么样?然后菱形虚拟继承又是什么样子呢? 这些等我总结一下会专门写一个博客来讨论菱形继承。虚函数表我们应该已经知道是什么东西了,也知道单继承和多继承中它的应用,这些应该就足够了,这些其实都是都是为你让你更好的理解继承和多态,当然你一定到分清楚重写,重定义,重载的他们分别的含义是什么. 这一块可能有点绕,但是我们必须要掌握.
위 내용은 C++의 상속 및 다형성과 관련된 기본 가상 함수 클래스 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!