本文转载自C语言指针变量的定义和使用(精华) (biancheng.net),部分内容根据我的想法进行了添加和修改,以便更加清晰

大家直接去看原文就行了,作者写的很棒

C语言指针

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

下面的代码演示了如何输出一个地址:

#include <stdio.h>
int main(){
    int a = 100;
    char str[20] = "c.biancheng.net";
    printf("%#X, %#X\n", &a, str);
    return 0;
}

运行结果:
0X28FF3C, 0X28FF10

关于下面这部分可以看

%#X表示以十六进制形式输出,并附带前缀0X。a 是一个变量,用来存放整数,需要在前面加&来获得它的地址;str 本身就表示字符串的首地址,不需要加&

C语言中有一个控制符%p,专门用来以十六进制形式输出地址,不过 %p 的输出格式并不统一,有的编译器带0x前缀,有的不带,所以此处我们并没有采用。

一切都是地址

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。

数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。

CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:

0X3000 = (0X1000) + (0X2000);

( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存

变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址,那场景简直让人崩溃。

需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。

指针变量的定义和使用

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。

现在假设有一个 char 类型的变量 c,它存储了字符 ‘K’(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制表示)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。

img

定义指针变量

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:

数据类型 *变量名   
比如: int *a 定义了一个名称为a的指针变量,指向int类型数据

*表示这是一个指针变量

#include <stdio.h>
int main(){
    int a = 100;
    int *p1 = &a;
}
在这串代码中,在定义指针变量p1的同时对它进行初始化,并把变量a的地址赋给它,此时p1就指向a

&表示取地址符,&a表示取a的地址

和普通变量一样,指针变量也可以被多次写入,只要你想,随时都能够改变指针变量的值,请看下面的代码:

#include <stdio.h>
int main(){
//定义普通变量
    float a = 99.5, b = 10.6;
    char c = '@', d = '#';
//定义指针变量
    float *p1 = &a;
    char *p2 = &c;
//修改指针变量的值
    p1 = &b;
    p2 = &d;
}

*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*

假设变量 a、b、c、d 的地址分别为 0X1000、0X1004、0X2000、0X2004,下面的示意图很好地反映了 p1、p2 指向的变化:

img

需要强调的是,p1、p2 的类型分别是float*char*,而不是floatchar,它们是完全不同的数据类型,读者要引起注意。

指针变量也可以连续定义,例如:

int *a, *b, *c;  //a、b、c 的类型都是 int*

注意每个变量前面都要带*。如果写成下面的形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量:

int *a, b, c;

通过指针变量取得数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*指针变量名

这里的*称为指针运算符,用来取得某个地址上的数据,请看下面的例子:

#include <stdio.h>
int main(){
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值,均为15
    return 0;
}

假设 a 的地址是 0X1000,p 指向 a 后,p 本身的值也会变为 0X1000,*p 表示获取地址 0X1000 上的数据,也即变量 a 的值。从运行结果看,*p 和 a 是等价的。

注意一点,p是个变量,它是存储了a的地址,而不是p的地址和a的地址相同

#include <stdio.h>
int main(){
 int a = 15;
 int *p = &a;
 printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值,均为15
 printf("%#X %#X",&a,&p); //0X61FE1C 0X61FE10
 return 0;
}

看上面这段代码,局可以看到a和p的地址是不一样的,我们用*p只是表示解引用,通过其存储的a的地址才能拿到a的数值

上节我们说过,CPU 读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过 *p 和 a 获取到的数据一样,但它们的运行过程稍有不同:a 只需要一次运算就能够取得数据,而 *p 要经过两次运算,多了一层“间接”。

假设变量 a、p 的地址分别为 0X1000、0XF0A0,它们的指向关系如下图所示:

img

程序被编译和链接后,a、p 被替换成相应的地址。使用 *p 的话,要先通过地址 0XF0A0 取得变量 p 本身的值,这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。

也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。

指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:

#include <stdio.h>
int main(){
    int a = 15;
    int b = 20;
    int *p = &a;
    *p = b; //通过指针修改a的值 通过指针变量修改内存上的数据
    c = *p; //解引用,相当于 c = a 通过指针变量获取内存上的数据
    printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值,均为20
    return 0;
}

*在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身。

指针变量也可以出现在普通变量能出现的任何表达式中,例如:

函数指针

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针

那么这个指针变量怎么定义呢?虽然同样是指向一个地址,但指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的。例如:

int(*p)(int, int);

这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即(p);其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int()(int,int)。

所以函数指针的定义方式为:

函数返回值类型 (* 指针变量名) (函数参数列表);

“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。

我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。

最后需要注意的是,指向函数的指针变量没有 ++ 和 – 运算。

如何用函数指针调用函数

给大家举一个例子:

#include <stdio.h>
int max(int,int);//函数声明 
int main(void)
{
	int (*p)(int,int);//定义一个函数指针
	p = max;//把函数max赋值给指针变量p,使得p指向max函数
	printf("please enter a and b");
	int a,b,c;
	scanf("%d %d",&a,&b);
	c = (*p)(a,b);//通过函数指针调用max函数
	printf("a = %d;b = %d\n max = %d",a,b,c);
	return 0;	
	}
int max(int x,int y)
{
	int z;
	if(x > y)
	{
		z = x;
	}
	else{
		z = y;
	}
	return z;
}

image-20220902212230676

野指针

野指针:访问一个已销毁或者访问受限的内存区域的指针,野指针不能判断是否为NULL来避免,指针指向了一块随机的空间,不受程序控制。

因此,为了避免产生野指针,初始化指针的时候将其置为NULL

int *d = NULL;

C语言指针变量的运算(加法、减法和比较运算)

指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等,请看下面的代码:

#include <stdio.h>

int main() {
    int a = 10,*pa = &a,*paa = &a;
    double b = 99.9,*pb = &b;
    char c = '@',*pc = &c;
    //最初的值
    printf("&a = %#X,&b = %#X,&c = %#X\n",&a,&b,&c);
    printf("pa = %#X paa = %#X,pb = %#X , pc = %#X\n",pa,paa,pb,pc);
    //加法运算
    printf("加法运算\n");
    pa++;
    pb++;
    pc++;
    printf("&a = %#X,&b = %#X,&c = %#X\n",&a,&b,&c);
    printf("pa = %#X paa = %#X,pb = %#X , pc = %#X\n",pa,paa,pb,pc);
    //减法运算
    printf("减法运算\n");
    pa -= 2;
    pb -=2;
    pc -= 2;
    printf("&a = %#X,&b = %#X,&c = %#X\n",&a,&b,&c);
    printf("pa = %#X paa = %#X,pb = %#X , pc = %#X\n",pa,paa,pb,pc);
    //比较运算
    if(pa == paa){
        printf("%d\n",*paa);
    }else {
        printf("%d\n",*pa);
    }
    return 0;
}

运行结果:

&a = 0X3EFFF71C,&b = 0X3EFFF710,&c = 0X3EFFF70F
pa = 0X3EFFF71C paa = 0X3EFFF71C,pb = 0X3EFFF710 , pc = 0X3EFFF70F
加法运算
&a = 0X3EFFF71C,&b = 0X3EFFF710,&c = 0X3EFFF70F
pa = 0X3EFFF720 paa = 0X3EFFF71C,pb = 0X3EFFF718 , pc = 0X3EFFF710
减法运算
&a = 0X3EFFF71C,&b = 0X3EFFF710,&c = 0X3EFFF70F
pa = 0X3EFFF718 paa = 0X3EFFF71C,pb = 0X3EFFF708 , pc = 0X3EFFF70E
-2027218456

从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。

这很奇怪,指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1,这是为什么呢?

以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:

img

刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。

如果pa++;使得地址加 1 的话,就会变成如下图所示的指向关系:

img

这个时候 pa 指向整数 a 的中间,*pa 使用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,后面 1 个是其它数据的,把它们“搅和”在一起显然没有实际的意义,取得的数据也会非常怪异。

如果pa++;使得地址加 4 的话,正好能够完全跳过整数 a,指向它后面的内存,如下图所示:

img

我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义

不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。

下面的例子是一个反面教材,警告读者不要尝试通过指针获取下一个变量的地址:

#include <stdio.h>

int main(){
    int a = 1, b = 2, c = 3;
    int *p = &c;
    int i;
    for(i=0; i<8; i++){
        printf("%d, ", *(p+i) );
    }
    return 0;
}

在 VS2010 Debug 模式下的运行结果为:

3, -858993460, -858993460, 2, -858993460, -858993460, 1, -858993460

可以发现,变量 a、b、c 并不挨着,它们中间还参杂了别的辅助数据。

指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。

上面的代码(第一个例子)在比较 pa 和 paa 的值时,pa 已经指向了 a 的上一份数据,所以它们不相等。而 a 的上一份数据又不知道是什么,所以会导致 printf() 输出一个没有意义的数,这正好印证了上面的观点,不要对指向普通变量的指针进行加减运算

C语言数组指针(指向数组的指针)详解

数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:

img

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向

img

数组名的本意是表示整个数组,也就是表示多份数据的集合,但在使用过程中经常会转换为指向数组第 0 个元素的指针,所以上面使用了“认为”一词,表示数组名和数组首地址并不总是等价。初学者可以暂时忽略这个细节,把数组名当做指向第 0 个元素的指针使用即可

下面的例子演示了如何以指针的方式遍历数组元素:

#include <stdio.h>

int main() {
    int arr[5] = {99,14,20,19,100};
    int len = sizeof(arr) / sizeof(int); //求数组长度
    for (int i = 0; i < len; i++) {
        printf("%d ",*(arr+i));//*(arr+i)等价于arr[i]
    }
    printf("\n");
    printf("%d ",*arr);//99 索引0
    printf("%d ",*(arr+1));//14 索引1
    printf("%d ",*arr + 1);//注意,这里是解引用后加一 100
}

第 5 行代码用来求数组的长度,sizeof(arr) 会获得整个数组所占用的字节数,sizeof(int) 会获得一个数组元素所占用的字节数,它们相除的结果就是数组包含的元素个数,也即数组长度。

上面的例子足以说清楚怎么遍历数组

以及*(arr+1)*arr + 1的区别

第 6 行代码中我们使用了*(arr+i)这个表达式,arr 是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,它等价于 arr[i]。

arr 是int*类型的指针,每次加 1 时它自身的值会增加 sizeof(int),加 i 时自身的值会增加 sizeof(int) * i

我们也可以定义一个指向数组的指针,例如:

#include <stdio.h>

int main() {
    int arr[5] = {99,14,20,19,100};
    int *p = arr;
}

arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。

如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。

数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *

更改上面的代码,使用数组指针来遍历数组元素:

#include <stdio.h>

int main() {
    int arr[5] = {99,14,20,19,100};
    int len = sizeof(arr) / sizeof(int);
    int *p = arr; //arr本身就是地址
    for (int i = 0; i < len ; i++) {
        printf("%d ",*(p+i));
    }
}

上节我们讲到,对指针变量进行加法和减法运算时,是根据数据类型的长度来计算的。如果一个指针变量 p 指向了数组的开头,那么 p+i 就指向数组的第 i 个元素;如果 p 指向了数组的第 n 个元素,那么 p+i 就是指向第 n+i 个元素;而不管 p 指向了数组的第几个元素,p+1 总是指向下一个元素,p-1 也总是指向上一个元素。

#include <stdio.h>

int main() {
    int arr[5] = {99,14,20,19,100};
    int len = sizeof(arr) / sizeof(int);
    int *p = &arr[1]; //arr本身就是地址
    for (int i = 0; i < len-1 ; i++) {
        printf("%d ",*(p+i));
    }
}

引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。

访问数组元素

1) 使用下标

