函式和關鍵字
本篇主要介紹:自定義函式
、宏函式
、字串處理函式
和關鍵字
,
自定義函式
基本用法
實作一個 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
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555553.html
標籤:其他
上一篇:Rust語言 - 介面設計的建議之顯而易見(Obvious)
下一篇:返回列表