一、题目 19:字符指针与字符串常量的修改问题
题目
char *s="AAA";
printf("%s",s);
s[0]='B';
printf("%s",s);
解析
上述代码试图通过字符指针修改字符串常量的内容,这是错误的。下面从知识点、拓展示例逐步分析:
1. 字符串常量的存储特性
- 知识点:在 C 语言中,用双引号括起来的字符串(如
"AAA"
)是字符串常量,存储在只读的常量区。其目的是防止程序意外修改,保证程序运行时的稳定性和数据一致性。例如:
char *ptr = "hello"; // "hello" 在常量区,ptr 指向它
// ptr[0] = 'x'; // 错误!试图修改常量区内容,会导致未定义行为
- 解释:常量区的内容在程序运行时不可写,若强行通过指针修改,在一些编译器上会直接报错,在另一些环境下可能导致程序崩溃。
2. 字符指针与字符数组的本质区别
- 知识点:
- 字符指针:如
char *s = "AAA";
,s
是一个指针变量,指向常量区的字符串。它本身只是一个地址值,不能通过它修改所指向的字符串常量内容。 - 字符数组:如
char arr[] = "AAA";
,会在栈区分配一块连续内存存储字符串"AAA"
。由于数组名本质是指向数组首元素的指针(可写),因此可以通过数组名修改数组内的字符。
- 字符指针:如
- 示例对比:
- 字符指针(不可修改常量区):
#include <stdio.h>
int main() {char *s = "AAA";printf("修改前: %s\n", s);// s[0] = 'B'; // 错误!编译可能报错或运行时崩溃printf("(无法成功修改)\n");return 0;
}
- 字符数组(可修改栈区内容):
#include <stdio.h>
int main() {char arr[] = "AAA"; // 在栈区创建数组,内容可修改printf("修改前: %s\n", arr);arr[0] = 'B'; // 合法操作,修改栈区数组内容printf("修改后: %s\n", arr);return 0;
}
- 解释:字符数组在栈区分配内存,其内存区域可写;而字符指针指向常量区时,该区域只读。这是 C 语言内存管理的重要特性。
3. 拓展:正确修改字符串的方式
若要实现字符串内容的修改,应使用字符数组。示例:
#include <stdio.h>
int main() {char str[] = "AAA"; // 栈区存储,可修改printf("原始字符串: %s\n", str);str[0] = 'B'; // 合法修改printf("修改后字符串: %s\n", str);return 0;
}
- 解释:上述代码中,
str
是字符数组,通过下标访问并修改数组元素是合法操作,因为数组在栈区的内存是可写的。
总结
题目 19 的代码错误在于试图通过字符指针修改字符串常量的内容,违背了 C 语言中常量区只读的特性。在实际开发中,若需修改字符串,应使用字符数组而非指向字符串常量的指针。通过理解字符串常量存储位置、字符指针与数组的区别,可避免此类常见错误。
二、题目 20:缓冲区溢出问题
题目
int main() {char a;char *str=&a;strcpy(str,"hello");printf(str);return 0;
}
解析
1. strcpy
函数的特性
- 知识点:
strcpy
是 C 语言中的字符串复制函数,原型为char *strcpy(char *dest, const char *src)
。它的作用是将源字符串src
(包括字符串结束符\0
)逐字节复制到目标缓冲区dest
中。关键特点是不检查目标缓冲区的大小,这就要求程序员确保dest
有足够的空间来容纳src
的内容,否则会导致溢出。 - 解释:在函数参数中,
dest
是目标缓冲区的指针,src
是源字符串的指针。例如strcpy(buf, "test");
,如果buf
的空间不足以容纳 "test"(包括\0
),就会出问题。
2. 缓冲区溢出的概念
- 知识点:缓冲区溢出是指程序试图向固定大小的缓冲区中写入超过其容量的数据。这会导致数据覆盖相邻的内存空间,可能破坏其他变量、函数指针、程序栈结构等,进而引发程序崩溃、错误行为甚至安全漏洞(如恶意代码执行)。
- 解释:就像一个小杯子(缓冲区),却要倒入大量水(数据),水就会流到杯子外面(溢出到其他内存区域),破坏周围环境(其他内存数据)。
3. 本题中的缓冲区溢出分析
- 步骤:
- 代码中
char a;
只给变量a
分配了 1 个字节的空间。 char *str = &a;
让指针str
指向这个 1 字节的空间。strcpy(str, "hello");
中,"hello" 字符串包含 5 个字符,再加上结束符\0
,共 6 个字节。- 由于
strcpy
不检查目标缓冲区大小,将 6 字节数据写入 1 字节的a
所在空间,必然发生缓冲区溢出,覆盖相邻的内存区域,程序行为不可预测,可能崩溃。
- 代码中
- 示例对比:
- 错误示例(本题):
#include <stdio.h>
#include <string.h>
int main() {char a;char *str = &a;strcpy(str, "hello"); // 尝试将6字节数据写入1字节空间,溢出printf(str);return 0;
}
- 正确示例(足够缓冲区):
#include <stdio.h>
#include <string.h>
int main() {char buf[10]; // 分配10字节缓冲区char *str = buf;strcpy(str, "hello"); // "hello"占6字节,10字节空间足够,无溢出printf(str);return 0;
}
- 解释:正确示例中,
buf
有足够空间容纳要复制的字符串,避免了溢出。而本题中a
空间过小,导致溢出。
4. 拓展:避免缓冲区溢出的方法
- 使用安全函数:如
strncpy
,原型为char *strncpy(char *dest, const char *src, size_t n)
,它最多复制n
个字符。这样可以控制复制的数据量,避免溢出。 - 示例:
#include <stdio.h>
#include <string.h>
int main() {char a[5]; // 假设只有5字节空间char *str = a;strncpy(str, "hello", sizeof(a) - 1); // 最多复制4字节(sizeof(a)-1),避免溢出str[sizeof(a) - 1] = '\0'; // 手动添加字符串结束符printf(str);return 0;
}
- 解释:
strncpy
通过参数n
限制复制字符数,这里sizeof(a) -1
确保不超过a
的空间(留一个字节给\0
),然后手动添加\0
保证字符串完整性。
总结
题目 20 的代码因strcpy
不检查缓冲区大小,将过长字符串复制到过小的缓冲区a
中,导致缓冲区溢出。在嵌入式开发中,缓冲区溢出可能引发严重问题,必须谨慎处理字符串复制等操作,可使用strncpy
等安全函数避免此类问题。通过理解strcpy
特性、缓冲区溢出概念及避免方法,能更好地写出安全可靠的代码。
三、题目 21:未初始化数组与 strlen
的使用
题目
void main() {char aa[10];printf("%d",strlen(aa));
}
解析
1. strlen
函数的工作原理
- 知识点:
strlen
函数用于计算字符串的长度,其原型为size_t strlen(const char *s)
。它从指针s
指向的地址开始,逐字节查找字符\0
(字符串结束标志),在找到\0
之前所经过的字符个数就是字符串的长度,且不包含\0
本身。 - 解释:例如,对于字符串
"abc"
,strlen
会依次检查'a'
、'b'
、'c'
,直到遇到\0
,返回值为3
。
2. 未初始化数组的内容特性
- 知识点:当定义一个数组
char aa[10];
但未对其进行初始化时,数组元素的值是不确定的,它们是内存中原来的随机值。这些值可能包含\0
,也可能不包含,完全不可预测。 - 解释:数组在内存中分配了一段连续的空间,但如果没有显式初始化,这块空间的内容是之前其他程序或操作留下的,就像一个房间没整理,里面的东西杂乱无章。
3. 本题代码的问题分析
- 步骤:
- 代码中定义了
char aa[10];
,这是一个未初始化的字符数组,它在内存中的值是随机的。 strlen(aa)
会从aa
的首地址开始查找\0
。由于aa
未初始化,aa
中何时出现\0
是不确定的。- 这可能导致
strlen
访问到数组边界之外的内存(如果\0
不在aa
所分配的 10 个字节内),结果完全不可预测,并且这种行为是危险的,可能引发程序崩溃或其他未定义行为。
- 代码中定义了
- 示例对比:
- 错误示例(本题):
#include <stdio.h>
#include <string.h>
void main() {char aa[10]; // 未初始化,内容随机printf("%d\n", strlen(aa)); // 结果不可预测,可能访问非法内存
}
- 正确示例(初始化数组):
#include <stdio.h>
#include <string.h>
void main() {char aa[10] = {0}; // 初始化数组,所有元素为 0,即包含 `\0`printf("%d\n", strlen(aa)); // 输出 0,因为遇到第一个 `\0` 就停止
}
- 解释:正确示例中,数组
aa
被初始化为全 0,strlen
很快就能找到\0
,得到确定的结果0
。而本题中未初始化的aa
无法让strlen
得到有意义的、可预测的结果。
4. 拓展:数组的正确使用与初始化
- 知识点:为了确保程序的正确性和可预测性,数组在使用前最好进行初始化。初始化方式有多种,例如:
- 完全初始化:
char arr[5] = {'a', 'b', 'c', 'd', 'e'};
- 部分初始化:
char arr[5] = {'a'};
(其余元素自动初始化为 0) - 全部初始化为 0:
char arr[5] = {0};
- 完全初始化:
- 示例:
#include <stdio.h>
#include <string.h>
void main() {char arr1[5] = {'a', 'b', 'c', 'd', 'e'};char arr2[5] = {'a'};char arr3[5] = {0};printf("%d\n", strlen(arr1)); // 输出 5printf("%d\n", strlen(arr2)); // 输出 1printf("%d\n", strlen(arr3)); // 输出 0
}
- 解释:通过不同的初始化方式,
strlen
能得到符合预期的结果,程序行为可预测。
总结
题目 21 的代码由于使用未初始化的数组 aa
作为 strlen
的参数,导致结果不可预测且存在安全隐患。在嵌入式开发中,对数组进行合理初始化是良好的编程习惯,能避免许多不必要的错误和风险。理解 strlen
的工作原理以及未初始化数组的特性,有助于写出更可靠的代码。
四、题目 22:结构体大小与位域、对齐
题目
struct A {char t:4;char k:4;unsigned short i:8;unsigned long m;
};
解析
1. 位域的概念与存储
- 知识点:位域是 C 语言中一种特殊的结构体成员定义方式,用于指定成员占用的二进制位数。如
char t:4;
表示t
占用char
类型的 4 位(char
通常为 8 位)。多个位域成员若在同一类型内且总位数不超过该类型大小,可共享同一存储单元。 - 解释:对于
char t:4; char k:4;
,两者共占 8 位(1 字节),因为char
类型恰好容纳 8 位,它们在同一char
存储单元内分配。
2. 结构体成员对齐规则
- 知识点:
- 每个成员的存储地址必须是其自身大小的整数倍(对齐)。例如,
unsigned short
通常大小为 2 字节,其地址应是 2 的倍数;unsigned long
通常大小为 4 字节,地址应是 4 的倍数。 - 结构体的总大小必须是其最大成员大小的整数倍。
- 每个成员的存储地址必须是其自身大小的整数倍(对齐)。例如,
- 解释:对齐是为了提高内存访问效率,不同类型的成员在内存中按规则排布,可能会产生填充字节(编译器自动添加)。
3. 计算结构体 A
的大小
- 步骤:
- 处理位域
t
和k
:t:4
和k:4
共占 1 字节(char
类型)。 - 处理
unsigned short i:8
:unsigned short
大小为 2 字节,虽i:8
仅占 1 字节,但需按 2 字节对齐。前 1 字节后,填充 1 字节,i
占 2 字节(累计到第 3 字节)。 - 处理
unsigned long m
:unsigned long
大小为 4 字节,需按 4 字节对齐。前 3 字节后,填充 1 字节,m
占 4 字节(从第 4 字节到第 7 字节)。 - 检查总大小:结构体最大成员为
unsigned long
(4 字节),总大小需是 4 的整数倍。当前计算为 8 字节(满足 4 的倍数)。
- 处理位域
- 示例辅助理解:
- 假设内存地址从 0 开始:
t
和k
占地址 0(1 字节)。- 填充 1 字节(地址 1),
i
占地址 2 - 3(2 字节)。 - 填充 1 字节(地址 4),
m
占地址 4 - 7(4 字节)。总大小 8 字节。
- 假设内存地址从 0 开始:
总结
通过位域共享存储单元和结构体对齐规则的分析,sizeof(struct A)
的结果为 8。在嵌入式开发中,理解结构体对齐和位域的使用至关重要,这不仅影响内存占用,还关系到程序的性能和兼容性。例如,在与硬件寄存器交互时,精确的位域定义和结构体大小计算能确保数据正确读写。
五、题目 23:unsigned char
溢出导致死循环
题目
#define Max_CB 500
void LmiQueryCSmd(StructMSgCB *pmsg) {unsigned char ucCmdNum;for(ucCmdNum=0;ucCmdNum<Max_CB;ucCmdNum++) {//... 循环体(假设执行与 ucCmdNum 相关的操作)}
}
解析
1. 无符号字符型 unsigned char
的取值范围
- 知识点:
unsigned char
是无符号字符型数据类型,在 C 语言中通常占用 1 字节(8 位),取值范围为 0 ~ 255(因为无符号数不能表示负数,所有位用于表示数值,即 28−1=255)。 - 解释:
当对unsigned char
类型的变量进行赋值或运算时,若结果超过 255,会发生 无符号整数溢出。根据 C 语言标准,无符号整数溢出的行为是 定义明确的—— 结果会对 2n 取模(n 为数据类型的位数),即溢出后的值为(原值) % 256
。
例如:unsigned char a = 255; a++; // 溢出,a 的值变为 0(因为 255 + 1 = 256,256 % 256 = 0)
2. 循环条件与变量类型的不匹配
- 题目分析:
代码中循环条件为ucCmdNum < Max_CB
(即ucCmdNum < 500
),但ucCmdNum
的类型是unsigned char
,最大只能表示 255。- 当
ucCmdNum
递增到 255 时,下一次递增会溢出为 0(255 + 1 = 0
)。 - 此时
0 < 500
条件成立,循环会无限继续,形成死循环。
- 当
- 步骤分解:
- 初始化:
ucCmdNum = 0
(满足0 < 500
,进入循环)。 - 递增:每次循环
ucCmdNum++
,直到ucCmdNum = 255
(第 256 次循环)。 - 溢出:
255 + 1 = 0
(无符号溢出,值回到 0)。 - 条件判断:
0 < 500
成立,再次进入循环,重复步骤 2-4,导致死循环。
- 初始化:
3. 错误示例与正确修改方案
-
错误示例(死循环):
#include <stdio.h> #define Max_CB 500 int main() {unsigned char ucCmdNum;int count = 0; // 用于统计循环次数for(ucCmdNum = 0; ucCmdNum < Max_CB; ucCmdNum++) {count++;if (count > 1000) break; // 防止无限循环,仅用于演示}printf("循环次数:%d\n", count); // 输出远大于 500,说明死循环return 0; }
输出(示例):
循环次数:1001(实际次数因溢出会不断循环,此处为强制终止后的结果)
-
正确示例(避免溢出):
#include <stdio.h> #define Max_CB 500 int main() {unsigned int ucCmdNum; // 使用 unsigned int(取值范围 0~4294967295,足够容纳 500)int count = 0;for(ucCmdNum = 0; ucCmdNum < Max_CB; ucCmdNum++) {count++;}printf("循环次数:%d\n", count); // 输出 500,正常结束return 0; }
修改关键:
将循环变量类型改为unsigned int
(或int
),确保其取值范围覆盖循环上限Max_CB
(500),避免溢出。
4. 拓展:嵌入式开发中数据类型的选择原则
-
原则 1:根据数据范围选择最小合适的类型
- 若数据范围在 0~255 内,使用
unsigned char
; - 若数据范围在 0~65535 内,使用
unsigned short
; - 若数据范围更大,使用
unsigned int
或unsigned long
。
示例:
// 错误:数据范围 0~1000 超过 unsigned char 的 0~255 unsigned char error_var = 1000; // 实际存储为 1000 % 256 = 232(错误赋值)// 正确:使用 unsigned int 或 unsigned short unsigned short correct_var = 1000; // 合法,1000 ≤ 65535
- 若数据范围在 0~255 内,使用
-
原则 2:警惕无符号类型的溢出风险
无符号类型溢出虽定义明确(取模),但可能导致逻辑错误(如本例的死循环)。在循环、计数等场景中,优先使用有符号类型(如int
)或确保类型范围足够。
示例:// 有符号类型溢出是未定义行为,需避免 char a = 127; a++; // 有符号溢出,行为未定义(可能变为 -128 或其他值)// 无符号类型溢出定义明确,但需注意逻辑正确性 unsigned char b = 255; b++; // 确定变为 0
-
原则 3:利用编译器警告
编译时开启严格警告(如 GCC 中使用-Wall -Wextra
),编译器会提示类型范围不匹配的风险:gcc -Wall -Wextra main.c # 可能提示:comparison between 'unsigned char' and 'int' (500 is int type)
总结
题目 23 的核心错误是 循环变量类型(unsigned char
)的取值范围小于循环上限(500),导致溢出后循环条件永远成立。解决方法是根据数据范围选择足够大的类型(如 unsigned int
)。在嵌入式开发中,数据类型的选择直接影响程序的正确性和稳定性,需严格遵循 “根据范围选类型” 的原则,避免因溢出导致死循环、逻辑错误或安全漏洞。