【编程语言】C语言指针(包括:指针与数组、指针与字符串、指针与函数)

指针概述

指针的概念

在计算机中,所有的数据都是存放在存储器中的。一般把存储器中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不等,例如int型占4个内存单元,char型占1个内存单元。为了正确地访问这些内存单元,必须为每个内存单元编上号,根据一个内存单元的编号即可准确地找到该内存单元。内存单元的编号也叫做地址。既然根据内存单元的编号或地址就可以找到所需的内存单元,所以通常也把这个地址称为指针。可以说,指针就是内存地址。

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址。

访问内存中的数据有两种方式:直接访问和间接访问。

  • 直接访问:通过变量来实现,因为变量是内存中某一块存储区域的名称;
  • 间接访问:通过指针来实现。指针并不是用来存储数据的,而是用来存储数据在内存中的地址的。

指针的类型

区分指针的类型和指针所指向的类型:

  • 指针的类型:将指针声明语句中去掉指针名字,而剩下的部分;
  • 指针所指向的类型:将指针声明语句中去掉指针名字和指针声明符(*),而剩下的部分。例如:
int* ptr;

指针的类型:int*;指针所指向的类型:int。

指针的值

指针的值是指指针本身存储的数值,这个值将被编译器当做一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全部都是32位长。

指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。换句话说,指针的值即为指针指向了以该值为首地址的一片内存区;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区的首地址。

运算符&和*

C语言中提供了地址运算符(&)来表示变量的地址,指针运算符(*)来表示获取指针变量所指向的变量的值。

&*p和*&p的区别:

因为*和&具有相同的优先级,且是右结合性(从右向左),故分析可得:

  • &*p:相当于&(*p),首先进行一次*p运算再取地址。若p是一个非指针变量,*p是非法的;若p是一个指针变量,&(*p)=p;
  • *&p:相当于*(&p),先取地址再做*运算。若p是一个非指针变量,*(&p)=p;若p是一个指针变量,*(&p)=p。

可能你会有疑问,为什么p为指针变量,*(&p)=p?

p为指针变量时,表示指针的值为一个地址,但是这个地址也存储在一个变量里,也存储在一个内存单元里。&p的意义,就是存放了一个地址的内存单元的地址。

下面结合一个例子来具体分析理解:

#include<stdio.h>

int main()
{
	int a=0;
	int* p = &a;

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

	return 0;
}

这段程序的运行结果为:

0 5240964
5240964 0 5240952
0 0
5240964 5240964 5240964
请按任意键继续. . .

这段程序主要理解两个点:当a为一个非指针变量的时候,*a是非法的;当p为一个指针变量的时候,&p的意义是存储了一个地址的内存单元的地址。


指向变量的指针

指针变量的使用

变量的指针就是变量的地址,存放变量地址的变量就是指针变量。指针变量中只能存放指针(地址),不要将一个非零数(或任何其他非地址类型的数据)赋予一个指针变量。如:

int* p = 2;            /* 错误的赋值,需要指向一个地址或0 */
int* p = 0;            /* 正确的赋值,表示指针指向空 */

指针变量被初始化为0,表示为空指针。C语言中用NULL表示空指针,它没有指向任何对象。一般来说,所有的指针变量在定义的时候都必须初始化,如果不明确要给指针赋什么值,就赋值为0。

指针变量作为函数函数

函数的参数不仅仅可以为整型、实型、字符型等数据,还可以是指针类型。它的作用是将以个变量的地址传送到另一个函数中。也就是说,如果想要将函数的过程中的操作结果反馈到主调函数,指针变量做参数是一个不错的选择。

例子1:

#include<stdio.h>

void swap(int* point1, int* point2);

int main()
{
	int a,b;
	int* p1 = &a;
	int* p2 = &b;

	scanf_s("%d %d", &a, &b);
	swap(p1, p2);
	printf("%d %d\n", a, b);
	printf("%d %d\n", *p1, *p2);

	return 0;
}

void swap(int* point1, int* point2) {
	int temp;
	temp = *point1;
	*point1 = *point2;
	*point2 = temp;
}

这段程序的运行结果为:

1 2
2 1
2 1
请按任意键继续. . .

例子2:

#include<stdio.h>

void swap(int point1, int point2);

int main()
{
	int a,b;

	scanf_s("%d %d", &a, &b);
	swap(a, b);
	printf("%d %d\n", a, b);

	return 0;
}

void swap(int point1, int point2) {
	int temp;
	temp = point1;
	point1 = point2;
	point2 = temp;
}

这段程序的运行结果为:

1 2
1 2
请按任意键继续. . .

两段程序对应着看,例子1使用指针传递地址,将函数的过程中的操作结果反馈到主调函数;而例子2没有使用指针,函数过程中的操作与主调函数之间没有任何关系。


指针与一维数组

指针与一维数组的关系

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

定义一个指向数组元素的指针变量的方法,与上文指向变量的指针变量相同。如果要让指针指向某个数组,只需要将这个数组的元素地址赋值给指针即可:

int a[5], *p;
p = a;                /* 数组名表示数组的首地址,可将数组名直接赋值给指针变量 */
p = &a[0];            /* 数组第一个元素的地址也就是整个数组的首地址 */

p=a,并不是将a数组的所有元素都赋值给p,只是把数组a的第一个元素的地址即首地址赋值给指针p。C语言规定:数组名代表数组的首地址,也就是第0个元素的地址。

例子:

#include <stdio.h>

int main()
{
	int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int *p;
	p = &a[0];

	printf("%d %d %d %d\n", a[0], &a[0], &a, a);
	printf("%d %d\n", p, *p);

	return 0;
}

这段程序的运行结果为:

0 2948652 2948652 2948652
2948652 0
请按任意键继续. . .

这里着重看一下:数组a的地址&a和数组名a的值都等于收元素a[0]处的地址

指针的运算

对于指向数组的指针变量,可以加上或减去一个整数n,这意味着把指针从当前指向的位置(指向的某个数组元素)向后或向前移动n个位置。

这里应当注意:

  • 数组指针变量向前向后移动一个位置和地址加1或减1在概念上是不同的。因为数组的元素可以由不同的数据类型,所占用的字节长度也是不同的。如指针变量加1,即表示指针变量指向下一个元素的首地址,而不是在原地址的基础上加1;
  • 指针变量的加减运算只能对数组指针变量进行,对只想其他数据类型的指针变量做加减运算是毫无意义的。
  • 两个指针变量之间的运算:只有指向同一数组的两个指针变量才能进行运算,否则运算是毫无意义的。
  • 两指针变量相减:两指针变量相减所得之差,是两个指针所指向的数据元素之间相差的元素个数,乘以该数组每个数据元素的长度(字节数);
  • 两指针变量相加:两指针变量不能进行加法运算,毫无实际意义。


指针与二维数组

通过指针来访问二维数组,主要有列指针、行指针(数组指针)、指针数组(列指针数组)三种方式来进行访问。下面依次进行讲解:

二维数组与一维数组的关系

在C语言中,二维数组是按行优先的规律转换为一维数组地址存放在内存中的,因此可以通过指针访问二维数组中的元素,如果有:

int a[M][N];

则将其转换为一维线性地址,一般公式如下:

线性地址=a+i×M+j

其中,a为数组的首地址。

从这里可以看出,我们需要的是一个列指针来进行转化访问的。

#include <stdio.h>

int main()
{
	int a[2][3] = { 1,2,3,4,5,6 };
	int i;
	int* p;
	p = *a;

	for (i = 0; i < 6; i++) {
		printf("%d\n", *p);
		p++;
	}

	return 0;
}

这段程序的运行结果为:

1
2
3
4
5
6
请按任意键继续. . .

这里使用列指针来遍历,将二维数组转化为一维数组的方式来处理。需要注意的是:

	int* p;
	p = *a;

因为p的定义,是一个指向整型的指针(列指针),而a是一个指向一个一维数组的指针(行指针),需要通过*a转化为列指针赋值才行,如果直接写p=a的话会出错。

数组指针(行指针)

二维数组可以看成多个一维数组组成的,每一行为一个一维数组,因此二维数组可以定义一维数组指针。该指针移动一次即移动二维数组的一行。

如果定义一个指针p,让它指向一个包含n个元素的一维数组,且p的增值以一位数组的长度为单位,此时如果p指向二维数组的某一行,则p+1就指向了该二维数组的下一行。这样的指针称为数组指针,使用数组指针可以很方便地处理二维数组。

数组指针定义的一般形式如下:

存储类型 数据类型(*指针变量名)[元素个数];
int (*p)[4];

在使用数组指针时,有两点一定要注意:

  • *p两端的括号一定不能漏掉,如果写成*p[4]的形式,由于[]的运算级别高,所以p先与[]结合,是数组,然后在与前面的*结合,*p[4]是指针数组;
  • p是一个行指针,它只能指向一个包含n个元素的一维数组,不能指向一位数组的元素;
  • 可以理解为int[4] *p,即p指向的是一个int[4]类型的一维数组。

例子:

#include<stdio.h>

int main()
{
	int array[2][3] = { 1,2,3,4,5,6 };
	int i, j;
	int(*p)[3];
	p = array;
	for (i = 0; i < 2; i++) {
		for (j = 0; j < 3; j++) {
			printf("%d %d %d\n", p[i][j], *(*(p+i)+j), *(p[i]+j));
		}
	}

	return 0;
}

这段程序的运行结果为:

1 1 1
2 2 2
3 3 3
4 4 4
5 5 5
6 6 6
请按任意键继续. . .

从这段程序来分析数组指针:

  • 它是一个指针,所以(*p)[4]按照优先级来算,p与*先结合,指针;
  • 由于是行指针,数组指针p可以通过p+1来转换到下一行,那么列中怎么操作呢?通过[]内的数字来访问;或者对行指针取*运算,可以变成列指针。

理解数组名和数组指针变量

假设有一个二维数组和一个数组指针:

int a[2][2] = {1, 2, 3, 4};
int (*p)[2];
p = a;

分析:

  • a是一个二维数组名,类型是指向一维数组的指针(行指针),不是变量,a的值是指针常量,即不能有a++或者a=p这些操作。a指向这块连续空间的首地址,值是&a[0][0];
  • a[0]是一维数组名,类型是指向整型的指针(列指针),值是&a[0][0],这个值是一个常量;
  • a[1]是一维数组名,类型是指向整型的指针(列指针),值是&a[1][0],这个值是一个常量;
  • p是一个数组指针变量,指向一维数组的指针变量,值是&a[0][0]。可以执行p++;p=a等操作;
  • a+1表示指向下一行元素,也可以理解为指向下一个一维数组;
  • *(a+1)是取出第一行的首地址(列指针);
  • a[0]+1是指向第0行第1个元素,也可以理解为指向一维数组a[0]的第一个元素;
  • p+1同a+1;
  • *(p+1)同*(a+1);

虽然a跟a[0]值是一样,但类型不一样,表示的意义不一样。a是一个指向一维数组的指针,a[0]是指向整型的指针。也就是说,a+1跳过一个一维数组,是个行指针;a[0]+1跳过一个int整型,是一个列指针。

通过分析就不难理解为什么*(*(a+i)+j)和a[i][j]等效了。

总结一下二维数组的地址表示方法:

二维地址意义
表示方法 意义
a、a[0]、*(a+0)、*a、&a[0][0] 0行0列元素的地址
a+i、*(a+i)、&a[i][0]、&a[i]、a[i] i行0列元素的地址
*(a[i]+j)、*(*(a+i)+j)、a[i][j] i行j列元素的地址

区分数组名和数组指针变量:

数组名是指针,类型是指向某个元素类型的指针,但值是指针常量,声明数组时编译器会为声明所指定的元素数量保留内存空间。数组指针是指向数组的指针,声明指针变量时编译器只为指针本身保留内存空间。

例子:

#include <stdio.h>

int main()
{
	int a[2][3] = { 1,2,3,4,5,6 };
	int (*p)[3];
	p = a;

	printf("%d\n%d\n", sizeof a, sizeof p);

	return 0;
}

上述程序的运行结果为:

24
4
请按任意键继续. . .

当sizeof用于变量时返回这个变量占用的实际空间的大小。当sizeof用于数组名时,返回整个数组的大小(这里的大小指占用的字节数)。p是一个指针变量,这个变量占用四个字节。而a是数组名,所以sizeof a返回数组a中的全部元素占用的字节数。

指针数组

数组的每个元素都是一个指针数据的数组称为指针数组。指针数组的所有元素都必须是具有相同存储类型和指向相同数据类型的指针变量。

指针数组定义的一般形式如下:

存储类型 数据类型 *指针变量名[元素个数];
int* p[4];

一般也可以使用一个指针数组来指向一个二维数组。指针数组的每个元素被赋予二维数组每一行的首地址,也可以理解成将每一行的首地址的列指针罗列成一个数组。

