5. 指針運算
指針的基本運算有三種,分別是:
指針+-整數
指針-指針
指針的關系運算
5.1 指針運算
在上面,我們知道,數組在內存中是連續存放的,只要知道第一個元素的地址,順藤摸瓜就能找到后面的所有元素。
那么,運用這一點,我們就可以寫出下面的代碼:
#include <stdio.h>int main()
{int i;int arr[] = {1,2,3,4,5,6,7,8,9};int sz = sizeof(arr)/ sizeof(arr[0]);for(i = 0;i < sz;i++){printf("%d ",*(arr + i));}return 0;
}
我們利用指針 arr (數組名即為數組首元素的地址)?+ i?,訪問數組中下標為 i 的元素,并打印出來。而指針與整數運算后跳過的字節數的大小是與數據的類型有關的。例如,上面代碼中, arr 數組是整型數組,所以在運算時,會在 arr 的位置,跳過4 * i 個字節,訪問到數組中下標為 i 的元素。
5.2 指針 -?指針
上面,我們知道指針可以和整數進行加減運算,那指針是否可以與指針進行加減運算呢?
#include <stdio.h>int main()
{int arr[] = {1,2,3,4,5,6,7,8,9};int a = (arr + 9) - (arr + 3);//正常運行int b = (arr + 9) + (arr + 3);//編譯器報錯:Invalid operands to binary expression ('int *' and 'int *')printf("%d",a);return 0;
}
將指針加法的那一行代碼刪去后,我們得到了如下輸出:
6
進程已結束,退出代碼為 0
輸出結果為6,這代表了(arr + 9)與(arr + 3)兩個指針之間一共有6個元素。因此,指針的減法運算所得到的結果就是兩個地址之間的元素個數。
利用這一點,我們可以自己寫出類似于函數 strlen()的效果的代碼:
int my_strlen(char* s)
{char *p = s;while(*p != '\0'){p++;}return p - s;
}
#include <stdio.h>
int main()
{char s1[] = "asdf";int a = my_strlen(s1);printf("%d",a);return 0;
}//輸出結果
4
我們發現,輸出結果為4,正等于字符數組中的字符數。
5.3?指針的關系運算
我們知道,指針就是地址,而地址有高低之分,那指針是否可以比較大小呢?
#include <stdio.h>int main()
{int arr[] = {1,2,3,4,5,6,7,8,9};int sz = sizeof(arr)/ sizeof(arr[0]);int *p = &arr[0];while(p < arr + sz){printf("%d ",*p);p++;}return 0;
}//輸出結果
1 2 3 4 5 6 7 8 9
我們可以發現,循環正常進行,說明表達式是合法有效的,指針可以用來進行比較大小。
6.?野指針
概念:野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
6.1 產生野指針的原因
6.1.1.?指針未初始化
#include <stdio.h>int main()
{int* p ;*p = 0;//編譯器警告:Variable 'p' is uninitialized when used herereturn 0;
}
指針未初始化時,默認為隨機值。直接使用可能導致系統報錯。
6.1.2?指針越界訪問
這種錯誤可以類比數組訪問越界:
#include <stdio.h>int main()
{int i;int arr[10] = {0};int* p = &arr[0];for(i = 0;i < 11;i++){*p = i;p++;}return 0;
}
在這個代碼中,當指針指向的范圍超出數組范圍時,p就會成為野指針,執行預期外的操作。
6.1.3?指針指向的空間釋放
當指針所指向的空間已經被釋放時,就會導致野指針的產生:
int* test()
{int n = 1;return &n;
}
#include <stdio.h>int main()
{int* p = test();printf("%p",p);return 0;
}
由于變量n是在函數test中創建,因此函數執行完畢后,變量n的內存也會被回收,空間被釋放。此時,程序就會打印出一個無效地址或者程序崩潰。
6.2?如何規避野指針
6.2.1?指針初始化
在創建指針變量時,如果明確知道指針指向哪里就直接賦值地址;如果不知道指針應該指向哪里,可以給指針賦值NULL,再后面使用時再進行賦值。
NULL是C語言中定義的一個標識符常量,值是0,地址也是0,這個地址是無法使用的,讀寫該地址時程序會報錯。
#include <stdio.h>int main()
{int n = 0;int* p1 = &n;int* p2 = NULL;return 0;
}
6.2.2 防止指針越界
一個程序向內存申請了哪些空間,指針也就只能訪問哪些空間,不能超出范圍訪問,否則就是越界訪問。
6.2.3?指針變量不再使用時,及時賦值NULL,指針使用之前檢查有效性
當指針變量指向?塊區域的時候,我們可以通過指針訪問該區域,后期不再使用這個指針訪問空間的時候,我們可以把該指針置為NULL。因為約定俗成的?個規則就是:只要是NULL指針就不去訪問,同時使用指針之前可以判斷指針是否為NULL。
#include <stdio.h>int main()
{int i;int arr[10] = {0};int* p = &arr[0];for(i = 0;i < 11;i++){*p = i;p++;}//此時,指針已經訪問越界p = NULL;//將p賦值為NULL,防止p成為野指針...if(p != NULL)//使用前,檢驗p是否為空指針{...}return 0;
}
6.2.4?避免返回局部變量的地址
如上面的示例,避免返回局部變量的地址,防止使用野指針。
7. assert 斷言
assert.h 頭文件中定義了宏assert(),用于在運行時確保程序符合指定條件,如果不符合,就報錯終止運行。這個宏常常被稱為“斷言”。
例如:
assert(p != NULL);
上面代碼在程序運行到這?行語句時,驗證變量p是否為空指針。如果表示,程序正常運行;否則,程序終止運行,并且會給出錯誤信息。
assert() 宏接受?個表達式作為參數。如果該表達式為真(返回值非零),assert()宏則不會產生任何作用,程序繼續運行。如果該表達式為假(返回值為零),?assert() 就會報錯,在標準錯誤流stderr 中寫入一條錯誤信息,顯示沒有通過的表達式,以及包含這個表達式的文件名和行號。
assert()的使用對程序員非常友好,使用assert()的好處在于:它不僅能自動標識文件和出問題的行號,還有一種無需更改代碼就能開啟或關閉assert()的機制。如果已經確認程序沒有問題,不需要再做斷言,就在 #include <assert.h> 語句前面定義一個 NDEBUG 。
#define NDEBUG#include <assert.h>
然后,重新編譯程序,編譯器就會禁用文件中所有的assert()語句。如果程序又出現問題,可以移除 #define NDEBUG 這條語句(或者是注釋掉),再次編譯,這樣就重新啟用了assert()語句。
而使用assert()的缺點在于:引入了額外的檢查,增加了程序的運行時間。
一般我們可以在Debug中使用,在Release版本中選擇禁用assert()就行。這樣在debug版本寫有利于程序員排查問題,在Release版本不影響用戶的使用體驗。
8. 指針的使用和傳址調用
8.1?strlen的模擬實現
庫函數strlen的功能是求字符串長度,統計的是字符串中 '\0' 前的字符數。
函數原型如下:
size_t strlen ( const char * str );
參數str接收一個字符串的起始地址,然后開始統計字符串中 '\0' 之前的字符個數,最終返回長度。
因此,我們模擬就需要從起始地址開始向后逐個檢查字符,如果不為 '\0' ,計數器就+1,知道遇到 '\0' 為止。
例如:
#include <stdio.h>
#include <assert.h>int my_strlen(const char* s)
{assert(s);int count = 0;while(*s != '\0'){count++;s++;}return count;
}//輸出結果
5
8.2?傳值調用和傳址調用
學習了指針的知識,現在我們來看看專門用指針來解決的問題。
例如:寫一個函數,交換兩個整型變量的值
思考之后,我們可能會寫出這樣的代碼:
#include <stdio.h>void Swap(int x,int y)
{int temp;temp = x;x = y;y = temp;
}int main()
{int a = 1,b = 2;printf("交換前:a = %d,b = %d",a,b);Swap(a,b);printf("交換后:a = %d,b = %d",a,b);return 0;
}
但是當我們檢查打印結果時:
交換前:a = 1,b = 2
交換后:a = 1,b = 2
我們發現a,b的值并沒有和我們預期中一樣實現交換,這是為什么呢?
這個時候,我們就要回顧一下前面的知識:形參是實參的一份臨時拷貝,也就是說,形參與實參的地址是不同的。在函數內部實現的值的交換只是交換了形參的地址中的值,而實參的地址的值并沒有變化。在函數結束后,內存被釋放。所以,x和y的值的交換不會影響a和b的值。
像Swap函數這樣,在調用函數時傳遞變量本身的調用方法被稱為傳值調用。
結論:實參傳遞給形參的時候,形參會單獨創建?份臨時空間來接收實參,對形參的修改不影響實 參。
因此,這種寫法是錯誤的,那我們應該怎么實現題目要求呢?
我們現在要解決的事情就是,在函數Swap內部實現main函數中變量a和b的值的交換。既然直接傳遞變量時,形參與實參的地址是不同的,那我們直接傳遞地址是否能解決這個問題呢?
于是,我們可以得到下面的代碼:
#include <stdio.h>void Swap(int* const px,int* const py)
{int temp;temp = * px;* px = * py;* py = temp;
}int main()
{int a = 1,b = 2;printf("交換前:a = %d,b = %d\n",a,b);Swap(&a,&b);printf("交換后:a = %d,b = %d\n",a,b);return 0;
}
此時,我們再檢查打印結果:
交換前:a = 1,b = 2
交換后:a = 2,b = 1
可以發現,代碼成功實現了值的交換。
而像這樣在調用函數時傳遞變量的地址的調用方式被稱為傳址調用。
傳址調用,可以讓函數和主調函數之間建立真正的聯系,在函數內部可以修改主調函數中的變量;所以未來函數中只是需要主調函數中的變量值來實現計算,就可以采用傳值調用。如果函數內部要修改主調函數中的變量的值,就需要傳址調用。