C语言5之什么是数组?

时间:2018.2.4  作者:Tom   工作:HWE 说明:如需转载,请注明出处。
说明:本文主要参考朱有鹏老师linux嵌入式C语言高级篇笔记,已注明转载。

1 什么是数组

1.1 从内存角度来理解数组

  1. 从内存角度讲,数组变量就是一次分配多个变量,而且这多个变量在内存中的存储单元是依次相连接的。除了字符数组以外(可以使用%s以字符串的形式打印),数组中的元素不能同时赋值或者输出,必须单独操作。
  2. 单独定义时a、b、c、d(譬如int a, b, c, d;)在内存中的地址不一定相连,但是定义成数组后(int a[4]),数组中的4个元素地址肯定是依次相连的。
  3. 数组中多个变量虽然必须单独访问但是因为他们的地址彼此相连,因此很适合用指针来操作,因此数组和指针天生就叫纠结在一起。

     

1.2 从编译器角度来理解数组

  1. 从编译器角度来讲,数组变量也是变量,和普通变量和指针变量并没有本质不同。变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度。
  2. 搞清楚:变量、变量名、变量类型这三个概念的具体含义,很多问题都清楚了。这里可以看下前面博客讲解的数据类型的本质!

     

2 数组中几个关键符号(a,a[0],&a,&a[0])的理解(前提是 int a[10];)

这4个符号搞清楚了,数组相关的很多问题都有答案了。理解这些符号的时候要和左值右值结合起来,也就是搞清楚每个符号分别做左值和右值时的不同含义。

  1. a就是数组名。a做左值时表示整个数组的所有空间(10×4=40字节),又因为C语言规定数组操作时要独立单个操作,不能整体操作数组,所以a不能做左值;a做右值表示数组首元素的首地址首地址就是起始地址,就是4个字节中最开始第一个字节的地址)。a做右值等同于&a[0];

    备注:这个明显应用在哪里?举个例子就是sinzof(a),还有一个就是数组指针的定义!!!

  2. a[0]表示数组的首元素,也就是数组的第0个元素。做左值时表示数组第0个元素对应的内存空间(连续4字节);做右值时表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数)
  3. &a就是数组名a取地址,字面意思来看就应该是数组的地址。&a不能做左值(&a实质是一个常量,不是变量因此不能赋值,所以自然不能做左值。);&a做右值时表示整个数组的首地址
  4. &a[0]字面意思就是数组第0个元素的首地址(搞清楚[]和&的优先级,[]的优先级要高于&,所以a先和[]结合再取地址)。&a[0]不能做左值。做右值时&a[0]等同于a。

解释:为什么数组的地址是常量?因为数组是编译器在内存中自动分配的。当我们每次执行程序时,运行时都会帮我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。那么我们在程序中只能通过&a来获取这个分配的地址,却不能去用赋值运算符修改它。

总结:

1:&a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。举例:

    int *p;

    int a[5];

    p = a;        //类型匹配,体现a做右值,是数组首元素的首地址,地址类型就是int*类型的

    

p = &a;    //类型不匹配,&a做右值,是整个数组的首地址,类型是数组指针:(*p)[5]类型的;

    int (*b)[5];

    b = &a;        //类型匹配的。

2:a和&a[0]做右值时意义和数值完全相同,完全可以互相替代。

3:&a是常量,不能做左值。

4:a做左值代表整个数组所有空间,所以a不能做左值。

3.指针与数组的天生姻缘

3.1 以指针方式来访问数组元素

  1. 数组元素使用时不能整体访问,只能单个访问。访问方式有2种:数组形式和指针形式。
  2. 数组格式访问数组元素是:数组名[下标]; (注意下标从0开始)
  3. 指针格式访问数组元素是:*(指针+偏移量); 如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标;指针也可以不是首元素地址而是其他哪个元素的地址,这时候偏移量就要考虑叠加了。
  4. 数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的。在编译器内部都是用指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者一种壳(语法糖)而已。所以用指针方式来访问数组才是本质的做法。

     

3.2 从内存角度理解指针访问数组的实质

  1. 数组的特点就是:数组中各个元素的地址是依次相连的,而且数组还有一个很大的特点(其实也是数组的一个限制)就是数组中各个元素的类型比较相同。类型相同就决定了每个数组元素占几个字节是相同的(譬如int数组每个元素都占4字节,没有例外)。
  2. 数组中的元素其实就是地址相连接、占地大小相同的一串内存空间。这两个特点就决定了只要知道数组中一个元素的地址,就可以很容易推算出其他元素的地址。

     

