C語言指針複習—— 空指針和野指針、數組與指針、指針與函數傳參、指針與函數

  

  
  

1. 普通指針的理解

  每個變量都有一個符號地址(變量名)和物理地址(在內存中的位置,又叫做指針), 指針變量用來存放普通變量的地址,即指針變量是用來存放普通變量的指針。

1、 指針變量也是一個變量,在內存中也是佔內存的

2、 指針變量也有自己的物理地址,其存儲是用比該指針類型高一級的指針變量來存放指針變量的地址,如二級指針變量存放一級指針變量的地址

3、當定義了一個指針變量,一定要綁定進行指針綁定,使該指針變量所存放的信息爲有效地址
  
  
以下編譯代碼的環境:// gcc 64位系統

#include <stdio.h>

int main()
{
	int    a = 4;
	int*   b = &a;
	int**  c = &b;
	printf("變量a的值爲:%d\t地址爲:%p\t長度爲:%d\n",a,&a,(int)sizeof(a));
	printf("變量b的值爲:%p\t地址爲:%p\t長度爲:%d\n",b,&b,(int)sizeof(b));
	printf("變量c的值爲:%p\t地址爲:%p\t長度爲:%d\n",c,&c,(int)sizeof(c));
	return 0;
}

  

1.1 空指針和野指針

  
  在C語言中,如果一個指針不指向任何數據,我們就稱之爲空指針,用NULL表示;對於下面的a,a = (int *)0;是允許的,而a = 0;是不行的,因爲類型不相同,給指針賦初值爲NULL,其實就是讓指針指向0地址處

 int     *a    =    NULL; 
 /* 以下爲NULL的定義
	#ifdef _cplusplus        // 定義這個符號就表示當前是C++環境
	#define NULL 0    // 在C++中NULL就是0
	#else
	#define NULL (void *)0 // 在C中NULL是強制類型轉換爲void *的0
	#endif
*/

  0地址在一般的操作系統中都是不可被訪問的,而指針變量未經定義就解引用時會報錯,當一個指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的),這個指針稱爲野指針

#include <stdio.h>
int main()
{
	int* a;
	printf("a的值爲:%d\n",*a);
	return 0;
}

  

1.2 指針的運算

  指針變量如果是局部變量,則分配在棧上,本身遵從棧的規律(反覆使用,使用完不擦除,所以是髒的,本次在棧上分配到的變量的默認值是上次這個棧空間被使用時餘留下來的值),就決定了棧的使用多少會影響這個默認值

  指針參與運算時,實際是地址的運算,指針變量+1,並不是真的加1,而是加 1 * sizeof(指針類型);如果是int * 指針,則+1就實際表示地址+4,如果是char * 指針,則+1就表示地址+1;如果是double *指針,則+1就表示地址+8(注意:sizeof是C語言的一個運算符,不是函數,作用是用來返回()裏面的變量或者數據類型佔用的內存字節數)
  
  

2. 數組與指針

  
  從內存角度講,數組變量就是一次分配多個相同類型變量,而且這多個變量在內存中的存儲單元是依次相連接的;從編譯器角度來講,數組變量也是變量,變量的本質就是一個地址,這個地址在編譯器中決定具體數值,具體數值和變量名綁定,變量類型決定這個地址的延續長度

比如定義一個數組 int a[5];
1、a 是數組名,其作爲右值表示數組首元素(數組的第1個元素,也就是a[0])的首地址,此時 a 與 &a[0] 是等價的
2、&a 是數組名 a 取地址,實質是一個常量,做右值時表示整個數組的首地址
3、&a 是整個數組的首地址,而 a 是數組首元素的首地址。這兩個在數字上是相等的,但是意義不相同,a和&a[0]是元素的指針,也就是int * 類型;而&a是數組指針,是int (*)[5];類型

比如當賦值給一個指針時,就不能用 &a ,而應該用 a 來賦值(此處要注意數組與int型或者其他數據類型賦值地址給指針變量時的區別與不同)

#include <stdio.h>
int main()
{
	int a[5] = {1,2,3,4,5};
	printf("整個數組 a 的地址爲:%p\n",&a); 
	printf("數組a的首元素地址爲:%p\n",a);
	
	//int * b = &a; //報錯
	int * b = a; //指針變量b應該存放數組首元素地址
	printf("數組 a[2] = %d\t 指針解引用 *(b+2) = %d\n",a[2],*(b+2));
	
	return 0;
}

  

2.1 數組的訪問

  以指針方式來訪問數組元素時,格式爲:* (指針+偏移量); 其中的 * 號表示解引用,當指針變量是數組首元素地址(a或者&a[0])時,那麼偏移量就是下標;值得注意的是,當一個指針指向的是一個數組時,* (a+1) 所表示的和 a[1] 所表示的時等價的
  

2.2 字符串數組

