關於指針、數組、字元串的恩怨,這裡有你想知道的一切

来源:https://www.cnblogs.com/LeeHero/archive/2023/03/25/17254033.html
-Advertisement-
Play Games

關於指針、數組、字元串的恩怨,這裡有你想知道的一切 記憶體組成、字元串定義、一/二維數組結構、數組中的指針等價關係、數組結構中對“指針常量”的理解、 指針 vs 數組 記憶體結構一圖流、One More Thing:當二維數組遇見qsort()庫函數,關於比較函數cmp的迷思 ...


關於指針、數組、字元串的恩怨,這裡有你想知道的一切

目錄

記憶體組成

在這裡插入圖片描述

堆區

堆區 (Heap):由程式員手動申請釋放的記憶體空間。

  1. C中:malloc()colloc()函數申請,用free()釋放

若不用free()釋放,容易造成記憶體泄露(即記憶體被浪費、耗盡)。

  • ptr = (castType*) malloc(size);

    傳入參數為記憶體的位元組數,記憶體未被初始化。

  • ptr = (castType*)calloc(n, size);

    存入參數為記憶體塊數與每塊位元組數,記憶體初始化為0

  • free(ptr);

    釋放申請的記憶體。

  1. C++中:new申請,delete釋放。newdelete都是操作符
  • 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;
} 

對於列印結果的一些解釋:

· 對二維數組進行操作與輸出

  1. s 等價於&s[0],是指向[存儲"Lee"的一維數組]的指針

  2. s+1等價於&s[1],是指向[存儲"Zero"的一維數組]的指針

  3. *s+1等價於(*s)+1s通過*解析首先得到[一維數組"Lee"]

    即指向[一維數組"Lee"的第一個元素'L'的地址]的指針s[0]

    對該指針+1,相當於s[0]+1,使得指針指向[一維數組"Lee"第二個元素'e'的地址]

    格式控制符%s將該元素看成字元串的首地址,因而列印出"ee"

· 二維數組傳參

二維數組主要有兩種傳參方式(以下兩種是函數聲明的方式。聲明函數後,都是使實參為數組名來調用函數:f(s);

  1. void f(char (*s)[10]) {} —— 一維數組指針作形參

    二維數組名實際上就是指向一維數組的指針。因此這裡形參s是個指向行元素的指針,與二維數組名匹配。

  2. void f(char s[][10]) {} —— 二維數組指針作形參
    對於這種方法,僅二維數組的數組列數可以省略,不可省略行數。f(char s[][])是錯誤的。

    也就是說,1.和2.方式中都需要正確指定行數。

  3. 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\) 行的起始地址。

image-20230323214508306

二維數組中的指針等價關係

優先順序:() \(>\) ++ \(>\) 指針運算符* \(>\) +

二級指針 <—— 一級指針 <—— <—— 數組元素 <—— <——
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]

image-20230323232346347

數組結構中對“指針常量”的理解

指針常量:不能修改指針所指向的地址,但指向的值可以改變。

數組名是指針常量。數組名代表數組的首地址,它的值不能改變,也就是說不能讓數組名指向其他地址。

二維數組中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級的陽太學長提供~

image-20230325010908380

image-20230325002950287

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()動態記憶體管理存儲郵件地址:

image-20230325020950813

為了和這篇博客主題契合,這裡只介紹這種數據存儲結構的實現方式與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;
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 建模 首先我們需要一些貼圖素材 貼圖素材一般可以在3dtextures網站上找到,這裡我找了2份,包含了牆的法線貼圖和潮濕地面的法線、透明度、粗糙度貼圖 通過kokomi.AssetManager將貼圖素材一次性全部載入出來,將它們應用到 ...
  • NPM(Node Package Manager)是 Node.js 的包管理工具,用來安裝各種 Node.js 的擴展。 NPM是 JavaScript 的包管理工具,也是世界上最大的軟體註冊表。有超過 60 萬個 JavaScript 代碼包可供下載,每周下載約 30 億次。NPM讓 JavaS ...
  • 響應式基本原理就是,在初始化vue實例的時候,對data的每一個屬性都通過 Object.defineProperty 定義一次,在數據被set的時候,做一些操作,改變相應的視圖 ...
  • JavaScript(簡稱“JS”)是當前最流行、應用最廣泛的客戶端腳本語言,用來在網頁中添加一些動態效果與交互功能,在 Web 開發領域有著舉足輕重的地位。JavaScript 與 HTML 和 CSS 共同構成了我們所看到的網頁,其中: HTML 用來定義網頁的內容,例如標題、正文、圖像等; C ...
  • Vue是一款國產前端框架,它的作者尤雨溪(Evan You)是一位美籍華人,2014年2月,尤雨溪開源了一個前端開發庫 Vue.js,2015年發佈1.0.0版本,2016年4月發佈2.0版本,目前,尤雨溪全職投入 Vue.js 的開發與維護,立志將 Vue.js 打造成與 Angular/Reac ...
  • 外觀模式(Facade Pattern):它提供了一個簡單的介面,用於訪問複雜的系統或子系統。通過外觀模式,客戶端可以通過一個簡單的介面來訪問複雜的系統,而無需瞭解系統內部的具體實現細節。 在前端開發中,外觀模式常常被用於封裝一些常用的操作,以簡化代碼複雜度和提高代碼可維護性。比如,一個用於處理數據 ...
  • 最近,在看 LPL 比賽的時候,看到這樣一個有意思的六芒星能力圖動畫: 今天,我們就來使用純 CSS 實現這樣一個動畫效果! 實現背景網格 對於如下這樣一個背景網格,最好的方式當然肯定是切圖,或者使用 SVG 路徑。 如果一定要使用 CSS,勉強也能做,這就涉及了不規則圖形邊框效果,我們有一些方式可 ...
  • title: "modern C++ DesignPattern-Part3" date: 2018-04-12T19:08:49+08:00 lastmod: 2018-04-12T19:08:49+08:00 keywords: [設計模式, C++] tags: [設計模式] categori ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...