一、何为指针?
首先我们有了解以下预备知识
(1) 内存:内存是顺序编号的一些列存储单元组成的,在内存中,每个存储单元都赋予为敌的编号(地址),计算机通过地址可以方便的在内存单元中存储数据。
(2) 可以将函数名通过%x打印出该函数的地址。
(3) 一个程序载入内存,代码和数据都有地址编号。
(4) 变量名就是对内存一段空间里面数据的抽象,变量的类型决定了读取的字节数。
指针:相当于门牌号码,而每一个号码对应一户人家,由此对应到计算机就是指针是保存着某个变量的地址,我们可以通过这个地址去寻找到这个地址下对应的内存单元中的数据;因此指针变量就是这么个作用,保存某个变量的地址编号,然后我们可以由此找到我们想要的数据。
指针的定义格式
Type var;
Type *varpointer=&var
注意,指针变量一定要初始化,要不就取某个变量的地址,要不就初始化为NULL。因为我们声明一个指针变量时编译器不会自动对指针初始化,由此指针的值是不确定的,因而指针所指向的那块内存单元是完全随机的。因此为了避免不必要的麻烦,我们必须初始化指针变量。
(5) 指针变量可以指向任何一个变量
Int x=10;
Int *p=&x; //p可以是任何变量的地址
(6) 指针和地址的区别
两个方面:
一,指针是以变量,对应着一块内存区域
二,指针存储的是某个内存单元的地址
如:
Int num=10;
Int *p=#
&num是一个地址,是一个常量,而p是一个指针变量,可以存储一个地址
再如:
Int *p=(int *)350000是一个指针,p存储的是地址,指针有类型,二者和一块就可以知晓从哪里开始读取长度为多少的数据,得知类型之后,便得知这一片内存数据时如何解析的。
(7) 空指针
也就是NULL,意味着指针并不指向任何地址。在头文件中stdio.h定义为
#define NULL ((void *)0)
(8)直接访问与间接访问
1,取地址符&,和间接运算符*
&取地址运算符,&m即取m在内存中的实际地址
*指针运算符(又称为:间接引用运算符),它返回其操作数(即一个指针)多指向的对象的值。细说便是将作用于当前的指针变量存储的地址中的数据取出。
Int num=20;
Printf(“%d,%x”,num,&num); 直接调用
Int *p=#
*(&num)=100; //根据地址写入内容
Printf(“%d,%x”,num,&num);
直接访问:按照变量的地址取出变量的值;
间接访问:通过存放变量的地址的变量去访问变量;
(9)打印指针的地址 %x,%p
%x:以十六进制打印数字,无意义的0将不被打印
%p:显示地址的位数,32位情况有32/4=8个十六进制数,64位有16个十六进制位
int num = 10;
int*p = #
printf("hexfromat is %x,pointer format is %p", p,p);
32位下
64bit
(10)指针类型和指针所指向的类型
1,指针类型是指声明指针变量是位于指针变量前的(类型*),而指针所指向的类型是指为指针初始化或者复制的变量类型
2,不是同一类型的指针,不可以任意赋值
数据类型不同,大小不一样,如果强行赋值的话,就会读取不完整,解析的方式也不一样;例如整形和浮点的存储方式不一样,因此在读取对应的数据时,二者的解析方式是有所不同的。
如 char *pchar1,*pchar2;
Int *pint;
Char c=’a’
int x=100;
pchar1=&c;
pchar2=pchar1; //正确, pchar1,pchar2是同类型指针,解析pchar2没什么问题
pint=&c //错误 二者不是同一类型的指针,int读取2个word而char读取一个byte
3,同类型指针赋值的意义在于,指向同一块内存区域。二者之一修改数据,都会影响另一个读取
(11)指针变量的值
是指指针本身存贮的数值,从上面的介绍到现在,我们知道 指针变量的值是对于某个变量的地址.而不是一般的数值。在32bit的程序中,所有类型的指针都是一个32bit整数,因为32bit程序里内存地址长度为32Bit
指针所指向的内存区:就是从指针的值所代表的的那一个内存地址开始,sizeof(指针指向的类型)的一片内存区。由此你会发现上面的图例有错误,假设有
Int num=10;
Int *p=#
那么应该是这样的:
(详细见:C语言深度剖析 p76)
当然这里涉及大小端模式问题,因此为方便起见我们使用小端法(高地址存高位,低地址存低位);p是占有4字节的指针。假设&num是100(D),那么p=&num就是p=64H,由于64H小于28,所以在105号地址(&p)的存储单元就足够存储64H,接着往上的三个字节补零即可。
回到我们的话题,指针变量的值,说明的是指针的的内容à所指向变量的地址。如上图,便是100.
(12)指针的运算
作为一种特殊的运算,指针可以进行一些运算,但是并非所有运算都是合法的,指针运算的局限性在于加减算术和其他一些为数不多的运算。
情景1:把a的值5作为地址0x00000005H赋值给*p是发生访问冲突。整数和指针最好不要直接运算。
Inta=5;
Int*p=a;
*P=3;
再说明一点就是在指针声明时,*只有指示作用,指示说p是一个指针,然后类型Int说明它占用4字节(32bIt下),a是一个int类型变量,与int*变量是不同的。因此在赋值过程中会报错
原因在于
Int*p=a相当于int *p=5,p=5,接下来又想要向5号地址单元里写入数据3,而前面的内存是操作系统所占用的,一般用户程序是不能对其进行修改的,所以在此出错
情景2:指针的算术运算
1, 使用自增,自减运算符
指针++就是按照指针类型的大小,前进一个类型的大小,int前进4个字节,
指针++和—在数组内部才有意义
inta = 5;
int *p = &a;
printf("%d %x\n", *p, p);
p++;
printf("%d %x", *p,p);
上面例子变量a的地址为CFFA0CH,也就是p的初值,后来p++之后为CFFA10,两者相隔4个单元,p++运算之后结果相当于是 p+sizeof(int),所以指向了未知区域的数据所以打印了垃圾数据。
有了上面的基础,我们回顾来看数组int a[5]={0,1,2,3,4};
int a[5] = { 0, 1, 2, 3, 4 };
int *p = a;
int i = 0;
for (i = 0; i < 5; i++){
printf("addr of a[%d] is%p\n", i,&a[i]);
}
每个元素首地址相差4,这也是有*(a+i)和*(p+i)的操作原理,它们的意义都在于a+i*sizeof(type)个字节,也即是以数组首元素地址为基址向后偏移i*sizeof(type)个单位,每sizeof(type)都是一个数组元素。
通过上述解析,我们可以得到指针版的遍历数组
int j = 0;
for (p; p != &a[5]; p++){
printf("addr of a[%d] is %p\n", j++, p);
}
其中j是为了打印下标的,p初始为a,即数组首元素的地址;接着不断后移,直到与&a[5]一样结束循环。为什么是p!=&a[5],这种写法是允许的,在 c和指针一书中有提到,此处不赘述。但是要注意a[5]这个元素是未知的!为什么?因为声明数组a[5]有5个元素,下标只有0-4.a[5]这个元素是未知的,不在我们申请的数组内存内,但是由于数组是连续的地址单元,引用它的地址是可以的。
注:以上的sizeof(type)也成为步长。
由上面我们可以总结出:一个指针变量加减一个整数,相当于以此指针变量的内容为基准,向后移动sizeof(type)*整数值个单元。
在此引入 C语言深度剖析的一段内容
先看下面这个例子:
structTest
{
int Num;
char *pcName;
shortsDate;
char cha[2];
short sBa[4];
}*p;
假设 p 的值为 0x100000。 如下表表达式的值分别为多少?
p + 0x1 =0x___ ?
(unsigned long)p + 0x1 = 0x___?
(unsigned int*)p + 0x1 = 0x___?
我相信会有很多人一开始没看明白这个问题是什么意思。其实我们再仔细看看,这个知识点
似曾相识。一个指针变量与一个整数相加减,到底该怎么解析呢?
还记得前面我们的表达式“a+1”与“&a+1”之间的区别吗?其实这里也一样。指针变
量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是
byte 而是元素的个数。所以:
p + 0x1 的值为 0x100000+sizof(Test) *0x1。至于此结构体的大小为 20byte,前面的章节已经详细讲解过。所以 p + 0x1 的值为: 0x100014。(unsigned long)p +0x1 的值呢?这里涉及到强制转换,将指针变量p 保存的值强制转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。(unsigned int*)p +0x1 的值呢?这里的p 被强制转换成一个指向无符号整型的指针所以其值为:0x100000+sizof(unsigned int) *0x1,等于 0x100004。
上面的例子是加深了以上的指针算术运算的说明,请细细品味。
接着我们要说明一下数组首元素的地址和数组的首地址,二者是不同的概念!
Int a[5]={0,1,2,3,4};
Int *p=a; (1)
Int *q=&a; (2)
看(2)int*p=a,a和a+0一样表示数组首元素的地址,一个数组的数组名就是这个数组首元素的地址。这里a的步长为4字节即sizeof(a[0]);再看int *q=&a,这里q保存的是数组的首地址,步长为:sizeof(a)=a的元素个数*sizeof(int)。我们编程查看不同
printf("addr of a= %p,the a+1 is %p\n", a,a+1);
printf("addr of &a=%p &a+1= %p",&a,&a+1);
F8H-E4H=20H,20H/4=5个int步长。
再次延伸:
在 x86 系统下,其值为多少?
int main()
{
int a[4]={1,2,3,4};
int *ptr1=(int *)(&a+1);
int *ptr2=(int *)((int)a+1);
printf("%x,%x",ptr1[-1],*ptr2);
return 0;
}
下面就来分析分析这个问题:根据上面的讲解, &a+1 与 a+1 的区别已经清楚。
ptr1:将&a+1 的值强制转换成 int*类型,赋值给 int* 类型的变量 ptr, ptr1 肯定指到数
组 a 的下一个 int 类型数据了。 ptr1[-1]被解析成*(ptr1-1),即 ptr1 往后退 4 个 byte。所以其值为 0x4。ptr2:按照上面的讲解, (int)a+1 的值是元素 a[0]的第二个字节的地址。然后把这个地址强制转换成 int*类型的值赋给 ptr2,也就是说*ptr2 的值应该为元素 a[0]的第二个字节开始的连续 4 个 byte 的内容。
(来源:C语言深度剖析)
2.前置后置++对于指针运算的影响
若有int a[5]={0,1,2,3,4};
Int *p=a;
P++(or p+=1)表示指针p指向下一个元素
*p++:与*(p++)等价。同级优先级,结合方向右到左。
*(p++)和*(++p):前者是取*p的值,后使得p后移一个步长(sizeof(int));后者先是然p指向下一个元素,再取出*P的值。
(*p)++表示p指向的元素+1,并非指针加1(括号优先级高,先算括号内的再算括号外的)
3指针相减:
指针相减返回的是个有符号整数,减法一般都有计算距离的意思,但是两个指针的距离并不是两个指针值的简单做差,还有除以指针所指类型占用的字节数(一个类型步长),意味着指针之间相隔了几个元素的大小。指针相减在数组里可以判断出下标。
int *q = &a[3];
printf("distance of a[0] and a[3] has %d int word", q - p);
总结:指针加减法在非数组内部没有任何意义,而且容易越界报错,一个可执行程序不能读写其他程序的内存。
4.指针之间的比较
1.对于两个毫无关联的指针比较大小是无意义的,因为指针只代表了位置这么一个信息,但是如两个指针所指向的元素位于同一个数组(或者同一块动态申请的内存中),指针的大小反映了指针在数组中的先后顺序。
2.对于指针的比较,只能判断指针的先后顺序
If(p1>p2){
Puts(“p1在前面”);
}else{
Puts(“p2在后面”);
}
3.对于指针是否相等ß--->是否指向同一内存空间。
If(p1==p2){
Pust(“pointthe same mem”);
}else{
Puts(“point different mem”);
}
以及经常使用的判断指针是否为空
If(p->next!==NULL){
statements
}else{
Statements;
}
5.指向元素的指针和指向数组的指针
若有定义int(*p)[3];则定义了一个名为p的指针变量,表示p是一个指针变量,它可以指向每行有三个整数(即int型)元素的二维数组.p是指向一维数组的指针变量。这句话的理解是首先(*p)[3]是一个指向一维数组的指针变量,意思就是p这个指针是指向一个含有3个元素的数组的,那么p指针每一次加1就相当于把p中存的地址加12.举个例子:int a[3][3]; int( *p)[3]; p =a; //p=a的意思是把数组a的首元素地址存放到p中那么p[1]就是a[1][0]的地址,p[1][0]就等于a[1][0],而p[1][2]就等于a[1][2].
前面对于&a的赋值操作便是如此,原因在于int a[5],是含有5的元素的int类型数组,因此&a相当于取数组a的首地址,步长为sizeof(a)=5*sizeof(int)=20BYTE,因此如果使用int *r=&a的话,两边的“类型”是不一样的
,至少你现在知道它们步长不一样。所以必须要int (*r)[5]=&a,这样两边“类型”才一致,步调才一致。由此再到二维数组a[3][3],p=a,p取到的是a+0这一列,因此有p+1正确的步长为p加上3个int长度。即a[1][0]第地址。
6.指针引用多维数组
Inta[3][4]={0,1,2,3,4,5,6,7,8,9,10,11,12};
Printf”%p %p%p”,a,&a,*a);
首先a是一个步长为4*4=16字节的指针,指向a[3][4]的第0行;
&a是一个二维数组指针,步长为sizeof(a)=3*4*4=64BYTE
*a是一个int *的指针,相当于
小测试
&a[0][0],
综上所说可以得到二维数组的指针访问
int arr[3][3] = {0, 1, 2 , 3, 4, 5 , 6, 7, 8 };
int (*p4)[3] = arr;
//int j = 0;
for (i = 0; i < 3; i++){
for (j = 0; j <3; j++)
//printf("a[%d][%d]=%d\n",i, j, arr[i][j]);
//printf("a[%d][%d]=%d\n",i, j, *(*(arr+i)+j));
// printf("a[%d][%d]=%d\n", i, j,*(arr[i] + j));
//以下为指针访问
//printf("arr[%d][%d]=%d\n",i, j, *(*(p4 + i) + j));
//printf("arr[%d][%d]=%d\n",i, j, *(*p4 + i + j));
// printf("arr[%d][%d]=%d\n", i, j,*(p4[i] + j));
printf("arr[%d][%d]=%d\n",i, j, *(*arr+i + j));
}
测试结果
int (*p4)[3] =arr;
//int j = 0;
for (i = 0; i < 3; i++){
for (j = 0; j <3; j++){
printf("a[%d][%d]=%d\n ", i, j, arr[i][j]);
printf(" addr=%p\n", &arr[i][j]);
//printf("a[%d][%d]=%d\n",i, j, *(*(arr+i)+j));
// printf("a[%d][%d]=%d\n", i, j,*(arr[i] + j));
//printf("arr[%d][%d]=%d\n",i, j, *(*(p4 + i) + j));
//printf("arr[%d][%d]=%d\n",i, j, *(*p4 + i + j));
// printf("arr[%d][%d]=%d\n", i, j,*(p4[i] + j));
// printf("arr[%d][%d]=%d\n", i, j,*(*arr + i + j));
//printf("%d",sizeof(arr));
}
}
printf("*p4=%p arr=%parr+1=%p", *p4, arr,arr+1);
对于二维数组:
A[i][i] 等价于 *(*(a+i)+j) &a[i][j] 等价于 *(a+i)+j a[i] 等价于 *(a+i)
7.数组做函数参数
1、用指向数组的指针作函数参数
一维数组名可以作为函数参数,多维数组名也可作函数参数。
用指针变量作形参,以接受实参数组名传递来的地址。
可以有两种方法:
①一维数组用指向变量的指针变量
②二维数组用指向一维数组的指针变量
(1)一维数组用指向变量的指针变量
int a[10] 数组作为函数参数,传递的是地址,地址就是指针占4个字节,
函数的参数对于数组没有副本机制,为了节约内存,拷贝数组浪费空间与CPU。
Void fun1(inta[10]){ }
指针变量作为一维数组的参数
Void fun2(int *p)( )
(2)二维数组指向一维数组的指针变量
Void fun3(int(*p)[4]){ }
8.函数指针
意为:指向函数入口地址的指针。
由来:在程序中定义的函数在编译时,编译系统会为函数代码分配一段存储空间,这段存储空间的起始地址成为该函数的函数指针。
可以定义一个指向函数的指针变量,用于存放某一个函数的起始地址,这意味着此指针变量指向该函数。如
Int (*p)(int ,int);
定义的首先是一个函数,再有需要一个指针变量,这个指针变量指向类型为int的有两个Int参数的函数地址。P的类型可以用int (*)(int,int)表示
举例:
Void go(){
Puts(“hello”);
}
方式一:void (*p)()=go;
P();
方式二: void (*p)();
P=go;
注意定义的函数指针的参数表和返回值与要指向函数保持一致
9.返回值是指针
一个函数可以返回一个整型值、字符值、实型值等,也可以返回指针型的数据,即地址。其概念与以前类似,只是返回的值的类型是指针类型而已
定义返回指针值的函数的一般形式为
类型名 *函数名(参数表列);
如:
Define globle varies
Int a=0;
int* (void){
return &a;
}
10.void*指针
void *指针是一种特殊的指针,不指向任何类型的数据,如果需要用此地址指向某类型的数据,应先对地址进行类型转换。可以在程序中进行显式的类型转换,也可以由编译系统自动进行隐式转换。
如malloc函数的声明
Malloc函数返回一个void*类型的指针,因此我们使用的时候一般都要进行强制类型转换。
如:int *p=(int *)malloc(sizeof(int));
其一般用于参数和返回值,不明确指针类型的情况传递指针如memset何memcopy函数。
ANSI认为Void*指针不支持算术运算 因此void*p, p++是不合法的 .见 C语言深度剖析34
Void*指针可以指向任何类型的数据,包括它们的地址。
char ch = 'a';
int num = 100;
double db = 10.9;
void *p;
p = &ch;
p = #
p = &db;
printf("%lf",*(double *)p); //在使用之前需要对其进行类型明确
上面的例子还说明:任何指针都可以赋值给void*指针,用于地址保存。
经历上面的种种经历,你是否发现指针包含了哪些信息?
没错:地址,步长,还有解析方式
参考资料《C语言深度剖析》,《深入理解计算机系统》
如有异议请联系我,改正或者删除,谢谢