對於字符串數組 char str[] = “I love you!”;

  str表示的是該字符串第一個字符的地址,即首地址,可以將str看作是指針,所以str + 1, str + 2等用法是合法的,用來獲取字符串中某個字符的地址,即 * (str+2) 和 str[2] 是等價的

  字符串數組一旦定義,編譯器會將其內容存放在棧,可以通過指針去訪問和修改數組內容,訪問方式即通過(數組名+下標)或者通過(指針+偏移量),當字符串數組作爲右值時,與%s結合可以一次性輸出字符串(調用數組名即可)

#include <stdio.h>
//#include <string.h>
int main()
{
	//字符串數組
	char str1[] = "I love you!"; //當使用雙引號時,編譯器默認在後面加上\0
	printf("字符串數組的首地址爲:%p\n",str1);
	printf("第一個字符的值:%c\t地址爲:%p\n",*str1,str1);
	printf("第三個字符的值:%c\t地址爲:%p\n",*(str1+2),str1+2);
	
	return 0;
}

2.3 字符型指針

對於字符型指針 char* str = “I love you two!”;

  指針所指向的內容存放在靜態存儲區,即常量區。因此上式的字符型指針str也可以先定義char* str;然後再進行賦值 str = “I love you two!”; 這一點對於字符串數組不是合法的

  str 爲字符串中的首元素的地址,與%s結合可以打印全部字符,缺點就是裏面的內容不可修改,但是相比於字符串數組存放在棧區,通過字符型指針去訪問速度會更快

#include <stdio.h>
//#include <string.h>
int main()
{
	//字符型指針
	char* str2 = "I love you two!";
	printf("字符串數組的首地址爲:%p\n",str2);//即str2變量的值,存放着字符串首地址
	printf("第一個字符的值:%c\t地址爲:%p\n",*str2,str2);
	printf("第三個字符的值:%c\t地址爲:%p\n",*(str2+2),str2+2);
	
	printf("作爲右值可以一次性地賦值:%s\n",str2);
	
	return 0;
}

2.4 數組指針與指針數組

  指針數組的實質是一個數組,這個數組中存儲的內容全部是指針變量。數組指針的實質是一個指針,這個指針指向的是一個數組

符號 * 與 [] 的優先級:後者更高

① int *p[5]; 等價於int *(p[5]);
② int (*p)[5];

  第一個核心是p,p是一個數組,數組有5個元素,數組中的元素都是指針,指針指向的元素類型是int類型的;整個符號是一個指針數組

  第二個核心是p,p是一個指針,指針指向一個數組,數組有5個元素,數組中存的元素是int類型; 整個符號的意義就是數組指針

  對於指針數組,同一般數組一樣,數組名代表首個元素的地址,下面以字符型指針數組舉例,注意對比上文的字符型數組與字符型指針來使用;定義一個字符型指針數組,數組元素裏都是字符型指針,每一個元素的內容爲指向某個字符串的地址值,對數組的元素值進行解引用,就可以得到該字符串

#include <stdio.h>
//#include <string.h>
int main()
{
	//字符型指針數組
	char *str3[] = {"I love you!","I love you two!"};
	printf("字符型指針數組的首地址爲:%p\n\n",str3);
	
	//變量str3[0]爲字符型指針變量,變量的內容爲地址
	printf("str3[0]指向的內容爲:%s\n",str3[0]);
	printf("str3[0]字符型指針變量的內容爲:%p\n",str3[0]);
	printf("str3[0]指向內容的第三個字符爲:%c\n\n",*(str3[0]+2));
	
	printf("str3[1]指向的內容爲:%s\n",str3[1]);
	printf("str3[1]字符型指針變量的內容爲:%p\n",str3[1]);
	printf("str3[1]指向內容的第五個字符爲:%c\n",*(str3[1]+4));
	
	return 0;
}

  
  

3. 指針與函數傳參

3.1 數組作爲函數形參

  數組名作爲形參傳參時,實際傳遞的是數組的首元素的首地址(也就是整個數組的首地址),在子函數內部,傳進來的數組名就等於是一個指向數組首元素首地址的指針;這種特性叫做“傳址調用”,此時可以通過傳進去的地址來訪問實參
  1、數組作爲函數形參時,[] 裏的下標數字是可有可無的
  2、數組做函數形參時,也可以使用指針作爲形參代替,即使沒有數組的長度信息,只要偏移量不超過實際長度,都是合法的
  3、在子函數內部,也可以直接對外部的數組實參作修改

#include <stdio.h>
void test1(int a[])
{
	printf("test1:a[1]的值爲:%d\n\n",a[1]);
}

void test2(int b[5])
{
	printf("test2:a[1]的值爲:%d\n\n",b[1]);
}

void test3(int* c)
{
	printf("test3:a[1]的值爲:%d\n\n",*(c+1));
}

int main()
{
	int a[5] = {1,2,3,4,5};
	printf("main:a[1]的值爲:%d\n\n",a[1]);
	test1(a); test2(a); test3(a);
	
	return 0;
}

