十、指針
🎈使用&
取地址
對於scanf 將輸入的值傳給一個變量 那麼要加上&
符號
scanf("%d",&i);
C語言的變量是放在內存中的 每個變量都有個地址 地址就是變量在內存中所存放的地方的位置
而&
符號 就能夠獲取到指定變量的地址 它是C語言中的取地址符
獲取變量的地址 它的操作數必須是變量 而不能對沒有地址的取地址
使用%p
來輸出地址:
int i=0;
printf("%p\n",&i);
地址的大小取決於編譯器和系統是32位還是64位架構
相鄰的變量的地址
同時定義的兩個變量 它們在內存中的地址位置也是緊挨着的 是相鄰的
int i1;
int i2;
printf("%p\n",&i1); // 0061FECC
printf("%p\n",&i2); // 0061FEC8
這是因爲 在堆棧中 內存是自頂向下分配的 因此先定義的變量會在上面(即地址值更高)
數組的地址
數組的引用和數組的取地址是一樣的 都是數組的地址
數組的地址使用的是數組中第一項(也就是下標爲0)的地址
且數組中的每一項之間都是相鄰的 緊貼着的
int i[10];
printf("%p\n",&i); // 0061FEA8
printf("%p\n",i); // 0061FEA8
printf("%p\n",&i[0]); // 0061FEA8
printf("%p\n",&i[1]); // 0061FEAC
printf("%p\n",&i[2]); // 0061FEB0
🎈指針變量
將取得的變量的地址傳遞給一個函數 那麼可以通過該地址在那個函數內訪問這個變量
【指針】 是一種變量的類型
指針類型的變量就是專門用於保存地址的變量
下面是一個簡單的例子:
將int類型的變量 i 的地址交給了指針p
int*中的*
代表這是一個指針 int
表示該指針指向的是int
int i;
int* p=&i;
p裏保存的是變量i的地址 那麼 可以說是p指向i
🚩指針變量
星號*可以靠近類型 也可以靠近變量名
因此 並不是將星號加給了int這個類型 而是加給了距離星號最近的那個變量名
int* a,b; // 此時 a是指針類型 而b只是普通的int類型
int *a,b; // 此時 a是指針類型 而b只是普通的int類型
在C中並沒有int*這個類型
🚩指針變量的特點
在普通的變量中 存放的是實際的值
而在指針變量中 不會存放實際的值 存放的是具有實際的值的變量的地址
🚩指針作爲參數
在指針作爲參數的時候 用void f(int *p);
來定義
在調用的時候 要這麼傳入:
int i=123;
f(&i); // 傳入的是int類型的變量的【引用】而不是原本的變量名或值
🚩訪問指針地址上的變量
若要訪問 也很簡單 只需要使用星號即可
當然 星號若作爲雙目運算符 那麼是乘的意思
但作爲單目運算符 用於訪問指針的值所表示的地址上的變量
#include <stdio.h>
void f(int *p);
int main()
{
int i=123;
printf("%p\n",&i); // 0061FECC
f(&i);
return 0;
}
void f(int *p)
{
printf("%p\n",p); // 0061FECC
printf("%d",*p); // 123
}
*n
可以作爲右值也可以作爲左值
比如:
int i=*p;
*p=i+2;
可以直接通過指針來很方便地直接改變指針所指向的變量的值:
#include <stdio.h>
void f1(int *p);
void f2(int *p);
int main()
{
int i=123;
printf("%p\n",&i); // 0061FECC
f1(&i);
f2(&i);
return 0;
}
void f1(int *p)
{
printf("%p\n",p); // 0061FECC
printf("%d\n",*p); // 123
// 修改指針指向的位置的值
*p=321;
}
void f2(int *p)
{
printf("%p\n",p); // 0061FECC
printf("%d",*p); // 321
}
左值 之所以被稱爲左值 就是因爲在賦值號=
左邊的並不是變量 而是一個實實在在的值 是表達式計算後的結果 是特殊的值
i[0]=123;
*p=123;
🚩指針運算符的互相反作用
在指針中 用&
獲取地址 用*
獲取指定地址所代表的變量
它們是互相反作用的 即:
- 若對一個獲取的地址取所代表的變量 那就是原來的變量
- 同樣 若對一個指針所指向的變量取地址 那就是原來的地址
🎈指針的應用場景
在使用指針之後 即可用其交換變量
由於函數的作用域範圍問題 直接將值傳入函數內 那麼值在函數外 是不會被改變的
但若傳入的是指針 那麼即可改變指針所指向的地址上的值了
#include <stdio.h>
void swap(int *pa,int *pb);
int main()
{
int a=1;
int b=2;
swap(&a,&b);
printf("%d\n",a); // 2
printf("%d\n",b); // 1
return 0;
}
void swap(int *pa,int *pb)
{
int t=*pa;
*pa=*pb;
*pb=t;
}
因爲函數只能有一個返回值 無法返回多個值 (而且也不能像Java一樣返回一個對象… 😵 )
那麼 當函數返回的是多個值 那麼值只能通過指針返回
或者 遇到另一種情況 就是要分開返回結果
C中不像Java可以異常處理
在C中 只能函數返回(return)運算的狀態 而函數的結果通過指針來返回
🎈指針的注意點
需要注意的是 指針必須先指向一個變量的地址 然後才能被賦值
否則 指針裏面什麼都沒有 此時可能默認指向的是內存中的一塊未知區域 然後此時就被賦值了 那麼可能會導致程序崩潰
🎈指針和數組
在函數中通過參數接收到的外界的數組 其實只是數組的指針
在函數中接收到的數組的地址和外部的數組的地址是完全一模一樣的
但 可以用數組的運算符[]
來對該數組進行運算或做其它操作
因此:
int f(int *arr);
其實等於
int f(int arr[]);
在C中 數組變量是特殊的指針 數組變量本身表達的就是地址
因此 對於數組 無需用&
來取地址了(這是多此一舉)
int arr[10];
int *p=arr; // 這樣就行了
但數組的單元表達的是單個的變量 因此需要用&
來取地址 因爲裏面存放的是確切的值
數組變量名其實就是數組的地址 a=&a[0]
int a[]={1,2,3};
printf("%x\n",a); // 61feac
printf("%x\n",&a); // 61feac
printf("%x\n",&a[0]); // 61feac
[]
運算符除了可以對數組使用 同樣也可以對指針使用
對於指針變量 可以將一個普通變量當作是數組
int i=123;
int* p=i;
printf("%d\n",p); // 123
printf("%d",&p[0]); // 123
同樣的 *
運算符除了可以對指針使用 同樣也可以對數組使用
int i[]={12,23,34};
printf("%d",*i); // 12
在C中 數組變量實際上是const(常量)的指針 因此不能被賦值
int a[]={1,2,3};
int b[]=a; // ×
int *p=a; // √
int b[] --> int * const b;
在C99中 指針本身和指針所指向的那個值 都可以是const
情況一
、指針本身爲const
一旦得到某個變量的地址 那麼不能再指向其它變量 相當於是兩者進行了綁定了
int i=123;
const int *p=&i;
*p=321; // √
p++; // ×
情況二
、指向的值爲const
無法通過指針去修改該變量(但並不意味着不能修改該變量)
int i=123;
int j=111;
const int *p=&i;
*p=321; // ×
i=321; // √
p=&j; // √
const在*的前面 則代表指針所指向的值不能被修改
const在*的後面 則代表該指針不能被修改
int i;
const int* p1=&1;
int const* p2=&i;
int *const p3=&i;
🚩轉換
可以將非const的值轉換爲const的值
void f(const int* i);
當要傳遞的參數的類型比地址還要大的時候 可用該方法 傳遞一個const的指針變量
如此 既能用較少的字節數來傳遞值給參數 同時又能避免函數外面對變量進行修改
🚩const數組
數組變量已經const的指針了 再加const 代表數組中的每個單元都是const的int類型的值 不能再改變
const int arr[]={1,2,3,4,5};
因此 爲保護數組不被函數修改 可設置該函數的參數爲const
int f(const int arr[],int length);
🎈指針的運算
當給指針+1的時候 實際上指針所增加的不一定是1
指針所增加的是sizeof(類型)
的值
printf("%d\n",sizeof(char)); // 1
printf("%d\n",sizeof(int)); // 4
那麼 char類型的指針+1 地址值增加的就是1
int類型的指針+1 地址值增加的就是4
由於數組中存放的值都是緊挨着的 且指針默認指向的是數組中的第一位 那麼 指針 +1實際上就是讓指針指向數組中的下一位
int arr[]={12,23,34,45,56};
int *p=arr;
printf("%p\n",p); // 0061FEB8 此時指向的是數組arr中的12這個數
printf("%p",p+1); // 0061FEBC 此時指向的是數組arr中的23這個數
同理 指針 -1實際上就是讓指針指向數組中的上一位
printf("%p",p-1); // 0061FEB4
可以將指針看作是數組 也可以將數組看作是指針
那麼 實際上 因爲指針p默認是指向數組arr[0]
因此 當指針+1後 指針(p+1)指向的是數組arr[1]
當指針+2後 指針*(p+1)指向的是數組arr[2] 以此類推
int arr[]={12,23,34,45,56};
int *p=arr;
printf("%p\n",p); // 0061FEB8
printf("%d\n",*p); // 12
printf("%p\n",p+1); // 0061FEBC
printf("%d\n",*(p+1)); // 23
printf("%p\n",p-1); // 0061FEB4
printf("%d\n",*(p-1)); // 4201696 下標越界後 取到的就是一串地址值
*(p+n) <==> arr[n]
❗注意
這一切的運算都是基於指針指向的是連續空間 例如數組
若指針指向的壓根不是一片連續的空間 那又何來上一位下一位之說?
那就根本沒有意義了
🚩指針可以做哪些計算
- 爲指針加/減一個整數:
+ += - -=
- 遞增/遞減:
++ --
*p++
即爲 取出指針p所指向的位置的數據 在結束後將指針p指向的位置移到下一個 *p- -同理 - 兩個指針相減:
相減後的差 不是地址值的差 而是地址值的差/sizeof(類型)
即爲它們中間相距幾個該類型的距離
指針還可以進行比較
<
<=
==
>
>=
!=
這些 都可以對指針使用
當然 比較的並不是指針的值的大小 而是比較指針在內存中的地址的大小
🚩0地址
在計算機的內存中 有個0地址 該地址無法讀寫
因此 0地址通常可以用於表達狀態 例如:
- 返回的指針無效
- 指針未被正常初始化(指針先初始化爲0)
但是 若用0來表示0地址 則有可能會崩潰
在C語言中 使用 NULL
(全部大寫)來表示0地址
🚩指針的類型
用void*
來表示不知道指向什麼類型的指針 (只知道有一個指針指向某個內存空間 僅此而已)
指向不同類型的指針無法互相賦值 因此需要避免用錯了指針
當然 也可以進行強制類型轉換
int i=123;
int *p=&i;
void *q=(void*)p;
上述代碼的含義是將int類型的指針p強制類型轉換爲void類型的指針q 這並沒有改變指針p指向的變量i的類型
若通過指針q看指針p 那麼它就是void類型的
若通過指針p看 那麼他依舊還是int類型的
🚩指針的用途
- 在需要傳入較大的數據時 指針可以用作參數
- 在函數傳入數組後 可以用指針對數組進行操作
- 當函數返回不止一個結果 可以用指針作爲參數帶出結果
- 當需要用函數修改不止一個變量 可以傳入指針來修改變量的值
- 還可以動態申請內存
🎈動態內存分配 / malloc&free
當遇到一種情況:預先不知道數組的大小 當輸入的時候才知道
在這種情況下 C99可以用變量來定義數組的大小
但在C99之前 並不支持這麼做 在這種情況下 就必須使用動態內存分配了
使用 malloc
函數來指定要分配多少內存
(使用前需導入stdlib頭文件#include <stdlib.h>
)
malloc所要的參數是佔據多少空間(以字節爲單位)
malloc返回的是void*
的類型 因此還需要強制類型轉換
比如這樣:
int n;
scanf("%d",&n);
int *a=(int *)malloc(n*sizeof(int)) // 分配一塊用於存放int類型的內存空間
最後 在使用完後還需使用free函數將內存歸還
free是和malloc相配套的函數 用於將申請來的空間還給計算機系統
在歸還的時候 只能還申請來的空間的首地址 而不是地址多次改變後的值
free(a); // 歸還內存空間
若不free的話 隨着長時間運行 計算機的內存會逐漸下降 導致產生內存垃圾
==================== 例子 ====================
作爲例子
可以用動態類型分配來實現自動控制數組大小:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int number;
int* arr=0;
int i;
scanf("%d",&number);
arr=(int *)malloc(number*sizeof(int));
for (i=0;i<number;i++)
{
scanf("%d",&arr[i]);
}
for (i=number-1;i>=0;i--)
{
printf("%d\n",arr[i]);
}
// 動態內存分配 在用完之後 還需將內存歸還
free(arr);
return 0;
}
❗注意點:
在main函數的參數中 最好將參數定義成int argc,char const *argv[]
main(int argc,char const *argv[])
{
...
}
argc是啓動C程序時的參數個數 argv是啓動C程序時的參數數組的指針 (這裏的參數就是在運行C程序時在命令行輸入的一串字符 用空格進行分隔)
(就像Java的main函數的String [] args)