类的继承,是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。

派生类的定义

class 派生类名:继承方式 基类名1,继承方式 基类名2
{派生类成员声明
}

多继承和单继承的UML表示

访问控制

基类的成员可以有public(公有)、protected(保护)和private(私有)三种访问属性。

基类的自身成员可以对基类中任何一个其他成员进行访问,但是通过基类的对象,就只能访问该类的公有成员。

类的继承方式有public(公有继承)、protected(保护继承)和private(私有继承)三种。

不同的继承方式,导致原来具有不同访问属性的基类成员在派生类中的访问属性也有所不同。

  1. 派生类中的新增成员访问从基类继承的成员
  2. 在派生类外部(非类族内的成员),通过派生类的对象访问从基类继承的成员

公有继承

当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问。也就是说

  • 基类的公有成员和保护成员被继承到派生类中访问属性不变,仍作为派生类的公有成员和保护成员,派生类的其他成员可以直接访问它们。
  • 在类族之外只能通过派生类的对象访问从基类继承的公有成员,
  • 无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员。
//Rectangle.h
class Point //基类Point类的声明
{public: //公有函数成员void InitP(float xx=0, float yy=0) {X=xx;Y=yy;}void Move(float xOff, float yOff) {X+=xOff;Y+=yOff;}float GetX() {return X;}float GetY() {return Y;}
private:    //私有数据成员float X,Y;
};
class Rectangle: public Point   //派生类声明部分
{public: //新增公有函数成员void InitR(float x, float y, float w, float h){InitP(x,y);W=w;H=h;} //调用基类公有成员函数float GetH() {return H;}float GetW() {return W;}
private:    //新增私有数据成员float W,H;
};

//7_1.cpp
#include<iostream>
#include<cmath>
#include "rectangle.h"
using namespace std;
void main()
{Rectangle rect;    //声明Rectangle类的对象rect.InitR(2,3,20,10); //设置矩形的数据rect.Move(3,2);    //移动矩形位置cout<<"The data of rect(X,Y,W,H):"<<endl;cout<<rect.GetX()<<"," //输出矩形的特征参数<<rect.GetY()<<","<<rect.GetW()<<","<<rect.GetH()<<endl;
}

主函数中首先声明了一个派生类的对象rect,对象生成时调用了系统所产生的默认构造函数,这个函数的功能是什么都不做。然后通过派生类的对象,访问了派生类的公有函数initR,move等,也访问了派生类从基类继承来的公有函数getX(),getY()。这样我们看到了,从一个基类以公有方式产生了派生类之后,在派生类的成员函数中,以及通过派生类的对象如何访问从基类继承的公有成员。

私有继承

当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。也就是说

  • 基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类族外部通过派生类的对象无法直接访问它们。
  • 无论是派生类的成员还是通过派生类的对象,都无法直接访问从基类继承的私有成员。
//rectangle.h
class Point //基类声明
{public:void InitP(float xx=0, float yy=0) {X=xx;Y=yy;}void Move(float xOff, float yOff) {X+=xOff;Y+=yOff;}float GetX() {return X;}float GetY() {return Y;}
private:float X,Y;
};
class Rectangle: private Point  //派生类声明
{public: //新增外部接口void InitR(float x, float y, float w, float h){InitP(x,y);W=w;H=h;}   //派生类访问基类公有成员void Move(float xOff, float yOff) {Point::Move(xOff,yOff);}float GetX() {return Point::GetX();}float GetY() {return Point::GetY();}float GetH() {return H;}float GetW() {return W;}
private:    //新增私有数据float W,H;
};

同样,派生类Rectangle继承了Point类的成员,因此在派生类中,实际所拥有的成员就是从基类继承来的成员与派生类新成员的总和。继承方式为私有继承,这时,基类中的公有和保护成员在派生类中都以私有成员的身份出现。派生类的成员函数及对象无法访问基类的私有成员(例如基类的x,y)。派生类的成员仍然可以访问到从基类继承过来的公有和保护成员(例如在派生类函数成员initRectangle中直接调用基类的函数initPoint),但是在类外部通过派生类的对象根本无法直接访问到基类的任何成员,基类原有的外部接口(例如基类的getX()和getY()函数)被派生类封装和隐蔽起来。当然,派生类新增的成员之间仍然可以自由地互相访问。

