C++ 三大特性 封装,继承,多态编程
封装函数
定义:封装就是将抽象获得的数据和行为相结合,造成一个有机的总体,也就是将数据与操做数据的源代码进行有机的结合,造成类,其中数据和函数都是类的成员,目的在于将对象的使用者和设计者分开,优化
以提升软件的可维护性和可修改性ui
特性:1. 结合性,便是将属性和方法结合 2. 信息隐蔽性,利用接口机制隐蔽内部实现细节,只留下接口给外界调用 3. 实现代码重用this
继承 spa
定义:继承就是新类从已有类那里获得已有的特性。 类的派生指的是从已有类产生新类的过程。原有的类成为基类或父类,产生的新类称为派生类或子类,设计
子类继承基类后,能够建立子类对象来调用基类函数,变量等指针
单一继承:继承一个父类,这种继承称为单一继承,通常状况尽可能使用单一继承,使用多重继承容易形成混乱易出问题调试
多重继承:继承多个父类,类与类之间要用逗号隔开,类名以前要有继承权限,假使两个或两个基类都有某变量或函数,在子类中调用时须要加类名限定符如c.a::i = 1;对象
菱形继承:多重继承掺杂隔代继承1-n-1模式,此时须要用到虚继承,例如 B,C虚拟继承于A,D再多重继承B,C,不然会出错
继承权限:继承方式规定了如何访问继承的基类的成员。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限
继承权限:子类继承基类除构造和析构函数之外的全部成员
继承能够扩展已存在的代码,目的也是为了代码重用
继承也分为接口继承和实现继承:
普通成员函数的接口老是会被继承: 子类继承一份接口和一份强制实现
普通虚函数被子类重写 : 子类继承一份接口和一份缺省实现
纯虚函数只能被子类继承接口 : 子类继承一份接口,没有继承实现
访问权限图以下:
为了便于理解,伪代码以下,注意这个例子编译是不过的,仅是为了能够更简洁的说明继承权限的做用:
class Animal //父类
{
public:
void eat(){
cout<<"animal eat"<<endl;
}
protected:
void sleep(){
cout<<"animal sleep"<<endl;
}
private:
void breathe(){
cout<<"animal breathe"<<endl;
}
};
class Fish:public Animal //子类
{
public:
void test() {
eat(); //此时eat()的访问权限为public,在类内部可以访问
sleep(); //此时sleep()的访问权限为protected,在类内部可以访问
breathe(); //此时breathe()的访问权限为no access,在类内部不可以访问
}
};
int main(void) {
Fish f;
f.eat(); //此时eat()的访问权限为public,在类外部可以访问
f.sleep(); //此时sleep()的访问权限为protected,在类外部不可以访问
f.breathe() //此时breathe()的访问权限为no access,在类外部不可以访问
}
多态
定义:能够简单归纳为“一个接口,多种方法”,即用的是同一个接口,可是效果各不相同,多态有两种形式的多态,一种是静态多态,一种是动态多态
动态多态: 是指在程序运行时才能肯定函数和实现的连接,此时才能肯定调用哪一个函数,父类指针或者引用可以指向子类对象,调用子类的函数,因此在编译时是没法肯定调用哪一个函数
使用时在父类中写一个虚函数,在子类中分别重写,用这个父类指针调用这个虚函数,它实际上会调用各自子类重写的虚函数。
运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,咱们总但愿可以抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),
而后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。
运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张惟一的虚函数表,虚函数表中存放的是该类虚函数地址。
运行期间经过虚函数表指针与虚函数表去肯定该类虚函数的真正实现。
优势: OO设计重要的特性,对客观世界直觉认识; 可以处理同一个继承体系下的异质类集合
vector<Animal*>anims;
Animal * anim1 = new Dog;
Animal * anim2 = new Cat;
//处理异质类集合
anims.push_back(anim1);
anims.push_back(anim2);
缺点:运行期间进行虚函数绑定,提升了程序运行开销;庞大的类继承层次,对接口的修改易影响类继承层次;因为虚函数在运行期才绑定,因此编译器没法对虚函数进行优化
虚函数
定义:用virtual关键字修饰的函数,本质:由虚指针和虚表控制,虚指针指向虚表中的某个函数入口地址,就实现了多态,做用:实现了多态,虚函数能够被子类重写,虚函数地址存储在虚表中
虚表:虚表中主要是一个类的虚函数的地址表,这张表解决了继承,覆盖的问题,保证其真实反应实际的函数,当咱们用父类指针来指向一个子类对象的时候,虚表指明了实际所应调用的函数
基类有一个虚表,能够被子类继承,(当类中有虚函数时该类才会有虚表,该类的对象才有虚指针,子类继承时也会继承基类的虚表),子类若是重写了基类的某虚函数,那么子类继承于基类的虚表中该虚函数的地址也会相应改变,指向子类
自身的该虚函数实现,若是子类有本身的虚函数,那么子类的虚表中就会增长该项,编译器为每一个类对象定义了一个虚指针,来定位虚表,因此虽然是父类指针指向子类对象,但由于此时子类
重写了该虚函数,该虚函数地址在子类虚表中的地址已经被改变了,因此它实际调用的是子类的重写后的函数,正是因为每一个对象调用的虚函数都是经过虚表指针来索引的,也就决定了虚表指针的
正确初始化是很是重要的,便是说,在虚表指针没有正确初始化以前,咱们是不能调用虚函数的,由于生成一个对象是构造函数的工做,因此设置虚指针也是构造函数的工做,编译器在构造函数
的开头部分秘密插入能初始化虚指针的代码, 在构造函数中进行虚表的建立和虚指针的初始化
一但虚指针被初始化为指向相应的虚表,对象就“知道”它本身是什么类型,但只有当虚函数被调用时这种自我认知才有用
类中若没有虚函数,类对象的大小正好是数据成员的大小,包含有一个或者多个虚函数的类对象。编译器会向里面插入一个虚指针,指向虚表,这些都是编译器为咱们作的,咱们彻底没必要关心
这些,全部有虚函数的类对象的大小是数据成员的大小加一个虚指针的大小;对于虚继承,若子类也有本身的虚函数,则它自己须要有一个虚指针,指向本身的虚表,另外子类继承基类时,
首先要经过加入一个虚指针来指向基类,所以可能会有两个或多个虚指针(多重继承会多个),其余状况通常是一个虚指针,一张虚表
每个带有virtual函数的类都有一个相应的虚表,当对象调用某一virtual函数时,实际被调用的函数取决于该对象的虚指针所指向的那个虚表-编译器在其中寻找适当的函数指针。
效率漏洞:咱们必须明白,编译器正在插入隐藏代码到咱们的构造函数中,这些隐藏代码不只必须初始化虚指针,并且还必须检查this的值(以避免operator new返回零)和调用基类构造函数。放在一块儿,
这些代码能够影响咱们认为是一个小内联函数的调用,特别是,构造函数的规模会抵消函数调用代价的减小,若是作大量的内联函数调用,代码长度就会增加,而在速度上没有任何好处,
固然,也许不会当即把全部这些小构造函数都变成非内联,由于它们更容易写为内联构造函数,可是,当咱们正在调整咱们的代码时,请务必去掉这些内联构造函数
虚函数使用:将函数声明为虚函数会下降效率,通常函数在编译期其相对地址是肯定的,编译器能够直接生成imp/invoke指令,若是是虚函数,那么函数的地址是动态的,譬如取到的地址在eax寄存
器里,则在call eax以后的那些已经被预取到流水线的全部指令都将失效, 流水线越长,那么一次分支预测失败的代价越大,建议若不打算让某类成为基类,那么类中最好不要出现虚函数,
纯虚函数:含有至少一个纯虚函数的类叫抽象类,由于抽象类含有纯虚函数,因此其虚表是不健全的,在虚表不健全的状况下是不能实例化对象的,子类继承抽象基类后必须重写基类的全部纯虚函数
不然子类仍为纯虚函数子类将抽象基类的纯虚函数所有重写后会将虚表完善,此时子类才能实例化对象,纯虚函数只声明不定义,形如 virtual void print() = 0
静态多态:是在编译期就把函数连接起来,此时便可肯定调用哪一个函数或模板,静态多态是由模板和重载实现的,在宏多态中,是经过定义变量,编译时直接把变量替换,实现宏多态
优势: 带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的武器; 在编译期完成多态,提升运行期效率; 具备很强的适配性和松耦合性,(耦合性指的是两个功能模块之间的依赖关系)
缺点: 程序可读性下降,代码调试带来困难;没法实现模板的分离编译,当工程很大时,编译时间不可小觑 ;没法处理异质对象集合
调用基类指针建立子类对象,那么基类应该有虚析构函数,由于若是基类没有虚析构函数,那么在删除这个子类对象的时候会调用错误的析构函数而致使删除失败产生不明确行为,
int main() {
Base *p = new Derive(); //调用基类指针建立子类对象,那么基类应有虚析构函数,否则当删除的时候会调用错误的析构函数而致使删除失败产生不明确行为,
delete p; //删除子类对象时,若是基类有虚析构函数,那么delete时会先调用子类的析构函数,而后再调用基类的析构函数,成功删除
return 0; //若是基类没有虚析构函数,那么就只会调用父类的析构函数,只删除了对象内的父类部分,形成一个局部销毁,可能致使资源泄露
} //注:只有当此类但愿成为 基类时才会打算声明一个虚析构函数,不然没必要要给此类声明一个虚函数