c语言的数组详解
- 一维数组
- 一维数组的声明
- 变长数组
- 一维数组的定义和初始化
- 一维数组的存储和访问
- 一维数组作为实参
- sizeof和数组
- 二维数组和多维数组
- 二维数组的声明和定义
- 多维数组的存储
- 二维数组的访问方式和sizeof
- 多维数组作为实参
- 二维变长数组
一维数组
用c语言解决问题时,一些问题需要申请很多同类型的变量,这时再用int a
、int b
这种一个变量一个变量地申请,代码会变得非常长。
因此有了数组。数组可以认为是数据结构的一种,数据结构是管理数据的一种方式,通过数据结构实现对数据的增、删、查、改,可以解决很多问题。
而且很多数据结构都是通过数组实现。
这里的干货可能有细节上的欠缺,毕竟目的是快速上手和复习使用的笔记,并不是查询用的字典。
一维数组的声明
首先是一维数组,一维数组是只用1个下标即可访问的数组。一维数组的声明:
Type array[num];
例如这个样例,声明了10个成员的数组,赋值后并输出:
#include<stdio.h>int main(){int a[10],i;for(i=0;i<10;i++){a[i]=i;printf("%d",a[i]);}return 0;
}
C语言规定:数组的每个成员都有一个下标,下标是从0开始的。
数组可以通过下标来访问的。这里int a[10]
表示声明10个成员的一维数组,它的下标从{0,1,2,3,4,5,6,7,8,9}
中选择。
int a[10]
相当于10个成员,每个成员相当于一个独立的变量。
一维数组声明后严格来说不能访问,除非使用数组存储过数据。经过声明的一维数组,访问它的成员得到的值是随机值。例如这里将字符数组最后一个成员初始化为空,然后用字符串的方式输出:
#include<stdio.h>int main() {char a[10];a[9] = '\0';printf("%s", a);return 0;
}
在vs它输出4个“烫”,在Dev-c++ 5.11输出的结果为T,但本质是为了输出未被初始化的值,这个值有的编译器会处理,而有的不会,因此会产生很多未定义行为例如程序崩溃。
未定义行为指代码执行了标准(C/C++规范)未明确定义的操作,导致程序的行为无法预测。
变长数组
数组严格来说不能用变量来声明或定义。
C99标准支持变长数组,数组的大小可以使用变量指定,但是数组不能初始化。
#include<stdio.h>int main(){int num=10;int a[num],i;for(i=0;i<10;i++){a[i]=i;printf("%d",a[i]);}return 0;
}
遗憾的是,vs系列的MSVC不支持变长数组,但是gcc编译器支持,支持gcc编译器的IDE有Dev-c++、使用mingW的vscode、绝大多数OJ平台等。
但需要注意,经过const
修饰的常量在c语言中不能用于声明数组。
#include<stdio.h>
const int N = 10;int main() {const int O = 9;int a[N];int b[O];return 0;
}
但是c++可以,因为c++对const做了一定的修改。
一维数组的定义和初始化
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
这里就有各种初始化的技巧。
#include<stdio.h>int main() {int i;int a[10] = {1,2,3};//不完全初始化:前3个成员初始化,其余用0初始化int b[10] = { 1 };//不完全初始化:除了b[0]初始化为1,取余都初始化为0int c[5] = { 1,2,3,4,5 };//完全初始化:所有成员都初始化int d[] = { 0,0,1,-1 };//让操作系统自己去计算数组的成员个数char e[3] = { '6',54,'6' };char f[10] = "asdfghjkl";//初始化为字符串,前提是字符+\0的长度小于等于数组长度char g[10] = { "3.141" };char h[] = "qwerty";//让系统自己去为字符串分配内存return 0;
}
数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的成员个数根据初始化的内容来确定。因此[]
内的内容可不指定,但一定要给初始值。
但是对于e[10]
和f[]
,若用printf
的%s
格式输出,因为e
没有用\0
结尾,会导致越界访问,f
有用\0
结尾可正常输出。
#include<stdio.h>int main() {char e[3] = { '6',54,'6' };char f[10] = "asdfghjkl";printf("%s\n", f);printf("%s", e);return 0;
}
输出结果之一(vs):
asdfghjkl
666烫烫甜v鳢?
因为%s
会一直输出字符,直到找到\0
为止。
所以e
在输出前3个字符后并没有找到\0
,于是继续在内存深处寻找,直到找到\0
。在找到\0
之前,访问到很多未被许可的空间,这些空间被初始化为特定的值,组合起来正好是某种文字的编码。比如在vs上所有空间统一初始化为0xcc
,而“烫”字的GBK编码又刚好是两个字节的0xcccc
,所以正好输出这个汉字。
例如通过char
字符串输出烫字:
#include<stdio.h>int main() {char e[] = { 0xcc,0xcc,'\0' };//烫字的GBK编码printf("%s", e);return 0;
}
一维数组的存储和访问
sizeof(数组名)
可以得到整个数组的大小,因此可以用sizeof(数组名)/sizeof(数组的某个成员)
得到数组的成员数。
通过这个例子可以观察数组在计算机中的存储。其中char
型变量使用的内存是 1 byte。
#include <stdio.h>
int main()
{char arr[10] = { '\0' };int i = 0;//得到数组的元素个数int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; ++i){printf("&arr[%d] = %p\n", i, &arr[i]);}return 0;
}
输出结果之一:
&arr[0] = 00CFFCF0
&arr[1] = 00CFFCF1
&arr[2] = 00CFFCF2
&arr[3] = 00CFFCF3
&arr[4] = 00CFFCF4
&arr[5] = 00CFFCF5
&arr[6] = 00CFFCF6
&arr[7] = 00CFFCF7
&arr[8] = 00CFFCF8
&arr[9] = 00CFFCF9
这说明数组的所有成员在内存中是连续存放的。
成员间的地址之差若不经过强制转换,会返回间隔的成员数。
#include <stdio.h>
int main()
{int arr[10] = { 0 };printf("%d\n", (&arr[6]) - (&arr[4]));printf("%d", (int)(&arr[6]) - (int)(&arr[4]));return 0;
}
输出:
2
8
知道了数组的存储机制,则可以用指针去访问数组。
#include <stdio.h>
int main()
{int a[10] = { 0,1,2,3,4,5,6,7,8,9 };int i = 0;int* p = &a[0];//三种访问方式是等同的for (i = 0; i < 10; i++)printf("%d ", *(a + i));printf("\n");for (i = 0; i < 10; i++)printf("%d ", *(p + i));printf("\n");for (i = 0; i < 10; i++)printf("%d ", a[i]);return 0;
}
一维数组作为实参
例如这个例子。
#include <stdio.h>void f1(int a[10]) {int sz = sizeof(a) / sizeof(a[0]);for(int i=0;i<sz;i++)printf("%d ",a[i]);printf("\n");
}void f2(int a[]) {int sz = sizeof(a) / sizeof(a[0]);for (int i = 0; i < sz; i++)printf("%d ", a[i]);printf("\n");
}int main()
{int a[10] = { 0,1,2,3,4,5,6,7,8,9 };f1(a);f2(a);return 0;
}
输出:
0
0
可以发现,将main
函数内的数组的数组名,作为实参上传给函数后,便无法通过sizeof(a) / sizeof(a[0])
获得数组的成员个数。
因为形参int a[10]
和int a[]
中的a
并不代表数组,而是代表同类型的指针。所以sizeof(a)
在这种情况下求的是指针本身的大小。指针本身的大小在vs的环境下根据配置是x86(32位)还是x64(64位),使用的内存大小是4和8。
这里用的是x86的环境,所以sz
实际等于1,也就是说只会访问a[0]
。
sizeof和数组
例如这个案例:
#include <stdio.h>void f(short int a[]) {printf("\n\nIn f:\n");printf("sizeof(a)=%d", sizeof(a));printf("\n");printf("sizeof(a[0])=%d", sizeof(a[0]));
}int main()
{short int a[10] = { 0,1,2,3,4,5,6,7,8,9 };printf("In main:\n");printf("sizeof(a)=%d", sizeof(a));printf("\n");printf("sizeof(a[0])=%d", sizeof(a[0]));f(a);return 0;
}
输出(vs的x86环境下):
In main:
sizeof(a)=20
sizeof(a[0])=2In f:
sizeof(a)=4
sizeof(a[0])=2
short [int]
是2个字节。[int]
表示可有可无。
和数组定义的作用域一样的情况下,sizeof(a)
是整个数组的大小,sizeof(a[0])
是成员的大小。因此输出 20 和 2 。
但到了f
之后,形参不再是数组而是普通的short
指针,因此sizeof(a)
返回的是指针的大小,而sizeof(a[0])
依旧是成员的大小。因此输出 4 和 2 。
二维数组和多维数组
多维数组是用多个个下标才可访问的数组。例如二维数组a
的访问方式是a[1][2]
,三维数组b
同理:b[1][2][3]
。
二维数组可以看成是一个矩阵,例如a[i][j]
,i
表示矩阵的行,j
表示矩阵的列。
多维数组和二维数组有很多相似之处,因此可以通过二维来推导多维数组的使用。
二维数组的声明和定义
例如这个例子:
#include <stdio.h>int main() {int a[2][2];//声明2*2的数组int b[2][2] = { 1 };//除b[0][0]初始化为1,其余成员均初始化为0int c[2][2] = { 1,2,3 };//4个元素,前3个初始化为{1,2,3}int d[3][3] = { {1,2,3},{4,5,6} };//初始化d[0]和d[1]的3个成员,其余用0初始化int e[3][3] = { {1,2},{4} };//除了e[0][0],e[0][1],e[1][0],其余用0初始化int f[][3] = { {2,3},{4} };//给初始化的话,行可以省略,但列不能省略,系统会自动计算行数return 0;
}
多维数组的声明和定义可以参考二维。
多维数组的存储
依旧是使用char
数组。
案例:
#include <stdio.h>int main() {char a[2][2] = { '\0' };for (int i = 0; i < 2; i++)for (int j = 0; j < 2; j++)printf("&a[%d][%d]=%p\n", i, j, &a[i][j]);printf("\n");char b[3][2][2] = { '\0' };for (int i = 0; i < 3; i++)for (int j = 0; j < 2; j++)for (int k = 0; k < 2; k++)printf("&c[%d][%d][%d]=%p\n", i, j, k, &b[i][j][k]);return 0;
}
输出结果之一:
&a[0][0]=004FFEDC
&a[0][1]=004FFEDD
&a[1][0]=004FFEDE
&a[1][1]=004FFEDF&c[0][0][0]=004FFEB0
&c[0][0][1]=004FFEB1
&c[0][1][0]=004FFEB2
&c[0][1][1]=004FFEB3
&c[1][0][0]=004FFEB4
&c[1][0][1]=004FFEB5
&c[1][1][0]=004FFEB6
&c[1][1][1]=004FFEB7
&c[2][0][0]=004FFEB8
&c[2][0][1]=004FFEB9
&c[2][1][0]=004FFEBA
&c[2][1][1]=004FFEBB
这说明,无论数组的维度有多少,数组的存储方式都是连续的。
二维数组的访问方式和sizeof
同样可以根据数组连续的特性进行数组的访问。
二维数组a[i][j]
中,a[i]
表示第i
行一维数组的首元素地址。
因此sizeof(a)
表示整个数组的大小(前提是a[i][j]
非形参),sizeof(a[0])
表示第0行一维数组的大小,sizeof(a[0][0])
才是这个数组的成员的大小。
#include <stdio.h>int main() {int a[3][3] = { 0,1,2,3,4,5,6,7,8 };printf("%d\n%d\n%d\n", sizeof(a), sizeof(a[0]), sizeof(a[0][0]));return 0;
}
输出:
36
12
4
通过非正常方式访问二维数组:
#include <stdio.h>int main() {int a[3][3] = { 0,1,2,3,4,5,6,7,8 };//正常访问for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", a[i][j]);printf("\n");}printf("\n");//二次解引用for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", *(*(a + i) + j));printf("\n");}//当成一维数组进行访问int* p = &a;printf("\n");for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", *(p + 3*i + j));printf("\n");}return 0;
}
其中二次解引用是因为二维数组的a[0]
、a[1]
等也相当于一维数组的地址。
根据这段代码:
//当成一维数组进行访问int* p = &a;printf("\n");for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", *(p + 3*i + j));printf("\n");}
因此多维数组可以用成员数量相等的一维数组代替。
但这样做会导致表达式变得非常长。例如一维数组实现和二维数组一样的访问方式:
*(数组名+每行列数*行+列)
。
#include <stdio.h>int main() {int a[9] = { 0,1,2,3,4,5,6,7,8 };for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", *(a + 3 * i + j));printf("\n");}return 0;
}
多维数组作为实参
首先想到的是作为形参的二维数组,数组名只是一个指针。
其次就是,形参的二维数组,可以省略第1个[]
内的数量但不能省略第2个[]
的数量。而且第2个[]
内需要是一个常量表达式。
#include <stdio.h>void f(int a[3][3]) {printf("sizeof(a)=%d\n", sizeof(a));
}
void f2(int a[][3]) {//行可以省略printf("sizeof(a)=%d\n", sizeof(a));
}int main() {int a[3][3] = { 0,1,2,3,4,5,6,7,8 };f(a);f2(a);return 0;
}
输出:
sizeof(a)=4
sizeof(a)=4
除了这种方式,还存在一种指向多维数组的指针。例如:
int a[3][3];
,指向它的指针就是int (*p)[3]
。因为[]
的优先级高于*
,所以需要加括号调整使p
先与*
结合。
或者说,可以这样理解:int (*)[3] p;
,即int(*)[3]
表示每行有3个元素的指向二维数组的指针p
。
因此二维数组作为形参还能这样表示:
#include <stdio.h>void f(int (*a)[3]) {for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", a[i][j]);printf("\n");}
}int main() {int a[3][3] = { 0,1,2,3,4,5,6,7,8 };f(a);return 0;
}
同理可以推广到三维或多维。但除了第1个[]
,后面的[]
内都不能省略。
#include <stdio.h>void f1(int a[3][3][3]) {for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {for (int k = 0; k < 3; k++) {a[i][j][k] = i * 3 * 3 + j * 3 + k * 3;printf("%d ", a[i][j][k]);}printf("\n");}printf("\n\n");}printf("\n\n\n");
}void f2(int a[][3][3]) {for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {for (int k = 0; k < 3; k++) {a[i][j][k] = i * 3 * 3 + j * 3 + k * 3;printf("%d ", a[i][j][k]);}printf("\n");}printf("\n\n");}printf("\n\n\n");
}void f3(int (*a)[3][3]) {for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {for (int k = 0; k < 3; k++) {a[i][j][k] = i * 3 * 3 + j * 3 + k * 3;printf("%d ", a[i][j][k]);}printf("\n");}printf("\n\n");}printf("\n\n\n");
}int main() {int a[3][3][3] = { 0 };f1(a);f2(a);f3(a);return 0;
}
二维变长数组
没错,二维数组也可以是变长数组,但不能初始化,vs同样不支持但gcc支持。前提是gcc支持C99。
#include <stdio.h>int main() {int r=3,c=3;int a[r][c];for(int i=0;i<3;i++){//for内的第1个表达式申请变量需要编译器支持C99 for(int j=0;j<3;j++){a[i][j]=i*3+j;printf("%d ",a[i][j]);}printf("\n");}return 0;
}