3.2 指針作爲函數形參

  在上面的demo裏的test3函數,就演示了,和數組作爲函數形參是一樣的,這就好像指針方式訪問數組元素和數組方式訪問數組元素的結果一樣是一樣的

3.3 結構體指針作爲函數形參

  結構體一般都很大,如果直接用結構體變量進行傳參,那麼函數調用效率就會很低。(因爲在函數傳參的時候需要將實參賦值給形參,所以當傳參的變量越大調用效率就會越低)

  可以使用指針來傳參,效率將大大提高;需要注意一點,當指針作爲形參時,比如struct Student *st,如果使用st.name點的方式訪問成員,會報錯,需要使用解引用的方式或者->的方式,如果使用解引用,則要注意結構體對齊方式,可以看我的上一篇博文 C語言結構體的內存存儲方式和字節對齊

#include <stdio.h>
struct Studet{
	int age;
	char sex;	//結構體對齊
};
void test(struct Studet *st)
{
	printf("st的大小爲:%ld\n",sizeof(st));
	printf("age of st: %d\n\n",st->age);
}

int main()
{
	struct Studet student = 
	{
		.age = 16,
		.sex = 'G',//gcc編譯器裏面的初始化寫法
	};
	
	printf("Studet的大小爲:%ld\n",sizeof(student));
	printf("age of studet: %d\n\n",student.age);
	
	test(&student);
	
	return 0;
}

  通過上面的展示可以看出,如果要在一個子函數裏面來改變傳進來的實參賦給形參的值(也就是主函數裏面的變量值),採用指針的方式就能解決這個問題,效率高

  
  

4. 對於函數的理解

  函數名是一個符號,表示整個函數代碼段的首地址,實質是一個指針常量,如果沒有形參列表和返回值,函數也能對數據進行操作,用全局變量即可;但是數參數傳參用的比較多,因爲這樣可以實現模塊化編程,而C語言中也是儘量減少使用全局變量

  如果參數很多,通常的做法是把很多參數打包成一個結構體,然後傳結構體變量指針進去;此時可以使用const來修飾形參,用法是const int *p;(意義是指針變量p本身可變的,而p所指向的變量是不可變的),以此來聲明在函數內部不會改變這個指針所指向的內容

  在典型的linux風格函數中,返回值是不用來返回結果的,而是用來返回0或者負數用來表示程序執行結果是對還是錯,是成功還是失敗;當函數需要返回多個值時,可以使用指針傳參的方式把數據的地址傳進子函數中,並且對處理數據的結果加以判斷,如果操作成功,則改變指針所指向數據的內容,並且返回成功的信息

#include <stdio.h>
struct B{
	int x;
	int y;
	short z;
}b = {1,1,1};

int test(int a , struct B *p)
{
	a = a*5;
	if(a>30) return -1;
	else{
		p->x = a;
		p->y = a+1;
		p->z = a+2;
		return 0;
	}
}
int main()
{
	if(0 == test(4,&b))
	{
		printf("成功!\n\n");
		printf("x = %d\n",b.x);
		printf("y = %d\n",b.y);
		printf("z = %d\n",b.z);
	}
	else{
		printf("錯誤!\n");
	}
	
	return 0;
}

4.1 指針函數和函數指針的區別

符號 () 和符號 * 的優先級:前者最大

① int *fun(int x);
② int (*fun)(int x);

通過判斷,可以知道①爲指針函數,②爲函數指針

  • 指針函數,簡單的來說,就是一個返回指針的函數,其本質是一個函數,而該函數的返回值是一個指針

注意:在調用指針函數時,需要一個同類型的指針來接收其函數的返回值

#include <stdio.h>
#include <stdlib.h>

typedef struct Data{
	int a;
	int b;
}Date;  //這裏的意思是把結構體類型重新命名成Date

//指針函數
Date * fun(int a,int b)
{
	Date* date = (Date *)malloc(sizeof(Date)) ;
	date->a = a;
	date->b = b;
	return date;
} 

int main(void)
{
	Date *myDate = fun(2,3);
	printf("myDate->a=%d\tmyDate->b=%d\n",myDate->a,myDate->b);

	return 0;
}
  • 函數指針,其本質是一個指針變量,該指針指向這個函數。總結來說,函數指針就是指向函數的指針,需要把一個函數的地址賦值給它,有兩種寫法:
    1、fun = &Function;
    2、fun = Function;
#include <stdio.h>
#include <stdlib.h>

int add(int x,int y)
{
	return (x+y);
}

int sub(int a,int b)
{
	return (a-b);
}

//函數指針 
int (*fun)(int x,int y);

int main(void)
{
	fun = add;
	printf("the (*fun)(2,3)is %d\n)",(*fun)(2,3));
	
	//另外一種寫法:
	fun = &sub;
	printf("the (*fun)(7,3)is %d\n)",(*fun)(7,3));

	return 0;
}

  
  

5. 結束

  如有錯誤,還望指正!🤞
  
  
  

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