c语言 数组指针(剖析 必看!)

这几天专心看了c语言的指针和数组 终于觉得是弄透彻了。

1、指针是什么?

例如 int *p 定义一个int型的指针p

指针是用来存放地址 比如你们家住在那条街门牌号是多少 指针就存这个

*就相当于一把钥匙 可以打开你家的门,具有开锁的功能。

使用指针的时候,没有它,你是不可能读写某块内存的。

指针的大小 sizeof(p)  = 4; 无论什么类型的指针大小都为4个byte。

 

指针的内存布局

由这个图 可以清晰的知道 指针的内部存储。

 

int *p = NULL 和*p = NULL 有什么区别?

int *p = NULL

这句代码的意思是:定义一个指针
变量p,其指向的内存里面保存的是int类型的数据;在定义变量p的同时把p 的值设置为
0x00000000
,而不是把*p的值设置为0x00000000。这个过程叫做初始化,是在编译的时候进行的。

int *p;
*p = NULL;

第一行代码,定义了一个指针变量p,其指向
的内存里面保存的是int类型的数据;但是这时候变量p本身的值是多少不得而知,也就是
说现在变量p保存的有可能是一个非法的地址。第二行代码,给*p赋值为NULL,即给p
指向的内存赋值为NULL。但是由于p指向的内存可能是非法的,所以调试的时候编译器可

能会报告一个内存访问错误。这样的话,我们可以把上面的代码改写改写,使p指向一块合
法的内存:
int i = 10;
int *p = &i;
*p = NULL;

p指向的内存由原来的10变为0 了;而p 本身的值,即内
存地址并没有改变。

NULL  是空字符ASCII 码值为0 但与0 有本质的区别。

 

如何将数值存储到指定的内存地址?

假设现在需要往内存0x12ff7c 地址上存入一个整型数0x100。

可以这样写:

int *p = (int *)0x12ff7c;
*p = 0x100;

把0x12ff7c这个地址强转为int * 型然后赋给p 在给p指向的内存赋值

其实 还可以更简单

*(int *)0x12ff7c = 0x100; 一句话解决 道理是一样的。

 

上面是指针的内容 当然指针肯定是要与数组连用的不然就没意思了。

下面就是数组了

 

数组:

 

先看个例子:

int a[5];  当定义一个这样的数组的时候,

就会有大一堆的问题,这个数组的数组名是谁,这个数组占内存多少个字节,数组的首地址是多少?

带着问题往下看:

1、数组的内存布局:

 

如上图所示,当我们定义一个数组a时,编译器根据指定的元素个数和元素的类型分配确定
大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为a。名字a一旦
与这块内存匹配就不能被改变。a[0],a[1]等为a的元素,但并非元素的名字。数组的每一个
元素都是没有名字的。

 

所以 sizeof(a) 的大小 应该是sizeof(int) * 5 =20个字节

         sizeof(a[0])的大小 应该是4。

sizeof(a[5]) 是多少 也许你会说a[5] 根本不存在 但这句话确实编译可以通过 而且求出来的值也是4

因为sizeof它不是函数是个关键字它仅仅根据数组元素的类型来确定其值。a[5] 的类型是int型的 所以为4

sizeof(&a[0])  为4,这很好理解。取元素a[0]的首地址。

sizeof(&a)     的值也为4,这也很好理解。取数组a 的首地址

 

&a[0]和&a 的区别

&a[0] 是表示数组首元素的首地址 &a是代表数组的首地址。

举个例子:湖南的省政府在长沙,而长沙的市政府也在长沙。两个政府都在长沙,但其代表的
意义完全不同。这里也是同一个意思。

 

指针与数组

 

指针就是指针,指针变量在32 位系统下,永远占4 个byte,其值为某一个内存的地址。
指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型
和个数。数组可以存任何类型的数据,但不能存函数。

 

以指针的形式访问和以下标的形式访问

例:

A),char *p = “abcdef”;
B),char a[] = “123456”;

A)中 定义一个指针 p指向一串字符串 p在栈上占4 个byte 