在私有继承情况下,为了保证基类的一部分外部接口特征能够在派生类中也存在,就必须在派生类中重新声明同名的成员。这里在派生类Rectangle中,重新声明了move,getX,getY等函数,利用派生类对基类成员的访问能力,把基类的原有成员函数的功能照搬过来。这种在派生类中重新声明的成员函数具有比基类同名成员函数更小的作用域,因此在调用时,根据同名隐藏的原则,自然会使用派生类的函数。

//7_2.cpp
#include<iostream>
#include<cmath>
#include "rectangle.h"
using namespace std;
void main()
{Rectangle rect;    //声明Rectangle类的对象rect.InitR(2,3,20,10); //设置矩形的数据rect.Move(3,2);    //移动矩形位置cout<<"The data of rect(X,Y,W,H):"<<endl;cout<<rect.GetX()<<"," //输出矩形的特征参数<<rect.GetY()<<","<<rect.GetW()<<","<<rect.GetH()<<endl;
}

本例的Rectangle类对象rect调用的函数都是派生类自身的公有成员,因为是私有继承,它不可能访问到任何一个基类的成员。

保护继承

保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问。这样,

派生类的其他成员就可以直接访问从基类继承来的公有和保护成员,但在类外部通过派生类的对象无法直接访问它们

无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员。

类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下的情况。

  • 派生类的对象可以隐含转换为基类对象。
  • 派生类的对象可以初始化基类的引用。
  • 派生类的指针可以隐含转换为基类的指针。

在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。

class B{…}
class D:public B{…}
B b1,*pb1;
D d1;

派生类对象可以隐含转换为基类对象,即用派生类对象中从基类继承来的成员,逐个赋值给基类对象的成员:

b1=d1;

派生类的对象也可以初始化基类对象的引用

B &rb = d1;

派生类对象的地址也可以隐含转换为指向基类的指针

pb1=&d1;

类型兼容规则是多态性的重要基础之一

//7_4.cpp
#include <iostream>
using namespace std;
class B0    //基类B0声明
{public:void display(){cout<<"B0::display()"<<endl;}   //公有成员函数
};
class B1: public B0 //公有派生类B1声明
{public:void display(){cout<<"B1::display()"<<endl;}   //公有成员函数
};
class D1: public B1 //公有派生类D1声明
{public:void display(){cout<<"D1::display()"<<endl;}   //公有成员函数
};
void fun(B0 *ptr)   //普通函数
{       //参数为指向基类对象的指针ptr->display();    //"对象指针->成员名"
//  (*ptr).display();
}
void main() //主函数
{B0 b0; //声明B0类对象B1 b1; //声明B1类对象D1 d1; //声明D1类对象B0 *p; //声明B0类指针p=&b0;    //B0类指针指向B0类对象fun(p);p=&b1;    //B0类指针指向B1类对象fun(p);b1.display();p=&d1;   //B0类指针指向D1类对象fun(p);
}

程序运行的结果

派生类的构造和析构函数

由于基类的构造函数和析构函数不能被继承,在派生类中,如果对派生类新增的成员进行初始化,就必须为派生类添加新的构造函数。但是派生类的构造函数只负责对派生类新增的成员进行初始化,对所有从基类继承下来的成员,其初始化工作还是由基类的构造函数完成。同样,对派生类对象的扫尾、清理工作也需要加入新的析构函数。

构造函数

派生类构造函数的一般语法形式为:

派生类名::派生类名(参数表):基类名1(基类1初始化参数表),…,基类名n(基类n初始化参数表),
成员对象名1《战员对象1初始化参数表)…,成员对象名m(成员对象m初始化参数表)
{派生类构造函数的其他初始化操作;}

例子:

//7_5.cpp
#include <iostream>
using namespace std;
class B1    //基类B1,构造函数有参数
{public:B1(int i) {cout<<"constructing B1 "<<i<<endl;}
private:int i;
};
class B2    //基类B2,构造函数有参数
{public:B2(int j) {cout<<"constructing B2 "<<j<<endl;}
private:int j;
};
class B3    //基类B3,构造函数无参数
{public:B3(){cout<<"constructing B3 *"<<endl;}
};
class C: public B2, public B1, public B3    //派生新类C
//注意基类名的顺序
{public: //派生类的公有成员C(int a, int b, int c, int d):B1(a),memberB2(d),memberB1(c),B2(b){}//注意基类名的个数与顺序//注意成员对象名的个数与顺序
private:    //派生类的私有对象成员B1 memberB1;B2 memberB2;B3 memberB3;
};
void main()
{C obj(1,2,3,4);
}

