当前位置: 首页 > news >正文

暑期自学嵌入式——Day05(C语言阶段)

接续上文:暑期自学嵌入式——Day04(C语言阶段)-CSDN博客

点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!

主页:

一位搞嵌入式的 genius-CSDN博客

目录

Day05

数组

一、数组概述

1. 数组的定义

2. 数组的特性

3. 理解要点

二、一维数组

1. 一维数组的定义

(1)基本概念

(2)数组内存特性验证

(3)数组名特性

2. 一维数组的注意事项

(1)C 语言对数组不作越界检查

(2)数组越界的后果

(3)变量定义数组的注意事项

2. 一维数组的引用

3. 一维数组的初始化

(1)初始化规则

(2)注意事项

三、应用案例:冒泡排序

1. 算法原理

2. 代码实现

3. 关键要点

四、知识小结

五、编程建议

二维数组

一、二维数组的定义

1. 基本语法与规则

2. 应用场景(以邻接矩阵为例)

二、数组元素的存放顺序

1. 存储原则:行序优先

2. 存储过程示例(以int a[2][3]为例)

三、二维数组的理解

1. 分层理解:二维数组 = 多个一维数组

2. 行名(如a[0])的特性

四、二维数组的定义举例与编程验证

1. 验证存储连续性与行名特性

五、知识小结

六、编程建议

二维数组:内存、初始化与访问

一、二维数组的内存连续性验证

1. 验证原理与方法

2. 示例代码

3. 输出分析(地址为示例,实际因系统而异)

二、二维数组的本质理解

1. 行名的概念与特性

2. 内存结构与逻辑结构的区别

三、二维数组的初始化

1. 初始化方式与规则

(1)全初始化(分行显式赋值)

(2)部分初始化(未赋值元素自动补 0)

(3)省略行数(编译器自动推导)

2. 初始化注意事项

(1)大括号的作用

(2)常见错误

四、二维数组的访问与遍历

1. 元素访问方式

2. 遍历方法(双重循环)

五、知识小结

六、编程技巧

字符数组和字符串

一、字符数组

1. 字符数组的概念

2. 字符数组的初始化

(1)逐个字符赋值(单引号)

(2)字符串常量赋值(双引号)

3. 应用案例:字符数组输出问题

(1)未以'\0'结尾的风险

(2)正确输出方式

二、字符串与字符数组的关系

三、多维字符数组

四、知识小结

五、编程建议

一、字符串的核心特性

1. C 语言中字符串的本质

2. 字符串结束标志'\0'的作用

(1)内存占用与长度计算

(2)'\0'的核心作用

二、字符数组的初始化与应用

1. 字符数组的两种初始化方式对比

2. 二维字符数组(存储多个字符串)

三、应用案例:字符串逆序输出

1. 需求

2. 两种实现思路

(1)方法一:仅逆序输出(不修改原字符串)

(2)方法二:修改原字符串为逆序(再输出)

四、知识小结

五、编程技巧与注意事项


 

Day05

数组

一、数组概述

1. 数组的定义

  • 构造类型:数组是由若干个相同基本类型组成的构造数据类型(例如字符数组char s[]用于存储字符串,本质是多个字符的集合)。

  • 元素组成:由多个变量(称为 “元素”)构成,每个元素通过 “下标” 唯一标识(如下标0对应第一个元素,下标1对应第二个元素)。

  • 有序集合:元素之间存在严格的顺序关系(按内存地址连续排列),而非无序堆放。

  • 类型统一:所有元素的数据类型必须相同(如int a[5]的元素均为int型),通过 “数组名 + 下标” 可唯一定位元素(如a[2]表示数组a的第 3 个元素)。

2. 数组的特性

  • 访问方式:必须通过下标访问具体元素(如a[0]),不能直接操作整个数组(如printf("%d", a)无法打印所有元素)。

  • 维度扩展:支持一维(int a[5])、二维(int b[3][4])甚至多维数组,维度表示元素的组织层次(一维是线性排列,二维是行列结构)。

  • 存储特点:元素在内存中连续存储(相邻元素地址差等于单个元素的字节数),这是下标快速访问的基础(通过首地址 + 偏移量计算元素地址)。

  • 类型扩展:除基本类型(intchar等),结构体、共用体等自定义类型也可构成数组(如struct Student stu[30]存储 30 个学生信息)。

3. 理解要点

  • 编程意义:数组解决了 “多个同类型变量批量管理” 的问题(如存储 100 个成绩,无需定义 100 个变量)。

  • 元素视角:操作数组时需关注单个元素(如遍历数组需逐个访问a[i])。

  • 下标重要性:下标从0开始(而非 1),这是 C 语言的核心规则(如a[0]是第一个元素,a[n-1]是最后一个元素)。

  • 类型一致性:所有元素类型相同,确保内存分配连续且计算简单(如int数组每个元素占 4 字节,地址间隔固定)。

二、一维数组

1. 一维数组的定义

(1)基本概念

  • 语法格式

    <存储类型> <数据类型> <数组名>[<长度>];
    • 存储类型:auto(默认)、static(静态存储)、register(寄存器,极少用)、extern(外部声明)。

    • 数据类型:元素的数据类型(如intfloat)。

    • 长度:数组元素个数(可是常量或已初始化的变量)。

  • 示例

    int a[5];  // 定义int型数组a,含5个元素(a[0]~a[4])
    static float b[10];  // static数组,未初始化时元素默认0

