3. 数组

什么是C语言数组?

  1. C语言中数组是一组固定大小且相同类型元素的集合,数组分为一维数组和二维数组。
  2. 数组类型的声明并不是对一个元素进行声明而是对整个数组里面的元素类型都进行声明。
  3. 数组所有的元素都在一块连续的地址(内存空间)上的存储,第一个元素占最低的地址,最后一个元素占最高的地址;
  4. 数组的下标从0开始到元素的个数减1结束。

3.1 一维数组

1. 创建

格式:type_t array_name [const_n]

  • type_t:数组类型,如int、char;
  • array_name:数组名;
  • const_n:常量表达式,用来指定数组的长度(元素个数);
/*举例*/
1.type_t array_name[ const_n ];
2.type_t array_name[ const_n ] = {1,值2 ...};
3.type_t array_name[ ] = {1,值2 ...};

/*举例*/
//定义一个数组并给定数组大小,不进行初始化
int arry1[10]// 定义一个常量并给定数组大小为这个常量,不进行初始化
const int a = 5int arry5[a]//用宏定义的方式并给定数组大小为这个常量,不进行初始化
#define 	X 		3
int arr5[X];

2. 初始化

int main()
{
    int arry[10] = { 0 };					//完全初始化
    int arry1[6] = { 1,2,3 };				 //不完全初始化
    int arry2[5] = { 1,2,3,4,5 };			 //完全初始化
    char arry3[] = { 'a','b','c' };			 //完全初始化
    char arry4[] = { 'a','b',66,'c' };		 //完全初始化
    char arry5[] = "abcdef";				//完全初始化
    char arry6[10] = "abc";					//不完全初始化
    return 0;
}

分别分析如下:

int arry[10] = { 0 };					 //完全初始化
// 地址0x0099fa4c

在这里插入图片描述

int arry1[6] = { 1,2,3 };				 //不完全初始化
// 地址0x0099fa2c 其余自动补0

在这里插入图片描述

int arry2[5] = { 1,2,3,4,5 };			 //完全初始化
// 地址0x0099fa10 

在这里插入图片描述

char arry3[] = { 'a','b','c' };			 //完全初始化
// 地址:0X0099fa04
// 字符在计算机中以ascll码存储

在这里插入图片描述

char arry5[] = "abcdef";				//完全初始化
// arry5数组是完全初始化,未给定数组大小,编译器通过初始化内容来确定元素个数,但由于初始化是用双引号("")引起来,所以编译器自行在字符串末尾加上字符串结束标识符(转义字符)'\0'。
// 因此虽输入5个字符,实际大小为6个字符。

在这里插入图片描述

3. 一维数组在内存中的存储

​ 由上面分析可知,一维数组在内存中是连续着存放的,随着下标的增长下标所在的地址是由低往高增长的。

int main()
{
	int arry[] = { 1,2,3,4,5,6,7 };
	int n = sizeof(arry) / sizeof(arry[0]);
	for (int i = 0; i < n; i++)
	{
		printf("&arry[%d] = %p\n", i, &arry[i]);
	}
	return 0;
}

// 输出结果
&arry[0] = 0098FC34
&arry[1] = 0098FC38
&arry[2] = 0098FC3C
&arry[3] = 0098FC40
&arry[4] = 0098FC44
&arry[5] = 0098FC48
&arry[6] = 0098FC4C

同理,可以根据地址找到特定元素

int main()
{
	int arry[] = { 1,2,3,4,5,6,7 };
	int* p = &arry[4];
	printf("arry[4] = %d\n", *p);
	return 0;
}

// 输出结果
arry[4] = 5

4. 特征总结

  1. 数组中的元素在内存中是连续存储的,且每个元素占用相同大小的内存,用从0开始的下标进行索引;
  2. 数组只能够整体初始化,不能被整体赋值;
  3. 数组是具有相同类型的若干变量按有序形式组织的有序序列,是实现列表的方式之一,常用的数据结构;

3.2 数组名

数组名”的含义有以下几种情况:

1. 数组名是指针常量

C 语言中,数组名的值是一个指针常量,类型取决于数组元素的类型,指向数组第一个元素;也就是说,它存储的是数组第一个元素的地址,并且不能被修改。

