目錄
一、內存和地址
(一)內存
(二)內存單元
(三)地址
(四)拓展:CPU與內存的聯系
二、指針變量和地址
(一)創建變量的本質
(二)取地址操作符:&
(三)指針變量和解引用操作符:*
1、指針變量
2、指針變量的理解
(1)【int* pa】的理解
(2)【int*】的理解
3、解引用操作符:*
(四)指針變量的大小
三、指針變量類型的意義
(一)解引用操作時,決定可以操作多少個字節
(二)指針 + -? 整數時,向前/向后走多大的區別
(三)void* 指針
四、const修飾指針
(一)const修飾變量
(二)const修飾指針變量
五、指針的運算
(一)指針 + 或 -? 整數
(二)指針 - 指針
(三)指針的關系運算(指針的比較)
六、野指針
(一)野指針造成的原因
1、指針未初始化
2、指針越界訪問
3、指針指向的空間釋放
(二)如何規避野指針
1、指針初始化
2、小心指針越界
3、指針變量不再使用時,及時置NULL,指針使用之前檢查有效性
4、避免返回局部變量的地址
七、assert斷言
八、指針的使用和傳址調用
(一)指針的使用:strlen的模擬實現
(二)傳值調用和傳址調用
一、內存和地址
(一)內存
????????又稱內存儲器或主存儲器,計算機中所有程序的運行都在內存中進行,計算機上CPU(中央處理器)在處理數據的時候,需要的數據是在內存中讀取的,處理后的數據也會放回內存中,這樣使用內存則需要高效地管理內存空間;
(二)內存單元
????????就是把內存劃分為一個個的內存單元,每個內存單元的大小取1個字節(8個比特位),每個內存單元都有?個編號,有了這個內存單元的編號,CPU就可以快速找到?個內存空間;
(三)地址
????????在計算機中我們把【內存單元的編號】也稱為【地址】,C語言中給【地址】起了新的名字叫:【指針】
? ? ? ? 可以理解為:【內存單元的編號 == 地址 == 指針】
(四)拓展:CPU與內存的聯系
????????有三條總線將CPU與內存連接彼此,交換數據:①地址總線;②數據總線;③控制總線
????????交換過程:地址信息通過【地址總線】被下達給內存,在內存上就可以找到相應的數據,將數據通過【數據總線】傳入CPU做處理,【控制總線】則負責傳遞對數據的操作,如讀操作、寫操作等
二、指針變量和地址
(一)創建變量的本質
????????創建變量的本質是在內存中申請空間,例如創建一個 int 變量就是向內存申請4個字節的空間,每個字節都有自己的編號(地址),變量的名字僅僅是給程序員看的,編譯器不看名字,編譯器是通過地址找內存單元的
(二)取地址操作符:&
?????????使用:拿到變量的地址
????????例如:
int a = 10;&a;
? ? ? ? &a 就可以拿到變量a的地址,雖然整型變量占用4個字節,我們只要知道了第?個字節地址,春藤摸瓜訪問到4個字節的數據也是可行的
? ? ? ? 注:當一個變量占多個內存單元的時候,總會取出該變量的第一個內存單元(地址較小的那個字節)
(三)指針變量和解引用操作符:*
1、指針變量
????????通過取地址操作符(&)拿到的地址是?個數值,比如:0x0012ff40,這個數值有時候也是需要存儲起來,方便后期再使用的,那我們把這樣的地址值存放在哪里呢?答案是:指針變量中,例如:
#include <stdio.h>
int main()
{int a = 10;int * pa = &a;//取出a的地址并存儲到指針變量pa中return 0;
}
????????指針變量也是?種變量,這種變量就是?來存放地址的,存放在指針變量中的值都會理解為地址
2、指針變量的理解
? ? ? ? 上面例子的寫法中的 int *pa 拆開來理解:
(1)【int* pa】的理解
? ? ? ? ①【int *】是變量pa的類型;
? ? ? ? ② pa是一個變量,用來存放地址(指針)的,所以pa又叫指針變量
(2)【int*】的理解
? ? ? ? ① * 表示pa是指針變量;
????????② int 表示【pa 指針變量中保存的地址】所指向的【變量 a】的類型是int
3、解引用操作符:*
? ? ? ? 又稱為間接訪問操作符,用法:
int main()
{int a = 100;int* pa = &a;*pa = 0;此處*pa == a,相當于對a進行修改return 0;
}
? ? ? ? 總結:通過【指針變量pa】找到指向的變量a—— *pa(通過pa的值,找到a)
? ? ? ? ① pa —— 指針變量
? ? ? ? ② &pa —— 指針變量pa的地址
? ? ? ? ③ *pa —— pa指向的變量a
(四)指針變量的大小
? ? ? ? 【指針變量類型的大小】取決于【地址的大小】,而地址大小由計算機是32位操作系統還是64位操作系統決定
????????① 指針變量是用來存放地址的,一個地址的存放需要多大空間,那么指針變量類型就是多大,所以32位平臺總共有32根地址總線,每根線的電信號轉化成數字信號后是1或0,那我們把32根地址總線產生的2進制序列作為一個地址,那么一個地址就是32個比特位,就是4個字節;同理,在64位的機器中,一個地址的大小就是8字節
????????② 地址的大小與【指向的原變量的類型大小】無關,就是4字節或者8字節
#include <stdio.h>//指針變量的??取決于地址的??
//32位平臺下地址是32個bit位(即4個字節)
//64位平臺下地址是64個bit位(即8個字節)int main()
{printf("%zd\n", sizeof(char *));printf("%zd\n", sizeof(short *));printf("%zd\n", sizeof(int *));printf("%zd\n", sizeof(double *));return 0;
}
? ? ? ? X86環境輸出結果如下:
? ? ? ? X64環境輸出結果如下:
????????結論:
? 32位平臺下地址是32個bit位,指針變量大小是4個字節
? 64位平臺下地址是64個bit位,指針變量大小是8個字節
? 指針變量的大小和類型是無關的,只要指針類型的變量,在相同的平臺下,大小都是相同的
三、指針變量類型的意義
????????指針變量的大小和類型無關,只要是指針變量,在同?個平臺下,大小都是?樣的,都是4字節或者8字節,為什么還要有各種各樣的指針類型呢?
(一)解引用操作時,決定可以操作多少個字節
? ? ? ? 如下演示:
#include <stdio.h>
int main()
{int a = 0x11223344;int* p = &a;*p = 0;return 0;
}
? ? ? ? 變量a的地址與4個字節的值如下:
? ? ? ? 經過 *p = 0;的語句后,4個字節的值全部改為0,如下:
????????若代碼中指針變量的類型改為char*:
#include <stdio.h>
int main()
{int a = 0x11223344;char* p = &a;*p = 0;return 0;
}
????????變量a的地址與4個字節的值如下:
????????經過 *p = 0;的語句后,4個字節的值只有一個字節改為0,如下:
? ? ??
????????結論:指針的類型決定了,解引用操作時,決定可以操作多少個字節
????????比如: char* 的指針解引用就只能訪問一個字節,而?int* 的指針的解引用就能訪問四個字節
(二)指針 + -? 整數時,向前/向后走多大的區別
? ? ? ? 如下代碼演示:
#include <stdio.h>
int main()
{int n = 10;char *pc = (char*)&n;int *pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc+1);printf("%p\n", pi);printf("%p\n", pi+1);return 0;
}
? ? ? ? 代碼結果如下:
? ? ? ? 從結果可以得出:char* 類型的指針變量+1跳過1個字節, int* 類型的指針變量+1跳過了4個字節;
????????結論:指針的類型決定了指針向前或者向后走一步有多大(距離)? ?????????
?補充:
????????int* pa; ??
????????pa+1——> +1 * sizeof (int)
????????pa+n——> +n * sizeof (int)????????char* pa; ??
????????pa+1——> +1 * sizeof (char)
????????pa+n——> +n * sizeof (char)總結:
? ? ? ? 類型* 變量名;
? ? ? ? 變量名 + 1 ——> +1 * sizeof(指針指向的變量類型)
(三)void* 指針
????????void* ——無具體類型的指針(泛型指針)
????????可以接收任何類型的地址,但是正因為他是泛型指針,所以沒有特定類型指針的用法,即無法解引用和進行指針的 + - 操作;
????????作用:?般 void* 類型的指針是使用在函數參數的部分,用來接收不同類型數據的地址,這樣的設計可以實現泛型編程的效果,使得?個函數來處理多種類型的數據
四、const修飾指針
(一)const修飾變量
????????const修飾變量的時候,叫:常變量;
????????本質還是變量,只是不能被修改;
????????變量是可以修改的,如果把變量的地址交給?個指針變量,通過指針變量的也可以修改這個變量,若不想變量被直接修改,就使用const修飾變量起限制作用
#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的const int n = 0;n = 20;//n是不能被修改的return 0;
}
????????上述代碼中n是不能被修改的,其實n本質是變量(無法在數組長度中使用),只不過被const修飾后,在語法上加了限制,只要我們在代碼中對n進行修改,就不符合語法規則,就報錯,致使沒法直接修改n
? ? ? ? 但是可以拿到n的地址,通過指針對它進行修改,但這是在打破語法規則
int main()
{const int n = 0;printf("n = %d\n", n);int*p = &n;*p = 20;printf("n = %d\n", n);return 0;
}
? ? ? ? 結果如下:
? ? ? ? 這里的初衷是不讓變量改變,但是通過指針還是能打破const的限制,接下來就要對這一象限改進,直接對指針變量做const限制
(二)const修飾指針變量
?????????般來講const修飾指針變量,可以放在 * 的左邊,也可以放在 * 的右邊,意義是不?樣的
int * p;//沒有const修飾
int const * p;//const 放在*的左邊做修飾
int * const p;//const 放在*的右邊做修飾
? ? ? ? 如下代碼演示:
? ? ? ? 代碼一:
int a = 10;
int b = 20;
int const * p = &a;*p = 200;err
p = &b;√
? ? ? ? 代碼一分析:
????????這個const限制的是 *p,即p指向的變量a不能改變;但是并沒有限制p,所以可以修改p所指向的變量;
????????放在*的左邊,限制的是指針指向的內容,也就是不能通過指針變量來修改它所指的內容;但是指針變量本身可以改變的
? ? ? ? 代碼二:
int a = 10;
int b = 20;
int * const p = &a;*p = 200;√
p = &b;err
? ? ? ? 代碼二分析:
????????放在*的右邊,限制的是指針變量本身,也就是指針變量本身不可以改變,但可以通過指針變量來修改它所指的內容
????????結論:const修飾指針變量的時候
const如果放在 * 的左邊,修飾的是【指針指向的內容 *p】,保證指針指向的內容不能通過指針來改變,但是【指針變量本身 p】的內容可變;
const如果放在*的右邊,修飾的是【指針變量本身 p】,保證了指針變量的地址指向不能修改,但是【指針指向的內容*p】,可以通過指針改變
五、指針的運算
????????指針的基本運算有三種,分別是:
????????? 指針 + 或 -? 整數
????????? 指針 - 指針
????????? 指針的關系運算(指針的比較)
(一)指針 + 或 -? 整數
????????因為數組在內存中是連續存放的,只要知道第?個元素的地址,順騰摸瓜就能找到后?的所有元素
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
#include <stdio.h>
//指針+- 整數
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int sz = sizeof(arr)/sizeof(arr[0]);for(int i = 0; i < sz; i++){printf("%d ", *(p+i));//p+i 這?就是指針+整數}return 0;
}
????????注意:指針運算是指對 p 進行運算,而不是對*p,若對 *p 運算,就是對變量a運算了
????????在數組中,指針能夠“順騰摸瓜”的原因是:
????????①指針類型決定了【指針+1】的步長,和指針解引用之后的權限;
????????②數組在內存中的地址是連續的
? ? ? ? 錯誤演示代碼:
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};char *p = &arr[0];int sz = sizeof(arr)/sizeof(arr[0]);for(int i = 0; i < sz; i++){printf("%d ", *p);p += 4;}return 0;
}
? ? ? ? 代碼分析:
????????每次打印時,都讓p += 4,在打印1~10時恰好正確,
? ? ? ? 每次訪問都只會訪問第一個字節,后面三個字節是直接跳過的,所以兩位數的時候是正確的,但是數字大一些就會忽略掉第二個字節的數字,就會出錯
(二)指針 - 指針
? ? ? ? 【指針 - 指針】的運算前提條件是兩個指針指向的是同一個空間,否則運算無意義;
????????指針 - 指針的【絕對值】,是指針和指針之間【元素的個數】
????????應用:求字符串長度 ,如下代碼演示:
#include <stdio.h>int my_strlen(char *s)
{char *p = s;//設置尾指針while(*p != '\0' )p++;return p-s;
}int main()
{printf("%d\n", my_strlen("abc"));return 0;
}
????????拓展:指針 + 指針?
????????答:無意義,類似于 【日期 +- 天數(計算日期)】、【日期 - 日期(算的是兩個日期之間差多少天)】有意義,而【日期 + 日期】無意義
(三)指針的關系運算(指針的比較)
????????應用:做判斷條件使用,數組中,若一個地址小于另一個地址,則執行語句
#include <stdio.h>int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int sz = sizeof(arr)/sizeof(arr[0]);while(p<arr+sz) //指針的???較{printf("%d ", *p);p++;}return 0;
}
六、野指針
????????概念: 野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
(一)野指針造成的原因
1、指針未初始化
????????指針變量也是局部變量,不初始化就會給隨機值;
????????如果將未初始化的指針變量的值作為地址來進行解引用操作,就會形成非法訪問
#include <stdio.h>int main()
{ int *p;//局部變量指針未初始化,默認為隨機值*p = 20;return 0;
}
2、指針越界訪問
#include <stdio.h>int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i=0; i<=11; i++){//當指針指向的范圍超出數組arr的范圍時,p就是野指針*(p++) = i;}return 0;
}
3、指針指向的空間釋放
#include <stdio.h>int* test()
{int n = 100;return &n;
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}
(二)如何規避野指針
1、指針初始化
????????如果明確知道指針指向哪里就直接賦值地址,如果不知道指針應該指向哪里,可以給指針賦值NULL,NULL 是C語言中定義的?個標識符常量,值是0(這個0在C語言中會被強制轉化為void*類型),0也是地址,這個地址是無法使用的,讀寫該地址會報錯
? ? ? ? 演示代碼如下:
#include <stdio.h>
int main()
{int num = 10;int* p1 = #int* p2 = NULL;return 0;
}
2、小心指針越界
?????????個程序向內存申請了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是越界訪問
3、指針變量不再使用時,及時置NULL,指針使用之前檢查有效性
????????當指針變量指向?塊區域的時候,我們可以通過指針訪問該區域,后期不再使用這個指針訪問空間的時候,我們可以把該指針置為NULL;因為約定俗成的?個規則就是:只要是NULL指針就不去訪問,同時使用指針之前可以判斷指針是否為NULL
? ? ? ? 演示代碼如下:
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];for(int i = 0; i<10; i++){*(p++) = i;}//此時p已經越界了,可以把p置為NULLp = NULL;//下次使?的時候,判斷p不為NULL的時候再使?//...p = &arr[0];//重新讓p獲得地址if(p != NULL) //判斷{//...}return 0;
}
4、避免返回局部變量的地址
????????不要返回局部變量的地址
七、assert斷言
????????
????????assert.h 頭文件定義了宏 assert ( ) ,用于在運行時確保程序符合指定條件,如果不符合,就報錯終止運行,這個宏常常被稱為“斷言”
????????使用:#include <assert.h>;assert(表達式)
????????作用:判斷是否符合指定條件,如果不符合就會終止運行;【通常用來判斷指針變量的有效性】
????????判斷:判斷為真則程序繼續向下走,判斷為假則報錯
int* p = NULL;
...
assert(p != NULL);
此處經過一些列的代碼后,若 p 不等于NULL則正常運行下去,若還是等于NULL,則程序報錯,終止運行
????????若想取消assert斷言,則在#include <assert.h>上面 #define NDEBUG;
????????assert斷言只在Debug版本中有效,在Release版本中會被優化掉
? ? ? ? 缺點:引入了額外的檢查,增加了程序的運行時間
八、指針的使用和傳址調用
(一)指針的使用:strlen的模擬實現
????????庫函數strlen的功能是求字符串?度,統計的是字符串中 \0 之前的字符的個數
????????函數原型如下:
size_t strlen ( const char * str );
????????參數str接收?個字符串的起始地址,然后開始統計字符串中 \0 之前的字符個數,最終返回長度;
????????如果要模擬實現只要從起始地址開始向后逐個字符的遍歷,只要不是 \0 字符,計數器就+1,這樣直到 \0 就停止,代碼如下:
size_t my_strlen(const char * str)
{int count = 0;assert(str);//為了保險,判斷傳來的是不是空地址while(*str){count++;str++;}return count;
}int main()
{size_t len = my_strlen("abcdef");printf("%zd\n", len);return 0;
}
????????注:代碼中的 const(不希望原值被修改)和 assert(保險判斷)來加強代碼使用時的健壯性(魯棒性)
?
(二)傳值調用和傳址調用
????????傳址調用,可以讓函數和主調函數之間建立真正的聯系,在函數內部可以修改主調函數中的變量;所以未來函數中只是需要主調函數中的變量值來實現計算,就可以采用傳值調用,如果函數內部要修改主調函數中的變量的值,就需要傳址調用
? ? ? ? 以上內容僅供分析,若有錯誤,請多多指正