前端學習C語言

2023-06-19 21:00:49

函數和關鍵字

本篇主要介紹:自定義函數宏函數字串處理常式關鍵字

自定義函數

基本用法

實現一個 add() 函數。請看範例:

#include <stdio.h>

// 自定義函數,用於計算兩個整數的和
int add(int a, int b) { // a, b 叫形參
    int sum = a + b;
    return sum;
}

int main() {
    int num1 = 3;
    int num2 = 5;
    
    // 呼叫自定義函數計算兩個整數的和
    int result = add(num1, num2); // num1, num2 叫實參
    
    printf("兩個整數的和為:%d\n", result);
    
    return 0;
}

其中a, b 叫形參,num1, num2 叫實參

Tip:形參和實參的個數不同,筆者編譯器報錯如下(一個說給函數的引數少,一個說給函數的引數多了):

// 3個形參,2個實參
int add(int a, int b, int c) {}

//  error: too few arguments to function call, expected 3, have 2
int result = add(num1, num2);
// 2個形參,3個實參
int add(int a, int b) {}

// error: too many arguments to function call, expected 2, have 3
int result = add(num1, num2, num1);

函數呼叫過程

函數呼叫過程:

  1. 通過函數名找到函數的入口地址
  2. 給形參分配記憶體空間
  3. 傳參。包含值傳遞地址傳遞(比如js中的物件)
  4. 執行函數體
  5. 返回資料
  6. 釋放空間。例如棧空間

請看範例:

#include <stdio.h>

// 2. 給形參分配記憶體空間
// 3. 傳參:值傳遞和地址傳遞(比如js中的物件)
// 4. 執行函數體
// 5. 返回資料
// 6. 釋放空間。例如棧空間:區域性變數 a,b,sum
int add(int a, int b) {
    int sum = a + b;
    return sum;
}

int main() {
    int num1 = 3;
    int num2 = 5;
    
    // 1. 通過函數名找到函數的入口地址
    int result = add(num1, num2); 
    
    printf("add() 的地址:%p\n", add);
    printf("%d\n", result);
    
    return 0;
}

輸出:

add() 的地址:0x401130
8

練習-sizeof

題目:以下兩次 sizeof 輸出的值相同嗎?

#include <stdio.h>

void printSize(int arr[]) {
    printf("Size of arr: %zu\n", sizeof(arr));
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    
    printf("Size of nums: %zu\n", sizeof(nums));
    printSize(nums);
    
    return 0;
}

執行:

開始執行...
// sizeof(arr) 獲取的是指標型別 int * 的大小(在此例中是8位元組)
/workspace/CProject-test/main.c:4:40: warning: sizeof on array function parameter will return size of 'int *' instead of 'int[]' [-Wsizeof-array-argument]
    printf("Size of arr: %zu\n", sizeof(arr));
                                       ^