指针数组是介于单纯列指针访问和单纯行指针访问之间的一种方式,指针数组中的每个元素是一个列指针,但是它又避免了列指针比较繁琐的位置计算,可以通过数组中不同元素的切换,模拟行指针的各行之间的跳跃。

例子:

#include<stdio.h>  

int main()
{
	int a[2][3] = { 1,2,3,4,5,6 };
	int i, j;
	int* p[2] = { a[0], a[1] };

	for (i = 0; i < 2; i++) {
		for (j = 0; j < 3; j++) {
			printf("%d\n", *(p[i] + j));
		}
	}

	return 0;
}

上述程序的运行结果为:

1
2
3
4
5
6
请按任意键继续. . .

通常数组指针和指针数组的一个小区别:

int* p[2];                /* 二维数组有两行 */
int (*p)[2];                /* 二维数组有两列 */

指针数组还常用来表示一组字符串:

char* p[2] = { "Hello", "World" };


指针与字符串

字符串是以字符串数组的形式实现的,既然指针可以指向数组,那么也就可以指向字符串。将字符串的首地址赋值给指向字符类型的指针变量:

char *p;
p = "Hello World!";

这里应说明的是并不是把整个字符串装入指针变量,而是把存放该字符串的字符数组的首字符地址装入指针变量。

使用字符串指针变量引用字符串的方法有两种:逐个引用和整体引用。

char *p;
p = "Hello World!";
printf("%c\n", *p);            /* H */
printf("%s\n", p);            /* Hello World! */

这里需要注意的是:逐个引用时是一次使用*p来输出,而整体引用时是一次使用p来输出。

整体引用所指向的字符串的原理:系统会首先输出指针指向的第一个字符,然后依次自动加1,使之指向下一个字符;重复上述过程,直到遇到字符串结束标志'\0'。

而对于其他数据类型的数组,是不能用数组名来一次性输出它的全部元素的,只能逐个元素输出!

字符数组和字符串指针处理字符串时的区别:

  • 存储内容不同:字符串指针变量本身是一个变量,存储的是字符串的首地址。而字符串本身存放在以该首地址为首的一块连续的内存空间中并以'\0'作为串的结束。字符数组中存储的是字符串本身;
  • 赋值方式不同:对字符串指针变量的赋值方式,可以使用如下两种:
char *c="Hello World!";            /* 正确 */
char *c;
c="Hello World!"            /* 正确 */

而字符串数组虽然可以在定义初始化时整体赋初值,但不能使用赋值语句整体赋值,只能对字符串的各个元素逐个赋值:

char c[12] = "Hello World!";            /* 正确 */
char c[12];
c = "Hello World!"            /* 错误 */

  • 存储器空间分配不同:字符串指针变量只会给这个变量分配空间,而字符串数组会给整个数组分配空间。这部分介绍的归根结底仍然是字符数组和字符串变量之间的本质区别,先看两个例子:

例子1:

#include<stdio.h>  

int main()
{
	char *p;
	scanf_s("%s", p, 10);

	return 0;
}

例子2:

#include<stdio.h>  

int main()
{
	char p[10];
	scanf_s("%s", p, 10);

	return 0;
}

例子1运行错误,例子2运行成功。

原因是:如果程序中定义了一个字符数组,编译器就会为这个数组分配内存单元,数组中的每个元素都有明确的地址。但是定义一个字符串指针变量,编译器只会给这个指针变量分配存储空间,而不会为其指向的字符变量分配空间。也就是说,除非将字符串指针变量指向一个具体的字符变量或者字符数组,否则就会处于“悬空”的状态。


指向指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针,也称二级指针。

一般来说,声明指向指针的指针的形式如下:

存储类型 数据类型 **指针变量名;

指向指针的指针可以用于数组指针的应用,看一个例子:

#include <stdio.h>

int main()
{
	char* p[5] = { "Hello","World","How","Are","You" };
	char** pp;
	pp = p;
	int i;
	for (i = 0; i < 5; i++) {
		printf("%s %s\n", *pp, p[i]);
		pp++;
	}

	return 0;
}

这段程序的运行结果为:

Hello Hello
World World
How How
Are Are
You You
请按任意键继续. . .

p[5]是一个数组指针,数组的元素都是指针。p是数组名,数组名表示着数组的首地址,也就是数组首元素的地址,而数组首元素是一个指针。归根结底,数组名也就是指向指针的指针。所以pp=p,是没有问题的。

同时,*pp和p[i]都是一个指向char类型的指针,可以整体引用直接输出。

