C/C++基础:函数/指针/多态

变量内存地址与指针

我们知道内存是一块能够存储数据的区域。从本质上来讲,存储器被划分为若干个存储单元,每个存储单元都有一个固定的编号,即内存地址。计算机的最小信息单位为1bit,但是作为内存的最小处理单位是字节,即8bit(【1】p4)。

变量的存取方式

int a = 100;

如上所示,在声明了一个int型的变量。此时,编译器会为其申请一块int大小的内存区域,并在这块内存区域中放入值100。
从理论上来讲,我们如何来访问这个值呢?那自然是通过这块内存地址来访问。
这让我们想到了程序语言的发展史:
我们知道CPU大概的工作原理是什么:拥有一套寄存器来存放数据,定义一套指令集,用于约定某个二进制组合的意义,然后程序员根据定义好的指令集来自定义输入,来处理数据。
这被称为机器语言,是整个程序设计的底层。举例如下:

在这里插入图片描述

在最一开始的上古程序员用纸带打孔的形式来作为输入,通过有孔和无孔来表示二进制0和1,读入后便可以存储到内存中,然后由约定的表示方式来进行计算工作。
先不提多任务的处理,进程,内存管理等高级功能,就拿简单的计算器功能来看,显然这也是不直观的。程序员需要记住或者查表来知道什么指令是加法符,什么指令是输出,关键的是这些指令都是0和1,稍微不慎写错了很难查出来。慢,又难debug。

很快程序员们发现了这个问题。很显然,抽象出所有需要的功能,在这机器语言的基础之上封装一层,将其定义为关键字,就可以避免必须直接输入0和1的来实现关键字。于是汇编语言诞生了。举例如下:
在这里插入图片描述
这样就简化了机器指令的查询或记录。汇编语言定义了很多中内存的寻址方法,允许直接根据内存地址将该处的值读到寄存器中,也可以通过其他的地址转换的方式读到寄存器中。无论如何,这样本质上还是需要知道内存地址才能处理期望处理的值。
这还是不方便,因为需要知道内存地址。那么能不能不用记住内存地址,用一个标记来表示呢?
答案是肯定的,后期发展出来的高级语言在这基础上又封装了一层:可以通过名字而不是内存地址来访问内存中的位置。这些名字就是我们所称的变量。
变量名与内存位置的关系不是硬件实现的,而是编译器实现的。这些变量给了我们一种更方便的方法来记住地址,而对于下层来说,硬件仍然通过地址访问内存位置(【1】p92)。
在这里插入图片描述
回到我们刚刚看到的那个例子,int a = 100表示为一个变量a分配了一块内存地址,假设该内存地址为0x0000,其大小为一个int的大小,在32位处理器下为4Byte。内存地址的表示中,最小管理单位为1Byte,则该变量的值需要的内存地址为0x0000到0x0004。在这块区域的内存中放入值100。
当我们访问变量a的时候,实际上就是通过a的地址来得到该地址的值本身。而我们可以通过

&a;

来获得变量a的地址,也就是要表示的变量的地址。

那么回到指针这个问题。什么是指针?为什么C要发明指针这么一个概念?有篇文章讲得很好:参考一下为什么说指针是C语言的精髓

有位不知名的知友给出了个有趣的编程语言发展史:

asm: machine code is hard to r/w

c: asm is too trivial

c++: c doesn’t have class

java: c++ has pointers

javascript: scheme doesn’t like c

nodejs: c is too low-level for asyn programming

clojure: java is not lisp

本质上C是加了一层语法糖的汇编,引进当时先进的函数,保留了汇编强大的地址直接访问功能 —— 也就是所谓的指针功能,以增加操作灵活性。

我们知道上面的这个变量名称a是这块内存的地址的一个昵称,而该处保存的是值本身,通过这个变量名称a可以获取到这个地址的值。
如果我们想实现一种更加灵活的功能:

  1. 在进行消息传递时,对于一段二进制数据作为上下文,传过来后我们怎么能最快地解析出来呢?答案很简单,因为我们知道这个上下文的结构体是怎么定义的,我们就能知道在内存中这个结构体中每个变量的地址分布。也就是只要能知道这段二进制在内存中的起始位置,就能通过一个简单的强制转换获得这个结构体中所有成员变量。此处要说的是,我们需要一个变量来直接访问内存中的某个地址的数据。
  2. C只有值传递,因此若要传值时都需要复制整个值。这将会占用大量的计算资源和存储资源。毫无疑问,如果能够不复制这个值,而只是将这个地址传过去,对方通过这个地址就能知道要处理的对象是什么,这样就能提升大量的效率。这便是传递引用。如何来实现地址的传递呢?显然我们需要一个变量来保存地址,然后通过这个变量来进行地址的传递。(当然此时还是值传递,拷贝了这个地址,但是拷贝一个地址自然比拷贝整个数据结构占用的资源少得多)。
  3. C中的数组本质上就是一片连续的内存区域,在数组创建时提供了每个数组内结构占用的空间大小和数目,这样编译器可以有基本的容错能力。实际上我们也可以直接通过地址和合理的偏移来直接获得数组中的某个变量的值(这个时候就没有编译器来进行边界检查,而且若偏移的位置不对,得到的数据也是混乱的,这就需要自行注意)。另外,函数中不能传递数组,此时便可以通过指针的形式来传递。

