日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

什么是指針?先看看什么是內存地址

首先,我們要搞清楚數據結構在計算機里面到底怎么存取?怎么描述它們。

任何數據結構(struct)以及組成數據結構的基本數據類型,一旦分配了內存空間,那么就會有兩個部分來描述這塊內存:內存的地址(紅色部分,不占用實際空間,相當于門牌號,用于尋址)與內存的值(綠色部分,是實際的信息存儲部分,占用內存空間,以byte為單位)。就像下面這張圖:

數據在內存中的結構

所以一塊內存,或者一個符號(編程語言的符號其實就代表了一塊內存,所以它們代表同一個意思)有兩個重要的屬性:

 

  1. 內存或者符號的地址
  2. 內存或者符號的值

 

這兩個屬性如同一個事物的兩面,不可分割,形影不離。

有時候,如果對事情的本質進行深挖的話,你可能對一些基本概念有更深刻的理解。比如,到這里,如果你理解了內存或者編程語言的符號有兩個基本的屬性:地址與值,那么你就可以理解C/C++中的&與=操作符的含義。

 

  • &作用在一個符號上的底層含義就是——獲取這個符號的兩個重要屬性之一——符號的地址
  • =作用在一個符號上的底層含義就是——獲取這個符號的兩個重要屬性之一——符號的內存值。int a=1;含義就是獲取符號a的內存值,并將內存值賦值成1。

 

可以推斷出,從CPU的角度,或者編程語言底層來看,沒有數據類型的概念,任何數據都是一塊塊連續的、長短不一的內存存儲單元而已,就像上圖所畫。那么問題就變成了,怎么描述這塊內存呢?

答案是:內存的起始地址+長度。比如下面這個結構:

struct Test {int a;short b;

對于test這個結構,怎么描述它?

答案是:struct test是——符號a的內存地址+6個字節長度的數據塊,如果要讀取或者寫入test某個部分(a或者b),編譯器至少要編譯兩條指令:1、獲取test也就是a符號的地址,2、根據類型定位偏移量就行了。這就是數據結構的本質了。

那么對數據結構成員變量的訪問就很容易理解了:

 

  • test.a就可以被編譯成符號a的地址+向高地址取4個字節的內存塊。
  • test.b就可以看成符號a的地址向高地址偏移4個字節+向高地址取2個字節的內存塊。

 

是不是有點類似數學中的極坐標系的概念。而實際上系統確實是這么做的。

站在編譯器的角度看看符號與變量

指針在C與C++中很難理解,但是又是重要的構成部分,沒有了指針其實就發揮不出語言的光芒了。因為指針是很自然的事物,它是承接CPU取址與程序可讀性的關鍵概念,理解了它就既能看穿機器的運行,又能寫出合理的優雅的代碼去描述業務。

要真正理解指針或者更普遍的意義來說,理解符號,就得將自己想象成編譯器去讀代碼,這樣一切都會變得理所當然的容易起來。

我們看到的程序都是由變量符號組成的,本質上符號代表一塊內存,比如上面的結構體就有三個變量符號或者簡稱符號:test,a,b。每個符號其實都對應一塊型如下圖的內存塊:

內存塊的兩個維度

再來看看這個代碼片段

typedef struct test {int a;short b;} Test;Test t;t.a =1;t.b =2;Test* t_ptr = &t;t_ptr->a = 3;