(2)数组内存特性验证

#include <stdio.h>
​
int main() {int a[3] = {10, 20, 30};// 打印元素地址(%p用于输出地址)printf("a[0]地址:%p\n", &a[0]);printf("a[1]地址:%p\n", &a[1]);printf("a[2]地址:%p\n", &a[2]);// 计算总内存(sizeof(数组名)返回总字节数)printf("数组总大小:%zu字节\n", sizeof(a));  // 输出12(3×4)return 0;
}
  • 输出分析:

    • 元素地址连续递增(如0x7ffd...000x7ffd...040x7ffd...08,间隔 4 字节,对应int类型)。

    • sizeof(a)返回总字节数(元素个数 × 单个元素字节数)。

(3)数组名特性

  • 地址常量性:

    数组名代表数组首元素的地址(

    a == &a[0]

    ),但它是地址常量(不可修改):

    a = a + 1;  // 错误!数组名不能作为左值(不能被赋值)
  • sizeof 运算:

    • sizeof(a)返回数组总字节数(如int a[5]sizeof(a)=20)。

    • sizeof(a[0])返回单个元素的字节数(如4),因此数组长度 = sizeof (a)/sizeof (a [0])(通用计算方式)。

2. 一维数组的注意事项

(1)C 语言对数组不作越界检查

  • 编译器行为:定义int a[5]后,访问a[5]a[-1]属于越界,但编译器不报错(仅检查语法,不检查逻辑)。

  • 风险示例:

    int a[5] = {1,2,3,4,5};
    a[5] = 6;  // 越界访问,可能修改其他变量的内存(行为不可预测)
  • 责任归属:程序员必须保证下标在0 ~ 长度-1范围内(如a[5]的合法下标是0~4)。

(2)数组越界的后果

  • 内存污染:越界访问可能修改其他变量、函数返回地址等关键内存,导致程序崩溃、逻辑错误甚至安全漏洞。

  • 隐蔽性:越界不一定立即报错(如a[5]可能恰好修改未使用的内存),增加调试难度。

(3)变量定义数组的注意事项

  • 允许变量作为长度:C99 标准支持 “变长数组”(VLA),如int n=5; int a[n];

  • 风险:若变量未初始化,数组长度随机(如int n; int a[n];n值不确定,数组大小不可控)。

  • 正确做法:确保长度变量已初始化:

    int n;
    scanf("%d", &n);  // 先初始化n
    int a[n];  // 再定义数组

2. 一维数组的引用

  • 访问规则:通过数组名[下标]访问(如下标i需满足0 ≤ i < 长度)。

  • 示例:

    int a[3] = {10, 20, 30};
    printf("%d\n", a[0]);  // 正确:输出10(第一个元素)
    printf("%d\n", a[2]);  // 正确:输出30(最后一个元素)
  • 错误示例:

    // 错误1:整体访问数组(无法直接输出整个数组)
    printf("%d", a);  // 输出数组首地址(无意义)
    ​
    // 错误2:越界访问
    printf("%d", a[3]);  // 下标3超过长度3(合法下标0~2)

3. 一维数组的初始化

(1)初始化规则

  • 完整初始化:初始值个数与数组长度相同:

    int a[3] = {1, 2, 3};  // a[0]=1, a[1]=2, a[2]=3
  • 部分初始化:初始值个数少于长度,剩余元素自动补0:

    int a[5] = {1, 2};  // a[0]=1, a[1]=2, a[2]=0, a[3]=0, a[4]=0
  • 省略长度:编译器根据初始值个数自动确定长度:

    int a[] = {1, 2, 3};  // 长度自动为3(等价于int a[3])
  • static 数组:未初始化时,元素默认值为0(非 static 数组未初始化时值随机):

    static int a[3];  // a[0]=0, a[1]=0, a[2]=0(自动初始化)
    int b[3];  // 元素值随机(不确定)

(2)注意事项

  • 初始值过多:编译报错(如int a[3] = {1,2,3,4};提示 “初始值过多”)。

  • 初始化时机:仅在定义时生效(后续赋值属于普通修改,非初始化):

    int a[3];
    a = {1,2,3};  // 错误!初始化不能在定义后执行(需逐个赋值:a[0]=1; ...)

三、应用案例:冒泡排序

1. 算法原理

  • 基本思想:通过重复比较相邻元素,顺序错误则交换,使最大值 “沉底”(升序排序)。

  • 终止条件:某趟遍历无交换时,数组已排序。

  • 示例(升序排序):

    初始数组:

    [38, 49, 76, 13, 27, 30, 97]
    • 第一趟:比较并交换相邻元素,97 沉底 → [38, 49, 13, 27, 30, 76, 97]

    • 第二趟:76 沉底(无需比较最后一个元素) → [38, 13, 27, 30, 49, 76, 97]

    • 直到某趟无交换,排序完成。

2. 代码实现

