Skip to content

数组

概述

在程序设计中,为了⽅便处理数据把具有相同类型的若⼲变量按有序形式组织起来——称为数组。

同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。
  • 元素类型角度:数组是相同类型的变量的有序集合
  • 内存角度:连续的一⼤⽚内存空间

数组属于构造数据类型:

  • 一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型构造类型

    c
    int a[10]; 
    struct Stu boy[10];
  • 按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别。

    c
    int a[10];
    char s[10];
    char *p[10];

通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组、四维数组等。通常情况下,

一维数组

定义和使用

  • 数组名字符合标识符的书写规定(数字、英文字母、下划线)
  • 数组名不能与其它变量名相同,
  • []中常量表达式表⽰数组元素的个数
    • int a[3] 表⽰数组a有3个元素
    • 其下标从0开始计算,因此3个元素分别为 a[0]a[1]a[2]
  • `[]` `[]`
c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int a[10];//定义了一个数组,名字叫a,有10个成员,每个成员都是int类型
    //a[0]…… a[9],没有a[10]
    //没有a这个变量,a是数组的名字,但不是变量名,它是常量
    a[0] = 0;
    //……
    a[9] = 9;

    int i = 0;
    for (i = 0; i < 10; i++) {
        a[i] = i;//给数组赋值
    }

    //遍历数组,并输出每个成员的值
    for (i = 0; i < 10; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");

    return 0;
}

程序输出:

shell
0 1 2 3 4 5 6 7 8 9

初始化

在定义数组的同时进行赋值,称为

c
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有 成员变量 
int a[10] = { 1, 2, 3 };//初始化前三个成员,后⾯所有元素都设置为0
int a[10] = { 0 };//所有的成员都设置为0

//[]中不定义元素个数,定义时必须初始化 
int a[] = { 1, 2, 3, 4, 5 };//定义了一个数组,有5个成员

数组名

考虑下⾯这些声明:

c
int a; 
int b[10];

我们把a称作标量,因为它是个单一的值,这个变量是的类型是一个整数。我们把b称作数组,因为它是一些值的集合。下标和数名一起使用,用于标识该集合中某个特定的值。例如,b[0]表⽰数组b的第1个值,b[4]表⽰第5个值。每个值都是一个特定的标量。

那么问题是b的类型是什么?它所表⽰的又是什么?一个合乎逻辑的答案是它表⽰整个数组,但事实并非如此。在C中,在⼏乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。

请问:指针和数组是等价的吗?

答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:

  • 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
  • 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
c
int arr[10]; 
//arr = NULL; //arr作为指针常量,不可修改 
int *p = arr; //此时arr作为指针常量来使用 
printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度 
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*

数组名是,代表数组中

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
    printf("a = %p\n", a);
    printf("&a[0] = %p\n", &a[0]);

    size_t n = sizeof(a);    //数组占用内存的⼤⼩,10个int类型,10 * 4 = 40
    size_t n0 = sizeof(a[0]);//数组第0个元素占用内存⼤⼩,第0个元素为int,4
    printf("n = %zu, n0 = %zu\n", n, n0);

    int i = 0;
    for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
        printf("%d ", a[i]);
    }
    printf("\n");

    return 0;
}

程序输出:

shell
a = 0000009BC2CFF708
&a[0] = 0000009BC2CFF708
n = 40, n0 = 4
1 2 3 4 5 6 7 8 9 10

下标引用

c
int arr[] = { 1, 2, 3, 4, 5, 6 };

*(arr + 3),这个表达式是什么意思呢?

⾸先,我们说数组在表达式中是一个指向整型的指针,所以此表达式表⽰arr指针向后移动了3个元素的长度。然后通过间接访问操作符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程完全相同。所以如下表达式是等同的:

c
*(arr + 3) 
arr[3]

问题1:数组下标可否为负值?

问题2:请阅读如下代码,说出结果:

c
int arr[] = { 5, 3, 6, 8, 2, 9 };
int *p = arr + 2; 
printf("*p = %d\n", *p); 
printf("*p = %d\n", p[-1]);

那么是用下标还是指针来操作数组呢?对于⼤部分人而⾔,下标的可读性会强一些。

数组和指针

指针和数组并不是相等的。为了说明这个概念,请考虑下⾯两个声明:

c
int a[10];
int *b;

声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本⾝分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。

因此,表达式*a是完全合法的,但是表达式*b却是非法的。*b将访问内存中一个不确定的位置,将会导致程序终⽌。另一⽅⾯b++可以通过编译,a++却不行,因为a是一个常量值。

作为函数参数的数组名