也就是采用arr[i]的形式访问数组元素。如果p是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]

2) 使用指针

也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)

不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。

更改上面的代码,借助自增运算符来遍历数组元素:

#include <stdio.h>

int main() {
    int arr[5] = {99,14,20,19,100};
    int len = sizeof(arr) / sizeof(int);
    int *p = arr; //arr本身就是地址
    for (int i = 0; i < len ; i++) {
        printf("%d ",*(p++));
    }
    printf("\n");
    for (int i = 0; i < len ; i++) {
        printf("%d ",*(arr+i));
    }

}

*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素

*++p 等价于 *(++p) 会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。

C语言字符串指针(指向字符串的指针)详解

C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中,这在《C语言字符数组和字符串》中已经进行了详细讲解,这里不妨再来演示一下:

#include <stdio.h>
#include <string.h>
int main() {
    char str[] ="https://meowrain.cn";
    int len;
    len = strlen(str);
    //直接输出字符串
    printf("%s\n",str);

    //每次输出一个字符
    for (int j = 0; j < len; ++j) {
        printf("%c",*(str+j));
    }
}

运行结果:

https://meowrain.cn
https://meowrain.cn

除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

char *str = "http://c.biancheng.net";

字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *

#include <stdio.h>
#include <string.h>

int main() {
    char *str = "https://meowrain.cn";
    //直接输出字符串
    printf("%s\n", str);
    //使用*(str+i)
    int len = strlen(str);
    for (int i = 0; i < len; i++) {
        printf("%c",*(str+i));
    }
    //使用str[i]
    printf("\n");
    for (int i = 0; i < len; ++i) {
        printf("%c",str[i]);
    }
}