#include <stdio.h>int main() {int a[] = {38, 49, 76, 13, 27, 30, 97};int n = sizeof(a) / sizeof(a[0]);  // 计算数组长度int i, j, temp;int swapped;  // 交换标志(优化用)// 外层循环:控制趟数(最多n-1趟)for (i = 0; i < n - 1; i++) {swapped = 0;  // 每趟初始化为0(无交换)// 内层循环:每趟比较到n-1-i(最后i个已排序)for (j = 0; j < n - 1 - i; j++) {if (a[j] > a[j + 1]) {  // 前元素>后元素,交换(升序)temp = a[j];a[j] = a[j + 1];a[j + 1] = temp;swapped = 1;  // 标记有交换}}if (swapped == 0) break;  // 无交换,提前终止}// 打印排序结果for (i = 0; i < n; i++) {printf("%d ", a[i]);}return 0;
}

3. 关键要点

  • 双重循环:

    • 外层i:控制趟数(0 ~ n-2)。

    • 内层j:控制每趟比较次数(0 ~ n-2-i,因最后i个元素已排序)。

  • 交换逻辑:通过临时变量temp交换a[j]a[j+1]

  • 优化技巧swapped标志避免无效遍历(已排序时提前退出)。

  • 时间复杂度:

    • 最坏情况(逆序):O(n²)(需n-1趟,每趟n-1次比较)。

    • 最好情况(已排序):O(n)(1 趟无交换即终止)。

四、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
数组基本概念相同类型元素的有序集合,通过下标访问;数组名是地址常量数组名不可修改(a = &b错误);下标从 0 开始(a[0]是第一个元素)⭐⭐
一维数组定义语法:类型 数组名[长度];支持变量作为长度(VLA)长度变量必须初始化;static数组默认初始化为 0 vs auto 数组值随机⭐⭐
数组初始化完整初始化、部分初始化(剩余补 0)、省略长度(自动推导)初始值过多编译报错;定义后不能整体赋值(需逐个元素操作)⭐⭐⭐
内存特性元素连续存储;sizeof(数组名)返回总字节数元素地址计算(首地址 + 下标 × 元素大小);越界访问的隐蔽风险⭐⭐⭐⭐
冒泡排序双重循环 + 相邻元素交换;交换标志优化效率内层循环边界(j < n-1-i);升序 / 降序的比较条件(> vs <⭐⭐⭐⭐⭐

五、编程建议

  1. 数组长度计算:始终用sizeof(a)/sizeof(a[0])动态获取(避免硬编码,适应数组修改)。

  2. 越界检查:遍历数组时,确保i0 ~ 长度-1范围内(如for(i=0; i < n; i++))。

  3. 初始化习惯:定义数组时尽量初始化(如int a[5] = {0};确保初始值可控)。

  4. 排序调试:打印每趟排序结果(如printf("第%d趟:", i);),观察元素移动是否符合预期。

 

二维数组

一、二维数组的定义

1. 基本语法与规则

  • 语法结构

    数据类型 数组名[行数][列数];
    • 核心要求列数必须明确(不能省略),行数在初始化时可省略(由编译器自动推导)。

    • 元素个数:总元素数量 = 行数 × 列数(如int a[3][4]有 3×4=12 个元素)。

  • 示例说明

    int a[3][4];  // 3行4列的整型二维数组(合法)
    float b[2][5]; // 2行5列的浮点型数组(合法)
    int c[][3] = {{1,2}, {3,4}};  // 省略行数(编译器推导为2行,合法)
    int d[3][];  // 错误!列数不能省略

2. 应用场景(以邻接矩阵为例)

二维数组常用于表示 “行列关联关系”,例如图结构的邻接矩阵:

  • 场景:5 个顶点的无向图,用5×5二维数组graph表示连接关系。

  • 规则graph[i][j] = 1表示顶点i与顶点j相连,0表示不相连。

  • 示例:

    int graph[5][5] = {{0, 1, 0, 1, 0},  // 顶点0与1、3相连{1, 0, 1, 0, 0},  // 顶点1与0、2相连{0, 1, 0, 1, 1},  // 顶点2与1、3、4相连{1, 0, 1, 0, 1},  // 顶点3与0、2、4相连{0, 0, 1, 1, 0}   // 顶点4与2、3相连
    };
  • 存储意义:通过数组元素的行列索引,可直观还原图的连接关系(如graph[0][1]=1表示 0 和 1 相连)。

二、数组元素的存放顺序

1. 存储原则:行序优先

内存是 “一维线性” 的,二维数组的元素按 “先存满一行,再存下一行” 的顺序连续存储(行序优先)。

2. 存储过程示例(以int a[2][3]为例)

数组定义:int a[2][3] = {{1,2,3}, {4,5,6}}

  • 存储顺序a[0][0]a[0][1]a[0][2]a[1][0]a[1][1]a[1][2]

  • 地址验证:

    #include <stdio.h>int main() {int a[2][3] = {{1,2,3}, {4,5,6}};// 打印元素地址(%p输出地址)printf("a[0][0]地址:%p\n", &a[0][0]);  // 假设:0x7ffd...00printf("a[0][1]地址:%p\n", &a[0][1]);  // 0x7ffd...04(+4字节,int类型)printf("a[0][2]地址:%p\n", &a[0][2]);  // 0x7ffd...08printf("a[1][0]地址:%p\n", &a[1][0]);  // 0x7ffd...0c(紧接a[0][2],连续存储)return 0;
    }
    • 相邻元素地址差 = 元素类型字节数(int占 4 字节,故差 4)。

    • 行尾元素(a[0][2])与下一行首元素(a[1][0])地址连续,验证 “行序优先”。

