目录
1.继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
class Person
{
public:
void print()
{
}
string name;
}
class Student:public Person
{
protected:
int _id;
}
继承的语法
如上,
class Student:public Person
Student叫做派生类,或者子类。Person叫做基类,或者父类。public表示继承方式,有三种private,public,protected。
继承的内容
一般的成员函数和成员变量都会被继承。
如果基类有静态成员变量,则在整个继承体系中都只有一个这样的成员变量实例。故,静态成员变量和成员函数会被继承,但其属于整个继承体系。
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
string _seminarCourse ; // 研究科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人数 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人数 :"<< Person ::_count << endl;
}
基类的友元函数不会被继承。
继承方式对比
关于基类的不同访问属性的成员在不同的继承方式下表现什么访问属性的表格:
总结:
1.访问属性和继承方式,取权限小的一个就是派生类中成员的访问属性了。
2.基于以上特点,一般在写C++类的时候,成员变量的访问属性为protected,继承方式多为public
2.基类和派生类对象赋值转换
关于赋值转换,有以下结论:
1.对于char和int,二者都属于整型家族,如这样的代码
int i = 9; char c = i; // char c = 'c' int a = c;
会发生整型截断或整型提升,并不是类型转换。
2.
int i = 2; double d = i;
不同的类型之间要怎样赋值呢?这种语法设计就叫做类型转换。当时不同类型的存储结构不同,二者又无法直接转换。因此,这时,会生成一个临时变量x,这块空间具有常量性,不可以访问修改,用x赋值给d
在知道以上基础之后,来看看继承体系中的类型转换:
1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
3.继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
函数名相同,子类将父类的同名函数隐藏。
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
void Test()
{
B b;
b.fun(10);
};
4.派生类的默认函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
解释:
1.继承体系中,派生类和基类的处理是独立的,派生类并不干涉基类的处理。
2.基于第一点,派生类对象在初始化时,先调用基类的构造函数,再调用派生类的构造函数。这样设计是一种保护机制,如果先调用派生类构造函数,那么就意味着派生类对象可以访问基类的成员,而此时基类还没有完成初始化,这时会导致非法访问。同理,在调用析构函数的时候,先析构派生类会更安全。
5.菱形继承
基于以上的继承设计。
这是单继承:
class Person
{
};
class Student:public Person
{
};
class PostStudent: public Student
{
};
这是多继承:
class Person
{
};
class Student:public Person
{
};
class Teacher: public Person
{
};
class Assistant:public Student,public Teacher
{
};
多继承中有一种特殊情况:菱形继承。
如图,Assistant继承了两份Person的成员,有数据冗余和二义性的问题。我们来看看内存中的具体情况。
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";
}
C++的设计人员苦思冥想,想出来了一种解决办法 ,虚拟继承。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
关键字virtual在哪里加呢?由于是Person中的成员造成的问题,那么哪个类继承了Person,就在Person派生出的类加virtual。
来看看内存中的具体情况:
要怎么理解这种内存设计?
1.冗余的部分只存了一份,在所有成员的最下面(vs是这样设计的),B和C中除了存自己的成员变量之外,还存了一个指针,该指针指向的内存空间中,有一个数值,如图这里是0c,这个值是什么意思呢?用C的地址加该值,就是冗余的那一个值,即类A的地址。这样设计就是为了,中间继承的类也可以访问最开始被继承的那个类。
2.B和C的两个指针,指向的这块内存空间,形容为一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
基于以上解释,在使用C++的继承设计时,不要设计出菱形继承!!!
6.继承和组合
在C++设计出菱形继承后,C++的语法犹如当头一棒,因此,在后来的面向对象的语言中,比如java,没有设计多继承。
继承本质是一种代码复用,那么还有一种写法:
//组合
class A
{
public:
void func()
{
}
protected:
int i;
}
class B
{
protected:
A _a;
}
这种写法也是代码复用的手段,称为组合。
组合和继承的区别:
1.public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
2.组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。举例来说:
Person类派生Student,每一个Student都是一个Person,这是is -a 的关系,不能使用has -a
汽车类和轮胎类,汽车有轮胎,并不是is -a的关系,这时就要用组合
而stack和deque/list/vector,它们之间的关系既可以是is-a,也可以是has-a,但是,优先使用has-a
3.实际开发过程中,组合优于继承。
4.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
5.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
6.实际尽量多去用组合。组合的耦合度低,代码维护性好。如果要实现多态,则必须要用继承。