C语言数组详解

  • Post author:
  • Post category:其他


基本语法

数组是一组相同类型元素的集合。


声明数组

在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:

type arrayName[arraySize];

这叫做一维数组。

arraySize

必须是一个大于零的整数常量,

type

可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组

balance

,声明语句如下:

double balance[10];

现在

balance

是一个可用的数组,可以容纳10个类型为 double 的数字。


初始化数组

在 C 中,您可以先声明,然后逐个初始化数组:

balance[0] = 1000.0;
balance[1] = 2.0;
balance[2] = 3.4;
balance[3] = 7.0;
balance[4] = 50.0;

也可以声明的同时初始化,如下所示:

double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。

如果您省略掉了数组的大小,数组的大小则为初始化时元素的个数。因此,如果:

double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};

您将创建一个数组,它与前一个实例中所创建的数组是完全相同的。

但是要注意,如果是单独声明,不初始化,则必须指定数组长度。

#include <stdio.h>

int main()
{
	int a[];
	a[0] = 1;

    printf("Hello, World! %s\n", a[0]);
   
    return 0;
}

以上数组声明时未指定大小,则报错:


访问数组元素

数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:

double salary = balance[9];

补充和总结:

  • 数组是具有相同类型的集合,数组的大小(即所占字节数)由元素个数乘以单个元素的大小。
  • 数组只能够整体初始化,不能被整体赋值。只能使用循环从第一个逐个遍历赋值。
  • 初始化时,数组的维度或元素个数可忽略 ,编译器会根据花括号中元素个数初始化数组元素的个数。
  • 当花括号中用于初始化值的个数不足数组元素大小时,数组剩下的元素依次用0初始化。
  • 字符型数组在计算机内部用的时对应的ascii码值进行存储的。
  • 一般用”“引起的字符串,不用数组保存时,一般都被直接编译到字符常量区,并且不可被修改。

所有的数组都是由

连续的内存

位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

在这里插入图片描述

数组中的特定元素可以通过索引访问,第一个索引值为 0。


从内存角度来理解数组


从内存角度讲,数组变量就是一次分配多个变量,而且这多个变量在内存中的存储单元是依次相连接的。

我们分开定义多个变量(譬如int a, b, c, d;)和一次定义一个数组(int a[4]);这两种定义方法相同点是都定义了4个int型变量,而且这4个变量都是独立的单个使用的;不同点是单独定义时a、b、c、d在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是依次相连的。

数组中多个变量虽然必须单独访问,但是因为他们的地址彼此相连,因此很适合用指针来操作,由此可见数组和指针天生就纠结在一起。

数组名的含义

创建一个数组  :char a[10] ;

a作为右值 , 很多人估计也在学习的时候,估计会把它作为数组的地址,这是错误的 !

a作为右值时代表的意义和 &a[0]的意义是一样的,代表

数组首元素的首地址

,而不是数组的地址。

其实就可以理解成首元素的地址。因为各种数据类型对应的地址都是指首地址,比如int占4个字节,其地址就是第一个字节的地址。

上面说了a作为右值,我们清楚了其含义,那么a作为左值呢?

a不能作为左值 !!!编译器会认为数组名作为左值代表的是a的首元素的首地址,但是这个地址开始的一块内存是一个整体,我们只能访问数组的某个元素,而无法把数组数组当做一个整体来进行访问。所以,我们可以把a[i]当左值,无法把a当左值。也可以这么理解:a的内部是由很多小部分组成,我们只能通过访问这些小部分来达到访问a的目的。

进一步理解:

&a和a做右值时的区别:

&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。我的理解是,&a指向的是数组;a指向的是数组里的数。

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

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

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

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

sizeof和数组

创建一个数组  :char a[10] ;

sizeof(a)


数组名单独在sizeof内,表示整个数组,得到的是整个数组的字节数大小,即10字节。

sizeof(a+0)

此处数组名不是单独在sizeof内,那表示的就是首元素地址,+0,那还是首元素地址,存地址的指针变量大小是4个字节。


sizeof(a[0])