三、二维数组的理解

1. 分层理解:二维数组 = 多个一维数组

二维数组可看作 “一维数组的数组”—— 每个元素是一个一维数组。

  • 示例:

    int a[3][4]

    (3 行 4 列)

    • 可视为 3 个一维数组:a[0]a[1]a[2](每个是 “4 个 int 元素的一维数组”)。

    • a[0] 是第一行的一维数组名,包含元素 a[0][0]~a[0][3]

    • a[1] 是第二行的一维数组名,包含元素 a[1][0]~a[1][3]

2. 行名(如a[0])的特性

行名本质是 “一维数组名”,具备一维数组名的所有特征:

  • 地址常量:行名代表对应行的首地址(a[0] == &a[0][0]),不可修改(如a[0]++错误)。

  • sizeof 运算sizeof(a[0])返回对应行的总字节数(如int a[3][4]中,sizeof(a[0])=16 → 4 元素 ×4 字节)。

四、二维数组的定义举例与编程验证

1. 验证存储连续性与行名特性

#include <stdio.h>
​
int main() {int a[2][3] = {{1,2,3}, {4,5,6}};
​// 1. 验证存储连续性(行序优先)printf("验证存储顺序:\n");for (int i = 0; i < 2; i++) {for (int j = 0; j < 3; j++) {printf("a[%d][%d]地址:%p  ", i, j, &a[i][j]);}printf("\n");}
​// 2. 验证行名是一维数组名printf("\n行名a[0]的地址:%p(与a[0][0]地址相同)\n", a[0]);printf("a[0]行的总字节数:%zu(3元素×4字节)\n", sizeof(a[0]));
​return 0;
}
  • 输出分:

    • 地址依次递增 4 字节,验证连续存储和行序优先。

    • a[0]的地址与a[0][0]相同,sizeof(a[0])=12(3×4),验证行名的一维数组特性。

五、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
二维数组定义语法:类型 数组名[行数][列数];列数不可省略;元素总数 = 行数 × 列数行列顺序(先行数后列数);初始化时行数可省略(列数必须写)⭐⭐
内存存储特点行序优先存储(先存满一行再存下一行);元素连续排列相邻元素地址差 = 元素类型字节数(如 int 差 4);行尾与下行首地址连续⭐⭐⭐
与一维数组的关系二维数组是 “一维数组的数组”(如a[0]是第一行的一维数组名)行名的地址常量特性(a[0]++错误);sizeof(a[0])返回行总字节数⭐⭐⭐⭐
应用场景表示图结构(邻接矩阵),行和列对应顶点,元素值表示连接状态(0/1)5×5 数组可表示 5 个顶点的连接关系;graph[i][j]=1的实际意义(i 与 j 相连)⭐⭐⭐
编程验证要点打印地址验证连续性;通过sizeof验证行名的一维数组特性存储顺序是后续指针操作的基础(需牢记行序优先)⭐⭐⭐

六、编程建议

  1. 行列遍历:使用双重循环(外层行、内层列),确保下标在0~行数-1和0~列数-1范围内。

    int a[3][4];
    for (int i = 0; i < 3; i++) {  // 行循环for (int j = 0; j < 4; j++) {  // 列循环a[i][j] = i * 4 + j;  // 按存储顺序赋值}
    }
  2. 初始化技巧:使用嵌套大括号明确行列(增强可读性):

    int a[2][3] = {{1, 2, 3},  // 第一行{4, 5, 6}   // 第二行
    };
  3. 结合后续学习:二维数组的存储规律是指针操作的基础(如用指针遍历二维数组需按行序优先计算偏移量),需重点理解。

 

二维数组:内存、初始化与访问

一、二维数组的内存连续性验证

1. 验证原理与方法

二维数组在逻辑上是 “行列结构”,但物理内存中按行优先顺序连续存储(先存满一行,再存下一行)。通过打印元素地址可直观验证这一特性。

2. 示例代码

#include <stdio.h>int main() {int a[2][3] = {{1, 2, 3}, {4, 5, 6}};  // 2行3列的二维数组// 双重循环遍历并打印元素地址for (int i = 0; i < 2; i++) {  // 外层循环控制行(i=0、1)for (int j = 0; j < 3; j++) {  // 内层循环控制列(j=0、1、2)printf("a[%d][%d]地址:%p  ", i, j, &a[i][j]);}putchar('\n');  // 每行结束后换行}return 0;
}

3. 输出分析(地址为示例,实际因系统而异)

a[0][0]地址:0xbfd04958  a[0][1]地址:0xbfd0495c  a[0][2]地址:0xbfd04960  
a[1][0]地址:0xbfd04964  a[1][1]地址:0xbfd04968  a[1][2]地址:0xbfd0496c  
  • 地址规律:

    • 相邻元素地址差为 4 字节(int类型大小),如0xbfd049580xbfd0495c差 4。

    • 第一行末尾(a[0][2])与第二行开头(a[1][0])地址连续(0xbfd049600xbfd04964差 4),验证 “行优先存储”。

