数据类型之指针

1、指针基础

C程序设计中使用指针,可以使程序简洁、紧凑、高效;有效地表示复杂的数据结构;实现动态分配内存;得到多于一个的函数返回值。
在计算机内部存储器(内存) 中,每一个字节单元,都有一个编号,称为地址。
内存单元的地址称为指针,专门用来存放地址的变量,称为指针变量。在不影响理解的情况下,对地址、指针、指针变量不区分,统称为指针。
由于现在大多数计算机都是32位的,也就是说地址的字宽是32位的,因此,指针也就是32位的。
在32位机上所有类型的指针都是32位(4个字节)

1.1指针变量的定义

类型说明符 * 变量名;
例: int * a;
其中“ * ”表示一个指针变量,a即为定义的指针变量名,int表示a指向一个整型变量。,至于a究竟指向哪一个整型变量,应由向a赋予的地址来决定。
注意事项:
1.指针变量名是“ * ”后面的内容,而不是*a。
2.虽然所有的指针变量都是等长的,但是仍需要定义指针的类型说明符。因为指针变量的其他操作(如加减运算)都涉及指针所指向变量的数据宽度。要注意的是,一个指针变量只能指向同类型的变量。

1.2指针变量的赋值

指针变量在使用前不仅要定义说明,而且还需要赋予具体的值,未经赋值的指针变量不能随便使用,否则将造成程序运行错误。指针变量的值只能是变量的地址,不能是其他数据,否则将引起错误。
在C语言中,变量的地址是由编译系统分配的,用户不知道变量的具体地址,但是,C语言中提供了地址运算符“ & ”,用来表示变量的地址,一般形式:&变量名,
如:&a,表示变量a的地址。
同时对指针赋值:

int i,*a=&i;

int i,*a;
a=&i; 

这两种的方式都可以;这里需要注意的是,指针变量只能存放地址,而不能将一个整型数据赋值给指针变量(NULL除外);

1.3指针变量的引用

指针指向的内存区域中的数据称为指针的目标。如果它指向的区域是程序中的一个变量的内存空间,则这个变量称为指针的目标变量。指针的目标变量简称为指针的目标。对指针目标的控制,需要用到下面两个运算符:

  • &———取地址运算符
  • *———指针运算符(间接存取运算符)。
    这两个运算符互为逆操作,例如“&a”就是取变量a的地址。而“b”就是取指针变量所指向的存储单元里的内容。通过一个指针访问它所指向的对象的值称为变量的间接访问(通过操作符“”);
    一个指定类型的指针变量通过引用后可以表示相应类型的变量。
    例:
int a,*b=&a;
a=30;
*b=20;
//这时变量a也等于20了。

小结:int *p;

p——指针变量,它的内容是地址;
*p——指针所指向的对象,它的内容是数据;
&p——指针变量占用的存储区域的地址,是个常量。

2、指针的运算

指针运算是以指针变量所存放的值(地址量)作为运算量而进行运算的。因此,指针运算实质就是地址的运算
指针的运算种类是有限的,它只能进行算术运算、关系运算、赋值运算。

2.1指针的算术运算

运算符计算形式意义
+p+n指针向地址大的方向移动n个数据
-p-n指针向地址小的方向移动n个数据
++p++或++p指针向地址大的方向移动1个数据
p–或--p指针向地址小的方向移动1个数据
-p-q两个指针之间相隔数据元素的个数

不同数据类型的两个指针之间的减运算是没有意义的。指针间的加法运算也是无意义的。

2.3指针的关系运算

指针之间的关系运算,表示它们指向的地址之间的关系预算,指向地址大的指针大于指向地址小的指针。

运算符说明例子
>大于p>q
<小于p<q
>=大于等于p>=q
<=小于等于p<=q
!=不等于p !=q
==等于p==q

注意事项:
(1)具有不同数据类型的指针之间的关系运算没有意义,指向不同数据区域的指针的关系运算也没有意义。
(2)指针与一般整数变量之间的关系运算没有意义,但可以和零进行等于或不等于的关系运算,用来判断指针是否为空。

3、指针与数组

3.1指针与一维数组

