C語言結構體與指標完整教學|從基礎到進階的程式設計實例

目次

1. 前言

C語言是一種廣泛應用於系統開發與嵌入式程式的程式語言。其中「結構體」與「指標」是實現高效資料管理與記憶體操作不可或缺的要素。本文將從基礎到進階,詳細說明這些概念。

透過閱讀本文,您將能理解C語言中結構體與指標的角色,並透過實際程式碼範例掌握其用法。即使是初學者,也能藉由具體的範例輕鬆理解。

2. 結構體與指標的基礎知識

什麼是結構體?

結構體是一種能將多種不同型別的資料組合在一起的資料結構。例如,在管理一個人的資訊(姓名、年齡、身高等)時,結構體就非常方便。

以下程式碼示範了結構體的基本定義與使用方式。

#include <stdio.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person person1;  // 宣告結構體變數

    // 資料賦值
    strcpy(person1.name, "Taro");
    person1.age = 20;
    person1.height = 170.5;

    // 輸出資料
    printf("姓名: %sn", person1.name);
    printf("年齡: %dn", person1.age);
    printf("身高: %.1fcmn", person1.height);

    return 0;
}

在這個例子中,我們定義了一個名為 Person 的結構體,將三種不同的資料型別整合成一個資料結構。這樣可以更方便地集中管理相關資料。

什麼是指標?

指標是一種儲存變數記憶體位址的變數,用於在程式中動態操作記憶體。以下是指標的基本範例。

#include <stdio.h>

int main() {
    int a = 10;
    int *p;  // 宣告指標變數

    p = &a;  // 將變數a的位址指定給指標p

    printf("變數a的值: %dn", a);
    printf("指標p所指向的值: %dn", *p);

    return 0;
}

在這個例子中,指標變數 p 用來存取變數 a 的值。雖然指標在記憶體操作上非常強大,但若使用不當,可能會造成程式錯誤或記憶體洩漏,因此必須小心使用。

結構體與指標的關係

將結構體與指標結合後,可以實現更靈活的資料操作。詳細的應用方式會在後續章節解說,但只要先掌握基本概念,就能更順利地進入進階應用。

3. 什麼是結構體?

結構體的基本定義

結構體是一種能將多種不同型別的資料組合在一起的資料結構。在C語言中,結構體常用於將相關的資訊分組,使資料管理更加簡潔。

以下程式碼示範了結構體的定義範例:

struct Person {
    char name[50];
    int age;
    float height;
};

這個例子中,Person 結構體包含以下三個成員:

  • name:以字串(陣列)形式儲存姓名
  • age:以整數形式儲存年齡
  • height:以浮點數形式儲存身高

結構體的定義是一種「型別宣告」,之後可以用它來建立具體的變數。

結構體變數的宣告與使用

要使用結構體,首先需要宣告變數。以下為範例:

#include <stdio.h>
#include <string.h>

struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person person1;  // 宣告結構體變數

    // 資料賦值
    strcpy(person1.name, "Taro");
    person1.age = 20;
    person1.height = 170.5;

    // 輸出資料
    printf("姓名: %sn", person1.name);
    printf("年齡: %dn", person1.age);
    printf("身高: %.1fcmn", person1.height);

    return 0;
}

這段程式碼宣告了一個名為 person1 的結構體變數,並為各個成員設定值。

結構體的初始化

結構體變數在宣告時也可以直接初始化:

struct Person person2 = {"Hanako", 25, 160.0};

這樣可以更簡潔地一次完成初始設定。

結構體陣列

若需要管理多筆資料,可以使用結構體陣列。

struct Person people[2] = {
    {"Taro", 20, 170.5},
    {"Hanako", 25, 160.0}
};

for (int i = 0; i < 2; i++) {
    printf("姓名: %s, 年齡: %d, 身高: %.1fcmn", people[i].name, people[i].age, people[i].height);
}

在這個例子中,使用陣列管理兩筆資料,並透過迴圈一次處理。

將結構體傳遞給函式

結構體也可以作為參數傳入函式。以下為範例:

void printPerson(struct Person p) {
    printf("姓名: %s, 年齡: %d, 身高: %.1fcmn", p.name, p.age, p.height);
}

