函数和关键字
本篇主要介绍:自定义函数
、宏函数
、字符串处理函数
和关键字
。
自定义函数
基本用法
实现一个 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("两个整数的和为:%dn", 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() 的地址:%pn", add);
printf("%dn", result);
return 0;
}
输出:
add() 的地址:0x401130
8
练习-sizeof
题目
:以下两次 sizeof 输出的值相同吗?
#include <stdio.h>
void printSize(int arr[]) {
printf("Size of arr: %zun", sizeof(arr));
}
int main() {
int nums[] = {1, 2, 3, 4, 5};
printf("Size of nums: %zun", 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: %zun", 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("hellon")
// 有参
#define PRINT2(n) printf("%dn", 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("%sn", 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("%sn", 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("%sn", 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("%dn", 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() - 用于获取字符串的长度,即字符串中字符的个数(不包括结尾的空字符' ')
语法:
#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: %zun", 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)中,包括字符串的结束符 。语法:
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: %sn", destination);
return 0;
}
比如destination之前有字符串,而且比source要长,你说最后输出什么?
char source[] = "Hello, world!";
char destination[20] = "world, Hello!XXXXXXX";
strcpy(destination, source);
输出不变
。source 拷贝的时候会将结束符