# Lecture11 - 继承、虚函数
能声明成 const 就声明成 const
# 单继承
protected:
- 如果没有继承的话,protected 和 private 是相同的
- 派生类可以访问基类中 protected 的属性的成员。(student 可以访问自己的 id)
- 派生类不可以访问基类中的对象的 protected 的属性。(Undergraduate_Student 不能访问自己引用的 student 的 protected 属性)
- 派生类含有基类的所有成员变量
class 和 struct 的默认权限
使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
class 继承默认是 private 继承,而 struct 继承默认是 public 继承
class Student { | |
int id;//id 在 Undergraduated_Student 中仍然是私有的 | |
public: | |
char nickname[16]; | |
void set_ID (int x) {id = x;} | |
void SetNickName (char *s) {strcpy (nickname,s);} | |
void showInfo () {cout << nickname << ":" << id << endl ;} | |
void showInfo(int x){cout << x << endl;} | |
}; | |
class Undergraduated_Student: public Student { | |
int dept_no;// 学院编号 | |
public: | |
void setDeptNo(int x){dept_no = x;} | |
void showInfo(){cout << dept_no << ":" << nickname << endl;} | |
void set_ID (int x) {……} | |
void showInfo(){ | |
cout << dept_no << ":" << nickname << endl; | |
} | |
private: | |
Student::nickname;// 这样在才能修改可见性 | |
void SetNickName();// 新定义了一个 private 方法,父类对应方法被隐藏 | |
}; | |
Undergraduated_Student us; | |
us.showInfo(10);// 可以吗?不可以,因为是新的名空间,重定义后面的名空间访问不到 |
基类的方法,将函数指针传给派生类。
虚函数,virtual,延迟到运行的时候确定调用的函数类型,和 java 类似。
class Undergraduated_Studnet: public Student
在函数定义的时候需要写出 Student
# 继承方式
# 继承声明方式
区分声明和定义,前向声明只是告诉别人有这个东西,定义要明确需要占用多少内存空间
// 错误声明 | |
class Undergraduated_Student : public Student;// 声明的时候是不用声明继承的 | |
// 正确声明 | |
class Undergraduated_Student; |
# 基类和继承类的方法关系
# 基类与派生类中的 showInfo 访问
隐藏 (不是重写 / 覆盖),隐藏基类的所有的 showInfo () 函数,重定义将名空间进行了覆盖
class Student { | |
int id;//id 在 Undergraduated_Student 中仍然是私有的 | |
public: | |
char nickname[16]; | |
void showInfo () {cout << nickname << ":" << id << endl ;} | |
void showInfo(int x){cout << x << endl;} | |
}; | |
class Undergraduated_Student: public Student { | |
int dept_no;// 学院编号 | |
public: | |
void showInfo(){cout << dept_no << ":" << nickname << endl;} | |
}; | |
int main(){ | |
Undergraduated_Student *student = new Undergraduated_Student; | |
student->showInfo(1);// 不能够运行,在 undergraduated_student 里面设置之后,student 含参和无参的函数都没有了。 | |
} |
区分重载、隐藏、重写(覆盖)
https://blog.csdn.net/zx3517288/article/details/48976097
如何访问父类中被隐藏的函数?
父类中的所有的函数都不可见:但是我们可以通过指定名空间来完成访问: using Student::showInfo
, 所有的版本都可以见,这时候是重写。
# 继承权限修改
- class ABC : private XYZ //private derivation
- class ABC : public XYZ //public derivation
- class ABC : protected XYZ //protected derivation
- class ABC: XYZ //private derivation by default
public 继承,protected 继承,private 继承
# 不可以被继承的部分
构造函数和析构函数是不可以被继承的:是对类进行初始化的,无法继承
运算符重载函数也是不可以被继承的
# 访问权限的修改方法
private: | |
Student::nickname;//char nickname [16]; 语法上没问题,没有将原来的 nickname 变为私有的 | |
void SetNickName();// 新定义了一个 private 方法,父类对应方法被隐藏 |
如果重新声明 char nickname[16];
, 并没有修改原来父类中 nickname 访问等级,语法无误,但是语义不对;
# 友元和 protected
void clobber(Sneaky &s)
通过传入派生类访问基类的 protected 成员。 void clovver(Base &b)
传入基类不能够访问。
protected 只能由派生类来访问。否则只要声明某基类的派生类,就能够访问该基类的 protected 成员,绕过保护机制,随意访问保护成员。
友元不具有传递性。不能通过成为派生类的友元而成为基类的友元。
# 继承的初始化
4
没有在成员初始化表中指出,则调用默认构造函数
B(const B& b){ | |
} | |
B b1(10, 100); | |
B b2(b1); | |
// 有 B 的 copy 构造函数,b2 的值 copy 错误,编译器无法明确是只 copy x 还是都 copy | |
b1 10 100; | |
b2 0 100; | |
A() B(&b);// 构造函数调用顺序 | |
// 如果没有 B 的 copy 构造函数,b2 的值 copy 正确 | |
b1 10 100; | |
b2 10 100; | |
// 可以 copy x 的 copy 函数 | |
B(const B&b):A(b); |
语法糖
using A::A;
继承 A 的构造函数
# 虚函数
类型相容:类型相同、派生类
赋值相容(不丢失信息):类型相同、派生类【发生对象切片,属于派生类的属性不复存在】
A a; B b; a = b; | |
B b; | |
A a = b;// 相当于调用了 A 的拷贝构造函数,调用哪个,取决于声明的变量是什么类型的 |
func1(A &a){ | |
a.f(); | |
} | |
func2(A *pa){ | |
pa->f(); | |
} | |
// 均调用 A 的 f |
前期绑定
在编译的时候就可以确定【func1\func2 在编译时就已经确定调用 A 的 f】
cpp 默认前期绑定
动态绑定
java 虚拟机动态地决定调用哪个版本
cpp 是一门注重效率的语言
cpp 需要通过显式指出使用后期绑定
在方法前加 virtual 关键字动态绑定
# 限制
- 成员函数才可以时虚函数,全局函数不能是虚函数
- 静态成员函数通过类来调用,编译期间确定,相当于全局函数
- 内联成员函数编译时要展开,在编译时需要确定
- 构造函数,虚函数表需要构造函数构造,虚函数只有在声明基类指向子类的时候才会调用,构造函数不存在声明类型与构造函数的类型不一致的情况。没有实际意义。
- 析构函数(子类会声明新的变量)
p = &a; | |
p -> h();// 调用 a 的函数 | |
p -> f();// 调用 a 的函数 | |
p = &b; | |
p -> h();// 调用 a 的函数 | |
p -> f();// 调用 b 的函数 |
如何确定调用哪个?传入一个虚函数表【vtable】
如何确定虚函数表的偏移?传入一个虚函数指针,vpointer,确认排序,查找函数。
如何实现查表的操作? (**((char *)p-4))(p)
(char*) p -4,偏移 4 个字节,第一次解引用解出了虚函数表的指针,第二次解引用解出了 f 的函数指针。
B b;//A 的构造函数 A 的 f B 的构造函数,B 的构造函数还未调用,vtable 还没有改变 | |
A *p = &b; | |
p -> f();// B::f | |
p -> g();// A::g g 是非虚函数 | |
p -> h();// A::h, B::f, A::g |
构造函数不应该调用莫名其妙的东西(比如虚函数)
直到构造函数返回之后,对象方可正常使用(虚函数表才构建好)
B b; | |
A *p = &b; | |
p -> f();// B::f() B::g() |
虚函数调用非虚函数,类型变为一致。
非虚函数调用虚函数,非虚函数就是非虚函数,虚函数就是虚函数。【 非虚接口, 可以用不同的虚函数来实现这个非虚函数,非虚函数调用虚函数来获得虚函数的特性】【实现算法骨架的复用】
# final, override
override 告诉编译器是一个虚函数的重定义。
final 不能够再 override。
不能够写成 void f1(int) override
, 这样更改了 f1 的类型
void f2(int)
名隐藏,不能够再调用 d.f2()
纯虚函数用于抽象类,抽象类用于框架中
为什么不能直接设置 display 的空函数,而是设置纯虚函数?
因为抽象类不能创建对象,而且会出现对象切片,出现在栈上(降低效率);抽象类不能创建对象,只能通过指针和引用的方式传递参数,保证了一定会出现多态的特性。
通过构建非虚函数 CreateButton()
,可以实现更好的代码复用
抽象工厂模式
是否需要新的析构函数,看是否声明了新的成员变量。
需要释放新的变量,与基类不同,需要新的析构函数,需要是虚函数。
调用完 D 的析构函数后,就会调用 B 的析构函数。【倒着的次序调用】
A *p_a; | |
B b; | |
p_a = &b; | |
p_a -> f(); | |
// 打印 0 | |
A *p_a1; | |
C c; | |
p_a1 = &c; | |
p_a1 ->f(); | |
// 打印 0 | |
// 如果 class C:public B 仍然打印 0 |
编译器不知道指针指向谁,默认参数值为声明的类型的默认值。
从效率的角度出发,没有动态绑定参数值,参数值相当于一个全局或者静态的变量,不被改变。
只有虚函数表示动态的,默认参数是静态的。
# 公有继承 is_a
(嘿嘿公开继承应该不考
前置条件更弱,后置条件更强。
比如 base.f (x),参数要求正整数,返回 int;derive.f (x),参数要求整数,返回 int 的子集。以上,derive.f (x) 就可以替代 base.f (x)
//v1 | |
class Rectangle { | |
public: | |
void setHeight(int); | |
void setWidth(int); | |
int height() const;// 不修改的声明为 const 是好习惯 | |
int width() const; | |
}; | |
assert(s.width() == s.height()); | |
class Square: public Rectangle { | |
public: | |
void setLength (int); | |
private:// 设置为 private 可以避免父类的方法被单独调用 | |
void setHeight(int); | |
void setWidth(int); | |
}; | |
assert(s.width() == s.height());// 前后校验不变式 | |
Square s(1,1); | |
Rectangle *p = &s;// 即使 square 声明成 private 方法,改成 rectangle 指针之后,还是可以调用方法 | |
p->setHeight(10); | |
//v2 添加虚函数声明 | |
class Rectangle { | |
public: | |
virtual void setHeight(int); | |
virtual void setWidth(int); | |
int height() const; | |
int width() const; | |
}; | |
assert(s.width() == s.height()); | |
class Square: public Rectangle { | |
public: | |
void setLength (int); | |
public:// 注意是 public 的 | |
void setHeight(int); | |
void setWidth(int); | |
}; | |
// 问题:如下的操作如果传入正方形 | |
void Widen(Rectangle& r, int w) | |
{ | |
int oldHeight = r.height(); | |
r.setWidth(r.width() + w); | |
assert(r.height() == oldHeight);// 原来的高和现在的高不一致,长宽一起增加了 | |
} | |
//v3 | |
class Rectangle { | |
public: | |
virtual void setHeight(int); | |
virtual void setWidth(int); | |
int height() const; | |
int width() const;}; | |
assert(s.width() == s.height()); | |
class Square: public Rectangle { | |
public: | |
void setLength (int); | |
private:// 修改为 private | |
// 完全无效,编译时函数即被确定 | |
void setHeight(int); | |
void setWidth(int ); | |
}; | |
assert(s.width() == s.height()); | |
void Widen(Rectangle& r, int w) | |
{ | |
int oldHeight = r.height(); | |
r.setWidth(r.width() + w); | |
// 编译的时候检查 rectangle 是 public 的,可以通过编译。 | |
// 之后调用的时候发现是虚函数,然后从虚函数表能找到 private 的函数,并且调用。 | |
assert(r.height() == oldHeight); | |
} |
长方形的性质和正方形的性质不兼容
程序中的基类和子类,与生活中的一般与特殊并不一致
class B { | |
public: | |
void mf(); | |
}; | |
class D: public B { | |
public: | |
void mf(); | |
}; | |
D x; | |
B* pB = &x; | |
pB->mf();//B:mf | |
D* pD = &x; | |
pD->mf();//D:mf |
同一个对象更换指针,就出现了不同的行为。实际上是更换了名空间。
修改:不要定义继承而来的同名函数。
# 私有继承 has_a
实际上是 Has-A 关系(is a,是这个类的一种,继承,派生类是一个基类对象;has a, 含有这个类,包含,在派生类中拥有一个基类对象)
Student 私有继承 HumanBeing,则不能够调用 b.eat。
函数参数无法从派生类隐式转换为基类:如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。不能在派生类外部隐式转化。
一般私有继承是很罕见的,仅是可以使用基类的 protected 成员。
# 虚函数的使用情景
纯虚函数 —— 每个派生类依赖小,自己实现自己的。
一般虚函数 —— 覆盖功能多,派生类只需要修改部分。依赖性强。
非虚函数 —— 绝对不要重定义。保证所有的行为一致。
# 多继承
# 名冲突
出现名冲突的问题,Bed 和 Sofa 同时都有 SetWeight ()
如何解决名冲突的问题:
- 都是自己设计 —— 基类分解 Base-Class Decomposition,形成格的结构
- 不是自己设计的类 —— 用名空间的调用来消除名冲突
# 虚基类
virtual public 和 public virtual 等价
虚基类什么时候构造?D 只有一个 A,不能有两个 A。D 先构造 A,BC 不再构造 A。所有对象中只有一个 A 的对象。解决了在菱形设计中名冲突的问题。
# 内存存储
多继承有多个虚函数表指针 B1、B2、B3。
B1、B2 中都有 virtual 的 f,那么 D 重写,父类中的虚函数 f 全部都被重写。
# 期末题型
读程序理解(编译是否正确,如果错误,在哪里出了错) 要么得分,要么没分
手写代码
# 1
#include<iostream> | |
using namespace std; | |
class B{ | |
int x = 0; | |
public: | |
void setX(int px){ | |
x = px; | |
} | |
void getX(){ | |
cout <<x; | |
} | |
}; | |
class D: public B{ | |
public: | |
using B::x; | |
void setX(int px){ | |
x = 2 * px; | |
} | |
}; | |
int main(){ | |
B * pb = new D; | |
pb->setX(15); | |
pb->getX(); | |
return 0; | |
} |
只能修改原来就是可见(public)的
x 原本就没有权限
# 2
#include<iostream> | |
using namespace std; | |
class B{ | |
int x = 0; | |
public: | |
void setX(int px){ | |
x = px; | |
} | |
void getX(){ | |
cout <<x; | |
} | |
}; | |
class D: public B{ | |
public: | |
int x = 0; | |
void setX(){ | |
x = 5; | |
} | |
void setX(int px){ | |
B::setX(2*px); | |
} | |
}; | |
int main(){ | |
D * pb = new D; | |
pb->setX(); | |
pb->getX(); | |
return 0; | |
} | |
// 打印出来为 0,调用的是 B 的 getY |
# 3
#include<iostream> | |
using namespace std; | |
class B{ | |
int x = 0; | |
public: | |
void setX(int px){ | |
x = px; | |
} | |
void getX(){ | |
cout <<x; | |
} | |
}; | |
class D: public B{ | |
public: | |
int x = 0; | |
void setX(){ | |
x = 5; | |
} | |
void setX(int px){ | |
B::setX(2*px); | |
} | |
}; | |
int main(){ | |
D * pb = new D; | |
pb->setX(5);// 这里多了参数 | |
pb->getX(); | |
return 0; | |
} | |
// 打印出来是 10 |
# 4
#include<iostream> | |
using namespace std; | |
class B{ | |
int x = 0; | |
public: | |
void setX(int px){ | |
x = px; | |
} | |
void getX(){ | |
cout <<x; | |
} | |
}; | |
class D: public B{ | |
public: | |
int x = 0; | |
void setX(){ | |
x = 5; | |
} | |
void setX(int px){ | |
B::setX(2*px); | |
} | |
}; | |
int main(){ | |
B * pb = new D;// 这里声明为 B | |
pb->setX();//B 中没有无参数的 setX 函数 | |
pb->getX(); | |
return 0; | |
} |