继承
一、初识继承
概念
继承:保持原有类特性的基础上进行扩展,增加功能,产生新的类。新的类就叫做派生类(子类),原有类就叫做基类(父类)。
继承的作用:继承机制是面向对象程序设计使代码可以复用的最重要的手段,继承是类设计层次的复用,呈现了面向对象程序设计的层次结构。
“登场”
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
cout << endl;
}
protected:
string _name = "张三";
int _age = 18;
};
//继承后,父类的Person成员(成员函数 + 成员变量)都会成为子类一部分
class Student : public Person
{
protected:
int _stuId;
};
int main()
{
Student s;
s.Print();
return 0;
}
结论:代码体现出Student对Person的继承(复用)
语法格式
继承方式
继承方式和访问限定符:
九种继承方式组合
C++中的继承方式和访问限定符组合,形成了九种情况的继承结果
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
eg:实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:
void Print()
{
cout << "名字:" << _name << endl;
}
protected:
string _name = "张三";
private:
int _age = 18;
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:
int _stuId = 111;
};
int main()
{
Student s;
s.Print();
return 0;
}
小结(对九种组合解释)
不需要全部记完,这两种是最常用的记住即可:
- 在九种组合表中,基类的私有成员是不可见的。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符, 继承方式),(public > protected > private)。 Min:两者的较小者。
- 基类private成员在派生类中什么方式都不可见。不可见:基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能访问
- 基类成员不想在类外直接被访问,但要在派生类中能访问,就要定义为protected。保护成员限定符是因为继承才出现的
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。建议:显示写
- 实际运用一般都是public继承,扩展维护性强
二、继承的特性
赋值转换 一一 切片 / 切割
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这个过程叫切片或者切割,不会产生临时变量,发生赋值兼容,就同把派生类中父类那部分切来赋值过去。
原理:(切片 / 切割)
eg:证明:不会产生临时变量
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _No;
};
int main()
{
int i = 0;
//double& d = i; //error
const double& rd = i; //int赋值给double类型的值,会产生临时变量,所以要+const
//派生类对象可以直接赋值给基类对象,不要+const,也就证明,这个过程没有产生临时变量
Student s;
Person& p = s;
return 0;
}
注意:派生类对象赋值给基类的对象(或者基类的指针,或者基类的引用)这个过程称为向上转换。
拓展(不作详细介绍): 基类的指针和引用可以通过强转赋值给派生类的指针或者引用,但基类的指针是指向派生类对象时才安全。-- 这个过程称为向下转换。注意:基类对象不能赋值给派生类对象。
eg:
class Person
{
//protected:
public:
string _name = "peter";
string _sex = "male";
int _age = 18;
};
class Student : public Person
{
public:
int _No = 2140104111;
};
int main()
{
Student s;
//1.派生类对象可以赋值给父类对象/指针/引用
Person p = s;
Person* ptrp = &s;
ptrp->_age = 21;
Person& rp = s;
rp._name = "张三";
//2.基类对象不能赋值给派生类对象
//s = p; //error
return 0;
}
代码分析:
注意:使用保护继承,成员权限会发生变化
Person p = s; //就会出现错误
这是因为保护继承下,派生类的对象只能被派生类或派生类的子类引用,而不能被基类引用。
原理: 因为非公有派生类(私有或保护派生类)不能实现基类的全部功能,例如在派生类外不能调用基类的公用成员函数访问基类的私有成员。因此,只有公有派生类才是基类真正的子类型,它完整地继承了基类的功能。
作用域 一一 隐藏 / 重定义
- 在继承体系中基类和派生类都有独立的作用域
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问(子类成员隐藏父类成员),这叫做隐藏(或者重定义)。 (在子类成员中,可以 基类::基类成员 显示访问。但是指定作用域,如果找不到会直接报错,不会再去访问别的域。eg:派生类成员)
- 成员函数隐藏:函数名相同就构成隐藏
- 建议:最好不要定义同名成员
拓展:访问成员遵循就近原则(编译器既定顺序):局部域-当前类域-父类域-全局域
eg1:成员变量
class Person
{
protected:
string _name = "张三";
int _num = 111;
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
//指定显示访问
cout << "Person::_num:" << Person::_num << endl;
//默认访问子类。子类隐藏了父类
cout << "_num:" << _num << endl;
}
protected:
int _num = 999;
};
int main()
{
Student s;
s.Print();
return 0;
}
//output:
//姓名:张三
//Person::_num:111
//_num : 999
eg2:成员函数
//fun不构成重载,因为不在同一作用域
//fun构成隐藏,成员函数满足函数名相同
class A
{
public:
void fun()
{
cout << "fun()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "fun(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(1);
//b.fun(); //参数不匹配
//指定访问
b.A::fun();
return 0;
}
//output:
//fun()
//fun(int i)->1
//fun()
三、派生类的默认成员函数
派生类的默认成员函数
演示代码:后面会分为四个部分进行拆分讲解
class Person
{
public:
//如果没有默认构造,必须在派生类的初始化列表显示调用
//Person(const char* name)
Person(const char* name = "peter")
:_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;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
:Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num;
};
int main()
{
Student s1("jack", 18); //构造函数
Student s2(s1); //拷贝构造函数
Student s3("rose", 17);
s1 = s3; //赋值运算符重载
return 0;
}
构造和析构调用和执行顺序图:
1. 构造函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造,则必须在派生类构造的初始化列表阶段显式调用
- 派生类对象初始化先调用基类构造再调派生类构造
eg1:(有默认构造)
class Person
{
public:
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
:_stuId(num)
{
cout << "Student()" << endl;
}
protected:
int _stuId;
};
int main()
{
Student s("jack", 18);
return 0;
}
代码F11逐语句执行过程:
eg2:(没有默认构造)
基类没有默认构造,在派生类必须显示调用,Person先初始化,然后是_stuId。(基类先声明,所以先初始化Person)
//没有默认构造
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
:Person(name) //在初始化列表调用基类默认构造。如同定义匿名对象
, _stuId(num)
{
cout << "Student()" << endl;
}
protected:
int _stuId;
};
int main()
{
Student s("jack", 18);
return 0;
}
代码F11逐语句执行过程:
2. 拷贝构造
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
演示代码的拷贝构造部分:
3. 赋值运算符重载
- 派生类的operator=必须调用基类的operator=完成基类的复制
4. 析构函数
- 派生类的析构会在调用完成后自动调用基类的析构函数清理基类成员。原因:为了保证派生类对象,先清理派生类成员再清理基类成员的顺序
- 因为后续的一些场景,析构函数要构成重写,重写的条件之一就是函数名相同。所以编译器对析构函数名进行特殊处理,处理成destructor()。所以父类析构函数不+virtual,子类析构函数和父类析构函数构成隐藏关系。
四、延伸知识
1. 继承与友元
友元关系不能继承,所以基类的友元不能访问子类私有成员和保护成员
eg:
class Student; //先声明,因为在Person中引用了Student对象
class Person
{
friend void Dispaly(const Person& p, const Student& s);
protected:
string _name = "张三";
};
class Student : public Person
{
protected:
int _stuId = 0;
};
void Dispaly(const Person& p, const Student& s)
{
cout << s._name << endl; //ok
cout << p._name << endl; //ok
cout << s._stuId << endl; //error
}
int main()
{
Dispaly(Person(), Student());
return 0;
}
注意:如果想要在Display()中调用s._stuId,要在Student类中也加上友元
2. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name;
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int stuId;
};
class Graduate : public Student
{
protected:
string _seminarCourse;
};
int main()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << "人数:" << Person::_count << endl;
Graduate::_count = 0;
cout << "人数:" << Person::_count << endl;
return 0;
}
//output:
//人数:4
//人数:0
注意:静态成员属于父类和派生类,在派生类不会单独再拷贝一份,继承的是使用权。eg:上面的代码使用的始终都是一个_count
五、单继承和多继承
单继承
单继承:一个子类只有一个直接父类时称这个关系为单继承
多继承
菱形继承
菱形继承:多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承由数据冗余(浪费空间)和二义性(不知道访问谁) 问题,在Assistant的对象中Person成员有两份
菱形继承代码:
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _stuId;
};
class Teacher : public Person
{
protected:
int _workId;
};
class Assistant : public Student, public Teacher
{
protected:
string _course;
};
int main()
{
Assistant a;
//这样会有二义性问题,无法明确访问的哪一个
//a._name = "peter"; //error
//显示指定访问那个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
对于菱形继承解决不了的问题,出现了虚拟继承。
菱形虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余问题。
语法
如上面菱形继承代码的继承关系,在Student和Teacher继承Person时使用虚拟继承。
eg:
原理
借用简化的菱形继承体系,通过内存窗口观察对象成员的模型
菱形继承
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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
菱形虚拟继承
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
注意: D中为什么B和C部分要找属于自己的A
解释:
菱形虚拟继承的原理解释:
总结
- 虚拟继承的缺陷:虚拟继承会增加程序的复杂性,因为派生类需要特别处理虚基类的初始化和访问。虚拟继承还可能导致一些性能上的损失,因为派生类需要额外的指针来访问虚基类。-- 不建议使用菱形继承
- 继承和组合:
- public继承是一种is-a的关系。eg:植物和花
- 组合是一种has-a的关系。 eg:轮胎和车
拓展知识:组合
**优先使用对象组合,而不是类继承,**组合耦合度低,代码维护性好
- 白盒测试:知道底层
- 黑盒测试:不知道底层
- 继承,通过生成派生类的复用称为白箱复用(white-box reuse)。白箱(相对可视性而言):在继承方式中,基类内部细节对子类可见。继承一定程度破坏了基类的封装。耦合度高:基类的改变极大的影响派生类,两者关系紧密
- 组合,新的更复杂的功能可以通过组装或组合对象获得。被组合对象具有良好定义的接口。这种复用风格称为黑箱复用(black-box reuse),**对象内部细节不可见。**组合类之间没有很强的依赖关系,耦合度低
eg:
//继承
//Car和BMW Car和Benz构成is-a关系
class Car
{
protected:
string _color = "白色";
string _num = "陕IT6666";
};
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 _color = "白色";
string _num = "陕IT6666";
Tire _t;
};