C语言:指针传递VS值传递

指针传递和值传递乍一看似乎很相似,实际上它们有着截然不同的传参行为。值传递在函数之间传递数据的副本,指针传递则是在函数之间传递数据的内存地址,能够直接对原始数据进行操作。

值传递

值传递就是在函数调用时,将实际参数的值复制一份,传递给函数的形式参数。在 C 语言中,系统会为函数的形式参数分配一块新的内存空间,然后把实际参数的值复制到这块新的空间中。这就意味着,函数内部对形式参数的任何操作,都只是在这块新的内存空间中进行,不会影响到实际参数所在的内存空间。例如,定义一个简单的函数add,用于将传入的参数加 1:​

​
#include <stdio.h>​
void add(int num) {​
num = num + 1;​
printf("Inside function: %d\n", num);​
}​
int main() {​
int a = 5;​
printf("Before function call: %d\n", a);​
add(a);​
printf("After function call: %d\n", a);​
return 0;​
}​

在这个例子中,main函数中的变量a是实际参数,当调用add函数时,a的值 5 被复制到add函数的形式参数num中。add函数对num进行加 1 操作,此时num的值变为 6,但这并不会改变main函数中a的值,a仍然是 5。这就是值传递的基本原理,通过复制数据,保证了函数内部的操作不会对外部数据造成意外影响。​

下面通过一个更复杂的代码示例,深入了解值传递在程序执行过程中的具体行为。假设有一个函数swap,试图通过值传递来交换两个整数的值:​

​
#include <stdio.h>​
void swap(int a, int b) {​
int temp;​
temp = a;​
a = b;​
b = temp;​
printf("Inside swap function: a = %d, b = %d\n", a, b);​
}​
int main() {​
int x = 3;​
int y = 4;​
printf("Before swap: x = %d, y = %d\n", x, y);​
swap(x, y);​
printf("After swap: x = %d, y = %d\n", x, y);​
return 0;​
}​


当main函数执行到swap(x, y)时,程序会为swap函数的形参a和b分配新的内存空间,并将x的值 3 和y的值 4 分别复制到a和b中。在swap函数内部,通过临时变量temp,成功地交换了a和b的值,此时a变为 4,b变为 3。然而,当swap函数执行结束,其内部的形参a和b所占用的内存空间被释放。回到main函数,x和y的值并没有发生改变,因为swap函数操作的只是x和y的副本a和b,而不是x和y本身。所以,最终输出的结果是 “Before swap: x = 3, y = 4” 和 “After swap: x = 3, y = 4”。

值传递适用于只需要在函数内部使用数据,而不需要修改原始数据的应用场景。例如,在数学计算函数中,如计算一个数的平方、立方等,使用值传递可以保证原始数据的完整性。对于小型数据类型,如int、char、float等,值传递的效率较高,因为复制这些数据的开销相对较小。但是,值传递也存在一定的局限性。当传递大型对象,如结构体、数组时,值传递需要复制整个对象,这会带来较大的内存开销和时间开销。例如有一个包含大量成员的结构体:​​

struct BigStruct {​
int data[1000];​
// 其他成员​
};​

如果使用值传递将这样的结构体传递给函数,每次调用函数都需要复制 1000 个整数以及其他成员,这会消耗大量的内存和时间,降低程序的性能。此外,由于值传递无法修改原始数据,如果在某些情况下需要在函数内部修改传入的数据,并让这些修改影响到函数外部,值传递就无法满足需求了。

指针传递

指针传递与值传递不同,指针传递时,传递的不是数据本身的值,而是数据在内存中的地址。当将一个指针作为参数传递给函数时,函数内部的指针变量指向的是实参所指向的内存地址,通过这个指针,函数可以直接访问和修改实参所指向的内存中的数据。例如,定义一个简单的函数modify,用于修改传入的整数的值:​​

#include <stdio.h>​
void modify(int *num) {​
*num = *num * 2;​
printf("Inside function: %d\n", *num);​
}​
int main() {​
int a = 3;​
printf("Before function call: %d\n", a);​
modify(&a);​
printf("After function call: %d\n", a);​
return 0;​
}​

在这个例子中,main函数中的变量a的地址&a被传递给modify函数的指针参数num。此时,num指向a在内存中的地址,通过*num可以访问并修改a的值。modify函数将a的值乘以 2,a的值在函数内部和外部都发生了改变。这就是指针传递的核心原理,通过传递地址,实现对原始数据的直接操作。​
下面来看一个稍微复杂一些的代码示例,实现两个整数的交换:​​

#include <stdio.h>​
void swap(int *a, int *b) {​
int temp;​
temp = *a;​
*a = *b;​
*b = temp;​
printf("Inside swap function: a = %d, b = %d\n", *a, *b);​
}​
int main() {​
int x = 5;​
int y = 7;​
printf("Before swap: x = %d, y = %d\n", x, y);​
swap(&x, &y);​
printf("After swap: x = %d, y = %d\n", x, y);​
return 0;​
}​


在main函数中,定义了两个整数变量x和y,并初始化为 5 和 7。当调用swap函数时,将x和y的地址&x和&y分别传递给swap函数的指针参数a和b。此时,a指向x的内存地址,b指向y的内存地址。在swap函数内部,通过临时变量temp,借助*a和*b,成功地交换了x和y的值。当swap函数执行结束,回到main函数时,x和y的值已经发生了交换,因为swap函数操作的是它们的原始内存地址。最终输出结果为 “Before swap: x = 5, y = 7” 和 “After swap: x = 7, y = 5”。

