解码C语言数组

一维数组

数组相同类型数据元素的有序集合,通过下标(索引)访问元素,内存中连续存储。

数组名表示首元素地址,sizeof(arr) 返回整个数组的字节大小

核心特点

  • 元素类型一致:所有元素必须为同一数据类型(如 int, float)。
  • 固定大小:数组长度在声明时确定,静态数组无法动态调整。
  • 下标访问:下标从 0 开始,最大下标为 长度-1

一维数组的定义与初始化

基本语法
数据类型 数组名[长度];// 声明未初始化的数组
数据类型 数组名[长度] = {值列表};// 声明并初始化
示例
 int numbers[5];// 声明长度为5的整型数组(元素值不确定)
 float scores[3] = {8.5, 9.0, 7.8};// 完全初始化
 char vowels[] = {'a', 'e', 'i', 'o', 'u'};// 自动推断长度为5
初始化规则
  • 未完全初始化:剩余元素自动填充零(数值类型)或空字符(字符类型)。

    int arr[5] = {1, 2};// [1, 2, 0, 0, 0]
    char str[10] = "Hi";// ['H','i','\0',0,...,0],\0是字符串数组的结束符(必须有)
    
  • 静态初始化(全局数组):未显式初始化时,默认全零。

    static int global_arr[3];// [0, 0, 0]
    
数组越界初始化

在C语言中,数组的初始化分为声明时初始化动态初始化。越界问题在这两种情况下有不同的表现。

声明时的初始化(使用初始化列表)

定义:指在声明数组的同时用大括号 {} 提供初始值列表时,提供的值的个数超过了数组声明的长度。

例子

#include <stdio.h>
int main() 
{
	// 声明一个长度为5的数组,但尝试用6个值初始化它
	int arr[5] = {1, 2, 3, 4, 5, 6};// 编译器会报错!
	return 0;
}
  • 后果:这是一种编译时错误。编译器(如GCC)会直接报错,提示类似 excess elements in array initializer(初始化列表中的元素超出了数组的范围)。程序无法通过编译,无法生成可执行文件。
  • 原因:数组的大小在编译时是固定的,编译器可以明确检查出初始值的数量是否匹配。
动态初始化(运行时赋值)

定义:指在程序运行时,通过循环或其他逻辑为数组元素赋值时,循环变量或索引计算错误,导致试图对数组范围之外的内存进行赋值。

例子

#include <stdio.h>
int main() {
    int arr[5];
    int i;

	// 错误:循环了6次 (i从0到5),最后一次arr[5]是越界写入
	for(i = 0; i <= 5; i++) {// 应该是 i < 5
        arr[i] = i * 10;
    }

    return 0;
}
  • 后果:这是一种运行时错误,属于未定义行为。编译器通常不会报错(最多可能给出警告),但程序运行时的行为是不可预测的。它可能会覆盖其他重要的数据(如其他变量、函数调用的返回地址),导致程序崩溃或产生诡异的结果。
逐个赋值

声明数组后,通过循环或直接索引为每个元素赋值:

int arr[5];// 声明未初始化的数组
// 逐个赋值
arr[0] = 10;// 第一个元素
arr[1] = 20;
arr[2] = 30;
arr[3] = 40;
arr[4] = 50;

// 或通过循环赋值
for (int i = 0; i < 5; i++) {
    arr[i] = (i + 1) * 10;// 赋值 10, 20, 30, 40, 50
}

数组元素的操作

访问元素

通过下标(索引)访问,范围从 0长度-1

int data[4] = {10, 20, 30, 40};
printf("%d\n", data[0]);// 输出10
data[3] = 50;// 修改最后一个元素为50
遍历数组

使用 for 循环遍历所有元素:

for (int i = 0; i < sizeof(data)/sizeof(data[0]); i++) {
    printf("data[%d] = %d\n", i, data[i]);
}
  • 关键技巧:用 sizeof(data)/sizeof(data[0]) 计算数组长度。
数组访问越界

这是C语言中最常见且最危险的错误之一。

定义:指在使用数组时,下标索引的值不在 [0, 数组长度 - 1] 这个有效范围内。包括读越界写越界

例子