p里存储的是一块内存的首地址 这块内存的大小为7个字节

这块内存是没有名字的要对它进行访问完全是匿名的 比如要读取字符‘d’ 有两种方法

1、以指针的形式 *(p+3);//先取出p 里存储的地址值.然后加上4 个字符的偏移量,最后取这个地址的内容

2),以下标的形式:p[3];//先取出p 里存储的地址值,然后加上中括号中4 个元素的偏移量,然后从新的地址中取值。

也就是说以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了

 

以指针的形式访问和以下标的形式访问数组

例如B)

定义了一个数组a,a 拥有7 个char 类型的元素,其空间大小为7。数组a 本身
在栈上面。对a 的元素的访问必须先根据数组的名字a 找到数组首元素的首地址,然后根据
偏移量找到相应的值。这是一种典型的“具名+匿名”访问。比如现在需要读取字符‘5’,
我们有两种方式:
1),以指针的形式:*(a+4)。a 这时候代表的是数组首元素的首地址,假设为0x0000FF00,
然后加上4 个字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的
值。
2),以下标的形式:a[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操
作。a[4]这个操作会被解析成:a 作为数组首元素的首地址,然后加上中括号中4 个元素的
偏移量,计算出新的地址,然后从新的地址中取出值。

 

a 和&a 的区别

例子:

main()
{
       int a[5]={1,2,3,4,5};
       int *ptr=(int *)(&a+1);
       printf("%d,%d",*(a+1),*(ptr-1));
}

可能你开始看着有点晕,没关系一句一句的看。

a代表数组首元素的首地址

&a代表数组的首地址

&a+1 就表示下一个数组的首地址

int *ptr=(int *)(&a+1); 这句话就是将下一个数组的首地址强转为int* 然后赋给ptr 则ptr->a[5]。

输出 *(a+1)就是首元素的首地址+1的地址里的内容就是2

*(ptr-1) 就是ptr 的地址-1 就是数组a[4]的地址 它的内容是5

所以打印出 2 5。

不信 可以把相关的地址打印出来
 printf("&a = %p\n",(a)); //a的地址
 printf("&(a+1) = %p\n",(a+1));//a+1的地址
 printf("&(&a) =%p\n",(&a));//数组的首地址
 printf("&(&a+1) = %p\n",(&a+1));//数组的首地址+1的地址

打印出:

&a = 0xbfee40cc
&(a+1) = 0xbfee40d0
&(&a) =0xbfee40cc
&(&a+1) = 0xbfee40e0

可以看出(&a+1) 地址是 (&a)的地址0xbfee40cc+5*sizeof(int);

 

指针数组和数组指针

指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身
决定。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32 位系统下永远是占4 个字节,
至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。

下面到底哪个是数组指针,哪个是指针数组呢:
A),int *p1[10];
B),int (*p2)[10];

先看A)我们知道 []的优先级要比*高 所以显示p1先于[]组成一个数组p1[10]

数组名p1  它的元素的类型是int *型的。,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组

在看看B)明显()的优先级要比[]高则先是*p2构成一个指针 指针的变量名为p2。

int 修饰的是数组的内容,
即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指
针,它指向一个包含10 个int 类型数据的数组,即数组指针

看图:

 

int (*p2)[10]; 一般这么定义 int (*)[10]p2;

再看 a 和&a 之间的区别

例:

int main()
{
      char a[5]={'A','B','C','D'};
      char (*p3)[5] = &a;
      char (*p4)[5] = a;
      return 0;
}

上面对p3 和p4 的使用,哪个正确呢?

我们可以看到 p3和p4都是数组指针 数组指针首先是个指针它是指向数组的即存的是数组的地址

上面 &a代表数组的首地址而a代表数组首元素的首地址 所以按道理讲p3的使用是对的p4的使用是错的。

 

地址的强制转换

看个例子:

struct Test

{

       int Num;

       char *pcName;

       short sDate;

       char cha[2];

       short sBa[4];

}*p;

假设p 的值为0x100000。如下表表达式的值分别为多少?

p + 0x1 = 0x___ ?

