C學習-指針(五)

指針是C語言中很是重要的數據類型,若是你說C語言中除了指針,其餘你都學得很好,那你乾脆說沒學過C語言。究竟什麼是指針呢?咱們先來看一個概念。html

直接引用ios

  1. 回想一下,以前咱們是如何更改某個變量的值?
    咱們以前是經過變量名來直接引用變量,而後進行賦值:數組

char a;
a = 10;
  1. 看上去是很簡單,其實程序內部是怎麼操做的呢?
    其實,程序對變量的讀寫操做,其實是對變量所在的存儲空間進行寫入或取出數據。就上面的代碼而言,系統會自動將變量名a轉換爲變量的存儲地址,根據地址找到變量a的存儲空間,而後再將數據10以2進制的形式放入變量a的存儲空間中。安全

clipboard.png

  1. 經過變量名引用變量,由系統自動完成變量名和其存儲地址之間的轉換,稱爲變量的"直接引用"方式函數

1、什麼是指針?

1.咱們已經知道,"直接引用"是直接經過變量名來讀寫變量spa

2.C語言中還有一種"間接引用"的方式(以變量a爲例):首先將變量a的地址存放在另外一個變量中,好比存放在變量b中,而後經過變量b來間接引用變量a,間接讀寫變量a的值。這就是"間接引用"。指針

clipboard.png

若是程序經過"間接引用"的方式來修改a的值,能夠這樣作:先根據 變量名b 獲取 變量b 的地址ffc2,取出變量b中存儲的內容ffc1,也就是變量a的地址,再根據變量a的地址ffc1找到a的存儲空間,而後修改裏面的數據。code

3.總結一句:用來存放變量地址的變量,就稱爲"指針變量"。在上面的狀況下,變量b就是個"指針變量",咱們能夠說指針變量b指向變量a。htm

2、指針的定義

通常形式:類名標識符 *指針變量名;blog

int *p;

float *q;

"*"是一個說明符,用來講明這個變量是個指針變量,是不能省略的,但它不屬於變量名的一部分
前面的類型標識符表示指針變量所指向的變量的類型,並且只能指向這種類型的變量

3、指針的初始化

1.先定義後初始化

1 // 定義int類型的變量a
2 int a = 10;
3 
4 // 定義一個指針變量p
5 int *p;
6 
7 // 將變量a的地址賦值給指針變量p,因此指針變量p指向變量a
8 p = &a;

注意第8行,賦值給p的是變量a的地址&a

2.在定義的同時初始化

// 定義int類型的變量a
int a = 10;

// 定義一個指針變量p
// 並將變量a的地址賦值給指針變量p,因此指針變量p指向變量a
int *p = &a;

3.初始化的注意

指針變量是用來存放變量地址的,不要給它隨意賦值一個常數。下面的寫法是錯誤

int *p; 
p = 200; // 這是錯誤的

4、指針運算符

1.給指針指向的變量賦值

1 char a = 10;
 2 printf("修改前,a的值:%d\n", a);
 3 
 4 // 指針變量p指向變量a
 5 char *p = &a;
 6 
 7 // 經過指針變量p間接修改變量a的值
 8 *p = 9;
 9 
10 printf("修改後,a的值:%d", a);

當程序剛執行完第5行代碼時,內存中大概的分佈狀況是這樣的

clipboard.png

a值是10,p值就是變量a的地址ffc3。

注意下第五、第8行,都有個"*",它們的含義是不同的:

(1) 第5行的"*"只是用來講明p是個指針變量

(2) 第8行的"*"是一個指針運算符,這裏的*p表明根據p值ffc3這個地址訪問對應的存儲空間,也就是變量a的存儲空間,而後將右邊的數值9寫入到這個存儲空間,至關於 a = 9;,因而內存中就變成這樣了

clipboard.png

輸出結果爲:
clipboard.png
,能夠發現,咱們經過變量p間接修改了變量a的值。

