c-programming-chap8

If two people write exactly the same program, each should be put in micro-code and then they certainly won’t be the same.


[TOC]

指针

零 前言

指针是 C 语言最重要——也是最常被误解——的特性之一。本节重点介绍指针的基础内容。

一 指针变量

现代大多数计算机将内存分割为字节(byte),每个字节可以存储 8 位的信息:0000 0001

每个字节都有唯一的地址(address),用来和内存中的其他字节相区别。如果内存中有 n 个字节,那么可以把地址看作 0 ~ n - 1的数。

可执行程序由代码(原始 C 程序中于语句对应的机器指令)和 数据(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节,把第一个字节的地址称为是变量的地址。

上图中,i 占有的字节是 2000 ~ 2003 4 个字节,2000 就是 i 的地址。

我们可以用特殊的指针变量(pointer variable)存储地址。在用指针变量存储 p 存储变量 i 的地址时,我们说 p “指向” i 。换句话说,指针就是地址,而指针变量就是存储地址的变量。

1. 指针变量的声明

1
2
3
4
// 类型名 *指针变量名;

int *pointer_1, *pointer_2;

上述声明说明p 是指向 int 类型对象的指针变量

左端的int是在定义指针变量时必须指定的“基类型”。指针变量的基类型用来指定此指针变量可以指向的变量的类型。
前面介绍过基本的数据类型(如int,char,float等),既然有这些类型的变量,就可以有指向这些类型变量的指针,因此,指针变量是基本数据类型派生出来的类型,它不能离开基本类型而独立存在。

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

1
int a, b[10], *p, *q;

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

1
2
3
int* p;
double* q;
char* r;

关于指针变量声明中 * 与谁挨着的问题

请看下面的声明:

1
int* p,q;

请问,上面的声明中 p 和 q 都是指针变量吗?

小黄:我觉得是,如果你写成这样:

1
int *p, q;

那就是只有 p 是指针变量了。

程序圆:你这样想就大错特错啦,上面这两种写法是等价的。都是声明 p 为指针变量而 q 是一个普通的 int 类型变量。

小黄:哦~那我们平时应该选择那种写法呢?

程序圆:通常情况下我们都是选择第一种写法,即:int* p。但是这样确实容易造成误解,所以我们通常一行只声明一个指针变量就可以了。


二 如何应用指针变量

1. 取地址运算符【指针变量初始化】

声明指针变量时我们没有将它指向任何对象:

1
int* p;

在使用之前初始化 p 是至关重要的。使用取地址运算符&把某个变量的地址赋值给它。

1
2
int i;
p = &i; //&i 就是 i 在内存中的地址

现在 p 就指向了整型变量 i

我们也可以声明的同时初始化:

1
2
int i;
int* p = &i;

甚至可以这样:

1
int i, *p = &i;

但是需要先声明 i

2. 间接寻址运算符

间接寻址运算符 *

1
2
int i;
int* p = &i;

指针变量 p 指向 i,使用*运算符可以访问存储在对象中的内容

(即访问存储在指针变量指向的地址上的内容)。

1
printf("%d", *p); // (*p == i)

*&互为逆运算”:

1
j = *&i;// same as j = i;   因为 *p = &i;

只要 p 指向 i,*p 就是 i 的别名。*p 不仅拥有和 i 相同的值,而且 *p 的改变也会改变 i 的值。

1
2
3
4
5
6
7
8
9
int i = 0;
int* p = &i;
printf("i = %d\n", i);
printf("p = %d\n", *p);
// 输出:0 0
*p = 1;
printf("now i = %d\n", i);
printf("now p = %d\n", *p);
//输出:1 1

注意:

未初始化的指针变量会导致未定义行为

1
2
int* p;
printf("%d", *p);

3. 指针赋值

C 语言允许相同类型的指针变量进行赋值。

1
2
3
4
int i;
int* p = &i;
int* q;
q = p;

或者直接初始化并赋值:

1
int* q = p;

现在可以通过改变 *p 的值来改变 i :

1
2
3
4
5
6
7
8
9
10
int i = 0;
int* p = &i;
int* q = p;
printf("now i = %d\n", i);
printf("now p = %d\n", *q);
// 输出:0 0
*q = 2; // 将2赋给指针q指向的变量
printf("now i = %d\n", i);
printf("now p = %d\n", *q);
//输出:2 2

不要将 *q = *pq = p 搞混,前者是将 p 指向的对象的值(变量 i 的值)赋值给 q 指向的对象(变量 j)中。

4.指针作为参数

函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法


如果想通过函数调用得到n个要改变的值,可以这样做:

  1. 在主调函数中设n个变量,用n个指针变量指向它们;
  2. 设计一个函数(自定义),有n个指针形参。在这个函数中改变这n个形参的值;
  3. 在主调函数中调用这个函数,在调用时将这n个指针变量作实参,将它们的值,也就是相关变量的地址传给该函数的形参;
  4. 在执行该函数的过程中,通过形参指针变量,改变它们所指向的n个变量的值;
  5. 主调函数中就可以使用这些改变了值的变量。
程序1:对输入的两个整数按大小顺序输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int main()
{ void swap(int *p1,int *p2);
int a,b;
int *pointer_1,*pointer_2; //pointer_1,pointer_2是int *型变量
printf("please enter two integer numbers:");
scanf("%d,%d",&a,&b);
pointer_1=&a;
pointer_2=&b;
if(a<b) swap(pointer_1,pointer_2);
//调用swap函数,用指针变量作实参
printf("max=%d,min=%d\n",*pointer_1,*pointer_2);
return 0;
}

void swap(int *p1,int *p2) //形参是指针变量
{ int *p;
p=p1; //下面3行交换p1和p2的指向
p1=p2;
p2=p;
}


程序2:找出数组中的最大元素和最小元素

与程序的交互如下:

1
2
3
Enter 5 numbers:9 5 2 7 8
Largest: 9
Smallest: 2

参考程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<stdio.h>

#define SIZE 5

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

int main(void) {

int a[SIZE];
int max, min, i;

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

max_min(a, SIZE, &max, &min);

printf("Largest: %d\n", max);
printf("Smallest: %d\n", min);

return 0;
}

void max_min(int a[], int len, int* max, int* min) {

int i;

*max = *min = a[0];
for (i = 1; i < len; i++) {
// a[i] 如果比 *max 大 那肯定不会比 *min 小,反之也成立
if (a[i] > * max)
*max = a[i];
else if (a[i] < *min)
*min = a[i];
}
}

指针应用小结

① 给指针变量赋值。
② 引用指针变量指向的变量。
③引用指针变量的值。

1
2
3
4
5
6
int a, *p;
p=&a; //把a的地址赋给指针变量p①
printf("%d",*p); //以整数形式输出指针变量p所指向的变量的值,即a的值 ②
*p=1; //将整数1赋给p当前所指向的变量,由于p指向变量a,相当于把1赋给a,即a=1 ②
printf("%o",p);//以八进制形式输出指针变量p的值,由于p指向a,相当于输出a的地址,即&a ③

要熟练掌握两个有关的运算符:
(1) &取地址运算符。&a是变量a的地址。
(2) * 指针运算符(或称“间接访问”运算符),*p代表指针变量p指向的对象。
(3) 注意区分printf("%d",*p)printf("%o",p)的区别

例题

若有int *p,a=10;和p=&a;下面( )均代表地址。

A) a,p,*&a
B) &*a,&a,*p
C) *&p,*p,&a
D) &a,&*p,p ✅

