接上篇开发实战进阶之函数【c++】二
函数传参原理
在C/C++中,函数传参的内存原理主要涉及到如何在函数调用时处理参数的传递。不同的传参方式(按值传递、按引用传递、按指针传递)会影响内存的分配和管理方式。以下是各个传参方式的内存原理。
1. 按值传递(Pass by Value)
内存原理:
- 值复制:当函数通过按值传递接收参数时,函数调用时会在栈中分配内存空间来存储形参的值。实参的值会被复制到这个新的内存空间中。因此,函数内部对形参的任何修改都不会影响到实参。
- 栈帧:函数调用时,系统会为该函数在栈上分配一个栈帧(stack frame),其中包括形参的存储空间。当函数返回时,这个栈帧会被销毁。
内存布局:
- 实参值 -> 被复制到 -> 函数栈帧中的形参
- 形参的内存地址与实参的内存地址不同,形参在函数内存生命周期结束时被销毁。
示例:
void func(int a) {
a = 10; // 只修改了形参的副本,不影响实参
}
int main() {
int x = 5;
func(x);
printf("%d\n", x); // 输出5,x的值没有改变
return 0;
}
2. 按引用传递(模拟通过指针传递)
C语言中没有真正的按引用传递,但可以通过传递指针来模拟按引用传递的效果。
内存原理:
- 指针传递:当通过指针传递参数时,传递的是实参的内存地址。函数内部通过指针操作实参的值。因为指针指向的是实参的内存地址,对指针的解引用操作将直接修改实参的值。
- 指针复制:指针本身被按值传递,因此在函数调用时,实参指针的副本会被复制到栈帧中。
内存布局:
- 实参的地址 -> 被复制到 -> 函数栈帧中的指针形参
- 通过指针形参修改实参,直接操作实参的内存地址。
示例:
void func(int *p) {
*p = 10; // 修改了p指向的内存地址上的值,影响了实参
}
int main() {
int x = 5;
func(&x);
printf("%d\n", x); // 输出10,x的值被修改了
return 0;
}
3. 数组传参
内存原理:
- 指针传递:在C语言中,数组名实际上是指向数组首元素的指针。当数组作为参数传递给函数时,传递的其实是数组首元素的地址。因此,数组元素在函数中是可修改的。
- 无大小信息:由于数组退化为指针,函数内无法直接获取数组的大小信息,需要通过额外的参数传递数组的长度。
内存布局:
- 数组名(首元素地址) -> 被复制到 -> 函数栈帧中的指针形参
- 函数通过指针访问和修改数组的元素,数组的实际存储不在函数栈帧内。
示例:
void func(int arr[], int size) {
arr[0] = 10; // 修改了数组的第一个元素,影响了实参
}
int main() {
int array[5] = {1, 2, 3, 4, 5};
func(array, 5);
printf("%d\n", array[0]); // 输出10,数组的值被修改了
return 0;
}
4. 结构体传参
内存原理:
- 按值传递:当结构体按值传递时,整个结构体的数据会被复制到函数的栈帧中。这可能会引入较大的内存和性能开销,特别是当结构体较大时。
- 按指针传递:为了减少开销,通常会将结构体的指针传递给函数,这样函数操作的是结构体的原始内存,而不需要复制数据。
内存布局:
- 结构体按值传递 -> 复制整个结构体到函数栈帧
- 结构体按指针传递 -> 复制结构体指针到函数栈帧,通过指针操作结构体数据。
示例(按值传递):
typedef struct {
int x, y;
} Point;
void func(Point p) {
p.x = 10; // 只修改了形参的副本,不影响实参
}
int main() {
Point pt = {5, 5};
func(pt);
printf("%d\n", pt.x); // 输出5,pt的值没有改变
return 0;
}
内存布局:
pt
在main
函数中被存储在栈中。func
函数被调用时,pt
的副本被复制到func
的栈帧中。对p
的修改不会影响pt
,因为它们占用不同的内存空间。
函数传参原理进阶之不定参
在C和C++中,不定参函数(variadic functions)是指可以接收可变数量参数的函数。经典的例子包括printf
和scanf
等标准库函数。理解不定参函数的传参原理涉及到堆栈内存的处理,以及如何在函数内部正确解析这些参数。
1. C语言中的不定参函数
在C语言中,处理不定参函数需要使用stdarg.h
头文件中的宏来操作可变参数列表。主要的宏包括:
va_list
:用于声明一个变量,该变量将用来存储传递给函数的额外参数信息。va_start
:初始化va_list
变量,使其指向可变参数列表的起始位置。va_arg
:用于获取可变参数列表中的下一个参数。va_end
:清理va_list
变量。
示例代码
#include <stdio.h>
#include <stdarg.h>
void printNumbers(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
int num = va_arg(args, int); // 取出下一个参数
printf("%d ", num);
}
va_end(args); // 结束可变参数的处理
printf("\n");
}
int main() {
printNumbers(3, 10, 20, 30); // 输出: 10 20 30
printNumbers(5, 1, 2, 3, 4, 5); // 输出: 1 2 3 4 5
return 0;
}
内存原理
-
堆栈布局:在不定参函数的调用中,除了固定参数外,可变参数也会依次压入堆栈。函数通过
va_list
变量的初始化,指向可变参数的起始位置,然后使用va_arg
宏依次获取这些参数。这些参数是按顺序从栈中弹出的,因此函数必须知道每个参数的类型和数量。 -
类型安全:C语言的可变参数函数存在一个问题,即无法自动检查参数类型和数量。这可能导致运行时错误,比如从栈中提取错误的参数类型。因此,函数的设计者必须确保所有参数的类型和顺序与
va_arg
的使用相匹配。
2. C++中的不定参函数
C++继承了C中的不定参机制,并且提供了两种实现方式:
- 使用C语言风格的
<cstdarg>
库(即stdarg.h
)。 - 使用C++11引入的模板和
std::initializer_list
实现更类型安全的变参函数。
C风格的可变参数
C++仍然支持C风格的可变参数,方法和C类似,使用va_list
等宏。
示例代码:
#include <iostream>
#include <cstdarg>
void printNumbers(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
int num = va_arg(args, int);
std::cout << num << " ";
}
va_end(args);
std::cout << std::endl;
}
int main() {
printNumbers(3, 10, 20, 30);
printNumbers(5, 1, 2, 3, 4, 5);
return 0;
}
C++11引入的变参模板
C++11引入了更类型安全和灵活的变参模板(variadic templates),避免了C风格变参函数的类型安全问题。
示例代码:
#include <iostream>
template<typename... Args>
void printNumbers(Args... args) {
(std::cout << ... << args) << std::endl; // C++17的折叠表达式
}
int main() {
printNumbers(10, 20, 30);
printNumbers(1, 2, 3, 4, 5);
return 0;
}
内存原理:
- 模板展开:在编译期,通过模板展开(template expansion)将可变参数处理展开为具体的函数调用。每个参数的类型在编译时已经确定,因此避免了C语言变参函数的类型不安全问题。
- 堆栈使用:模板展开后,每个参数的传递和处理与普通函数的参数传递方式相同,不需要在运行时解析参数类型。
实战一:
你可以使用stdarg.h
库提供的宏来实现解析不定参数的功能。下面是一个示例,展示了如何编写一个可以解析和处理不定参数的函数:
示例:实现一个简单的加法函数,接收不定数量的整数参数
#include <stdio.h>
#include <stdarg.h>
// 定义一个可以接受不定参数的函数,参数的第一个参数是参数的个数
int sum(int count, ...) {
va_list args; // 定义一个 va_list 类型的变量,用来存储可变参数列表
va_start(args, count); // 初始化 args,使其指向第一个可变参数
int total = 0; // 用来保存计算的总和
for (int i = 0; i < count; i++) {
int num = va_arg(args, int); // 逐个获取参数,va_arg 获取当前参数并使 args 指向下一个参数
total += num; // 将当前参数加到 total 中
}
va_end(args); // 清理可变参数列表
return total; // 返回计算结果
}
int main() {
// 调用不定参函数,计算多个整数的和
int result = sum(4, 10, 20, 30, 40);
printf("Sum: %d\n", result); // 输出结果 Sum: 100
return 0;
}
解析:
-
va_list: 定义一个
va_list
类型的变量,用于存储不定参数的列表。 -
va_start: 使用
va_start
宏初始化va_list
变量。这个宏需要两个参数:第一个是va_list
变量,第二个是最后一个确定的参数(在这个例子中是count
)。 -
va_arg:
va_arg
宏用于获取不定参数列表中的下一个参数,并将其类型转换为指定类型。在上面的示例中,我们期望所有的参数都是int
类型。 -
va_end: 使用
va_end
宏清理va_list
变量。当你完成了对可变参数的处理后,应该调用va_end
。
使用:
在主函数中,我们调用sum
函数并传递了4个整数作为参数,函数将这些参数累加并返回总和。因为函数的第一个参数指定了参数的个数,sum
函数就能够正确地解析剩余的参数。
这个示例展示了如何使用C语言处理不定参数的基本方法。可以根据需要扩展这个例子,比如处理不同类型的参数或实现更复杂的逻辑。
实战进阶之不实用标准库提供的宏
实现一个类似于printf
的解析不定参数的函数,而不使用标准库提供的宏(如va_list
、va_start
、va_arg
等),在C语言中是非常复杂的。下面是一个简化的实现,这个例子主要用于教育目的,并且假设我们只处理整数(%d
)和字符串(%s
)的情况。
实现思路
- 理解参数传递:C语言的函数参数通常通过栈传递。第一个固定参数的地址可以通过函数中的第一个局部变量或参数获取。
- 手动解析参数:由于我们不使用标准宏,解析参数需要通过指针直接访问内存。我们将依次解析格式化字符串中的占位符,并从内存中提取相应的参数值。
示例代码:实现一个简单的my_printf
函数
#include <stdio.h>
void my_printf(const char *format, ...) {
char *p = (char *)&format; // 获取第一个参数的地址
p += sizeof(char*); // 跳过format参数的地址,指向第一个不定参数
while (*format) {
if (*format == '%' && (*(format + 1) == 'd' || *(format + 1) == 's')) {
format++; // 跳过 '%'
if (*format == 'd') {
int value = *(int *)p; // 从指针中获取整数值
printf("%d", value);
p += sizeof(int); // 移动指针到下一个参数
}
else if (*format == 's') {
char *str = *(char **)p; // 从指针中获取字符串指针
printf("%s", str);
p += sizeof(char*); // 移动指针到下一个参数
}
} else {
putchar(*format); // 直接输出非格式化字符
}
format++;
}
}
int main() {
int num = 42;
char *text = "Hello, world!";
my_printf("Number: %d, String: %s\n", num, text); // 输出: Number: 42, String: Hello, world!
return 0;
}
解析
-
char *p = (char *)&format;
:p
被初始化为指向format
参数的地址。因为C语言参数在栈上是按顺序存储的,我们可以通过跳过第一个参数来访问后续的可变参数。
-
p += sizeof(char*);
:- 跳过
format
参数的地址,p
现在指向第一个不定参数的地址。
- 跳过
-
参数解析:
- 解析
format
字符串中的占位符。如果遇到%d
,从内存中提取一个int
类型的值并输出;如果遇到%s
,则提取一个指向字符串的指针并输出。
- 解析
-
指针移动:
- 每解析一个参数,指针
p
就会移动,以指向下一个参数的地址。
- 每解析一个参数,指针
限制
- 类型安全:这个实现假设你传递的参数类型和顺序与格式化字符串完全匹配,否则会导致未定义行为。
- 可移植性:这种方法依赖于编译器和平台的实现细节(比如参数传递顺序和栈布局),因此在不同的系统上可能表现不同。
- 功能简陋:这个简单的
my_printf
实现只支持%d
和%s
,而真正的printf
支持更多格式和更复杂的选项。
这种方法只适用于理解C语言函数参数传递的底层机制。在实际开发中,应该使用标准库提供的宏来处理不定参数,以确保代码的安全性和可移植性。