OverView of Content
指標對於底層系統開發來說相當重要,而驅動又是透過控制 Register 來控制硬體,在這操控中就常常使用到指標
在學習C語言時,理解指標(pointers
)、數組(arrays
)、以及常見的語法規則如何運作是至關重要的。本文將探討指標和數組的基本概念,以及如何使用它們來更有效地編寫C代碼。
指標的概念和特性
指標是C語言中一個重要且強大的概念。它們是用來存儲變量地址的變量,通常用來間接訪問數據。指標具有首地址(即變量的地址)的特性,這使得它們可以在程序中輕鬆地操作和訪問內存中的數據。
Null
指標和 const
修飾符
在C語言中,Null指標是一個特殊的指標值,表示該指標不指向任何有效的內存地址。這在程序中很常見,用於標識指標的無效狀態。此外,const修飾符用於聲明常量,可以應用於變量和指標,以確保它們的值不會被修改。
陣列 雙重指標、數組和二維數組
數組是一系列相同類型的元素的集合。它們在C語言中被廣泛使用,可以通過索引來訪問元素。二維數組則是數組的一種特殊形式,其中的元素是一維數組。理解如何訪問和操作數組對於有效地處理複雜數據結構至關重要。
比較使用 typedef
和 define
在C語言中,typedef和define都是用來創建類型別名的工具。它們可以使代碼更易讀且更易於維護。通過將複雜的類型別名簡化為更可讀的形式,typedef和define有助於提高代碼的可讀性和可理解性。
強制轉換和 sizeof
運算符
強制轉換(type casting
)在C語言中用於將一種數據類型轉換為另一種數據類型。這在處理不同類型的數據時很有用。另一方面,sizeof運算符用於返回數據類型或變量的大小(以字節為單位),這對於動態分配內存和確定數據結構的大小非常有用。
通過深入理解這些基本概念,您將能夠更好地理解和應用C語言中的指標和數組。在接下來的文章中,我們將逐一探討每個主題,並提供清晰的示例代碼和詳細的解釋,以幫助您更好地理解和應用這些概念。
指標概念
● 普通變數:首先我們要知道一般的讀寫都不會涉及 強制轉換,哪種類型的變數,就會以相對應的格式存在 RAM 中
int a = 10;
long b = (long) a;
● 指標變數:
A. 可以把指標當成是另一種類型的宣告
B. 指標的內容:跟普通變數一樣,是儲存某一個數值,只是 指標是儲存一個地址 (使用關鍵字 &
來取得某個變數的地址)
int a = 10; // 儲存 10
int* p = &a; // 儲存 a 的地址, p 本身也有地址
指標本身也有地址
指標特性 - 首地址
● 內存的大小:這取決於 尋址總線數量,如果尋址總現有 32 條,那地址最大可到 232;相對來說,如果有 64 條,那地址最大可到 264
● 由於 在電腦中,硬體是以 1 Byte 作為單元切割,所以當一個指標拿到一個類型的首地址,就會自動順延到符合該類型的長度內容
int a = 10;
// 取得 a 的第一個 Byte 的首地址,自動往後推 3 Byte
// 最終以 4 Byte 作為該變量的空間
int* p = &a;
// --------------------------------------------------------
struct hello_t {
int a;
int b;
int c;
};
struct hello_t hello_world = {0};
// 取得 hello_world 的第一個 Byte 的首地址,自動往後推 11 Byte (符合類型)
// 最終以 12 Byte 作為該變量空間的地址
struct hello_t* hw = &hello_world;
● 指標可以透過類型推導出接下來需要幾個 Byte 的數據
● 一個地址能儲存的大小就為 1 個 Byte (跟硬體有關)
指標級量
● 指標可以指向另外一個指標,層層疊加,這就是指標的級量
int a = 10;
// 一級指標
int* b = &a;
// 二級指標
int** c = &b;
// 三級指標
int*** d = &c;
(n + 1 級) = &(n 級)
建議指標不要超過 3 級,不僅降低了可讀性,效率也會不好
指標其他作用 - 作用域
● 一般我們在規範函數不讓其它文件訪問時會使用 static,但是只要透過指標就可以取得該函數並執行
static
關鍵字在 C 語言中相當於「存取限制符號」,使用 static
描述的函數、屬性,只能在該檔案中被訪問,其他檔案不可訪問!
// 只有該檔案內部可訪問
static void hello() {
}
// 只要有 include 的檔案都可以使用
void world() {
}
指標使用
指標符號
● *
符號:
A. 宣告指標變量使用 ,前後都可
int *p;
// 同上
int* p;
// ------------------------------------------------
int *p1, *p2; // 兩個指標變量
int *p1, p2; // 一個指標、一個整數變量
B. 解引用
int a = 10;
int *p = &a;
// 解引用,並賦予值
*a = 20;
● &
符號:
取變量的第一個 Byte 的地址
int a = 10;
int *p = &a;
野指標
● 只要指標可能出現未知性錯誤,它就是一個野指標;可能產生野指標的操作如下
●
Segmentation Fault
:程式為了防止雪崩性錯誤,會使用Segmentation Fault
停止該應用程式;而錯誤又分為兩種● 大段錯誤:地址不存在
● 小段錯誤:地址存在,但訪問受限
A. 尚未初始化就直接使用
void wild_ptr() {
char* p;
*p += 1; // Segment Error
}
B. 不清楚空間權限,試圖訪問、修改資料
void wild_ptr_2() {
char *p = "hello"; // "hello" 放置在常量區,常量區不許須改
*(p + 1) = 'w'; // Segment Error
}
C. 越界訪問
void wild_ptr_3() {
int buf[4] = {0};
*(buf + 4) = 10; // + 4 已越界
}
●
*(buf + 4)
與*buf + 4
是不同的,這有關於符號的優先度A.
*(buf + 4)
是 buf 這個地址加上 4 個 ByteB.
*buf + 4
是數組第一個數 buf[0] 再加 4
What is Null
● Null 在 C/C 中是不同定義的存在,C 中被定義為 0,但是在 C 中定義為 (void*) 0
,會被嚴格檢查
#ifdef _cplusplus
#define NULL 0
#else
#define NULL (void*) 0
#endif
const 修飾符
const 也就是 constant 代表不變,用來修飾變量,希望變量轉為常量
const 修飾普通變數
● 修飾變數,不管 const 是在前還是在後都可以,只要保證在變數之前即可
A. 類型之前
#include <stdio.h>
int main()
{
const int a = 10;
a = 30; // const 不可修改,所以編譯會錯
printf("Hello: %d\n", a);
return 0;
}
B. 類型之後
#include <stdio.h>
int main()
{
// 不同之處
int const a = 10;
a = 30; // const 不可修改,所以編譯會錯
printf("Hello: %d\n", a);
return 0;
}
const 修飾指標
● const 修飾指標有三種表現方式,個代表了不同的限制(注意 const
放置的位置)
A. const 修飾指標指向的空間:及說明 該空間的內容物為常量;修飾指標指向的空間常量有 2 種表達方式,代表的意思是相同
void const_ptr_1() {
int tmp = 20;
int const *a = &tmp;
// const int *a = &tmp; // 同上
*a = 100; // Read-only 編譯器檢查錯誤,不可修改 !
printf("Hello: %d\n", *a);
}
B. const 修飾指標:使用 const 修飾指標說明 該指標指向不可在修改,也就是 指標無法再指去其他地方,但其 值能是可被修改
void const_ptr_2() {
int tmp = 20;
int * const a = &tmp;
*a = 100; // OK
int tmp2 = 30;
a = &tmp2; // Read-only 編譯器檢查錯誤,不可修改 !
printf("Hello: %d\n", *a);
}
C. const 修飾指標 & 修飾指向空間:代表指標 & 其指向的內容物都不可以修改
void const_ptr_3() {
int tmp = 20;
int const * const a = &tmp;
// const int * const a = &tmp; // 同上
*a = 100; // Read-only 編譯器檢查錯誤,不可修改 !
int tmp2 = 30;
a = &tmp2; // Read-only 編譯器檢查錯誤,不可修改 !
printf("Hello: %d\n", *a);
}
指標修改 const
● const 機制是通過編譯器檢查實現,實際上真正運行的過程中並不關心變數是否被 const 修飾,只要保證編譯通過,程式仍可跳過 const 檢查
void const_ptr_4() {
const int a = 100;
int *p = NULL;
p = &a; // 會有警告而已
*p = 300;
printf("Hello: %d\n", a); // 可修改 a 的值
}
● 既然可以被修改,那為何要使用 const 修飾?
讓程式更加健壯,提醒使用者某修地方不能被修改,或是保證不會被修改
const & 變量 & 常量
● 在程式中我們常會使用 1
、2
、3
、"HELLO"
... 等等數值,而這些數值在編譯器的處理下會以兩種方式存在 RAM 中
A. 變量
經過編譯後,會將 變量放置在 .data
、.bss
中,常出現在 堆
、棧
中,這些變量都是 可讀可寫;經過編譯檢查 const 關鍵字,可將這些數值看做 偽常量
真正的常量不可修改,而偽常量 其實仍可修改
:::info
Linux 可使用 readelf
來查看編譯出來的執行檔中的 .data
、.bss
區塊
B. 常量
經過編譯後,會將 變量放置在 .ro.data
中,訪問權限為 可讀 (不可改)
// p 儲存首字 `H` 的地址,而 "Hello const" 則是放置在常量區
char *p = "Hello const"; // 如果透過指標修改這個變量,則會失敗 (Segmention Fault)
Array
深刻了解一維數組,是了解二維(甚至多維)數組的關鍵
// 格式如下
<類型> <變量名>[<數量>]
int a[100]; // 0 ~ 100
long b[1] // 0 ~ 1
以內存(記憶體)的角度來看,Array 的物理記憶體是連續的,並不會斷開,所以訪問速度也快
Array 訪問
A. 變量名訪問:最基礎的訪問方式就是透過變量名稱來訪問
void accessByName() {
int a[10] = {0};
a[0] = 100;
printf("a[0]: %d\n", a[0]);
a[10] = 9; // 越界,但仍可正常設定值
printf("a[10]: %d\n", a[10]);
}
B. 指標訪問:不受到編譯器的 作用域檢查 規範
void accessByPtr() {
int a[10] = {0};
int *p = a; // a 本身就是一個地址,加上了 `[]` 才能解析其內容
*p = 100;
printf("a[0]: %d\n", a[0]);
*(p + 10) = 9;
printf("a[10]: %d\n", a[10]);
}
一維數組 & 符號
● 數組的符號有 4 種不同的意思(而部分其中還有細分,說明如下):1 a
、2 a[0]
、3 &a[0]
、4 &a
int a[10] = {0};
A. Array 符號 a
:有兩種含意
● Array 名稱:sizeof(a)
時,可以計算出該數組占用幾個 byte 大小
● Array 的第一個地址:等同於 &a[0]
,是 數組的首元素的首個字節,是一個常量值
● 如果是地址,那代表了是常量不可修改,所以永遠不會是左值
int a[10] = {0}; a = 1000; // a 是常量,不可為左值 (被賦予)
B. Array 符號 a[0]
:取第一個元素的空間,並可以對其讀寫操作
int a[10] = {0};
ntf("a[0]: %d\n", a[0]); // read
a[0] = 1000; // write
C. Array 符號 &a[0]
:取締一個元素的首位元空間地址,就等同於符號 a
D. Array 符號 &a
:數組首地址,代表一個地址常量,同樣不可為左值
● 符號
&a
、a
的差異 ?兩者的數值皆是 Array 的首地址,但是 意義完全不同;
&a
代表該空間的全體,而a
只代表了該空間的 1 個元素void symbleTest2() { int a[5] = {0}; printf("a: %p, &a: %p\n\n", a, &a); printf("a+1: %p, &a+1: %p\n", a+1, &a+1); }
●
&a+1
代表地址前進int a[5]
●
a+1
代表地址前進 1 個int
指標 & Array
● 一般訪問 Array 是透過 index 來指定要訪問 Array 的第幾個元素
void iterate_array() {
int array[10] = {0};
for(int i = 0; i < sizeof(array)/sizeof(array[0]); i++) {
// array[i] 使用 index 取元素
printf("array[%d]: %d\n", i, array[i]);
}
}
● Array 的 變數名本身就有指標的意義,所以可以透過指標來訪問,這指標也分為兩中 1. 常量指標 (不可修改)、2. 變量指標 (可修改)
A. 常量指標:常量指標其實就是代表 Array 宣告的變數名 (Symble),該變數不可再修改,它是一個常量值 !
void ptr_with_array_1() {
int array[10] = {0};
for(int i = 0; i < sizeof(array)/sizeof(array[0]); i++) {
// 使用 `array + i` 改變常量指標 array
printf("array[%d]: %d\n", i, *(array + i));
}
}
● 可否修改成
*(array)
?不行!因為 Array 宣告出的變數名是一個常量,既然是常量就不可以修改 !
B. 變量指標:
void ptr_with_array_2() {
int array[10] = {0};
int *p = array;
for(int i = 0; i < sizeof(array)/sizeof(array[0]); i++) {
// 如果是變數,就可以修改為 *(p++),以下還有幾種方案,都可以達到相同的效果
// *(p++) 可以寫作
// 1. p[i]
// 2. *(p + 1)
// 3. *(p + 1 * sizeof(int))
printf("array[%d]: %d\n", i, *(p++));
}
}
● 以下寫法是錯誤的
int array[10] = {0}; // 錯誤! array 原本就是首地址的第一個 Byte,語意變成了 // array 首地址的首地址 p = &array;
● 使用指標位置 + 1,編譯器會依照指標的大小,新增一個單元
int array[10] = {0}
int *p = array;
int a = *(p + 1); // (p + 1) 相等於 (p + 1 *sizeof(int))
指標類型 & 強制轉換
對於編譯器來說,數據類型就是告訴編譯器該變數,要已什麼樣的格式儲存 (數據結構),儲存的空間又是多大 ?
A. 儲存空間:依照類型、硬體裝置,編譯器會給予不同變量,不同空間大小
sizeof(char); // 1 Byte
sizeof(short); // 2 Byte
sizeof(int); // 4 Byte
sizeof(float); // 4 Byte
sizeof(double); // 8 Byte
B. 儲存結構:即便是相同大小的 int
、float
儲存格式也不同
int a = 10;
printf("%d", a); // 正確
printf("%f", a); // 亂碼
// --------------------------------------------
float b = 10;
printf("%d", b); // 亂碼
printf("%f", b); // 正確
一般類型強制轉換 - 顯示
float 是用科學計數法形式儲存 (其中就包括:小數、指數、符號... 等等)
● 強制轉換一般類型,需要注意幾個點:空間
、結構
● 空間大小改變
A. 小轉大:沒啥問題,資料可以正常轉換
void small_to_big() {
short a = 10;
int b = (int)a;
printf("b value: %d, size: %d", b, sizeof b);
}
B. 大轉小:小心數據丟失、改變!(並非一定會丟失,但是很大機率會將數據解釋錯)
void big_to_small() {
int a = 0x0000ffff;
printf("a value: %d, size: %d\n", a, sizeof a);
short b = (short)a;
printf("b value: %d, size: %d", b, sizeof b);
}
● 結構改變:將原儲存資料方式就不同的結構強制轉型,像是浮點數(浮點數的儲存結構很不同)與整數的轉換
A. 整數轉浮點數,數據不丟失
void change_struct_1() {
int a = 100;
// 轉換儲存結構
float b = (float) a;
printf("b value: %f, size: %d", b, sizeof b);
}
B. 浮點數轉整數,數據丟失
void change_struct_2() {
float a = 3.1415926;
// 丟失小數部分數據
int b = (int)a;
printf("b value: %d, size: %d", b, sizeof b);
}
一般類型強制轉換 - 隱示
● 上面使用 (<類型>)
來做強制轉換,而隱式轉換則如下
A. 使用 =
符號的隱示轉換:
void implict_change_1() {
char c = 0x11223344;
printf("c value: %d, size: %d", c, sizeof c);
}
:::info
● 這種隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題)
B. 當使用 return
關鍵字,在返回之前隱示轉換:
int implict_change_2() {
char c = 0x11223344;
return c;
}
● 這種
return
隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題)
指標強制轉換
● 指標轉換「最好使用」顯示轉換,盡量不要使用隱式轉換;指標轉換也涉及兩個層面:1. 指標類型轉換、2. 指標指向類型
A. 指標類型轉換:改變數據的解析方式
void ptr_value_change_1() {
int a = 100;
int *pa = &a;
printf("a ptr value: %d, size: %d\n", *pa, sizeof(*pa));
float *pb = NULL;
// 指標類型轉換
pb = (float*) pa;
printf("b ptr value: %f, size: %d", *pb, sizeof(*pb));
}
:::warning
● 結構的解析方式不同,導致數據解析異常
從這邊可以看出,改變指標的類型,使用 *
也會改變對於該結構的解析方式;上面改變指標為 float*
導致解析時不以 int
的結構來解析數據內容
B. 指標指向類型(不改變指標,改變解指標後的數據)
void ptr_value_change_2() {
int a = 0;
float b = 3.1415926;
int *pa = &a;
float *pb = &b;
*pa = (int)*pb;
printf("a ptr value: %d, size: %d", *pa, sizeof(*pa));
}
sizeof
sizeof
看似 Function,但其實它是 C 語言中的 運算符號 !
sizeof vs. array
char array[] = "Hello";
| sizeof 計算 | 結果 | 說明 | 注意 |
sizeof 計算 | 結果 | 說明 | 注意 |
---|---|---|---|
sizeof(array) | 6 | 前面有說過 array 符號用在 sizeof 會計算該 Array 所有的空間 | 包括 \0 (字符算的結尾) |
sizeof(array[0]) | 1 | 一個 array 元素大小 | |
strlen(array) | 5 | 使用 C 標準函式庫 | 會解析到 \0 為止 (不包含),所以結果會少 1 |
● 測試 1:測試 Array 首指標、Array 元素、C 標準庫的 strlen
測試
#include <stdio.h>
#include <stdlib.h>
void sizeof_vs_array() {
char array[] = "Hello";
printf("sizeof(array): %d\n", sizeof(array));
printf("sizeof(array[0]): %d\n", sizeof(array[0]));
printf("strlen: %d\n", strlen(array));
}
● 測試 2:sizeof
配合 Array 首元素、首地址、C 標準庫的 strlen
測試
#include <stdio.h>
#include <stdlib.h>
void test_ptr_strlen() {
char str[] = "Hello";
char* p = str;
printf("sizeof(*p): %d, sizeof(p): %d, strlen(p): %d\n",
sizeof(*p),
sizeof(p),
strlen(p)); // 會計算 *p 到 '\0' 之間的位元數
}
sizeof vs. 指標
char array[] = "Hello";
sizeof 計算 | 結果 | 說明 | 注意 |
---|---|---|---|
sizeof(p) | 4 | 指標 p 的大小 | sizeof 會根據類型判別大小,array 才會計算整體 |
sizeof(*p) | 1 | 一個 char 大小 | |
strlen(p) | 5 | 使用 C 標準函式庫 | 會解析到 \0 為止 (不包含) |
● 測試:指標、指標取得的元素、C 標準庫的 strlen
測試
void sizeof_vs_ptr() {
char array[] = "Hello";
char *p = array;
printf("sizeof(p): %d\n", sizeof(p));
printf("sizeof(*p): %d\n", sizeof(*p));
printf("strlen(p): %d\n", strlen(p));
}
array 作為參數傳遞
● C 語言由於 考慮到 Stack
大小的關係 (入參會導致棧溢出),設計在 傳遞 Array 時,傳入的入參是 Array address
而不是整體
void transfer_array(int array[20]) {
printf("inner sizeof(array): %d\n", sizeof(array));
}
void template_array() {
int array[20] = {0};
printf("outsize sizeof(array): %d\n", sizeof(array));
transfer_array(array);
}
● 知道 Function 傳入的是 Array address
後,其實可以修改如下(將接收的類型改為 pointers
),也會有一樣的功能
void transfer_array_2(int *array) {
for(int i = 0; i < 20; i++) {
printf("Value: %d\n", *(array + i));
}
}
void template_array() {
int array[20] = {0};
printf("outsize sizeof(array): %d\n", sizeof(array));
transfer_array_2(array);
}
● 那要決定用哪一種,要選宣告接收 Pointer 還是 Array?
對於程式的「可讀性」來說,我們還是會 選擇使用
a[10]
這種寫法,因為 這能明確標示請使用者傳入的是一個數組,而不是一個普通數值的 Ptr
● 使用 Array 類型的另類寫法:使用 Array 前,宣告大小變數,這個變數就可以讓 Array 變數使用
void transfer_array_3(int size, int array[size]) {
for(int i = 0; i < size; i++) {
printf("Value: %d\n", *(array + i));
}
}
void template_array() {
int array[20] = {0};
printf("outsize sizeof(array): %d\n", sizeof(array));
transfer_array_3(5, array);
}
●
size
參數必須定義在 array 之前! 否則編譯檢查不能通過
高級指標
其實沒啥高級指標,應該說是指標比較高級的用法
指標數組 & 數組指標
● C 語言語法的重點是:前面是修飾詞,後面才是主語
● 變數 - 指標數組:代表該變數,主語是 Array,並且修飾(類型)是 Ptr
● 變數 - 數組指標:代表該變數,主語是 Ptr,並且修飾(類型)是 Array
類型 | 主語 (本質) | 修飾 | 舉例 |
---|---|---|---|
指標 數組 | 數組 (Array) | 指標 (Ptr) | int *p[5]; (本質是 Array,每個元素都為 (int*) ,代表 5 個指標) |
數組 指標 | 指標 (Ptr) | 數組 (Array) | int (*p)[5] (本質是 1 指標,指向 int[5] 的空間) |
● *
、[]
在符號的前後,指明了該變量是指標還是數組 !(這還有關於到優先級) 定義一個符號時思考的步驟如下
A. 找到定義的符號,誰是「核心」
// `p` 是核心
// `int`、`*`、`[]` 都是為了定義 p
int *p[6];
B. 看誰跟核心最近,誰的結合優先度 (優先級) 高,就先與之結合;以下嘗試讓核心與不同的符號結合
// 以下分號 `;` 不結合
// 核心 a,跟 [] 結合,所以是數組
int a[5];
// 核心 p,跟 * 結合,所以是數組
int *p;
// 核心 function,跟 `()` 結合,所以是函數
int function();
● 以下列出幾個常見的優先度,從上到下代表優先度的高到低
運算符號 | 描述 | 結合性 |
---|---|---|
() | 函數呼叫 | |
[] | Array 引用 | 先於 * 符號結合 |
-> | 指標指向成員 | 左到右 |
. | Struct 成員引用 | |
-/+ | 負號、正號 | |
++/– | 遞增、遞減 | |
! | 邏輯否 | |
~ | 1 的補數 | 右到左 |
* | 指標引用 | |
& | 記憶體位置 | |
sizeof | 計算物件 Byte 大小 | |
(type) | 強制轉型 | |
* | 乘法 | |
/ | 除法 | 左到右 |
% | 取模 | |
+/- | 加、減 | 左到右 |
符號的建立、分析,都先從基礎去分析,沒有無緣無故的規則 !
● 以下範例是「指標函數」、「函數指標」正確的使用方式
// 指標數組
void major_array() {
int *p[5]; // 主體是 Array(5 個指標)
// int array[5] = {0};
// p = &array; // Error
*(p + 0) = 1; // 可用指標的方式訪問 Array
*(p + 1) = 2;
*(p + 2) = 3;
*(p + 3) = 4;
*(p + 4) = 5;
printf("p: %p, p+1: %p\n", p, (p+1));
}
// 數組指標 (主要用在二維數組)
void major_ptr() {
int (*p)[5]; // 主體是指標(1 個指標)
int array[5] = {0};
p = &array;
(*p)[0] = 1; // 可用 Array 的方式訪問訪問指標指向的元素
(*p)[1] = 2;
(*p)[2] = 3;
(*p)[3] = 4;
(*p)[4] = 5;
printf("p: %p, p+1: %p\n", p, (p+1));
}
函數指標 - function pointer
● 首先要知道,函數指標也是一個 指標,與其它指標並無不同;
● 函數(
Function
)到底是什麼?函數本值一段程式,在經過編譯器編譯成匯編碼後,載入到記憶體 (RAM) 中,是一段連續記憶體,而 函數指標就是該記憶體的第一個地址!
// 偽程式,以下記憶體地址都是假的 // 下面這段程式載入 RAM 中佔用了 0x11221100 ~ 0x11221140 的連續空間 // 函數指標則是 0x11221100 int function_hello() { // 0x11221100 char str[] = "Hello"; // 0x11221110 char* p = str; // 0x11221120 printf("sizeof(*p): %d, sizeof(p): %d, strlen(p): %d\n", // 0x11221130 sizeof(*p), sizeof(p), strlen(p)); } // 0x11221140
● 我們可以看到 數組指標
的類型是 <數組類型> (*)[]
;而 函數指標
也是,主要是指標所以它的類型是 <回傳類>(*)(接收參數)
#include <stdio.h>
void test_hello(void) {
printf("Hello Function\n");
}
int main(void) {
// pFunc 是一個指標
void (*pFunc)(void);
// pFunc = &test_hello; // 同上
pFunc = test_hello;
pFunc(); // 加上 `()` 代表調用函數
return 0;
}
typedef & 函數指標
● typedef 這個關鍵字是用來定義新類型,其實我們上面看到的都是自定義類型
● 數組指標
:像是 int (*p)[5]
#include <stdio.h>
typedef int (*IntArrayPointer)[5];
int main() {
int arr[5] = {1, 2, 3, 4, 5};
IntArrayPointer p = &arr;
for (int i = 0; i < 5; ++i) {
printf("%d ", (*p)[i]);
}
return 0;
}
● 指標數組
:像是 int *p[5]
#include <stdio.h>
typedef int* IntPointerArray[5];
int main() {
int arr[5] = {1, 2, 3, 4, 5};
IntPointerArray p;
for (int i = 0; i < 5; ++i) {
p[i] = &arr[i];
printf("%d ", *p[i]);
}
return 0;
}
● 函數指標
:像是 void (*p)(int)
#include <stdio.h>
typedef void (*FunctionPointer)(int);
void printNumber(int num) {
printf("Number: %d\n", num);
}
int main() {
FunctionPointer p = printNumber;
p(42); // 調用函數指標
return 0;
}
● typedef 定義出的新類型並不占用 RAM
二重指標
● 二重指標
可以存放 一重指標
的地址
#include <stdio.h>
int main() {
int num = 42;
int *ptr = # // 一重指標指向變數 num
int **doublePtr = &ptr; // 二重指標指向一重指標 ptr
printf("Value of num: %d\n", num);
printf("Value through ptr: %d\n", *ptr);
printf("Value through doublePtr: %d\n", **doublePtr);
return 0;
}
● 二重指標
可以指向 指標數組 (*p[]
):二重指標也就是 用來儲存 指標數組
的第一個元素的指標變量
指標 | 儲存 |
---|---|
一重 int* | int 的 addr |
二重 int** | int* 的 addr |
#include <stdio.h>
int main() {
int num1 = 1, num2 = 2, num3 = 3;
int *arr[] = {&num1, &num2, &num3}; // 指標數組
int **doublePtr = arr; // 二重指標指向指標數組的第一個元素
for (int i = 0; i < 3; ++i) {
printf("Value through doublePtr[%d]: %d\n", i, *doublePtr[i]);
}
return 0;
}
typedef
A. typedef 用來定義新類型,形式越複雜 typedef 的優勢則越明顯
B. typedef 的另一個優點是 方便移植
● typedef 是一個儲存類的關鍵字,而 變量只能被一種儲存類的關鍵字修飾
typedef static int ClzNum[10]; // 編譯錯誤
其他儲存類的關鍵字:
auto
、extern
、static
、register
錯誤如下
typedef 看法解析
A. typedef 是給類型取別名,所以 typedef 定義的東西都是類型;所以 typedef 定義中一定會有一個 類型
B. typedef 定義出的類型:移除定義中 typedef 關鍵字,再將 類型看作變量,就能知道它的類型
● 從這裡可以發現將 typedef 關鍵字 移除後,它就只是一個普通的變量語句
● 數組類型
// MyClass 就是類型
typedef int MyClass[5];
// 去除 typedef 關鍵字
int MyClass[5];
// 再將 MyClass 看成變量
int <變量>[]; // 數組類型
● 函數指標類型
// MyFunc 就是類型
typedef int* (MyFunc*)(int);
// 去除 typedef 關鍵字
int* (MyFunc*)(int);
// 再將 MyFunc 看成變量
// MyFunc 變量是一個指標
// MyFunc 是函數指標(因為後面是 `()`)
// MyFunc 該函數指標回傳一個 int*
int* (<變量>*)(int);
define & typedef
使用 | 功能 | 編譯時機 |
---|---|---|
define | 簡單 宏替換 | 預處理 |
typedef | 重新定義類型 | 編譯期 |
● define
& typedef
的區別:
A. typedef 不是簡單替換,而是 typedef 對類型重新定義
#define dpInt int* // 不可加 `;` 號
typedef int* tpInt;
void typedef_define_diff_1() {
// int* dp1, dp2; // 同下
dpInt dp1, dp2;
// int* tp1, *tp2; // 同下
tpInt tp1, tp2;
int a = 20;
tp1 = tp2 = dp1 = &a;
dp2 = a;
printf("tp1: %d\n", *tp1);
printf("tp2: %d\n", *tp2);
printf("dp1: %d\n", *dp1);
printf("dp2: %d\n", dp2); // 這是一個 int 類型
}
B. #define 可以實現類型組合,而 typedef 不行:define 過的仍可再使用其他關鍵字修飾,而 typedef 不行
#define dInt int
typedef int tInt;
void typedef_define_diff_2() {
unsigned dInt c1;
// unsigned tInt c2; // 不可再組合
}
C. define 無法創建新類型
// 定義新類型 (新類型為 Class)
typedef char Class[10];
void typdef_create_new_definition() {
Class clz;
for(int i = 0; i < sizeof(clz)/sizeof(clz[0]); i++) {
*(clz + i) = i * 3;
printf("index: %d, value: %d\n", i, *(clz + i));
}
}
typedef & struct
● struct 結構最簡單的定義如下
struct Node {};
● struct
配合使用上 typedef
有以下幾種情況
A. 串上 typedef
可省去 struct
關鍵字
typedef struct MyNode {} Node_T;
void my_node() {
Node_T node;
}
B. 定義兩個類型:一個結構類型,另一個結構指標類型
// 定義等同於
// typedef <類型> Node_T2;
// typedef <類型> *pNode_T2;
typedef struct MyNode_2 {} Node_T2, *pNode_T2;
void my_node_2() {
Node_T2 node;
pNode_T2 pNode;
}
typedef & const
● 我們知道 const
是如何修飾指標的,有分為 3 個種類
A. const int* p
、const int* p
:修飾指標指向內容不可改
B. int* const p
:修飾指標指向不可改
C. const int* const p
:內容、指向都不可改
● 如果以上功能要配合 typedef
使用
A. const
修飾新類型變量:指向不可修改
typedef int* pInt;
void const_typedef() {
int apply = 10;
int bannana = 5;
const pInt p = &apply; // 等同於 `int* const p`
// pInt const p = &apply; // 同上
// p = &bannana; // read-only, Error 編譯錯誤
printf("p value: %d\n", *p);
}
B. const
修飾新類型宣告:修飾內容不可修改
void const_typedef_2() {
short apply = 10;
pShort p = &apply;
printf("initialize p value: %d\n", *p);
short bannana = 5;
p = &bannana;
// *p = bannana; // read-only, Error 編譯錯誤
printf("After change p value: %d\n", *p);
}
C. const
修飾內容、指向皆不可改
typedef const long* pLong;
void const_typedef_3() {
long apply = 200;
const pLong p = &apply;
printf("initialize p value: %d\n", *p);
short bannana = 103;
p = &bannana;
p = bannana;
printf("After change p value: %d\n", *p);
}
typedef & 函數指標
● 我們就分析一個比較複雜的函數指標,以下兩個是相等意思
A. 函數指標原型
void printTest(int count) {
for(int i = 0; i < count; i++) {
printf("Hello: %d\n", i);
}
}
// ---------------------------------------------------------
// 1. 首先知道 a[10] 是一個指標數組 (主體是數組
// 2. 之後接上 `()` 代表是一個函數,得知外層是一個函數指標
// 3. 該函數指標返回 void、接收 `void(*)(int)` 函數指標
void (*a[10]) (void(*)(int));
void func_ptr_1() {
a[0] = printTest; // 指定函數指標
a[0](5); // 呼叫函數
}
B. typedef
改寫上面的範例
// 功能完全同上
void printTest(int count) {
for(int i = 0; i < count; i++) {
printf("Hello: %d\n", i);
}
}
// 宣告一個新類型 pFunc (函數指標)
typedef void (*pFunc)(void(*)(int));
// 定義一個 Array 的 pFunc
pFunc pFuncArray[10];
void func_ptr_2() {
pFuncArray[0] = printTest;
pFuncArray[0](10);
}
typedef & sizeof
● typedef
在使用 sizeof
要注意一定要括號,否則會報錯誤
A. 正常可以測量出 size 是 8 byte
B. 兩者配合使用沒有括號,會拋出錯誤 error: expected expression
typedef struct {
char a;
short b;
int c;
} Test_T;
int main()
{
//"1. "
printf("%d\n", sizeof (Test_T) );
//"2. "
printf("%d\n", sizeof Test_T);
return 0;
}
二維 Array
// 二維 Array
int a[2][5]; // [2] 代表一維,[5] 代表二維,可解釋成 2 個 [5] 的空間
二維 Array 的首地址
● 在一維 Array 中我們可以知道,一維 Array 的符號,等價於 &a[0]
的地址
void One_dimen_array_head() {
int a[6];
if(a == &a[0]) {
printf("Same");
} else {
printf("Different");
}
}
● 推斷可得知二維的符號,等價於 &(&a[0])[0]
的地址
void Two_dimen_array_head() {
int a[6][6];
if(a == &(&a[0])[0]) {
printf("Same on a == &&a[0][0]");
} else {
printf("Different");
}
}
訪問 二維 Array
● 使用普通 Pointer 訪問
void visit_by_ptr() {
int array[6][6] = {0};
array[0][0] = -1;
array[0][1] = 10;
array[0][2] = 7;
array[1][0] = -3;
array[1][1] = 100;
array[1][2] = 97;
int *p1 = array[0]; // 指向第一行的第一個元素
int *p2 = array[1]; // 指向第二行的第一個元素
printf("array[0][0]: %d\n", *p1);
printf("array[0][1]: %d\n", *(p1 + 1));
printf("array[0][2]: %d\n", *(p1 + 2));
// 證明 Array 是連續空間,其實可以用一個 ptr 訪問全部二維 Array
printf("array[1][0]: %d\n", *(p1 + 6));
printf("array[1][1]: %d\n", *(p2 + 1));
printf("array[1][2]: %d\n", *(p2 + 2));
}
● 使用 陣列指標 訪問
void visit_by_ptr_array() {
int array[6][6] = {0};
array[0][0] = -1;
array[0][1] = 10;
array[0][2] = 7;
array[1][1] = 100;
array[1][2] = 97;
int (*p)[6] = array; // 指向第一個元素
printf("array[0][0]: %d\n", *(*p));
printf("array[0][1]: %d\n", *(*p + 1)); // *p 是第一個地址
printf("array[0][2]: %d\n", *(*p + 2));
printf("array[1][1]: %d\n", *(*(p + 1) + 1));
printf("array[1][2]: %d\n", *(*(p + 1) + 2));
}
A.
a[i][j]
對於陣列指標來說,等同於*( *(p + i) + j)
B. 上面宣告的陣列指標
int (*p)[6]
,其中的[6]
並不能亂定義,必須要與二維數組的數量相同才可以 !
更多的 C 語言相關文章
關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言
C 語言基礎
● C 語言基礎:有關於到 C 語言的「語言基礎、細節」
編譯器、系統開念
● 編譯器、系統開念:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節
C 語言與系統開發
● C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用