得到的是单个元素的字节数大小,即1字节。

sizeof(&a)

此处&a就代表整个数组的地址,但是地址啊,放指针变量里面的,所以还是4字节。

sizeof(*&a)

&a是a的地址,那*&a就代表整个数组了,所以是10字节。

sizeof(&a+1)

此处&a代表的是数组a的地址(整个数组),虽然数组地址和数组首元素地址的值是一样的,但代表的意义完全不相同。这里(&a+1),代表的是数组a尾元素后一位的那个元素地址。

如何计算一个数组的元素个数?

sizeof(a)/sizeof(a[0])

二维数组

一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:

type arrayName [x][y];

这个表示,有x个一维数组,每个一维数组的元素个数是y个。

比如int x[3][4]

可以进行如下赋值:

int x[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};

既然二维数组都可以用一维数组来表示,那二维数组存在的意义和价值在哪里?

明确告诉大家:二维数组a和一维数组b在内存使用效率、访问效率上是完全一样的(或者说差异是忽略不计的)。在某种情况下用二维数组而不用一维数组,原因在于二维数组好理解、代码好写、利于组织。

总结:我们使用二维数组(C语言提供二维数组),并不是必须,而是一种简化编程的方式。想一下,一维数组的出现其实也不是必然的,也是为了简化编程。

二维数组的应用和更多维数组

最简单情况,有10个学生成绩要统计;如果这10个学生没有差别的一组,就用b[10];如果这10个学生天然就分为2组,每组5个,就适合用int a[2][5]来管理。

最常用情况:一维数组用来表示直线,二维数组用来描述平面。数学上,用平面直角坐标系来比拟二维数组就很好理解了。

三维数组和三维坐标系来比拟理解。三维数组其实就是立体空间。

四维数组也是可以存在的,但是数学上有意义,现在空间中没有对应(因为人类生存的宇宙是三维的)。

总结:一般常用最多就到二维数组,三维数组除了做一些特殊与数学运算有关的之外基本用不到。(四轴飞行器中运算飞行器角度、姿态时就要用到三维数组)

补充


1、指向数组的指针

上面说了,数组名表示首元素的首地址。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。

一旦您把第一个元素的地址存储在 p 中,您就可以使用 *p、*(p+1)、*(p+2) 等来访问数组元素。下面的实例演示了上面讨论到的这些概念:

#include <stdio.h>
 
int main ()
{
   /* 带有 5 个元素的整型数组 */
   double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
   double *p;
   int i;
 
   p = balance;
 
   /* 输出数组中每个元素的值 */
   printf( "使用指针的数组值\n");
   for ( i = 0; i < 5; i++ )
   {
       printf("*(p + %d) : %f\n",  i, *(p + i) );
   }
 
   printf( "使用 balance 作为地址的数组值\n");
   for ( i = 0; i < 5; i++ )
   {
       printf("*(balance + %d) : %f\n",  i, *(balance + i) );
   }
 
   return 0;
}


2、传递数组给函数

如果您想要在函数中传递一个一维数组作为参数,您必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。

方式1:形式参数是一个指针

void myFunction(int *param)
{
    .
    .
    .
}

方式2:形式参数是一个已定义大小的数组

void myFunction(int param[10])
{
    .
    .
    .
}

方式3:形式参数是一个未定义大小的数组

void myFunction(int param[])
{
    .
    .
    .
}

可以看到,就函数而言,数组的长度是无关紧要的,因为C不会对形式参数执行边界检查。

C语言中,数组无法进行值传递。

数组作为形参,调用函数时,把数组名传递进去即可。

这是由C/C++函数的实现机制决定的。传数组给一个函数,数组类型自动转换为指针类型,因而传的实际是地址。直接传a即可,不要传&a,没有意义。

由此,传递数组的同时,传递数组的长度常常很有必要。

因为数组的这个问题,我就对结构体的传递产生了疑惑,其实,是我心里默认就想成了传递结构体的类型定义,但事实上,传递的从来都不是类型,而是结构体变量。所以,直接按照常规变量来传递即可。因为结构体类型就相当于int等类型,该定义变量就定义变量,该定义指针就定义指针。