C(int a, int b, int c, int d):B1(a),memberB2(d),memberB1(c),B2(b){}

构造函数的参数表中给出了基类及内嵌成员对象所需的全部参数,在冒号之后,分别列出了各个基类及内嵌对象名和各自的参数。这里,有两个问题需要注意:首先,这里并没有列出全部基类和成员对象,由于Base3类只有默认构造函数,不需要给它传递参数,因此,基类Base3以及Base3类成员对象member3就不必列出。其次,基类名和成员对象名的顺序是随意的。这个派生类构造函数的函数体为空,可见实际上只是起到了传递参数和调用基类及内嵌对象构造函数的作用。

复制构造函数

当存在类的继承关系时,复制构造函数该如何编写呢?对一个类,如果程序员没有编写复制构造函数,编译系统会在必要时自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,然后对派生类新增的成员对象一一执行复制。

如果要为派生类编写复制构造函数,一般需要为基类相应的复制构造函数传递参数。

例如,假设Derived类是Base类的派生类,Derived类的复制构造函数形式如下:

Derived::Derived(const Derived &v):Base(v){............}

为什么Base类的复制构造函数参数类型不是Base类对象的引用?怎么这里用Derived类对象的引用v作为参数呢?

这是因为类型兼容规则在这里起了作用:可以用派生类的对象去初始化基类的引用。因此当函数的形参是基类的引用时,实参可以是派生类的对象。

析构函数

派生类的析构函数的功能是在该类对象消亡之前进行一些必要的清理工作。析构函数没有类型,也没有参数,和构造函数相比情况略微简单些。

//7_6.cpp
#include <iostream>
using namespace std;
class B1    //基类B1声明
{public:B1(int i) {cout<<"constructing B1 "<<i<<endl;}   //B1的构造函数~B1() {cout<<"destructing B1 "<<endl;}   //B1的析构函数
};
class B2    //基类B2声明
{public:B2(int j) {cout<<"constructing B2 "<<j<<endl;}   //B2的构造函数~B2() {cout<<"destructing B2 "<<endl;}   //B2的析构函数
};
class B3    //基类B3声明
{public:B3(){cout<<"constructing B3 *"<<endl;} //B3的构造函数~B3() {cout<<"destructing B3 "<<endl;}   //B3的析构函数
};
class C: public B2, public B1, public B3    //派生类C声明
{public:C(int a, int b, int c, int d):B1(a),memberB2(d),memberB1(c),B2(b){}//派生类构造函数定义
private:B1 memberB1;B2 memberB2;B3 memberB3;
};
void main()
{   C obj(1,2,3,4);
}

程序的运行结果如下:

程序中,给3个基类分别加入了析构函数,派生类没有做任何改动,仍然使用的是由系统提供的默认析构函数。主函数也保持原样。程序在执行时,首先执行派生类的构造函数,然后执行派生类的析构函数。构造函数部分已经讨论过了,派生类默认的析构函数又分别调用了成员对象及基类的析构函数,这时的次序刚好和构造函数执行时完全相反。

多态

面向对象的多态性可以分为4类:重载多态强制多态包含多态参数多态

重载多态:类的成员函数的重载就属于重载多态。除此以外还有运算符重载,加法运算可以分别使用于浮点数、整型数之间就是重载的实例。

强制多态:是指将一个变元的类型加以变化,以符合一个函数或者操作的要求,加法运算符在进行浮点数与整型数相加时,首先进行类型强制转换,把整型数变为浮点数再相加的情况,就是强制多态的实例。

包含多态:是类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现。

参数多态:与类模板相关联,在使用时必须赋予实际的类型才可以实例化。这样,由类模板实例化的各个类都具有相同的操作,而操作对象的类型却各不相同。

运算符重载

C++中预定义的运算符的操作对象只能是基本数据类型。实际上,对于很多用户自定义类型(比如类),也需要有类似的运算操作。

运算符重载的规则如下:

  1. C++中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。
  2. 重载之后运算符的优先级和结合性都不会改变
  3. 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造,一般来讲,重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。

注意:不能重载的运算符只有5个,它们是类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”、sizeof运算符和三木运算符“?:”。前面两个运算符保证了C++中访问成员功能的含义不被改变。作用域分辨符和sizeof运算符的操作数是类型。

  • 运算符重载的一般语法形式为:
函数类型  operator 运算符(形参表)
{函数体
}

