数组
概述
在程序设计中,为了⽅便处理数据把具有相同类型的若⼲变量按有序形式组织起来——称为数组。
同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。- 元素类型角度:数组是相同类型的变量的有序集合
- 内存角度:连续的一⼤⽚内存空间
数组属于构造数据类型:
一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型或构造类型。
cint a[10]; struct Stu boy[10];
按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别。
cint a[10]; char s[10]; char *p[10];
通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组、四维数组等。通常情况下,
一维数组
定义和使用
- 数组名字符合标识符的书写规定(数字、英文字母、下划线)
- 数组名不能与其它变量名相同,
[]
中常量表达式表⽰数组元素的个数int a[3]
表⽰数组a有3个元素- 其下标从0开始计算,因此3个元素分别为
a[0]
,a[1]
,a[2]
- `[]` `[]`
#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;
}
程序输出:
0 1 2 3 4 5 6 7 8 9
初始化
在定义数组的同时进行赋值,称为。
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个成员
数组名
考虑下⾯这些声明:
int a;
int b[10];
我们把a称作标量,因为它是个单一的值,这个变量是的类型是一个整数。我们把b称作数组,因为它是一些值的集合。下标和数名一起使用,用于标识该集合中某个特定的值。例如,b[0]表⽰数组b的第1个值,b[4]表⽰第5个值。每个值都是一个特定的标量。
那么问题是b的类型是什么?它所表⽰的又是什么?一个合乎逻辑的答案是它表⽰整个数组,但事实并非如此。在C中,在⼏乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。
请问:指针和数组是等价的吗?
答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:
- 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
- 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
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*
数组名是,代表数组中。
#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;
}
程序输出:
a = 0000009BC2CFF708
&a[0] = 0000009BC2CFF708
n = 40, n0 = 4
1 2 3 4 5 6 7 8 9 10
下标引用
int arr[] = { 1, 2, 3, 4, 5, 6 };
*(arr + 3)
,这个表达式是什么意思呢?
⾸先,我们说数组在表达式中是一个指向整型的指针,所以此表达式表⽰arr指针向后移动了3个元素的长度。然后通过间接访问操作符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程完全相同。所以如下表达式是等同的:
*(arr + 3)
arr[3]
问题1:数组下标可否为负值?
问题2:请阅读如下代码,说出结果:
int arr[] = { 5, 3, 6, 8, 2, 9 };
int *p = arr + 2;
printf("*p = %d\n", *p);
printf("*p = %d\n", p[-1]);
那么是用下标还是指针来操作数组呢?对于⼤部分人而⾔,下标的可读性会强一些。
数组和指针
指针和数组并不是相等的。为了说明这个概念,请考虑下⾯两个声明:
int a[10];
int *b;
声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本⾝分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。
因此,表达式*a
是完全合法的,但是表达式*b
却是非法的。*b
将访问内存中一个不确定的位置,将会导致程序终⽌。另一⽅⾯b++可以通过编译,a++却不行,因为a是一个常量值。
作为函数参数的数组名
当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢?我们现在知道数组名其实就是一个指向数组第1个元素的指针,所以很明⽩此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新⼿容易上⼿一些,编译器也接受数组形式的函数形参。因此下⾯两种函数原型是相等的:
int print_array(int *arr);
int print_array(int arr[]);
我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样sizeof arr值是指针的长度,而不是数组的长度。
现在我们清楚了,为什么一维数组中⽆须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一⽅⾯,这种⽅式使得函数⽆法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。
强化训练
求数组中的最大值
#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;
}
程序输出:
数组中最⼤值为:10
一维数组的逆置
#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;
}
程序输出:
原始数组:1 2 3 4 5 6 7 8 9 10
逆序后的数组:10 9 8 7 5 6 4 3 2 1
冒泡法排序
#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;
}
程序输出:
-9 -8 -6 -4 -2 1 3 5 7 10
二维数组
如果某个数组的维数不⽌1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。
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]
行,并且每行有四个元素,也是依次存放的。
二维数组在概念上是二维的:其下标在两个⽅向上变化,对其访问一般需要两个下标。
在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,,即放完一行之后顺次放⼊第二行,和一维数组存放⽅式是一样的。
#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;
}
程序输出:
0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
初始化
//分段赋值 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个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:
int arr[3][10]
可以理解为这是一个一维数组,包含了3个元素,只是每个元素恰好是包含了10个元素的数组。arr就表⽰指向它的第1个元素的指针,所以arr是一个指向了包含了10个整型元素的数组的指针。
#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;
}
程序输出:
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定义一个数组类型:
定义数组指针有一下三种⽅式:
//⽅式一
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");
}
指针数组
指针数组,数组中的元素为指针。
栈区指针数组
//数组做函数函数,退化为指针
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);
}
堆区指针数组
//分配内存
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);
}
线性存储特性
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);
}
三种参数形式
//二维数组的第一种形式
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);
}
强化训练
#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;
}
程序输出:
各科的平均成绩:
71.80
65.00
66.40
各科不及格的人数:
2
1
2
多维数组
多维数组的定义与二维数组类似,其语法格式具体如下:数组类型修饰符 数组名 [n1][n2]…[nn];
int a[3][4][5];
定义了一个三维数组,数组的名字是a
,数组的长度为3,每个数组的元素又是一个二维数组,这个二维数组的长度是4,并且这个二维数组中的每个元素又是一个一维数组,这个一维数组的长度是5,元素类型是int。
#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;
}
程序输出:
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个元素的指针。
我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。