這個函式接收結構體作為參數,並輸出其內容。

總結

結構體是一種能將相關資料集中管理的便利資料型別。只要掌握基本用法,就能有效率地整理與存取資料。

4. 指標的基礎

什麼是指標?

指標是C語言中一個強大的功能,可以直接操作變數的記憶體位址。本節將從指標的基本概念、宣告方法、使用方式到實際範例進行詳細解說。

指標的宣告與初始化

指標的宣告方式是在型別前加上*符號:

int a = 10;     // 一般變數
int *p;         // 宣告指標變數
p = &a;         // 將變數a的位址指定給p
  • *p 表示指標所指向位址中的「值」(間接存取)。
  • &a 表示取得變數a的位址(位址運算子)。

利用指標操作數值

以下範例展示如何使用指標操作變數的值:

#include <stdio.h>

int main() {
    int a = 10;      // 一般變數
    int *p = &a;     // 宣告指標並指向a的位址

    printf("a的值: %dn", a);           // 10
    printf("a的位址: %pn", &a);       // a的記憶體位址
    printf("p的值(位址): %pn", p);    // p存放的位址
    printf("p所指向的值: %dn", *p);   // 10

    *p = 20;  // 透過指標修改數值
    printf("a的新值: %dn", a);        // 20

    return 0;
}

在這段程式碼中,透過指標p,可以間接修改變數a的值。

陣列與指標

存取陣列元素時,也可以利用指標來完成:

#include <stdio.h>

int main() {
    int arr[3] = {10, 20, 30};
    int *p = arr; // 指向陣列第一個元素的位址

    printf("第一個元素: %dn", *p);      // 10
    printf("第二個元素: %dn", *(p+1));  // 20
    printf("第三個元素: %dn", *(p+2));  // 30

    return 0;
}

在這個例子中,利用指標p存取陣列的各個元素。

總結

指標是C語言中非常重要的功能,能實現高效的記憶體管理與靈活的程式設計。本節介紹了指標的基本概念與使用方式。接下來將在「5. 結構體與指標的結合」中進一步深入探討。

5. 結構體與指標的結合

結構體指標的基礎

當結構體與指標結合時,可以更靈活且高效地進行資料管理。本節將從基本用法到應用範例逐步介紹。

以下是基本範例:

#include <stdio.h>
#include <string.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person person1 = {"Taro", 20, 170.5}; // 初始化結構體
    struct Person *p = &person1;                // 宣告並初始化結構體指標

    // 透過指標存取資料
    printf("姓名: %sn", p->name);
    printf("年齡: %dn", p->age);
    printf("身高: %.1fcmn", p->height);

    // 透過指標修改數值
    p->age = 25;
    printf("修改後的年齡: %dn", p->age);

    return 0;
}

與動態記憶體配置的搭配

結構體指標與動態記憶體配置搭配時,特別適合管理大量資料。以下範例示範了這種用法:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    // 透過動態記憶體配置建立結構體
    struct Person *p = (struct Person *)malloc(sizeof(struct Person));

    // 資料賦值
    strcpy(p->name, "Hanako");
    p->age = 22;
    p->height = 160.0;

    // 輸出資料
    printf("姓名: %sn", p->name);
    printf("年齡: %dn", p->age);
    printf("身高: %.1fcmn", p->height);

    // 釋放記憶體
    free(p);

    return 0;
}

利用這種方式可以靈活建立與管理結構體資料。

陣列與結構體指標

結構體陣列搭配指標,可以有效率地管理多筆資料:

#include <stdio.h>
#include <string.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person people[2] = {{"Taro", 20, 170.5}, {"Hanako", 25, 160.0}};
    struct Person *p = people; // 指向陣列起始位址的指標

    for (int i = 0; i < 2; i++) {
        printf("姓名: %sn", (p + i)->name);
        printf("年齡: %dn", (p + i)->age);
        printf("身高: %.1fcmn", (p + i)->height);
    }

    return 0;
}

總結

結構體與指標的結合能大幅提升資料管理效率與記憶體操作的靈活性。本節涵蓋了從基礎到動態記憶體配置的應用。