#include <stdio.h>
int main() 
{
    int arr[5] = {10, 20, 30, 40, 50};// 有效索引: 0, 1, 2, 3, 4
    // 1. 读越界 - 读取不存在的数据
    printf("越界读取: %d\n", arr[5]);// 索引5无效
    printf("越界读取: %d\n", arr[-1]);// 索引-1无效
    // 2. 写越界 - 向非法内存地址写入数据(极其危险!)
    arr[5] = 100;// 破坏未知的内存
    arr[100] = 200;// 更严重的破坏
    return 0;
}
字符数组字符串 的对比
对比项字符数组字符串
本质存储字符序列的一维数组,元素类型为 char(纯粹的字符集合)。'\0'(空字符)结尾的特殊字符数组(本质是字符数组的子集,附加终止符约束)。
终止符可无 '\0'(仅作字符集合时);若用于字符串场景需手动添加。必须包含 '\0'(否则字符串函数如 strlen 会越界访问,导致未定义行为)。
初始化方式- 显式列表:char arr1[] = {'H', 'i'};(无终止符,长度=2)
- 字符串赋值:char arr2[] = "Hi";(自动添加 '\0',长度=3)
只能通过字符串字面量初始化(隐式带 '\0'):
char str[] = "Hello";(长度=6,含终止符)
内存占用长度 = 初始化字符数(无终止符时)或 字符数+1(含自动添加的 '\0' 时)。长度 = 可见字符数 + 1(固定多1字节存放 '\0',如 "abc" 占4字节)。
操作函数无专用函数,需手动遍历(如 for 循环逐个处理字符)。可直接使用标准库函数:
- 长度计算:strlen()
- 复制:strcpy()
- 拼接:strcat()
常见用途存储原始字节数据(如二进制缓冲区、非文本编码数据)、固定长度字符集合。文本处理(如用户输入、文件内容、字符串拼接/比较)、需用字符串函数操作的场景。
长度计算sizeof(arr):返回数组总字节数(含所有元素,包括可能的 '\0')。strlen(str):返回 '\0' 前的字符数(不含终止符,如 "abc" 返回3)。
修改权限const 修饰时,可自由修改任意元素(如 arr[0] = 'h')。字符串字面量(如 "Hello")存储在只读区(.rodata),不可修改;数组形式字符串(char str[] = "...")可修改。
输入输出需逐个字符处理(如 for 循环打印),或指定长度(printf("%.2s", arr))。可直接用 %s 格式化输入输出(自动识别 '\0' 终止):printf("%s", str);

二维数组

二维数组的定义与初始化

基本语法
数据类型 数组名[行数][列数];// 声明未初始化的二维数组
数据类型 数组名[行数][列数] = {初始化值};// 声明并初始化
  • 示例

    int matrix[3][4];// 声明3行4列的整型数组(元素值不确定)
    float grid[2][3] = {{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}};// 完全初始化
    char chessboard[][3] = {{'X', 'O'}, {'O', 'X'}};// 自动推断行数为2(列数表示元素类型列数不能不声明)
    
初始化规则
  • 逐行初始化:内层花括号对应行,可省略内部括号(按顺序填充)。

    int arr[2][3] = {1, 2, 3, 4, 5, 6};// 等效于 {{1,2,3}, {4,5,6}}
    
  • 部分初始化:未初始化的元素自动填充零。

    int arr[2][3] = {{1}, {4, 5}};// 第一行 [1,0,0],第二行 [4,5,0]
    

二维数组的内存布局

存储方式

二维数组在内存中按 行优先顺序 连续存储:

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

内存布局(假设 int 占4字节):

地址: 0x1000 → 1 (arr[0][0])
地址: 0x1004 → 2 (arr[0][1])
地址: 0x1008 → 3 (arr[0][2])
地址: 0x100C → 4 (arr[1][0])
地址: 0x1010 → 5 (arr[1][1])
地址: 0x1014 → 6 (arr[1][2])
计算元素地址

对于 arr[row][col],其内存地址为:

基地址 + (row * 列数 + col) * sizeof(数据类型)

二维数组的常见操作

遍历数组

嵌套循环访问所有元素:

for (int i = 0; i < 行数; i++) {
    for (int j = 0; j < 列数; j++) {
        printf("arr[%d][%d] = %d\n", i, j, arr[i][j]);
    }
}

变长数组

核心特性:一旦创建,其大小就固定了,不能再改变。 这里的“变长”指的是“长度由变量决定”,而不是“长度可以变化”。

基本语法和示例

#include <stdio.h>
int main() 
{
    int size;

    printf("请输入数组的大小: ");
    scanf("%d", &size);// 运行时才确定大小
    // 声明一个变长数组 (Variable-Length Array, VLA)
    // 其长度由变量size的值决定
    int myArray[size];

	// 像普通数组一样使用
	for (int i = 0; i < size; i++) {
        myArray[i] = i * 2;
    }

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

    return 0;
}

主要特点

长度由变量决定:数组的长度可以是任何整数表达式(只要结果为正数),而不仅仅是常量。

int n = 10;
int m = 5;
float arr1[n];// 合法
float arr2[n*2 + 1];// 合法
float arr3[m*n];// 合法

