全面學習C語言【四】:指針(指針變量、指針運算符、指針數組、指針運算)、malloc動態內存分配

十、指針

🎈使用&取地址

對於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)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章