日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)

發布時間:2025/6/17 编程问答 30 豆豆

相關文章鏈接 :
1.【嵌入式開發】C語言 指針數組 多維數組
2.【嵌入式開發】C語言 命令行參數 函數指針 gdb調試
3.【嵌入式開發】C語言 結構體相關 的 函數 指針 數組
4.【嵌入式開發】gcc 學習筆記(一) - 編譯C程序 及 編譯過程
5.【C語言】 C 語言 關鍵字分析 ( 屬性關鍵字 | 常量關鍵字 | 結構體關鍵字 | 聯合體關鍵字 | 枚舉關鍵字 | 命名關鍵字 | 雜項關鍵字)
6.【C 語言】編譯過程 分析 ( 預處理 | 編譯 | 匯編 | 鏈接 | 宏定義 | 條件編譯 | 編譯器指示字 )
7.【C 語言】指針 與 數組 ( 指針 | 數組 | 指針運算 | 數組訪問方式 | 字符串 | 指針數組 | 數組指針 | 多維數組 | 多維指針 | 數組參數 | 函數指針 | 復雜指針解讀)


文章目錄

  • 一. 指針
    • 1. 指針 簡介
      • ( 1 ) 指針 概念 ( 本質 | 占用內存 ① 32位 4字節 ② 64 位 8 字節 | * ① 聲明指針 ② 獲取指向的值 )
      • ( 2 ) 指針 簡單示例 ( * 的讀寫內存作用 | 指針相關類型大小)
    • 2. 傳值 和 傳址 調用
      • ( 1 ) 相關概念 ( 傳值調用 復制實際值 | 傳址調用 復制地址值 )
      • ( 2 ) 傳址調用 ( 改變外部變量值 )
    • 3. 常量 和 指針
      • ( 1 ) 相關概念 ( 核心原則 左數右指 | 左數 ① const int* p ② int const* p 數據時常量 | 右指 int* const 指針是常量 )
      • ( 2 ) 驗證 常量 指針 相關概念 ( 左數右指 )
  • 二. 數組
    • 1. 數組 簡介
      • ( 1 ) 數組 概念 ( 數組地址 | 數組大小 顯示 隱式 聲明 | 數組初始化 [ 效率比后期賦值高 ] )
      • ( 2 ) 數組 示例 ( 定義 | 大小 | 初始化 )
    • 2. 數組地址與名稱 概念
      • ( 1 ) 數組 概地址 ( 數組名 [ 數組首元素地址 ] 和 &數組名 [ 數組地址 ] | 數組名 類似于 常量指針 | 數組拷貝 )
      • ( 2 ) 數組 示例 ( 數組名 | 地址 | 數組拷貝禁止情況 )
    • 3. 數組 與 指針 區別
      • ( 1 ) 概念簡介 ( ① 數組名就是首元素地址 不需要尋址 | ② 指針 中保存一個地址 指向首元素地址 需要尋址 | printf 打印 數組 或 指針 : 根據占位符自動判斷打印地址還是打印內存中的具體內容 )
      • ( 2 ) 代碼示例 ( 數組 | 指針 編譯器處理上的區別 )
  • 三. 數組 指針 分析
    • 1. 指針 加減 運算方式
      • ( 1 ) 指針 加減法 運算 ( 指針指向的位置在同一個數組中改變才有意義 )
      • (2) 數組大小計算示例
      • ( 3 ) 指針 加法運算示例 ( 指針地址 + 4/8 * 被加數 )
      • ( 4 ) 指針 減法 運算示例
    • 2. 指針 比較 運算方式
      • ( 1 ) 指針 比較 運算 ( 大于 小于 大于等于 小于等于 運算的前提是 必須指向同一數組 中的元素 | 任意兩指針只能進行 等于 不等于 的比較 )
      • ( 2 ) 指針 比較 運算代碼示例 ( 用 指針 遍歷數組 )
    • 3. 數組訪問方式
      • ( 1 ) 下標 指針 訪問 ( 推薦使用下標訪問 )
      • ( 2 ) 下標 指針 訪問 數組 性能 代碼示例
    • 3. int array[]; array 和 &array 區別
      • ( 1 ) int array[] 中 array 和 &array 意義 ( ① array 數組首元素地址 | ② &array 數組地址 )
      • ( 2 ) array 和 &array 計算 代碼示例
    • 4. 數組參數
      • ( 1 ) 數組參數 概念 ( 退化成指針 | 需要帶上數組長度作為 附屬參數 )
      • ( 2 ) 數組參數 代碼示例 ( 數組大小 | 數組參數大小 )
    • 5. 數組 指針 對比 ( 內存分配 : ① 指針 分配 4 / 8 字節 ② 數組分配所有元素地址 | 作為參數 | 常量[ 數組 ] 變量[ 指針 ] 區別 )
  • 四. 字符串
    • 1. 字符串概念
      • ( 1 ) 概念 ( 本質 是 char[] 數組 | '\0' 結尾 | 存儲位置 棧 堆 常量區 )
      • ( 2 ) 示例代碼 ( 字符串概念 | 字符串 )
    • 2. 字符串 長度
      • ( 1 ) 字符串長度計算 ( 不包括 '\0' | 標準庫中有該函數)
      • ( 2 ) 代碼示例 ( 字符串長度計算示例 )
      • ( 3 ) 代碼示例 ( 自己實現 strlen 方法 )
    • 3. 字符串函數 長度不受限制 情況
      • ( 1 ) 不受限制的字符串函數 ( 函數自動尋找 '\0' 確定字符串大小 | stpcpy | strcat | strcmp )
      • ( 2 ) 代碼示例 ( 自己實現字符串拷貝函數 )
    • 4. 字符串函數 長度受限制 情況
      • ( 1 ) 受限制的字符串函數 ( 推薦使用 降低錯誤率 )
  • 五. 指針數組 與 數組指針
    • 1. 數組指針
      • ( 1 ) 數組類型介紹 ( 數組元素類型 | 數組大小 | 舉例 int[8] )
      • (2) 數組指針簡介 ( 指向數組的 一個 指針 | 數組指針類型定義方式 : 數組元素類型 ( * 指針名稱 ) [數組大小] )
      • ( 3 ) 代碼示例 ( 定義數組類型 | 數組指針用法 )
    • 2. 指針數組
      • ( 1 ) 指針數組簡介 ( 數組中存儲的元素是指針 | 數組指針 int (*array)[5] 本質是指針 | 指針數組 int* array[5] 本質是數組 )
      • ( 2 ) 代碼示例 ( 指針數組使用案例 )
    • 3. main 函數參數 分析
      • ( 1 ) main 函數簡介
      • (2) main 函數 代碼示例
  • 六. 多維數組 和 多維指針
    • 1. 二維指針 ( 指向指針的指針 )
      • ( 1 ) 二維指針簡介 ( 指向指針的指針 )
      • ( 2 ) 代碼示例 ( 指針的傳址調用 | 指向指針的指針 | 重置指針指向的空間 )
    • 2. 二維數組
      • ( 1 ) 二維數組 ( 存放方式 | 數組名 | 首元素類型 | 數組名 類似 常量指針 | )
      • (2) 代碼示例 ( 以一維數組方式遍歷二維數組 | 體現二維數組的數據排列 )
    • 3. 數組名
      • ( 1 ) 數組名 簡介 ( 數組首元素地址 | &數組名 是 數組地址 )
      • ( 2 ) 代碼示例 ( 數組名指針指向的內容 | 二維指針數組名對應的指針運算 )
      • ( 3 ) 代碼示例 ( 一維數組遍歷 | 二維數組遍歷 )
      • ( 4 ) 代碼示例 ( 為二維數組申請內存空間 )
  • 五. 數組參數 與 指針參數
    • 1. 數組參數退化為指針參數的意義
      • ( 1 ) 數組參數退化的相關概念 ( 指針退化成數組 )
      • ( 2 ) 代碼示例 ( 二維數組參數 的指針退化 | 外層指針退化 | 內層數組指針沒有退化 )
  • 六. 函數指針
    • 1. 函數類型 和 函數指針
      • (1) 相關概念 ( 函數類型要素 ① 返回值, ② 參數類型, ③ 參數個數, ④ 隱含要素 : 參數順序 | 函數指針類型 返回值類型 (*變量名) (參數列表) )
      • ( 2 ) 代碼示例 ( 定義函數指針 : ①typedef int(FUN)(int); FUN* p; 或者 ② void(*p1)(); | 給 函數指針 賦值 , 右值 可以直接使用 ① 函數名 或 ② &函數名 | 調用函數指針方法 : ① 函數指針變量名(參數) ② (*函數指針變量名)(參數) | 函數名 和 &函數名 是等價的 | 函數指針變量名(參數) 和 (*函數指針變量名)(參數) 也是等價的 )
    • 2. 回調函數
      • ( 1 ) 回調函數相關概念
      • ( 2 ) 代碼示例 ( 回調函數示例 )
    • 3. 解讀 復雜的 指針聲明 ( 難點 重點 | ①找出中心標識符 ②先右 后左 看 確定類型 提取 ③ 繼續分析 左右看 ... )


注意 : 博客中出現的關于指針的計算方式, 如果在 32 位電腦中, 指針的地址類型是 unsigned int 類型 , 占 4 字節 , 在 64 位電腦中 指針地址的類型是 unsigned long int , 占 8 個字節 ;




一. 指針



1. 指針 簡介


( 1 ) 指針 概念 ( 本質 | 占用內存 ① 32位 4字節 ② 64 位 8 字節 | * ① 聲明指針 ② 獲取指向的值 )


指針簡介 :

  • 1.指針本質 : 指針本質也是一個變量 ;
  • 2.占用內存 : 指針變量也要在內存中占用一定大小的空間, 不同 類型的指針占用的內存大小都是 相同的 ;

32位系統 指針 占用內存大小 4 字節, 64位系統 指針 占用內存大小 8 字節;

  • 3.指針變量保存的值 : 指針變量中保存的是內存地址的值 ;

符號簡介 :

  • 1.聲明指針 : 在 聲明指針變量時, * 表示聲明一個指定類型變量的指針 ;
  • 2.使用指針 : 使用指針的時候, * 表示指針變量地址指向的內存中的值, 可以讀取該地址的實際數據值 或者 向地址中寫入實際數據值 ;


( 2 ) 指針 簡單示例 ( * 的讀寫內存作用 | 指針相關類型大小)


指針簡單示例 :

  • 1.代碼示例 :
#include <stdio.h>int main() {//1. 指針簡單使用, * 符號作用int i = 666;//聲明 int 類型 指針, 使用 * 符號聲明指針int *p = &i;//這里驗證下 i 即存放在 p 地址的內容, * 用于讀取 指針 p 地址中的數據printf("%d, %x, %d\n", i, p, *p);//等價于 i = 888, *p 代表指針指向的內容, p 是指針的地址, 之類 * 用于向 p 地址指向的內存中寫入數據*p = 888;//改變一個變量的大小可以使用其地址來改變, 不一定必須使用變量名稱printf("%d, %x, %d\n", i, p, *p);//2. 指針大小示例//32位系統 指針 占用內存大小 4 字節, 64位系統 指針 占用內存大小 8 字節int* p_int;char* p_char;//a. 打印 int* 類型指針 和 char* 類型指針的 指針變量本身大小//b. 打印 指針指向的內容大小, int 指針指向 int 類型, 因此 sizeof(*p_int) 結果是 4, sizeof(*p_char) 結果是 1printf("%ld, %ld, %ld, %ld\n", sizeof(p_int), sizeof(p_char), sizeof(*p_int), sizeof(*p_char));//打印 int* 和 char* 類型大小, 打印 int 和 char 類型大小 printf("%ld, %ld, %ld, %ld\n", sizeof(int*), sizeof(char*), sizeof(int), sizeof(char));return 0; }
  • 2.運行結果 :



2. 傳值 和 傳址 調用


( 1 ) 相關概念 ( 傳值調用 復制實際值 | 傳址調用 復制地址值 )


傳值調用 :

  • 1.產生復制情況 : 傳值調用時 會發生 實參數據值 復制到 形參中 ;