(unsigned long)p + 0x1 = 0x___?

(unsigned int*)p + 0x1 = 0x___?

 

指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。

这个整数的单位不是byte 而是元素的个数

p + 0x1 = 0x100000 + sizeof(Test)*0x1 由内存对齐的知识可知道sizeof(Test) = 20byte 化为十六进制为0x14 即p + 0x1 = 0x100014;

 

下面(unsigned long)p + 0x1 首先把p的地址强转为unsigned long的一个unsigned long 可以与一个int型的直接相加 所以 (unsigned long)p + 0x1 =  0x100001。

 

(unsigned int*)p + 0x1 这里的p 被强制转换成一个指向无符号整型的指针只要把第一个改一下就行 p + 0x1 = 0x100000 + sizeof(Test)*0x1 是Test*型的 现在是(unsigned int*) 那么

(unsigned int*)p + 0x1 = 0x100000 + sizeof(unsigned int)*0x1 = 0x100004。

这下相信都知道了。

 

再看个比较难的呵呵:

intmain()

{

       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;

}

好像貌似看起来挺麻烦的一句一句看;

int *ptr1=(int *)(&a+1); 这一句定义了一个ptr1指针 &a代表数组的首地址

&a+1表示下一个数组的首地址。(int *)&a+1 把地址强转为int*然后赋给int*的 ptr1。

那么ptr1[-1] =*(ptr1-1)  ptr1-1就是ptr1减4个字节 即ptr1指向a[3]的地址

*(ptr1-1) 就是取a[3]的内容。

 

int *ptr2=(int *)((int)a+1); 先看a a代表的是数组首元素的首地址,即a[0]的地址(int)a 把a的地址强转为整型就可以直接与1相加。加上1就相当于加1个字节即((int)a+1)表示的是元素a[0]的第二个字节的地址。

即 *ptr2就是从这个地址开始的4个字节大小里的内容。

可以画图来分析:

我也是看这个图才看懂的。

 

 

二维数组

二维数组其实可以当做一个一维数组来看。

如 char a[3][4]

它的内部布局为:

这是常规的画法。

下面看它的本质char a[3][4]可以这么分析它是一个 数组名为a元素个数为3的一维数组即a[3],它的类型是char [4]看图:

 

更详细的解释:

以数组下标的方式来访问其中的某个元素:a[i][j]。编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组。a[3]这个一维数组的三个元素分别为:a[0],a[1],a[2]。每个元素的大小为sizeof(a[0]),即sizof(char)*4。由此可以计算出a[0],a[1],a[2]

三个元素的首地址分别为& a[0],& a[0]+ 1*sizof(char)*4,& a[0]+ 2*sizof(char)*4。亦即a[i]的首地址为& a[0]+ i*sizof(char)*4。

即a[i][j]的首地址为&a[i]+j*sizof(char)

这就是二维数组的内部布局!!

下面看一个例子:

int a[5][5];

int (*p)[4];

p = a;

问&p[4][2] - &a[4][2]的值为多少?

做这个题先不要慌一句一句看

int a[5][5];定义一个二维数组,为了更好的分析最好把图画出来:

int (*p)[4]; p = a;

这明显是一个数组指针 指针为p指向a这个数组 a这个数组有5个元素即a[0],a[1],a[2],a[3],a[4],每个元素的类型是int[5] 即含有5个int型数据。但p的代表的类型是 int[4]的即一个p的地址只占一个a的4个字节

&p[4][2] 表示a[4][2]的首地址看图:

由这个图我们可以很好的看出

&p[4][2]  = 4*4*sizeof(int) + 2*sizeof(int) = 80个字节

而&a[4][2] = 4*5sizeof(int) + 2*sizeof(int) = 96个字节

相差 16 个字节正好4个int 从图上也可以数得出,所以以后碰到二维的就必须画图这样才能一目了然。

 

二级指针

char **p;

一级指针保存的是数据的地址,二级指针保存的是一级指针的地

址。下图帮助理解:

关于二级指针的用处通过下面举几个例子一起分析:

1、关于数组的传参:

C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。

void fun(char a[10])

{

       char c = a[3];

}

intmain()

{

       char b[10] = “abcdefg”;

       fun(b[10]);

       return 0;

}

 

这样的传参显然是不对的。b[10] 这个元素根本不存在 ,就是存在也不行,传递一维数组要把地址传过去。

要改为:

void fun(char *p)

{

      char c = p[3];//或者是char c = *(p+3);

   同样,你还可以试试这样子:

}

 

void fun(char a[10])

{

      char c = a[3];

}

 

intmain()

{

      char b[100] = “abcdefg”;

      fun(b); //b代表数组首元素的首地址

      return 0;

}

能否把指针变量本身传递给一个函数

这就要用到二维指针。

如:

void fun(char *p)

{

       char c = p[3];//或者是char c = *(p+3);

}

intmain()

{

       char *p2 = “abcdefg”;

       fun(p2);

       return 0;

}

 

这个函数调用,真的把p2 本身传递到了fun 函数内部吗?我们知道p2 是main 函数内的一个局部变量,它只在main 函数内部有效。(这里需要澄清一个问题:main 函数内的变量不是全局变量,而是局部变量,只不过它的生命周期和全局变量一样长而已。全局变量一定是定义在函数外部的。初学者往往弄错这点。)既然它是局部变量,fun 函数肯定无法使用p2 的真身。那函数调用怎么办?好办:对实参做一份拷贝并传递给被调用的函数。即对p2 做一份拷贝,假设其拷贝名为_p2。那传递到函数内部的就是_p2 而并非p2 本身。

无法把指针变量本身传递给一个函数

这很像孙悟空拔下一根猴毛变成自己的样子去忽悠小妖怪。所以fun 函数实际运行时,

用到的都是_p2 这个变量而非p2 本身。如此,我们看下面的例子:

void GetMemory(char * p, int num)

{

      p = (char *)malloc(num*sizeof(char));

}

intmain()

{

      char *str = NULL;

      GetMemory(str,10);

      strcpy(str,”hello”);

      free(str);//free 并没有起作用,内存泄漏

      return 0;

}

在运行strcpy(str,”hello”)语句的时候发生错误。这时候观察str 的值,发现仍然为NULL。也就是说str 本身并没有改变,我们malloc 的内存的地址并没有赋给str,而是赋给了_str。而这个_str 是编译器自动分配和回收的,我们根本就无法使用。所以想这样获取一块内存是不行的。

那怎么办? 两个办法:

 

第一:用return。

char * GetMemory(char * p, int num)

{

       p = (char *)malloc(num*sizeof(char));

       return p;

}

intmain()

{

       char *str = NULL;

       str = GetMemory(str,10);

       strcpy(str,”hello”);

       free(str);

       return 0;

}

这个方法简单,容易理解。

第二:用二级指针。

void GetMemory(char ** p, int num)

{

      *p = (char *)malloc(num*sizeof(char));

      return p;

}

intmain()

{

      char *str = NULL;

      GetMemory(&str,10);

      strcpy(str,”hello”);

      free(str);

      return 0;

}

二维数组参数与二维指针参数

一个数组指针可以指向一个二维数组。

一个双指针可以指向一个数组指针。

解释为什么一个数组指针可以指向一个二维数组?

一个二维数组的声明:void fun(char a[3][4])

可以看出一个一维数组a[3],它有三个元素即a[0],a[1],a[2]每个元素的类型是char [4]。其实就是每个元素里值是一个数组,所以二维数组常被叫做数组的数组,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。

 

数组指针 数组指针首先是一个指针 指针可以指向某一个数组。即可以通过它去指向这个二维数组:

void fun(char (*p)[4]) 这里 p = a[0] p+1 = a[1] p+2 = a[2] p+3=a[3]

 

解释为什么一个双指针可以指向一个数组指针?

指针数组: char *a[5]

指针的指针:char **p

一个 char a[5] 可以由一个char *p来指向,那么这个指针数组

char *a[5] 当然可以由指针的指针来指向char **p。指针的指针也叫双指针。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值