1.引入继承
学生管理系统
学生
老师
社管阿姨
保安大叔
4个类 4个类有很多重复的东西
name age telephone ...等
继承 Person->老师 学生等
从这里我们可以看到继承目的:类之间的复用
2.继承
1°定义
class Student(派生类):public(继承方式) Person(基类)
{};
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;//年龄
};
//继承
//private和protected在当前类无差别
//在继承的子类有差别
//继承下来的private不可用 protected可以用
//谁的范围小就取谁
//public>protected>private
//实际当中都用public继承 几乎不使用protected/private
class Student :public Person
{
protected:
int _stuid;//学号
};
class Teacher :public Person
{
protected:
int _jobid;//工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
3.切片
-
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用->切片
-
而基类对象不能赋值给派生类对象 范围大的可以给范围小的
-
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指
向派生类对象时才是安全的
class Person
{
protected:
string _name; //姓名
string _sex; //性别
int _age; //年龄
};
class Student :public Person
{
public:
int _No;//学号
};
int main()
{
Person p;
Student s;
//子类和父类之间的赋值兼容规则
//1.子类对象可以赋值给父亲对象/指针/引用
p = s;
Person* ptr = &s;
Person& ret = s;
//切片
//自己的_No不给 其他继承的可以赋值
//指针和别名也是 只会有部分的指针和别名
//2.反过来行不行?
//s=p;绝对不行
//3.Student* sptr=(Student*)ptr;
//有可能可以 这个父类的指针指向子类对象v
return 0;
4.继承中的作用域
就近:如果两个变量是一样的不会报错 先找自己类里的变量 再找父类里的变量
如果就想访问父类的同名变量的话 加上Person::进行限定
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();
}
int main()
{
Test();
return 0;
}
//这两个num不会报错 因为两个num在不同的作用域
//那么访问num的时候到底是父类的num还是子类的nums
//子类的num 就近原则 先找自己类里面 再找其他类
//当父类和子类同时有同名成员时,子类的成员隐藏了父类的成员
//那假设就想访问父类怎么办
//指定 Person::_num
-
练习
-
A和B的fun构成什么关系
a.重载 b.重写 c.重定义 d.编译不通过/以上都不对
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);
//b.fun();//调父类调不动 不可以
//b.A::fun();//指定去调父类的 可以
};
很容易选到重载 应该是c 重定义 也就是隐藏
隐藏不是说调不动 而是要指定父类去调用
重载的要求:必须在同一作用域
5.派生类的默认成员函数
- 派生类的默认成员函数是如何生成的?
- 构造:必须调用基类的构造函数初始化基类的那一部分成员 如果基类没有构造函数 构造时必须显示调用(加上作用域)
- 拷贝构造:必须调用基类的拷贝构造
- 赋值:必须调用基类的operator=
- 析构:派生类的析构函数调用完后自动调用基类的析构函数 保证正确顺序
- 派生类对象初始化先调用基类构造再调派生类构造
- 派生类对象析构清理先调用派生类析构再调基类的析构
#include <iostream>
using namespace std;
class Person
{
public:
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 stuid)
:Person(name)//指定
,_stuid(stuid)
{
cout << "Student(const char* name, int stuid)" << endl;
}
Student(const Student& s)
:Person(s)//除了stuid 其余的继承父类 先调用父类的 再调用自己的
,_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);//调父类 完成切片 隐藏了父类 所以要指定作用域 调父类的时候全部需要指定
_stuid = s._stuid;//自己成员
cout << "Student& operator=(const Student& s)" << endl;
}
return *this;
}
~Student()
{
//还是需要指定
//Person::~Person();//子类的析构函数和父类的析构函数构成隐藏 因为他们的名字会被编译器统一处理成destructor(跟多态有关)
//析构时会发现有三个析构 多一个Person析构 应该去掉第一个Person析构
//不需要 析构函数会自动调用
//结束时会自动调用父类的析构函数 因为这样才能保证先析构子类后析构父类
//不需要显示的调 因为显示的调会多出一个析构 而且析构顺序不对
cout << "~Student()" << endl;
}
protected:
int _stuid;
};
int main()
{
//Student s1("jack",1);
//Student s2(s1);//不写也会调用父类的拷贝构造 写了后先调父类 再调子类
//Student s3 = s1;
//Student s4("rose", 2);//析构时会发现有三个析构 多一个Person析构 应该去掉第一个Person析构
return 0;
}
s1
s2
s3
s4
-
派生类实现默认成员函数时 先要调用基类相关函数把基类那一部分成员实现功能(需
要指定基类) 再实现自己类里面的成员功能
-
析构函数比较特殊 不需要去实现 因为调用基类的析构函数的话 基类会先析构 然后派
生类再析构 基类会再次析构**(当派生类析构后 基类会自动调用析构函数 保证析构的顺**
序)
-
除了析构以外 全部都需要显示调用父类 相当于切片那一段直接找父类的就行
然后子类自己的成员自己处理
-
如何设计一个不能被继承的类?
构造私有 析构私有
继承后不可见私有的 子类调不动父类的构造和析构 子类对象都生成不了 所以不能被
继承
6.继承和友元
父类中友元关系不能被继承下来
7.基类与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少
个子类,都只有一个static成员实例。
#include <iostream>
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; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Person p;
Student s;
p._count = 1;
s._count = 2;
Person::_count++;
cout << Person::_count << endl;
cout << Student::_count << endl;
//派生了多少子类 静态成员只有一个
return 0;
}
_count都是3
8.菱形继承
1°继承类型
- 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 菱形继承:菱形继承是多继承的一种特殊情况
-
菱形继承的问题:
**Assistant会有两份相同数据导致数据冗余 两份数据到底用哪份 产生歧义(二义性) **
需要指定是哪个 但还是有数据冗余的问题
1.数据冗余
2.二义性
-
如何解决菱形继承问题?
官方引入了virtual继承 虚继承 解决了数据冗余和二义性
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person//添加virtual
{
protected:
int _num; //学号
};
class Teacher : virtual public Person//添加virtual
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";//不加virtual会报错
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
- C++的缺陷有哪些?
多继承就是一个问题->菱形继承->虚继承->底层结构的对象模型非常复杂 且有一定
效率损失
- 什么是菱形继承?
多继承的一种特殊情况
- 菱形继承问题是什么?
数据冗余和二义性
- 如何解决菱形继承?
虚继承
- 那么解决原理是什么?
需要了解内存对象模型 也就是对象在内存中是怎么存的
2°内存对象模型
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;
cout << sizeof(d) << endl;//8+8+4=20 //加了virtual变为了24
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 6;
//a放到了公共位置
//多了两个像指针一样的东西 变为了24
//所以这两个指针是干啥的
//两个指针最后表示的是偏移量 多付出了8个字节
//存偏移量的表叫虚基表->虚基类
return 0;
}
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚
基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
通过偏移量地址(指针)->偏移量值->对象
9.总结
- 多继承是C++的缺陷之一,Java设计的时候避开了多继承
继承与组合
//继承
class A{};
class B:public A
{};
//组合
class C{};
class D
{
C c;
}
继承是一种白箱(看得见)复用 父类对子类基本是透明的 但是一定程度破坏了父类的封装
组合是一种黑箱(看不见)复用 C对D是不透明的 C保持着封装
对比认为组合是更好的
组合的类耦合度更低
继承的类是一种高耦合
- 面试:用组合还是继承?
更符合is a 就继承
更符合has a 就组合
都可以 优先使用组合
【C++】12.继承 完