傳址調用 :

  • 1.實現方式 : 將指針當做函數的參數, 因為指針也是變量, 可以當做參數使用 ;
  • 2.適用場景 : 如果需要在函數中修改實參的值, 并且執行函數完畢后保留下來, 這里就用到傳址調用, 使用指針作為函數參數 ;
  • 3.適用場景2 : 參數數據類型較復雜, 如果參數很大, 傳值調用需要實參到形參的復制, 會浪費性能 ;



( 2 ) 傳址調用 ( 改變外部變量值 )


代碼示例1 :

  • 1.代碼 :
#include <stdio.h>//傳值調用案例, 任意改變參數的值, 不影響傳入的變量值 int fun_1(int a, int b) {a = 444;b = 444; }//傳址調用案例, 如果在函數中修改了地址指向的內存的值, 那么最終的值改變了 int fun_2(int* a, int* b) {*a = 444;*b = 444; }int main() {int x = 666, y = 888;//傳值調用fun_1(x, y);printf("x = %d, y = %d\n", x, y);//傳址調用fun_2(&x, &y);printf("x = %d, y = %d\n", x, y);return 0; }
  • 2.執行結果 :

代碼示例2 :

  • 1.代碼 :
#include <stdio.h>//傳址調用, 替換傳入的變量值 int swap(int *a, int *b) {int tmp = *a;*a = *b;*b = tmp; }int main() {int x = 666, y = 888;printf ("x = %d, y = %d\n", x , y);swap(&x, &y);printf ("x = %d, y = %d\n", x , y);return 0; }
  • 2.執行結果 :



3. 常量 和 指針


( 1 ) 相關概念 ( 核心原則 左數右指 | 左數 ① const int* p ② int const* p 數據時常量 | 右指 int* const 指針是常量 )


參考 : const 關鍵字 ;

const 修飾指針 : 需要符合下面的規則 :

聲明特征
const int* pp指針地址可變 p指針指向的內容不可變 (const 在 * 左邊, 數據不可變)
int const* pp指針地址可變 p指針指向的內容不可變 (const 在 * 左邊, 數據不可變)
int* const pp指針地址不可變 p指針指向的內容不可變 (const 在 * 右邊, 地址不可變)
const int* const pp指針地址不可變 p指針指向的內容不可變 (const 在 * 左邊 和 右邊, 數據和地址都不可變)

const 修飾指針規則 : ***左數 右指 (左邊數據是常量, 右邊指針是常量)***;
左數 : const 出現在 * 左邊時, 指針指向的數據為常量, 指向的數據不可改變;
右指 : const 出現在 * 右邊時, 指針地址本身是常量, 指針地址不可改變;



( 2 ) 驗證 常量 指針 相關概念 ( 左數右指 )


參考 : const 關鍵字 ;

const 修飾指針規則 : 左數右指;
左數 : const 出現在 * 左邊時, 指針指向的數據為常量, 指向的數據不可改變;
右指 : const 出現在 * 右邊時, 指針地址本身是常量, 指針地址不可改變;


const 關鍵字 代碼示例 : 修飾指針

  • 1.代碼示例1 : const 出現在 * 左邊, const int* p = &i;
#include <stdio.h>int main() {//定義普通的變量, 用于取地址用int i = 666;//定義一個 const 在 * 左邊的例子, 意義是 指針指向的內容是常量//按照規則, 指針地址可改變, 指針指向的數據不可變const int* p = &i; //指針指向的數據不可改變, 這里會報錯*p = 444;return 0; }

  • 2.代碼示例2 : const 出現在 * 左邊, int const* p = &i;
#include <stdio.h>int main() {//定義普通的變量, 用于取地址用int i = 666;//定義一個 const 在 * 左邊的例子, 意義是 指針指向的內容是常量//按照規則, 指針地址可改變, 指針指向的數據不可變int const* p = &i;//指針指向的數據不可改變, 這里會報錯*p = 444;return 0; }

  • 3.代碼示例3 : const 出現在 * 右邊, int* const p = &i;
#include <stdio.h>int main() {//定義普通的變量, 用于取地址用int i = 666;//定義一個 const 在 * 右邊的例子, 意思是 地址是常量//按照規則, 指針地址不可改變, 指針指向的內容可變int* const p = &i;//指針指向的數據不可改變, 這里會報錯p = NULL;return 0; }

  • 4.代碼示例4 : const 同時出現在 * 左邊 和 右邊, const int* const p = &i;
#include <stdio.h>int main() {//定義普通的變量, 用于取地址用int i = 666;//定義 const 同時出現在 * 左邊 和 右邊, 則指針的地址 和 指向的數據都不可改變const int* const p = &i;//下面的兩個操作, 一個是想修改指針地址, 一個是想修改指針值, 這兩個都報錯.p = NULL;*p = 444;return 0; }





二. 數組



1. 數組 簡介



( 1 ) 數組 概念 ( 數組地址 | 數組大小 顯示 隱式 聲明 | 數組初始化 [ 效率比后期賦值高 ] )


數組 簡介 :

  • 1.概念 : 數組 是 相同類型 的 變量 的 有序集合 ;
  • 2.數組示例 :
int array[6];

定義數組 int array[6];
意義 : 數組中包含 6 個 int 類型的數據 , 數組中每個元素都是 int 類型的 ;
第一個元素地址 : array 是數組中第一個元素的起始地址;
下標 : 可以通過下標來獲取數組中指定位置的元素, array[0] 是第一個元素的位置, array[5] 是第六個元素的位置 ;


數組大小 :

  • 1.數組定義時必須聲明大小 : 數組在定義時, 必須顯示 或 隱式 的聲明數組的大小 ;
  • 2.顯示聲明數組大小 : 定義數組時, 在數組名稱后的中括號中聲明數組大小 ;
int array[5]; int array[5] = {1, 2, 3} ; //這個也是顯示聲明, 數組大小為 5, 但是只指定了 前三個元素的大小 ;
  • 3.隱式聲明數組大小 : 聲明數組時, 不在中括號中聲明數組大小, 只在初始化中初始化指定個數的元素, 那么元素的個數就是數組的大小 ;
//隱式初始化, 該數組個數為 4 int array[] = {0, 1, 2, 3};

數組初始化 :

  • 1.完全初始化 : 數組大小為5, 將 5 個元素都在定義時指定位置 ;
  • 2.部分初始化 : 數組大小為5, 如果初始化前 1 ~ 4 個元素, 剩余的元素默認初始化為 0 ;
  • 3.初始化效率 : 初始化效率很高, 遠遠比依次賦值要高, 因此建議定義數組時最好初始化 ;
  • 4.最佳實踐 :
//這里只對數組的第一個元素進行初始化為0, 那么其余的元素默認也初始化為0, 初始化效率要遠遠高于依次賦值的效率 int array[5] = {0}


( 2 ) 數組 示例 ( 定義 | 大小 | 初始化 )


數組 大小 初始化 示例 :

  • 1.代碼 :
#include <stdio.h>//數組大小 和 初始化 示例 //數組大小 : //初始化 : 如果不初始化, 那么數組中就是隨機值; 全部初始化, 部分初始化 : 其余默認為 0int main() {//1. 顯示聲明數組大小, 其實際大小以中括號為準, 大小為 5, 5個元素只有 前3個初始化為 0, 1, 2//初始化說明 : int array_1[5] = {0, 1, 2};int array_3[5];int array_4[5] = {0};//2. 隱式聲明數組大小, 其實際大小為 3, 三個元素全部初始化int array_2[] = {0, 1, 2};printf("array_1 大小 : %ld, array_1 數組個數 : %ld\n", sizeof(array_1), sizeof(array_1)/sizeof(*array_1));printf("array_2 大小 : %ld, array_2 數組個數 : %ld\n", sizeof(array_2), sizeof(array_2)/sizeof(*array_2));//打印 array_2 數組結果, 其中數組元素內容是 初始化值printf("打印 int array_1[5] = {0, 1, 2}; 數組結果 : \n");int i = 0;for(i = 0; i < sizeof(array_1)/sizeof(*array_1); i ++){printf("array_1[%d] = %d\n", i, array_1[i]);}//打印 array_3 數組結果, 其中數組元素內容是隨機值printf("打印 int array_3[5]; 數組結果 : \n");for(i = 0; i < sizeof(array_3)/sizeof(*array_3); i ++){printf("array_3[%d] = %d\n", i, array_3[i]);}//打印 array_4 數組結果, 其中數組元素內容是隨機值printf("打印 int array_4[5] = {0}; 數組結果 : \n");for(i = 0; i < sizeof(array_4)/sizeof(*array_4); i ++){printf("array_4[%d] = %d\n", i, array_4[i]);}return 0; }
  • 2.執行結果 :



2. 數組地址與名稱 概念


( 1 ) 數組 概地址 ( 數組名 [ 數組首元素地址 ] 和 &數組名 [ 數組地址 ] | 數組名 類似于 常量指針 | 數組拷貝 )


數組地址名稱 簡介 :

  • 1.數組名稱 : 數組名稱 等價于 數組 首元素 地址 ;
    • 注意 : 數組名 不是 數組的首地址 , &數組名 才是數組的首地址 , 但是這兩個的值是相同的 ;
  • 2.數組地址 : 使用 & 取數組的地址, 才能獲取數組的地址 ;
  • 3.值相同 : 數組的 首元素地址 與 數組地址是相同的 ;
  • 4.數組地址 與 數組首元素地址 : 這兩個地址不是等價的, 其意義完全不同 ;

數組名稱 :

  • 1.數組名稱的本質 : 數組名 類似于 常量指針, 數組名稱 不能作為左值, 不能被賦值 ; 數組名 只能作為右值, 被賦值給別的指針 , 數組名在***大多數情況下可以當做常量指針理解***, 但是 數組名絕對不是真正的常量指針 ;
  • 2.數組名代表的地址 : 數組名稱 指向 數組首元素的地址, 其絕對值 與 數組地址 相同;

數組名稱不作為常量指針的場合 : 數組名類似于常量, 但不是常量, 下面兩種場合數組名與常量指針不同 ;

  • 1.sizeof 取大小時 : 使用 sizeof 操作符獲取 array 數組大小時, sizeof 作用域常量指針獲取的是指針的大小, sizeof 作用于數組名, 獲取的是數組的大小 , 不是單個指針的大小;
  • 2.作為 & 參數時 : & 只能用于變量, 不能用于常量, 因此 &數組名 是取數組的地址, 這種用法 不符合常量指針的特點 ;

數組拷貝禁用數組名直接賦值 :

  • 1.禁止使用的方式 : 數組拷貝不能 直接使用 數組名1 = 數組名2 的方式進行拷貝 或者 賦值 ;
  • 2.常量指針 : 數組名 類似于 常量指針, 其***不能作為賦值的左值, 只能做右值使用*** ;
  • 3.數組大小 : 數組還有一個隱含的大小屬性, 如 sizeof(數組名) 就可以獲取整個數組的大小, 單純的數組名稱只是一個地址, 如果使用地址進行互相賦值, 數組的大小屬性無法體現, 因此 C 語言規范, 禁用數組名 作為左值 ;


( 2 ) 數組 示例 ( 數組名 | 地址 | 數組拷貝禁止情況 )


數組代碼示例 :

  • 1.代碼示例 :
#include <stdio.h>int main() {int array_1[8] = {0};int array_2[] = {0, 1, 2, 3};//array_1 的類型是 int *//&array_1 的類型是 int*[8], 根據編譯時的 warning 警告可以看到這兩個類型printf("array_1 : %x, &array_1 : %x \n", array_1, &array_1 );//這種用法是錯誤的, array_1 類似于一個常量指針, 其不能當做左值//數組除了地址信息之外, 還附帶大小信息, 如果只是地址賦值, 大小信息無法帶過去, 因此數組不能這樣拷貝賦值//C語言 不支持 這樣的賦值//array_1 = array_2; return 0; }
  • 2.執行結果 :



3. 數組 與 指針 區別


( 1 ) 概念簡介 ( ① 數組名就是首元素地址 不需要尋址 | ② 指針 中保存一個地址 指向首元素地址 需要尋址 | printf 打印 數組 或 指針 : 根據占位符自動判斷打印地址還是打印內存中的具體內容 )


