C语言中指针和数组

C语言数组与指针的那些事儿

在C语言中,要说到哪一部分最难搞,首当其冲就是指针,指针永远是个让人又爱又恨的东西,用好了能够事半功倍,用很差,就会有改不完的bug和通不完的宵。可是程序员通常都有一种迷之自信,总认为本身是天选之人,明知山有虎,偏向虎山行,直到最后用C的人都要被指针虐一遍。linux

指针

首先,明确一个概念,指针是什么,一旦提到这个老生常谈且富有争议性的话题,那真是1000我的有1000种见解。程序员

在国内的不少教材中,给出的定义通常就是"指针就是地址",从初步理解指针的角度来讲,这种说法是最容易理解的,可是这种说法明显有它的缺陷所在。
"指针就是地址"这种说法至关于"指针=字面值地址(或者说一个具体的右值)",这种说法的错误所在就是弄错了指针的本质属性:指针是变量!编程


试想一下,若是指针是地址成立,那么二级指针怎么理解呢?地址的地址吗,这明显是错误的。数组

下面咱们从指针是变量这个原则出发,来分析什么是指针:网络

  1. 做为一个变量,确定有本身的地址
  2. 做为一个变量,确定有本身的值,和普通变量的区别就是指针变量的值是地址。
  3. 从第二点延伸过来,既然指针变量的值是地址,那么那个地址上的内容就是指针变量指向的数据,指针的类型就是指针变量指向数据的类型。
  4. 指针有自己的类型,这个自己的类型区别于指向对象的类型。

在这里,最容易弄混的就是指针自己的类型和指针的类型,指针自己的类型是int型,通常状况下同一平台上全部类型指针都是同样的(注①),长度则是平台相关,通常状况下32位机中为4字节,64位机中为8字节,事实上,指针的大小由处理器中所使用的地址总线宽度决定,指针自己的类型有什么意义呢?
(为何说通常状况下同一平台上全部类型指针都是同样,而不是全部状况呢?事实上,在某些地址总线宽度与数据总线宽度不一样的特殊机器上指针类型可能不一致)函数

内存的访问是以字节为单位的,同时指针的值为一个地址,指针的类型就直接决定了指针的所能表示地址的上界和下界,32位指针访问范围为0~2^32字节,因此是4GB。
注:如下讨论中,对于指针指向数据的类型统一称为指针的类型,这篇博客主要讨论指针的类型而非指针自己的类型学习

而指针指向数据的类型则是在定义时指定的,好比int ptr,char str,在这里,ptr指针的数据类型就是int型,而str指针指向的类型是char型,区分指针指向数据的类型主要是用在对指针解引用时的不一样,指针的值是具体的某一个位置,指向数据的不一样则表明解引用的时候所取数据的不一样,当ptr为int*类型时,表示在ptr表示的地址处取sizeof(int)个数据,依次类推。指针

指针的地址:若是一个指针变量存储的值是另外一个指针的地址,那这个指针就是二级指针,一样的定义能够递推到多级指针。code

指针的操做

解引用:用*来获取指针指向的数据,这个不用多说。
指针的运算:加减运算,须要注意的是,指针的加减运算的粒度是基于指针类型的长度,在下例中:对象

int *p = (int*)0x1000;
char *str = (char*)0x1000;
p++;
str++;
print("p=%d,str=%d\r\n",p,str);

输出结果:
p=0x1004,str=0x1001
能够看到,p指向int型数据,p++就至关于p+sizeof(int),而str++就至关于str+sizeof(char).

关于指针定义的争议

怎么样定义一个指针你们都知道,在编程时一般有两种写法:

int* ptr;
int *ptr;

咋一看,这俩不是同样吗?若是你仔细观察就能够发现其中的不一样,第一种定义方法中靠近类型,而第二种靠近变量,看到这里,有些朋友就要说了,你个杠精!这不就是个写法问题吗,至于这么纠结吗!

这还真不只仅是个写法问题。这两种写法背后表明着不一样的逻辑:

  • 第一种写法的背后的逻辑是,将int做为一个总体,将其视为一个类型,即int、char*与int、char这些同样,都是一种独立的类型,再用这些类型来定义指针变量,从这个角度来看,指针是比较好理解的,并且看起来更能解释得通。
  • 第二种写法的背后逻辑是,在指针的定义中,仅仅是一个标识符,如int p,代表*后面所接的变量p是一个指针变量,指向数据类型为int型。
    其实在早期,你们一直都更倾向于经过第一种去理解指针,后来又有第二种看起来比较生涩的理解,为何会这样呢?咱们来看下面的例子:

    int* p1,p2;
    p2=p1;
    咱们来编译这个例子,结果是这样:

    warning: assignment makes integer from pointer without a cast [-Wint-conversion]
    编译信息显示,p2为普通int型变量,而p1是int型指针变量,这明显违背咱们的初衷。若是要定义两个指针变量,咱们应该这么作:

    int p1,p2;
    p2=p1;
    相信到这里,你们可以看出来了,第一种写法背后逻辑的缺陷所在。

