现在有一个记录身份ID的类:
//head.h
#ifndef HEAD_H_
#define HEAD_H_
#include
#include
using namespace std;
class IDcard
{
string name;
int id;
public:
IDcard(const string n = "null", const int i = 0);
friend ostream &operator<<(ostream &os, const IDcard &c);
void show()const;
~IDcard(){}
};
#endif
实现部分:
//file.cpp
#include "head.h"
IDcard::IDcard(const string n, const int i) :name(n), id(i)
{}
void IDcard::show()const
{
cout << name<
这个类对象可以记录一个人的名字和id号,show方法显示名字。它是最基础的,没什么大作用。
现在学校需要一个记录学生的类,可以记录姓名、id号、所在年级、直属老师。
然而上面的类并不能满足我们的需求,也许你可以说修改一下不就行了?
但这样需要修改的太多了,假如已经是一个大型程序的类,那么修改起来可能会出很多问题,而且也强烈不建议修改已经投入使用的类。
有两种方法可以实现:派生出一个类、包含一个类 派生出一个类,在原先有一个类的情况下,如现在有IDcard类,从这个类的基础派生出一个新的类,这个类称为派生类,原来的类称为基类。
派生类继承了基类的数据成员与接口(成员函数),派生类需要自己的构造函数,派生类可以添加新的数据成员与接口(方法、函数)。
class 派生类名 : public 基类{ //数据成员与成员函数}
例如现在派生一个Student类:
//head.h
...
class Student :public IDcard
{
string grade;
string teacher;
public:
Student(const string n = "null", const int i = 0, const string g = "null", const string t = "null");
Student(const IDcard &c, const string g, const string t);
friend ostream &operator<<(ostream &os, const Student &s);
void show()const; ~Student(){}
}
实现部分:
//file.cpp
...
Student::Student(const string n, const int i, const string g, const string t) :IDcard(n, i), grade(g), teacher(t)
{}
Student::Student(const IDcard &c, const string g, const string t) : IDcard(c), grade(g), teacher(t)
{}
ostream &operator<<(ostream &os, const Student &s)
{
os << (IDcard)s << endl;
os << "grade:\t" << s.grade << "\nteacher:\t" << s.teacher;
return os;
}
void Student::show()const
{
IDcard::show();
cout<
class Student :public IDcard 表示Student类从IDcard派生而来,IDcard是Student的基类,Student类继承IDard基类的成员。
(IDcard)s表示把对象s强制转换为Dcard对象,这样就会调用IDcard的operator<<()函数。
方法show会调用基类的show显示名称、id与成绩。
public表示派生方法是公有派生,表示基类的公有成员将成为派生类的公有成员、保护成为保护,私有成为私有,但是基类的私有部分只能通过基类的公有和保护方法访问。
派生方式: |
private派生 |
protected派生 |
public派生 |
基类private |
无法访问 |
无法访问 |
无法访问 |
基类protected |
变成派生的私有 |
变成派生的保护 |
变成派生的保护 |
基类public |
变成派生的私有 |
变成派生的保护 |
变成派生的公有 |
基类的初始化方法是使用成员初始化列表的方法直接调用基类的构造函数,如果没有调用基类构造函数,这会自动使用基类的默认构造函数。
创建派生类时的顺序是:首先创建基类对象,根据成员初始化列表调用基类的构造函数创建基类,然后对派生类各个数据成员初始化。
派生类过期时调用析构函数的顺序是:先调用派生类的析构函数,然后再调用基类的析构函数。
基类的指针或引用可以指向基类对象和由这个基类派生出的派生类:
Student zhangsan("张三",1,"二年1班","李明");IDcard lisi("李四",2);IDcard *ps=zhangsan;IDcard *pi=lisi;IDcard &stu=zhangsan;
然而基类的指针或引用只能调用基类的方法,因此不能用指针或引用调用派生类的方法。
基类的引用或指针可以指向派生类,但是派生类的引用或指针只能指向派生类,不能指向基类。
这样会产生一个有趣的现象:
例如有这样一段代码:
IDcard n1;n1=zhangsan;
这将调用n1对象的赋值构造函数,这里没有显式的提供所以会使用默认的赋值构造函数。
在这里n1会接受zhangsan对象里IDcard基类的数据部分,其他的将丢弃。
这里存在一个 is-a 关系
即“派生类对象也是一个基类对象”,可以对基类进行的操作,也可以用在基类上,因为派生类时从基类派生而来的,它继承基类的成员。
再说说基类指针或引用,它可以指向基类或从基类派生而来的对象,但是指向对象时调用的方法是哪一个呢?
IDcard *p=zhangsan;
p->show();
这里的show是基类的还是派生类的?
因为这里声明的指针类型是IDcard,所以调用的是zhangsan对象的IDcard基类的show()。
引用也是如此,指针或引用的类型决定使用的方法。
那么能够根据指向的对象类型来确定方法么?
例如:
p=n1时show使用的是IDcard类的方法,p=zhangsan时show使用的是Student的方法?
这需要使用虚函数来实现。
下面来介绍虚函数
在说虚函数前来说说函数的多态性,多态性是指一个接口,多个方法,例如函数重载,可以根据不同的参数列表来识别不同的功能的同名函数。
也可以说是同一方法对不同的对象会产生不同的效应,有点难理解,例如看看一个简单的函数重载:
void show(int a,int b){ cout<
这里重载了函数show计算两个数的和,一另一个显示n次c字符串。
在这里编译器使用了静态联编,根据不同的参数来识别函数,这样就能做到在编译的时候使用相同的函数名却不同结果的多态性。
程序调用函数时,具体应使用哪个代码块是由编译器决定的。
这个过程出现在编译阶段,编译器根据参数列表决定要调用的函数。
然而像指针对象指向的类型这就出现在运行阶段,比如你有个switch根据用户的输入来new分配内存,然后用基类指针指向
int n;
cout << "enter 1 is IDcard, 2 is Student:";
IDcard *p;
switch (n)
{
case 1:
p = new IDcard("zhangsan", 1);
break;
case 2:
p = new Student("zhangsan", 1, "one", "liming");
break;
default:
cout << "error";
break;
}
p->show();
在这里你可能希望p能够根据指向的类型来确定show方法,但是在这里永远只会使用基类的show。
有什么方法可以实现?使用虚函数(虚方法) 使用关键字virtual
虚函数的概念:在基类中的成员函数前加上一个关键字 virtual
virtual 返回值类型 函数名(形参列表)
{
//函数体
}
如果使用关键字virtual后,该方法被声明为虚,这样会程序将根据引用或指针指向的对象的类型来选择方法。
改变一下之前定义的两个show方法
virtual void show()const; //IDcard
virtual void show()const; //Student
virtual ~IDcard(){};
当用户选择1时使用的是IDcard的show,显示名字和id
当用户选择2时使用的是Student的show,显示名字、id和score
当基类定义虚函数时,例如上面的show(),在派生类里的同名、参数列表相同的函数也会自动变成虚函数,不管有没有加virtual关键字。
虚函数要注意的一些:
在基类方法的声明中使用关键字virtual可以使该方法在基类以及所有的派生类中是虚的(包括从派生类派生出来的类)。
在使用指针或引用调用虚函数时,程序将使用对象类型对应的虚函数,而不是使用指针或引用的类型的虚函数,这称为动态联编,这样基类指针可以指向派生类对象并调用派生类对象的虚函数。
基类应该把需要在派生类中重新定义的方法声明为虚(函数名、参数列表都相同)。
构造函数不能为虚,因为构造函数只对应该类,构造该类,派生类不继承基类的构造函数。
析构函数应当为虚的,除非该类不用做基类,例如:IDcard *p=char Student; delete p; 这时如果析构函数不为虚的,这只会调用基类的析构函数,而派生类因为没有调用,所以派生类部分没有释放内存。
当析构函数为虚的时候delete p;将调用指向的对象的类型的析构函数,而派生类的析构函数会调用基类的析构函数。
友元不能是虚函数,因为友元根本就不是类成员,而且只有类成员才能使虚函数。
如果派生类中没有重新定义基类中有的函数,如show,将使用该函数的基类版本,因为只有基类有这个方法,如果派生类中定义了与基类参数列表不同的虚函数(函数名相同),如是show(int n),而不是show();
这将会隐藏基类的版本:
Student stu;
stu.show(); //无效的
stu.show(3); //有效的
如果重新定义继承的方法,应该确保与原来的函数原型完全相同,但是如果返回的类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性称为返回类型协变,因为允许返回类型随类的类型的变化而改变:
class A
{
public:
virtual A &show(int n);
};
class B : public A
{
public:
virtual B &show(int n);
};
这种方法只适用于返回值,不适用于参数。
如果基类的声明被重载了,则需要在派生类中重新定义所有的基类版本:
class A
{
public:
virtual void funa()const;
virtual void funb(int n);
virtual void func(double d)const;
...
}
class B : public A
{
public:
virtual void funa()const;
virtual void funb(int n);
virtual void func(double d)const;
...
}
再来看看继承与动态内存分配的一些问题:
class A
{
char *label;
int n;
public:
A(const char *l=”null”,int r=0);
A(const A &temp);
virtual ~A();
A &operator=(const A &temp);
...
}
class B : public A
{
char clolor[40];
public:
...
}
类A采用new分配内存给label。
现在有几个问题:
派生类B需要显式的析构函数、复制构造函数和赋值运算符么?
首先不需要析构函数,因为数组可以通过默认的析构函数来释放,然后默认的析构函数会自动调用基类的析构函数,所以可以完全清理。
然后复制构造函数也不需要,默认的复制构造函数能实现把数组复制的功能,然后基类部分会自己调用基类的复制构造函数。
赋值运算符也是如此,默认的赋值运算符能复制数组,也会调用基类的赋值运算符。
那么现在类B来改变一下:
class B : public A
{
char *type;
public:
...
}
首先需要定义析构函数,因为默认的析构函数只会清理type指针所占的内存,不会清理指向的内存,所以需要定义一个使用delete的析构函数。
然后也需要复制构造函数,默认的复制构造函数赋值的是值,也就是说会把type指针保存的内存地址复制,这回造成严重的后果,所以需要定义一个实现释放旧内存、分配新内存和靠背数组的复制构造函数。
复制运算符和复制构造函数差不多,也是只会复制地址,所以需要定义一个实现释放旧内存、分配新内存和靠背数组的复制运算符。