printf 打印 數組 與 指針 變量 :

  • 1.處理數組 : 編譯器不會尋址 , 直接將 數組名代表的內存空間地址對應的數據打印出來 , 因為數組名就代表了數組的首地址, 不需要再次尋址 ; 數組名代表的地址 就是 內容的首地址 , 不用去尋址查找內容 ;
  • 2.處理指針 : 編譯器會尋址 , 查找 指針變量的四個字節的內容, 指針變量的四個字節的地址指向的內容 , 然后將指針指向的內容打印出來 , 指針的地址 與 實際內容的地址 不連續, 是斷開的 ;

下面這張圖形象的說明了 指針 與 數組的 區別 :


指針的起始地址 和 數組的起始地址 :

  • 1.指針起始地址 : 這里要區分 指針保存的地址 和 指針起始地址,
    ( 1 )指針保存的地址 : 是指 指針變量 4 字節 (32位系統的, 64位 8 個字節) , 這四個或 8個字節中保存了一個地址 , 這個地址指向另外一段內存空間, 這個地址是指針保存的地址, 又叫指針指向的地址, 在下圖中標注的 指針變量中保存(指向)的地址① , 這個地址還是 實際內容的起始地址① ;
    ( 2 )指針起始地址 : 是指指針變量所在的地址, 是 ***指針變量的四個字節的第一個字節所在內存的首地址 ***, 在下圖中標注的 指針起始地址② ;
  • 2.數組起始地址 : 數組名就是數組的起始地址, 又是數組首元素地址 , int array[10], array 是一個地址, 在下圖中標注的 數組首地址③, 這個地址還是數組 數組實際內容的首地址③ ;
  • 3.圖示 :

printf 打印 數組 或 指針 的 內容 或 地址 : 針對 字符數組 和 字符指針, 根據占位符自動判斷打印地址還是打印內存中的具體內容 ;

  • 1.打印字符串 : 如果想要打印出 數組或指針的 字符串, 那么使用 %s 作為占位符 ;
  • 2.打印地址 : 如果想要打印出 數組或指針的地址 , 那么使用 %x 作為占位符 ;
  • 3.智能判斷 : printf 時, 方法中會自動判斷 占位符 的類型, 來判斷是否要尋址, 如果 %x 則只打印地址, 如果使用 %s, 則會自動根據對應的地址打印出其內容 ;
  • 4.代碼示例 :
#include <stdio.h>int main() {char array[10] = {'H', 'e', 'l', 'l', 'o'};char *str = "Hello";//1. 針對數組打印 // ( 1 ) 如果檢測到 占位符 為 %s, 則會將組名首地址內存中的數據, 并一直到 \0 都打印出來(注意 不尋址)// ( 2 ) 如果檢測到 占位符 為 %x, 則會自動將數組首地址打印出來printf("array : %s\n", array);printf("array : %x\n", array);//2. 針對指針打印 // ( 1 ) 如果檢測到 占位符 為 %s, 則會尋址查找指針地址指向的內存, 將該內存中的字符串打印出來// ( 2 ) 如果檢測到 占位符 為 %x, 則會將指針地址打印出來printf("str : %s\n", str);printf("str : %x\n", str);return 0; }
  • 5.執行結果 :


( 2 ) 代碼示例 ( 數組 | 指針 編譯器處理上的區別 )


代碼示例 :

  • 1.代碼1 : 文件 test_1.c 內容 ;
#include <stdio.h>//編譯器如何處理 數組 和 指針 //1. 外部文件定義 : 在另外一個文件定義 char 指針 : char *p = "Hello"; // ( 1 ) 編譯器操作 : 在符號表中放置符號 p, 然后為符號 p 分配空間, // 查詢到 p 是指針, 給 p 符號分配 4 字節, 4 字節存放地址, 指向 "Hello" 字符串 地址; // 當打印 p 指針時, 編譯器會按照指針地址尋址, 將指針指向的內存中取值并打印出來 ; //2. 本文件聲明 : 在本文件聲明 : extern char p[] ; // ( 2 ) 編譯器操作 : 聲明引用外部文件變量 p, 到符號表中查找 p, 但是編譯器認為 p 是數組, // p 是數組名, 代表數組地址; // 當打印 p 數組時, 編譯器會直接將 p 當做數組地址 打印出來;//printf 打印變量規則 : //( 1 ) 打印指針 : 編譯器會尋址, 查找指針指向的內容, 然后將指針指向的內容打印出來 ; //( 2 ) 打印數組 : 編譯器不會尋址, 直接將數組名代表的內存空間地址打印出來 ;//代碼遵循原則 : 聲明指針 數組, 在外部聲明時類型要一致 ; extern char p[];int main() {//1. 此時 p 是數組, 直接打印 p, 會將數組地址打印出來, 以 %s 打印一個數組地址 會出亂碼 printf("*p = %s\n", p);//2. 正確使用數組 p 打印字符串的方法(模仿編譯器行為手工尋址) : p 是指針, 指向 "Hello", 但是本文件中聲明為類數組, 數組與指針打印時編譯器會做不同處理;// ( 1 ) 首先 p 是地址 ( 數組首地址 ), // ① 將 p 轉為 unsigned int* 類型的指針 : (unsigned int*)p ;// ② 說明 : 此處是將一個變量強制轉為 指向 unsigned int 類型的 指針, 這個是一個二維指針, 是指向地址的指針// 為了獲取 p 的地址(其地址是 unsigned int 類型的), 使用 * 即可提取 p 地址 ; // ( 2 ) 獲取字符串地址 : 獲取 (unsigned int*)p 指向的地址, 即 字符串的地址, 使用 *((unsigned int*)p) 獲取字符串地址;// ( 3 ) 將字符串地址強轉為char*指針 : (char*) (*((unsigned int*)p)) 即指向字符串的指針, 打印這個指針會將字符串打印出來printf("*p = %s\n", (char*) (*((unsigned int*)p)));return 0; }
  • 2.代碼2 : 文件 test_2.c 中的內容 ;
char *p = "Hello";
  • 3.執行結果 : 執行 gcc test_1.c test_2.c 命令進行編譯 , 執行 編譯后的可執行文件 ./a.out ;




三. 數組 指針 分析



1. 指針 加減 運算方式


( 1 ) 指針 加減法 運算 ( 指針指向的位置在同一個數組中改變才有意義 )


指針運算規則 :

  • 1.指針是變量 : 只要是變量就可以進行運算, 可以加減運算, 指針 + 1 運算如下 ;
  • 2.指針 + num 運算規則 : p + num(整數) , 反應到地址運算上 即 等價于 *(unsigned int)p + num * sizeof(p) , 其地址不是加1個字節, p 的地址的增量 是 所指向的數據類型的大小 乘以 被加數 的地址;

指針指向數組元素規則 :
前提 : 指針指向一個數組元素時有以下規則 :
: 指針 + 1 指向數組的下一個元素 ;
: 指針 - 1 指向數組的上一個元素 ;


數組運算規則 :

  • 1.數組本質 : 數組的***元素存儲空間是連續的***, 從數組首地址(數組元素首地址 | 數組名)開始 ;
  • 2.數組空間大小 : 數組的空間通過 sizeof(數組元素類型) * 數組大小 計算的, 這個數組元素類型是數組聲明的時候指定的, 數組大小是數組聲明或者初始化時指定的 ;
  • 3.數組名 : 數組名 是 數組首元素的地址, 又是***數組地址***, 即***數組所在內存空間的首地址*** ;
  • 4.數組名 看做 常量指針 : 數組名可以看做指向數組首元素的常量指針, 當數組名 + 1 時, 可以看做指針 進行了 加 1 運算, 其地址按照指針運算規則, 增加了 數組元素大小 * 1 ;


指針減法運算 :

  • 1.指針之間的運算 : 兩個指針之間 只能進行 減法運算, 加法乘法除法不行, 并且 進行減法運算的兩個指針的類型必須相同 ;
  • 2.指針減法運算的前提 : 進行減法運算的***兩個指針類型必須是同一個類型*** ;
  • 3.指針減法運算的意義 : 指針減法運算時 兩個指針指向同一個數組才有實際的意義, 計算結果是 同一個數組 兩個指針指向位置的下標差 ;
  • 4.同類型無意義減法 : 如果兩個指針指向相同類型的不同數組, 即使減法有結果, 這個結果也是沒有任何意義的;

指針減法的過程 : 指針1 - 指針2 = ( 指針1指向的地址 - 指針2指向的地址 ) / sizeof (指針1和指針2的相同類型)



(2) 數組大小計算示例


數組大小計算代碼示例 :

  • 1.代碼示例 :
#include <stdio.h>int main() {int array[10] = {0};//打印出數組整體占用的內存數, 以及數組元素個數 printf("sizeof(array) = %ld, size = %ld \n", sizeof(array), sizeof(array)/sizeof(*array));return 0; }
  • 2.編譯執行結果 :


( 3 ) 指針 加法運算示例 ( 指針地址 + 4/8 * 被加數 )


數組名 指針 加法示例 :

  • 1.代碼示例 :
#include <stdio.h>int main() {int array[10] = {0};//打印出數組首元素地址, 打印出數組名 + 1 的值printf("array 地址 : %x, array + 1 地址 : %x\n", array, array + 1);return 0; }
  • 2.編譯運行結果 : 示例中的 array + 1 比 array 的地址大 4 個字節 ;

指針運算 : int * p, p + 1 代表的地址是 p 的地址 加上 4, 即加上了 一個 int 類型大小的地址;



( 4 ) 指針 減法 運算示例


指針減法代碼示例 :

  • 1.代碼示例 :
#include <stdio.h>int main() {int array_1[] = {0, 1, 2, 3, 4, 5};int array_2[] = {6, 7, 8, 9};//1. 定義指針 p1_0 指向 array_1 數組中的第 0 個元素int* p1_0 = array_1;//2. 定義指針 p1_5 指向 array_1 數組中的第 5 個元素int* p1_5 = &array_1[5];//3. 定義指針 p2_0 指向 array_2 數組中的第 0 個元素int* p2_0 = array_2;char c = 'c';//4. 定義了一個 char 類型指針char *p_c = &c;//1. 計算 p1_5 指針指向的元素 與 p1_0 指向的元素, 兩個元素在數組中的下標差printf("%d\n", p1_5 - p1_0);//2. ( p1_5 - p1_0 ) 與 ( ( unsigned int ) p1_5 - ( unsigned int ) p1_0 ) / sizeof ( int ) ) 是等價的 ;printf("%d\n", ( ( unsigned int ) p1_5 - ( unsigned int ) p1_0 ) / sizeof ( int ) );//3. 指針之間不支持加法, 這個操作在編譯時就會報錯, 這里注釋掉 ; //printf("%d\n", p1_5 + p1_0);//4. 兩個指針間的計算倒是沒毛病, 但是兩個指針分別指向兩個數組, 這個計算的結果沒有實際意義 ; printf("%d\n", p1_5 - p2_0);//5. 指針間進行運算的前提是 : 兩個指針的類型必須相同, 這兩個指針類型不同, 一個 int* 一個 char* , 編譯時報錯 ; //printf("%d\n", p1_5 - p_c);//6. 指針之間 不能進行乘法 和 除法, 編譯時會報錯//printf("%d\n", p1_5 * p1_0);//7. 指針之間 不能進行乘法 和 除法, 編譯時會報錯//printf("%d\n", p1_5 / p1_0);return 0; }
  • 2.編譯運行結果 :



2. 指針 比較 運算方式


( 1 ) 指針 比較 運算 ( 大于 小于 大于等于 小于等于 運算的前提是 必須指向同一數組 中的元素 | 任意兩指針只能進行 等于 不等于 的比較 )


指針的比較運算 :

  • 1.同一數組的比較運算 : 對于 大于 ( > ) , 小于 ( < ) , 大于等于 ( >= ) , 小于等于 ( <= ) 四種類型運算, 指針之間進行這四種運算的前提示 兩個指針 必須都指向同一個數組的元素 ;
  • 2.任意指針的比較運算 : 對于 等于 ( == ) , 不等于 ( != ) 兩種比較運算, 指針之間進行這兩種比較運算, 可以是任意指針, 指針指向不同數組也可進行這兩種運算 ;


