构造函数不能为虚函数的理由
- 格式:docx
- 大小:15.03 KB
- 文档页数:2
不能声明为虚函数的几种情况在面向对象的程序设计中,虚函数是一个相当重要的概念,它可以让派生类通过继承、重写的方式来实现多态性。
但是并不是所有的函数都能声明为虚函数。
下面我们就来详细地介绍一下哪些情况下不能声明为虚函数。
第一种情况:static函数static函数是一个非常常用的函数类型,它与类的实例无关,不受类的内存布局和实例化影响,因此无法通过实例来调用它。
由于这个函数没有被继承,因此也不能被重写,所以它不能声明为虚函数。
第二种情况:构造函数和析构函数构造函数和析构函数是C++面向对象开发中非常重要的两个函数,它们分别用于对象的初始化和销毁。
由于在子类对象构造或析构时,父类的构造函数或析构函数都会被自动调用。
但是,由于构造函数和析构函数的特殊性质,它们不能被声明为虚函数。
构造函数不能声明为虚函数,因为虚函数需要在对象完全构造成功后才能调用,而构造函数中的虚函数还没有完成构造对象过程。
同样的道理,析构函数也不能为虚函数,因为它需要在对象被完全析构前调用,如果是虚函数,那么在调用析构函数时可能导致类成员已经被销毁了,程序会崩溃。
第三种情况:友元函数友元函数是一个与类的访问权限无关的函数,能够访问类的私有成员。
虽然友元函数不是类的成员函数,但是在某些情况下,为了方便访问私有成员,我们会将其定义为类的友元函数。
但是友元函数不能被继承,也不能被重写,因此也不能被声明为虚函数。
第四种情况:全局函数全局函数是定义在类外部,且不属于任何类的函数,又称为独立函数。
由于它不属于任何类,因此也不能被继承和重写,所以也不能声明为虚函数。
在C++中,为了实现多态性,我们可以将需要多态的函数声明为虚函数。
但是无法将构造函数、析构函数、static函数、友元函数、全局函数声明为虚函数。
需要在实际编程中注意这些细节问题,才能更好地利用类的特性,提高程序的效率。
C++——类继承以及类初始化顺序对于类以及类继承, ⼏个主要的问题:1) 继承⽅式: public/protected/private继承.这是c++搞的, 实际上继承⽅式是⼀种允许⼦类控制的思想. ⼦类通过public继承, 可以把基类真实还原, ⽽private继承则完全把基类屏蔽掉.这种屏蔽是相对于对象层⽽⾔的, 就是说⼦类的对象完全看不到基类的⽅法, 如果继承⽅式是private的话, 即使⽅法在基类中为public的⽅法.但继承⽅式并不影响垂直⽅向的访问特性, 那就是⼦类的函数对基类的成员访问是不受继承⽅式的影响的.⽐较(java): java是简化的, 其实可认为是c++中的public继承. 实在没必要搞private/protected继承, 因为如果想控制,就直接在基类控制就好了.2) 对象初始化顺序: c++搞了个成员初始化列表, 并确明确区分初时化跟赋值的区别. c++对象的初始化顺序是:(a) 基类初始化(b) 对象成员初时化(c) 构造函数的赋值语句举例:假设 class C : public A, public B {D d;//}则初始化的顺序是A, B, D, C的构造函数.这⾥基类的初始化顺序是按照声明的顺序, 成员对象也是按照声明的顺序. 因此 c(int i, int j) : B(i), A(j) {} //这⾥成员初始化列表的顺序是不起作⽤的;析构函数的顺序则刚好是调过来, 构造/析构顺序可看作是⼀种栈的顺序;⽐较(java): java中初始化赋值是⼀回事. ⽽且对基类的构造函数调⽤必须显⽰声明, 按照你⾃⼰写的顺序.对成员对象, 也叫由你初始化.没有什么系统安排的顺序问题, 让你感觉很舒服;3) 多继承问题: c++⽀持多继承, 会导致"根"不唯⼀. ⽽java则没有该问题;此外c++没有统⼀的root object, java所有对象都存在Object类使得很多东西很⽅便. ⽐如公共的seriall, persistent等等.4) 继承中的重载: c++中, 派⽣类会继承所有基类的成员函数, 但构造函数, 析构函数除外.这意味着如果B 继承A, A(int i)是基类构造函数, 则⽆法B b(i)定义对象. 除⾮B也定义同样的构造函数.c++的理由是, 假如派⽣类定义了新成员, 则基类初始化函数⽆法初始化派⽣类的所有新增成员.⽐较(java): java中则不管, 就算有新增对象基类函数没有考虑到, ⼤不了就是null, 或者你⾃⼰有缺省值. 也是合理的.5) 继承中的同名覆盖和⼆义性: 同名覆盖的意思是说, 当派⽣类跟基类有完全⼀样的成员变量或者函数的时候, 派⽣类的会覆盖基类的.类似于同名的局部变量覆盖全局变量⼀样. 但被覆盖的基类成员还是可以访问的.如B继承A, A, B都有成员变量a,则B b, b.a为访问B的a, b.A::a 则为访问基类中的a. 这对于成员函数也成⽴.但需要注意的是, 同名函数必须要完全⼀样才能覆盖. int func(int j)跟int func(long j)其实是不⼀样的. 如果基类,派⽣类有这两个函数, 则不会同名覆盖.最重要的是, 两者也不构成重载函数. 因此假如A有函数int func(int j), B有函数int func(long j). 则B的对象b.func(int)调⽤为错误的. 因为B中的func跟它根本就不构成重载.同名覆盖导致的问题是⼆义性. 假如C->B=>A, 这⾥c继承B, B继承A. 假如A, B都有同样的成员fun, 则C的对象c.fun存在⼆义性. 它到底是指A 的还是B的fun呢?解决办法是⽤域限定符号c.A::fun来引⽤A的fun.另外⼀个导致⼆义性的是多重继承. 假设B1, B2都继承⾃B, D则继承B1, B2. 那么D有两个B⽽产⽣⼆义性.这种情况的解决办法是⽤虚基类. class B1 : virtual public B, class B2:virtual public B, D则为class D : public B1, public B2. 这样D中的成员只包含⼀份B的成员使得不会产⽣⼆义性.⽐较(java). java中是直接覆盖. 不给机会这么复杂, 还要保存基类同名的东西. 同名的就直接覆盖, 没有同名的就直接继承.虚基类的加⼊, 也影响到类的初始化顺序. 原则是每个派⽣类的成员化初始化列表都必须包含对虚基类的初始化.最终初始化的时候, 只有真正实例化对象的类的调⽤会起作⽤. 其它类的对虚基类的调⽤都是被忽略的. 这可以保证虚基类只会被初始化⼀次.c++没有显式接⼝的概念, 我觉得是c++语⾔的败点. 这也是导致c++要⽀持组件级的重⽤⾮常⿇烦. 虽然没有显式的接⼝, 但c++中的纯虚函数以及抽象类的⽀持, 事实上是等同于接⼝设施的. 当⼀个类中, 所有成员函数都是纯虚函数, 则该类其实就是接⼝.java c++接⼝类(所有成员函数都是纯虚函数)抽象类类(部分函数是虚函数)对象类对象类C++构造函数调⽤顺序1. 如果类⾥⾯有成员类,成员类的构造函数优先被调⽤;2. 创建派⽣类的对象,基类的构造函数优先被调⽤(也优先于派⽣类⾥的成员类);3. 基类构造函数如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序⽽不是它们在成员初始化表中的顺序;4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调⽤顺序是对象在类中被声明的顺序⽽不是它们出现在成员初始化表中的顺序;5. 派⽣类构造函数,作为⼀般规则派⽣类构造函数应该不能直接向⼀个基类数据成员赋值⽽是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。
C++第三章课后答案C++第三章习题及答案1、什么是结构化程序设计?它有什么优缺点?所谓结构化程序设计,是一种自顶而下、逐步求精的模块化程序设计方法。
2、什么是对象?什么是类?简述对象与类之间的关系!对象是系统中用来描述客观事物的一个实体,它是用于构成系统的一个基本单位,而系统可以看作是由一系列相互作用的对象组成。
类定义了同类对象的公共属性和行为,属性用数据结构表示,行为用函数表示!《类=数据结构+对数据进行操作的函数》。
对象和类的关系相当于元素和集合的关系、变量和变量的“数据类型”的关系。
从程序设计的角度来说,类是一种复杂的自定义数据类型,对象是属于这种数据类型的变量。
3、什么是面向对象程序设计?面向对象程序设计方法具有哪些基本特征?请比较面向对象程序设计和面向对象过程程序设计有何异同?4、何谓成员变量?何谓成员函数?C++将对象的属性抽象为数据成员,将对象的行为抽象为成员函数。
5、C++中结构和类之间有何异同?结构在默认情况下的成员是公共的,而类在默认情况下的成员是私有的。
在C++中,结构是特殊的类。
6、在C++中如何定义类?如何实现定义的类?如何利用类申明对象?7、类的成员的访问权限有哪几种?请说明它们分别有什么作用?三种,privte:类的私有成员,只能被本类的成员函数访问或调用。
Public:公有成员,可以被本类的成员或其他类的成员函数(通过对象)访问或调用。
Protected:保护成员,可以被本类的成员函数或派生类的成员函数访问或调用。
8、何谓构造函数?何谓析构函数?请说明它们分别有什么作用?构造函数:不需要用户程序调用,就能在创建对象时由系统自动调用,其作用是在对象被创建时利用初始值去构造对象,使得在声明对象时就能自动完成对象的初始化。
析构函数:在对象的生存周期即将结束时由系统自动调用的,其作用是用来在对象被删除前做一些清理工作和数据保存工作。
9、如何定义一个内联成员函数?内联函:内联函数必须是和函数体申明在一起,才有效。
构造函数可以是虚函数吗?构造函数和析构函数可以调⽤虚函数
吗?虚表和虚表指针的概念
构造函数不可以是虚函数。
因为类的虚函数表指针是在构造函数中初始化的,在虚表指针没有被正确初始化之前,我们不能调⽤虚函数。
构造函数和析构函数也不能调⽤虚函数,前者是因为虚表指针还没有被初始化,后者是因为虚表指针可能已经被析构了。
0i
存在虚函数的类都有⼀个⼀维的虚函数表,简称虚表。
类的每个对象都有⼀个指向虚表开始的虚表指针。
虚表是和类对应的,虚表指针是和对象对应的。
抽象类是指⾄少包含⼀个纯虚函数的类。
编译器在编译的时候发现类⾥有虚函数就会为该类创建⼀个虚表。
编译器另外还为每个类的对象提供⼀个虚表指针,这个指针指向对象所属的类的虚表。
在虚表指针没有被正确初始化之前,我们不能调⽤虚函数。
虚表的创建和虚表指针的初始化都是在构造函数中完成的。
派⽣类的虚函数表存放重写的虚函数,当基类的指针指向派⽣类的对象时,调⽤虚函数时都会根据虚表指针来选择虚函数。
⽽基类的虚函数在派⽣类中已经被重写了,因此只能调⽤派⽣类的虚函数版本了。
**这也揭⽰了虚析构函数的作⽤,当基类指针指向派⽣类对象时,调⽤虚析构函数会先调⽤派⽣类的析构函数。
如果析构函数不是虚函数,那么基类指针调⽤析构函数时只会调⽤基类的析构函数。
重载函数属于编译时多态,虚函数属于运⾏时多态。
一、构造函数不能为虚函数的理由:1,从存储空间角度虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。
问题出来了,如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
2,从使用角度虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。
构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。
所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。
而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3、构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。
这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数5、当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。
因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。
当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
所以它使用的V P T R必须是对于这个类的V TA B L E。
而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,V P T R将保持被初始化为指向这个V TA B L E, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的V TA B L E,等.直到最后的构造函数结束。
C++基抽象类的构造析构(纯)虚函数⼀、析构函数可定义为⼀、析构函数可定义为纯虚函数纯虚函数,但也必须给出函数定义,但也必须给出函数定义 Effective C++ 条歀07: 为多态基类声明virtual 析构函数(Declare destructors virtual in polymorphic base classes ) 在某些类⾥声明纯虚析构函数很⽅便。
纯虚函数将产⽣抽象类——不能实例化的类(即不能创建此类型的对象)。
有些时候,你想使⼀个类成为抽象类,但刚好⼜没有任何纯虚函数。
怎么办?因为抽象类是准备被⽤做基类的,基类必须要有⼀个虚析构函数,纯虚函数会产⽣抽象类,所以⽅法很简单:在想要成为抽象类的类⾥声明⼀个纯虚析构函数。
1 //这⾥是⼀个例⼦:2 class awov {3 public :4 virtual ~awov() = 0; // 声明⼀个纯虚析构函数5 }; 这个类有⼀个纯虚函数,所以它是抽象的,⽽且它有⼀个虚析构函数,所以不会产⽣析构函数问题。
但这⾥还有⼀件事:必须提供纯虚析构函数的定义: awov::~awov() { ... } // 纯虚析构函数的定义 这个定义是必需的,因为虚析构函数⼯作的⽅式是:最底层的派⽣类的析构函数最先被调⽤,然后各个基类的析构函数被调⽤。
这就是说,即使是抽象类,编译器也要产⽣对~awov 的调⽤,所以要保证为它提供函数体。
如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
⼆、? 关于C++为什么不⽀持虚拟构造函数,Bjarne 很早以前就在C++Style and Technique FAQ ⾥⾯做过回答 Avirtual call is a mechanism to get work done given partialinformation. In particular, "virtual" allows us to call afunction knowing only an interfaces and not the exact type of theobject. To create an object you need complete information.Inparticular, you need to know the exact type of what you want tocreate. Consequently, a "call to a constructor" cannot bevirtual. 含义⼤概是这样的:虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝⽽不知道对象的确切类型。
常见的不不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。
1、为什么C++不支持普通函数为虚函数?普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。
2、为什么C++不支持构造函数为虚函数?这个原因很简单,主要是从语义上考虑,所以不支持。
因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。
另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。
(这不就是典型的悖论)3、为什么C++不支持内联成员函数为虚函数?其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。
(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)4、为什么C++不支持静态成员函数为虚函数?这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。
5、为什么C++不支持友元函数为虚函数?因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
*********************************************************************1、顶层函数:多态的运行期行为体现在虚函数上,虚函数通过继承方式来体现出多态作用,顶层函数不属于成员函数,是不能被继承的。
2、构造函数:(1)构造函数不能被继承,因而不能声明为virtual函数。
(2)构造函数一般是用来初始化对象,只有在一个对象生成之后,才能发挥多态的作用,如果将构造函数声明为virtual函数,则表现为在对象还没有生成的情况下就使用了多态机制,因而是行不通的,如下例:#include <iostream>using namespace std;class B{public:B() {}virtual void show(){cout<<"***"<<endl;}};class D:public B{public:D() {}void show(){cout<<"==="<<endl;}};int main(void){B *pb;D d; //先生成对象pb=&d;pb->show(); //再体现多态pb=new D(); //先调用构造函数pb->show(); //再多态delete pb;return 0;}3、static函数:不能被继承,只属于该类。
1.构造函数与析构函数可不可以为虚函数构造函数不能声明为虚函数,析构函数可以声明为虚函数,而且有时是必须声明为虚函数。
不建议在构造函数和析构函数里面调用虚函数。
构造函数不能声明为虚函数的原因是:1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。
而在构造一个对象时,由于对象还未构造成功。
编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。
无法确定。
2 虚函数的执行依赖于虚函数表。
而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。
而在构造对象期间,虚函数表还没有被初始化,将无法进行。
虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。
然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。
(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。
析构函数设为虚函数的作用: 解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
2.堆栈溢出如果堆栈的顶指针超出了定义的堆栈范围,将出现堆栈溢出,这是灾难性的。
您可以在写程序的时候定义一个顶指针的最大值,每次压栈都判断一次就可以了。
3内存泄露指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。
内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
堆内存泄漏对内存指的是程序运行中根据需要分配通过等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。
如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.系统资源泄露主要指程序使用系统分配的资源比如Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
一、构造函数不能为虚函数的理由:
1,从存储空间角度
虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。
问题出来了,如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
2,从使用角度
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。
构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。
所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个
成员函数。
而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3、构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它
但析构却不一定,我们往往通过基类的指针来销毁对象。
这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数
从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数
5、当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。
因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。
当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
所以它使用的V P T R必须是对于这个类的V TA B L E。
而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,V P T R将保持被初始化为指向这个V TA B L E, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的V TA B L E,等.直到最后的构造函数结束。
V P T R的状态是由被最后调用的构造函数确定的。
这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。
但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置V P T R
指向它自己的V TA B L E。
如果函数调用使用虚机制,它将只产生通过它自己的V TA B L E的调用,而不是最后的V TA B L E(所有构造函数被调用后才会有最后的V TA B L E)。