扩展:请用至少两种不同的方式输出变量a的值

三 指针与数组

用指针引用数组元素

一个变量有地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。 可以用一个指针变量指向一个数组元素。

1
2
3
4
5

int a[10]={1,3,5,7,9,11,13,15,17,19}; //定义a为包含10个整型数据的数组
int *p; //定义p为指向整型变量的指针变量
p=&a[0];//把a[0]元素的地址赋给指针变量p

引用数组元素可以用下标法,也可以用指针法,即通过指向数组元素的指针找到所需的元素。

初始化数组指针

即将指针变量指向数组首个元素

1
2
3
4
p=&a[0];//p的值是a[0]的地址
p=a; //p的值是数组a首元素(即a[0])的地址

// 注意:程序中的数组名不代表整个数组,只代表数组首元素的地址。

在定义指针变量时可以对它初始化:

1
2
3
4
5
6
int *p;
p=&a[0]; //不应写成*p=&a[0];

// 等价于以下两种写法
int *p=&a[0];
int *p=a;

在引用数组元素时指针的运算

在指针已指向一个数组元素时,可以对指针进行以下运算:

  • 加一个整数(用+或+=),如p+1,表示指向同一数组中的下一个元素;
  • 减一个整数(用-或-=),如p-1,表示指向同一数组中的上一个元素;
  • 自加运算,如p++,++p;
  • 自减运算,如p–,–p。
1
// 注意:执行p+1时并不是将p的值(地址)简单地加1,而是根据定义的基类型加上一个数组元素所占用的字节数

两个指针相减,如p1-p2(只有p1和p2都指向同一数组中的元素时才有意义),结果是两个地址之差除以数组元素的长度。注意: 两个地址不能相加,如p1+p2是无实际意义的。

