走到这一步,咱们能够进一步讨论有关OOP在编程上的具体实现。
上一次,咱们提出来类的概念。其实类对应了OOP中的抽象这一律念。咱们将事物的共同点提取出来,抽象成了类。
这一次,为了提升代码的重用性,C++提出了继承语法。很好理解,咱们将各类种类的羊抽象出来,写出了class 羊。将各类牛特色提取出来,抽象成了 class 牛。若是咱们还有抽象出来 马 这一动物。咱们发现了牛,羊,马自己也都有共同点,就是它们动物,通常动物会吃喝跑,它们也都会。因此咱们抽象出动物这一特性,让牛羊马来继承动物的特性。这样能够有效提升咱们代码的效率。ios
经过以上的例子,咱们称动物类为父类(又称基类),牛羊马类为继承动物类的子类(又称派生类)程序员
//父类 class Base { }; //子类继承父类 class Son : public Base { };
继承的格式:
class 派生类名 : 继承方式 基类名
{
//派生类新增的成员变量或者成员函数
}编程
固然,这不意味这子类能够啃老。子类还须要本身实现一些事情:数组
有关访问权限(继承方式)安全
在这里有一件事须要重点关注:ide
派生类不能直接访问基类的private函数和变量,可是能够访问protected函数和变量
即,对于外部世界来讲,protected和private是同样的;对于派生类来讲,protected和public是同样的
派生类不能直接访问基类的私有成员,必须经过基类的方法来进行访问(get和set函数)
这使得咱们想到构造函数,不过很遗憾派生类的构造函数不能直接设置继承成员,而必须使用基类的公有方法来访问私有基类成员。即,派生类构造函数必须使用基类构造函数函数
Son::Son(int a,int b,int c,int d):Base(_b,_c,_d) { this->a = a; }
上述代码如何理解呢?
子类Son的构造函数其实只赋值了a这一成员变量。后面Base(_b,_c,_d)
咱们叫成员初始化列表。举一个例子,若是咱们给子类Son实例化 Son son(1,2,3,4);
时Son的构造函数把实参 2, 3, 4赋值给形参_a,_b,_c而后将这些参数做为实参赋值给父类Base构造函数,后者将嵌套一个Base对象,并将数据存入这个Base对象,而后进入Son的构造函数,完成对Son对象的建立,并将参数a赋值给this->a。
固然若是使用基类的拷贝构造函数也是能够的:性能
Son::Son(int a,int b,int c,int d):Base(base) { this->a = a; }
这里使用的是拷贝构造函数,这种方式咱们称是隐式的,而上述方法是显式的
若是说咱们后面什么都不写,编译器默认调用默认构造函数,即:this
Son::Son(int a,int b,int c,int d) { this->a = a; }
就等于spa
Son::Son(int a,int b,int c,int d):Base() { this->a = a; }
咱们总结一下刚刚说过的要点
咱们在这里并无讨论析构函数,可是须要强调的是:析构函数的顺序与构造函数是相反的,也就是先调用子类的析构,再调用父类的析构。
在使用子类的时候请记住要包含头文件。
[注]:能够和函数的初始化列表作一个联系
这个用法其实须要注意:
基类指针能够在没有进行显示类型转的状况下指向派生类对象;同时基类的引用能够在不进行显示类型转的状况下引用派生类对象。
这样作听起来很美好,不过要注意的是:
基类的指针或者引用只用于调用基类的方法。
这一点是相当重要的,即
Son son1(1,2,3,4); Base & sn = son1; Base * psn = &son1; //这样是容许的。可是使用sn 或者 *psn调用派生类(Son)的方法是不容许的!
其实,我到目前为止一直在避免说起内存的问题,可是时至今日也应该慢慢开始C++的内存管理问题了。没错,这里就是涉及一个内存的问题,请慢慢看下去:
首先,毋庸置疑的是子类的存储空间确定比父类的存储空间大。(父类有的子类都有,子类有的父类却不必定有)
因此,一个指向父类的指针 的寻址范围是否是比起子类的存储空间要小。这时,你用这个指向父类的指针去寻子类方法的地址,颇有可能会超出这个指向父类的指针的寻址能力,这样是会有很大的安全隐患,编译器是不容许的。
固然引用也是这个道理。(咱们等等还会继续说这个问题,目前先这样。)
可是,反过来——指向派生类的指针或者引用能够调用父类的方法吗?
答案是能够的,咱们把这种手法称之为“多态”。
C++中的继承并不像Java中只能单继承。C++的子类是能够继承多个父类。
可是,在多继承中很容易引起二义性。这时请使用做用域运算符进行解决。
实际上,多继承容易出现的问题并不只仅是命名问题,还有一个就是菱形继承。
(这里就不给UML图了,本人仍是懒)
这时,咱们又要引入C++的一个解决办法:虚基类
首先,具体怎么作?在继承方式前加 vitual 关键字
class Animal { public: int Age; }; class Sheep : virtual public Animal { };//虚基类 class Tuo : virtual public Animal { }; class SheepTuo: public Sheep,public Tuo { };
虚继承能够解决多种继承前面提到的两个问题:
虚继承底层实现原理与编译器相关,通常经过虚基类指针和虚基类表实现,每一个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(须要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并非不在子类里面了);当虚继承的子类被当作父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;经过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份一样的拷贝,节省了存储空间。
后面咱们会和虚函数(多态)进行比较。
同时这里会有一个使用Visual Studio命令提示功能来查看内存分布的技巧,就不展开说了。
其实,刚刚的描述已经解释了什么是多态了。用指向子类对象的指针或者引用去调用父类的函数。为何要这么作?咱们从常识来理解一下。好比:猫和鱼均可以继承动物这个类。若是动物这个类里面有 move() 移动这个方法。可是,咱们都知道猫和鱼的移动方式是不一样的。因此咱们要利用多态,来使得咱们的程序更符合现实。
函数重载,从常识来考虑。好比:咱们经过一个函数来计算得某个结果。可是,给这个函数一个参数 函数能够计算,给函数两个参数 函数也能够计算(计算的方法可能不同),或者给函数三个参数仍然能够计算(可能计算出来的精确度进一步提高了)。这样的话,咱们的函数名字同样,可是参数却不同。这样的就是函数重载。固然没有必要计算的意义也同样。简单的来讲,函数重载就名字同样,返回值类型同样,就是参数不同了。
因此,咱们提炼出几点:
[注]:当函数重载遇到默认参数时,要避免二义性。
void func(int a,int b = 10) { } void func(int a) { } void test() { func(10);//二义性,编译器不知道使用哪一个func()了 }
简单的说一下默认参数,其实很简单。就是给函数参数设置一个默认值,在参数列表直接等于就好了,按照以上例子你能够不给b值,默认是10;可是默认参数必须是最后面。不能插入没有设定默认值的参数void test(int a = 10 ,int b);
这样是不行的。
在面对func()时,编译器会可能默认把名字改为_func;当碰到func(int a)时,可能会默认改为_func_int;当碰到func(int a,char b)编译器可能会默认该成_func_int_char。这个“可能”意思时指,如何修饰函数名,编译器并无一个统一的标准,因此不一样编译器会产生不一样的内部名。
C++同时也容许给算符赋予新的意义
返回值 opertaor算符 (参数列表)
可是C++中并非全部算符均可以重载的:
如下是能够重载的算符:
如下是不能够重载的算符:
虽然在规则上是能够重载 && 和 ||,可是在实际应用中最好很差重载这两个运算符。其缘由是内置版本的&& ||首先计算左边的表达式,若是能够肯定结果,就无需计算右边了,咱们已经习惯这种特性了,一旦重载便会失去这种特性。
因为C++语言没有自动内存回收机制,程序员每次new出来的内存都要手动delete。程序员忘记delete,流程太复杂,最终致使没有delete,异常致使程序过早退出,没有执行delete的状况并不罕见。
因此,开发者能够经过算符重载,从而达到智能管理内存的效果。
1.对于编译器来讲,智能指针其实是一个栈对象,并不是指针类型,在栈对象生命期即将结束时,智能指针经过析构函数释放有它管理的堆内存。全部智能指针都重载了“operator->”操做符,直接返回对象的引用,用以操做对象。访问智能指针原来的方法则使用“.”操做符。
2.所谓智能指针,是根据不一样的场景来定制智能指针。如下给出一个最简单的应用:
class Person { public: Person(int age) { this->Age = age; } ~Person() { } void showAge() { cout<<"年龄为:"<<this->Age<<endl; } private: int Age; }; //智能指针,用来托管自定义类型的对象,让对象自动释放。 class smartPointer { public: smartPointer(Person * person) { this->person = person; } //重载->让智能指针像Person *p同样去使用 Person * operator->() { return this->person; } //重载* Person & operator*() { return * this->person; } ~smartPointer() { cout<<"智能指针析构了!"<<endl; if(this->person != NULL) { delete this->person; this->oerson = NULL; } } private: Person * person; } void test() { Person p(10);//自动析构 //至关于: //Person * p = new Person(10); //delete p; smartPointer sp(new Person(10));//开辟到栈上,自动析构 sp->showAge();//自己sp不支持这样的调用,因此要重载-> (*sp).showAge();//一样做为智能指针,也要支持这样的写法。因此依旧重载* }
咱们上文中是经过算符重载来实现的智能指针,在C++11标准中引入了智能指针概念。
1.理解智能指针
C++和Java有一处最大的区别在于语义不一样,在Java里面下列代码:
Animal a = new Animal(); Animal b = a; //你固然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样, Animal a; Animal b = a; //这里倒是就是生成了两个对象。
2.智能指针的本质
智能指针是一个类对象,这样在被调函数执行完,程序过时时,对象将会被删除(对象的名字保存在栈变量中),
这样不只对象会被删除,它指向的内存也会被删除的。
3.智能指针的使用
智能指针在C++11版本以后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、auto_ptr
这里只给出建议(智能指针会涉及到不少知识,属于C++的综合题):
简单来讲,重写就是返回值,参数,函数名都和圆脸同样,以后函数体里面的方法重写了。
下面将详细介绍。
程序调用函数时,编译器将源代码中的函数调用解释为特定函数代码块被称为函数名联编(binding)。C语言中没有重载,因此每一个函数名字都不一样,因为C++中有重载的概念,因此编译器必须查看函数参数以及函数名才能肯定使用哪一个函数。C/C++编译器能够在编译过程当中完成联编。而在编译过程实现的联编称静态联编(static binding)。所谓动态联编(dynamic binding)是指联编在程序运行时动态地进行,根据当时的状况来肯定调用哪一个同名函数,其实是在运行时虚函数的实现。国内教材有的称之为束定。
经过动态联编引出了虚函数:
语法上来讲,虚函数的写法是:在类成员函数声明的时候添加 vitual关键字。
咱们继续刚刚有关基类和派生类的特殊用法继续说,
将派生类的引用或指针转换成基类的引用和指针咱们称之为:向上强制转换(upcasting)
相反,将基类的引用或指针转换成派生类的引用和指针咱们称之为:向下强制转换(downcasting)
咱们如今知道,向下转型是不被容许的。
虚函数指针
虚函数指针 (virtual function pointer) 从本质上来讲就只是一个指向函数的指针,与普通的指针并没有区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,其实是经过调用该虚函数指针从而找到接口。
虚函数指针是确实存在的数据类型,在一个被实例化的对象中,它老是被存放在该对象的地址首位,这种作法的目的是为了保证运行的快速性。与对象的成员不一样,虚函数指针对外部是彻底不可见的,除非经过直接访问地址的作法或者在DEBUG模式中,不然它是不可见的也不能被外界调用。
只有拥有虚函数的类才会拥有虚函数指针,每个虚函数也都会对应一个虚函数指针。因此拥有虚函数的类的全部对象都会由于虚函数产生额外的开销,而且也会在必定程度上下降程序速度。与JAVA不一样,C++将是否使用虚函数这一权利交给了开发者,因此开发者应该谨慎的使用
虚函数表(如下解释,来自于https://blog.csdn.net/haoel/a...。由于作图太麻烦,因此直接选择性的截取一点。)
在这个表中(V-Table),主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,因此,当咱们用父类的指针来操做一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图同样,指明了实际所应该调用的函数。
编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——若是有多层继承或是多重继承的状况下)。 这意味着咱们经过对象实例的地址获得这张虚函数表,而后就能够遍历其中函数指针,并调用相应的函数。
举个例子:
class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } };
按照上面的说法,咱们经过把Base实例化,来得到虚函数表。
typedef void(*Fun)(void); Base b; Fun pFun = NULL; cout << "虚函数表地址:" << (int*)(&b) << endl; cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl; // Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b)); pFun();
实际结果以下:
虚函数表地址:0012FED4
虚函数表—第一个函数地址:0044F148
Base::f
经过这个示例,咱们能够看到,咱们能够经过强行把&b转成int ,取得虚函数表的地址,而后,再次取址就能够获得第一个虚函数的地址了,也就是Base::f(),这在上面的程序中获得了验证(把int 强制转成了函数指针)。经过这个示例,咱们就能够知道若是要调用Base::g()和Base::h(),其代码以下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f() (Fun)*((int*)*(int*)(&b)+1); // Base::g() (Fun)*((int*)*(int*)(&b)+2); // Base::h()
在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”同样,其标志了虚函数表的结束。这个结束标志的值在不一样的编译器下是不一样的。(有多是NULL也有多是0)
同时,派生类是否对父类函数进行了覆盖,虚函数表也是不同的,因此咱们分状况来讨论。
1.无覆盖
定义以下的继承关系:
对于实例而言,其虚函数表:
2.有覆盖(这才是通常状况,由于虚函数不覆盖便毫无心义)
咱们只重载了f()。因此其虚函数表:
Base \*b =newDerive(); b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,因而在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
3.有多个继承可是无覆盖
这是其虚函数表:
4.有多个继承且有覆盖
其虚函数表是:
三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,咱们就能够任一静态类型的父类来指向子类,并调用子类的f()了。
Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g()
走到这一步,咱们就能够总结一下了。
刚刚一直在说一个新的词汇——覆盖。但其实,可能不少人如今已经知道了,这里的覆盖就是重写。
所谓静态联编就是函数重载,所谓动态联编就是函数重写
向下强制转型不被编译器容许。
同时,咱们利用虚函数表的特性仍能够作非法的行为:
访问non-public的函数
若是父类的虚函数是private或是protected的,但这些非public的虚函数一样会存在于虚函数表中,因此,咱们一样可使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易作到的。
class Base { private: virtual void f() { cout << "Base::f" << endl; } }; class Derive : public Base{ }; typedef void(*Fun)(void); void main() { Derive d; Fun pFun = (Fun)*((int*)*(int*)(&d)+0); pFun(); }
咱们如今应该明白:编译对虚函数使用动态联编的意思了。
Q:为何编译默认是静态联编?
A:咱们除了功能之外始终不能忽视就是效率。由于根据上文的描述,咱们不难想到用一些方法来跟踪基类指针或引用指向的类模型这件事自己其实增长咱们的开销。所谓C++编译器选择了开销更低的方式。咱们应该优先选择效率更高的方式来开发程序。
当咱们知道了虚函数的原理的同时也必须知道虚函数到底增长哪些开销:
class A { public: vitual void A(int a){...} }; class B:piblic A { public: vitual void A(){...} };
派生类中没有参数的A把基类中有参数的A给隐藏了,并无重写。有可能编译器给你警告,也有可能不会。在《C++ Prime Plus》中将这样的错误称为“返回类型协变(covariance of return type)”
抽象类(abstract base class ,ABC),这里的抽象类其实就是Java中所说的接口。并不难理解。
这里举一个例子:羊类。咱们能够写出山羊类来继承羊类,一样也能够写绵羊类来继承羊类,也许咱们还能写出更不同的羊来继承羊类。但别忘了,咱们必须给羊类的成员函数作出一个定义,即使羊类的成员函数里根本没有有意义的代码。那咱们与其写没有意义的代码,倒不如干脆什么都别写。再具体一点: 羊会跑——void run() 同时 void run()中可能会用到羊类里面的一个属性——奔跑的速度。可是,不一样种类的羊跑的速度又不同快。这是咱们会在void run()里面什么都不写。直接一个{}就完事。等待子类重写这个void run()。因此,这里的run()虽然有定义,可是这倒是一个接口的思想。因此,咱们能够把void run()写出ABC的样子:vitual void run() = 0;
这样这个类就变成了抽象类,而run这个函数就成为了纯虚函数即,这个羊类纯粹是为了让其余类继承重写而出现的。这样若是之后有新的需求能够直接来实现这个羊的接口。
//"dma.h" #ifndef DMA_H_ #define DMA_H_ #include <iostream> class baseDMA { private: char * label; int rating; public: baseDMA(const char * l = "null", int r = 0); baseDMA(const baseDMA & rs); virtual ~baseDMA(); baseDMA & operator= (const baseDMA & rs); friend std::ostream & operator<<(std::ostream & os,const baseDMA & rs); }; class lacksDMA:public baseDMA { private: enum{COL_LEN = 40}; char color[COL_LEN]; public: lacksDMA(const char * c = "blank", const char * l = "null", int r = 0); lacksDMA(const char * c, const baseDMA & rs); friend std::ostream & operator<<(std::ostream & os,const lacksDMA & rs); }; class hasDMA:public baseDMA { private: char * style; public: hasDMA(const char * s = "none", const char * l = "null", int r = 0); hasDMA(cosnt char * s, const baseDMA & rs); ~hasDMA(); hasDMA & operator= (cosnt hasDMA & rs); friend std::ostream & operator<< (std::ostream & os,const hasDMA & rs); }; #ennif
#include "dam.h" #include <cstring> baseDMA::baseDMA(const char * l, int r) { label = new char[std::strlen(l) + 1]; std::strcpy(label, rs.label); rating = rs.rating; } baseDMA::~baseDMA() { delete [] label; } baseDMA & baseDMA::operator=(const baseDMA & rs) { if(this == &rs) reurn *this; delete [] label; label = new char[std::strlen(rs.label) + 1]; std::strcpy(label, rs.label); rating = rs.rating; return *this; } std::ostream & operator<<(std::ostream & os, const baseDMA & rs) { os<<"Label:"<< rs.label <<std::endl; os<<"Rating:"<< rs.rating << std::endl; return os; } lacksDMA::lacksDMA(const char * c, const char * l, int r):baseDMA(l,r) { std::strcpy(color, c, 39); color[39] = '\0'; } lacksDMA::lacksDMA(const char * c, const baseDMA & rs):baseDMA(rs) { std::strncpy{color, c, COL_LEN - 1}; color[COL_LEN - 1] = '\0'; } std::ostream & operator<<(std::ostream & os, const lacksDMA & ls) { os<< (const baseDMA &) ls; os<<"Color: "<< ls.color << std::endl; } hasDMA::hasDMA(cosnt char * s, const char * l, int r):baseDMA(l, r) { style = new char[std::strlen(s) + l]; std::strcpy(style,s); } hasDMA::hasDMA(const char * s, const baseDMA & rs):baseDMA(rs) { style = new char[std::strlen(s) + l]; std::strcpy(style,hs.style); } hasDMA::~hasDMA() { delete [] style; } hasDMA & hasDMA::operator=(const hasDMA & hs) { if(this == &hs) return *this; baseDMA::operator=(hs); style = new char[std::strlen(hs.style) + 1]; std::strcpy(style, hs.style); return *this; } std::ostream & operator<<(std::ostream & os, const hasDMA & hs) { os << (cosnt baseDMA & ) hs; os << "Style: " << hs.style << std::endl; return os; } //main.cpp #include <iostream> #include "dma.h" int main() { using std::cout; using std::endl; baseDMA shirt("Porablelly", 8); lacksDMA balloon("red", "Blimpo", 4); hasDMA map("Mercator", "Buffalo Keys", 5); cout << shirt << endl; cour << balloon << endl; lacksDMA balloon2(balloon); hasDMA map2; map2 = map; cout << balloon2 << endl; cout << map2 << endl; return 0; }
-----本文仅我的观点,欢迎讨论。