因此如今愈来愈多的专业书籍都推荐第二种写法,毕竟做为一门底层语言,严谨性比易读性要重要。

对教材错误写法的小见解

说实话,博主学习C语言也是从国内教材开始,一开始接触到的也是“指针就是地址”的概念,其实于我而言,这种说法让我快速地理解了指针,后来慢慢接触到复杂的逻辑,看了一些更好的教材,慢慢地才开始有了更深刻的理解。

其实博主更倾向于这样去理解这个事情:就像小学老师会告诉咱们0是最小的数,这个概念固然是错的,可是这种教法正是能够剥去语言的外壳,让咱们避免陷入繁杂的分支和细节中,快速地理解使用和培养兴趣,至于后面的进阶,天然会有进阶的书籍来纠正,就像高中或者大学以致于更高的平台,总会告诉你你以前创建的部分概念并不彻底正确,关键是从新创建这个概念并不会太难,由于须要从新创建的时候每每是初级到中级的进阶过程。

至于网络上的一些比较过激的言论,我是不抱以支持态度的,不管如何,在咱们没有能力接触国外教材且资源缺少的时候,是这些不完美的教材使咱们踏入了计算机的世界。

指针和数组的区别

废话说了那么多,咱们来回到正题,看看指针和数组。不得不说,指针和数组就像孪生兄弟,有时候让人分不清楚,这种状况主要发生在函数参数传递的时候,当一个函数须要一个数组做为一个参数时,咱们并不会将整个数组做为参数传递给函数,而是传入一个同类型指针p,而后在函数中就可使用p[N]来访问数组中元素(这个你们都懂,就不放示例了)。

那么,指针和数组究竟是不是同一个东西呢?
咱们来看看下面的例子:

file1.c:
    int buf[10];
file2.c:
    extern int *buf;

编译结果:

error: conflicting types for ‘buf’。

从这里能够看出,数组和指针并不相等。至于具体的区别,且听我细细道来。

数据访问的本质区别

毫无疑问,咱们常用指针的数组,也常常混用。可是咱们有没有关注过它们背后的执行原理呢?咱们看下面的代码:

int buf[10] = {5};
int *p = buf;
*p = 10;

首先,有必要来说讲数组的初始化,在定义时,若是咱们不对数组进行初始化操做,有两种状况:

  • 数组为全局变量或者静态变量时,在程序加载阶段默认全部元素都被初始化为0。
  • 数组为局部变量,由于数组数据在栈上分配,就延续了了栈上上一次的值,因此这个值是不肯定的。

同时,咱们能够对其进行初始化,能够所有初始化或者部分初始化,部分初始化时,未被初始化部分所有默认被初始化为0.因此咱们经常使用buf[N]={0}来在定义时初始化一个数组。

根据C语言的规定,数组名=数组首元素指针,因此直接能够用数组名的解引用buf来访问第一个元素,也可使用(buf+N)来访问第N个元素。

咱们须要知道的是,在程序编译的时候,会对全部的变量分配一个地址,这个地址和变量的对应在符号表中被呈现,数组和指针在符号表中的区别就体如今这里:

  • 对于数组而言,符号表中存在的地址为数组首元素地址,因此当咱们使用素组下标访问元素N时,它执行的是这样的操做
    • 先取出数组首元素地址
    • 目标地址=首地址+sizeof(type)*N,获得被访问元素的地址,type是指针指向数据类型,指针加法参考上面。
    • 解引用(至关于在变量前加*),从地址上取出被访问元素。
  • 对于指针变量而言,符号表中存储的是指针变量的地址,它访问元素时这样的过程:
    • 取出指针变量的地址,解引用以获取指针变量
    • 继续对指针变量进行解引用,获取目标元素的值。

看到这里,我想你已经知道了指针和数组访问数据的本质区别,可是,咱们在这里须要讨论的状况并不是这两种.

而是:参数定义为指针,可是以数组的方式引用。这个在函数调用时才是发生得最频繁的,那这时候会发生什么呢?