3.3 指针和数组类型的匹配问题

  1. int *p; int a[5];    p = a;        // 类型匹配
  2. int *p; int a[5];    p = &a;        // 类型不匹配。p是int *,&a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配
  3. &a、a、&a[0]从数值上来看是完全相等的,但是意义来看就不同了。从意义上来看,a和&a[0]是数组首元素首地址,而&a是整个数组的首地址;从类型来看,a和&a[0]是元素的指针,也就是int *类型;而&a是数组指针,是int (*)[5];类型。

     

3.4 总结:指针类型决定了指针如何参与运算

  1. 指针参与运算时,因为指针变量本身存储的数值是表示地址的,所以运算也是地址的运算。
  2. 指针参与运算的特点是,指针变量+1,并不是真的加1,而是加1*sizeof(指针类型);如果是int *指针,则+1就实际表示地址+4,如果是char *指针,则+1就表示地址+1;如果是double *指针,则+1就表示地址+8.
  3. 指针变量+1时实际不是加1而是加1×sizeof(指针类型),主要原因是希望指针+1后刚好指向下一个元素(而不希望错位)。

4 指针与强制类型转换

4.1变量的数据类型的含义

  1. 所有的类型的数据存储在内存中,都是按照二进制格式存储的。所以内存中只知道有0和1,不知道是int的、还是float的还是其他类型。
  2. int、char、short等属于整形,他们的存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的存储方式彼此不同,和整形更不同。
  3. int a = 5;时,编译器给a分配4字节空间,并且将5按照int类型的存储方式转成二进制存到a所对应的内存空间中去(a做左值的);我们printf去打印a的时候(a此时做右值),printf内部的vsprintf函数会按照格式化字符串(就是printf传参的第一个字符串参数中的%d之类的东西)所代表的类型去解析a所对应的内存空间,解析出的值用来输出。也就是说,存进去时是按照这个变量本身的数据类型来存储的(譬如本例中a为int所以按照int格式来存储);但是取出来时是按照printf中%d之类的格式化字符串的格式来提取的。此时虽然a所代表的内存空间中的10101序列并没有变(内存是没被修改的)但是怎么理解(怎么把这些1010转成数字)就不一定了。譬如我们用%d来解析,那么还是按照int格式解析则值自然还是5;但是如果用%f来解析,则printf就以为a对应的内存空间中存储的是一个float类型的数,会按照float类型来解析,值自然是很奇怪的一个数字了。

总结:C语言中的数据类型的本质,就是决定了这个数在内存中怎么存储的问题,也就是决定了这个数如何转成二进制的问题。一定要记住的一点是内存只是存储1010的序列,而不管这些1010怎么解析。所以要求我们平时数据类型不能瞎胡乱搞。

分析几个题目:

* 按照int类型存却按照float类型取        一定会出错

* 按照int类型存却按照char类型取        有可能出错也有可能不出错

* 按照short类型存却按照int类型取        有可能出错也有可能不出错

* 按照float类型存却按照double取            一定会出错

 

4.2 指针的数据类型的含义

  1. 指针的本质是:变量,指针就是指针变量
  2. 一个指针涉及2个变量:一个是指针变量自己本身,一个是指针变量指向的那个变量
  3. int *p;定义指针变量时,p(指针变量本身)是int *类型,*p(指针指向的那个变量)是int类型的。
  4. int *类型说白了就是指针类型,只要是指针类型就都是占4字节,解析方式都是按照地址的方式来解析(意思是里面存的32个二进制加起来表示一个内存地址)的。结论就是:所有的指针类型(不管是int * 还是char * 还是double *)的解析方式是相同的,都是地址。
  5. 对于指针所指向的那个变量来说,指针的类型就很重要了。指针所指向的那个变量的类型(它所对应的内存空间的解析方法)要取决于指针类型。譬如指针是int *的,那么指针所指向的变量就是int类型的。

     

4.3 指针数据类型转换实例分析1(int * -> char *)

  1. int和char类型都是整形,类型兼容的。所以互转的时候有时候错有时候对。
  2. int和char的不同在于char只有1个字节而int有4个字节,所以int的范围比char大。在char所表示的范围之内int和char是可以互转的不会出错;但是超过了char的范围后char转成int不会错(向大方向转就不会错,就好比拿小瓶子的水往大瓶子倒不会漏掉不会丢掉),而从int到char转就会出错(就好象拿大瓶子水往小瓶子倒一样)

     

