├── .gitignore ├── README.md ├── SUMMARY.md ├── chapter0.md ├── chapter1.md ├── chapter2.md └── chapter5.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C/C++语言 错误排查教程 2 | 3 | 这是一个面向新手的C/C++语言错误排查教程,目前还在开垦填坑阶段,欢迎大家贡献内容。 4 | 5 | 成为编程大牛的第一步就是学会自己解决问题,而不是一味的寻求他人帮助。 6 | 7 | ## 本书的在线预览地址 8 | https://zurl.gitbooks.io/c-cpp-debug/content/ 9 | 10 | ### 在阅读本教程之前希望你能够掌握: 11 | - C语言的基本语法 12 | - 学会求助百度 13 | 14 | ### 如果能掌握这些更好: 15 | - 学会求助Google和Stack Overflow 16 | 17 | ### 索引 18 | - [目录](https://github.com/ZJU-Shaonian-Biancheng-Tuan/c-cpp-debug/blob/master/SUMMARY.md) 19 | - [第零章 代码书写建议](https://github.com/ZJU-Shaonian-Biancheng-Tuan/c-cpp-debug/blob/master/chapter0.md) 20 | - [第一章 错误的分类](https://github.com/ZJU-Shaonian-Biancheng-Tuan/c-cpp-debug/blob/master/chapter1.md) 21 | - [第五章 逻辑错误](https://github.com/ZJU-Shaonian-Biancheng-Tuan/c-cpp-debug/blob/master/chapter5.md) 22 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [第零章 代码书写建议](chapter0.md) 5 | * [第一章 错误的分类](chapter1.md) 6 | * [第二章 编译错误](chapter2.md) 7 | * [第三章 链接错误](chapter3.md) 8 | * [第四章 运行时错误](chapter4.md) 9 | * [第五章 逻辑错误](chapter5.md) 10 | * [第六章 调试技巧](chapter6.md) 11 | * [第七章 单步调试](chapter7.md) 12 | * [第八章 编译警告](chapter8.md) 13 | * [第九章 提问的智慧](chapter9.md) 14 | -------------------------------------------------------------------------------- /chapter0.md: -------------------------------------------------------------------------------- 1 | # 第零章 代码书写建议 2 | 3 | 整洁干净的代码意味着更清楚的逻辑、更好的可读性与可维护性,能有效降低错误出现的可能。 4 | 5 | 尽管这可能并不影响最后机器执行的结果。 6 | 7 | ## 格式化代码 8 | 9 | > 新手可能会有这样的想法:反正我不在乎代码的美观程度;计算机也不在乎;我也不想把自己的代码给别人看;那么为什么我要将代码格式化? 10 | > 因为格式化的代码增加了代码的可读性、可维护性。在程序的规模不断扩大的时候,代码的可读性非常重要。将代码写得更好懂一些,方便了其他人理解你的代码,这里的其他人也有可能是几个月后的你自己。 11 | 12 | 代码的格式包括缩进、括号风格,通常可以使用代码格式化工具一步调优。 13 | 14 | 比如 Visual Studio 就有这样的功能。 15 | 16 | 在其他的 MinGW IDE, 如 Dev-C++, C-Free 等,则可以运用 AStyle 这个工具来执行代码格式化。 17 | 18 | 在Linux下的编辑器, 如 Vim, Emacs 等 也可以配置 代码格式化插件。 19 | 20 | **不过,最好还是养成良好的代码书写习惯。** 21 | 22 | 以下先给出一个反面教材 23 | 24 | ```c 25 | int main() 26 | {int i; 27 | for(i=0;i<10;i++) 28 | { 29 | printf("%d ",i); 30 | }printf("\n"); 31 | return 0; 32 | } 33 | ``` 34 | 35 | 笔者曾经看过不少新手编码的过程: 36 | 37 | 他们不会也不知道格式化代码 38 | 39 | 他们没有将输入光标定位到合适位置 40 | 1. 他们可能知道要将局部变量定义到函数内部,但不知道将其放在何处 41 | 2. 他们可能只是知道这条语句要放在哪两个花括号的内部,却从来不在意更具体的位置 42 | 43 | 很多新手在编程的时候发现一些地方要改的时候,笨拙地使用鼠标去定位,而且点不准 44 | 45 | 一则是慢,二则是会使得代码格式非常糟糕。 46 | 47 | 同样逻辑的代码,不同的人写起来的风格是不一样的。 48 | 49 | 以下是笔者的代码风格 50 | 左花括号不换行,右花括号与左花括号所在行缩进一致。 51 | ```c 52 | int main(){ // 53 | //do something 54 | } 55 | 56 | if (a > 10){ // 57 | //do something 58 | }else{ 59 | //else do something 60 | } 61 | ``` 62 | 也有人习惯大括号换行,比如这样 63 | ```c 64 | int main() 65 | { 66 | int a = 0; 67 | if(a > 10) 68 | { 69 | //do something 70 | } 71 | else 72 | { 73 | //do something 74 | } 75 | return 0; 76 | } 77 | ``` 78 | 一元以上运算符与其操作数之间有一个空格。 79 | ```c 80 | a = a + 1; //这里要加空格 81 | a = -a + 1; //-a不需要加空格 82 | b = a > 0 ? 1 : 0; 83 | ``` 84 | 函数参数列表中的逗号后面有一个空格,前面没有,分号后面如果不换行,那么有一个空格。 85 | ```c 86 | for (int i = 0; i < 10; i++){ 87 | printf("i = %d\n", i); 88 | } 89 | ``` 90 | 对于空格的要求也可以更苛刻,比如控制块 (if, for, switch) 的括号前要加空格,不换行的大括号前要加空格 91 | ```c 92 | if (a > 10) { //注意左右括号旁的空格 93 | ``` 94 | 最后的代码写起来就是这个样子的 95 | ```c 96 | int main() { 97 | int i; 98 | for(i = 0; i < 10; i++) { 99 | printf("%d ", i); 100 | } 101 | printf("\n"); 102 | return 0; 103 | } 104 | ``` 105 | ## 基本的代码格式化要求 106 | 107 | * 缩进规范统一 108 | * 大括号不换行/换行统一 109 | * 空格统一 110 | 111 | ### 可供参考的常见代码格式化模板 112 | 113 | * `while` 114 | ```c 115 | while (i < 10) { 116 | printf("%d", i); 117 | i--; 118 | } 119 | ``` 120 | * `do-while` 121 | ```c 122 | do { 123 | printf("%d", i); 124 | i++; 125 | } while(i < 10) 126 | ``` 127 | * `for` 128 | ```c 129 | for (int i = 0; i < 10; i++) { 130 | printf("%d", i); 131 | } 132 | ``` 133 | * `switch` 134 | ```c 135 | switch (c) { 136 | case 'A': { 137 | printf("You got 90!"); 138 | break; 139 | } 140 | case 'B': { 141 | printf("You got 80!"); 142 | break; 143 | } 144 | default: { 145 | printf("You lose."); 146 | } 147 | } 148 | ``` 149 | 150 | * `if-else` 151 | ```c 152 | if (a < 0) { 153 | printf("less than 0"); 154 | } else if (a < 20) { 155 | printf("0 <= a < 20"); 156 | } else if (a < 90) { 157 | printf("20 <= a < 90"); 158 | } else { 159 | printf("a >= 90"); 160 | } 161 | ``` 162 | 163 | ## 变量命名规则 164 | 165 | 变量的命名应当是符合语义的,尽量使得变量从其名字上即可看出它的用途。 166 | 167 | **对于生命周期越长的变量,合适的命名就越重要** 168 | 169 | 变量的生命周期与其作用域息息相关,生命周期最长的是全局变量,而最短的是块作用域中定义的变量。 170 | 171 | 因为在阅读代码的时候,活得越久的变量越是需要被记住。 172 | 173 | 若是出现`A, B` 这样如同阿猫阿狗的名字会让人费解,而像是`EndFlag, StartTime` 这样的名字只需要少量的上下文即可理解。 174 | 175 | 反之,当变量的生命周期非常短的时候,也可以以非常简洁的方式进行命名。 176 | 177 | ```c 178 | for(int i = 0; i < 100; i++) sum += i; 179 | ``` 180 | 181 | 在这个例子中,变量i的作用域仅有这一行,出了这一行便不存在i,对于正常人来说,理解这一行代码还是很简单的。 182 | 183 | 至于在多大范围内可以使用简短的变量名,并不固定,还需各位自己把握。 184 | 185 | 笔者建议不要在一个过长的作用域(如全局域)中使用简短命名。 186 | 187 | > 为常量命名的思想与此如出一辙,如`#define SUCCESS 0`。 188 | 189 | ### 关于代码注释 190 | 有人建议在每一行代码后都加上注释,这看起来相当极端。 191 | 192 | 笔者推崇的方式是注释含于代码,代码即文档的形式。 193 | 194 | 一种可读性好的代码风格完全不需要多余的注释即可让人轻松读懂。 195 | 196 | ```c 197 | int f(int *A, int n){ // get the minimum of A with size n 198 | int a = *A; // assume that the answer is A[0] 199 | for(int i = 1; i < n; i++) // for each number in A[1...n) 200 | if(A[i] < a) // if A[i] < a then update the minimum 201 | a = A[i]; 202 | return a; // return the answer 203 | } 204 | ``` 205 | 206 | 相比之下,一个仅仅是改了变量名的版本: 207 | 208 | ```c 209 | int get_min(int *candidates, int size){ 210 | int ans = candidates[0]; 211 | for(int i = 1; i < size; i++) 212 | if(candidates[i] < ans) 213 | ans = candidates[i]; 214 | return ans; 215 | } 216 | ``` 217 | 218 | ### 常用的变量名规则 219 | 220 | * `camelCase` - 驼峰式 221 | 222 | 这种命名方式将单词首位相接,并首字母小写,其余首字母大写 223 | 224 | 如:`numberOfDigits`, `longLongAgo`, `numberOfDaysInThisMonth`, `getNumberFromUser` 225 | 226 | * `PascalCase` - 帕斯卡式 227 | 228 | 这种命名方式因Pascal语言而得名,他与驼峰命名法的区别就是首字母也大写 229 | 230 | 如:`ToString`, `GetNumber`, `CreateHttpRequest` 231 | 232 | * `snake_case` 233 | 234 | 这种命名方式中所有字母小写,使用下划线 `_` 连接 235 | 236 | 如:`find_first_of`, `unordered_map`, `from_chars` 237 | 238 | * ansi C 命名法(不建议) 239 | 240 | 这种命名方式常见于C标准库,主要是将单词中的元音字母尽量去除,来减少变量名的字数。 241 | 242 | 如:`strlen`(`string length`),`strcmp`(`string compare`), `stdio`(`standard IO`) 243 | 244 | 这种做法主要是因为古老的C语言并不支持很长的变量名,以及古老的键盘打字很费力,降低字母数可以提高编码速度。因为会降低可读性,及现代的键盘已经十分轻便,所以现在已经不建议使用。 245 | 246 | * 匈牙利命名(已弃用) 247 | 248 | 匈牙利命名是由类型前缀后跟Pascal命名的变量构成,为了在不支持智能提示的编程环境中提醒程序员变量的类型,这种命名法常见于古老的 Win32 程序代码中,现在已经弃用 249 | 250 | 如:`lpszWindowName`, `lParam`, `hInstance`。 251 | 252 | 253 | 254 | ### 使用有意义的名字做变量名 255 | 256 | 对于表示数量或多少的变量,尽量使用能代表他们的意义的名词或名词短语作为变量名,比如`numberOfBanana`, `age`, `year`等 257 | 258 | 对于表示状态的变量,应使用能代表这个短语命名,如`hasApple`, `hasHit`, `isHuman`,这样在写`if`语句的时候会使表述十分清楚,如`if (hasApple)`, `if (isHuman) `使表述十分清楚。 259 | 260 | 对于函数,一般使用动词或动词短语来命名,比如`getNumber`, `printDigit`,这样的目的也是为了使表述更加清楚,如`int number = getNumber()`, `printDigit(number)`。 261 | 262 | ### 使用无歧义的名字做变量名 263 | 264 | 例如,当你需要一个表时间的变量时,一个可选的变量名是`now`,但是,一个更好的名字可能是`nowMs`来表示这个时间的单位是毫秒。变量类型无法提供更多的信息时,这个任务便落在了变量名上。同理,一个把时间(`int`)转换到`Time`类型的函数,可以叫`TimeFromUnix`,但可选的更好的名字也许是`TimeFromUnixMs`。 265 | 266 | 另外,有一些常用的变量名其实并没有任何的实际含义,它们往往能使用在非常多的地方。例如:`data`, `state`, `value`, `object`, `instance`。 267 | 268 | -------------------------------------------------------------------------------- /chapter1.md: -------------------------------------------------------------------------------- 1 | # 第一章 错误的分类 2 | 3 | > 学会如何正确的将错误进行分类有利于错误排查,这让你能对错误进行精准定位,而不是大海捞针。 4 | 5 | 富有经验的程序员能够通过观察若干个现象快速甄别出错误发生的原因,进而有针对性地调试程序。 6 | 7 | 而菜鸟程序员,本身对语法就不是很熟悉,再加上没有定位错误的经验,很容易懵逼。 8 | 9 | 典型的现象如下: 10 | 11 | + 编译错误越改越多。 12 | + 看不懂或不看编译错误提示。 13 | + 手足无措,被水淹没。 14 | 15 | 有没有觉得膝盖中了一箭? 16 | 17 | 第一章介绍如何根据基本的程序行为现象来分类错误,并提供深入查找问题解决方案的提示。 18 | 19 | ## 第一节 基本分类原则 20 | 21 | 我们通常在这些情况下认为一个程序产生了错误: 22 | + 无法运行 (第二章到第三章) 23 | + 运行时崩溃 (第四章) 24 | + 结果和预期不符 (第五章) 25 | 26 | > 对于逻辑正确的代码,本书可能会提供一些在代码结构方面更优雅的实现,但专门对于算法的调优不在本书的讨论范围之内。 27 | 28 | 下面对这三种情况进行讨论 29 | 30 | ## 第二节 无法运行 31 | 32 | 无法运行是指我们无法启动我们所编写的程序,当使用IDE或直接使用编译器时,同时会产生**错误信息**。 33 | 34 | 请注意在这种情况下会产生错误信息,这些错误信息对排查错误**非常有帮助**。 35 | 36 | 在懂得基本的英语的前提下,你会发现编译器的错误信息的含义很明确,这时利用你的智慧去理解这些信息,你将会很快地排除错误。 37 | 38 | 在这种情况下,编译器所产生的错误信息一般是错误(Error)级别的,这些错误又可以分成两类: 39 | + 编译错误(见第二章) 40 | + 链接错误(见第三章) 41 | 42 | > 编译器会产生 Error(错误) 与 Warning(警告:非致命的错误) 两种级别的提示。 43 | > 如果有 Error 提示, 将不会产生可执行文件,因而无法运行。 44 | > 如果仅有 Warning, 会继续产生可执行文件,因而可以运行。 45 | > 对于 Warning 的详细说明,参见第八章。 46 | 47 | 其中**编译错误**在编译(Compile)单个文件时发生,**链接错误**在构建(Build)工程或单纯链接(Link)文件时发生。 48 | 49 | 下面将举出一些常见的例子来帮助你分类这两类错误: 50 | + 如果你只有一个文件且仅使用了标准库,编译错误。 51 | + 如果出现unsolved symbol之类的错误,**常常**是链接错误。(一个特例是你仅仅声明了一个变量/函数却没有定义它,或位置不对) 52 | + 如果错误代码里出现了Link字样,那是链接错误。 53 | 54 | ## 第三节 运行时崩溃 55 | 56 | > 运行时崩溃本质上都是程序没有处理的异常。 57 | 58 | 这种情况产生的原因是程序进行了非法操作,比如: 59 | + 对空指针解引用 (空指针异常) 60 | + 访问非法内存 (段错误) 61 | + 整数除0 (算术异常) 62 | + 其他用户自定义的抛出异常(C++) 63 | 64 | 详细见第四章描述 65 | 66 | ## 第四节 结果和预期不符 67 | 68 | > Program testing can be used to show the presence of bugs, but never to show their absence! 69 | > 程序测试只能证明错误的存在,但不能证明错误不存在! 70 | > —— E.W.Dijkstra (*"Notes On Structured Programming"*, 1970) (计算机科学家, 1972年图灵奖得主) 71 | 72 | 切记不要轻易断言程序正确,即便: 73 | 74 | + 编译通过 75 | + 通过样例测试 76 | + 通过大量数据测试 77 | + 通过了某在线评测系统 78 | 79 | 数学,唯有数学证明,才能支撑程序的正确性而且屹立不倒。 80 | 81 | > 程序的正确性与性能是两个完全不同的方面。 82 | > 对于自动评测机来说,程序的性能也会被考虑其中,当程序正确时,你至少不会碰到 Wrong Answer (答案错误) 83 | > 但你有可能仍然因为一些性能问题通不过评测。 84 | > 因为它不仅要求程序正确,还要求程序高效。 85 | 86 | 即便程序是正确而且高效的,不代表代码是优秀的,参见第零章。 87 | 88 | 切记不要再提问中包括:“我的程序是正确的啊,怎么结果就不对呢?”之类的描述,关于提问详情参见第九章。 89 | 90 | 结果和预期不符往往是逻辑错误,详情参见第五章。 91 | 92 | -------------------------------------------------------------------------------- /chapter2.md: -------------------------------------------------------------------------------- 1 | # 第二章 编译错误 2 | 3 | 编译错误是最常见的一类错误,也是最容易排除的一类错误,它们会导致你的程序无法编译通过,并且会产生红色的错误信息。 4 | 5 | ## 第一节 词法错误 6 | 7 | 词法错误是最好排除的错误。 8 | ### C/C++的变量名 9 | 1.必须以下划线或字母开头 10 | 2.只能包含下划线和字母或数字 11 | 3.不能包含其他符号和中文 12 | ### 无效的字符 13 | 一般会出现如 “未声明的标识符”(MSVC) 14 | stray 'XXXX' in program (GCC) 15 | 这时要检查源程序中是否存在中文括号 中文分号 还有其他中文字符。 16 | 英文字符多是C语言支持的字符,除了少部分(比如 @ ) 17 | ## 第二节 语法错误 18 | 19 | ## 第三节 语义错误 20 | 21 | -------------------------------------------------------------------------------- /chapter5.md: -------------------------------------------------------------------------------- 1 | # 第五章 逻辑错误 2 | 3 | 逻辑错误是指能通过编译,并且运行时也不会抛出异常的一种错误。 4 | 包含逻辑错误的程序是正确的程序,但是他们的表现往往和你希望的不太一样。 5 | 6 | 这一节所列出的是新手***最最最常见***的错误,希望大家铭记在心,切记避免这些错误。 7 | 8 | 9 | ## 1. 变量未初始化 10 | 11 | > **建议**:无论何时C语言声明变量时一定要初始化。 12 | 13 | ### C语言 14 | > **基本类型**:指C语言内置的不包括Struct和Union的类型,如int,char,char\*等 15 | 16 | 未初始化的基本类型局部变量的值是**不确定**的。 17 | 18 | 未初始化的基本类型全局变量的值在二进制层面的全为0 19 | 20 | 结构体/联合也符合上述规则(全局结构体的成员值置0,局部结构体的成员值不确定) 21 | 22 | malloc出的空间值不确定,calloc出的空间值置0。 23 | 24 | 可利用memset完成空间置0操作。 25 | 26 | ### C++语言 27 | **占位符** 28 | 29 | ## 2. 混淆=与== 30 | 在 C/C++ 中, 赋值运算符 `=` 也具有返回值 31 | 32 | 返回值为赋值符右边的表达式的值 33 | 34 | 在有需要的时候 C/C++ 可以将这个值强制转换为逻辑值 35 | 36 | ```c 37 | if(a = 0){ // a = 0 返回值为 0, 强制转换为逻辑值 false 38 | printf("Interesting"); // 不可能被执行 39 | } 40 | ``` 41 | 42 | 对于涉及常数的判定,可以将常数放在左边,让编译器检查。 43 | 44 | ```c 45 | if(0 = a); // 编译错误 46 | if(0 == a); // OK 47 | ``` 48 | 49 | ## 3. 对空指针解引用 50 | **占位符** 51 | 52 | ## 3. 代码块结构混乱 53 | **占位符** --------------------------------------------------------------------------------