C++继承
继承(inheritance)是面向对象的主要特征(此外还有封装和多态)之一,它使得一个类可以从现有类中派生,而不必重新定义一个新类。继承的实质就是用已有的数据类型创建新的数据类型,并保留己有数据类型的特点,以旧类为基础创建新类,新类包含了旧类的数据成员和成员函数,并且可以在新类中添加新的数据成员和成员函数。旧类被称为基类或父类,新类被称为派生类或子类。这是继数据封装和信息隐藏性质的类的抽象性编程的又一杰作。有了继承,过去的代码就可以不被丢弃,只要经过稍加修改就可重用。类的继承就是反映了原始的简单代码慢慢发展到丰富的高级代码的过程。程序越来越完善,功能越来越强,人们不是通过外在的代码复制和保存,而是通过语言内在的继承功能,自动地、滚动式地重用代码,增强代码,使得编程的方法开始根本改变,分析问题和解决问题的模式从功能模式转向对象结构模式。
派生类总是依附于基类,派生类对象中总是含有基类对象,即含有基类的数据成员。或者说,基类对象是派生类对象的组成部分。至于在具体的实现中空间的安排,并不一定基类排在前,派生类排在后。显然,派生类对象一定不会比基类对象小,基类也称超类,派生类也称子类,它保存了更多的数据,提供了更多的操作。
1.类的单继承的形式如下:
class 派生类名标识符:[继承方式]基类名标识符
{
[访问控制修饰符:]
[成员声明列表]
};
类的多继承形式如下:[继承方式]基类名标识符,[继承方式]基类名标识符
{
[访问控制修饰符:]
[成员声明列表]
};
多继承和单继承的差异不仅是是否仅有一个基类的问题,单继承永远不会存在对象切片问题,如果使用时不注意,多继承出现对象切片是常见的。
继承方式有3种派生类型,分别为公有型(public )、保护型(protected)和私有型(private);访问控制修饰符也是public,protected,private 3种类型;成员声明列表中包含类的成员变量及成员函数,是派生类新增的成员。":"是一个运算符,表示基类和派生类的继承关系。
公有继承,反映了派生类对基类使用方式的全部接受(除私有),在此基础上进行扩充,以便能被外界更广泛地使用。因此,通过派生类对象,仍然可以使用基类中原来公有的操作。派生类获得了所有基类成员原封不动的访问控制权限的继承。继承了基类,并不是说派生类就能访问基类的私有成员了。如果是那样的话,那些使用基类的人,靠派生就能达到访问基类私有成员的变态目的了。
在类中,还有一种保护(protected )型的访问控制符,保护成员与私有成员一样,不能被使用类的程序员进行公共访问,但可以被类内部的成员函数访问。除此之外用的类是派生类成员,则可以被访问,这是私有成员所不具有的能力。也就是说如果使只要将类成员声明为保护成员,则其派生类在继承之后,就可以坐享其父类的公有和保护操作了。
2.继承后可访问性
继承方式有public, private, protected 3种,基类的私有成员在派生类采用任何继承方式下都是隔离的,也就是视派生类为外人,必须通过基类的保护或公有成员函数去访问它们。除此之外,公有继承将基类的保护成员和公有成员视为自己的保护和公有成员。保护继承则将基类的保护和公有成员全变为自己的保护成员。
其说明如下:
public(公有继承)
在公有继承中,基类的每个成员在子类中保持同样的访问方式。即在基类中为public的成员,在派生类中也为public成员。基类中为protected成员在派生类中也为protected成员,惟有基类的private成员到了派生类中就变得不可访问等于将派生类看做是使用基类的任何外部函数,只能通过基类的保护和公有成员函数的了去访问其私有成员。任何引用基类指针(引用)的函数可接受一个派生类对象的指针(引用)这种使用方式,仅对public继承才成立,private继承则描述完全不同的另类关系。
private(私有型派生)
私有型派生表示对于基类中的public, protected数据成员和成员函数,在派生类中可以访问。基类中的private数据成员,在派生类中不可以访问。私有继承中,基类中的所有Public,protected成员函数会成为派生类中的private成员。派生类和基类不是is_a关系,而是一种实现关系。如果声明class Drived:private Base,只能说明你对class Base的某些特性感兴趣,而不是因为在class Drived和class Base的对象之间有什么概念上的关系。所以尽量不要用private继承。is_a:基类的所有实现,派生类都是满足的。
protected(保护型派生)
保护型派生表示对于基类中的public, protected数据成员和成员函数,在派生类中均为protected。protected类型在派生类定义时可以访问,用派生类声明的对象不可以访问.也就是说在类体外不可以访问.protected成员可以被基类的所有派生类使用.这一性质可以沿继承树无限向下传播。
不管是public,private,还是protected基类成员对其对象都是公有成员可见,私有和保护不可见。派生类可继承公有和受保护成员,而私有不可见(不能继承私有成员)。
因为保护类的内部数据不能被随意更改,实例类本身负责维护,这就起到很好的封装性作用。把一个类分作两部分,一部分是公共的,另一部分是保护的,保护成员对于使用者来说是不可见的,也是不需了解的,这就减少了类与其他代码的关联程度。类的功能是独立的,它不依赖于应用程序的运行环境,既可以放到这个程序中使用,也可以放到那个程序中使用。这就能够非常容易地用一个类替换另一个类。类访问限制的保护机制使人们编制的应用程序更加可靠和易维护。
对于一个成熟的类设计来说,数据成员往往只是私有的,公有的不多见,那都是为了一时方便的权宜之计。而保护数据成员则更见不到,因为保护数据更多的是用于类设计中的待定考虑,倒是经常能见到保护的成员函数,它是隐蔽在类内部衔接父子类关系的桥梁。
3.派生类的构造
派生类也是类,如果没有定义构造函数,则根据类机制,将会执行默认的无参构造函数。派生类的默认无参构造函数会首先调用父类的无参构造函数,如果父类定义了有参构造函数(因此没有默认无参构造函数),又没有重载定义无参构造函数,则会导致编译发怒。如果父类还有父类,则父类会先调用父类的父类的无参构造函数,以此递归。
在构造一个子类时,完成其基类部分的构造由基类的构造函数去做,将基类对象看作是完全独立于派生类的对象。这样做的好处是,一旦基类的实现有错误,只要不涉及界面,那么,基类实现中的修改不会影响派生类的操作。类与类之间,你做你的,我做我的,职责分明,即使有父子继承关系的类之间也不例外。
在有继承的初始化过程中,如果要实现本类的构造,首先要完成基类的构造,然后才能进行本类的构造。因为继承对象至少可以被"低"看为一个基类对象,具有基类的所有行为和表现,所以基类的构造函数首先被调用。如果所谓的基类也是从其他类继承过来的,这就形成了一个调用链。最后的情况是,最基础的类的构造函数首先被执行,然后才是上一层的构造函数,如此到最外层的继承类。这个过程必须是严格有序的。如果没有这个次序保证,继承类就有机会在基类还没有构建好的情况下访问基类的数据或函数,这将导致不可预料的,灾难性结果。
由于父类和子类中都有构造函数和析构函数,当从父类派生一个子类并声明一个子类的对象时,它将先调用父类的构造函数,然后调用当前类的构造函数来创建对象;在释放子类对象时,先调用的是当前类的析构函数,然后是父类的析构函数。
在分析完对象的构建、释放过程后,会考虑这样一种情况:定义一个基类类型的指针,调用子类的构造函数为其构建对象,当对象释放时,是直接调用父类的析构函数还是先调用子类的析构函数,再调用父类的析构函数呢?答案是如果析构函数是虚函数,则先调用子类的析构函数,然后再调用父类的析构函数;如果析构函数不是虚函数,则只调用父类的析构函数。可以想象,如果在子类中为某个数据成员在堆中分配了空间,父类中的析构函数不是虚成员函数,将使子类的析构函数不被调用,其结果是对象不能被正确地释放,导致内存泄漏的产生。因此,在编写类的析构函数时,析构函数通常是虚函数(是在多态基类的情况下)。构造函数调用顺序不受基类在成员初始化表中是否存在以及被列出的顺序的影响。
全部子对象的初始化列表做完后,就开始执行自身的构造函数体,这就是构造函数执行的递归顺序。因此,对象空间是在构造函数体执行前就分配完成的。另外,析构函数的执行顺序,在有基类的情形下,也是与构造的顺序严格相反的。
1.单继承类在进行对象构造时,总是从类的最根本处开始,由深入浅,先基类后子类,层层构造,这个顺序不能改变。多继承类在进行对象构造时,总是从类的最根本处开始,由深及浅,先基类后子类(其中基类的构造顺序按照声明的顺序由前及后),层层构造,这个顺序不能改变。
2.单继承类和多继承类在析构时,析构函数的执行顺序是构造顺序的严格逆序。
二.继承与组合
类含有对象成员的情形称为组合(composition ),例如,程序f1002.cpp中的GraduateStudent类中含有Advisor对象。GraduateStudent同时又继承了Student类,所以一个GraduateStudent对象中既含有Student对象,又含有Advisor对象,只是包含对象的方式不同,一个是继承式包含,另一个是组合式包含。
继承和组合都重用了类设计。因为继承重用,Student对象,而组合重用,只是包含了其中一个所以在性质上GraduateStudent对象就是Advisor成分而己,即有一个Advisor对象。
组合和继承并不是绝对的,组合完全可以用继承来实现,继承也可以由组合来实现,无非在继承中可以通过调整成员的访问控制属性,以达到方便编程的目的。而组合则职责分明,虽然可能麻烦一点,但调试很直截了当。
三.虚函数
在类的继承层次结构中,在不同的层次中可以出现名字、参数个数和类型都相同而功能不同的函数。编译器按照先自己后父类的顺序进行查找覆盖,如果子类有与父类相同原型的成员函数时,要想调用父类的成员函数,需要对父类重新引用调用。虚函数则可以解决子类和父类相同原型成员函数的函数调用问题。虚函数允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
在基类中用virtual声明成员函数为虚函数,在派生类中重新定义此函数,改变该函数的功能。在C++语言中虚函数可以继承,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,但如果派生类没有覆盖基类的虚函数,则调用时调用基类的函数定义。
虚函数就是以virtual关键字声明的基类函数。一旦标记基类的函数为虚函数,便有连锁反应,后面继承的类中一切同名成员函数都变成了虚函数。因此,在声明派生类的虚函数时,可以添加virtual,也可以不添加。如果在派生类中没有对基类的虚函数重新定义,则派生类将直接继承基类中的虚函数。基类与派生类的同名操作,只要标记上victual(虚拟),则该操作便具有多态性。虚函数是实现多态性的一个极为重要的机制。在交互式应用程序中,常常不能事先确定要处理哪种类型的对象,即在设计或编译期间不确定类型,只能在运行期间确定。
如果是引发实际复制动作的传递,则子类对象完全变成基类对象了,这时候,便不会再有悬念了,即不会有多态了。例如:
void fn (Student a);//传值
void gn(){
Student s;
GraduateStudent gs;
fn(s);
fn(gs);//无多态可言
因为在参数传递的过程中己经将对象的性质做了肯定的转变。而对于确定的对象,是没有选择操作可言的。因此说白了,就是仅仅对于对象的指针和引用的间接访问,才会发生多态现象。
构造函数无论在什么时候都不能声明为virtual函数
(1)从存储空间分析
如果一个类只存在一个virtual函数,那么此类在创建时就会创建一个vtable。类就是通过此表确定函数调用的,也就是动态绑定。这个vtable存储于对象的内存空间。这样问题就出现了,如果构造函数是虚的,就需要通过vtable来调用,可是这时对象还没有实例化,也就是内存空间还没有,更不可能存在vtable,无法找到vtable,所以构造函数不能是虚函数。
(2)从使用角度分析
虚函数的作用在于通过父类的指针或引用来调用它时,能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或引用去调用,因为这是虚函数机制还未成形。
(3)从实际含义上看
在调用构造函数时还不能确定对象的真实类型(因为子类调用父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。
(4)当一个构造函数被调用时,它首先要做的事情是初始化它的vtable指针。因此,它只能知道它是"当前"类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码-------既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的vtable指针必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,vtable指针将保持被初始化为指向这个VTABLE,但如果接着琮有一个更晚派生的构造函数被调用,这个构造函数又将设置vtable指针指向它的VTABLE等,直到最后的构造函数结束。vtable指针的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更晚派生类顺序的另一个理由。但是,当这一系列构造函数调用发生时,每个构造函数都已经设置vtable指针指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数的调用 需要class构造完成之后才会有最后的VTABLE)
构造函数不能声明为虚函数,因为虚函数的调用需要class构造完成之后方可调用。
避免在构造函数调用虚函数
原因:基类的构造函数会在派生类构造函数执行之前被调用,所以当基类构造函数运行时,派生类的数据成员都没有进行初始化,如果基类在构造过程中对虚函数的引用传递到派生类了,这些虚函数就会引用派生类的数据成员,但这些数据成员都没有进行初始化,所以此虚函数的运行结果将是无法预测的。这将导致未定义行为。另外,虚函数的调用机制完全由基类控制,所以如果基类没有完成构造,虚函数的调用机制(即虚函数表)没有完成初始化。构造函数中调用虚函数,此调用不会沿着继承调用向下传递,所以无论在构造函数中调用的函数是否为虚函数,最终调用的都是本类的函数。
同样,在对象的析构期间,同样存在构造函数所遇到的问题。在析构函数中调用虚函数也是一件危险的事情,因为按照析构函数的调用机制,当一个派生类析构进,其本身的析构函数将被调用,此后派生类的数据成员都变成未定义的值。这时一旦进入基类的析构函数,该对象就变成了一个基类的对象,而不是派生类的对象了。不仅如此,C++中的其他各个部分都是这样处理的。
C++析构函数为什么要为虚函数?
#include<iostream>
using namespace std;
class Base
{
public:
virtual ~Base() //虚析构函数
{
cout<<"~Base"<<endl;
}
};
class Derived:public Base
{
public:
virtual ~Derived() //又是虚拟析构函数
{
cout<<"~Derived"<<endl;
}
};
int main()
{
Base *pb = new Derived;
delete pb;
return 0;
}
输出结果为:~Derived
~Base
如果析构函数不为虚,那么输出结果为:~Base
原因:如果析构函数为虚函数的话,则在释放pd时,根据虚函数的多态性,这会调用派生类的虚析构函数。如果不声明析构函数为虚函数,则只会调用基类的析构函数。当派生类中有动态内存分配时,我们应该将派生类的析构函数声明为虚函数。但有一个前提,就是当基类中必须有至少一个虚函数的情况下。因为如果其他成员函数不为virtual函数时,会在基类和派生类引入vtable,而引入VPTR会造成运行时的性能损失。如果确定不需要直接而只是通过派生类对象使用基类,可以把析构函数定义为protected.如果不想让外面的用户直接构造一个类(假设这个类的名字为A)的对象,而希望用户只能构造这个类A的子类,那就可以将类A的构造函数/析构函数声明为protected,而将类A的构造函数/析构函数声明为public.
虚函数有以下几方面限制:
(1)只有类的成员函数才能为虚函数。
(2)静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
(3)内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时,仍将其看做是非内联的
(4)构造函数不能是虚函数,因为构造是,对象还是一片未定型的处女地,只有在构造完成后,对象才能成为一个类的名副其实的对象,析构函数通常是虚函数。
四.类型转换
1.动态转换(dynamic_cast)
通常在基类和派生类之间转换时使用。dynamic_cast操作是专门针对有虚函数的继承结构来的,它将基类指针转换成想要的子类指针,以做好子类操作的准备,因为各个不同的子类,其操作可能是不同的。例如:
Student* pS = dynamic_cast<Student*>(gs);
2.静态类型(static_cast)
相对动态类型转换,静态类型转换则做范围更广的转换,但前提必须是相关的类型,也就是说,编译器必须认为可理解。例如,一个非多态的类层次结构,祖孙对象的指针互易。如,研究生对象的指针到学生对象的指针,或反之。由于void*到任何类型的指针都可以进行相融性转换,所以,void*到学生对象的指针转换也可以由static_cast来进行,还有从局部堆空间①申请的空间转换为整型数组空间等。甚至有时候,要将void*转到多态类对象的指针,也要先经过static_cast过渡一下
static_cast转换并不是专门针对指针的,它主要是针对确定的类型,而不是针对多态。只要是相关类型的转换,都可以操作。无非关于多态的类型转换由dynanuc_cast去做。
3.常量类型(const_cast)
编译是计较常量或常对象的写操作的。因此,如果将常量或常对象的地址赋给指针,那是绝对不干的。
const int a = 1;
int& ra = a; //错
int* pa = &a; //错
const int& cra = a; //OK
const int* cpa = &a; //OK
int b = 2;
int& rb = b; //OK
int* pb = &b; //OK
const int& rb = b; //OK
const int* pb = &b; //OK
也就是说,从type类型转换到const type类型是允许的。意思是,在作为参数传递到函数后,具有对参数使用的写约束作用。但对原来const_type类型的,拒绝转换到type。原因也是清楚的,因为常量或者常对象的地址托付给变量或者对象的指针和引用,简直是拿艺术品给小孩玩—有很大的损坏危险。所以凡是以这样的形式进行参数传递,休想让编译通过。
(4)reinterpret_cast<T*>(a)
编译器在编译期处理
任何指针都可以转换成其它类型的指针,T必须是一个指针、引用、算术类型、指向函数的指针或指向一个类成员的指针。
表达式reinterpret_cast<T*>(a)能够用于诸如char* 到 int*,或者One_class* 到 Unrelated_class*等类似这样的转换,因此可能是不安全的。
Cpp代码
class A { ... };
class B { ... };
void f()
{
A* pa = new A;
void* pv = reinterpret_cast<B*>(pa);
// pv 现在指向了一个类型为B的对象,这可能是不安全的
...
}
使用reinterpret_cast 的场合不多,仅在非常必要的情形下,其他类型的强制转换不能满足要求时才使用。
== ================================================
static_cast .vs. reinterpret_cast
== ================================================
reinterpret_cast是为了映射到一个完全不同类型的意思,这个关键词在我们需要把类型映射回原有类型时用到它。我们映射到的类型仅仅是为了故弄玄虚和其他目的,这是所有映射中最危险的。(这句话是C++编程思想中的原话)
static_cast 和 reinterpret_cast 操作符修改了操作数类型。它们不是互逆的;
static_cast 在编译时使用类型信息执行转换,在转换执行必要的检测(诸如指针越界计算, 类型检查). 其操作数相对是安全的。
另一方面;reinterpret_cast是C++里的强制类型转换符,操作符修改了操作数类型,但仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换。
例子如下:
int n=9;
double d=static_cast < double > (n);
上面的例子中, 我们将一个变量从 int 转换到 double。这些类型的二进制表达式是不同的。 要将整数 9 转换到 双精度整数 9,static_cast 需要正确地为双精度整数 d 补足比特位。其结果为 9.0。
而reinterpret_cast 的行为却不同:
int n=9;
double d=reinterpret_cast<double & > (n);
这次, 结果有所不同. 在进行计算以后, d 包含无用值. 这是因为 reinterpret_cast 仅仅是复制 n 的比特位到 d, 没有进行必要的分析.
因此, 你需要谨慎使用 reinterpret_cast.
reinterpret_casts的最普通的用途就是在函数指针类型之间进行转换。
例如,假设你有一个函数指针数组:
typedefvoid(*FuncPtr)();//FuncPtr is一个指向函数的指针,该函数没有参数,返回值类型为void
FuncPtrfuncPtrArray[10];//funcPtrArray是一个能容纳10个FuncPtrs指针的数组
让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:
int doSomething();
你不能不经过类型转换而直接去做,因为doSomething函数对于funcPtrArray数组来说有一个错误的类型。在FuncPtrArray数组里的函数返回值是void类型,而doSomething函数返回值是int类型。
funcPtrArray[0] = &doSomething;//错误!类型不匹配
reinterpret_cast可以让你迫使编译器以你的方法去看待它们:
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);
转换函数指针的代码是不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果.