C9 数组和指针

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的用法很相似,但却不完全相同。

  1. C++允许在声明数组大小时使用const整数,而C不允许
  2. 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};

数组的复合字面量格式:

(类型名) 初始化列表;