4.4 指针数据类型转换实例分析2(int * -> float *)

之前分析过:int和float的解析方式是不兼容的,所以int *转成float *再去访问绝对会出错。

 

4.5 指针、数组与sizeof运算符

  1. sizeof是C语言的一个运算符(主要sizeof不是函数,虽然用法很像函数),sizeof的作用是用来返回()里面的变量或者数据类型占用的内存字节数。
  2. sizeof存在的价值?主要是因为在不同平台下各种数据类型所占的内存字节数不尽相同(譬如int在32位系统中为4字节,在16位系统中为2字节•••)。所以程序中需要使用sizeof来判断当前变量/数据类型在当前环境下占几个字节。

    实例一

sizeof(数组名)的时候,数组名不做左值也不做右值,纯粹就是数组名的含义。那么sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)。

    char str[] = "hello";

    printf("sizeof(str) = %d.\n", sizeof(str));        //6, 字符串均以'\0'结束,所以共有6个

    printf("sizeof(str[0]) = %d.\n", sizeof(str[0]));    //1,指就是数组第一个元素,因为是char类型

    printf("strlen(str) = %d.\n", strlen(str));        //5,返回的是除'\0'之外的字符个数

实例二

char str[] = "hello";

    char *p = str;

    printf("sizeof(p) = %d.\n", sizeof(p));        //4,p是地址,指向数组首元素首地址,地址都是4字节

    printf("sizeof(*p) = %d.\n", sizeof(*p));    //1,p是数组首元素首地址,*p自然就是首元素

    printf("strlen(p)= %d.\n", strlen(p));        //5,取得以p为首地址的字符串的长度

    32位系统中所有指针的长度都是4,不管是什么类型的指针。

strlen所作的仅仅是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,然后返回计数器值(长度不包含'\0')strlen是一个C库函数,用来返回一个字符串的长度(注意,字符串的长度是不计算字符串末尾的'\0'的)。一定要注意strlen接收的参数必须是一个字符串(字符串的特征是以'\0'结尾)    

实例三

    int n = 10;

    printf("sizeof(n) = %d.\n", sizeof(n));        //4

sizeof测试一个变量本身,和sizeof测试这个变量的类型,结果是一样的。

实例四

int b[100];

    printf("sizeof(b) = %d.\n", sizeof(b));        //400

sizeof(数组名)的时候,数组名不做左值也不做右值,纯粹就是数组名的含义。那么sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)。

实例五

void fun(int b[])

{

    printf("sizeof(b) = %d.\n", sizeof(b));

}

void main(void)

{

    int a[20];    

    fun(a);             //4,a在函数fun内就是个指针,不是数组

 

}    

---------------------------------------------------------------------------------------------

void fun(int *b)

{

    printf("sizeof(b) = %d.\n", sizeof(b));         

}    

void main(void)

{

    int a[20];        //4,a在fun内部就是个指针,不是数组

    fun(a);

}

数组作形参和指针作形参没有本质区别。

  1. 函数传参,形参是可以用数组的。
  2. 函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址。也就是说函数传参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。        
  3. 在实际应用中,我们把数组首地址传入函数中,同时也会把数组的大小传进去。因为我们把数组传入函数中后,函数中我们数组的大小就丢了,因为参数传递的是数组的首地址而已,并不知道大小。void fun(int *a, int num);

    其中num =sizeof(a);

    实例六

#define         dpchar    char *                //宏定义dpchar = char *,只是关联取代的作用,是位置取代

typedef        char *    tpchar;                //用户重定义数据类型,数据类型就是指针类型

 

void main(void)

{

    dpchar p1, p2;                            //实际定义是char *p1, p2;p1是个指针,p2是个字符

    printf("sizeof(p1) = %d. ", sizeof(p1));    //4,指针都是四个字节

    printf("sizeof(p2)= %d.\n ", sizeof(p2));    //1,变量本身就是数据类型本身

 

    tpchar p3, p4;                            //实际定义是char *p3;char *p4;

    printf("sizeof(p3) = %d. ", sizeof(p3));    //4, 指针都是四个字节

    printf("sizeof(p4)= %d.\n ", sizeof(p4));    //4, 指针都是四个字节

}

需要注意或者经常用的就是数组元素的个数:sizeof(a)/sizeof(a[0])

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值