专题十、指针

本文详细介绍了C语言中的指针概念,包括指针变量、取地址运算符和间接寻址运算符的使用,指针赋值,以及如何将指针作为函数参数和返回值。重点讨论了如何处理内存地址、指针的初始化与间接寻址,以及如何避免潜在的编程陷阱,如未初始化指针和返回局部变量地址的问题。
摘要由CSDN通过智能技术生成

本专题介绍 C 语言中内存地址及其与指针变量的关系,然后介绍取地址运算符和间接寻址运算符以及指针赋值的内容,最后介绍函数与指针的相关内容。

参考资料:《C 语言程序设计 · 现代方法 第 2 2 2 版》

1. 指针变量

理解指针的第一步是在机器级上观察指针表示的内容。大多数现代计算机都将内存分割为字节 byte \text{byte} byte),每个字节可以存储 8 8 8 位的信息。每个字节都有唯一的地址 address \text{address} address),用来和内存中的其他字节相区别。如果内存中有 n n n 个字节,那么可以把地址看作 0 ∼ n − 1 0 \sim n - 1 0n1 的数。
内存地址
可执行程序由代码(原始 C 程序中与语句对应的机器指令)和数据(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节内存,把第一个字节的地址称为是变量的地址。下图中,变量 i 占有地址为 2000 2000 2000 2001 2001 2001 的两个字节,所以变量 i 的地址是 2000 2000 2000
内存地址
这就是指针的出处。虽然用数表示地址,但是地址的取值范围可能不同于整数的范围,所以一定不能用普通整型变量存储地址。但是,可以用特殊的指针变量 pointer variable \text{pointer variable} pointer variable)存储地址。在用指针变量 p 存储变量 i 的地址时,我们说 p “指向” i。换句话说,指针就是地址,而指针变量就是存储地址的变量。为了说明指针变量 p 存储变量 i 的地址,将把 p 的内容显示为指向 i 的箭头:
指针定义
指针变量的声明
对指针变量的声明与对普通变量的声明基本一样,唯一的不同就是必须在指针变量名字前放置星号:

int *p;

上述声明说明 p 是指向 int 类型对象的指针变量。这里我们用术语对象来代替变量,是因为 p 可以指向不属于变量的内存区域(见专题十六)。

指针变量可以和其他变量一起出现在声明中:

int i, j, a[10], b[20], *p, *q;

C 语言要求每个指针变量只能指向一种特定类型(引用类型)的对象:

int *p;			/* points only to integers */
double *q;		/* points only to doubles */
char *r;		/* points only to characters */

至于引用类型是什么类型则没有限制。事实上,指针变量甚至可以指向另一个指针,即指向指针的指针(见专题十六)。

2. 取地址运算符和间接寻址运算符

为使用指针,C 语言提供了一对特殊设计的运算符。为了找到变量的地址,可以使用 &取地址)运算符。如果 x 是变量,那么 &x 就是 x 在内存中的地址。为了获得对指针所指向对象的访问,可以使用 *间接寻址)运算符。如果 p 是指针,那么 *p 表示 p 当前指向的对象。

2.1 取地址运算符

声明指针变量是为指针留出空间,但是并没有把它指向对象:

int *p;			/* points nowhere in particular */

在使用前初始化 p 是至关重要的。一种初始化指针变量的方法是使用 & 运算符把某个变量的地址赋给它,或者更常采用左值:

int i, *p;
...
p = &i;

通过把 i 的地址赋值给变量 p 的方法,上述语句把 p 指向了 i
指针赋值
在声明指针变量的同时对它进行初始化是可行的:

int i;
int *p = &i;

甚至可以把 i 的声明和 p 的声明合并,但是需要首先声明 i

int i, *p = &i;

2.2 间接寻址运算符

一旦指针变量指向了对象,就可以使用 *(间接寻址)运算符访问存储在对象中的内容。例如,如果 p 指向 i,那么可以如下所示显示出 i 的值:

printf("%d\n", *p);

printf 函数将会显示 i 的值,而不是 i 的地址。

习惯于数学思维的读者可能希望把 * 想象成 & 的逆运算。对变量使用 & 运算符产生指向变量的指针,而对指针使用 * 运算符则可以返回到原始变量:

j = *&i;		/* same as j = i; */