  • Test t:如果“我”是編譯器,看到這行代碼,我獲得的信息是:t是一個符號,它有兩個維度的信息:1、地址是&t;2、長度是sizeof(Test) = 6(不考慮對齊)。而且我會自動補全表達式為Test t = 0初始化t代表的這塊內存。生成的底層代碼應該做這些:1、給符號t分配一個內存地址,地址一共6個byte長度;2、將這6個byte的地址的值都填充為0。
  • t.a = 1:語義是給符號a的值,賦值1。符號a的地址就是t的地址,符號a的長度4個字節,=的含義就是獲取a的內存值,最后將int 1填充到這4個字節的內存中,完成賦值。
  • t.b = 2:語義是給符號b的值,賦值2。符號b的地址是t的地址往高處偏移a的長度;同時符號b的值的長度是2個字節,=就是獲取b的內存值,最后將short 2填充到這2個字節的內存中,完成賦值。
  • 再看看復雜點的Test* t_ptr = &t;:t_ptr是個符號,它有地址與值兩個屬性。Test*修飾部分用來描述t_ptr的長度,這里的Test*說明 t_ptr的值是一塊內存的起始地址,長度為8字節(x64平臺)。這就是按照編譯器的角度解釋指針:不管*號前面是什么類型(task_struct* ptr還是int* ptr還是char* ptr)被修飾的符號長度永遠就是8個字節的地址。而t_ptr = &t就是將符號t_ptr的值賦值成符號t的地址——就是t_ptr這個符號的地址開始連續8個byte的內存填入符號t的地址值。
  • 看了指針的本質,我們來看看對指針的操作指令:t_ptr->a = 3。=的本質是獲取符號a的值進行賦值;而找到a的地址比較復雜,先要拿到t_ptr符號的值,也就是&t,然后在t的地址基礎根據a的偏移找到a的地址,這里偏移是0,a的地址等于t的地址。然后根據a的類型int將int 3賦值到這4個字節上就行了。
到這里我們可以推敲一下指針的本質了

 

接著上面的例子,我們已經分析了t_ptr的內存布局,它的值是一個地址。問題就來了,你想過沒有,如果一個符號,它的值保存了一個地址,我對他能做什么操作?我們知道,如果t_ptr的值是int、long,我就能用CPU的算術模塊對它們進行“加減乘除”,這樣是有意義的,因為我在做代數運算。那么對一個地址,顯然,做加減乘除運算是沒有意義的。我們唯一能對地址做的有意義的操作就是找到這塊地址,對這個地址對應的內存進行操作,這才是地址類型數據的意義。

因為對地址進行普通意義上的四則運算是沒有代數意義的,所以,C語言為地址數據類型(指針)增加了兩個操作符*與->。

 

  • *就是切換符號的含義,如*ptr = 3那么=獲取的內存值,并不是ptr這個符號本身的值,而是ptr的值所對應的內存地址的內存值。相當于將符號ptr的含義進行了切換,切換到了新的目標內存地址;
  • ->也是切換符號含義,但是不是切換到ptr指向的大內存塊,而是里面的小內存塊,可以理解成切換成對成員變量的符號的內存訪問。
看看linux中一些指針操作——二重指針

 

看個Linux內核中的例子,這是mcs spinlock的加鎖操作

static inlinevoid mcs_spin_lock(struct mcs_spinlock **lock, struct mcs_spinlock *node)struct mcs_spinlock *prev;/* Init node */node->locked = 0;node->next = NULL;prev = xchg(lock, node); //相當于把mcslock.next = node;同時返回*lock修改之前的值。if (likely(prev == NULL)) { //原來*lock指向NULL。也就是現在鏈表還沒形成,沒有競爭。return;} // 如果有值說明有競爭,要排隊。所以直接插入最后就行了。prev就是最后一個元素。WRITE_ONCE(prev->next, node);/*這里是個spin loop。在percpu自身的lock上面自旋,等待變成1,獲取鎖。/* Wait until the lock holder passes the lock down. */arch_mcs_spin_lock_contended(&node->locked);

