看前须知:
本篇博客是作者听课时的笔记,不喜勿喷,若有疑问可以评论区一起讨论。
继承
定义:
继承机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有 类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类(子类)。
继承格式
继承基类成员访问格式
1.基类(父类)的private成员在派生类(子类)中无论是以什么样的继承方式都是不可见的,但可以通过基类的成员函数进行访问。
2.protected:派生类(子类)可以继承且访问基类(父类)的成员函数,但是其他类中不能被访问。
3.基类的私有成员在派生类中访问是不可见的,其他成员在派生类中的访问方式==MIn(成员在基类的访问限定符,继承方式),public>protected>private
4.在实际运用中一般使用都是public继承
应用(利用继承实现栈)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
using namespace std;
//用继承模拟实现栈
//#define CONTAINER std::vector
#define CONTAINER std::list
namespace lph {template<class T>class stack :public CONTAINER<T> {public:void push(const T& x) {//基类是类模板,需要指定类域否者编译报错CONTAINER<T>::push_back(x);}void pop() {CONTAINER<T>::pop_back();}T& top() {return CONTAINER<T>::back();}size_t size() {return CONTAINER<T>::size();}bool empyt() {return CONTAINER<T>::empty();}};
}
int main() {lph::stack<int> s1;//进行实例化时调用模板的实例化,但并不会实例化基类(父类)的成员函数,所有在调用基类(父类)的成员函数时需要指定类域。s1.push(1);s1.push(2);s1.push(3);s1.push(4);while (!s1.empyt()) {cout<<s1.top()<<" ";s1.pop();}return 0;
}
基类和派⽣类间的转换
public继承的派⽣类对象,可以赋值给基类(父类)的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。
寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分(父类不能赋值给子类)
继承中的作⽤域
隐藏规则
1.继承体系中基类(父类)和派生类(子类)都有独立的作用域
2.派生类(子类)和基类(父类)中有同名成员,派生类将屏蔽基类对基类对同名成员的直接访问(在派生类成员函数中,可以使用基类::基类成员 显示访问)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class person {
protected:int num = 999;//同名成员变量
};
class student : public person {
public:void print() {cout << num << endl;//访问到的是派生类的成元变量100cout<<person::num<<endl;//访问基类成员变量999}
protected:int num = 100;//同名成员变量
};
int main() {student s1;s1.print();return 0;
}
3.对于成员函数的隐藏规则,只要派生类中有与基类中同名函数就会进行隐藏。
using namespace std;
class person {
public:void fun() {cout << "fun()" << endl;}
};
class student : public person {
public:void fun(int i) {cout << "fun(int i)" << endl;}
};int main() {student s1;s1.fun(1);//带参数的直接进行访问派生类s1.person::fun();//不带参数需要指定类域return 0;
}
//只要子类有函数名与基类函数名像似就把基类进行隐藏
4.在实际继承体系中,最好不要定义同名成员或则成员函数。
派⽣类的默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这 ⼏个成员函数是如何⽣成的呢?
类和对象中默认成员函数规则。
1.对于内置类型是否初始化不确定。
2.对与自定义类型会调用他的默认构造。
3.派生类中的默认成员函数调用其父类的默认成员函数。
对应规则
1.默认构造
派⽣类的默认构造函数必须调⽤基类的默认构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
由上图可知在父类我们并没有显示实现默认构造,派生类无法调用父类默认构造,因此必须在派生类初始化列表显示写默认构造,但对于父类成员变量要调用父类的初始化列表进行初始化(person(name)相当于一个匿名对象)
2.拷贝构造
派⽣类(子类)的拷⻉构造函数必须调⽤基类(父类)的拷⻉构造完成基类的拷⻉初始化。
对于该函数,成员变量有一个内置类行变量和一个person(父类)变量,对于自定义变量(person)调用person的拷贝构造即可,但对于内置变量直接进行值拷贝,所以该子类并不需要直接写拷贝构造。
如果有资源开辟那么我们就需要进行自己写子类的拷贝钩爪
3.赋值重载
派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
与拷贝构造分析类似
4.析构
对于派生类调用基类的析构,如果有要释放的资源则需要自己写就行。
自己写的话显示调用父类析构(也可以不写因为子类进行析构后会自动调用父类析构,这样保证先清理子类资源在清理父类资源 ),需要注意的是子类析构和父类析构构成隐藏关系。
5.子类的初始化先调用父类在子
6.析构 先子后父
构造析构图解
实现不能被继承的类
方法一:
c++98规定:只要把构造函数私有化就不能被继承
原因:因为实例化派生类要调用基类的构造函数,如果没有就不能继承
方法二:
在父类后边加一个final说明这个类是最后一个类
class parent final{};
继承与友元
基类的友元关系不能被继承如果想被继承需要在两个类中都加友元
继承与静态成员
普通成员变量定义在基类和派生类则两则是两个不同的变量,静态成员变量如果继承下来则两个类是公用该同一个变量。
多继承及其菱形继承问题
继承类型
单继承:只继承一个父类
多继承:一个子类继承两个父类
菱形继承:多继承继承的两个父类又继承了同一个父类
菱形继承出现的问题:
当实例化菱形继承时存在二议性,和冗余性,冗余性体现在所继承继承继承的两个类代码重复,二义性表现在实例化时不清楚用的时那个类进行实例化的。
虚继承
虚继承可以解决以上问题,在多继承中继承的两个父类加上关键字virtual即可解决。
总结:
现实中尽量不要使用或则设计菱形继承,因为很复杂。
多继承中指针偏移问题?
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;return 0;
}
多态
概念:简单来说就是多种形态。
分类:编译时多态(静态多态)和运行时多态(动态多态)(重点)
编译时多态
类似于前面学的函数重载和函数模板
以下面例子来讲:
template<class T>
void fun(int,int);
void fun(double,double)
利用函数模板进行传参时,进行不同类型实例化时,函数在编译时就会生成不同的函数。
运行时多态(重点)
一句话概括:不同的对象去完成不同的行为 这样就表现出多态。
多态的构成条件
多态是一个继承关系下的类对象,去调用同一个函数,产生了不同的行为。
虚函数
虚函数:加在类中成员函数前面一个virtual构成虚函数
实现多态的两个必须重要条件
先看以下代码
class person {
public:virtual void Buyticket() {cout << "全价票" << endl;}
};
class student :public person {virtual void Buyticket() {cout << "半价票" << endl;}
};
void fun(person& p) {//必须为父类引用p.Buyticket();//如果不满足多态指向谁就调用谁
}
int main() {person p1;student s1;fun(p1);fun(s1);return 0;
}
1.必须是基类指针或则引用调用虚函数。
2.被调用的的函数必须是虚函数
说明:
要实现多态效果,第一必须是基类的指针或者引用如下:
void fun(person& p) {//必须为父类引用p.Buyticket();
}
int main() {person p1;student s1;fun(p1);fun(s1);return 0;
}
原因:学过继承应该知道只有时基类的指针或则引用才能指向自己或则时子类(对其进行切割)。
第二点就i是派生类必须对基类的虚函数完成重写覆盖,只有重写/覆盖基类和派生类才能有不同的函数,多态的不同形态才能体现出来。
虚函数重写规则:派生类虚函数与基类虚函数返回值相同,函数参数相同,函数名相同)。如下代码即是重写/覆盖。
class person {
public:virtual void Buyticket() {cout << "全价票" << endl;}
};
class student :public person {virtual void Buyticket() {cout << "半价票" << endl;}
};
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使⽤,不过在考试选择题中,经常会故意买这个坑,让你 判断是否构成多态(笔试常考)。
加深理解:
多个类构成的多态(只是为了能加深理解)
class animal {
public:virtual void talk() {cout << "0" << endl;};
};
class dog:public animal {
public:virtual void talk() {cout << "汪汪" << endl;}
};
class cat :public animal {
public:virtual void talk() {cout << "喵喵" << endl;}
};
void fun1(animal& a) {a.talk();
}
int main() {animal a;dog d;cat c;fun1(a);fun1(d);fun1(c);return 0;
}
关于多态的一道面试题目
class A
{
public:virtual void func(int val = 1) {cout << "A->" << val << endl; }virtual void test() {func(); //把对象b的指针传给A*//原因是对于继承只是一种形象化的说法,并不会真的把代码拷贝到b对象中,只是存在一种搜索规则,在b类中搜索不到后就去a类中进行搜索。所以这个地方还是A*}
};class B : public A
{
public:void func(int val = 0) {//虽然没加virtual 但与基类构成重写cout << "B->" << val << endl;}
};
int main(int argc, char* argv[])
{B* p = new B;//实例化一个B对象 pp->test();//相当于对象A指针 preturn 0;
}
//A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确---B
//不是D的原因是因为真正的重写是由基类的声明部分加派生类构造部分(重写的本质就是重写虚函数) 所以真正的写法因该是
virtual void func(int val = 1){cout << "B->" << val << endl};//B->1
虚函数重写的⼀些其他问题
协变(了解)
定义:基类虚函数返回基类对象的指针或者引用,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变
class person {
public:virtual person* Buyticket() {//返回值为personcout << "全价票" << endl;return nullptr;}
};
class student :public person {virtual student* Buyticket() {//返回值为studentcout << "半价票" << endl;return nullptr;}
};
void fun(person& p) {//必须为父类引用p.Buyticket();//如果不满足多态指向谁就调用谁
}
int main() {person p1;student s1;fun(p1);fun(s1);return 0;
}
//运行结果与多态一致 像这种情况称之为协变只需了解即可
析构函数的重写(面试考题:)
问题:为什么基类中的析构 函数建议设计为虚函数
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键之,都与基类析构函数构成重写,实际上编译器对析构函数的名称构成了重写,编译后的名称统一处理成destructor,因此符合重写条件,所以就构成重写。
c++设计初衷:
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
对于以上程序,基类指针既能指向自己也能指向派生类,如果指向派生类(不构成重写的情况下),并且派生类中由资源需要释放,那么就不能去释放派生类中的资源,照成内存泄漏,只有两则构成多泰才能更好的解决这里的问题。
override和final关键字
override,可以帮助⽤⼾检测是否重写。
先看一段程序
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive(){ cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}
细心的你可能不能发现以上函数并不构成重写(仔细观察Dirve Drive)函数名不同,不构成重写,这时候方便快速检查可以在派生类虚函数后边加一个override既能快速发现错误
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive()override{ cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}
如果我们不想让 派⽣类重写这个虚函数,那么可以⽤final去修饰
virtual void Drive() final {}//基类的虚函数加final
重载/重写/隐藏三者对比(常考)
虚函数和抽象类
定义:在虚函数的后⾯写上=0,则这个函数为纯虚函数,只要在基类声明即可,无意义。
包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。
纯虚函数某种程度上强制了 派⽣类重写虚函数,因为不重写实例化不出对象。
virtual void Drive() = 0;//基类中的虚函数
//基类成为抽象类 强制派生类必须重写
多态的原理
话题引入
虚函数表指针
class Base
{
public:virtual void Func1(){cout<<"Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
Base b;
cout << sizeof(b) << endl;
return 0;
}
//A.编译报错 B.运⾏报错 C.8 D.12----D
想必大家根据内容对齐的知识都会选择c,但是在该题中存在的virtual函数是一个指针(4个字节)所以一共是12bytes.
⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针(该指针指向一个指针数组),因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
多态是如何实现的
问题引入:
从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicket, ptr指向Student对象调⽤Student::BuyTicket的呢?
通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址(虚拟函数地址会进行重写)。所以通过这种方式传不同对象就能调用不同的虚函数,完成不同的行为。
虚函数表
注意点:
1.基类对象的虚函数表中存放基类中所有的虚函数地址。
问:为什么一个类中会存放一张虚函数表?
答:为了多个类能够共享一张虚函数表,一定角度节约了资源。
结论:同类型的对象虚表共用,不同对象虚表各自独立。
2.派生类有两部分构成,继承下来的基类和自己的成员变量,继承下来的基类中有虚函数表指针,自己就不在生成虚函数表指针(但这里的虚函数表指针进行的重写/覆盖 ,指针地址也发生了变化)。
3.派生类的虚函数表包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址。
4.虚函数的存放地址与普通函数的地址是一样的,编译好后是一段指令,都是存放在代码段的,只是虚函数的地址又存放在虚表中。
5.虚函数表存放在哪里并不确定。