具体就不多加文字赘述,用一张表格体现的更清楚。

指针操作数组操作说明
array&array[0]数组首地址
*arrayarray[0]数组首元素
array+i&array[i]数组第i个元素的地址
*(array+i)array[i]数组第i个元素
*array+barray[0]+b数组首元素的值加b
*(array+i) +barray[i] +b数组第i个元素加b
*array++(指向第i个元素,下同)array[i++]先取得第i个元素的值,再i+1
*++arrayarray[++i]先i+1,再取第i个元素的值
*array–array[i–]先取得第i个元素的值,再i-1
*–array&array[–i]先i-1,再取第i个元素的值

3.2 指针与多维数组

多维数组其实就是具有两个或者两个以上下标的数组,实际上,在C语言中并没有多维数组的概念,多维数组就是低维数组的组合,依然可以理解为若干个数据类型相同的变量的集合。这里主要记录二维数组。
在C语言中,二维数组的元素连续存储,按行优先存,存储了第一行的元素,存第二行的元素,依次类推,基于这个特点,可以用一级指针来访问二维数组。

3.2.1列指针遍历二维数组

示例程序:

//用指针遍历二维数组
#include<stdio.h>
int main()
{
	int a[][3]={9,1,4,2,3,6},i,j,r,c,n,*p;
	
	p=&a[0][0];
	r=sizeof(a)/sizeof(a[0]);//算出二维数组的行数
	c=sizeof(a[0])/sizeof(int);//算出一行有多少个元素
	n=sizeof(a)/sizeof(int);//算出总共有多少个元素
	
	for(i=0;i<r;i++)
	{
		for(j=0;j<c;j++)
		{
			printf("%d    %p\n",a[i][j],&a[i][j]);
		}
	}
	printf("\n");
	for(i=0;i<n;i++)
	{
		printf("%d    %p\n",*(p+i),p+i);
	}
	return 0;
}

由于一级指针p,p+1移动了i个数,相当于移动了i列,因此也称指针p为列指针,该程序就是用来列指针对二维数组进行了遍历。

3.2.2行指针遍历二维数组

从内存管理角度而言,二维数组的储存方式与一维数组的储存方式是一样的,都是连续存储,因此可以用一级指针循环遍历二维数组的所有元素。
换一个角度来理解二维数组,把二维数组看作由多个一维数组组成,比如inta[2][3],可以理解为含有两个特殊的元素,a[0]、a[1]。元素a[0]是一个一维数组名含有a[0][1]、a[0][1]、a[0][2],三个元素,即二维数组的第一行,a[1]同上。
二维数组名代表了数组的起始地址,数组名加 1 ,是移动一行的元素。如inta[2][3],a+1就代表了第二行的首地址即a1
下面,来理解一下行指针:
问题1:数组int a[2][3],如何表示第二行第二列的元素地址。
答:&a[1][1], &a[1][0]+1, a[1]+1 和 (a+1)+1。
这里(a+1)+1有点绕,需要好好理解。

问题2:数组int a[2][3],如何表示第二行第二列的元素。
答:a[1][1], * (&a[1][0]+1), * (a[1]+1) 和 * (*(a+1)+1)。
重点:
确定指针偏移量“1”所代表的单位是通过“1”之前的元素单位来定的。在二维数组中,当偏移量前的元素单位是整个数组时,偏移加粗样式量单位为行;当偏移量前的元素单位是行时,偏移量单位为行中的元素
这里多加一点赘述,行指针 int (*a)[10],其实就是数组指针,指向的是一行数组,这一行数组里有10个元素。
二维数组名和行指针在表达形式上很相似,但有本质的区别,二维数组名是地址常量,指针是变量。

4、多级指针

4.1多级指针的定义与引用

我们把一个指向指针变量的指针变量,称为多级指针。对于指向处理数据的指针变量称为一级指针变量,简称一级指针,而把指向一级指针变量的指针称为二级指针变量,简称二级指针,以此类推三级指针、四级指针…
二级指针变量定义:int **p;
二级指针变量的引用:

int m=100,*p,**q;
p=&m;
q=&p;
printf("%d,%d%,%d\n",m,*p,**q);

结果可想而知都是100。