只要 p 指向 i*p 就是 i别名*p 不仅拥有和 i 相同的值,而且对 *p 的改变也会改变 i 的值。(*p 是左值,所以对它赋值是合法的)下面的例子说明了 *pi 的等价关系,这些图显示了在计算中不同的点上 pi 的值。
取址运算于间址运算
注意不要把间接寻址运算符用于未初始化的指针变量。如果指针变量 p 没有初始化,那么试图使用 p 的值会导致未定义的行为,给 *p 赋值尤其危险。如果 p 恰好具有有效的内存地址,下面的赋值会试图修改存储在该地址的数据:

int *p;
*p = 1;			/*** WRONG ***/

如果上述赋值改变的内存单元属于该程序,那么可能会导致不规律的行为;如果改变的内存单元属于操作系统,那么很可能会导致系统崩溃。编译器可能会给出警告消息,告知 p 未初始化,所以请留意获得的警告消息。

3. 指针赋值

C 语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。假设有如下声明:

int i, j, *p, *q;

语句 p = &i; 是指针赋值的示例,把 i 的地址复制给 pq = p 是另一个指针赋值的示例,这条语句是把 p 的内容(即 i 的地址)复制给 q,效果是把 q 指向了 p 所指向的地方:
在这里插入图片描述
现在 pq 都指向了 i,所以可以用对 *p*q 赋新值的方法来改变 i
在这里插入图片描述
任意数量的指针变量都可以指向同一个对象。注意不要把 q = p;*q = *p; 搞混。第一条语句是指针赋值,而第二条语句不是。就如下面的例子显示的:
在这里插入图片描述
赋值语句 *q = *p 是把 p 指向的值(i 的值)复制到 q 指向的对象(变量 j)中。

4. 指针作为参数

因为 C 语言用值进行参数传递,所以在函数调用中用作实际参数的变量无法改变。当希望函数能够改变变量时,C 语言的这种特性就很麻烦。

指针提供了此问题的解决方法:不再传递变量 x 作为函数的实际参数,而是提供 &x,即指向 x 的指针。声明相应的形式参数 p 为指针。调用函数时,p 的值为 &x,因此 *p 将是 x 的别名。函数体内 *p 的每次出现都将是对 x 的间接引用,而且允许函数既可以读取 x 也可以修改 x

下面通过把形式参数 int_partfrac_part 声明成指针的方法来修改专题八中的 decompose 函数:

void decompose(double x, long *int_part, double *frac_part)
{
	*int_part = (long) x;
	*frac_part = x - *int_part;
}

decompose 函数的原型可以是以下两种:

void decompose(double x, long *int_part, double *frac_part);
void decompose(double, long *, double *);

以下列方式调用 decompose 函数:

decompose(3.14159, &i, &d);

因为 id 前有取地址运算符 &,所以 decompose 函数的实际参数是指向 id 的指针,而不是 id 的值。调用 decompose 函数时,把值 3.14159 3.14159 3.14159 复制到 x 中,把指向 i 的指针存储在 int_part 中,而把指向 d 的指针存储在 frac_part 中:
在这里插入图片描述
decompose 函数体内的第一个赋值把 x 的值转换为 long 类型,并且把此值存储在 int_part 指向的对象中。因为 int_part 指向 i,所以赋值把值 3 3 3 放到 i 中:
在这里插入图片描述
第二个赋值把 int_part 指向的值(即 i 的值)取出,现在这个值是 3 3 3,把此值转换为 double 类型,并且用 x 减去它,得到 0.14159 0.14159 0.14159。然后把这个值存储在 frac_part 指向的对象中:
在这里插入图片描述
decompose 函数返回时,就像原来希望的那样,id 将分别有值 3 3 3 0.14159 0.14159 0.14159

用指针作为函数的实际参数,我们在 scanf 函数调用中一直在使用。思考下面的例子:

int i;
...
scanf("%d", &i);

必须把 & 放在 i 的前面以便给 scanf 函数传递指向 i 的指针,指针会告诉 scanf 函数把读取的值放在哪里。如果没有 & 运算符,传递给 scanf 函数的将是 i 的值。

虽然 scanf 函数的实际参数必须是指针,但并不总是需要 & 运算符。在下面的例子中,我们向 scanf 函数传递了一个指针变量:

int i, *p;
...
p = &i;
scanf("%d", p);

既然 p 包含了 i 的地址,那么 scanf 函数将读入整数并且把它存储在 i 中。在调用中使用 & 运算符将是错误的,语句 scanf("%d", &p); 将读入整数并且把它存储在 p 中而不是 i 中。