指针传递适用于需要在函数内部修改参数的值,并让这些修改影响到函数外部时的场景。例如,在动态内存分配中,使用malloc函数分配内存后,返回的是一个指向分配内存块的指针,通过传递这个指针,可以在不同的函数中对这块内存进行操作。对于大型对象,如结构体、数组等,如果使用值传递,会导致大量的数据复制,消耗内存和时间,而指针传递只需要传递一个地址,大大提高了效率。例如有一个包含多个成员的结构体:​

struct Student {​
char name[20];​
int age;​
float score;​
};​

如果要将一个Student结构体传递给函数进行处理,使用指针传递可以避免复制整个结构体,提高程序性能。
指针传递也需要注意一些问题:在使用指针之前,一定要确保指针不是空指针(NULL),否则会导致程序崩溃,出现空指针异常。在动态内存分配中,要记得释放不再使用的内存,避免内存泄漏。例如:​

int *ptr = (int *)malloc(sizeof(int));​
if (ptr != NULL) {​
// 使用ptr​
free(ptr);​
ptr = NULL;​
}​


在这个例子中,先检查ptr是否为非空指针,然后使用free函数释放分配的内存,并将ptr赋值为NULL,防止成为野指针。

两者的差异

内存层面的差异

下图给出了值传递和指针传递在内存层面的差异:

假设我们有一个变量int num = 5;,当进行值传递时,如调用函数void func(int a) { a = a + 1; },系统会为形参a分配一块新的内存空间,然后将num的值 5 复制到a的内存空间中。此时,num和a位于不同的内存地址,对a的修改不会影响num。而在指针传递中,若调用函数void func(int *p) { *p = *p + 1; },形参p是一个指针,它指向num的内存地址。通过p,可以直接访问和修改num所在内存地址中的数据,num的值会发生改变。​

功能效果的差异


从能否修改实参值来看,值传递由于传递的是数据副本,函数内部对形参的修改不会影响实参,无法改变实参的值;而指针传递传递的是实参的地址,函数内部可以通过指针直接修改实参的值。在数据传递效率方面,对于小型数据类型,值传递和指针传递的效率差异不大,但对于大型数据结构,如结构体、数组等,值传递需要复制整个数据结构,开销较大,效率较低;而指针传递只传递地址,效率更高。例如,传递一个包含 1000 个元素的整型数组:​​

#include <stdio.h>​
// 值传递函数​
void processArrayValue(int arr[], int size) {​
for (int i = 0; i < size; i++) {​
arr[i] = arr[i] * 2;​
}​
}​
// 指针传递函数​
void processArrayPointer(int *arr, int size) {​
for (int i = 0; i < size; i++) {​
*(arr + i) = *(arr + i) * 2;​
}​
}​
int main() {​
int array[1000];​
for (int i = 0; i < 1000; i++) {​
array[i] = i;​
}​
// 值传递调用​
int tempArray[1000];​
for (int i = 0; i < 1000; i++) {​
tempArray[i] = array[i];​
}​
processArrayValue(tempArray, 1000);​
// 指针传递调用​
processArrayPointer(array, 1000);​
return 0;​
}​


在这个例子中,值传递需要先复制数组array到tempArray,然后对tempArray进行操作,而指针传递直接对array进行操作,避免了数据复制,效率更高。

选择值传递还是指针传递

在实际编程中,选择值传递还是指针传递,需要根据具体的需求来决定。当我们只需要在函数内部使用数据,而不需要修改原始数据时,或者传递的数据量较小,如基本数据类型int、char、float等,值传递是一个简单而安全的选择,它可以避免意外修改原始数据,使程序的逻辑更加清晰。例如,在一个计算圆面积的函数中:​

#include <stdio.h>​
#define PI 3.14159​
double calculateArea(double radius) {​
return PI * radius * radius;​
}​
int main() {​
double r = 5.0;​
double area = calculateArea(r);​
printf("The area of the circle is: %lf\n", area);​
return 0;​
}​


这里使用值传递传递半径radius,函数只需要读取该值进行计算,不需要修改它,值传递非常合适。当需要在函数内部修改参数的值,并让这些修改影响到函数外部,或者传递的数据量较大,如大型结构体、数组时,指针传递则是更好的选择,它可以提高程序的效率,减少内存开销。比如,在一个管理学生信息的系统中,有一个包含学生姓名、年龄、成绩等信息的结构体:​

#include <stdio.h>​
#include <string.h>​
struct Student {​
char name[50];​
int age;​
float score;​
};​
void updateStudent(struct Student *stu) {​
strcpy(stu->name, "New Name");​
stu->age = 20;​
stu->score = 90.0;​
}​
int main() {​
struct Student s = {"Old Name", 19, 85.0};​
printf("Before update: Name: %s, Age: %d, Score: %f\n", s.name, s.age, s.score);​
updateStudent(&s);​
printf("After update: Name: %s, Age: %d, Score: %f\n", s.name, s.age, s.score);​
return 0;​
}​


这里通过指针传递Student结构体,函数可以直接修改结构体中的信息,并且避免了复制整个结构体的开销。