引用
- 从内存的角度去看,变量有三种,一种是值value,一种是指针,一种是引用
- 大小
- 32位电脑上,int是四个字节,double是四个字节,指针也是四个字节
- 引用是其所引用对象的别名,但是引用的实现都是指针,引用的大小就是它所绑定对象的大小,即sizeof引用和sizeof它所绑定的对象得到的结果是相等的。对引用取地址与对它所绑定到对象取地址得到的是一样的结果。
- 引用一定要有初值,引用一旦被定义就不能改变绑定的对象
- 引用的常见用途
- 参数传递,传入函数参数,返回值传递
- 引用传递效率高,且调用接口与传值基本一致
void func1(Cls* pobj) { pobj ->xxx();}
void func2(Cls obj) { obj.xxx(); }//被调用端用法相同,很好
void func3(Cls& obj) { obj.xxx(); }//被调用端用法相同,很好
...
Cls obj;
func1(&obj);
func2(obj);//接口端接口相同,很好
func3(obj);//接口端接口相同,很好
double imag(const double& im) /*const*/{ ... }
double imag(const double im) { ... }
//上面的二者无法通过编译,有二义性
//它们的函数签名是相同的,签名不包含返回值类型
//const是函数签名的一部分
对象模型
- 面向对象实际上谈的是对象和对象之间的关系,对象是由类创建出来的,实际上谈的是类与类之间的关系。
- 继承
- 子类对象里面有父类的成分,如桃子是水果
- 构造要由内而外,即先构建基类的成分,再构建派生类的成分
- 析构要由外而内,先执行释放派生类的成分,再释放基类的成分
- 复合has a
- 一个类里面有另一个类
- 构造由内而外,先构造成员类,再构造外部的类
- 析构要由外而内,先调用释放外部的类,再释放内部的类
- 组合
- 基类有派生类,派生类中有一个成员类,相当于派生类对象中有两个类的部分,一个是基类,另一个是成员类
- 构造由内而外,先构造内部的两个类的成分,再构造自己的部分
- 析构由外而内,先释放自己的部分,再释放基类以及成员类的部分
- 继承
虚指针和虚表
-
只要类中有虚函数,它的对象中就会多一个指针,称作虚指针,类对象的大小等于所有的数据大小之和加一个指针的大小
-
继承不但继承了基类的数据,还继承了基类函数的调用权,而不是函数的内存大小,函数的内存大小是无法计算的
-
父类有虚函数,子类一定有
-
虚指针指向虚表,虚表里面放的都是函数指针,指向虚函数
- 基类有两个虚函数,如果派生类改写了基类的一个虚函数,则派生类对象的虚指针所指向的虚表的两个函数指针,一个与基类的相同因为派生类没有重写该虚函数,重写虚函数则与基类不同
- 编译器发现对象调用虚函数时会通过虚表找到对应的虚函数
-
静态绑定与动态绑定
- 静态绑定:编译器看到一个调用的动作,会将它编译成callxxxx,xxxx为所调用函数的地址。要调用该函数,编译器就将它解析出来,跳到那个地方去,运行完在return回来,这就是静态绑定
- 通过指针去调用虚函数不能做静态绑定。
- 动态绑定:通过指向对象的指针,找到虚指针,再找到虚表,再根据函数名寻找真正要调用的函数,取出其中的第n个,把它当成函数指针去调用。解析成c的语法如下图底部,n表示虚表中的第n个。n是由编译器在编代码时根据函数声明的顺序确定的,n从0开始。
-
示例
- 设计一个shape类,它派生出各种形状
- 为了让容器能容纳各式各样的形状,每个形状所表示的类对象的大小不同,所以容器存放的必须是指向父类的指针
- 对每一个图形,都要有自己的draw函数,将draw设计为虚函数,避免了c中用if else依次判断指针指向的到第是哪种图形。且c的这种写法,如果后期再增加新的图形,又要重新增加if else程序
-
总结
- c++编译器看到一个函数调用,它会考虑是将它静态绑定还是动态绑定
- 静态绑定就是CALL+函数的地址
- 满足三个条件编译器会进行动态绑定
- 必需通过指针调用
- 该指针必需是向上的关系up cast,即new一个桃子,必需声明为水果,实际指向的必须是子类对象,这个保证安全
- 调用的是虚函数
- 从虚指针到虚表到虚函数,这一条路线称作虚机制
- 动态绑定要看指针实际上指的是哪种对象,
- 虚函数的这种用法称作多态,一种声明的指针却可以指向不同类型的对象,指针有很多的类型,叫多态
- 多态、虚函数、动态绑定(虚指针+虚表),这些实际上是同一间事情
this指针
- 通过对象来调用函数,对象的地址就是this的值
- 虚函数在使用时一般有两种形式的用法
- 一个是多态,如上例
- 另一个是模板方法,是一种设计模式
- 有两个类,基类是CDocument,派生类是CMyDoc。基类有一个叫OnFileOpen的函数,它将普通的处理工作写完,对于最重要的Serialize函数,它现阶段无法写,需要子类重写该函数。
- main函数中,子类创建了一个对象,通过子类对象调用父类的函数,
- 灰色的这条线是实际的调用过程
- 子类对象调用父类的函数,对象的地址就是this指针的值,函数执行到Serialize时,该函数是虚函数,实际调用的是子类中的Serialize函数,满足动态绑定的三个条件
- 通过指针调用
- 指针指向的是子类对象
- 调用的是虚函数
- 编译器将调用虚函数的代码编译成如上图左上角的形式,继续动态绑定
- Serialize执行结束后,继续执行OnFileOpen中剩下的部分
- 子类对象调用父类的函数,对象的地址就是this指针的值,函数执行到Serialize时,该函数是虚函数,实际调用的是子类中的Serialize函数,满足动态绑定的三个条件