目录
思维导图大纲:
1.基类和派生类
1.1 定义格式
1.2 继承方式
1.3 基类和派生类的转换
2. 继承中的作用域(隐藏关系)
2.1 考察继承作⽤域相关选择题
3. 派生类的默认成员函数
4. 继承类模板
5. 一个不能被继承的类
编辑 6.继承与友元
编辑 7. 继承与静态成员
编辑 8. 继承类型
8.1 单继承
8.2 多继承
8.2.1 菱形继承
二义性:
数据冗余:
8.3 多继承中的指针偏移问题
9. 继承和组合
继承:is-a
组合:has-a
思维导图大纲:
1.基类和派生类
基类和派生类又可以称作父类和子类,派生类可以继承基类中的成员,并且还可以拥有自己的成员,就好比植物是一个大类,而蒲公英也是一种植物拥有植物的特性(也就是植物类的成员属性),同时蒲公英也拥有自己的其他特点,如传播种子的方式;所以基类和派生类的关系如下图:
1.1 定义格式
1.2 继承方式
我们之前学习过每个类域都有着不同的成员,如public,protected,private。对于公共的成员public不管是类域内还是外都可以访问,对于后面两者只有在类域才可以访问,同样的是基类和派生类之间的基础方式也存在这种关系!
- 由上图可知基类的不同成员和继承方式,会选取权限小的一方继承给派生类
-
基类成员不希望外部访问,只希望派生类访问,因此出现了protected成员
- 如果是基类的private成员无论以什么方式继承给派生类成员,在派生类中我们都不可见,语法上也不可以访问,但是基类的private成员确实继承给了派生类!
- class默认为private继承,struct默认为public继承!
// 人->学生
class Person
{
public:
protected:
string _name = "欧阳";
int _age = 20;
string gender = "男";
private:
int _hide = 1;
};
class Student : public Person // public 继承方式
{
public:
void Print() const
{
cout << _name << endl;
cout << _age << endl;
cout << gender << endl;
cout << _id << endl;
/*cout << Person::_hide << endl; */ // err
}
protected:
string _id = "001";
};
int main()
{
Student s;
s.Print();
return 0;
}
1.3 基类和派生类的转换
由之前我们可以得知派生类会继承基类的成员,所以我们可以将派生类的对象赋值给基类对象,实现这种操作的关键是切片操作!编译器会将派生类对象中与基类对象重合的成员切出来赋值给基类对象 ,但是基类对象不可以赋值给派生类对象,因为基类对象不含有派生类对象的部分成员!基类的指针和引用可以指向派生类对象
// 人->学生
class Person
{
public:
protected:
string _name = "欧阳";
int _age = 20;
string gender = "男";
};
class Student : public Person // public 继承方式
{
public:
protected:
string _id = "001";
};
int main()
{
Person p; // 基类对象
Student s; // 派生类对象
p = s; // 派生类对象->基类对象
//s = p; // err
Person* ptr1 = &s; // 指针
Person& ptr2 = s; // 引用
return 0;
}
2. 继承中的作用域(隐藏关系)
我们都知道基类和派生类属于两个类,因此他们拥有不同的类域,但是如果基类和派生类直接存在相同的变量或者函数时,会发生什么呢?
class A
{
public:
protected:
int _num = 999;
};
class B : public A
{
public:
void Print()
{
cout << _num << endl;
}
protected:
int _num = 111;
};
int main()
{
B b;
b.Print();
return 0;
}
以上代码b.Print()会打印什么呢?是打印基类A的_num,还是派生类B的_num。
为什么是111呢,其实这边基类的_num和派生类的_num构成了隐藏的关系,隐藏起了基类的成员,如果我们需要访问打印基类的成员需要指定类域!
- 注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
class A
{
public:
protected:
int _num = 999;
};
class B : public A
{
public:
void Print()
{
cout << _num << endl;
cout << A::_num << endl;
}
protected:
int _num = 111;
};
int main()
{
B b;
b.Print();
return 0;
}
2.1 考察继承作⽤域相关选择题
class A
{
public :
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public :
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
};
- 看问题一:
首先肯定不是重载关系,重载需要在同一作用域,函数名相同,参数类型,个数,顺序需要有一项不同,返回类型可以相同也可以不同!
A与B是继承的关系,根据前面基类和派生类中的函数只要函数名相同就构成隐藏关系!
- 看问题二:
由于构成了隐藏关系,所以b类中只存在 void fun(int i)函数,b.fun(10);的传参调用是正确的,但是b.fun();的函数调用是错误的!所以会编译报错!
3. 派生类的默认成员函数
- 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
- 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
- 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
-
派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
-
派⽣类对象初始化先调⽤基类构造再调派⽣类构造
-
派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
class Person
{
public:
Person(const char* name = "xxx")
: _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, const char* addrss)
:Person(name)
,_num(num)
,_addrss(addrss)
{}
// 严格说Student拷贝构造默认生成的就够用了
// 如果有需要深拷贝的资源,才需要自己实现
Student(const Student& s)
:Person(s)
,_num(s._num)
,_addrss(s._addrss)
{
// 深拷贝
}
// 严格说Student赋值重载默认生成的就够用了
// 如果有需要深拷贝的资源,才需要自己实现
Student& operator=(const Student& s)
{
if (this != &s)
{
// 父类和子类的operator=构成隐藏关系
Person::operator=(s);
_num = s._num;
_addrss = s._addrss;
}
return *this;
}
// 严格说Student析构默认生成的就够用了
// 如果有需要显示释放的资源,才需要自己实现
// 析构函数都会被特殊处理成destructor()
~Student()
{
// 子类的析构和父类析构函数也构成隐藏关系
// 规定:不需要显示调用,子类析构函数之后,会自动调用父类析构
// 这样保证析构顺序,先子后父,显示调用取决于实现的人,不能保证
// 先子后父
//Person::~Person();
//delete _ptr;
}
protected:
int _num = 1; //学号
string _addrss = "西安市高新区";
int* _ptr = new int[10];
};
int main()
{
Student s1("张三", 1, "西安市");
Student s2(s1);
Student s3("李四", 2, "咸阳市");
s1 = s3;
/*Person* ptr = new Person;
delete ptr;*/
return 0;
}
4. 继承类模板
// 继承类模板
#include <vector>
template<class T>
class Stack : public std::vector<T>
{
public:
void Push(const T& x)
{
push_back(x);
}
void Pop()
{
pop_back();
}
T& top()
{
return back();
}
bool empty()
{
return empty();
}
};
int main()
{
Stack<int> st;
st.Push(1);
st.Push(2);
st.Push(3);
while (!st.empty())
{
cout << st.top() << endl;
st.Pop();
}
return 0;
}
我们编译以上代码会报错!
这是为什么呢?首先我们是一个模板,我们使用Stack模板创建了st这个对象,但是在调用函数时会涉及到vector<int>,这时我们不仅实例化Stack<int>,也实例化vector<int>但是模板是需要按需实例化,所以push_back等函数并没有实例化,就会产生报错,我们在使用继承类模板时需要注意
- 基类是类模板时,需要指定⼀下类域,
- 否则编译报错:error C3861: “push_back”: 找不到标识符
- 因为stack<int>实例化时,也实例化vector<int>了
- 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
// 继承类模板
#include <vector>
template<class T>
class Stack : public std::vector<T>
{
public:
void Push(const T& x)
{
vector<T>::push_back(x);
}
void Pop()
{
vector<T>::pop_back();
}
T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
int main()
{
Stack<int> st;
st.Push(1);
st.Push(2);
st.Push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.Pop();
}
return 0;
}
5. 一个不能被继承的类
需要使用关键字final
class A final
{
public:
};
class B : public A
{
public:
};
int main()
{
B b;
return 0;
}
6.继承与友元
派生类继承基类,但是基类中的友元函数不可以被派生类继承
class Student;
class Person
{
public :
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected :
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员
// 解决⽅案:Display也变成Student 的友元即可
Display(p, s);
return 0;
}
7. 继承与静态成员
派生类继承基类的非静态成员,都会额外开空间进行存储,但是对于静态成员有且只有一个,无论存在多少派生类
class Person
{
public :
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected :
int _stuNum;
};
int main()
{
Person p;
Student s;
// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
cout << &p._name << endl;
cout << &s._name << endl;
// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
// 说明派⽣类和基类共⽤同⼀份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
8. 继承类型
8.1 单继承
8.2 多继承
8.2.1 菱形继承
菱形继承会带来两项问题:
- 二义性
- 数据冗余
二义性:
class Person
{
protected:
string _name;
string _gender;
int _age;
};
class Student : public Person
{
protected:
int _StuNum;
};
class Teacher : public Person
{
protected:
int _id;
};
class Total : public Student, public Teacher
{
protected:
string _data;
};
int main()
{
Total tl;
tl._name = "欧阳"; // 二义性!
return 0;
}
我们可以指定类域解决这种问题
int main()
{
Total tl;
//tl._name = "欧阳";
tl.Student::_name = "欧阳";
tl.Teacher::_name = "ouyang";
return 0;
}
数据冗余:
需要使用虚继承virtual解决这种问题!
class Person
{
public:
string _name;
string _gender;
int _age;
};
class Student : virtual public Person
{
protected:
int _StuNum;
};
class Teacher : virtual public Person
{
protected:
int _id;
};
class Total : public Student, public Teacher
{
protected:
string _data;
};
int main()
{
Total tl;
//tl._name = "欧阳";
tl.Student::_name = "欧阳";
tl.Teacher::_name = "ouyang";
return 0;
}
8.3 多继承中的指针偏移问题
当一个派生类继承多个基类时,指向不同基类的顺序会根据继承顺序产生指针偏移的问题,偏移的大小为前一个基类的数据大小
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;
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
return 0;
}
9. 继承和组合
继承:is-a
继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可 ⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
组合:has-a
对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。组合类之间没有很强的依赖关 系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
低耦合高内聚
// 继承 is-a
class Stack1 : public std::vector<int>
{
};
// 组合 has-a
class Stack2
{
protected:
std::vector<int> _v;
};
int main()
{
return 0;
}