这一切看起来和字符数组是多么地相似,它们都可以使用%s输出整个字符串,都可以使用*[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?

有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。

我们将第二种形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。请看下面的演示:

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "I love C!";  //正确
    str[3] = 'P';  //错误
    return 0;
}

这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。

第4行代码是正确的,可以更改指针变量本身的指向;第5行代码是错误的,不能修改字符串中的字符。

看到这里的时候,我想写额指针型字符串,然后通过scanf的方式修改它的内容,再输出,结果是不可行的,可以看下面这个。

(82条消息) 【C语言】为什么指针型字符串不能作为scanf的参数_许多仙的博客-CSDN博客_指针可以用scanf吗

还学到一点

gets函数,C语言gets函数详解 (biancheng.net)

puts函数,C语言puts函数用法详解 (biancheng.net)

到底使用字符数组还是字符串常量

在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。

C语言指针变量作为函数参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。

有些初学者可能会使用下面的方法来交换两个变量的值:

#include <stdio.h>
void swap(int a, int b){
    int temp;  //临时变量
    temp = a;
    a = b;
    b = temp;
}
int main(){
    int a = 66, b = 99;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

运行结果:
a = 66, b = 99

从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为 swap() 函数内部的 a、b 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,它们除了名字一样,没有其他任何关系,swap() 交换的是它内部 a、b 的值,不会影响它外部(main() 内部) a、b 的值。

想实现交换效果,就应该把printf的内容写到swap函数里面

#include <stdio.h>

void swap(int a,int b){
    int temp;
    temp = a;
    a = b;
    b = temp;
    printf("%d %d",a,b);
}
int main(void) {
    int a = 10;
    int b =20;
    swap(a,b);
}

结果:20 10

改用指针变量作参数后就很容易解决上面的问题:

#include <stdio.h>

void swap(int *a,int *b){
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}
int main(void) {
    int a = 10;
    int b =20;
    swap(&a,&b);
    printf("%d %d",a,b);
}

运行结果:20 10

用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。下面的例子定义了一个函数 max(),用来查找数组中值最大的元素:

#include <stdio.h>
int max(int *intArr, int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}
int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //读取用户输入的数据并赋值给数组元素
    for(i=0; i<len; i++){
        scanf("%d", nums+i);
    }
    printf("Max value is %d!\n", max(nums, len));
    return 0;
}

运行结果:
12 55 30 8 93 27↙
Max value is 93!

参数 intArr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址,nums+i就是第 i 个数组元素的地址。


#include <stdio.h>

int max(int *intArr, int len){
    int maxValue = intArr[0]; //假设第0个元素是最大值
    for (int i = 0; i < len; ++i) {
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}
int main(void) {
    int num[6] = {20,100,343,21,5,55};
    int len = sizeof(num)/sizeof(int);
    printf("最大值为:%d\n",max(num,len));
}

最大值为:343

需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

C语言指针作为函数返回值

C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。下面的例子定义了一个函数 strlong(),用来返回两个字符串中较长的一个: