1、继承的概念
继承:在保存原有类的属性和功能的基础上,扩展新的功能。
开发类库的团队和使用类库的团队很可能不是一个,有些东西是不能访问的。
继承和派生是同一个问题的不同视角:
保持已有类的特性而构建新类的过程成为继承;在已有类的基础上新增自己的特性而产生新类的过程叫做派生。
被继承的已有类称为基类或者父类;派生出的新类叫做派生类或者子类。另外还有直接基类和间接基类体现是不是直接参与派生。
继承的目的:实现设计和代码的重用。
派生的目的:当新的问题出现,原有的程序无法解决或者不能完全解决时,需要对原有的程序进行改造。
派生类的构成:吸收基类的成员,改造基类成员(部分),添加新的成员(主要)。
吸收基类成员:默认情况下派生类包含了基类中除构造函数和析构函数之外的全部成员。c++11中规定可以用using语句继承基类构造函数。
改造基类成员(部分):使用相同的成员名,覆盖原有的成员。
添加新的成员(主要):完全增加新的成员和新的数据。
2、继承的方式:公有继承(public),私有继承(private),保护继承(protected)
不同的继承方式的影响主要体现在:a、派生类成员对基类成员的访问权限;b、通过派生类对象对基类成员的访问权限。
继承的访问控制:
基类的public和protected成员:访问属性在派生类中保持不变。
基类的private成员:不可直接访问
访问权限:
派生类中的成员函数:可以之间访问基类中的public和protected成员,但不能直接访问基类的private成员;
通过派生类的对象:只能访问public成员。
举例:
//Rectangle.h
#ifndef _RENCTANGLE_H
#define _RENCTANGLE_H
#include"Point.h"
class Rectangle:public Point//派生类定义部分
{
public://新增公有函数
void initRectangle(float x, float y, float w, float h) {
initPoint(x, y);//调用基类公有函数
this->w = w;
this->h = h;
}
~Rectangle();
float getH() const { return h; }
float getW() const { return w; }
private:
float h, w;//新增私有数据成员
};
Rectangle::~Rectangle()
{
}
#endif // !_RENCTANGLE_H
//Point.h
#ifndef _POINT_H_
#define _POINT_H_
class Point
{
public:
void initPoint(float x = 0, float y = 0) {
this->x = x;
this->y = y;
}
void move(float offx, float offy) {
x += offx;
y += offy;
}
float getX() const{ return x; }
float getY() const{ return y; }
private:
float x, y;
};
#endif // !_POINT_H_
//test.cpp
#include"Rectangle.h"
#include
using namespace std;
int main() {
Rectangle rect;//定义Rectangle类对象
rect.initRectangle(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;
return 0;
}
继承的访问控制:
基类的public和protected成员:都以private身份出现在派生类中;
基类的private成员:不可以直接访问
访问权限:
派生类的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
通过派生类的对象:不能直接访问从基类继承的任何成员。
首先要记住的是:私有成员在类外是不能通过对象直接调用的。但是在类内可以调用。将上面的例子做一定修改:
//Rectangle.h
#ifndef _RENCTANGLE_H
#define _RENCTANGLE_H
#include"Point.h"
class Rectangle:private Point//派生类定义部分
{
public://新增公有函数
void initRectangle(float x, float y, float w, float h) {
initPoint(x, y);//调用基类公有函数
this->w = w;
this->h = h;
}
~Rectangle();
void move(float offx, float offy) { Point::move(offx, offy); }
float getX() const { return Point::getX(); }
float getY()const { return Point::getY(); }
float getH() const { return h; }
float getW() const { return w; }
private:
float h, w;//新增私有数据成员
};
Rectangle::~Rectangle()
{
}
#endif // !_RENCTANGLE_H
仅将Rectangle.h做如上修改。其他两个文件都不变。主要变化:将公有继承改为了私有继承,使得原来Point类中的公有成员变成了私有成员,所以再次调用move和getX,getY的时候需要在类内调用。
继承的访问控制:
基类的public和protected成员:都以protected身份出现在派生类中;
基类的private成员:不可以直接访问
访问权限:
派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
通过派生类的对象:不能直接访问从基类继承的任何成员。
多继承的情况:看下面的例子,在类外不能通过对象直接访问私有成员。
3、类型转换(基类和派生类之间)
公有派生类对象可以被当作基类对象使用,反之则不可。
派生类的对象可以隐含的转换为基类对象
派生类的对象可以初始化基类的引用;
派生类的指针可以隐含的转换为基类的指针。
通过基类对象名,指针只能使用从基类继承的成员。
例子:假设有一个基类,其中能够有一个显示函数实现一个输出功能。我们用类2实现基类的派生,并且修改显示函数,再用类3派生类2并修改显示函数。并且我们希望有一个通用的函数使得可以通过不同的派生类型显示不同的输出情况。
#include
using namespace std;
class Base1
{
public:
void display()const {
cout << "Base1::display()" << endl;
}
private:
};
class Base2:public Base1
{
public:
void display() const {
cout << "Base2::display()" << endl;
}
private:
};
class Derived :public Base2
{
public:
void display() const {
cout << "Derived::display()" << endl;
}
private:
};
void fun(Base1 *ptr) {
ptr->display();
}
int main() {
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
return 0;
}
程序并没有像我们想象中的那样子输出。者对于程序的可读性很不好。所以:不要重新定义继承而来的非虚函数。(虚函数后面会讲),虚函数可以达到我们想要的输出那个样子。
4、派生类的构造函数
一般说来,每个额勒的构造函数只负责本类成员的初始化。
默认情况下:
基类的构造函数不被继承;
派生类需要定义自己的构造函数。——派生类的构造函数还要负责向基类构造函数传递参数,实现基类的初始化
c++11规定:可以使用using语句继承基类构造函数;但是只能初始化从基类继承的成员。语法格式:using B::B;。即using 基类名::构造函数。适用于派生类很少增加新的数据成员或者不增加新的数据成员这种情况。因为派生类新增的成员不能通过构造函数初始化了。如果派生类定义了类内的初始值,就可以通过类内的初始值去初始化,如果没有定义类内的初始值,就只能按默认的方式初始化:就是不进行初始化,里面存放的是垃圾值。
建议:如果派生类需要自己新增的成员进行可定制的初始化,也就是说可以用构造函数的参数去指定的初始化,不用继承基类的构造函数而是自己写初始函数。但是问题来了:
问题:从基类继承来的成员怎么样去初始化呢?来看看派生类有哪些必须完成的任务?
若不继承基类的构造函数:
派生类新增的成员:派生类定义构造函数初始化
继承来的成员:自动调用基类构造函数进行初始化。可是参数从哪里来呢?——派生类构造函数来传递
派生类的构造函数还需要给基类的构造函数传递参数。
如果派生类只继承一个基类,这种情况叫做单继承:
单继承情况下构造函数的定义语法:
派生类类名::派生类类名(基类所需的形参,本类成员所需的形参):基类名(参数表),本类成员初始化列表{//其他初始化;};
例子:
#include
using namespace std;
class B
{
public:
B();
B(int i);
~B();
void print()const;
private:
int b;
};
B::B()
{
b = 0;
cout << "B默认构造函数:" << endl;
}
B::B(int i)
{
b = i;
cout << "B构造函数:" << endl;
}
B::~B()
{
cout << "B析构函数:" << endl;
}
void B::print()const {
cout << b << endl;
}
class C:public B
{
public:
C();
C(int i, int j);
~C();
void print()const;
private:
int c;
};
C::C() {
c = 0;
cout << "C构造函数:" << endl;
}
C::~C()
{
cout << "C析构函数:" << endl;
}
void C::print()const {
cout << c << endl;
}
C::C(int i, int j):B(i),c(j) {
}
int main() {
C obj(5, 6);
obj.print();
return 0;
}
多继承时构造函数的定义语法:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表),基类名2(基类2初始化参数表),……基类名n(基类n初始化参数表),本类成员初始化列表{//其他初始化;};
派生类与基类的构造函数:
当基类中有默认构造函数时:派生类构造函数可以不想基类构造函数传递参数。
构造派生类的对象时,积累的默认构造函数将被调用
如需执行基类中带参数的构造函数:派生类构造函数应当为基类构造函数提供参数。
当又有继承又有对象成员的时候:多继承且有对象成员时派生的够赞函数定义语法:
派生类名::派生类名(形参表):基类名1(参数),基类名2(参数),……基类名n(参数),本类成员(含对象成员)初始化列表{//其他初始化;};这里的本类成员初始化列表里面包含对象成员基本类型的数据成员。
调用次序:
派生类的构造函数举例:(既有继承又有组合)
#include
using namespace std;
class Base1
{
public:
Base1(int i) { cout << "Constructing Base1" <<" "<< i << endl; }
~Base1();
private:
};
Base1::~Base1()
{
}
class Base2
{
public:
Base2(int j) { cout << "Constructing Base2" << " "<
顺序:被继承的顺序从左至右public Base2,public Base1,public Base3,对象成员初始化顺序Base1 member1;Base2 member2;Base3 member3;。
若派生类没有申明复制构造函数:
编译器会在需要时生成一个隐含的复制构造函数;
先调用基类的复制构造函数;
再为派生类新增的成员执行复制。
若派生类定义复制构造函数:
一般都要为基类的复制构造函数传递参数;复制构造函数只能接受一个参数,即用来初始化派生类定义的成员,也将被传递给基类的复制构造函数。基类的复制构造函数参数类型是基类对象的引用,实参可以是派生类对象的引用。
例如:C::C(const C& c1):B(c1){……}
5、派生类的析构函数
析构函数不被继承。派生类如果需要,要自行声明析构函数。
声明方法与无继承关系时类的析构函数相同。
不需要显式的调用基类的析构函数,系统会自动隐式调用。
先执行派生类析构函数的函数体,在调用基类的析构函数。
#include
using namespace std;
class Base1
{
public:
Base1(int i) { cout << "Constructing Base1" <<" "<< i << endl; }
~Base1();
private:
};
Base1::~Base1()
{
cout << "析构Base1" << endl;
}
class Base2
{
public:
Base2(int j) { cout << "Constructing Base2" << " "<
6、从派生类访问基类的成员
当派生类与基类中有相同的成员时,若未特别限定,则通过派生类对象使用的是派生类中的同名成员。
如果通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定。
例子:
#include
using namespace std;
class Base1
{
public:
int var;
void fun() {
cout << "*********1111" << endl;
}
};
class Base2
{
public:
int var;
void fun() {
cout << "*********2222222" << endl;
}
};
class Derived: public Base1,public Base2
{
public:
int var;
void fun() {
cout << "*********d" << endl;
}
};
int main() {
Derived b;
Derived *p = &b;
//对象名.成员名标识
b.var = 1;
b.fun();
//Base1作用域分辨符标识
b.Base1::var = 2;
b.Base1::fun();
//Base2
p->Base2::var = 3;
p->Base2::fun();
return 0;
}
二义性问题:如果从不同的基类中继承了同名的成员,但是他在派生类中没有定义同名成员,“派生类对象名或引用名.成员名”、“派生类指针->成员名”访问成员存在二义性问题。这时可以简单的通过类名来限定。
虽然可以通过类名来限定访问类成员,但是也会造成内存冗余的情况。——可以通过虚基类来解决
7、虚基类
如果某个派生类的多个基类实际上又曾经派生过一个共同的最远基类。那么这些积累里面就有相同的成员,最后在派生类中有汇聚到了一起。就会产生冗余,并有可能因冗余问题带来不一致性。
虚基类声明:以virtual说明基类继承方式
例如:class B1:virtual public B
作用:主要用来解决多继承时可能发生的对同意积累继承多次而产生的二义性问题。为最远派生类提供唯一的基类成员,二部重复产生多次复制。
注意:在第一级继承时就要将共同基类设计为虚基类。
举例:
实际上,从最远基类继承来的成员,在最远派生类中只有一份。
虽然虚基类解决了冗余问题,但是他同时带来了新的问题:派生类的构造函数怎么写?
建立对象时所指定的类称为最远派生类,虚基类的成员是有最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
在整个继承结构中,直接或者间接继承虚基类的所有派生类都必须在构造函数的成员初始化列表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。
Base0的构造函数只被调用了一次。只有最远派生类的传递的参数被Base0接收了。