南京江北新区包括哪些地方如何优化关键词

张小明 2026/1/1 14:21:34
南京江北新区包括哪些地方,如何优化关键词,设备管理系统网站模板,台州低价关键词优化第一部分#xff1a;C 语言基础 1. helloworld 配置环境 编辑器: Visual Studio Code (VS Code)#xff0c;一款轻量且强大的代码编辑器。编译器: MinGW-w64#xff0c;在 Windows 上提供 GCC 编译环境#xff0c;可将 C 代码编译为可执行文件。推荐插件: C/C (by Microso…第一部分C 语言基础1. helloworld配置环境编辑器: Visual Studio Code (VS Code)一款轻量且强大的代码编辑器。编译器: MinGW-w64在 Windows 上提供 GCC 编译环境可将 C 代码编译为可执行文件。推荐插件:C/C(by Microsoft),Code Runner(用于快速运行代码)。VSCode 配置 C/C环境使用 MinGW 编译器VS Code安装 Code Runner 插件方法:点击 VS Code 菜单栏的文件 (File) - “首选项” (Preferences) - “设置” (Settings)或者使用快捷键Ctrl ,。在设置页面中搜索Run Code configuration。找到Run In Terminal 选项并勾选上。实现程序在 vscode 中输入main即可快速实现示例程序main函数可以接收来自命令行的参数这在编写工具类程序时非常有用。int argc: (Argument Count) 整型变量存储传递给程序的命令行参数的数量包括程序本身。char *argv[]: (Argument Vector) 字符串数组argv[0]是程序名argv[1]是第一个参数以此类推。###include stdio.h // 编译后运行: ./a.out param1 param2 int main(int argc, char *argv[]) { printf(程序名: %s\n, argv[0]); // 输出: ./a.out printf(第一个参数: %s\n, argv[1]); // 输出: param1 printf(第二个参数: %s\n, argv[2]); // 输出: param2 return 0; }2. 数据类型与格式化 IO基本数据类型C 语言中的数据类型定义了变量可以存储的数据种类和范围。类型分类关键字描述格式说明符整型int基本整数通常 4 字节%dshort短整型通常 2 字节%hdlong长整型通常 4 或 8 字节%ldlong long更长整型至少 8 字节%lldunsigned无符号修饰范围从 0 开始%u,%lu,%llu浮点型float单精度浮点数约 7 位有效数字%fdouble双精度浮点数约 15 位有效数字 (更常用)%lf字符型char单个字符本质是 1 字节的整数 (ASCII 码)%c,%hhd(整数)常量与变量变量: 内存中用于存储数据的具名空间其值可以改变。常量: 值在程序运行期间不可改变。字面常量: 如100,3.14,A,hello。宏定义常量: 使用#define在预编译阶段进行文本替换无类型检查。const修饰的变量: 具有类型会进行类型检查更安全。###define PI 3.14159 // 宏常量 int main() { int age 25; // 变量 const int MAX_USERS 100; // const常量 age 26; // OK // MAX_USERS 101; // Error: 试图修改const变量 }格式化输入与输出printf(): 格式化输出函数将数据按指定格式打印到屏幕。scanf(): 格式化输入。scanf返回成功读取的项数。如果输入类型不匹配会返回0并将错误输入留在输入缓冲区 (stdin)中可能导致后续读取问题。scanf需要传入变量的地址使用运算符获取。格式符描述示例%d十进制整数123%ffloat类型浮点数3.14%lfdouble类型浮点数3.1415926%cchar单个字符A%sstring字符串hello%ppoint指针地址十六进制0x7ffc...%xhexadecimal十六进制整数ff%oOctal八进制整数177%%输出一个%符号%注printf 使用 %f 即可打印 float 和 double。这是因为在传递给 printf 这样的可变参数函数时float 类型的实参会被自动提升(promote)为 double 类型。而 scanf 传入的是指针需要通过 %lf 明确告知函数要写入一个 double 大小的内存。清理输入缓冲区: 当scanf失败时必须清理缓冲区中的非法输入。int a; while (1) { printf(请输入一个整数: ); int retval scanf(%d, a); // a是取地址 if (retval 1) { printf(成功读取: %d\n, a); break; // 成功则退出循环 } else { printf(输入错误正在清理输入缓冲区...\n); char buf[10]; // 准备一个“垃圾桶” fgets(buf, 10, stdin); // 把留在输入队列里的垃圾读走扔到垃圾桶里 //或者用下面这句话每次读一个来清掉直到输入队列清空 // while (getchar() ! \n); } }单字符输入输出 (getchar,putchar)这两个函数效率更高用于处理单个字符的 I/O。getchar(): 从stdin读取一个字符并以int类型返回。putchar(): 将一个字符以int形式传递写入stdout。###include stdio.h int main() { printf(请输入一个字符: ); int c getchar(); // 用int接收以处理EOF printf(你输入的字符是: ); putchar(c); putchar(\n); return 0; }类型转换自动类型转换: 在运算中低精度类型会自动转换为高精度类型。 (char-int-float-double)强制类型转换: 使用(目标类型)语法强制转换。int a 10; int b 3; float result (float)a / b; // 将a强制转为float结果为3.333... // 如果不转换int / int 结果为3 int int_result a / b; // 结果为 3科学计数法浮点数可以使用e或E表示 10 的幂这在表示极大或极小值时很有用。double large_num 3.14e8; // 3.14 * 10^8 double small_num 1.23e-5; // 1.23 * 10^-5数字的进制表示我们可以用不同进制来输出数字这对调试程序操作寄存器等底层硬件非常重要前缀:十六进制:0x或0X八进制:0十进制: 无前缀在内存中所有进制的数最终都以二进制形式存储。int dec_val 100; // 十进制 int hex_val 0x64; // 十六进制 (值为100) int oct_val 0144; // 八进制 (值为100) // 使用不同格式说明符打印 printf(Decimal: %d, %d, %d\n, dec_val, hex_val, oct_val); printf(Hex: %x, %x, %x\n, dec_val, hex_val, oct_val); printf(Octal: %o, %o, %o\n, dec_val, hex_val, oct_val); // 使用#标志位打印进制前缀 printf(Hex with prefix: %#x, Octal with prefix: %#o\n, hex_val, oct_val);面试题实战-浮点数的安全比较为什么不能直接用比较两个浮点数由于计算机使用二进制表示浮点数时存在固有的精度误差直接用比较通常会失败。例子十进制 0.1 转二进制计算过程正确做法是判断两个浮点数之差的绝对值是否小于一个预先设定的极小值epsilon。代码示例:##include float.h ##include math.h ##include stdbool.h ##include stdio.h /* * 浮点数比较因存在精度误差应判断差的绝对值是否小于一个极小阈值(epsilon) * 而非直接使用 。 */ // 1. 绝对误差法 ##define EPSILON 1e-8 // 定义固定的误差阈值 // 判断值是否小于固定阈值。简单但对极大或极小值可能失效。 bool is_zero_abs(double value) { return fabs(value) EPSILON; } // 2. 相对误差法 (推荐) // 使用与基准值相关的动态阈值更通用 bool is_zero_rel(double value, double base_value) { // 阈值上线随基准值大小动态调整下线为1.0 double epsilon EPSILON * fmax(1.0, fabs(base_value)); return fabs(value) epsilon; } int main(int argc, char const *argv[]) { // 错直接用 比较浮点数因精度问题导致判断失败 if (0.1 * 3 - 0.3 0) printf(是0\n); else printf(不是0\n); // 此行被输出 // 绝对误差法对于小数值的运算结果比较有效 if (is_zero_abs(0.1 * 3 - 0.3)) printf(是0\n); // 此行被输出 else printf(不是0\n); // 相对误差法对于大数值的比较也能正确处理 double f1 3.5e7; double f2 f1 0.1; if (is_zero_rel(f2 - f1, f1)) printf( f2-f1 是0\n); // 此行被输出 else printf( f2-f1 不是0\n); return 0; }面试题实战-大型字面量后缀在处理大型数字的宏或常量时中间计算结果可能会超出默认int类型的范围导致溢出即便最终结果可以存放在long long中。为了避免这种情况可以使用后缀强制指定字面量的类型。L或l:longLL或ll:long longU或u:unsignedULL或ull:unsigned long long代码示例:##include stdio.h // 错误的方式: 所有数字都是int, 乘法过程中会溢出 // #define SECONDS_PER_YEAR (365 * 24 * 60 * 60) // 正确的方式: 确保第一个数是 long long 类型 // 后续的乘法都会被提升到 long long 类型进行避免溢出 ##define SECONDS_PER_YEAR (365 * 24 * 60 * 60ULL) int main(void) { printf(每年秒数: %llu\n, SECONDS_PER_YEAR); return 0; }面试题实战-类型提升中的符号扩展当一个宽度较小的有符号类型如char,short被提升为一个宽度较大的类型如int时如果原始值是正数高位用0填充。如果原始值是负数即最高位为 1则会发生符号扩展 (Sign Extension)高位用1填充以保持其负值。代码示例:##include stdio.h int main(void) { // 0xaa 的二进制是 10101010。作为有符号char其最高位是1为负数。 char c 0xaa; // 当 c 被赋值给整型变量 j 时它被提升为 int // 由于 c 是负数发生符号扩展高位全用1填充 int j c; // 结果: 0xffffffaa printf(j %#x\n, j); return 0; }这个特性在处理来自硬件或网络的字节流时尤其重要.面试题实战-大小端序转换建议先看下面的内容理解字节序探析大端与小端的比较简单总结如下如果需要逐位运算或者需要到从个位数开始运算都是小端序占优势反之如果运算只涉及到高位或者数据的可读性比较重要则是大端序占优势所以计算机存储一般都用小端序方便计算大端序一般方便人类阅读题目写一个函数要求把小端序存放的整数转换成大端序存放代码示例:int change(int n){ int result0; unsigned char *p (unsigned char*)n; unsigned char *q (unsigned char*)result; for(int i0; i4; i){ *(q 3 - i) *(p i); } return result; }3. 运算符算术运算符(加),-(减),*(乘),/(除),%(取余/模)自增/自减:,--前缀 (a): 先自增再使用变量的值。后缀 (a): 先使用变量的值再自增。int a 5; int b a; // a变为6, b被赋值为6 int c a; // c被赋值为6, a变为7 printf(a%d, b%d, c%d\n, a, b, c); // 输出: a7, b6, c6关系与逻辑运算符关系运算符:(大于),(小于),(不小于),(不大于),(等于),!(不等于)。结果为1(真) 或0(假)。逻辑运算符:!(逻辑非),(逻辑与),||(逻辑或)。短路求值:expr1 expr2: 如果expr1为假expr2不会被执行。expr1 || expr2: 如果expr1为真expr2不会被执行。int x 0, y 5; // x为0(假)右侧不执行y的值不变 if (x (y 10)) { /* ... */ } printf(y %d\n, y); // 输出: y 5位运算符直接对数据的二进制位进行操作。(按位与),|(按位或),^(按位异或),~(按位取反)(左移):x n相当于x * 2^n(右移):x n相当于x / 2^n// 常用位操作技巧 unsigned char reg 0b10101100; // 假设这是一个8位寄存器 // 注意二进制字面量 0b10101100 是GCC扩展 // 1. 置位 (将第3位置1) reg reg | (1 3); // 2. 清零 (将第2位清0) reg reg ~(1 2); // 3. 取反 (翻转第1位) reg reg ^ (1 1); // 4. 测试 (检查第7位是否为1) if (reg (1 7)) { // 位为1 }其他运算符赋值运算符:,,-,*,/,%,,|,^,,条件运算符 (三目运算符):表达式1 ? 表达式2 : 表达式3int a 10, b 20; int max (a b) ? a : b; // max 的值为 20sizeof运算符: 计算数据类型或变量占用的内存字节数。int arr[10]; printf(int类型占 %zu 字节\n, sizeof(int)); // 通常输出 4 printf(数组arr占 %zu 字节\n, sizeof(arr)); // 输出 40 (4 * 10)运算符优先级与结合性优先级: 决定了哪个运算符先执行如*优先于。结合性: 决定了相同优先级的运算符的执行顺序如赋值是从右至左。黄金法则:当不确定优先级时使用圆括号()来明确指定运算顺序。这能避免错误并提高代码可读性。int x 2 * 3 4; // 乘法优先, x 6 4 10 int y 10 5 5 0; // 关系运算符优先于逻辑运算符 int a, b, c; a b c 5; // 赋值运算符从右向左结合, c5, b5, a5面试题实战-字节内比特位反转问题: 编写一个函数将一个字节中的 8 个比特位进行反转。核心知识: 这是对位运算符的深入考察。核心思想是逐位从原字节中取出然后逐位放入新字节的相反位置。代码示例:void revert(int len, char A[], char B[]) { for (int i 0; i len; i) { B[i] 0; for (int k 0; k 8; k) { B[i] B[i] 1; // 先移位 B[i] | (A[i] k) 0x01; // 后赋值 } // 最后不会多移位 } }4. 控制流分支结构 (if / switch)if-else: 用于二路或多路分支判断。int score 85; if (score 90) { printf(优秀\n); } else if (score 60) { printf(及格\n); } else { printf(不及格\n); }switch: 用于基于一个整数值的多路分支通常比if-else if更高效、清晰。case标签后必须是常量表达式。每个case通常以break结束否则会发生“穿透”继续执行下一个case。default处理所有未匹配的case。int day 3; switch (day) { case 1: printf(周一\n); break; case 2: printf(周二\n); break; case 3: printf(周三\n); break; // case 4 ... 5: ... (GCC/Clang 扩展语法) default: printf(其他\n); break; }循环结构 (while / for / do-while)while循环: 先判断条件再执行循环体。可能一次都不执行。int i 0; while (i 5) { //条件成立执行 printf(%d , i); i; } // 输出: 0 1 2 3 4for循环: 集初始化、条件判断、迭代于一体结构清晰常用于已知循环次数的场景。for (int i 0; i 5; i) { //条件成立执行 printf(%d , i); } // 输出: 0 1 2 3 4do-while循环: 先执行一次循环体再判断条件。保证循环体至少执行一次。int i 10; do { printf(至少执行一次\n); } while (i 5); //条件成立执行跳转语句 (break / continue / goto / return)break: 立即跳出当前所在的switch结构或循环结构。continue: 立即结束本次循环跳到循环的下一次迭代判断处。return:立即终止当前函数的执行。将一个值如果函数不是void类型返回给调用者。在void函数中return;可用于提前退出。int check_value(int val) { if (val 0) { printf(Error: invalid value.\n); return -1; // 提前退出并返回错误码 } // ... 正常处理 ... return 0; // 返回成功码 }goto: 无条件跳转到同一函数内的标签处。应谨慎使用易破坏程序结构通常只在错误处理等多层嵌套退出场景下考虑。// 示例: goto用于统一的错误处理 int func() { FILE *fp fopen(a.txt, r); if (fp NULL) { goto error_handle; } char *mem malloc(1024); if (mem NULL) { fclose(fp); // 释放已获取资源 goto error_handle; } // ... 正常逻辑 ... free(mem); fclose(fp); return 0; error_handle: printf(发生错误程序退出\n); return -1; }面试题实战-fgets字符串输入与scanf不同fgets在读取字符串时更加安全因为它会检查缓冲区的边界有效防止溢出。函数原型:char *fgets(char *str, int n, FILE *stream);参数:str: 用于存储输入字符串的缓冲区。n: 最多读取n-1个字符最后一个位置留给\0。stream: 输入流通常是stdin标准输入。特点:安全: 不会超出缓冲区大小。会读取换行符: 如果输入行中包含换行符\nfgets会将其一并读入缓冲区。通常需要手动移除。##include stdio.h ##include string.h int main() { char buffer[10]; printf(请输入一行文字 (最多9个字符): ); if (fgets(buffer, sizeof(buffer), stdin) ! NULL) { // 移除可能存在的换行符 buffer[strcspn(buffer, \n)] \0; printf(你输入的是: %s\n, buffer); } return 0; }第二部分内存、函数与程序结构5. 内存布局与管理对于嵌入式开发深刻理解程序内存组织至关重要它直接影响程序的稳定性、效率和资源消耗。每个 C 程序都运行在一个独立的虚拟内存空间中其结构从低地址到高地址通常如下代码段只读.text: 存放程序的可执行二进制代码.init: 存放系统初始化代码数据段此区域用于存储**生命周期与整个程序相同的全局变量和静态变量**。.rodata (Read-only Data) 存只读数据如字符串字面量 (hello)、const修饰的全局常量。.data存已初始化的全局变量和静态变量。.bss (Block Started by Symbol)存未初始化的全局变量和静态变量程序启动时该区域会被系统自动清零。// 示例不同变量在数据段中的位置 int global_init 10; // 存放在 .data 段 int global_uninit; // 存放在 .bss 段 (值为0) const char* str hello; // hello在 .rodata 段, 指针str在 .data 段 void counter() { static int count 0; // static修饰局部变量使其存入.data段 // 只在首次调用时初始化为0 count; printf(Counter called %d times.\n, count); }static对局部变量的影响当static用于修饰局部变量时它会改变该变量的存储期存储位置: 从栈 (Stack)移动到数据段 (.data / .bss)。生命周期: 从函数调用时创建、返回时销毁自动存储期延长为与整个程序运行时间相同静态存储期。这意味着static局部变量只在第一次执行其定义时被初始化并且它的值在函数调用之间得以保留。关于static的全面总结看笔记 12.4 节void counter() { // count 存储在数据段只在首次调用时初始化为0 static int count 0; count; printf(函数被调用 %d 次.\n, count); } int main() { counter(); // 输出: 函数被调用 1 次. counter(); // 输出: 函数被调用 2 次. return 0; }栈 (Stack)存储内容: 函数的局部变量、函数参数、环境变量、命令行参数及函数调用的上下文返回地址等。核心特点:自动管理: 函数调用时栈向下增长分配函数返回时栈向上收缩释放。程序员无法干预。空间有限: 栈大小预设且较小过大的局部变量或过深的递归会导致栈溢出 (Stack Overflow)。高效率: 分配和释放仅涉及栈指针的移动速度极快。void func(int n) { // n 和 c 都在 func 的栈帧上 char c[10]; } // 函数返回时栈帧自动销毁其内存被释放 int main() { int main_var 100; // main_var 在 main 函数的栈帧上 func(main_var); return 0; }堆 (Heap)堆是唯一一块可由程序员完全控制的内存区域用于动态分配和管理内存。核心特点:手动管理: 必须通过函数如malloc申请并通过free释放。忘记释放会导致内存泄漏。空间巨大: 大小理论上只受限于系统可用物理内存。匿名访问: 堆内存没有变量名只能通过指针来访问。生命周期灵活: 从申请到释放其生命周期由程序员决定。涉及函数(stdlib.h):void* malloc(size_t size): 申请size字节的内存内容未初始化随机值。void* calloc(size_t num, size_t size): 申请num个size字节的内存内容被自动置零。void free(void* ptr): 释放由malloc或calloc申请的内存。free最佳实践:free()后指针ptr会变成一个指向无效内存的悬空指针。必须在free()后立即将指针置为NULL以防止后续误用。###include stdlib.h int main() { // 申请一块能存放5个int的堆内存 int *arr (int*)malloc(5 * sizeof(int)); if (arr NULL) { // 检查内存申请是否成功 return -1; } // 使用内存 for (int i 0; i 5; i) { arr[i] i * 10; } free(arr); // 释放内存 arr NULL; // 防止悬空指针 return 0; }6. 存储期存储期描述了变量在内存中从创建到销毁的生命周期。它与变量存储在哪个内存区域栈、堆、数据段直接相关。自动存储期对应内存区域:栈 (Stack)。生命周期: 从程序执行进入其所在的代码块开始到离开该代码块时结束。内存的分配和释放由系统自动完成。包含变量: 所有局部变量包括函数参数除非被static修饰。void func() { int auto_var 10; // 进入函数时创建 } // 离开函数时auto_var被自动销毁静态存储期对应内存区域:数据段 (.data / .bss)。生命周期: 与整个程序的运行时间相同。在程序启动时创建在程序结束时销毁。包含变量:所有全局变量无论是否用static修饰。使用static修饰的局部变量。int global_var 1; // 静态存储期 void counter() { static int count 0; // 静态存储期只在首次调用时初始化 count; // count的值在函数调用间得以保留 }自定义存储期对应内存区域:堆生命周期: 完全由程序员手动控制。从malloc/calloc成功调用时开始到free被调用时结束。核心风险: 忘记调用free会导致内存泄漏。void dynamic_example() { int *p (int*)malloc(sizeof(int)); // 动态内存创建 if (p) { *p 100; // ... 使用 ... free(p); // 手动销毁 p NULL; } }static关键字全面总结理解static的关键在于区分它修饰的是局部变量还是全局变量/函数。上下文改变的属性效果目的修饰局部变量存储期从自动(栈上) 变为静态(数据段)。生命周期与程序相同。在函数调用之间保持变量值的持久性。修饰全局变量/函数链接属性从外部链接变为内部链接。作用域被限制在当前文件。信息隐藏和避免命名冲突增强模块化。详细解释与示例修饰局部变量 (改变存储期)默认情况: 局部变量存储在栈上函数调用时创建函数返回时销毁。static修饰后:存储位置: 变量从栈移到数据段.data 或 .bss。生命周期: 变量在程序启动时就已创建直到程序结束才销毁。初始化: 只在编译时初始化一次。因此其初始值必须是一个常量表达式如字面量10、c或hello)不能是运行时才存在的变量。核心效果: 变量的值在多次函数调用之间得以保留。###include stdio.h void counter() { // a1 是 static 局部变量存储在数据段生命周期贯穿程序 // 它只在程序加载时被初始化一次 static int a1 0; // OK: 0是常量 int runtime_var 10; // static int a2 runtime_var; // 错误不能用变量初始化static变量 // a3 是 auto 局部变量存储在栈上 // 每次调用 counter() 时a3 都会被重新创建并初始化为 0 int a3 0; a1; a3; printf(static a1 %d, auto a3 %d\n, a1, a3); } int main() { counter(); // 输出: static a1 1, auto a3 1 counter(); // 输出: static a1 2, auto a3 1 counter(); // 输出: static a1 3, auto a3 1 return 0; }修饰全局变量/函数 (改变链接属性)默认情况: 全局变量和函数具有外部链接属性意味着它们可以被项目中任何其他.c文件通过extern关键字访问。static修饰后:链接属性: 变为内部链接。核心效果: 该全局变量或函数的作用域被严格限制在定义它的那个源文件内部对其他文件不可见。module_a.c文件:###include stdio.h // g_global_var 具有外部链接可以被其他文件访问 int g_global_var 10; // g_static_var 具有内部链接仅在此文件内可见 static int g_static_var 20; // global_func() 具有外部链接 void global_func() { printf(这是全局函数.\n); } // static_func() 具有内部链接 static void static_func() { printf(这是静态函数只能在本文件里调用.\n); } void access_vars() { printf(在本文件中: g_global_var %d, g_static_var %d\n, g_global_var, g_static_var); static_func(); }main.c文件:// 声明我们希望从 module_a.c 中使用的变量和函数 extern int g_global_var; extern void global_func(); // 下面的声明会导致链接错误因为 g_static_var 和 static_func 是 static 的 // extern int g_static_var; // extern void static_func(); // 声明一个在 module_a.c 中定义的函数 extern void access_vars(); int main() { printf(在主函数中: g_global_var %d\n, g_global_var); // OK global_func(); // OK // g_static_var 30; // 链接错误: undefined reference to g_static_var // static_func(); // 链接错误: undefined reference to static_func access_vars(); // OK, 调用 module_a 内部的函数来访问其内部变量 return 0; }这个例子清晰地展示了static如何作为模块化的工具将实现细节g_static_var,static_func隐藏在模块内部只暴露公共接口g_global_var,global_func。7. 函数函数是 C 语言的功能模块它将一段可重用的代码封装成一个“黑箱”对外提供清晰的接口隐藏内部实现。函数的构成函数头:返回类型 函数名(参数列表)定义了函数的对外接口。函数体:{ ... }包含函数的具体实现。参数:形参: 函数定义中的变量作为输入。实参: 函数调用时传递的实际值用于初始化形参。返回值: 使用return关键字从函数中返回一个值。void类型表示不返回任何值。局部变量: 定义在函数体内的变量存储在栈上函数返回后即销毁。严禁返回局部变量的地址。// 通过指针交换两个变量的值 // p1和p2是形参(指针)temp是局部变量 void swap(double *p1, double *p2) { double temp *p1; *p1 *p2; *p2 temp; } int main(void) { double a 3.14, b 0.618; // a, b是实参 swap(a, b); // 传递a和b的地址进行交换 return 0; }特殊函数类型递归函数函数在体内调用自身。必须包含递推关系和终止条件否则会因无限递归导致栈溢出。// 计算阶乘: f(n) n * f(n-1) long long factorial(int n) { if (n 0) return 1; // 终止条件 return n * factorial(n - 1); // 递推关系 }静态函数使用static修饰的函数其作用域被限制在当前源文件内用于实现模块化和避免命名冲突。// 该函数只能在定义它自己所在的.c文件中被调用 static void helper_function() { /* ... */ }回调函数通过函数指针作为参数传递给另一函数由后者在特定时机“回调”执行。这是一种强大的解耦机制常见于事件处理、分层设计和系统 API 中。示例 1简单的策略切换下面的eat函数并不关心具体“做什么菜”只负责“吃”。具体菜系yuecai或chuancai由调用方通过函数指针传入实现了行为的灵活切换。###include stdio.h // 定义两个菜系函数它们的签名参数、返回值完全相同 void yuecai(int a, float b) { printf(做粤菜: %d %.1f\n, a, b); } void chuancai(int a, float b) { printf(做川菜: %d %.1f\n, a, b); } // eat函数接收一个函数指针cook作为参数 void eat(void (*cook)(int a, float b)) { int a 1; float f 2.5; cook(a, f); // 调用传入的函数指针 printf(吃饭\n); } int main() { printf(今晚想吃粤菜:\n); eat(yuecai); // 传入粤菜函数的地址 printf(\n明天想吃川菜:\n); eat(chuancai); // 传入川菜函数的地址 return 0; }示例 2系统事件处理 (信号)这是回调函数最经典的应用之一。我们告诉操作系统“当某个事件如SIGINT信号即按下 CtrlC发生时请调用我指定的这个函数my_handler”。这个过程就是注册回调。signal函数详解signal函数是 C 标准库signal.h中用于处理异步信号的核心函数。函数原型:void (*signal(int signum, void (*handler)(int)))(int);这个原型非常复杂是“返回函数指针的函数”的典型例子。我们可以用typedef来简化理解这也是推荐的最佳实践// 1. 为信号处理函数指针定义一个别名 sighandler_t typedef void (*sighandler_t)(int); // 2. 原型现在变得清晰易读 sighandler_t signal(int signum, sighandler_t handler);这样就很清楚了signal函数接收一个信号编号和一个处理函数指针并返回一个旧的处理函数指针。参数说明:int signum: 信号的编号。通常使用标准宏来表示例如SIGINT: 中断信号通常由用户在终端按下CtrlC产生。SIGTERM: 终止信号是kill命令发送的默认信号用于请求程序正常退出。SIGSEGV: 段错误信号当程序试图访问其无权访问的内存区域时产生。sighandler_t handler: 指向信号处理函数的指针。这里可以传递三种值一个自定义函数: 这就是我们的回调函数。它必须接收一个int(信号编号) 参数并返回void。SIG_DFL: 一个特殊的宏表示恢复对该信号的默认处理行为例如SIGINT的默认行为是终止程序。SIG_IGN: 一个特殊的宏表示忽略该信号。返回值:成功时: 返回之前的信号处理函数的指针。这允许你保存旧的处理方式以便之后可以恢复它。失败时: 返回SIG_ERR并设置errno。###include signal.h ###include stdio.h ###include unistd.h // 1. 实现具体的回调函数 (也称 handler 或钩子函数) void my_handler(int sig) { printf(\n接收到信号 %d正在处理...\n, sig); // 在实际应用中这里会执行清理工作 // exit(0); // 可以在处理函数中决定是否退出程序 } int main() { // 2. 注册回调将函数指针my_handler传给signal函数 // 相当于告诉内核当收到SIGINT信号时请调用my_handler signal(SIGINT, my_handler); printf(程序正在运行PID: %d。请按 CtrlC 来触发回调...\n, getpid()); // 无限循环等待信号的到来 while(1) { sleep(1); // sleep可以减少CPU占用 } return 0; // 这行通常不会被执行 }面试题实战-函数参数传递传值 vs 传址C 语言中所有函数参数传递本质上都是传值。但通过传递指针可以达到“传址”的效果。传值:过程: 将实参的副本传递给形参。效果: 函数内部对形参的任何修改都不会影响到函数外部的实参。传址:过程: 将实参的地址通过运算符获取传递给一个指针类型的形参。效果: 函数内部通过解引用指针 (*p)可以直接访问和修改函数外部实参的值。##include stdio.h // 传值a是x的副本 void modify_value(int a) { a 99; // 只修改了副本a不影响外部的x } // 传址p接收了y的地址 void modify_address(int *p) { *p 99; // 通过地址直接修改了外部y的值 } int main() { int x 10; modify_value(x); printf(传值调用后, x %d\n, x); // 输出: 10 int y 10; modify_address(y); printf(传址调用后, y %d\n, y); // 输出: 99 return 0; }8. 作用域作用域定义了标识符变量名、函数名等在代码中的可见范围。合理利用作用域可以避免命名冲突是模块化编程的基础。作用域的类型块作用域:范围: 一对花括号{}内部。特点: 在代码块内定义的变量局部变量仅在该块内可见从定义处开始到右花括号结束。遮蔽: 内层作用域可以定义与外层同名的变量此时内层变量会临时遮蔽外层变量。int a 100; // 全局作用域 int main() { // main函数的块作用域 int a 200; // 遮蔽全局a, 此处a为200 { // 内部块作用域 int a 300; // 再次遮蔽, 此处a为300 printf(%d\n, a); // 输出: 300 } printf(%d\n, a); // 输出: 200, 内部块结束, 遮蔽解除 }文件作用域:范围: 从定义处开始到当前源文件末尾。特点: 在任何函数之外定义的变量全局变量具有文件作用域。默认情况下全局变量可以被其他文件通过extern关键字访问。// a.c 文件 int global_var 10; // 全局变量, 文件作用域 // b.c 文件 extern int global_var; // 声明以访问a.c中的全局变量 void use_global() { printf(%d\n, global_var); // 输出: 10 }函数原型作用域:范围: 仅限于函数声明的括号内。特点: 参数名只在声明中起解释作用在编译时会被忽略可以省略。// a 和 b 的作用域仅在此行 void func(int a, int b);static对全局变量/函数的影响当static用于修饰全局变量或函数时它会改变标识符的链接属性从“外部链接(external)”改为“内部链接(internal)”。效果:static修饰的全局变量或函数的作用域被限制在当前源文件内其他.c文件无法通过extern关键字访问它们。这对于信息隐藏和模块化编程至关重要可以有效避免不同模块间的命名冲突。关于static的全面总结看笔记 12.4 节// a.c 文件 static int file_local_var 5; // 此变量仅在 a.c 中可见 static void helper_func() { /* ... */ } // 此函数仅在 a.c 中可调用 // b.c 文件 // extern int file_local_var; // 错误无法链接到 a.c 中的 static 变量 // extern void helper_func(); // 错误无法链接到 a.c 中的 static 函数第三部分C 语言的精髓指针与复合类型9. 数组数组定义与初始化定义:类型说明符 数组名 [常量表达式];初始化:int b1[5] {1, 2, 3, 4, 5}; // 完全初始化 int b3[5] {1, 2, 3}; // 部分初始化其余元素为未知 int b4[] {1, 2, 3, 4, 5}; // 根据初始化内容推断大小 int b6[5] {[0 ... 4] 1}; // GCC/Clang 的扩展指定初始化所有元素为1数组元素引用通过下标访问:数组名[下标]下标从0开始。long a[5] {1, 2, 3, 4, 5}; printf(%d\n, a[1]); // 输出第二个元素: 2字符数组与多维数组字符数组初始化:char s1[3] {a, b, c}; char s3[5] abc; // 自动在末尾添加 \0多维数组定义与初始化://编译器里只有一维数组 int (c[3]); // 内存分布 |int|int|int| //二维数组定义分为以下两个部分 // 1, x[3] 是数组的定义表示该数组拥有3个元素 // 2, int [4]是元素的类型表示该数组元素是一个具有4个元素的整型数组 int (x[3]) [4]; // 内存分布 |int[4]|int[4]|int[4]|// 定义一个3行4列的二维数组 int y1[3][4] { {1,2,3,4}, {4,5,6,7}, {6,7,8,9} }; // 可以省略第一维的大小 int y4[][4] { {1,2,3,4}, {4,5,6,7} };[面试题实战] 二维数组边界元素遍历这是一个常见的算法问题考验对二维数组索引的精确控制能力。代码示例 (天彩电子-23.c)##include stdio.h ##include stdlib.h ##include time.h void show(int N, int w[N][N]) { for(int i0; iN; i) { for(int j0; jN; j) printf(%d\t, w[i][j]); printf(\n); } } int main(int argc, char const *argv[]) { int N 5; // 示例大小 int w[N][N]; srand(time(NULL)); for(int i0; iN; i) { for(int j0; jN; j) { w[i][j] rand()%1000; } } printf(生成的 %d x %d 数组:\n, N, N); show(N, w); if(N 1) { printf(周围数据平均数是: %f\n, (float)w[0][0]); return 0; } float sum 0.0; // 累加首行和末行 for(int i0; iN; i) { sum w[0][i]; // Top row sum w[N-1][i]; // Bottom row } // 累加首列和末列不含已计算过的角点 for(int i1; iN-1; i) { sum w[i][0]; // Left column (excluding corners) sum w[i][N-1]; // Right column (excluding corners) } // 总边界元素个数为 4*N - 4 printf(\n边界元素总和: %.2f\n, sum); printf(边界元素平均数是: %f\n, sum / (4.0 * N - 4.0)); return 0; }10. 指针指针定义与赋值指针: 存储另一个变量的内存地址。定义:类型说明符 *指针变量名;赋值 (取地址): 使用运算符获取变量地址。int a; int *p; // p是一个指针准备指向一个int型变量 p a; // 将变量a的地址赋给p取地址指针解引用解引用: 使用*运算符访问指针指向的内存地址中的值。*p 100; // 将p指向的地址(即变量a)的值设为100 printf(%d, a); // 输出 100野指针与空指针野指针: 指向不确定或已释放内存的指针。避免: 初始化时置为NULL释放后也置为NULL。空指针: 不指向任何对象的指针其值为NULL。int *p NULL; // 良好习惯初始化指针为NULL指针运算对指针进行,-运算其移动的单位是指针所指向类型的大小。long a[5] {1, 2, 3, 4, 5}; // a是数组首元素地址a1指向第二个元素 printf(%d\n, *(a 1)); // 输出 2面试题实战 -const指针的三种模式辨析const int *p:常目标指针。指针指向的内容*p不可变但指针自身p的指向可以改变。记忆const在*左边修饰的是目标。int * const p:常指针。指针自身p的指向不可变但指针指向的内容*p可以改变。记忆const在*右边修饰的是指针p。const int * const p: 指针自身和其指向的内容都不可变。代码示例int a 10, b 20; // 1. 常目标指针 const int *p1 a; // *p1 100; // 错误: 不能通过p1修改a的值 p1 b; // 正确: p1可以指向其他变量 // 2. 常指针 int * const p2 a; *p2 100; // 正确: 可以通过p2修改a的值 // p2 b; // 错误: p2的指向不能改变 // 3. 两者都不可变 const int * const p3 a; // *p3 100; // 错误 // p3 b; // 错误11. 数组与指针的关系数组名即指针在任意表达式中除了 sizeof 和之外数组名 a 都代表数组的首地址a[i]本质上是*(a i)的语法糖。long a[5] {1, 2, 3, 4, 5}; // a[1] 和 *(a[0]1) 和 *(a1) 是等价的 printf(%d\n, a[1]); // 输出 2 printf(%d\n, *(a 1)); // 输出 2 printf(%d\n, *a); //1 printf(%#lun\n, a); //a的首地址另外一个例子如果看起来吃力可以先看 7.0 复杂指针解读方法#include stdio.h int main(int argc, char const* argv[]) { // a: 二维整型数组 // a 是一个包含2个元素的数组它的每个元素是“一个包含3个int的数组”。 int a[2][3] {1, 2, 3, 5, 6, 7}; // b: 数组指针 // b 是一个指针数组它其中的每个元素指向的目标是“一个包含3个int的数组”。 int (*b)[3]; int sum 0; b a; for (int i 0; i sizeof(a) / 4; i) { // b 指向 a 的第一行*b 的类型是 int[3] // 在表达式中*b 会退化为指向其首元素(a[0][0])的指针 // 因此 **b 就是 a[0][0] 的值 sum *(*b i); } // c: 数组指针的数组 // c 是一个包含2个元素的数组它的每个元素是“一个指向包含3个int的数组的指针”。 int (*c[2])[3] {a[0], a[1]}; // c[]存a[]的地址 for (int i 0; i 2; i) { for (int j 0; j 3; j) { // c[i]存a[i]的地址,a[i]是个数组 // 因为这是一个数组所以也可以等于首元素1的地址 // 向右移动int类型的4字节得到2的地址 // **c[i]解引用得到地址里的值2 sum *(*c[i] j); } } // d: 二维指针数组 // d 的每个元素(d[i][j])的类型是“指向int的指针(int *)”。 int* d[2][3]; for (int i 0; i 2; i) { for (int j 0; j 3; j) { d[i][j] a[i][j]; } } for (int i 0; i 2; i) { for (int j 0; j 3; j) { sum *d[i][j]; } } printf(%d\n, sum); return 0; }函数中的数组参数数组作为函数参数时实际上传递的是指向数组首元素的指针。void func(int arr[])的写法本质上等价于void func(int *arr)。编译器会自动将数组形式的参数转换为指针。核心规则:在任意表达式中除了 sizeof 和之外数组名 a 都代表数组的首地址。当数组名作为函数实参传递时它被转化为指向其首元素的指针。#include stdio.h // 参数 k[3] 实际上是一个指针 (int *k)因此 sizeof(k) 会得到指针的大小通常是8字节 on 64-bit system // 而不是数组的大小 (3 * 4 12 字节)。 void f1(int a, float f, int *p, int k[3]) { printf(sizeof(k) in function: %ld\n, sizeof(k)); // 输出指针大小, e.g., 8 } int main(int argc, char const *argv[]) { int a 100; float f 1.23; int *p a; int k[3] {1, 2, 3}; printf(sizeof(k) in main: %ld\n, sizeof(k)); // 输出数组大小, 12 // 当数组 k 作为参数传递时它退化为指向 k[0] 的指针 f1(a, f, p, k); return 0; }12. 特殊数组与高级指针复杂指针解读方法核心方法右左法则 (Right-Left Rule)从变量名开始。优先向右看遇到[](数组) 或()(函数)。再向左看遇到*(指针)。遇到括号先解析完括号内的再跳出。最后读最左边的类型名。示例 1数组指针int (*b[2])[3]; // 1. b: b 是一个... // 2. b[2]: ...包含2个元素的数组 // 3. (*b[2]): 数组的元素是 指针 // 4. (*b[2])[3]: 指针指向 一个含3个int类型元素的数组 // 结论: b 是一个包含2个元素的数组每个元素都是一个指针指向一个包含3个int的数组示例 2函数指针与typedef最佳实践对于极端复杂的声明手动分析既困难又易错。最佳实践是使用typedef定义别名。识别步骤找到标识符signal。向右看是 ()说明 signal 是一个函数。signal 函数括号内接收两个参数int sig 和 void (*func)(int)对于第二个参数 func我们可以独立应用右左法则func 是一个指向“接收 int返回 void”的函数的指针解析完函数参数回到 signal向左看是 *。说明 signal 函数的返回值是一个指针跳出 signal(…) 的范围继续向右看是 (int)。说明 signal 返回的那个指针指向一个接收 int 参数的函数最后看最左边的类型 void。这个函数返回void 类型// 原始声明难以阅读 void (*signal(int sig, void (*func)(int)))(int); // 最佳实践: 使用 typedef 别名简化 // 1. 为该函数指针类型起一个清晰的别名 typedef void (*handler_t)(int); // 2. 使用别名重写声明一目了然 handler_t signal(int sig, handler_t func);零长数组GNU C 扩展type name[0]用于变长结构体不占用结构体大小。struct stu { int age; float score; char sign[0]; // 零长数组成员 }; // 为结构体和签名分配连续内存 struct stu *s malloc(sizeof(struct stu) 10); strcpy(s-sign, helloworl); printf(%s\n, s-sign); // 输出 helloworl变长数组C99 标准引入数组大小在运行时由变量确定。注意: 变长数组具有自动存储期即在栈上分配因此不能使用static关键字修饰。int len 5; int b[len]; // 正确在栈上创建变长数组 // static int c[len]; // 错误static数组的大小必须在编译时确定 // 无法在编译时给运行时确定大小的数组初始化 // int b[len] {1,2,3,4,5}; for (int i 0; i len; i) b[i] 666;字符指针char *s hello;指针s指向一个字符串字面量是只读的。char *p abcd; printf(%s\n, p 1); // 输出 bcd char *k[3] {12345, abcddegg, www.yeu}; printf(%s\n, *k); // 12345 printf(%s\n, *(k 1)); // 输出 abcddegg printf(%s\n, k[0] 1); // 2345 printf(%c\n, *(k[0] 1)); //2通过 char *p “hello”; 创建的指针 p 指向的是一个字符串字面量。字符串字面量通常存储在内存的只读数据段 (.rodata)。任何试图通过指针修改其内容的行为都是未定义的通常会导致程序崩溃段错误。char *p hello; p[0] H; // 错误非常危险的操作如果你需要一个可以修改的字符串必须使用数组来创建char s[] “hello”;void指针通用指针void *可存储任何类型的地址但不能直接解引用需先强制类型转换。void *m malloc(20); // 将void*指针转换为int*指针再进行操作 for (int i 0; i 5; i) { *((int *)m i) i*100; printf(%d\n, *((int *)m i)); }const指针const int *p: const 修饰*p指针指向的内容不可变 (常目标指针)。int const *p: const 修饰*p指针指向的内容不可变 (常目标指针)。int * const p: const 修饰 p指针自身存的地址不可变 (常指针)。int a 100; // 常指针: const修饰指针变量pp的指向不可改但*p的值可改 (可读写它指向的内容但无法修改它指向谁) int *const p a; *p 200; // OK // p b; // Error // 常目标指针: const修饰类型int*k的值不可改但k的指向可改只读不写 const int *k a; // *k 300; // Error k b; // OK多级指针指向指针的指针如int **p。int w [10]; // w的类型是int [10] int (*pw) [10]; // 一级指针 int (**pw2)[10]; // 二级指针pw2--pw --w (*pw) [0] 666; (**pw2)[0] 666; printf(%p\n, w); // 输出 数组w的首地值 printf(%d\n, w[0]); // 输出: 666 printf(%d\n, (*pw)[0]); // 输出: 666函数指针指向函数的指针是实现回调函数等高级技巧的基础。定义:返回类型 (*指针名)(参数列表);#include stdio.h // 这是一个普通函数 int maxValue(int a, int b) { return a b ? a : b; } // 这是一个接收函数指针作为参数的函数 void f(int (*function)(int a, int b)) { printf(通过函数指针调用结果: %d\n, function(7, 8)); } int main(int argc, char const *argv[]) { // 1. 定义一个函数指针p它专门指向“接收两个int返回int”的函数 int (*p)(int a, int b); // 2. 将函数maxValue的地址赋给p // 特殊语法函数名取地址时可以省略 p maxValue; // 3. 通过函数名直接调用 int m maxValue(2, 3); printf(直接调用结果: %d\n, m); // 4. 通过函数指针p间接调用 // 特殊语法通过函数指针调用函数时解引用*可以省略 int n p(3, 4); printf(通过指针调用结果: %d\n, n); // 5. 将函数名作为参数传递 f(maxValue); return 0; }13. 结构体 (Struct)结构体是 C 语言的核心特性它允许我们将不同类型的数据项组合成一个单一的、逻辑相关的整体。定义、初始化与使用定义: 使用struct关键字创建一个“蓝图”描述这个复合数据类型包含哪些成员。变量: 根据“蓝图”定义实际的结构体变量。初始化:传统初始化: 按顺序为成员赋值{...}。指定成员初始化: 使用.成员名 值的方式可以不按顺序更清晰。成员访问:对结构体变量使用点运算符.。对结构体指针使用箭头运算符-(它等价于(*指针).成员的语法糖)。#include stdio.h #include stdlib.h // 1. 定义一个结构体类型设计蓝图 // 结构体类型名叫student struct student { // 成员 int age; float score; char *name; // 内嵌结构体 struct { char *name; float price; } book;//结构体变量叫book }; // 推荐将结构体作为指针传递避免内存拷贝开销 void show_student_info(const struct student *p); int main(int argc, char const *argv[]) { // 2. 定义结构体变量并初始化 // 传统初始化按成员顺序赋值 struct student zhangsan {20, 90.0, zhangsan, {嵌入式参考书, 40.5}}; // 指定成员初始化 (推荐) // 顺序改变不影响给成员赋值 struct student lisi { .age 21, .name lisi, .book { .name 数学书, .price 20.0}, .score 70.0}; // 3. 访问成员 printf(姓名%s\n, zhangsan.name); printf(教材价格%f\n, zhangsan.book.price); // 推荐通过指针访问指针只有8字节而结构体变量本身可能很大 // 如果直接传递结构体值传递会复制整个结构体到栈上开销巨大 struct student *p lisi; printf(姓名%s\n, p-name); // 箭头运算符 printf(分数%f\n, (*p).score); // 等价的点运算符形式 show_student_info(zhangsan); return 0; } void show_student_info(const struct student *p) { printf(----学生信息----\n); printf(姓名%s\n, p-name); printf(分数%f\n, p-score); }结构体内存对齐为了让 CPU 高效访问内存编译器会自动对结构体成员进行对齐。对齐规则: 每个成员的地址会是其自身大小的整数倍。例如int(4 字节) 会被放在能被 4 整除的地址上。填充: 为了满足对齐编译器可能会在成员之间填充一些“空白”字节。整体大小: 整个结构体的大小会是其最宽成员大小的整数倍。取消对齐: 在某些特定场景如处理硬件数据流可以使用__attribute__((packed))(GCC/Clang 扩展)来告诉编译器不要进行内存对齐以节省空间。#include stdio.h // M 结构体成员占用最大的成员占用大小 4 struct node { int a; // 4字节, 放在偏移0处, size4 char c; // 1字节, 放在偏移4处, size5 short f; // 2字节, 放在偏移6处(需对齐到2的倍数), size8 // 编译器在c和f之间填充了1个字节 }; // 整体大小需为M(4)的倍数, 8已经是4的倍数, 所以最终大小为8 // 使用packed属性取消对齐 struct node_packed { int a; char c; short f; } __attribute__((packed)); int main(int argc, char const *argv[]) { printf(默认对齐大小: %ld\n, sizeof(struct node)); // 输出: 8 printf(取消对齐大小: %ld\n, sizeof(struct node_packed)); // 输出: 7 (412) return 0; }位域 (Bit-field)位域允许我们在结构体中定义宽度为“位”(bit)的成员这对于需要精确控制内存布局如硬件寄存器、网络协议的场景非常有用。定义:类型 成员名 : 位数;限制:成员必须是整型 (int,unsigned int,char等)。不能对位域成员取地址 ()。建议成员尽量是同类型不然对于某些平台可能会出现内存没有按照预期对齐的问题#include stdio.h // 模拟一个硬件设备的数据包 struct device_data { // 2字节(16位)上存储了温度和湿度 unsigned short temp : 8; // 低8位 unsigned short humi : 8; // 高8位 // 1字节(8位)上存储了8个开关状态 unsigned char door1 : 1; unsigned char door2 : 1; unsigned char door3 : 1; unsigned char door4 : 1; unsigned char light1 : 1; unsigned char light2 : 1; unsigned char light3 : 1; unsigned char light4 : 1; }; int main(int argc, char const *argv[]) { struct device_data d; unsigned int input_data 0x5A96F5; // 假设从硬件读到数据 // 将数据直接映射到结构体 d *(struct device_data *)input_data; printf(原始数据: %#x\n, input_data); printf(温度%d°C\n, d.temp); // 0xF5 - 245 printf(湿度%d%%\n, d.humi); // 0x96 - 150 printf(门1的状态%s\n, d.door1 ? 开 : 关); // 0x5 - 0101, door11 printf(灯4的状态%s\n, d.light4 ? 开 : 关); // 0x5 - 0101, light40 return 0; }14. 联合体 (Union)联合体所有成员共享同一块内存空间其大小由最大的成员决定。这使得联合体非常适合表示“互斥”的数据。特点:任何时刻只有一个成员是有效的。对一个成员赋值会覆盖其他成员的值。初始化:默认初始化第一个成员。使用指定成员初始化法可以初始化任意成员但只有最后被初始化的那个有效。###include stdio.h // 定义一个联合体类型 union attribute { int i_val; char c_val; double d_val; }; int main(int argc, char const *argv[]) { printf(联合体大小: %ld\n, sizeof(union attribute)); // 8, 由double决定 // 指定成员初始化只有最后一个有效 union attribute attr {.i_val 100, .c_val k, .d_val 3.14}; // 此时只有 d_val 是有效的 printf(d_val: %lf\n, attr.d_val); // 正确输出 3.14 // 读取其他成员会得到无意义的数据 printf(i_val: %d\n, attr.i_val); // 输出垃圾值 printf(c_val: %c\n, attr.c_val); // 输出垃圾值 // 重新赋值 attr.i_val 999; printf(\n赋值后 i_val: %d\n, attr.i_val); // 正确输出 999 printf(d_val: %lf\n, attr.d_val); // d_val被覆盖输出垃圾值 return 0; }15. 枚举 (Enum)枚举用于定义一组命名的整数常量它比使用#define更具优势因为枚举是类型安全的且调试器可以识别枚举名。定义:enum 枚举名 {常量1, 常量2, ...};赋值:默认从0开始依次递增。 -可以手动为任何一个常量指定一个整数值后续未指定的常量会在此基础上递增。###include stdio.h // 定义交通灯颜色的枚举 enum traffic_light { RED, // 默认为 0 GREEN, // 在RED基础上1, 为 1 YELLOW10, // 手动指定为 10 BLUE // 在YELLOW基础上1, 为 11 }; int main(int argc, char const *argv[]) { // 定义枚举变量 enum traffic_light color YELLOW; printf(RED%d, GREEN%d, YELLOW%d, BLUE%d\n, RED, GREEN, YELLOW, BLUE); if (color YELLOW) { printf(当前颜色是黄灯, 请注意!\n); } return 0; }第四部分构建大型程序16.volatile关键字volatile是一个类型修饰符它告诉编译器被修饰的变量可能会在任何时候被程序外部的因素如硬件、中断服务程序、其他线程意外地改变。核心作用:防止编译器过度优化。确保每次访问该变量时都直接从其内存地址中读取而不是使用可能已过时的寄存器缓存值。使用场景:硬件寄存器: 嵌入式系统中硬件状态寄存器的值会由硬件实时更新。多线程共享变量: 一个变量被多个线程共享和修改时。中断服务程序: 中断处理函数中会修改的全局变量。代码示例// 示例一个可能被硬件修改的状态寄存器 volatile unsigned int *DEVICE_STATUS_REGISTER (unsigned int *)0x12345678; void check_device() { // 每次循环都必须从内存地址0x12345678重新读取状态 // 如果没有volatile编译器可能优化为只读一次导致死循环 while (*DEVICE_STATUS_REGISTER 0) { // 等待设备就绪... } // ...设备已就绪继续操作... }17. 预处理与高级技巧高级宏定义宏在预处理阶段进行简单的文本替换功能强大但易出错。宏运算符#(字符串化)##(连接)#: 将宏参数转换为一个字符串字面量。##: 将两个记号token连接成一个记号。###include stdio.h // 使用 # 将宏参数 a 和 b 变成字符串 ###define DOMAIN_NAME(a, b) www. #a . #b .com // 使用 ## 连接参数构造函数名 ###define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num // 模拟内核的初始化函数 void __zinitcall_service_1(void) { printf(%s\n, __FUNCTION__); } void __zinitcall_feature_2(void) { printf(%s\n, __FUNCTION__); } int main(int argc, char const *argv[]) { // # 的使用 printf(%s\n, DOMAIN_NAME(yueqian, lab)); // 输出: www.yueqian.lab.com // ## 的使用 LAYER_INITCALL(1, service)(); // 宏展开为: __zinitcall_service_1(); LAYER_INITCALL(2, feature)(); // 宏展开为: __zinitcall_feature_2(); return 0; }安全的带参宏 (GCC 扩展)简单的带参宏有副作用风险如MAX(a, b)。Linux 内核中广泛使用({...})语句表达式和typeof关键字 (均为 GCC 扩展) 来创建更安全的宏。typeof(x): 获取变量x的类型。({...}): 语句表达式将多条语句包裹成一个单一的表达式其值为最后一条语句的结果{}将多句话整合为一句话()将内部的 {…} 代码块“提升”为一个表达式使其可以被赋值或用在其他需要值的地方。#include stdio.h // 传统宏的风险 // #define MAX(a, b) ((a) (b) ? (a) : (b)) // 安全的、类型通用的宏 (Linux内核风格) #define MAX(a, b) ({ \ typeof(a) _a (a); \ typeof(b) _b (b); \ (void) (_a _b); /* 警告: 如果a和b类型不同 */ \ _a _b ? _a : _b; \ }) int main(int argc, char const *argv[]) { int x 10; int y 20; // 对于 MAX(x, y)传统宏会将执行两次而安全宏只执行一次 printf(Max is %d\n, MAX(x, y)); float f1 3.14, f2 2.71; printf(Max is %f\n, MAX(f1, f2)); // 不同类型的参数会产生编译警告 // printf(Max is %f\n, MAX(x, f1)); return 0; }内联函数 (inline)对于功能简单、调用频繁的函数函数调用的开销可能超过函数本身的执行时间。inline关键字建议编译器将函数体直接嵌入到调用处以消除调用开销。特点:它是对编译器的建议而非强制命令。相比宏函数内联函数有类型检查更安全。内联函数的定义通常放在头文件中。适用场景: 函数体小且被频繁调用的函数。inline.h文件:// inline.h #ifndef INLINE_H #define INLINE_H // 将简短、频繁调用的函数定义为inline static inline int max2(int a, int b) { return a b ? a : b; } #endifmain.c文件:#include stdio.h #include inline.h int main(int argc, char const *argv[]) { // 编译器可能会将 max2 的函数体直接替换到这里 printf(%d\n, max2(1, 2)); printf(%d\n, max2(10, 20)); return 0; }18. 多文件编程与头文件随着项目变大将所有代码放在一个.c文件里是不可行的。我们需要将代码按模块拆分到多个.c和.h文件中。条件编译条件编译允许我们根据编译时定义的宏来决定哪些代码块被编译哪些被忽略。这对于编写平台兼容代码、管理调试信息等非常有用。#if/#elif/#else/#endif: 基于宏的值进行判断功能类似if-else。#ifdef 宏名: (if defined) 如果宏宏名已被定义则编译后续代码。#ifndef 宏名: (if not defined) 如果宏宏名未被定义则编译后续代码。#include stdio.h // 可以在代码中定义宏 #define FEATURE_A 1 #define FEATURE_B 0 // 也可以在编译时通过-D选项定义例如: gcc -DDEBUG_MODE condition.c // #define DEBUG_MODE int main(int argc, char const *argv[]) { #if FEATURE_A printf(功能A已启用。\n); #endif #if FEATURE_B printf(功能B已启用。\n); #else printf(功能B未启用。\n); #endif // 常用于输出调试信息 #ifdef DEBUG_MODE printf(调试信息: 进入函数 %s, 第 %d 行\n, __FUNCTION__, __LINE__); #endif #ifndef RELEASE_MODE printf(当前为非发布版本。\n); #endif return 0; }头文件的作用与设计头文件 (.h) 是多文件编程的核心它扮演着模块“接口说明书”的角色。头文件的内容:全局变量的声明: 使用extern关键字告知其他文件该变量的存在。 (extern int g_count;)函数声明: 告知其他文件该函数的存在。 (void print_hello(void);)宏定义: 模块提供的常量或宏函数。 (#define MAX_USERS 100)结构体/联合体/枚举的定义: 允许多个.c文件使用相同的数据结构。static inline函数: 对于小且频繁调用的函数可以定义在头文件中。头文件防卫:为了防止同一个头文件在编译时被重复包含这会导致重定义错误必须使用ifndef机制。这是强制性的最佳实践。my_module.h文件示例:// 1. 头文件防卫开始 #ifndef __MY_MODULE_H__ #define __MY_MODULE_H__ // 2. 包含此模块依赖的其他头文件 #include stdio.h // 3. 模块的“接口”声明 #define MODULE_VERSION 1.0 extern int g_module_status; // 全局变量声明 struct my_data; // 结构体声明 void module_init(void); // 函数声明 int module_get_status(void); // static inline函数可以直接在头文件中实现 static inline void print_version() { printf(Version: %s\n, MODULE_VERSION); } // 4. 头文件防卫结束 #endif // __MY_MODULE_H__my_module.c文件 (模块的实现):###include my_module.h // 首先包含自己的头文件 // 全局变量的定义 int g_module_status 0; // 结构体的具体定义 struct my_data { int id; char *name; }; // 函数的具体实现 void module_init(void) { g_module_status 1; printf(模块已初始化。\n); } int module_get_status(void) { return g_module_status; }main.c文件 (模块的使用者):###include my_module.h // 包含模块头文件以使用其功能 int main() { module_init(); printf(模块状态: %d\n, module_get_status()); print_version(); return 0; }19. 常用字符串函数strlen- 获取长度lenth返回字符串的长度不包括末尾的\0。size_t len strlen(hello); // len 的值为 5strcpy- 字符串复制copystrncpy是strcpy的安全版本推荐使用以防止内存溢出。char dest[10] 123456789; // 复制hello到dest最多复制sizeof(dest)-1个字符 strncpy(dest, hello, sizeof(dest) - 1); dest[sizeof(dest) - 1] \0; // 确保字符串以\0结尾 // dest现在是 hellostrcat- 字符串拼接catchstrncat是strcat的安全版本推荐使用。char dest[10] Hi, ; // 将Bob拼接到dest末尾 strncat(dest, Bob, sizeof(dest) - strlen(dest) - 1); // dest现在是 Hi, Bobstrcmp- 字符串比较compare按字典序比较字符串返回0(s1s2),0(s1s2),0(s1s2)。int result strcmp(abc, abd); // result 0strchr- 查找字符charstrchr: 从左向右查找第一个匹配的字符。strrchr: 从右向左查找第一个匹配的字符。char *p strchr(a.b.c.d, .); // p 指向 .b.c.dstrstr- 查找子串string在字符串中查找子字符串首次出现的位置。char *p strstr(main.c, .c); // p 指向 .cstrtok- 分割字符串token注意: 此函数会修改原始字符串。首次调用传入字符串后续调用传入NULL。char str[] www.yueqian.com; char *p strtok(str, .); // p 指向 www while (p ! NULL) { printf(%s\n, p); p strtok(NULL, .); } // 依次输出: www, yueqian, com第五部分高级应用入门20. 算法入门经典排序冒泡排序问题: 实现冒泡排序算法。核心知识: 通过重复遍历数组比较相邻元素并交换每一趟都将当前未排序部分的最大或最小元素“冒泡”到最终位置。代码示例:void bubble_sort(int len, int A[]) { for (int i 0; i len - 1; i) { for (int j 0; j len - i - 1; j) { if (A[j] A[j 1]) { int temp A[j]; A[j] A[j 1]; A[j 1] temp; } } } }21. 数据结构入门单链表单链表是一种动态数据结构它由一系列节点组成每个节点包含数据和一个指向下一个节点的指针。核心思想: 在堆内存中动态创建节点 (malloc)并通过指针将这些分散的节点链接成一个有序序列。关键知识点:struct结构体用于定义链表节点。malloc/free动态内存分配和释放。指针操作通过p-next遍历和操作链表。#include stdio.h #include stdlib.h // 1. 定义链表节点结构体 typedef struct Node { int data; struct Node *next; } Node; // 2. 创建新节点 Node* create_node(int data) { Node *new_node (Node*)malloc(sizeof(Node)); if (new_node NULL) return NULL; new_node-data data; new_node-next NULL; return new_node; } // 3. 打印链表 void print_list(Node *head) { Node *current head; while (current ! NULL) { printf(%d - , current-data); current current-next; } printf(NULL\n); } int main() { // 创建链表: head - node1 - node2 Node *head create_node(10); head-next create_node(20); head-next-next create_node(30); print_list(head); // 输出: 10 - 20 - 30 - NULL // 释放链表内存 (此处省略) return 0; }22. 面试题精选巧妙的级数求和问题: 计算S 1 - 1/2 1/3 - 1/4 ... 1/99 - 1/100。核心知识: 直接计算涉及大量浮点减法可能损失精度。通过数学变换可以优化S (1 ... 1/100) - 2 * (1/2 ... 1/100) (1 ... 1/100) - (1 ... 1/50) 1/51 ... 1/100这个变换将问题转换成了一个简单的正项级数求和。代码示例:#include stdio.h // 计算 S 1 - 1/2 1/3 - 1/4 ... 1/99 - 1/100 的值 int main(int argc, char const *argv[]) { float sum 0; int i 1; while (i 100) { if (i % 2 1) sum 1.0 / i; else sum - 1.0 / i; // 或者sumpow(-1,i1)*(1/(float)i); //用(-1)的i1次方代替判断需要引用数学库 // 或者sum1/(i*(i1)); //规律是1-1/21/2(1/3-1/41/12)(1/5-1/61/30) // 或者 // for (int i 51; i 100; i) sum_a 1.0 / i; // 将加法和减法分开 // S (1 1/3 1/5 ... 1/99) - (1/2 1/4 1/6 ... 1/100) // 然后加上减去的部分减去两倍的部分 // S (1 1/2 1/3 ... 1/100) - 2 * (1/2 1/4 1/6 ... 1/100) // 然后吧后半部分的2乘进去 // S (1 1/2 1/3 ... 1/100) - (1 1/2 1/3 ... 1/50) // 所以 // S (1/51 1/52 1/53 ... 1/100) i; } printf(%f\n,sum); return 0; }凑零钱问题#include stdio.h int main(int argc, char const *argv[]) { // 目标用1元、2元、5元的纸币凑出100元 int num 0; for(int i0; i20; i) // i个5元 { for(int j0; j50; j) // j个2元 { // 剩下的用1元补足 if(100 - i*5 - j*2 0) num; } } printf(num: %d , num); return 0; }解法二#include stdio.h int main() { int count 0; for (int i 0; i 100 / 5; i) { for (int j 0; j (100 - 5 * i) / 2; j) { int k 100 - 5 * i - 2 * j; // printf(5元: %d个, 2元: %d个, 1元: %d个 , i, j, k); count; } } printf(使用数值 1, 2, 5 组合成 %d共有 %d 种不同的组合。 , 100, count); return 0; }统计数字’1’的出现次数#include stdio.h // 计算从1到n的整数中数字1出现的总次数 long long count(int n) { if (n 1) return 0; long long count 0; long long base 1; int round n; while (round 0) { int weight round % 10; round / 10; count round * base; if (weight 1) count (n % base) 1; else if (weight 1) count base; base * 10; } return count; } int main() { unsigned int n 213; printf(从 1 到 %u, 数字 1 出现的总次数为: %lld , n, count(n)); return 0; }二维数组边界元素求平均值#include stdio.h #include stdlib.h #include time.h void show(int N, int w[N][N]) { for(int i0; iN; i) { for(int j0; jN; j) printf(%d , w[i][j]); printf( ); } } int main(int argc, char const *argv[]) { int N 5; // Example size int w[N][N]; srand(time(NULL)); for(int i0; iN; i) { for(int j0; jN; j) { w[i][j] rand()%1000; } } show(N, w); if(N 1) { printf(周围数据平均数是: %f , (float)w[0][0]); return 0; } float sum 0.0; for(int i0; iN; i) { sum w[0][i]; // Top row sum w[N-1][i]; // Bottom row } for(int i1; iN-1; i) { sum w[i][0]; // Left column (excluding corners) sum w[i][N-1]; // Right column (excluding corners) } printf(周围数据平均数是: %f , sum/(4.0*N-4.0)); return 0; }23. 扩展阅读(待补充…)24. 笔记总结C 语言的核心是围绕指针、内存管理和作用域/存储期这三大支柱展开的static关键字是贯穿其中的关键。基础与类型系统程序由main函数启动其根本是数据类型。必须精确掌握int,char,double等类型与printf/scanf格式说明符的对应关系printf中%f可通用打印float和double因float参数会自动提升为double而scanf接收double必须用%lf。scanf通过传递变量地址来实现对调用方变量的修改务必警惕其输入缓冲区的残留问题。运算符与表达式重点掌握逻辑运算的短路求值,||、高效的位运算,|,^,~,,、条件运算符? :以及sizeof它是一个运算符不是函数。当不确定优先级时用**圆括号()**是最佳实践。控制流熟练运用if-else、switch牢记case的穿透特性与break的重要性、for/while/do-while循环以及break跳出循环/switch、continue跳过本次迭代、return终止函数三种跳转语句。指针难点中的难点核心操作指针是存储地址的变量用取地址、*解引用。必须初始化为NULL以防野指针。数组与指针在表达式中除sizeof和外数组名会“降维/退化”为指向其首元素的指针因此a[i]本质是*(ai)的语法糖。这也导致数组作为函数参数时sizeof在函数内外结果不同。const修饰必须严格区分“指向常量的指针”const int *p*p不可改p可改和“常量指针”int * const pp不可改*p可改。高级指针理解void*作为通用指针使用前须强制类型转换以及函数指针返回类型 (*指针名)(参数)作为实现回调函数的基础。内存布局代码段 (.text)存放二进制指令只读。数据段.rodata存放字符串字面量、const全局变量只读。.data存放已初始化的全局变量和静态变量。.bss存放未初始化的全局变量和静态变量程序启动时系统自动清零。栈 (Stack)存放函数的局部变量和参数由编译器自动管理空间有限有栈溢出风险。严禁返回局部变量的地址因其在函数返回后即被销毁。堆 (Heap)由程序员通过malloc/calloc/realloc/free手动管理空间巨大但易产生内存泄漏忘记free或悬空指针free后未置NULL。static关键字重中之重修饰局部变量改变其存储期从自动存储期改为静态存储期使其从栈移至数据段。变量生命周期与程序相同其值在函数多次调用间保持不变。修饰全局变量/函数改变其链接属性从外部链接改为内部链接使其作用域被限制在当前源文件内是实现模块化和信息隐藏的关键。复合类型struct将不同类型数据聚合成单一实体是 C 语言实现面向对象思想的基础。通过.变量或-指针访问成员并须注意编译器为提高效率而进行的内存对齐以及用于硬件编程的位域。union所有成员共享同一块内存大小由最大成员决定常用于节省空间。enum创建类型安全的命名整型常量比#define更优。预处理在编译前执行的文本替换。核心指令包括#include文件包含、#define定义宏可用\换行、#if/#ifdef等条件编译指令。宏操作符#字符串化和##符号连接功能强大。多文件编程中必须使用**#ifndef...#define...#endif**结构进行头文件防卫防止重复包含。高级关键字volatile的应用场景与原理二维数组边界元素遍历const指针的三种模式辨析数组与指针的异同
版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

