文章目录
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] | 数组首地址 |
*array | array[0] | 数组首元素 |
array+i | &array[i] | 数组第i个元素的地址 |
*(array+i) | array[i] | 数组第i个元素 |
*array+b | array[0]+b | 数组首元素的值加b |
*(array+i) +b | array[i] +b | 数组第i个元素加b |
*array++(指向第i个元素,下同) | array[i++] | 先取得第i个元素的值,再i+1 |
*++array | array[++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=10;
void *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)指针操作超越变量作用域
--------不要返回指向栈内存的指针或应用,因为栈内存会在函数结束后会被释放。
总结
在指针这一篇中,总结的比较仔细,基本上是照到我的教材抄了,因为我觉得指针是比较复杂的,如果用自己的语言来描述指针,估计会出很多错误,后来在写这篇文章的时候越发证实了这一点,也很庆幸自己选了这一种方式,在“抄课本”的时候发现了许多细节,并且学到了一写原来不知道的知识点。继续加油!!!!!!!!!!!!!