python assert斷言函式
定義指標變數與定義普通變數非常類似,不過要在變數名前面加星號*
,格式為:
或者
datatype *name = value;
*
表示這是一個指標變數,datatype
表示該指標變數所指向的資料的型別 。例如:
int *p1;
p1 是一個指向 int 型別資料的指標變數,至於 p1 究竟指向哪一份資料,應該由賦予它的值決定。再如:
int a = 100;
int *p_a = &a;
在定義指標變數 p_a 的同時對它進行初始化,並將變數 a 的地址賦予它,此時 p_a 就指向了 a。值得注意的是,p_a 需要的一個地址,a 前面必須要加取地址符&
和普通變數一樣,指標變數也可以被多次寫入,只要你想,隨時都能夠改變指標變數的值,請看下面的程式碼:
//定義普通變數
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定義指標變數
float *p1 = &a;char *p2 = &c;
//修改指標變數的值
p1 = &b;
p2 = &d;
*
是一個特殊符號,表明一個變數是指標變數,定義 p1、p2 時必須帶*
。而給 p1、p2 賦值時,因為已經知道了它是一個指標變數,就沒必要多此一舉再帶上*
,後邊可以像使用普通變數一樣來使用指標變數。也就是說,定義指標變數時必須帶*
*
。
需要強調的是,p1、p2 的型別分別是float*
和char*
,而不是float
和char
,它們是完全不同的資料型別,讀者要引起注意。
指標變數儲存了資料的地址,通過指標變數能夠獲得該地址上的資料,格式為:
*pointer;
這裡的*
稱為指標運算子,用來取得某個地址上的資料,請看下面的例子:
純文字複製
CPU 讀寫資料必須要知道資料在記憶體中的地址,普通變數和指標變數都是地址的助記符,雖然通過 *p 和 a 獲取到的資料一樣,但它們的執行過程稍有不同:a 只需要一次運算就能夠取得資料,而 *p 要經過兩次運算,多了一層“間接”。
假設變數 a、p 的地址分別為 0X1000、0XF0A0,它們的指向關係如下圖所示:
程式被編譯和連結後,a、p 被替換成相應的地址。使用 *p 的話,要先通過地址 0XF0A0 取得變數 p 本身的值,這個值是變數 a 的地址,然後再通過這個值取得變數 a 的資料,前後共有兩次運算;而使用 a 的話,可以通過地址 0X1000 直接取得它的資料,只需要一步運算。
也就是說,使用指標是間接獲取資料,使用變數名是直接獲取資料,前者比後者的代價要高。
*
在不同的場景下有不同的作用:*
可以用在指標變數的定義中,表明這是一個指標變數,以和普通變數區分開;使用指標變數時在前面加*
表示獲取指標指向的資料,或者說表示的是指標指向的資料本身。
指標除了可以獲取記憶體上的資料,也可以修改記憶體上的資料,例如:
執行結果: 99, 99, 99, 99
*p 代表的是 a 中的資料,它等價於 a,可以將另外的一份資料賦值給它,也可以將它賦值給另外的一個變數。
也就是說,定義指標變數時的*
和使用指標變數時的*
意義完全不同。以下面的語句為例:
int *p = &a;
*p = 100;
第1行程式碼中*
用來指明 p 是一個指標變數,第2行程式碼中*
用來獲取指標指向的資料。
需要注意的是,給指標變數本身賦值時不能加*
。修改上面的語句:
int *p;
p = &a;
*p = 100;
第2行程式碼中的 p 前面就不能加*
。
指標變數也可以出現在普通變數能出現的任何表示式中,例如:
純文字複製
int x, y, *px = &x, *py = &y;
y = *px + 5; //表示把x的內容加5並賦給y,*px+5相當於(*px)+5
y = ++*px; //px的內容加上1之後賦給y,++*px相當於++(*px)
y = *px++; //相當於y=*(px++)
py = px; //把一個指標的值賦給另一個指標
關於 * 和 & 的謎題
假設有一個 int 型別的變數 a,pa 是指向它的指標,那麼*&a
和&*pa
分別是什麼意思呢?
*&a
可以理解為*(&a)
,&a
表示取變數 a 的地址(等價於 pa),*(&a)
表示取這個地址上的資料(等價於 *pa),繞來繞去,又回到了原點,*&a
仍然等價於 a。
&*pa
可以理解為&(*pa)
,*pa
表示取得 pa 指向的資料(等價於 a),&(*pa)
表示資料的地址(等價於 &a),所以&*pa
等價於 pa。
對星號*
的總結
在我們目前所學到的語法中,星號*
主要有三種用途:
-
表示乘法,例如
int a = 3, b = 5, c; c = a * b;
,這是最容易理解的。 -
表示定義一個指標變數,以和普通變數區分開,例如
int a = 100; int *p = &a;
。 -
表示獲取指標指向的資料,是一種間接操作,例如
int a, b, *p = &a; *p = 100; b = *p;
。
不能對指標變數進行乘法、除法、取餘等其他運算,除了會發生語法錯誤,也沒有實際的含義。
陣列(Array)是一系列具有相同型別的資料的集合,每一份資料叫做一個數組元素(Element)。陣列中的所有元素在記憶體中是連續排列的,整個陣列佔用的是一塊記憶體。以int arr[] = { 99, 15, 100, 888, 252 };
為例,該陣列在記憶體中的分佈如下圖所示:
定義陣列時,要給出陣列名和陣列長度,陣列名可以認為是一個
陣列名的本意是表示整個陣列,也就是表示多份資料的集合,但在使用過程中經常會轉換為指向陣列第 0 個元素的指標,所以上面使用了“認為”一詞,表示陣列名和陣列首地址並不總是等價。初學者可以暫時忽略這個細節,把陣列名當做指向第 0 個元素的指標使用即可
下面的例子演示瞭如何以指標的方式遍歷陣列元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int len = sizeof(arr) / sizeof(int); //求陣列長度
int i;
for(i=0; i<len; i++){
printf("%d ", *(arr+i) ); //*(arr+i)等價於arr[i]
}
printf("\n");
return 0;
}
執行結果: 99 15 100 888 252
第 8 行程式碼中我們使用了*(arr+i)
這個表示式,arr 是陣列名,指向陣列的第 0 個元素,表示陣列首地址, arr+i 指向陣列的第 i 個元素,*(arr+i) 表示取第 i 個元素的資料,它等價於 arr[i]。
我們也可以定義一個指向陣列的指標,例如:
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;
arr 本身就是一個指標,可以直接賦值給指標變數 p。arr 是陣列第 0 個元素的地址,所以int *p = arr;
也可以寫作int *p = &arr[0];
。也就是說,arr、p、&arr[0] 這三種寫法都是等價的,它們都指向陣列第 0 個元素,或者說指向陣列的開頭。
如果一個指標指向了陣列,我們就稱它為陣列指標(Array Pointer)。
陣列指標指向的是陣列中的一個具體元素,而不是整個陣列,所以陣列指標的型別和陣列元素的型別有關,上面的例子中,p 指向的陣列元素是 int 型別,所以 p 的型別必須也是int *
。
反過來想,p 並不知道它指向的是一個數組,p 只知道它指向的是一個整數,究竟如何使用 p 取決於程式設計師的編碼。
更改上面的程式碼,使用陣列指標來遍歷陣列元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++){
printf("%d ", *(p+i) );
}
printf("\n");
return 0;
}
陣列在記憶體中只是陣列元素的簡單排列,沒有開始和結束標誌,在求陣列的長度時不能使用sizeof(p) / sizeof(int)
,因為 p 只是一個指向 int 型別的指標,編譯器並不知道它指向的到底是一個整數還是一系列整數(陣列),所以 sizeof(p) 求得的是 p 這個指標變數本身所佔用的位元組數,而不是整個陣列佔用的位元組數。
C語言字串指標(指向字串的指標)
C語言中沒有特定的字串型別,我們通常是將字串放在一個字元陣列中
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "http://c.biancheng.net";
int len = strlen(str), i; //直接輸出字串
printf("%s\n", str); //每次輸出一個字元
for(i=0; i<len; i++){
printf("%c", str[i]);
}
printf("\n");
return 0;
}
執行結果: http://c.biancheng.net http://c.biancheng.net
除了字元陣列,C語言還支援另外一種表示字串的方法,就是直接使用一個指標指向字串,例如:
char *str = "http://c.biancheng.net";
或者:
char *str;
str = "http://c.biancheng.net";
字串中的所有字元在記憶體中是連續排列的,str 指向的是字串的第 0 個字元;我們通常將第 0 個字元的地址稱為字串的首地址。字串中每個字元的型別都是char
,所以 str 的型別也必須是char *
。
這一切看起來和字元陣列是多麼地相似,它們都可以使用%s
輸出整個字串,都可以使用*
或[ ]
獲取單個字元,這兩種表示字串的方式是不是就沒有區別了呢?
有!它們最根本的區別是在記憶體中的儲存區域不一樣,字元陣列儲存在全域性資料區或棧區,第二種形式的字串儲存在常量區。全域性資料區和棧區的字串(也包括其他資料)有讀取和寫入的許可權,而常量區的字串(也包括其他資料)只有讀取許可權,沒有寫入許可權。
記憶體許可權的不同導致的一個明顯結果就是,字元陣列在定義後可以讀取和修改每個字元,而對於第二種形式的字串,一旦被定義後就只能讀取不能修改,任何對它的賦值都是錯誤的。
我們將第二種形式的字串稱為字串常量,意思很明顯,常量只能讀取不能寫入。請看下面的演示:
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "I love C!"; //正確
str[3] = 'P'; //錯誤
return 0;
}
這段程式碼能夠正常編譯和連結,但在執行時會出現段錯誤(Segment Fault)或者寫入位置錯誤。
第4行程式碼是正確的,可以更改指標變數本身的指向;第5行程式碼是錯誤的,不能修改字串中的字元。
C語言指標變數作為函式引數
在C語言中,函式的引數不僅可以是整數、小數、字元等具體的資料,還可以是指向它們的
像陣列、字串、動態分配的記憶體等都是一系列資料的集合,沒有辦法通過一個引數全部傳入函式內部,只能傳遞它們的指標,在函式內部通過指標來影響這些資料集合。
有的時候,對於整數、小數、字元等基本型別資料的操作也必須要藉助指標,一個典型的例子就是交換兩個變數的值
用陣列作函式引數
陣列是一系列資料的集合,無法通過引數將它們一次性傳遞到函式內部,如果希望在函式內部運算元組,必須傳遞陣列指標。下面的例子定義了一個函式 max(),用來查詢陣列中值最大的元素:
#include <stdio.h>
int max(int *intArr, int len){
int i, maxValue = intArr[0]; //假設第0個元素是最大值
for(i=1; i<len; i++){
if(maxValue < intArr[i]){
maxValue = intArr[i];
}
}
return maxValue;
}
int main(){
int nums[6], i;
int len = sizeof(nums)/sizeof(int); //讀取使用者輸入的資料並賦值給陣列元素
for(i=0; i<len; i++){
scanf("%d", nums+i);
}
printf("Max value is %d!\n", max(nums, len));
return 0;
}
執行結果: 12 55 30 8 93 27↙ Max value is 93!
C語言為什麼不允許直接傳遞陣列的所有元素,而必須傳遞陣列指標呢?
引數的傳遞本質上是一次賦值的過程,賦值就是對記憶體進行拷貝。所謂記憶體拷貝,是指將一塊記憶體上的資料複製到另一塊記憶體上。
對於像 int、float、char 等基本型別的資料,它們佔用的記憶體往往只有幾個位元組,對它們進行記憶體拷貝非常快速。而陣列是一系列資料的集合,資料的數量沒有限制,可能很少,也可能成千上萬,對它們進行記憶體拷貝有可能是一個漫長的過程,會嚴重拖慢程式的效率,為了防止技藝不佳的程式設計師寫出低效的程式碼,C語言沒有從語法上支援資料集合的直接賦值。
C語言指標作為函式返回值
言允許函式的返回值是一個
純文字複製
#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){
if(strlen(str1) >= strlen(str2)){
return str1;
}
else{
return str2;
}
}
int main(){
char str1[30], str2[30], *str;
gets(str1);
gets(str2);
str = strlong(str1, str2);
printf("Longer string: %s\n", str);
return 0;
}
用指標作為函式返回值時需要注意的一點是,函式執行結束後會銷燬在它內部定義的所有區域性資料,包括區域性變數、區域性陣列和形式引數,函式返回的指標請儘量不要指向這些資料,C語言沒有任何機制來保證這些資料會一直有效,它們在後續使用過程中可能會引發執行時錯誤。請看下面的例子:
#include <stdio.h>int *func(){ int n = 100; return &n;}int main(){ int *p = func(), n; n = *p; printf("value = %d\n", n); return 0;}
執行結果:
value = 100
n 是 func() 內部的區域性變數,func() 返回了指向 n 的指標,根據上面的觀點,func() 執行結束後 n 將被銷燬,使用 *p 應該獲取不到 n 的值。但是從執行結果來看,我們的推理好像是錯誤的,func() 執行結束後 *p 依然可以獲取區域性變數 n 的值,這個上面的觀點不是相悖嗎?
為了進一步看清問題的本質,不妨將上面的程式碼稍作修改,在第9~10行之間增加一個函式呼叫,看看會有什麼效果:
#include <stdio.h>
int *func(){
int n = 100;
return &n;
}
int main(){
int *p = func(), n;
printf("c.biancheng.net\n");
n = *p;
printf("value = %d\n", n);
return 0;
}
執行結果:
c.biancheng.net value = -2
可以看到,現在 p 指向的資料已經不是原來 n 的值了,它變成了一個毫無意義的甚至有些怪異的值。與前面的程式碼相比,該段程式碼僅僅是在 *p 之前增加了一個函式呼叫
C語言二級指標(指向指標的指標)詳解
如果一個指標指向的是另外一個指標,我們就稱它為二級指標,或者指向指標的指標。
假設有一個 int 型別的變數 a,p1是指向 a 的指標變數,p2 又是指向 p1 的指標變數,它們的關係如下圖所示:
將這種關係轉換為C語言程式碼:
int a =100;int *p1 = &a;int **p2 = &p1;
指標變數也是一種變數,也會佔用儲存空間,也可以使用&
獲取它的地址。C語言不限制指標的級數,每增加一級指標,在定義指標變數時就得增加一個星號*
。p1 是一級指標,指向普通型別的資料,定義時有一個*
;p2 是二級指標,指向一級指標 p1,定義時有兩個*
。
C語言指標陣列(陣列每個元素都是指標)詳解
指標陣列還可以和字串陣列結合使用,請看下面的例子:
#include <stdio.h>
int main(){
char *str[3] = {
"c.biancheng.net",
"C語言中文網",
"C Language" };
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
執行結果: c.biancheng.net C語言中文網 C Language
需要注意的是,字元陣列 str 中存放的是字串的首地址,不是字串本身,字串本身位於其他的記憶體區域,和字元陣列是分開的。
也只有當指標陣列中每個元素的型別都是char *
時,才能像上面那樣給指標陣列賦值,其他型別不行。
C語言二維陣列指標(指向二維陣列的指標)詳解
int a
從概念上理解,a 的分佈像一個矩陣:
0 1 2 3
4 5 6 7
8 9 10 11
但在記憶體中,a 的分佈是一維線性的,整個陣列佔用一塊連續的記憶體:
C語言中的二維陣列是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最後存放 a[2] 行;每行中的 4 個元素也是依次存放。陣列 a 為 int 型別,每個元素佔用 4 個位元組,整個陣列共佔用 4×(3×4) = 48 個位元組。
C語言允許把一個二維陣列分解成多個一維陣列來處理。對於陣列 a,它可以分解成三個一維陣列,即 a[0]、a[1]、a[2]。每一個一維陣列又包含了 4 個元素,例如 a[0] 包含 a
假設陣列 a 中第 0 個元素的地址為 1000,那麼每個一維陣列的首地址如下圖所示:
為了更好的理解
int (*p)[4] = a;
括號中的*
表明 p 是一個指標,它指向一個數組,陣列的型別為int [4]
,這正是 a 所包含的每個一維陣列的型別。
[ ]
的優先順序高於*
,( )
是必須要加的,如果赤裸裸地寫作int *p[4]
,那麼應該理解為int *(p[4])
,p 就成了一個指標陣列,而不是二維陣列指標,這在《
對指標進行加法(減法)運算時,它前進(後退)的步長與它指向的資料型別有關,p 指向的資料型別是int [4]
,那麼p+1
就前進 4×4 = 16 個位元組,p-1
就後退 16 個位元組,這正好是陣列 a 所包含的每個一維陣列的長度。也就是說,p+1
會使得指標指向二維陣列的下一行,p-1
會使得指標指向陣列的上一行。
陣列名 a 在表示式中也會被轉換為和 p 等價的指標!
下面我們就來探索一下如何使用指標 p 來訪問二維陣列中的每個元素。按照上面的定義: \1) p
指向陣列 a 的開頭,也即第 0 行;p+1
前進一行,指向第 1 行。
\2) *(p+1)
表示取地址上的資料,也就是整個第 1 行資料。注意是一行資料,是多個數據,不是第 1 行中的第 0 個元素,下面的執行結果有力地證明了這一點:
#include <stdio.h>int main(){ int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; int (*p)[4] = a; printf("%d\n", sizeof(*(p+1))); return 0;}
執行結果: 16
\3) *(p+1)+1
表示第 1 行第 1 個元素的地址。如何理解呢?
*(p+1)
單獨使用時表示的是第 1 行資料,放在表示式中會被轉換為第 1 行資料的首地址,也就是第 1 行第 0 個元素的地址,因為使用整行資料沒有實際的含義,編譯器遇到這種情況都會轉換為指向該行第 0 個元素的指標;就像一維陣列的名字,在定義時或者和 sizeof、& 一起使用時才表示整個陣列,出現在表示式中就會被轉換為指向陣列第 0 個元素的指標。
\4) *(*(p+1)+1)
表示第 1 行第 1 個元素的值。很明顯,增加一個 * 表示取地址上的資料。
根據上面的結論,可以很容易推出以下的等價關係:
a+i == p+i a[i] == p[i] == *(a+i) == *(p+i) a
【例項】使用指標遍歷二維陣列。
#include <stdio.h>
int main(){
int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
int(*p)[4];
int i,j;
p=a;
for(i=0; i<3; i++){
for(j=0; j<4; j++)
printf("%2d ",*(*(p+i)+j));
printf("\n");
}
return 0;
}
執行結果:
0 1 2 3
4 5 6 7
8 9 10 11
指標陣列和二維陣列指標的區別
指標陣列和二維陣列指標在定義時非常相似,只是括號的位置不同:
int *(p1[5]); //指標陣列,可以去掉括號直接寫作 int *p1[5];int (*p2)[5]; //二維陣列指標,不能去掉括號