1. 继承概念
- 将共有数据和方法提取到一个类中,这个类就叫做父类/基类
- 一个类继承(调用)父类/基类,这个类就叫做子类/派生类
- 继承体现的就是类设计定义层次的复用
2. 继承定义
#include <iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
//protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
//....
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
s._name = "张三";
s._age = 18;
s.Print();
Teacher t;
t._name = "赵老师";
t._age = 40;
t.Print();
return 0;
}
2.1 定义格式
2.2 继承方式和访问限定符
2.3 继承基类成员访问方式的变化
类成员/继承方式
|
public继承
|
protected继承
|
private继承
|
基类的public成员
|
派生类的public成员
|
派生类的protected成员
|
派生类的private成员
|
基类的protected成员
|
派生类的protected成员
|
派生类的protected
成员
|
派生类的private成员
|
基类的private成员
|
在派生类中不可见
|
在派生类中不可见
|
在派生类中不可见
|
-
基类 private 成员在派生类中无论以什么方式继承都是不可见的, 只有基类自己能够访问
-
基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 取两者权限小的那个
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承
2. 4 默认继承方式
- 使用关键字class时默认的继承方式是private
- 使用struct时默认的继承方式是public
3. 基类和派生类对象赋值转换
#include <iostream>
#include <string>
using namespace std;
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//sobj = (Student)pobj; // error
return 0;
}
- 子类对象可以赋值给父类对象/指针/引用
- 这里虽然是不同类型,但是它们之间也不是隐式类型转换
- 这里算是一个特殊支持,语法天然支持的
4. 继承中的作用域
#include <iostream>
using namespace std;
class Person
{
public:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl; // 999
cout << " 身份证号:" << Person::_num << endl; // 111
}
//protected:
int _num = 999; // 学号
};
int main()
{
Student s;
cout << s._num << endl;
cout << s.Person::_num << endl;
return 0;
}
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫 重定义 (在子类成员函数中,可以使用 基类::基类成员 显示访问)
-
注意在实际中在继承体系里面最好不要定义同名的成员
4.1 隐藏/重定义 VS 重载
需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏/重定义
函数重载要求在同一作用域
#include <iostream>
using namespace std;
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
void main()
{
B b;
b.fun(10);
b.A::fun();
};
5. 派生类的默认成员函数
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
//Person(const char* name = "peter")
Person(const char* name)
: _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)
{
if (this != &s)
{
// 这里写成operator=(s);将会触发父类和子类同名函数的隐藏,引发无穷递归
Person::operator=(s);
_num = s._num;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
// ---------赋值重载函数---------
// ---------析构函数---------
~Student()
{
//Person::~Person();// 不需要显示调用父类析构函数
// ...处理子类自己的
cout << "~Student()" << endl;
}
// ---------析构函数---------
//Student* operator&()
//{
// return this;
//}
protected:
int _num;
// int* _ptr;
};
int main()
{
Student s1("张三", 1);
Student s2 = s1;
Student s3("李四", 2);
return 0;
}
- 派生类和基类的赋值运算符重载函数因为函数名相同构成隐藏,因此在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用。
- 由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();。因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。
- 在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用
5.1 相关说明
- 创建派生类对象时是先创建的基类成员再创建的派生类成员,
- 编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,
所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数
6. 继承与友元
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
//friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
-
这个函数虽然是父类的友元,但是友元关系不能继承,无法访问子类的私有和保护成员
7. 继承与静态成员
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person() { ++_count; }
//protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;// 全局变量要在类外定义
class Student : public Person
{
protected:
int _stuNum; // 学号
};
int main()
{
Person p;
Student s;
p._name = "张三";
cout << s._name << endl;
cout << Student::_count << endl;
++Person::_count;
cout << Student::_count << endl;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
return 0;
}
- 父类的static静态成员和子类继承父类的static静态成员,是一样的,无论派生出多少个子类,都只有一个static成员实例
8. 多种继承方式
8.1 单继承
- 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
8.2 多继承
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
8.3 菱形继承
- 菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。
- 补充除了上面的那种继承关系,这种继承也叫做菱形继承
9.复杂的菱形继承 && 菱形虚拟继承
9.1 复杂的菱形继承
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//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._a = 105;//_a不明确,可能是B的,也可能是C的
d._b = 3;
d._c = 4;
d._d = 5;
B b = d;
B* pb = &d;
C c = d;
C* pc = &d;
/*cout << "pb->" << pb << endl;
cout << "pc->" << pc << endl;*/
cout << &d.B::_a << endl;
cout << &d.C::_a << endl;
return 0;
}
- B类中的_a和C类中的_a是不一样,都开辟了不同空间来存储
9.2 菱形虚拟继承
为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
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._a = 105;//这就是A中的_a
d._b = 3;
d._c = 4;
d._d = 5;
B b = d;
B* pb = &d;
C c = d;
C* pc = &d;
/*cout << "pb->" << pb << endl;
cout << "pc->" << pc << endl;*/
cout << &d.B::_a << endl;
cout << &d.C::_a << endl;
return 0;
}
- virtual使一个继承变成虚拟继承
- B类中的_a和C类中的_a是一样,_a只开辟了一段公共空间来存储
- sizeof(B)大小是12,它里面存了一个_a,_b,还有一个指针
9.3 菱形虚拟继承原理
- 其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
- 虚基表中包含两个数据,
第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),
第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a
10. 继承的总结与反思
-
继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
-
人和学生,植物和玫瑰花,就是is-a的关系
-
-
组合是一种 has-a 的关系。假设B组合了A,每个B对象中都有一个A对象。
-
轮胎和车,脑袋和眼睛,就是has-a的关系
-
-
即能用 public继承 也能用 组合 的的关系
-
锅和铁的关系,vector/list/deque和stack的关系,属于两种都能用,但是 优先使用对象组合,而不是类继承
-
11. 相关笔试面试题
11. 1 请回答下列问题
什么是菱形继承?菱形继承的问题是什么?
- 菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,我们称这种继承为菱形继承
- 菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
什么是菱形虚拟继承?如何解决数据冗余和二义性?
- 菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式
- 菱形虚拟继承对于D类对象当中重复的A类成员只存储一份,
然后利用虚基表指针通过虚基表使得D类对象当中
继承的B类和C类可以找到自己继承的A类成员,
从而解决了数据冗余和二义性的问题。
继承和组合的区别?什么时候用继承?什么时候用组合?
- 继承是一种is-a的关系,而组合是一种has-a的关系。
- 如果两个类之间是is-a的关系,使用继承;如果两个类之间是has-a的关系,则使用组合;
- 如果两个类之间的关系既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合
11. 2 下面程序中p1,p2,p3的大小关系
#include <iostream>
#include <string>
using namespace std;
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
return 0;
}
- Base1* p1 = &d;这是一种切片操作
- 三个指针之间的关系是:p1 == p3 < p2