📄 c++11.dat
字号:
double f();
double g();
};
class B : public A {
private:
double u;
public:
B(int x, double z, double y): A(x,z), u(y) {}
~B() {}
double g(); // 重新定义A的成员函数g
double h(); // 增加一个新的接口函数h
};
我们应该注意到:A的构造函数被用在B的构造函数的初始化表中.B的公有接口包括函数f(从A继承)、函数g(从A继承,但被重新定义)和函数h(在B中新增加的).B的成员函数能够使用A的public成员(即函数f和g)和保护成员(即数据成员w),但是,不能访问A的数据成员x,因为它是私有的.
例如,B的成员函数g的中,调用A的成员函数g和数据成员w:
double B::g()
{
double res1 = A::g();
// 调用A中的函数g
double res2 = h();
// 调用B中的函数h
w = w+res1+res2+u+f();
// 修改w
return res1;
}
在B的成员函数中调用A的成员函数时,要用域运算符::,这是因为A中的成员函数g与B中的成员函数g的原型完全相同,如果不加域运算符,虽不会有语法错误,但编译器便会调用B中g成员函数,而与编程意图不符.
一、类内访问说明符
二、继承访问说明符
这里要明确几点:
1. 基类的私有成员不能被派生类访问.
2. 公有继承时,派生类原样继承基类的公有成员和保护成员,它们的属性在派生类中没有改变.
3. 保护继承时,基类的公有成员和保护成员,变成派生类的保护成员,它只能在派生类内被访问,但能够被该派生类的派生类访问.
4. 私有继承时,基类的公有成员和保护成员,变成派生类的私有成员.
在一般情况下,我们都是用public继承,这样使得基类的接口也是派生类的接口.当我们想隐藏这个类的基类部分的功能时,我们可以用private继承.当私有继承时,基类的所有公有和保护成员都变成派生类的私有成员,如果希望它们中任何一个成为public,只要用派生类的public选项声明它们的名字即可.
1.3 函数的隐藏与覆盖
我们已经知道了函数的重载是怎么回事,重载的函数名字相同,但它们的参数个数和类型不同.函数的隐藏和覆盖,与函数的重载不同,它们只是在继承的情况下才存在.
如果在派生类中定义了一个与基类同名的函数,也就是说为基类的成员函数提供了一个新的定义,有两种情况:
* 在派生类中的定义与在基类中的定义有完全相同的信号(signature)(即参数个数与类型均相同)和返回类型,对于普通成员函数,这便称之为重定义;而对于虚成员函数(在本章的后面介绍),则称之为覆盖.
* 在派生类中,改变了成员函数参数表与返回类型.此时会出现什么情况?
我们看看下面的实例.
例1
// 在派生类中,隐藏重载的函数名
#include <iostream.h>
class Base
{
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(char *) const { return 1; }
void g() {}
};
class Derived1 : public Base
{
public:
void g() const {}
};
class Derived2 : public Base
{
public:
// 函数f被重定义
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base
{
public:
// 改变函数f的返回类型
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base
{
public:
// 改变函数f的参数表
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
void main()
{
char s[]="hello";
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//! d2.f(s); // 基类带char *参数的成员函数f被隐藏
Derived3 d3;
//! x = d3.f(); // 基类带返回int类型的成员函数f被隐藏
Derived4 d4;
//! x = d4.f(); // 基类带成员函数f被隐藏
x = d4.f(1);
}
第二节 虚函数
2.1 虚函数的定义与使用
虚函数的定义很简单,只要在成员函数原型前加一个关键字virtual即可.如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字.
2.2 多态的实现
这一部分,我们简要地介绍一下在C++中多态是怎样实现的.
多态的基本思想是:在编译时,C++编译器不知道调用哪一个函数,而要到运行时确定.这意味着应把函数的入口地址保存在某一个地方,以便于在调用前查询,而存储函数入口地址的地方也应能被相关的对象访问.例如,一个Vehicle * unicycle指针指向car对象,然后,unicycle->message()调用car的成员函数,这个函数的入口地址由unicycle指向的对象决定.
在C++中,一般实现方法如下:包含虚函数的对象,增加了一个隐含的数据成员,且是它的第一个数据成员,该数据成员指向一个指针数组,而指针数组存储对象的虚函数地址,需要说明的是这个实现与具体的编译器有关.
某一个类的虚函数地址表被该类的所有对象共享,甚至有可能两个类共享同一个虚函数地址表.内存开销包括:
* 每一个对象增加了一个额外的数据成员.
* 每一个类有一个指针表,用于存储该类各虚函数的地址.
所以,unicycle->message()的调用过程是:首先检查unicycle指向的对象的隐含的数据成员,在我们前面所举的例子中,该数据成员指向的指针表只有一个元素,即message函数的入口地址,被调用的函数根据指针表确定.
有虚函数的的对象的内部组织,我们可以用11-4的示意图来说明:
正象我们在图11-4中看到的,有虚函数的所有对象均有一个隐含的指针数据成员,且指向存放虚函数入口地址的指针表.类Vehicle对象与truck对象共用一个表,而car和boat有自己的message函数,所以,它们需要自己的虚函数指针表.
virtual将一个成员函数说明为虚函数,对于编译器来讲,它的作用是告诉编译器,这个类含有虚函数,对于这个函数不使用静态联编,而是使用动态联编机制.编译器就会按照动态联编的方案进行一系列的工作.
对于每个包含虚函数的类,编译器都为其创建一个表(称之为VTABLE表).在VTABLE表中放置的是每个类自己的虚函数地址,在每个包含虚函数的类中放置了一个指针(VPTR),指向VTABLE表.通过基类指针调用虚函数时,编译器会在函数调用的地方插入一段特定的代码.这段代码的作用就是得到VPTR,找到VTABLE,并在VTABLE表中找到相应的虚函数地址,然后进行调用.
第三节 抽象类
有的时候我们的基类只是起到接口的作用,没有必要生成该类的对象,该类本身也不需要对函数进行实现,这样的基类称之为抽象类.在C++中,有一种虚函数称之为纯虚函数,含有纯虚函数的类,就是抽象类.C++编译器不允许用抽象类创造对象,它只能被其它类继承.要定义抽象类,就必须定义纯虚函数,它实际上是起到一个接口的作用.
如果试图使用抽象类创造对象,编译器会给出错误信息.另外,由抽象类派生的类,必须对抽象类定义的纯虚函数进行实现.
当一个类声明了纯虚函数后,首先,编译器知道这个函数应使用动态联编,然后它会为这个类建立VTABLE表.由于这个函数是纯虚函数,没有实现,所以没有函数指针.编译器遇到这种情况会在VTABLE表中为它留下一个间隔,即一个函数指针大小的空间,不放任何东西.只要类中声明了一个纯虚函数,这个类的VTABLE表就是不完全的.编译器会禁止使用这个类创建对象.
虽然定义了纯虚函数的类是抽象类,不能用这个类创建对象,但是这不意味这这个类所有的函数都是纯虚函数,在该类里的其它函数就失去了意义.事实上,我们希望把公共的代码放在尽可能靠近基类的地方,这样不仅节省了空间,而且能使修改变得更加容易.
在基类中我们可能希望一段代码对于大部分或者所有的派生类都能使用,而不希望在每个类中重复这样的代码.在这种情况下,我们可以对纯虚函数进行实现.虽然纯虚函数有实现部分,但是,仍然不允许用抽象类创建对象.
下面是一个有纯虚函数实现例子:
例1
#include <iostream.h>
class A
{
public:
int a;
A():a(0){}
virtual void func1() =0 {
cout<<"A::func1"<<endl;
}
virtual void func2() =0;
};
void A::func2()
{
cout<<"A::func2"<<endl;
}
class B:public A
{
public:
int a;
B():a(0){}
virtual void func1(){
A::func1();
cout<<"B::func1" <<endl;
}
virtual void func2(){
A::func2();
cout<<"B::func2"<<endl;
}
virtual void func3(){};
};
void main()
{
A *pa=new B();
pa->func1();
pa->func2();
}
程序运行结果是:
A::func1
B::func1
A::func2
B::func2
第四节 设计继承
继承是软件复用的一个重要方法,我们在设计基类和派生类时,必须考虑如何复用基类.设计类时,有一些基本的原则需要遵循:
(1) 类应该定义有:
* public接口,供派生类或其它类使用.
* 友元类或友元函数(如果需要地话).
* protected接口,供派生类使用.
* private部分.
(2) 如果对象创建了受管的资源(例如申请了堆内存),必须提供析构函数释放该资源.
(3) 在继承链中,如果用基类的指针操作对象.要保证调用正确的析构函数,就必须把基类的析构函数应定义为虚函数.
(4) 如果一个类包含指向另一个对象的指针数据成员,应为该类提供复制构造函数,通常还要提供一个重载的赋值运算符,以确保对象能够被正确地复制和赋值.
(5) 被派生类覆盖的成员函数,通常定义为虚函数.
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -