1. 数组
数组由一系列类型相同的元素组成。使用数组时,需要通过声明数组告诉编译器数据类型和元素数量。
int scores[5];
float temperatures[7];
声明数组后,可以通过下标(也称索引)访问数组中的各个元素。数组下标从0开始。
1.1 初始化数组
初始化数组可以通过花括号列表对其各个元素进行赋值。若列表项数小于数组长度,则未初始化的元素被设置为0。若列表项数大于数组长度,则编译器会报告错误。若省略方括号中的数组,则数组会自动匹配数组大小和初始化列表。
int a1[3] = {1, 2, 3}; // [1, 2, 3]
int a2[4] = {1, 2}; // [1, 2, 0, 0]
int a3[4] = {1, [2] = 3, 2}; // [1, 0, 3, 2]
int a4[4] = {1, 2, 3, [0] = 4}; // [4, 2, 3, 0]
int a5[] = {1, 2, 3, 4, 5}; // 等同于 a5[5] = {1, 2, 3, 4, 5};
int a6[] = {1, [10] = 3}; // 等同于 a6[11] = {1, [10] = 3};
⚠️注意:sizeof days是整个数组的大小,sizeof(day[0])是单个元素的大小,两个值相除可以得到数组元素的个数。
⚠️注意:当数组名作为参数传递给函数时。在函数内部数组形参会退化为指向其首元素的指针,此时sizeof返回的是指针变量的大小,而不是整个数组的大小,此时再用sizeof来计算数组长度就会出错。
1.2 指定初始化器
上面的初始化是按顺序初始化的,如果想要初始化指定的几个元素,应当使用指定初始化器(designed initializer,C99新增)。在出场线列表中使用带方括号的下标指明待初始化的元素:
int a[5] = {1, 2, 3, [4] = 5};
// 初始化后的数组内容为:[1, 2, 3, 0, 5]
当初始化列表与指定初始化器混用时:
int days[12] = {31, 28, [4] = 31, 30, 31, [1] = 29};
// 初始化后的数组内容为:[31, 29, 0, 0, 31, 30, 31, 0, 0, 0, 0, 0 ]
⚠️注意:
- 如果指定初始化器后有更多的值,那么这些值会被用于初始化指定元素后边
- 如果有多次初始化,则后初始化会覆盖前初始化
1.3 给数组元素赋值
声明数组后可使用数组下标(或索引)给数组元素赋值。
int arr[5];
arr[2] = 100;
arr[4] = 999;
1.4 数组边界
C不会检查数组下标是否超出数组边界,可以使用数组边界之外的下标,但访问的结果是未知的。
1.5 指定数组大小
在C99标准之前,声明数组只能在方括号中使用整型常量表达式且值不许大于0。
⚠️注意:sizeof表达式被视为整型常量,但const值不是(与C++不同)。
2. 多维数组
声明多维数组需要使用多个方括号,有几个维度就需要几个方括号。
int a[3][4];
double b[2][5][6];
2.1 初始化二维数组
初始化二维数组是建立在初始化一维数组的基础上。
int ar1[4] = {1, 2, 3, 4};
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}
这个初始化中使用3个数值列表,每个数值列表用一对花括号,每一行的初始化与初始化一维数组时相同,若数值数量小于数组长度,则未初始化的被设置为0,若数值数量长度大于数组长度,则会报错,但不会影响其他行的初始化。
初始化时可以省略内部的花括号,只保留外部的花括号,但此时就需要注意数值的顺序。
int arr1[3][4] = {
{1, 2, 3, 4}, {5, 6}, {9, 10, 11, 12}
};
//初始化结果:1, 2, 3, 4, 5, 6, 0, 0, 9, 10, 11, 12
int arr2[3][4] = {
1, 2, 3, 4, 5, 6, 9, 10, 11, 12
};
//初始化结果:1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 0, 0
3. 指针和数组
数组表示法是变相地使用指针。数组名就是数组首元素的地址。
int a[3] = {};
a == &a[0];
在我们的系统中,地址按字节编址,用一串十六进制数字表示,int类型占4字节,double占8字节。当int类型的指针和double类型的指针分别+1时,int类型的地址数字会+4,double类型的地址数字会+8。对指针而言,+1指的是增加一个储存单元,而不是字节数+1。对数组而言,+1是指向下一个元素的地址。
3.1 指针变量的基本操作
赋值、解引用、求解、与整数相加、与整数相减、递增、递减、指针求差、指针比较
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr1 = &arr[1]; // 赋值:ptr1指向arr[1]
int *ptr2 = &arr[3]; // 赋值:ptr2指向arr[3]
// 解引用
printf("*ptr1 = %d\n", *ptr1); // 输出: 20
// 取地址
printf("&arr[1] = %p, ptr1 = %p\n", &arr[1], ptr1);
// 与整数相加
ptr1 = ptr1 + 2; // ptr1现在指向arr[3]
printf("After +2: *ptr1 = %d\n", *ptr1); // 输出: 40
// 与整数相减
ptr1 = ptr1 - 1; // ptr1现在指向arr[2]
printf("After -1: *ptr1 = %d\n", *ptr1); // 输出: 30
// 递增
ptr1++; // ptr1现在指向arr[3]
printf("After ++: *ptr1 = %d\n", *ptr1); // 输出: 40
// 递减
ptr1--; // ptr1现在指向arr[2]
printf("After --: *ptr1 = %d\n", *ptr1); // 输出: 30
// 指针求差
ptrdiff_t diff = ptr2 - ptr1; // ptr2指向arr[3], ptr1指向arr[2]
printf("ptr2 - ptr1 = %td\n", diff); // 输出: 1
// 指针比较
if (ptr1 < ptr2) {
printf("ptr1 points to an element before ptr2\n");
}
return 0;
}
4. 函数、指针和数组
ANSI C规定,对形式参数使用const修饰数组,并不要求数组时常量,而是在处理过程中,将其当做是常量来看待,这意味着你无法通过指针来修改数组的值。
4.1 使用const修饰指针
int a[3] = {1, 2, 3};
const int* p1 = &a[0]; // 指向 const 的指针
int* const p2 = &a[1]; // const 指针
const int* const p3 = &a[2]; // 既是 const 指针,又是指向 const 的指针
- 对于
p1,不可以修改指针指向的值,但可以修改指针指向的位置。 - 对于
p2,可以修改指针指向的值,但不可以修改指针指向的位置。 - 对于
p3,不可以修改指针指向的值,也不可以修改指针指向的位置。
4.1.1 const的其他内容
关于指针赋值和const:
- 把
const数据或非const数据的地址初始化为const指针是合法的。 - 只能把非
const数据的地址赋给普通指针。
⚠️注意:C标准规定,使用非const标识符的形参修改const数据,导致的结果是未定义的。
4.2 指针表示法和数组表示法
数组名是指向数组首元素的指针。对于C语言,ar[i]和*(ar+i)是等价的,但是只有ar是指针变量时,才能使用递增或递减。
⚠️注意:为什么ar是数组名时不能使用递增或递减?
数组名是常量指针,不是指针变量,使用递增或递减时会对其进行赋值,这是不允许的。如果允许对数组名使用递增或递减的话,那么一个数组名就无法保证它指向的是数组首元素,因为它可能被修改。
5. 指针和多维数组
int arr[4][2];
arr是数组首元素的地址,它与&arr[0]的值相同,但含义不同。arr是一个占用两个int类型大小对象的地址,&arr[0]是一个占用一个int类型大小对象地址。- 对
arr和&arr[0]+1后的值不同。arr增加两个int大小,而&a[0]只增加一个int大小。 - 解引用或在数组名后加方括号的值不同,解引用
arr得到的是一个长度为二的int类型的数组的地址,解引用&arr[0]得到的是存储在对应位置上的值。**arr才和*&arr[0][0]等价。
5.1 指向多维数组的指针
int (* pz)[2]; // pz指向一个内含两个int类型的数组
为什么要加圆括号?因为方括号的优先级比星号高,如果不加圆括号,那么int * pz[2]会被理解为一个长度为二、储存指向int类型指针的数组。
5.2 指针的兼容性
指针之间的赋值比数值类型的赋值要更加严格。
int ar1[2][3] = {1, 2, 3};
int** p1;
p1指向的指针是指向int类型的指针,ar1是数组首元素的指针。
**ar1;//结果:1
虽然可以对ar1进行两次解引用,但不能将ar1赋值给p1。
p1 = ar1; // 报错,指向的类型不相关
ar1是指向数组的指针,ar1[0]才是指向int的指针,因此可以将ar1[0]赋值给*p1。
int* ptr; // 仅用于给**p1初始化
int **p1 = &ptr;
*p1 = ar1[0];
⚠️注意:Cconst和C++const C和C++中const的用法很相似,但却不完全相同。
- C++允许在声明数组大小时使用
const整数,而C不允许 - C++不允许将
const指针赋给非const指针,而C允许,但是使用非const指针修改const变量会发生什么是未定义的
6. 变长数组(VLA)
C规定数组的维数必须是常量,不能用变量。C99新增了变长数组(variable-length array, VLA),允许使用变量声明数组的维度。
⚠️注意:变长数组不能在声明后改变数组的大小,变长数组可以使用变量来声明数组的长度。
6.1 变长数组的形参声明
int sum2d(int rows, int cols, int ar[rows][cols]);
⚠️注意:C99/C11标准规定可以在函数原型中的形参名,但必须用星号代替省略的维度。
int sum2d(int, int, int ar[*][*]);
7. 复合字面量
字面量,符号常量外的常量。
// 基本类型的字面量
5 // int 类型字面量
81.3 // double 类型字面量
'y' // char 类型字面量
"elephant" // 字符串字面量
C99新增了复合字面量。对于数组,复合字面量类似初始化列表。
`int diva[2] = {10, 20};`
(int [2]) {10, 20};
数组的复合字面量格式:
(类型名) 初始化列表;