2.取出指針所指向變量的值

指針運算符除了能夠賦值以外,還能夠用於取值

1 char a = 10;
2  
3 char *p;
4 p = &a;
5 
6 char value = *p;
7 printf("取出a的值:%d", value);

輸出結果:
clipboard.png
,第6行中的*p的意思是:根據p值(即變量a的地址)訪問對應的存儲空間,並取出存儲的內容(即取出變量a的值),賦值給value

3.使用注意

在指針變量沒有指向肯定地址以前,不要對它所指的內容賦值。下面的寫法是錯誤

int *p; 
*p = 10; //這是錯誤的

應該在指針變量指向一個肯定的變量後再進行賦值。下面的寫法纔是正確

// 定義2個int型變量
int a = 6, b;

// 定義一個指向變量b的指針變量p
int *p;
p = &b;

// 將a的值賦值給變量b
*p = a;

5、指針的用途舉例

1.例子1

前面咱們經過指針變量p間接訪問了變量a,在有些人看來,以爲指針變量好傻B,直接用變量名a訪問變量a不就行了麼,幹嗎搞這麼麻煩。彆着急,接下來舉個例子,讓你們看看指針還能作什麼事情。

如今有個要求:寫一個函數swap,接收2個整型參數,功能是互換兩個實參的值。

1> 若是沒學過指針,你可能會這樣寫

void swap(char v1, char v2) {
    printf("更換前:v1=%d, v2=%d\n", v1, v2);
    
    // 定義一箇中間變量
    char temp;
    
    // 交換v1和v2的值
    temp = v1;
    v1 = v2;
    v2 = temp;
    
    printf("更換後:v1=%d, v2=%d\n", v1, v2);
}

int main()
{
    char a = 10, b = 9;
    printf("更換前:a=%d, b=%d\n", a, b);
    
    swap(a, b);
    
    printf("更換後:a=%d, b=%d", a, b);
    return 0;
}

輸出結果:clipboard.png

雖然v1和v2的值被交換了,可是變量a和b的值根本就沒有換過來。由於基本數據類型做爲函數實參時,只是純粹地將值傳遞給形參,形參的改變並不影響實參。

咱們能夠簡要分析一下這個過程:

  • 在第20行中,將變量a、b的值分別傳遞給了swap函數的兩個形參v一、v2

2> 若是學了指針,就應該這樣寫

1 void swap(char *v1, char *v2) {
 2     // 中間變量
 3     char temp;
 4     
 5     // 取出v1指向的變量的值
 6     temp = *v1;
 7     
 8     // 取出v2指向的變量的值,而後賦值給v1指向的變量
 9     *v1 = *v2;
10     
11     // 賦值給v2指向的變量
12     *v2 = temp;
13 }
14 
15 int main()
16 {
17     char a = 10, b = 9;
18     printf("更換前:a=%d, b=%d\n", a, b);
19     
20     swap(&a, &b);
21     
22     printf("更換後:a=%d, b=%d", a, b);
23     return 0;
24 }

先看看輸出結果:

clipboard.png
變量a和b的值終於換過來了。

解釋一下:

(在16位編譯器環境下,一個指針變量佔用2個字節)

先注意第20行,傳遞是變量的地址。所以swap函數的形參v1指向了變量a,v2指向了變量b

clipboard.png

第6行代碼是取出v1指向的變量的值,也就是變量a的值:10,而後賦值給變量temp

clipboard.png

第9行代碼是取出v2指向的變量(變量b)的值,而後賦值給v1指向的變量(變量a)

clipboard.png

第12行代碼是將temp變量的值賦值給v2指向的變量(變量b)

clipboard.png

相信你已經感覺到指針的強大了,若是沒有指針,在一個函數的內部根本改變不了外部的實參。

2.例子2

接下來再舉一個指針的實用例子。默認狀況下,一個函數只能有一個返回值,有了指針,咱們能夠實現函數有"多返回值"。

