一维数组
定义数组,数组名为a
,数组元素的数据类型为int
整型,数组长度为5
,数组是一个整体,它的内存是连续的
1
int a[5];
给每个元素赋值,index
索引从0
开始
1
2
3
4
5
a[0] = 1;
a[1] = 2;
a[2] = 3;
a[3] = 4;
a[4] = 5;
也可以在定义的同时进行赋值,数组初始化
1
2
3
4
5
6
7
int a[5] = {1, 2, 3, 4, 5};
int a[] = {1, 2, 3, 4, 5}; // 如果给全部元素赋值,可以省略数组长度
int a[5] = {1, 2, 3}; // 剩下的两个元素自动赋0值
int a[5] = {0}; // 全部元素赋0值
// 数组长度 length 最好是整数或者常量表达式,例如 10、20*4 等,这样在所有编译器下都能运行通过
// 如果没有进行显示初始化,static存储类的数组,系统会自动赋0值,auto存储类的数组,元素的值为垃圾值
访问数组元素,下标的取值范围为 0 ≤ index < length
,过大或过小都会越界,导致数组溢出,发生不可预测的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
int array1[5]; // static存储类,自动赋0值
static int array2[5]; // static存储类,作用域为本文件,自动赋0值
int main(){
int array3[5]; // auto存储类,垃圾值
static int array4[5]; // static存储类,自动赋0值
int i;
for(i=0; i<5; i++){
printf("array1[%d] = %d\n", i, array1[i]);
}
for(i=0; i<5; i++){
printf("array2[%d] = %d\n", i, array2[i]);
}
for(i=0; i<5; i++){
printf("array3[%d] = %d\n", i, array3[i]);
}
for(i=0; i<5; i++){
printf("array4[%d] = %d\n", i, array4[i]);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ gcc a.c
$ ./a.out
array1[0] = 0
array1[1] = 0
array1[2] = 0
array1[3] = 0
array1[4] = 0
array2[0] = 0
array2[1] = 0
array2[2] = 0
array2[3] = 0
array2[4] = 0
array3[0] = 4195856
array3[1] = 0
array3[2] = 4195392
array3[3] = 0
array3[4] = -803210736
array4[0] = 0
array4[1] = 0
array4[2] = 0
array4[3] = 0
array4[4] = 0
二维数组
所谓二维数组,就是数组的每个元素都是数组(实际上还是一维数组,应理解为逻辑上的二维数组)。
假设我们要存放 5 个同学的语数英分数,就可以使用一个 5 行 3 列的二维数组,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int main() {
/* 定义一个5行3列的二维数组 */
int a[5][3] = {
{98, 87, 89},
{78, 90, 76},
{79, 94, 84},
{67, 56, 69},
{55, 67, 98},
};
int i, j;
for (i=0; i<5; i++){
printf("Student %d: ", i+1);
for (j=0; j<3; j++){
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
1
2
3
4
5
6
7
8
$ gcc a.c
$ ./a.out
Student 1: 98 87 89
Student 2: 78 90 76
Student 3: 79 94 84
Student 4: 67 56 69
Student 5: 55 67 98
如果是给全部元素赋值,也可以省略{}
,结果是一样的
1
2
3
4
int a[2][2] = {{1, 2}, {3, 4}};
int a[2][2] = {1, 2, 3, 4};
int a[2][2] = {{0}, {0}}; // 全部元素赋0值
int a[][2] = {1, 2, 3, 4}; // 给全部元素赋值时,也可以省略第一维数组的长度
元素查询
对于无序数组,我们只能通过遍历整个数组进行查询
比如,我们输入一个数字,如果该数字在数组中,则打印其下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
int main(){
int nums[10] = {1, 10, 6, 296, 177, 23, 0, 100, 34, 999};
int num, i, index = -1;
printf("enter num: ");
scanf("%d", &num);
for(i=0; i<10; i++){
if(num == nums[i]){
index = i;
break;
}
}
if(index == -1){
printf("%d isn't in the array!\n", num);
}else{
printf("%d in the array, index: %d\n", num, index);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gcc a.c
$ ./a.out
enter num: 3
3 isn't in the array!
$ ./a.out
enter num: 6
6 in the array, index: 2
$ ./a.out
enter num: 100
100 in the array, index: 7
$ ./a.out
enter num: 11
11 isn't in the array!
对于有序数组,并不需要遍历所有元素,只需要遍历部分元素就可以判断给出的数字是否在数组中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
int main(){
int nums[10] = {1, 10, 66, 296, 377, 523, 660, 700, 734, 999};
int num, i, index = -1;
printf("enter num: ");
scanf("%d", &num);
for(i=0; i<10; i++){
if(nums[i] >= num){
if(nums[i] == num){
index = i;
break;
}
}
}
if(index == -1){
printf("%d isn't in the array!\n", num);
}else{
printf("%d in the array, index: %d\n", num, index);
}
return 0;
}
1
2
3
4
5
6
7
8
9
$ gcc a.c
$ ./a.out
enter num: 1
1 in the array, index: 0
$ ./a.out
enter num: 100
100 isn't in the array!
字符数组
由于c语言没有字符串数据类型,只能使用字符数组
或者字符串指针
来表示字符串string
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(void) {
char s1[3] = {'a', 'b', 'c'}; // size = 3
char s2[] = {'a', 'b', 'c'}; // size = 3
char s3[] = {"abc"}; // size = 4, "abc\0"
char s4[] = "abc"; // size = 4, "abc\0"
printf("s1-sizeof: %ld\n", sizeof(s1));
printf("s2-sizeof: %ld\n", sizeof(s2));
printf("s3-sizeof: %ld\n", sizeof(s3));
printf("s4-sizeof: %ld\n", sizeof(s4));
return 0;
}
录入字符串、打印字符串:
1
2
3
4
5
6
7
8
9
10
11
// 输出 printf() puts()
char str[] = "www.zfl9.com"; // 字符数组
char* string = "www.zfl9.com"; // 字符串指针(字符串常量)
printf("char[]: %s\tchar*: %s\n", str, string);
puts(str);
puts(string);
// 输入 scanf() gets()
char str[100];
scanf("%s", str); // 数组名代表第0个元素的地址,所以不需要&取地址符,scanf会自动添加\0字符
gets(str);
字符串字面量总是以'\0'
结尾,因此字符数组长度总是比字符串长度大 1,puts()
、printf()
扫描到'\0'
就结束输出了。
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
char str1[] = "www.zfl9.com";
char str2[] = "www\0.zfl9.com";
puts(str1);
puts(str2);
printf("%s\n", str1);
printf("%s\n", str2);
return 0;
}
1
2
3
4
5
6
7
$ gcc a.c
$ ./a.out
www.zfl9.com
www
www.zfl9.com
www
string.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// strlen(str) 返回字符串长度(不包括'\0'字符)
char str[] = "www.zfl9.com";
int len = strlen(str); // len = 12
// strcat(str1, str2) 将str2拼接在str1后面,并删除str1的'\0'字符,要保证str1有足够的空间,否则会越界
char str1[30] = "www.zfl9.com";
char str2[] = "www\0.zfl9.com";
strcat(str1, str2); // str1: "www.zfl9.comwww"
char* str = strcat(str1, str2); // strcat的返回值为str1的首地址,字符串指针str指向str1第一个字符的地址
printf("str: %s\n", str); // "www.zfl9.comwww"
// strcpy(str1, str2) 将str2复制到str1(覆盖),结尾'\0'一起复制
char str1[13] = "$";
char str2[] = "www.zfl9.com";
strcpy(str1, str2); // str1: "www.zfl9.com\0", str2: "www.zfl9.com\0"
// strcmp(str1, str2) 比较每个字符的 ASCII 码,从第 0 个字符开始比较,若相等,则比较下一个,
// 直到遇见不同字符,或者到末尾。如果 str1 和 str2 字符串相同,则返回 0,
// 如果 str1 大于 str2,则返回大于 0 的值,反之则返回小于 0 的值。
char str1[] = "abce";
char str2[] = "abcd";
strcmp(str1, str2); // 'e'比'd'的ASCII码大1,所以返回1
strcmp(str2, str1); // 'd'比'e'的ASCII码小1,所以返回-1
strcmp(str1, str1); // 返回0
静态数组
在C语言中,数组一旦定义之后,其占用的内存空间就是固定的,不能增加或删除元素,只能修改元素的值,这样的数组称之为静态数组
当使用下标访问数组元素时,如果下标大于数组长度或者小于0,就会发生越界
(out of bounds),小于0发生下限越界
(off normal lower),大于数组长度发生上限越界
(off normal upper)
C语言为了提高效率,并不会对数组越界进行检查,即使越界了,也能通过编译,只有在运行期间才可能会发生问题
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int a[3] = {1, 2, 3};
int i;
for(i=-2; i<5; i++){
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
$ gcc a.c
$ ./a.out
a[-2] = 4195392
a[-1] = 0
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 3
a[4] = 0
上面的程序运气好,没有出现什么大问题,但是,如果不小心访问了系统不允许访问的内存,程序就直接蹦了
1
2
3
4
5
6
7
#include <stdio.h>
int main(){
int a[3] = {1, 2, 3};
int num = a[100000];
return 0;
}
1
2
3
4
$ gcc a.c
$ ./a.out
[1] 54630 segmentation fault ./a.out
发生段错误
,因为访问了无法访问的内存,程序就被系统直接kill了
数组溢出
:当赋予数组的元素个数超过数组长度时,就会发生溢出
(overflow)
1
2
3
int a[3] = {1, 2, 3, 4, 5}; // 只会保存前3个元素,后2个被丢弃
// vc/vs发生数组溢出时,编译会报错,不能通过编译,但是gcc只是警告
// 对于字符数组,一定要留有足够的空间,以存放末尾的'\0'字符
变长数组
目前主要有三个c语言标准:C89/ANSI C、C99、C11
- 对于C89,基本上所有的编译器都支持,如微软的VC/VS,GNU的gcc
对于C99,老版本的VC/VS并不支持,而gcc很早就支持了
- 对于C89,必须使用数值常量指明数组长度,不能包含变量
- 对于C99,数组长度可以为变量,也就是可以在运行时确定长度,这种就叫做
变长数组
(vla)
注意,变长数组
仍属于静态数组
,一旦确定长度,就不可再次更改
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
unsigned int len;
printf("array len: ");
scanf("%d", &len);
char str[len];
printf("enter string: ");
scanf("%s", str);
printf("%s\n", str);
return 0;
}
1
2
3
4
5
6
$ gcc a.c
$ ./a.out
array len: 20
enter string: www.zfl9.com
www.zfl9.com
注意:变长数组不能在定义的同时进行初始化!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int max(int a, int b){
return a > b ? a : b;
}
int main(void){
int a = 2, b = 3;
int len = a * b;
int arr1[6] = {0}; // 正确
int arr2[2 * 3] = {0}; // 正确
int arr3[a * b] = {0}; // 错误,变长数组不能在定义的同时进行赋值!
int arr4[len]; // 正确
int arr5[max(a, b)]; // 正确
return 0;
}
动态数组
静态数组
由于要在创建数组的时候就确定长度,并且长度length
还不能为变量,只能是常数,有时候我们并不知道我们到底需要多长的数组,并且很多时候数组要能动态扩容的,这时候就需要动态数组
了
所谓动态数组
,其实就是在堆
(heap)中创建的数组,通过执行代码为其分配内存,我们自己负责内存的释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>
int main(){
int len, i;
int* array = NULL;
printf("array len: ");
scanf("%d", &len);
array = (int*)malloc(sizeof(int) * len);
if(array == NULL){
printf("分配内存失败!");
exit(1);
}
for(i=0; i<len; i++){
array[i] = i+100;
}
for(i=0; i<len; i++){
printf("array[%d] = %d\n", i, array[i]);
}
free(array);
array = NULL;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ gcc a.c
$ ./a.out
array len: 10
array[0] = 100
array[1] = 101
array[2] = 102
array[3] = 103
array[4] = 104
array[5] = 105
array[6] = 106
array[7] = 107
array[8] = 108
array[9] = 109
也可使用指针的方式赋值和取值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
int main(){
int len, i;
int* array = NULL;
int* arrayCp = NULL;
printf("array len: ");
scanf("%d", &len);
arrayCp = array = (int*)malloc(sizeof(int) * len);
if(array == NULL){
printf("分配内存失败!");
exit(1);
}
for(i=0; i<len; i++){
*arrayCp++ = i+100;
}
arrayCp = array;
for(i=0; i<len; i++){
printf("array[%d] = %d\n", i, *arrayCp++);
}
free(array);
arrayCp = array = NULL;
return 0;
}
1
2
3
4
5
6
7
8
9
$ gcc a.c
$ ./a.out
array len: 5
array[0] = 100
array[1] = 101
array[2] = 102
array[3] = 103
array[4] = 104
数组小结
数组
(Array)是一系列相同类型的数据的集合,可以是一维的、二维的、多维的,最常用的是一维数组
和二维数组
,多维数组较少用到。
数组的定义格式为:type array[length]
(此时分配内存)
type
为数据类型,array
为数组名,length
为数组长度
数组长度 length 最好是整数或者常量表达式,例如 10、20*4 等,这样在所有编译器下都能运行通过
数组在内存中占用一段连续的空间,数组名表示的是这段内存空间的首地址
访问数组中某个元素的格式为:array[index]
,0 <= index <= length-1
数组赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对单个元素赋值
int a[3];
a[0] = 3;
a[1] = 100;
a[2] = 34;
// 整体赋值 (不指明数组长度)
float b[] = { 23.3, 100.00, 10, 0.34 };
// 整体赋值 (指明数组长度)
int m[10] = { 100, 30, 234 };
// 字符数组赋值
char str1[] = "https://www.zfl9.com";
// 将数组所有元素都初始化为0
int arr[10] = {0};
char str2[20] = {0};
排序和查找
简单应用,给一个数组,计算其最大和最小值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
int main(){
int i, max, min, array[10] = {0};
// 初始化数组
printf("enter 10 nums: ");
for(i=0; i<10; i++){
scanf("%d", &array[i]);
}
// 假设最大值和最小值都为 array[0]
max = min = array[0];
// 查找最大值和最小值
for(i=1; i<10; i++){
if(array[i] > max){
max = array[i];
}
if(array[i] < min){
min = array[i];
}
}
printf("max: %d, min: %d\n", max, min);
return 0;
}
1
2
3
4
5
6
7
8
9
$ gcc a.c
$ ./a.out
enter 10 nums: 1 2 3 4 5 6 7 8 9 10
max: 10, min: 1
$ ./a.out
enter 10 nums: 2 123 45 100 575 240 799 710 10 90
max: 799, min: 2
数组与指针
数组名的本意是表示一组数据的集合,和普通变量一样,都用来指代一块内存;但在使用过程中,数组名有时候会转换成指向第0个元素的指针。
当数组名作为数组定义的标识符(也就是定义或声明数组时)
、sizeof
或 &
的操作数时,它才表示整个数组本身
;在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。
再谈数组下标[]
数组的下标与指针的偏移量相同,都是1个元素占用的字节数,如:
int a[] = {1, 2, 3, 4, 5}, *p, i = 2;
可以通过下面任何一种方式访问a[i]
:
p = a; p[i];
、p = a; *(p + i);
、p = a + i; *p;
对数组的引用a[i]
在编译时总是被编译器改写成*(a+i)
的形式,C语言标准也要求编译器必须具备这种行为
所以,对于数组a,如果要访问第3个元素的值,可以用a[3]
,也可以用3[a]
,都会改写为*(a+3)
、*(3+a)
,这并没有任何区别
数组作为函数参数
C语言标准规定,作为类型的数组
的形参应该调整为类型的指针
在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式
编译器只向函数传递数组的地址
,而不是整个数组的拷贝
这种隐式的转换,意味着下面的三种方式完全是等价的
1
2
3
void func(int *arr){ ...... }
void func(int arr[]){ ...... }
void func(int arr[5]){ ...... }
在函数内部,arr 会被转换成一个指针变量,编译器为 arr 分配 4 个字节(32位平台)的内存
用 sizeof(arr) 求得的是指针变量的长度,而不是数组长度
如果想在函数内知道数组的长度,必须由调用者来告诉他
数组和指针绝不等价
数组和指针不等价的一个典型案例就是求数组的长度(sizeof),这个时候只能使用数组名,不能使用指针。
数组名代表的是一组数据,对数组变量求长度,拿到的是这组数据的总长。
指针代表的是一个内存地址,对指针求长度,拿到的是指针变量的长度,而非指向的数据的长度。
站在编译器的角度讲,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。
变量名指代一份数据,数组名指代同一类型的多份连续数据,符号是有类型
的,以便推断其数据长度。
- 对于数组
int a[6] = {1, 2, 3, 4, 5, 6};
,其类型为int [6]
,表示一个拥有6个int数据的集合 - 对于指针
int *p;
,其类型为int *
,表示一个内存地址,该地址上存放了1个int数据
归根结底,a和p这两种符号的类型不同,指代的数据不同,用sizeof求得的长度自然不同。
sizeof 是一个操作符,不是函数,sizeof 表达式在编译时会被替换为 整型常量值。
与普通变量名相比,数组名既有一般性也有特殊性:
- 一般性表现在数组名可用来指代特定的内存块,有类型和长度
- 特殊性表现在数组名有时候会转换为指向第一个元素的指针