C++指针运算符(& 和 *) 从内存看指针

  C++ 提供了两种指针运算符,一种是取地址运算符 &,一种是间接寻址运算符 *。

  指针是一个包含了另一个变量地址的变量,您可以把一个包含了另一个变量地址的变量说成是"指向"另一个变量。变量可以是任意的数据类型,包括对象、结构或者指针。

取地址运算符 &

  & 是一元运算符,返回操作数的内存地址。例如,如果 var 是一个整型变量,则 &var 是它的地址。该运算符与其他一元运算符具有相同的优先级,在运算时它是从右向左顺序进行的。

您可以把 & 运算符读作"取地址运算符",这意味着,&var 读作"var 的地址"。

间接寻址运算符 *

  第二个运算符是间接寻址运算符 ,它是 & 运算符的补充。 是一元运算符,返回操作数所指定地址的变量的值。

请看下面的实例,理解这两种运算符的用法。

#include <iostream>
 
using namespace std;
 
int main ()
{
   int  var;
   int  *ptr;
   int  val;

   var = 3000;

   // 获取 var 的地址
   ptr = &var;

   // 获取 ptr 的值
   val = *ptr;
   cout << "Value of var :" << var << endl;
   cout << "Value of ptr :" << ptr << endl;
   cout << "Value of val :" << val << endl;

   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Value of var :3000
Value of ptr :0xbff64494
Value of val :3000

总结

调用时,指针变量前加 " * " :使用间接寻址运算符获取该指针变量所指向的变量。
声明时,变量前加 " 基本类型* " -------声明指针变量。它的值是另一个变量的地址,该指针变量表示另一个普通变量的地址 ,例:int * 或 char *。

调用时,变量前加 " & " :使用取地址运算符获取该变量的地址,返回该变量的地址。
声明时,变量前加 " 基本类型& " :声明引用变量。它是某个已存在变量的别名,即该引用变量名称与原始变量名称都代表同一个变量。

调用时,二级指针变量前加 " ** " :获取该二级指针变量所指向的指针所指向的变量。
声明时,变量前加 " 基本类型 ** “:声明二级指针变量。它的值是另一个一级” 基本类型* "指针变量的地址 (指针的指针)。该二级指针变量表示另一个一级"基本类型 *"指针变量地址 。

所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同

示例:

#include <iostream>
using namespace std; 
int main(){
    int  var;     // 声明int类型变量var
    int * ptr;    // 声明指针变量ptr
    ptr = &var;   // 先使用 & 运算符获取变量var的地址,再把该地址赋值给指针变量ptr
    int ** pptr;  // 声明二级指针变量pptr
    pptr = &ptr;  // 先使用 & 运算符获取变量ptr的地址,再把该地址赋值给二级指针变量pptr
    int & ref1 = var;   // 声明引用变量ref1, ref1是变量var的别名(引用必须在创建时被初始化)
    int & ref2 = *ptr;  // 先使用*运算符获取指针变量ptr所指向的变量(即var),再用该变量(var)初始化引用变量ref2(声明引用变量ref2的同时对它进行初始化)。也就是说,该行代码执行后,ref2也是变量var的别名 var = 20
    cout << "Value of var: "; 
    cout << var << endl; 
    cout << "Value of &var: "; 
    cout << &var << "\t(var的地址)" << endl; 
    cout << endl; 
    cout << "Value of ptr: "; 
    cout << ptr << "\t(等于&var)" << endl; 
    cout << "Value of *ptr: "; 
    cout << *ptr << "\t\t(等于var)" << endl; 
    cout << "Value of &ptr: "; 
    cout << &ptr << "\t(ptr的地址)" << endl; 
    cout << endl; 
    cout << "Value of pptr: "; 
    cout << pptr << "\t(等于&ptr)" << endl; 
    cout << "Value of *pptr: "; 
    cout << *pptr << "\t(等于ptr, 等于&var)" << endl; 
    cout << "Value of **pptr: "; 
    cout << **pptr << "\t\t(等于*ptr, 等于var)" << endl; 
    cout << "Value of &pptr: "; 
    cout << &pptr << "\t(pptr的地址)" << endl; 
    cout << endl; 
    cout << "Value of ref1: "; 
    cout << ref1 << "\t\t(等于var)" << endl; 
    cout << "Value of &ref1: "; 
    cout << &ref1 << "\t(等于&var)" << endl; 
    cout << endl; 
    cout << "Value of ref2: "; 
    cout << ref2 << "\t\t(等于var)" << endl; 
    cout << "Value of &ref2: "; 
    cout << &ref2 << "\t(等于&var)" << endl; 
    return 0; 
}

输出结果:

Value of var: 20
Value of &var: 0x7ffce63490bc (var的地址)
Value of ptr: 0x7ffce63490bc (等于&var)
Value of *ptr: 20 (等于var)
Value of &ptr: 0x7ffce63490b0 (ptr的地址)
Value of pptr: 0x7ffce63490b0 (等于&ptr)
Value of pptr: 0x7ffce63490bc (等于ptr, 等于&var)
Value of **pptr: 20 (等于
ptr, 等于var)
Value of &pptr: 0x7ffce63490a8 (pptr的地址)
Value of ref1: 20 (等于var)
Value of &ref1: 0x7ffce63490bc (等于&var)
Value of ref2: 20 (等于var)
Value of &ref2: 0x7ffce63490bc (等于&var)

  
  

从内存看指针

  C语言最核心的知识就是指针,所以,这一篇的文章主题是「指针与内存模型」说到指针,就不可能脱离开内存,学会指针的人分为两种,一种是不了解内存模型,另外一种则是了解。不了解的对指针的理解就停留在“指针就是变量的地址”这句话,会比较害怕使用指针,特别是各种高级操作。而了解内存模型的则可以把指针用得炉火纯青,各种 byte 随意操作,让人直呼 666。
  

一、内存本质

  编程的本质其实就是操控数据,数据存放在内存中。因此,如果能更好地理解内存的模型,以及 C 如何管理内存,就能对程序的工作原理洞若观火,从而使编程能力更上一层楼。所以,要想彻底理解指针,首先要理解 C 语言中变量的存储本质,也就是内存。
  

1.1内存编址

  计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,就像下面这样,

  在这里插入图片描述

  
  
  每一个单元格都表示 1 个 Bit,一个 bit 在 EE 专业的同学看来就是高低电位,而在 CS 同学看来就是 0、1 两种状态。由于 1 个 bit 只能表示两个状态,所以大佬们规定 8个 bit 为一组,命名为 byte。

  在这里插入图片描述

  
  
  并且将 byte 作为内存寻址的最小单元,也就是给每个 byte 一个编号,这个编号就叫内存的地址。

  在这里插入图片描述

  
  
  这就相当于,我们给小区里的每个单元、每个住户都分配一个门牌号: 301、302、403、404、501…在生活中,我们需要保证门牌号唯一,这样就能通过门牌号很精准的定位到一家人。

  同样,在计算机中,我们也要保证给每一个 byte 的编号都是唯一的,这样才能够保证每个编号都能访问到唯一确定的 byte。
  

1.2 内存地址空间

  上面我们说给内存中每个 byte 唯一的编号,那么这个编号的范围就决定了计算机可寻址内存的范围。所有编号连起来就叫做内存的地址空间,这和大家平时常说的电脑是 32 位还是 64 位有关。

  早期 Intel 8086、8088 的 CPU 就是只支持 16 位地址空间,寄存器和地址总线都是 16 位,这意味着最多对 2^16 = 64 Kb 的内存编号寻址。这点内存空间显然不够用,后来,80286 在 8086 的基础上将地址总线和地址寄存器扩展到了20 位,也被叫做 A20 地址总线。当时在写 mini os 的时候,还需要通过 BIOS 中断去启动 A20 地址总线的开关。

  但是,现在的计算机一般都是 32 位起步了,32 位意味着可寻址的内存范围是 2^32 byte = 4GB。所以,如果你的电脑是 32 位的,那么你装超过 4G 的内存条也是无法充分利用起来的。好了,这就是内存和内存编址。
  

1.3 变量的本质

  有了内存,接下来我们需要考虑,int、double 这些变量是如何存储在 0、1 单元格的。在 C 语言中我们会这样定义变量:

int a = 999;
char c = ‘c’;

  当你写下一个变量定义的时候,实际上是向内存申请了一块空间来存放你的变量。
  我们都知道 int 类型占 4 个字节,并且在计算机中数字都是用补码(不了解补码的记得去百度)表示的。999 换算成补码就是:00000000 00000000 00000011 11100111。这里有 4 个byte,所以需要四个单元格来存储,虽然前面两个高位为0:

在这里插入图片描述

  有没有注意到,我们把高位的字节放在了低地址的地方。那能不能反过来呢?当然,这就引出了大端和小端。像上面这种将高位字节放在内存低地址的方式叫做大端反之,将低位字节放在内存低地址的方式就叫做小端:

在这里插入图片描述

  上面只说明了 int 型的变量如何存储在内存,而 float、char 等类型实际上也是一样的,都需要先转换为补码。对于多字节的变量类型,还需要按照大端或者小端的格式,依次将字节写入到内存单元。记住上面这两张图,这就是编程语言中所有变量的在内存中的样子,不管是 int、char、指针、数组、结构体、对象… 都是这样放在内存的。

  

二、指针是什么东西?

  

2.1 变量放在哪?

  上面我说,定义一个变量实际就是向计算机申请了一块内存来存放。那如果我们要想知道变量到底放在哪了呢?可以通过取地址运算符&来取得变量实际的地址,这个值就是变量所占内存块的起始地址。(PS: 实际上这个地址是虚拟地址,并不是真正物理内存上的地址。

printf(“%x”, &a);

  我们可以把这个地址打印出来:,大概会是像这样的一串数字:0x7ffcad3b8f3c

  

2.2 指针本质

  上面说,我们可以通过&符号获取变量的内存地址,那获取之后如何来表示这是一个地址,而不是一个普通的值呢?也就是在 C 语言中如何表示地址这个概念呢?
  对,就是指针,你像下面可以这样,pa 中存储的就是变量 a 的地址,也叫做指向 a 的指针。

int *pa = &a;

在这里我想谈几个看起来有点无聊的话题:

问:为什么我们需要指针?直接用变量名不行吗?
答:当然可以,但是变量名是有局限的。

问:变量名的本质是什么?
答:是变量地址的符号化,变量是为了让我们编程时更加方便,对人友好,可计算机可不认识什么变量 a,它只知道地址和指令。

  所以当你去查看 C 语言编译后的汇编代码,就会发现变量名消失了,取而代之的是一串串抽象的地址。你可以认为,编译器会自动维护一个映射,将我们程序中的变量名转换为变量所对应的地址,然后再对这个地址去进行读写。也就是有这样一个映射表存在,将变量名自动转化为地址:

a | 0x7ffcad3b8f3c
c | 0x7ffcad3b8f2c
h | 0x7ffcad3b8f4c

  说的好!可是我还是不知道指针存在的必要性,那么问题来了,看下面代码:

int func(...) {
  ... 
};

int main() {
 int a;
 func(...);
};

  假设我有一个需求:

  要求在func 函数里要能够修改 main 函数里的变量 a,这下咋整,在 main 函数里可以直接通过变量名去读写
a 所在内存。 但是在 func 函数里是看不见a 的呀。

你说可以通过&取地址符号,将 a 的地址传递进去:

int func(int* address) {
  ....
};

int main() {
 int a;
 func(&a);
};

  这样在 func 里就能获取到 a 的地址,进行读写了。理论上这是完全没有问题的,但是问题在于:编译器该如何区分一个 int 里你存的到底是 int 类型的值,还是另外一个变量的地址(即指针)。

  这如果完全靠我们编程人员去人脑记忆了,会引入复杂性,并且无法通过编译器检测一些语法错误。

  而通过int * 去定义一个指针变量,会非常明确:这就是另外一个 int 型变量的地址。编译器也可以通过类型检查来排除一些编译错误。这就是指针存在的必要性。实际上任何语言都有这个需求,只不过很多语言为了安全性,给指针戴上了一层枷锁,将指针包装成了引用。可能大家学习的时候都是自然而然的接受指针这个东西,但是还是希望这段啰嗦的解释对你有一定启发。

  同时,在这里提点小问题:既然指针的本质都是变量的内存首地址,即一个 int 类型的整数。那为什么还要有各种类型呢? 比如 int 指针,float 指针,这个类型影响了指针本身存储的信息吗? 这个类型会在什么时候发挥作用?

  

2.3 解引用

  上面的问题,就是为了引出指针解引用的。pa中存储的是a变量的内存地址,那如何通过地址去获取a的值呢?这个操作就叫做解引用,在 C 语言中通过运算符 *就可以拿到一个指针所指地址的内容了。比如 *pa就能获得a的值。

  我们说指针存储的是变量内存的首地址,那编译器怎么知道该从首地址开始取多少个字节呢?这就是指针类型发挥作用的时候,编译器会根据指针的所指元素的类型去判断应该取多少个字节。如果是 int 型的指针,那么编译器就会产生提取四个字节的指令,char 则只提取一个字节,以此类推。

  下面是指针内存示意图:

在这里插入图片描述

  pa 指针首先是一个变量,它本身也占据一块内存,这块内存里存放的就是 a 变量的首地址。当解引用的时候,就会从这个首地址连续划出 4 个 byte,然后按照 int 类型的编码方式解释。

  

  

  
参考文献:https://www.runoob.com/cplusplus/cpp-pointer-operators.html
     https://zhuanlan.zhihu.com/p/298363575?utm_medium=social&utm_oi=740193666076794880

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值