如果p的初值为&a[0],则p+i和a+i就是数组元素a[i]的地址,或者说,它们指向a数组序号为i的元素。

*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。[]实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。

例题:有一个整型数组a,有10个元素,要求输出数组中的全部元素

  1. 方法1:下标法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{ int a[10];
int i;
printf("please enter 10 integer numbers:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
for(i=0;i<10;i++)
printf("%d ",a[i]);
//数组元素用数组名和下标表示
printf("%\n");
return 0;
}

  1. 方法2:通过数组名计算数组元素地址,找出元素的值
1
2
3
4
5
6
7
8
9
10
11
12
13
 #include <stdio.h>
int main()
{ int a[10];
int i;
printf("please enter 10 integer numbers:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
for(i=0;i<10;i++)
printf("%d ",*(a+i));
//通过数组名和元素序号计算元素地址找到该元素
printf("\n");
return 0;
}
  1. 方法3:用指针变量指向数组元素
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{ int a[10];
int *p,i;
printf("please enter 10 integer numbers:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
for(p=a;p<(a+10);p++)
printf("%d ",*p);
//用指针指向当前的数组元素
printf("\n");
return 0;
}

三种方法的比较

第(1)和第(2)种方法执行效率是相同的。C编译系统是将a[i]转换为*(a+i)处理的,即先计算元素地址。因此用第(1)和第(2)种方法找数组元素费时较多。
第(3)种方法比第(1)、第(2)种方法快,用指针变量直接指向元素,不必每次都重新计算地址,像p++这样的自加操作是比较快的。这种有规律地改变地址值(p++)能大大提高执行效率。

例题

若有定义 int a[5],*p=a;则对 a 数组元素的正确引用是( )。
A) *&a[5]
B) a+2
C) *(p+5)
D) *(a+2) ✅

注意事项

用下标法比较直观,能直接知道是第几个元素。适合初学者使用。
用地址法或指针变量的方法不直观,难以很快地判断出当前处理的是哪一个元素。单用指针变量的方法进行控制,可使程序简洁、高效。

在使用指针变量指向数组元素时,有以下几个问题要注意:
(1) 可以通过改变指针变量的值指向不同的元素。
如果不用p变化的方法而用数组名a变化的方法(例如,用a++)行不行呢?

1
2
for(p=a;a<(p+10);a++)
printf(″%d″,*a); // 错误 ❎

因为数组名a代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。既然a是常量,所以a++是无法实现的。

(2) 要注意指针变量的当前值。

例题

