## 函數和關鍵字 本篇主要介紹:`自定義函數`、`巨集函數`、`字元串處理函數`和`關鍵字`。 ### 自定義函數 #### 基本用法 實現一個 add() 函數。請看示例: ```c #include // 自定義函數,用於計算兩個整數的和 int add(int a, int b) { // a ...
函數和關鍵字
本篇主要介紹:自定義函數
、巨集函數
、字元串處理函數
和關鍵字
。
自定義函數
基本用法
實現一個 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);
函數調用過程
函數調用過程:
- 通過函數名找到函數的入口地址
- 給形參分配記憶體空間
- 傳參。包含
值傳遞
和地址傳遞
(比如js中的對象) - 執行函數體
- 返回數據
- 釋放空間。例如棧空間
請看示例:
#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;
}
出處:https://www.cnblogs.com/pengjiali/p/17492014.html
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。