1. 程式人生 > >你需要知道關於C語言指標的一切

你需要知道關於C語言指標的一切

Everything you need to know about pointers in C

 

你需要知道關於C語言指標的一切

指標的定義

 

指標是記憶體地址。
( 嗯,簡短的段落。)

 


開始

假設你宣告一個名為foo的變數。

int foo;

 

這個變數佔用一些記憶體。 在當前主流的Intel處理器上,它佔用四個位元組的記憶體(因為int是四個位元組寬)。
現在讓我們宣告另一個變數。

 

int *foo_ptr

= &foo;

 

foo_ptr被宣告為指向int的指標。我們已經初始化它指向foo。
正如我所說,foo佔據一些記憶。它在記憶體中的位置稱為其地址。 &foo是foo的地址(這就是為什麼&被稱為“地址操作符”)。
把每個變數想象成一個盒子。 foo是sizeof(int)位元組大小的盒子。此框的位置是其地址。當您訪問地址時,實際訪問它指向的框的內容。
這是所有變數的真實,不管型別。事實上,從語法上講,沒有像“指標變數”這樣的東西:所有的變數都是一樣的。但是,有不同型別的變數。 foo的型別是int.foo_ptr的型別是int *。 (因此,“指標變數”真的意味著“指標型別的變數”。)
這就是說,指標不是變數!指向foo的指標是foo_ptr的內容。你可以在foo_ptr框中放一個不同的指標,框仍然是foo_ptr。但它不會再指向foo.

 

指標也有一個型別,順便說一句。 它的型別是int。 因此,它是一個“int指標”(int指標)。 int **的型別是int *(它指向int的指標)。 指標對指標的使用稱為多重間接。 更多關於這一點。


插曲:宣告語法

在單個宣告中宣告兩個指標變數的明顯方法是:

int* ptr_a, ptr_b;

  • 如果包含指向int的指標的變數的型別是int *,
並且單個宣告可以通過簡單地提供逗號分隔列表(ptr_a,ptr_b)來宣告相同型別的多個變數,
那麼可以通過簡單地給出int指標型別(int *),然後使用逗號分隔的變數列表(ptr_a,ptr_b)來宣告多個int指標變數。
鑑於此,ptr_b的型別是什麼? int *,對不對?
* bzzt *錯誤!
ptr_b的型別為int。 它不是指標。
C的宣告語法忽略指標星號時攜帶型別到多個宣告。 如果你將ptr_a和ptr_b的宣告分成多個宣告,你會得到這樣:

 

int *ptr_a;int ptr_b;

 

可以把它看作一個基本型別(int),加上一個間接級別,用星號(ptr_b的值為0,ptr_a的值為1)表示。
有可能以清晰的方式做單行宣告。 這是立即改進:

 

int *ptr_a, ptr_b;

 

請注意,星號已移動。 它現在緊挨著詞ptr_a。 關聯的微妙含義。
將非指標變數放在第一位甚至更清楚:

 

int ptr_b, *ptr_a;

 

絕對最清楚的是保持每個宣告在其自己的線上,但是可以佔用很多垂直空間。 只使用你自己的判斷。
最後,我應該指出,你可以做到這一點很好:

 

int *ptr_a, *ptr_b;

 

沒有什麼問題。
順便說一下,C允許變數名和星號周圍的沒有或多個級別的括號:

 

int ((not_a_pointer)), (*ptr_a), (((*ptr_b)));

 

這對任何東西都沒有用,除了宣告函式指標(稍後描述)。
進一步閱讀:讀取C宣告的右左規則。

 


賦值和指標

現在,如何為這個指標指定一個int? 這個解決方案可能很明顯:

foo_ptr = 42;

 

這也是錯誤的。
對指標變數的任何直接賦值都將改變變數中的地址,而不是該地址處的值。 在這個例子中,foo_ptr的新值(即該變數中的新“指標”)為42.但我們不知道這指向任何東西,所以它可能不是。 嘗試訪問此地址可能會導致細分違規(閱讀:崩潰)。
(順便說一下,編譯器通常會在你試圖為一個指標變數賦值時發出警告,gcc將會說“warning:initialization讓指標從整型變為無轉義”。)
那麼如何在指標訪問值呢? 您必須取消引用它。

 


解除引用

int bar = *foo_ptr;

 

在此宣告中,取消引用運算子(字首*,不要與乘法運算子混淆)查詢存在於地址處的值。 (這被稱為“載入”操作。)
它也可以寫一個解引用表示式(C的方式說:解引用表示式是一個左值,意味著它可以出現在一個賦值的左邊):

 