在将数组作为函数参数的时候,有一个需要注意的问题,卡了我好几个小时。

问题如下,51单片机编程时,我将数组传入函数,然后在函数中求取数组的元素个数,如下:sizeof(arr) / sizeof(arr[0])

如果不涉及到函数传参,其实没有什么问题,但是一旦传入函数形参,然后再在函数里求取就有问题了,因为

数组名传入函数形参时,会自动退化为指针

。那么这时候sizeof(arr)得到的就不是整个数组的字节大小了,返回的只是一个指针的大小,显然,就会出现问题了。

C语言中只会以值拷贝的方式传递参数,当向函数传递数组时,却并不采用将整个数组拷贝一份传入函数的方法,而是采用将数组名看做常量指针传数组首元素地址。原因在于:C语言以高效作为最初设计目标来开发UNIX操作系统,且参数传递的时候如果拷贝整个数组执行效率将大大下降,参数位于栈上,太大的数组拷贝将导致栈溢出,总结两个缺点就是低效以及不安全。

有什么解决方法吗?那就是不要在函数中使用sizeof,而是用sizeof计算完成后,再传入函数。这里不是说不能在函数中计算数组的元素个数,是说不要在函数外定义数组,然后再传入后求个数,二者要放在同一个地方。


3、从函数返回数组

C 语言不允许返回一个完整的数组作为函数的参数。但是,您可以通过指定不带索引的数组名来返回一个指向数组的指针。

让我们来看下面的函数,它会生成 10 个随机数,并使用数组来返回它们,具体如下:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
/* 要生成和返回随机数的函数 */
int * getRandom( )
{
  static int  r[10];
  int i;
 
  /* 设置种子 */
  srand( (unsigned)time( NULL ) );
  for ( i = 0; i < 10; ++i)
  {
     r[i] = rand();
     printf( "r[%d] = %d\n", i, r[i]);
 
  }
 
  return r;
}
 
/* 要调用上面定义函数的主函数 */
int main ()
{
   /* 一个指向整数的指针 */
   int *p;
   int i;
 
   p = getRandom();
   for ( i = 0; i < 10; i++ )
   {
       printf( "*(p + %d) : %d\n", i, *(p + i));
   }
 
   return 0;
}

补充:字符数组

字符型数据是以字符的ACSII代码存储在代码单元格中的,一般占一个字节。由于ASCII代码也属于整数形式,所以C99标准中,把字符类型归纳为整形类型中的一种。


怎样定义字符数组?


用来存放字符型数据的数组称为字符型数组,在字符数组中一个元素内存放一个字符。定义字符型数组的方法与定义数值型数组的方法类似,例如:

char arr[10];

由于字符型数组是以整数形式存放的,也可以用整形数组来存放字符型数据,缺点就是浪费空间,一个字符只占一个字节,而一个整形数据占四个字节,将字符放在整形数组中会浪费空间。


字符数组的初始化

对字符型数组进行初始化,最容易理解的方法就是:

char arr[10]={'s','d','f','e','t','p','q','z','k','r'};

如果在定义字符数组时不进行初始化,那么数组中元素的值是不可预料的;

如果花括号中提供的初值个数大于数组长度,则出现语法错误;


如果初值个数小于数组长度,那么初值只会赋给前面的元素,后面的元素会自动赋值为空值,即‘\0’。


字符串和字符串结束标志


在C语言中,可以用字符数组来表示字符串。

在实际工作中,人们往往关心的是字符串的有效长度,而不是字符数组的长度。例如:定义一个字符数组长度为100,而字符串的长度为60。所以为了测字符串的实际长度,C语言规定了“字符串结束标志”,即‘\0’。

如果字符数组中有若干字符,前9个都不是空字符,而第10个是空字符,那么认为空字符之前是一个字符串,而字符串的有效字符为9个。

注意:C系统会在字符数组存储字符串常量时自动加一个‘\0’,作为字符串结束的标志,例如:“Cprogram”共八个字符,但其存放在一维数组中占9的字节,最后的‘\0’是系统自动加的。