如今有個要求:寫一個函數sumAndMinus,能夠同時計算2個整型的和與差,函數執行完畢後,返回和與差(注意了,這裏要返回2個值)

// 計算2個整型的和與差
int sumAndMinus(int v1, int v2, int *minus) {
    // 計算差,並賦值給指針指向的變量
    *minus = v1 - v2;
    
    // 計算和,並返回和
    return v1 + v2;
}

int main()
{
    // 定義2個int型變量
    int a = 6, b = 2;

    // 定義2個變量來分別接收和與差
    int sum, minus;

    // 調用函數
    sum = sumAndMinus(a, b, &minus);
    
    // 打印和
    printf("%d+%d=%d\n", a, b, sum);
    
    // 打印差
    printf("%d-%d=%d\n", a, b, minus);
    return 0;
}

輸出結果:
clipboard.png
,和與差都由同一個函數計算並返回出來。和是函數的直接返回值,差是經過函數的第3個指針參數間接返回。

所以有了指針,咱們可讓函數有"無限個"返回值。

6、關於指針的疑問

剛學完指針,均可能有一大堆的疑惑,這裏我列出幾個常見的疑惑吧。

1.一個指針變量佔用多少個字節的內存空間?佔用的空間是否會跟隨所指向變量的類型而改變?

在同一種編譯器環境下,一個指針變量所佔用的內存空間是固定的。好比,在16位編譯器環境下,任何一個指針變量都只佔用2個字節,並不會隨所指向變量的類型而改變。

clipboard.png

2.既然每一個指針變量所佔用的內存空間是同樣的,並且存儲的都是地址,爲什麼指針變量還要分類型?並且只能指向一種類型的變量?好比指向int類型的指針、指向char類型的指針。

其實,我以爲這個問題跟"數組爲何要分類型"是同樣的。

看下面的代碼,利用指針p讀取變量c的值

int i = 2;
char c = 1;

// 定義一個指向char類型的指針
char *p = &c;

// 取出
printf("%d", *p);

這個輸出結果應該難不倒你們:1,是能夠成功讀取的。

若是我改一下第5行的代碼,用一個本應該指向int類型變量的指針p指向char類型的變量c

int *p = &c;

clipboard.png

咱們再來看一下輸出:513,c的原值是1,如今取出來倒是513,怎麼回事呢?這個要根據內存來分析

根據變量的定義順序,這些變量在內存中大體以下圖排布:

其中,指針變量p和int類型變量i各佔2個字節,char類型的c佔一個字節,p指向c,所以p值就是c的地址

clipboard.png

1> 最初的時候,咱們用char p指向變量c。當利用p來獲取變量c的值時,因爲指針p知道變量c是char類型的,因此會從ffc3這個地址開始讀取1個字節的數據:0000 0001,轉爲10進制就是1

2> 後來,咱們用int p指向變量c。當利用p獲取變量c的值時,因爲指針p認爲變量c是int類型的,因此會從ffc3這個地址開始讀取2個字節的數據:0000 0010 0000 0001,轉爲10進制就是513

可見,給指針分類是多麼重要的一件事,並且一種指針最好只指向一種類型的變量,那是最安全的。

7、指向一維數組元素的指針

1.用指針指向一維數組的元素

// 定義一個int類型的數組
int a[2];

// 定義一個int類型的指針
int *p;

// 讓指針指向數組的第0個元素
p = &a[0];

// 修改所指向元素的值
*p = 10;

// 打印第一個元素的值
printf("a[0] = %d", a[0]);

輸出結果:a[0] = 10,說明已經經過指針間接修改了數組元素的值,跟指向一個普通int類型變量是同樣的。

因爲數組名錶明着數組的首地址,即 a == &a[0],所以第8行代碼等價於:

// 讓指針指向數組的第0個元素
p = a;

內存分析圖以下,一個指針變量佔用2個字節,一個int類型的數組元素佔用2個字節