返回类型指定了重载运算符的返回值类型,也就是运算结果类型;operator是定义运算符重载函数的关键字;运算符即是要重载的运算符名称,必须是C++中可重载的运算符,比如要重载加法运算符,这里就写“+”;形参表中给出重载运算符所需要的参数和类型。

运算符重载为成员函数

//8_1.cpp
#include<iostream>
using namespace std;
class complex   //复数类声明
{public: //外部接口complex(double r=0.0,double i=0.0){real=r;imag=i;}    //构造函数complex operator + (complex c2); //运算符+重载成员函数complex operator - (complex c2);   //运算符-重载成员函数void display(); //输出复数
private:    //私有数据成员double real;    //复数实部double imag;  //复数虚部
};
complex complex::operator +(complex c2)    //重载运算符函数实现
{return complex(real+c2.real, imag+c2.imag); //创建一个临时无名对象作为返回值
}
complex complex::operator -(complex c2) //重载运算符函数实现
{return complex(real-c2.real, imag-c2.imag);  //创建一个临时无名对象作为返回值
}
void complex::display()
{cout<<"("<<real<<","<<imag<<")"<<endl;
}
void main() //主函数
{complex c1(5,4),c2(2,10),c3;   //声明复数类的对象cout<<"c1=";c1.display();cout<<"c2=";c2.display();c3=c1-c2;    //使用重载运算符完成复数减法cout<<"c3=c1-c2=";c3.display();c3=c1+c2; //使用重载运算符完成复数加法cout<<"c3=c1+c2=";c3.display();
}

重载单目运算符“++”

//8_2.cpp
#include<iostream>
using namespace std;
class Clock //时钟类声明
{public: //外部接口Clock(int NewH=0, int NewM=0, int NewS=0);void ShowTime();void operator ++();    //前置单目运算符重载void operator ++(int); //后置单目运算符重载
private:    //私有数据成员int Hour,Minute,Second;
};
Clock::Clock(int NewH, int NewM, int NewS)  //构造函数
{if(0 <= NewH && NewH < 24 && 0 <= NewM && NewM < 60 && 0 <= NewS && NewS < 60){    Hour=NewH;Minute=NewM;Second=NewS;}elsecout<<"Time error!"<<endl;
}
void Clock::ShowTime()  //显示时间函数
{cout<<Hour<<":"<<Minute<<":"<<Second<<endl;
}
void Clock::operator ++() //前置单目运算符重载函数
{Second++;if(Second>=60){Second=Second-60;Minute++;if(Minute>=60){Minute=Minute-60;Hour++;Hour=Hour%24;}}cout<<"++Clock: ";
}
void Clock::operator ++(int)  //后置单目运算符重载
{       //注意形参表中的整型参数Second++;if(Second>=60){Second=Second-60;Minute++;if(Minute>=60){Minute=Minute-60;Hour++;Hour=Hour%24;}}cout<<"Clock++: ";
}
void main()
{Clock myClock(23,59,59);cout<<"First time output:";myClock.ShowTime();myClock++;myClock.ShowTime();++myClock;myClock.ShowTime();
}

运算符重载为非成员函数

//8_3.cpp
#include<iostream>
using namespace std;
class complex   //复数类声明
{public: //外部接口complex(double r=0.0,double i=0.0){real=r;imag=i;}    //构造函数friend complex operator + (complex c1,complex c2);   //运算符+重载友元函数friend complex operator - (complex c1,complex c2); //运算符-重载友元函数void display(); //显示复数的值
private:    //私有数据成员double real;double imag;
};          //显示函数实现
void complex::display()
{   cout<<"("<<real<<","<<imag<<")"<<endl;}
complex operator +(complex c1,complex c2)  //运算符重载友元函数实现
{   return complex(c2.real+c1.real,c2.imag+c1.imag);}
complex operator -(complex c1,complex c2)   //运算符重载友元函数实现
{   return complex(c1.real-c2.real,c1.imag-c2.imag);}
void main() //主函数
{complex c1(5,4),c2(2,10),c3;cout<<"c1=";c1.display();cout<<"c2=";c2.display();c3=c1-c2; //使用重载运算符cout<<"c3=c1-c2=";c3.display();c3=c1+c2;   //使用重载运算符cout<<"c3=c1+c2=";c3.display();
}