二、二维数组的本质理解

1. 行名的概念与特性

二维数组可视为 “一维数组的数组”,其中行名(如a[0])是对应行的一维数组名,具备以下特性:

  • 地址常量:行名代表该行首元素地址(a[0] == &a[0][0]),不可修改(如a[0]++会报错 “左值必须为可修改的左值”)。

  • sizeof计算sizeof(a[0])返回该行的总字节数(如int a[2][3]中,sizeof(a[0]) = 3×4 = 12字节)。

2. 内存结构与逻辑结构的区别

  • 逻辑结构:以

    int a[2][3]

    为例,可想象为 2 行 3 列的表格:

    a[0][0]  a[0][1]  a[0][2]
    a[1][0]  a[1][1]  a[1][2]
  • 物理内存结构:实际存储为连续的线性空间,顺序为: a[0][0] → a[0][1] → a[0][2] → a[1][0] → a[1][1] → a[1][2]

三、二维数组的初始化

1. 初始化方式与规则

(1)全初始化(分行显式赋值)

int a[2][3] = {{1, 2, 3}, {4, 5, 6}};  // 规范写法:外层大括号包含所有行,每行用内层大括号

 

  • 特点:大括号嵌套明确区分行,元素数量需与声明的 “行数 × 列数” 完全匹配(2 行 3 列共 6 个元素)。

(2)部分初始化(未赋值元素自动补 0)

int b[2][3] = {{1, 2}, {4}};  // 部分初始化
// 实际存储为:
// 第一行:1, 2, 0(未赋值的a[0][2]补0)
// 第二行:4, 0, 0(未赋值的a[1][1]、a[1][2]补0)
  • 规则:

    • 若某行元素少于列数,剩余元素补 0。

    • 若初始化行数少于声明行数,剩余行的所有元素补 0(如int c[3][3] = {{1}, {2}},第三行为0,0,0)。

(3)省略行数(编译器自动推导)

int d[][3] = {1, 2, 3, 4};  // 列数必须显式声明(3列),行数由编译器推导
// 推导逻辑:总元素数4 ÷ 列数3 = 1行余1个 → 自动扩展为2行(1+1)
// 实际存储为2行3列:
// 第一行:1, 2, 3
// 第二行:4, 0, 0
  • 关键限制列数绝对不能省略(否则编译器无法确定每行元素数量),行数可省略(由总元素数 ÷ 列数推导)。

2. 初始化注意事项

(1)大括号的作用

  • 规范的大括号嵌套可实现 “分散赋值”(指定某行某列赋值,其他补 0):

    int e[3][3] = {{}, {2}, {3, 4}};  // 第一行全0,第二行2,0,0,第三行3,4,0
  • 若省略内层大括号,元素会按行优先顺序填充(可能不符合预期):

    int f[2][3] = {1, 2, 3, 4};  // 等价于{{1,2,3}, {4,0,0}},而非{{1,2}, {3,4}}

(2)常见错误

  • 列数省略int g[][] = {{1,2}, {3,4}} → 编译错误(“不完整的元素类型”)。

  • 元素数量超过声明int h[2][3] = {{1,2,3}, {4,5,6,7}} → 编译错误(“初始值过多”)。

四、二维数组的访问与遍历

1. 元素访问方式

通过 “数组名 行下标” 访问,如a[i][j]i为行索引,j为列索引,均从 0 开始)。

2. 遍历方法(双重循环)

#include <stdio.h>int main() {int a[2][3] = {{1, 2, 3}, {4, 5, 6}};// 外层循环控制行(i:0 ~ 行数-1)for (int i = 0; i < 2; i++) {// 内层循环控制列(j:0 ~ 列数-1)for (int j = 0; j < 3; j++) {printf("a[%d][%d] = %d  ", i, j, a[i][j]);}printf("\n");  // 每行结束后换行}return 0;
}
  • 输出:

    a[0][0] = 1  a[0][1] = 2  a[0][2] = 3  
    a[1][0] = 4  a[1][1] = 5  a[1][2] = 6  

五、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
内存连续性按行优先连续存储,元素地址差 = 元素类型字节数(如 int 差 4);行尾与下行首地址连续地址增量规律(如0xbfd049580xbfd0495c);行优先 vs 列优先的区别⭐⭐
数组名与行名特性数组名是地址常量(a++错误),sizeof(a)返回总字节数;行名(a[0])是一维数组名,sizeof(a[0])返回单行字节数sizeof计算层级(总数组 vs 单行);行名不可修改(a[0] = &b错误)⭐⭐⭐
初始化规则全初始化需分行大括号;部分初始化未赋值元素补 0;行数可省略(列数必须写)列数省略导致编译错误;大括号嵌套实现分散赋值(如{{}, {2}}⭐⭐⭐
访问与遍历双重循环嵌套(外层行、内层列);元素访问a[i][j],地址&a[i][j]循环变量范围(i < 行数j < 列数);地址连续性验证代码的实现⭐⭐
与指针的关联行名(a[0])等价于一维数组名,为后续指针操作(如*(a[i]+j))铺垫地址常量特性的继承;多维数组指针运算的基础(需结合内存连续性)⭐⭐⭐⭐

六、编程技巧

  1. 动态计算行数和列数:

    int a[2][3] = {{1,2,3}, {4,5,6}};
    int rows = sizeof(a) / sizeof(a[0]);  // 总行数 = 总字节数 ÷ 单行字节数 → 2
    int cols = sizeof(a[0]) / sizeof(a[0][0]);  // 总列数 = 单行字节数 ÷ 单个元素字节数 → 3
  2. 验证内存连续性的通用代码:

    for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("&a[%d][%d] = %p  ", i, j, &a[i][j]);}printf("\n");
    }
  3. 初始化技巧:如需特定位置赋值(其他为 0),用大括号明确标注:

    int graph[5][5] = {{}, {0,0,1}, {}, {0,1,0,0,1}};  // 仅指定行的部分元素赋值

 