( 2 ) 指針 比較 運算代碼示例 ( 用 指針 遍歷數組 )


使用指針遍歷數組代碼示例 :

  • 1.代碼示例 :
#include <stdio.h>int main() {char array_str[] = {'H', 'e', 'l', 'l', 'o'};//1. 定義數組第一個元素的起始指針char* p_start = array_str;//2. 定義數組最后一個元素 之后的指針, 這個指針只是做比較用的, 不會真正的尋址char* p_end = array_str + (sizeof(array_str) / sizeof(*array_str));//3. 定義循環控制變量char* p = NULL;//4. 遍歷數組for(p = p_start; p < p_end; p ++){printf("%c", *p);}//5.此處換行printf("\n");return 0; }
  • 2.編譯執行結果 :



3. 數組訪問方式


( 1 ) 下標 指針 訪問 ( 推薦使用下標訪問 )


下標訪問數組 和 指針訪問數組 的示例 : 這兩種訪問數組的方式是等價的 ;

  • 1.下標訪問數組 :
int array[5] = {0}; array[1] = 1; array[2] = 2;
  • 2.指針訪問數組 :
int array[5] = {0}; *(array + 1) = 1; *(array + 2) = 2;

下標訪問 和 指針訪問 對比 :

  • 1.可讀性 : 使用下標訪問數組, 數組的可讀性會大大的提高, 指針訪問數組不易理解 , 下標訪問在可讀性上優于指針訪問數組 ;
  • 2.性能 : 當使用一個固定的增量訪問數組時, 指針訪問 的性能 優于 下標訪問;

推薦使用方式 : 現在的編譯器編譯出來的代碼, 性能上 指針訪問 與 下標訪問基本相同, 出于代碼可讀性考慮, 推薦使用下標訪問數組的方式 ;


下標 指針訪問數組性能分析 : 以 數組 中的元素互相賦值為例 ;

  • 1.下標訪問 : 如訪問 array[3] ( 數組第 4 個元素 ) , 其首地址地址是 array 首地址 加上 3 個元素地址 ( 第三個元素的尾地址就是第四個元素的首地址 ) , 其預算方式是這樣的 : ( unsigned int ) array + sizeof(int) * 3 ;
  • 2.指針訪問 : 如訪問 array[3] , 以指針的形式, 如果每次遞增 1 個下標, 那么運算方式是 ( unsigned int ) array + 4 即可, 這里每次只做加法, 下標訪問每次都要用乘法, 乘法運算要比加法運算費時 ;


( 2 ) 下標 指針 訪問 數組 性能 代碼示例





3. int array[]; array 和 &array 區別


( 1 ) int array[] 中 array 和 &array 意義 ( ① array 數組首元素地址 | ② &array 數組地址 )


數組 int array[] 中 array 和 &array 意義 :

  • 1.數組元素首地址 : array 是數組首元素地址, sizeof ( *array ) 計算的是數組中單個元素的大小 ;
  • 2.數組地址 : & 是數組的首地址, 代表的是整個數組的地址 ;

兩種指針的運算 :

  • 1.array + 1 運算 : array + 1 的運算過程是 ( unsigned int ) array + sizeof (array) , 該運算相當于計算***數組中第二個元素的首地址** , 等價于 array[1] ;
  • 2.&array + 1 運算 : &array + 1 的運算過程是 ( unsigned int ) ( &array ) + sizeof(&array)***, 其中 &array 結果是 array, sizeof ( &array) 的結果 等價于 sizeof ( array ), 這是整個數組的大小, 因此 &array + 1 的*結果是數組的尾地址* ;


( 2 ) array 和 &array 計算 代碼示例


代碼示例 :

  • 1.代碼 :
#include <stdio.h>//注意 : 在 64 位電腦上, 計算指針時需要將指針地址墻磚為 unsigned long int 類型int main() {int array[5] = {0, 1, 2, 3, 4};//1. 計算 p1 指針指向 : // ( 1 ) &array 相當于 數組的地址, & array + 1 等價于 (unsigned long int) &array + sizeof ( *&array )// 等價于 (unsigned long int) &array + sizeof ( array ) , sizeof ( array ) 計算的是整個數組的長度// ( 2 ) 經過上述計算, 該指針指向 數組的尾地址, 即最后一個元素的結尾處int *p1 = ( int* )( &array + 1 );//2. 計算 p2 指針指向 : // ( 1 ) 單純地址 : (unsigned long int)array 這個操作將一個有類型的指針 直接強轉為單純的地址;// ( 2 ) 單純地址增加 : (unsigned long int)array + 1 就是之前的地址單純的加 1, 不再含有指針運算中 增加 數組元素 字節大小倍數 的意義;// ( 3 ) 拆分int類型字節 : 這樣如果計算 p2 指針指向的數據, 會將 4 個字節拆散計算, 可能從 元素1 中1取 3 個字節, 元素2 中取 1個字節// ( 4 ) 大小端方式選擇 : 計算方式注意大端模式 和 小端模式, 一版的電腦都是小端模式, // ( 5 ) 小端模式計算方式 : 這里我們按照小端模式計算, 小端模式即 高地址存放高字節, 低地址存放低字節int *p2 = ( int* )( (unsigned long int)array + 1 );//3. 計算 p3 指針指向// ( 1 ) array + 1 等價于 ( unsigned long int ) array + sizeof ( *array ), 該地址等價于 array[1] 地址// ( 2 ) 經過上述計算, p3 指針指向了 第二個元素的首地址int *p3 = ( int* )( array + 1 );//1. p1[-1] : p1 指向數組的尾地址, 其 -1 下標 即指向了數組最后一個元素, 等價于 array[4];//2. p2[0] : p2 指向了數組的第一個元素的 第二個字節地址, // 那么 p2[0] 的值是 數組第一個元素的 2 , 3, 4 三個字節, 再加上 第二個元素的 第一個字節;// 小端地址策略 : 高位地址存高位, 低位地址存低位, 那么 第二元素第一字節是 高位, 其次是第一數組元素的 4, 3, 2 字節//3. p3[1] : p3 指向了數組的第二個元素首地址, p3[1] 等價于 array[3]printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);return 0; }
  • 2.編譯運算結果 :
  • 3.圖示 :
  • 4.p2 指針計算過程 : 由上圖可以看到 指針指向的位置開始取一個 int 類型, 地址由低到高 四個字節存儲的數據為 0 0 0 1, 由于是小端模式, 高位地址存放在高位 其大小為 0x01 00 00 00 , 轉為十進制是 16777216 ;



4. 數組參數


( 1 ) 數組參數 概念 ( 退化成指針 | 需要帶上數組長度作為 附屬參數 )


數組參數相關概念 :

  • 1.數組作為參數時編譯器行為 : 數組作為參數時, 編譯器會將數組 退化成 指針, 此時這個指針就沒有數組的長度信息了 ;

    示例 1 : ***void method(int array[]) 等價于 method(int p)**, 此時 *p 中是不包含數組的長度信息的 ;
    示例 2 : *void method(int array[100]) 等價于 method(int p), 此時 *p 中是不包含數組的長度信息的 ;

  • 2.數組作為參數時的最佳用法 : 數組作為參數時, 應該定義另一個 int 類型的參數, 作為數組的長度信息 ;



