├── Coding ├── Advanced_C │ ├── 01 Examples │ │ └── Address_Book │ │ │ ├── AddressBook.c │ │ │ └── readme.md │ └── 02 Exercises │ │ ├── 01 │ │ ├── 01 打印杨辉三角 │ │ │ ├── test1.c │ │ │ ├── test2.c │ │ │ └── 题目.md │ │ ├── 02 字符串旋转 │ │ │ ├── test1.c │ │ │ └── 题目.md │ │ ├── 03 字符串左旋 │ │ │ └── test1.c │ │ ├── 04 杨氏矩阵 │ │ │ └── test1.c │ │ ├── 05 qsort实现 │ │ │ └── test1.c │ │ ├── 06 猜凶手 │ │ │ └── test1.c │ │ └── 07 猜名次 │ │ │ └── test1.c │ │ └── 02 │ │ ├── 01 实现atoi │ │ ├── MyAtoi.c │ │ └── MyAtoi2.c │ │ ├── 02 strncpy & strncat │ │ ├── MyStrncat.c │ │ └── MyStrncpy.c │ │ ├── 03 单身狗问题 │ │ └── SingleDog.c │ │ ├── 04 两数平均值 │ │ └── main.c │ │ └── 05 字符串左旋问题 │ │ ├── StringLeftRotate.c │ │ └── readme.md ├── C_Crash_Course │ └── 01 Examples │ │ ├── 06 循环 │ │ └── 01 账簿计算 │ │ │ ├── main.c │ │ │ └── readme.md │ │ ├── 08 数组 │ │ ├── 01 计算利息 │ │ │ ├── main.c │ │ │ └── readme.md │ │ └── 02 发牌 │ │ │ ├── main.c │ │ │ └── readme.md │ │ ├── 09 函数 │ │ └── 01 判定素数 │ │ │ ├── main.c │ │ │ └── readme.md │ │ ├── 10 程序结构 │ │ ├── 01 猜数 │ │ │ ├── global_variable.c │ │ │ ├── no_global_variable.c │ │ │ └── readme.md │ │ └── 02 手牌分类 │ │ │ ├── main.c │ │ │ └── readme.md │ │ ├── 11 指针 │ │ └── 01 数组的最大元素和最小元素 │ │ │ ├── main.c │ │ │ └── readme.md │ │ ├── 13 字符串 │ │ ├── 01 设计字符串读取函数 │ │ │ ├── read_line.c │ │ │ └── readme.md │ │ ├── 02 显示一个月的提醒列表 │ │ │ ├── readme.md │ │ │ └── remind.c │ │ └── 03 核对行星名字 │ │ │ ├── planets.c │ │ │ └── readme.md │ │ ├── 15 编写大型程序 │ │ └── 01 文本格式化 │ │ │ ├── justify.c │ │ │ ├── line.c │ │ │ ├── line.h │ │ │ ├── quote.txt │ │ │ ├── readme.md │ │ │ ├── word.c │ │ │ ├── word.h │ │ │ └── 重定向.png │ │ ├── 16 结构&联合&枚举 │ │ └── 01 维护零件数据库 │ │ │ ├── inventory.c │ │ │ ├── readline.c │ │ │ ├── readline.h │ │ │ └── readme.md │ │ ├── 17 指针的高级应用 │ │ ├── 01 显示一个月的提醒列表 │ │ │ ├── readme.md │ │ │ └── reminder.c │ │ └── 02 维护零件数据库(链表) │ │ │ ├── inventory2.c │ │ │ └── readme.md │ │ ├── 19 程序设计 │ │ └── 01 栈的实现 │ │ │ ├── 01 栈模块 │ │ │ ├── array_stack.c │ │ │ ├── linkedlist_stack.c │ │ │ └── stack.h │ │ │ └── 02 栈抽象数据类型 │ │ │ ├── 01 定长数组实现 │ │ │ ├── stackADT.c │ │ │ └── stackADT.h │ │ │ ├── 02 动态数组实现 │ │ │ ├── stackADT3.c │ │ │ └── stackADT3.h │ │ │ ├── 03 链表实现 │ │ │ ├── stackADT4.c │ │ │ └── stackADT4.h │ │ │ ├── 04 最终形态 │ │ │ ├── final_stackADT.c │ │ │ ├── final_stackADT.h │ │ │ └── stackclient.c │ │ │ ├── readme.md │ │ │ └── stackclient.c │ │ ├── 20 底层程序设计 │ │ ├── 01 XOR加密 │ │ │ ├── msg.txt │ │ │ ├── newmsg.txt │ │ │ ├── readme.md │ │ │ └── xor.c │ │ └── 02 查看内存单元 │ │ │ ├── readme.md │ │ │ └── view_memory.c │ │ ├── 22 输入&输出 │ │ ├── 01 fopen 使用框架 │ │ │ ├── frame.c │ │ │ └── readme.md │ │ ├── 02 检查文件是否可以打开 │ │ │ ├── canopen.c │ │ │ └── readme.md │ │ ├── 03 复制文件 │ │ │ ├── fcopy.c │ │ │ └── readme.md │ │ └── 04 修改零件记录文件 │ │ │ ├── invclear.c │ │ │ └── readme.md │ │ └── 24 错误处理 │ │ ├── 01 测试信号 │ │ └── readme.md │ │ └── 02 测试 setjmp 和 longjmp │ │ └── readme.md ├── C_Mooc │ └── 01 Examples │ │ ├── 常见字符串函数 │ │ ├── Test │ │ │ ├── main.c │ │ │ └── readme.md │ │ └── mystrcmp │ │ │ ├── VSstrcmp.c │ │ │ ├── main.c │ │ │ └── mystrcmp.c │ │ ├── 结构 │ │ ├── stucture-as-func-parameter │ │ │ ├── main.c │ │ │ └── readme.md │ │ └── test │ │ │ └── test.c │ │ ├── 编写大型程序 │ │ └── test.c │ │ └── 输入&输出 │ │ ├── binary file │ │ └── fwrite │ │ │ └── student information │ │ │ ├── read.c │ │ │ ├── readme.md │ │ │ ├── student.h │ │ │ └── write.c │ │ └── test code │ │ └── test.c ├── C_Traps_and_Pitfalls │ ├── Exercises │ │ └── 03 语义陷阱 │ │ │ └── 01 二分查找 │ │ │ ├── bsearch.c │ │ │ └── readme.md │ └── readme.md ├── Expert_C_Programming │ └── readme.md ├── Practice │ └── string │ │ └── README.md ├── Project │ ├── 01_信息管理系统 │ │ └── 01_工人信息管理系统 │ │ │ ├── README.md │ │ │ ├── worker.c │ │ │ └── worker.h │ └── 02_通讯录 │ │ ├── AddressBook.c │ │ ├── AddressBook.h │ │ ├── Main.c │ │ └── address_book.txt └── readme.md ├── README.md ├── content ├── c-games │ └── 猜数字.md ├── c-mordern-approch │ ├── 01-C语言概论.md │ ├── 02-C语言基本概念.md │ ├── 03-C语言基本概念.md │ ├── 04-格式化输入输出.md │ ├── 05-基本类型.md │ ├── 06-表达式.md │ ├── 07-选择语句.md │ ├── 08-循环.md │ ├── 09-数组.md │ ├── 10-函数.md │ ├── 11-程序结构.md │ ├── 12-指针.md │ ├── 13-指针和数组.md │ ├── 14-字符串.md │ ├── 15-预处理器.md │ ├── 16-编写大型程序.md │ ├── 17-结构&联合&枚举.md │ ├── 18-指针的高级应用.md │ ├── 19-声明.md │ ├── 20-程序设计.md │ ├── 21-底层程序设计.md │ ├── 22-输出&输出.md │ ├── 23-标准库.md │ └── 24-错误处理.md ├── c-notes │ ├── 5分钟看懂什么是 malloc.md │ ├── C 语言还值得学习吗?C 语言会过时吗?C 语言解惑.md │ ├── C语言 3 道面试题,不会还敢说你C学的好?带详解.md │ ├── C语言 文件 看这一篇就够了.md │ ├── C语言指针笔试题这么变态?我可能白学C语言了!带详解!.md │ ├── if-else语句详解.md │ ├── 一篇看懂 C语言常用 字符串函数,全网最全整理!.md │ ├── 两个数组为何不能赋值.md │ ├── 什么 是 枚举 & 结构 & 联合,看这一篇就够了.md │ ├── 什么是 全局变量 & 宏 & 大程序怎么写,看这一篇就够了.md │ ├── 你不知道的几种素数判断方法,由浅入深,详解.md │ ├── 关于字符串你不知道的知识点.md │ ├── 内存对齐.md │ ├── 动态内存管理.md │ ├── 字符串函数与内存函数.md │ ├── 小端和整型存储.md │ ├── 指针进阶.md │ ├── 教你用简单的程序判断你的电脑是大端还是小端.md │ ├── 有关指针.md │ ├── 浅谈 C 语言实现重载,多态和模板.md │ ├── 这些关于数组的基础知识点你都知道吗.md │ └── 那些关于函数我们容易忽略的基础知识.md ├── c-review │ ├── 1-数据类型和变量.md │ ├── 2-数组.md │ ├── 3-分支和循环.md │ ├── 4-函数.md │ ├── 5-指针.md │ └── 6-操作符.md ├── c-traps-and-pitfalls │ ├── 01 词法陷阱.md │ ├── 02 语法陷阱.md │ ├── 03 语义陷阱.md │ ├── 04 连接.md │ ├── 05 库函数.md │ ├── 06 预处理器.md │ ├── 07 可移植性缺陷.md │ └── 08 建议.md └── other │ └── Q&A.md └── img ├── QR Code └── 1.png └── logo └── logo.png /Coding/Advanced_C/01 Examples/Address_Book/AddressBook.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/01 Examples/Address_Book/AddressBook.c -------------------------------------------------------------------------------- /Coding/Advanced_C/01 Examples/Address_Book/readme.md: -------------------------------------------------------------------------------- 1 | ## 简易版通讯录 2 | 3 | 第一版通讯录的实现是定义一个结构体里面包含一个数组。技巧不高,其实就是结构体的一个应用 4 | 5 | 后面可能会优化,可能不会。。 6 | -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/01 打印杨辉三角/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/01 打印杨辉三角/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/01 打印杨辉三角/test2.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/01 打印杨辉三角/test2.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/01 打印杨辉三角/题目.md: -------------------------------------------------------------------------------- 1 | 2 | #### 1.打印杨辉三角 3 | 4 | ```c 5 | 1 6 | 1 1 7 | 1 2 1 8 | 1 3 3 1 9 | 1 4 6 4 1 10 | ... 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/02 字符串旋转/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/02 字符串旋转/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/02 字符串旋转/题目.md: -------------------------------------------------------------------------------- 1 | #### 2. 字符串旋转 2 | 3 | 4 | 5 | 写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串。 6 | 7 | 例如:给定s1 =AABCD和s2 = BCDAA,返回1 8 | 给定s1=abcd和s2=ACBD,返回0. 9 | 10 | AABCD左旋一个字符得到ABCDA 11 | AABCD左旋两个字符得到BCDAA 12 | AABCD右旋一个字符得到DAABC -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/03 字符串左旋/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/03 字符串左旋/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/04 杨氏矩阵/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/04 杨氏矩阵/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/05 qsort实现/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/05 qsort实现/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/06 猜凶手/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/06 猜凶手/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/01/07 猜名次/test1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/01/07 猜名次/test1.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/01 实现atoi/MyAtoi.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/01 实现atoi/MyAtoi.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/01 实现atoi/MyAtoi2.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/01 实现atoi/MyAtoi2.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/02 strncpy & strncat/MyStrncat.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/02 strncpy & strncat/MyStrncat.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/02 strncpy & strncat/MyStrncpy.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/02 strncpy & strncat/MyStrncpy.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/03 单身狗问题/SingleDog.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/03 单身狗问题/SingleDog.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/04 两数平均值/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/04 两数平均值/main.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/05 字符串左旋问题/StringLeftRotate.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Advanced_C/02 Exercises/02/05 字符串左旋问题/StringLeftRotate.c -------------------------------------------------------------------------------- /Coding/Advanced_C/02 Exercises/02/05 字符串左旋问题/readme.md: -------------------------------------------------------------------------------- 1 | ## 字符串左旋 2 | 3 | 这个问题我们在【习题一】也就是 上一级 【01】目录中讨论过,此次给出另一个中解法 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/06 循环/01 账簿计算/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/06 循环/01 账簿计算/main.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/06 循环/01 账簿计算/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:账薄结算 2 | 3 | 这个程序帮你理解一种简单的交互式程序设计,我们可以通过这种方式设计菜单。 4 | 5 | 题目: 6 | 7 | 开发一个程序用来维护账簿的余额。程序将为用户提供选择菜单:清空余额账户,向账户存钱,从账户取钱,显示当前余额,退出程序。选项用 0,1,2,3,4表示。程序的会话类似这样: 8 | 9 | ```c 10 | **** ACME checkbook-balancing program **** 11 | Comands: 0 = clear, 1 = credit, 2 = debit, 3 = balance, 4 = exit 12 | 13 | Enter command: 1 14 | Enter amount of credit: 1042.56 15 | Enter command: 2 16 | Enter amount of debit : 133.56 17 | Enter command: 3 18 | Current balance: 909 19 | Ener command: 4 20 | Goodbye~ 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/08 数组/01 计算利息/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/08 数组/01 计算利息/main.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/08 数组/01 计算利息/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:计算利息 2 | 3 | 编写一个程序显示一个表格。这个表格显示了几年时间内 100 美元投资在不同利率下的价值。用户输入利率和要投资的年数。投资总价值一年算一次,表格将显示输入的利率和紧随其后的 4 个更高的利率下投资的总价值。程序会话如下: 4 | 5 | ```c 6 | Enter intrest rate: 6 7 | Enter number of years: 2 8 | 9 | Years 6% 7% 8% 9% 10% 10 | 1 106.00 107.00 108.00 109.00 110.00 11 | 2 112.36 114.49 116.64 118.81 121.00 12 | ``` 13 | 14 | 第一行用一个 for 语句来显示。 15 | 16 | 我们在计算第一年的价值的时候将结果存放到数组中,然后使用数组中的结果继续计算下一年的价值。 17 | 18 | 在这一过程中我们将需要两个 for 语句,一个控制年份,一个控制不同的利率。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/08 数组/02 发牌/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/08 数组/02 发牌/main.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/08 数组/02 发牌/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:发牌 2 | 3 | 下面这个程序说明了二维数组和常量数组的用法。 4 | 5 | **要求:** 6 | 7 | 程序负责发一副标准纸牌。每张标准指派都有一个花色(梅花,方块,红桃,黑桃)和一个点数(2 ~ 10, J, Q, K, A)。用户需要指明发多少张牌: 8 | 9 | ```c 10 | Enter number of cards in hand: 5 11 | Your card(s): S8 SA D7 H8 SK 12 | ``` 13 | 14 | **程序说明: ** 15 | 16 | - 创建两个常量数组,分别放置 4 中花色 和 13 个点数 17 | 18 | - 程序要可以生成 随机数 。我们需要三个函数: 19 | 20 | time 21 | 22 | srand 23 | 24 | rand 25 | 26 | 这三个函数组合就可以完成这一功能,原理在我另一篇文章:【随机数发生器】 中讲解过。 27 | 28 | - 生成的随机数必须在:0 ~ 3 和 0 ~ 13 之间: 29 | 30 | 只需要让 `rand() % 4` 那么随机数就在 0 ~ 3 之间,另一个同理。 31 | 32 | - 两次拿到的牌不能是一样的。创建一个 bool 类型的数组,开始时全部初始化 false。每次拿到两个随机数后,如果数组对应的值为 false 那么将该元素置为 true 然后将此牌“发”给用户;否则,重新生成随机数。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/09 函数/01 判定素数/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | bool is_prime(int n) { 5 | 6 | int divisor; 7 | 8 | if (n <= 1) 9 | return false; 10 | 11 | for (divisor = 2; divisor * divisor <= n; divisor++) { 12 | if (n % divisor == 0) 13 | return false; 14 | } 15 | 16 | return true; 17 | } 18 | 19 | 20 | int main(void) { 21 | 22 | int n; 23 | 24 | printf("Enter a number: "); 25 | scanf("%d", &n); 26 | 27 | if (is_prime(n)) 28 | printf("Prime\n"); 29 | else 30 | printf("Not Prime\n"); 31 | 32 | return 0; 33 | } 34 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/09 函数/01 判定素数/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:判断素数 2 | 3 | 编写程序提示用户录入数,然后给出一条信息说明此数是否为素数。 4 | 5 | ```c 6 | Enter a number: 24 7 | Not prime 8 | ``` 9 | 10 | 把判断素数的实现写到另外一个函数中,此函数返回值为 true 就表示是素数,返回 false 表示不是素数。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/10 程序结构/01 猜数/global_variable.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/10 程序结构/01 猜数/global_variable.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/10 程序结构/01 猜数/no_global_variable.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/10 程序结构/01 猜数/no_global_variable.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/10 程序结构/01 猜数/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:猜数 2 | 3 | 程序产生一个 1 ~ 100 的随机数,用户尝试用尽可能少的次数猜出这个数。程序运行如下: 4 | 5 | ```c 6 | Guess the secret number between 1 and 100. 7 | 8 | A new number has been chosen. 9 | Enter guess:55 10 | Too low; try again. 11 | Enter guess:65 12 | Too high; try again. 13 | Enter guess: 60 14 | You won in 3 guesses! 15 | 16 | Play again?(Y/N) n 17 | ``` 18 | 19 | 使用两种方法:使用全局变量/不使用全局变量 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/10 程序结构/02 手牌分类/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/10 程序结构/02 手牌分类/main.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/10 程序结构/02 手牌分类/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:手牌分类 2 | 3 | 编写程序对手牌进行读取和分类。手中的每张牌都有花色(方块,梅花,红桃和黑桃)和等级(2,3,4,5,6,7,8,9,T,J,Q,K 和 A)。不允许使用王牌,并且假设 A 是最高等级的。一手 5 张牌,然后把手中的牌分为下列某一类(列出的顺序从好到坏)。 4 | 5 | - 同花顺(顺序连续且同花色) 6 | - 四张(4 张牌等级相同) 7 | - 葫芦(3 张牌等级一样,另外2 张等级一样) 8 | - 同花(5 张牌同花色) 9 | - 顺子(5 张牌等级顺序连续) 10 | - 三张(3 张牌等级连续) 11 | - 两对 12 | - 一对(2 张牌等级一样) 13 | - 其他牌 14 | 15 | 如果一手牌可以分为两种或多种类别,程序将选择最好的一种。 16 | 17 | 为了便于输入,将牌的等级和花色简化如下: 18 | 19 | - 等级: 2,3,4,5,6,7,8,9,T,J,Q,K ,A 20 | - 花色:c d h s 21 | 22 | 如果用户输入非法牌或者输入同一张牌两次,程序将此牌忽略掉,产生错误信息,然后要求输入另一张牌。如果输入为 0 而不是一张牌,就会导致程序终止。 23 | 24 | 与程序的会话如下: 25 | 26 | ```c 27 | Enter a card : 2s 28 | Enter a card : 5s 29 | Enter a card : 4s 30 | Enter a card : 3s 31 | Enter a card : 6s 32 | Straight flush 33 | 34 | Enter a card : 8c 35 | Enter a card : as 36 | Enter a card : 8c 37 | Duplicated card; ignored. 38 | Enter a card : 7c 39 | Enter a card : ad 40 | Enter a card : 3h 41 | Pair 42 | 43 | Enter a card : 6s 44 | Enter a card : d2 45 | Bad card; ignored. 46 | Enter a card : 2d 47 | Enter a card : 9c 48 | Enter a card : 4h 49 | Enter a card : ts 50 | High card 51 | 52 | Enter a card: 0 53 | ``` 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/11 指针/01 数组的最大元素和最小元素/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/11 指针/01 数组的最大元素和最小元素/main.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/11 指针/01 数组的最大元素和最小元素/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:找出数组中的最大元素和最小元素 2 | 3 | 与程序的交互如下: 4 | 5 | ```c 6 | Enter 5 numbers:9 5 2 7 8 7 | Largest: 9 8 | Smallest: 2 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/13 字符串/01 设计字符串读取函数/read_line.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/13 字符串/01 设计字符串读取函数/read_line.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/13 字符串/01 设计字符串读取函数/readme.md: -------------------------------------------------------------------------------- 1 | #### 3. 逐个字符读取字符串 2 | 3 | 因为对许多程序而言,scanf 函数和 gets 函数都有风险而且不够灵活,C 程序员经常会自己编写输入函数。通过每次读一个字符的方式读取字符串。 4 | 5 | 如果决定自己设计输入函数,那么需要考虑以下问题: 6 | 7 | - 在开始存储字符串之前,函数应该跳过空白字符吗? 8 | - 什么字符导致函数停止读取:换行符,任意空白字符,还是其他某种字符?需要存储这些字符还是忽略掉? 9 | - 如果输入的字符串太长以至于无法存储,那么函数应该忽略额外的字符还是把它们留给下一次输入操作? 10 | 11 | 示例中,我们选择:不跳过空白字符,换行符结束,不存储换行符,忽略掉额外字符。 12 | 13 | 函数原型如下: 14 | 15 | ```c 16 | int read_line(char str[], int read_num); 17 | ``` 18 | 19 | 参数:str 表示存储输入的数组,read_num 表示读入字符的最大数量。 20 | 21 | 返回值:返回读入字符的个数。 22 | 23 | 使用 getchar 实现按字符读入。 24 | 25 | 26 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/13 字符串/02 显示一个月的提醒列表/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:显示一个月的提醒列表 2 | 3 | 此程序会显示每一个月的每日提醒列表。用户需要输入一系列提醒,每条提醒都要有一个前缀来说明是那一个月中的那一天。当用户输入的是 0 而不是有效日期时,程序会显示出录入的全部提醒列表(按日期排序)。下面是会话示例: 4 | 5 | ```c 6 | Enter day and reminder: 24 Suan's birstday 7 | Enter day and reminder: 5 6:00 - Dinner with Marge 8 | Enter day and reminder: 7 10:30 - Movie - "Chinatown" 9 | Enter day and reminder: 0 10 | Day Reminder: 11 | 5 6:00 - Dinner with Marge 12 | 7 10:30 - Movie - "Chinatown" 13 | 24 Suan's birstday 14 | ``` 15 | 16 | - 读入提醒使用我们写的 read_line 函数 17 | - 将提醒存放在二维数组中,数组的每一行看作一个字符串。日期和提示消息都要放进去 。 18 | - 日期我们用整型输入,然后转换为字符串放入二维数组的前面。 19 | - 每次读入新的日期和提示消息后,将转为字符串的当前日期和二维数组每行前面表示日期的部分比较。如果当前日期字符串小于二维数组当前行的字符串,说明当前日期较小,应当插入到当前数组的行前一行。我们可以将二维数组从当前行到存放提示的最后一行每行依次向后移动一行,从而使得当前日期和提示可以插入二维数组的当前行。 20 | - 打印二维数组 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/13 字符串/02 显示一个月的提醒列表/remind.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/13 字符串/02 显示一个月的提醒列表/remind.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/13 字符串/03 核对行星名字/planets.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define NUM_PLANETS 9 5 | 6 | int main(int argc, char* argv[]) { 7 | 8 | int i, j; 9 | char* planets[NUM_PLANETS] = { 10 | "Mercury", "Venus", "Earth", 11 | "Mars", "Jupiter", "Saturn", 12 | "Uranus", "Neptune", "Pluto" 13 | }; 14 | 15 | for (i = 1; i < argc; i++) { 16 | for (j = 0; j < NUM_PLANETS; j++) 17 | if (strcmp(argv[i], planets[j]) == 0) { 18 | printf("%s is a planet %d\n", argv[i], j + 1); 19 | break; 20 | } 21 | if (j == NUM_PLANETS) 22 | printf("%s is not a planet\n", argv[i]); 23 | } 24 | 25 | return 0; 26 | } 27 | // 程序会依次访问每个命令行参数,把它与 planets 中的字符串进行比较,直到找到匹配的名字或到了数组末尾才停止。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/13 字符串/03 核对行星名字/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:核对行星的名字 2 | 3 | 设计一个程序检查一系列字符串,从而找出那些字符串是行星的名字。执行程序时,用户把待测试的字符串放置在命令行中: 4 | 5 | ```c 6 | planet Mercury Aotoman Pluto Thebug Earth 7 | ``` 8 | 9 | 程序会指出每个字符串是否为行星名。如果是,程序还将显示行星的编号: 10 | 11 | ```c 12 | Mercury is a planet 1 13 | Aotoman is not a planet 14 | Pluto is a planet 9 15 | Thebug is not a planet 16 | Earth is a planet 3 17 | ``` 18 | 19 | **注意:**命令行输入的第一个参数 planet 是 c 程序编译出的可执行程序名。一般一个叫 x.c 的程序编译后的可执行程序就叫做 x 。 20 | 21 | 我们命名这个 c 程序为 planet.c 所以编译后的可执行文件应该叫做 planet (在 Windows 上后缀应该为 .exe) -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/justify.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/justify.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/line.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/line.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/line.h: -------------------------------------------------------------------------------- 1 | #ifndef LINE_H 2 | #define LINE_H 3 | 4 | /******************************************************** 5 | * 6 | * clear_line: Clears the current line. 7 | * 8 | *********************************************************/ 9 | void clear_line(); 10 | 11 | 12 | /******************************************************** 13 | * 14 | * add_word: Adds word to the end of current line. 15 | * If this is not the first word on the line, 16 | * puts one space before word. 17 | * 18 | *********************************************************/ 19 | void add_word(const char* word); 20 | 21 | 22 | /******************************************************** 23 | * 24 | * space_remaining: Returns the number of characters left 25 | * in the current line. 26 | * 27 | *********************************************************/ 28 | int space_remaining(); 29 | 30 | 31 | /******************************************************** 32 | * 33 | * write_line: Writes the current line with justification. 34 | * 35 | *********************************************************/ 36 | void write_line(); 37 | 38 | 39 | /******************************************************** 40 | * 41 | * flush_line: Write the current line without 42 | * justification.If the line is empty, 43 | * does nothing. 44 | * 45 | *********************************************************/ 46 | void flush_line(void); 47 | 48 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/quote.txt: -------------------------------------------------------------------------------- 1 | C is quirky, flawed, and an 2 | enormous success. Although accidents of history 3 | surely helped, it evidently satisfied a need 4 | 5 | for a system implementation language efficient 6 | enough to displace assembly language, 7 | yet sufficiently abstract and fluent to describe 8 | algorithms and interactions in a wide variety 9 | of environments. 10 | -- Dennis M. Ritchie -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/readme.md: -------------------------------------------------------------------------------- 1 | ### 二 把程序划分成多个文件 2 | 3 | #### 程序:文本格式化 4 | 5 | 输入未格式化的引语:来自 Dennis M. Ritchie 写的"The Development of the C programming language" 一文: 6 | 7 | ```c 8 | C is quirky, flawed, and an 9 | enormous success. Although accidents of history 10 | surely helped, it evidently satisfied a need 11 | 12 | for a system implementation language efficient 13 | enough to displace assembly language, 14 | yet sufficiently abstract and fluent to describe 15 | algorithms and interactions in a wide variety 16 | of environments. 17 | -- Dennis M. Ritchie 18 | ``` 19 | 20 | 程序完成对这段文字的调整: 21 | 22 | ```c 23 | C is quirky, flawed, and an enormous success. Although 24 | accidents of history surely helped, it evidently satisfied a 25 | need for a system implementation language efficient enough 26 | to displace assembly language, yet sufficiently abstract and 27 | fluent to describe algorithms and interactions in a wide 28 | variety of environments. -- Dennis M. Ritchie 29 | ``` 30 | 31 | 程序分析: 32 | 33 | 完成这个程序需要两步:读入和输出。 34 | 35 | 读入我们选择按单词读入到当前行中,然后按当前行输出。注意输出的每一行最后“对”的很齐,我们 write_line 函数对这种格式做了特殊处理。 36 | 37 | 按单词读入我们创建 word.h 和 word.c 38 | 39 | 按行输出我们创建 line.h 和 line.c 40 | 41 | 最后用 justify.c 包含 main 函数 42 | 43 | ![](./重定向.png) -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/word.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/word.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/word.h: -------------------------------------------------------------------------------- 1 | #ifndef WORD_H 2 | #define WORD_H 3 | 4 | /*********************************************************************** 5 | * 6 | * read_word: Read the next word from the input and stores it in word. 7 | * Make word empty if no word could be read because of EOF. 8 | * Truncates the word if its length exceeds len. 9 | * 10 | ************************************************************************/ 11 | 12 | void read_word(char* word, int len); 13 | 14 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/重定向.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/15 编写大型程序/01 文本格式化/重定向.png -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/16 结构&联合&枚举/01 维护零件数据库/inventory.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include"readline.h" 5 | 6 | #define NAME_LEN 20 7 | #define MAX_PARTS 100 8 | 9 | struct part { 10 | int number; 11 | char name[NAME_LEN + 1]; 12 | int on_hand; 13 | }inventory[MAX_PARTS]; 14 | 15 | int num_parts = 0; //number of parts current stored 16 | 17 | void menu(); 18 | int find_part(int number); 19 | void insert(); 20 | void search(); 21 | void update(); 22 | void print(); 23 | 24 | 25 | int main(void) { 26 | 27 | char code = 'a'; 28 | 29 | menu(); 30 | 31 | for (;;) { 32 | printf("Enter operation code: "); 33 | scanf(" %c", &code); 34 | while (getchar() != '\n') // ships to end of line 35 | ; 36 | switch (code) { 37 | case 'i': insert(); break; 38 | case 's': search(); break; 39 | case 'u': update(); break; 40 | case 'p': print(); break; 41 | case 'q': return 0; 42 | default: printf("Illegal code.\n"); break; 43 | } 44 | } 45 | 46 | 47 | 48 | 49 | return 0; 50 | } 51 | 52 | void menu() { 53 | 54 | printf(" ==================================\n"); 55 | printf(" * *\n"); 56 | printf(" * i: insert *\n"); 57 | printf(" * s: search *\n"); 58 | printf(" * u: undate *\n"); 59 | printf(" * p: print *\n"); 60 | printf(" * q: quit *\n"); 61 | printf(" * *\n"); 62 | printf(" ==================================\n"); 63 | } 64 | 65 | 66 | /********************************************************** 67 | * 68 | * find_part: Looks up a part number in the inventory 69 | * array.Returns the array index if the part 70 | * number is found;otherwise,return -1 71 | * 72 | ***********************************************************/ 73 | int find_part(int number) { 74 | 75 | int i; 76 | 77 | for (i = 0; i < num_parts; i++) { 78 | if (inventory[i].number == number) 79 | return i; 80 | } 81 | 82 | return -1; 83 | } 84 | 85 | 86 | /********************************************************** 87 | * 88 | * insert: Inserts the part into the database.Prints 89 | * an error message and returns prematurely 90 | * if the part already exists or the database 91 | * is full. 92 | * 93 | ***********************************************************/ 94 | void insert() { 95 | 96 | int part_number; 97 | 98 | if (num_parts == MAX_PARTS) { 99 | printf("Database is full; can't add more parts.\n"); 100 | return; 101 | } 102 | 103 | printf("Enter part number: "); 104 | scanf("%d", &part_number); 105 | 106 | if (find_part(part_number) >= 0) { 107 | printf("Part already exists.\n"); 108 | return; 109 | } 110 | 111 | inventory[num_parts].number = part_number; 112 | printf("Enter part name: "); 113 | read_line(inventory[num_parts].name, NAME_LEN); 114 | printf("Enter quantity on hand: "); 115 | scanf("%d", &inventory[num_parts].on_hand); 116 | num_parts++; 117 | } 118 | 119 | 120 | /************************************************************ 121 | * 122 | * search: Look up a part by the number user enters. 123 | * If the part exists, prints the name and quantity 124 | * on hand;if not, print an error message. 125 | * 126 | ************************************************************/ 127 | void search() { 128 | 129 | int index, number; 130 | 131 | printf("Enter part number: "); 132 | scanf("%d", &number); 133 | 134 | index = find_part(number); 135 | 136 | if (index == -1) { 137 | printf("Part not found.\n"); 138 | return; 139 | } 140 | 141 | printf("Part name: %s\n", inventory[index].name); 142 | printf("Quantity on hand: %d\n", inventory[index].on_hand); 143 | 144 | } 145 | 146 | 147 | /************************************************************ 148 | * 149 | * update: Prompts user to enter a number. 150 | * Print an error message if the part doesn't exist; 151 | * otherwise,prompts the user to enter change in 152 | * quantity on hand and updates the database. 153 | * 154 | ************************************************************/ 155 | 156 | void update() { 157 | 158 | int number, index, change; 159 | 160 | printf("Enter part number: "); 161 | scanf("%d", &number); 162 | 163 | index = find_part(number); 164 | 165 | if (index == -1) { 166 | printf("Part not found.\n"); 167 | return; 168 | } 169 | 170 | printf("Enter change in quantity on hand(- means minus): "); 171 | scanf("%d", &change); 172 | inventory[index].on_hand += change; 173 | 174 | } 175 | 176 | 177 | /************************************************************ 178 | * 179 | * print: Print a listing of all parts in the database, 180 | * showing the part number,part name and quantity 181 | * on hand.Parts are printed in the order in which 182 | * they were entered into the database. 183 | * 184 | ************************************************************/ 185 | 186 | void print() { 187 | 188 | int i; 189 | 190 | printf("Part Number Part Name Quantity on Hand\n"); 191 | for (i = 0; i < num_parts; i++) { 192 | printf("%6d%20s%15d\n", inventory[i].number, inventory[i].name, inventory[i].on_hand); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/16 结构&联合&枚举/01 维护零件数据库/readline.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include 5 | #include"readline.h" 6 | 7 | int read_line(char str[], int n) { 8 | 9 | int ch, i = 0; 10 | 11 | while (isspace(ch = getchar())) 12 | ; 13 | 14 | while (ch != '\n' && ch != EOF) { 15 | if (i < n) 16 | str[i++] = ch; 17 | 18 | ch = getchar(); 19 | } 20 | str[i] = '\0'; 21 | 22 | return i; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/16 结构&联合&枚举/01 维护零件数据库/readline.h: -------------------------------------------------------------------------------- 1 | #ifndef READLINE_H 2 | #define READLINE_H 3 | 4 | /*********************************************************** 5 | * 6 | * read_line: Skips leading white-space characters, then 7 | * reads the remainder of the input line and 8 | * stores it in str. Truncates the line if its 9 | * length exceeds n. Return the number of 10 | * characters stores. 11 | * 12 | ***********************************************************/ 13 | 14 | int read_line(char str[], int n); 15 | 16 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/16 结构&联合&枚举/01 维护零件数据库/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:维护零件数据库 2 | 3 | 此程序用来维护仓库存储的零件信息的数据库。程序围绕一个结构数组构建,且每个结构包含以下信息:零件编号,名称和数量。程序将支持下列操作: 4 | 5 | - **添加新零件信息**。如果零件已经存在,或数据库已满,显示出错信息。 6 | - **给定零件编号,显示零件的名称,数量信息**。如果零件编号不存在,那么给出出错信息。 7 | - **给定零件编号,改变零件的数量**。如果零件编号不存在,给出出错消息。 8 | - **显示列出数据库中的全部信息**。零件必须按照录入顺序显示。 9 | - **终止程序的执行** 10 | 11 | 使用: 12 | 13 | - `i`:插入 14 | - `s`:搜索 15 | - `u`:更新 16 | - `p`:显示 17 | - `q`:退出 18 | 19 | 分表表示这种操作,与程序得到会话如下: 20 | 21 | ```c 22 | Enter operation code: i 23 | Enter part number: 833 24 | Enter part name: Disk Drive 25 | Enter quantity on hand: 90 26 | Enter operation code: i 27 | Enter part number: 788 28 | Enter part name: USB 3.0 29 | Enter quantity on hand: 67 30 | Enter operation code: s 31 | Enter part number: 832 32 | Part not found. 33 | Enter operation code: 833 34 | Illegal code. 35 | Enter operation code: s 36 | Enter part number: 833 37 | Part name: Disk Drive 38 | Quantity on hand: 90 39 | Enter operation code: u 40 | Enter part number: 788 41 | Enter change in quantity on hand(- means minus): 3 42 | Enter operation code: p 43 | Part Number Part Name Quantity on Hand 44 | 833 Disk Drive 90 45 | 788 USB 3.0 70 46 | Enter operation code: q 47 | ``` 48 | 49 | 注意:菜单可以没有 50 | 51 | 因为 readline 函数和这个程序的主干没有太大关系,我们用单独的头文件和源文件包含它。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/17 指针的高级应用/01 显示一个月的提醒列表/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:显示一个月的提醒列表 2 | 3 | 前面我们把字符串存储在二维数组中,但是这可能会浪费空间。后面的教学中我们设想使用指针数组存储字符串,让一维数组的每个元素都指向一个字符串字面量。如果数组元素是指向动态分配的字符串的指针,那么是可以实现我们的设想的。 4 | 5 | 下面的程序对之前的程序作了小部分修改,修改的地方后面用注释注明了。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/17 指针的高级应用/01 显示一个月的提醒列表/reminder.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/17 指针的高级应用/01 显示一个月的提醒列表/reminder.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/17 指针的高级应用/02 维护零件数据库(链表)/inventory2.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/17 指针的高级应用/02 维护零件数据库(链表)/inventory2.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/17 指针的高级应用/02 维护零件数据库(链表)/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:维护零件数据库 2 | 3 | 下面重做前面的程序,这次把数据库存储在链表中。链表代替数组主要有两个好处: 4 | 5 | 1. 不需要事先限制数据库的大小 6 | 2. 可以很容易地按零件编号对数据库排序(本程序采用默认升序排序) -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/01 栈模块/array_stack.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 2 | 3 | #include 4 | #include 5 | #include"stack.h" 6 | 7 | #define STACK_SIZE 100 8 | 9 | static int contents[STACK_SIZE]; 10 | static int top = 0; 11 | 12 | static void terminate(const char* message) { 13 | printf("%s\n", message); 14 | exit(EXIT_FAILURE); 15 | } 16 | 17 | void make_empty() { 18 | top = 0; 19 | } 20 | 21 | bool is_empty() { 22 | return top == 0; 23 | } 24 | 25 | bool is_full() { 26 | return top == STACK_SIZE; 27 | } 28 | 29 | void push(int i) { 30 | if (is_full()) 31 | terminate("Error in push: stack is full\n"); 32 | contents[top++] = i; 33 | } 34 | 35 | int pop() { 36 | if (is_empty()) 37 | printf("Error in pop: stack is empty\n"); 38 | return contents[--top]; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/01 栈模块/linkedlist_stack.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include 5 | #include"stack.h" 6 | 7 | typedef struct node { 8 | int data; 9 | struct node* next; 10 | }node; 11 | 12 | static node* top = NULL; 13 | 14 | static void terminate(char* message) { 15 | printf("%s\n", message); 16 | exit(EXIT_FAILURE); 17 | } 18 | 19 | void make_empty() { 20 | while (!is_empty()) 21 | pop(); 22 | } 23 | 24 | bool is_empty() { 25 | return top == NULL; 26 | } 27 | 28 | bool is_full() { 29 | return false; 30 | } 31 | 32 | void push(int i) { 33 | 34 | node* new_node = (node*)malloc(sizeof(node)); 35 | if (new_node == NULL) { 36 | terminate("Error in push: stack is full.\n"); 37 | exit(EXIT_FAILURE); 38 | } 39 | 40 | new_node->data = i; 41 | new_node->next = top; 42 | top = new_node; 43 | } 44 | 45 | int pop() { 46 | 47 | if (is_empty()) 48 | terminate("Error in pop: stack is empty.\n"); 49 | 50 | int data = top->data; 51 | 52 | node* del = top; 53 | top = top->next; 54 | free(del); 55 | 56 | return data; 57 | } 58 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/01 栈模块/stack.h: -------------------------------------------------------------------------------- 1 | #ifndef STACK_H 2 | #define STACK_H 3 | 4 | #include //C99 only 5 | 6 | void make_empty(); 7 | bool is_empty(); 8 | bool is_full(); 9 | void push(int i); 10 | int pop(); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/01 定长数组实现/stackADT.c: -------------------------------------------------------------------------------- 1 | //#define _CRT_SECURE_NO_WARNINGS 1 2 | // 3 | //#include 4 | //#include 5 | //#include"stackADT.h" 6 | // 7 | //#define STACK_SIZE 100 8 | // 9 | //typedef struct stack_type { 10 | // int contents[STACK_SIZE]; 11 | // int top; 12 | //}stack_type; 13 | // 14 | // 15 | // 16 | //static void terminate(char* message) { 17 | // printf("%s\n", message); 18 | // exit(EXIT_FAILURE); 19 | //} 20 | // 21 | //Stack create() { 22 | // 23 | // Stack s = (Stack)malloc(sizeof(stack_type)); 24 | // if (s == NULL) { 25 | // terminate("Error in create: stack could not be created.\n"); 26 | // exit(EXIT_FAILURE); 27 | // } 28 | // s->top = 0; 29 | // 30 | // return s; 31 | //} 32 | // 33 | // 34 | //void destory(Stack s) { 35 | // 36 | // free(s); 37 | //} 38 | // 39 | // 40 | //void make_empty(Stack s) { 41 | // 42 | // s->top = 0; 43 | //} 44 | // 45 | //bool is_empty(Stack s) { 46 | // return s->top == 0; 47 | //} 48 | // 49 | //bool is_full(Stack s) { 50 | // return s->top == STACK_SIZE; 51 | //} 52 | // 53 | //void push(Stack s, int i) { 54 | // 55 | // if (is_full(s)) { 56 | // terminate("Error in push: stack is full.\n"); 57 | // exit(EXIT_FAILURE); 58 | // } 59 | // 60 | // s->contents[s->top++] = i; 61 | //} 62 | // 63 | //int pop(Stack s) { 64 | // 65 | // if (is_empty(s)) { 66 | // terminate("Error in pop: stack is empty.\n"); 67 | // exit(EXIT_FAILURE); 68 | // } 69 | // 70 | // return s->contents[--s->top]; 71 | //} -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/01 定长数组实现/stackADT.h: -------------------------------------------------------------------------------- 1 | #ifndef STACKADT_H 2 | #define STACKADT_H 3 | 4 | #include 5 | 6 | typedef struct stack_type* Stack; 7 | 8 | Stack create(); 9 | void destory(Stack s); 10 | void make_empty(Stack s); 11 | bool is_empty(const Stack s); 12 | bool is_full(const Stack s); 13 | void push(Stack s, int i); 14 | int pop(Stack s); 15 | 16 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/02 动态数组实现/stackADT3.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/02 动态数组实现/stackADT3.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/02 动态数组实现/stackADT3.h: -------------------------------------------------------------------------------- 1 | #ifndef STACKADT3_H 2 | #define STACKADT3_H 3 | 4 | #include 5 | 6 | typedef int Item; 7 | 8 | typedef struct stack_type* Stack; 9 | 10 | Stack create(int size); 11 | void destory(Stack s); 12 | void make_empty(Stack s); 13 | bool is_empty(const Stack s); 14 | bool is_full(const Stack s); 15 | void push(Stack s, Item i); 16 | Item pop(Stack s); 17 | 18 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/03 链表实现/stackADT4.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include 5 | #include"stackADT4.h" 6 | 7 | typedef struct node{ 8 | Item data; 9 | struct node* next; 10 | }node; 11 | 12 | typedef struct stack_type { 13 | node* top; 14 | }stack_type; 15 | 16 | 17 | static void terminate(char* message) { 18 | printf("%s\n", message); 19 | exit(EXIT_FAILURE); 20 | } 21 | 22 | Stack create() { 23 | Stack s = (Stack)malloc(sizeof(stack_type)); 24 | if (s == NULL) { 25 | terminate("Error in create: stack could not be created.\n"); 26 | exit(EXIT_FAILURE); 27 | } 28 | s->top = NULL; 29 | 30 | return s; 31 | } 32 | 33 | void destory(Stack s) { 34 | 35 | make_empty(s); 36 | free(s); 37 | } 38 | 39 | 40 | 41 | void make_empty(Stack s) { 42 | while (!is_empty(s)) 43 | pop(s); 44 | } 45 | 46 | bool is_empty(Stack s) { 47 | return s->top == NULL; 48 | } 49 | 50 | bool is_full(Stack s) { 51 | return false; 52 | } 53 | 54 | void push(Stack s, Item i) { 55 | 56 | node* new_node = (node*)malloc(sizeof(node)); 57 | if (new_node == NULL) { 58 | terminate("Error in push: stack is full.\n"); 59 | exit(EXIT_FAILURE); 60 | } 61 | 62 | new_node->data = i; 63 | new_node->next = s->top; 64 | s->top = new_node; 65 | } 66 | 67 | Item pop(Stack s) { 68 | 69 | if (is_empty(s)) 70 | terminate("Error in pop: stack is empty.\n"); 71 | 72 | int data = s->top->data; 73 | 74 | node* del = s->top; 75 | s->top = s->top->next; 76 | free(del); 77 | 78 | return data; 79 | } 80 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/03 链表实现/stackADT4.h: -------------------------------------------------------------------------------- 1 | #ifndef STACKADT4_H 2 | #define STACKADT4_H 3 | 4 | #include 5 | 6 | typedef int Item; 7 | 8 | typedef struct stack_type* Stack; 9 | 10 | Stack create(); 11 | void destory(Stack s); 12 | void make_empty(Stack s); 13 | bool is_empty(const Stack s); 14 | bool is_full(const Stack s); 15 | void push(Stack s, Item i); 16 | Item pop(Stack s); 17 | 18 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/04 最终形态/final_stackADT.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include"final_stackADT.h" 8 | 9 | #define PUBLIC 10 | #define PRIVATE static 11 | 12 | typedef struct node{ 13 | Item data; 14 | struct node* next; 15 | }node; 16 | 17 | typedef struct stack_type { 18 | node* top; 19 | Item pop_val; 20 | }stack_type; 21 | 22 | 23 | PUBLIC Stack stack_create() { 24 | 25 | Stack s = (Stack)malloc(sizeof(stack_type)); 26 | 27 | assert(s != NULL); 28 | 29 | s->top = NULL; 30 | 31 | return s; 32 | } 33 | 34 | PUBLIC void stack_destory(Stack s) { 35 | 36 | stack_make_empty(s); 37 | free(s); 38 | } 39 | 40 | 41 | 42 | PUBLIC void stack_make_empty(Stack s) { 43 | while (!stack_is_empty(s)) 44 | stack_pop(s); 45 | } 46 | 47 | PUBLIC bool stack_is_empty(Stack s) { 48 | return s->top == NULL; 49 | } 50 | 51 | PUBLIC bool stack_is_full(Stack s) { 52 | return false; 53 | } 54 | 55 | PUBLIC bool stack_push(Stack s, Item i) { 56 | 57 | node* new_node = (node*)malloc(sizeof(node)); 58 | if (new_node == NULL) 59 | return false; 60 | 61 | new_node->data = i; 62 | new_node->next = s->top; 63 | s->top = new_node; 64 | 65 | return true; 66 | } 67 | 68 | PUBLIC Item* stack_pop(Stack s) { 69 | 70 | if (stack_is_empty(s)) 71 | return NULL; 72 | 73 | node* del = s->top; 74 | s->pop_val = del->data; 75 | s->top = s->top->next; 76 | free(del); 77 | 78 | return &s->pop_val; 79 | } 80 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/04 最终形态/final_stackADT.h: -------------------------------------------------------------------------------- 1 | #ifndef FINAL_STACKADT_H 2 | #define FINAL_STACKADT_H 3 | 4 | #include 5 | 6 | typedef int Item; 7 | 8 | typedef struct stack_type* Stack; 9 | 10 | Stack stack_create(); 11 | void stack_destory(Stack s); 12 | void stack_make_empty(Stack s); 13 | bool stack_is_empty(const Stack s); 14 | bool stack_is_full(const Stack s); 15 | bool stack_push(Stack s, Item i); 16 | Item* stack_pop(Stack s); 17 | 18 | #endif -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/04 最终形态/stackclient.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include"final_stackADT.h" 5 | 6 | int main(void) { 7 | 8 | Stack s1, s2; 9 | 10 | s1 = stack_create(); 11 | s2 = stack_create(); 12 | 13 | stack_push(s1, 1); 14 | stack_push(s1, 2); 15 | 16 | printf("%d\n", *stack_pop(s1)); 17 | printf("%d\n", *stack_pop(s1)); 18 | 19 | stack_destory(s1); 20 | stack_destory(s2); 21 | 22 | return 0; 23 | } 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/readme.md: -------------------------------------------------------------------------------- 1 | 不同的实现方式中 接口(头文件)和客户(包含main函数的文件)改变的很少 2 | 3 | 我们一共使用了三种方式实现栈抽象数据类型: 4 | 1. 使用定长数组 5 | 2. 用动态数组 6 | 3. 使用链表 7 | 8 | “最终形态”是基于链表的一些“改进”: 9 | - 基于“不完整类型”的封装 10 | - 使用宏来定义 PUBLIC 和 PRIVATE 11 | - 使用不容易冲突的命名 12 | - 提供错误处理的可能(push 和 pop 函数返回值) 13 | 14 | 最后,本想通过改变 push 和 pop 来实现“通用抽象数据类型”,在修改程序时发现类型名还是不是很会处理,作罢。 15 | 可以参考下面的函数声明来完成它: 16 | ```c 17 | bool stack_push(Stack s, void* i); 18 | void* stack_pop(Stack s); 19 | ``` -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/19 程序设计/01 栈的实现/02 栈抽象数据类型/stackclient.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include"stackADT.h" 5 | 6 | int main(void) { 7 | 8 | Stack s1, s2; 9 | 10 | s1 = create(); 11 | s2 = create(); 12 | 13 | push(s1, 1); 14 | push(s1, 2); 15 | 16 | printf("%d\n", pop(s1)); 17 | printf("%d\n", pop(s1)); 18 | 19 | destory(s1); 20 | destory(s2); 21 | 22 | return 0; 23 | } 24 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/20 底层程序设计/01 XOR加密/msg.txt: -------------------------------------------------------------------------------- 1 | If two people write exactly the same program, each should be put in 2 | micro-code and then they certainly won't be the same. 3 | -- epigrams-on-programming 4 | Time:4/21/2020 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/20 底层程序设计/01 XOR加密/newmsg.txt: -------------------------------------------------------------------------------- 1 | o@ RQI VCIVJC QTORC C^GERJ_ RNC UGKC VTIATGK, CGEN UNISJB DC VSR OH 2 | KOETI-EIBC GHB RNCH RNC_ ECTRGOHJ_ QIH'R DC RNC UGKC. 3 | -- CVOATGKU-IH-VTIATGKKOHA 4 | rOKC:4/21/2020 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/20 底层程序设计/01 XOR加密/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:XOR 加密 2 | 3 | 对数据加密的一种最简单的方法就是,将每个字符与一个密钥进行异或(XOR)运算。假设密钥时一个 & 字符。如果将它与字符 z 异或,我们会得到 \ 字符(假定字符集位 ACSII 字符集)。具体计算如下: 4 | 5 | ```c 6 | 00100110 (& 的 ASCII 码) 7 | XOR 01111010 (z 的 ASCII 码) 8 | 01011100 (\ 的 ASCII 码) 9 | ``` 10 | 11 | 要将消息解密,只需要采用相同的算法。例如,如果将 & 与 \ 异或就可以得到 &: 12 | 13 | ```c 14 | 00100110 (& 的 ASCII 码) 15 | XOR 01011100 (\ 的 ASCII 码) 16 | 01111010 (z 的 ASCII 码) 17 | ``` 18 | 19 | 下面的程序 xor.c 通过每个字符于 & 字符进行异或来加密消息。原始消息可以由用户输入也可以输入重定向从文件读入。加密后的消息可以在屏幕上显示也可以通过输出重定向存入到文件中。例如 msg 文件包含以下内容: 20 | 21 | ``` 22 | If two people write exactly the same program, each should be put in 23 | micro-code and then they certainly won't be the same. 24 | -- epigrams-on-programming 25 | Time:4/21/2020 26 | ``` 27 | 28 | 为了对文件 msg 加密并将加密后的消息存入文件 newmsg 中,输入以下命令: 29 | 30 | ```c 31 | xor newmsg 32 | ``` 33 | 34 | 文件 newmsg 将包含下面的内容: 35 | 36 | ``` 37 | o@ RQI VCIVJC QTORC C^GERJ_ RNC UGKC VTIATGK, CGEN UNISJB DC VSR OH 38 | KOETI-EIBC GHB RNCH RNC_ ECTRGOHJ_ QIH'R DC RNC UGKC. 39 | -- CVOATGKU-IH-VTIATGKKOHA 40 | rOKC:4/21/2020 41 | ``` 42 | 43 | 要恢复原始消息,需要命令: 44 | 45 | ```c 46 | xor 4 | #include 5 | 6 | #define KEY '&' 7 | 8 | int main(void) { 9 | 10 | int orig_ch, new_ch; 11 | 12 | while ((orig_ch = getchar()) != EOF) { 13 | new_ch = orig_ch ^ KEY; 14 | if (isprint(orig_ch) && isprint(new_ch)) 15 | putchar(new_ch); 16 | else 17 | putchar(orig_ch); 18 | } 19 | 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/20 底层程序设计/02 查看内存单元/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:查看内存单元 2 | 3 | 这个程序允许用户查看计算机内存段,这主要得益于 C 允许把整数用作指针。大多数 CPU 执行程序时都是处于“保护模式”,这就意味着程序只能访问那些分配给它的内存。这种方式还可以阻止对其他应用程序和操作系统本身所占用的内存的访问。因此我们只能看到程序本身分配到的内存,如果要对其他内存地址进行访问将导致程序崩溃。 4 | 5 | 程序 veiw_memory.c 先显示了该程序主函数和主函数中第一个变量的地址,这样可以给用户一个线索去了解那个内存可以被探测。程序接下来提示用户输入地址(16 进制格式)和需要查看的字节数,然后从指定地址开始显示指定字节内存块的内容。 6 | 7 | 字节按 10 个一组的方式显示(最后一组可能达不到 10 个)。每组字节的首地址显示在一行的开头,然后是该组的字节(16 进制格式),再后面为该组字节的字符显示。只有打印字符(使用 `isprint`函数判断)会被显示,其余的被显示为 `.`。 8 | 9 | 假设 int 类型大小为 32 位,地址也是 32 位长。 10 | 11 | 格式如下: 12 | 13 | ``` 14 | Address of main function: 5712bc 15 | Address of addr variable: bcf784 16 | 17 | Enter a (hex)address: 5712bc 18 | Enter number of bytes to view: 40 19 | 20 | Address Bytes Characters 21 | ---------------------------------------------------- 22 | 5712BC E9 6F 06 00 00 E9 EA 04 00 00 .o........ 23 | 5712C6 E9 45 22 00 00 E9 50 3F 00 00 .E"...P?.. 24 | 5712D0 E9 FB 0C 00 00 E9 A6 27 00 00 .......'.. 25 | 5712DA E9 14 3E 00 00 E9 AC 1E 00 00 ..>....... 26 | ``` 27 | 28 | . 29 | 30 | ``` 31 | Address of main function: 5712bc 32 | Address of addr variable: effbc8 33 | 34 | Enter a (hex)address: effbc8 35 | Enter number of bytes to view: 64 36 | 37 | Address Bytes Characters 38 | ---------------------------------------------------- 39 | EFFBC8 C8 FB EF 00 CC CC CC CC 99 76 .........v 40 | EFFBD2 90 86 F4 FB EF 00 63 24 57 00 ......c$W. 41 | EFFBDC 01 00 00 00 F8 4F 2E 01 B0 70 .....O...p 42 | EFFBE6 2E 01 01 00 00 00 F8 4F 2E 01 .......O.. 43 | EFFBF0 B0 70 2E 01 50 FC EF 00 B7 22 .p..P...." 44 | EFFBFA 57 00 1D 71 90 86 48 13 57 00 W..q..H.W. 45 | EFFC04 48 13 57 00 H.W. 46 | 47 | ``` 48 | 49 | (前 4 个字节是我们输入的表示地址的整数,注意它的每个字节存储顺序) -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/20 底层程序设计/02 查看内存单元/view_memory.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/20 底层程序设计/02 查看内存单元/view_memory.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/01 fopen 使用框架/frame.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/22 输入&输出/01 fopen 使用框架/frame.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/01 fopen 使用框架/readme.md: -------------------------------------------------------------------------------- 1 | 下面给出了一个程序的框架。此程序打开文件 example.dat 进行读操作,并检查打开是否成功,让后在程序终止前把文件关闭: 2 | 3 | 可以将 fp 的声明与函数调用结合: 4 | 5 | ```c 6 | FILE* fp = fopen(FILE_NAME, "r"); 7 | ``` 8 | 9 | 还可以将函数调用与 NULL 判定结合: 10 | 11 | ```c 12 | if((fp = fopen(FILE_NAME, "r")) == NULL) ... 13 | ``` 14 | 15 | 16 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/02 检查文件是否可以打开/canopen.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include 5 | 6 | int main(int argc, char* argv[]) { 7 | 8 | FILE* fp; 9 | 10 | if (argc != 2) { 11 | printf("usage: canopen filename\n"); 12 | exit(EXIT_FAILURE); 13 | } 14 | 15 | if ((fp = fopen(argv[1], "r")) == NULL) { 16 | printf("%s can't be opend.\n", argv[1]); 17 | exit(EXIT_FAILURE); 18 | } 19 | 20 | printf("%s can be opend.\n", argv[1]); 21 | 22 | fclose(fp); 23 | 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/02 检查文件是否可以打开/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:检查文件是否可以打开 2 | 3 | 下面程序判断文件是否存在,如果存在是否可以打开进行读入。在运行程序时,用户给出要检查的文件的名字: 4 | 5 | ```c 6 | canopen file 7 | ``` 8 | 9 | 然后程序将显示 : 10 | 11 | ``` 12 | file can be opend. 13 | 或 14 | file can't be opend. 15 | ``` 16 | 17 | 如果在命令行中录入的实际参数的数量不对,那么程序将显示出消息: 18 | 19 | ``` 20 | usage: canopen filename 21 | ``` 22 | 23 | 来提示用户 canopen 需要一个文件名。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/03 复制文件/fcopy.c: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 1 2 | 3 | #include 4 | #include 5 | 6 | int main(int argc, char* argv[]) { 7 | 8 | FILE* src_fp, * dest_fp; 9 | int ch; 10 | 11 | if (argc != 3) { 12 | fprintf(stderr, "usage: fcopy source dest\n"); 13 | exit(EXIT_FAILURE); 14 | } 15 | 16 | if ((src_fp = fopen(argv[1], "rb")) == NULL) { 17 | fprintf(stderr, "Can't open file %s\n", argv[1]); 18 | exit(EXIT_FAILURE); 19 | } 20 | 21 | if ((dest_fp = fopen(argv[2], "wb")) == NULL) { 22 | fprintf(stderr, "Can't open file %s\n", argv[2]); 23 | fclose(src_fp); 24 | exit(EXIT_FAILURE); 25 | } 26 | 27 | while ((ch = getc(src_fp)) != EOF) 28 | putc(ch, dest_fp); 29 | 30 | fclose(src_fp); 31 | fclose(dest_fp); 32 | 33 | return 0; 34 | } -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/03 复制文件/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:复制文件 2 | 3 | 下面的程序用来进行文件的复制操作。当程序执行时,在命令行上指定原始文件名和新文件名。例如,将文件 f1.c 复制给文件 f2.c,使用命令: 4 | 5 | ```c 6 | fcopy f1.c f2.c 7 | ``` 8 | 9 | 如果命令行上的文件名不是两个,或者至少有一个文件无法打开,那么程序 fcopy 都将产出出错消息。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/04 修改零件记录文件/invclear.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Crash_Course/01 Examples/22 输入&输出/04 修改零件记录文件/invclear.c -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/22 输入&输出/04 修改零件记录文件/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:修改零件记录文件 2 | 3 | 下面这个程序打开包含 part 结构的二进制文件,把结构读到数组中,把每个结构的成员 on_hand 置为 0,然后再把此结构写回到文件中。注意,程序使用 `"rb+"`模式打开文件,因此既可以读又可以写。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/24 错误处理/01 测试信号/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序:测试信号 2 | 3 | 下面的程序说明了如何使用信号。首先,给 SIGINT 信号安装了一个惯用的处理函数(并小心地保存了原先的处理函数),然后调用`raise_sig` 产生该信号:接下来,程序将 SIG_IGN设置为SIGINT的处理函数并再次调用 raise_sig;最后,它重新安装信号 SIGINT原先的处理函数,并最后调用一次 raise_sig。 4 | 5 | **tsignal.c** 6 | 7 | ```c 8 | #include 9 | #include 10 | 11 | void handler(int sig) ; 12 | void raise_sig(void) ; 13 | 14 | int main(void){ 15 | 16 | void (*orig_handler) (int); 17 | 18 | printf("Installing handler for signal %d\n", SIGINT) ; 19 | 20 | orig_ handler = signal(SIGINT, handler); 21 | raise_sig(); 22 | 23 | printf("Changing handler to SIG_IGN\n"); 24 | signal (SIGINT, SIG_IGN) ; 25 | raise_ sig(); 26 | 27 | printf("Restoring original handler\n"); 28 | signal (SIGINT, orig_handler); 29 | raise_ sig(); 30 | 31 | printf("Program terminates normally\n"); 32 | return 0 33 | } 34 | 35 | void handler(int sig){ 36 | printf("Handler called for signal ad\n", sig); 37 | } 38 | 39 | void raise_sig(void){ 40 | raise (SIGINT); 41 | } 42 | ``` 43 | 44 | 45 | 46 | 当然,调用 raise 并不需要在单独的函数中。这里定义 raise_sig 函数只是为了说明一点:无论信号是从哪里产生的(无论是在main函数中还是在其他函数中),它都会被最近安装的该信号的处理函数捕获。 47 | 48 | 这段程序的输出可能会有多种。下面是一 种可能的输出形式: 49 | 50 | ``` 51 | Installing handler for signal 2 52 | Handler called for signal 2 53 | Changing handler to SIG_IGN 54 | Restoring original handler 55 | ``` 56 | 57 | 这个输出结果表明,我们的实现把 SIGINT 的值定义为 2,而且 SIGIN原先的处理函数一定是 SIG_DFL。(如果是 SIG_IGN,应该会看到信息 Program terminates normally) 最后, 我们注意到 SIG_DFL 会导致程序终止,但不会显示出错消息。 -------------------------------------------------------------------------------- /Coding/C_Crash_Course/01 Examples/24 错误处理/02 测试 setjmp 和 longjmp/readme.md: -------------------------------------------------------------------------------- 1 | #### 程序测试setjmp和1ongjmp 2 | 3 | 下面的程序使用 setjmp 宏在 main 函数中标记一个位置,然后函数 f2 通过调用 1ongjmp 函数返回到这个位置。 4 | 5 | **tsetjmp.c** 6 | 7 | ```c 8 | /* Tests setjmp/1ongjmp */ 9 | #include 10 | #include 11 | 12 | jmp_buf env; 13 | 14 | void f1(void); 15 | void f2(void); 16 | 17 | int main(void) { 18 | 19 | if (setjmp(env) == 0) 20 | printf("setjmp returned 0\n"); 21 | else { 22 | printf("Program terminates: longjmp called\n"); 23 | return 0; 24 | } 25 | 26 | f1(); 27 | printf("Program terminates normally\n"); 28 | 29 | return 0; 30 | } 31 | void f1(void) { 32 | printf("f1 begins\n"); 33 | f2(); 34 | printf("f1 returs\n"); 35 | } 36 | void f2(void) { 37 | printf("f2 begins\n"); 38 | longjmp(env, 1); 39 | printf("f2 returns\n"); 40 | } 41 | ``` 42 | 43 | 这段程序的输出如下: 44 | 45 | ``` 46 | setjmp returned 0 47 | f1 begins 48 | f2 begins 49 | Program terminates: longjmp called 50 | ``` 51 | 52 | setjmp 宏的最初调用返回 0,因此main函数会调用 f1。接着,f1 调用 f2,f2 使用1ongjmp 函数将控制权重新转给 main 函数,而不是返回到 f1 。当 longjmp 函数被执行时,控制权重新回到 setjmp 宏调用。这次 setjmp宏返回 1 (就是在longjmp函数调用时所指定的值)。 -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/常见字符串函数/Test/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/常见字符串函数/Test/main.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/常见字符串函数/Test/readme.md: -------------------------------------------------------------------------------- 1 | 写文章中的测试代码 -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/常见字符串函数/mystrcmp/VSstrcmp.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/常见字符串函数/mystrcmp/VSstrcmp.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/常见字符串函数/mystrcmp/main.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | int main() { 4 | 5 | char* str1 = "Hello "; 6 | char* str2 = "Hello"; 7 | 8 | printf("%d\n", mystrcmp(str1, str2)); 9 | 10 | return 0; 11 | } -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/常见字符串函数/mystrcmp/mystrcmp.c: -------------------------------------------------------------------------------- 1 | int mystrcmp(char* str1, char* str2) { 2 | 3 | while (*str1 == *str2 && *str1) 4 | ++str1, ++str2; 5 | 6 | return (*str1 - *str2); 7 | } -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/结构/stucture-as-func-parameter/main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/结构/stucture-as-func-parameter/main.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/结构/stucture-as-func-parameter/readme.md: -------------------------------------------------------------------------------- 1 | ## 结构体作为函数参数 2 | **明天的日期** 3 | *写一个程序:输入今天的日期,求明天的日期* 4 | 5 | 看着这个问题简单,实际上稍微有点技巧. 6 | 7 | 给出下面四个特殊日期,大家可以思考一下: 8 | 9 | >2020 - 1 -31 10 | > 11 | >2020 - 11 - 31 12 | > 13 | >2020 - 12 - 31 14 | > 15 | >2000 - 2 - 28 16 | 17 | ## Use sturcture itself as a function's parameter 18 | it's not an efficient way 19 | 20 | >K & R said (p.131 ) 21 | >"if a large structure is to be passed to a function, 22 | >in is generally more efficent to pass a pointer than to copy the whole structure" 23 | -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/结构/test/test.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/结构/test/test.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/编写大型程序/test.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/编写大型程序/test.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/输入&输出/binary file/fwrite/student information/read.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/输入&输出/binary file/fwrite/student information/read.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/输入&输出/binary file/fwrite/student information/readme.md: -------------------------------------------------------------------------------- 1 | 程序中向文件中写入/读取2进制文本 2 | 3 | linux 下可以用 `od` 命令打开文件 4 | 5 | -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/输入&输出/binary file/fwrite/student information/student.h: -------------------------------------------------------------------------------- 1 | #ifndef _STUDENT_H_ 2 | #define _STUDENT_H_ 3 | 4 | #define STR_LEN 20 5 | 6 | typedef struct _student { 7 | char name[STR_LEN]; 8 | char gender; 9 | char age; 10 | }Student; 11 | #endif -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/输入&输出/binary file/fwrite/student information/write.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/输入&输出/binary file/fwrite/student information/write.c -------------------------------------------------------------------------------- /Coding/C_Mooc/01 Examples/输入&输出/test code/test.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Mooc/01 Examples/输入&输出/test code/test.c -------------------------------------------------------------------------------- /Coding/C_Traps_and_Pitfalls/Exercises/03 语义陷阱/01 二分查找/bsearch.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/C_Traps_and_Pitfalls/Exercises/03 语义陷阱/01 二分查找/bsearch.c -------------------------------------------------------------------------------- /Coding/C_Traps_and_Pitfalls/Exercises/03 语义陷阱/01 二分查找/readme.md: -------------------------------------------------------------------------------- 1 | 编写一个函数,对一个已排序的整数表执行二分查找。函数的输入包含一个指向表头的指针,表中的元素个数,以及待查找的数值。函数输出是一个指向满足查找要求的元素的指针,当未查找的要求的数值时,输出一个NULL指针。 2 | -------------------------------------------------------------------------------- /Coding/C_Traps_and_Pitfalls/readme.md: -------------------------------------------------------------------------------- 1 | ## 《C 陷阱与缺陷》 -------------------------------------------------------------------------------- /Coding/Expert_C_Programming/readme.md: -------------------------------------------------------------------------------- 1 | ## 《C 专家编程》 -------------------------------------------------------------------------------- /Coding/Practice/string/README.md: -------------------------------------------------------------------------------- 1 | ## 常见字符串/内存函数实现 2 | 3 | 4 | 5 | ### strlen 6 | 7 | 8 | 9 | ### strcpy 10 | 11 | 12 | 13 | ### strcat 14 | 15 | 16 | 17 | ### strcmp 18 | 19 | 20 | 21 | ### strstr 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Coding/Project/01_信息管理系统/01_工人信息管理系统/README.md: -------------------------------------------------------------------------------- 1 | 讲解见:[https://www.cctechblog.cn/_posts/2020-06-18-%E9%87%8D%E6%B8%A9-C-%E8%AF%AD%E8%A8%80%E8%AF%BE%E7%A8%8B%E8%AE%BE%E8%AE%A1/](https://www.cctechblog.cn/_posts/2020-06-18-重温-C-语言课程设计/) -------------------------------------------------------------------------------- /Coding/Project/01_信息管理系统/01_工人信息管理系统/worker.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Project/01_信息管理系统/01_工人信息管理系统/worker.c -------------------------------------------------------------------------------- /Coding/Project/01_信息管理系统/01_工人信息管理系统/worker.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Project/01_信息管理系统/01_工人信息管理系统/worker.h -------------------------------------------------------------------------------- /Coding/Project/02_通讯录/AddressBook.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Project/02_通讯录/AddressBook.c -------------------------------------------------------------------------------- /Coding/Project/02_通讯录/AddressBook.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Project/02_通讯录/AddressBook.h -------------------------------------------------------------------------------- /Coding/Project/02_通讯录/Main.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Project/02_通讯录/Main.c -------------------------------------------------------------------------------- /Coding/Project/02_通讯录/address_book.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/Coding/Project/02_通讯录/address_book.txt -------------------------------------------------------------------------------- /Coding/readme.md: -------------------------------------------------------------------------------- 1 | ## 代码 Code 2 | 3 |
4 | 5 | ### 导航 6 | *** 7 | #### C 慕课 8 | #### C 入门到精通 9 | #### C 进阶 10 | #### 《C 陷阱与缺陷》 11 | #### 《C 专家编程》 12 |
13 | 14 | 15 | 16 |
-------------------------------------------------------------------------------- /content/c-games/猜数字.md: -------------------------------------------------------------------------------- 1 | # C语言——猜数字小游戏 2 | 3 | ## 如何用rand,srand,time来完成随机数发生 4 | 5 | ------ 6 | 7 | 这是这款小游戏的简单玩法。期待着与你一同完善,改进这个小游戏! 8 | 9 | ------ 10 | 11 | 接下来我们看一下如何来实现这样一个游戏。 12 | 纵观这个游戏,我们发现,这个游戏的难点其实就是**如何生成一个随机数**。 13 | 生成随机数我们这里用到了三个个函数分别是: 14 | 15 | - `rand()` 16 | - `srand()` 17 | - `time()` 18 | 19 | 由于生成随机数是我们这个游戏的核心,我们把这三个函数在这里细讲一下 20 | 21 | ------ 22 | 23 | > **`int rand()`** 24 | > **头文件**:stdlib.h 25 | > **定义**:srand() 播种 rand() 所用的伪随机数生成器。若在任何对 srand() 的调用前使用 rand() ,则 rand() 表现如同它以 srand(1) 播种。每次以 srand() 播种 rand() 时,它必须产生相同的值数列。 26 | > **返回值**:返回 ​0​ 与 RAND_MAX 间的随机整数值(包含 0 与 RAND_MAX )。 27 | > **`void srand( unsigned seed )`** 28 | > **头文件**:stdlib.h 29 | > **定义**:以值 seed 播种 rand() 所用的随机数生成器。若在任何到 srand() 的调用前使用 rand() ,则 rand() 表现为如同它被以 srand(1) 播种。每次以同一 seed 播种 rand() 时,它必须产生相同的值数列。 30 | > **返回值**:无 31 | 32 | 是不是听了之后很懵逼?没关系,请看下面的例子 33 | 34 | ``` 35 | //大家可以用自己的编译器反复运行下面的代码,可以发现,每次产生的五个数都是一样的。 36 | #include 37 | #include 38 | int main() { 39 | int x = 0; 40 | int test = 5; 41 | int i = 0; 42 | i = test; 43 | //srand(1);//不写srand函数是默认srand内参数为1 44 | while (i--) { 45 | x = rand()%10 + 1; 46 | printf("%d ", x); 47 | } 48 | printf("\n"); 49 | } 50 | ``` 51 | 52 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 53 | 54 | ``` 55 | //这时候我们调用srand函数并且改变参数,再次观察产生的随机序列,会发现这次的五个数和之前的不同了 56 | #include 57 | #include 58 | int main() { 59 | int x = 0; 60 | int test = 5; 61 | int i = 0; 62 | i = test; 63 | srand(2); 64 | while (i--) { 65 | x = rand() % 10 + 1; 66 | printf("%d ", x); 67 | } 68 | printf("\n"); 69 | } 70 | //这就说明了srand内的参数可以让rand函数产生不同的序列,但是这些序列并不是随机的。 71 | //如果我们要产生随机数,就必须不断改变srand函数内的参数,这时我们就需要引入time函数。 72 | ``` 73 | 74 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 75 | 76 | > **`time_t time( time_t \*arg )`** 77 | > **头文件**:time.h 78 | > **定义**:返回编码成 time_t 对象的当前日历时间,并将其存储于 arg 指向的 time_t 对象(除非 arg 为空指针) 79 | > **参数**:arg - 指向将存储时间的 time_t 对象的指针,或空指针 80 | > **返回值**:成功时返回编码成 time_t 对象的当前日历时间。错误时返回 (time_t)(-1) 。若arg不是空指针,则返回值也会存储于 arg 所指向的对象。 81 | > 82 | > ------ 83 | > 84 | > 需要注意的是,在我的32位的vs2019编译器上,time_t的类型是long long 85 | 86 | 对于time的用法请看下例 87 | 88 | ``` 89 | #include 90 | #include 91 | #include 92 | int main() { 93 | time_t t1, t2; 94 | t1 = time(NULL);//传入空指针,需要用t1接收返回的时间 95 | time(&t2);//传入指针,当前的时间戳写入t2 96 | printf("%lld\n",t1); 97 | printf("%lld\n",t2); 98 | } 99 | //两个printf输出的数相同 100 | ``` 101 | 102 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 103 | 104 | ------ 105 | 106 | **综上所述,我们把time函数返回的随时变化的时间戳(从1970年1月1日至今)当作srand函数的参数,这样就可以让rand每次产生的数列都是不同的,随机的** 107 | **切记要在使用rand函数前先调用srand函数哦!** 108 | 109 | ------ 110 | 111 | 它们组合起来怎么写呢?我们可以这么来写: 112 | 113 | ``` 114 | #include 115 | #include 116 | #include 117 | int main() { 118 | srand((unsigned)time(NULL));//关注一下srand函数的参数类型 119 | int i = 0; 120 | int random = 0; 121 | for (; i < 5; i++) { 122 | random = rand(); 123 | printf("%d ", random); 124 | } 125 | } 126 | ``` 127 | 128 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 129 | 130 | ------ 131 | 132 | 想必大家看到了我的程序还有一个让你选择猜数范围的功能,那么它又是怎么实现的呢? 133 | 其实也很简单,核心就是**求余** 134 | 假如你想猜数的范围是1~10 135 | rand函数产生数是随机数,用 `rand() % 10` 得到的范围是 0~9 那就再给它加上1就好了,其他范围都是这个道理 136 | 137 | ------ 138 | 139 | 140 | 141 | [猜数字游戏完整源代码](https://github.com/hairrrrr/project/tree/master/C games/guessing-number) 142 | 143 | -------------------------------------------------------------------------------- /content/c-mordern-approch/01-C语言概论.md: -------------------------------------------------------------------------------- 1 | # C语言概述 2 | 3 | *One man's constant is another man's variable。*[^1] 4 | 5 | 6 | 7 | :arrow_forward: 此符号表示该内容以后的章节会讲解,此章节内不要求理解。 8 | 9 | 10 | 11 | ### 本节内容 12 | 13 | *** 14 | 15 | C语言的历史,C语言的优缺点以及如何高效的使用C语言 16 | 17 | C语言还值得学习吗?C语言查错的工具 18 | 19 | 20 | 21 | ### C语言的历史 22 | *** 23 | #### 起源 24 | 25 | C语言是贝尔实验室的 Ken Thompson, Dennis Ritchie 等人开发的 UNIX 操作系统的“副产品”。 26 | 27 | 与同时代的其他操作系统一样,UNIX 系统最初也是用汇编语言写的。用汇编语言编写的程序往往难以调试和改进,UNIX 操作系统也不例外。Thompson 意识到需要用一种高级的编程语言来完成 UNIX 系统未来的开发,于是他设计了一种小型的 B语言。Thompson 的 B语言是在 BCPL语言(20世纪60年代中期产生的一种系统编程语言)的基础上开发的,而 BCPL语言又可以追溯到最早(且影响深远)的语言之一——Algol 60语言。 28 | 29 | 1970年,贝尔实验室为 UNIX 项目争取到了一台 PDP-11 计算机。当 B语言经过改进并能够在 PDP-11 计算机上成功运行后,Thompson 用 B语言重新编写了部分 UNIX 代码。 30 | 31 | 到了1971年,B语言已经明显不适合 PDP-11 计算机了,于是 Ritchie 着手开发 B语言的升级版。最初他将新开发的语言命名为 NB语言(意味New B),但是后来新语言越来越偏离 B语言,于是他将其改名为 C语言。 32 | 33 | 到1973年,C语言已经足够稳定,可以用来重新编写 UNIX 系统了。 34 | 35 | #### 标准化 36 | 37 | C语言在20世纪七十年代(尤其是1977年到1979)持续发展。这一时期出现了第一本有关 C语言的书。Brian Kernighan 和 Dennis Ritchie 合作编写的 *The C Programming Language* 于1978年出版,并迅速成为 C程序员必读的“圣经”。由于当时没有 C语言的正式标准,这本书就成为了事实上的标准,编程爱好者把它称为“K&R”或者“白皮书”。(公众号后台回复:【KR】即可获得) 38 | 39 | 随着C语言的迅速普及,一系列问题也接踵而至。首先, K&R 对一些语言特性描述得非常模糊,以至于不同编译器对这些特性会做出不同的处理。而且,K&R 也没有对属于 C语言的特性和属于 UNIX 系统的的特性进行明确的区分。更糟糕的是,K&R 出版后 C语言仍然在不断变化,增加了一些新特性并除去了一些旧特性。很快,C语言需要一个全面,准确的最新描述开始成为共识。 40 | 41 | ##### C89/C90 42 | 43 | 1983年,在美国国家标准协会(ANSI)的推动下(ANSI 于此年组建了一个委员会称为 X3Jll),美国开始制定本国的 C语言标准。 44 | 45 | 1988年完成并于1989年12月正式通过的 C语言标准成为 ANSI 标准 X3.159-1989。 46 | 47 | 1990年,国际标准化组织(ISO)通过了此项标准,将其作为 ISO/IEC 9899:1990 国际标准(中国国家标准为 GB/T 15272—1994)。 48 | 49 | 我们把这一C语言版本称为 **C89** 或 **C90**,以区别原始的 C语言版本。 50 | 51 | 委员会在制定的指导原则中的一条写道:保持 C 的精神。委员会在描述这一精神时列出了一下几点: 52 | 53 | - 信任程序员 54 | - 不要妨碍程序员做需要做的事 55 | - 保持语言精炼简单 56 | - 只提供一种方法执行一项操作 57 | - 让程序运行更快,即使不能保持其可移植性 58 | 59 | 在最后一点上,标准委员会的用意是:作为实现,应该针对目标计算机来定义最合适的某特定操作,而不是强加一个抽象,统一的定义。在学习 C语言的过程中,许多方面都反映了这一哲学思想。 60 | 61 | ##### C99 62 | 63 | 1995 年,C语言发生了一些改变。 64 | 65 | 1999年通过的 ISO/IEC 9899:1999 新标准中包含了一些更重要的改变,这一标准所描述的语言通常称为 **C99** 66 | 67 | 此次改变,委员会的用意不是在C语言中添加新的特性,而是为了达到新的目标。 68 | 69 | 1. **支持国际化编程**。如:提供多种方法处理国际字符集 70 | 2. **调整现有实践致力于解决明显的缺陷**。因此,在遇到需要将 C移至64位处理器时,委员会根据现实生活中处理问题的经验来添加标准。 71 | 3. 为**适应科学和工程项目中的关键计算**,提高 C 的适应性,让 C 比 FORTRAN 更有竞争力。 72 | 73 | 其他方面的改变则更为保守,如,尽量让C90,C++兼容,让语言在概念上保持简单。 74 | 75 | 虽然改标准已经发布了很长时间,但**并非所有编译器都完全支持C99**的所有改动。因此,你有可能发现 C99 的一些改动在自己的系统中不可用,或者需要改变编译器的设置才可用。 76 | 77 | ##### C11 78 | 79 | 2011年,**C11**标准问世。 80 | 81 | #### 基于C的语言 82 | 83 | - C++:包含所有C的特性 84 | - Java:基于C++,所以也继承了C的许多特性 85 | - C#:由C++于java发展起来的较新的语言 86 | - Perl:最初是一种简单的脚本语言,在发展过程中采用了C的许多特性 87 | 88 | ### C语言还值得学吗? 89 | 90 | 答案是肯定的。 91 | 92 | 第一,学习C有助于更好的理解C++,Java,C#,Perl以及其他基于C的特性的语言。第一开始就学习其他语言的程序员往往不能很好的掌握继承自C语言的基本特性。 93 | 94 | 第二,目前仍有许多C程序,我们需要读懂并维护这些代码。 95 | 96 | 第三,C语言仍广泛应用于新软件的开发,特别是在内存或处理能力受限的情况下以及需要使用C语言简单特性的地方。 97 | 98 | ### C语言的优缺点 99 | *** 100 | 101 | 与其他任何一种编程语言一样,C语言也有自己的优缺点。这些优缺点都源于该语言的最初用途(编写操作系统和其它系统软件)和它自身的基础理论体系。 102 | 103 | - **C语言是一种底层语言** 为了适应系统编程的需要,C语言提供了对机器级概念(例如,字节和地址)的访问,而这些都是其他编程语言试图隐藏的内容。 104 | - **C语言是一种小型语言** 与许多其他编程语言相比,C语言提供了一套更有限特性集合。(在K&R第二版的参考手册中仅用49页就描述了整个C语言。)为了使特性较少,C语言在很大程度上依赖一个标准函数的“库”。 105 | - **C是一种包容性语言** C假设用户知道自己在干什么,因此它提供了比其他许多语言更广阔的自由度。此外,C语言不像其他语言那样强制进行详细的错误检查。 106 | 107 | #### C语言的优点 108 | 109 | C语言的众多优点解释了C语言为何如此流行。 110 | 111 | - **高效** 高效性是C语言与生俱来的优点之一。发明C语言就是为了编写那些以往由汇编语言编写的程序,所以对C语言来说,能够在有限的内存空间快速运行就显得至关重要。 112 | 113 | - **可移植** 当程序必须在多种机型(从个人计算机到超级计算机)上运行时,常常会用C语言来编写。 114 | 115 | 原因一:C语言没有分裂成不兼容的多种分支。这主要归功于C语言早期与UNIX系统的结合以及后来的ANSI/ISO标准。 116 | 117 | 原因二:C语言编译器规模小且容易编写,这使得它们得以广泛应用。 118 | 119 | 原因三:C语言的自身特性也支持可移植性(尽管它没有阻止程序员编写不可移植的程序)。 120 | 121 | - **功能强大** C语言拥有一个庞大的数据类型和运算符集合,这个集合使得C语言具有强大的表达能力,往往寥寥几行代码就可以实现许多功能。 122 | 123 | - **灵活** C语言最初设计是为了系统编程,但没有固有的约束将其限制在此范围内。C语言现在可以用于编写从嵌入式系统到商业数据处理的各种应用程序。 124 | 125 | - **标准库** C语言的突出优点就是它具有标准库,该标准库包括了数百个可以用于输入/输出,字符串处理,储存分配以及其他实用操作的函数。 126 | 127 | - **与UNIX的集成** C语言在与UNIX系统(包括Linux)结合方面特别强大。事实上,一些UNIX工具甚至假设用户是了解C语言的。 128 | 129 | #### C语言的缺点 130 | 131 | - **C语言容易隐藏错误** C语言的灵活性使得用它编程出错的概率极高。在用其他语言时可以发现的错误,C语言的编译器却无法检查到。更糟糕的是,C语言还包含大量不易察觉的隐患。 132 | - **C程序可能难以理解** C程序的简明扼要与灵活性,可能导致程序员编写出除了自己别人无法读懂的代码。 133 | - **C程序可能难以修改** 如果在设计中没有考虑到维护的问题,那么C编写的大型程序可能很难修改。现代的编程语言通常提供“类”和“包”之类的语言特性,这样的特性可以把大的程序分解成许多更容易管理的模块。遗憾的是,C语言恰恰缺少这样的特性。 134 | 135 | ### 高效的使用C语言 136 | 137 | 要高效的使用C语言,就需要利用C语言优点的同时尽量避免它的缺点,一下给出一些建议。 138 | 139 | - **学习如何规避C语言的缺陷** 140 | - **使用软件工具使程序更可靠**(详细见下文) 141 | - **利用现有的代码库** 使用C语言的一个好处是其他许多人也在使用C。把别人编写好的代码用于自己的程序是一个非常好多主意。C代码通常被打包成库(函数的集合)。获取适当的库既可以大大减少错误,也可以节省很多编程工作。 142 | - **采用一套切合实际的编码规范** 良好的编码习惯和规范易于自己和他人对自己代码的阅读和修改。(公众号回复:【编码规范】,让你学会如何写出规范的代码。) 143 | - **避免“投机取巧”和极度复杂的代码**。C语言鼓励使用编程技巧。但是,过犹不及,不要对技巧毫无节制,最简单的解决方案往往也是最难理解的。 144 | - **紧贴标准** 大多数编译器都提供不属于 C89/C99 标准的特征和库函数。为了程序的可移植性,若非确有必要,最好避免这些特性和库函数。 145 | 146 | ### 怎么让程序更加安全可靠? 147 | 148 | - 分析错误工具——lint 149 | - 越界检查工具——bounds-checker 150 | - 内存泄漏监测工具——leak-finder 151 | - 调高你的编译器的“警告级别” 152 | 153 | 154 | 155 | *** 156 | 157 | 以上就是本次的内容,感谢观看。 158 | 159 | 如果文章有错误欢迎指正和补充,感谢! 160 | 161 | 最后,如果你还有什么问题或者想知道到的,可以在评论区告诉我呦,我在后面的文章可以加上。 162 | 163 | 最后,**关注我**,看更多干货! 164 | 165 | 我是程序圆,我们下次再见。 166 | 167 | 168 | 169 | [^1]:*吾之常量,彼之变量。摘自《epigrams-on-programming》* 170 | 171 | *参考资料:《C Primer Plus》《C语言程序设计:现代方法》* 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /content/c-mordern-approch/02-C语言基本概念.md: -------------------------------------------------------------------------------- 1 | ## C语言基本概念 2 | 3 | *Syntactic sugar causes cancer of the semi-colons.*[^0] 4 | 5 | 6 | 7 | ### :globe_with_meridians:目录 8 | 9 | *** 10 | 11 | [TOC] 12 | 13 | ### :books:教学 14 | 15 | *** 16 | 17 | ### 第一个C程序 18 | 19 | **main.c** 20 | 21 | ```c 22 | #include 23 | 24 | int main(void){ 25 | 26 | printf("Hello,World\n");//a simple C program 27 | 28 | return 0; 29 | } 30 | ``` 31 | 32 | 33 | 34 | 将上述程序写在你的编译器里。 35 | 36 | 然后给文件命名,并以`.c`作为扩展名,例如`main.c`。 37 | 38 | 现在,编译并运行该程序。如果一切正常,输出的应该是: 39 | 40 | ```c 41 | Hello,World 42 | ``` 43 | 44 | 45 | 46 | 恭喜你,你已经是一名C程序员了!:laughing: 47 | 48 | 49 | 50 | *Hello,World 是伟大的。它像着一个呱呱坠地的婴儿对世界的问好,它憧憬着美好的世界,一切事物都是新鲜的。* 51 | 52 | ​ ——不会编程的程序圆 53 | 54 | 现在,让我们看看这个程序到底是什么意思。 55 | 56 | ### 正式开始之前 57 | 58 | #### 编译和链接 59 | 60 | C程序编译链接的过程:(知道即可) 61 | 62 | ![](C:\Users\1\Desktop\素材\23.png) 63 | 64 | #### 集成开发环境 65 | 66 | > 集成开发环境(integrated development enviroment,IDE):集成开发环境是一个软件包,我们可以在其中编辑,编译,链接,执行和调试程序。 67 | 68 | IDE推荐: 69 | 70 | CodeBlock(本教学中的简单的程序会用这个软件来完成) 71 | 72 | VS2019(编写需要调试的程序用它来完成) 73 | 74 | ### 简单程序的一般形式 75 | 76 | #### 1. 指令 77 | 78 | 示例程序第一行`#include`就是一条指令。 79 | 80 | 在程序**编译之前**,C编译器的**预处理器**(preprocessor)会首先对源代码进行一些准备工作,即预处理(preprocessing)。 81 | 82 | > **指令(directive):**我们把 预处理器 执行的命令称为 预处理器指令(preprocessor directive),简称指令。 83 | > 84 | > 指令的结尾不需要添加分号 85 | 86 | `#include`的作用相当于把 **头文件** `stdio.h` 中的所有内容都输入到该行所在的位置。 87 | 88 | 实际上,这是一种**复制+粘贴**的操作。 89 | 90 | **include 文件提供了一种方便的途径共享许多程序共有的信息**。 91 | 92 | `stdio.h`文件中包含了供编译器使用的输入和输出函数(如 `printf()`)信息。 93 | 94 | 该文件名的含义为**标准输入/输出**头文件(stadard input&output .header) 95 | 96 | > **头文件(header):**在C程序顶部的信息集合。 97 | 98 | 每个头文件都包含一些标准库的内容。 99 | 100 | 示例程序引入stdio.h头文件的原因:C语言不同于其他编程语言,它没有内置的“读/写”命令。输入/输出功能由标准库中的函数实现。[^1] 101 | 102 | **每次用到库函数,必须用#include指令包含相关的头文件。**省略必要的头文件可能不会对某一个特定程序造成影响,但是最好不要这样做。 103 | 104 | [^1]:为何不内置输入/输出? 原因之一是并非所有程序都会用到I/O(输入输出)包 。简洁高效表现了C语言的哲学。 105 | 106 | 107 | 108 | #### 2.函数 109 | 110 | `int main(void)` 111 | 112 | > **函数:**类似于其他编程语言的“过程”或“子例程”,它们是用来构建程序的构建块。 113 | 114 | 事实上,C语言就是函数的集合。 115 | 116 | 函数分两大类:第一种是程序员自己编写的函数;另一类则是C作为语言实现的一部分提供的函数,即**库函数**(library function)。因为它们属于一个由编译器提供的函数“库”。 117 | 118 | **main函数**:C程序都是从`main()`函数“开始”执行。`main()`函数是程序的唯一入口。**可以理解为程序是从main函数开始运行到main函数结尾结束。** 119 | 120 | **返回类型**:`int`是main函数的 返回类型。这表明 main函数返回的值是整型。 121 | 122 | *返回给哪里?返回给操作系统,我们后面再来讲解* 123 | 124 | **参数**:`()`内包含的信息为函数的参数。示例中的`void`表示该例中没有传入任何参数。 125 | 126 | > **请注意** 127 | > 128 | > 有背景颜色的地方都是重要的知识,但是在这里不管你是初学者/学了一段时间了,我都建议你遵守以下规范: 129 | > 130 | > **main函数到底应该怎么写?**我在这里不详细展开说。 131 | > 132 | > **正确的形式**:`int main(int argc, char* argv[])` 133 | > 134 | > **可以接受的形式:**`int main(void)` 135 | > 136 | > **错误的写法**:`int main()` 谭老师书中的写法。跟我学,不要用这种写法 137 | > 138 | > **脑瘫的写法**:`void main()` `void main(void)`所有C语言的标准都未认可这种写法,你在赣神魔? 139 | 140 | 141 | 142 | `return 0` 143 | 144 | **返回值**:前面我们讲到了*返回类型*,那么就应该有个返回值。示例中 `return `就代表返回,`0`是这个main函数的返回值。 145 | 146 | > **main函数中return的作用**: 147 | > 148 | > 1.**使main函数终止**。mian函数在这里结束。 149 | > 150 | > 2.main函数返回值是0,**表示程序正常终止**。 151 | 152 | **所以,`return 0`在main函数中是不可省略的** 153 | 154 | 虽然即使你不写,可能也可以通过编译,但是不写是不对的。 155 | 156 | 157 | 158 | #### 3.语句 159 | 160 | > 语句是程序运行时执行的命令 161 | > 162 | > 语句是带顺序执行的 C 程序段。任何函数体都是一条复合语句,继而为语句或声明的序列 163 | 164 | 例如: 165 | 166 | ```c 167 | int main(void) 168 | { // 复合语句的开始 169 | int n = 1; // 声明(非语句) 170 | n = n+1; // 表达式语句 171 | printf("n = %d\n", n); // 表达式语句 172 | return 0; // 返回语句 173 | } // 复合语句之结尾,函数体之结尾 174 | ``` 175 | 176 | 177 | 178 | **C语言中的六种语句** 179 | 180 | 1. **标号语句** 181 | 182 | 1) [goto](https://zh.cppreference.com/w/c/language/goto) 语句的目标。 (*标识符* **:** *语句*) 183 | 184 | 2) [switch](https://zh.cppreference.com/w/c/language/switch) 语句的 `case` 标号。(**case** *常量表达式* **:** *语句*) 185 | 186 | 3) [switch](https://zh.cppreference.com/w/c/language/switch) 语句的默认标号。 (**default** **:** *语句*) 187 | 188 | 2. **复合语句** 189 | 190 | 复合语句,或称**块**,是**花括号**所包围的语句与声明的序列。 191 | 192 | `{声明(可选)| 语句 }` 193 | 194 | 3. **表达式语句** 195 | 196 | 典型的 C 程序中大多数语句是表达式语句,例如赋值或函数调用。 197 | 198 | 无表达式的表达式语句被称作*空语句*。它通常用于提供空循环体给 [for](https://zh.cppreference.com/w/c/language/for) 或 [while](https://zh.cppreference.com/w/c/language/while) 循环。 199 | 200 | 4. **选择语句** 201 | 202 | 选择语句根据表达式的值,选择数条语句之一执行。 203 | 204 | 1) [if](https://zh.cppreference.com/w/c/language/if) 语句 205 | 206 | 2) [if](https://zh.cppreference.com/w/c/language/if) 语句带 `else` 子句 207 | 208 | 3) [switch](https://zh.cppreference.com/w/c/language/switch) 语句 209 | 210 | 5. **迭代语句** 211 | 212 | 迭代语句重复执行一条语句。 213 | 214 | 1) [while](https://zh.cppreference.com/w/c/language/while) 循环 215 | 216 | 2) [do-while](https://zh.cppreference.com/w/c/language/do) 循环 217 | 218 | 3) [for](https://zh.cppreference.com/w/c/language/for) 循环 219 | 220 | 6. **跳转语句** 221 | 222 | 跳转语句无条件地转移控制流。 223 | 224 | 1) [break](https://zh.cppreference.com/w/c/language/break) 语句 225 | 226 | 2) [continue](https://zh.cppreference.com/w/c/language/continue) 语句 227 | 228 | 3) [return](https://zh.cppreference.com/w/c/language/return) 语句带可选的表达式 229 | 230 | 4) [goto](https://zh.cppreference.com/w/c/language/goto) 语句 231 | 232 | 233 | 234 | **为什么需要分号?** 235 | 236 | 由于语句可以连续占用多行,有时很难确定它结束的位置,因此需要用分号来向编译器表示语句结束的位置。但预处理指令通常只用占一行,因此**不需要**分号结尾 237 | 238 | 239 | 240 | #### 4.打印字符串 printf() 函数 241 | 242 | `printf("Hello,World\n");` 243 | 244 | `printf()`是一个功能十分强大的函数。*后面我们会进一步介绍* 245 | 246 | 示例中我们只是用printf函数打印了出了一条**字符串字面量(string literal)** —— 用一对双引号引起来的一系列字符。 247 | 248 | **字符串**,顾名思义就是一串字符。 249 | 250 | printf函数不会自动换行到下一行打印,它只会在它最开始那一行一直打印直到程序迫使它换行。 251 | 252 | `\n`表示printf函数打印完成后跳转到下一行 253 | 254 | 255 | 256 | 请看如下程序,思考它的效果与示例中有何不同? 257 | 258 | ```c 259 | printf("Hello,"); 260 | printf("World\n"); 261 | ``` 262 | 263 | 答案[^2](点击或到文章尾查看) 264 | 265 | 266 | 267 | 如果想输出下面的结果,请考虑一下,应该如何写程序呢? 268 | 269 | ```c 270 | Hello, 271 | World 272 | ``` 273 | 274 | 答案: 275 | 276 | ```c 277 | printf("Hello,\n"); 278 | printf("World\n"); 279 | ``` 280 | 281 | 对于这个问题,第二个printf函数的 \n 可以省略。简单来说,printf函数会在 \n 出现的地方换行。 282 | 283 | 284 | 285 | #### 5.注释 286 | 287 | `//a simple C program` 288 | 289 | > 写注释可以让自己和别人更容易明白你写的程序。 290 | > 291 | > C语言注释的好处是:可以写在任何地方。注释的部分会被编译器忽略。 292 | 293 | 我们试想一件事你昨天吃了什么饭,记性好是吧?上周五吃的什么饭?如果连上周 一天三顿的饭都不能记住,何况你自己查看你很久之前写的代码呢? 294 | 295 | ##### 两种注释符号 296 | 297 | 第一种:`/* */` 298 | 299 | 单行注释 300 | 301 | ``` c 302 | /* 关注微信公众号:不会编程的程序圆 */ 303 | /* 看更多干货,获取第一时间更新 */ 304 | /* 码字不易,对你有帮助 点赞/转发/关注,鼓励一下作者 */ 305 | ``` 306 | 307 | 多行注释 308 | 309 | ```c 310 | /* 关注微信公众号:不会编程的程序圆 311 | 看更多干货,获取第一时间更新 312 | 码字不易,对你有帮助 点赞/转发/关注,鼓励一下作者 */ 313 | ``` 314 | 315 | 但是,上面这一种注释方式可能难以阅读,因为人不不容易发现注释结束的位置。 316 | 317 | 改进: 318 | 319 | ```c 320 | /*关注微信公众号:不会编程的程序圆 321 | 看更多干货,获取第一时间更新 322 | 码字不易,对你有帮助 点赞/转发/关注,鼓励一下作者 323 | */ 324 | ``` 325 | 326 | 更好的方法:将注释部分围起来 327 | 328 | ```c 329 | /************************************************* 330 | * 关注微信公众号:不会编程的程序圆 * 331 | * 看更多干货,获取第一时间更新 * 332 | * 码字不易,对你有帮助 点赞/转发/关注,鼓励一下作者 * 333 | *************************************************/ 334 | ``` 335 | 336 | 当然如果你嫌麻烦,也可以简化一下: 337 | 338 | ```c 339 | /* 340 | * 关注微信公众号:不会编程的程序圆 341 | * 看更多干货,获取第一时间更新 342 | * 码字不易,对你有帮助 点赞/转发/关注,鼓励一下作者 343 | */ 344 | ``` 345 | 346 | 简短的注释可以放在同一行 347 | 348 | ```c 349 | printf("Hello World\n");/* 不会编程的程序圆 */ 350 | ``` 351 | 352 | 353 | 354 | 但是,如果你忘记了终止注释可能会导致你的编译器跳过程序的一部分,请思考下列: 355 | 356 | ```c 357 | printf("不会"); /* 关注我的公众号呦~ 358 | printf("编程"); 359 | printf("的"); /* 更多鼓励,更多干货!*/ 360 | printf("程序圆"); 361 | ``` 362 | 363 | 你可以在自己的编译器上自己敲一下,看看会输出什么。 364 | 365 | 由于第一条注释忘记输入结束标志,导致编译器将直到找到结束标志之前的程序都当成了注释! 366 | 367 | 368 | 369 | 第二种:`//` 370 | 371 | C99提供的新的注释方式。 372 | 373 | ```C 374 | //关注微信公众号:不会编程的程序圆 375 | //看更多干货,获取第一时间更新 376 | //码字不易,对你有帮助 点赞/转发/关注,鼓励一下作者 377 | ``` 378 | 379 | > 新的注释风格有两大优点: 380 | > 381 | > 1. 这种注释会在行末自动终结,所以不用担心会出现未终止的注释意外吞噬部分程序的情况 382 | > 2. 每行前都有 // ,所以多行的注释更加醒目 383 | 384 | 综上所述,建议采用 `//` 这种注释方式 385 | 386 | 387 | 388 | [^0]:*语法糖导致分号癌。摘自《epigrams-on-programming》* 389 | [^2]:相同。 390 | 391 | *参考资料:《C Primer Plus》《C语言程序设计:现代方法》 网上资料:cppreference.com* 392 | 393 | *** 394 | 395 | -------------------------------------------------------------------------------- /content/c-mordern-approch/03-C语言基本概念.md: -------------------------------------------------------------------------------- 1 | ## C语言基本结构(下) 2 | 3 | *Every program is a part of some other program and rarely fits.*[^0] 4 | 5 | 6 | 7 | ### :globe_with_meridians:目录 8 | 9 | *** 10 | 11 | [TOC] 12 | 13 | 14 | 15 | ### :apple:简单的程序结构 16 | 17 | *** 18 | 19 | 下面是一个简单的程序,身高是给出的,体重是在程序中得到的,我们输出的是体重与身高/体重的值。 20 | 21 | 这里我们更注重的是**程序的结构**而非程序本身。 22 | 23 | 24 | 25 | 示例 26 | 27 | ![](https://hairrrrr.github.io/assets/2020-11-30-1.png) 28 | 29 | 30 | 31 | #### 1. 类型 32 | 33 | > 每一个变量都有**类型**(type)。类型用来描述变量的数据的种类,也称**数据类型**。 34 | 35 | 数值型变量的类型决定了变量所能存储的最大值与最小值,以及是否允许小数点后出现数字。 36 | 37 | 示例中只有一种数据类型:`int` 38 | 39 | > **int**(integer):即整型,表示整数。 40 | 41 | 数据类型还有很多,目前除了 int 以外,我们只再使用另一种: 42 | 43 | > **float**(floating-point): 浮点型,可以表示小数 44 | 45 | **注意**:虽然 float 型可以带小数,但是进行**算术运算**时,float 型要比 int 型慢,而且 float 通常只是一个值的近似值。(比如在一个float 型变量中存储 0.1, 但其实可能这个变量的值为 0.09999987,这是舍入造成的误差) 46 | 47 | *题外话:我当时学的时候,就没有人告诉我这些知识,你们如果现在是初学,我都感觉到羡慕,你们要少走多少弯路啊!* 48 | 49 | 50 | 51 | #### 2. 关键字 52 | 53 | > int 与float 都是C语言的**关键字**(keyword),关键字是语言定义的单词,**不能用做其他用途**。比如不能用作命名函数名与变量名。 54 | 55 | 关键字:*斜体代表C99新增关键字* 56 | 57 | | auto | enum | unsigned | break | extern | 58 | | ---------- | -------- | -------- | ---------- | ------------ | 59 | | return | void | case | float | short | 60 | | volatile | char | for | signed | while | 61 | | const | goto | sizeof | continue | if | 62 | | static | default | struct | do | int | 63 | | switch | double | long | typedef | else | 64 | | register | union | | | | 65 | | *restrict* | *inline* | *_Bool* | *_Complex* | *_Imaginary* | 66 | 67 | 如果关键字使用不当(关键字作为变量名),编译器会将其视为语法错误。 68 | 69 | 70 | 71 | > 保留标识符(reserved identifier):下划线开头的标识符和标准库函数名(如:printf()) 72 | 73 | C语言已经指定了这些标识符的用途或保留了它们的使用权,如果你使用它们作为变量名,即使没有语法错误,也不能随便使用。 74 | 75 | #### 3. 声明 76 | 77 | > **声明**(declaration):在使用变量(variable)之前,必须对其进行声明(为编译器所作的描述)。 78 | > 79 | > 声明的方式为:数据类型 + 变量名(程序员自己决定变量名,命名规则后面会讲) 80 | 81 | 示例中的 `int weight`完成了两件事情。第一,函数中有个变量名为 weight。第二,int 表明这个变量是整型。 82 | 83 | 编译器用这些信息为变量 weight 在内存中分配空间。 84 | 85 | 86 | 87 | **C99** 前,如果有声明,声明一定要在语句之前。(就像示例那样,函数体中第一块是声明,第二块才是语句。) 88 | 89 | C99 和 C11 遵循 C++ 的惯例,可以把声明放在任何位置。即可以使用时再声明变量。以后C程序中这种做法可能会很流行。**但是目前不建议这样。** 90 | 91 | 92 | 93 | 就**书写格式**而言,我建议将声明全部放在**函数体头部**,声明与语句之间**空出一行**。 94 | 95 | 96 | 97 | #### 4. 命名 98 | 99 | > weight,height 都是**标识符**,也就是一个变量,函数或其他实体的名称。因此,声明将特定标识符与计算机内存的特定位置联系起来,同时也就确定了存储在某位置的信息类型或数据类型。 100 | 101 | 102 | 103 | 给变量命名时要使用有意义的变量名或标识符。如果变量名无法清楚的表达自身的用途,可以在注释中进一步说明,这是一种良好的编程习惯与编程技巧。 104 | 105 | C99 与 C11 允许使用更长的标识符,但是编译器只识别前 63个字符。*对于外部标识符,只允许 31 个字符*。事实上,你可以使用更长的字符,但是编译器可能忽略超出的部分。(比如有两个标识符都是 64 个字符,但只有最后一个字符不同。编译器可能会视其为同一个名字,也可能不会。标准并未定义在这种情况下会发生什么。) 106 | 107 | 108 | 109 | > 命名规则:可以用小写字母,大写字母,数字和下划线(_)来命名。**名称的第一个字符必须是字符或下划线,不能是数字** 110 | 111 | **操作系统和C库经常使用一个下划线或两个下划线开始的标识符(如:_kcab),因此最好避免在自己的程序中使用这种名称。(避免与操作系统和c库的标识符重复)** 112 | 113 | C语言的名称区分大小写。即:star,Star,STAR 是不同的。 114 | 115 | 116 | 117 | **声明变量的理由**: 118 | 119 | 1. 把所有变量放在一处,方便读者查找和理解程序的用途。 120 | 2. 声明变量可以促使你在编写程序之前做好计划(比如你的程序要用什么变量,你可以提前规划)。 121 | 3. 声明变量有助于发现程序中的小错误,如拼写错误。 122 | 4. **不提前声明变量,C程序编译将无法通过** 123 | 124 | 125 | 126 | #### 5. 赋值 127 | 128 | > 赋值(assignment):变量通过赋值的方式获得值。 129 | 130 | 示例中,`weight = 160; `是一个 **赋值表达式语句**。意思是“把值 160 赋给 变量 weight”。 131 | 132 | 在执行 `int weight;`时,编译器在计算机内存中为变量 weight 预留的空间,然后在执行这行代码时,把值存储在之前预留的位置。可以给 weight 赋不同的值,这就是 weight 之所以被称为变量的原因。 133 | 134 | **注意:** 135 | 136 | - 该行表达式将值从右侧赋到左侧。 137 | 138 | - 该语句以分号结尾。 139 | - `=` 在计算机中不是相等的意思,而是赋值。我们在读 `weight = 160; `时,我们应该这么读:“将 160 赋给 weight” 140 | - `==`表示相等 141 | 142 | 143 | 144 | #### 6. printf() 函数 145 | 146 | `printf(“我的体重是:%d斤\n,身高与体重的比为:%d”, weight, height / weight);` 147 | 148 | 这是我们示例中的 printf 函数,我们来看两个不那么复杂的: 149 | 150 | ```c 151 | main(void); 152 | printf("Hi"); 153 | ``` 154 | 155 | 156 | 157 | 首先,printf() 的 **圆括号**是不是很像 main() ?这表示 printf 是一个函数名,它也是一个函数。圆括号内的内容是从 main() 函数传递给 printf() 函数的信息。该信息被称为**参数**,更确切的说,是**实际参数**(actual argument),简称**实参**。 158 | 159 | 既然是函数,它其实也是像我们看到的 main函数一样,也有函数头和函数体。 160 | 161 | printf() 函数是一个库函数,库函数我们上一节讲函数种类时说过,这是一种不需要程序员去写的,只需要引用头文件 `stdio.h`就可以直接使用的。但是我们应该知道这一点,详细情况我们后面会说讲。 162 | 163 | **当程序运行到 printf() 函数这一行时,控制权被转给了printf()函数。函数执行结束后,控制权被返回至主调函数(calling function),该例中是 main()** 。 164 | 165 | 166 | 167 | printf() 函数的作用是向我们的显示器输出内容。 168 | 169 | 此例中,printf() 函数的括号内 分为两部分,一部分在双引号内,另一部分在双引号外,它们中间以逗号隔开。双引号外有两个参数 weight 和 height / weight ,他们分别是变量和**表达式**(含有常量,变量和运算符的式子),也是指定要被打印的参数(打印到你的屏幕上)。 170 | 171 | 我们发现,最终我们屏幕上看到的是引号内的内容。我们可以来看一下输出的内容: 172 | 173 | ```c 174 | 我的体重是:160斤 175 | 身高与体重的比为:1 176 | ``` 177 | 178 | 179 | 180 | 我们发现:首先引号内的 `%d` 和`\n`并没有被输出,`%d`的位置被替换成了一个整数。为什么会这样呢? 181 | 182 | > `\n`代表**一个换行符(newline character)**。对于 printf 函数来说,它的意思是:“**在下一行的最左边开始新的一行**”。 183 | > 184 | > 也就是说换行符和在键盘上按下 Enter按键相同。既然如此,为何不在键入 printf() 参数时直接使用 Enter键呢?因为编辑器可能认为这是直接的命令,而不是存储在源代码中的指令。换句话说,如果直接按下 Enter键,编辑器会退出当前行并开始新的一行。但是,换行符会影响程序输出的(显示)格式。 185 | 186 | 换行符是一个**转义序列**(escape sequence)。转义序列用于难以表示或无法输入的字符。如,`\t`代表 Tab键,即制表符。`\b`代表 Backspace键,即退格键。我们在后面会继续讨论。 187 | 188 | 这样就解释了为何一行的printf() 函数会输出两行。 189 | 190 | 191 | 192 | *以下这部分不能理解可以只看结论,能理解更好。* 193 | 194 | 在解释 %d 之前我们先来看一下,weight 和 height / weight 所代表的值。 195 | 196 | weight 是被赋值为 160 的,所以 weight 的值就是 160 197 | 198 | C语言中,`/`表示除法, `*` 表示乘法。 199 | 200 | 那么 height / weight 的值是多少呢?我们现在不知道这个表达式的值是多少,但是我们知道这个它肯定代表 180 / 160 201 | 202 | 而最终输出的值是 1 ,这和我们想的不一样,我们知道结果应该是个小数,那么这是为什么呢? 203 | 204 | 我想可能的原因有两个: 205 | 206 | 1. %d 将小数转换为整数 207 | 2. 180 / 160 本身在C语言中的值就是整数 208 | 209 | 我们来测试一下: 210 | 211 | ```c 212 | int main(void) { 213 | 214 | int a = 3; 215 | int b = 2; 216 | float c = 1.1f;//f 表示1.1是浮点数 217 | 218 | printf("%d\n", c);//%d 用来输出整型 219 | printf("%f\n", a / b);//%f 用来输出浮点型 220 | 221 | return 0; 222 | } 223 | ``` 224 | 225 | 输出: 226 | 227 | ```c 228 | -1717986918 229 | 0.000000 230 | ``` 231 | 232 | 输出并不是我们想要的内容,我们来看一下编译器的警告: 233 | 234 | 编译器警告: 235 | 236 | ```c 237 | “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“double” 238 | “printf”: 格式字符串“%f”需要类型“double”的参数,但可变参数 1 拥有了类型“int” 239 | ``` 240 | 241 | 可以不去理解报错的内容。输出与报错至少说明了一点: 242 | 243 | **%d 在我的编译器上无法输出浮点型;整型 / 整型 不是浮点型。** 244 | 245 | 那就说明了原因2是对的,即:**180 / 160 的值就是 1** 246 | 247 | 248 | 249 | 为什么 `180 / 160 == 1 `(180 / 160 的值是 1)呢? 250 | 251 | 因为 weight 和 height 都整数,它们相除结果取整数(向下取整)。 252 | 253 | 254 | 255 | 如何输出 float 类型的浮点数? 256 | 257 | ```c 258 | printf("%f", 2.0f); 259 | ``` 260 | 261 | 262 | 263 | >`%d`是一个占位符,其作用是指明 num 值的位置。d 代表 以十进制的格式。 264 | 265 | 266 | 267 | 还有一点要注意的是,在示例中,第二个输出的整数的参数(height / weight )是一个表达式,我们也可以在程序中添加一个新的变量,然后用这个变量代替上面的表达式作为 printf() 的参数。如: 268 | 269 | ```c 270 | int main(void) 271 | { 272 | int height = 180; 273 | int weight, scale;//scale:比例 274 | weight = 160; 275 | scale = height / weight; 276 | printf(“我的体重是:%d斤\n,身高与体重的比为:%d”, weight, scale); 277 | return 0; 278 | } 279 | ``` 280 | 281 | 282 | 283 | 合理的使用表达式作为函数的参数可以简化程序。 284 | 285 | 也说明**在任何需要数值的地方,都可以使用具有相同类型的表达式**。 286 | 287 | 288 | 289 | #### 7. 初始化 290 | 291 | 当程序开始执行时,某些变量会被自动设置为0,而大多数不会。没有默认值并且尚未在程序中被赋值的变量时未初始化的(uninitialized)。 292 | 293 | 如果试图访问未初始化的变量,可能会得到不可预知的值。在某些编译器中,可能会发生更坏的情况(甚至程序崩溃)。 294 | 295 | 296 | 297 | 我们可以用赋值的办法给变量赋初值,但还有更简洁的做法:在变量声明中加入初始值。 298 | 299 | 例如示例中的 `int height = 180`数值 180 就是一个**初始化式**(initializer)。 300 | 301 | 302 | 303 | 同一个声明中可以对任意数量的变量进行初始化。如: 304 | 305 | ```c 306 | int a = 10, b = 15, c = 20; 307 | ``` 308 | 309 | 上述每个变量都拥有属于自己的初始化式。接下来的例子,只有 c 有初始化式,a,b没有。 310 | 311 | ```c 312 | int a, b, c = 20; 313 | ``` 314 | 315 | 316 | 317 | *** 318 | 319 | [^0]:每个程序都是其他程序不合适的一部分。 320 | 321 | *参考资料:《C Primer Plus》《C语言程序设计:现代方法》* 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /content/c-mordern-approch/06-表达式.md: -------------------------------------------------------------------------------- 1 | # 表达式 2 | 3 | *Symmetry is a complexity reducing concept (co-routines include sub-routines); seek it everywhere.* [^1] 4 | 5 | 6 | 7 | ## 目录 8 | 9 | *** 10 | 11 | [TOC] 12 | 13 | 14 | 15 | ### 一 算术运算符 16 | 17 | #### 1.概念 18 | 19 | | 一元运算符(只需要 1 个操作数) | 20 | | ------------------------------- | 21 | | + 一元正号运算符 | 22 | | - 一元负号运算符 | 23 | 24 | ​ 25 | 26 | **二元运算符** 27 | 28 | | 加法类 | 乘法类 | 29 | | ---------------- | --------------- | 30 | | + 加法运算符 | * 乘法运算符 | 31 | | - 减法运算符 | / 除法运算符 | 32 | | | % 求余运算符 | 33 | 34 | 35 | 36 | **注意:** 37 | 38 | - int 型与 float 型混合在一起时,运算结果是 float 型。 39 | 40 | 比如,9 + 2.5f 的值为 11.5;6.7f / 2 的值为 3.35。 41 | 42 | - 运算符 `/`:当两个操作数都是整型时,结果会**向下取整**。如,1 / 2 的值是 0,而不是 0.5 。 43 | 44 | - 运算符 `%`要求两个操作数都是**整型**。 45 | 46 | - 把 0 作为 `/` 或 `%` 的右操作数会导致未定义行为。 47 | 48 | - 当运算符 `/` 和 `%` 用于负操作数时,其结果难以确定。 49 | 50 | 根据 C89 的标准,如果两个操作数中有一个是负数,那么除法结果**既可以向上取整也可以向下取整**(例如,-9 / 7 的结果既可以是 -1 也可以是 -2);i % j 的符号与具体实现有关(例如,-9 % 7 可以是 -2 也可以是 5)。 51 | 52 | 在 C99 中,除法的结果总是**向零取整**(因此,-9 / 7 的结果是 -1);i % j 的符号与 i 相同(因此,-9 % 7 的结果是 -2;我特意测试了以下,9 % -7 的值是 2,-9 % -7 的值还是 2)。 53 | 54 | 55 | 56 | > **“由实现定义”**的行为: 57 | > 58 | > 术语由实现定义(implementation-defined)指的是 C标准对 C语言的部分内容未加指定,并认为其细节可有“实现”来具体定义。所谓实现是指程序在特定平台上编译,链接和执行所需要的软件。因此,根据实现的不同,程序的行为可能稍微有差异。 59 | > 60 | > 这样做的可能很奇怪甚至危险。但是这正是 C语言的目标之一——高效,这常常意味着与硬件相匹配。 61 | > 62 | > 对于我们来说,我们要**尽量避免编写这种由实现定义的行为的程序**。如果不能做到,起码要仔细查阅手册。 63 | 64 | 65 | 66 | #### 2. 运算符的优先级和结合性 67 | 68 | 当表达式包含多个运算符时,其含义可能不是一目了然的。我们的解决方法是: 69 | 70 | - 用括号进行分组 71 | - 了解运算符的优先级和结合性 72 | 73 | ##### 运算符优先级 74 | 75 | (operator precedence) 76 | 77 | | 最高优先级 | + | - | (一元运算符) | 78 | | ---------- | ---- | ---- | -------------- | 79 | | | * | / | % | 80 | | 最低优先级 | + | - | (二元运算符) | 81 | 82 | **例 1-1:** 83 | 84 | ```c 85 | i + j * k 等价于 i + (j * k) 86 | -i + -j 等价于 (-i) + (-j) 87 | ``` 88 | 89 |
90 | 91 | ##### 运算符的结合性 92 | 93 | 当表达式包含两个或更多相同优先级的运算符时,仅有运算符优先级规则是不够的。这种情况下,运算符的**结合性**(associativity)开始发挥作用。 94 | 95 | > 如果运算符是从左向右开始结合的,那么称这种运算符是左结合的。 96 | 97 | 二元运算符即:`*,/,%,+,-`都是左结合的。所以: 98 | 99 | **例 1-2:** 100 | 101 | ```c 102 | i - j - k 等价于 (i - j) - k 103 | ``` 104 | 105 | 106 | 107 | 运算符是右结合的,如一元运算符:`+,-`。 108 | 109 | **例 1-3:** 110 | 111 | ```c 112 | - + i 等价于 -(+i) 113 | ``` 114 | 115 | 116 | 117 | #### 3.总结 118 | 119 | >在许多语言(特别是 C 语言)中,优先级和结合性规则都是十分重要的。然而 C 语言的运算符太多了(差不多 50 种)。**为了自己和他人理解代码的方便,请最好加上足够多的圆括号。** 120 | 121 | 122 | 123 | ### 二 赋值运算符 124 | 125 | > 求出表达式的值后往往需要将其存储在变量中,以便将来使用。C语言的 = (简单赋值 simple assignment)运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)。 126 | 127 | 128 | 129 | #### 1. 简单赋值 130 | 131 | 表达式 `v = e`的赋值效果是求出表达式 e 的值,然后将此值赋值给 v。 132 | 133 | **例 2-1:** 134 | 135 | ```c 136 | i = 5;// i is now 5 137 | j = i;// j is now 5 138 | k = 10 * i + j;// k is now 55 139 | ``` 140 | 141 | 如果 v 与 e 的类型不同,那么赋值运算发生时会将 e 的值转化为 v 的类型: 142 | 143 | **例 2-2:** 144 | 145 | ```c 146 | int i; 147 | double j; 148 | i = 72.99f;// i is now 72 149 | f = 136;// f is now 136.0 150 | ``` 151 | 152 | 153 | 154 | 在很多编程语言中,赋值是**语句**;然而在 C语言中,赋值就像 + 那样是**运算符**。 155 | 156 | 既然赋值是运算符,那么多个赋值语句可以串联在一起: 157 | 158 | **例 2-3:** 159 | 160 | ```c 161 | i = j = k = m = 0; 162 | ``` 163 | 164 | **运算符 = 是右结合的**,所以,上面的语句等价于: 165 | 166 | ```c 167 | i = (j = (k = (m = 0))); 168 | ``` 169 | 170 | 作用是先将 0 赋值给 m,再将 m 赋值给 k,再将 k 赋值给 j,再将 j 赋值给 i 。 171 | 172 | ​ 173 | 174 |
175 | 176 | ###### ! 注意 177 | 178 | 因为赋值运算符存在**类型转换**(本节后面会讲),串在一起赋值运算的结果可能不是预期的结果: 179 | 180 | ```c 181 | int i; 182 | float j; 183 | 184 | j = i = 33.3f; 185 | //先将 33 赋值给 i,然后将 33.0 赋值给 j 186 | ``` 187 | 188 | #### 2. 左值 189 | 190 | 赋值运算要求它的左操作数必须是**左值**(lvalue)。左值表示在计算机中的存储对象,而不是常量或计算的结果。**左值是变量。** 191 | 192 | **例 2-4:** 193 | 194 | ```c 195 | 12 = i; 196 | i + j = 0; 197 | -i = j; 198 | ``` 199 | 200 | 以上三种表达式都是错误的。 201 | 202 | #### 3. 复合赋值 203 | 204 | ```c 205 | i = i + 2; 206 | //等同于 207 | i += 2; 208 | ``` 209 | 210 | 上面的例子中 += 就是一种符合运算符,表示:将自身表示的数增加 2 后再赋值给自己。 211 | 212 |
213 | 214 | 与加法相似,所有赋值运算符的工作原理大体相同。 215 | 216 | > `+=` 217 | > 218 | > `-=` 219 | > 220 | > `*=` 221 | > 222 | > `/=` 223 | > 224 | > `%=` 225 | 226 | **注意:** 227 | 228 | 1. `i *= j + k` 和 `i = i * j + k` 是不一样的。 229 | 230 | 2. 使用复合赋值运算符时,注意不要交换组成运算符的两个字符的位置。如: 231 | 232 | `i += j`写成了`i =+ j` 后者等价于:`i = (+j)` 233 | 234 | 复合运算符有着和 `=`运算符一样的特性。它们也是右结合的,所以: 235 | 236 | `i += j += k`等价于`i += (j += k)` 237 | 238 | 239 | 240 | #### 4. 自增运算符和自减运算符 241 | 242 | > `++` 243 | > 244 | > `--` 245 | 246 | “自增”(加1)和“自减”(减1)也可以通过下面的方式完成: 247 | 248 | ```c 249 | i = i + 1; 250 | j = j - 1; 251 | ``` 252 | 253 | 复合赋值运算符可以简化上面的语句: 254 | 255 | ```c 256 | i += 1; 257 | j -= 1; 258 | ``` 259 | 260 | 而 C语言 允许用 ++ 和 -- 运算符将这些语句缩的更短。比如: 261 | 262 | ```c 263 | i++; 264 | j--; 265 | ``` 266 | 267 | 或者: 268 | 269 | ```c 270 | ++i; 271 | --j; 272 | ``` 273 | 274 | 这两种形式的写法的意义不同的: 275 | 276 | - `++i` (前缀(prefix)自增),意味着“立即自增 i ” 277 | 278 | ```c 279 | int i = 1; 280 | printf("%d\n", ++i); 281 | printf("%d\n", i); 282 | //输出 283 | 2 284 | 2 285 | ``` 286 | 287 | - `i++`(后缀(postfix)自增),意味着“先使用 i 的原始值,稍后再自增”。稍后是多久?C语言标准没有给出精确的时间,但是可以放心的假设 i 再下一条语句执行之前进行自增。 288 | 289 | ```c 290 | int i = 1; 291 | printf("%d\n", i++); 292 | printf("%d\n", i); 293 | //输出 294 | 1 295 | 2 296 | ``` 297 | 298 | `--`运算符具有相同的特性。 299 | 300 | 301 | 302 | > 后缀的 ++ 和 -- 比一元的正号,负号优先级高,而且都是左结合的。 303 | > 304 | > 前缀的 ++ 和 -- 与一元的正号,负号优先级相同,并且是右结合的。 305 | 306 | 比如: 307 | 308 | ```c 309 | int main(void) { 310 | 311 | int i = 1; 312 | 313 | printf("%d", -i++); 314 | printf("%d", i); 315 | } 316 | //输出: 317 | -1 318 | 2 319 | ``` 320 | 321 | 322 | 323 | #### 5.表达式求值 324 | 325 | **部分C语言运算符表** 326 | 327 | | 优先级 | 类型名称 | 符号 | 结合性 | 328 | | ------ | ------------ | ---------------- | ------ | 329 | | 1 | (后缀)自增 | ++ | 左结合 | 330 | | | (后缀)自减 | -- | | 331 | | 2 | (前缀)自增 | ++ | 右结合 | 332 | | | (前缀)自减 | -- | | 333 | | | 一元正号 | + | | 334 | | | 一元符号 | - | | 335 | | 3 | 乘法类 | `* / %` | 左结合 | 336 | | 4 | 加法类 | `+ -` | 左结合 | 337 | | 5 | 赋值 | `= *= /= -= +=` | 右结合 | 338 | 339 | 能理解下面这个表达式的意义,就算掌握了这一部分的表达式求值规则: 340 | 341 | ```c 342 | a = b += c++ - d + --e / -f 343 | ``` 344 | 345 | 等价于: 346 | 347 | ```c 348 | a = ( b += ( (c++) - d + (--e) / (-f) ) ) 349 | ``` 350 | 351 | ##### 子表达式的求值顺序 352 | 353 | C语言没有定义子表达式的求值顺序(除了含有 逻辑与,逻辑或 或 逗号运算符的表达式(后面会讲))。 354 | 355 | 但是不管子表达式的计算顺序如何,大多数表达式都有相同的值。但是,当子表达式改变了某个操作数的值时,产生的值就可能不一致了。思考下面的例子: 356 | 357 | ```c 358 | a = 5; 359 | c = (b = a + 2) + (a = 1); 360 | ``` 361 | 362 | 第二条语句的执行结果是未定义的。对大多数编译器而言,c 的值是 6 或者 2。取决于 子表达式 b = a + 2 和 a = 1 的求值顺序。 363 | 364 | 像上例那样,**在表达式中,既在某处访问变量的值,又在别处修改它的值是不可取的。** 365 | 366 | 为了避免出现此类情况,我们可以将子表达式分离: 367 | 368 | ```c 369 | a = 5; 370 | b = a + 2; 371 | a = 1; 372 | c = b - a; 373 | ``` 374 | 375 | 执行完这些语句后,c 的值将始终是 6 376 | 377 | 除此之外,自增自减运算符也要小心使用。如下例: 378 | 379 | ```c 380 | i = 2; 381 | j = i * i++; 382 | ``` 383 | 384 | j 有两种可能:4 或 6 385 | 386 | 我们很自然的认为结果是 4 。但是其实该语句的执行结果是未定义的。 387 | 388 | j 的值为 6 的情况: 389 | 390 | 1. 取出第二个操作数(i 的原始值),然后 i 自增 391 | 2. 取出第一个操作数(i 的新值) 392 | 3. 将取除的两个操作数相乘(2 和 3),结果是 6 393 | 394 | “取出”变量意味着从内存中获取它们的值。变量后续变化不会影响已经取出的值,因为取出的值通常存储在 CPU 中称为**寄存器**的一个特殊位置。 395 | 396 | ##### 未定义行为 397 | 398 | > 未定义行为(undefined behavior): 类似上面两个例子中的语句会导致 未定义行为,这和我们前面讲的**由实现定义**的行为是不同的。当程序中出现未定义行为时,后果是不可预料的。不同的编译器给出的结果可能是不同的。也就是说,程序可能无法通过编译,也可能运行时崩溃,不稳定或者产生无意义的结果。**换句话说,我们应该像躲避“新冠”一样避免未定义行为**。 399 | 400 | 401 | 402 | *参考资料:《C Primer Plus》《C语言程序设计:现代方法》* 403 | 404 | [^1]: 对称性有助于减少复杂度(协程包含例程)。对称性无处不在。[Epigrams on Programming 编程警句 ](https://epigrams-on-programming.readthedocs.io/zh_CN/latest/epigrams.html) 405 | 406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /content/c-mordern-approch/12-指针.md: -------------------------------------------------------------------------------- 1 | ## 指针 2 | 3 | *If two people write exactly the same program, each should be put in micro-code and then they certainly won't be the same.* 4 | 5 | 6 | 7 | 8 | 9 | ## 目录 10 | 11 | *** 12 | 13 | [TOC] 14 | 15 | ## 指针 16 | 17 | *** 18 | 19 | ### 零 前言 20 | 21 | 指针是 C 语言最重要——也是最常被误解——的特性之一。本节重点介绍指针的基础内容。 22 | 23 | 24 | 25 | ### 一 指针变量 26 | 27 | 现代大多数计算机将内存分割为**字节**(byte),每个字节可以存储 8 位的信息:`0000 0001`。 28 | 29 | 每个字节都有唯一的**地址**(address),用来和内存种的其他字节相区别。如果内存中有 n 个字节,那么可以把地址看作 0 ~ n - 1的数。 30 | 31 | ![](https://hairrrrr.github.io/assets/2020-11-30-13.PNG) 32 | 33 | 34 | 35 | 可执行程序由**代码**(原始 C 程序中于语句对应的机器指令)和 **数据**(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节,把**第一个字节的地址**称为是变量的地址。 36 | 37 | ![](https://hairrrrr.github.io/assets/2020-11-30-14.PNG) 38 | 39 | 上图中,i 占有的字节是 2000 ~ 2003 4 个字节,2000 就是 i 的地址。 40 | 41 | 虽然用数表示地址,但是地址的取值范围可能不同于整数的取值范围,所以一定不能用普通的整型变量存储地址。 42 | 43 | 但是,我们可以用特殊的**指针变量**(pointer variable)存储地址。在用指针变量存储 p 存储变量 i 的地址时,我们说 p “指向” i 。换句话说,指针就是地址,而指针变量就是存储地址的变量。 44 | 45 | ![](https://hairrrrr.github.io/assets/2020-11-30-15.PNG) 46 | 47 | #### 1. 指针变量的声明 48 | 49 | ```c 50 | int* p; 51 | ``` 52 | 53 | 上述声明说明**p 是指向 int 类型对象的指针变量**。这里我们用术语**对象**代替**变量**,这是因为 p 可以指向不属于变量的内存区域。(后面会讲) 54 | 55 | 指针变量可以与其他变量一起出现在声明中: 56 | ```c 57 | int a, b[10], *p, *q; 58 | ``` 59 | 60 | C 语言要求每个指针变量**只能指向一种**特定类型(引用类型)的对象。 61 | 62 | ```c 63 | int* p; 64 | double* q; 65 | char* r; 66 | ``` 67 | 68 | *** 69 | 70 | **关于指针变量声明中 * 与谁挨着的问题**: 71 | 72 | 请看下面的声明: 73 | 74 | ```c 75 | int* p,q; 76 | ``` 77 | 78 | 请问,上面的声明中 p 和 q 都是指针变量吗? 79 | 80 | 小黄:我觉得是,如果你写成这样: 81 | 82 | ```c 83 | int *p, q; 84 | ``` 85 | 86 | 那就是只有 p 是指针变量了。 87 | 88 | 程序圆:你这样想就大错特错啦,上面这两种写法是**等价的**。都是声明 p 为指针变量而 q 是一个普通的 int 类型变量。 89 | 90 | 小黄:哦~那我们平时应该选择那种写法呢? 91 | 92 | 程序圆:通常情况下我们都是选择第一种写法,即:`int* p`。但是这样确实容易造成误解,所以我们通常一行只声明一个指针变量就可以了。 93 | 94 | *** 95 | 96 | ### 二 取地址运算符和间接寻址运算符 97 | 98 | #### 1. 取地址运算符 99 | 100 | 声明指针变量时我们没有将它指向任何对象: 101 | 102 | ```c 103 | int* p; 104 | ``` 105 | 106 | 在使用之前初始化 p 是至关重要的。使用**取地址运算符**`&`把某个变量的地址赋值给它。 107 | 108 | ```c 109 | int i; 110 | p = &i; //&i 就是 i 在内存中的地址 111 | ``` 112 | 113 | 现在 p 就指向了整型变量 i 114 | 115 | 我们也可以声明的同时初始化: 116 | 117 | ```c 118 | int i; 119 | int* p = &i; 120 | ``` 121 | 122 | 甚至可以这样: 123 | 124 | ```c 125 | int i, *p = &i; 126 | ``` 127 | 128 | 但是需要先声明 i 129 | 130 | #### 2. 间接寻址运算符 131 | 132 | 间接寻址运算符也叫**解引用**运算符,我个人还是喜欢叫它用解引用运算符。 133 | 134 | ```c 135 | int i; 136 | int* p = &i; 137 | ``` 138 | 139 | 指针变量 p 指向 i,使用`*`运算符可以访问存储在对对象中的内容(访问存储在指针变量指向的地址上的内容)。 140 | 141 | ```c 142 | printf("%d", *p); // (*p == i) 143 | ``` 144 | 145 | **“`*`和`&`互为逆运算”:** 146 | 147 | ```c 148 | j = *&i;// same as j = i; 149 | ``` 150 | 151 | 只要 p 指向 i,\*p 就是 i 的**别名**。***p 不仅拥有和 i 相同的值,而且 *p 的改变也会改变 i 的值。** 152 | 153 | ```c 154 | int i = 0; 155 | int* p = &i; 156 | printf("i = %d\n", i); 157 | printf("p = %d\n", *p); 158 | // 输出:0 0 159 | *p = 1; 160 | printf("now i = %d\n", i); 161 | printf("now p = %d\n", *p); 162 | //输出:1 1 163 | ``` 164 | 165 | **注意:** 166 | 167 | 解引用**未初始化**的指针变量会导致**未定义行为**: 168 | 169 | ```c 170 | int* p; 171 | printf("%d", *p); 172 | ``` 173 | 174 | 给 **`*p`赋值尤为危险**。如果 p 恰好具有有效的内存地址,程序会试图修改存储在该地址的数据: 175 | 176 | ```c 177 | int* p; 178 | *p = 1; // wrong 179 | ``` 180 | 181 | 这是极度不安全的行为。好在我们的编译器会给出警告。即使这样使用了,编译器不会真的让你去修改其他地方(比如操作系统等)的数据。 182 | 183 | 所以如果你定义的指针特别多,你也不知道那个会被用上,可以这样初始化指针变量: 184 | 185 | ```c 186 | int* p = NULL; // NULL 表示空指针,该处的内存无法修改 187 | ``` 188 | 189 | 然后在需要对 p 解引用的地方添加一个判断: 190 | 191 | ```c 192 | if(p != NULL){ 193 | ...; 194 | } 195 | ``` 196 | 197 | 198 | 199 | ### 三 指针赋值 200 | 201 | C 语言允许**相同类型**的指针变量进行赋值。 202 | 203 | ```c 204 | int i; 205 | int* p = &i; 206 | int* q; 207 | q = p; 208 | ``` 209 | 210 | 或者直接初始化并赋值: 211 | 212 | ```c 213 | int* q = p; 214 | ``` 215 | 216 | ![](https://hairrrrr.github.io/assets/2020-11-30-16.PNG) 217 | 218 | 现在可以通过改变 *p 的值来改变 i : 219 | ```c 220 | int i = 0; 221 | int* p = &i; 222 | int* q = p; 223 | printf("now i = %d\n", i); 224 | printf("now p = %d\n", *q); 225 | // 输出:0 0 226 | *q = 2; 227 | printf("now i = %d\n", i); 228 | printf("now p = %d\n", *q); 229 | //输出:2 2 230 | ``` 231 | 232 | 不要将 `*q = *p` 和 `q = p` 搞混,前者是将 p 指向的对象的值(变量 i 的值)赋值给 q 指向的对象(变量 j)中。 233 | 234 | ![](https://hairrrrr.github.io/assets/2020-11-30-17.PNG) 235 | 236 | 237 | 238 | ### 四 指针作为参数 239 | 240 | 还记得之前分解小数的函数 decompose 吗?我们曾将想在这个函数中通过改变形参来改变实参,但是我们失败了,今天我们再来重新看一下如何用指针作为参数完成这一任务: 241 | 242 | 将 decompose 函数定义中的形参 int_part 和 frac_part 声明成指针类型。 243 | 244 | ```c 245 | void decompose(double x, long* int_part; double* frac_part){ 246 | *int_part = (long)x; 247 | *frac_part = x - *int_part; 248 | } 249 | ``` 250 | 251 | 调用该函数: 252 | 253 | ```c 254 | int i; 255 | double x, d; 256 | decompose(x, &i, &d); 257 | ``` 258 | 259 | ![](https://hairrrrr.github.io/assets/2020-11-30-18.PNG) 260 | 261 | 262 | 263 | 当函数调用完成,实参 i 和 d 的值也修改了。你可以再 main 函数中输出一下 i 和 d 测试一下。 264 | 265 | 266 | 267 | 用指针作为参数其实并不新鲜: 268 | 269 | ```c 270 | int i; 271 | scanf("%d", &i); 272 | ``` 273 | 274 | 必须将 & 放在 i 前以便传给 scanf 函数指向 i 的指针,指针会告诉 scanf 函数将读取的值放在那里。如果没有 & 传递给 scanf 的将是 i 的值。 275 | 276 | 虽然 scanf 函数的实参必须是指针,但是并不是总需要 & 运算符: 277 | 278 | ```c 279 | int i; 280 | int* p = &i; 281 | scanf("%d", p); 282 | ``` 283 | 284 | p 已经包含了 i 的地址,所以不需要 &。使用 & 是错误的: 285 | 286 | ```c 287 | scanf("%d", &p); 288 | ``` 289 | 290 | scanf 函数将把读入的整数放在 p 中而不是 i 中。 291 | 292 | **注意:** 293 | 294 | 向函数传递需要的指针却失败了可能会造成严重后果。比如,如果我们在调用 decompose 函数时没有在 i 和 d 前加上 & : 295 | 296 | ```c 297 | decompose(x, i, d); 298 | ``` 299 | 300 | 函数期望的第二和第三个参数是指针,但传入的却是 i 和 d 的值。decompose 函数没有办法区分,所以它会把 i 和 d 的值当作指针来使用(指针本身是整数)。当函数修改 *int_part 和 *frac_part 时,它会修改未知的内存地址,而不是修改 i 和 d。 301 | 302 | 如果已经提供了函数原型,那么编译器将告诉我们实参类型不对。然而对于 scanf 来说,编译器通常不会检查出传递指针失败,因此 scanf 函数特别容易出错。 303 | 304 | #### 程序:找出数组中的最大元素和最小元素 305 | 306 | 与程序的交互如下: 307 | 308 | ```c 309 | Enter 5 numbers:9 5 2 7 8 310 | Largest: 9 311 | Smallest: 2 312 | ``` 313 | 314 | 参考程序: 315 | 316 | ```c 317 | #include 318 | 319 | #define SIZE 5 320 | 321 | void max_min(int a[], int len, int* max, int* min); 322 | 323 | int main(void) { 324 | 325 | int a[SIZE]; 326 | int max, min, i; 327 | 328 | printf("Enter 5 numbers: "); 329 | for (i = 0; i < SIZE; i++) 330 | scanf("%d", &a[i]); 331 | 332 | max_min(a, SIZE, &max, &min); 333 | 334 | printf("Largest: %d\n", max); 335 | printf("Smallest: %d\n", min); 336 | 337 | return 0; 338 | } 339 | 340 | void max_min(int a[], int len, int* max, int* min) { 341 | 342 | int i; 343 | 344 | *max = *min = a[0]; 345 | for (i = 1; i < len; i++) { 346 | // a[i] 如果比 *max 大 那肯定不会比 *min 小,反之也成立 347 | if (a[i] > * max) 348 | *max = a[i]; 349 | else if (a[i] < *min) 350 | *min = a[i]; 351 | } 352 | } 353 | ``` 354 | 355 | 356 | 357 | #### 用 const 保护参数 358 | 359 | 指针传参时,可以使用 `const`来表明函数不会改变指针参数所指向的对象。 const 应放于形参中: 360 | 361 | ```c 362 | void f(const int* p){ 363 | ...; 364 | } 365 | ``` 366 | 367 | 试图改变 *p 时编译器会报错。 368 | 369 | *** 370 | 371 | 小黄:const 一定只能放在 int 之前吗?这样写合不合法: 372 | 373 | ```c 374 | int* const p; 375 | ``` 376 | 377 | 程序圆:合法。但是和上面那种声明方式的含义不同。上面的 const 修饰的是 *p,使得 *p 不能被修改;而这一种 const 修饰的 p,使得 p 的指向不能发生改变: 378 | 379 | ```c 380 | int i,j; 381 | const int* p = &i; 382 | int* const q = &j; 383 | 384 | *p = 0; // wrong 385 | p = q; // ok 386 | 387 | *q = 0; // ok 388 | q = p; // wrong 389 | ``` 390 | 391 | 你甚至可以这样写: 392 | 393 | ```c 394 | int const *p; 395 | ``` 396 | 397 | 这和第一中写法是同一个意思:使得 *p 不能被修改。 398 | 399 | *** 400 | 401 | 402 | 403 | ### 五 指针作为返回值 404 | 405 | 请看返回值类型为 `int*`类型的函数 max: 406 | 407 | ```c 408 | int* max(int* a, int* b){ 409 | if(*a > *b) 410 | return a; 411 | else 412 | return b; 413 | } 414 | ``` 415 | 416 | max 返回较大数的指针。 417 | 418 | 调用: 419 | 420 | ```c 421 | int a,b; 422 | int* p = max(&a, &b); 423 | ``` 424 | 425 | 需要使用相同的指针类型接收返回值。 426 | 427 | **注意:** 428 | 429 | 永远不要返回指向**自动局部变量**的指针: 430 | 431 | ```c 432 | int* f(){ 433 | int i; 434 | ... 435 | return i; 436 | } 437 | ``` 438 | 439 | 一旦 f 返回,i 就不存在了,所以指向 i 的指针是无效的。有的编译器可能给出警告:“function returns address of local variable” 440 | 441 | 442 | 443 | *参考资料:《C语言程序设计:现代方法》* 444 | 445 | [^1]: 如果两个人用低级语言写同一个程序,它们显然不会相同。[Epigrams on Programming 编程警句 ](https://epigrams-on-programming.readthedocs.io/zh_CN/latest/epigrams.html) 446 | 447 | 448 | 449 | 450 | 451 | -------------------------------------------------------------------------------- /content/c-mordern-approch/23-标准库.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 标准库 4 | 5 | *Perhaps if we wrote programs from childhood on, as adults we'd be able to read them.* [^1] 6 | 7 | ## 目录 8 | 9 | [TOC] 10 | 11 | 12 | 13 | ### 一 标准库的使用 14 | 15 | C89 标准库总共划分成 15 个部分,每个部分用一个头描述。C99 新增了 9 个头,总共有 24 个。 16 | 17 | | C89 | | | 18 | | ------------ | ------------ | ------------ | 19 | | `` | `` | `` | 20 | | `` | `` | `` | 21 | | `` | `` | `` | 22 | | `` | `` | `` | 23 | | `` | `` | `` | 24 | 25 | | C99 新增 | | 26 | | ------------- | ------------ | 27 | | `` | `` | 28 | | `` | `` | 29 | | `` | `` | 30 | | `` | `` | 31 | | `` | | 32 | 33 | 34 | 35 | 大多数编译器都会使用更大的库,其中包含很多上表中没有的头。额外添加的头当然不属于标准库的范畴,所以我们不能假设其他的编译器也支持这些头。这类头通常提供些针对特定机型或特定操作系统的函数(这也解释了为什么它们不属于标准库),它们可能会提供允许对屏幕或键盘做更多控制的函数。用于支持图形或窗口界面的头也是很常见的。 36 | 37 | 标准头主要由**函数原型、类型定义以及宏定义**组成。如果我们的文件中调用了头中声明的函数,或是使用了头中定义的类型或宏,就需要在文件开头将相应的头包含进来。当一个文件包含多个标准头时,`#include`指令的顺序无关紧要。多次包含同一个标准头也是合法的。 38 | 39 | #### 1. 对标准库 中所用名字的限制 40 | 任何包含了标准头的文件都必须遵守两条规则。 41 | 42 | - 第一,**该文件不能将头中定义过的宏的名字用于其他目的**。例如,如果某个文件包含了,就不能重新定义NULL了,因为使用这个名字的宏已经在中定义过了。 43 | - 第二,**具有文件作用域的库名(尤其是typedef名)也不可以在文件层次重定义**。因此,一旦文件包含了,由于中已经将size_ t定义为typedef名,那么在文件作用域内都不能将size_ t重定义为任何标识符。 44 | 45 | 上述这些限制是显而易见的,但C语言还有一些其他的限制,可能是你想不到的。 46 | 47 | - **由一个下划线和一个大写字母开头或由两个下划线开头的标识符**是为标准库保留的标识符。程序不允许为任何目的使用这种形式的标识符。 48 | 49 | - **由一个下划线开头的标识符被保留用作具有文件作用域的标识符和标记**。除非在函数内部声明,否则不应该使用这类标识符。 50 | 51 | - **在标准库中所有具有外部链接的标识符被保留用作具有外部链接的标识符**。特别是所有标准库函数的名字都被保留。因此,即使文件没有包含,也不应该定义名为printf的外部函数,因为在标准库中已经有一个同名的函数了。这些规则对程序的所有文件都起作用,不论文件包含了哪个头。虽然这些规则并不总是强制性的,但不遵守这些规则可能会导致程序不具有可移植性。 52 | 53 | 54 | 55 | 上面列出的规则不仅适用于库中现有的名字,也适用于留作未来使用的名字。至于哪些名字是保留的,完整的描述太冗长了,你可以在C标准的 “future library directions" 中找到。例如,C保留了以str和一个小写字母开头的标识符,从而具有这类名字的函数就可以被添加到 头中。 56 | 57 | #### 2. 使用宏隐藏的函数 58 | 59 | C 程序员经常会用带参数的宏来替代小的函数,这在标准库中同样很常见。C 标准允许在头中定义与库函数同名的宏,为了起到保护作用,还要求有实际的函数存在。因此,对于库的头,声明一个函数并同时定义一个有相同名字的宏的情况并不少见。 60 | 61 | 我们已经见过宏与库函数同名的例子。getchar 是声明在中的库函数,它具有如下原型: 62 | 63 | ```c 64 | int getchar (void); 65 | ``` 66 | 67 | 通常也把 getchar 定义为一一个宏: 68 | 69 | ```c 70 | #define getchar() getc (stdin) 71 | ``` 72 | 73 | 在默认情况下,对 getchar 的调用会被看作宏调用(因为宏名会在预处理时被替换)。在大多数情况下,我们喜欢使用宏来替代实际的函数,因为这样可能会提高程序的运行速度。然而在某些情况下,我们可能需要的是一个真实的函数,可能是为了尽量缩小可执行代码的大小。 74 | 75 | 如果确实存在这种需求,我们可以使用`#undef`指令来删除宏定义。例如,我们可以在包含了后删除宏getchar的定义: 76 | 77 | ```c 78 | #include 79 | #undef getchar 80 | ``` 81 | 82 | 即使 getchar 不是宏,这样的做法也不会带来任何坏处,因为当给定的名字没有被定义成宏时,#undef指令不会起任何作用。 83 | 84 | 此外,我们也可以通过给名字加圆括号来禁用个别宏调用: 85 | 86 | ```c 87 | ch = (getchar)(); /* instead of ch= getchar(); */ 88 | ``` 89 | 90 | 91 | 92 | ### 三 C89 & C99 标准宏概状 93 | 94 | 大家可以下去自行去看一看上面标准头表中的头都是主要用来做什么的,它们都有什么函数原型,类型定义或宏定义。 95 | 96 | 97 | 98 | ### 四 了解两个简单的头 99 | 100 | 前面我们已经了解了 ``,`` 大部分函数和``中的一些函数。现在开始,我们就要继续了解一些常用的头中的函数。今天我们要学习的头是``和``。 101 | 102 | #### 1. ``常用定义 103 | 104 | stddef.h 头提供了常用类型定义,但没有声明任何函数。定义的类型包括一下几个: 105 | 106 | - `ptrdiff_t` 当进行指针相减运算时,其结果的类型。 107 | - `size_t` sizeof 运算符返回的类型 108 | - `wchar_t` 一种足够强大的,可以用于表示所有支持的地区所有字符的类型。 109 | 110 | stddef.h 头中还定义了两个宏。 111 | 112 | - 一个是 `NULL`,用来表示空指针。 113 | - 另一个宏是 `offsetof`需要两个参数:类型(结构类型)和成员指示符(结构的一个成员)。offsetof 会计算计算结构起点到指定成员间的字节数。 114 | 115 | 考虑下面的结构: 116 | 117 | ```c 118 | struct S { 119 | char a; 120 | int b[2]; 121 | float c; 122 | } 123 | ``` 124 | 125 | `offsetof(struct s, a)`的值一定是0,C 语言确保结构的第一个成员的地址与结构自身地址相同。我们无法确定地说出 b 和 c 的偏移量是多少。一种可能是`offsetof(structs, b)`是1 (因为 a 的长度是一个字节), 而`offsetof(struct s, c)`是9 (假设整数是32位)。然而,一些编译器会在结构中留下一些空洞,从而会影响到 offsetof 产生的值。例如,如果编译器在a后面留下了3个字节的空洞,那么 b 和 c 的偏移量分别是4和12。但这正是offsetof宏的魅力所在:对任意编译器,它都能返回正确的偏移量,从而 126 | 使我们可以编写可移植的程序。 127 | 128 | offsetof有很多用途。例如,假如我们需要将结构 s 的前两个成员写入文件,但忽略成员 c。我们不使用 fwrite 函数来写 sizeof(struct s)个字节,因为这样会将整个结构写入,而只要写`offsetof(struct s, c)`个字节。 129 | 130 | 最后一点:一些在 中定义的类型和宏在其他头中也会出现。(例如, NULL宏不仅在C99的头中定义,在中也有定义。)因此,只有少数程序真的需要包含) 131 | 132 | 133 | 134 | #### 2. `stdbool.h` 布尔类型和值 135 | 136 | stdbool.h 头定义了 4 个宏: 137 | 138 | - `bool` (定义为`_Bool`) 139 | - `true`(定义为 1) 140 | - `false`(定义为 0) 141 | - `__bool_true_false_are_defined`(定义为 1) 142 | 143 | 在自己定义 bool,true,false 之前可以使用预处理指令(#if 或 #endif)来测试这个宏。 144 | 145 | 146 | 147 | 148 | 149 | [^1]: 从童年开始写程序,长大了就能读懂了。[Epigrams on Programming 编程警句 ](https://epigrams-on-programming.readthedocs.io/zh_CN/latest/epigrams.html) 150 | 151 | *参考资料:《C语言程序设计:现代方法》* 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /content/c-notes/5分钟看懂什么是 malloc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 初识 动态内存分配 4 | ### 动态内存分配的引入 5 | 初学数组的时候,有一个问题经常困扰着我,就是:我们可不可以**自己在程序里定义一个数组的大小**而不是在函数开头先声明一个很大的数组,然后仅仅使用它的一小部分? 6 | 7 | 请看下面的程序: 8 | 我们需要一个大小为 N ( N < 1000)的数组,我们通常这么写: 9 | ```c 10 | int main(void) { 11 | 12 | int arr[1000] = { 0 }; 13 | int N = 0; 14 | int i = 0; 15 | 16 | printf("请输入数组的大小\n"); 17 | scanf("%d", &N); 18 | 19 | printf("请输入%d个数\n", N); 20 | for (i = 0; i < N; i++) 21 | scanf("%d", &arr[i]); 22 | 23 | return 0; 24 | } 25 | ``` 26 | 每次这么写我都觉得自己在绕远路,为什么就不能直接把输入的变量 N 当作数组的大小直接使用? 27 | 比如这样:`arr[N]`,但是很遗憾,每次编译器都把你扼杀在程序编译之前! 28 | ![](https://img-blog.csdnimg.cn/20200204000901763.gif) 29 | >C99才可以用变量做数组定义的大小 30 | >并且可以在程序中随时声明变量。(C99前我们需要在函数的最前面的区域对所有变量进行声明) 31 | 32 | 如果我不想用上面那种笨笨的办法,又没有支持C99的编译器,我该怎么办? 33 | 34 | 可以这么做: 35 | `int* arr = (int*)malloc(sizeof(int) * N)` 36 | 37 | `sizeof(int)` 代表数组中每个元素的类型 38 | `N` 代表数组的元素个数 39 | 40 | 所以malloc的意义是向 堆区 要了一块`sizeof(int) * N` 这么大的空间 41 | 42 | ### malloc 与 free ——好哥俩 43 | #### malloc 44 | >**头文件**:`stdlib` 45 | >**原型**:`void* malloc(size_t size)` 46 | >所以需要根据实际你需要的类型对其强制类型转换 47 | >**返回值**: 48 | >成功时,返回指向新分配内存的指针。为避免内存泄漏,必须用 free() 或 realloc() 解分配返回的指针。 49 | >失败时,返回空指针(NULL) 50 | >**参数**:size - 要分配的字节数 51 | >*** 52 | >**定义** 53 | >分配 size 字节的未初始化内存。 54 | >若分配成功,则返回为任何拥有基础对齐的对象类型对齐的指针。 55 | > —— 56 | >若 size 为零,则 malloc 的行为是实现定义的。例如可返回空指针。亦可返回非空指针;但不应当解引用这种指针,而且应将它传递给 free 以避免内存泄漏。 57 | >[更多关于malloc](https://zh.cppreference.com/w/c/memory/malloc) 58 | 59 | 60 | #### free 61 | >**头文件**:`stdlib` 62 | >**原型**:`void free( void* ptr );` 63 | >**参数**:指向要解分配的内存的指针 64 | >**返回值**:无 65 | >**此函数接收空指针(并对其不处理)以减少特例的数量。不管分配成功与否,分配函数返回的指针都能传递给 free()** 66 | >—— 67 | >这是什么意思?意思就是malloc与free成对出现,不要忘记写free哦。 68 | >*** 69 | >定义: 70 | >解分配之前由 malloc() 、 calloc() 、 aligned_alloc (C11 起) 或 realloc() 分配的空间。 71 | >—— 72 | >**若 ptr 为空指针,则函数不进行操作**。[^1] 73 | >—— 74 | >**若 ptr 的值 不等于之前从 malloc() 、 calloc() 、 realloc() 或 aligned_alloc() (C11 起) 返回的值**[^2],则行为未定义。 75 | >—— 76 | >**若 ptr 所指代的内存区域已经被解分配[^3]**,则行为未定义,即是说已经以ptr 为参数调用 free() 或 realloc() ,而且没有后继的 malloc() 、 calloc() 或 realloc() 调用以 ptr 为结果。 77 | >—— 78 | >**若在 free() 返回后通过指针 ptr 访问内存[^4]**,则行为未定义(除非另一个分配函数恰好返回等于 ptr 的值)。 79 | >[更多关于free](https://zh.cppreference.com/w/c/memory/free) 80 | 81 | 82 | **free()**:将申请来的空间的 **首地址** 还给“系统”,只要申请到了空间就**一定要归还** 83 | 84 | 毕竟有借有还,再借不难嘛 85 |
86 | #### 解读 free 87 | 88 | 注释1:释放空指针有何意义? 89 | >我们在声明一个指针时,一般把它初始化为0,也就是NULL。 90 | >—— 91 | >这样做的好处是,如果我们在后面的程序中没有让这个指针指向一块具体的空间,这个指针不会是野指针,方便我们用来判断。比如`if(p != NULL)` 92 | >—— 93 | >我们还知道,当malloc失败时返回的是 NULL 94 | >所以我们一开始写上free是好习惯,因为我们不知道我们会不会用到我们声明的指针,也不知道malloc能不能成功 95 | >这时候,free空指针就是有意义的了 96 | 97 | 注释2:molloc申请到的指针 与 free要释放的指针保持一致 98 | ```c 99 | #include 100 | 101 | int main() { 102 | 103 | int* p; 104 | 105 | p = (int*)malloc(100 * 1024); 106 | 107 | p++; //改变了 p 的首地址; 108 | 109 | free(p);//free 没有得到 malloc时 分配给p的首地址,程序崩溃 110 | return 0; 111 | } 112 | ``` 113 | 114 | 注释3:free释放空间后,被释放的指针成为野指针,不能直接使用它 115 | 116 | ```c 117 | #include 118 | 119 | int main() { 120 | 121 | int* p; 122 | 123 | p = (int*)malloc(100 * 1024); 124 | 125 | p++; 126 | 127 | free(p); 128 | p++;//free 释放后 p 成为了野指针,程序崩溃 129 | 130 | return 0; 131 | } 132 | ``` 133 | 134 | 注释4:不能多次释放同一次malloc申请的地址 135 | 136 | ```c 137 | #include 138 | 139 | int main() { 140 | 141 | int* p; 142 | 143 | p = (int*)malloc(100 * 1024); 144 | 145 | p++; 146 | 147 | free(p); 148 | free(p); 149 | 150 | return 0; 151 | } 152 | ``` 153 | 154 |
155 | 156 | **现在我们就可以改进我们上面的程序啦!** 157 | ```c 158 | #include 159 | 160 | int main(void) { 161 | 162 | int i = 0; 163 | int N = 0; 164 | int* arr; 165 | 166 | printf("请输入数组的大小\n"); 167 | scanf("%d", &N); 168 | 169 | arr = (int*)malloc(sizeof(int) * N); 170 | 171 | printf("请输入%d个数\n", N); 172 | for (i = 0; i < N; i++) 173 | scanf("%d", &arr[i]); 174 | 175 | free(arr); 176 | return 0; 177 | } 178 | ``` 179 |
180 | 181 | 什么?不是改进吗?怎么行数反而变多了? 182 | ![](https://img-blog.csdnimg.cn/20200204003505997.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70 =300x300) 183 | 184 | 185 | #### 测测你能给系统分配多大空间? 186 | ```c 187 | #include 188 | 189 | int main(void) { 190 | 191 | void* p; 192 | int i = 0; 193 | 194 | //每次申请100M,失败返回空指针0,退出循环 195 | while ((p = malloc(1024 * 1024 * 100))) 196 | i++; 197 | 198 | printf("最多分配%d00M内存", i); 199 | 200 | return 0; 201 | 202 | } 203 | ``` 204 | #### 如果忘记了free? 205 | 我们一次程序中可以申请的内存是有限的。 206 | 207 | 如果你只是平时写简单的程序,写完就关闭,退出去了,这时忘记了free的话,不会对任何人造成影响,因为操作系统有清除曾使用的内存的机制 208 | 209 | 但是如果是一个持续运行的服务器呢?堆区中所有的空间都被你申请了呢? 210 | 211 | ###### free的常见问题 212 | >- 申请了没有free -> 长时间运行内存逐渐下降 213 | >- free 后再free 214 | >- 地址变更后,直接去free 215 | 216 | *** 217 | ###### 小测试: 218 | 1.对于以下的代码段,正确的说法是: 219 | ```c 220 | char *p; 221 | while(1){ 222 | p = malloc(1); 223 | *p = 0; 224 | } 225 | ``` 226 | A:最终程序会因为没有空间了而退出 227 | B:最终程序会因为向0地址写入而退出 228 | C:程序会一直运行下去 229 | D:程序不能被编译 230 | 231 | 2.对于以下代码段: 232 | ```c 233 | int a[] = {1, 2, 3, 4, 5}; 234 | int *p = a; 235 | int *q = &a[5]; 236 | printf("%d",q-p); 237 | ``` 238 | 当 `sizeof(int) = 4`时,以下说法正确的是: 239 | A:因为第三行的错误不能编译 240 | B:因为第三行的的错误运行时崩溃 241 | C:输出5 242 | D:输出20 243 | 244 | 3.使用malloc就可以做出运行时可以随时改变大小的数组 245 | A:√ 246 | B:❌ 247 | 248 | 查看答案可在后台回复:**2020 0204**查看答案哦 249 | *** 250 | 欢迎各位与我交流讨论! -------------------------------------------------------------------------------- /content/c-notes/C 语言还值得学习吗?C 语言会过时吗?C 语言解惑.md: -------------------------------------------------------------------------------- 1 | ### C 语言还值得学吗? 2 | 3 | 答案是肯定的。 4 | 5 | 第一,学习C有助于更好的理解C++,Java,C#,Perl以及其他基于C的特性的语言。第一开始就学习其他语言的程序员往往不能很好的掌握继承自C语言的基本特性。 6 | 7 | 第二,目前仍有许多C程序,我们需要读懂并维护这些代码。 8 | 9 | 第三,C语言仍广泛应用于新软件的开发,特别是在内存或处理能力受限的情况下以及需要使用C语言简单特性的地方。 10 | 11 | ### C 语言会过时吗? 12 | 13 | > 对所有的编程语言,他们的最后的目的其实就是两种:**提高硬件的运行效率和提高程序员的开发效率**。遗憾的是,这两点是不可能并存的!你只能选一样。在提高硬件的运行效率这一方面,C语言没有竞争者!举个简单的例子,实现一个列表,C语言用数组int a[3],经过编译以后变成了(基地址+偏移量)的方式。对于计算机来说,没有运算比加法更快,没有任何一种方法比(基地址+偏移量)的存取方法更快。C语言已经把硬件的运行效率压缩到了极致。这种设计思想带来的问题就是易用性和安全性的缺失。例如,你不能在数组中混合保存不同的类型,否则编译器没有办法计算正确的偏移量。同时C语言对于错误的偏移量也不闻不问,这就是C语言中臭名昭著的越界问题。C语言自诩的“相信程序员”都是漂亮的说辞,它的唯一目的就是快,要么飞速的运行,要么飞速的崩溃。C语言只关心程序飞的高不高,不关心程序猿飞的累不累。就是这样! 14 | 15 | 16 | 17 | >伴随着嵌入和实时系统的兴起,AI,机器人,自动驾驶等。这些都是C语言的核心应用,而且在这种应用上面,C语言没有竞争者。所以我感觉C语言会稳定在自己核心的应用中,并开始逐步回升。但是Java语言我个人不乐观。小型和灵活性上,Python更胜一筹。一行python代码后,你根本不知道自己还是不是duck类型?平台领域,每个平台都推出自己专属的语言。Windows会继续支持C#,苹果偏爱Swift, Android推出Kotlin,Google用go。Java宣称自己可以自由到每家做客,但是无论是到谁家,都会发现客厅里面坐着一个亲儿子,这个时候自己这个干儿子多多少少有点尴尬。所以我猜测,最后Java会稳定在对跨平台有严格要求的,大型非实时应用上。 18 | 19 | > 最后说点闲话,C++不会淘汰C语言。有了对象后你会发现再简朴的对象也耗费资源,而且有了对象以后,总是不由自主的去想继承这个事,一但继承实现了,你会发现继承带来的麻烦远超过你的想象。Java的发明人James被问到如果可以从新设计Java语言的话,第一个要做什么事?他说:“去掉对象”!作为一个已婚,有两个孩子的程序猿,我感同身受。如果大家感兴趣,我可以再写一个博客,聊聊C++和C的真实区别所在。 20 | 21 | > 如果你看到这里,还什么都没记住。那就只记住一点:没人能预测未来。 22 | > -------------------------- 23 | > 全世界只需要五台电脑 -IBM创始人 24 | > 640K内存足够了 -微软创始人 25 | > 没必要在家里用电脑-DEC创始人 26 | > -------------------------- 27 | > 如果再有人对你说C语言已经过时了,最好自己思考一下,能求真最好,如果不能,至少要做到存疑。 28 | 29 | ### C 语言的前世今生 30 | 31 | #### 1. 起源 32 | 33 | C语言是贝尔实验室的 Ken Thompson, Dennis Ritchie 等人开发的 UNIX 操作系统的“副产品”。 34 | 35 | 与同时代的其他操作系统一样,UNIX 系统最初也是用汇编语言写的。用汇编语言编写的程序往往难以调试和改进,UNIX 操作系统也不例外。Thompson 意识到需要用一种高级的编程语言来完成 UNIX 系统未来的开发,于是他设计了一种小型的 B语言。Thompson 的 B语言是在 BCPL语言(20世纪60年代中期产生的一种系统编程语言)的基础上开发的,而 BCPL语言又可以追溯到最早(且影响深远)的语言之一——Algol 60语言。 36 | 37 | 1970年,贝尔实验室为 UNIX 项目争取到了一台 PDP-11 计算机。当 B语言经过改进并能够在 PDP-11 计算机上成功运行后,Thompson 用 B语言重新编写了部分 UNIX 代码。 38 | 39 | 到了1971年,B语言已经明显不适合 PDP-11 计算机了,于是 Ritchie 着手开发 B语言的升级版。最初他将新开发的语言命名为 NB语言(意味New B),但是后来新语言越来越偏离 B语言,于是他将其改名为 C语言。 40 | 41 | 到1973年,C语言已经足够稳定,可以用来重新编写 UNIX 系统了。 42 | 43 | #### 2. 标准化 44 | 45 | C语言在20世纪七十年代(尤其是1977年到1979)持续发展。这一时期出现了第一本有关 C语言的书。Brian Kernighan 和 Dennis Ritchie 合作编写的 *The C Programming Language* 于1978年出版,并迅速成为 C程序员必读的“圣经”。由于当时没有 C语言的正式标准,这本书就成为了事实上的标准,编程爱好者把它称为“K&R”或者“白皮书”。 46 | 47 | 随着C语言的迅速普及,一系列问题也接踵而至。首先, K&R 对一些语言特性描述得非常模糊,以至于不同编译器对这些特性会做出不同的处理。而且,K&R 也没有对属于 C语言的特性和属于 UNIX 系统的的特性进行明确的区分。更糟糕的是,K&R 出版后 C语言仍然在不断变化,增加了一些新特性并除去了一些旧特性。很快,C语言需要一个全面,准确的最新描述开始成为共识。 48 | 49 | ##### C89/C90 50 | 51 | 1983年,在美国国家标准协会(ANSI)的推动下(ANSI 于此年组建了一个委员会称为 X3Jll),美国开始制定本国的 C语言标准。 52 | 53 | 1988年完成并于1989年12月正式通过的 C语言标准成为 ANSI 标准 X3.159-1989。 54 | 55 | 1990年,国际标准化组织(ISO)通过了此项标准,将其作为 ISO/IEC 9899:1990 国际标准(中国国家标准为 GB/T 15272—1994)。 56 | 57 | 我们把这一C语言版本称为 **C89** 或 **C90**,以区别原始的 C语言版本。 58 | 59 | 委员会在制定的指导原则中的一条写道:保持 C 的精神。委员会在描述这一精神时列出了一下几点: 60 | 61 | - 信任程序员 62 | - 不要妨碍程序员做需要做的事 63 | - 保持语言精炼简单 64 | - 只提供一种方法执行一项操作 65 | - 让程序运行更快,即使不能保持其可移植性 66 | 67 | 在最后一点上,标准委员会的用意是:作为实现,应该针对目标计算机来定义最合适的某特定操作,而不是强加一个抽象,统一的定义。在学习 C语言的过程中,许多方面都反映了这一哲学思想。 68 | 69 | ##### C99 70 | 71 | 1995 年,C语言发生了一些改变。 72 | 73 | 1999年通过的 ISO/IEC 9899:1999 新标准中包含了一些更重要的改变,这一标准所描述的语言通常称为 **C99** 74 | 75 | 此次改变,委员会的用意不是在C语言中添加新的特性,而是为了达到新的目标。 76 | 77 | 1. **支持国际化编程**。如:提供多种方法处理国际字符集 78 | 2. **调整现有实践致力于解决明显的缺陷**。因此,在遇到需要将 C移至64位处理器时,委员会根据现实生活中处理问题的经验来添加标准。 79 | 3. 为**适应科学和工程项目中的关键计算**,提高 C 的适应性,让 C 比 FORTRAN 更有竞争力。 80 | 81 | 其他方面的改变则更为保守,如,尽量让C90,C++兼容,让语言在概念上保持简单。 82 | 83 | 虽然改标准已经发布了很长时间,但**并非所有编译器都完全支持C99**的所有改动。因此,你有可能发现 C99 的一些改动在自己的系统中不可用,或者需要改变编译器的设置才可用。 84 | 85 | ##### C11 86 | 87 | 2011年,**C11**标准问世。 88 | 89 | 90 | 91 | ### 那些基于 C 的语言,你知道吗? 92 | 93 | - C++:包含所有C的特性 94 | - Java:基于C++,所以也继承了C的许多特性 95 | - C#:由C++于java发展起来的较新的语言 96 | - Perl:最初是一种简单的脚本语言,在发展过程中采用了C的许多特性 97 | - Python 98 | - ... 99 | 100 | 101 | 102 | ### C 语言的优缺点 103 | 104 | 与其他任何一种编程语言一样,C语言也有自己的优缺点。这些优缺点都源于该语言的最初用途(编写操作系统和其它系统软件)和它自身的基础理论体系。 105 | 106 | - **C语言是一种底层语言** 为了适应系统编程的需要,C语言提供了对机器级概念(例如,字节和地址)的访问,而这些都是其他编程语言试图隐藏的内容。 107 | - **C语言是一种小型语言** 与许多其他编程语言相比,C语言提供了一套更有限特性集合。(在K&R第二版的参考手册中仅用49页就描述了整个C语言。)为了使特性较少,C语言在很大程度上依赖一个标准函数的“库”。 108 | - **C是一种包容性语言** C假设用户知道自己在干什么,因此它提供了比其他许多语言更广阔的自由度。此外,C语言不像其他语言那样强制进行详细的错误检查。 109 | 110 | #### 1. C语言的优点 111 | 112 | C语言的众多优点解释了C语言为何如此流行。 113 | 114 | - **高效** 高效性是C语言与生俱来的优点之一。发明C语言就是为了编写那些以往由汇编语言编写的程序,所以对C语言来说,能够在有限的内存空间快速运行就显得至关重要。 115 | 116 | - **可移植** 当程序必须在多种机型(从个人计算机到超级计算机)上运行时,常常会用C语言来编写。 117 | 118 | 原因一:C语言没有分裂成不兼容的多种分支。这主要归功于C语言早期与UNIX系统的结合以及后来的ANSI/ISO标准。 119 | 120 | 原因二:C语言编译器规模小且容易编写,这使得它们得以广泛应用。 121 | 122 | 原因三:C语言的自身特性也支持可移植性(尽管它没有阻止程序员编写不可移植的程序)。 123 | 124 | - **功能强大** C语言拥有一个庞大的数据类型和运算符集合,这个集合使得C语言具有强大的表达能力,往往寥寥几行代码就可以实现许多功能。 125 | 126 | - **灵活** C语言最初设计是为了系统编程,但没有固有的约束将其限制在此范围内。C语言现在可以用于编写从嵌入式系统到商业数据处理的各种应用程序。 127 | 128 | - **标准库** C语言的突出优点就是它具有标准库,该标准库包括了数百个可以用于输入/输出,字符串处理,储存分配以及其他实用操作的函数。 129 | 130 | - **与UNIX的集成** C语言在与UNIX系统(包括Linux)结合方面特别强大。事实上,一些UNIX工具甚至假设用户是了解C语言的。 131 | 132 | #### 2. C语言的缺点 133 | 134 | - **C语言容易隐藏错误** C语言的灵活性使得用它编程出错的概率极高。在用其他语言时可以发现的错误,C语言的编译器却无法检查到。更糟糕的是,C语言还包含大量不易察觉的隐患。 135 | - **C程序可能难以理解** C程序的简明扼要与灵活性,可能导致程序员编写出除了自己别人无法读懂的代码。 136 | - **C程序可能难以修改** 如果在设计中没有考虑到维护的问题,那么C编写的大型程序可能很难修改。现代的编程语言通常提供“类”和“包”之类的语言特性,这样的特性可以把大的程序分解成许多更容易管理的模块。遗憾的是,C语言恰恰缺少这样的特性。 137 | 138 | #### 3. 高效的使用C语言 139 | 140 | 要高效的使用C语言,就需要利用C语言优点的同时尽量避免它的缺点,一下给出一些建议。 141 | 142 | - **学习如何规避C语言的缺陷** 143 | - **使用软件工具使程序更可靠** 144 | - **利用现有的代码库** 使用C语言的一个好处是其他许多人也在使用C。把别人编写好的代码用于自己的程序是一个非常好多主意。C代码通常被打包成库(函数的集合)。获取适当的库既可以大大减少错误,也可以节省很多编程工作。 145 | - **采用一套切合实际的编码规范** 良好的编码习惯和规范易于自己和他人对自己代码的阅读和修改。 146 | - **避免“投机取巧”和极度复杂的代码**。C语言鼓励使用编程技巧。但是,过犹不及,不要对技巧毫无节制,最简单的解决方案往往也是最难理解的。 147 | - **紧贴标准** 大多数编译器都提供不属于 C89/C99 标准的特征和库函数。为了程序的可移植性,若非确有必要,最好避免这些特性和库函数。 148 | 149 | ### 为什么 C 语言难学? 150 | 151 | >不同与JAVA和python,C语言面临的任务几乎都是要求实时,高速或者是嵌入的。例如医疗,军事,飞控,航天,金融等领域。举个栗子,NASA大部分软件要基于三个不同的时钟系统,自转(公转)时间,CPU的晶振时间和原子钟时间。一秒要分成500份,基于2毫秒的基础进行操作同步;同时用全球的原子钟时间均值对所有时钟系统调整。在这种环境下,JAVA那种“大约一分钟以后”的虚拟机管理方式一定是不行的。 所以我在NASA工作所接触的软件,几乎都是C语言编写的。可想而之,这种软件的开发难度,当你阅读这种程序代码的时候,你说C语言太难了,这是否有点不公平? 152 | 153 | > 其次是开发环境难。C语言一开始就和UNIX(LINUX)有不解之缘,它们是伴生的系统。所以要想发挥C语言的全部威力,最好的开发环境就是UNIX(LINUX)系统。但是问题来了,UNIX(LINUX)系统里的各种开发工具,每一个都不是省油的灯。它们设计的最初目的就是效率,而不是易学性。再举个栗子,gcc的各种编译开关就很复杂了,make系统为了解决gcc的部分问题,自己随之带来了更大的问题。git目的就是帮你保存历史备份,但是你会发现你经常会串改历史,或者干脆迷失在历史中。就连最简单的一个编辑器VIM,头一个月内,你最多的使用体验就是“恨不得拽自己的头发把自己提溜起来。” 154 | 155 | > 好吧,外面的世界太凶险!让我们回到Windows妈妈哪里。虽然Windows的大部分内核都是C语言写的,但是它对C语言的支持缺最差。Why?如果你用Window的编译器去编译C语言,你会发现变量必须要写到函数的开头。它是唯一一个只支持到C89标准的编译器。Windows本身不想去抢这份实时,高速,嵌入的市场,老老实实做消费电子市场就好,这种市场要求开发容易,发布快。所以C#语言和后面的.Net平台才是它发展的重心。像玩LEGO那样的编程,你需要做的就是把一个个控件拽到窗口上,用鼠标来编程!所以还是算了吧,毕竟你也不想你在做飞机的时候,飞机上控制降落的电脑突然蓝屏了吧!所以如果你是一个C程序员,你唯一能做的就是在linux下使用哪些臭名昭彰的难学的工具。这笔账难到也要算到C语言的头上吗? 156 | 157 | > 最后是底层难。这必须要要聊聊C语言两个最受诟病的特性,位操作和指针。这两个概念本身很简单。但是通过这两个概念,它把很多底层操作系统的知识和体系结构的知识都暴露了出来。指针指向地址后,马上引入了一大堆内存管理知识。什么是堆?什么是栈?这个地址在内存的那个区域?这个区域可以修改吗?这个区域自动回收吗?指针指向函数后,又引入了一堆操作系统知识,什么是回调函数啊?什么是事件驱动啊?以及位操作后面的二进制,溢出,浮点数精度等等一系列的问题。我用手指指向了一本《相对论》,然后就有人跑过来对我说,你这个手指头太难了! 158 | 159 | > 如果编程只是你的业余爱好,使用那种语言真的无所谓。大部分初学者面临的任务规模下,三种语言的开发难度都差不多。 就是打个招呼,英语的“hello”,中文的“你好”,或者是日语的‘牙买碟’,我实在看不出这有什么难度上的区别。但是如果你立志要当一名高水准的程序员,C语言你是逃避不开的。或者编程序是你的饭碗,你也要认真考虑一下C语言。语言的易学性在就业上是一把双刃剑。如果一个公司招聘C程序员,你第一个反应就是他为什么不去招聘满大街的JAVA程序员?你面临的一定不是什么图书管理系统,也一定不是一个什么网站。想明白了这一点,就完全有理由要一个高价钱! 160 | 161 | > C语言很难,要逃避这种难,却很难!C语言很简单,要理解这种简单,却不简单(文章排比对账,我只服自己!) 162 | 163 | 164 | 165 | *** 166 | 167 | 168 | 169 | 参考资料: 170 | 171 | 赵岩的博客(http://zhaoyan.website/blog) 172 | 173 | 《C 语言程序设计 —— 现代方法》 -------------------------------------------------------------------------------- /content/c-notes/C语言 3 道面试题,不会还敢说你C学的好?带详解.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 关于整数类型存储的面试问题 4 | 5 | 以下三个问题大家可以先独立思考一下,看看如果真的面试官问你,你能不能正确的回答并清晰的讲出其中的原理。 6 | 7 | ### 8 | 9 | ### 问题 1 10 | 11 | 请问,printf 函数会打印出什么内容?并解释原因。 12 | 13 | ``` 14 | char a = -1; 15 | signed char b = -1; 16 | unsigned char c = -1; 17 | printf("a = %d, b = %d, c = %d\n", a, b, c); 18 | ``` 19 | 20 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 21 | 22 | ``` 23 | a = -1, b = -1, c = 255 24 | ``` 25 | 26 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 27 | 28 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWM0LnpoaW1nLmNvbS84MC92Mi03MWJmOTFkMjAxYzc5NDE5MjNiNjlkZDNjYjdjNjE3Y183MjB3LnBuZw?x-oss-process=image/format,png)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 29 | 30 | 31 | 32 | signed char 与 char 表示同一种类型,原理一样 33 | 34 | 35 | 36 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWM0LnpoaW1nLmNvbS84MC92Mi05Y2YwM2M0MjlkNDQyZWJjNjE3YzZlNDZhMjc4NzVmMV83MjB3LnBuZw?x-oss-process=image/format,png)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 37 | 38 | 39 | 40 | ### 41 | 42 | ### 问题 2 43 | 44 | 请问,printf 函数会打印出什么内容?并解释原因。 45 | 46 | ``` 47 | char a = -128; printf("%u\n", a); 48 | ``` 49 | 50 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 51 | 52 | ``` 53 | 4294967168 54 | ``` 55 | 56 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 57 | 58 | 你想到了吗? 59 | 60 | 61 | 62 | 我们还是按照上面的思路分析: 63 | 64 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWM0LnpoaW1nLmNvbS84MC92Mi1lYWUwMWM0YTUxZTkwNWVkNDBkMzQ2MDA0YjkzYTBiNV83MjB3LnBuZw?x-oss-process=image/format,png)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 65 | 66 | 67 | 68 | ### 69 | 70 | ### 问题 3 71 | 72 | 请问,printf 函数会打印出什么内容?并解释原因。 73 | 74 | ``` 75 | char a = 128; printf("%u\n", a); 76 | ``` 77 | 78 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 79 | 80 | ``` 81 | 4294967168 82 | ``` 83 | 84 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 85 | 86 | 神奇吗?并不神奇。 87 | 88 | 原因就在于“截断”时得到的二进制序列是一模一样的,后面的操做是相同的。 89 | 90 | 另外说一句,char 的范围是 -128 ~ 127,所以上面的 char 型变量 a 溢出了。 91 | 92 | 93 | 94 | 试着想想下面的 printf 函数又会输出什么呢? 95 | 96 | ``` 97 | unsigned char a = -128; 98 | unsigned char b = 128; 99 | printf("a = %u, b = %u\n", a, b); 100 | ``` 101 | 102 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 103 | 104 | ### 推荐阅读: 105 | 106 | [给你三个必须要学C语言的理由!](https://blog.csdn.net/qq_44954010/article/details/104334319) -------------------------------------------------------------------------------- /content/c-notes/if-else语句详解.md: -------------------------------------------------------------------------------- 1 | 2 | **关于 if else 选择结构 的两种写法:** 3 | ```c 4 | if () { 5 | ; 6 | } 7 | if () { 8 | ; 9 | } 10 | if () { 11 | ; 12 | } 13 | else { 14 | ; 15 | } 16 | 17 | 18 | if () { 19 | ; 20 | } 21 | else if () { 22 | ; 23 | } 24 | else if () { 25 | ; 26 | } 27 | else { 28 | ; 29 | } 30 | ``` 31 | 上面两种写法有区别吗? 32 | 33 | 直接看程序吧: 34 | #### 多个if 直接并列 35 | ```c 36 | int main() { 37 | 38 | int a = 5; 39 | 40 | if (a > 0) 41 | printf("a > 0\n"); 42 | if (a > 2) 43 | printf("a > 2\n"); 44 | if (a > 4) 45 | printf("a > 4\n"); 46 | if (a > 5) 47 | printf("a > 5\n"); 48 | else 49 | printf("a < 5\n"); 50 | 51 | return 0; 52 | } 53 | ``` 54 | 输出: 55 | ```c 56 | a > 0 57 | a > 2 58 | a > 4 59 | a < 5 60 | ``` 61 | #### 多个else if 并列 62 | ```c 63 | int main() { 64 | 65 | int a = 0; 66 | 67 | if (a > 0) 68 | printf("a > 0\n"); 69 | else if (a > 2) 70 | printf("a > 2\n"); 71 | else if (a > 4) 72 | printf("a > 4\n"); 73 | else if (a > 5) 74 | printf("a > 5\n"); 75 | else 76 | printf("a < 5\n"); 77 | 78 | return 0; 79 | } 80 | ``` 81 | 输出: 82 | ```c 83 | a > 0 84 | ``` 85 | 86 | ### 总结 87 | - 多个if 并列 程序会遍历所有的 if 条件。最后一个 else 与最后一个 if 配对,两者必有一个为真 88 | - 多个 else if 并列 程序只要找到一个 真,就会退出整个 “条件体”。最后一个else 与 前面的任意一个语句 必有一个为真。 89 | - 关于else: 90 | * 第一种:else 与 最后一个 if 形成对立 91 | * 第二种:else 与 除 else 外的整体形成对立 -------------------------------------------------------------------------------- /content/c-notes/两个数组为何不能赋值.md: -------------------------------------------------------------------------------- 1 | # 指针 关于const 2 | ### 关于const 3 | #### 数组变量 是 const 的指针 4 | 在初学数组时,我们都有这样的思考:既然变量可以互相赋值,那么 数组 可以相互赋值吗? 5 | 比如说: 6 | ```C 7 | int a = 1; 8 | int b = 2; 9 | int arr1[3] = {1, 2, 3}; 10 | int arr2[3] = {0}; 11 | b = a;//ok 12 | arr2 = arr1;//error 13 | ``` 14 | 一但这么些程序就会报错,为什么会这样呢? 15 | >这是因为,以上面的为例:`int arr2[3] = {0}`在编译器看来其实是这样的:`int* const arr2 ` 上一篇我们也学到了,const在 * 后 const 修饰的是地址 arr2,因此arr2是不能被改变的 16 | 17 | #### `int* const arr` 与 `int arr[]`是否可以划等号? 18 | 我们先来看下面这个程序: 19 | ```C 20 | int main() { 21 | 22 | int arr[3] = { 1, 2, 3 }; 23 | int* const q = arr; 24 | 25 | printf("arr = %p\n", arr); 26 | printf("&arr = %p\n", &arr); 27 | 28 | printf("q = %p\n", q); 29 | printf("&q = %p\n", &q); 30 | 31 | } 32 | ``` 33 | 这个程序里,arr 的值与 q 的值相同我们应该是提前都会想到的。问题就是 这个 arr 的地址 与 q 的地址问题。他们会相同吗?虽然他们都指向 arr,但是这是两个不同的指针变量,所以他们的地址肯定是不会相同的。请看在我的机器上输出结果: 34 | ```C 35 | arr = 004FF824 36 | &arr = 004FF824 37 | q = 004FF824 38 | &q = 004FF818 39 | ``` 40 | `&arr` 竟然与 `arr` 与 `q` 是一样的! 为什么会 这样?`&arr` 与 `arr` 有什么区别?请看下面的程序: 41 | ```C 42 | int main() { 43 | 44 | int arr[3] = { 1, 2, 3 }; 45 | int* const q = arr; 46 | 47 | printf("arr = %p\n", arr); 48 | printf("&arr = %p\n", &arr); 49 | 50 | printf("arr + 1 = %p\n", arr + 1); 51 | printf("&arr + 1 = %p\n", &arr + 1); 52 | 53 | printf("%d\n", ((int)(&arr + 1) - (int)(&arr)));//将指针转变为int,看地址相差多少 54 | 55 | printf("q = %p\n", q); 56 | printf("&q = %p\n", &q); 57 | 58 | printf("&q + 1 = %p\n", &q + 1); 59 | 60 | } 61 | ``` 62 | ```C 63 | arr = 0020F860 64 | &arr = 0020F860 65 | arr + 1 = 0020F864 66 | &arr + 1 = 0020F86C 67 | 12 68 | q = 0020F860 69 | &q = 0020F854 70 | &q + 1 = 0020F858 71 | ``` 72 | `&arr + 1` 和 `arr + 1`差了 12 个字节, 刚好是一整个arr数组的长度。这意味着什么? 73 | >取数组的地址 实际上 取走的是 ==整个数组==的 地址,它将整个数组视为整体,对它进行加减,大小是整个数组的大小 74 | >而`&q + 1`得值仅仅变化了 4 个字节 ,就是一个指针的大小 75 | 76 | <关于const 在程序中的使用教学 后续会在本编中加上 ,敬请期待 !> -------------------------------------------------------------------------------- /content/c-notes/你不知道的几种素数判断方法,由浅入深,详解.md: -------------------------------------------------------------------------------- 1 | 我们要判断素数,首先要知道素数的定义。 2 | 3 | > 素数:质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。 4 | 5 | 知道了素数的定义,那么我们应该想一下,如何去判断一个数是否为素数? 6 | 7 | > 一种思路是,我们在每次得到一个数后,都去计算,去尝试因式分解它,看它除了1和自身之外还有没有其他因子 8 | > 另一种是,我们去查阅素数表,看这个数在不在素数表上。那我们就要先得到素数表。 9 | 10 | 以下除了第一种方法,第2~4种方法都是用第二种思路做的 11 | 当要判断的目标数很少时,第一种高效。但是当给定的目标数组很多,数也很大时。后面的思路配上高效的查找算法,显然更高效 12 | 13 | ------ 14 | 15 | ### 方法1:暴力求解 16 | 17 | 1-1:稍微动动脑 18 | 19 | > **思想**: 20 | > 根据素数的定义思考。素数是大于1的自然数,除了1和自身外,其他数都不是它的因子。 21 | > 那我们就可以用一个循环,从2开始遍历到这个数减去1,如果这个数都不能被整除,那么这个数就是素数。 22 | > 也就是说: 23 | > 给定一个数 n , i 从 2 开始取值,直到 n - 1(取整数),如果 n % i != 0 , n 就是素数 24 | > 进一步思考,有必要遍历到 n - 1 吗? 25 | > 除了1以外,任何合数最小的因子就是2,那最大的因子就是 n/2 26 | > 那我们就遍历到 n/2就足够了 27 | 28 | 这样我们就可以写出这个算法的核心代码: 29 | 30 | ``` 31 | int isPrime(int target) { 32 | 33 | int i = 0; 34 | 35 | if (target <= 1) { 36 | printf("illegal input!\n");//素数定义 37 | return -1; 38 | } 39 | 40 | for (i = 2; i <= target / 2; i++) { 41 | if (target % i == 0) 42 | return 0;//不是素数直接返回0 43 | } 44 | 45 | return 1;//是素数返回1 46 | } 47 | ``` 48 | 49 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 50 | 51 | 1-2:再进一步 52 | 53 | > **思想**: 54 | > 55 | > 在上面的基础上,其实不需要遍历到 n/2,只需要到 根号n(包含根号n) 就可以了。为什么呢?这是个数学问题,大家自行思考一下。 56 | 57 | 核心代码: 58 | 59 | ``` 60 | int isPrime(int target) { 61 | 62 | int i = 0; 63 | 64 | if (target <= 1) { 65 | printf("illegal input!\n");//素数定义 66 | return -1; 67 | } 68 | 69 | for (i = 2; i <= (int)sqrt(target); i++) { 70 | if (target % i == 0) 71 | return 0; 72 | } 73 | 74 | return 1; 75 | } 76 | ``` 77 | 78 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 79 | 80 | ------ 81 | 82 | > 从第二种方法开始,我们都是先完成判断素数数组,然后用二分法去查找判断数组 83 | > 84 | > 这里说一下以下三种方法牵扯的概念: 85 | > 86 | > - 范围:1 ~ 范围上限N 87 | > - 范围上限N:判断素数需要用户输入随机素数,这个随机素数的范围是1 ~ N 88 | > - 判断素数数组:将数组的`下标`与`1 ~ N`的自然数一一对应起来。 89 | > 判断 1到N 的自然数是否为素数,其实就是判断数组的下标是否为素数,如果是 给这个下标所对应的判断素数数组元素赋1,否则赋0 90 | > 比如:我要判断3是否为素数,我们就找到`判断素数数组isPrime`中的下标为3的元素,即:`isPrime[3]` 91 | > 如果 `3` 是素数 , 赋值1,即`isPrime[3] = 1` 92 | > `如果 3 不是素数,赋值0 ,即isPrime[3] = 0` 93 | > 这样我们在用二分法查找时,查找数组下标就可以,找到下标后返回下标对应的判断素数数组的值。 94 | > 如果是1说明下标对应的自然数是素数,否则不是 95 | 96 | ------ 97 | 98 | ### 方法2:用素数表来判断素数 99 | 100 | > **思路**: 101 | > 如果一个数不能整除比它小的任何素数,那么这个数就是素数 102 | > 这种“打印”素数表的方法效率很低,不推荐使用,可以学习思想 103 | 104 | 核心代码: 105 | 106 | ``` 107 | //target:输入的要查找的数 108 | //count:当前已知的素数个数 109 | //PrimeArray:存放素数的数组 110 | int isPrime(int target, int count, int* PrimeArray) { 111 | 112 | int i = 0; 113 | for (i = 0; i < count; i++) { 114 | if (target % PrimeArray[i] == 0) 115 | return 0; 116 | } 117 | 118 | return 1; 119 | } 120 | ``` 121 | 122 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 123 | 124 | ------ 125 | 126 | ### 方法3:普通筛法——埃拉托斯特尼(Eratosthenes)筛法 127 | 128 | > **思路**: 129 | > \1. 我们的想法是,创建一个比范围上限大1的数组,我们只关注下标为 1 ~ N(要求的上限) 的数组元素与数组下标(一一对应)。 130 | > \2. 将数组初始化为1。然后用for循环,遍历范围为:【2 ~ sqrt(N)】。如果数组元素为1,则说明这个数组元素的下标所对应的数是素数。 131 | > \3. 随后我们将这个下标(除1以外)的整数倍所对应的数组元素全部置为0,也就是判断其为非素数。 132 | > 这样,我们就知道了范围内(1 ~ 范围上限N)所有数是素数(下标对应的数组元素值为1)或不是素数(下标对应的数组元素值为0) 133 | 134 | > 用百度百科对埃拉托斯特尼筛法简单描述:**要得到自然数n以内的全部素数,必须把不大于 的所有素数的倍数剔除,剩下的就是素数。** 135 | 136 | 核心代码: 137 | 138 | ``` 139 | // 判断素数的数组 范围上限N 140 | void Eratprime(int* isprime, int upper_board) { 141 | 142 | int i = 0; 143 | int j = 0; 144 | //初始化isprime 145 | for (i = 2; i <= upper_board; i++) 146 | isprime[i] = 1; 147 | 148 | 149 | for (i = 2; i < (int)sqrt(upper_board); i++) { 150 | if (isprime[i]) { 151 | isprime[i] = 1; 152 | } 153 | for (j = 2; i * j <= upper_board; j++) {//素数的n倍(n >= 2)不是素数 154 | isprime[i * j] = 0; 155 | } 156 | } 157 | 158 | } 159 | ``` 160 | 161 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 162 | 163 | ------ 164 | 165 | ### 方法4:线性筛法——欧拉筛法 166 | 167 | > **思路**: 168 | > 我们再思考一下上面的埃拉托斯特尼筛法,会发现,在“剔除“非素数时,有些合数会重复赋值。这样就会增加复杂度,降低效率。 169 | > 比如:范围上限N = 16时 170 | > 171 | > ``` 172 | > 2是素数,剔除”2 的倍数“,它们是:4,6, 8,10, 12, 14, 16 173 | > 3是素数,剔除”3 的倍数”,它们是,6,9,12,15 174 | > ``` 175 | > 176 | > ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 177 | > 178 | > 6,12是重复的。如何减少重复呢? 179 | 180 | 核心代码: 181 | 182 | ``` 183 | void PrimeList(int* Prime, bool* isPrime, int n) { 184 | 185 | int i = 0; 186 | int j = 0; 187 | int count = 0; 188 | 189 | if (isPrime != NULL) {//确保isPrime不是空指针 190 | //将isPrime数组初始化为 1 191 | for (i = 2; i <= N; i++) { 192 | isPrime[i] = true; 193 | } 194 | } 195 | 196 | if (isPrime != NULL && Prime != NULL) { 197 | //从2遍历到范围上限N 198 | for (i = 2; i <= N; i++) { 199 | if (isPrime[i])//如果下标(下标对应着1 ~ 范围上限N)对应的isPrime值没有被置为false,说明这个数是素数,将下标放入素数数组 200 | Prime[count++] = i; 201 | //循环控制表达式的意义:j小于等于素数数组的个数 或 素数数组中的每一个素数与 i 的积小于范围上限N 202 | for (j = 0; (j < count) && (Prime[j] * (long long)i) <= N; j++)//将i强制转换是因为vs上有warning,要求转换为宽类型防止算术溢出。数据上不产生影响 203 | { 204 | isPrime[i * Prime[j]] = false;//每一个素数的 i 倍(i >= 2)都不是素数,置为false 205 | 206 | //这个是欧拉筛法的核心,它可以减少非素数置false的重复率 207 | //意义是将每一个合数(非素数)拆成 2(最小因数)与最大因数 的乘积 208 | if (i % Prime[j] == 0) 209 | break; 210 | } 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 217 | 218 | 如果你没有理解,可以参考下例 219 | 220 | ![img](https://img-blog.csdnimg.cn/20200129202409493.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 221 | 222 | ![img](https://img-blog.csdnimg.cn/20200129202427159.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 223 | 224 | 225 | 226 | [以上四种算法的完整代码在我的github上,帮助到你了不妨给我点个star哦~](https://github.com/hairrrrr/win.ccode/tree/master/Pactise/2020WinterVacation/Prime/Prime Judgement) 227 | 228 | ------ 229 | 230 | 231 | 232 | 233 | 234 | ------ 235 | 236 | 感谢指出我错误的微信网友: 大异小同 。 237 | 238 | 本次修改内容: 239 | 240 | \1. 1-1中的代码,for循环的循环控制 i < target / 2 改为 i <= target 241 | 242 | 错误情况:当 target == 4 时,target / 2 的值是 2,i 从 2开始,如果 循环控制是:i < target / 2, 则不会进入 for 循环,所以会将 4 误判为素数 243 | 244 | \2. sqrt 函数的返回值是 double 类型。 245 | 246 | 将 i <= sqrt(target) 改为 i <= (int)sqrt(target) 247 | 248 | sqrt 函数的函数原型:double sqrt(double arg); 249 | 250 | 251 | 252 | 2020 - 2 - 24 日修改: -------------------------------------------------------------------------------- /content/c-notes/关于字符串你不知道的知识点.md: -------------------------------------------------------------------------------- 1 | # 字符串入门 2 | ### 字符串基础: 3 | #### 基本概念: 4 | - 以 0 结尾的一串字符 5 | - 0 和 '\0' 是一样的,但是与 ’0‘ 不同 6 | - 0标志着字符串的结束,但它不是字符串的一部分 7 | - 计算字符串长度不包括这个0 8 | - 字符串以数组的形式存在,以数组或指针的形式访问(更多以指针形式) 9 | - 头文件 string.h 10 | 11 | #### 表示方法 12 | ```c 13 | char* str = "Hello"; 14 | char string[] = "Hello"; 15 | char line[10] = "Hello"; 16 | ``` 17 | #### 字符串常量 18 | - 形如"Hello"这样**被双引号引起来的字符串**就叫字符串常量(字面量) 19 | - **字符串常量**:"Hello" 20 | **大小** 6 加上结尾表示结束的0 21 | - C的编译器会将**两个连续的字符串连接起来** 22 | - 字符串常量通常放在 `代码段`,这个区域的数据通常是只读的 23 | 24 | 比如: 25 | ```c 26 | printf("Hello","world"); 27 | ``` 28 | 这样会输出 29 | ```c 30 | Hello World 31 | ``` 32 | **或者你也可以这样写:** 33 | 这种写法在我的计算机上提示有语法错误,那位大佬能指导一下?(翁恺老师可以在这样写,难道是C99语法。。。) 34 | ```c 35 | printf("Hello \ 36 | World"); 37 | ``` 38 | #### 注意 39 | - 不能对字符串做运算 40 | - 可通过数组方式遍历字符串 41 | - 可以通过字符串常量来初始化字符串数组 42 | 43 | ### 字符串输入输出 44 | ```c 45 | char* str = "Hello"; 46 | //输入 47 | scanf("%s",string); 48 | //输出 49 | printf("%s",string); 50 | ``` 51 | **读取方式** 52 | 53 | 可以用以下程序测试: 54 | ```c 55 | int main() { 56 | 57 | char str1[4]; 58 | char str2[4]; 59 | 60 | scanf("%s", str1); 61 | scanf("%s", str2); 62 | 63 | printf("%s##%s", str1, str2); 64 | 65 | return 0; 66 | } 67 | ``` 68 | 69 | - `scanf`读入一个单词(遇到 **空格**,**tab**,**回车** 结束) 70 | - `scanf`不安全,因为不知道读入的内容的长度 71 | 72 | **安全的输入** 73 | >`scanf("%3s",str1)` 74 | >% 与 s 之间的数字表示,最多允许读入的字符数量,这个数的最大值应该等于 `数组大小 - 1` 75 | 76 | 请看下面的程序: 77 | 分别输入以下两组值,会输出什么? 78 | 1234 1234 79 | 123 123 80 | ```c 81 | int main() { 82 | 83 | char str1[4]; 84 | char str2[4]; 85 | 86 | scanf("%3s", str1); 87 | scanf("%s", str2); 88 | 89 | printf("%s##%s", str1, str2); 90 | 91 | return 0; 92 | } 93 | ``` 94 | 95 | ```c 96 | //1 97 | 123 4 98 | //2 99 | 123 123 100 | ``` 101 | ![](https://img-blog.csdnimg.cn/2020020422550928.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70 =200x200) 102 | **注意** 103 | `char buffer[100] = ""`(""是紧挨的,下同) 104 | 它的意思是 `buffer[0] == '\0'` 表示空字符串 105 | `char buffer[] = ""` 106 | 它表示字符串长度为1 107 | 108 | 我们不妨来测试一下: 109 | ```c 110 | int main() { 111 | 112 | char buffer1[100] = ""; 113 | char buffer2[] = ""; 114 | 115 | printf("%c %d\n", buffer1[0], sizeof(buffer1)); 116 | printf("%c %d\n", buffer2[0], sizeof(buffer2)); 117 | 118 | return 0; 119 | } 120 | ``` 121 | 输出 122 | ```c 123 | 100 124 | 1 125 | ``` 126 | **0 用%c 的格式输出为空格** 127 | 128 | ### 字符串数组 — 指针数组的一类 129 | 这里我们重点介绍 字符串数组。什么是指针数组,可以参考我CSDN上的这篇文章: 130 | [[C语言复习巩固]指针(入门)](https://blog.csdn.net/qq_44954010/article/details/103742120) 131 | 132 | #### char* str1[5] 与char str2[5][10] 133 | ![char* a[] 与char a[][10]在栈中的不同](https://img-blog.csdnimg.cn/20200205151248503.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 134 | 从上图可以看出,char* str1[5] 内存放的是**字符串的指针**,指向了一个地址 135 | 而char str2[5][10]则是在栈中开辟了50char大小的空间来存放这些字符 136 | 137 | 下面的程序通过初始方式来帮助你理解两者的不同: 138 | ```c 139 | int main() { 140 | 141 | char Str1[10] = "Hello"; 142 | char Str2[10]; 143 | char Str3[10]; 144 | char Str4[10]; 145 | char Str5[10] = "World"; 146 | 147 | char* str1[5] = {Str1, Str2, Str3, Str4, Str5}; 148 | char str2[5][10] = { 149 | "Hello",//注意写逗号 150 | "", 151 | "", 152 | "", 153 | "World"}; 154 | 155 | str1[1] = " Goodbye"; 156 | //str2[1] = " Goodbye";//报错:表达式必须是可修改的左值 157 | scanf("%s", str2[1]); 158 | 159 | printf("%s", str1[0]); 160 | printf("%s\n", str1[4]); 161 | printf("%s ", str2[0]); 162 | printf("%s\n", str2[4]); 163 | 164 | return 0; 165 | } 166 | ``` 167 | #### 字符串数组的一个应用 168 | **写一个程序。输入一个数代表月份,输出这个月的英语单词。** 169 | 170 | 我们可以用 if else 来做也可以用 switch 来做。但是今天学习了字符串数组,我们有了更简单的方式来完成。 171 | 172 | 请看下面的程序: 173 | ```c 174 | int main() { 175 | 176 | char* str[12] = { "January", "February", "March", "April", "May", "June", 177 | "July", "August", "September", "October", "November", "Decenmber", 178 | }; 179 | int month = 0; 180 | 181 | while (1) { 182 | printf("input a month\n"); 183 | scanf("%d", &month); 184 | 185 | if (month >= 1 && month <= 12) 186 | printf("%s\n", str[month - 1]); 187 | else 188 | printf("invalid input\n"); 189 | } 190 | 191 | return 0; 192 | } 193 | ``` 194 | 这样写代码被大大缩短了 195 | 196 | #### main函数的参数? 197 | 通常我们写 main 函数会这样写: 198 | `int main()` ,这样写其实并不严谨。 199 | 但这并不是大家的错,是谭老师没教好。 200 | 如果你的main函数没有参数,从今天起我们都这样写`int main(void)`,为什么呢? 201 | 首先你的main函数和大多初学者不一样,可以装一装大佬。 202 | ![](https://img-blog.csdnimg.cn/2020020516121023.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70 =200x200) 203 | 其次,我们知道,main函数也是函数,是被其他函数调用的。这个在我函数的文章中讲解过。感兴趣的朋友可以看一下。[点击查看](https://mp.weixin.qq.com/s/JEalmGOwNXp9IM0W7B7YJw) 204 | 205 | 这是mian函数有参数的形式(我们在这里简化了一下,方便理解) 206 | `int main(int argc, char* argv[])` 207 | ![](https://img-blog.csdnimg.cn/20200205161524446.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70 =250x200) 208 | >`int argc` 是字符串数组`char* argv[]`的大小 (也称“参数计数” ,是命令行参数的数量) 209 | >`int argv[]` 也称“参数向量” 是指向命令行参数的指针数组 210 | >`argv[0]`指向可执行程序的名称(.exe) 211 | >`argv[1] ~ argv[argc - 1]` 指向实际参数,也就是命令行 212 | >`argv[argc]`是空指针NULL 213 | 214 | 如果你不知道我上面写的是什么,那么就要注意啦!其他的都是开玩笑,这里才是这一节的重点。 215 | *** 216 | 一下内容面向小白,大佬可以跳过这一段 217 | 218 | 什么是命令行? 219 | 220 | 看完这篇文章后,去搜索一下linux是什么以及linux对程序员有什么用相关问题。如果你有收获,分享一下这篇文章不过分吧 221 | 222 | 命令行说白了就是一个替代你鼠标的东西,它可以让你更加高效的去做很多事情。 223 | 比如在win10下你在键盘上按下 `win + R` 然后再跳出的窗口里输入 `cmd` 打开的小黑窗口其实就是命令行窗口。在这个窗口里你可以输入指令,来完成各种事情。比如基础的`dir` 展示当前目录下内容 `cd`进入文件夹 224 | `cd .. `退出文件夹等等。 225 | *** 226 | 为了更深入的了解mian函数的参数有什么用,我们在实际的程序中来感受一下。 227 | 228 | **用main函数的参数实现的 命令行中 两个整数的 四则运算** 229 | 230 | 先看一下程序运行的效果: 231 | ![](https://img-blog.csdnimg.cn/20200205182745931.gif) 232 | 关注公众号后台回复 【2020 0205】获得源码以及详细讲解 -------------------------------------------------------------------------------- /content/c-notes/内存对齐.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### 零 前言 4 | 5 | 自定义类型也就是:结构体,联合和枚举。这部分的基础知识在前面的文章中我们也详细的讲过。[点击阅读](https://mp.weixin.qq.com/s/NkXZSdM-gnAuG7_jAM8ZiA) 6 | 7 | 我们这一节主要来讲一相关的些比较重要的知识。 8 | 9 | 10 | 11 | ### 一 结构体 12 | 13 | #### 1. 内存对齐 14 | 15 | ##### Ⅰ)引入 16 | 17 | ```c 18 | struct S1 19 | { 20 | char c1; 21 | int i; 22 | char c2; 23 | }; 24 | ``` 25 | 26 | 上面是一个结构体,也是我们自定义的一种类型。我们知道,任何类型都有大小,那么结构体 S1 的大小是多少? 27 | 28 | 是结构体各成员变量大小的和吗?如果是的话,那结构体 S1 的大小就是 6 29 | 30 | 那我们设计一个程序验证一下: 31 | 32 | ```c 33 | int main(void) { 34 | 35 | printf("%d", sizeof(struct S1)); 36 | 37 | return 0; 38 | } 39 | ``` 40 | 41 | **输出是:12**,这个 12 是怎么得来的呢? 42 | 43 | 想要知道这个问题答案,那我们就要了解一下 **内存对齐**。 44 | 45 | ##### Ⅱ)为什么要内存对齐? 46 | 47 | 内存对齐关系到 CPU 读取数据的效率 和 一些其他原因。我们这里不做展开,有兴趣可以自己查一下。 48 | 49 | ##### Ⅲ)规则 50 | 51 | >- 第一个成员在与结构体变量偏移量为0的地址处。 52 | > 53 | >- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 54 | > 55 | >**对齐数** = 编译器默认的一个对齐数 与 该成员大小的**较小值**。 56 | >VS中默认的值为8 57 | > 58 | >- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 59 | > 60 | >- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。 61 | 62 | 63 | 64 | ##### 四)练习 65 | 66 | 判断下面结构体的大小: 67 | 68 | VS 默认的对齐数是 8,32 位机器 69 | 70 | **1** 71 | 72 | ```c 73 | struct S1 74 | { 75 | char c1; 76 | int i; 77 | char c2; 78 | }; 79 | ``` 80 | 81 | 解析:`1(char)` (+` 3`(int 应该对齐到 4 的整数倍上,也就是 4,所以应该给 1 加上 3 凑成 4)) +` 4(int)` +` 1(char)` (+` 3`最后整个结构体大小为最大对齐数(也就是 4)的整数倍处,所以结构体的大小不是 9 而是 12 )(最大对齐数是最大成员的对齐数,这个是前面算过的(成员大小和默认对齐数取小)) 82 | 83 | 答案:12 84 | 85 | **2** 86 | 87 | ```c 88 | struct S2 89 | { 90 | char c1; 91 | char c2; 92 | int i; 93 | }; 94 | ``` 95 | 96 | 第一个例题已经详细的分析了判断结构体大小的步骤,下面不再赘述。 97 | 98 | `1 (char)`+ `1 (char)` (+`2`) + `4 (int)` 99 | 100 | 答案:8 101 | 102 | **3** 103 | 104 | ```c 105 | struct S3 106 | { 107 | double d; 108 | char c; 109 | int i; 110 | } 111 | ``` 112 | 113 | `8 (double)` + `1 (char)` (+`3`) + `4 (int)` 114 | 115 | 答案:16 116 | 117 | **4** 118 | 119 | ```c 120 | struct S3 121 | { 122 | double d; 123 | char c; 124 | int i; 125 | }; 126 | 127 | struct S4 128 | { 129 | char c1; 130 | struct S3 s3; 131 | double d; 132 | }; 133 | ``` 134 | 135 | 例 3 中,我们已经知道了 S3 的大小是 16 136 | 137 | `1 (char)` (+ `7`(结构体大小是 16 和 编译器默认对齐数 8 取较小值,所以结构体要对齐的整数倍是 8)) + `16 (S3)` + `8 (double)` 138 | 139 | 答案:32 140 | 141 | 142 | 143 | 不确定你可以自己在你的编译器上敲一下,看看运行结构,前提是编译器的默认对齐数是 8 ,如果不是,结果可能会不一样,那么编译器的默认对齐数可以修改吗? 144 | 145 | 146 | 147 | #### 2. 修改默认对齐数 148 | 149 | 只需要加上一条指令即可: 150 | 151 | ```c 152 | #pragma pack(4)//设置默认对齐数为4 153 | ``` 154 | 155 | 如果你想取消设置的默认对齐数,还原为默认: 156 | 157 | ```c 158 | #pragma pack() 159 | ``` 160 | 161 | 162 | 163 | ### 二 位段 164 | 165 | #### 1.了解位段 166 | 167 | 位段的声明和结构是类似的,有两个不同: 168 | 169 | 1. 位段的成员必须是 int、unsigned int 或signed int 。 170 | 2. 位段的成员名后边有一个冒号和一个数字。 171 | 172 |
173 | 174 | ```c 175 | struct S 176 | { 177 | char a : 3;// a 的大小为 3 个比特位 178 | char b : 4; 179 | char c : 5; 180 | char d : 4; 181 | }; 182 | int main(void) { 183 | 184 | struct S s = { 0 }; 185 | 186 | // 可以像一般的结构体成员访问一样访问它们 187 | s.a = -4;// 3 个字节存储数的范围是 -4 ~ 3 188 | s.b = 7; 189 | s.c = 3; 190 | s.d = 4; 191 | 192 | 193 | printf("%d\n", s.a); 194 | 195 | return 0; 196 | } 197 | ``` 198 | 199 | 200 | 201 | **存储方式:** 202 | 203 | 1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型 204 | 2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。 205 | 3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段 206 | 207 | 208 | 209 | **位段的应用:** 210 | 211 | 可以自行了解**IP数据报格式**。 212 | 213 | 214 | -------------------------------------------------------------------------------- /content/c-notes/小端和整型存储.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### 1.如何用程序判断自己的机器是大端还是小端? 4 | 5 | *** 6 | 7 | 通常情况下,我们的计算机都是小端存储模式。 8 | 9 | > 小端:数字的低位存储到内存的低地址上。 10 | > 11 | > 大端:数字的低位存储到内存的高地址上。 12 | 13 | 我们在 VS 中创建一个临时变脸 14 | 15 | ```c 16 | int a = 0x11223344;// 十六进制数 17 | ``` 18 | 19 | 然后打开调试器,看到变量 a 在内存中是这样存储的: 20 | 21 | ```c 22 | 0x0133FC50 44 33 22 11 23 | ``` 24 | 25 | 对于 Vs 调试中内存窗口的这行信息应该如何理解呢?它就表示: 26 | 27 | ![](https://hairrrrr.github.io/assets/2020-11-30-49.png) 28 | 29 | 十六进制数每两位表示一个字节,地址也是十六进制数;int 类型在 32 位机器上大小为 4 个字节。 30 | 31 | 32 | 33 | **如何理解十六进制数每两位表示一个字节?** 34 | 35 | 十六进制数每一位的取值范围是 0 ~ 15,表示 16 种不同可能,对应 4 个二进制位(0000 ~ 1111),所以每一位十六进制可以表示 4 个二进制位,那么两个十六进制位就表示 8 个二进制位,也就是 1 个字节。 36 | 37 | 38 | 39 | 可以看到,在我的机器上,低位 44 存储在 低地址(0x0133FC50)上,所以我的机器是 小端存储模式。 40 | 41 | 42 | 43 | 如果是大端存储模式,变量 a 在内存中的存储应该如下图所示: 44 | 45 | ![](https://hairrrrr.github.io/assets/2020-11-30-50.png) 46 | 47 | 48 | 49 | 现在,让我们用程序来验证一下我们的机器到底是大端还是小端。 50 | 51 | 52 | 53 | #### 方法一 54 | 55 | ```c 56 | #include 57 | 58 | int main(void) { 59 | 60 | int a = 0x11223344; 61 | int* pi = &a; 62 | char* pc = (char*)pi;//指针强转 63 | 64 | printf("%x\n", *pc);//输出 44 ,得到证实 65 | 66 | return 0; 67 | } 68 | ``` 69 | 70 | 71 | 72 | #### 方法二 73 | 74 | ```c 75 | #include 76 | 77 | typedef union { 78 | int a; 79 | char ch[sizeof(int)]; 80 | }BOS;//big or small 81 | 82 | int main(void) { 83 | 84 | BOS bos; 85 | bos.a = 0x11223344; 86 | 87 | printf("%x", (unsigned int)bos.ch[0]);//输出 44 88 | 89 | return 0; 90 | } 91 | ``` 92 | 93 | 94 | 95 | ### 2.关于整数类型存储的面试问题 96 | 97 | *** 98 | 99 | 以下问题大家可以先独立思考一下,看看如果真的面试官问你,你能不能正确的回答并清晰的讲出其中的原理。 100 | 101 | #### 1 102 | 103 | 请问,printf 函数会打印出什么内容?并解释原因。 104 | 105 | ```c 106 | char a = -1; 107 | signed char b = -1; 108 | unsigned char c = -1; 109 | printf("a = %d, b = %d, c = %d\n", a, b, c); 110 | ``` 111 | 112 | ```c 113 | a = -1, b = -1, c = 255 114 | ``` 115 | 116 | ![](https://hairrrrr.github.io/assets/2020-11-30-51.png) 117 | 118 | **signed char 与 char 表示同一种类型,原理一样** 119 | 120 | 121 | 122 | ![](https://hairrrrr.github.io/assets/2020-11-30-52.png) 123 | 124 | #### 2 125 | 126 | 请问,printf 函数会打印出什么内容?并解释原因。 127 | 128 | ```c 129 | char a = -128; 130 | printf("%u\n", a); 131 | ``` 132 | 133 | ```c 134 | 4294967168 135 | ``` 136 | 137 | 你想到了吗? 138 | 139 | 140 | 141 | 我们还是按照上面的思路分析: 142 | 143 | ![](https://hairrrrr.github.io/assets/2020-11-30-53.png) 144 | 145 | #### 3 146 | 147 | 请问,printf 函数会打印出什么内容?并解释原因。 148 | 149 | ```c 150 | char a = 128; 151 | printf("%u\n", a); 152 | ``` 153 | 154 | ```c 155 | 4294967168 156 | ``` 157 | 158 | 神奇吗?并不神奇。 159 | 160 | 161 | 162 | 原因就在于“截断”时得到的二进制序列是一模一样的,后面的操做是相同的。 163 | 164 | 165 | 166 | 另外说一句,char 的范围是 -128 ~ 127,所以上面的 char 型变量 a 溢出了。 167 | 168 | 169 | 170 | 试着想想下面的 printf 函数又会输出什么呢? 171 | 172 | ```c 173 | unsigned char a = -128; 174 | unsigned char b = 128; 175 | printf("a = %u, b = %u\n", a, b); 176 | ``` 177 | 178 | 179 | 180 | #### 4 181 | 182 | ```c 183 | int i = -20; 184 | unsigned int j = 10; 185 | printf("%d\n", i + j); 186 | ``` 187 | 188 | 首先,i 与 j 相加时,**int 转换为 unsigned int** 。 189 | 190 | ![](https://hairrrrr.github.io/assets/2020-11-30-54.png) 191 | 192 | #### 5 193 | 194 | 请问:下面的程序会输出什么? 195 | 196 | ```c 197 | unsigned int i; 198 | for(i = 9; i >= 0; i--){ 199 | printf("%u\n", i); 200 | } 201 | ``` 202 | 203 | 204 | 205 | 这个问题的关键点就是在 i == 0 时。如果 i 的类型是 int ,毫无疑问,for 循环会在这里结束。可是,现在 i 的类型是 unsigned int。 206 | 207 | 我们知道,`i--`等同于 `i -= 1`,也就是 `i = i - 1` 。对于编译器来说,其实这个操作是 `i = i + (-1)`,我们知道, -1 的补码是: 208 | 209 | 11111111 11111111 11111111 11111111 210 | 211 | 当它与 0(i)相加时,i 的补码就变成了全 1。问题就在于,这时候 i 是 unsigned int 类型,这个全 1 的补码的含义并不是 -1 而是 unsigned int 的最大值。所以循环条件 `i >= 0 `依然满足。 212 | 213 | 214 | 215 | 换句话说,对于 unsigned int 类型的 i 来说,`i >= 0`是恒成立的。 216 | 217 | 218 | 219 | 所以答案是无限循环。 220 | 221 | 222 | 223 | #### 6 224 | 225 | ```c 226 | int main(void) 227 | { 228 | char a[1000]; 229 | int i; 230 | 231 | for(i=0; i<1000; i++){ 232 | a[i] = -1-i; 233 | } 234 | 235 | printf("%d",strlen(a)); 236 | 237 | return 0; 238 | } 239 | ``` 240 | 241 | ![](https://hairrrrr.github.io/assets/2020-11-30-55.png) 242 | 243 | #### 7 244 | 245 | ```c 246 | 7. 247 | #include 248 | unsigned char i = 0; 249 | int main(void){ 250 | for(i = 0;i<=255;i++){ 251 | printf("hello world\n"); 252 | } 253 | 254 | return 0; 255 | } 256 | 257 | ``` 258 | 259 | 这个情况与例5相同。 260 | 261 | 262 | 263 | ### 3.浮点数 264 | 265 | 浮点数我们不做过多说明,详情我们在【C入门到精通】讲过。 266 | 267 | 我们着重强调一下,对于 **2 个浮点数的比较** 来说,不能像整型那样直接比较,应该引入一个误差范围,比如: 268 | 269 | ```c 270 | #define E 1e-4 //定义一个精度 271 | 272 | float i = 19.0; 273 | float j = i / 7.0; 274 | 275 | if (j * 7.0 - i < E && j * 7.0 - i > -E) { 276 | printf("相等!\n"); 277 | } else { 278 | printf("不相等!\n"); 279 | } 280 | ``` 281 | 282 | 283 | 284 | **如果本文你有地方没有看懂,推荐阅读以下文章,可以帮助你理解**: 285 | 286 | - [一文看懂枚举&结构&联合](https://mp.weixin.qq.com/s/NkXZSdM-gnAuG7_jAM8ZiA) 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /content/c-notes/教你用简单的程序判断你的电脑是大端还是小端.md: -------------------------------------------------------------------------------- 1 | ### **如何用程序判断自己的机器是大端还是小端?** 2 | 3 | 通常情况下,我们的计算机都是小端存储模式。 4 | 5 | > 小端:数字的低位存储到内存的低地址上。 6 | > 大端:数字的低位存储到内存的高地址上。 7 | 8 | 我们在 VS 中创建一个临时变脸 9 | 10 | ``` 11 | int a = 0x11223344;// 十六进制数 12 | ``` 13 | 14 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 15 | 16 | 然后打开调试器,看到变量 a 在内存中是这样存储的: 17 | 18 | ``` 19 | 0x0133FC50 44 33 22 11 20 | ``` 21 | 22 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 23 | 24 | 对于 Vs 调试中内存窗口的这行信息应该如何理解呢?它就表示: 25 | 26 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWM0LnpoaW1nLmNvbS84MC92Mi1mNGE3OGNlNTdmZTUwYzNlOTcwMDM1OWM2MGFlZTMzN183MjB3LmpwZw?x-oss-process=image/format,png)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 27 | 28 | 十六进制数每两位表示一个字节,地址也是十六进制数;int 类型在 32 位机器上大小为 4 个字节。 29 | 30 | 31 | 32 | **如何理解十六进制数每两位表示一个字节?** 33 | 34 | 十六进制数每一位的取值范围是 0 ~ 15,表示 16 种不同可能,对应 4 个二进制位(0000 ~ 1111),所以每一位十六进制可以表示 4 个二进制位,那么两个十六进制位就表示 8 个二进制位,也就是 1 个字节。 35 | 36 | 37 | 38 | 可以看到,在我的机器上,低位 44 存储在 低地址(0x0133FC50)上,所以我的机器是 小端存储模式。 39 | 40 | 41 | 42 | 如果是大端存储模式,变量 a 在内存中的存储应该如下图所示: 43 | 44 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWMxLnpoaW1nLmNvbS84MC92Mi00ZmI1YWJmN2Q3OWYwNzU5OTM5ZjBhMjliYzRkMzlhY183MjB3LmpwZw?x-oss-process=image/format,png)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 45 | 46 | 现在,让我们用程序来验证一下我们的机器到底是大端还是小端。 47 | 48 | 49 | 50 | ### **方法一** 51 | 52 | ``` 53 | #include 54 | 55 | int main(void) { 56 | 57 | int a = 0x11223344; 58 | int* pi = &a; 59 | char* pc = (char*)pi;//指针强转 60 | 61 | printf("%x\n", *pc);//输出 44 ,得到证实 62 | 63 | return 0; 64 | } 65 | ``` 66 | 67 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 68 | 69 | 70 | 71 | ### **方法二** 72 | 73 | ``` 74 | #include 75 | 76 | typedef union { 77 | int a; 78 | char ch[sizeof(int)]; 79 | }BOS;//big or small 80 | 81 | int main(void) { 82 | 83 | BOS bos; 84 | bos.a = 0x11223344; 85 | 86 | printf("%x", (unsigned int)bos.ch[0]);//输出 44 87 | 88 | return 0; 89 | } 90 | ``` 91 | 92 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 93 | 94 | 95 | 96 | **如果本文你有地方没有看懂,推荐阅读以下文章,可以帮助你理解**: 97 | 98 | - [一文看懂枚举&结构&联合](https://mp.weixin.qq.com/s/NkXZSdM-gnAuG7_jAM8ZiA) 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /content/c-notes/有关指针.md: -------------------------------------------------------------------------------- 1 | # 指针的运算 详解 2 | ### 指针的运算 3 | ### 指针加减 常量 4 | 请看下面的程序,猜测一下结果: 5 | 6 | ```c 7 | int main() { 8 | 9 | int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 10 | int* a = &arr; 11 | 12 | printf("a = %p\n", a); 13 | printf("a + 1 = %p\n", a + 1); 14 | printf("a - 1 = %p\n", a - 1); 15 | 16 | } 17 | ``` 18 | 运行结果: 19 | ```c 20 | a = 00AFF82C 21 | a + 1 = 00AFF830 22 | a - 1 = 00AFF828 23 | ``` 24 | 可以看到, a 与 a + 1 和 a - 1 都差了四个字节 25 | >**指针加减常量** 加减的大小为 **`sizeof(类型) * 常量 `** 26 | 27 | 再试试 char 类型? 28 | 29 | ```c 30 | int main() { 31 | 32 | char arr[10] = { ' 1', '2', '3', '4', '5', '6', '7', '8', '9', '10',}; 33 | char* a = &arr; 34 | 35 | printf("a = %p\n", a); 36 | printf("a + 1 = %p\n", a + 1); 37 | printf("a - 1 = %p\n", a - 1); 38 | 39 | } 40 | ``` 41 | 结果如我们所料: 42 | 43 | ```c 44 | a = 0095F9E0 45 | a + 1 = 0095F9E1 46 | a - 1 = 0095F9DF 47 | ``` 48 | 相差大小 为 1 49 | 50 | #### 指针 - 指针 51 | 先来看一段程序吧: 52 | 53 | ```c 54 | int main() { 55 | 56 | char arr1[10] = { ' 1', '2', '3', '4', '5', '6', '7', '8', '9', '10', }; 57 | int arr2[5] = { 1, 2, 3, 4, 5 }; 58 | 59 | char* ch1 = &arr1[4]; 60 | char* ch2 = &arr1; 61 | int* i1 = &arr2[4]; 62 | int* i2 = &arr2; 63 | 64 | printf("ch1 - ch2 = %d\n", ch1 - ch2); 65 | printf("ch2 - ch1 = %d\n", ch2 - ch1); 66 | printf("\ni1 - i2 = %d\n", i1 - i2); 67 | printf("i2 - i1 = %d\n", i2 - i1); 68 | 69 | } 70 | ``` 71 | 指针相减 结果会是 指针相差的大小吗?看结果: 72 | 73 | ```c 74 | ch1 - ch2 = 4 75 | ch2 - ch1 = -4 76 | 77 | i1 - i2 = 4 78 | i2 - i1 = -4 79 | ``` 80 | >指针 减 指针 意义是 **两个地址之间相隔的单元格数** 81 | >也可以理解为:指针相差的大小 / sizeof(类型) 82 | 83 | 如果想输出两个指针 相差的距离(大小)只需要将变量类型 更改成普通类型,如下: 84 | ```c 85 | int main() { 86 | 87 | char arr1[10] = { ' 1', '2', '3', '4', '5', '6', '7', '8', '9', '10', }; 88 | int arr2[5] = { 1, 2, 3, 4, 5 }; 89 | 90 | //变量不再是指针变量 91 | char ch1 = &arr1[4]; 92 | char ch2 = &arr1; 93 | int i1 = &arr2[4]; 94 | int i2 = &arr2; 95 | 96 | printf("ch1 - ch2 = %d\n", ch1 - ch2); 97 | printf("ch2 - ch1 = %d\n", ch2 - ch1); 98 | printf("\n"); 99 | printf("i1 - i2 = %d\n", i1 - i2); 100 | printf("i2 - i1 = %d\n", i2 - i1); 101 | 102 | } 103 | ``` 104 | 输出结果: 105 | 106 | ```c 107 | ch1 - ch2 = 4 108 | ch2 - ch1 = -4 109 | 110 | i1 - i2 = 16 111 | i2 - i1 = -16 112 | ``` 113 | **普通类型是无法进行解引用操作的** 114 | ##### 总结一下 115 | >指针 可以 加减常数,指针之间可以相减,可以比较(如:> == < >=等) 116 | >但是指针不能乘除,相加 **这是没有意义的** 117 | >举个很简单的例子,时间可以相减,但是时间乘除或者相加有什么意义呢? 118 | 119 | #### NULL 120 | >通过前面的学习,我们知道:内存中的地址有很多编号。如果你的机器是 32 位, 121 | >那么内存范围是:`0 ~ 2^32 -1`(32位2进制数全1) 最大值大约为 4GB 122 | >**NULL其实就表示 0地址** 123 | >补充个小知识点: 124 | > 1kB=1024B =2^10(次方是二进制形式) 125 | > 1MB=1024kB =2^20 126 | > 1GB=1024MB =2^30 127 | > 1TB=1024GB =2^40 128 | 129 | ##### NULL有什么用? 130 | 0地址规定为我们不能写入的地址,你的指针不指向 0地址,如果你的指针指向了 0地址 那么程序运行时会崩溃。基于这个特点,0地址 也就是NULL有了很重要的功能: 131 | > - 函数返回 NULL指示错误 132 | > - 防止野指针(什么是野指针?参考 [C语言复习巩固(五) 指针(初阶)](https://blog.csdn.net/qq_44954010/article/details/103742120))。用NULL初始化指针,如果指针使用时没有指向任何实际地址,程序崩溃。 133 | 134 | **NULL类型时 void * 可以设置任何类型为NULL** 135 | 下面的程序是官网上讲NULL时给出的例子: 136 | ```c 137 | #include 138 | #include 139 | int main(void) 140 | { 141 | // 能设置任何类型指针为 NULL 142 | int* p = NULL; 143 | struct S* s = NULL; 144 | void(*f)(int, double) = NULL; 145 | 146 | // 多数返回指针的函数用空指针指示错误 147 | char* ptr = malloc(10); 148 | if (ptr == NULL) printf("Out of memory"); 149 | free(ptr); 150 | } 151 | ``` 152 | 153 | #### void* 154 | void* 表示 不知道指向什么类型的 指针 155 | 比如: 156 | 157 | ```c 158 | int i = 1; 159 | int* p = &i; 160 | void* q = (void*)p; 161 | ``` 162 | 这么写并没有改变 p 所指向的变量的类型, 而是可以让程序用不同的眼光通过 p看它所指的变量。 163 | #### 指针类型的作用 164 | >1. 指针的类型决定了指针向前或者向后走一步有多大 165 | >2. 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节) 166 | 167 | (具体示例参考 [C语言复习巩固(五) 指针(初阶)](https://blog.csdn.net/qq_44954010/article/details/103742120)) 168 | 169 | *** 170 | 更多关于指针的可以参考我的其他篇文章: 171 | [C语言复习巩固(五) 指针(初阶)](https://blog.csdn.net/qq_44954010/article/details/103742120) 172 | [指针运算详解,const详解,NULL?void* 指针基础](https://mp.weixin.qq.com/s/x3un4tnaHSISUfP8n3V_6g) -------------------------------------------------------------------------------- /content/c-notes/浅谈 C 语言实现重载,多态和模板.md: -------------------------------------------------------------------------------- 1 | ### C 语言实现重载,多态和模板 2 | 3 | #### 为什么 C 语言不支持重载 4 | 5 | 这和 C 和 C++ 的函数名称修饰有关。编译(并汇编)一个 C 和 Cpp 程序,使用 `objdump -dS `命令查看 ELF 格式文件发现: 6 | 7 | ``` 8 | [root@SuperhandsomeChuan ~]# objdump -dS test.o 9 | 10 | 0000000000000000
: 11 | 12 | [root@SuperhandsomeChuan ~]# objdump -dS testcpp.o 13 | 14 | 0000000000000000 <_Z3sumii>: 15 | 16 | 0000000000000014 <_Z3sumdd>: 17 | ``` 18 | 19 | `gcc` 编译器下,C 程序的函数名没有变化,但是 Cpp 程序的函数名称有了参数相关的后缀,这使得重载的 sum 函数底层的函数名称不同,编译器可以区分。 20 | 21 | #### C 语言实现重载 22 | 23 | | 函数 | 描述 | 24 | | ----------------------- | ------------------------------------------------------------ | 25 | | col 3 is | right-aligned | 26 | | va_list arg_ptr | 定义一个可变参数列表指针 | 27 | | va_start(arg_ptr, argN) | 让arg_ptr指向参数argN | 28 | | va_arg(arg_ptr, type) | 返回类型为type的参数指针,并指向下一个参数 | 29 | | va_copy(dest, src) | 拷贝参数列表指针,src->dest, | 30 | | va_end(arg_ptr) | 清空参数列表,并置参数指针arg_ptr无效。每个va_start()必须与一个va_end()对应 | 31 | 32 | 参考文章:http://locklessinc.com/articles/overloading/ 33 | 34 | ```c 35 | #include 36 | #include 37 | 38 | void va_overload2(int p1, int p2) 39 | { 40 | printf("va_overload2 %d %d\n", p1, p2); 41 | } 42 | 43 | void va_overload3(int p1, int p2, int p3) 44 | { 45 | printf("va_overload3 %d %d %d\n", p1, p2, p3); 46 | } 47 | 48 | static void va_overload(int p1, int p2, ...) 49 | { 50 | if(p2 == 7) 51 | { 52 | va_list v; 53 | va_start(v, p2); 54 | 55 | int p3 = va_arg(v, int); 56 | va_end(v); 57 | va_overload3(p1, p2, p3); 58 | 59 | return; 60 | } 61 | va_overload2(p1, p2); 62 | } 63 | 64 | int main(void) 65 | { 66 | va_overload(1, 2); 67 | va_overload(1, 7, 4); 68 | 69 | return 0; 70 | } 71 | //输出: 72 | va_overload2 1 2 73 | va_overload3 1 7 4 74 | ``` 75 | 76 | 上面的代码,我们可以解析函数参数,然后选择调用 `va_overload2()` 或 `va_overload3()` 。POSIX 的 `open()` 函数在你的机器上也许有着类似的实现方式。 77 | 78 | 另一种 `va_args`常见的用法是接受数量没有限制参数,没有直接的可接受数量的说明符。通过 NULL 来结束参数列表,我们可以解析任意对我们函数的输入。 79 | 80 | ```c 81 | #include 82 | #include 83 | 84 | static void print_nt_strings(const char* s, ...) 85 | { 86 | va_list v; 87 | va_start(v, s); 88 | 89 | /* Stop on NULL */ 90 | while(s) 91 | { 92 | printf("%s\n", s); 93 | /* Grab next parameter */ 94 | s = va_arg(v, const char*); 95 | } 96 | 97 | va_end(v); 98 | } 99 | 100 | int main(void) 101 | { 102 | 103 | print_nt_strings("Hello", "World", "brrrrr~~~", NULL); 104 | 105 | return 0; 106 | } 107 | // 输出 108 | Hello 109 | World 110 | brrrrr~~~ 111 | ``` 112 | 113 | 114 | 115 | 上面的函数将会打印传递给他的 C 字符串。无论最后一个指针是否是 NULL 。这里的问题是需要记得最后在参数列表后加上 NULL 。如果它丢掉了,上面的函数将会把栈上的值当作 `const char*` 指针然后尝试将其打印出来。这回引发未定义行为,这可能会使这个程序崩溃。 116 | 117 | 一种解决办法是明确的说明有多少参数存在,将最后的参数 NULL 删除。 让用户人为的确定个数是不便且易错的。 118 | 119 | *这段我感觉自己翻译的不是很好,有兴趣可以自己去看原文* 120 | 121 | ```c 122 | #include 123 | #include 124 | 125 | static void print_nt_strings(int n, ...) 126 | { 127 | va_list v; 128 | va_start(v, n); 129 | int i; 130 | for(i = 0; i < n; i++) 131 | { 132 | const char* s = va_arg(v, const char*); 133 | 134 | printf("%s\n", s); 135 | } 136 | 137 | va_end(v); 138 | } 139 | 140 | int main(void) 141 | { 142 | 143 | print_nt_strings(3, "Hello", "World", "brrr~~"); 144 | print_nt_strings(2, "Gooble", "World"); 145 | 146 | return 0; 147 | } 148 | //输出 149 | Hello 150 | World 151 | brrr~~ 152 | Gooble 153 | World 154 | ``` 155 | 156 | 157 | 158 | #### C 语言实现多态 159 | 160 | 下面程序的本质就是 C++ 多态的实现 161 | 162 | ```c 163 | #include 164 | 165 | typedef void (*Func1)(); 166 | typedef void (*Func2)(); 167 | 168 | // 虚函数表 169 | typedef struct VFunTable 170 | { 171 | Func1 eat; 172 | Func2 sleep; 173 | }VFunTable; 174 | 175 | // 基类 176 | typedef struct base 177 | { 178 | VFunTable vptr; 179 | const char kind[20]; 180 | }Base; 181 | 182 | // 派生类 183 | typedef struct derive 184 | { 185 | Base base; 186 | const char kind[20]; 187 | }Derive; 188 | 189 | void base_eat() 190 | { 191 | printf("base eat\n"); 192 | } 193 | 194 | void base_sleep() 195 | { 196 | printf("base sleep\n"); 197 | } 198 | 199 | void derive_eat() 200 | { 201 | printf("derive eat\n"); 202 | } 203 | 204 | void derive_sleep() 205 | { 206 | printf("derive sleep\n"); 207 | } 208 | 209 | void init(Base* b, Derive* d) 210 | { 211 | b->vptr.eat = base_eat; 212 | b->vptr.sleep = base_sleep; 213 | 214 | d->base.vptr.eat = derive_eat; 215 | d->base.vptr.sleep = derive_sleep; 216 | } 217 | 218 | int main(void) 219 | { 220 | struct base b; 221 | Derive d; 222 | init(&b, &d); 223 | 224 | Base* pb = &b; 225 | Base* pb2 = (Base*)&d; 226 | 227 | pb->vptr.eat(); 228 | pb2->vptr.eat(); 229 | 230 | return 0; 231 | } 232 | //输出 233 | base eat 234 | derive eat 235 | ``` 236 | 237 | 238 | 239 | #### C 语言实现模板 240 | 241 | `##` 运算符可以将两个表达式“拼”起来 242 | 243 | ```c 244 | #include 245 | 246 | #define GENERATE(type) \ 247 | type type##_max(type A, type B) \ 248 | { \ 249 | return (A > B ? A : B); \ 250 | } \ 251 | 252 | GENERATE(int); 253 | GENERATE(double); 254 | 255 | int main(void) 256 | { 257 | 258 | printf("%d\n", int_max(3, 5)); 259 | printf("%f\n", double_max(3.0, 5.0)); 260 | 261 | return 0; 262 | } 263 | ``` 264 | 265 | 看一下预处理后的代码: 266 | 267 | ```c 268 | int int_max(int A, int B) { return (A > B ? A : B); }; 269 | double double_max(double A, double B) { return (A > B ? A : B); }; 270 | 271 | int main(void) 272 | { 273 | 274 | printf("%d\n", int_max(3, 5)); 275 | printf("%f\n", double_max(3.0, 5.0)); 276 | 277 | return 0; 278 | } 279 | ``` 280 | 281 | 参考资料: 282 | 283 | 《C 语言程序设计 —— 现代方法》 284 | 285 | http://locklessinc.com/articles/overloading/ 286 | 287 | https://blog.csdn.net/gatieme/article/details/50921577 288 | 289 | https://www.cnblogs.com/qingergege/p/9594432.html 290 | -------------------------------------------------------------------------------- /content/c-notes/这些关于数组的基础知识点你都知道吗.md: -------------------------------------------------------------------------------- 1 | 各位同学,你觉得你数组学会了吗?不妨看看下面的问题,你能看一眼程序就回答上来吗? 2 | 3 | 引子:观察下面的程序,这个程序有安全隐患吗? 4 | 5 | ``` 6 | #include 7 | 8 | int main() { 9 | 10 | int x = 0; 11 | double sum = 0; 12 | int number[100] = { 0 }; 13 | int cnt = 0; 14 | 15 | scanf("%d", &x); 16 | while (x != -1) { 17 | number[cnt] = x; 18 | sum += x; 19 | cnt++; 20 | scanf("%d", &x); 21 | } 22 | 23 | if (cnt > 0) { 24 | int i = 0; 25 | double average = sum / cnt; 26 | for (i = 0; i < cnt; i++) { 27 | if (number[i] > average) 28 | printf("number %d: %d\n", i, number[i]); 29 | } 30 | } 31 | 32 | } 33 | ``` 34 | 35 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 36 | 37 | > 答案是有的 `while`循环种没有限制 `cnt` 有可能导致 **数组越界**! 38 | 39 | **不能快速找到错误和找不到错误其实是一样的**,因为不能快速找到这个错误说明你没有深刻的理解数组。这种基础的概念如果没有渗透到你的脑中,并不能说自己学好了数组吧!我学了一学期C,课设1000行代码都是自己独立完成的。依然没有立刻看出这个问题来,我也是自愧没有学好啊! 40 | 41 | ### 数组特性与一个注意 42 | 43 | > 1.数组是一种容器(放东西的东西) 44 | > 2.基本特点是: 45 | > 46 | > - 其中所有元素具有相同的数据类型 47 | > - 一旦创建,不能改变大小 48 | > - 在内存中连续依次排列 49 | > 50 | > ------ 51 | > 52 | > 3.注意: 53 | > **数组作为函数参数时,往往必须再用另一个参数来传入数组的大小** 54 | > 我们常用`sizeof(arr) / sizeof(arr[0])`来判断数组元素个数 55 | > 但是这种情况下不能在函数中用`sizeof(arr)`判断数组大小 56 | 57 | 例1 58 | 59 | //写一个程序,输入数量不确定的【0 ~ 9】范围内的整数,统计每一种数字出现的次数,输入 -1 表示结束 60 | 61 | 方法一:先看一个基础做法 62 | 63 | ``` 64 | #include 65 | 66 | int main() { 67 | 68 | const int number = 10;//记录数组元素。用const修饰,数组大小规定后不可改变 69 | int count[10] = { 0 }; 70 | int i = 0; 71 | int input = 1; 72 | 73 | while (input + 1) {//避免输入0时退出,-1 + 1 = 0 刚好满足退出要求 74 | printf("input a number\n"); 75 | scanf("%d",&input); 76 | switch (input) { 77 | case 0: 78 | count[0]++; 79 | break; 80 | case 1: 81 | count[1]++; 82 | break; 83 | case 2: 84 | count[2]++; 85 | break; 86 | case 3: 87 | count[3]++; 88 | break; 89 | case 4: 90 | count[4]++; 91 | break; 92 | case 5: 93 | count[5]++; 94 | break; 95 | case 6: 96 | count[6]++; 97 | break; 98 | case 7: 99 | count[7]++; 100 | break; 101 | case 8: 102 | count[8]++; 103 | break; 104 | case 9: 105 | count[9]++; 106 | break; 107 | default: 108 | break; 109 | } 110 | 111 | 112 | 113 | for (i = 0; i < 10; i++) 114 | printf("%d:%d times\n", i, count[i]); 115 | 116 | return 0; 117 | } 118 | ``` 119 | 120 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 121 | 122 | 方法2 123 | 124 | 当我们要统计的数是像 0 ~ 9 这样连续的数时,我们可以把数组下标与这些数一一对应起来,可以更方便,快捷 125 | 126 | ``` 127 | #include 128 | 129 | int main() { 130 | 131 | int count[10] = { 0 }; 132 | int i = 0; 133 | int input = 1; 134 | int error = 0; 135 | 136 | 137 | //更简单的方法: 138 | while (input + 1) { 139 | printf("input a number\n"); 140 | scanf("%d", &input); 141 | if (input >= 0 && input <= 9) 142 | count[input]++; 143 | if (input == -1) 144 | break; 145 | } 146 | 147 | for (i = 0; i < 10; i++) { 148 | printf("%d:%d times\n", i, count[i]); 149 | } 150 | 151 | return 0; 152 | } 153 | ``` 154 | 155 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 156 | 157 | ------ 158 | 159 | > **思考一下:字符常量可以做数组下标吗?** 160 | > 例如,形如arr['a'] 可以吗? 161 | 162 | 如果可以,那么当我们想要统计字符串种某个字母(或者任何ASCII码表上存在的字符)的具体个数时,就会很方便。可以自己尝试着写一下哦~思路和上面的数字判断差不多。 163 | 我写的供大家参考:[代码](https://github.com/hairrrrr/win.ccode/blob/master/Pactise/2020WinterVacation/Array/%E7%BB%9F%E8%AE%A1%E6%AF%8F%E4%B8%AA%E5%AD%97%E6%AF%8D%E5%87%BA%E7%8E%B0%E7%9A%84%E6%AC%A1%E6%95%B0%EF%BC%88%E5%8F%AF%E6%8B%93%E5%B1%95%E5%88%B0%E7%BB%9F%E8%AE%A1%E6%89%80%E6%9C%89ascii%E8%A1%A8%E5%86%85%E5%87%BA%E7%8E%B0%E7%9A%84%E5%AD%97%E7%AC%A6%E6%AC%A1%E6%95%B0%EF%BC%89.c) 164 | 165 | ------ 166 | 167 | 例2 168 | 169 | ``` 170 | #include 171 | 172 | #define N 10 //数组元素个数 173 | 174 | int search(int want, int lenth, int* arr) { 175 | 176 | int right = lenth - 1; 177 | int left = 0; 178 | int mid = 0; 179 | int ret = 0; 180 | 181 | while (right >= left) { 182 | mid = (right + left) / 2; 183 | if (want > arr[mid]) 184 | left = mid + 1; 185 | if (want < arr[mid]) 186 | right = mid - 1; 187 | if (want == arr[mid]) { 188 | ret = mid; 189 | break; 190 | } 191 | 192 | } 193 | 194 | if (right < left) 195 | return -1; 196 | else 197 | return ret; 198 | } 199 | 200 | int main() { 201 | 202 | int arr[N] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 203 | int lenth = sizeof(arr) / sizeof(arr[0]);//计算数组大小 204 | int want = 0; 205 | int index = 0; 206 | 207 | printf("input the number you want to search\n"); 208 | scanf("%d", &want); 209 | 210 | index = search(want, lenth, arr);//切记:数组作为函数参数时,往往必须再用另一个参数来传入数组的大小 211 | 212 | if (index == -1) 213 | printf("Can't find!\n"); 214 | else 215 | printf("the index of %d: %d\n", want, index); 216 | 217 | return 0; 218 | } 219 | ``` 220 | 221 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 222 | 223 | ------ 224 | 225 | ### 二维数组 226 | 227 | 1.初始化 228 | 229 | > `int a[][3] = {{1, 1, 1}, {2, 2, 2}, {3},}` 230 | > 1.列数必须给出,行数可以空出 231 | > 2.每行都有有一个单独的大括号 `{ }`(可以不写,建议写上) 232 | > 3.最后的逗号可以写上,老一代程序员们约定俗成的经验(如果你写上,可以装装逼) 233 | > 4.缺省表示补零 234 | > 5.强烈推荐的另一种书写方式: 235 | > 236 | > ```html 237 | > int a[][3] = { 238 | > {1, 1, 1}, 239 | > {2, 2, 2}, 240 | > {3}, 241 | > } 242 | > ``` 243 | > 244 | > ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 245 | > 246 | > 这样写的好处不言而喻,更加形象立体 247 | 248 | ### 练习题 249 | 250 | 1. 251 | 252 | 若有定义:int a[2][3].则下列不越界的正确访问有: 253 | A: a[2][0] 254 | B: a[2][3] 255 | C: a[1 > 2][0] 256 | D: a[0][3] 257 | 258 | 2. 259 | 260 | 以下程序片段输出的结果是: 261 | 262 | ``` 263 | int m[][3] = {1, 4, 7, 2, 5, 8, 3, 6, 9,} 264 | int i, j, k = 2; 265 | for( i = 0; i < 3; i++){ 266 | printf("%d",m[k][j]); 267 | } 268 | ``` 269 | 270 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 271 | 272 | A: 369 273 | B: 不能通过编译 274 | C:789 275 | D:能编译,但是数组下标越界了 276 | 277 | 3 278 | 279 | 若有int a[][3] = {{0}, {1}, {2}}; 280 | a[1][2] 的值是? 281 | 282 | > 答案:C A 0 -------------------------------------------------------------------------------- /content/c-notes/那些关于函数我们容易忽略的基础知识.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ------ 4 | 5 | 相信在学校同学们看谭老师的教材的时候已经对函数有了“初步的认识”。 6 | 但是,如果你没有理解下面这几个例子,那并不能说你对函数入门了。 7 | 8 | ------ 9 | 10 | ## 1.为什么要声明函数? 11 | 12 | ``` 13 | #include 14 | 15 | void swap(); 16 | 17 | int main() { 18 | 19 | int a = 0; 20 | int b = 1; 21 | swap(a, b); 22 | return 0; 23 | 24 | } 25 | 26 | void swap(double x, double y) { 27 | 28 | double tep = x; 29 | x = y; 30 | y = tep; 31 | printf("a = %f b = %f\n", x, y); 32 | 33 | } 34 | ``` 35 | 36 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 37 | 38 | 上面代码`void swap();`就是swap函数的声明 39 | 40 | > 把对swap的声明写在main函数的上面是因为: 41 | > C的编译器从main函数的第一行开始,自上而下的分析你的代码。 42 | > 在看到`swap(a, b)`时,它需要知道`swap()`的样子 43 | > 也就是swap()的**返回类型**,**参数类型**,**参数个数** 44 | > 这样它才能检查你对swap()的调用是否正确 45 | 46 | 接下来我们讨论,如果编译器不知道函数的返回类型,参数类型,参数个数的话,编译能否继续进行? 47 | 下面我们看两个例子,分析编译器能否正确执行我们的代码 48 | 49 | ``` 50 | #include 51 | 52 | int main() { 53 | 54 | int a = 0; 55 | int b = 1; 56 | swap(a, b); 57 | return 0; 58 | 59 | } 60 | 61 | int swap(int x, int y) { 62 | 63 | int tep = x; 64 | x = y; 65 | y = tep; 66 | printf("a = %d b = %d\n", x, y); 67 | return 0; 68 | 69 | } 70 | ``` 71 | 72 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 73 | 74 | ------ 75 | 76 | ``` 77 | #include 78 | 79 | int main() { 80 | 81 | int a = 0; 82 | int b = 1; 83 | swap(a, b); 84 | return 0; 85 | 86 | } 87 | 88 | double swap(int x, int y) { 89 | 90 | int tep = x; 91 | x = y; 92 | y = tep; 93 | printf("a = %d b = %d\n", x, y); 94 | return 0; 95 | 96 | } 97 | ``` 98 | 99 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 100 | 101 | 第二种情况会报错: 102 | “swap”未定义,假设外部返回int 103 | “swap”重定义,不同的基类型 104 | 105 | > 在没有对函数进行声明的情况下 106 | > 旧标准会假设你所调用的函数所有的参数和返回类型都是int类型。 107 | > 如果不是,就会发生上述的问题 108 | 109 | ## 2.定义函数的方式 110 | 111 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9pYUpDckZtTVkyYmVxRldRcGdHY0pSSmliUnY0eUJ4RThVOHBtdGhha0lIYnpnSkNtZUVwaWN0OGE1U1RCV2dvaWFtWUl0aWFyT0JTY24yVnI0ZmYxWnlYRDhRLzY0MA?x-oss-process=image/format,png)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 112 | 113 | 请观察下面三种函数声明,这三种写法哪些是正确的? 114 | 115 | ``` 116 | //写法1: 117 | void swap(int x, int y); 118 | //写法2: 119 | void swap(int w, int z); 120 | //写法3: 121 | void swap(int,int); 122 | ``` 123 | 124 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 125 | 126 | > **答案是,这三种写法都是可以的。** 127 | > **事实上,编译器看的是第三种写法。而我们常用的第一种写法是为了让我们自己和别人更好的理解我们写的代码,这是良好的写代码习惯。** 128 | 129 | 然而还有这样一种声明方式,大家想一下,编译器可以识别吗? 130 | 131 | ``` 132 | void swap(); 133 | ``` 134 | 135 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 136 | 137 | > 答案是可以的。 138 | 139 | 这种写法可以说是这几种写法中最方便的写法,那么这种写法是不是无懈可击的呢? 140 | 如果我改变了临时变量a,b的类型再试试呢? 141 | 142 | ``` 143 | #include 144 | 145 | void swap(); 146 | 147 | int main() { 148 | 149 | int a = 0; 150 | int b = 1; 151 | swap(a, b); 152 | return 0; 153 | 154 | } 155 | 156 | void swap(double x, double y) { 157 | 158 | double tep = x; 159 | x = y; 160 | y = tep; 161 | printf("a = %f b = %f\n", x, y); 162 | 163 | } 164 | ``` 165 | 166 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 167 | 168 | 这回,输出的x,y就不我们所期望的了。为什么会这样?我在这里给出简单解释: 169 | 170 | > `main函数`执行到调用swap函数的地方的时候,由于我们在声明`swap函数`的时候并没有指定参数的类型,所以程序会猜测这两个变量的类型为`int`类型。而在函数定义的时候类型却为`double`,所以程序就有点“疑惑”了。 171 | > **所以,这种类型的写法是错误的!** 172 | 173 | ## 3.了解概念:块(block) 174 | 175 | 简单的来说,一个大括号`{ }`就是一个块 176 | 177 | ## 4.作用域和生存期的关系 178 | 179 | 如果变量在作用域内,那么它的生存期一定存在 180 | 如果变量的生存期还没结束,它不一定在作用域内 181 | 182 | ## 5.没有参数时 183 | 184 | 当一个函数没有参数,应如何表示呢? 185 | `void f(void)`还是`void f()`? 186 | 187 | > `void f(void)`表示函数没有参数 188 | > `void f()`表示参数未知 189 | 190 | ## 6.函数括号内的逗号与逗号运算符 191 | 192 | 我们思考一下,`f(a,b)`会不会引发歧义? 193 | 如果有人认为这个逗号表示的是逗号运算符怎么办? 194 | 195 | > `f(a,b)`表示函数f有a,b两个参数 196 | > `f((a,b))`这个逗号才是逗号运算符,表示函数f只有一个参数b 197 | 198 | ## 7.函数内是否可以定义函数 199 | 200 | C语言不允许函数内嵌套定义 201 | 202 | ## 8.return(i) 203 | 204 | 这样写会引起歧义,它的意思究竟是返回i还是调用return函数? 205 | 206 | ## 9.关于main函数 207 | 208 | - main函数也是函数,是被其函数调用的 209 | - main函数返回值是有意义的 210 | - `int main()`还是`int main(void)`? 211 | 212 | -------------------------------------------------------------------------------- /content/c-review/1-数据类型和变量.md: -------------------------------------------------------------------------------- 1 | ## 数据类型及大小 2 | 3 | char 字符型 1 byte 4 | 5 | short 短整型 2 byte 6 | 7 | int 整型 4 byte 8 | 9 | long 长整型 4 byte 10 | 11 | long long 更长的整型 8 byte 12 | 13 | float 单精度浮点型 4 byte 14 | 15 | double 双精度浮点型 8 byte 16 | 17 | long double 16 byte 18 | 19 | ### 看大小的程序: 20 | 21 | ``` 22 | #include 23 | int main(){ 24 | printf("%d\n",sizeof(long)); 25 | printf("%d\n",sizeof(long double)); 26 | } 27 | ``` 28 | 29 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 30 | 31 | ## 定义变量的位置 32 | 33 | ```c 34 | int main() 35 | { 36 | int num1 = 0; 37 | int num2 = 0; 38 | printf("请输入两个数字:>"); 39 | scanf("%d%d", &num1, &num2); 40 | int sum = num1+num2; 41 | printf("sum = %d\n", sum); 42 | 43 | return 0; 44 | } 45 | ``` 46 | 47 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 48 | 49 | 有的编译器会在这里提示错误,因为c规定对变量的声明应该在函数的开头。 50 | 51 | 修改后: 52 | 53 | ``` 54 | int main() 55 | { 56 | int num1 = 0; 57 | int num2 = 0; 58 | int sum = 0; 59 | printf("请输入两个数字:>"); 60 | scanf("%d%d", &num1, &num2); 61 | sum = num1+num2; 62 | printf("sum = %d\n", sum); 63 | 64 | return 0; 65 | } 66 | ``` 67 | 68 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 69 | 70 | 这样应该就不会有问题了 71 | 72 | ## 关于变量的作用域和生存期 73 | 74 | 注意变量的作用域 75 | 76 | ``` 77 | int main() 78 | { 79 | { 80 | int a = 10; //对变量a的声明再括号内,所以a的作用域仅在括号内 81 | } 82 | printf("a = %d\n", a); 83 | 84 | return 0; 85 | } 86 | ``` 87 | 88 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 89 | 90 | 外部变量引用 91 | 92 | ``` 93 | //声明外部符号(仅需要再同一个项目的另一个c文件中声明这个变量即可,可以直接调用) 94 | extern int g_val; 95 | 96 | int main() 97 | { 98 | printf("g_val = %d\n", g_val); 99 | return 0; 100 | } 101 | ``` 102 | 103 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 104 | 105 | ### 全局变量的注意事项:先声明,后引用 106 | 107 | 大家思考下面这个代码sum函数是否可以算出a+b的和 108 | 109 | ``` 110 | #include 111 | 112 | int sum(int x){ 113 | 114 | return x+a; 115 | } 116 | int a=2; //a 声明在sum函数后 117 | int main(){ 118 | int b=2; 119 | 120 | printf("%d",sum(b)); 121 | return 0; 122 | } 123 | ``` 124 | 125 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 126 | 127 | 运算结果并不是我们像要的,应该如何修改呢? 128 | 129 | 只需要将对 a的声明放在要调用a的sum函数前即可 130 | 131 | 132 | 133 | ``` 134 | #include 135 | int a=2; // 136 | int sum(int x){ 137 | 138 | return x+a; 139 | } 140 | 141 | int main(){ 142 | int b=2; 143 | 144 | printf("%d",sum(b)); 145 | return 0; 146 | } 147 | ``` 148 | 149 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 150 | 151 | 也有例外,供大家思考 152 | 153 | ``` 154 | #include 155 | int sum(int x,int y){ 156 | return x+y; 157 | } 158 | int a=2; 159 | int main(){ 160 | int b=2; 161 | 162 | printf("%d",sum(a,b)); 163 | return 0; 164 | } 165 | ``` 166 | 167 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 168 | 169 | ## 总而言之,一定要在引用变量前声明变量,就算是全局变量 170 | 171 | ## 172 | 173 | ## 常量的类型 174 | 175 | ### 1.字面常量 176 | 177 | 如:3.14,"abc" ,'a' 178 | 179 | ### 2.const 修饰的常变量 180 | 181 | 声明方式const int num = 10 182 | 183 | 一般来说常变量无法再赋其他值 184 | 185 | 常变量依然是变量,并不是常量(int arr[num] = {0};这个语句是有错误提示的,证明num并不是常量) 186 | 187 | ### 3.#define定义的标识符常量 188 | 189 | ``` 190 | #include 191 | #define MAX 10 192 | int main(){ 193 | int a=MAX; 194 | printf("%d",a); 195 | } 196 | 197 | //输出结果为10 198 | ``` 199 | 200 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 201 | 202 | ### 4. 枚举常量 203 | 204 | ``` 205 | enum Sex 206 | { 207 | MALE, 208 | FEMALE=5, 209 | SECRET 210 | }; 211 | #include 212 | int main(){ 213 | printf("%d\n", MALE); 214 | printf("%d\n", FEMALE); 215 | printf("%d\n", SECRET); 216 | return 0; 217 | } 218 | ``` 219 | 220 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 221 | 222 | ![img](https://img-blog.csdnimg.cn/20191205005759730.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 223 | 224 | enum 枚举类型常量 如果没有赋予其他处置默认从0开始递增。 225 | 226 | ## 字符串的一个重要问题 227 | 228 | ```c 229 | char arr1[] = "abc"; 230 | char arr2[] = {'a', 'b', 'c'}; 231 | printf("%s\n", arr1); 232 | printf("%s\n", arr2); 233 | ``` 234 | 235 | 236 | 237 | 思考一下上述两个printf函数会输出什么?是否一样? 238 | 239 | 结果是 240 | 241 | arr1可以正常输出 242 | 243 | 但是arr2却在输出abc后出现了乱码(这个乱码是随机的,可能出现也可能不出现) 244 | 245 | 为什么会这样? 246 | 247 | 因为c语言中会在字符串后面自动加上空字符'\0',它代表着字符串的结束 248 | 249 | 再输出arr2过程中由于后面没有终止符,系统会继续向下搜寻,而后面是什么内容是未知的,因此会出现乱码。 250 | 251 | ```c 252 | printf("%d\n", strlen(arr1));//string length 253 | printf("%d\n", strlen(arr2)); 254 | ``` 255 | 256 | 257 | 258 | 字符串的长度都是3 259 | 260 | ## 转义字符的重要用法 261 | 262 | 如果我想输出目录“ c:\test\090\test.c”用下面代码输出是否可以? 263 | 264 | ``` 265 | #include 266 | int main(){ 267 | printf("c:\test\070\test.c"); 268 | } 269 | ``` 270 | 271 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 272 | 273 | 输出结果竟然是:c: est8 est.c 274 | 275 | 为什么会这样?因为‘\’是一个转义字符,‘\t’为水平制表符 会进行缩进 276 | 277 | 那么应该正确的输出我们想要的结果呢? 278 | 279 | 我们只需要再'\'之前再加上一个'\'再次进行转义就好了 280 | 281 | ### \ddd ddd表示1~3个八进制的数字 282 | 283 | ### \xdd dd表示2个十六进制数字 284 | 285 | 注:八进制数位上最大值为7 286 | 287 | 288 | 289 | 以上就是本次关于c语言要点的归纳,感谢观看 290 | 291 | -------------------------------------------------------------------------------- /content/c-review/2-数组.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 数组 4 | 5 | ### 1.数组的赋值 6 | 7 | int arr[10]={1,2}的意义为将数组arr前2个元素初始化为1,2后面的元素初始化为0。 8 | 9 | ![数组调试](https://img-blog.csdnimg.cn/20191209133038438.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 10 | 11 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 12 | 13 | ------ 14 | 15 | 16 | 17 | ## 操作符 18 | 19 | ### 1.算数操作符 20 | 21 | > \+ - * / % 22 | 23 | " / "除法运算 向下取整 24 | 25 | " % "求余运算 计算余数 26 | 27 | ### 2.移位操作符 28 | 29 | > \>> << 30 | 31 | \>> 左移 << 右移 表示2进制位左移右移 32 | 33 | ![img](https://img-blog.csdnimg.cn/20191209151332400.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 34 | 35 | ![img](https://img-blog.csdnimg.cn/20191209151507856.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 36 | 37 | 为方便理解,可以参考下图: 38 | 39 | ![img](https://img-blog.csdnimg.cn/20191209154117736.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 40 | 41 | ### 3.位操作符 42 | 43 | > & | ^ 44 | 45 | a.按位与 46 | 47 | 与的原理等同于数学中的且 48 | 49 | 按位就是按照变量的2进制位 50 | 51 | ![img](https://img-blog.csdnimg.cn/20191209154410365.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 52 | 53 | ![img](https://img-blog.csdnimg.cn/20191209160908751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 54 | 55 | b.按位或 56 | 57 | 101 5 58 | 59 | 011 3 60 | 61 | 111 7 62 | 63 | ![img](https://img-blog.csdnimg.cn/20191209161006972.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 64 | 65 | c.按位异或 66 | 67 | 101 5 68 | 69 | 011 3 70 | 71 | 110 6 72 | 73 | ![img](https://img-blog.csdnimg.cn/20191209161152280.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 74 | 75 | ### 赋值操作符 76 | 77 | > ### = += -= *= /= &= ^= |= >>= <<= 78 | 79 | ### 单目操作符 80 | 81 | > ! 逻辑反操作 82 | > 83 | > \- 负值 84 | > 85 | > \+ 正值 86 | > 87 | > & 取地址 88 | > 89 | > sizeof() 操作数的类型长度(以字节为单位) 90 | > 91 | > ++ 92 | > 93 | > \-- 94 | > 95 | > \* 间接访问操作符(解引用操作符) 96 | > 97 | > (类型) 强制类型转换 98 | > 99 | > ~ 对一个数的二进制按位取反 100 | 101 | ### 关系操作符 102 | 103 | > \> >= < <= != ==(不要写成=) 104 | 105 | ### 逻辑操作符 106 | 107 | > && || 108 | 109 | ![img](https://img-blog.csdnimg.cn/20191209164325187.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 110 | 111 | ### 条件操作符 112 | 113 | > exp1 ? exp2 : exp3 114 | 115 | ### 逗号表达式 116 | 117 | > exp1, exp2, exp3, …expN 118 | 119 | 从左到右依次计算,最终值等于最后一个表达式 120 | 121 | ### 下标引用、函数调用和结构成员 122 | 123 | > [] () . -> 124 | 125 | ## 常见关键字 126 | 127 | > auto break case char const continue default do double else enum exte 128 | > 129 | > rn float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while 130 | 131 | ### 1.typedef 132 | 133 | 类型重命名 134 | 135 | ```c 136 | //将unsigned int 重命名为uint_32, 所以uint_32也是一个类型名 137 | typedef unsigned int uint_32; 138 | 139 | int main() { 140 | //观察num1和num2,这两个变量的类型是一样的 141 | unsigned int num1 = 0; 142 | uint_32 num2 = 0; 143 | return 0; 144 | 145 | } 146 | ``` 147 | 148 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 149 | 150 | ### 2.static 151 | 152 | ```c 153 | //代码1 154 | #include 155 | void test() { 156 | int i = 0; 157 | i++; 158 | printf("%d ", i); 159 | } 160 | int main() { 161 | int i = 0; 162 | for(i=0; i<10; i++){ 163 | test(); 164 | } 165 | return 0; 166 | } 167 | //代码2 168 | #include 169 | void test() { 170 | //static修饰局部变量 171 | static int i = 0; 172 | i++; 173 | printf("%d ", i); 174 | } 175 | int main(){ 176 | int i = 0; 177 | for(i=0; i<10; i++){ 178 | test(); 179 | } 180 | return 0; 181 | } 182 | ``` 183 | 184 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 185 | 186 | 代码1 中会输出十个2。而代码2种则会输出0~9 187 | 188 | ------ 189 | 190 | 关于static 的思考 191 | 192 | ![img](https://img-blog.csdnimg.cn/20191209200257213.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 193 | 194 | 对于静态变量的声明应该在static语句中声明 195 | 196 | 197 | 198 | a)*一个全局变量被statica修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。 199 | extern int a (所要执行的源文件内声明)* 200 | 201 | *static int a (包含a的源文件内声明)* 202 | 203 | b)*一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用*。 204 | extern int add(int x,int y) 205 | 206 | ### 3.define定义的常量和宏 207 | 208 | ``` 209 | #define MAX 100//定义常量 210 | #define Add(x,y) (x+y)//定义函数 211 | #define Max(x,y) (x>y?x:y) 212 | ``` 213 | 214 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 215 | 216 | ## 指针 217 | 218 | ![img](https://img-blog.csdnimg.cn/20191209211129353.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 219 | 220 | 指针变量也需要地址存放。 221 | 222 | ![img](https://img-blog.csdnimg.cn/20191209212044627.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 223 | 224 | *结论:指针大小在32位平台是4个字节,64位平台是8个字节。* 225 | 226 | *printf("%p\n", p) 输出地址* 227 | 228 | ## 结构体 229 | 230 | ![img](https://img-blog.csdnimg.cn/20191209215525659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70)![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 231 | 232 | -------------------------------------------------------------------------------- /content/c-review/3-分支和循环.md: -------------------------------------------------------------------------------- 1 | **目录** 2 | 3 | [分支和循环语句](#分支和循环语句) 4 | 5 | 6 | 7 | [顺序结构](#顺序结构) 8 | 9 | [分支语句(选择结构)](#分支语句(循环结构)) 10 | 11 | [if语句](#if语句) 12 | 13 | [if语句建议以以下两种方式书写:](#if语句建议以以下两种方式书写:) 14 | 15 | [ 表达式内的关系操作符与逻辑操作符:](# 表达式内的关系操作符与逻辑操作符:) 16 | 17 | [ else:](# else:) 18 | 19 | [switch语句:](#switch语句:) 20 | 21 | [ 循环语句:](# 循环语句:) 22 | 23 | [while语句:](#while语句:) 24 | 25 | [while语句中的break与continue:](#while语句中的break与continue:) 26 | 27 | [ do...while语句](# do...while语句) 28 | 29 | [ for语句:](# for语句:) 30 | 31 | [答案:](#答案:) 32 | 33 | ------ 34 | 35 | # 分支和循环语句 36 | 37 | # 38 | 39 | ------ 40 | 41 | 42 | 43 | # 顺序结构 44 | 45 | 46 | 47 | # 分支语句(选择结构) 48 | 49 | ## 50 | 51 | ## if语句 52 | 53 | > 语法结构: 54 | > 55 | > 1.if(表达式) 56 | > 57 | > ​ 语句; 58 | > 59 | > 2.if(表达式) 60 | > 61 | > ​ 语句1; 62 | > 63 | > else 64 | > 65 | > ​ 语句2; 66 | > 67 | > 3.if(表达式) 68 | > 69 | > ​ 语句1; 70 | > 71 | > else if(表达式) 72 | > 73 | > ​ 语句2; 74 | > 75 | > else 76 | > 77 | > ​ 语句3; 78 | > 79 | > **表达式结果为非0(真)则执行语句* 80 | > 81 | > *{ }代表代码块* 82 | 83 | 84 | 85 | ### if语句建议以以下两种方式书写: 86 | 87 | ```c 88 | if(5 == a ){//这么写if结构更加清晰明了 89 | ...; //变量放在右侧可以避免少写= 90 | } 91 | else{ 92 | ...; 93 | } 94 | ``` 95 | 96 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 97 | 98 | > 注意:“==”与“=”的区别 99 | 100 | ### 表达式内的关系操作符与逻辑操作符: 101 | 102 | 示例: 103 | 104 | ![img](https://img-blog.csdnimg.cn/20191211000117967.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 105 | 106 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 107 | 108 | 18 <= age <= 30的程序中判断过程是: 109 | 110 | 18 <= age (40) 为真 所以左边部分变为1 111 | 112 | 1 <= 30 为真 值为 1 113 | 114 | 所以if内的语句可以被执行 115 | 116 | 正确写法: 117 | 118 | ![img](https://img-blog.csdnimg.cn/20191211000644277.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 119 | 120 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 121 | 122 | ### else: 123 | 124 | > else 与相邻最近的if匹配(就近) 125 | 126 | 127 | 128 | ![img](https://img-blog.csdnimg.cn/20191211001852375.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 129 | 130 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 131 | 132 | else会与第二个if匹配而不是第一个 133 | 134 | ![img](https://img-blog.csdnimg.cn/20191211002052895.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 135 | 136 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 137 | 138 | 正确的写代码规范十分重要,如下这样写就不会有问题: 139 | 140 | ![img](https://img-blog.csdnimg.cn/20191211002507559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 141 | 142 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 143 | 144 | 当然也是可以简化的: 145 | 146 | ![img](https://img-blog.csdnimg.cn/20191211002722224.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 147 | 148 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 149 | 150 | ## switch语句: 151 | 152 | > switch(整型常量表达式){ 153 | > 154 | > ​ case 1: 155 | > 156 | > ​ ........; 157 | > 158 | > ​ break; 159 | > 160 | > ​ case 2: 161 | > 162 | > ​ ........; 163 | > 164 | > ​ break; 165 | > 166 | > ​ case 3: 167 | > 168 | > ​ ........; 169 | > 170 | > ​ break; 171 | > 172 | > ​ ... 173 | > 174 | > ​ case n: 175 | > 176 | > ​ ........; 177 | > 178 | > ​ break; 179 | > 180 | > ​ default: 181 | > 182 | > ​ ........; 183 | > 184 | > ​ break; 185 | > 186 | > } 187 | 188 | 1.*switch中必须为整型的常量表达式* 189 | 190 | *2**.break语句建议在每个case后加上,避免以后修改时忘记添加* 191 | 192 | 3.default只能出现一次 193 | 194 | 思考: 195 | 196 | ```c 197 | #include 198 | int main() { 199 | int n = 1; 200 | int m = 2; 201 | switch (n) { 202 | case 1: 203 | m++; 204 | case 2: 205 | n++; 206 | case 3: 207 | switch (n) {//switch允许嵌套使用 208 | case 1: 209 | n++; 210 | case 2: 211 | m++; 212 | n++; 213 | break; 214 | } 215 | case 4: 216 | m++; 217 | break; 218 | default: 219 | break; 220 | } 221 | printf("m = %d, n = %d\n", m, n); 222 | return 0; 223 | } 224 | ``` 225 | 226 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 227 | 228 | # 循环语句: 229 | 230 | ## while语句: 231 | 232 | > while(表达式){ 233 | > 234 | > ​ 循环语句; 235 | > 236 | > } 237 | 238 | ### while语句中的break与continue: 239 | 240 | 以下给出三个示例 241 | 242 | 示例1: 243 | 244 | ![img](https://img-blog.csdnimg.cn/20191211010157563.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 245 | 246 | 247 | 248 | 示例2: 249 | 250 | ![img](https://img-blog.csdnimg.cn/2019121101035586.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 251 | 252 | 253 | 254 | 程序没有结束,这是一个死循环 255 | 256 | 示例3: 257 | 258 | ![img](https://img-blog.csdnimg.cn/20191211010836579.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) 259 | 260 | 261 | 262 | ------ 263 | 264 | ## do...while语句 265 | 266 | > do{ 267 | > 268 | > ​ 循环语句; 269 | > 270 | > }while(表达式);//分号不要忘记 271 | 272 | ------ 273 | 274 | 275 | 276 | ## for语句: 277 | 278 | > for( 初始化部分;条件判断部分;调整部分 ){ 279 | > 280 | > ​ 循环语句; 281 | > 282 | > } 283 | > 284 | > 1. 不可在for 循环体内修改循环变量,防止 for 循环失去控制。 285 | > 2. 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。 286 | > 287 | 288 | \1. 请问下面的代码中循环体执行几次? 289 | 290 | ```c 291 | #include 292 | int main() { 293 | int i = 0; 294 | int k = 0; 295 | for(i =0,k=0; k=0; i++,k++) 296 | k++; 297 | return 0; 298 | } 299 | ``` 300 | 301 | 302 | 303 | 2.下面的代码会输出什么? 304 | 305 | ```c 306 | #include 307 | int main(){ 308 | int i = 0; 309 | int j = 0; 310 | for(; i < 10; i++) 311 | for(; j < 10; j++) 312 | printf("%d ",j); 313 | return 0; 314 | } 315 | ``` 316 | 317 | 代码应该书写规范: 318 | 319 | ```c 320 | #include 321 | int main(){ 322 | int i = 0; 323 | int j = 0; 324 | for(i = 0; i < 10; i++) 325 | for(j = 0; j < 10; j++) 326 | printf("%d ",j); 327 | return 0; 328 | } 329 | ``` 330 | 331 | 332 | 333 | # 答案: 334 | 335 | > m=5,n=3 336 | > 337 | > 0 1 2 3 4 5 6 7 8 9(因为在第一次大循环中j已经变为10 后面的大循环中第二个循环是没有输出的) -------------------------------------------------------------------------------- /content/c-review/4-函数.md: -------------------------------------------------------------------------------- 1 | # 什么是函数? 2 | 3 | 这个大家自己思考吧(没必要去复制粘贴百度的定义到这里来。每个人有自己的理解,这个东西多用就会了) 4 | 5 | 函数(function) 通过实参(argument)初始 形参(parameter) 执行完函数体(function body) 返回(return value)一个值。(或者不返回) 6 | 7 | # 函数类型 8 | 9 | ## 1.库函数 10 | 11 | 提供给大家一个学习库函数的网站:http://www.cplusplus.com/reference/ 12 | 13 | ## 2.自定义函数 14 | 15 | 比如我们常用的 16 | 17 | int main(){ 18 | 19 | } 20 | 21 | 1.这个 int 就是返回值的类型 我们一般在main函数最后一行加 return 0 22 | 23 | 2.main 是函数名 可以自己起 24 | 25 | 3.()括号内 可以放形式参数 也可以不写 26 | 27 | 4.大括号内就是函数体 写函数的功能 定义函数 28 | 29 | ### 强调一下函数的声明与定义不一样! 30 | 31 | 声明就像你在main函数开头初始化变量 它的作用就是让main函数顺序执行到调用语句时知道这个你在前面说过,不至于让main函数很懵逼 只用写上面的1,2,3 32 | 33 | 如:int Max(int a, int b); 34 | 35 | 定义就需要具体实现这个函数的功能 1,2,3,4都需要写完 36 | 37 | ```c 38 | int Max(int a,int b){ 39 | 40 | return (a>b)?a:b;//返回a,b中较大的数 41 | 42 | } 43 | ``` 44 | 45 | 46 | 47 | # 形参与实参 48 | 49 | 这个不同大家自己百度就行。 50 | 51 | 自己可以在vs里调试看看你设置的形参与实参的地址(形参与实参地址时不一样的) 52 | 53 | # 嵌套调用 54 | 55 | 可以类比数学的复合函数f(g(x)) 56 | 57 | ```c 58 | #include 59 | #include 60 | #define e 2.7 61 | float g(int x){ 62 | return pow(e,x); 63 | } 64 | float f(int x){ 65 | return 2*g(x)-1; 66 | } 67 | int main(){ 68 | int x = 0; 69 | printf("计算2*e^x-1\n请输入 x:\n"); 70 | scanf("%d",&x); 71 | printf("f(%d) = %.2f",x,f(x)); 72 | } 73 | ``` 74 | 75 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 76 | 77 | # 链式访问 78 | 79 | 例1: 80 | 81 | ```c 82 | #include 83 | #include 84 | int main() { 85 | char arr[20] = "hello "; 86 | int ret = strlen(strcat(arr, "world")); 87 | printf("%d\n", ret); 88 | return 0; 89 | } 90 | ``` 91 | 92 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 93 | 94 | arr数组在经过strcat后变成了"hello world" 95 | 96 | 我们知道strlen读取的长度是11(不懂为什么可以按照我给的网站去查strlen ,strcat的用法,里面说的很到位) 97 | 98 | 例2: 99 | 100 | ```c 101 | #include 102 | int Max(int a,int b){ 103 | return (a>b)?a:b; 104 | } 105 | int main(){ 106 | int max = 0; 107 | max = Max(Max(7,Max(1,2)),6); 108 | printf("%d",max); 109 | } 110 | ``` 111 | 112 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 113 | 114 | 最后输出是7(其实直接看谁最大就好了,不知道原理也没事) 115 | 116 | 例3: 117 | 118 | ```c 119 | #include 120 | int main(){ 121 | printf("%d",printf("%d",printf("%d",printf("%d",43)))); 122 | //printf返回打印字符的个数 123 | //最里面的printf 打印43 返回2 124 | // 打印2 返回1 125 | // 打印1 返回1 126 | //最外面的 。。 。。。 127 | } 128 | ``` 129 | 130 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 131 | 132 | 我们从最内层开始看起: 133 | 134 | printf("%d,43")它会输出 43 135 | 136 | printf函数会返回它打印的字符数 所以它返回 2 137 | 138 | 我们看下一层: 139 | 140 | printf("%d",2); 141 | 142 | 这时打印 2 返回 1 143 | 144 | 继续下一层: 145 | 146 | printf("%d",1); 147 | 148 | 打印 1 返回 1 149 | 150 | 同理最外层会打印1 151 | 152 | 所以最总结果是:43211 153 | 154 | 总结一下:链式访问需要关注函数的返回值 155 | 156 | # 递归 157 | 158 | ## 什么是递归? 159 | 160 | 程序调用自身的编程技巧称为递归( recursion) 161 | 162 | ## 递归的两个必要条件 163 | 164 | 1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。 165 | 166 | 2.每次递归调用之后越来越接近这个限制条件 167 | 168 | 举个例子: 169 | 170 | 求第 n 个斐波那契数列 171 | 172 | 先用数组法 173 | 174 | 以我的经验 普通的做法往往能给递归法找到规律 175 | 176 | ```c 177 | #include 178 | int main(){ 179 | int arr[50] = {0}; 180 | int i = 0; 181 | int n = 0; 182 | arr[0] = 1; 183 | arr[1] = 1; 184 | printf("你想知道斐波那契而数列的第几个数:\n"); 185 | scanf("%d",&n); 186 | if(n==1 || n==2) 187 | printf("第 %d 个斐波那锲数列是 %d",n,1);//两类情况分开讨论一下 188 | else { 189 | for(i = 2; i 203 | int Fibonacci(int n){ 204 | if(n==1 || n==2) 205 | return 1; 206 | else 207 | return (Fibonacci(n-1)+Fibonacci(n-2)); 208 | } 209 | int main(){ 210 | int n = 0; 211 | printf("你想知道斐波那契数列的第几个:\n"); 212 | scanf("%d",&n); 213 | printf("%d",Fibonacci(n)); 214 | return 0; 215 | } 216 | ``` 217 | 218 | ![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 219 | 220 | 但是递归法做有的问题并不聪明==(比如这个问题) 221 | 222 | 因为如果给的n很大它会重复计算很多次,不断调用意味着更大的空间。可能造成栈溢出(stack overflow) 223 | 224 | 所以下面这个做法也许是解决这一类问题的好做法 225 | 226 | 迭代法: 227 | 228 | ```c 229 | #include 230 | int main(){ 231 | int Fir = 1; 232 | int Sec = 1; 233 | int Thi = 1; 234 | int N = 0; 235 | printf("请输入你想知道第几个斐波那契数列:\n"); 236 | scanf("%d",&N); 237 | while(N>2){ 238 | Thi = Fir + Sec; 239 | Fir = Sec; 240 | Sec = Thi; 241 | N--; 242 | } 243 | printf("%d",Thi); 244 | } 245 | ``` 246 | 247 | -------------------------------------------------------------------------------- /content/c-traps-and-pitfalls/01 词法陷阱.md: -------------------------------------------------------------------------------- 1 | ## 【C 陷阱与缺陷 】(一)词法陷阱 2 | 3 | 4 | 5 | 6 | 7 | ### 一 内容 8 | 9 | #### 0. `=`不同于`==` 10 | 11 | 当程序员本意是作比较运算时,却可能无意中误写成了赋值运算。 12 | 13 | 1.本意是检查 x 与 y 是否相等: 14 | 15 | ```c 16 | if(x = y) 17 | break; 18 | ``` 19 | 20 | 实际上是将 y 的值赋值给了 x ,然后再检查该值是否为 0 。 21 | 22 | 2.本意是跳过文件中的空白字符: 23 | 24 | ```c 25 | while(c = '' || c == '\t' || c == '\n') 26 | c = getc(f); 27 | ``` 28 | 29 | 因为 `' '`不等于 0 (`' '`的 ASCII 码值为 32),那么无论变量为何值,上述表达式求值的结果都为 1,因此循环将进行下去直到整个文件结束。 30 | 31 | 32 | 33 | C 编译器发现形如 x = y 的表达式出现在选择语句,循环语句的条件判断部分时,会给出警告。当确实需要对变量进行赋值时,为了避免警告,我们应该这样处理: 34 | 35 | ```c 36 | if((x = y) != 0) 37 | foo(); 38 | ``` 39 | 40 | 41 | 42 | 如果将赋值写成了比较,也会造成混淆: 43 | ```c 44 | if((filedesc == open(argv[i], 0)) < 0) 45 | error(); 46 | ``` 47 | 48 | 本例中,open 执行成功返回非零值,失败返回 -1。本意是将 open 函数的返回值存储在变量 filedesc 中,然后将其和 0 比较大小,判断 open 执行是否成功 。`==`运算符的结果只可能是 1 或 0,永远不会小于 0,所以 error() 将没有机会被调用。 49 | 50 | 51 | 52 | #### 1. `&`和`|`不同于`&&`和`||` 53 | 54 | 比较 `i & j` 和 `i && j` ,只要 i 和 j 是 0 或 1 ,两个表达式的值是一样的(`|` 和 `||` 同理。)。然而,一旦 i 和 j 的值为其他,两个表达式的值不会始终一致。 55 | 56 | 另一个区别是操作数带有自增自减的运算: 57 | 58 | `i & j++`, j 始终会自增;但是 `i && j++` 有时 j 不会自增。 59 | 60 | 61 | 62 | #### 2. 词法分析中的“贪心法” 63 | 64 | 当 C 的编译器读入一个字符`/`后跟着一个字符`*`时,那么编译器就必须做出判断:时将其作为两个符号对待,还是合起来作为一个符号对待。这类问题的规则:**每个符号应该包含尽可能多的符号**。 65 | 66 | 例如:`a---b`和`(a--) - b`含义相同,而与`a - (--b)`含义不同。 67 | 68 | 又如:下面的语句本意是 x 除以 p 指向的值然后将结果赋值给 y 69 | 70 | ```c 71 | y = x/*p; 72 | ``` 73 | 74 | 但是,实际上 `/*`被编译器理解为一段注释的开始。 75 | 76 | 将上面的语句重写如下: 77 | 78 | ```c 79 | y = x / *p; 80 | ``` 81 | 82 | 或者: 83 | 84 | ```c 85 | y = x/(*p); 86 | ``` 87 | 88 | 老版本的编译器允许使用`=+`来代表现在`+=`的含义,这种编译器会将: 89 | 90 | ```c 91 | a=-1; 92 | ``` 93 | 94 | 理解为: 95 | 96 | ```c 97 | a =- 1; 98 | ``` 99 | 100 | 即为: 101 | 102 | ```c 103 | a = a - 1; 104 | ``` 105 | 106 | 因此,如果程序员的原意为: 107 | 108 | ```c 109 | a = -1; 110 | ``` 111 | 112 | 那么结果会让其大吃一惊。 113 | 114 | 再如: 115 | 116 | ```c 117 | a=/*b; 118 | ``` 119 | 120 | 在老版本的编译器会将其当作: 121 | 122 | ```c 123 | a =/ *b; 124 | ``` 125 | 126 | 127 | 128 | #### 3. 整型常量 129 | 130 | 许多编译器会把 8 和 9 作为把八进制的数字处理,这种处理方式来源于八进制数的定义。例如:0195 的含义是`1x8^2 + 9x8 + 5x8^0`也就是 141(十进制)或 0215(八进制)。**ANSI C 标准中禁止这种用法。** 131 | 132 | 133 | 134 | #### 4. 字符与字符串 135 | 136 | **单引号引起的一个字符实际上代表一个整数**。整数值对应于该字符在编译器采用的字符集中的序列值。因此,对于采用 ASCII 字符集的编译器而言,`'a'`的含义与 97 (十进制)严格一致。 137 | 138 | **用双引号引起的字符串,代表的确实一个指向无名数组起始字符的指针**。该数组被双引号之间的字符以及一个额外的二进制值为 0 的字符`\0`初始化。 139 | 140 | 比如,下面的这个语句: 141 | 142 | ```c 143 | printf("Hello World\n"); 144 | ``` 145 | 146 | 等价于: 147 | 148 | ```c 149 | char hello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\n', 0}; 150 | printf(hello); 151 | ``` 152 | 153 | 154 | 155 | 整数型(一般为 16 或 32 位)的存储空间可以容纳多个字符(一般为 8 位),因此有的编译器允许在一个字符常量(以及字符串常量)中包含多个字符。也就是说:用`'yes'`代替`"yes"`不会被该编译器检测到。前者的含义大多数编译器理解为一个整数值,由`'y','e','s'`所代表的整数值按照特定编译器实现中的定义方式组合得到。 156 | 157 | 158 | 159 | ### 二 练习 160 | 161 | #### 练习 1 162 | 163 | 某些 C 编译器允许嵌套注释。请写一个测试程序,要求:无论编译器是否允许嵌套注释,该程序都能正常通过编译,但是两种情况下程序执行结果不同。 164 | 165 | 对于符号序列: 166 | 167 | ```c 168 | /*/**/"*/" 169 | ``` 170 | 171 | 如果允许嵌套注释,上面的符号序列表示:一个单独的双引号`"`,因为最后的注释符前出现的符号都会被当作注释的一部分。 172 | 173 | 如果不允许嵌套注释,上面的符号就表示一个字符串:`"*/"` 174 | 175 | Doug Mcllroy 发现了下面这个令人拍案叫绝的解法: 176 | 177 | ```c 178 | /*/*/0 */**/1 179 | ``` 180 | 181 | 这个解法主要利用了编译器作词发分析时的“贪心法”规则。 182 | 183 | 如果编译器允许嵌套注释,则将上式解释为: 184 | 185 | ```c 186 | /* /*/0 */ * */ 1 187 | ``` 188 | 189 | 上式的值为 1 190 | 191 | 如果编译器不允许嵌套注释,则解释为: 192 | 193 | ```c 194 | /* / */ 0 * /**/ 1 195 | ``` 196 | 197 | 也就是 `0*1`,值为 0 198 | 199 | 200 | 201 | #### 练习 2 202 | 203 | `a+++++b` 的含义是什么? 204 | 205 | 上式唯一有意义的解析方式就是: 206 | 207 | ```c 208 | a++ + ++b 209 | ``` 210 | 211 | 可是,根据“贪心法”的规则,上式应该被解释为: 212 | 213 | ```c 214 | a++ ++ + b 215 | ``` 216 | 217 | 等价于: 218 | 219 | ```c 220 | (a++)++ + b; 221 | ``` 222 | 223 | 但是 `a++`的值不能作为左值,因此编译器不会接受 a++ 作为后面 ++ 运算的操作数。 224 | 225 | 226 | 227 | **参考资料**:*《C 缺陷与陷阱》* 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /content/c-traps-and-pitfalls/02 语法陷阱.md: -------------------------------------------------------------------------------- 1 | ## 【C 陷阱与缺陷 】(二)语法“陷阱” 2 | 3 | 4 | 5 | ### 零 6 | 7 | #### 0. 理解函数声明 8 | 9 | 请思考下面语句的含义: 10 | 11 | ```c 12 | (*(void(*)())0)() 13 | ``` 14 | 15 | 前面我们说过 C 语言的声明包含两个部分:类型和类似表达式的声明符。 16 | 17 | 最简单的声明符就是单个变量: 18 | 19 | ```c 20 | float f, g; 21 | ``` 22 | 23 | 由于声明符和表达式的相似,我们可以在声明符中任意使用括号: 24 | 25 | ```c 26 | float ((f)); 27 | ``` 28 | 29 | 这个声明的含义是:当对 f 求值时,`((f))`的类型为 float 类型,可以推知 `f` 也是浮点类型。 30 | 31 | 同样的,我们可以声明函数: 32 | 33 | ```c 34 | float ff(); 35 | ``` 36 | 37 | 这个声明的含义是:表达式 `ff()`求值结果是 float 类型,也就是返回 float 类型的函数。 38 | 39 | 类似的: 40 | 41 | ```c 42 | float *pf; 43 | ``` 44 | 45 | 这个声明的含义是:`*pf`是一个 float 类型的数,也就是说 pf 是指向 float 类型的指针。 46 | 47 | 以上的声明可以结合起来: 48 | 49 | ```c 50 | float *g(), (*h)(); 51 | ``` 52 | 53 | `*g()`和`(*h)()`是浮点表达式。因为`()`(和`[]`)的优先级高于`*`。`*g()`也就是`*(g())`:g 是一个函数,该函数返回一个指向浮点数的指针。同理,可以得到 h 是一个函数指针,h 所指向的函数返回值为浮点类型。 54 | 55 | 56 | 57 | 一旦我们知道如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到:**只需要把声明中的变量名和声明末尾的分号去掉,再用括号整体括起来**。 58 | 59 | 比如: 60 | 61 | ```c 62 | float (*h)(); 63 | 64 | (float (*)())p; 65 | ``` 66 | 67 | 68 | 69 | 假定变量 fp 是一个函数指针,那么如何调用 fp 所指向的函数呢?调用方法如下: 70 | 71 | ```c 72 | (*fp)(); 73 | ``` 74 | 75 | *fp 就是该指针所指向的函数。ANSI C 标准允许将上式简写为: 76 | 77 | ```c 78 | fp(); 79 | ``` 80 | 81 | 但是要记住这是一种简写方法。 82 | 83 | 注意:`(*fp)()`和`*fp()`的含义完全不同,不要省略 *fp 两侧的分号。 84 | 85 | 86 | 87 | 现在我们声明一个返回值为 void 类型的函数指针: 88 | 89 | ```c 90 | void (*fp)(); 91 | ``` 92 | 93 | 如果我们现在要调用存储位置为 0 的子例程,我们是否可以这样写: 94 | 95 | ```c 96 | (*0)(); 97 | ``` 98 | 99 | 上式并不能生效,因为运算符 * 需要一个函数指针作为操作数。我们需要对 0 进行类型转换: 100 | 101 | ```c 102 | (* (void (*)())0 )(); 103 | ``` 104 | 105 | 106 | 107 | 我们可以使用 `typedef`来使表述更加清晰: 108 | 109 | ```c 110 | typedef void (*funcptr)(); 111 | (*(funcptr)0)(); 112 | ``` 113 | 114 | 115 | 116 | #### 1. 运算符优先级问题 117 | 118 | ```c 119 | if(FLAG & flags != 0){ 120 | ... 121 | } 122 | ``` 123 | 124 | FLAG 是一个已经定义的常量,FLAG 是一个整数,该数的二进制表示中只有某一位是 1,其余的位都为 0 ,也就是 2 的某次幂。为了判断整数 flags 的某一位是否也是 1,并且将结果与 0 作比较,我们写出了上面 if 的判断表达式。 125 | 126 | 但是`!=`的优先级高于`&`,上面的式子被解释为: 127 | 128 | ```c 129 | if(FLAG & (flags != 0)){ 130 | ... 131 | } 132 | ``` 133 | 134 | 这显然不是我们想要的。 135 | 136 | high 和 low 是两个 0 ~ 15 的数,r 是一个八位整数,且 r 的低 4 位与 low 一致,高 4 位与 high 一致,很自然想到: 137 | 138 | ```c 139 | r = high<<4 + low; 140 | ``` 141 | 142 | 但是,加法的优先级高于移位运算,本例相当于: 143 | 144 | ```c 145 | r = high<<(4 + low); 146 | ``` 147 | 148 | 对于这种情况,有两种更正方法: 149 | 150 | ```c 151 | r = (high<<4) + low; 152 | ``` 153 | 154 | 或利用移位运算的优先级高于逻辑运算: 155 | 156 | ```c 157 | r = high<<4 | low; 158 | ``` 159 | 160 | 161 | 162 | ![](https://hairrrrr.github.io/assets/2020-11-29-1.jpg) 163 | 164 | 165 | 166 | 下面我们说几个比较常见的运算符的用法: 167 | 168 | - `a.b.c`的含义是`(a.b).c`而不是`a.(b.c)` 169 | 170 | - 函数指针要写成:`(*p)()`,如果写成了`*p()`,编译器会解释为:`*(p())` 171 | 172 | - `*p++`会解释为:`*(p++)`而不是`(*p)++` 173 | 174 | - 记住两点: 175 | 176 | - 任何一个逻辑运算符的优先级低于任何一个关系运算符。 177 | - 移位运算符的优先级比算数运算符要低,但是高于关系运算符。 178 | 179 | - 赋值运算符结合方式从右到左,因此: 180 | 181 | ```c 182 | a = b = 0; 183 | ``` 184 | 185 | 等价于: 186 | 187 | ```c 188 | b = 0; 189 | a = b; 190 | ``` 191 | 192 | - 关于涉及赋值运算时优先级的混淆: 193 | 194 | 复制一个文件到另一个文件中: 195 | 196 | ```c 197 | while(c = getc(in) != EOF) 198 | putc(c, out); 199 | ``` 200 | 201 | 但是上式被解释为: 202 | 203 | ```c 204 | while(c = (getc(in) != EOF)) 205 | putc(c, out); 206 | ``` 207 | 208 | 关系运算符的结果只有 0 或 1 两种可能。最后得到的文件副本中只包含了一组二进制为 1 的字节流。 209 | 210 | 211 | 212 | #### 2. 注意作为语句结束标志的分号 213 | 214 | 考虑下面的例子: 215 | 216 | ```c 217 | if(x[i] > big); 218 | big = x[i]; 219 | ``` 220 | 221 | 这与: 222 | 223 | ```c 224 | if(x[i] > big) 225 | big = x[i]; 226 | ``` 227 | 228 | 大不相同。 229 | 230 | 前面的例子相当于: 231 | 232 | ```c 233 | if(x[i] > big) {} 234 | big = x[i]; 235 | ``` 236 | 237 | 无论 x[i] 是否大于 big,赋值都会被执行。 238 | 239 | 240 | 241 | 如果不是多写了分号,而是遗漏了分号,一样会招致麻烦: 242 | 243 | ```c 244 | if( n < 3) 245 | return 246 | logrec.date = x[0]; 247 | logrec.time = x[1]; 248 | logrec.code = x[2]; 249 | ``` 250 | 251 | 遗漏了 return 后的分号,这段程序仍然会顺利通过编译而不会报错,它等价于: 252 | 253 | ```c 254 | if( n < 3) 255 | return logrec.date = x[0]; 256 | logrec.time = x[1]; 257 | logrec.code = x[2]; 258 | ``` 259 | 260 | 261 | 262 | 还有一种情形,也是有分号与没有分号实际效果相差极为不同。那就是当一个声明的结尾紧跟一个函数定义时,如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型。考虑下例: 263 | 264 | ```c 265 | struct logrec{ 266 | int date; 267 | int time; 268 | int code; 269 | } 270 | main(){ 271 | 272 | } 273 | ``` 274 | 275 | 上面代码段的实际效果是声明函数 main 返回值是结构 logrec 类型。 276 | 277 | 如果分号没有被省略,函数 main 的返回值类型会缺省定义为 int 类型。 278 | 279 | 280 | 281 | #### 3. switch 语句 282 | 283 | ```c 284 | switch(color){ 285 | case 1: printf("red"); 286 | break; 287 | case 2: printf("blue"); 288 | break; 289 | case 3: printf("yellow"); 290 | break; 291 | } 292 | ``` 293 | 294 | 如果稍作改动: 295 | 296 | ```c 297 | switch(color){ 298 | case 1: printf("red"); 299 | case 2: printf("blue"); 300 | case 3: printf("yellow"); 301 | } 302 | ``` 303 | 304 | 假定 color 的值为 2,那么将会输出: 305 | 306 | ```c 307 | blueyellow 308 | ``` 309 | 310 | 因为程序的控制流程在执行了第二个 printf 函数的调用后,会自然地顺序执行下去。第三个 printf 函数也会被调用。 311 | 312 | 313 | 314 | switch 的这种特性,即使它的弱点,也是它的优势所在。 315 | 316 | 对于两个操作数的加减运算,我们可以将操作数变号来取代减法: 317 | 318 | ```c 319 | case SUBTRACT: 320 | opnd2 = -opnd2; 321 | case ADD: 322 | ... 323 | ``` 324 | 325 | 在这里,我们是有意省略 break 语句。 326 | 327 | 328 | 329 | #### 4. 函数调用 330 | 331 | C 语言要求:在函数调用时,即使函数不带参数,也应该包含参数列表。如果,f 是一个函数: 332 | 333 | ```c 334 | f(); 335 | ``` 336 | 337 | 是一个函数调用语句,而: 338 | 339 | ```c 340 | f; 341 | ``` 342 | 343 | 却是一个什么也不作的语句,f 表示函数的地址。 344 | 345 | 346 | 347 | #### 5. 悬挂 else 引发的问题 348 | 349 | 这个相信大家学习 C 的时候老师都会讲,在我的 【C 必知必会】系列教程中也有详细讲解,不懂可以去参考相关。 350 | 351 | 这里说一点,写 if 语句时,不要省略括号是一种可以学习的习惯。 352 | 353 | 354 | 355 | **参考资料**:*《C 缺陷与陷阱》* 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | -------------------------------------------------------------------------------- /content/c-traps-and-pitfalls/05 库函数.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [C 陷阱与缺陷] (五) 库函数 4 | 5 | 6 | 7 | C语言中没有定义输入/输出语句,任何一个有用的 C 程序(起码必须接受零个或多个输入,生成一个或多个输出)都必须调用库函数来完成最基本的输入和输出操作。ANSI C 标准毫无疑问地意识到了这一点, 因而定义了一个包含大量标准库函数的集合。从理论上说,任何一个 C 语言实现都应该提供这些标准库函数。 8 | 9 | 有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件。 10 | 11 | ### 一 库函数 12 | 13 | #### 1. 返回整数的 getchar 函数 14 | 15 | ```c 16 | #include 17 | 18 | main(void){ 19 | char c; 20 | 21 | while((c = getchar()) != EOF) 22 | putchar(c); 23 | } 24 | ``` 25 | 26 | getchar 函数在一般情况下返回的是标准输入文件中的下一个字符,当没有输入时返回EOF (一个在头文件stdio.h 中被定义的值,不同于任何一个字符)。这个程序乍一看似乎是把标准输入复制到标准输出,实则不然。 27 | 28 | 原因在于程序中的变量 c 被声明为 char 类型,而不是 int 类型。这意味着c无法容下所有可能的字符,特别是,可能无法容下 EOF 。 29 | 30 | 因此,最终结果存在两种可能。一种可能是,某些合法的输入字符在被“截断”后使得 c 的取值与 EOF 相同;另一种可能是, c 根本不可能取到EOF这个值。对于前一种情况,程序将在文件复制的中途终止;对于后一种情况,程序将陷入一个死循环。 31 | 32 | 实际上,还有可能存在第三种情况:程序表面上似乎能够正常工作,但完全是因为巧合。尽管函数 getchar 的返回结果在赋给 char 类型的变量 c 时会发生“截断”操作,尽管 while 语句中比较运算的操作数不是函数 getchar 的返回值,而是被“截断”的值 c,然而令人惊讶地是许多编译器对上述表达式的实现并不正确。这些编译器确实对函数 getchar 的返回值作了“截断”处理,并把低端字节部分赋给了变量c。但是,它们在比较表达式中并不是比较 c 与 EOF,而是比较 getchar 函数的返回值与 EOF ! 编译器如果采取的是这种做法,上面的例子程序看 上去就能够“正常”运行了。 33 | 34 | 35 | 36 | #### 2. 更新顺序文件 37 | 38 | 许多系统中的标准输入/输出库都允许程序打开一个文件,同时进行写入和读出的操作: 39 | 40 | ```c 41 | FILE *fp; 42 | fp = open(file, "r+"); 43 | ``` 44 | 45 | 上面的例子代码打开了文件名由变量file 指定的文件,对于存取权限的设定表明程序希望对这个文件进行输入和输出操作。 46 | 47 | 编程者也许认为,程序一旦执行上述操作完毕,就可以自由地交错进行读出和写入的操作。遗憾的是,事实总难遂人所愿,为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之亦然。如果要同时进行输入和输出操作,必须在其中插入fseek 函数的调用。 48 | 49 | 下面的程序片段似乎更新了一个顺序文件中选定的记录:. 50 | 51 | ```c 52 | FILE *fp; 53 | 54 | struct record rec; 55 | 56 | ... 57 | 58 | while(fread((char*)&rec), sizeof(rec), 1, fp) == 1 ){ 59 | /* 对 rec 执行某些操纵 */ 60 | if(/* rec 必须被重新写入 */){ 61 | fseek(fp, -(long)sizeof(rec), 1); 62 | fwrite( (char*)&rec, sizeof(rec), 1, fp ); 63 | } 64 | } 65 | ``` 66 | 67 | 68 | 69 | 这段代码乍看上去毫无问题: `&rec` 在传入 fread 和fwrite 函数时被小心翼翼地转换为字符指针类型,`sizeof(rec)` 被转换为 长整型(fseek 函数要求第二个参数是 long 类型,因为 int类型的整数可能无法包含一个文件的大小;sizeof 返回一个unsigned 值,因此首先必须将其转换为有符号类型才有可能将其反号)。但是这段代码仍然可能运行失败,而且出错的方式非常难于察觉。 70 | 71 | 问题出在:如果一个记录需要被重新写入文件,也就是说,fwrite 函数得到执行,对这个文件执行的下一个操作将是循环开始的 fread 函数。因为在fwrite函数调用与fread函数调用之,间缺少了一个fseek函数调用,所以无法进行上述操作。解决的办法是把这段代码改写为: 72 | 73 | ```c 74 | while(fread((char*)&rec), sizeof(rec), 1, fp) == 1 ){ 75 | /* 对 rec 执行某些操纵 */ 76 | if(/* rec 必须被重新写入 */){ 77 | fseek(fp, -(long)sizeof(rec), 1); 78 | fwrite( (char*)&rec, sizeof(rec), 1, fp ); 79 | fseek(fp, 0L, 1); 80 | } 81 | } 82 | ``` 83 | 84 | 第二个fseek函数虽然看上去什么也没做,但它改变了文件的状态,使得文件现在可以正常地进行读取了。 85 | 86 | **程序圆帮你理解**: 87 | 88 | - **`&rec`为何要强转成 `char*`类型**:这就要理解 fread 函数(`size_t fread ( void * ptr, size_t size, size_t count, FILE * stream )`):fread 函数的参数有四个,简单的来说就是:从 stream 中读 count 个 size 大小的元素到 ptr 指向的内存中。而 fread 内部在读取一个 size 大小的元素时会调用 size 次 fputc 函数,所以我猜测是每次用 fputc 函数读一个字节然后将该值赋给 ptr 指向的那个地址。既然 fputc 每次只能读一个,那也应该将 ptr 强转为 char* 类型。(但是函数原型是 `void*` 类型,会发生实参提升,转成 `void*`,这又是个问题了)。 89 | 90 | - 其实上面的程序可以简化为: 91 | 92 | ```c 93 | fread(); 94 | fseek(); 95 | fwrite(); 96 | fread(); 97 | ``` 98 | 99 | 我们知道,读写之间需要调用一次 fseek,这就是为什么要在 fwrite 后调用 fseek 了。 100 | 101 | 102 | 103 | #### 3.缓冲输出 与内存分配 104 | 105 | 当一个程序生成输出时,是否有必要将输出立即展示给用户?这个问题的答案根据不同的程序而定。 106 | 107 | 程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。因此,C语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量。 108 | 109 | 这种控制能力一般是通过库函数 setbuf 实现的。如果buf是一个大小适当的字符数组,那么 110 | 111 | ```c 112 | setbuf(stdout, buf); 113 | ``` 114 | 115 | 语句将通知输入/输出库,所有写入到 stdout 的输出都应该使用 buf 作为输出缓冲区,直到 buf 缓冲区被填满或者程序员直接调用 flush (译注:对于由写操作打开的文件,调用 fflush 将导致输出缓冲区的内容被实际地写入该文件),buf 缓冲区中的内容才实际写入到stdout 中。缓冲区的大小由系统头文件中的 BUFSIZ 定义。 116 | 117 | **程序圆帮你理解:** setbuf 比较老,现在可以用 C99 引入的函数 `setvbuf` 118 | 119 | 下面的程序的作用是把标准输入的内容复制到标准输出中,演示了setbuf 库函数最显而易见的用法: 120 | 121 | ```c 122 | #include 123 | 124 | main() 125 | int C; 126 | 127 | char buf [BUFSIZ]; 128 | setbuf(stdout, buf) ; 129 | 130 | while((c = getchar()) != EOF) 131 | putchar(c) ; 132 | 133 | ) 134 | ``` 135 | 136 | 遗憾的是,这个程序是错误的,仅仅是因为一个细微的原因。程序中对库函数 setbuf 的调用,通知了输入输出库所有字符的标准输出应该首先缓存在 buf 中。要找到问题出自何处,我们不妨思考一下buf缓冲区最后一次被清空是在什么时候?答案是在 main 函数结束之后,作为程序交回控制给操作系统之前 C 运行时库所必须进行的清理工作的一部分。但是,在此之前 buf 字符数组已经被释放! 137 | 138 | 要避免这种类型的错误有两种办法。第一种办法是让缓冲数组成为静态数组,即可以直接显式声明 buf 为静态: 139 | 140 | ```c 141 | static char buf[BUFSIZ]; 142 | ``` 143 | 144 | 也可以把 buf 声明完全移到 main 函数之外。 145 | 146 | 第二种办法是动态分配缓冲区,在程序中并不主动释放分配的缓冲区(译注:由于缓冲区是动态分配的,所以 main 函数结束时并不会释放该缓冲区,这样 C 运行时库进行清理工作时就不会发生缓冲区已释放的情况): 147 | 148 | ```c 149 | char *malloc() ; 150 | 151 | setbuf(stdout, malloc(BUFSIZ)); 152 | ``` 153 | 154 | 如果读者关心一些编程“小技巧”,也许会注意到这里其实并不需要检查 malloc 函数调用是否成功。如果 malloc 函数调用失败,将返回一个 NULL 指针。setbuf 函数的第二个参数取值可以为 NULL,此时标准输出不需要进行缓冲。这种情况下, 155 | 程序仍然能够工作,只不过速度较慢而已。 156 | 157 | #### 4. 使用errno检测错误 158 | 159 | 很多库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为 `errno` 的外部变量,通知程序该函数调用失败。下面的代码利用这一 特性进行错误处理,似乎再清楚明白不过,然而却是错误的: 160 | 161 | ```c 162 | /*调用库函数*/ 163 | if (errno) 164 | /*处理错误*/ 165 | ``` 166 | 167 | 出错原因在于,在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置 errno 为0,这样errno 的值就可能是前一个执行失败的库函数设置的值。 168 | 169 | 下面的代码作了更正,似乎能够工作,很可惜还是错误的: 170 | 171 | ```c 172 | errno = 0; 173 | /*调用库函数*/ 174 | if (errno) 175 | /*处理错误*/ 176 | ``` 177 | 178 | 库函数在调用成功时,既没有强制要求对 errno 清零,但同时也没有禁止设置 errno。既然库函数已经调用成功,为什么还有可能设置 errno 呢? 要理解这一点,我们不妨假想一下库函数 fopen 在调用时可能会发生什么情况。 179 | 180 | 当 fopen 函数被要求新建一个文件以供程序输出时,如果已经存在一个同名文件,fopen 函数将先删除它,然后新建一个文件。 这样,fopen 函数可能需要调用其他的库函数,以检测同名文件是否已经存在。(译注:假设用于检测文件的库函数在文件不存在时,会设置 errno 。那么,fopen 函数每次新建一个事先并不存在的文件时,即使没有任何程序错误发生,errmo 也仍然可能被设置。) 181 | 182 | 因此,在调用库函数时,我们应该首先检测作为错误指示的返回值,确定程序执行已经失败。然后,再检查 errno,来搞清楚出错原因: 183 | 184 | ```c 185 | /*调用库函数*/ 186 | if (返回的错误值) 187 | /* 检查errno */ 188 | ``` 189 | 190 | 191 | 192 | #### 5. 库函数 signal 193 | 194 | 关于 signal 函数使用需要避免的情况: 195 | 196 | - **信号处理函数不应该调用复杂的库函数**(例如:malloc) 197 | 198 | 例如,假设malloc函数的执行过程被一个信号中断。 此时,malloc 函数用来跟踪可用内存的数据结构很可能只有部分被更新。如果 signal 处理函数再调用 malloc 函数,结果可能是 malloc 函数用到的数据结构完全崩溃,后果不堪设想! 199 | 200 | - **从 siganl 函数中使用 longjup 退出** 201 | 202 | 基于同样的原因,从 signal 处理函数中使用 longjmp 退出,通常情况下也是不安全的:因为信号可能发生在 malloc 或者其他库函数开始更新某个数据结构,却又没有最后完成的过程中。因此,signal 处理函数能够做的安全的事情,似乎就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。 203 | 204 | - **算数运算错误** 205 | 206 | 然而,就算这样做也并不总是安全的。当一个算术运算错误(例如溢出或者零作除数)引发一个信号时,某些机器在signal 处理函数返回后还将重新执行失败的操作。而当这个算术运算重新执行时,我们并没有一个可移植的办法来改变操作数。这种情况下,最可能的结果就是马上又引发一个同样的信号。因此,对于算术运算错误,signal 处理函数的惟一安全、 可移植的操作就是打印一条出错消息,然后使用 longjmp 或 exit 立即退出程序。 207 | 208 | 由此,我们得到的结论是:信号非常复杂棘手,而且具有一些从本质上而言不可移植的特性。解决这个问题我们最好采取“守势”,让signal处理函数尽可能地简单,并将它们组织在一起。这样,当需要适应一个新系统时,我们可以很容易地进行修改。 209 | 210 | 211 | 212 | ### 练习 213 | 214 | #### 练习5-1 215 | 216 | 当一个程序异常终止时,程序输出的最后几行常常会去失,原因是什么?我们能够采取怎样的措施来解决这个问题? 217 | 218 | 一个异常终止的程序可能没有机会来清空其输出缓冲区。 219 | 220 | 解决方案就是在调试时强制不允许对输出进行缓冲。要做到这一点,不同的系统有不同的做法,这些做法虽然存在细微差别,但大致如下: 221 | 222 | ```c 223 | setbuf(stdout, (char *)0); 224 | ``` 225 | 226 | 这个语句必须在任何输出被写入到 stdout(包括任何对 printf 函数的调用)之前执行。该语句最恰当的位置就是作为main函数的第一个语句。 227 | 228 | 229 | 230 | #### 练习5-2 231 | 232 | 下 面程序的作用是把它的输入复制到输出: 233 | 234 | ```c 235 | #include 236 | main() 237 | register int c; 238 | 239 | while ((c = getchar()) != EOF) 240 | putchar(c); 241 | } 242 | ``` 243 | 244 | 从这个程序中去掉 `#include` 语句,将导致程序不能通过编译,因为这时 EOF 是未定义的。假定我们手工定义了EOF (当然,这是一种不好的做法): 245 | 246 | ```c 247 | #define EOP -1 248 | main() 249 | { 250 | register int c; 251 | 252 | while ((c = getchar()) != EOF) 253 | putchar (c) ; 254 | } 255 | ``` 256 | 257 | 这个程序在许多系统中仍然能够运行,但是在某些系统运行起来却慢得多。这是为什么? 258 | 259 | 函数调用需要花费较长的程序执行时间,因此getchar经常被实现为宏。这个宏在stdio.h头文件中定义,因此如果一个程序没有包含 stdio.h 头文件,编译器对 getchar 的定义就一无所知。 在这种情况下,编译器会假定 getchar 是一个返回类型为整型的函数。 260 | 261 | 实际上,很多C语言实现在库文件中都包括有 getchar 函数,原因部分是预防编程者粗心大意,部分是为了方便那些需要得到 getchar 地址的编程者。因此,程序中忘记包含 stdio.h 头文件的效果就是,在所有 getchar 宏出现的地方,都getchar 函数调用来替换 getchar 宏。这个程序之所以运行变慢,就是因为函数调用所导致的开销增多。同样的依据也完全适用于putchar 。 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | **参考资料**:*《C 缺陷与陷阱》* 272 | 273 | -------------------------------------------------------------------------------- /content/c-traps-and-pitfalls/08 建议.md: -------------------------------------------------------------------------------- 1 | ## 【C 陷阱与缺陷】(八)建议 2 | 3 | 4 | 5 | #### 1. 不要说服自己相信“皇帝的新装” 6 | 7 | 有的错误极具伪装性和欺骗性。比如,第一章原来的例子是这样写的: 8 | 9 | ```c 10 | while (c == '\t' || c = ' ' || c == '\n') 11 | c = getc(f) ; 12 | ``` 13 | 14 | 如上,这个例子在 C 语言中是非法的。因为赋值运算符 = 的优先级比 while 子句中其他运算符的优先级都要低,因此上例可以这样解释: 15 | 16 | ```c 17 | while ( (c == '\t' || c) = (' ' || c == '\n') ) 18 | c = getc(f) ; 19 | ``` 20 | 21 | 当然,这是非法的: 22 | 23 | `(c == '\t' || c)`不能出现在赋值运算的左侧。 24 | 25 | 26 | 27 | #### 2. 直截了当地表明意图 28 | 29 | 当你编写代码的本意是希望表达某个意思,但这些代码有可能被误解为另一种意思时,请使用括号或者其他方式让你的意图尽可能清楚明了。这样做不仅有助于你日后重读程序时能够更好地理解自己的用意,也方便了其他程序员日后维护你的代码。 30 | 31 | 有时候我们还应该预料哪些错误有可能出现,在代码的编写方式上做到事先预防,一旦错误真正发生能够马上捕获。例如,有的程序员把常量放在判断相等的比较表达式的左侧。换言之,不是按照习惯的写法: 32 | 33 | ```c 34 | while (c == '\t' || c == ' '|| c == '\n') 35 | c = getc(f) ; 36 | ``` 37 | 38 | 而是写作: 39 | 40 | ```c 41 | while('\t' == c || ' ' == c || '\n' == c) 42 | c = getc(f) ; 43 | ``` 44 | 45 | 这样,如果程序员不小心把比较运算符 `==` 写成了赋值运算符 `=`,编译器将会捕获到这种错误,并给出一条编译器诊断信息: 46 | 47 | ```c 48 | while('\t'= c || ' ' == c || '\n' == c) 49 | c = getc(f) ; 50 | ``` 51 | 52 | 上面的代码试图给字符常量 `'\t'` 赋值,因而是非法的。 53 | 54 | 55 | 56 | #### 3. 考查最简单的特例 57 | 58 | 无论是构思程序的工作方式,还是测试程序的工作情况,这一原则都是适用的。当部分输入数据为空或者只有一个元素时,很多程序都会执行失败,其实这些情况应该是一早就应该考虑到的。这一原则还适用于程序的设计。在设计程序时,我们可以首先考虑一组输入数据全为空的情形,从最简单的特例获得启发。 59 | 60 | #### 4. 使用不对称边界 61 | 62 | 本系列第三章节关于如何表示取值范围的讨论,值得一读再读。C 语言中数组下标取值从 0 开始,各种计数错误的产生与这一点或多或少有关系。 63 | 64 | 我们一旦理解了这个事实,处理这些计数错误就变得不那么困难了。 65 | 66 | #### 5. 注意潜伏在暗处的Bug 67 | 68 | 各种C语言实现之间,都存在着或多或少的细微差别。我们应该坚持只使用C语言中众所周知的部分,而避免使用那些“生僻”的语言特性。这样做,我们能够很方便地将程序移植到一个新的机器或编译器,而且“遭遇”到编译器Bug的可能性也会大大降低。 69 | 70 | #### 6. 防御性编程 71 | 72 | 对程序用户和编译器的假设不要太多! 73 | 74 | 如果 C 编译器能够捕获到更多的编程错误,这当然不错。不幸的是,因为几方面的原因,要做到这一点很困难。最重要的原因也许是历史因素:长期以来,人们惯于用C语言来完成以前用汇编语言做的工作。因此,许多C程序中总有这样的部分,刻意去做那些严格说来在 C 语言所允许范围以外的工作。最明显的例子就是类似操作系统的东西。这样,一个C编译器要做到严格检测程序中的各种错误,就要对程序中本意是可移植的部分做到严格检测,同时对程序中那些需要完成与特定机器相关工作的部分网开一面。 75 | 76 | 另一个原因是,某些类型的错误从本质上说是难于检测的。考虑下面的函数: 77 | 78 | ```c 79 | void set(int *p, int n) { 80 | *p = n; 81 | ``` 82 | 83 | 这个函数是合法还是非法?离开一定的上下文,我们当然不可能知道答案。 84 | 85 | 如果像下面的代码一样调用这个函数: 86 | 87 | ```c 88 | int a[10]; 89 | set (a+5,37) ; 90 | ``` 91 | 92 | 这当然是合法的,但如果这样来调用 set 函数: 93 | 94 | ```c 95 | int a[10] ; 96 | set (a+10,37) ; 97 | ``` 98 | 99 | 上面的代码就是非法的了。ANSI C 标准允许程序得到数组尾端出界的第一个位置的地址,因此上面的后一个代码段从它本身来说并没有什么错误。C编译器要想捕获到这样的错误,就必须非常地“聪明”。 100 | 101 | 102 | 103 | **参考资料**:*《C 缺陷与陷阱》* 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /content/other/Q&A.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/content/other/Q&A.md -------------------------------------------------------------------------------- /img/QR Code/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/img/QR Code/1.png -------------------------------------------------------------------------------- /img/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairrrrr/C-CrashCourse/e06dc5030c093ef020c457c306441d559055817c/img/logo/logo.png --------------------------------------------------------------------------------