这个用来保存地址的变量就被称为指针。可以通过指针来访问内存中任何位置的数据。这就是指针的起源。

要理解并灵活地应用指针,就需要理解数据在内存中的组织与分布。

举例说明:

int a = 100;
int *d = &a;//将a通过&取得地址,将该地址赋给指针变量d。此时d的值为a的地址,*d则是通过解引用获得d保存的地址即a处的值100;

*操作符表示要访问其操作数表示的地址。
&操作符表示要获得其操作数表示的地址。

int *d;
d = &a;:
int *d = &a;

变量和指针的const限定符

const限定符具有限定其为常量的功能. 在ANSI C中,可以使用int const a;或者const int a来将a声明为一个整数,且该值除了在声明以外不能被更改.

在涉及到指针的时候情况就变得比较有意思:

int *pi;: pi是一个普通的指向整型的指针.
int const *pci; pci是一个指向整型常量的指针. 可以修改指针的值, 但不能修改它指向的值.即指针可以指向其他地方,但当前指向值的已经固定为常量,不能更改.
int *const cpi;: pci是一个指向整型的常量指针.该指针是个常量, 它的值无法被修改, 即该指针不能指向其他地方. 但可以修改它所指向的整型的值.
int const * const cpci;: cpci无论是指针本身还是它所指向的值都是常量, 都不允许修改.

其实也很好理解, const限定符起作用是该限定符后面的那一个修饰符. 若为*, 则表示是该指针指向的值是const; 若为指针所保存的地址,则表示该指针所保存的地址不能被修改, 即不能指向其他地方.

C++的面向对象思想:动态绑定/虚函数/多态

什么是多态?

是指类中具有相似功能的不同函数,使用同一个名称来实现;是对类的行为再抽象.
使得不同的对象对于收到的同一消息可以产生完全不同的结果,这一现象被称为多态.

使用同一个名称来实现不同的函数在语言的前半部分就学过一个典型的操作: 函数重载. 函数重载允许函数名相同而形参类型不同, 编译器会根据传入参数进行匹配, 来选择合适的函数.
这是一种多态, 且是一种静态的编译时的多态:静态联编(重载、强制、参数),程序编译连接阶段完成。
还有一种多态更加典型一点, 通常我们说起来C++的多态的时候更加想表达的是动态联编(在函数运行时才决定)的多态: 虚函数实现的多态.

那么, 虽然是想要实现一个"使用相同的接口来实现差异化功能"的效果, 为什么要引入一个多态呢? 这动态联编来决定调用哪个接口又是怎么实现的呢?

这要从类与对象的思想说起. 我们设计类的起因在于将想要表达的东西进行抽象, 父类具有更一般的性质, 而不同的子类可以继承父类的一般性质的基础上, 拥有自己独特的性质. 按照这样的思想进行开发, 就可以实现可扩展性.

但这还不够灵活. 试想一下, 如果我们已经有个父类定义好了通用的框架, , 但是需要针对不同的需求给出不同的版本, 即需要定义许多子类,并且这个需求经常需要更改, 怎样设计合理一点? 我们当然希望万变不离其宗, 无论需求怎么变, 无论新创建多少个不同的子类, 都用一套接口来调用管理.

举个例子说明, 有一个父类"动物" 有多个子类, 分别为"猫", “狗”,“猫头鹰”. 父类有个基本的属性,为"吃饭",显然这对于动物类来说都是通用的.
不过我们假设的是猫, 狗, 猫头鹰吃的东西不同, 主人给猫喂的是猫粮, 给狗喂的是狗粮, 给猫头鹰喂的是老鼠. 这样在猫/狗/猫头鹰各自的类中实现的"吃饭"就有所区别.

这样我们就可以实例化一个父类 “动物"的对象, 然后看是针对哪个动物进行"吃饭"操作. 若是猫, 则实例化一个"猫” 的对象, 然后将父类的指针指向该子类(猫), 然后调用"吃饭"接口来进行操作. 反之若是狗,则实例化一个"狗"的对象, 然后将父类的指针指向该子类(狗).

这样做有什么好处呢? 显然最大的好处是"统一接口, 精准实例化". 无论有多少个不同的子类需要处理任务, 我们都可以只操作一个父类的对象指针, 调用相同的接口, 只需将其指向不同的子类就可以实现该子类的效果. 这样做就十分灵活了!

语法: 虚函数, 构造函数, 虚析构函数

多态的实现:如何实现动态联编?

多态的本质在于运行时才决定使用哪个对象的接口.
我们知道对于函数来说, 其能够运行时指定了函数的地址. 因此我们可以通过指定地址的方式来决定使用哪个对象的函数. 这么一想, 显然我们需要一张表来保存这种对应关系. 此处便引出了虚函数表这一概念.

参考

【1】 《汇编语言》第三版,王爽
【2】 《C和指针》
【3】《C++ Primer》第五版

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值