数组
对于Python开发者习以为常的列表数据类型在C语言中并不存在,存在的则是列表的原始版:数组。
数组是一种数据类型,需要在定义时指定其存储数据的类型与长度,然后这个数组将在内存中占用连续的内存空间,实现更简单地定义与更快速地连续读写。
定义数组
任何一种数据类型都可以定义对应的数组:
#include <stdio.h>
int main(void) {
int intArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double doubleArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
char charArray[10] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};
for (int i = 0; i < 10; i++) {
printf("%d, %lf, %c\n", intArray[i], doubleArray[i], charArray[i]);
}
return 0;
}
字符数组?
字符数组的另一种直截了当的称呼就是C语言中的字符串。我们在本页仅讨论数组的特性,而针对字符数组/字符串类型的特殊特性将在后面介绍。
格式控制符?
字符串"%d, %lf, %c\n"
中的%d
、%lf
和%c
是格式控制符,用来在打印时将对应的变量按照格式嵌入到字符串中。格式控制符将随字符串一起介绍。
在上面我们定义了整形、浮点与字符三种类型的数组,长度均为10,并且给出了值。然后依次打印,结果如下:
1, 1.000000, a
2, 2.000000, b
3, 3.000000, c
4, 4.000000, d
5, 5.000000, e
6, 6.000000, f
7, 7.000000, g
8, 8.000000, h
9, 9.000000, i
10, 10.000000, j
你也可以定义一个你自己建立的结构体的数组:
#include <stdio.h>
struct {
char name[20];
int age;
} typedef Student;
int main(void) {
int intArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double doubleArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
char charArray[10] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};
for (int i = 0; i < 10; i++) {
printf("%d, %lf, %c\n", intArray[i], doubleArray[i], charArray[i]);
}
Student students[10] = {{"Mango", 18}, {"FanFan", 18}};
for (int i = 0; i < 10; i++) {
printf("%s, %d\n", students[i].name, students[i].age);
}
return 0;
}
这一次,运行程序的输出增加了十行:
Mango, 18
FanFan, 18
, 0
, 0
, 0
, 0
, 0
, 0
, 0
, 0
结构体
如果你不知道结构体是什么,没关系因为这是后面的内容。
简单来说,结构体是在C语言中的自定义复杂数据类型,用来将具有逻辑关联的变量存储在一起,比如在上面的例子中,我们就将学生的姓名和年龄两个变量存储在一起。
初始化注意
看起来,C语言会为声明类型但未初始化赋值的变量设置默认值,比如students
数组中的后八个Student
结构体都变成了name="", age=0
的结构。
但是,不应该依赖这种默认,因为在不同的环境下,C语言编译器处理这种为初始化变量的方法可能有所不同。
所以在你的程序中,应当尽可能在声明变量时就给出初始值,或者默认值。
数组下标
继续观察上面的代码:
#include <stdio.h>
struct {
char name[20];
int age;
} typedef Student;
int main(void) {
int intArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double doubleArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
char charArray[10] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};
for (int i = 0; i < 10; i++) {
printf("%d, %lf, %c\n", intArray[i], doubleArray[i], charArray[i]);
}
Student students[10] = {{"Mango", 18}, {"FanFan", 18}};
for (int i = 0; i < 10; i++) {
printf("%s, %d\n", students[i].name, students[i].age);
}
return 0;
}
我们定义的所有数组的长度都是10,因此需要构建一个循环10次的操作来打印数组中的数据。我们构建一个for循环,定义格式为for (<1>; <2>; <3>) {...}
,其中<1>
是在进入循环时执行的操作,<2>
是在一次循环完成后是否继续循环的判断标志,<3>
是一次循环完成后、根据<2>
判断是否再次循环之前执行的操作。
在这里就是,定义一个循环计数i
,初始化为0,循环一次则+1,直到第十次循环结束后由于不满足条件而结束循环。这也是for循环最常见的用法,即循环固定次数。
理论上,如果需要循环十次,以下两种for循环写法是等价的,甚至第二种才是更符合普通人类的理解的:
for (int i = 0; i < 10; i++) {...}
for (int i = 1; i <= 10; i++) {...}
但是,在包括C语言在内的许多编程语言中,下标都是从0开始的。 在C语言中也就是说,数组的第一个数据的下标是0,第二个数据的下标是1,第十个数据的下标是9。
数组指针
注意!⚠️!
请务必严格区别数组指针与指针数组!
如果存在一些复杂的逻辑需要多次重复使用,我们可以将其从main
函数中剥离出来,封装成一个独立的函数,就像下面这样。
#include <stdio.h>
void printArray(int a[], int size) {
for (int i = 0; i < size; i++) {
printf("%d: %d\n", i+1, a[i]);
}
}
int main(void) {
int intArray[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
printArray(intArray, 10);
return 0;
}
与函数有关的更多知识将在后面介绍,这里我们不涉及函数本身,仅研究数组相关的特性。
在上面的代码中,当我们调用printArray
函数时,传入的参数是数组intArray
。除了直接传入intArray
之外,我们还可以传入数组指针。
如果你不清楚指针是什么,去看最前面的语言特性哦。
数组指针,即指向数组的内存地址的指针。数组指针指向的地址严格上说是数组第一个数据的地址,由于数组内的数据的内存地址是连续的,因此通过对数组指针进行操作,即可快速访问到数组中的每一个数据。
我们可以这样改写我们的代码:
#include <stdio.h>
void printArray(int a[], int size) {
void printArray(int (*p)[], int size) {
for (int i = 0; i < size; i++) {
printf("%d: %d\n", i+1, a[i]);
printf("%d: %d\n", i+1, (*p)[i]);
}
}
int main(void) {
int intArray[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
printArray(intArray, 10);
int (*p)[10] = &intArray;
printArray(*p, 10);
return 0;
}
先看main
函数中第14行的定义,&
是取地址符,&intArray
即数组intArray
的地址。我们定义的int (*p)[10]
即为数组指针,其中*p
是指针,int
是类型,[10]
是数组与其长度。()
的作用是确保p
先与*
运算,否则[]
的运算优先级比*
高,会定义出一个长度为10的整形指针数组,而不是一个指向长度为10的整形数组的指针。
除了定义时声明指针类型之外,*p
在其他地方表示对指针变量p
的解引用,即获取这个指针所指向的地址中存储的数据,例如*p
就是获取了intArray
这个数组。
然后你聪明的小脑袋瓜可能就发现了许多问题,让我们依次分析:
*p
和直接intArray
有什么区别?
没有区别。
我记得函数传数组类型参数时会自动取首地址?
会自动取首地址。也就是说,在调用printArray(intArray, 10);
时,intArray
作为数组会自动退化成其首指针,在地址上等同于数组intArray
的数组指针指向的地址、数组intArray
的首个数据的地址和intArray
自身的地址。
但是,C语言约定指向同一地址的指针必须同时具有相同的类型(如果是数组指针还需要有相同的长度)才能被视作类型相同的指针。因此,定义并传递指针参数而不是直接传递数组有助于更严格的代码规范要求。
如果你没看懂这两段话是什么意思,那么请看下面的代码讲解:由于在向函数传入数组类型参数时,C语言会自动取数组的首指针进行传递,因此以下两种定义函数printArray
的写法是等价的,且在用法上也是完全相同的:
void printArray(int a[], int size) {...}
void printArray(int (*p)[], int size) {...}
但是,C语言认为(*p)[]
和(*p)[10]
不是同一类型的指针,所以在你写出形如这样的代码时,会得到编译警告,或者甚至无法通过编译:
void printArray(int (*p)[], int size) {...}
int main(void) {
// 一些代码
int intArray[10] = {...}
int (*p)[10] = &intArray;
printArray(p, 10)
// 一些代码
return 0;
}
我为什么要传递数组指针?
既然传递数组指针与直接传递数组没有什么差别,那么为什么我们要使用数组指针呢?
答案是数组指针在其他的一些情况下有很大意义,比如当你使用多维数组时。多维数组会在后面介绍。
指针数组
注意!⚠️!
请务必严格区别数组指针与指针数组!
不同于数组指针,指针数组是与整形数组、字符数组等类似的数据结构,即一个装满了指针的数组。
定义指针数组非常简单 。如果你错误地定义了一个数组指针,那么你大概率就得到了一个正确的指针数组:
int (*p)[10] // 正确的数组指针
int *p[10] // 正确的指针数组
指针数组最显著的优势是在处理多个字符串时,我们会在下一页介绍C语言中的字符串。