6. 函式與結構體指標的搭配

將結構體傳遞給函式的方法

在C語言中,將結構體傳遞給函式時有兩種方式:

  1. 值傳遞
    將結構體的完整複本傳入函式。但若資料量龐大,會消耗較多記憶體。
  2. 參考傳遞(指標傳遞)
    傳遞結構體的位址,能提升記憶體效率,且可以在函式內直接修改原始資料。

值傳遞範例

#include <stdio.h>
#include <string.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
};

// 函式:值傳遞
void printPerson(struct Person p) {
    printf("姓名: %sn", p.name);
    printf("年齡: %dn", p.age);
}

int main() {
    struct Person person1 = {"Taro", 20};
    printPerson(person1);  // 值傳遞

    return 0;
}

在這個範例中,函式printPerson接收結構體的複本。但若結構體資料龐大,效率會較低。

參考傳遞(指標傳遞)範例

#include <stdio.h>
#include <string.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
};

// 函式:指標傳遞
void updateAge(struct Person *p) {
    p->age += 1;  // 年齡加1
}

void printPerson(const struct Person *p) {
    printf("姓名: %sn", p->name);
    printf("年齡: %dn", p->age);
}

int main() {
    struct Person person1 = {"Hanako", 25};

    printf("修改前:n");
    printPerson(&person1);

    updateAge(&person1);  // 指標傳遞,直接修改原始資料

    printf("修改後:n");
    printPerson(&person1);

    return 0;
}

在這個例子中,函式updateAge透過指標直接修改了原始結構體的內容。

動態記憶體與函式的搭配

利用動態記憶體建立的資料,也可以透過函式進行操作:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 結構體定義
struct Person {
    char name[50];
    int age;
};

// 函式:初始化記憶體
struct Person *createPerson(const char *name, int age) {
    struct Person *p = (struct Person *)malloc(sizeof(struct Person));
    strcpy(p->name, name);
    p->age = age;
    return p;
}

// 函式:輸出資訊
void printPerson(const struct Person *p) {
    printf("姓名: %sn", p->name);
    printf("年齡: %dn", p->age);
}

// 函式:釋放記憶體
void deletePerson(struct Person *p) {
    free(p);
}

int main() {
    struct Person *person1 = createPerson("Taro", 30);  // 動態建立結構體
    printPerson(person1);

    deletePerson(person1);  // 釋放記憶體

    return 0;
}

在這個範例中,透過函式管理動態配置的記憶體,可以更安全高效地處理資料。

總結

本節介紹了函式與結構體指標的搭配。利用指標傳遞,可以在函式間共享資料,並提高程式的記憶體效率與靈活性。

7. 在結構體中使用指標

在結構體中使用指標的優點

在結構體內包含指標,可以更靈活有效地進行資料管理與記憶體操作。本節將解說基本用法與應用範例。

基本範例:動態管理字串資料

以下範例示範如何在結構體內利用指標動態管理字串:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 結構體定義
struct Person {
    char *name;  // 姓名字串指標
    int age;
};

// 初始化資料
void setPerson(struct Person *p, const char *name, int age) {
    p->name = (char *)malloc(strlen(name) + 1);
    strcpy(p->name, name);
    p->age = age;
}

// 輸出資訊
void printPerson(const struct Person *p) {
    printf("姓名: %sn", p->name);
    printf("年齡: %dn", p->age);
}

// 釋放記憶體
void freePerson(struct Person *p) {
    free(p->name);
}

int main() {
    struct Person person;

    setPerson(&person, "Taro", 30);
    printPerson(&person);

    freePerson(&person);

    return 0;
}

此範例透過動態配置記憶體管理字串資料,不受字串長度限制。同時需注意,使用完畢後必須釋放記憶體。

陣列與指標的組合

在需要管理多筆資料時,利用指標可以更靈活地操作結構體陣列。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 結構體定義
struct Student {
    char *name;
    int score;
};

// 建立學生資料
struct Student *createStudent(const char *name, int score) {
    struct Student *s = (struct Student *)malloc(sizeof(struct Student));
    s->name = (char *)malloc(strlen(name) + 1);
    strcpy(s->name, name);
    s->score = score;
    return s;
}