int arr[] = {1, 2, 3, 4, 5};
int *pa = arr;   
// 等价于 int *p = &arr[0];

2. sizeof数组名

数组名通常表示的都是数组首元素的地址,但:

sizeof(数组名), 这里的数组名表示整个数组,计算的是整个数组的大小,不是单个元素的大小。

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

sizeof(arr) = 20;

3. &数组名

数组名通常表示的都是数组首元素的地址,但:

&数组名,取出的是数组的地址数值上等于首元素的地址,但是含义不同。

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);	     /*0115F92C*/
	printf("%p\n", arr + 1);     /*0115F92C + 00000004 = 0115F930*/
	
	printf("%p\n", &arr[0]);     /*0115F92C*/
	printf("%p\n", &arr[0] + 1); /*0115F92C + 00000004 = 0115F930*/

	printf("%p\n", &arr);		/*0115F92C*/
	printf("%p\n", &arr + 1);	/*0115F92C + 00000028 = 0115F954*/
	return 0;
}

【注意】

​ 此时,&arr表示数组的地址,不是首元素的地址,&arr的类型为int(*)[10],数组的地址+1,表示跳过整个数组的字节大小,所以&arr+1相对于&arr差值是40个字节。

4. 数组名称的用途

  1. 查看数组所占内存空间
  2. 获取地址
  3. 统计长度
int arr1[] = { 1,2,3,4,5,6,7,8 };

/*1. 获取内存空间大小*/
printf("整个1维数组所占内存空间:%d 字节\n", sizeof(arr1));
printf("1维数组每个元素所占内存空间:%d 字节\n", sizeof(arr1[0]));

/*2. 获取数组元素个数*/
printf("1维数组元素个数:%d 个\n", sizeof(arr1)/sizeof(arr1[0]));

/*3. 获取地址*/
printf("1维数组首地址:%p\n", arr1);
printf("1维数组第1个元素地址:%p\n", &(arr1[0]));
printf("1维数组第2个元素地址:%p\n", &(arr1[1]));

	
/*       
整个1维数组所占内存空间:32 字节
1维数组每个元素所占内存空间:4 字节
1维数组元素个数:8 个
1维数组首地址:008FF94C
1维数组第1个元素地址:008FF94C
1维数组第2个元素地址:008FF950
*/

3.3 数组和指针

1. 数组下标引用和间接访问

结论:除了优先级之外, 下标引用和间接访问完全相同。

解释:

  1. 数组元素的访问方式有下标引用间接访问两种(二者等价);
  2. arrayname[subscript] = *(arrayname + (subscript))

举例:声明以下变量和数组:

int array[10];
int *ap = array + 2;

即:

/* ap = array+2 = &array[2] */
printf("%p\n", ap);


/* ap = array+2 = &array[2] */
printf("%p %p %p\n", ap, array + 2,&array[2]);
//00B5FDF0 00B5FDF0 00B5FDF0

/* *ap = *(array+2) = array[2] */
printf("%d %d %d\n", *ap, *(array + 2), array[2]);
//3 3 3

/* ap[0]= *(ap+(0)) = array[2] = *(array +2) */
printf("%d %d %d\n", ap[0], *(ap + (0)), array[2]);
//3 3 3

/* ap + 6 = array +8 = &array[8] */
printf("%p %p %p\n", ap + 6, array + 8, &array[8]);
//00B5FE08 00B5FE08 00B5FE08

/* *ap + 6 = array[2] + 6 */
printf("%d %d \n", *ap + 6, array[2] + 6);
//9 9


/* *(ap + 6) = array[8]*/
printf("%d %d \n", *(ap + 6), array[8]);
//0 0

/* ap[-1] = array[1]*/
printf("%d %d \n", ap[-1], array[1]);
//2 2

/* ap[9]*/
printf("%d \n", ap[9]);
//-500648627
//越界

