C语言 指针理解


C语言 指针理解


正文

指针概念中有变量地址相关的内容,那变量名呢?是有个地方存有变量名、变量值的地址? 访问变量就是读到这个变量值的地址,然后去这个地址取储存的内容?取地址就是直接取这个变量属性块中变量值的地址? 下面来看看变量名是什么?

变量名存放在哪里

有时候当我们写下int a = 123;这样的语句,在代码运行的时候,a 在哪里呢? 了解了变量在内存中存储方式的人会知道,一般变量的值在存放在栈内存里面的,但是名字呢?

针对这个问题,我们先要区分一下编译型语言和解释型语言,这2种语言运行方式完全不一样,C/C++是典型的编译型语言,而且PHP/JS则是典型的解释型语言。

编译型语言要想运行,必须使用一个编译器去把代码转换成目标平台机器代码。而解释型语言是通过一个解释器实时翻译成一种中间代码一行行运行。 前者又被称为静态语言,后者又被称为动态语言。 像Java,C#则属于这2种中间,因为他们有一个预编译的过程,会先把代码转换成中间代码存放起来,在Java里面就叫字节码, 然后在虚拟机(jvm)里面执行,效率比纯解释执行高。 PHP就有一个opcache扩展可以把生成的中间代码opcode缓存起来以提高效率,不必每次运行的时候都生成。

说这么多,想说明一个问题,那就是变量名和变量在这2种语言里面的存储是有区别的,回到最开始的问题,咱先说说经典的C语言:

C语言里面变量和变量名的存储

为了说明这个问题,咱们简单的来说一下C里面变量在内存里面的存储:

  1. 栈区(stack)— 由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap) — 一般由程序员分配释放, 用来存储数组,结构体,对象等。若程序员不释放,程序结束时可能由OS回收。
  3. 全局区(静态区)(static)— 存放全局变量、静态数据、常量。程序结束后由系统释放。
  4. 文字常量区 — 常量字符串就是放在这里的。 程序结束后由系统释放。
  5. 程序代码区 — 存放函数体(类成员函数和全局函数)的二进制代码。

栈内存是有大小限制的,比如默认情况下,Linux平台的是8MB,如果超过这个限制,就会出现 stackoverflow,而堆内存并无限制,内存有多大就可以申请多大。

看完上面的说明,我们可以得出一个结论: 全局变量存放在全局区,在程序一开始就分配好了,而且局部变量在存放在栈区,运行的时候分配内存,用完之后内存会被自动释放。

但是这好像并没有说明变量名在哪里吧?比如下面这段C代码,a, b到底存在哪里:

#include <stdio.h>
 
int a = 1;  // 全局初始化区
 
int main(int argc, char const *argv[])
{
    int b;  //栈
    b = a + 5;
    printf("%d\n", b);
    return 0;
}

为了搞明白这个问题,我们需要了解一下C语言的执行过程。 C语言执行需要经过预处理(Preprocessing)、编译(Compilation)、汇编(Assemble)、链接(Linking)等几个阶段, 在编译成汇编语言这个阶段就已经没有变量名了,使用gdb可以查看编译后的汇编代码:

(gdb) disass main
Dump of assembler code for function main:
   0x0000000000400526 <+0>:    push   %rbp
   0x0000000000400527 <+1>:    mov    %rsp,%rbp
   0x000000000040052a <+4>:    sub    $0x20,%rsp
   0x000000000040052e <+8>:    mov    %edi,-0x14(%rbp)
   0x0000000000400531 <+11>:    mov    %rsi,-0x20(%rbp)
   0x0000000000400535 <+15>:    mov    0x200afd(%rip),%eax        # 0x601038 <a>
   0x000000000040053b <+21>:    add    $0x5,%eax
   0x000000000040053e <+24>:    mov    %eax,-0x4(%rbp)
   0x0000000000400541 <+27>:    mov    -0x4(%rbp),%eax
   0x0000000000400544 <+30>:    mov    %eax,%esi
   0x0000000000400546 <+32>:    mov    $0x4005e4,%edi
   0x000000000040054b <+37>:    mov    $0x0,%eax
   0x0000000000400550 <+42>:    callq  0x400400 <printf@plt>
=> 0x0000000000400555 <+47>:    mov    $0x0,%eax
   0x000000000040055a <+52>:    leaveq 
   0x000000000040055b <+53>:    retq   
End of assembler dump.

虽然上面这个很难读懂,但是应该能看到在这一大堆汇编指令执行的背后,并没有变量名这个东西, 所有的变量名到最后都变成了内存地址,汇编指令操作的是各种寄存器和内存地址。

定义int a;时,编译器分配4个字节内存,并命名该4个字节的空间名字为a(即变量名),当用到变量名a时,就是在使用那4个字节的内存空间。 5是一个常数,在程序编译时存放在代码的常量区存放着它的值(就是5),当执行a=5时,程序将5这个常量拷贝到a所在的4个字节空间中,就完成了赋值操作。 a是我们对那个整形变量的4个字节取的”名字”,是我们人为给的,实际上计算机并不存储a这个名字,只是我们编程时给那4个字节内存取个名字好用。 实际上程序在编译时,所有的a都转换为了那个地址空间了,编译成机器代码后,没有a这个说法了。 a这个名字只存在于我们编写的代码中。 5不是被随机分配的,而总是位于程序的数据段中,可能在不同的机器上在数据段中的位置可能不一致, 它的地址其实不能以我们常用到的内存地址来理解,因为牵扯到一个叫”计算机寻址方式”的问题。