这个时候其实就是两种访问方式的结合了,假设定义了指针buf,那么在符号表中存在的就是buf指针的地址(注意是buf的地址,并且buf自己是个指针),参考上述指针的访问方式.以获取buf中第二个元素为例:

  • 首先,根据buf变量的地址,获取buf指针。
  • 使用第一步中获取的地址进行偏移,获得目标数组元素的地址,此时目标地址为(&buf[0]+2)
  • 解引用(至关于在变量前加),从地址上取出被访问元素,至关于执行(&buf[0]+2)。

到这里,我想你已经大概清楚了数组和指针的区别,以及参数传递时,指针的下标引用背后的原理。

数组指针和数组元素指针

在上一小节中,我指出了数组名=数组首元素指针的概念,若是朋友们不仔细看,或者本身不去写代码尝试,很容易把它记成了数组名=数组的指针 这个概念,请特别注意,数组名=数组的指针这个概念是彻底错误的,这也是数组中很是容易混淆和犯错的地方,咱们不妨来看下面的例子:

char buf[5]={0};
printf("address of origin buf = %x\r\n",buf);
printf("address of changed buf = %x\r\n",&buf+1);

输出结果:

address of origin buf = de157880
address of changed buf = de157885

咱们先定义一个长度为5的buf,buf中首元素地址为0xde157880,而后再打印&buf+1的值,显示为0xde157885,那么问题就来了,为何明明只是+1,而地址却加了5,5正好是sizeof(buf)。咱们再来看看下面的例子:

char buf[5]={0};
printf("address of changed buf = %x\r\n",(&buf+1)-buf);

编译时信息以下:

error: invalid operands to binary - (have ‘char (*)[5]’ and ‘char *’)

从这个报错信息,咱们能够看出,&buf的类型为char ()[5],为数组指针类型,而buf类型为char ,字符指针类型。

看到这里,问题也就慢慢地清晰了。在C语言中,数组名是一个特殊的存在,与咱们惯有的思惟相反,数组名表明数组首元素的指针,而不是数组指针,若是要声明一个数组指针,咱们能够这样来声明:char (*p)[5] = buf;

说了这么多,那么,区分数组指针和数组元素指针的意义在哪里呢?参考上面所说的指针的加减运算,即:指针的加减运算的粒度是基于指针类型的长度,数组指针的长度为sizeof(数组),而数组元素指针是sizeof(单个元素)(再啰嗦一次!数组名为数组元素指针而不是数组指针)。

指针数组和二维数组

数组指针是一个指针类型为数组的指针,好比定义一个带有5个char元素数组的指针:char (*buf)[5]。

那么指针数组又是什么东西呢?其实指针数组要比数组指针容易理解,它就是一个普通数组,只不过特殊的是数组内全部元素都是指针,好比定义一个字符指针数组:char *buf[5],注意它们之间的区别;数组指针是一个指针,指针数组是一个数组。


二维数组,你们可能没有使用过,可是必定听过,二维数组的定义:char buf[x][y],其中x可缺省,y不能缺省。对于二维数组,咱们能够这样理解:二维数组是一维数组的嵌套,即一维数组中全部元素为同类型数组。 例如:char array[3][3],咱们能够将其理解成array数组是一个一维数组,数组的元素分别是array[0],array[1],array[2]三个char[3]型数组,这种理解能够递推到多维数组,从而来理解二维数组的内存模型。

下面详细说说为何须要将多维数组当作一维数组。

二维数组和二级指针

"既然一维数组和指针在必定程度上能够"混合使用",那么二维数组确定也是可使用二维指针来访问了" —— 某不知名程序员语录

问:上面这句话有没有什么问题?

答:大错特错!

很惭愧,博主曾经也是这么认为的,二维数组确定是能够像一维数组那样使用指针访问,只不过要用二级指针(二维嘛)。

话很少说,咱们先看下面代码:

char buf[2][2]={{1,2},{3,4}};
char **p = buf;
printf("buf[] = %d,%d,%d,%d\r\n",p[0][0],p[0][1],p[1][1],p[1][2]);

输出结果:

Segmentation fault (core dumped)

在这个示例中,博主的本意是使用二级指针p赋值为二维数组名,而后使用p访问数组中元素,可是结果明显跑偏了,这是为何?

有些朋友可能在学习上面的"数组和指针数据访问的本质区别"的时候会想,我只要会用就好了,我要去关注这些底层细节有什么做用?在简单的应用中固然没什么做用,可是在这种时刻就须要对底层扎实的理解了。