铜陵app网站做营销招聘产品效果图怎么做出来的

智慧驾培云平台:基于JavaSpringBoot的全渠道数字化驾考解决方案在驾培行业数字化转型的浪潮下,为应对传统管理模式中信息不透明、预约效率低、学习体验割裂等痛点,我们基于Java SpringBoot MyBatis-Plus MySQL 这一稳健高效的技术栈&#…

张小明 2025/12/31 18:52:06 网站建设

莒南建设局网站服装公司网站设计

OpenPCDet坐标变换完整指南:从零掌握3D目标检测核心技术 【免费下载链接】OpenPCDet 项目地址: https://gitcode.com/gh_mirrors/ope/OpenPCDet 在自动驾驶3D目标检测领域,OpenPCDet提供了业界领先的坐标变换解决方案。本文将深入解析如何通过坐…

张小明 2025/12/28 1:46:02 网站建设

上海网站制作库榆什么是网络营销定义

智能助手革命:3分钟掌握自动化工具的终极使用指南 【免费下载链接】LiteLoaderQQNT-OneBotApi NTQQ的OneBot API插件 项目地址: https://gitcode.com/gh_mirrors/li/LiteLoaderQQNT-OneBotApi 你是否曾经为了重复性的工作任务而感到疲惫不堪?每天…

