上传文档的网站,广州建设职业培训学校,做自己的网站流量怎么,网站设计在营销中的作用看前须知#xff1a;
本篇博客是作者听课时的笔记#xff0c;不喜勿喷#xff0c;若有疑问可以评论区一起讨论。
继承
定义#xff1a;
继承机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段#xff0c;它允许我们在保持原有 类特性的基础上进⾏扩展#xff0c;增…看前须知
本篇博客是作者听课时的笔记不喜勿喷若有疑问可以评论区一起讨论。
继承
定义
继承机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段它允许我们在保持原有 类特性的基础上进⾏扩展增加⽅法(成员函数)和属性(成员变量)这样产⽣新的类称派⽣类子类。
继承格式 继承基类成员访问格式 1.基类父类的private成员在派生类子类中无论是以什么样的继承方式都是不可见的但可以通过基类的成员函数进行访问。
2.protected:派生类子类可以继承且访问基类父类的成员函数但是其他类中不能被访问。
3.基类的私有成员在派生类中访问是不可见的其他成员在派生类中的访问方式MIn(成员在基类的访问限定符继承方式)publicprotectedprivate
4.在实际运用中一般使用都是public继承
应用利用继承实现栈
#define _CRT_SECURE_NO_WARNINGS 1
#includeiostream
#includevector
#includelist
using namespace std;
//用继承模拟实现栈
//#define CONTAINER std::vector
#define CONTAINER std::list
namespace lph {templateclass Tclass stack :public CONTAINERT {public:void push(const T x) {//基类是类模板需要指定类域否者编译报错CONTAINERT::push_back(x);}void pop() {CONTAINERT::pop_back();}T top() {return CONTAINERT::back();}size_t size() {return CONTAINERT::size();}bool empyt() {return CONTAINERT::empty();}};
}
int main() {lph::stackint s1;//进行实例化时调用模板的实例化但并不会实例化基类父类的成员函数所有在调用基类父类的成员函数时需要指定类域。s1.push(1);s1.push(2);s1.push(3);s1.push(4);while (!s1.empyt()) {couts1.top() ;s1.pop();}return 0;
}
基类和派⽣类间的转换
public继承的派⽣类对象可以赋值给基类父类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。
寓意把派⽣类中基类那部分切出来基类指针或引⽤指向的是派⽣类中切出来的基类那部分父类不能赋值给子类 继承中的作⽤域
隐藏规则
1.继承体系中基类父类和派生类子类都有独立的作用域
2.派生类子类和基类父类中有同名成员派生类将屏蔽基类对基类对同名成员的直接访问在派生类成员函数中可以使用基类基类成员 显示访问
#define _CRT_SECURE_NO_WARNINGS 1
#includeiostream
using namespace std;
class person {
protected:int num 999;//同名成员变量
};
class student : public person {
public:void print() {cout num endl;//访问到的是派生类的成元变量100coutperson::numendl;//访问基类成员变量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.析构 先子后父
构造析构图解 实现不能被继承的类
方法一
c98规定只要把构造函数私有化就不能被继承
原因因为实例化派生类要调用基类的构造函数如果没有就不能继承
方法二
在父类后边加一个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;
} 多态
概念简单来说就是多种形态。
分类编译时多态静态多态和运行时多态动态多态重点
编译时多态
类似于前面学的函数重载和函数模板
以下面例子来讲
templateclass 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(){coutFunc1() 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.虚函数表存放在哪里并不确定。