一、概念
C++中,继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类,又称父类;新建立类称为派生类,又称为子类。
基类是对派生类的抽象,派生类是对基类的具体化。
1.派生类的定义与构成
1)派生类的定义
class 派生类名:类派生列表{
成员列表
};
类派生列表指定了一个或多个基类,形式如下:
访问权限标号 基类名1,访问权限标号 基类名2,...
2)派生类的构成
派生类由两部分组成:第一部分是从基类继承得到的,另一部分是自己定义的新成员,这些成员仍然分为三种访问属性。
注意,友元关系是不能继承的:一方面,基类的友元对派生类成员没有特殊的访问权限;另一方面,如果基类被授予了友元关系,则只有基类有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
实际编程中,设计一个派生类包括三个方面工作:
a)从基类接收成员。
除了构造函数与析构函数之外,派生类会把基类的全部成员继承过来。
b)调整基类成员的访问。
程序员可以对接收的成员指定访问策略。
c)在定义派生类时增加新的成员。
另外还应该自己定义派生类的构造函数和析构函数,因为它们不能从基类继承过来。
二、规则
1.继承重要说明
1)子类拥有父类的所有成员变量和成员函数
2)子类就是一种特殊的父类
3)子类对象可以当作父类对象使用
4)子类可以拥有父类没有的方法和属性
2.public、protected、private,三个关键字分别修饰变量的权限:
public: 修饰的成员变量 方法 在类的内部 类的外部都能使用
protected: 修饰的成员变量方法,在类的内部使用 ,在继承的子类内部中可用 ;其他 类的外部不能被使用。
private: 修饰的成员变量方法 只能在类的内部使用 不能在类的外部
3.不同的继承方式会改变继承成员的访问属性
1)C++中的继承方式会影响子类的对外访问属性
public继承方式: 父类成员在子类中保持原有访问级别
private继承方式: 父类成员在子类中变为private成员。
(注意,在子类中变成了私有的,子类本身内部可以访问,只是不能访问父类私有的而已)
protected继承方式:父类中public成员会变成protected,其余两个保持不变。
2)private成员在子类中依然存在,但是却无法访问到。
重点:不论种方式继承基类,派生类都不能直接使用基类的私有成员。
4.派生类访问控制的结论
1)protected 关键字 修饰的成员变量 和成员函数 ,是为了在家族中使用 ,是为了继承。
2)项目开发中 一般情况下 是 public。保护继承与私有继承在实际编程中极少使用,它们只在技术理论上有意义。
三、类型兼容性原则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下情况:
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。
类型兼容规则是多态性的重要基础之一。
#include <iostream>
using namespace std;
class Parent
{
public:
void printP()
{
cout<<"我是爹..."<<endl;
}
Parent()
{
cout<<"parent构造函数"<<endl;
}
Parent(const Parent &obj) //未具体实现的copy构造函数
{
cout<<"copy构造函数"<<endl;
}
private:
int a;
};
class child : public Parent
{
public:
void printC()
{
cout<<"我是儿子"<<endl;
}
protected:
private:
int c;
};
//C++编译器 是不会报错的 .....
void howToPrint(Parent *base)
{
base->printP(); //父类的 成员函数
}
void howToPrint2(Parent &base)
{
base.printP(); //父类的 成员函数
}
void main()
{
Parent p1;
p1.printP();
child c1;
c1.printC();
c1.printP();
//赋值兼容性原则
//1-1 基类指针 (引用) 指向 子类对象
Parent *p = NULL;
p = &c1;
p->printP();
//1-2 指针做函数参数
howToPrint(&p1);
howToPrint(&c1);
//1-3引用做函数参数
howToPrint2(p1);
howToPrint2(c1);
//第二层含义
//可以让子类对象 初始化 父类对象
//子类就是一种特殊的父类
Parent p3 = c1; //用一个类去初始化另一个类的时候回调用copy构造函数
cout<<"hello..."<<endl;
system("pause");
return ;
}
四、继承中派生类的构造和析构函数
问题:如何初始化父类成员?父类与子类的构造函数有什么关系?
首先:
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
如果:
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
最后:
4、析构函数调用的先后顺序与构造函数相反
#include <iostream>
using namespace std;
//结论
//先 调用父类构造函数调用 再调用 子类构造函数
//析构的顺序 和构造相反
class Parent
{
public:
Parent(int a, int b)
{
this->a = a;
this->b = b;
cout<<"父类构造函数..."<<endl;
}
~Parent()
{
cout<<"析构函数..."<<endl;
}
void printP(int a, int b)
{
this->a = a;
this->b = b;
cout<<"我是爹..."<<endl;
}
private:
int a;
int b;
};
class child : public Parent
{
public:
child(int a, int b, int c) : Parent(a, b) //用初始化列表来调用父类的构造函数
{
this->c = c;
cout<<"子类的构造函数"<<endl;
}
~child()
{
cout<<"子类的析构"<<endl;
}
void printC()
{
cout<<"我是儿子"<<endl;
}
protected:
private:
int c;
};
void playObj()
{
child c1(1, 2, 3);
}
void main()
{
//Parent p(1, 2);
playObj();
cout<<"hello..."<<endl;
system("pause");
return ;
}
五、继承与组合(对象B包含了对象A)混搭情况下,构造和析构调用原则
原则:先构造父类,再构造成员变量(成员变量是一个对象)、最后构造自己
先析构自己,在析构成员变量、最后析构父类
假设如下:派生类对象child,继承了父类Parent,同时有组合(包含)了对象Object。
class child : public Parent //继承了父类
{
public:
child(char *p) : Parent(p) , obj1(3, 4), obj2(5, 6) //先构造父类,再构造成员变量(成员变量是对象)、最后构造自己
{ //成员变量的构造是按定义的顺序来构造的。
this->myp = p;
cout<<"子类的构造函数"<<myp<<endl;
}
~child()
{
cout<<"子类的析构"<<myp<<endl;
}
void printC()
{
cout<<"我是儿子"<<endl;
}
protected:
char *myp;
Object obj1; //成员变量类型,是对象Object。
Object obj2;
};
六、继承中的同名成员变量和函数的处理方法
1、当子类成员变量与父类成员变量同名时
2、子类依然从父类继承同名成员
3、在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符)
4、同名成员存储在内存中的不同位置
重点:如果没有使用作用域分辨符,而直接使用得话,默认使用的是子类的成员变量或者函数。
案例:对象B继承了对象A,并且对象B和对象A,都包含了同名变量b。
#include <iostream>
using namespace std;
class A
{
public:
int a;
int b;
public:
void get()
{
cout<<"b "<<b<<endl;
}
void print()
{
cout<<"AAAAA "<<endl;
}
protected:
private:
};
class B : public A
{
public:
int b;
int c;
public:
void get_child()
{
cout<<"b "<<b<<endl;
}
void print()
{
cout<<"BBBB "<<endl;
}
protected:
private:
};
//同名成员函数
void main()
{
B b1;
b1.print(); // 没有指定作用域的时候,默认情况使用的是子类的成员函数
b1.A::print(); //指定作用域A
b1.B::print(); //指定作用域B
system("pause");
}
//同名成员变量
void main71()
{
B b1;
b1.b = 1; // 没有指定作用域的时候,默认情况修改的是子类的成员变量
b1.get_child();
b1.A::b = 100; //修改父类的b 用作用域分辨符指定作用域
b1.B::b = 200; //修改子类的b 用作用域分辨符指定作用域
b1.get();
cout<<"hello..."<<endl;
system("pause");
return ;
}
七、多继承和虚继承
1.多继承
1)多继承的定义以及多继承的语法
a. 一个类有多个基类,这样的继承关系称为多继承;
b. 多个直接基类,构造函数执行顺序,取决于定义派生类时,指定的各个继承基类的顺序。
c. 多继承声明语法:
class 派生类名: 访问控制符 基类名1,访问控制符 基类名2
{
数据成员和成员函数声明;
}
class A: public B,public c
{
}
图示:
2.虚继承
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题。如下图所示:图1,菱形继承
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。下面是菱形继承的具体实现:
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: public A{
protected:
int m_b;
};
//直接基类C
class C: public A{
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //命名冲突
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
虽然为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
void seta(int a){ B::m_a = a; }
void seta(int a){ C::m_a = a; }
但是这种做法显然不是很合理。
因此,C++为了解决多继承时的命名冲突和冗余数据问题,提出了虚继承,使得在派生类中只保留一份间接基类的成员。
如下图2:使用虚继承解决菱形继承中的命名冲突问题
在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
虚继承的实现分析:
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。