张小明 2026/1/1 3:17:57 网站建设

网站技术团队做网站开发的薪酬怎么样

学长亲荐10个AI论文软件,自考本科毕业论文轻松搞定! AI 工具如何助力论文写作?自考学生必看 在当今数字化学习环境中,AI 工具已经逐渐成为学术写作的重要助手。对于自考本科生而言,撰写毕业论文是一项既重要又充满挑战…

张小明 2025/12/28 3:45:31 网站建设

校园门户网站系统建设最好的营销型网站

如何快速搭建Voron 2.4:开源3D打印机终极指南 【免费下载链接】Voron-2 项目地址: https://gitcode.com/gh_mirrors/vo/Voron-2 Voron 2.4是一款高性能开源3D打印机,以其卓越的打印速度和专业级精度而闻名。作为社区驱动的项目,它结合…

张小明 2026/1/1 11:43:47 网站建设

南充能够建设网站的公司有网站建设选择

Path of Building PoE2:终极BD构建神器深度解析 【免费下载链接】PathOfBuilding-PoE2 项目地址: https://gitcode.com/GitHub_Trending/pa/PathOfBuilding-PoE2 Path of Building PoE2 是《流放之路2》最强大的离线建BD工具,为玩家提供全面的伤…

张小明 2025/12/28 3:45:27 网站建设