關于指標、陣列、字串的恩怨,這里有你想知道的一切
目錄- 關于指標、陣列、字串的恩怨,這里有你想知道的一切
- 記憶體組成
- 堆區
- 堆疊區
- 靜態存盤區
- 代碼區
- 字串定義 - 一維
char s[10] = "Hello"char *s = "Hello"
- 字串定義 - 二維
char s[10][10] = {"Hello","World"}char *s[10] = {"Hello", "World"}
- 對二維陣列結構的認識
- 關于二維陣列
- 二維陣列中的指標等價關系
- 陣列結構中對“指標常量”的理解
- 指標 vs 陣列 記憶體結構一圖流
- One More Thing
- 題面
- 輸入格式
- 輸出格式
- 記憶體組成
記憶體組成
堆區
堆區 (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;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548026.html
標籤:其他