/workspace/CProject-test/main.c:3:20: note: declared here
void printSize(int arr[]) {
                   ^
1 warning generated.
Size of nums: 20
Size of arr: 8

執行結束。

結果:輸出不相同,一個是陣列的大小,一個卻是指標型別的大小。

結果分析:將一個陣列作為函數的引數傳遞時,它會被隱式地轉換為指向陣列首元素的指標,然後在函數中使用 sizeof 運運算元獲取陣列大小時,實際上返回的是指標型別的大小((通常為4或8位元組,取決於系統架構)),而不是整個陣列的大小。

宏函數

宏函數是C語言中的一種預處理指令,用於在編譯之前將程式碼片段進行替換

之前我們用 #define 定義了常數:#define MAX_NUM 100。定義宏函數就是將常數改為函數。就像這樣

#include <stdio.h>
// 無參
#define PRINT printf("hello\n")

// 有參
#define PRINT2(n) printf("%d\n", n)

int main() {

    // 無參呼叫
    PRINT;
    // 有參呼叫
    PRINT2(10);    
    return 0;
}

輸出:hello 10

編譯流程

宏函數發生在編譯的第一步。

編譯可以分為以下幾個步驟:

  • 預處理(Preprocessing):在這一步中,前處理器將對原始碼進行處理。它會展開宏定義、處理條件編譯指令(如 #if、#ifdef 等)、包含標頭檔案等操作。處理後的程式碼會生成一個被稱為預處理檔案(通常以 .i 或 .ii 為擴充套件名)。
  • 編譯(Compilation):在這一步中,編譯器將預處理後的程式碼翻譯成組合語言。它會進行詞法分析、語法分析、語意分析和優化等操作,將高階語言的程式碼轉化為低階機器可以理解的形式。輸出的檔案通常以 .s 為擴充套件名,是一個組合語言檔案。
  • 組合(Assembly):組合器將組合語言程式碼轉換為機器語言指令。它將每條組合語句對映到對應的機器語言指令,並生成一個目標檔案(通常以 .o 或 .obj 為擴充套件名),其中包含已組合的機器指令和符號表資訊。
  • 連結(Linking):如果程式涉及多個原始檔,以及使用了外部庫函數或共用的程式碼模組,連結器將合併和解析這些檔案和模組。它會將目標檔案與庫檔案進行連結,解析符號參照、處理重定位等。最終生成可執行檔案(或共用庫),其中包含了完整的機器指令。

這些步驟並非一成不變,具體的編譯過程可能因為編譯器工具鏈和目標平臺的不同而有所差異。但是大致上,這是一個常見的編譯流程。

宏函數 vs 普通函數

用普通函數和宏函數實現平方的功能,程式碼分別如下:

int square(int x) {
    return x * x;
}
#define SQUARE(x) ((x)*(x))

宏函數在編譯過程中被簡單地替換為相應的程式碼片段。它沒有函數呼叫的開銷,可以直接插入到呼叫的位置,這樣可以提高程式碼執行效率

這發生在預處理階段,不會進行型別檢查錯誤檢查,可能導致意外的行為或結果。例如:宏函數中需列印字串,而引數傳遞數位1:

#include <stdio.h>

#define PRINT2(n) printf("%s\n", n)

int main() {

    PRINT2(1);    
    return 0;
}

編譯有告警,執行檔案還是生成了:

pjl@pjl-pc:~/ph$ gcc demo-3.c -o demo-3
demo-3.c: In function ‘main’:
demo-3.c:3:26: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
    3 | #define PRINT2(n) printf("%s\n", n)
      |                          ^~~~~~
......
    7 |     PRINT2(1);
      |            ~
      |            |
      |            int
demo-3.c:7:5: note: in expansion of macro ‘PRINT2’
    7 |     PRINT2(1);
      |     ^~~~~~
demo-3.c:3:28: note: format string is defined here
    3 | #define PRINT2(n) printf("%s\n", n)
      |                           ~^
      |                            |
      |                            char *
      |                           %d

但執行還是報錯:

pjl@pjl-pc:~/ph$ ./demo-3
段錯誤 (核心已轉儲)

普通函數具備了型別檢查作用域錯誤檢查等功能,可以更加安全可靠地使用。但是函數呼叫需要一定的開銷,涉及儲存現場、跳轉等操作。例如:

#define ADD(a, b) (a + b)

int result = ADD(3, 5);

編譯器會將宏函數展開為 (3 + 5),並直接插入到 ADD(3, 5) 的位置,避免了函數呼叫的開銷。

練習

題目:請問以下輸出什麼?

#include <stdio.h>

#define SQUARE(x) x * x

int main() {
    int result = SQUARE(1 + 2); 
    printf("%d\n", result);

    return 0;
}

輸出:5。

分析:

// 1 + 2 * 1 + 2
#define SQUARE(x) x * x

如果希望輸出 9 可以用括號,就像這樣:

//(1 + 2) * (1 + 2)
#define SQUARE(x) (x) * (x)

字串處理常式

以下幾個字串處理常式都來自 <string.h> 庫函數。

strlen()

strlen() - 用於獲取字串的長度,即字串中字元的個數(不包括結尾的空字元'\0')

語法:

#include <string.h>

size_t strlen(const char *str);

範例:

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

int main() {
    char str[] = "Hello, world!";
    size_t length = strlen(str);
    // Length of the string: 13
    printf("Length of the string: %zu\n", length);

    return 0;
}

Tip: %zu只用于格式化輸出 size_t 型別的格式控制符

size_t

size_t是無符號整數型別。unsigned int 也是無符號整數,兩者還是有區別的。

size_t 被定義為足夠大以容納系統中最大可能的物件大小的無符號整數型別,可以處理比 unsigned int更大的值。

在涉及到記憶體分配、陣列索引、迴圈迭代等需要表示大小的場景中,建議使用size_t型別,以保證程式碼的可移植性和相容性。儘管許多編譯器將size_t 定義為 unsigned int,但不依賴於它與unsigned int之間的精確關係是一個好的程式設計實踐。

strcpy()

strcpy - 將源字串(src)複製到目標字串(dest)中,包括字串的結束符\0。語法:

char *strcpy(char *dest, const char *src);

範例:

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

int main() {
    char source[] = "Hello, world!";
    char destination[20];
    strcpy(destination, source);
    // Destination: Hello, world!
    printf("Destination: %s\n", destination);

    return 0;
}

比如destination之前有字串,而且比source要長,你說最後輸出什麼?

char source[] = "Hello, world!";
char destination[20] = "world, Hello!XXXXXXX";
strcpy(destination, source);

輸出不變。source 拷貝的時候會將結束符\0也複製到目標,destination 最後是Hello, world!\0XXXXXX。如果不需要拷貝結束符,可以使用 strncpy()

strncpy()

strncpy - 將源字串的指定長度複製到目標字串中。比如不拷貝源字元的結束符:

範例:

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

int main() {
    char source[] = "Hello, world!";
    char destination[20] = "world, Hello!XXXXXXX";
    
    // 將源字串的指定長度複製到目標字串中,不要source的結束符
    strncpy(destination, source, sizeof(source)-1);
    // Destination: Hello, world!XXXXXXX
    printf("Destination: %s\n", destination);

    return 0;
}

最後輸出:Destination: Hello, world!XXXXXXX

strcat()

strcat - 將源字串(src)連線到目標字串(dest)的末尾,形成一個新的字串。語法:

char *strcat(char *dest, const char *src);

範例:

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

int main() {
    char destination[20] = "Hello";
    char source[] = ", world!";
    
    strcat(destination, source);
    // Destination: Hello, world!
    printf("Destination: %s\n", destination);

    return 0;
}
strncat()

strncat - 將源字串連線到目標字串的末尾,並限制連線的字元數量。

範例:

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

int main() {
    char destination[20] = "Hello";
    char source[] = ", world!";
    
    strncat(destination, source, 3);
    // Destination: Hello, w
    printf("Destination: %s\n", destination);

    return 0;
}

strcmp()

strcmp - 用於比較兩個字串的大小。

  • 字串的比較是按照字典順序進行的,根據每個字元的 ASCII 值進行比較。
  • 比如 apple 和 applea比較,第一次是 a 等於 a,繼續比較,直到第六次 \0 和 a 比較,\0 的 ASCII 是0,而a 是97(可列印字元的ASCII值通常位於32-126之間),所以 applea 更大
  • 大小寫敏感。例如 A 的 ASCII 是 65。

例如:比較字串 str1 和字串 str2 的大小

  • 如果 str1 小於 str2,則返回一個負整數(通常是 -1)。
  • 如果 str1 大於 str2,則返回一個正整數(通常是 1)。
  • 如果 str1 等於 str2,則返回 0。

範例:

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

int main() {
    char str1[] = "apple";
    char str2[] = "applea";

    int result = strcmp(str1, str2);

    if (result < 0) {
        printf("str1 is less than str2\n");
    } else if (result > 0) {
        printf("str1 is greater than str2\n");
    } else {
        printf("str1 is equal to str2\n");
    }

    return 0;
}

輸出:str1 is less than str2

strncmp()

strncmp - 比較兩個字串的前n個字元是否相等

範例:

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

int main() {
    char str1[] = "apple";
    char str2[] = "applea";

    // int result = strcmp(str1, str2);
    int result = strncmp(str1, str2, strlen(str1));

    if (result < 0) {
        printf("str1 is less than str2\n");
    } else if (result > 0) {
        printf("str1 is greater than str2\n");
    } else {
        printf("str1 is equal to str2\n");
    }

    
    return 0;
}

輸出:str1 is equal to str2

strchr()

strchr - 在一個字串中查詢指定字元第一次出現的位置。語法:

// str是要搜尋的字串;
// c是要查詢的字元。
char* strchr(const char* str, int c);

函數返回值:

  • 如果找到指定字元,則返回該字元在字串中的地址(指標)。
  • 如果未找到指定字元,則返回NULL。

Tip:可以通過地址相減返回字元在字串中出現的索引值。請看範例:

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

int main() {
    char str[] = "Hello, world!";
    char* result = strchr(str, 'o');
    
    if (result != NULL) {
        // 返回字元'o'的位置,並輸出字元 'o' 到結尾的字串
        printf("找到了字元'o',位置為:%s\n", result);
        // 地址相減,返回字元第一次出現的索引
        printf("找到了字元'o',位置為:%ld\n", result - str);
    }
    else {
        printf("未找到字元'o'\n");
    }
    
    return 0;
}

輸出:

開始執行...

找到了字元'o',位置為:o, world!
找到了字元'o',位置為:4

執行結束。
strrchr

strrchr - 相對strchr()逆序查詢。

修改上述範例(strchr)一行程式碼:

- char* result = strchr(str, 'o');
+ char* result = strrchr(str, 'o');

執行:

開始執行...

找到了字元'o',位置為:orld!
找到了字元'o',位置為:8

執行結束。

strstr

strstr - 用於在一個字串中查詢指定子字串的第一次出現位置。語法:

char* strstr(const char* str1, const char* str2);

範例:

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

int main() {
    char str[] = "Hello, World!";
    char *subStr = "World";
    char *result = strstr(str, subStr);

    if (result != NULL) {
        printf("找到子字串:%s\n", result);
    } else {
        printf("未找到子字串。\n");
    }

    return 0;
}

關鍵字

C 語言有如下關鍵字:

關鍵字 描述 關鍵字 描述
auto 宣告自動變數 enum 宣告列舉型別
break 跳出當前迴圈或開關語句 extern 宣告外部變數或函數
case 開關語句分支標籤 float 宣告單精度浮點型變數或函數返回值型別
char 宣告字元型變數或函數返回值型別 for 迴圈語句
const 宣告唯讀變數 goto 無條件跳轉語句
continue 結束當前迴圈的迭代,並開始下一次迭代 if 條件語句
default 開關語句的預設分支 int 宣告整型變數或函數返回值型別
do 迴圈語句的迴圈體 long 宣告長整型變數或函數返回值型別
double 宣告雙精度浮點型變數或函數返回值型別 register 宣告暫存器變數
else 條件語句中否定條件的執行體 return 從函數返回值
short 宣告短整型變數或函數返回值型別 signed 宣告有符號數型別
sizeof 獲取某個資料型別或變數的大小 static 宣告靜態變數
struct 宣告結構體型別 switch 開關語句
typedef 宣告型別別名 union 宣告聯合體型別
unsigned 宣告無符號數型別 void 宣告無型別
volatile 說明變數可以被意外修改,應立即從記憶體中讀取或寫入而不進行優化 while 迴圈語句

重點說一下:static、const(已講過)、extern。

auto

在C語言中,auto 關鍵字並不是必需的,因為所有在函數內部宣告的變數預設都是自動變數。所以在實際編碼中,很少使用 auto 關鍵字進行變數宣告

#include <stdio.h>

int main() {
    auto int x = 10; // 等於 int x = 10;
    
    printf("The value of x is: %d\n", x); // 輸出:The value of x is: 10
    
    return 0;
}

在現代的C語言標準中(C99及以上),auto 關鍵字的使用已經不常見,並且很少被推薦使用,因為它已經成為了預設行為

register

比如 for(int i = 0; i< 1000; i++),每次i都會自增1,如果編譯器沒有任何優化,有可能會導致暫存器記憶體之間的資料互動發生一千次。如果將 i 宣告成暫存器變數(register int count;),可能就無需互動這麼多次。但這也只是給編譯器提供一個建議,指示它將變數儲存在暫存器中。實際上,編譯器可以選擇忽略這個建議,根據自身的優化策略和限制來決定是否將變數儲存在暫存器中。

Tip: 暫存器是不能被直接取地址。C 語言標準已經從 C99 開始將 register 關鍵字標記為過時(deprecated)

extern

extern - 用於宣告變數或函數的外部連結性。

通俗點說,比如我在b.c中定義了一個方法,在另一個檔案中想使用,無需重複編寫,通過 extern 宣告後可直接使用。編譯時需要將多個檔案一起編譯成可執行檔案。

定義b.c和main.c兩個檔案:

  • b.c 中通過 extern int number 宣告 number 變數在外部已定義,不會分配記憶體空間
  • main.c 先宣告 show() 函數已在外部定義,然後使用
  • 最後通過 gcc 將這兩個檔案編譯成 main 可執行函數

完整程式碼如下:

b.c:

#include <stdio.h>
extern int number;  // 宣告外部變數 number 已存在。不會分配記憶體空間

void show() {
    printf("x = %d\n", number);  // 使用外部變數 number
}

main.c:

#include <stdio.h>

extern void show();  // 宣告函數 show 的存在

// 全域性變數
int number = 101;
int main() {
    show();  // 呼叫 b.c 中的函數,列印外部變數 number
    return 0;
}

兩兩個檔案一起編譯,執行輸出 x = 101

pjl@pjl-pc:~/$ gcc main.c b.c -o main && ./main
x = 101

static

static 有3個作用:

  • static int number = 101;, 指明 number 只能在本檔案中使用,其他檔案即使使用了 extern 也不能存取
  • static void show(), 指明 show 只能在本檔案中使用,其他檔案即使使用了 extern 也不能存取
  • static 修飾區域性變數,會改變變數的宣告週期,直到程式結束才釋放

通過三個範例一一說明。

:在 extern 範例基礎上進行。

範例1:修改 main.c:

- int number = 101;
+ static int number = 101;

編譯報錯如下:

pjl@pjl-pc:~/$ gcc main.c b.c -o main
/usr/bin/ld: /tmp/ccEOKXoI.o: in function `show':
b.c:(.text+0xa): undefined reference to `number'
collect2: error: ld returned 1 exit status

範例2:修改 b.c:

- void show() {
+ static void show() {

編譯報錯:

pjl@pjl-pc:~/$ gcc main.c b.c -o main
/usr/bin/ld: /tmp/cc8XfhVS.o: in function `main':
main.c:(.text+0xe): undefined reference to `show'
collect2: error: ld returned 1 exit status

範例3:請問下面這段程式碼輸出什麼?

#include <stdio.h>
void show(); 

void fn1(){
    int i = 1;
    printf("%d\n", ++i);
}

int main() {
    for(int i = 0; i < 3; i++){
        fn1();
    }
    return 0;
}

輸出三個2。因為 fn1() 每次執行完,存放在棧中的變數 i 就被釋放。

如果給 i 增加 static 會輸出什麼:

- int i = 1;
+ static int i = 1;

輸出2 3 4

Tip:在C語言中,全域性變數靜態變數都屬於靜態儲存類別,預設情況下會被分配到靜態資料區。靜態資料區在程式啟動時被分配,在程式結束時釋放。

練習

Tip:以下4個都是字串相關的程式設計練習

練習1

題目:查詢字元陣列中字元的位置,例如 hello e,輸出1。

實現:

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

int findIndex(char array[], char target) {
    int length = strlen(array);
    for (int i = 0; i < length; i++) {
        if (array[i] == target) {
            return i;
        }
    }
    return -1; // 字元不在陣列中
}

int main() {
    char array[] = "hello";
    char target = 'e';
    int index = findIndex(array, target);

    if (index != -1) {
        printf("字元 %c 的位置是:%d\n", target, index);
    } else {
        printf("字元 %c 不在陣列中\n", target);
    }

    return 0;
}

輸出:字元 e 的位置是:1

練習2

題目:查詢字元陣列中字元的位置,例如 hello ll,輸出2。

實現:

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

int findIndex(char array[], char substring[]) {
    char *result = strstr(array, substring);
    if (result != NULL) {
        return result - array;
    }
    return -1; // 字串不在陣列中
}

int main() {
    char array[] = "hello";
    char substring[] = "ll";
    int index = findIndex(array, substring);

    if (index != -1) {
        printf("字串 \"%s\" 的位置是:%d\n", substring, index);
    } else {
        printf("字串 \"%s\" 不在陣列中\n", substring);
    }

    return 0;
}

輸出:字串 "ll" 的位置是:2

練習3

題目:在字串指定位置插入字串

實現

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

void insertString(char str[], int pos, const char insert_str[]) {
    int len1 = strlen(str);
    int len2 = strlen(insert_str);

    if (pos < 0 || pos > len1)
        return;  // 無效的插入位置
    
    // 建立臨時陣列,用於儲存插入後的新字串
    char temp[len1 + len2 + 1];
  
    // 將原字串中插入位置之前的部分複製到臨時陣列
    strncpy(temp, str, pos);
    
    // 將要插入的字串複製到臨時陣列的合適位置
    strcpy(&temp[pos], insert_str);

    // 追加原字串中插入位置之後的部分
    strcat(temp, &str[pos]);

    // 將新字串複製回原字串
    strcpy(str, temp);
}

int main() {
    char original_str[100] = "Hello, world!";
    int pos = 7;
    char insert_str[100] = "beautiful ";

    insertString(original_str, pos, insert_str);
    printf("%s\n", original_str);

    return 0;
}

輸出:Hello, beautiful world!

練習4

題目:計算字串中子串的次數,例如 "helloworldhelloworldhelloworld hello" 中 hello 出現4次

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

int countSubstringOccurrences(const char* str, const char* substring) {
    int count = 0;
    int substring_length = strlen(substring);
    const char* ptr = strstr(str, substring);

    while (ptr != NULL) {
        count++;
        ptr += substring_length;
        ptr = strstr(ptr, substring);
    }

    return count;
}

int main() {
    const char* str = "helloworldhelloworldhelloworld hello";
    const char* substring = "hello";

    int count = countSubstringOccurrences(str, substring);
    // 4
    printf("%d\n", count);

    return 0;
}