  • struct mcs_spinlock **lock是什么呢?書上會說指向指針的指針,這么說沒錯,但是對很多剛剛接觸到C語言的人來說其實很難理解,很難對應到實際內存中的樣子。不如,一步步拆解這條語句的含義。
    • 首先,我們要先找到符號,符號才是內存,才有意義。顯然這里描述的符號是lock;
    • 符號就是一塊內存,是內存就有地址與值,那么lock的值的信息可以從它的類型來推斷出來,struct mcs_spinlock **就是類型信息,它的主要作用是描述lock的值有多長。那么有多長呢?看到*就不用看struct mcs_spinlock了,就是一個地址,另一個符號的地址,8個字節長,保存在了lock符號的值里面。
    • struct mcs_spinlock **的含義是什么呢?這是一個遞歸定義。根據前面對*運算符的解釋,struct mcs_spinlock **可以展開成這種形式(struct mcs_spinlock *)(*lock)。*lock的含義是切換符號,假設切換成了(struct mcs_spinlock *)_lock符號,_lock符號的地址是lock的值,而_lock的值又是一個地址,一個struct mcs_spinlock *類型的地址。如下圖:

 

指針的指針到底是什么?

 

  • 而*lock = node;的含義就是:將符號node的值賦給*lock符號的值。

 

*就表示切換符號

 

  • 所以,不管多少個*都可以遞歸化簡了。
  • 比如:T *** t = &node;這個表達式:

 

圖解三重指針

看看Linux中一些指針操作——鏈表操作//鏈表頭指針struct wake_q_head {struct wake_q_node *first;struct wake_q_node **lastp;struct wake_q_node {struct wake_q_node *next;//初始化鏈表頭static inline void wake_q_init(struct wake_q_head *head)head->first = WAKE_Q_TAIL; // #define WAKE_Q_TAIL ((struct wake_q_node *) 0x01)head->lastp = &head->first;//添加新元素static bool __wake_q_add(struct wake_q_head *head, struct task_struct *task)struct wake_q_node *node = &task->wake_q;* Atomically grab the task, if ->wake_q is !nil already it means* it's already queued (either by us or someone else) and will get the* wakeup due to that.* In order to ensure that a pending wakeup will observe our pending* state, even in the failed case, an explicit smp_mb() must be used.smp_mb__before_atomic();if (unlikely(cmpxchg_relaxed(&node->next, NULL, WAKE_Q_TAIL)))return false;* The head is context local, there can be no concurrency.*head->lastp = node;head->lastp = &node->next;return true;

wake_q_init

添加第一個元素

 

  • first指向task_struct中的成員wake_q;指向隊列的第一個元素;
  • lastp指向task_struct的成員wake_q的成員next;next也是一個指針;

 

再添加一個元素:

再添加一個元素

 

  • first始終指向第一個元素;
  • lastp始終指向最后一個元素的next符號。
關于指針你只要記住
  • 看到表達式先找到符號;
  • 符號就等同于內存空間;
  • 符號有地址與值兩個維度的屬性,腦子中畫一個上面的圖來幫助理解;
  • *、->這兩個操作符的本質就是從一個符號切換到另一個符號,從一塊內存切換到另一塊;
  • &與=這兩個操作符號的本質就是獲取內存值。
理解了指針就能更進一步理解內存對齊了

 

內存對齊應該叫做內存的虛擬地址對齊,講的是如果我們為一個數據結構——抽象來講就是一塊內存——分配一個地址的時候,需要滿足的規則。那么規則有哪些?我們可以先列出來:

 