*foo_ptr = 42; Sets foo to 42

(這被稱為“儲存”操作。)


插段:陣列

這裡是一個三int陣列的宣告:

int array[] = { 45, 67, 89 };

 

注意,我們使用[]符號,因為我們宣告一個數組。 int * array在這裡是非法的;編譯器不會接受我們為其分配{45,67,89}初始化器。
這個變數,陣列,是一個超大的盒子:三個int值的儲存。
C的一個整潔的特性是,在大多數地方,當你再次使用名稱陣列,你實際上將使用指向它的第一個元素的指標(在C術語,&array [0])。這被稱為“衰變”:陣列“衰減”為指標。陣列的大多數用法等於if陣列已被宣告為指標。
當然,有不等同的情況。一個是自己分配給名稱陣列(array = ...) - 這是非法的。
另一個是將其傳遞給sizeof運算子。結果將是陣列的總大小,而不是指標的大小(例如,使用上面的陣列的sizeof(array)將在當前Mac OS X上評估為(sizeof(int)= 4)×3 = 12系統)。這說明你真的處理一個數組,而不僅僅是一個指標。
然而,在大多數使用中,陣列表示式的工作方式與指標表示式相同。
所以,例如,讓我們說,你想傳遞一個數組到printf。你不能:當你傳遞一個數組作為引數到一個函式,你真的傳遞一個指標到陣列的第一個元素,因為陣列衰減到一個指標。你只能給printf指標,而不是整個陣列。 (這就是為什麼printf沒有辦法列印陣列:它需要你告訴它的陣列中的內容的型別和有多少個元素,並且格式字串和引數列表將很快令人困惑。)
腐爛是一種隱性的array ==&array ==&array [0]。在英語中,這些表示式讀取“array”,“指向陣列的指標”和“指向陣列的第一個元素的指標”(下標運算子[])的優先順序高於操作符的地址。但在C中,所有三個表示式都意味著相同的東西。
(如果“array”實際上是一個指標變數,那麼它們並不意味著相同的事情,因為指標變數的地址不同於其中的地址 - 因此,中間表示式&array不會等於另外兩個表示式。只有當陣列真的是一個數組時,三個表示式都是相等的。)

 


指標算術(或:為什麼1 == 4)

假設我們要打印出陣列的所有三個元素。

int *array_ptr = array;printf(" first element: %i\n", *(array_ptr++));printf("second element: %i\n", *(array_ptr++));printf(" third element: %i\n", *array_ptr);

first element: 45second element: 67 third element: 89

 

如果你不熟悉++操作符:它加1到一個變數,同變數+ = 1(記住,因為我們使用字尾表示式array_ptr ++,而不是字首表示式++ array_ptr,表示式計算到array_ptr之前的值增加,而不是之後)。
但是我們在這裡做了什麼?
嗯,指標的型別很重要。這裡的指標型別是int。當您新增到指標或從指標減去時,您執行的量乘以指標型別的大小。在我們的三個增量的情況下,您新增的每個1乘以sizeof(int)。
順便說一句,雖然sizeof(void)是非法的,void指標遞增或遞減1個位元組。
如果你想知道1 == 4:記住,早些時候,我提到int是目前的英特爾處理器的四個位元組。因此,在具有這樣的處理器的機器上,從int指標加1或減1,將其改變4個位元組。因此,1 == 4.(程式設計師幽默。)

 


索引

printf("%i\n", array[0]);

 

好吧...剛剛發生了什麼?
這發生過:

 

45

 

好吧,你可能想到了。 但是這與指標有什麼關係?
這是C的另一個祕密。下標運算子(陣列[0]中的[])與陣列無關。
哦,當然,這是它最常見的用法。 但請記住,在大多數上下文中,陣列衰減到指標。 這是其中之一:這是一個傳遞給該運算子的指標,而不是陣列。
作為證據,我提交:

 

int array[] = { 45, 67, 89 };int *array_ptr = &array[1];printf("%i\n", array_ptr[1]);

89

那可能會大腦彎曲一點。 這是一個圖:

 

 

陣列指向陣列的第一個元素; array_ptr設定為&array [1],因此它指向陣列的第二個元素。 因此,array_ptr [1]等價於array [2](array_ptr從陣列的第二個元素開始,因此array_ptr的第二個元素是陣列的第三個元素)。
另外,你可能會注意到,因為第一個元素是sizeof(int)位元組寬(是一個int),第二個元素是sizeof(int)位元組前面的陣列的開始。 你是正確的:array [1]相當於*(array + 1)。 (記住,新增到指標或從指標減去的數字乘以指標型別的大小,因此“1”將sizeof(int)位元組新增到指標值。)

 


