第十章 继承与派生
10.1 继承与派生的基础语法
继承是面向对象的一块基石,是允许创建分等级层次类的关键。可以创建一个通用的类,然后派生出多个类,在这个类中可以增加新成员,以实现具体、多样的功能。
10.1.1 继承与派生概述
类的继承是新类从已有类那里获得特性,从已有类产生新类的过称为类的派生。已有的类称为基类或父类,派生出的新类则称为派生类或子类
继承主要功能:
(1)使父类与子类之间建立起逻辑上的层次关系
(2)使一个派生类获得其父类的属性和行为
根据派生类拥有的父类不同,可以分为单继承和多继承:单继承是一个子类只有一个直接父类,多继承是一个子类有多个直接父类。
父类与子类之间的关系:
(1)父类是对子类的抽象,子类是对父类的具体化,是父类的延续;
(2)子类是父类的组合,多继承可以看作是多个单继承的简单组合;
(3)公有类型的子类的对象可以看作父类的对象处理。
10.1.2 声明派生类
派生类声明一般形式如下:
clasa <派生类名>:<继承方式><基类名>
{派生类成员声明;
}
示例代码
class Base
{
private:int a;
public:void inita(int x);
};
class Derived:public Base//继承类
{
private:int b;
public:void initb(int y);
};
10.2 成员的访问
访问说明符用来控制相应成员的可访问性,使得信息封装和模块化的风格更好。
10.2.1 类的成员访问说明符
(1)public:修饰公有成员;
(2)protected:修饰保护成员;
(3)private:修饰私有成员。
10.2.2 类的成员的访问权限
(1)公有成员:可以被任何普通函数和任何类的成员函数或子类访问;
(2)保护成员:可以被类自身的成员和友元、子类的成员函数访问;
(3)私有成员:只能被类自身的成员和友元访问。
友元的主要作用是打破封装性,允许某些特定的外部函数或类访问类的内部实现细节。友元的声明需要在类的内部进行,使用friend
关键字。友元的声明不会改变友元本身的定义,只是告诉编译器,这个函数或类可以访问当前类的私有和保护成员。
10.3 继承的访问控制
类的继承方式有公有继承(public)、保护继承(protected)和私有继承(private)3种,不同的继承方式导致子类对基类成员的访问能力有所不同。总的来说,子类对父类成员的访问能力如下:
(1)基类(父类)中的私有成员在派生类(子)中是隐藏的,只能在基类内部访问,不能在派生类中访问;
(2)派生类从基类私有继承时,基类的公有和保护成员在派生类中都改为私有成员;
(3)派生类从基类保护继承时,基类的公有成员在派生类在改变为保护成员,保护成员不变;
(4)派生类从基类公有继承时,基类中的成员属性在派生类中保持不变。
10.3.1 私有继承
在私有继承中,派生类以私有方式继承基类。派生类不能直接访问基类的私有成员,而只能在派生类的成员函数中通过基类的公有或保护成员函数间接访问。在设计基类时,通常都要为其私有成员提供能够访问它们的公有函数,以便派生类和外部函数能够间接访问它们。
示例代码:
#include<iostream>
using namespace std;
class Base
{
private:int a;
public:void inita(int x){a=x;}int geta(){return a;}
};
class Derived:private Base
{private:int b;public:void initb(int y,int x){b=y;inita(x);}int getb(){return b*geta();}
};
int main()
{Derived ob;ob.initb(5,7);cout<<"a*b= "<<ob.getb()<<endl;return 0;
}
运行结果:
注意:在实际应用中,由于基类经过多次派生后,某私有成员可能会成为不可访问的,所以用的比较少。
10.3.2 公有继承
在公有继承中,基类的公有成员和保护成员在派生类中仍然是公有成员和保护成员,派生类的成员函数可直接访问它们,而外部函数只能通过派生类的对象间接访问它们。
示例代码:
#include<iostream>
using namespace std;
class Base
{
private:int a;
public:void inita(int x){a=x;}int geta(){return a;}
};
class Derived:public Base
{
private:int b;
public:void initb(int y){b=y;}int getb(){return b*geta();}
};
int main()
{Derived ob;ob.inita(12);ob.initb(5);cout<<"the result of ob getb() is: "<<ob.getb()<<endl;return 0;
}
运行结果:
在使用公有继承时要注意:
(1)派生类以公有的方式继承自基类,并不是说派生类就可以访问基类的私有成员。
(2)派生类中声明的成员名如果与基类中声明的成员名相同,则派生类中的成员名起支配作用。
示例:
10.3.3 保护继承
在保护继承中,基类的公有成员在派生类中称为保护成员,基类的保护成员在派生类中仍是保护成员,所以保护继承的派生类的所有成员在类的外部都是无法访问到它们(基类的成员)。只能通过派生类中的成员函数来间接访问基类中的成员。
示例代码:
#include<iostream>
using namespace std;
class Base
{
private:int a;
protected:int b;
public:int c;void setab(int x,int y){a=x;b=y;}int geta(){return a;}
};
class Derived:protected Base
{
private:int c;
public:void setabc(int m,int n,int l){setab(m,n);b=m;c=l;}int getc(){c=c+b*geta();return c;}
};
int main()
{Derived ob;//ob.setab(12,12);非法,不能通过类外对象访问从积累保护继承来的成员ob.setabc(12,12,5);cout<<"the result of ob getb() is: "<<ob.getc()<<endl;return 0;
}
运行结果:
10.3.4 特殊方法的继承——派生类的构造函数和析构函数
在C++中,基类成员的初始化有基类的构造函数完成,而派生类的初始化工作由派生类的构造函数完成。
1、派生类的构造函数与析构函数构建原则:
(1)基类的构造函数与析构函数不能被派生类继承;
(2)如果基类没有定义构造函数,派生类也可以不不定义构造函数,全都采用默认的构造函数;
(3)如果基类定义了带有形参的构造函数,派生类就必须定义新的构造函数,提供一个将参数传递给基类构造函数的途径;
(4)如果派生类的基类也是派生类,则每个派生类只需要负责其直接基类的构造,不负责间接基类的构造;
(5)派生类是否定义析构函数与所属的基类无关。
2、派生类构造函数的创建:
派生类的构造函数需要用合适的初值作为参数,隐含调用基类的构造函数和新增对象成员的构造函数来初始化各自的数据成员,再用新加的语句对新增的数据成员进行初始化。一般形式如下:
<构造函数名>(参数总表):基类名(参数表),对象成员名1(参数表1),...,对象成员名n(参数表n)
{派生类新增成员的初始化语句;
}class Base
{
int i;
public:Base(int n);//声明基类构造函数
};
class Derived:public Base
{
int j;//派生类新增数据成员
Base ob;//基类对象作为派生类对象成员
public:Derived(int m):Base(m),ob(m);//声明派生类的构造函数
}
3、派生类析构函数的构建
析构函数不能被继承,需要在派生类中重新定义。与基类的析构函数一样,派生类的析构函数也没有数据类型和参数,在对象撤销时进行必要的内存释放和善后处理。
示例代码:
#include<iostream>
using namespace std;
class Base
{
int i;
public:Base(int n)//基类构造函数{cout<<"constructing Base class\n";i=n;}~Base()//基类析构函数{cout<<"destructing Base class\n";}void showi(){cout<<i<<endl;}
};
class Derived:public Base//类Derived公有继承类Base
{
int j;
Base ob;//基类对象作为派生类对象成员
public:Derived(int n):Base(n),ob(n)//派生类构造函数{cout<<"constructing Derived class"<<endl;j=2*n;}~Derived()//派生类析构函数{cout<<"destructing Derived class\n";}void showj(){cout<<j<<endl;}
};
int main()
{Derived ob(10);//ob.setab(12,12);非法,不能通过类外对象访问从积累保护继承来的成员ob.showi();ob.showj();return 0;
}
运行结果:
代码中派生类中声明了基类对象作为成员变量,将基类对象作为派生类的成员变量,可以让派生类复用基类的实现,同时又保持了封装性。这种方式允许派生类在不继承基类所有行为的情况下,选择性地使用基类的功能。
基类与派生类构造函数与析构函数的执行顺序如下:
10.4 多重继承
当一个派生类具有多个基类时,称这种派生为多重继承。
10.4.1 声明多重继承
多重继承就是一个类继承多个基类,那么这个类具有多个基类的属性与行为。
多重继承的声明一般形式如下:
class <派生类名>:<派生方式1><基类名1>,...,<派生方式n><基类名n>
{派生类成员声明;
}
class Derived:public Base1,publicBase2
{...
}
使用多重继承时应注意以下两点:
(1)各种派生方式对于基类成员在派生类中的访问权限与单继承的规则相同;
(2)在使用多继承时,对基类成员的访问应无二义性。
(3)每个基类前都要指明继承方式,否则默认为私有继承。
10.4.2 二义性问题
在多继承中,需要解决的主要问题就是标识符不唯一的问题,即二义性问题。例如,当派生类继承的多个基类中有同名成员时,派生类中就会出现标识符不唯一(二义性)的情况。如一下代码中就出现了二义性问题:
class Base1
{
public:int x;int a();int b();
};
class Base2
{int x;int a();
public:float();
};
class Derived:Base1,Base2
{
};
void d(Derived &e)
{
//以下写法是错误的,不知道x,a(),b()是从哪个基类继承而来,有二义性e.x=10;e.a();e.b();
}
int main()
{Derived ob;return 0;
}
可以通过三种方式来解决多重继承中的二义性问题:
(1)使用域运算符“::”
(2)使用同名覆盖原则
(3)使用虚基类
1、使用域运算符
如果派生类的基类之间没有继承关系,同时有没有共同的基类,则在引用同名成员时,可在成员名前加上类名和域运算符来区分来自不同基类的成员。如下将上面出错的代码进行改进:
void d(Derived &e)
{
e.Base1::x=10;
e.Base1::a();
e.Base2::b();
}
2、使用同名覆盖原则
在派生类中重新定义与基类中同名的成员(如果是成员函数,则参数表也要相同,参数不同则为重载)以隐蔽掉基类中的同名成员。在引用这些同名的成员时,引用的就是派生类中的成员,这样二义性就得到了解决。
示例代码1:实现同名覆盖并使用域运算符
#include<iostream>
using namespace std;
class Base
{
public:int x;void show(){cout<<"This is Base x= "<<x<<endl;}
};
class Derived:public Base
{public:int x;void show(){cout<<"This is Derived x= "<<x<<endl;}
};
int main()
{Derived ob;ob.x=5;ob.show();ob.Base::x=12;ob.Base::show();return 0;
}
运行结果:
示例代码2:多重继承
#include<iostream>
using namespace std;
class Base1
{
public:int x;void show(){cout<<"This is Base1 x= "<<x<<endl;}
};
class Base2
{
public:int x;void show(){cout<<"This is Base2 x= "<<x<<endl;}
};
class Derived:public Base1,public Base2
{public:int x;void show(){cout<<"This is Derived x= "<<x<<endl;}
};
int main()
{Derived ob;ob.x=10;ob.show();ob.Base1::x=20;ob.Base1::show();ob.Base2::x=30;ob.Base2::show();return 0;
}
运行结果:
10.4.3 多重继承的构造函数与析构函数
声明多重继承的构造函数的一般形式如下:
<派生类名>::<派生类名>(参数总表)::基类名1(参数表1),...,基类名n(参数表n),对象成员名1(对象成员参数表1),...,对象成员名n(对象成员参数表n)
{
派生类新增成员的初始化语句;
}
Derived(int x, int y, int z,int v):Base1(x),Base2(y),ob1(z),ob2(v)
{
...
}
多重继承构造函数与析构函数的执行顺序与单继承相同,但是基类之间的执行顺序时严格按照声明时从左到右的顺序来执行,与在定义派生类构造函数中的次序无关。
示例代码:定义了多重继承的构造函数与析构函数
#include<iostream>
using namespace std;
class Base1
{int x1;public:Base1(int y1){x1=y1;cout<<"constructing Base1,x1="<<x1<<endl;}~Base1(){cout<<"destructing Base1"<<endl;}
};
class Base2
{int x2;public:Base2(int y2){x2=y2;cout<<"constructing Base1,x2="<<x2<<endl;}~Base2(){cout<<"destructing Base2"<<endl;}
};
class Derived:public Base1,public Base2
{private:Base1 ob1;Base2 ob2;public:Derived(int x,int y,int z,int v):Base1(x),Base2(y),ob1(z),ob2(v){cout<<"destructing Derived"<<endl;}
};
int main()
{Derived ob(1,2,3,4);return 0;
}
运行结果:
10.5 虚基类
虚基类是指基类被virtual修饰,也是一种用来解决基类中由于同名成员的问题而产生的二义性问题的方法。
10.5.1 声明虚基类
虚基类的声明是在派生类的声明过程中进行的,一般形式如下:
class <派生类名>:virtual<派生方式>(基类名)//虚基类关键字virtual只对紧跟其后的类起作用
eg:
class Base1:virtual public Base
{};
这种派生方式称为虚拟继承,声明了虚基类后,虚基类的成员在进一步派生过程中和派生类一起维护同一个内存复制
示例代码:
#include<iostream>
using namespace std;
class Base
{protected:int x;public:Base(){x=1;}
};
class Base1:virtual public Base
{public:Base1(){cout<<"constructing Base1,x="<<x<<endl;}
};
class Base2:virtual public Base
{public:Base2(){cout<<"constructing Base2,x="<<x<<endl;}
};
class Derived:public Base1,public Base2
{public:Derived(){cout<<"destructing Derived x="<<x<<endl;}
};
int main()
{Derived ob;return 0;
}
运行结果:
由于把公共基类Base声明为类Base1与Base2的虚基类,所以由类Base1与类Base2派生的类Derived只有一个基类Base,从而消除二义性。
10.5.2 虚基类的构造函数与初始化
虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的执行顺序不同,入下图所示:
示例代码:
#include<iostream>
using namespace std;
class Base
{protected:int x;public:Base(int x1){x=x1;cout<<"constructing Base,x="<<x<<endl;}
};
class Base1:virtual public Base
{int y;public:Base1(int x1,int y1):Base(x1){y=y1;cout<<"constructing Base1,y="<<y<<endl;}
};
class Base2:virtual public Base
{int z;public:Base2(int x1,int z1):Base(x1){z=z1;cout<<"constructing Base2,z="<<z1<<endl;}
};
class Derived:public Base1,public Base2
{int xyz;public:Derived(int x1,int y1,int z1, int xyz1):Base(x1),Base1(x1,y1),Base2(x1,z1){xyz=xyz1;cout<<"destructing Derived xyz="<<xyz<<endl;}
};
int main()
{Derived ob(1,2,3,4);return 0;
}
运行结果:
从截过图中可以看出,虚基类Base的构造函数只执行了一次。这是因为在派生类Derived调用虚基类Base的构造函数之后,类Base1和类Base2对虚基类Base的构造函数的调用被忽略了。这是初始化虚基类与初始化非虚基类的不同。
在使用虚基类是应注意几个问题:
(1)virtual与派生方式的关键字的书写位置无关紧要,可以先写虚基类的关键字,也可以先写派生方式的关键字。
(2)一个虚基类在作为某些类的虚基类的同时也可作为另一些类的非虚基类
(3)虚基类构造函数的参数必须是由罪行派生出来的类负责初始化,即使不是直接继承也应如此
10.6 友元
友元是关键字friend修饰的,他提供了不同类货对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。
10.6.1 友元的引入
引入友元的目的:为了使类的私有成员和保护成员能够被其他类或者其他成员函数访问
友元分类:
(1)友元函数:指友元是普通函数或类的成员函数
(2)友元类:指友元是一个类(友元类的所有成员函数都称为友元函数)
10.6.2 友元函数
友元函数与普通成员函数不同,它不是当前类的成员函数,而是独立于当前类的外部函数;它可以是普通函数或其他类的成员函数。友元函数定义后可以访问该类的所有对象的所有成员,包括私有成员、保护成员和公有成员。
友元函数使用前必须要在类定义是声明;其定义既可以在类内部进行,也可以在类外部进行,但通常都定义在类的外部。友元函数的一般声明形式如下:
friend<数据类型><友元函数名>(参数表);friend double area(Rectangle &rectangle);
示例代码:声明一个友元函数并在主程序中调用
#include<iostream>
using namespace std;
class Rectangle
{double length,width;public:Rectangle(double a=0,double b=0){length=a;width=b;}Rectangle(Rectangle &r);//重载构造函数
friend double area(Rectangle &rectangle);
};
double area(Rectangle &rectangle)
{return (rectangle.length*rectangle.width);
}
int main()
{Rectangle ob(1.5,2.0);cout<<"Area of Rextangle is "<<area(ob)<<endl;return 0;
}//主程序结束,调用析构函数释放对象
运行结果:
使用友元函数需要注意一下几个问题:
(1)由于友元函数不是成员函数,因此,在类外定义友元函数不需要加"类名::"
(2)友元函数不是类的成员,因而不能直接应用对象成员的名字,必须通过作为入口参数传递进来的对象名或对象指针来引用该对象的成员。因此,友元函数一般都带有一个该类的入口参数,如上例中的area(Rectangle &rectangle)
(3)当一个函数需要访问多个类时,应该把这个函数同时定义为这些函数的友元函数,这样才能访问这些类的数据。
10.6.3 友元成员
若一个类的成员函数是另一个类的友元函数,则称这个成员函数为友元成员。通过友元成员可以是两个类互相访问:
示例代码:声明一个类的成员函数为另一个类的友元函数
#include<iostream>
#include<cstring>
using namespace std;
class boy;//声明类boy
class girl
{char *name;int age;public:girl(const char *n,int a){name=new char[strlen(n)+1];strcpy(name,n);age=a;}void prt(boy &b);
};
class boy
{char *name;int age;public:boy(const char *n,int a){name=new char[strlen(n)+1];strcpy(name,n);age = a;}friend void girl::prt(boy &b);
};
void girl::prt(boy &b)
{cout<<"girl\'s name:"<<name<<" age:"<<age<<endl;cout<<"boy\'s name:"<<b.name<<" age:"<<b.age<<endl;}
int main()
{girl gl("Stacy",15);boy bl("Jim",16);gl.prt(bl);return 0;
}
运行结果:
使用友元成员时需要注意:
(1)必须先定义成员函数所在的类,如先定义类girl;
(2)声明投缘函数时,要加上成员函数所在类的类名和作用域运算符“::”,如friend void girl::prt("Stacy",165)
(3)在主函数在一定要创建类girl的一个对象,只有这样才能通过对象名调用友元函数。如girl("Stacy",15);
(4)如果在类定义前要使用到该类的成员,需要在使用前对该类进行声明,如class boy;
10.6.4 友元类
如果一个类作为另一个类的友元,称这个类问友元类。当一个类成为另一个类的友元类时,这个类的所有成员函数都成为另一个类的友元函数。友元类中的所有成员函数都可以通过对象名直接访问另一个类中的所有成员,从而实现不同类之间的数据共享。
声明形式如下:
friend class <友元类名>;friend class girl;
示例代码:
#include<iostream>
#include<cstring>
using namespace std;
class boy;//声明类boy
class girl
{char *name;int age;public:girl(const char *n,int a){name=new char[strlen(n)+1];strcpy(name,n);age=a;}void prt(boy &b);
};
class boy
{char *name;int age;friend class girl;public:boy(const char *n,int a){name=new char[strlen(n)+1];strcpy(name,n);age = a;}//friend void girl::prt(boy &b);
};
void girl::prt(boy &b)
{cout<<"girl\'s name:"<<name<<" age:"<<age<<endl;cout<<"boy\'s name:"<<b.name<<" age:"<<b.age<<endl;}
int main()
{girl gl("Stacy",15);boy bl("Jim",16);gl.prt(bl);return 0;
}
运行结果:
以上代码中将类girl声明为类boy的友元类,这样就不用声明友元函数area,而可以直接访问友元类girl中的area函数。