1. 程式人生 > >變數名存放在哪裡?

變數名存放在哪裡?

變數名存放在哪裡?

這是一個有意思且無聊的問題,之前在網上看到有人問道這個問題,比如說在PHP裡面我們寫下 $name = "名字" 這樣的程式碼語句,在程式碼執行的時候,$name 在哪裡呢? 瞭解了變數在記憶體中儲存方式的人會知道,一般變數的值在存放在棧記憶體裡面的,但是名字呢?

針對這個問題,咱們先要區分一下編譯型語言和解釋型語言,這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 <[email protected]>
=> 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引擎去執行。

有擴充套件可以打印出生成的opcode,下面看一下:

PHP程式碼:

<?php
$a = 1;
$b = 2;

function hello($d,$e)
{
    $c = $d+$e;
}

hello($a, $b);
複製程式碼

opcode結果:

[email protected]:~$ 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/jwang/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/jwang/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 結構體形式儲存的,講道理,大部分時候還在儲存在堆記憶體裡面的,既然儲存在堆裡面那麼就必須手動釋放記憶體,所以才有了自動垃圾回收機制!


所以,最後總結一下,變數名說到底還是方便程式設計師程式設計的,名字起的好便於記憶和閱讀程式碼,就像人一樣,名字只是一個代號,本質上只是一堆碳水化合物。變數名在程式碼執行的時候都會被一些特殊的符號代替,記憶體裡面並不會有變數名,所以變數名寫的長並不會影響執行速度,用中文還是英文也不影響。而變數無論什麼型別,最終執行的時候操作的還是記憶體地址裡面資料,變數之所以有型別,是為了方便編譯器處理。