字符数组和字符串

一、字符数组

1. 字符数组的概念

  • 本质:元素数据类型为char的数组,是一维 / 二维数组的特殊形式(专门用于存储字符序列)。

  • 定义格式:

    char c[10];  // 一维字符数组(可存10个字符)
    char ch[3][4];  // 二维字符数组(3行4列,可存3个短字符串)
  • 核心特点:

    • 由有序字符变量组成(元素有明确下标顺序)。

    • 所有元素均为char类型(占 1 字节,存储 ASCII 码)。

2. 字符数组的初始化

(1)逐个字符赋值(单引号)

char arr1[5] = {'h', 'e', 'l', 'l', 'o'};  // 全初始化(5个字符)
char arr2[5] = {'h', 'i'};  // 部分初始化(剩余元素自动补'\0')
  • 存储特点:

    • 未显式赋值的元素自动填充'\0'(ASCII 码为 0,等价于整数 0)。

    • '\0'是字符串终止符(非可打印字符),与0NULL在数值上相同(均为 0),但语义不同('\0'用于字符终止,NULL用于指针空值)。

(2)字符串常量赋值(双引号)

char str1[6] = "hello";  // 字符串常量赋值(自动添加'\0',共6个元素:'h','e','l','l','o','\0')
char str2[] = "world";  // 省略长度(编译器自动计算为6:5个字符 + '\0')
  • 核心要求:字符串常量赋值时,编译器会自动在末尾添加'\0',因此字符数组长度需至少为 “字符数 + 1”(否则'\0'可能被截断)。

  • 示例char str[5] = "hello"; 会截断'\0'"hello"含 5 个字符,数组长度 5,无空间存'\0')。

3. 应用案例:字符数组输出问题

(1)未以'\0'结尾的风险

#include <stdio.h>
​
int main() {char arr1[5] = {'h', 'i'};  // 部分初始化,arr1[2]~arr1[4]自动补'\0'char arr2[5] = {'a', 'b', 'c', 'd', 'e'};  // 全初始化,无'\0'
​printf("arr1用%%s输出:%s\n", arr1);  // 正确:遇到'\0'终止 → 输出"hi"printf("arr2用%%s输出:%s\n", arr2);  // 错误:无'\0',会继续读取后续内存(结果随机)return 0;
}
  • 现象分析:

    • %s输出字符串时,需以'\0'作为终止标志(遇到'\0'停止)。

    • '\0'的字符数组(如arr2)会越界读取内存,输出乱码(直到意外遇到'\0')。

(2)正确输出方式

  • 方法 1:用循环逐个输出(不依赖'\0'):

    for (int i = 0; i < 5; i++) {printf("%c", arr2[i]);  // 输出:abcde(无论是否有'\0')
    }
  • 方法 2:确保数组包含'\0'后用%s输出:

    char arr3[6] = {'a', 'b', 'c', 'd', 'e', '\0'};  // 显式添加'\0'
    printf("%s\n", arr3);  // 正确:abcde