咱们来详细分析一下上面代码中的背后访问逻辑:

  • 第一点,咱们须要确认的是,二维数组的数组名究竟是什么类型的指针。是二维数组中第一个char型元素的指针吗?仍是按照上一节"指针数组和二维数组"中说的那样,将二维数组当作一个一维数组,从一维数组的角度看,首元素为buf0,那二维数组名就是一个数组指针,类型为char (*)[2]。要验证这个很简单,咱们分别编译两份代码:

    代码1:
    char buf[2][2]={{1,2},{3,4}};
    char *p = buf;
    编译结果:

    warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]

    代码2:
    char buf[2][2]={{1,2},{3,4}};
    char (*p)[2] = buf;
    编译结果:
    无警告信息
    所谓实践出真知,结果很显然,答案是第二种:咱们应该将二维数组当成嵌套的一维数组,而数组名为首元素地址,注意,这里的首元素是从一维数组的角度出发,这个首元素的类型多是普通变量,数组甚至是多维数组。

  • 第二点,char **p = buf;这一条怎么去理解呢?根据上面的结论二维数组名buf是char (*)[2]类型,而p是char型二级指针,参数天然不匹配。
  • 即便是参数不匹配,可是编译只是警告,而非报错,咱们仍然能够执行它。那么执行这个程序的时候又发生了什么呢?咱们根据"指针与数组数据访问的本质区别"小节部分来分析:
    • 首先,p的地址是在编译时已知的,程序运行时,经过指针p的地址获得p的值,通过上面的分析,此时p = &buf[0],虽然&buf[0]是数组指针,可是p为char** 类型,因此&buf[0]被强制转换成char**型指针。
    • 在printf函数中访问p[0][0],事实上访问P[0][0]就先得访问p[0],那么就先找到p的值,那么p的值又是多少呢?答案是p=buf[0][0],p不是一个地址,而是一个字面值1,因此此时p[0] = 1,访问*p[0]天然会致使Segmentation fault (core dumped)。

鉴于上面的解析部分很是难以理解,并且仅仅是字面讲解几乎没法讲清楚,博主就尝试经过几个示例来进行讲解:

示例1:
char buf[2][2]={{1,2},{3,4}};
char **p = buf;
printf("array name--buf address = %x\r\n",buf);
printf("&buf[0] address = %x\r\n",&buf[0]);
printf("Secondary pointer address = %x\r\n",p);
输出:

array name--buf address = a836a2c0
&buf[0] address = a836a2c0
&buf[0][0] address = a836a2c0
Secondary pointer address = a836a2c0

尽管编译过程有好几个Warning,暂时不去理会,结果显示,至少从数值上来讲 p = buf = &buf[0] = &buf[0][0]。


示例2:

char buf[2][2]={{1,2},{3,4}};
char **p = buf;
printf("p[0] = %x\r\n",p[0]);

输出:
p[0] = 04030201
这个结果就很是有意思了,能够看到,指针p[0]的值,正好是数组buf的四个元素的值(内存中存储顺序将01020304反序存储,这里涉及到大小端的存储问题,不过多赘述)。可想而知,访问p[0][0]的时候会发生什么?按照以前的讲解,咱们先将p[0]作相应位移,即p[0]=p[0]+sizeof(char)*0,而后再解引用获取地址上的值,那就是直接取0x04030201地址上的值,结果固然不会是咱们所期待的!

再回到示例,为何p[0]的值会是0x04030201?

  • 首先,咱们要知道,p[0]是什么类型,p[0]即为*p,p是二级指针,*p也是一个指针,因此*p的自己的类型为int*,因此它的值为4个字节。
  • 根据前面的分析,p = buf = &buf[0] = &buf[0][0],对p解引用(即p)至关于取出p地址处的数据,根据int类型,取四个字节数据,而这四个字节正好就是buf中四个元素。

那若是咱们要使用指针来访问二维数组中的元素,该怎么作呢?
看下面的代码:

#define ROW     2
#define COLUMN  2
char buf[ROW][COLUMN]={{1,2},{3,4}};
char *p = (char*)buf;
//访问buf[x][y],即访问p[x*COLUMN+y]
printf("buf = %d,%d,%d,%d\r\n",p[COLUMN*0+0],p[COLUMN*0+1],p[COLUMN*1+0],p[COLUMN*1+1]);

若是你看懂了以前博主介绍的内容,理解这一份代码是很是简单的。

好了,关于C语言中指针和数组的讨论就到此为止了,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

我的邮箱:linux_downey@sina.com
原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.
(完)

结语:为了写这一篇博文,感受发际线又往上走了一公分...

相关文章
相关标签/搜索