补充点
引用(&)
&在C语言表示的是取地址符————用在函数传参中的指针赋值
在C++语言中引用是某一个变量的别名,对引用的操作与对变量直接操作完全一样。
1 | int a; |
- &在此不是求地址运算,而是起标识作用。
- 声明引用时,必须同时对其进行初始化。
- 类型标识符是指目标变量的类型。
- 引用声明完毕后,相当于目标变量名有两个名称————目标原名和引用名,且不能再把该引用名作为其他变量名的别名
- 引用本身不占存储单元,本身不是一种数据类型
- 不能建立数组的引用
引用的作用
- 作为函数的参数,效果是和传递指针的效果是一样的
- 使用引用传递函数的参数,由于在内存中没有产生实参的副本,它是直接对实参操作。
常引用
1 | 常引用声明方式:const 类型标识符 &引用名=目标变量名; |
不能通过引用对目标变量的值进行修改,从而使引用的目标成为const
引用作为返回值
1 |
|
1 |
|
引用和多态
1 | class A; |
Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。
虚函数
抽象类是指包括至少一个纯虚函数的类。
C++允许用户使用虚函数 (virtual function) 来完成 运行时决议 这一操作,这与一般的 编译时决定 有着本质的区别
- 在基类用virtual声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便被调用。
- 在派生类中重新定义此函数,要求函数名,函数类型,函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
- 在类外定义虚函数时,不必在定义virtual
- c++规定,当一个成员函数被声明为虚函数后,其派生类的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每层声明该函数时都加上virtual,使程序更加清晰。
- 如果再派生类中没有对基类的虚函数重新定义,则派生类简单的继承起基类的虚函数。
实现机制
用虚表和虚指针
是每个类用了一个虚表,每个类的对象用了一个虚指针。虚表是和类对应的,虚表指针是和对象对应的。
虚表
一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数
没有覆盖父类的虚函数是毫无意义的。
多态
C++中的虚函数的作用主要是实现了多态的机制。
多态用虚函数来实现,结合动态绑定,此时的调用就不会在编译时候确定而是在运行时确定。不在单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数
虚函数是在基类中定义的,目的是不确定它的派生类的具体行为。故可以理解成java的父类中待覆写的函数
混淆点
关于函数的调用机制
(1)调用过程:建立函数调用栈—保存调用函数的运行状态和返回地址—传参—进入被调函数。
(2)传参机制:传参不一定会压栈!fastcall(快调)一般会由寄存器传参,另外当参数不超过4个时一般也由寄存器传参,否则会压栈。压栈顺序一般是从右到左
传参方式
值传递(传副本)
形参只是实参的拷贝
在函数结束时,形参作为局部变量会被释放,对实参不会产生任何影响。若为类的对象会调用拷贝构造,这种深拷贝操作会影响到传参效率。(可理解为“单向接口”)
指针传递
指针本质上是一个变量,该变量的值是一个地址,指针在逻辑上是独立的,可以被改变。
传递的是实参的地址,所以函数内部对形参得操作会“同步更新”到实参。(可理解为“双向接口”)
引用传递
传递的是实参的别名,传参时形参被绑定到实参对象上,因此函数内部对形参的操作也都会“同步更新”到源实参。(可理解为“双向接口”)
引用传递和值传递的异同
相同点
- 都是地址的概念
不同点
指针是一个实体(替身);引用只是一个别名(本体的另一个名字)
引用只能在定义时被初始化一次,之后不可改变,即“从一而终”;指针可以修改,即“见异思迁”;
引用不能为空(有本体,才有别名);指针可以为空;
sizeof 引用,得到的是所指向变量的大小;sizeof 指针,得到的是指针的大小;
指针 ++,是指指针的地址自增;引用++是指所指变量自增;
引用是类型安全的,引用过程会进行类型检查;指针不会进行安全检查;
关于static用法
静态局部变量!=全局变量,二者生命周期相同,但作用域不同,静态局部变量只对函数体内部可见。
static修饰全局变量:限定该变量只在该文件中可用。
static修饰外部函数:往往在函数声明中加static修饰,限定该函数只在该文件可用(若在头文件中声明,则限定只在其对应的源文件中可用)
类中static修饰的成员。静态成员与类的对象实体无关,是该类的共享变量。
关于extern用法
C/C++头文件中的函数声明默认为extern,即外部可用(其他源文件只需包含头文件即可使用)。和static修饰的效果相反。
带extern的变量仅仅是声明而不是定义!用extern使变量可以在多文件中共享,主要有两种做法:
- 在源文件中定义,其他需要使用该变量的源文件用extern声明。(表示该变量在其它文件中定义,即一次定义,多次extern声明)
- 在源文件中定义,其对应的头文件中extern声明,其他需要使用该变量的源文件包含该头文件即可。(更加标准的做法)
内联
用途:定义或声明函数时返回值前加上inline修饰能避免函数调用带来的时间和空间开销,提高效率,适用于反复执行的核心代码。内部实现机制其实是编译时按函数体展开代码,避免了函数调用的一系列压栈出栈过程。
限制:
(1)不能出现复杂的控制结构语句;
(2)递归函数不能用作内联函数;
(3)内联函数体不宜代码过长,只适合数行的小函数。
注:1.内联和宏定义均属于代码替换机制,但前者安全性更好,宏只是预处理做简单的符号替换而不会做类型检查。内联可以完全替代宏,反之不能。
2.inline关键字只是一种“建议”,是否采用内联机制取决于编译器。
重载
用途:C++特有机制。让同一种算法针对不同类型使用相同的函数名,提高代码可读性,重载的函数至少在参数个数或类型上有所区别,仅返回值区别不能重载!
1 | void func(int);和int func(int);//编译器无法区分 |
ps:重写是面向对象中子类对父类虚函数的重新实现
底层const修饰的参数以及常量成员函数也可以重载。
友元
- 友元(frend)机制允许一个类将对其非公有成员的访问权授予指定的函数或者类
- 友元的声明以friend开始,它只能出现在类定义的内部
友元函数
友元函数是一个不属于类成员的函数,但它可以访问该类的私有成员。————友元函数视作好像是该类的一个成员
1 |
|
友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
关于友元类的注意事项:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。
1 |
|
友元成员函数
使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员了。
vector用法
vector 是向量类型,它可以容纳许多类型的数据,如若干个整数,所以称其为容器。
vector是一个类模板,而不是类型。
动态联编与静态联编
c++一般的子类和父类继承关系的时候,都是使用的是静态联编(和之前学习的指针的用法一致)参考:https://blog.csdn.net/neiloid/article/details/6934129
参考:https://blog.csdn.net/neiloid/article/details/6934129
参考:https://blog.csdn.net/gaoxin1076/article/details/8298279
联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的操作调用(函数调用)与执行该操作(函数)的代码段之间的映射关系;按照联编所进行的阶段不同,可分为静态联编和动态联编;
仔细读读红色字体的那部分句子。我们就能很清楚的明白什么是联编了。给大家举个最通俗易懂的例子好了:
A类中有fun这个函数, B类中也有fun这个函数,现在我在类外的main函数里面调用fun 函数。
那么main函数就是函数调用,调用fun函数,
而A类中的fun函数和B类中的fun函数就是执行该操作的代码段
所以现在联编就是实现两者的映射关系。
1 | class A |
联编就是决定将main函数中的func()的函数调用映射到A中的func函数还是B中的func函数的过程。
2.静态联编 和 动态联编 的定义
知道了什么事联编,那么再来理解动态联编 和静态联编也就不难了
静态联编:
是指联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编;因为这种联编是在程序开始运行之前完成的;
在程序编译阶段进行的这种联编又称静态束定;在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又被称为束定;编译时束定又称为静态束定;
拿上面的例子来说,静态联编就是在编译的时候就决定了main函数中调用的是A中的func还是B中的func。一旦编译完成,那么他们的映射关系就唯一确定了。
动态联编:
编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或动态束定,又叫晚期联编;C++规定:动态联编是在虚函数的支持下实现的;
动态联编在编译的时候还是不知道到底应该选择哪个func函数,只有在真正执行的时候,它才确定。
静态联编和动态联编都是属于多态性的,它们是在不同的阶段进对不同的实现进行不同的选择;
也可以这么说:C++多态有两种形式,动态多态和静态多态(函数重载);动态多态是指一般的多态,是通过类继承和虚函数机制实现的多态;静态多态是通过模板来实现,因为这种多态实在编译时而非运行时,所以称为静态多态。
3.静态联编
首先还是拿个例子来说事吧。
1 |
|
现在我们详细具体定义了一开始的A类和B类以及func函数。让我们来分析一下:
调用oneshape.fun()的时候,进入类shape中的fun函数。
现在我们的问题就是:fun函数调用的draw到底是shape里面的draw还是circle中的draw??
答案是:它调用了cshape这个基类的draw函数。所以输出了 I am shape
那么一直困扰我的问题是:为什么调用基类的draw而不是派生类中得draw呢?
书上好像没有具体讲,上课的时候老师那也根本不会讲。
自己想了一下,应该可以从汇编的角度理解:
1.调用oneshape.fun(),这里是一个跳转指令,进入类shape中的fun函数所在的代码段
2.类shape的代码段是依次顺序放置的。进入fun函数后,现在我们要调用draw的地址。
由于没有另外的数据结构来保存draw的地址,所以程序所知道的,必然只有在shape类中的draw地址了,仅仅用一个跳转指令
在我的vs2010的反汇编调试窗口下是这样的一句代码:
013B1546 call shape::draw (13B10F5h)
很明确这里指出了shape::draw,也就确定了映射关系,完成了联编。
C++和java的异同
Java源码会先经过一次编译,成为中间码,中间码再被解释器解释成机器码。对于Java而言,中间码就是字节码(.class),而解释器在JVM中内置了。
C++源码一次编译,直接在编译的过程中链接了,形成了机器码。
Java是纯面向对象的语言,所有代码(包括函数、变量)都必须在类中定义。而C++中还有面向过程的东西,比如是全局变量和全局函数。
C++支持多继承,Java中类都是单继承的。但是继承都有传递性,同时Java中的接口是多继承,类对接口的实现也是多实现。
变量和类型
Java没有无符号整数。(无符号右移在Java中强制用三个右尖括号表示)。
Java有内置类型String,而C++没有。C++的std::string是可变的,类似于Java的StringBuffer。
Java中不存在指针。Java的引用是功能弱化的指针,只能做“调用所指对象的方法”的操作。
Java中,对象只能由引用传递,C++中对象可由值或引用传递。
类机制
- Java是完全面向对象的,所有方法都必须写在类中。