// 釋放記憶體
void freeStudent(struct Student *s) {
    free(s->name);
    free(s);
}

int main() {
    // 建立學生陣列
    struct Student *students[2];
    students[0] = createStudent("Taro", 85);
    students[1] = createStudent("Hanako", 90);

    // 輸出資料
    for (int i = 0; i < 2; i++) {
        printf("姓名: %s, 分數: %dn", students[i]->name, students[i]->score);
    }

    // 釋放記憶體
    for (int i = 0; i < 2; i++) {
        freeStudent(students[i]);
    }

    return 0;
}

透過這種設計,可以靈活管理多筆動態資料,特別適合需要頻繁新增或刪除的情境。

總結

在結構體中使用指標,可以更容易處理動態記憶體管理與複雜的資料結構。本節介紹了從基本範例到應用設計的方式。

8. 實作範例:鏈結串列的建立

鏈結串列的基本結構

鏈結串列是一種以「節點」為單位管理資料的動態資料結構,能夠方便地新增與刪除元素。在C語言中,可以透過結構體與指標來實作。

鏈結串列的結構如下:

[資料 | 指向下一個節點的指標] → [資料 | 指向下一個節點的指標] → NULL

每個節點包含資料與指向下一個節點的指標,最後一個節點的指標為NULL,代表串列的終點。

節點定義

#include <stdio.h>
#include <stdlib.h>

// 節點定義
struct Node {
    int data;            // 資料
    struct Node *next;   // 指向下一個節點的指標
};

新增節點

以下範例示範如何在鏈結串列的尾端新增節點:

void append(struct Node **head, int newData) {
    // 建立新節點
    struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
    struct Node *last = *head;

    newNode->data = newData;
    newNode->next = NULL;

    // 如果串列為空
    if (*head == NULL) {
        *head = newNode;
        return;
    }

    // 找到最後一個節點
    while (last->next != NULL) {
        last = last->next;
    }

    // 新節點接到串列尾端
    last->next = newNode;
}

輸出節點資料

void printList(struct Node *node) {
    while (node != NULL) {
        printf("%d -> ", node->data);
        node = node->next;
    }
    printf("NULLn");
}

刪除節點

以下程式碼示範如何刪除指定值的節點:

void deleteNode(struct Node **head, int key) {
    struct Node *temp = *head, *prev;

    // 如果刪除的是頭節點
    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }

    // 尋找目標節點
    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }

    // 若未找到
    if (temp == NULL) return;

    // 從串列移除
    prev->next = temp->next;
    free(temp);
}

完整範例:鏈結串列操作

#include <stdio.h>
#include <stdlib.h>

// 節點定義
struct Node {
    int data;
    struct Node *next;
};

// 新增節點到尾端
void append(struct Node **head, int newData) {
    struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
    struct Node *last = *head;

    newNode->data = newData;
    newNode->next = NULL;

    if (*head == NULL) {
        *head = newNode;
        return;
    }

    while (last->next != NULL) {
        last = last->next;
    }

    last->next = newNode;
}

// 輸出串列
void printList(struct Node *node) {
    while (node != NULL) {
        printf("%d -> ", node->data);
        node = node->next;
    }
    printf("NULLn");
}

// 刪除節點
void deleteNode(struct Node **head, int key) {
    struct Node *temp = *head, *prev;

    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }

    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }

    if (temp == NULL) return;

    prev->next = temp->next;
    free(temp);
}

int main() {
    struct Node *head = NULL;

    append(&head, 10);
    append(&head, 20);
    append(&head, 30);

    printf("鏈結串列內容:n");
    printList(head);

    deleteNode(&head, 20);
    printf("刪除20後:n");
    printList(head);

    return 0;
}

總結

本節詳細解說了如何利用結構體與指標來實作鏈結串列。鏈結串列能動態增減元素,因此廣泛應用於演算法與資料管理系統。

9. 常見錯誤與除錯方法

C語言中的結構體與指標功能非常強大,但若使用不當,可能導致程式崩潰或產生非預期行為。本節將解說常見錯誤與對應的解決方式。