当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢?我们现在知道数组名其实就是一个指向数组第1个元素的指针,所以很明⽩此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新⼿容易上⼿一些,编译器也接受数组形式的函数形参。因此下⾯两种函数原型是相等的:

c
int print_array(int *arr); 
int print_array(int arr[]);

我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样sizeof arr值是指针的长度,而不是数组的长度。

现在我们清楚了,为什么一维数组中⽆须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一⽅⾯,这种⽅式使得函数⽆法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。

强化训练

求数组中的最大值

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int a[] = {1, -2, 3, -4, 5, -6, 7, -8, -9, 10};//定义一个数组,同时初 始化所有成员变量

    int i = 0;
    int max = a[0];
    for (i = 1; i < sizeof(a) / sizeof(a[0]); i++) { // 使用sizeof(a) / sizeof(a[0] 求数组的长度
        if (a[i] > max) {}
        max = a[i];
    }
    printf("数组中最⼤值为:%d\n", max);

    return 0;
}

程序输出:

shell
数组中最⼤值为:10

一维数组的逆置

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
    int len = sizeof(a) / sizeof(a[0]);
    printf("原始数组:");
    for (int i = 0; i < len; i++) {
        printf("%d ", a[i]);
    }
    putchar('\n');

    // 进行逆序操作
    int tmp;
    for (int i = 0; i <= len / 2; ++i) {
        tmp = a[i];
        a[i] = a[len - i - 1];
        a[len - i - 1] = tmp;
    }

    // 打印逆序后的数组
    printf("逆序后的数组:");
    for (int i = 0; i < len; i++) {
        printf("%d ", a[i]);
    }
    putchar('\n');

    return 0;
}

程序输出:

shell
原始数组:1 2 3 4 5 6 7 8 9 10
逆序后的数组:10 9 8 7 5 6 4 3 2 1

冒泡法排序

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int a[] = {1, -2, 3, -4, 5, -6, 7, -8, -9, 10};//定义一个数组,同时初 始化所有成员变量

    int i = 0;
    int j = 0;
    int n = sizeof(a) / sizeof(a[0]);
    int tmp;

    //1、流程
    //2、试数

    for (i = 0; i < n - 1; i++) {
        //内循环的目的是比较相邻的元素,把⼤的放到后⾯
        for (j = 0; j < n - i - 1; j++) {
            if (a[j] > a[j + 1]) {
                tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
            }
        }
    }
    for (i = 0; i < n; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");

    return 0;
}

程序输出:

shell
-9 -8 -6 -4 -2 1 3 5 7 10

二维数组

如果某个数组的维数不⽌1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。

c
void test01(){ 
    //二维数组初始化
    int arr1[3][3] = {
        { 1, 2, 3 }, 
        { 4, 5, 6 }, 
        { 7, 8, 9 } 
    };
    
    int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    //打印二维数组
    for (int i = 0; i < 3; i++){
        for (int j = 0; j < 3; j ++){
            printf("%d ",arr1[i][j]);
        } 
        printf("\n");
    }
}

定义和使用

二维数组定义的一般形式是类型说明符 数组名[常量表达式1][常量表达式2]

其中常量表达式1表⽰第一维下标的长度,常量表达式2 表⽰第二维下标的长度。

命名规则同一维数组, int a[3][4]; 定义了一个三行四列的数组,数组名为a其元素类型为整型,该数组的元素个数为3×4个,即:

二维数组a是按行进行存放的,先存放a[0]行,再存放a[1]行、a[2]行,并且每行有四个元素,也是依次存放的。

二维数组在概念上是二维的:其下标在两个⽅向上变化,对其访问一般需要两个下标。

在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,,即放完一行之后顺次放⼊第二行,和一维数组存放⽅式是一样的。

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    //定义了一个二维数组,名字叫a
    //由3个一维数组组成,这个一维数组是int [4]
    //这3个一维数组的数组名分别为a[0],a[1],a[2]
    int a[3][4];
    a[0][0] = 0;
    //……
    a[2][3] = 12;

    //给数组每个元素赋值
    int i = 0;
    int j = 0;
    int num = 0;

    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++) {
            a[i][j] = num++;
        }
    }

    //遍历数组,并输出每个成员的值
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++) {
            printf("%d, ", a[i][j]);
        }
        printf("\n");
    }

    return 0;
}

程序输出:

shell
0, 1, 2, 3, 
4, 5, 6, 7, 
8, 9, 10, 11,

初始化

c
//分段赋值 int a[3][4] = {{ 1, 2, 3, 4 },{ 5, 6, 7, 8, },{ 9, 10, 11, 12 }};
int a[3][4] ={
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8, },
    { 9, 10, 11, 12 } 
};

//连续赋值
int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12 };

