继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用
class person
{
public:
string name;
size_t year;
};
class student : public person
{
public:
int id;
};
继承包含三个部分:
- 基类 (父类)
- 继承方式
- 派生类(子类)
继承关系有3种:public继承,protected继承,private继承
访问限定符有3种:public,protected, private
组合共3*3 = 9种
举几个例子:
class person
{
public:
string name;
protected:
size_t year;
};
class A: public person
{ };
class B:protected person
{ };
int main(void)
{
A a;
a.name = "sfs";//正常
a.year = 15;//报错
//public继承下:name在A中是公有, year在A中是保护
B b;
b.name = "SFS";//报错
b.year = 34;//报错
//protected继承下:name和year在B中都是保护
return 0;
}
那么如何进行记忆呢?
特殊情况:
默认继承方式,当我们没写继承方式时,class定义的类按照private继承,struct按照public继承
虽然这里的组合有9种,但绝大多数情况我们只使用其中的两种:
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
class A
{
protected:
int m = 4;
int n = 3;
};
class a : public A
{
void print()
{
cout << m;
}
public:
int x = 2;
};
int main(void)
{
a a1;
// 子类对象可以赋值给父类对象/指针/引用
A A1 = a1;
A& A2 = a1;
A* A3 = &a1;
return 0;
}
但这种赋值并不是任意的,它有以下条件:
必须是public的继承方式,如果是protected或者private的继承方式,它会出现以下错误
基类的对象不能赋值给派生类的对象
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
对于切片的理解:
对于引用权限有了解的读者,一定对上面的A& A1 = a1
有疑惑。一般情况,当二者的类型不同进行时会进行强制类型转换:即a1会产生一个A类型的临时变量,而后再将临时变量给A1。由于临时变量具有常性,需要const的引用即const A& A1 = a1
。但真实情况是不用加const。这说明了一个事实,就是A& A1 = a1
并没有发生强制类型转换。它的真实情况如下:
就是直接将派生类的基类的那部分直接切给基类的对象/引用/指针,形象的称为切片,因此这个过程并没有强制类型转换
看下列代码:a1.id指的是那个id?
class A
{
public:
int id;
};
class a : public A
{
public:
int id;
};
int main(void)
{
a a1;
a1.id = 3;
return 0;
}
答案是 a中定义的id
在继承体系中基类和派生类都有独立的作用域。., 若子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
也就是说一般情况下,我们访问a中的id,会优先选择a新定义的id。
那如果想访问A中的id,则需要使用使用 基类::基类成员 显示访问
a1.A::id = 3
特别:
成员函数的隐藏,只要函数名相同就会隐藏,不需要参数也相同。
class A
{
public:
void func(int k = 4)
{
cout << "A";
}
int id;
};
class a : public A
{
public:
void func()
{
cout << "a";
}
int id;
};
int main(void)
{
a a1;
a1.func(3);//报错
return 0;
}
默认成员函数有6个
当我们调用派生类的默认成员函数时,必须调用基类的默认成员函数。
class person
{
public:
person(int m)
{
cout << "person()";
}
int a;
};
class student : public person
{
public:
student(int m)
:person(m) //手动调用基类的构造函数
{
cout << "student()";
}
int b;
};
小知识:如果我们想手动调用基类的构造函数,需要写成这样
person::~person
,这里之所以要指定类域,原因是对于析构函数,编译器统一将名字改为destructor
,而相同的名字便会被派生类的析构函数隐藏。
一句话:父亲的朋友不是我的朋友。
基类定义了静态成员变量,则整个继承体系里只有一个这样的成员。
也就是说所有的基类对象和派生类对象共用一个。
class A
{
public:
A()
{
cout++;
}
int id;
static int cout;
};
int A::cout = 0;
class a : public A
{
public:
int id;
};
int main(void)
{
a a1[100];
cout << A::cout; //cout等于100
return 0;
}
一个派生类是只能有一个基类?
c++之父当年在思考这个问题时,认为一个派生类可以有多个基类。毕竟,在现实中,一个人身份是程序员,也是一个父亲。它当然具备程序员类的特征和父亲类的特征,这是多么符合逻辑呀。但代价是什么呢?
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。如下
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";//报错
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
这就是问题,student和teather中都有继承自person的_name,因此在Assistant中会有两个_name.编译器不知道你的_name指的是哪个_name;这就导致很多问题。为了打补丁,c++在随后一个版本中添加了虚拟继承。
虚拟继承:派生类 :virtual 继承方式 基类
class B : virtual public A
没使用虚拟继承的情况:
class A
{
public:
int a = 1;
};
class B : public A
{
public:
int b = 2; //学号
};
class C : public A
{
public:
int c = 3; // 职工编号
};
class D : public B, public C
{
public:
int d = 4;
};
int main()
{
D d1;
return 0;
}
内存情况:
使用了虚拟继承
class A
{
public:
int a = 1;
};
class B : virtual public A
{
public:
int b = 2; //学号
};
class C : virtual public A
{
public:
int c = 3; // 职工编号
};
class D : public B, public C
{
public:
int d = 4;
};
int main()
{
D d1;
return 0;
}
内存情况:
问题:为什么指针不直接存储a的地址,而是指向一块空间,那块空间存储指针到a的偏移量?
答:如果A类中还有其他成员变量,即公有的除了a还有其他变量。那一个指针怎么存储多个变量的值呢?正是考虑到这一点,指针并没有直接存储a变量的地址,而是指向一块空间,存储a的偏移量。
这只是其中一个原因。
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car{
public:
void Drive() {cout << "好开-操控" << endl;}
};
class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};