通过指针变量输出整型数组a的10个元素。
判断下面哪一个代码正确??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{ int *p,i,a[10];
p=a; //p指向a[0] ①
printf("please enter 10 integer numbers:");
for(i=0;i<10;i++)
scanf("%d",p++); //输入10个整数给a[0]~a[9]
for(i=0;i<10;i++,p++)
printf("%d ",*p); //想输出a[0]~a[9] ②
printf("\n");
return 0;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码2
#include <stdio.h>
int main()
{ int i,a[10],*p=a; //p的初值是a,p指向a[0]
printf("please enter 10 integer numbers:");
for(i=0;i<10;i++)
scanf("%d",p++);
p=a; //重新使p指向a[0]
for(i=0;i<10;i++,p++)
printf("%d ",*p);
printf("\n");
return 0;
}


例题小结
  1. 从此例题可以看到,虽然定义数组时指定它包含10个元素,并用指针变量p指向某一数组元素,但是实际上指针变量p可以指向数组以后的存储单元。
  2. 指向数组元素的指针变量也可以带下标,如p[i]。p[i]被处理成*(p+i),如果p是指向一个整型数组元素a[0],则p[i]代表a[i]。但是必须弄清楚p当前指向的是哪一个元素?如果当前p指向a[3],则p[2]并不代表a[2],而是a[3+2],即a[5]。
  3. 利用指针引用数组元素,比较方便灵活,有不少技巧。请分析下面几种情况:
  • 设p开始时指向数组a的首元素(即p=a):
    1
    2
    3
    4
    5
    6
    p++;	//使p指向下一元素a[1]
    *p; //得到下一个元素a[1]的值
    *(p++); //先取*p值,然后使p加1
    *(++p); //先使p加1,再取*p
    *p++; /*由于++和*同优先级,结合方向自右而左,因此它等价于*(p++)。先引用p的值,实现*p的运算,然后再使p自增1*/
    ++(*p); /*表示p所指向的元素值加1,如果p=a, 则相当于++a[0],若a[0]的值为3,则a[0]的值为4。注意: 是元素a[0]的值加1,而不是指针p的值加1*/
  • 如果p当前指向a数组中第i个元素a[i],则:
    1
    2
    3
    *(p--)	//相当于a[i--],先对p进行“*”运算,再使p自减
    *(++p) //相当于a[++i],先使p自加,再进行“*”运算
    *(--p) //相当于a[--i],先使p自减,再进行“*”运算

用数组名作函数参数

如果有一个实参数组,要想在函数中改变此数组中的元素的值,实参与形参的对应关系有以下4种情况。

① 形参和实参都用数组名
② 实参用数组名,形参用指针变量。
③ 实参形参都用指针变量。
④ 实参为指针变量,形参为数组名。

例题 【例8.10】用指针方法对10个整数按由大到小顺序排序。

四 指针与字符串【重要!!】

如何用数组定义字符串??

忘记的同学请自行查阅!

字符串的引用方式

  1. 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明%s输出该字符串。
  2. 用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
字符型指针声明、赋值
1
2
3
4
5
6
7
8
9
char *string="I love China!"; 

// or

char *string; //定义一个char *型变量
string=″I love China!″;
//把字符串第1个元素的地址赋给字符指针变量string


string被定义为一个指针变量,基类型为字符型。它只能指向一个字符类型数据,而不能同时指向多个字符数据,更不是把″I love China!″这些字符存放到string中(指针变量只能存放地址),也不是把字符串赋给*string。只是把″I love China!″的第1个字符的地址赋给指针变量string。

可以对指针变量进行再赋值,如:

1
string=″I am a student.″; 	//对指针变量string重新赋值

可以通过字符指针变量输出它所指向的字符串,如:

1
printf(″%s\n″,string);	//%s可对字符串进行整体的输入输出

%s是输出字符串时所用的格式符,在输出项中给出字符指针变量名string,则系统会输出string所指向的字符串第1个字符,然后自动使string加1,使之指向下一个字符,再输出该字符……如此直到遇到字符串结束标志′\0′为止。注意,在内存中,字符串的最后被自动加了一个′\0′。

例题

【例8.18】将字符串a复制为字符串b,然后输出字符串b(用指针变量来处理);

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{ char a[]="I am a boy.",b[20],*p1,*p2;
p1=a;p2=b;
//p1,p2分别指向a数组和b数组中的第一个元素
for(;*p1!='\0';p1++,p2++) //p1,p2每次自加1
*p2=*p1;
//将p1所指向的元素的值赋给p2所指向的元素
*p2='\0'; //在复制完全部有效字符后加'\0'
printf("string a is:%s\n",a); //输出a数组中的字符
printf("string b is:%s\n",b); //输出b数组中的字符
return 0;
}

字符指针作函数参数

字符指针作为函数参数时,实参与形参的类型有以下几种对应关系:

【例8.20】

使用字符指针变量和字符数组的总结

  1. 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第1个字符的地址),绝不是将字符串放到字符指针变量中。
  2. 赋值方式。可以对字符指针变量赋值,但不能对数组名赋值。(数组名是常量)
  3. 初始化的含义。

  1. 存储单元的内容。编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元(Visual C++为指针变量分配4个字节)

  2. 指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。

  3. 引用数组元素。对字符数组可以用下标法(用数组名和下标)引用一个数组元素(如a[5]),也可以用地址法(如*(a+5))引用数组元素a[5]。如果定义了字符指针变量p,并使它指向数组a的首元素,则可以用指针变量带下标的形式引用数组元素(如p[5]),同样,可以用地址法(如*(p+5))引用数组元素a[5]。

  4. 字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。

  1. 用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串。
1
2
char *format="a=%d,b=%f\n";
printf(format,a,b);
例题

改变指针变量的值

1
2
3
4
5
6
7
8
//代码1
#include <stdio.h>
int main()
{ char *a="I love China!";
a=a+7; //改变指针变量的值,即改变指针变量的指向
printf("%s\n",a); //输出从a指向的字符开始的字符串
return 0;
}
1
2
3
4
5
6
7
8
//代码2
#include <stdio.h>
int main()
{ char *a="I love China!";
a=a+7; //改变指针变量的值,即改变指针变量的指向
printf("%s\n",a); //输出从a指向的字符开始的字符串
return 0;
}

指针变量a的值是可以变化的。printf函数输出字符串时,从指针变量a当时所指向的元素开始,逐个输出各个字符,直到遇’\0’为止。而数组名虽然代表地址,但它是常量,它的值是不能改变的。

参考资料:《C语言程序设计:现代方法》、《C语言程序设计(第五版)》

  1. 如果两个人用低级语言写同一个程序,它们显然不会相同。Epigrams on Programming 编程警句

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!