這篇筆記主要是介紹C語言中的指針,包括指針的重要性、定義、使用等。閱讀本文預計需要 10 分鐘。
Day06 郝斌C語言自學視頻之C語言的指針
指針的重要性
指針是繼流程控制
和函數
之後,又一個重點。可以說指針是 C 語言的靈魂
。
指針的重要性具體表現
- 表示一些複雜的數據結構。
- 快遞的傳遞數據,減少內存的耗用。【重點】
- 使函數返回一個以上的值。【重點】
- 能直接訪問硬件。
- 能夠方便處理字符串。
- 是理解面嚮對象語言中引用的基礎。
指針的定義
在介紹指針定義時,我們先看一下地址的概念。
地址:地址是內存單元的編號。它是從 0 開始的非負整數。對於32位系統,它的範圍是:4 G [0–(4G-1)]。4 G即2^32。
指針:指針就是地址,地址就是指針。
指針變量:指針變量就是存放內存單元編號的變量,或者說指針變量就是存放地址的變量。
指針和指針變量是兩個不同的概念。但是要注意,通常我們敘述時,會把指針變量簡稱爲指針,實際他們含義並不一樣。
指針的本質就是一個操作受限的非負整數
。
指針的使用
基本類型指針
格式:
指針變量類型 指針變量名
例:
/*
時間:2020年2月24日13:23:59
功能:
測試基本類型指針的使用
*/
# include <stdio.h>
int main(void)
{
int * p;
int i = 3;
int j;
p = &i; // OK
j = *p;
printf("i = %d, j= %d\n", i, j);
return 0;
}
/*
在VSCode中的輸出結果是:
--------------------------
i = 3, j= 3
--------------------------
*/
說明:
對於語句**int * p;
**:
p
是變量的名字,int *
表示p
變量存放的是int
類型變量的地址。int * p;
不表示定義了一個名字爲*p
的變量。int * p;
應該這樣理解:p
是變量名,p
變量的數據類型是int *
類型。所謂int *
類型,實際就是存放int
變量地址的類型。
對於語句**p = &i;
**:
p
保存了i
的地址,因此p
指向i
p
不是i
,i
也不是p
, 更準確的說,修改p
的值不影響i
的值,修改i
的值,也不影響p
的值。- 如果一個指針變量指向了某個普通變量,則
*指針變量
就完全等同於普通變量。解釋:如果 p 是個指針變量,並且 p 存放普通變量 i 變量的地址,則 p 指向了普通變量 i。*p
就完全等同於i
。或者說:在所有出現*p
的地方都可以替換成i
,在所有出現i
的地方都可以替換成*p
。*p
就是以p
的內容爲地址的變量。
對於語句**j = *p;
**,就相當於j = i;
* 的含義:
- 乘法。
- 定義指針變量。
int * p;
定義了一個名字叫p
的變量,int *
表示p
只能存放int
變量地址。 - 指針運算符。該運算符放在已經定義好的指針前面,如果
p
是一個已經定義好的指針變量,則*p
表示以p
的內容爲地址的變量。
如何通過被調函數修改主調函數普通變量的值
- 實參必須爲該普通變量的地址。
- 形參必須爲指針變量。
- 在被調函數中通過
* 形參名 = XXX
的方式就可以修改主調函數相關的變量。
例:
/*
時間:2020年2月24日14:45:35
功能:
通過被調函數修改主調函數普通變量的值
總結:
1. 實參必須爲該普通變量的地址。
2. 形參必須爲指針變量。
3. 在被調函數中通過 * 形參名 = XXX 的方式就可以修改主調函數相關的變量。
*/
# include <stdio.h>
void huhuan_1(int, int);
void huhuan_2(int *, int *);
void huhuan_3(int *, int *);
// 不能完成互換功能
void huhuan_1(int a, int b)
{
int t;
t = a;
a = b;
b = t;
return;
}
// 不能完成互換功能
void huhuan_2(int * p, int * q)
{
int * t; // 如果要互換 p 和 q 的值,則 t 必須是 int *, 不能是 int, 否則會出錯
t = p;
p = q;
q = t;
return;
}
// 可以完成互換功能
void huhuan_3(int * p, int * q)
{
int t; // 如果要互換 *p 和 *q 的值,則 t 必須定義成 int 不能定義成 int *, 否則語法錯誤
t = *p; // p 是 int *, *p 是 int
*p = *q;
*q = t;
return;
}
int main(void)
{
int a = 3;
int b = 5;
// huhuan_1(a, b);
// huhuan_2(&a, &b); // huhuan_2(*p, *q); 是錯誤的, huhuan_2(a, b); 也是錯誤的
huhuan_3(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
/*
在VSCode中的輸出結果是:
--------------------------
a = 5, b = 3
--------------------------
*/
指針和數組
指針和數組分爲指針和一維數組以及指針和二維數組。這裏主要介紹指針和一維數組。
一維數組名:一維數組名是個指針常量。它存放的是一維數組第一個元素
的地址。
下標和指針的關係:如果 p
是個指針變量,則 p[i]
永遠等價於 *(p+i)
確定一個一維數組需要幾個參數:需要兩個參數。數組第一個元素的地址和數組的長度。即:如果一個函數要處理一個一維數組,則需要接收該數組的第一個元素的地址和數組的長度。
例 指針和一維數組
/*
時間:2020年2月24日16:35:50
目的:
確定一個數組需要的參數
總結:
如果一個函數要處理一個一維數組,
則需要接收該數組的第一個元素的地址和數組的長度。
*/
# include <stdio.h>
void f(int * pArr, int len)
{
int i;
for (i=0; i<len; ++i)
printf("%d ", *(pArr+i));
/*
*(pArr+i) 等價於 pArr[i]
也等價於 b[i]
也等價於 *(b+i)
*/
printf("\n");
}
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int b[6] = {-1, -2, -3, 4, 5, -6};
int c[100] = {1, 99, 22, 33};
f(b, 6);
return 0;
}
/*
在VSCode中的輸出結果是:
--------------------------
-1 -2 -3 4 5 -6
--------------------------
*/
指針變量的運算
指針變量不能相加,不能相乘,也不能相除。如果兩個指針變量指向的是同一塊連續空間中的不同存儲單元,則這兩個指針變量纔可以相減。
例 指針的運算
/*
時間:2020年2月24日16:57:27
指針的運算
*/
# include <stdio.h>
int main(void)
{
int i = 5;
int j = 10;
int * p = &i;
int * q = &j;
int a[5];
p = &a[1];
q = &a[4];
printf("p和q所指向的單元相隔%d個單元\n", q-p);
// p-q 沒有實際意義
return 0;
}
/*
在VSCode中的輸出結果是:
--------------------------
p和q所指向的單元相隔3個單元
--------------------------
*/
一個指針變量到底佔幾個字節【非重點】
預備知識sizeof()
函數:
用法一:
sizeof(數據類型)
功能:返回值就是該數據類型所佔的字節數。
例:
sizeof(int) = 4
sizeof(char) = 1
sizeof(double) = 8
用法二:
sizeof(變量名)
功能:返回值是該變量所佔的字節數。
假設 p
指向 char
類型變量(1 個字節)。
假設 q
指向 int
類型變量(4 個字節)。
假設 r
指向 double
類型變量(8 個字節)。
p q r
本身所佔的字符數是否一樣?
答案: p q r
本身所佔的字符數是一樣的。
總結: 一個指針變量,無論它指向的變量佔幾個字節,該指針變量本身只佔 4
個字節。一個變量的地址是用該變量的首地址表示的。
多級指針
指針的指針就是多級指針了。對於多級指針需要明白, p
是指針變量,如果 q
是 p
的指針,則 *q = p
。
例 多級指針
/*
時間:2020年2月24日21:45:22
多級指針的示例
*/
# include <stdio.h>
int main(void)
{
int i = 10;
int * p = &i;
int ** q = &p;
int *** r = &q;
// r = &p; // error 因爲 r 是 int *** 類型,r只能存放int ** 類型的變量的地址
printf("%d\n", ***r);
printf("%d\n", **q);
printf("%d\n", *p);
return 0;
}
/*
在VSCode中的輸出結果是:
--------------------------
10
10
10
--------------------------
*/
對於指針和函數
以及指針和結構體
這裏先不介紹。
動態內存分配【重點難點】
傳統數組的缺點
傳統數組也叫靜態數組。
-
數組長度必須事先指定,且只能是常整數,不能是變量。如:
int a[5]; // Ok
int len = 5; int a[len]; // error
-
傳統形式定義的數組,該數組的內存程序員無法手動釋放。數組一旦定義,系統爲該數組分配的存儲空間就會一直存在,除非數組所在的函數運行結束。在一個函數運行期間,系統爲該函數中所分配的空間會一直存在,直到該函數運行完畢時函數的空間纔會被系統釋放。
-
數組的長度一旦定義,其長度就不能再更改。數組的長度不能在函數運行的過程中動態的擴充或縮小。
-
A 函數定義的數組,在 A 函數運行期間可以被其他函數使用,但 A 函數運行完畢之後,A 函數中的數組將無法被其他函數使用。即:
傳統定義的函數不能跨函數使用
。
爲什麼需要動態分配內存
動態數組的創造就是爲了解決靜態數組的 4 個缺陷。
靜態內存 VS 動態內存的比較
區別 | 靜態內存 | 動態內存 |
---|---|---|
內存分配 | 系統自動分配 | 程序員手動分配 |
內存釋放 | 系統自動釋放 | 程序員手動釋放 |
內存分配位置 | 在棧 中分配 |
在堆 中分配 |
動態分配內存舉例——動態數組的構造
假設需要動態構造一個 int
型一維數組。
int * p = (int *)malloc(int len);
-
本語句一共分配了兩塊內存,一塊是動態分配的,總共
len
個字節,一個是靜態分配的是 4 個字節,即變量p
本身所佔的內存。 -
malloc()
只有一個int
型的形參,表示要求系統分配的字節數。 -
malloc()
函數的功能是請求系統分配len
個字節的內存空間,如果請求分配成功,則返回第一個字節的地址,如果分配不成功,則返回NULL
。
malloc()
函數只能返回第一個字節的地址,所以我們需要把這個無任何實際意義的第一個字節的地址(俗稱乾地址)轉化成一個有實際意義的地址,因此 malloc
前面必須加 (數據類型 *)
,表示把這個無實際意義的第一個字節的地址轉化爲相應類型的地址。
如:
int * p = (int *)malloc(5);
表示將系統分配好的50個字節的第一個字節的地址轉化爲 int *
型的地址,更準確的說是把第一個字節的地址轉化爲 4 個字節的地址,這樣 p
就指向了第一個的 4 個字節, p+1
就指向了第二個的 4 個字節,p+i
就指向了第 i+1
個的 4 個字節,p[0]
就是第一個元素, p[i]
就是第 i+1
個元素。
double * p = (double *)malloc(80);
表示將系統分配好的80個字節的第一個字節的地址轉化爲 double *
型的地址,更準確的說是把第一個字節的地址轉化爲 8 字節的地址,這樣 p
就指向了第一個的 8 個字節, p+1
就指向了第二個的 8 個字節,p+i
就指向了第 i+1
個的 8 個字節,p[0]
就是第一個元素, p[i]
就是第 i+1
個元素。
freep(p);
表示將 p
所指向的內存給釋放掉,p
本身的內存是靜態的。不能由程序員手動釋放,p
本身的內存只能在 p
變量所在的函數運行終止時由系統自動釋放。
例 動態數組的構造
/*
時間:2020年2月26日21:15:46
動態數組的構造
*/
# include <stdio.h>
# include <malloc.h>
int main(void)
{
int a[5]; // 如果int 佔4個字節的話,則本數組總共包含有20個字節,每四個字節被當做了一個int變量來使用
int len;
int * pArr;
int i;
printf("請輸入你要存放元素的個數:");
scanf("%d", &len); //
pArr = (int *)malloc(4 * len); // 本行動態的構造了一個一維數組,該數組的長度是len,該數組的數組名是pArr,該數組的每個元素是int 整型。類似於 int pArr[len];
// 對一維數組進行操作, 如:對一維數組進行賦值
for (i=0; i<len; ++i)
scanf("%d", &pArr[i]);
// 對一維數組進行輸出
printf("一維數組的內容是:\n");
for (i=0; i<len; ++i)
printf("%d\n", pArr[i]);
free(pArr); // 釋放掉動態分配的數組
return 0;
}
跨函數使用內存的問題
靜態內存不可以跨函數使用。或者說是:靜態內存在函數執行期間可以被其他函數使用,但是在函數執行完畢後就不能再被其他函數使用了。
動態內存可以跨函數使用。動態內存在函數執行完畢之後仍然可以被其他函數使用。
【說明】
-
本學習筆記整理自B站郝斌老師的《郝斌C語言自學教程》片段P121-P150。
-
筆記中所有代碼均在windows10操作系統,在VSCode編輯器中通過測試。具體VSCode C語言開發環境搭建方法請參照我的另一篇CSDN博客——Windows10下利用Visual Studio Code搭建C語言開發環境。
後記
如果對你有所幫助,歡迎關注我的公衆號。這個公衆號主要是慢慢分享和記錄自己學習編程的筆記,比如:C,Python,Java等。後續也會分享自己面試以及在職場上的成長心得。