首先,指针实际上的含义就是储存内存空间首地址的变量
我认为理解指针要从他本身的内存空间和他指向内存空间的含义来理解
还要从一个变量名做左值与右值来理解(在本文请多多运用左值与右值的概念看代码)
这里引用一下在网上查到的资料
左值与右值(1)
放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值 = 右值;
(2)当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间;当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。
(3)左值与右值的区别,就好象现实生活中“家”这个字的含义。譬如“我回家了”,这里面的家指的是你家的房子(类似于左值);但是说“家比事业重要”,这时候的家指的是家人(家人就是住在家所对应的那个房子里面的人,类似于右值)
指针本身的空间是储存的二进制的作用是描述一个无符号整形数,而这个无符号整形数的含义是描述指针所指向内存空间的首地址
我们来看看指针的sizeof
#include <stdio.h>
int main()
{
printf("int * = %d\n", sizeof(int *));
printf("short * = %d\n", sizeof(short *));
printf("long * = %d\n", sizeof(long *));
printf("double * = %d\n", sizeof(double *));
printf("float * = %d\n", sizeof(float *));
printf("char * = %d\n", sizeof(char *));
return 0;
}
打印后的结果
在我的机器上是这样的,不同机器可能不同,建议自己去试试
关于地址的类型
(type * )类型 (既(int * )(float *) (double * )这些)
表示以此地址值(指针变量所储存的值)为首地址的内存空间(即指针访问的内存空间)中储放的是一个或者多个type类型的数。
举个例子
例1
int a = 1;
int * pa = &a;
上面第一行代码的含义是 首先申请一个int类型的空间,这个空间储存了sizeof(int)个字节的二进制, 储存的二进制的作用是描述一个int类型的元素,这个空间的名字是a, 然后将 1 这个int类型的元素赋值给a这个空间。
同理 ,第二行代码的含义是 首先申请一个int *类型的空间, 这个空间储存了sizeof(int *)个字节的二进制, 储存的二进制的作用是描述一个无符号整形数(既指针变量), 而这个无符号整形数的含义是描述指针所指向内存空间的首地址, 然后在声明这个指针所指向的空间储存的是一个int类型的元素,这个空间的名字是 pa,然后将a这个空间的首地址&a 赋值给pa ,而a这个空间储存的是是一个int类型的元素。
例2
int a[3] = {1, 2, 3};
int * pa = a; //一维数组的数组名描述的是这个数组中第一个元素的首地址 相当于&a[0]
上面第一行代码的含义是定义一个int类型的数组 ,申请3个int类型的空间用来储放这个数组的元素,这个内存空间的名字是a,
然后将1,2,3赋值到这个内存空间中,a作右值就相当于这个数组首元素的首地址, a是不能够做左值的,注意理解a与&a的区别,a与&a做右值的含义虽然一样,但是&a 是代表整个a[3]数组的首地址。
同理 ,第二行代码的含义是 首先申请一个int *类型的空间, 这个空间储存了sizeof(int *)个字节的二进制, 储存的二进制的作用是描述一个无符号整形数(既指针变量), 而这个无符号整形数的含义是描述指针所指向内存空间的首地址, 然后在声明这个指针所指向的空间储存的是一个int类型的元素 ,这个空间的名字是 pa,然后将a这个空间的首地址a 赋值给pa(注意这里不能将&a这个地址赋值给pa, 因为&a的类型是 int * [3], 而pa的类型是int *) ,而a这个空间储存的是是多个int类型的元素,这里是三个。
后面就不描述储存指针变量的内存空间了!!
(void *)类型
通用类型的地址,表示以此地址值(指针变量所储存的值)为首地址的内存空间(即指针访问的内存空间)中储放的二进制的作用暂时不确定。
举个例子
#include <stdio.h>
int main()
{
int a = 1, b = 0;
void * pa = &a;
*(int *)pa = 2; //做左值
b = *(int *)pa + 1; //做右值
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
运行结果
void * pa = &a的含义:申请一个viod 类型的空间,名字是pa ,指针指向的内存空间中储放的二进制的作用现在不知道,也就是这个指向的空间储存只是一串二进制码,他的含义是什么我们不知道,这个时候用访问运算符去访问pa是没有意义的,如果要访问就要想上述代码一样强制转化,指针的强制转化这里就不说了。
下面说说指针的运算
指针通常可以加上或者减去一个整数,指针的运算与普通的加减运算不一样,他的加减是以sizeof(type)为单位的。
举个例子
#include <stdio.h>
int main()
{
int a[5] = {0}; //初始化数组
int * pa = a; //使指针指向数组
int i = 0;
printf("----------------------\n");
for(i = 1; pa <= a + 4; pa++, i++) //a + 4表示数组中最后一个元素的首地址
{
*pa = i; //赋值
printf("%p ", pa); //打印每个元素的地址
}//循环结束后pa指向的空间已经不知道指向的空间的值是随机的 这就越界了
printf("\n last pa = %p", pa);//这个时候pa的值是不确定的 pa变成了野指针
printf("\n----------------------\n");
pa = a;//所以我们使其重新指向数组
for(i = 0; i < 5; i++)//打印数组元素
{
printf("%d ", *(pa + i));
}
return 0;
}
运行结果
所以的加减是以sizeof(type)为单位的,如上图,pa每加一,相当于储存指针的内存空间的值加了sizeof(int)也就是4个字节,
指针的运算要注意不要越界两个指针的值相加是没有意义的,我们不推荐2个指针相加,但是2个指针相减是有意义的,在一个连续的内存空间里,两个指针相减可以推出这个2个元素所间隔的位置。
**
然后我们用二维数组申请动态空间来加深理解
顺便理解一下取地址运算符&与访问运算符*
最好运算左值与右值的概念来理解这个
#include <stdio.h>
#include <stdlib.h>
void function1(int**);//分配空间只有二维数组中的一维数组连续
void function2(int**);//使整个二维数组空间连续
#define N 5 //二维数组第一维的元素个数
#define M 5 //二维数组第二维的元素个数
int *pa = NULL;
int main()
{
int **a = NULL;
int i = 0, j = 0;
a = (int **)malloc(sizeof(int *) * M); //分配指针数组
printf("_________分配后打印a申请的空间中储存的指针______________\n");
//分配后打印a申请的空间中储存的指针
for(i = 0; i < M; i++)
{
printf("%p\n", *(a + i));
}
printf("________________________________________\n");
function1(a); //分配空间只有二维数组中的一维数组连续
//function2(a); //使整个二维数组空间连续
//二维数组中的一维数组中元素的首地址
printf("____________二维数组中的一维数组中元素的首地址_____________\n");
printf(" %p\n", *(a) + 1);//相当于&a[0][1] 或者 a[0] + 1
printf(" %p\n", *(a) + 2);//相当于&a[0][2] 或者 a[0] + 2
printf("%p\n", &a[4][4]);//相当于*(a + 4) + 4 或者 a[4] + 4
//二维数组中储存的一维数组首地址
printf("_____________二维数组中储存的一维数组首地址________________\n");
printf(" %p\n", *a);//相当于a[0]
printf(" %p\n", *(a + 1));//相当于a[1]
printf("______________数组中的元素__________________\n");
//初始化数组元素
for(i = 0; i < M; i++)
{
for(j = 0; j < N; j++)
{
a[i][j] = i; //也可以写成*(*(a + i) + j) = i;
}
}
//打印数组元素
for(i = 0; i < M; i++)
{
for(j = 0; j < N; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
printf("____________打印数组中的元素的地址_______________\n");
//打印数组中的元素的地址
for(i = 0; i < M; i++)
{
for(j = 0; j < N; j++)
{
printf("%p\t", &a[i][j]);
}
printf("\n");
}
//释放内存
free(a[0]);
free(a);
a = NULL;
pa = NULL;
return 0;
}
void function1(int** a) //分配空间只有二维数组中的一维数组连续
{
int i = 0;
for(i = 0; i < M; i++)
{
*(a + i) = (int *)malloc(sizeof(int) *N);
}
}
void function2(int** a)//使整个二维数组空间连续
{
int i = 0;
a[0] = (int *)malloc(sizeof(int) * M * N);//一次性分配所有空间
printf("%p\n", a[i]);
for(i=1; i<M; i++)
{
a[i] = a[i-1] + N;//偏移N个元素 初始a[0][0] 偏移后a[1][0];其实就是逢N进1
pa = a[i];
//printf("%p\n", a[i]);
printf("pa%d = %p\n", i, pa);
}//采用如上内存分配方法,意味着将a的值初始化为m*n的二维数组首地址,且这块内存连续
}
用函数1的运行结果
首先申请二级指针的空间,然后发现这个空间的值是乱的 ,然后申请1级指针的空间,用a[1][0]的地址减去a[0][4]的地址发现发现二级指针的空间是不连续的
为了连续,我们使用函数2, 下面是函数2的运行结果
这个时候地址就连续了,没相邻的2个地址都相差sizeof(int)也就是4个字节,所以说数组的与指针是很像的,数组的下表运算符其实就相当于指针的偏移量
a[1] 相当于 *(a + 1)
这个时候指针一偏移相当于偏移了5乘以sizeof(int)个字节(偏移了一个一维数组所占空间个字节)- 如上图a[0] 的地址是007D2A00 而a[1]的地址是007D2A14
- 他们相减转化成10进值的值以后等于24字节在减去本身所占的4个字节等于20个字节
- 而a[0][0]与a[0][1]之间只有4个字节
运用这个上面的例子在来理解一下上文说的a 与 &a的关系
int* pb = *a;//这个代表上文中二维数组中的一个一维数组的首地址
这是时候 pb 相当于一维数组的首地址如上图007D2A00
pb + 1 后的地址007D2A04
也就是偏移4个字节
int** pb = a;//这个代表整个一维数组 相当于&(*a)
这是时候 pb 相当于一维数组的首地址如上图007D2A00
而pb + 1后的地址为007D2A14
一偏移就偏移1个数组 偏移20个字节
看下面代码
#include <stdio.h>
int main(int argc, char ** argv)
{
int arr[3] = {1, 2, 3};
int (*parr)[3] = &arr;
printf("%d\n", *(*parr + 1));//相当于打印了a[1]的值
//printf("%d\n", sizeof(int[3]));
//如果对parr做++运算 就会偏移12个字节 到一个未知可控的空间
//而对上文的pb做++运算是有意义的 直接偏移到下一个一维数组 因为他们空间是连续的
return 0;
}