插曲:結構和聯合

 

C中最有趣的兩種型別是結構和聯合。 您使用struct關鍵字建立一個結構型別,並使用union關鍵字建立聯合型別。
這些型別的確切定義超出了本文的範圍。 只需說一個結構或聯合的宣告就像這樣:

 

struct foo {size_t size;char name[64];int answer_to_ultimate_question;unsigned shoe_size;};

塊中的每個宣告都稱為成員。 聯合也有成員,但是使用方式不同。 訪問成員如下所示:

struct foo my_foo;my_foo.size = sizeof(struct foo);

 

表示式my_foo.size訪問my_foo的成員大小。
那麼,如果你有一個結構的指標,你該怎麼辦?

 

One way to do it(*foo_ptr).size = new_size;

但是有一個更好的方法,專門為此目的:指標到成員運算子。

Yummyfoo_ptr->size = new_size;

不幸的是,它並不看起來好多多間接。

Icky(*foo_ptr_ptr)->size = new_size; One way(**foo_ptr_ptr).size = new_size; or another

抱怨:Pascal做得更好。 它的dereference運算子是字尾^:

Yummyfoo_ptr_ptr^^.size := new_size;

(但拋開這個抱怨,C是一個更好的語言。)


多級間接地址

 

 

我想更多地解釋多個間接地址
考慮下面的程式碼:

 

int a = 3;int *b = &a;int **c = &b;int ***d = &c;

下面是這些指標的值如何相等:

  • *d == c; 解除引用an (int ***) once gets you an (int **) (3 - 1 = 2)
  • **d == *c == b; 解除引用an (int ***) twice, or an (int **) once, gets you an (int *) (3 - 2 = 1; 2 - 1 = 1)
  • ***d == **c == *b == a == 3; 解除引用 an (int ***) thrice, or an (int **) twice, or an (int *) once, gets you an int (3 - 3 = 0; 2 - 2 = 0; 1 - 1 = 0)

因此,&運算子可以被認為是新增星號(增加指標級別,因為我稱之為),和*, - >和[]運算子作為刪除星號(減少指標水平)。


指標和const

當涉及指標時,const關鍵字有點不同。 這兩個宣告是等效的:

const int *ptr_a;int const *ptr_a;

然而,這兩個不是等價的:

int const *ptr_a;int *const ptr_b;

在第一個例子中,int(即* ptr_a)是const; 你不能做* ptr_a = 42。在第二個例子中,指標本身是const; 你可以改變* ptr_b很好,但你不能改變(使用指標算術,例如ptr_b ++)指標本身。


函式指標

注意:所有這些的語法似乎有點異國情調。 它是。 它混淆了很多人,甚至C的騎士。 熊與我。

 

也可以取一個函式的地址。 並且,與陣列類似,當使用它們的名稱時,函式衰減到指標。 所以如果你想要的地址,說,strcpy,你可以說strcpy或&strcpy。 (&strcpy [0]不會工作,很明顯的原因。)
當呼叫函式時,使用一個稱為函式呼叫操作符的操作符。 函式呼叫操作符在其左側有一個函式指標。
在這個例子中,我們將dst和src作為內部引數傳遞,並將strcpy作為函式(即函式指標)呼叫:

 

enum { str_length = 18U }; Remember the NUL terminator!char src[str_length] = "This is a string.", dst[str_length];strcpy(dst, src); The function call operator in action (notice the function pointer on the left side).

有一個特殊的語法用於宣告型別為函式指標的變數。

char *strcpy(char *dst, const char *src); 一個普通的函式宣告,供參考char *(*strcpy_ptr)(char *dst, const char *src); Pointer to strcpy-like functionstrcpy_ptr = strcpy;strcpy_ptr = &strcpy; This works toostrcpy_ptr = &strcpy[0]; But not this

 

請注意上面宣告中* strcpy_ptr周圍的括號。 這些從星號指示返回型別(char *)的星號指示變數的指標級別(* strcpy_ptr - 一個級別,指向函式的指標)。
此外,就像在常規函式宣告中一樣,引數名稱是可選的:

 

char *(*strcpy_ptr_noparams)(char *, const char *) = strcpy_ptr; Parameter names removed — still the same type

指向strcpy的指標的型別是char *(*)(char *,const char *); 你可能會注意到這是上面的宣告,減去變數名。 你可以在轉換中使用它。 例如:

