1. 继承的介绍
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。原有类也叫父类、基类。派生类也叫子类。
问题
为什么会有继承?
有两个类(例如student和teacher),他们有相同的成员变量(name,sex,age,tele等),就会有冗余的代码,此时就可以将这些共有信息封装成一个类,然后继承这个类,以此为基础再去定义每个类的特有信息。
class Person
{
public:
string _name;
string _sex;
};
class Student :public Person
{
public:
int _grade;//成绩
};
class Teacher :public Person
{
public:
string title;//职称
};
Student和Teacher这两个子类继承了父类Person后,父类成员变成子类的一部分。
2. 继承的定义
继承方式包括public继承、protected继承和private继承,与访问限定符相同。
在不同的继承方式下,派生类继承不同的基类成员有不同的结果,如下表。
基类成员\派生类继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
- 基类的成员的访问方式,决定了继承后在派生类的成员的访问方式的上限。
- 基类public成员以什么方式继承,它便是什么成员;基类protected成员就算以public继承,它也只能是protected成员;基类private成员继承后不可见,并不意味着没有继承,而是继承后派生类无法使用(不管是类内还是类外)。
- 如果基类成员不想在类外被访问,但可以在派生类中访问,可以写成protected成员。
- 实际上应用最多的是public成员,protected成员和private成员在派生类中用得较多。
3. 基类和派生类赋值转换
- 派生类可以赋值给基类对象,指针和引用。可以理解为将派生类中父类那部分切割给子类。
class Person
{
public:
string _name;
string _sex;
};
class Student :public Person
{
public:
int _grade;
};
void test3()
{
Student s;
Person p1 = s;
Person* p2 = &s;
Person& p3 = s;
}
-
将派生类对象赋值给基类对象,不会生成临时变量。有人认为将s赋值给p1,实际上是对s进行强制类型转换。如果是强制类型转换的话,会产生临时变量,就得用const Person& p1 = s。但很显然不是,s只是将父类部分切出,拷贝给p1。
-
基类对象不能赋值给派生类对象。因为基类对象中没有派生类特有的成员。
4. 继承作用域和成员隐藏
- 基类和派生类有自己独立的作用域。这也就意味着父类和子类可以有同名变量。出现同名变量,编译器会隐藏父类的同名变量,优先考虑子类,这种情况叫做隐藏,也叫重定义。如果要访问父类的同名变量,得用域名::成员。
class Person
{
public:
string _name = "zhangsan";
string _sex = "nan";
int _age = 20;
};
class Student :public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "性别:" << _sex << endl;
cout << "成绩:" << _grade << endl;
//有同名变量_age,Person的_age会被隐藏,除非用(域名::同名变量)访问
cout << "年龄:" << _age << endl;
cout << "年龄:" << Person::_age << endl;
}
int _grade = 100;
int _age = 18;
};
void test4()
{
Student s;
s.Print();
}
- 子类也可以隐藏父类的同名成员函数,只要子类和父类的成员函数的函数名相同就构成隐藏。
//这又一个有趣的例子,A中的fun和B中的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 test5()
{
B b;
b.fun(10);
};
结果
A中的fun和B中的fun不构成重载,前面我们提到派生类和基类的作用域是独立的,而构成重载的条件之一是在同一作用域中,所以不构成重载。由于函数名相同,所以构成重定义。
- 因此,最好不要在继承体系中定义同名变量。
5. 派生类的默认成员函数
class Person
{
public:
Person(const char* name = "zhangsan")
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
{
_name = p._name;
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
string _name;
};
class Student :public Person
{
public:
//1. 构造
//如果基类没有默认构造,派生类的构造必须调用基类的构造;基类有默认构造,派生类构造时会自动调用
Student(const char* name, int grade)
:Person(name)//注意初始化顺序,先父类,再子类
,_grade(grade)
{
cout << "Student()" << endl;
}
//2. 拷贝构造
//子类的拷贝构造必须调用父类的拷贝构造完成父类的拷贝
Student(const Student& s)
:Person(s)//刚好利用了派生类和基类赋值切割,s切割出Person那部分赋值
,_grade(s._grade)
{
cout << "Student(const Student& s)" << endl;
//Person(s);
//_grade = s._grade;
}
//3. =重载
//派生类的operator=必须要调用基类的operator=完成基类的复制
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
//函数名相同,父类函数被隐藏,必须用(域名::函数)名访问
Person::operator=(s);
_grade = s._grade;
return *this;
}
//4. 析构
//析构函数必须满足先析构子类对象,再析构子类中父类部分。原因有二:
//一是因为构造子类对象前,会先构造父类部分,所以销毁时后构造的先析构;
//二是如果先析构父类部分,接下里子类还可能访问父类成员。而先析构子类,
//父类不可能用到子类的成员。
~Student()
{
cout << "~Student()" << endl;
}
int _grade = 100;
};
void test6()
{
Student s1("张三", 100);
Student s2(s1);
Student s3("李四", 90);
s1 = s3;
}
6. 继承中的友元与静态成员
- 友元关系是不能继承的。父类的友元不是子类的友元,不能调用子类的保护或者私有成员。
- 父类定义的静态成员,在整个继承体系中不会重新定义。无论派生出多少子类,都只有一个static成员实例。
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name = "zhangsan"; // 姓名
static int _count;//人数
};
int Person::_count = 0;
class Student : public Person
{
public:
void Print()
{
cout << "目前学生人数:" << _count << endl;
}
protected:
int _num = 0; // 学号
};
void test7()
{
Student s1;
Student s2;
Student s3;
Student s4;
s1.Print();
}
7. 多继承和菱形虚拟继承
- 单继承是指一个子类只有一个父类的继承关系;而多继承是指一个子类至少有两个父类的继承关系。
- 菱形继承也是多继承的一种。
但菱形继承有两个问题:数据冗余和二义性。
class Person
{
public:
string _name = "zhangsan";//姓名
};
class Classmate:public Person
{
protected:
int _snum = 1;//座号
};
class Roommate :public Person
{
protected:
int _bnum = 6;//床号
};
class Friend :public Classmate,public Roommate
{
protected:
int _tele = 16;//电话
};
void test8()
{
Friend f;
//f._name;//×,不知道访问哪个_name,这就是二义性问题
//得显示指定是哪个父类的成员,可以解决二义性问题,但不能解决数据冗余的问题
f.Classmate::_name = "nihao";
f.Roommate::_name = "buhao";
}
- 菱形虚拟继承可以同时解决数据冗余和二义性问题。在Classmate和Roommate继承Person时用虚拟继承。注意virtual的位置。
class Classmate : virtual public Person
{
protected:
int _snum = 1;//座号
};
class Roommate : virtual public Person
{
protected:
int _bnum = 6;//床号
};
这是为什么?那两个地址指向的又是什么?
- 菱形虚拟继承的原理
未加虚拟继承
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
class C :public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
void test9()
{
D d;
d.B::_a = 1;
d._b = 2;
d.C::_a = 10;
d._c = 11;
d._d = 0;
}
加虚拟继承后
- 实际上虚拟继承很复杂 ,上面只是简单的例子。一般不建议设计多继承,更不建议设计菱形继承。
8. 继承与组合
//这是继承
//继承关系是一种is-a的关系,例如Student is a person.
class Person
{
public:
string _name;
};
class Student :public Person
{
public:
int _num;
};
//这是组合
//组合关系是一种has-a的关系,例如Room has a bed.
class Bed
{
public:
int _size;
};
class Room
{
public:
Bed b;
int _NumOfP;
};
问题
那么继承和组合哪种更好用?
我的建议是能用组合的情况下,尽量用组合。原因如下:
- 在继承中,基类的内部细节对子类是可见的。这在一定程度上破坏了基类的封装,同时如果基类修改,派生类也会受到影响,派生类对基类的依赖性强,耦合度高。这种通过基类生成派生类的复用叫做“白箱复用” 。
- 在组合中,类通过组合其他类获得想要的功能,对其他类内部细节不可见。其他类的修改,彼此间的影响低,组合类间的依赖关系不强,耦合度低。这种复用叫做“黑色复用”。