向函数传递需要的指针却失败了可能会产生严重的后果。假设我们在调用 decompose 函数时没有在 id 前面加上 & 运算符,当 decompose 函数把值存储到 *int_part*frac_part 中时,它会把 id 的值当成指针来使用,从而修改未知的内存地址,而不是修改 id。如果已经提供了 decompose 函数的原型,那么编译器将告诉我们实际参数的类型不对。然而,在 scanf 的例子中,编译器通常不会检查出传递指针失败,因此 scanf 函数特别容易出错。

程序 maxmin.c:找出数组中的最大元素和最小元素
下面来看一个名为 max_min 的函数,该函数用于找出数组中的最大元素和最小元素。调用 max_min 函数时,将传递两个指向变量的指针;然后 max_min 函数把答案存储在这些变量中。

/* Finds the largest and smallest elements in an array */

#include <stdio.h>

#define N 10

void max_min(int a[], int n, int *max, int *min);

int main(void)
{
    int b[N], i, big, small;

    printf("Enter %d numbers: ", N);
    for (i = 0; i < N; i++)
        scanf("%d", &b[i]);

    max_min(b, N, &big, &small);

    printf("Largest: %d\n", big);
    printf("Smallest: %d\n", small);

    return 0;
}

void max_min(int a[], int n, int *max, int *min)
{
    int i;

    *max = *min = a[0];
    for (i = 1; i < n; i++) {
        if (a[i] > *max)
            *max = a[i];
        else if (a[i] < *min)
            *min = a[i];
    }
}

这个程序的运行过程如下(用户的输入用下划线标注):
Enter 10 numbers: 34   82   49   102   7   94   23   11   50   31 ‾ \underline{34\ 82\ 49\ 102\ 7\ 94\ 23\ 11\ 50\ 31} 34 82 49 102 7 94 23 11 50 31

Largest: 102
Smallest: 7

const 保护参数

当调用函数并且把指向变量的指针作为参数传入时,通常会假设函数将修改变量。例如,如果在程序中看到语句 f(&x);,大概是希望 f 改变 x 的值。但是,f 仅需要检查 x 的值而不是改变它的值也是可能的。指针可能高效的原因是:如果变量需要大量的存储空间,那么传递变量的值会浪费时间和空间。

可以使用单词 const 来表明函数不会改变指针参数所指向的对象。const 应放置在形式参数的声明中,后面紧跟着形式参数的类型说明:

void f(const int *p)
{
	*p = 0;			/*** WRONG ***/
}

这一用法表明 p 是指向 “常整数” 的指针。试图改变 *p 是编译器会检查的一种错误。

5. 指针作为返回值

我们不仅可以为函数传递指针,还可以编写返回指针的函数。当给定指向两个整数的指针时,下列函数返回指向两整数中较大数的指针:

int *max(int *a, int *b)
{
	if (*a > *b)
		return a;
	else
		return b;
}

调用 max 函数时,用指向两个 int 类型变量的指针作为参数,并且把结果存储在一个指针变量中:

int *p, i, j;
...
p = max(&i, &j);

调用 max 期间,*ai 的别名,而 *bj 的别名。如果 i 的值大于 j,那么 max 返回 i 的地址;否则,max 返回 j 的地址。调用函数后,p 可以指向 i 也可以指向 j

这个例子中 max 函数返回的指针是作为实际参数传入的两个指针中的一个,但这不是唯一的选择。函数也可以返回指向外部变量或指向声明为 static 的局部变量的指针。

永远不要返回指向自动局部变量的指针:

int *f(void)
{
	int i;
	...
	return &i;
}

一旦 f 返回,变量 i 就不存在了,所以指向变量 i 的指针将是无效的。有的编译器会在这种情况下给出类似 “ function returns address of local variable \text{function returns address of local variable} function returns address of local variable” 的警告。

指针可以指向数组元素,而不仅仅是普通变量。设 a 为数组,则 &a[i] 是指向 a 中元素 i 的指针。当函数的参数中有数组时,返回一个指向数组中的某个元素的指针有时是挺有用的。例如,下面的函数假定数组 a n n n 个元素,并返回一个指向数组中间元素的指针:

int *find_middle(int a[], int n)
{
	return &a[n/2];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朔北之忘 Clancy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值