clipboard.png

2.用指針遍歷數組元素

2.1 最普通的遍歷方式是用數組下標來遍歷元素

// 定義一個int類型的數組
int a[4] = {1, 2, 3, 4};

int i;
for (i = 0; i < 4; i++) {
    printf("a[%d] = %d \n", i, a[i]);
}

輸出結果:
clipboard.png

2.2 用指針來遍歷數組元素

先定義一個指針,指向數組的第一個元素

// 定義一個int類型的數組
int a[4] = {1, 2, 3, 4};

// 定義一個int類型的指針,並指向數組的第0個元素
int *p = a;

clipboard.png

p的值是a[0]的地址,所以,如今咱們利用指針p只能訪問數組的第0個元素a[0],用*p就可取出a[0]的值1。要想訪問其餘元素,就必須拿到元素的地址,能夠發現每一個元素的地址差值爲2,由於在16位編譯器環境下,一個int類型的變量佔用2個字節。如今只是知道a[0]的地址值爲p,怎麼根據a[0]的地址獲取其餘元素的地址呢?其實很是簡單,p+1就是a[1]的地址。注意了,這裏的p+1表明着p的值加2,並非p的值加1,好比p的值爲ffc3,p+1則爲ffc5,而非ffc4。依次類推,p+2就是a[2]的地址ffc7,p+3就是a[3]的地址ffc9。

我先解釋一下,爲何p+1表明p的值加2,而不是加1呢?

其實,p+1不必定表明p的值加2,也多是加一、加4或者加8。究竟加多少,這跟指針的類型有關。下圖是在16位編譯器環境下的狀況。

clipboard.png

聰明的你可能已經找到規律了,由於char類型的變量要佔用1字節,因此p+1表明p的值加1;float類型的變量佔用4字節,因此p+1表明p的值加4。從這一點,也能夠很好地說明爲何指針必定要分類型,不一樣類型的指針,p+1的含義是不同的。

上述代碼中的p指向了int類型的數組元素a[0],因此p+1表明p的值加2。知道怎麼獲取其餘元素的地址了,那麼就能夠利用指針p遍歷數組元素了。

1 // 定義一個int類型的數組
 2 int a[4] = {1, 2, 3, 4};
 3 
 4 // 定義一個int類型的指針,並指向數組的第0個元素
 5 int *p = a;
 6 
 7 int i;
 8 for (i = 0; i < 4; i++) {
 9     // 利用指針運算符*取出數組元素的值
10     int value = *(p+i);
11     
12     printf("a[%d] = %d \n", i, value);
13 }

注意第10行的代碼,*(p+i)表明根據p+i的值(其實就是第i個數組元素的地址)訪問對應的存儲空間,並取出存儲的內容(也就是取出第i個數組元素的值),賦值給左邊的value。

最後的輸出效果是同樣的:
clipboard.png

注意的是:遍歷完畢後,指針變量p仍是指向a[0],由於p值一直沒有變過,一直都是a[0]的地址ffc3

補充一下,其實第10行改爲下面的代碼也是能夠的:

int value = *(a+i);

你們都知道,a值表明數組的首地址,也就是a[0]的地址ffc3。a+1則表明a的值加2,即a[1]的地址ffc5,也就是說,a+i表明着元素a[i]的地址。相信你們也能猜出來了,a+1不必定表明着a值加2,究竟加多少,取決於數組的類型。a+i的計算方法與p+i相同

利用上面的方法遍歷完數組元素後,p一直指向元素a[0]。其實咱們也能夠直接修改p的值來訪問數組元素,只須要改一下第10行的代碼便可

// 利用指針運算符*取出數組元素的值
int value = *(p++);

p++其實就是至關於p = p + 1,直接修改了p值,並且每次是加2。所以,每執行一次p++,指針p就會指向下一個數組元素。

輸出結果確定是同樣的:
clipboard.png

