關於指針、數組、字元串的恩怨,這裡有你想知道的一切 記憶體組成、字元串定義、一/二維數組結構、數組中的指針等價關係、數組結構中對“指針常量”的理解、 指針 vs 數組 記憶體結構一圖流、One More Thing:當二維數組遇見qsort()庫函數,關於比較函數cmp的迷思 ...
關於指針、數組、字元串的恩怨,這裡有你想知道的一切
目錄記憶體組成
堆區
堆區 (Heap):由程式員手動申請釋放的記憶體空間。
- C中:
malloc()
和colloc()
函數申請,用free()
釋放
若不用
free()
釋放,容易造成記憶體泄露(即記憶體被浪費、耗盡)。
-
ptr = (castType*) malloc(size);
傳入參數為記憶體的位元組數,記憶體未被初始化。
-
ptr = (castType*)calloc(n, size);
存入參數為記憶體塊數與每塊位元組數,記憶體初始化為
0
。 -
free(ptr);
釋放申請的記憶體。
- C++中:
new
申請,delete
釋放。new
和delete
都是操作符
int *arr = new int[10];
delete[] arr;
棧區
棧區 (Stack):由系統管理,存放函數參數與局部變數。函數完成執行,系統自行釋放棧區記憶體。
靜態存儲區
靜態存儲區 (Static Storage Area):在編譯階段分配好記憶體空間並初始化。
其中全局區存放靜態變數(static
修飾的變數)、全局變數(具有全局作用域的變數);常量區存放常量(又稱為字面量)。
常量可分為整數常量(如1000L)、浮點常量(如314158E-5L)、字元常量(如'A'、'\n')和字元串常量(如"Hello")。
const
關鍵字修飾的的變數無法修改,但存放的位置取決於變數本身是全局變數還是局部變數。當修飾的變數是全局變數,則放在全局區,否則依然在棧區分配。
static
關鍵字修飾的變數存在全局區的靜態變數區。
常變數與巨集定義的概念不同。
常變數存儲在靜態存儲區,初始化後無法修改。
巨集定義在預處理階段就被替換。不存在與任何記憶體區域。
代碼區
代碼區 (Code Segment):存放程式體的二進位代碼。
/*示例代碼*/
int a = 0; //靜態全局變數區
char *p1; //編譯器預設初始化為NULL,存在靜態全局變數區
void main()
{
int b; //棧
char s[] = "abc"; //棧
char *p1 = "123"; //"123"在字元串常量區,p1在棧區
p2 = (char *)malloc(10); //堆區
strcpy(p2, "123"); //"123"放在字元串常量區
const int d = 0; //棧
static int c = 0; //c在靜態變數區,0為文字常量,在代碼區
static const int d; //靜態常量區
}
字元串定義 - 一維
char s[10] = "Hello"
記憶體:靜態存儲區上的字面量"Hello"
被覆制到棧區,數組在棧區上的存儲方式為'H''e''l''l''o''\0'
,可以通過s[i]
修改。但這不會影響到靜態存儲區上的"Hello"
。
定義與使用:
#include <stdio.h>
void f(char s[10]) { //等價於char *s
printf("%s\n", s);
}
int main() {
char s[10] = "LeeHero";
s[3] = 'Z';
printf("%s\n", s); //輸出:LeeZero
printf("%s\n", s+1); //輸出:eeZero
printf("%c\n", s[3]);//輸出:Z
f(s); //數組名作為函數參數傳遞時,會退化成指向數組首元素的指針 !IMPORTANT
return 0;
}
格式控制符
%s
跟隨一個地址,並當做是字元串第一個元素對應的地址.從該首地址開始解析,直到
'\0'
結束。在這裡指的是
s[0] = 'H'
的地址。
char *s = "Hello"
// 等價於const char *s = "Hello"
記憶體:s是指向字面量"Hello"
的指針,字面量在靜態記憶體區,因此該字元串不可被修改。
定義與使用:
#include <stdio.h>
void f(char s[10]) { //等價於char *s
printf("%s\n", s);
}
int main() {
char *s = "LeeHero";
//s[3] = 'Z'; //無法執行
printf("%s\n", s); //輸出:LeeHero
printf("%s\n", s+1); //輸出:eeHero
printf("%c\n", s[3]); //輸出:H
f(s);
return 0;
}
字元串定義 - 二維
char s[10][10] = {"Hello","World"}
記憶體:靜態存儲區上的字面量"Hello"
,"World"
被拷貝在棧區,與一維定義方式同理,可以通過語法糖s[i][j]
修改字元。
定義與使用:
#include <stdio.h>
void f(char (*s)[10]) { //形參s是個指針,指向有10個元素的字元數組
//把(*s)[10] 改成 s[][10] ,其他不變,最後效果相同
printf("%s\n", s[1]); //輸出:Zero
s[1][0] = 'H'; //通過語法糖s[i][j]修改字元
printf("%s\n", s[1]); //輸出:Hero
printf("%c\n", s[0][1]); //輸出:e
}
int main() {
char s[10][10] = {"Lee","Hero"};
//s[1] = "Hey"; //無法執行,這種賦值方式僅在初始化時可用
s[1][0] = 'Z';
printf("%s\n", s); //輸出:Lee
printf("%s\n", *s+1); //輸出:ee
printf("%s\n", s[0]+1); //輸出:ee
printf("%c\n", *(s[0]+1)); //輸出:e
printf("%c\n", s[0][1]); //輸出:e
printf("%s\n", s+1); //輸出:Zero
printf("%s\n", s[1]); //輸出:Zero
f(s);
printf("%s\n", s[1]); //輸出:Hero 這意味著函數內部的修改不是局部生效的
return 0;
}
對於列印結果的一些解釋:
· 對二維數組進行操作與輸出
s
等價於&s[0]
,是指向[存儲"Lee"
的一維數組]的指針
s+1
等價於&s[1]
,是指向[存儲"Zero"
的一維數組]的指針
*s+1
等價於(*s)+1
,s
通過*
解析首先得到[一維數組"Lee"
]即指向[一維數組
"Lee"
的第一個元素'L'
的地址]的指針s[0]
;對該指針+1,相當於
s[0]+1
,使得指針指向[一維數組"Lee"
第二個元素'e'
的地址]格式控制符
%s
將該元素看成字元串的首地址,因而列印出"ee"
· 二維數組傳參
二維數組主要有兩種傳參方式(以下兩種是函數聲明的方式。聲明函數後,都是使實參為數組名來調用函數:
f(s);
)
void f(char (*s)[10]) {}
—— 一維數組指針作形參二維數組名實際上就是指向一維數組的指針。因此這裡形參s是個指向行元素的指針,與二維數組名匹配。
void f(char s[][10]) {}
—— 二維數組指針作形參
對於這種方法,僅二維數組的數組列數可以省略,不可省略行數。f(char s[][])
是錯誤的。也就是說,1.和2.方式中都需要正確指定行數。
f(char **s)
,f(char *s[])
的方式聲明函數雖然能編譯輸出,但編譯器可能會出現以下警告信息:[Warning] passing argument 1 of 'f' from incompatible pointer type [Note] expected 'char **' but argument is of type 'char (*)[10]'
P.S. 當然,如果一定要用二維指針作實參
f(char **s)
,在傳參的時候可以將s
強制轉化:f((char **)s)
,函數內部操作元素可以通過*((int *)a+i*10+j)
的方式……但何必呢。如果一定要試試,這裡也有個例子:
#include <stdio.h> void f(char **s) { //形參s是個二維指針 printf("%c\n", *((char *)s)); //輸出:L printf("%s\n", ((char *)s)); //輸出:Lee printf("%c\n", *((char *)s+10)); //輸出:H printf("%s\n", ((char *)s+10)); //輸出:Hero } int main() { char s[10][10] = {"Lee","Hero"}; f((char **)s); //“我一定要把s看做二維指針去傳參!” return 0; }
char *s[10] = {"Hello", "World"}
記憶體:類比char *s = "Hello"
,這裡s是一個指針數組,s[0]
、s[1]
是兩個指針,分別指向字面量"Hello"
、"World"
。指向的內容可以訪問,無法修改。
定義與使用:
#include <stdio.h>
void f(char **s) {
printf("%s\n", s[0]); //輸出:Lee
printf("%c\n", s[0][0]); //輸出:L
}
int main() {
char *s[10] = {"Lee","Hero"};
printf("%s\n", s[0]); //輸出:Lee(等價於*s)
printf("%c\n", s[0][0]); //輸出:L (等價於*s[0])
f(s);
return 0;
}
解釋:
數組名作為函數參數傳遞時,會退化成指向數組首元素的指針。
當把
s
作為參數傳遞給f()
函數時,實際上是把指針數組的首地址傳遞給了f()
函數。這樣,f()
函數中的s
就是一個二級指針,它指向了指針數組的第一個元素,也就是第一個字元串的地址。
f()
函數接受一個二級指針作為參數。由此,f()
函數中的s[0]
和s[0][0]
與主函數中的s[0]
和s[0][0]
含義相同。
#include <stdio.h>
int main() {
/* s[10][10]與*s[10]的對比 */
char *s[10] = {"Lee","Hero"};
printf("%d %d\n", sizeof(s), &s); //輸出:80 6487488
printf("%s\n", s); //無輸出!
printf("%d %d\n", sizeof(s[0]), &s[0]); //輸出:8 6487488
printf("%s\n", s[0]); //輸出:Lee(等價於*s)
printf("%d %d\n", sizeof(s[0][0]), &s[0][0]);//輸出:1 4210692
printf("%c\n\n", s[0][0]); //輸出:L (等價於*s[0])
char t[10][10] = {"Lee","Hero"};
printf("%d %d\n", sizeof(t), &t); //輸出:100 6487376
printf("%s\n", t); //輸出:Lee
printf("%d %d\n", sizeof(t[0]), &t[0]); //輸出:10 6487376
printf("%s\n", t[0]); //輸出:Lee(等價於*t)
printf("%d %d\n", sizeof(t[0][0]), &t[0][0]);//輸出:1 6487376
printf("%c\n", t[0][0]); //輸出:L (等價於*t[0])
/* *s[10]內容無法修改 */
t[1][0] = 'Z'; //修改二維數組元素
printf("%s\n", t[1]); //輸出:Zero
s[1][0] = 'Z'; //程式運行到這裡崩潰!
printf("%s\n", s[1]); //無輸出!
return 0;
}
對二維數組結構的認識
關於二維數組
a[i][j]
: 第 \(i\) 行第 \(j\) 列元素
a[i]
:一級指針常量,指第 \(i\) 行首元素地址,第 \(i\) 行本質為一維數組,a[i]+j
是第 \(i\) 行第 \(j\) 列元素的地址
a
:數組指針常量,是二維數組的起始地址,第 \(0\) 行的起始地址。
二維數組中的指針等價關係
優先順序:
()
\(>\)++
\(>\)指針運算符*
\(>\)+
二級指針 | <—— | 一級指針 | <—— | <—— | 數組元素 | <—— | <—— |
---|---|---|---|---|---|---|---|
a |
&a[0] |
*a+j |
a[0]+j |
&a[0][j] |
*(*a+j) |
*(a[0]+j) |
a[0][j] |
a+i |
&a[i] |
*(a+i)+j |
a[i]+j |
&a[i][j] |
*(*(a+i)+j) |
*(a[i]+j) |
a[i][j] |
數組結構中對“指針常量”的理解
指針常量:不能修改指針所指向的地址,但指向的值可以改變。
數組名是指針常量。數組名代表數組的首地址,它的值不能改變,也就是說不能讓數組名指向其他地址。
二維數組中a[i][j]
中,a[i]
可以看做是指向第 \(i\) 個一維數組的指針,它的值是第 \(i\) 個一維數組的首地址。a[i]
的值不能改變,也就是說不能讓 a[i]
指向其他地址。可以類比為指針常量。
總之,數組結構中各元素地址都是連續且無法更改的。
char a[10][10] = {"Lee", "Hero"};
char *p[10] = {0} //定義指針數組
p[0] = a[0];
p[1] = a[1];
p[0] = p[1]; //合法
a[0] = a[1]; //非法
指針 vs 數組 記憶體結構一圖流
圖由ECNU16級的陽太學長提供~
One More Thing
當二維數組遇見qsort()
庫函數,關於比較函數cmp(const void *a, const void *b)
的迷思
利用qsort()
函數對一個整數數組進行排序,一般格式如下:
#include <stdio.h>
#include <stdlib.h>
// 比較函數,用於升序排序整數
int cmp(const void *a, const void *b) {
int n1 = *(int *)a;
int n2 = *(int *)b;
return n1 - n2;
}
int main() {
int arr[] = {10, 5, 15, 12, 90, 80};
int n = sizeof(arr) / sizeof(arr[0]), i;
// 調用qsort庫函數,傳入數組指針,元素個數,元素大小和比較函數
qsort(arr, n, sizeof(int), cmp);
// 列印排序後的數組
printf("Sorted array: ");
for (i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
/* 輸出結果:Sorted array: 5 10 12 15 80 90 */
return 0;
}
可見,傳入cmp()
函數的參數是兩個void
型指針,指向我們需要排序的數組中的每個元素。在上面的例子中,int n1 = *(int *)a;
即是將void
型指針強制轉換成int
型指針後用*
解地址,得到的便是數組中的元素。
ECNU Online Judge
有這樣一道題:[郵件地址排序]
題面
現接收到一大批電子郵件,郵件地址格式為:
用戶名@主機功能變數名稱
,要求把這些電子郵件地址做主機功能變數名稱的字典序升序排序,如果主機功能變數名稱相同,則做用戶名的字典序降序排序。輸入格式
第一行輸入一個正整數 \(n\),表示共有 \(n\) 個電子郵件地址需要排序。接下來 \(n\) 行,每行輸入一個電子郵件地址(保證所有電子郵件地址的長度總和不超過 \(10^6\))。
- 對於 \(50\%\) 的數據,保證 \(n \leqslant 100, |s_i| \leqslant 100\)。
用戶名只包含字母數字和下劃線,主機功能變數名稱只包含字母數字和點。
輸出格式
按排序後的結果輸出 \(n\) 行,每行一個電子郵件地址。
為節省記憶體,通過比較逆天的試例,考慮用指針與malloc()
動態記憶體管理存儲郵件地址:
為了和這篇博客主題契合,這裡只介紹這種數據存儲結構的實現方式與cmp()
的設計方法:
/* 數據輸入 */
int T; //要輸入的郵件個數
scanf("%d", &N);
//建立指針數組 email
char **email;
email = (char **)malloc(N * sizeof(char*)); //相當於實現了char *email[N]
//使指針數組 email 中的每個指針元素都指向一個郵件地址字元串
for (int i = 0; i < N; i++) {
scanf("%s", s); //讀取一個字元串
LEN = strlen(s); //獲取字元串長度
p = (char *)malloc((LEN+1) * sizeof(char)); //分配每個字元串的存儲空間
strcpy(p, s); //把字元串複製到p處,這兩行相當於實現了char p[LEN+1] = {s}
*(email + i) = p;
//使指針數組 email 中的指針元素指向 p ,p也是個指針,但藉助malloc()動態分配,實現了字元串的功能
}
數據輸入完畢後最終實現的效果,類似於char *email[50] = {"[email protected]", "[email protected]"}
的定義方式,只是一維字元數組的長度是藉助malloc()
動態分配的,並不是個定值。
數據輸入完畢,我們現在得到了一個名為email
的指針數組,數組裡的每個元素都是一個指針,指向共 \(N\) 個字元串。
設計cmp()
時,傳入cmp()
函數的參數是兩個void
型指針,指向我們需要排序的數組中的每個元素。因此,void
型指針指向一級指針,這樣的void
型指針就是二維指針——char **
。
int cmp (const void *a, const void *b) {
char *p1 = *((char **)a);
char *p2 = *((char **)b); //對二級指針a、b進行一次解地址,得到的就是一級指針p1,p2
//通過 *(p1+i) *(p2+i) 操作就可以解析到[一級指針所指字元串]的每個字元
//從而做進一步的比較處理
/* 後續省略 */
return ret;
}