如果可以互换的使用下标引用和间接(指针)引用, 那么应该选择哪一个呢?

  1. 下标访问更容易理解, 可读性更高;
  2. 有时候指针会比下标更有效率;(在正确使用指针的前提下

2. 指针数组和数组指针

​ 要在名字上方便的区别二者,其实只需要理解:

  • 核心运算符的优先级顺序:()>[]>*
  • *[]哪个先和变量名结合,就是“什么”(指针或数组);
  • 中文名的话,主要看后面的两个字是什么(前面是修饰作用),因此指针数组是数组,而数组指针是指针。
1)指针数组 *p[n]
  1. 存放指针的数组;
  2. 数组的每个元素存储的是指向特定类型数据的地址
  3. dataType *arrayName[size];
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	//用数组来存放同类型的数据
	int arr[10] = { 10, 20 ,30, };

	int* pa = &a;
	int* pb = &b;
	int* pc = &c;
	//用数组来存放同类型的数据
	int* parr[10] = { &a, &b , &c, };
	for (int i = 0; i < 3; i++)
	{
		printf("%p\n", parr[i]);	//打印了地址
		printf("%d\n", *(parr[i])); //打印了地址的值
	}
	return 0;
}
//007AF864
//10
//007AF858
//20
//007AF84C
//30

​ 上述结构示意图如下:

(1)应用
① 实现二维数组
int main()
{
	int arr[3][4] = { 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6 };
/*
* 1 2 3 4
* 2 3 4 5
* 3 4 5 6
*/
	//遍历
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
//用指针数组实现
	int arr1[4] = { 1, 2, 3, 4 };
	int arr2[4] = { 2, 3, 4, 5 };
	int arr3[4] = { 3, 4, 5, 6 };

	int* parr[3] = { &arr1, &arr2, &arr3 };
	printf("%p\n", &arr1);
	printf("%p\n", parr[0]);
/*
00AFFCE4
00AFFCA0
00AFFCE4
*/
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ",parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

模拟一个二维数组

在这里插入图片描述

② 函数指针数组

函数指针在 C 语言中是一种强大而灵活的工具,函数指针是指向函数的指针,它可以存储函数的地址,通过指针来灵活调用函数。

声明格式1:ReturnType (*ArrayName[Size])(ParameterType1, ParameterType2, ...);

声明格式2:typedef ReturnType (*ArrayName[Size])(ParameterType1, ParameterType2, ...);

​ 举一个简单的例子:

#include <stdio.h>

int add(int a, int b) 
{
    return a + b;
}

int subtract(int a, int b) 
{
    return a - b;
}

int multiply(int a, int b) 
{
    return a * b;
}

//int (*operation[3])(int, int) = {add, subtract, multiply};

// 使用 typedef 简化函数指针类型的声明
typedef int (*Operation)(int, int);

int main() 
{
    Operation operation[3] = {add, subtract, multiply};

    // 使用函数指针数组调用不同的函数
    printf("Addition: %d\n", operation[0](5, 3));
    printf("Subtraction: %d\n", operation[1](8, 2));
    printf("Multiplication: %d\n", operation[2](4, 6));

    return 0;
}
③ 任务函数表

概要: 任务函数表是一个指针数组,每个指针指向一个特定任务的处理函数,任务函数表用于动态配置和管理系统中的任务。

​ 例如:

#include <stdio.h>

void task1(void) 
{
    printf("Executing Task 1\n");
}

void task2(void)
{
    printf("Executing Task 2\n");
}

void task3(void) 
{
    printf("Executing Task 3\n");
}

typedef void (*TaskFunction)(void);

int main() 
{
    TaskFunction taskTable[3] = {task1, task2, task3};
    
    int choice;
    printf("Enter task number (1-3): ");
    scanf("%d", &choice);
    
    // Execute tasks dynamically
    if (choice >= 1 && choice <= 3) 
    {
        commandHandlers[choice - 1]();
    } 
    else 
    {
        printf("Invalid choice\n");
    }

    return 0;
}
④ 驱动函数表

概要: 使用指针数组,每个指针指向一个与硬件交互的函数,方便开发和维护。

​ 例如:

#include <stdio.h>

// 设备1的打开和关闭操作
void openDevice1()
{
    printf("Device 1 opened\n");
}

void closeDevice1()
{
    printf("Device 1 closed\n");
}

// 设备2的打开和关闭操作
void openDevice2()
{
    printf("Device 2 opened\n");
}

void closeDevice2()
{
    printf("Device 2 closed\n");
}

typedef void (*DeviceOperations[2][2])();

int main()
{
    // 定义设备操作函数指针数组
    DeviceOperations deviceOperations = { {openDevice1, closeDevice1}, {openDevice2, closeDevice2} };

    //void (*deviceOperations[2][2])() = { {openDevice1, closeDevice1}, {openDevice2, closeDevice2} };

    int deviceType, operation;

    printf("Enter device type (1 or 2): ");
    scanf("%d", &deviceType);

    printf("Enter operation (1: Open, 2: Close): ");
    scanf("%d", &operation);

    if (deviceType >= 1 && deviceType <= 2 && operation == 1)
    {
        // 执行打开操作
        deviceOperations[deviceType - 1][operation - 1]();
    }
    else if (deviceType >= 1 && deviceType <= 2 && operation == 2) 
    {
        // 执行关闭操作
        deviceOperations[deviceType - 1][operation - 1]();
    }
    else 
    {
        printf("Invalid choice\n");
    }

    return 0;
}
⑤ 状态机

概要: 状态机使用指针数组表示状态和状态转换,每个指针指向处理特定状态的函数。

#include <stdio.h>

void state1Handler(void) 
{
    printf("Handling State 1\n");
}

void state2Handler(void) 
{
    printf("Handling State 2\n");
}

void state3Handler(void) 
{
    printf("Handling State 3\n");
}

typedef void (*StateFunction)(void);

int main() 
{
    StateFunction stateTable[3] = {state1Handler, state2Handler, state3Handler};

    // Handle states dynamically
    for (int i = 0; i < 3; ++i)
    {
        stateTable[i]();
    }

    return 0;
}
(2)注意点
  1. 保持数据类型一致性:

    • 在定义指针数组时,确保所有数组元素的指针类型相同。
    int *intPtrArray[5];  // 正确,所有元素都是 int 类型的指针
    char *charPtrArray[5];  // 正确,所有元素都是 char 类型的指针
    
  2. 指针及时初始化与赋值:

    • 在定义指针数组时最好进行初始化,避免使用未初始化的指针。在给指针数组赋值前,确保每个元素指向有效的内存地址。
    int *intPtrArray[5] = {NULL};  // 初始化所有指针为 NULL
    
    int num1 = 10, num2 = 20;
    int *intPtrArray[2] = {&num1, &num2};  // 正确的初始化
    
2) 数组指针 (*p)[n]
  • 根据上面二维数组的例子可以得出,数组指针也称指向一维数组的指针,所以数组指针也称行指针
  • 数组指针是用来存放数组地址的指针
  • 既然数组指针是指针,那么也可以使用指针的运算
  • type (*ptr)[size];