可是,遍歷完畢後,指針變量p沒有指向任何數組元素,由於一共執行了4次p++,最後p值爲ffcb。固然,能夠從新讓p指向a[0]:p = &a[0];或者p = a;

注意,這裏的寫法是錯誤

int value = *(a++);

a++至關於a=a+1,數組名a是個常量!不能進行賦值運算!

三、指針與數組的總結

p是指針,a是一個數組

1> 若是p指向了一個數組元素,則p+1表示指向數組該元素的下一個元素。好比,假設p = &a[0],則p+1表示a[1]的地址

2> 對於不一樣類型的數組元素,p值的改變是不一樣的。若是數組元素爲int類型,p+1表明着p的值加上2(16位編譯器環境下)

3> 若是p的初值是&a[0],那麼

  • p+i和a+i均可以表示元素a[i]的地址,它們都指向數組的第i個元素。a表明數組首地址,a+i也是地址,它的計算方法與p+i相同

  • *(p+i)和*(a+i)都表示數組元素a[i]

  • 雖然p+i和a+i都指向數組的第i個元素,但兩者使用時仍是有區別的。由於做爲指針變量的p能夠改變自身值,如p++,使p的值自增。而數組名a是一個表明數組首地址的常量,它的值是不能改變的,即a++是不合法的

4> 引用一個數組元素能夠有兩種方法:

  • 下標法: 如a[i]

  • 指針法: 如(p+i) 或 (a+i)

四、數組、指針與函數參數

1.用數組名做爲函數實參時,是把實參數組的首地址傳遞給形參數組,兩個數組共同佔用同一段內存空間,這樣形參數組中的元素值發生變化就會使實參數組的元素值也同時變化

void change(int b[]) {
    b[0] = 10;
}

int main()
{
    // 定義一個int類型的數組
    int a[4] = {1, 2, 3, 4};

    // 將數組名a傳入change函數中
    change(a);
    
    // 查看a[0]
    printf("a[0]=%d", a[0]);
    
    return 0;
}

change函數的形參是數組類型的,在第11行調用change函數時,將數組名a,也就是數組的地址傳給了數組b。所以數組a和b佔用着同一塊內存空間。

輸出結果:clipboard.png

2.這種地址的傳遞也能夠用指針來實現。函數的實參和形參均可以分別使用數組或指針。這樣就有4種狀況:

clipboard.png

也就是說,若是一個函數的形參類型是一個數組,調用函數時,你能夠傳入數組名或者指針變量;

void change(int b[]) {
    b[0] = 10;
}

int main()
{
    // 定義一個int類型的數組
    int a[4] = {1, 2, 3, 4};
    
    int *p = a;

    // 將數組名a傳入change函數中
    change(p);
    
    // 查看a[0]
    printf("a[0]=%d", a[0]);
    
    return 0;
}

注意第1行的形參類型是個數組int b[],第10行定義了指針變量p,第13行將p當作實參傳入函數

若是一個函數的形參類型是一個指針變量,調用函數時,你能夠傳入數組名或者指針變量。

1 void change(int *b) {
 2     b[0] = 10;
 3     // 或者*b = 10;
 4 }
 5 
 6 int main()
 7 {
 8     // 定義一個int類型的數組
 9     int a[4] = {1, 2, 3, 4};
10 
11     // 將數組名a傳入change函數中
12     change(a);
13     
14     // 查看a[0]
15     printf("a[0]=%d", a[0]);
16     
17     return 0;
18 }

注意第1行的形參類型是個指針變量int *b,第12行將數組名a當作實參傳入函數。

由第2行能夠看出,在不少狀況下,指針和數組是能夠相互切換使用的。可是,並不能說指針就等於數組。

8、指針和字符串

指針和字符串 詳情

9、返回指針的函數與指向函數的指針

返回指針的函數與指向函數的指針


注:本文轉自李明傑老師的博文

相關文章
相關標籤/搜索