前端学习C语言 - 函数和关键字

函数和关键字

本篇主要介绍:自定义函数宏函数字符串处理函数关键字

自定义函数

基本用法

实现一个 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;
}

热门相关:峡谷正能量   妈妈的女儿   美景之屋3   私家侦探:爱情与战争   花月颂