① 对于一维数组

​ 一维数组指针遍历字符串数组。namePtr 是一个指向字符串数组的指针,用于遍历字符串数据。

#include <stdio.h>
// 定义字符串数组
char* names[] = {"Alice", "Bob", "Charlie", "David"};

int main() {
    // 定义一个指向字符串数组的指针
    char** namePtr = names;

    // 使用指针遍历字符串数组
    for (int i = 0; i < 4; ++i) 
    {
        printf("Name: %s\n", namePtr[i]);
    }

    return 0;
}

​ 一维数组指针作为函数参数,arr 是一个指向整数数组的指针,作为函数参数传递。

#include <stdio.h>

// 函数接受一个整数数组的指针和数组长度
void printArray(int* arr, int length) 
{
    for (int i = 0; i < length; ++i) 
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() 
{
    // 定义整数数组
    int numbers[] = {5, 10, 15, 20, 25};

    // 调用函数,将数组和数组长度传递给函数
    printArray(numbers, 5);

    return 0;
}
② 对于二维数组
  • 二维数组可以用行指针定义;
  • p++操作会使p指向下一行的首地址,这是因为p是行指针,指向的是一行。
  • 可以用sizeof(*p)测试p指向的内容的大小:

#include <stdio.h>

void print_arr_col(int(*parr)[4], int row, int col)
{
    for (int j = 0; j < col; j++) 
    {
        for (int i = 0; i < row; i++)
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
}

void print_arr_row(int(*parr)[4], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
}


int main() 
{
    int arr[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

    print_arr_col(arr, 3, 4);
    print_arr_row(arr, 3, 4);
    return 0;
}

//输出结果
0 4 8
1 5 9
2 6 10
3 7 11
0 1 2 3
4 5 6 7
8 9 10 11

更多推荐