4.2多级指针的运算

前面已经提到过指针的运算,指针变量加一,是向地址大的方向移动一个数据。这里的数据是指指针的目标变量。类似的道理,多级指针运算也是以其目标变量为单位进行偏移,比如 int * *p,p+1移动一个int *变量所占的内存空间,再比如int ***p,p+1移动一个int * *所占的内存空间。

5、const与指针

在C语言中,关键字const修饰变量,可以使变量常量化。const修饰简单类型(非指针)时,很容易理解,就是变量的值不允许修改。
例:

int const a=10;
const int a=10;

上面两种写法都是允许的,变量a有const修饰,则a的值不能修改。
若用const修饰指针变量,会使指针变量常量化,但const所处的位置不同意义不同,根据const位置不同,分出下面3种情况。
(1)常量化指针目标表达式
常量化指针目标是限制通过指针改变其目标的数值。一般说明形式如下:
const int * p(=指针运算表达式);

int m=100,n=10;
const int *p;
p=&m;
*p=20;//这里是不允许的。
m=200;//这里是允许的
p=&n;//这里是可以的

(2)常量化指针变量
常量化指针变量,使得指针变量存储的地址值不能改变
。一般说明形式如下:int * const p = 指针运算表达式;
这里需要注意的是需要在定义指针变量的时候就将其赋值,不然该指针就已经被常量化,不能再赋值。

int m=100,n=10;
int *const p=&m;
p=&n;//这里是不允许的
*p=10;//这里是可以的

(3)常量化指针变量及目标表达式
常量化指针变量及其目标表达式,使得既不可以修改指针变量的指向的地址,也不可以通过 *p 修改指针所指向变量的值。
一般说明形式:const int * const a=&m;

6、void指针与空指针

6.1 void指针

void型的指针变量是一种不确定数据类型的指针变量,它可以通过强制类型转换让该变量指向任何数据类型的变量或数组。
一般形式为:void * <指针变量名称>;
对于void型的指针变量,实际使用时,一般需通过强制类型转换才能使void型指针变量得到具体变量或数组地址。在没有强制类型转换之前,void型指针变量不能进行任何指针的算术运算。

int m=10void *p;
p=(void *)&m;
printf("%d %d %p\n",m,*(int *)p,p,++m);

在这里补充一下知识点:printf里最好不要有运算,不同的编译器对printf的处理方法存在差异,在linux里如果printf里有运算,会自右向左进行,有运算的先运算再打印,没运算的直接打印。

6.2 空指针

这里说的空指针,指的是指针变量存了零号地址(一般来说,零号地址约定位指针变量无效的地址,在定义指针变量时,将指针变量设置成零地址表示这个指针变量没有储存有效指针,在使用指针之前检查指针变量是否为0(NULL)则可以判断此指针是否可以操作),在程序中可以为指针赋零,如:int *p=0;或者 int *p=NULL;。在C语言中指针常量只有NULL一个。那么,NULL究竟代表什么意义呢?
C语言标准中定义了一个NULL指针,其就代表了0。在实际编程中,NULL指针的使用是非常普遍的,因为它可以用来表明一个指针目前并未指向任何对象。
注意事项:
将指针设置为NULL,这是一个好的习惯,但不可以对空指针进行间接引用操作,这样的操作是非法的。

7、字符指针

字符指针就是储存字符变量的地址,在这里单独提出字符指针,是为了讲解字符指针在处理字符串及字符指针数组时的用法。

7.1 字符串

在原来的博客中已经说明了,在C语言中没有字符串这个数据类型。通常借助字符数组来储存字符串。字符指针可以储存字符串的起始地址,即指针指向字符串的第一个字符,这样我们就可以用指针来处理字符串。
(1)字符指针指向字符数组
用字符指针来实现字符串的反转,示例程序如下:

#include<stdio.h>
#incude<string.h>
int main()
{
	char s[10];
	char *p,*q,t;
	printf("Please input a string:");
	scanf("%s",s);
	p=s;
	q=s+strlen(s)-1;
	while(p<q)
	{	
		t=*p;
		*p=*q;
		*q=t;
		p++;
		q--;
	}
	printf("%s\n",s);
	return 0;
}

