类继承
- 继承一笔财产比白手起家要轻松。
- 重用经过测试的、可靠的代码,比从头开始写代码要好得多。
- 类继承的目的是代码重用。
类继承-基本概念
继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。
语法:
class 派生类名:[继承方式] 基类名
{
派生类新增的成员
}
被继承的类称为基类或父类,继承的类称为派生类或子类。
继承和派生是一个概念,只是站的角度不同。
派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能。
使用继承的场景:
- 如果新创建的类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。
- 当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员提取出来,定义为基类,然后从基类继承。
对于1:
我们设计一个海选报名者类,这个类里面的成员函数只有姓名和联系方式。报名时需要唱一首歌,长的好听就可以留下,正式成为一名超女,所以我们还有一个超女类,继承海选报名表类。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CAIIComers //海选报名表类。
{
public:
string m_name;//姓名
string m_tel;//联系方式
//构造函数
CAIIComers() { m_name = "某女";m_tel = "不详"; }
//报名时需要唱一首歌
void sing() { cout << "我是一只小小鸟。\n"; }
//设置姓名
void setname(const string& name) { m_name = name; }
//设置电话号码
void settel(const string& tel) { m_tel = tel; }
};
class CGirl :public CAIIComers {//超女类
public:
int m_bh;//编号
CGirl() { m_bh = 8; }
void show(){ cout << "编号:" << m_bh << ",姓名:" << m_name << ",联系电话:" << m_tel << endl; }
};
int main()
{
CGirl g;
g.setname("西施");
g.show();
}
对于2:例如
类继承-继承方式
类成员的访问权限由高到低依次为: public -> protected -> private
,public成员在类外可以访问,private
成员只能在类的成员函数中访问。
如果不考虑继承关系,protected
成员和private
成员一样,类外不能访问。但是,当存在继承关系时,protected
和private
就不一样了。基类中的protected
成员可以在派生类中访问,而基类中的private
成员不能在派生类中访问。
继承方式有三种: public(公有的)
、 protected(受保护的)
和private(私有的)
。它是可选的,如果不写,那么默认为private
。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限。
这张图描述了三种继承方式下,派生类成员访问权限的变化。
-
基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为
protected
时,那么基类成员在派生类中的访问权限最高也为protected
,高于protected
的会降级为protected
,但低于protected
不会升级。再如,当继承方式为public
时,那么基类成员在派生类中的访问权限将保持不变。
也就是说,继承方式中的public
、protected
、private
是用来指明基类成员在派生类中的最高访问权限的。 -
不管继承方式如何,基类中的
private
成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。 -
如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为
public
或protected
;只有那些不希望在派生类中使用的成员才声明为private
。 -
如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为
protected
。
由于private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中,一般使用public
在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。
例如:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A //基类。
{
private:
int m_a = 10;
public:
int m_b = 20;
void func() {
m_a = 11;
cout << "m_a" << m_a << endl;
}
};
class B :public A {//派生类
};
int main()
{
B b;
// b.ma = 11;
b.func();
}
**使用`using`关键字可以改变基类成员在派生类中的访问权限。**
注意: `using`只能改变基类中`public`和`protected`成员的访问权限,不能改变`private`成员的访问权限,因为基类中`private`成员在派生类中是不可见的,根本不能使用。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A //基类。
{
public:
int m_a = 10;
protected:
int m_b = 20;
private:
int m_c = 30;
};
class B :public A {//派生类
public:
using A::m_b;//把m_b的权限修改为共有的
private:
using A::m_a;//把m_a的权限修改为私有的
};
int main()
{
B b;
//b.m_a = 11;
b.m_b = 21;
//b.m_c = 21;
}
类继承-继承的对象模型
- 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
- 销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
A() { cout << "调用了A的构造函数"; }
~A() { cout << "调用了A的析构函数"; }
};
class B :public A {//子类
public:
B() { cout << "调用了B的构造函数.\n"; }
~B() { cout << "调用了B的析构函数.\n"; }
};
class C :public B {//孙类
public:
C() { cout << "调用了C的构造函数.\n"; }
~C() { cout << "调用了C的析构函数.\n"; }
};
int main() {
C c;
}
- 创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间
this
指针相同的。 - 创建派生类对象时,先初始化基类对象,再初始化派生类对象。
- 在VS中,用
cl.exe
可以查看类的内存模型。 - 对派生类对象用sizeof得到的是基类所有成员(包括私有成员)+派生类对象所有成员的大小。
- 在C++中,不同继承方式的访问权限只是语法上的处理。
- 对派生类对象用memset()会清空基类私有成员。
- 用指针可以访问到基类中的私有成员(内存对齐)。
类继承-如何构造基类
派生类构造函数的要点如下:
- 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
- 如果没以指定基类构造函数,将使用基类的默认构造函数。
- 可以用初始化列表指明要使用的基类构造函数。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
int m_a;
private:
int m_b;
public:
A() :m_a(0), m_b(0) {//基类的默认构造函数
cout << "调用了基类的默认构造函数A().\n";
}
A(int a, int b):m_a(a),m_b(b) {//基类的两个参的默认构造函数
cout << "调用了基类的默认构造函数A(int a, int b).\n";
}
A(const A& a) :m_a(a.m_a + 1), m_b(a.m_b + 1) {//基类的拷贝构造函数
cout << "调用了基类的拷贝构造函数A(const A& a).\n";
}
//显示基类A全部的成员
void showA() {
cout << "m_a=" << m_a << ",m_b=" << m_b << endl;
}
};
class B :public A {//派生类
public:
int m_c;
B() :m_c(0),A() {//派生类的默认构造函数
cout << "调用了派生类的构造函数B()\n";
}
B(int a, int b, int c) :A(a, b), m_c(0) {//基类的三个构造函数
cout << "调用了派生类的构造函数B(int a,int b,int c)。\n";
}
B(const A& a, int c) :A(a), m_c(c) {//指明基类的拷贝构造函数
cout << "调用了派生类的拷贝构造函数B(constA &a,int c)。\n";
}
//显示派生类B全部的成员
void showB() {
cout << "m_c=" << m_c << endl << endl;
}
};
int main() {
B b1;//调用基类的默认构造函数
b1.showA();
b1.showB();
B b2(1, 2, 3);//调用两个参数的构造函数
b2.showA(); b2.showB();
A a(10, 20);//创建基类对象
B b3(a, 30);//将调用基类的拷贝构造函数
b3.showA();b3.showB();
}
- 基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。
- 派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
类继承-名字遮挡和类作用域
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的。
例如:这个执行结果是80;
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
int m_a = 30;
void func() { cout << "调用了A的func()函数.\n"; }
};
class B :public A {//派生类
public:
int m_a = 80;
void func() { cout << "调用了B的func()函数.\n"; }
};
int main() {
B b;
cout << "m_a的值是:" << b.m_a << endl;
b.func();
}
注意:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数。
类是一种作用域,每个类都有它自己的作用域,在这个作用域之内定义成员。
在类的作用域之外,普通的成员只能通过对象(可以是对象本身,也可以是对象指针或对象引用)来访问,静态成员可以通过对象访问,也可以通过类访问。
在成员前面加类名和域解析符可以访问对象的成员。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
int m_a = 30;
int m_b = 50;
void func() { cout << "调用了A的func()函数.\n"; }
void func(int m_a) { cout << "调用了A的func(int a)函数";A::m_a = m_a; }
};
int main() {
A a;
cout << "m_a的值是:" << a.A::m_a << endl;
a.A::func();
}
如果不存在继承关系,类名和域解析符可以省略不写。
当存在继承关系时,基类的作用域嵌套派生类的作用域中。
如果成员在派生类的作用域已经找到,就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找。
如果在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员。
例如这个:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
int m_a = 10;
void func() { cout << "调用了A的func()函数.\n"; }
};
class B :public A {//子类
public:
int m_a = 20;
void func() { cout << "调用了B的func()函数.\n"; }
};
class C :public B {//孙类
public:
int m_a = 30;
void func() { cout << "调用了C的func()函数.\n"; }
};
int main() {
C c;
cout << "C::m_a的值是:" << c.C::m_a << endl;
cout << "B::m_a的值是:" << c.B::m_a << endl;
cout << "A::m_a的值是:" << c.A::m_a << endl;
c.C::func();
c.B::func();
c.A::func();
}
类继承-继承的特殊关系
派生类和基类之间有一些特殊关系。
- 如果继承方式是公有的,派生类对象可以使用基类成员。
- 可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
int m_a = 0;
private:
int m_b = 0;
public:
void show() { cout << "A::show() m_a="<<m_a<<",m_b="<<m_b<<endl; }
void setb(int b) { m_b = b; }
};
class B :public A {//子类
public:
int m_c = 0;
void show() { cout << "B::show() m_a=" << m_a << ",m_c=" << m_c << endl; }
};
int main() {
A a;
B b;
b.m_a = 10;
b.setb(20);
b.m_c = 30;
a.show();
a = b;
a.show();
}
- 基类指针可以在不进行显式转换的情况下指向派生类对象。
如果是指针的话要用->
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {//基类
public:
int m_a = 0;
private:
int m_b = 0;
public:
void show() { cout << "A::show() m_a="<<m_a<<",m_b="<<m_b<<endl; }
void setb(int b) { m_b = b; }
};
class B :public A {//子类
public:
int m_c = 0;
void show() { cout << "B::show() m_a=" << m_a << ",m_c=" << m_c << endl; }
};
int main() {
B b;
A* a = &b;
b.m_a = 10;
b.setb(20);
b.m_c = 30;
a->m_a = 11;
a->setb(22);
//a->m_c = 30;
a->show();
}
- 基类引用可以在不进行显式转换的情况下引用派生类对象。
注意: - 基类指针或引用只能调用基类的方法,不能调用派生类的方法。(主要是多态哪里)
- 可以用派生类构造基类。
- 如果函数的形参是基类,实参可以用派生类。
- C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。
类继承-多继承和虚继承
多继承的语法:
class派生类名:[继承方式1] 基类名1,[继承方式2] 基类名2,…
{
派生类新增加的成员
}
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A1 {//基类一
public:
int m_a = 10;
};
class A2 {//基类二
public:
int m_b = 20;
};
class B :public A1, public A2 {//派生类
public:
int m_c = 30;
};
int main() {
B b;
cout << "m_a的值是:" << b.m_a << ",m_b的值是:" << b.m_b << ",m_c的值是:" << b.m_c << endl;
}
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A1 {//基类一
public:
int m_a = 10;
};
class A2 {//基类二
public:
int m_a = 20;
};
class B :public A1, public A2 {//派生类
public:
int m_a = 30;
};
int main() {
B b;
cout << "B::m_a的值是:" << b.m_a << endl;
cout << "A1::m_a的值是:" << b.A1::m_a << endl;
cout << "A2::m_a的值是:" << b.A2::m_a << endl;
}
菱形继承:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {
public:
int m_a = 10;
};
class B:public A {
};
class C :public A {
};
class D :public B, public C {};
int main() {
D d;
//d.m_a会报错
d.B::m_a = 30;
d.C::m_a = 20;
cout << "B::m_a的地址是:" << &d.B::m_a << ",值是:" << d.B::m_a << endl;
cout << "C::m_a的地址是:" << &d.C::m_a << ",值是:" << d.C::m_a << endl;
}
对于这个代码:如果直接赋值d.m_a
会报错。B,C这是两个地址两个值。
也就是说,菱形继承存在两个问题:数据冗余和名称的二义性。解决这个问题,C++引入了虚继承技术。语法很简单,在派生类B和C继承A的定义中,加上virtual关键字
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {
public:
int m_a = 10;
};
class B:virtual public A {
};
class C:virtual public A {
};
class D:public B, public C {};
int main() {
D d;
//d.m_a会报错
d.B::m_a = 30;
d.C::m_a = 20;
cout << "B::m_a的地址是:" << &d.B::m_a << ",值是:" << d.B::m_a << endl;
cout << "C::m_a的地址是:" << &d.C::m_a << ",值是:" << d.C::m_a << endl;
}