二、字符串与字符数组的关系

  • 字符串定义'\0'结尾的字符数组'\0'是字符串的 “终止符”,非字符串内容)。

  • 区别与联系:

    对比项字符数组字符串
    本质存储字符的数组(可无'\0''\0'结尾的字符数组(必须有'\0'
    初始化{'a','b'}(无自动'\0'"ab"(自动添加'\0'
    %s输出可能越界(无'\0'时)安全(遇'\0'终止)
    示例char a[2] = {'a','b'};char b[3] = "ab";(含'\0'

三、多维字符数组

  • 定义:二维字符数组可存储多个字符串(每行一个字符串):

    char strs[2][10] = {"hello", "world"};  // 2行10列,每行存一个字符串
  • 存储特点:

    • 每行是一个独立的字符数组(含'\0')。

    • 行宽(第二个方括号的值)需足够容纳最长字符串 +'\0'(如"hello"需 6 个空间,行宽 10 足够)。

  • 访问方式:

    // 输出所有字符串
    for (int i = 0; i < 2; i++) {printf("%s\n", strs[i]);  // strs[i]是第i行的字符串名
    }

四、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
字符数组定义字符型元素的有序集合,支持一维 / 二维;本质是特殊的数组与整型数组的区别(元素类型为char,初始化用单引号 / 双引号)⭐⭐
初始化方式逐个字符({'a','b'})或字符串常量("ab");后者自动加'\0'字符串常量赋值时的'\0'自动添加(需预留空间);部分初始化剩余元素补'\0'⭐⭐⭐
字符串与字符数组字符串是以'\0'结尾的字符数组;%s依赖'\0'终止'\0'%s输出乱码(越界风险);'\0'的 ASCII 值为 0(与整数 0 等价)⭐⭐⭐⭐
输出方式循环 +%c(逐个输出);%s(依赖'\0');putchar()(单个字符)%s的终止条件(必须有'\0');循环输出的下标范围(0 ~ 长度-1⭐⭐
多维字符数组二维字符数组可存多个字符串(每行一个);行宽需≥最长字符串 + 1行宽不足会截断'\0'(如char a[3][5] = {"hello"}会截断)⭐⭐⭐

五、编程建议

  1. 初始化时预留'\0'空间: 存储n个字符的字符串,数组长度至少为n+1(如"hello"需 6 个空间:char str[6] = "hello";)。

  2. 避免依赖%s输出未知数组: 不确定是否有'\0'时,用循环逐个输出(如for (int i=0; i<5; i++) putchar(arr[i]);)。

  3. 计算字符数组长度:

    char str[] = "hello";
    int len = sizeof(str) / sizeof(str[0]);  // 6(含'\0')
  4. 调试技巧:

    打印元素值和地址验证'\0'是否存在:

    for (int i=0; i<6; i++) {printf("str[%d]=%d(地址:%p)\n", i, str[i], &str[i]);  // '\0'对应值为0
    }

通过以上内容,可清晰掌握字符数组的定义、初始化及字符串的核心特性,避免因'\0'缺失导致的常见错误。

字符数组和字符串(进阶)

一、字符串的核心特性

1. C 语言中字符串的本质

  • 无专门字符串类型:C 语言没有string类型的变量,所有字符串均通过字符数组存储和处理。

  • 实现原理:字符串是 “以'\0'(ASCII 值 0)为结束标志的字符数组”,例如: 字符串"hello"在内存中存储为:'h'、'e'、'l'、'l'、'o'、'\0'(共 6 个字节)。

2. 字符串结束标志'\0'的作用

(1)内存占用与长度计算

#include <stdio.h>
​
int main() {char str[] = "hello";  // 字符串初始化,自动添加'\0'printf("数组长度(含\\0):%zu\n", sizeof(str));  // 输出6(5个字符 + 1个'\0')// 手动计算字符串长度(不含'\0')int len = 0;while (str[len] != '\0') {len++;}printf("字符串长度(不含\\0):%d\n", len);  // 输出5return 0;
}
  • 关键结论:

    • 字符串的内存占用 = 字符数 + 1('\0'的空间)。

    • 字符串的逻辑长度 = '\0'之前的字符总数(不含'\0')。

(2)'\0'的核心作用

  • 作为终止信号printf%sputs等字符串函数,会一直输出字符直到遇到'\0'(无论数组实际长度)。

  • 示例:

    char arr1[] = {'h','e','l','l','o','\0'};  // 标准字符串(含'\0')
    char arr2[] = {'h','e','l','l','o'};       // 普通字符数组(无'\0')
    printf("arr1: %s\n", arr1);  // 正确输出"hello"(遇'\0'终止)
    printf("arr2: %s\n", arr2);  // 输出"hello"后继续读取垃圾数据(直到意外遇到'\0')

二、字符数组的初始化与应用

1. 字符数组的两种初始化方式对比

初始化方式语法示例特点适用场景
逐个字符赋值char c[6] = {'h','e','l','l','o','\0'};需手动添加'\0'(否则不是字符串);未赋值元素自动补'\0'需精确控制每个字符(如特殊符号)
字符串常量赋值char c[] = "hello";编译器自动添加'\0';数组长度自动计算为 “字符数 + 1”(此处为 6)常规字符串(简洁高效)

2. 二维字符数组(存储多个字符串)

  • 应用场景:存储一组相关字符串(如水果名称、城市列表),每行代表一个字符串。

  • 定义与初始化

    // 存储3种水果,每行最多9个字符(含'\0')
    char fruits[3][10] = {"apple", "banana", "cherry"};
    • 第一维(行):字符串的数量(此处 3 行 = 3 个水果)。

    • 第二维(列):单个字符串的最大长度(含'\0',此处 10 列 = 最多 9 个字符)。

  • 行 / 列数计算

    int row = sizeof(fruits) / sizeof(fruits[0]);  // 行数=总字节数÷单行字节数 → 3
    int col = sizeof(fruits[0]) / sizeof(char);    // 列数=单行字节数÷单个字符字节数 → 10
  • 输出方式

    // 方法1:直接用%s输出每行(推荐,简洁)
    for (int i = 0; i < row; i++) {printf("第%d个水果:%s\n", i+1, fruits[i]);  // fruits[i]是第i行的字符串名
    }
    ​
    // 方法2:双重循环逐个字符输出(适用于非字符串数组)
    for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {if (fruits[i][j] == '\0') break;  // 遇到'\0'停止当前行putchar(fruits[i][j]);}putchar('\n');
    }

三、应用案例:字符串逆序输出

1. 需求

输入一个字符串(如"hello"),逆序输出为"olleh"

2. 两种实现思路

(1)方法一:仅逆序输出(不修改原字符串)

  • 核心逻辑:先找到字符串长度('\0'的位置),再从最后一个有效字符向前输出。

  • 代码实现:

    #include <stdio.h>
    ​
    int main() {char str[100];printf("请输入字符串:");scanf("%s", str);  // 输入字符串(自动添加'\0')
    ​// 步骤1:计算字符串长度(不含'\0')int len = 0;while (str[len] != '\0') {len++;}
    ​// 步骤2:从最后一个字符(len-1)逆序输出printf("逆序输出:");for (int i = len - 1; i >= 0; i--) {putchar(str[i]);}return 0;
    }

(2)方法二:修改原字符串为逆序(再输出)

  • 核心逻辑:交换第i个和第len-1-i个字符(对称交换)。

  • 代码实现:

    #include <stdio.h>
    ​
    int main() {char str[100] = "hello";int len = 0;while (str[len] != '\0') len++;  // 计算长度
    ​// 对称交换字符(前半段与后半段互换)for (int i = 0; i < len / 2; i++) {char temp = str[i];str[i] = str[len - 1 - i];str[len - 1 - i] = temp;}
    ​printf("逆序后的字符串:%s\n", str);  // 输出"olleh"return 0;
    }

四、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
字符串的本质C 语言无字符串变量,通过字符数组存储;以'\0'为结束标志字符串长度计算(不含'\0');"hello"的内存占用(6 字节,含'\0'⭐⭐
字符数组初始化单引号逐个赋值(需手动加'\0');双引号字符串赋值(自动加'\0'部分初始化时未赋值元素补'\0';字符串赋值需预留'\0'空间(否则截断)⭐⭐
二维字符数组用于存储多个字符串(每行一个);行 / 列数通过sizeof计算行宽不足会截断'\0'(如char a[2][5] = {"apple"}"apple"需 6 字节导致截断)⭐⭐⭐
字符串输出%s输出(依赖'\0');putchar逐字符输出(不依赖'\0'非字符串字符数组(无'\0')用%s输出会乱码⭐⭐
字符串逆序方法一:逆序打印(不修改原数据);方法二:交换字符实现逆序存储边界条件处理(len的计算、循环终止条件i < len/2⭐⭐⭐

五、编程技巧与注意事项

  1. 字符串长度计算: 通用公式(兼容任何字符串):

    int len = 0;
    while (str[len] != '\0') len++;  // 循环直到找到结束标志
  2. 避免数组越界

    • 定义数组时,长度至少为 “最大预期字符数 + 1”(预留'\0'空间)。

    • 输入字符串时,用scanf("%99s", str)限制输入长度(str为 100 字节数组时)。

  3. 二维字符串数组的简化操作

    • 输出时直接用

      %s

      打印每行(无需双重循环):

      for (int i = 0; i < 3; i++) {printf("%s\n", fruits[i]);  // 直接输出第i个字符串
      }
  4. '\0'的处理

    • 手动构造字符串时,务必在末尾添加'\0'(如char str[6] = {'h','i','\0'};)。

    • 逆序或修改字符串后,'\0'的位置不变(始终在最后)。

通过以上内容,可以掌握字符数组与字符串的核心关联,以及字符串处理的常见技巧,尤其要注意'\0'的作用 —— 它是区分 “字符数组” 和 “字符串” 的关键标志。

 

http://www.xdnf.cn/news/1138141.html

相关文章:

  • MyBatis 之配置与映射核心要点解析
  • 三轴云台之测距算法篇
  • 硅谷顶级风投发布《2025年AI实战手册》|附下载
  • 【Elasticsearch】Elasticsearch 快照恢复 API 参数详解
  • 一次多架构镜像构建实战:Docker Buildx + Harbor 踩坑记录
  • arping(ARP协议网络测试工具)
  • ota之.加密算法,mcu加密方式
  • git基本操作【GIT-2】
  • 进阶向:智能图像背景移除工具
  • Java并发编程第三篇(深入解析Synchronized)
  • 2025年5大国产ETL工具横向评测
  • 20250717 Ubuntu 挂载远程 Windows 服务器上的硬盘
  • ROS1/Linux——Launch文件使用
  • 创建项目:使用Spring Initializr初始化你的第一个应用程序
  • display:flex弹性布局
  • 聊聊数据和功能测试面临的挑战有哪些?
  • c++ 模板元编程
  • .NET Framework版本信息获取(ASP.NET探针),获取系统的.NET Framework版本
  • React 学习(4)
  • 学习软件测试的第十八天
  • NLP-文本预处理
  • UGUI 性能优化系列:第一篇——基础优化与资源管理
  • React事件处理
  • Redis学习系列之—— JDHotKey 热点缓存探测系统
  • 3D材质总监的“光影魔法”:用Substance Sampler AI,“擦除”照片中的光影
  • DeepSeek:大模型时代多模态AI数据库的破局者
  • 基于springboot+vue+mysql技术的在线考试系统设计与实现(源码+论文)
  • AndroidStudio环境搭建
  • x3CTF-2025-web-复现
  • 【SAP SD】跨公司销售、第三方销售、STO采购(公司间合同配件)