1. 指標未初始化

錯誤範例:

struct Node *p;  // 未初始化的指標
p->data = 10;    // 錯誤,會造成程式崩潰

問題原因:

指標p尚未指向有效記憶體,因此存取時會觸發錯誤。

解決方法:

指標必須指向有效的記憶體空間,例如:

struct Node *p = (struct Node *)malloc(sizeof(struct Node));
p->data = 10;  // 正常運作

2. 記憶體洩漏

錯誤範例:

struct Node *p = (struct Node *)malloc(sizeof(struct Node));
// 使用後未釋放記憶體

問題原因:

使用malloc配置的記憶體若未釋放,會一直被佔用直到程式結束,造成記憶體浪費。

解決方法:

使用完畢後,必須搭配free釋放:

free(p);

若是鏈結串列,需逐一釋放所有節點:

struct Node *current = head;
struct Node *next;

while (current != NULL) {
    next = current->next;
    free(current);
    current = next;
}

3. 懸掛指標(Dangling Pointer)

錯誤範例:

struct Node *p = (struct Node *)malloc(sizeof(struct Node));
free(p);  
p->data = 10;  // 錯誤,已釋放的記憶體仍被存取

問題原因:

記憶體釋放後,指標仍指向該位置,成為懸掛指標。

解決方法:

釋放後應將指標設為NULL

free(p);
p = NULL;

4. NULL 指標存取

錯誤範例:

struct Node *p = NULL;
p->data = 10;  // 錯誤,對NULL指標進行存取

問題原因:

存取NULL指標會導致程式崩潰。

解決方法:

使用前先進行檢查:

if (p != NULL) {
    p->data = 10;
} else {
    printf("指標為NULLn");
}

除錯方法

1. 使用除錯工具

透過GDB等工具可檢查執行過程中的變數與流程:

gcc -g program.c -o program
gdb ./program

2. 使用 printf 輸出

輸出變數數值與位址,確認程式行為:

printf("位址: %p, 值: %dn", (void *)p, *p);

3. 使用 Valgrind 偵測記憶體問題

Valgrind 能檢測記憶體洩漏與未初始化存取:

valgrind --leak-check=full ./program

總結

本節介紹了C語言中結構體與指標常見的錯誤與解決方式:

  • 未初始化的指標
  • 記憶體洩漏
  • 懸掛指標
  • 對NULL指標的存取

這些問題若未妥善處理,將對程式造成嚴重影響,因此必須謹慎實作與驗證。

10. 總結

學習重點回顧

本文從基礎到進階,完整介紹了C語言中的結構體與指標,以下是重點回顧:

  1. 結構體的基礎:將多種類型資料組合管理。
  2. 指標的基礎:直接操作記憶體位址,支援動態記憶體與資料存取。
  3. 結構體與指標的結合:提升資料管理效率,支援動態配置。
  4. 函式與結構體指標:透過指標實現跨函式資料共享與高效操作。
  5. 在結構體中使用指標:支援動態字串與複雜資料結構。
  6. 鏈結串列實作:建立可動態新增刪除元素的資料結構。
  7. 常見錯誤與除錯:避免未初始化、記憶體洩漏、懸掛指標等問題。

實務應用方向

  • 檔案管理系統:使用結構體與指標管理檔案資訊。
  • 動態資料結構:進一步實作堆疊、佇列等。
  • 遊戲與模擬:利用結構體管理角色與狀態。
  • 資料庫系統:以結構體與指標管理記錄,實現增刪查改。

下一步

  • 客製化程式碼並應用於專案。
  • 挑戰更高階的資料結構:雙向鏈結串列、樹、圖。
  • 結合演算法:以結構體與指標實作排序與搜尋。
  • 提升除錯與最佳化技巧,打造更安全高效的程式。

最後

C語言中的結構體與指標是設計高效靈活程式的核心概念。透過本篇文章的學習,您已具備基礎與進階應用能力,能夠進一步挑戰更複雜的系統開發與演算法設計。

建議您在實際專案中多加練習,以不斷提升程式設計能力!

侍エンジニア塾