//可以只给部分元素赋初值,未初始化则为0
int a[3][4] = { 1, 2, 3, 4 };

//所有的成员都设置为0
int a[3][4] = { 0 };

//[]中不定义元素个数,定义时必须初始化
int a[][4] = { 1, 2, 3, 4, 5, 6, 7, 8 };

数组名

一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:

c
int arr[3][10]

可以理解为这是一个一维数组,包含了3个元素,只是每个元素恰好是包含了10个元素的数组。arr就表⽰指向它的第1个元素的指针,所以arr是一个指向了包含了10个整型元素的数组的指针。

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    //定义了一个二维数组,名字叫a
    //二维数组是本质上还是一维数组,此一维数组有3个元素
    //每个元素又是一个一维数组int[4]
    int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

    //数组名为数组⾸元素地址,二维数组的第0个元素为一维数组 //第0个一维数组的数组名为a[0]
    printf("a = %p\n", a);
    printf("a[0] = %p\n", a[0]);

    //测二维数组所占内存空间,有3个一维数组,每个一维数组的空间为4*4
    //sizeof(a) = 3 * 4 * 4 = 48
    printf("sizeof(a) = %d\n", sizeof(a));

    //测第0个元素所占内存空间,a[0]为第0个一维数组int[4]的数组名,4*4=16
    printf("sizeof(a[0]) = %d\n", sizeof(a[0]));

    //测第0行0列元素所占内存空间,第0行0列元素为一个int类型,4字节
    printf("sizeof(a[0][0]) = %d\n", sizeof(a[0][0]));

    //求二维数组行数
    printf("i = %d\n", sizeof(a) / sizeof(a[0]));

    // 求二维数组列数
    printf("j = %d\n", sizeof(a[0]) / sizeof(a[0][0]));

    //求二维数组行*列总数
    printf("n = %d\n", sizeof(a) / sizeof(a[0][0]));

    return 0;
}

程序输出:

shell
a = 0x7fffffffe4a0
a[0] = 0x7fffffffe4a0
sizeof(a) = 48
sizeof(a[0]) = 16
sizeof(a[0][0]) = 4
i = 3
j = 4
n = 12

数组指针

数组指针,它是指针,是指向数组的指针。

数组的类型由元素类型和数组⼤⼩共同决定:int array[5] 的类型为 int[5];C语⾔可通过 typedef定义一个数组类型:

定义数组指针有一下三种⽅式:

c
//⽅式一
void test01() {
    //先定义数组类型,再用数组类型定义数组指针
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    //有typedef是定义类型,没有则是定义变量,下⾯代码定义了一个数组类型ArrayType
    typedef int(ArrayType)[10];
    //int ArrayType[10]; //定义一个数组,数组名为ArrayType

    ArrayType myarr;       //等价于 int myarr[10];
    ArrayType *pArr = &arr;//定义了一个数组指针pArr,并且指针指向数组arr

    for (int i = 0; i < 10; i++) {
        printf("%d ", (*pArr)[i]);
    }
    printf("\n");
}

//⽅式二
void test02() {
    int arr[10];//定义数组指针类型
    typedef int(*ArrayType)[10];
    ArrayType pArr = &arr;//定义了一个数组指针pArr,并且指针指向数组arr

    for (int i = 0; i < 10; i++) {
        (*pArr)[i] = i + 1;
    }

    for (int i = 0; i < 10; i++) {
        printf("%d ", (*pArr)[i]);
    }
    printf("\n");
}

//⽅式三
void test03() {
    int arr[10];
    int(*pArr)[10] = &arr;

    for (int i = 0; i < 10; i++) {
        (*pArr)[i] = i + 1;
    }

    for (int i = 0; i < 10; i++) {
        printf("%d ", (*pArr)[i]);
    }
    printf("\n");
}

指针数组

指针数组,数组中的元素为指针。

栈区指针数组