在保护继承中基类的共有成员_c++中的继承相关推荐

  1. 在保护继承中基类的共有成员_C++学习刷题13--继承的实现、继承的方式

    一.前言 本部分为C++语言刷题系列中的第13节,主要讲解这几个知识点:继承的实现.继承的方式.欢迎大家提出意见.指出错误或提供更好的题目! 二.知识点讲解 知识点1:继承的实现,可以理解派生类拥有成 ...

  2. 在保护继承中基类的共有成员_C++学习大纲:继承方式的调整

    C++ 继承方式的调整 在任何继承方式中,除了基类的private成员外,都可以在派生类中分别调整其访问控制. 调整格式 [public: | protected: | private: ] :: ; ...

  3. 在保护继承中基类的共有成员_C++面向对象:C++ 继承

    面向对象程序设计中最重要的一个概念是继承.继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易.这样做,也达到了重用代码功能和提高执行效率的效果. 当创建一个类时,您不需要重新 ...

  4. 在保护继承中基类的共有成员_C#初学者教程系列11:继承

    本文是C#初学者简单教程,这是第11篇.感谢观看,记得关注我,后续还有更多教程文章,谢谢. 本文环境为Visual Studio 2019. 一.什么是继承 继承是面向对象编程的一种基本特性. 借助继 ...

  5. java中的类跟结构体_C#中的结构体与类的区别

    经常听到有朋友在讨论C#中的结构与类有什么区别.正好这几日闲来无事,自己总结一下,希望大家指点. 1. 首先是语法定义上的区别啦,这个就不用多说了.定义类使用关键字class 定义结构使用关键字str ...

  6. 派生类继承虚基类后对象的大小----C++学习(1)

    C++中类继承虚基类对象的大小 原理 1.基类有虚函数,派生类无虚函数 2.基类有虚函数,派生类有虚函数 3.基类无虚函数,派生类无虚函数 4.基类无虚函数,派生类有虚函数 5.两个基类都有虚函数,派 ...

  7. C++ 类的继承,基类,派生类

    继承: 当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可. 这个已有的类称为基类,新建的类称为派生类. 例: //基类class Animal {// ...

  8. C++中基类与派生类的构造函数和析构函数

    1.Cpp中的基类与派生类的构造函数 基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承.构造函数不能被继承是有道理的,因为即使继承了,它的名字和 ...

  9. C++中基类的析构函数为什么要用virtual虚析构函数

    知识背景 要弄明白这个问题,首先要了解下C++中的动态绑定. 关于动态绑定的讲解,请参阅:  C++中的动态类型与动态绑定.虚函数.多态实现 正题 直接的讲,C++中基类采用virtual虚析构函数是 ...

最新文章

  1. Python数据分析几个比较常用的方法
  2. python_面向对象进阶之属性值的限制
  3. 第4章 Python 数字图像处理(DIP) - 频率域滤波12 - 选择性滤波 - 带阻
  4. ICPC网络赛第二场G Limit
  5. [转]Bing Maps Tile System 学习
  6. jbutton添加点击事件_electron-vue自定义边框后点击事件失效问题
  7. (软件工程复习核心重点)第四章总体设计-第三节:启发规则
  8. opcua协议服务器端口号,opc ua服务器 数据配置
  9. 做生意,没亏过钱,自然也没赚过钱
  10. [妙味Ajax]第一课:原理和封装
  11. JAVA使用摄像头录制_JavaCV开发详解之1:调用本机摄像头视频(建议使用javaCV最新版本)...
  12. 5-5 常用系统接口
  13. FCKEditor v2.6.3 最新版-ASP.NET 演示程序
  14. 计算机程序编程就业,计算机编程就业
  15. React-pdf:pdf预览插件实践
  16. java视频生成缩略图_Java调用ffmpeg工具生成视频缩略图实例
  17. Word手工双面打印
  18. Oracle序列的创建和使用
  19. 计算机网络ip地址划分计算机,计算机网络中IP地址大全
  20. 基于 FPGA 的便携式 DDS 信号发生器与示波器

热门文章

  1. php 检测 变量是否设置,PHP中检测一个变量是否有设置的函数是什么?
  2. html超链接同一页面,你绝对想要的HTML页面超链接的修改问题
  3. PyTorch: 各种图像格式相互转化
  4. 64位系统使用Access数据库文件的彻底解决方法
  5. mysql中修改表的还原命令_MySQL的增、删、改、查和备份、恢复的命令
  6. Linux基础命令---lp打印机命令
  7. 1月19日学习内容整理:Scrapy框架补充之scrapy-redis组件
  8. 产品经理做产品设计的九步法
  9. 2013年微软编程之美大赛初赛第二题(博客园居然可以插入代码!!)
  10. 动态为GridView控件创建列