strcpy_ptr = (char *(*)(char *dst, const char *src))my_strcpy;

正如你所期望的,指向函式的指標的指標在括號內有兩個星號:

char *(**strcpy_ptr_ptr)(char *, const char *) = &strcpy_ptr;

我們可以有一個函式指標陣列:

char *(*strcpies[3])(char *, const char *) = { strcpy, strcpy, strcpy };char *(*strcpies[])(char *, const char *) = { strcpy, strcpy, strcpy }; Array size is optional, same as everstrcpies[0](dst, src);

這是一個病理宣告,取自C99標準。 “[這個宣告]宣告一個沒有引數返回int的函式f,沒有返回指向int的引數的引數指定的函式fip和一個沒有返回int的引數指定的函式的pointerpfi”“(6.7.5.3 [ 16])

int f(void), *fip(), (*pfi)();

換句話說,上面的等價於以下三個宣告:

int f(void);int *fip(); Function returning int pointerint (*pfi)(); Pointer to function returning int

但如果你認為這是心靈彎曲,支撐自己...


 

函式指標甚至可以是函式的返回值。 這部分是真正的心靈彎曲,所以伸展你的大腦有點,以免造成傷害。
為了解釋這一點,我將總結你迄今為止學到的所有宣告語法。 首先,宣告一個指標變數:

 

char *ptr;

這個宣告告訴我們指標型別(char),指標級(*)和變數名(ptr)。 後兩個可以進括號:

char (*ptr);

如果我們用名稱後面跟一組引數替換第一個宣告中的變數名,會發生什麼?

char *strcpy(char *dst, const char *src);

 

嗯。 函式宣告。
但是我們也刪除了*指示指標級別 - 記住這個函式宣告中的*是函式返回型別的一部分。 所以如果我們新增指標級星號回來(使用括號):

 

char *(*strcpy_ptr)(char *dst, const char *src);

 

一個函式指標變數!
但等一下。 如果這是一個變數,並且第一個宣告也是一個變數,我們不能用一個名稱和一組引數替換THIS宣告中的變數名稱?
我們可以! 結果是返回一個函式指標的函式的宣告:

 

char *(*get_strcpy_ptr(void))(char *dst, const char *src);

 

請記住,指向不帶引數並返回int的函式的指標的型別是int(*)(void)。 所以這個函式返回的型別是char *(*)(char *,const char *)(同樣,inner *表示指標,outer *表示指向函式的返回型別的一部分) 。 你可能還記得這也是strcpy_ptr的型別。
所以這個沒有引數呼叫的函式返回一個指向strcpy-like函式的指標:

 

strcpy_ptr = get_strcpy_ptr();

因為函式指標語法是如此令人費解,大多數開發人員使用typedef來抽象它們:

typedef char *(*strcpy_funcptr)(char *, const char *);strcpy_funcptr strcpy_ptr = strcpy;strcpy_funcptr get_strcpy_ptr(void);


字串(和為什麼沒有這樣的東西)

 

C中沒有字串型別。
現在你有兩個問題:
1.如果沒有字串型別,為什麼我總是看到對“C字串”的引用?
2.這與指標有什麼關係?
事實是,“C字串”的概念是虛構的(除了字串字面量)。 沒有字串型別。 C字串實際上只是字元陣列:

 

char str[] = "I am the Walrus";

 

此陣列的長度為16個位元組:“I am the Walrus”為15個字元,加上NUL(位元組值為0)終止符。 換句話說,str [15](最後一個元素)是0.這是如何“訊號”的結尾。
此成語是C具有字串型別的程度。 但這就是:成語。 除了它支援:
.前面提到的字串文字語法
.字串庫
string.h中的函式用於字串操作。 但是怎麼可能,如果沒有字串型別?
為什麼,他們工作指標。
這裡有一個簡單函式strlen的一個可能的實現,它返回一個字串(不包括NUL終止符)的長度:

 

size_t strlen(const char *str) { Note the pointer syntax heresize_t len = 0U;while(*(str++)) ++len;return len;}

 

注意使用指標運算和取消引用。 這是因為,儘管函式的名字,這裡沒有“字串”; 只有一個指向至少一個字元的指標,最後一個為0。
這裡有另一個可能的實現:

 

size_t strlen(const char *str) {size_t i;for(i = 0U; str[i]; ++i);When the loop exits, i is the length of the stringreturn i;}

那一個使用索引。 其中,正如我們早先發現的,使用指標(不是陣列,絕對不是字串)。

 

 

再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://www.cnblogs.com/captainbed