c
//数组做函数函数,退化为指针
void array_sort(char **arr, int len) {
    for (int i = 0; i < len; i++) {
        for (int j = len - 1; j > i; j--) {
            //比较两个字符串
            if (strcmp(arr[j - 1], arr[j]) > 0) {
                char *temp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

//打印数组
void array_print(char **arr, int len) {
    for (int i = 0; i < len; i++) {
        printf("%s\n", arr[i]);
    }
    printf("----------------------\n");
}

void test() {
    //主调函数分配内存
    //指针数组
    char *p[] = {"bbb", "aaa", "ccc", "eee", "ddd"};
    //char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误

    int len = sizeof(p) / sizeof(char *);
    //打印数组
    array_print(p, len);
    //对字符串进行排序
    array_sort(p, len);
    //打印数组
    array_print(p, len);
}

堆区指针数组

c
//分配内存
char **allocate_memory(int n) {
    if (n < 0) { return NULL; }
    char **temp = (char **) malloc(sizeof(char *) * n);
    if (temp == NULL) { return NULL; }

    //分别给每一个指针malloc分配内存
    for (int i = 0; i < n; i++) {
        temp[i] = malloc(sizeof(char) * 30);
        sprintf(temp[i], "%2d_hello world!", i + 1);
    }
    return temp;
}

//打印数组
void array_print(char **arr, int len) {
    for (int i = 0; i < len; ++i) {
        printf("%s\n", arr[i]);
    }
    printf("----------------------\n");
}

//释放内存
void free_memory(char **buf, int len) {
    if (buf == NULL) { return; }
    for (int i = 0; i < len; ++i) {
        free(buf[i]);
        buf[i] = NULL;
    }
    free(buf);
}

void test() {
    int n = 10;
    char **p = allocate_memory(n);
    //打印数组
    array_print(p, n);
    //释放内存
    free_memory(p, n);
}

线性存储特性

c
void PrintArray(int *arr, int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

//二维数组的线性存储
void test() {
    int arr[][3] = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
    };
    int arr2[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int len = sizeof(arr2) / sizeof(int);

    //如何证明二维数组是线性的?
    //通过将数组⾸地址指针转成Int*类型,那么步长就变成了4,就可以遍历整个数组
    int* p = (int*)arr;
    for (int i = 0; i < len; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");

    PrintArray((int *) arr, len);
    PrintArray((int *) arr2, len);
}

三种参数形式

cpp
//二维数组的第一种形式
void PrintArray01(int arr[3][3]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
        }
    }
}

//二维数组的第二种形式
void PrintArray02(int arr[][3]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
        }
    }
}

//二维数组的第二种形式
void PrintArray03(int (*arr)[3]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
        }
    }
}

void test() {
    int arr[][3] = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}};

    PrintArray01(arr);
    PrintArray02(arr);
    PrintArray03(arr);
}

强化训练

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    //二维数组: 五行、三列
    //行代表人: ⽼⼤到⽼五
    //列代表科目:语、数、外

    float a[5][3] = {{80, 75, 56}, {59, 65, 71}, {59, 63, 70}, {85, 45, 90}, {76, 77, 45}};
    int i, j, person_low[3] = {0};
    float s = 0, lesson_aver[3] = {0};

    for (i = 0; i < 3; i++) {
        for (j = 0; j < 5; j++) {
            s = s + a[j][i];
            if (a[j][i] < 60) { person_low[i]++; }
        }
        lesson_aver[i] = s / 5;
        s = 0;
    }

    printf("各科的平均成绩:\n");

    for (i = 0; i < 3; i++) {
        printf("%.2f\n", lesson_aver[i]);
    }
    printf("各科不及格的人数:\n");

    for (i = 0; i < 3; i++) {
        printf("%d\n", person_low[i]);
    }

    return 0;
}

程序输出:

shell
各科的平均成绩:
71.80
65.00
66.40
各科不及格的人数:
2
1
2

多维数组

多维数组的定义与二维数组类似,其语法格式具体如下:数组类型修饰符 数组名 [n1][n2]…[nn];

int a[3][4][5]; 定义了一个三维数组,数组的名字是a,数组的长度为3,每个数组的元素又是一个二维数组,这个二维数组的长度是4,并且这个二维数组中的每个元素又是一个一维数组,这个一维数组的长度是5,元素类型是int。

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    //int a[3][4][5] ;//定义了一个三维数组,有3个二维数组int[4][5]
    int a[3][4][5] = {{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {0}, {0}}, {{0}, {0}, {0}, {0}}, {{0}, {0}, {0}, {0}}};

    int i, j, k;
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++) {
            for (k = 0; k < 5; k++) {
                //添加访问元素代码
                printf("%d, ", a[i][j][k]);
            }
            printf("\n");
        }
    }

    return 0;
}

程序输出:

shell
1, 2, 3, 4, 5, 
6, 7, 8, 9, 10, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 
0, 0, 0, 0, 0,

总结

编程提示

  • 源代码的可读性⼏乎总是比程序的运行时效率更为重要
  • 只要有可能,函数的指针形参都应该声明为const
  • 在多维数组的初始值列表中使用完整的多层花括号提⾼可读性

内容总结

在绝⼤多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则只有两个例外, sizeof和对数组名&。

指针和数组并不相等。当我们声明一个数组的时候,同时也分配了内存。但是声明指针的时候,只分配容纳指针本⾝的空间。

当数组名作为函数参数时,实际传递给函数的是一个指向数组第1个元素的指针。

我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。