( 2 ) 數組參數 代碼示例 ( 數組大小 | 數組參數大小 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h>/*編譯器在編譯時, 就將參數改為了 int* array 了C 語言中不會有數組參數的, 即使有, 也在編譯時被替換成指針了 */ void function(int array[100]) {printf("方法中數組參數 array 大小 : %ld\n", sizeof(array)); }int main() {int array[100] = {0};printf("main 方法中 array 數組大小 : array : %ld \n", sizeof(array));function(array);return 0; }
  • 2.編譯執行結果 :



5. 數組 指針 對比 ( 內存分配 : ① 指針 分配 4 / 8 字節 ② 數組分配所有元素地址 | 作為參數 | 常量[ 數組 ] 變量[ 指針 ] 區別 )


內存空間分配區別 :

  • 1.指針 ( 分配 4 或 8 字節 ) : 聲明指針的時候 只分配了 容納指針的 4字節 (32位系統) 或 8 字節 (64 位系統) ;
  • 2.數組 ( 分配連續內存 ) : 聲明數組時 分配了一篇容納數組所有元素的一片連續內存空間 ;

參數上的區別 ( 等價 ) : 作為參數時, 數組 和 指針 參數時等價的, 數組會退化為指針, 丟失長度信息 ;


指針 數組 的 性質 :

  • 1.數組 ( 常量 ) : 數組大部分情況下可以當做常量指針, 不能作為左值使用, 不能被賦值 ; (sizeof 和 & 作用域數組名時除外) ;
  • 2.指針 ( 變量 ) : 指針是變量, 變量中保存的值 是 內存中的一個地址 ;




四. 字符串



1. 字符串概念


( 1 ) 概念 ( 本質 是 char[] 數組 | ‘\0’ 結尾 | 存儲位置 棧 堆 常量區 )


字符串相關概念 :

  • 1.字符串本質 : C 語言中沒有字符串這個數據類型, 使用 char[] 字符數組來模擬字符串 ;
  • 2.字符串要求 : 不是所有的字符數組都是字符串, 只有***以 ‘\0’ 結尾的字符數組***才是字符串 ;
  • 3.字符串存儲位置 : 棧空間, 堆空間, 只讀存儲區 (常量區) ;


( 2 ) 示例代碼 ( 字符串概念 | 字符串 )


代碼示例 :

  • 1.代碼 (正確的版本) :
#include <stdio.h> #include <malloc.h>int main() {//1. s1 字符數組不是以 '\0' 結尾, 不是字符串char s1[] = {'H', 'e', 'l', 'l', 'o'};//2. s2 是字符串, 其在 棧內存 中分配內存控件char s2[] = {'H', 'e', 'l', 'l', 'o', '\0'};//3. s3 定義的是字符串, 在 只讀存儲區 分配內存空間 // s3 指向的內容無法修改, 如果想要修改其中的數據, 會在執行時報段錯誤char* s3 = "Hello";//這個操作在執行時會報段錯誤, 因為 s3 指針指向只讀存儲區//s3[0] = 'h';//4. s4 是以 '\0' 結尾, 是字符串, 在 堆空間 中分配內存char* s4 = (char*)malloc(2*sizeof(char));s4[0] = 'H';s4[1] = '\0';return 0; }
  • 2.編譯運行結果 ( 錯誤版本 報錯提示 ) : 取消 s3[0] = ‘h’; 注釋, 嘗試修改 只讀存儲區的數據 , 運行時會報段錯誤 ;



2. 字符串 長度


( 1 ) 字符串長度計算 ( 不包括 ‘\0’ | 標準庫中有該函數)


字符串長度 :

  • 1.概念 : 字符串包含的字符個數, 不包含 ‘\0’ , 只包括有效字符 ;
  • 2.計算字符串長度 : 根據從字符串開始到 ‘\0’ 結束, 計算不包括 ‘\0’ 的字符個數 ;
  • 3.數組不完全使用 : 如果數組長度100, 在第50個元素位置出現了 ‘\0’, 那么這個字符串長度是 49, 數組長度是 100 ;

針對 C 標準庫已有的函數 :

  • 1.不要自己實現 C 標準庫功能 : C 標準庫是優化到極致, 個人修改的效果比庫函數效果要差 ;
  • 2.復用庫函數效率高 : 不要重復制造輪子 ;


( 2 ) 代碼示例 ( 字符串長度計算示例 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h> #include <string.h>int main() {//1. 字符串長度 : 以 '\0' 之前的個數為準, 不包括 '\0' , 字符串長度 5// 即使后面有有效的字符, 那么也不屬于字符串 c //2. 數組長度 : 數組長度 666char c[666] = {'H', 'e', 'l', 'l', 'o', '\0', 'w', 'o', 'r', 'l', 'd'};printf("字符串長度 : %ld, 字符數組長度 : %ld\n", strlen(c), sizeof(c));return 0; }
  • 2.編譯運行結果 :


( 3 ) 代碼示例 ( 自己實現 strlen 方法 )


實現 strlen 方法代碼示例 ( 普通版本 ) :

  • 1.代碼 :
#include <stdio.h> #include <assert.h>size_t strlen(const char* s) {size_t len = 0;//1. 如果 s 為 NULL, 直接中斷程序assert(s);//2. 指針先自增, 在取指針內指向的數據值, 看看是否為'\0'// 如果指向的數據為 '\0', 那么循環中斷執行下面的內容while(* s++){len ++;}return len; }int main() {char * s1 = "0123";char * s2 = NULL;//1. 測試 s1 的實際長度, 返回 4printf("s1 長度 : %u\n", strlen(s1));//2. 測試空字符串長度, 運行時會中斷printf("s2 長度 : %u\n", strlen(s2));return 0; }
  • 2.編譯運行結果 :

實現 strlen 方法代碼示例 ( 遞歸版本 ) :

  • 1.代碼 :
#include <stdio.h> #include <assert.h>size_t strlen(const char* s) {//1. assert(s) 先驗證是否為 NULL , 如果為 NULL 中斷程序//2. 遞歸退出條件 : *s 為 '\0' 時, 遞歸退出//3. s + 1 即指向 字符串的 下一個 char 元素, 計算 下一個 char 到結尾的個數, // 當指向 '\0' 時, 之后的字符串個數為0, 然后依次退出遞歸return ( assert(s), ( *s ? (strlen(s + 1) + 1) : 0 ) ); }int main() {char * s1 = "0123";char * s2 = NULL;//1. 測試 s1 的實際長度, 返回 4printf("s1 長度 : %u\n", strlen(s1));//2. 測試空字符串長度, 運行時會中斷printf("s2 長度 : %u\n", strlen(s2));return 0; }
  • 2.編譯執行結果 :



3. 字符串函數 長度不受限制 情況


( 1 ) 不受限制的字符串函數 ( 函數自動尋找 ‘\0’ 確定字符串大小 | stpcpy | strcat | strcmp )


不受限制的字符串函數相關概念 :

  • 1.字符串常用方式 : 一般在函數中使用字符串時, 需要指明字符串的大小, 因為字符串數組 一旦當做函數參數時, 就退化成指針, 失去了大小信息 ;
  • 2.字符串相關的函數不需要大小信息 : 在 string.h 中的方法, 不需要傳入大小信息, 函數中會自動尋找 ‘\0’ 來計算字符串的長度 ;
  • 3.參數不是字符串則出錯 : 不受限制字符串函數如果傳入的字符串沒有 ‘\0’ , 則會出錯 ;

不受限制的字符串函數示例 :

char *stpcpy(char *dest, const char *src);//字符串拷貝char *strcat(char *dest, const char *src);//字符串拼接int strcmp(const char *s1, const char *s2);//字符串比較

不受限制字符串函數 的 相關注意事項 :

  • 1.字符串必須以 ‘\0’ 結尾 : 此類函數相關的字符串必須以 ‘\0’ 結尾, 因為字符串長度是根據找到的 ‘\0’ 來計算的, 如果沒有 ‘\0’ 會報錯 ;

  • 2.字符串長度改變相關 : strcpy ( 字符串拷貝 ) 和 strcat ( 字符串拼接 ) 必須保證 拷貝 或 拼接的 目標數組 有足夠的空間來保存結果字符串 ;

  • 3.字符串比較函數 : strcmp 兩個字符串比較, 如果返回 0 , 表示兩個字符串相等 ;

    • 函數 : int strcmp(const char *s1, const char *s2);
    • ( 1 ) 返回值 等于 0 : 兩個字符串相等 ;
    • ( 2 ) 返回值 大于 0 : 第一個字符串 大于 第二個字符串 ;
    • ( 3 ) 返回值 小于 0 : 第一個字符串 小于 第二個字符串 ;

    注意字符串要求 : strcmp 函數不會修改 s1 和 s2 字符串的值, 但是兩個字符串必須符合要求 以 ‘\0’ 結尾 ;



( 2 ) 代碼示例 ( 自己實現字符串拷貝函數 )


實現拷貝字符串函數 :

  • 1.代碼 :
#include <stdio.h> #include <assert.h>//函數作用, 將 src 字符串 拷貝到 dst 指針指向的內存中, 同時將拷貝完的結果 dst 返回 char* strcmp ( char* dst, const char* src ) {//1. 安全編程, 傳入的兩個值不能為 NULLassert(dst && src);//2. 將 src 指針指向的 字符 賦值給 dst 指針指向的值, 然后兩個指針自增 1// 如果賦值的指針不等于 '\0' , 那么繼續賦值, 如果賦值的值為 '\0' 就退出循環while( (* dst++ = * src++) != '\0' );//3. 其返回值也是 dst 參數, 參數也可以當作返回值使用return dst; }int main() {char dst[20];printf("%s\n", strcpy(dst, "字符串拷貝"));return 0; }
  • 2.編譯運行結果 :



4. 字符串函數 長度受限制 情況


( 1 ) 受限制的字符串函數 ( 推薦使用 降低錯誤率 )


長度受限制的字符串函數 :

  • 1.概念 : 長度受限制的字符串函數, 其 字符串參數 會 跟隨一個字符串先關的長度測參數, 一般為 size_t 類型, 用于限定字符串的字符數 ;
  • 2.推薦使用 : 在函數調用的時候, 優先使用長度受限制的字符串函數, 這樣會減少出現錯誤的幾率 ;

長度受限字符串函數 舉例說明 :

  • 1.字符串拷貝 : char *strncpy(char *dest, const char *src, size_t n) ;
    • ( 1 ) 作用 : 拷貝 src 中 n 個字符 到 dest 目標字符串中 ;
    • ( 2 ) src 長度 小于 n : 使用 ‘\0’ 填充剩余空間 ;
    • ( 3 ) src 長度 大于 n : 只賦值 n 個字符, 并且不會使用 ‘\0’ 結束 , 因為已經復制了 n 個字符了 ;
  • 2.字符串拼接 : char *strncat(char *dest, const char *src, size_t n) ;
    • ( 1 ) 作用 : 從 src 字符串中賦值 n 個字符 到 dest 字符串中 ;
    • ( 2 ) 始終 ‘\0’ 結尾 : 函數始終在 dest 字符串之后添加 ‘\0’;
    • ( 3 ) 不填充剩余空間 : 對于拼接后剩余的數組空間, 不使用 ‘\0’ 填充 ;
  • 3.字符串比較 : int strncmp(const char *s1, const char *s2, size_t n) ;
    • ( 1 ) 作用 : 比較 src 和 dest 中前 n 個字符 是否相等 ;




五. 指針數組 與 數組指針



1. 數組指針


( 1 ) 數組類型介紹 ( 數組元素類型 | 數組大小 | 舉例 int[8] )


數組類型 :

  • 1.數組類型要求 : 數組的類型有兩個決定要素, 分別是 ① 數組元素類型 和 ② 數組大小 ;
  • 2.數組類型示例 : int array[8] 的類型是 int[8] ;

數組類型定義 :

  • 1.數組類型重命名 : 使用 typedef 實現 , typedef type(數組名)[數組大小] ;
  • 2.數組類型重命名示例 :
    • ( 1 ) 自定義一個 int[5] 類型的數組 : typedef int(ARRAY_INT_5)[5] ;
    • ( 2 ) 自定義一個 float[5] 類型的數組 : typedef float(ARRAY_FLOAT_5)[5] ;
  • 3.根據自定義的數組類型聲明變量 :
    • ( 1 ) 使用自定義的 ARRAY_INT_5 聲明變量 : ARRAY_INT_5 變量名 ;
    • ( 2 ) 使用自定義的 ARRAY_FLOAT_5 聲明變量 : ARRAY_FLOAT_5 變量名 ;


(2) 數組指針簡介 ( 指向數組的 一個 指針 | 數組指針類型定義方式 : 數組元素類型 ( * 指針名稱 ) [數組大小] )


數組指針 : 本質是一個指針 ;

  • 1.數組指針作用 : 數組指針 用于 指向一個數組 ;
  • 2.數組名意義 : 數組名是數組首元素地址, 不是數組的首地址 , &數組名 是數組的首地址 ;
  • 3.數組首地址 : & 數組名 是數組首地址 , 數組首地址 不是 數組名( 數組首元素地址 ) ;
  • 4.數組指針定義 : 數組指針是通過 定義 數組類型 的指針;
    • ( 1 ) 數組類型 : 是之前說過的 包含 ① 數組元素類型 , ② 數組大小 兩個要素, 定義數組類型 : typedef int(ARRAY_INT_5)[5] 定義一個 int[5] 類型的數組類型 ;
    • ( 2 ) 定義數組指針 : ARRAY_INT_5* 指針名稱 ;
  • 4.數組指針定義的另外方式 : 類型 ( * 指針名稱 ) [數組大小] ;
    • ( 1 ) 示例 : *int (p)[5] , 定義一個指針 p, 指向一個 int[5] 類型的指針 ;
    • ( 2 ) 不推薦此種寫法 : 可讀性很差 ;

數組指針 和 數組首元素指針 大小打印 :

  • 1.代碼示例 :
#include <stdio.h>int main() {//定義數組int array[5] = {0};//1. 普通指針, 普通指針類型是 int, 指向數組首元素首地址, 其指向內容大小為數組的首元素int *p = array;//2. 數組指針, 數組指針類型是 int[5], 指向數組首地址, 其指向的內容大小是整個數組 typedef int(ARRAY_INT_5)[5];ARRAY_INT_5 *p1 = &array;//3. 打印數字首元素指針 和 數組指針 指向的內存大小// 數組首元素指針 大小 為 4// 數組指針 大小 為 20printf("%ld, %ld\n", sizeof(*p), sizeof(*p1));return 0; }
  • 2.編譯運行結果 :


( 3 ) 代碼示例 ( 定義數組類型 | 數組指針用法 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h>//1. 自定義數組類型 int[5] 類型為 ARRAY_INT_5, 包含信息 ①int 類型數組 ②數組包含5個元素 typedef int(ARRAY_INT_5)[5];//2. 自定義數組類型 float[5] 類型為 ARRAY_FLOAT_5, 包含信息 ①float 類型數組 ②數組包含5個元素 typedef float(ARRAY_FLOAT_5)[5];//3. 自定義數組類型 char[5] 類型為 ARRAY_CHAR_5, 包含信息 ①char 類型數組 ②數組包含5個元素 typedef char(ARRAY_CHAR_5)[5];int main() {//1. 使用自定義數組類型定義數組 : 定義一個 int 類型數組, 個數為 5個, 等價于 int array_1[5];ARRAY_INT_5 array_1;//( 1 ) sizeof(ARRAY_INT_5) 打印 ARRAY_INT_5 類型大小, // 該類型包含信息 ① int 類型數組 大小 5 個 ② 大小為 20 字節//( 2 ) sizeof(array_1) 打印 array_1 數組大小printf("%ld, %ld\n", sizeof(ARRAY_INT_5), sizeof(array_1));//2. 常規方法定義數組float array_2[5];//3. 數組指針 : 定義一個數組指針, 指向數組地址, 使用 &數組名 來獲取數組地址, 其指向的內存內容大小為 20 字節// 注意區分數組首元素地址, array_2 是數組首元素地址, 指向內容大小為 4 字節// 注意區分 float* p = array_2, 這個指針是指向數組首元素地址的指針ARRAY_FLOAT_5* p = &array_2;//( 1 ) (*p)[i] 解析 : p 是數組指針, 其地址是數組地址 &array_2, // *p 就是數組地址中存放的數組內容 *(&array_2), 即 (*p)[i] 等價于 array_2[i]// (*p)[i] = i 語句 等價于 array_2[i] = i ;int i = 0;for( i = 0; i < sizeof(array_2)/sizeof(*array_2); i ++){(*p)[i] = i;}//( 2 ) 打印數組中每個值for(i = 0; i < sizeof(array_2)/sizeof(*array_2); i ++){printf("array_2[%d] = %f\n", i, array_2[i]);}//4. 使用自定義數組類型定義數組ARRAY_CHAR_5 array_3;//5. 常規方法定義數組指針 : char(*p1)[5] 等價于 ARRAY_CHAR_5* p1; char(*p1)[5] = &array_3;//6. 定義數組指針, 但是這個賦值過程中 左右兩邊類型不一致, // array_3 會被強轉為 char[6] 類型的數組指針char(*p2)[6] = array_3;//( 1 ) &array_3 : 是 array_3 數組的 數組地址, 絕對值 等于 其數組首元素地址//( 2 ) p1 + 1 : p1 指針 是 array_3 的數組指針, 該指針指向一個數組, p1 + 1 增加的是一個數組的地址//( 3 ) p2 + 1 : p2 指針 也是一個數組指針, 這個數組比 array_3 數組大一個, 因此 p2 + 1 地址可能比 p1 + 1 大1字節printf("&array_3 地址值 : %x, p1 + 1 地址值 : %x, p2 + 1 地址值 : %x\n", &array_3, p1 + 1, p2 + 1);return 0; }
  • 2.編譯運行結果 :



2. 指針數組


( 1 ) 指針數組簡介 ( 數組中存儲的元素是指針 | 數組指針 int (array)[5] 本質是指針 | 指針數組 int array[5] 本質是數組 )


指針數組 相關概念 :

  • 1.指針數組概念 : 指針數組是一個普通的數組, 其元素類型是 指針類型 ;
  • 2.指針數組定義 : 類型* 數組名稱[數組大小] ;
    • ( 1 ) 指針數組 : int* array[5] ;
    • ( 2 ) 數組指針 : int (*array)[5] ;


( 2 ) 代碼示例 ( 指針數組使用案例 )


指針數組代碼示例 :

  • 1.代碼 :
#include <stdio.h>/*1. 函數作用 : 傳入一個字符串, 和 一個字符串數組, 找出字符串在字符串數組中的索引位置, 從 0 開始計數2. const char* key 參數分析 : ( 1 ) 常量分析 : 左數右指(const 在 * 左邊 數據是常量, const 在 * 右邊 指針是常量), 這里數據是常量, 不可修改( 2 ) 參數內容 : 字符串類型, 并且這個字符串內容不能修改3. const char* month[] 參數分析 : 指針數組( 1 ) 常量分析 : 左數右指, 指針指向的數組內容不能修改( 2 ) 參數內容 : 指針數組, 每個指針指向一個字符數組, 這些字符數組都是字符串, 這些指針不可改變 */ int find_month_index(const char* key, const char* month[], const int month_size) {int index = -1;//4. 遍歷指針數組中指向的每個字符串, 與傳入的 key 進行對比, 如果相等, 那么返回字符串在指針數組的索引// ( 1 ) 對比函數 : 注意 strcmp 函數, 對比兩個字符串, 如果相等 則 返回 0 ;int i = 0;for(i = 0; i < month_size; i ++){if(strcmp(key, month[i]) == 0){index = i;}}return index; }int main() {//1. 定義 指針數組, month 數組中, 每個元素都是一個指針, 每個指針指向字符串 const char* month[] = { "January", "Febrary", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; printf("查詢 July 字符串索引 : %d\n", find_month_index("July", month, sizeof(month)/sizeof(*month)));printf("查詢 HanShuliang 字符串索引 : %d\n", find_month_index("HanShuliang", month, sizeof(month)/sizeof(*month)));return 0; }
  • 2.編譯執行結果 :



3. main 函數參數 分析


( 1 ) main 函數簡介


main 函數分析 :

  • 1.main 函數 : main 函數是 ① 程序的 入口函數 , ② 操作系統調用的函數 ;
  • 2.main 函數示例 :
int main() int main(int argc) int main(int argc, char* argv[]) int main(int argc, char* argv[], char* env[])
  • main 函數參數說明 :
    • ( 1 ) int argc 參數 : 程序命令行參數個數 ;
    • ( 2 ) char argv[] 參數* : 程序命令行字符串參數數組, 這是一個數組指針, 數組中每個元素都是指向一個字符串的指針 ;
    • ( 3 ) char env[] 參數* : 環境變量數組, 這是一個數組指針, 數組中每個元素都是指向一個字符串的指針 ; 這個環境變量 在 Windows 中是配置的 環境變量, 在 Linux 中是配置在 /etc/profile ( 一種設置方式, 還有很多設置方式 ) 中定義的環境變量 ;


(2) main 函數 代碼示例


main 函數代碼示例 :

  • 1.代碼示例 :
#include <stdio.h>int main(int argc, char* argv[], char* env[]) {//1. 循環控制變量int i = 0;//2. 打印 main 函數參數printf("########參數個數%d 開始打印參數\n", argc);for(i = 0; i < argc; i ++){printf("%s\n", argv[i]);}printf("########參數打印完畢\n");printf("\n");printf("\n");//3. 打印環境變量printf("########開始打印環境變量\n");for(i = 0; env[i] != NULL; i ++){printf("%s\n", env[i]);}printf("########環境變量打印完畢\n");return 0; }
  • 2.編譯運行結果 : 環境變量打印出來的東西太多了, 就不一一截圖查看 ;





六. 多維數組 和 多維指針



1. 二維指針 ( 指向指針的指針 )



( 1 ) 二維指針簡介 ( 指向指針的指針 )


指向 指針 的 指針 ( 二維指針 ) :

  • 1.指針變量 : 指針變量會占用 內存空間 , 很明顯可以使用 & 獲取指針變量的地址 ;
    • ( 1 ) 32 位系統 : 指針占 4 字節空間 ;
    • ( 2 ) 64 位系統 : 指針占 8 字節空間 ;
  • 2.指向 指針變量 的指針 : 定義一個指針, 這個指針 保存一個 指針變量 的地址 ( 不是 指針變量指向的地址, 是指針變量所在的本身的地址 ) ;

指針變量 的 傳值 和 傳址 調用 :

  • 1.指針變量傳值調用 ( 一維指針 ) : 直接將指針值傳入, 修改的是 指針 指向的內存空間內容 ;

如 : void fun ( char *p ) , 這是相對于指針的傳值調用, 相對于 char 類型數據的傳址調用, 用于修改 p 指針指向的內存中的值 ;

  • 2.指針變量傳址調用 ( 二維指針 ) : 在函數內部 修改 函數外部的變量, 需要傳入一個地址值, 如果要修改的是一個指針, 那么需要傳入指針的地址, 即參數是一個指向指針的指針 ; 指針變量傳址調用, 修改的是 指針 指向的 指針變量 ;

如 : void fun(char ** pp) 該傳址調用 即 傳入的是 char* 指針的地址, 修改的是 pp 二維指針 指向的 char* 類型指針 ;

  • 3.函數中修改函數外部變量 : 只能使用指針 指向這個外部變量, 才可以修改這個外部變量 , 如果這個外部變量本身就是一個指針 , 那么就必須傳入這個指針的地址, 那么傳入的參數的內容就是一個二維指針 ;


( 2 ) 代碼示例 ( 指針的傳址調用 | 指向指針的指針 | 重置指針指向的空間 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h> #include <malloc.h>/*1. 方法作用 : 為參數 char **p 指向的 指針 重新分配內存空間2. char **p 參數 : 需要在函數中修改函數外部的變量, 就需要傳入一個 指向要修改的目標 的指針變量需要修改的內容 是一個指針, 那么需要傳入的參數就是 指向 指針變量 的指針這樣才能完成 傳址調用, 用來修改函數外部的變量 3. */ int reset_memory(char **p, int size, int new_size) {//1. 定義函數中使用的變量//( 1 ) 定義返回值int ret = 0;//( 2 ) 循環控制變量int i = 0;//( 3 ) 新空間的大小int len = 0;//( 4 ) 申請的新空間char* p_new = NULL;//( 5 ) 用于計算用的指向新空間的指針char* p_new_tmp = NULL;//( 6 ) 用于指向老空間指針, 使用 * 與 傳入的二維指針 計算 得來// char** p 是指向 char* 指針 的 指針, 使用 *p 即可獲得 指向 char* 的指針char* p_old = *p;//2. 前置條件安全判定, 避免無意義崩潰if(p == NULL || new_size <= 0){return ret;}//3. 重新分配空間, 并拷貝內存中的內容//( 1 ) 重新分配內存空間p_new = (char*)malloc(new_size);//( 2 ) 為計算使用的指針賦值, 之后賦值是需要使用指針的自增, 為了不改變指針內容, 這里我們設置一個臨時指針p_new_tmp = p_new;//( 3 ) 獲取 新空間 和 老空間 內存大小的最小值, 將老空間的內容拷貝到新空間中// 1> 新空間大于老空間 : 只拷貝所有老空間中的內容到新空間中// 2> 新空間小于老空間 : 只拷貝能將新空間填滿的內容, 字符串可能丟失 '\0'len = (size < new_size) ? size : new_size;//( 4 ) 將老空間中的內容拷貝到新空間中for(i = 0; i < len; i ++){*p_new_tmp++ = *p_old++ ;}//4.釋放原空間, 并修改傳址調用的參數內容free(*p);*p = p_new;ret = 1;return ret; }int main(int argc, char* argv[], char* env[]) {//1. 第一次為 char 類型指針分配 10 個字節空間char* p = (char*)malloc(10);// 打印指針 p 指向的內存地址, 即分配的內存空間地址printf("p 第一次分配空間后指向的地址 : %x\n", p);//2. 重置內存空間, 原來分配 10字節, 現在改為分配 8 字節// 注意 : 在 reset_memory 函數中改變函數外部變量的值, 需要傳址調用, 即將變量的地址傳到函數中reset_memory(&p, 10, 8);// 打印重置空間后的指針指向的地址printf("p 重置空間后指向的地址 : %x\n", p);return 0; }
  • 2.編譯運行結果 :



2. 二維數組


( 1 ) 二維數組 ( 存放方式 | 數組名 | 首元素類型 | 數組名 類似 常量指針 | )


二維數組 相關概念 : 二維數組 int array[5][5] ;

  • 1.二維數組存放方式 : 二維數組在內存中以 一維數組 方式排布 ;
  • 2.二維數組數組名 : 代表 二維 數組 首元素 地址, 其 首元素 是一個一維數組 , 即 array[0] ;
  • 3.二維數組首元素類型 : 數組名 array 指向二維數組首元素, 那么其類型是 數組指針, 數組類型 為 int[5] ( ① int 類型數組, ② 含有 5 個元素 ) ;
  • 4.數組名類似常量指針 : 二維數組的數組名可以看做常量指針, 除了兩種情況 sizeof 計算大小 和 & 獲取地址時 ;
  • 5.具體的數據值存放 : 二維數組第一維是 數組指針, 第二圍才是具體的數據值 ;
  • 6.二維數組圖示 :

一些注意點 :
1.編譯器沒有二維數組概念 : C語言中沒有二維數組改變, 編譯器 都按照一維數組來處理, 數組的大小在編譯時就確定了 ;
2.二維數組由來 : C 語言中的數組元素可以是任何類型, 即可以是一維數組, 這樣就產生了二維數組 ;
3.首元素地址確定時間 : 在編譯階段確定的 除了 數組大小外, 數組的首元素也是在編譯階段確定的, 在程序運行階段首元素地址不能被修改 (看做常量) ;



(2) 代碼示例 ( 以一維數組方式遍歷二維數組 | 體現二維數組的數據排列 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h> #include <malloc.h>/*遍歷一維數組1. int *array 參數解析 : 傳入的一維數組的首地址 2. int size 參數解析 : 用于限制數組大小, 數組傳入后也會退化為指針, 數組是帶有元素個數屬性的, 因為數組類型是 int[9], 但是指針不包含元素個數 指針類型是 int* */ void array_traverse (int *array, int size) {int i = 0;for(i = 0; i < size; i ++){//使用數組遞增的方式打印數組元素printf("array[%d] = %d\n", i, *array++);} }int main() {//1. 定義二維數組int array[2][2] = {{0, 1}, {2, 3}};//2. 獲取二維數組首地址, 將其賦值給 一維數組 int* pint *p = &array[0][0];//3. 打印一維數組, 可以看到, 二維數組的數據排列// 先將 {0, 1} 放入的內存, 然后緊接著存放 {2, 3} 數據array_traverse(p, 4);return 0; }
  • 2.編譯執行結果 :

代碼分析 :
將二維數組的首地址賦值給 類型相同 的一維數組, 遍歷該一維數組, 并且該數組的大小為 二維數組所有值得大小 , 由此可以看出, 二維數組的數據排布是按照索引, 先放入二維數組的第一個數組元素, 在按照索引依次將數組放入內存中 ;




3. 數組名


( 1 ) 數組名 簡介 ( 數組首元素地址 | &數組名 是 數組地址 )


數組名 相關概念 :

  • 1.數組名 : 數組名代表了 數組首元素地址 ;
  • 2.一維數組 數組名 : int array[2], array 指向數組首地址, 其指向了一個 int 類型首元素, array 類型為 int * ;
  • 3.二維數組 數組名 : int array[2][3] , array 指向數組首地址, 其指向了 類型 int[3] 數組的首元素, array 的類型是 int(*)[5] ;
    • ( 1 ) 類似常量指針 : 二維數組的數組名 可以看做為 常量指針 ;
    • ( 2 ) 看做一維數組 : 二維數組可以看做一維數組, 只是這個一維數組內的元素 是 一維數組 ;
    • ( 3 ) 二維數組元素 : 二維數組中每個元素都是 基礎類型同類型的 一維數組,


( 2 ) 代碼示例 ( 數組名指針指向的內容 | 二維指針數組名對應的指針運算 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h>int main() {//1. 定義二維數組, array 代表了 數組首地址// array 的本質是 指向 一個 int(*)[5] 類型的一維數組 的指針int array[5][5];//2. 定義數組指針, 指針 p 指向一個 int(*)[4] 一維數組 int(*p)[4];//3. 注意了, 兩個指針類型不同// ( 1 ) array 指針 : 指向 int(*)[5] 類型的一維數組, array + 1 即內存中移動了 5 個 int 大小的內存// ① 下標運算 : array[1] 即 array 指針 + 1 ;// ( 2 ) p 指針 : 指向 int(*)[4] 類型的一維數組, p + 1 即內存中移動了 4 個 int 大小的內存p = array;/*4. 指針計算過程 : ( 1 ) &array[4][2] 計算 : array 指針指向 int(*)[5] 一維數組, array[4] 計算過程是 要加上 4 個 int(*)[5] 一維數組 , array[4] 指向 內存中(二維數組起始為0) 第 20 個元素, array[4][2] 指向第 22 個元素, &array[4][2] 是一個指向 array[4][2] 的int 類型指針( 2 ) &p[4][2] 計算 : p 指針指向 int(*)[4] 一維數組, p[4] 計算過程是 要加上 4 個 int(*)[4] 一維數組 , p[4] 指向 內存中(二維數組起始為0) 第 16 個元素, p[4][2] 指向第 18 個元素, &p[4][2] 是一個指向 p[4][2] 的int 類型指針( 3 ) 一個指向第 22 個元素, 一個指向第 18 個元素, 其起始地址是一樣的, 因此結果是 -4*/printf("%ld\n", &p[4][2] - &array[4][2]);return 0; }
  • 2.編譯運行結果 :
  • 3.圖示 :

( 3 ) 代碼示例 ( 一維數組遍歷 | 二維數組遍歷 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h>//1. 宏定義, 使用該宏 計算數組大小 #define ARRAY_SIZE(a) (sizeof(a)/sizeof(*a))int main() {//1. 遍歷一維數組int array_1d[5] = {0, 1, 2, 3, 4};int i = 0;for(i = 0; i < ARRAY_SIZE(array_1d); i++){/**(array_1d + i) 執行過程 : ( 1 ) array_1d + i : 首元素指針 + i, 即獲取指向第 i 個元素的指針( 2 ) *(array_1d + i) : 獲取第 i 個元素指向的數據, 該表達式等價于 array_1d[i]*/printf("array_1d[%d] = %d\n", i, *(array_1d + i));}//2. 遍歷二維數組int array_2d[3][3] = { {0, 1, 2}, {3, 4, 5}, {6, 7, 8} };int j = 0;for(i = 0; i < ARRAY_SIZE(array_2d); i ++){for(j = 0; j < ARRAY_SIZE(array_2d[0]); j ++){/**(*(array_2d + i) + j) 計算過程 : ( 1 ) array_2d : 是二維數組數組首元素, 本質是數組指針, 類型是 int(*)[3], 指向一個 int[3] 數組 ;( 2 ) array_2d + i : 是指向數組 第 i 個元素, 其地址本質能上移動了 i 個 int[3] 數組所占的空間 ;( 3 ) *(array_2d + i) : array_2d + i 是一個數組指針, 使用 * 即獲得其指向的內容 一個 int[3] 數組;( 4 ) *(array_2d + i) + j : 獲取其指向的 int[3] 類型數組的 第 j 個指針 ;( 5 ) *(*(array_2d + i) + j) : 即獲取 int[3] 類型數組中 第 j 個指針指向的實際的 int 數據 ;*/printf("array_2d[%d][%d] = %d\n", i, j, *(*(array_2d + i) + j));}}return 0; }
  • 2.編譯運行結果 :

( 4 ) 代碼示例 ( 為二維數組申請內存空間 )


算法思想 : 為 int[3][3] 申請內存空間 ;

  • 1.申請空間 : 分別申請 數組指針空間 和 數據空間 ;
    • ( 1 ) 申請數組指針空間 : 申請三個 數組指針 空間, 只是三個普通的指針, 但是該指針類型是 int(*)[3], 即指向 int[3] 數組的指針 ;
    • ( 2 ) 申請數據空間 : 申請 能存放 9個 int 值的數據空間 ;
  • 2.分配指針 : 將申請的三個 數組指針 , 分別指向對應的 9 個int 值空間的對應位置 ;

代碼示例 :

  • 1.代碼 :
#include <stdio.h> #include <malloc.h>//1. 宏定義, 使用該宏 計算數組大小 #define ARRAY_SIZE(a) (sizeof(a)/sizeof(*a))/*為二維數組分配內存空間1. 參數說明 : ( 1 ) int row : 二維數組的行數, 即 有多少 一維數組; ( 2 ) int column : 每個一維數組中的元素個數 */ int** malloc_array_2d(int row, int column) {//1. 分配 數組指針 空間, 分配數組指針空間, 共有 row 個指針int** ret = (int**)malloc(sizeof(int*) * row);//2. 分配數據存放空間, 即 row * column 個 int 類型數據存放的空間int * p = (int*)malloc(sizeof(int) * row * column);if(ret && p){//3_1. ret 和 p 內存分配成功, 那么開始 將 ret 數組指針 與 p 內存空間 連接起來int i = 0;for(i = 0; i < row; i ++){/*說明 : ( 1 ) ret 是分配的 數組指針 數組的 首元素, ret[i] 是第 i 個 數組指針 元素( 2 ) p + i*column 是 具體到 int 類型元素的首地址( 3 ) 這里將 int* 類型指針賦值給了 int(*)[column] 類型指針, 所幸是地址賦值, 將int* 指針存放的地址 賦值給了了 int(*)[column] 類型指針的地址*/ret[i] = (p + i * column);}}else{//3_2. 如果分配空間失敗, 即 ret 或 p 有一個內存分配失敗, 釋放所有內存, 返回空free(ret);free(p);ret = NULL;}return ret; }//釋放申請的二維數組空間 void free_array_2d(int** array_2d) {//array_2d[0] 代表了 第一個數組 的收個 int 類型元素地址, 該操作釋放了int類型數據空間內存free(array_2d[0]);//array_2d 代表了 數組指針 數組的 首元素地址, 該操作釋放了數組指針空間的內存free(array_2d); }int main() {//1. 申請二維數組空間int** array_2d = malloc_array_2d(3, 3);int i = 0, j = 0;//2. 為二維數組賦值for(i = 0; i < 3; i ++){for(j = 0; j < 3; j ++){*(*(array_2d + i) + j) = i * 3 + j;}}//3. 打印二維數組內容for(i = 0; i < 3; i ++){for(j = 0; j < 3; j ++){printf("array_2d[%d][%d] = %d\n", i, j, array_2d[i][j]);}}return 0; }
  • 2.編譯運行結果 :
  • 3.圖示 :




五. 數組參數 與 指針參數



1. 數組參數退化為指針參數的意義


( 1 ) 數組參數退化的相關概念 ( 指針退化成數組 )


一維數組參數退化為指針 :

  • 1.C語言中的拷貝方式 : C 語言中只會以 傳值拷貝 的方式來傳遞參數 ;
    • ( 1 ) 傳遞指針也是傳值 ( 修改指針指向的地址的內容是用戶行為 ) : 只是傳的是指針變量的值, 但是這個變量中存放著地址, 函數中可以改變這個地址的值 ;
  • 2.數組傳遞的方式 :
    • ( 1 ) 傳遞整個數組 : 如果將整個數組傳遞過去, 如果數組中元素很多, 需要將數組所有元素都要拷貝一份 ;
    • ( 2 ) 傳遞數組首元素地址 : 將數組名 ( 數組首元素的地址 ) 看做常量指針傳入函數 ;
    • ( 3 ) C 語言針對數組參數的效率考慮 : 假如數組有 10000 個元素, 傳遞數組效率就非常低了, 如果傳遞數組首元素指針, 只用拷貝指針變量的值, 只拷貝 4 ( 32位系統 ) 或 8 ( 64位系統 ) 個字節, 這樣效率能大大提高 ;
  • 3.數組參數退化的意義 : 數組參數退化為指針, 程序的執行效率能大大的提高 ;

二維數組參數退化問題 :

  • 1.二維數組本質 : 二維數組也可以看做一維數組, 該一維數組中的每個數組元素都是一維數組 ;
  • 2.數組退化過程 :
    • ( 1 ) 一維數組參數退化過程 : void fun(int array[5]) <-> void fun(int array[]) <-> void fun(int* array) 以上的三種類型的參數都是等價的 ;
      • ① 第一次退化 : 數組的個數可以省略掉, 只需要表明數組元素類型即可, 數組元素類型 int[] 類型;
      • ② 第二次退化 : 只含有數組元素類型 不含數組個數的類型, 退化為 對應數組元素類型 的指針類型 ;
    • ( 2 ) 二維數組參數退化過程 : void fun(int array[3][3]) <-> void fun(int array[][3]) <-> void fun(int (*array)[3])
      • ① 第一次退化 : 數組的個數可以省略掉, 只需要表明數組元素類型即可, 數組元素類型 int[3] 類型;
      • ② 第二次退化 : 直接退化為指向 一維數組的 數組指針, 該數組指針類型為 int(*)[3] 類型;

下面列舉數組參數與指針參數一些等價關系 : 去中括號 ( [] ), 變星號 ( * ) , 放左邊;

數組參數指針參數
一維數組 int array[5]指針 *int array
一維指針數組 int array[5]*指針 int* array*
二維數組 int array[3][3]指針 *int (array)[3]

注意事項 :
1.多維數組參數要求 : 傳遞多維數組參數時, 需要將除第一維之外的其它所有維度的大小都帶上 , 否則無法確定數組大小 和 類型, 編譯時會報錯 ;
2.數組參數限制 :
( 1 ) 一維數組 : 可以不帶數組長度, 但是必須指定數組的大小 ;
( 2 ) 二維數組 : 數組 第一維 長度可以不帶 ( 即 數組指針 元素個數可以省略 ) , 但是數組指針 指向的 數組類型大小必須指定 ( 第二維的大小必須指定 ) ;
( 3 ) 三維數組 : 數組 第一維 長度可不帶, 但是第二維 和 第三維 長度 必須帶上 ;



( 2 ) 代碼示例 ( 二維數組參數 的指針退化 | 外層指針退化 | 內層數組指針沒有退化 )


代碼分析 : 如 int array[3][3] ;

  • 1.二維數組參數退化部分 : 二維數組本身 array 數組大小退化, 其退化為 int (*)[3] 類型, 指向一組數組指針的首地址 ;
  • 2.二維數組參數沒有退化部分 : array 數組中, array 作為首元素, 其類型為 int[3] 類型, 該類型 包含 ① 其指向的一維數組 中的元素類型 int 和 ② 一維數組大小 3;

代碼示例 :

  • 1.代碼 :
#include <stdio.h>void traverse_array_2d(int array_2d[][2], int row) {/*計算二維指針的列數( 1 ) array_2d 是二維指針中的 數組指針 數組中的首元素( 2 ) array_2d 是一個完整的數組指針, 該指針中包含著 其指向的數組的 類型 和 大小( 3 ) 數組指針退化時, 退化的只是 array_2d 的 數組指針 數組 (最外層的一維數組) 大小, 其每個元素都是一個 數組指針, 這個數組指針 包含 數組類型 和 大小, 沒有退化*/int column = sizeof(*array_2d) / sizeof(*array_2d[0]);int i = 0, j = 0;for(i = 0; i < row; i ++){for(j = 0; j < column; j ++){printf("array_2d[%d][%d] = %d\n", i, j, array_2d[i][j]);}} }int main() {int array_2d[2][2] = {{0, 1}, {2, 3}};traverse_array_2d(array_2d, 2);return 0; }
  • 2.編譯執行結果 :




六. 函數指針



1. 函數類型 和 函數指針


(1) 相關概念 ( 函數類型要素 ① 返回值, ② 參數類型, ③ 參數個數, ④ 隱含要素 : 參數順序 | 函數指針類型 返回值類型 (*變量名) (參數列表) )


函數類型 :

  • 1.函數類型引入 : 每個函數都有自己的類型 ;
  • 2.函數類型要素 : ① 返回值, ② 參數類型, ③ 參數個數, ④ 隱含要素 : 參數順序 ;
    • 示例 : void fun(int a, float b) 函數的類型為 void( int, float ) ;
  • 3.函數重命名 : 使用 typedef 可以為函數重命名 , typedef 返回值類型 函數名稱(參數列表) ;
    • 示例 : typedef void function(int, float), 就是將上面的 fun 函數重命名為 function ;

函數指針 :

  • 1.函數指針概念 : 函數指針 指向一個 函數類型 變量 ;
  • 2.函數名 : 函數名指向了 函數體 的 入口地址 ;
  • 3.函數指針定義( 宏定義類型 ) : 函數類型* 變量名 ;
  • 4.函數指針類型( 簡單類型 ) : 返回值類型 (*變量名) (參數列表) ;


( 2 ) 代碼示例 ( 定義函數指針 : ①typedef int(FUN)(int); FUN* p; 或者 ② void(*p1)(); | 給 函數指針 賦值 , 右值 可以直接使用 ① 函數名 或 ② &函數名 | 調用函數指針方法 : ① 函數指針變量名(參數) ② (*函數指針變量名)(參數) | 函數名 和 &函數名 是等價的 | 函數指針變量名(參數) 和 (*函數指針變量名)(參數) 也是等價的 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h>//1. 定義函數類型 FUN, 其類型為 int(int) typedef int(FUN)(int);//2. 定義一個 int(int) 類型的函數 int fun_1(int i) {return i * i * i; }//3. 定義一個 void() 類型的函數 void fun_2() {printf("調用 fun_2 函數\n"); }int main() {/*1. 將 fun_1 函數賦值給 FUN 類型指針( 1 ) FUN 是函數類型, 其類型是 int(int)( 2 ) fun_1 函數名是函數體的入口地址, 可以直接賦值給指針 ; */FUN* p = fun_1;//2. 通過指針調用函數, 指針變量名(參數) 可以調用指針指向的函數 ; printf("調用 p 指針結果 : %d\n", p(10));/*3. 定義 void() 類型的函數指針 p1( 1 ) 方法返回值(*函數指針變量名)(參數列表) 是定義一個函數指針( 2 ) &函數名 也可以獲取函數的地址, 與 函數名 是等價的;注意 : 這里與數組不同, 數組名 和 &數組名 是兩種不同的概念函數名 和 &函數名 是等價的*/void(*p1)() = &fun_2;/*4. 通過函數指針變量調用函數( 1 ) 通過 函數指針變量名(參數) 和 (*函數指針變量名)(參數) 兩種方法都可以調用函數指針變量指向的函數( 2 ) 函數名 和 &函數名 是等價的, 函數指針變量名(參數) 和 (*函數指針變量名)(參數) 也是等價的*/p1();(*p1)();return 0; }
  • 2.編譯運行結果 :



2. 回調函數


( 1 ) 回調函數相關概念


回調函數簡介 :

  • 1.回調函數實現 : 回調通過 函數指針 調用函數實現 ;
  • 2.回調函數特點 : 調用者 和 被調用的函數 互不知情, 互不依賴 ;
    • ( 1 ) 調用者 : 調用者不知道具體的函數內容, 只知道函數的類型 ;
    • ( 2 ) 被調函數 : 被調用的函數不知道 調用者 什么時候調用該函數, 只知道要執行哪些內容 ;
    • ( 3 ) 調用方式 : 調用者 通過 函數指針 調用具體的函數 ;


( 2 ) 代碼示例 ( 回調函數示例 )


代碼示例 :

  • 1.代碼 :
#include <stdio.h>//1. 定義函數類型 FUN, 其類型為 int(int) typedef int(FUN)(int);//2. 定義一個 int(int) 類型的函數 int fun_1(int i) {return i * i * i; }//3. 定義一個 int(int) 類型的函數 int fun_2(int i) {return i + i + i; }//4. 定義一個 int(int) 類型的函數 int fun_3(int i) {return i + i * i; }/*5. 此處參數 FUN function 是一個函數類型, 將 FUN 類型函數注冊給 execute 函數 */ int execute(int i, FUN function) {return i + function(i); }int main() {//1. 給 execute 注冊 fun_1 函數, 具體事件發生時調用 fun_1 函數printf("int i = 5; i * fun_1(i) = %d\n", execute(5, fun_1));//2. 給 execute 注冊 fun_2 函數, 具體事件發生時調用 fun_2 函數printf("int i = 8; i * fun_2(i) = %d\n", execute(8, fun_2));//3. 給 execute 注冊 fun_3 函數, 具體事件發生時調用 fun_3 函數printf("int i = 2; i * fun_3(i) = %d\n", execute(2, fun_3));return 0; }
  • 2.編譯運行結果 :



3. 解讀 復雜的 指針聲明 ( 難點 重點 | ①找出中心標識符 ②先右 后左 看 確定類型 提取 ③ 繼續分析 左右看 … )


指針 定義 復雜性來源 :

  • 1.數組指針 : 數組指針類型為 int (*) [5] , 即 一個指向 int[5] 的指針, 其指針變量名稱寫在中間的括號中
  • 2.函數指針 : 函數指針類型為 int(*)(int, int), 即 一個指向 int(int, int) 類型函數的指針, 其指針變量名稱寫在中間的括號中 ;
  • 3.數組指針混合函數指針 : 如果出現了 數組指針 指向一個函數, 這個指針可讀性很差, 理解需要一定的功力 ;

復雜指針閱讀技巧 ( 主要是 區分 函數指針 和 數組指針 ) 右左法則 :

  • 1.最里層標示符 : 先找到最里層的圓括號中的標示符;

    數組指針和函數指針的標示符 ( 指針變量名 ) 都在中間的圓括號中, 因此該步驟先找到指針變量名

  • 2.右左看 : 先往右看, 再往左看 ;

  • 3.確定類型 : 遇到 圓括號 “()” 或者 方括號 “[]” 確定部分類型, 調轉方向 ; 遇到 * 說明是指針 , 每次確定完一個類型 , 將該類型提取出來 , 分析剩下的 ;

    一種可能性 :
    int (*) [5] , 遇到中括號說明是數組指針類型,
    int(*)(int, int) , 遇到圓括號 說明是函數指針類型 ;

  • 4.重復 2 , 3 步驟 : 一直重復, 直到 指針 閱讀結束 ;


指針閱讀案例 :

  • 1.解讀案例 1 :
/*解讀步驟 : 1. 研究第一個標示符 p ( 1 ) 先找最里層的圓括號中的 標示符 p( 2 ) p 往右看, 是圓括號, 然后往左看, 是 * , 可以確定 p 是一個指針( 3 ) 將 (*p) 拿出來, 然后看剩下的部分, 右看是 圓括號 (, 明顯是個函數類型, int (int*, int (*f)(int*)) 很明顯是一個 函數類型2. 解讀函數類型 int (int*, int (*f)(int*))( 1 ) 函數類型 int (int*, int (*f)(int*)) 的返回值類型是 int 類型( 2 ) 函數類型的第一個參數類型是 int* , 即 int 類型指針類型( 3 ) 函數類型的 第二個參數是 int (*f)(int*) 也是一個函數類型指針3. 解讀 int (*f)(int*) 參數( 1 ) 標示符是 f, 由看 是 圓括號, 坐看是 * , 因此 f 是一個指針;( 2 ) 將(*f) 提取出來, int(int*) 是一個函數類型, 其返回值是 int 類型, 參數是 int* 指針類型總結 : 指針 p 是一個指向 int(int*, int (*f)(int*)) 類型函數的指針, 函數返回值是 int 類型, 參數是 int* 指針類型 和 int (*)(int*) 函數指針 類型指針 f 是一個指向 int(int*) 類型函數的指針, 其返回值是 int 類型, 參數是 int* 指針類型*/int (*p) (int*, int (*f)(int*));

  • 2.解讀案例 2 :
/*解讀步驟 : 1. 確定 p1 的類型( 1 ) 找出最中心圓括號中的標示符, p1;( 2 ) 數組類型確定 : 右看發現中括號, 說明 p1 是一個數組, 數組中有 3 個元素, 數組的類型目前還不知道( 3 ) 數組內容確定 : 左看發現 *, 說明數組中存儲的是 指針類型, 這里就知道了 ;目前知道了 數組 p1 的要素 : ① 數組中有 3 個元素, ② 數組元素類型是指針;2. 確定數組指針類型 : 上面確定了 p1 的數組個數 和 元素是指針, 但是指針指向什么不確定( 1 ) 將 (*p1[3]) 提取出來, int(int*) 明顯是一個函數類型, 返回值是 int 類型, 參數是 int* 類型總結 : p1 是一個數組, 數組中含有 3 個元素, 數組元素類型為 int(*)(int*) 函數指針, 即 指向 int(int*) 類型函數的指針 */int (*p1[3])(int*);

  • 3.解讀案例 3 :
/*解讀步驟 : 1. 確定 p2 類型 : ( 1 ) 找出最中心的圓括號中的標示符, p4, 右看是圓括號 ), 掉頭左看是 * , 說明 p2 是一個指針;( 2 ) 將 (*p2) 提取出來, 分析int (*[5])(int*), ( 3 ) 右看是 [, 說明指針指向了一個數組, 該數組有 5 個元素( 4 ) 左看是 * , 說明數組中的元素是指針, 下面分析指針指向什么2. 確定指針數組中指針指向什么 : ( 1 ) 將 (*(*p2)[5]) 提取出來, 可以看到 指針指向 int(int*) 類型的函數總結 : p2 是一個數組指針, 指向一個數組, 該數組有 5 個元素, 每個元素都是一個指針, 數組中的指針元素指向 int(int*) 類型的函數*/int (*(*p2)[5])(int*);

  • 4.解讀案例 4 :
/*解讀步驟 : 1. 確定 p3 基本類型 : ( 1 ) p3 右看是圓括號 ), 左看是 * , 說明p3 是指針( 2 ) 將 (*p3) 提取出來, int (*(int*))[5], 右看是 圓括號 (, 說明指針指向一個函數( 3 ) 函數的參數是 int*, 返回值是一個指針, 指向一個類型2. 確定返回值的類型( 1 ) 將 (*(*p3)(int*)) 提取出來, 右看是 [, 說明是數組類型, 剩下 int[5] 類型, 返回值指針指向一個 int[5] 類型的數組, 那么返回值類型是 int(*)[5] 數組指針總結 : p3 指向一個 函數, 函數的參數是 int* 指針, 返回值是 指向 int[5] 數組 的 數組指針*/int (*(*p3)(int*))[5];


《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

總結

以上是生活随笔為你收集整理的【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。