  • 基本數據類型(int,short,char,byte,long,float,double)的變量的首字節地址必須是類型字節數的整數倍;
  • 結構體(首字節地址)必須是最大成員變量數據類型的整數倍(編譯器維護);
  • 結構體中每個成員變量的首字節地址,必須是成員類型的整數倍,如果不是,則編譯器填充實現;
  • 結構體的總體長度必須是最大成員變量類型長度的整數倍,如果不是,編譯器在結構體最后一個字節末尾填充0實現。

 

下面具體說明下這些規則都是什么意思。

基本數據類型的首地址必須是類型字節數的整數倍

還是這個代碼片段:

#include#includetypedef struct test {short b;int a;} Test;int main(){printf("Test size is:%ldn",sizeof(Test));Test* t_ptr = (Test*)malloc(sizeof(Test));t_ptr->a = 1;t_ptr->b = 2;printf("a is:%dn",t_ptr->a);printf("b is:%dn",t_ptr->b);

為了迎合這個問題,我們調換了a,b符號在結構體中出現的順序。之前我們假設sizeof(Test)是6,但是真的如此嗎?我們看看運行的結果:

Test size is:8a is:1b is:2

其實是8個字節。為啥呢?就是因為int a要符合基本數據類型的首地址必須是類型字節數的整數倍這條規則,所以編譯器會在b與a之間插入2個字節的0,使得a的首字節地址是int的整數倍;變成:

typedef struct test {short b;short invisible_padding; //實際看不見int a;} Test;

反匯編驗證下:

t_ptr->a = 1;119c: 48 8b 45 f8 mov -0x8(%rbp),%rax /*拿到符號t_ptr的地址*/11a0: c7 40 04 01 00 00 00 movl $0x1,0x4(%rax) /*執行 = 操作符,給符號a的內存賦值*/t_ptr->b = 2;11a7: 48 8b 45 f8 mov -0x8(%rbp),%rax /*拿到符號t_ptr的地址*/11ab: 66 c7 00 02 00 movw $0x2,(%rax) /*執行 = 操作符,給符號b的內存賦值*/

可以從反匯編看到a的內存地址從偏移地址0x4開始,而b從偏移地址0x2開始,而padding是放在t_ptr的開始位置的,這跟我的猜想有點出入,但是并不破壞規則,因為int a的首字節地址依然變成了4的整數倍。如下圖:

反匯編1

那么問題就來了,為什么要填充呢?本質的原因是什么?

從CPU角度看看為什么要對齊

一圖勝千言:

CPU角度看內存加載問題

解釋:

 

  • 內存的訪問真的沒有程序員想的那么簡單,而是分組讀取的,也就是總線32位寬,其實不是連續的,而是分成了4組,每組讀取1個字節,然后拼成一個雙字的數據塊;x64就分成8個組;
  • 可以把組看成一個通道,CPU可以一次激活最多4個(32位)或者8個(64位)通道,一次讀取可以看成一個transaction;
  • 每個通道一次讀取一個字節的數據;
  • 每個通道讀取的地址是有規律的,比如1號通道(0,4,8,12,16…)二號通道(1,5,9,13,…)依次類推;
  • 數據讀取性能跟所需的transaction數量相關,越少性能越高;
  • 根據以上的事實,內存對齊的定義其實就是——讓數據結構的首字節地址始終在通道1上就是對齊的數據,否則,就不是;
  • 符號首地址是n字節對齊的含義是:**符號首字節地址是n字節的倍數。**比如,下圖,int a就是4字節對齊的,第二個int a’是6字節對齊的。
  • 數據不對齊會比對齊的數據,在訪問時,多1次內存的開銷。

 

一圖勝千言,上圖:

對齊的與沒有對齊的內存讀取差別

所以,內存必須對齊,不然同樣的數據結構,沒對齊比對齊后的內存要多一次內存的開銷。

不要小看這一次內存訪問的開銷,因為:

 

  • CPU可以說每時每刻都在以超高并發量訪問內存,假如1秒1千次的內存訪問,如果都多一次,一秒就是2千次,性能會下降50%。
  • 根據性能金字塔,內存的訪問可是在底層,延遲是很大的,所以在CPU這種高并發的場景下,特別是多核的SMP系統,性能問題就會更加嚴重。
關于結構體的三條規則
  1. 結構體(首字節地址)必須是最大成員變量數據類型的整數倍(編譯器維護);
  2. 結構體中每個成員變量的首字節地址,必須是成員類型的整數倍,如果不是,則編譯器填充實現;
  3. 結構體的總體長度必須是最大成員變量類型長度的整數倍,如果不是,編譯器在結構體最后一個字節末尾填充0實現。

 

其中2.就是基本數據類型的首地址必須是類型字節數的整數倍的推論,或者說是等價的,不需要證明。

關于1.與3.的證明,需要引入一個推論:如果符號的首地址是n字節對齊的,那么一定是n/2對齊的,也一定是n/4對齊的,依次類推下去。

舉個例子來說就是:符號a的首地址如果是8字節對齊,那么一定也是4字節對齊,一定也是2字節對齊的。其實很容易證明:如果a的地址是x,x%8 = 0;那么x = b×8;x%4 = b×4×2 %4=0;所以也是4字節對齊的。

結構體(首字節地址)必須是最大成員變量數據類型的整數倍的證明

可以由2.推導而來。步驟是:

1、假設結構體中的最大成員變量的長度是long8個字節;那么,根據2.可知,這個變量前面的變量的長度總和,必須是8的整數倍;

2、所以,如果結構體的首字節地址不是8的整數倍,那么就算最大成員變量之前的所有成員變量長度和滿足了是8的整數倍,也不能保證2.的成立;

3、所以結構體的首字節地址必定是最大成員長度的整數倍,也就是8字節對齊的。

內存布局

結構體的總體長度必須是最大成員變量類型長度的整數倍證明

這個主要是考慮數組的情況。在單個結構體中對齊的數據,必須在數組情況下也是對齊的,如果最大的成員變量是對齊的,則所有其他成員變量都是對齊的。證明如下圖:

證明

以一個例子總結#include#includetypedef struct testint a; // 4// padding 4long long b; // 8 b要8字節對齊,也就是前面的字節數要是8的倍數,而前面只有4字節,所以要padding4個字節char c; // 1// padding 1short d; // 2 同樣d前面的字節數要是2的倍數,所以padding 1個字節// padding 4 前面整體的test結構只有20個字節,而整體的大小也要是最大元素的整數倍,因為如果是數組,那么兩個my的元素那么第二個my的b變量位于28的位置,不是8的整數倍,所以結尾再padding4個字節。湊成24個字節。} My;int main(int argc, char *argv[])//棧上分配My my;my.a = 1;11ab: c7 45 e0 01 00 00 00 movl $0x1,-0x20(%rbp)my.b = 2L;11b2: 48 c7 45 e8 02 00 00 movq $0x2,-0x18(%rbp)11b9: 00my.c = 'a';11ba: c6 45 f0 61 movb $0x61,-0x10(%rbp)my.d = 4;11be: 66 c7 45 f2 04 00 movw $0x4,-0xe(%rbp)My my;my.a = 1;my.b = 2L;my.c = 'a';my.d = 4;printf("size of my:%dn", sizeof(My));printf("address of my is:%xn", &my);//堆上分配11f8: bf 18 00 00 00 mov $0x18,%edi11fd: e8 8e fe ff ff call 10901202: 48 89 45 c0 mov %rax,-0x40(%rbp)my_on_heap->a = 1;1206: 48 8b 45 c0 mov -0x40(%rbp),%rax120a: c7 00 01 00 00 00 movl $0x1,(%rax)my_on_heap->b = 2L;1210: 48 8b 45 c0 mov -0x40(%rbp),%rax1214: 48 c7 40 08 02 00 00 movq $0x2,0x8(%rax)121b: 00my_on_heap->c = 'a';121c: 48 8b 45 c0 mov -0x40(%rbp),%rax1220: c6 40 10 61 movb $0x61,0x10(%rax)my_on_heap->d = 4;1224: 48 8b 45 c0 mov -0x40(%rbp),%rax1228: 66 c7 40 12 04 00 movw $0x4,0x12(%rax)My* my_on_heap = (My*)malloc(sizeof(My));my_on_heap->a = 1;my_on_heap->b = 2L;my_on_heap->c = 'a';my_on_heap->d = 4;printf("address of my is:%xn", my_on_heap);參考

視頻——內存對齊到底是個什么鬼

分享到:
標簽:指針
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定