在使用字符指针指向字符数组时,有一下两点注意事项
1)虽然数组名也表示数组的首地址,但由于数组名为指针常量,其值是不能改变的(不能自加、自减、赋值等操作)。如果把字符数组的首地址赋值给一个字符指针,就可以移动这个指针来访问或修改数组中的字符。
2)在使用scanf时,其参数前要加上&取地址符,表明是一个地址。如果指针变量的值已经是一个字符数组的首地址,那么可以直接把指针数组作为参数而不用再加上取地址符&。(不能对没有指向数组首地址的指针用scanf,会出现段错误,非法)

(2)字符指针初始化直接赋值字符串首地址
初始化字符指针时,可以把内存中字符串的首地址赋予指针,这里并不是把该字符串复制到指针中,而是让指针指向字符串的起始地址。比如:
char *s=“Welcome!”;
这里指针变量s的值是“Welcome!”的首地址,而字符串本身储存在其他的地方(这里涉及到的储存方式,会有一篇文章来总结)。
提出一个问题:“(*s)++ ” 会出现什么现象?
答案是会出现段错误,**当一个字符指针初始化为指向一个字符串常量时,不能对字符指针变量的目标赋值。**从这个角度看,这里的
char *s="Welcom!"相当于const char *s=”Welcom!“。

7.2字符指针数组

若数组中储存了若干个字符串地址,则这样的数组就叫做字符指针数组。
对于字符指针数组用下面的示例代码来理解更通俗易懂

#include<stdio.h>
#include<string.h>
int main()
{
	char s1[]="Welcome";
	char s2[]="to";
	char s3[]="China";
	
	char *a1[3]={s1,s2,s3};
	char *a2[3]={"Welcome","to","China"};
	char **p;
	int i;
	p=a1;
	printf("array1:%s %s %s\n",a1[0],a1[1],a1[2]);

	for(i=0;i<sizeof(a1)/sizeof(char *);i++)
		printf("%s ",*(p+i));
	printf("\n");
	
	p=a2;
	printf("array2:%s %s %s\n",a2[0],a2[1],a2[2]);
	
	for(i=0;i<sizeof(a2)/sizeof(char *);i++)
		printf("%s ",*(p+i));
	printf("\n");
	
	return 0;
}

运行结果如下:
在这里插入图片描述在该程序中数组a1就是一个字符指针数组,分别储存了字符数组s1、s2、s3的起始地址。类似的数组a2也是一个字符指针数组,含有3个字符指针,分别指向了 “Welcome”、“to”、"China"三个常量字符串。
需要注意的是字符指针数组的数组名。它代表了数组的起始地址,即代表了第一个元素的地址,这里数组第一个元素已经是字符类型的指针了,所以元素的地址就是二级指针了,因此在该程序中,使用二级指针p来对字符指针数组进行了遍历。

8、野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。指针变量在定义时如果未初始化,其值时随机的,而指针变量的值是别的变量的地址,这就意味着指针指向了一个不确定的变量,此时取引用就是去访问了一个不确定的地址,所以结果是不可预知的。
野指针的成因:
(1)指针变量未初始化
-------任何指针变量刚被创建时不会自动成为NULL指针,它的值是缺省的,会乱指一气。所以在创建的同时应当初始化指针或者设置为NULL。
(2)指针释放后未置空
-------有时指针在free或delete后未赋值NULL。free和delete它们只是将指针所指的内存释放掉,但并没有把指针本身干掉,此时指针指向的就是“垃圾内存”。释放内存后应当立即为指针置NULL,防止“野指针”的产生。
(3)指针操作超越变量作用域
--------不要返回指向栈内存的指针或应用,因为栈内存会在函数结束后会被释放。

总结

在指针这一篇中,总结的比较仔细,基本上是照到我的教材抄了,因为我觉得指针是比较复杂的,如果用自己的语言来描述指针,估计会出很多错误,后来在写这篇文章的时候越发证实了这一点,也很庆幸自己选了这一种方式,在“抄课本”的时候发现了许多细节,并且学到了一写原来不知道的知识点。继续加油!!!!!!!!!!!!!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值