对C语言处理字符串的方法有了了解之后,再补充一种字符数组初始化的方法,即用字符串常量来对字符数组进行初始化,例如:

char arr[]={"I am happy"};
char arr[]="I am happy";

以上两种方式均可,这里是用一个字符串作为初值,很显然这种方法直观,方便更符合人们的习惯。

注意:直接定义字符数组并不会视为字符串,也不会自动在末尾加上’\0’。当然,如果是手动在字符数组末尾加上’\0’,应该也会视为一个字符串,不过通常不会这么操作。

如果是以字符串的形式来初始化字符数组,那么就会在末尾自动加上’\0’。


注意区分字符串底层对字符数组的处理和字符数组本身:

#include <stdio.h>

int main()
{
	char a[] = {"good"};
	char b[] = {'g', 'o', 'o', 'd'};

    printf("Hello, World! %d\n", sizeof(a)); //5
	printf("Hello, World! %d\n", sizeof(b)); //4
   
    return 0;
}

虽然可以通过字符串的形式对字符数组进行赋值,但是二者还是有点区别的。要注意字符串最后的空字符‘\0’。


字符数组的输入输出可以有两种方法。

1)逐个字符输入输出。用格式符“%c”输入或输出一个字符。

2)将整个字符串输入或输出。用格式符“%s”输入或输出一整个字符串。

注意:

◆输出的字符串中不包括结束符“\0”。

◆用printf函数输出字符串时,输出项是字符数组的名字,而不是数组元素名。写成下面这样是不对的:   printf(“%s”,arr[0]);

◆如果一个字符串包括一个以上结束符“\0”,则遇到第一个就输出结束。

有个问题需要注意下,如果指定了数组长度,那么sizeof计算出来的大小就是指定的长度对应的字节数;如果字符串形式定义时没有指定数组长度,那么sizeof计算时就要按照字符串长度加上一个结束符的字节和。

没想明白这里是咋回事,不过分纠结了。

#include <stdio.h>

int main()
{
	char a[5] = "happy";
	
	printf("Hello, World! %c\n", a[4]); //输出y
	printf("Hello, World! %d\n", sizeof(a)); //这种情况输出是5
   
    return 0;
}
#include <stdio.h>

int main()
{
	char a[] = "happy";
	
	printf("Hello, World! %c\n", a[4]); //输出y
	printf("Hello, World! %d\n", sizeof(a)); //这种情况输出是6
   
    return 0;
}

上面指定了长度就输出5,下面没指定长度就输出6。

这里很奇怪,第一段代码的结束符‘\0’去哪了?

要知道,上面的方式是有安全隐患的。

另外要注意:用strlen计算字符串长度时,是不包括结束符的,计算的是字符串的有效长度。

关于上述的问题,看一个题目:

这里为什么CD选项不对?

如果按照上面的测试,是可以通过编译的。

其实,这里是已经越界了,数组形式的字符串,应该算上末尾的结束符\0的。

虽然不算上也可以成功,但是个很大的安全隐患。

连续内存的下标操作

今天看到一道题:

根据这个题,可以引出,如果有一块连续的内存,那么就可以按照地址+数组下标的方式去引用。


也就是说,如果有一块连续的内存,其开始地址为p,那么p[0]就表示地址开始处的值。

这里需要注意一个问题。

我测试的时候,仿照上述程序写了如下代码:

发现报错:

奇怪,题目中的程序可以++b,但是这里的a++报错了,改成++a或者a = a + 1;仍然报错。

想了很久,为什么?

对于数组的数组名来说,虽然其是个地址,但是它是个常量,++a改变了a的值,显然是不允许的。那么++b为什么可以呢?因为b是个局部变量,通过传参, 等于是将a的地址值复制了一份,再传给b,此时b是可以进行加减运算的。

为了验证这个问题,写了如下代码:

定义了一个指针来接收地址值:

运行成功:

另外要注意,下面这种操作也是有效的(但通常不推荐):



版权声明:本文为qq_28576837原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。