有一点需要明白在操作系统里面,程序的内存地址并不是物理地址,而且通过一个基址+偏移量的方式的计算得到的虚拟地址, 操作系统为了更好的管理应用在内存这个层面做了很多抽象。

PHP里面的变量和变量名存储

PHP语句在执行的时候需要zend引擎进行词法分析,语法分析,编译成opcode,opcode可以理解为一种类似机器指令的语句,然后由zend引擎去执行。

<?php
$a = 1;
$b = 2;
 
function hello($d,$e)
{
    $c = $d+$e;
}
 
hello($a, $b);

看一下opcode结果:

testname@testname:~$ php7.0 -dvld.active=1 ~/index.php 
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename:       /home/testname/index.php
function name:  (null)
number of ops:  14
compiled vars:  !0 = $a, !1 = $b
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   EXT_STMT                                                 
         1        ASSIGN                                                   !0, 1
   3     2        EXT_STMT                                                 
         3        ASSIGN                                                   !1, 2
   5     4        EXT_STMT                                                 
         5        NOP                                                      
  10     6        EXT_STMT                                                 
         7        INIT_FCALL                                               'hello'
         8        EXT_FCALL_BEGIN                                          
         9        SEND_VAR                                                 !0
        10        SEND_VAR                                                 !1
        11        DO_FCALL                                      0          
        12        EXT_FCALL_END                                            
  11    13      > RETURN                                                   1
 
branch: #  0; line:     2-   11; sop:     0; eop:    13; out0:  -2
path #1: 0, 
Function hello:
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename:       /home/testname/index.php
function name:  hello
number of ops:  8
compiled vars:  !0 = $d, !1 = $e, !2 = $c
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   5     0  E >   EXT_NOP                                                  
         1        RECV                                             !0      
         2        RECV                                             !1      
   7     3        EXT_STMT                                                 
         4        ADD                                              ~3      !0, !1
         5        ASSIGN                                                   !2, ~3
   8     6        EXT_STMT                                                 
         7      > RETURN                                                   null
 
branch: #  0; line:     5-    8; sop:     0; eop:     7; out0:  -2
path #1: 0, 
End of function hello

zend引擎会把PHP代码转换成一组op命令操作,上面的就有2组操作。在第一组命令里面可以看到在开始的时候, 有一个compiled vars: !0 = $a, !1 = $b, 然后后面有2个ASSIGN操作。可以看到在最终执行的时候并不是使用的a、b, 而是使用了!0, !1这样的符号去代替。

!0, !1并不是一个固定的值,它每次执行的时候代表的是op命令的操作数。op命令是zend引擎自己定义好的一些操作,具体怎么执行得看zend引擎怎么处理了。

PHP的变量则是通过一个 _zval_struct 结构体形式存储的,讲道理,大部分时候还在存储在堆内存里面的, 既然存储在堆里面那么就必须手动释放内存,所以才有了自动垃圾回收机制!

最后总结一下,变量名说到底还是方便程序员编程的,名字起的好便于记忆和阅读代码。 变量名在代码运行的时候都会被一些特殊的符号代替,内存里面并不会有变量名,所以变量名写的长并不会影响运行速度,用中文还是英文也不影响。 而变量无论什么类型,最终运行的时候操作的还是内存地址里面数据,变量之所以有类型,是为了方便编译器处理。

指针和指针操作

C语言是比较偏底层的语言,指针可以对内存进行操作。

#include <stdio.h>
 
int main(int argc, char const *argv[])
{
    int a = 5;
    int *p;
    int **q;
    int ***m;
    
    p = &a;
    q = &p;
    m = &q;
    
    printf("a = %d\n",a);
    printf("p = %d\n",p);
    printf("*p = %d\n",*p);
    printf("q = %d\n",q);
    printf("*q = %d\n",*q);
    printf("**q = %d\n",**q);
    printf("m = %d\n",m);
    printf("*m = %d\n",*m);
    printf("**m = %d\n",**m);
    printf("***m = %d\n",***m);
    
    return 0;
}

输出结果:

a = 5
p = 6618636
*p = 5
q = 6618624
*q = 6618636
**q = 5
m = 6618616
*m = 6618624
**m = 6618636
***m = 5

***m = 5,先看成 *(*(*(m))),再从里向外看, 先从内存中拿到m存放的数据6618616(m中的数据,q的地址),完成了m操作, 再从内存中找到6618616那片内存,取出存放的数据6618624(q中的数据,p的地址),完成了*(m)操作, 再从内存中找到6618624那片内存,取出存放的数据6618636(p中的数据,a的地址),完成了*(*(m))操作, 再从内存中找到6618636那片内存,取出存放的数据5(a中的数据),完成了*(*(*(m)))操作, (几个*查找几层)。

另外说一下为什么有时候看到指针存储地址的空间大小默认是2字节?与编译器有关,16位就是2个字节,32位4个字节,64位8个字节。






参考资料

变量名存放在哪里? https://blog.csdn.net/weixin_33724659/article/details/88028054

C语言重点——指针篇(一篇让你完全搞懂指针) https://zhuanlan.zhihu.com/p/101934152

从汇编来看c语言之指针 https://www.cnblogs.com/stormpeach/p/4363646.html

C语言里面变量和变量名的存储(转) https://www.cnblogs.com/y4247464/p/12573434.html


返回