通常分配在栈上:与普通自动(局部)数组一样,VLA通常在函数调用栈上分配内存。这是其最大的限制之一。
作用域和生命周期:VLA是自动存储期的对象。它的生命周期仅限于其所在的作用域(例如,函数内部或块内部)。当程序离开声明它的块时,数组占用的内存会自动被释放。
不允许在文件作用域或静态存储期使用

// int global_vla[n]; // 错误!VLA不能是全局的
// static int static_vla[n]; // 错误!VLA不能是static的
int main() 
{
    // 只能在块作用域内使用
    int n = 5;
    int vla[n];// 正确
}

不能初始化:声明VLA时不能同时进行初始化。

int n = 5;
int vla[n] = {1, 2, 3, 4, 5};// 错误!编译不通过

零长数组

零长数组是一种在结构体中声明的、长度为零的数组。它的主要用途是作为结构体的最后一个成员,指向一段动态分配的内存空间,从而使整个结构体能够容纳可变长度的数据。

基本形式:

struct my_struct 
{
    int some_data;
    char flexible_data[0];// 这就是零长数组
};

在C语言中,结构体的大小通常是固定的。但有时我们需要一个结构体来存储一个可变长度的数据(例如,一个可变长度的字符串、一个动态数量的数组等)。

不优雅的解决方案(过去):

在零长数组出现之前,程序员常用一种称为“struct hack”的技巧,即声明一个长度为1的数组。

struct my_struct 
{
    int length;
    char data[1];// 长度为1的数组
};

然后动态分配内存时,分配 sizeof(struct my_struct) + (actual_length - 1) 的大小。这种方法可行,但语法上不美观,且标准未明确支持。

零长数组的解决方案:

零长数组让这个“Hack”变得更加直观和自然。它明确表示:“这个数组没有占用结构体本身的空间,它只是另一个内存区域的起点”。

使用零长数组的标准步骤是:

定义结构体:将零长数组作为最后一个成员。
动态分配内存:为结构体本身 加上 你希望零长数组拥有的实际长度分配内存。
使用数据:像使用普通数组一样使用零长数组成员。

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义包含零长数组的结构体
struct line 
{
    int length;
    char contents[0];// 零长数组
};

int main() 
{
    int desired_length = 20;
    // 动态分配内存:结构体基础大小 + 所需的数组大小
    struct line *thisline = (struct line *)malloc(sizeof(struct line) 
														+ desired_length * sizeof(char));
    thisline->length = desired_length;
    // 现在可以使用 contents 数组了,它拥有 20 个字符的空间
    strncpy(thisline->contents, "Hello, World!", desired_length);

    printf("Content: %s\n", thisline->contents);
    printf("Length: %d\n", thisline->length);
    // 释放内存free(thisline);
    return 0;
}

数组与数组名

数组名(Array Name)

数组名是一个标识符,它代表整个数组。但在大多数表达式中,它会自动转换为指向其首元素的指针。

int arr[5] = {10, 20, 30, 40, 50};

arr 的含义:

  • 类型int* (指向整型的指针)
  • &arr[0] (数组第一个元素的地址)
  • 指针运算:以单个元素为单位进行偏移

示例:

printf("arr:     %p\n", (void*)arr);// 输出数组首元素地址
printf("&arr[0]: %p\n", (void*)&arr[0]);// 输出数组首元素地址
// 两者输出相同

printf("*arr: %d\n", *arr);// 输出 10 (解引用得到第一个元素)
printf("*(arr + 1): %d\n", *(arr + 1));// 输出 20 (向后移动一个int的大小)

&数组名(Address of Array Name)

&arr 获取的是整个数组的地址。

&arr 的含义:

  • 类型int (*)[5] (指向长度为5的整型数组的指针)
  • :与 arr 相同(因为数组的地址就是其首元素的地址)
  • 指针运算:以整个数组为单位进行偏移

示例:

printf("&arr:    %p\n", (void*)&arr);// 输出整个数组的地址// 这个值与 arr 相同,但类型不同

// 指针运算的差异:
printf("arr + 1: %p\n", (void*)(arr + 1));// 地址增加 4 字节(一个int的大小)
printf("&arr + 1: %p\n", (void*)(&arr + 1));// 地址增加 20 字节(5个int的大小,即整个数组的大小)

对比表格

特性arr (数组名)&arr (数组的地址)
类型int*int (*)[5]
指向对象第一个元素 arr[0]整个数组 arr
值(地址)&arr[0] 相同arr 相同
+1 的偏移量sizeof(int) 字节sizeof(int[5]) 字节(即20字节)
主要用途访问数组元素操作整个数组(如传递给函数)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值