为什么不能用二级指针指向二维数组?

例子:

int a[2][3] = { 1,2,3,4,5,6 };
int** p;
p = a;                /* 错误 */

因为a是一个行指针,a+1指向下一行,也就是a每移动一下,移动12个字节;而p是一个二级指针,指向一个指针,p每移动一下,移动1个int*的字节大小。


指针与函数

由于函数名也表示函数在内存中的首地址,因此,指针也可以指向函数。函数指针就是指向函数的指针变量。

函数指针

函数指针就是指向一个函数的指针。函数在程序编译时被分配了一个入口地址,该入口地址就成为函数的指针。

函数指针的定义如下:

数据类型 (*函数指针名)(形参列表);
int (*f)(int,int);

其中:数据类型是函数的返回类型;(*函数指针名)的括号不能省去,如果省去了,代表函数的返回值是一个指针类型,这就成了指针函数了;形参列表只要标注数据类型即可。

函数指针变量常用的用途之一就是把指针作为参数传递给其他函数。指向函数的这孩子很也可以作为参数,以实现函数地址的传递,这样就能够在被调用的函数中使用实参函数。

函数指针的赋值和引用形式:

指针变量名 = 函数名;
f = fun;                /* 函数指针的赋值 */
(*f)(a,b);                /* 等价于调用函数fun(a,b); */

例子:

#include <stdio.h>

int max(int x, int y);

int main()
{
	int(*p)(int, int);
	p = max;
	int i;
	i = (*p)(2, 3);
	printf("%d\n", i);

	return 0;
}

int max(int x, int y) {
	int z;
	if (x > y)
		z = x;
	else
		z = y;
	return z;

}

这段程序的返回值为3。

指针函数

指针函数就是指函数的返回值类型是一个指针类型的函数,又称指针型函数。该函数的定义形式为:

类型名 *函数名(参数表列);

例子:

#include <stdio.h>

int* max(int a[], int* n);

int main()
{
	int a[5] = { 1,2,3,4,5 };
	int sum;
	int* value;
	value = max(a, &sum);
	printf("%d %d\n", *value, sum);

	return 0;
}

int* max(int a[], int* n) {
	int i;
	int* tem1 = &a[0];
	*n = a[0];
	for (i = 1; i < 5; i++) {
		if (a[i] > *tem1)
			*tem1 = a[i];
		*n += a[i];
	}
	return tem1;
}

这段程序的运行结果为:

5 15
请按任意键继续. . .

除了使用指针作为函数的返回值之外,我们还通过引用的方法再次返回给主函数,也就是说在函数的参数中也使用指针,在函数调用的时候,采用地址作为实参来传递。


void指针类型

在C语言中,可以声明指向void类型的指针,指向void类型的指针称为void指针。void在C语言中表示“无类型”,void指针则为无类型指针,void指针可以指向任何类型的数据。C语言中引入void指针的目的在于两个方面:一是对函数返回值的限定;二是对函数参数的限定。

C语言中void指针的定义格式为:

void* p;

上述定义表示指针变量p不指向一个确切的类型数据,其作用仅仅是用来存放一个地址。

一般来说只能用指向相同类型的指针给另一个指针赋值,而在不同类型的指针之间赋值是错误的。但void指针对于上述出现的错误而言是一个特例,C语言允许使用void指针,任何类型的指针都可以赋值给它,即不指定指针指向一个固定的类型。但如果需要将void指针的值赋给其他类型的指针,则还是需要进行强制类型转换。

int* p1;
void* p2;
p2 = p1;
p1 = (int*) p2;

void关键字在C语言中的注意事项:

  • 如果函数没有返回值,用声明为void类型。在C语言中,凡不加返回值类型限定的函数,就会被编译器默认返回整数型处理,包括main函数;
  • 如果函数不接受参数,应指明参数为void。在C语言中,可以给五参数的函数传送任意类型的参数,但若函数要不接受参数,应指明其参数为void;
  • 如果函数的参数为任意类型指针,应声明其参数为void*,即void指针型。void不能代表一个真实的变量,在C语言中定义一个void类型的变量是不合法的;
  • 不能对void指针进行算术运算,也就是说++,--运算都是不合法的;
  • void指针不等同于空指针,void指针是没有指向任何数据类型的指针,即其指向的是一块空白的内存区域;而空指针是指向NULL的指针,指向的是一个具体的区域。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/80205461