✨博客主页:心辛向荣
✨系列专栏:【从0到1,C语言学习】
✨一句短话:你若盛开,蝴蝶自来!
✨博客说明:尽己所能,把每一篇博客写好,帮助自己熟悉所学知识,也希望自己的这些内容可以帮助到一些在学习路上的伙伴,文章中如果发现错误及不足之处,还望在评论区留言,我们一起交流进步!?
文章目录
前言一.实用的调试技巧1.了解bug2.认识调试2.1 什么是调试2.2 调试的基本步骤2.3 Debug和Release的介绍。 3.学会调试(windows环境)3.1 调试环境的准备3.2 熟悉使用必要的快捷键3.3 调试的时候查看程序当前信息 5. 调试实例6. 编程常见的错误 二.如何写出优秀的的代码(一)学习运用良好的代码风格1. 文件结构1.1 头文件的结构1.2 定义文件的结构1.3 目录结构 2. 程序的版式2.1 空行2.2 代码行2.3 代码行内的空格2.4 对齐2.5 长行拆分 3. 命名规则3.1 共性规则3.2 简单的 Windows 应用程序命名规则 4. 表达式和基本语句4.1运算符4.2 复合表达式4.3 for 语句的循环控制变量4.4 switch 语句 5. 常量(如宏常量) (二)写出易于调试的代码1. 使用 assert(断言)2.const修饰指针变量的时候:3. 一些完美代码示例3.1 模拟实现库函数strcpy3.2 模拟实现strlen 结语
前言
?本篇介绍如何写出好的代码,从代码风格和实用调试技巧出发,代码的风格虽然不会对程序的运行造成影响,但好的代码风格可以让我们的代码逻辑更加的清晰,而学会调试程序对一个程序员来说更是非常重要的,我们写出的代码应当是易于调试的,好的代码可以避免很多不必要的麻烦,节省我们的时间!
一.实用的调试技巧
1.了解bug
程序错误,即英文的Bug、臭虫,是指在软件运行中因为程序本身有错误而造成的功能不正常、死机、数据丢失、非正常中断等现象。 早期的计算机由于体积非常庞大,有些小虫子可能会钻入机器内部,造成计算机工作失灵。史上的第一只 “Bug” ,真的是因为一只飞蛾意外走入一电脑而引致故障,因此Bug从原意为臭虫引申为程序错误。
我们在学习编码的过程中,初学时,由于对于语法的不熟悉,可能在代码的字里行间会出现语法上的错误而导致代码程序不能运行;还有我们写出来的代码不会报错,但代码的运行结果与我们所想的不同,不能得出正确的结果…这些各种原因导致代码程序出现了问题、错误便是与我们相关的bug了。
2.认识调试
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧, 就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。
顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探, 每一次调试都是尝试破案的过程。
2.1 什么是调试
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程;其实就是对代码中的错误进行纠正的一个过程!
对于修bug。。。
写代码…
排查出现的问题…
如果你是图片中的状态可是不行的,要学会正确调试程序的的方法,拒绝迷信式调试!!!
2.2 调试的基本步骤
发现程序错误的存在以隔离、消除等方式对错误进行定位确定错误产生的原因,提出纠正错误的解决办法对程序错误予以改正,重新测试2.3 Debug和Release的介绍。
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优 的,以便用户很好地使用。看下面这段代码在俩种环境下的运行效果!
#include<stdio.h>int main(){char* p = "hello world";printf("%s\n", p);return 0;}
上述代码在Debug环境的结果展示:
上述代码在Release环境的结果展示:
Debug和Release反汇编展示对比:
所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程;而在Release环境下中我们可以清晰的看到它优化后代码执行的的简洁,省略了一部分步骤,是不支持调试的!
这里再看一下Release模式下编译器进行了哪些优化?
看以下代码:
#include<stdio.h>int main(){int i = 0;int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9.,10 }; for (i = 0; i <= 12; i++){arr[i] = 0;printf("hehe\n");} return 0;}
如果是 debug 模式去编译,程序的结果是死循环。
如果是 release 模式去编译,程序没有死循环。
那他们之间有什么区别呢? 就是因为优化导致的。
3.学会调试(windows环境)
3.1 调试环境的准备
只有在Debug环境中,才能正常对代码进行调试!
3.2 熟悉使用必要的快捷键
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点的重要作用,可以在程序的任意位置设置断点; 这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
一般F5与F9配合使用,当我们写了比较长的代码,使用F10/F11逐步调试会很浪费时间;而使用F5和F9就会很方便,我们在想要让程序停下来的位置打上断点,在按F5就可以跳过前面的代码,使程序直接运行到断点处。
当再次按F5调试时会运行到逻辑上的下一处断点!
3.3 调试的时候查看程序当前信息
3.3.1 查看临时变量的值
在调试开始之后,用于观察变量的值。
这里最常用的是监视窗口,我们可以自行输入任意的合法变量对其值进行查看;而自动窗口和局部窗口随着调试过程,监视的内容是动态变化的,不能与我们所想同步,很不方便。
3.3.2 查看内存信息
3.3.3 调用堆栈
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置 。
3.3.4 查看汇编信息
在调试开始之后,有两种方式转到汇编:
第一种方式:右击鼠标,选择【转到反汇编】:
第二种方式:
可以切换到汇编代码。
3.3.5 查看寄存器信息
如果想要多了解一些关于的汇编知识,可以看一看我的另一篇博客 关于函数栈帧 。
建议多动手,要去尝试调试解决问题:
一定要熟练掌握调试技巧。初学者可能80%的时间在写代码,20%的时间在调试;但是一个程序员可能20%的时间在写程序,但是80%的时间在调试。这里所讲的是一些简单基本的调试;以后可能会出现很复杂调试场景:多线程程序的调试等,需要我们不断地去尝试解决。多多使用快捷键,提升效率。5. 调试实例
#include<stdio.h>int main(){int i = 0;int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9.,10 };for (i = 0; i <= 12; i++){arr[i] = 0;printf("hehe\n");}return 0;}
上面有介绍过这个代码在vs的Debug-x86环境下,运行结果出现了死循环!
那么出现死循环的原因是什么?
如果我们只是盯着代码找问题,能看出具体哪一步有问题吗?
所以这里就需要我们通过调试找出题所在!
我们F10去逐步进行调试,打开监视窗口对变量进行观察,调试过程中可以发现数组范围内的元素都正常被赋值为0;
当程序在进行下一步的调试,i的值就会加为10,此时是数组的越界访问了,此时的arr[10]是不是还会被赋值为0,考虑问题会不会是在这里,然后进行调试,可以发现越界后的arr[10]和arr[11]还是正常赋值;
再继续调试,此时i= 12,观察arr[12]还是被赋值为0,但在这一步调试中,观察变量i的值发现在arr[12]被赋值为0的同时i的值也变为了0;
此时我们就通过调试发现了造成程序死循环的原因,为什么将arr[12]赋值为0的同时i的值也变为了0,我们推测可能的原因是arr[12]和i占用的是同一处内存空间!取出俩者的内存地址比较发现确实是相同的!
分析一下俩者内存地址为什么相同!
计算机中的内存空间,分为栈区、堆区和静态区;
而对于局部的数据是在栈区进行内存分配的!
6. 编程常见的错误
编译型错误(语法错误) 直接看错误提示信息(双击),解决问题;或者凭借经验就可以搞定。相对来说简单。 链接型错误 看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在;一般是标识符名不 存在或者拼写错误。 运行时错误 借助调试,逐步定位问题;最难搞 。二.如何写出优秀的的代码
优秀的代码具有如下特征:
代码运行正常bug很少效率高可读性高可维护性高注释清晰文档齐全那么如何写出优秀的代码,这里从俩个方面来讲:
良好的代码风格易于调试的的代码(一)学习运用良好的代码风格
1. 文件结构
每个 C++/C 程序通常分为两个文件。一个文件用于保存程序的声明(declaration), 称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition) 文件。
C++/C 程序的头文件以“.h”为后缀,C 程序的定义文件以“.c”为后缀,C++程序 的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。
1.1 头文件的结构
头文件由俩部分内容组成:
(1)预处理块。
(2)函数和类结构声明等。
【规则 1-1-1】为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。
【规则 1-1-2】用 #include <…>格式来引用标准库的头文件(编译器将 从标准库目录开始搜索)。
【规则 1-2-3】用 #include “filename.h” 格式来引用非标准库的头文件(编译器将 从用户的工作目录开始搜索)。
【建议 1-1-1】头文件中只存放“声明”而不存放“定义”
【建议 1-1-2】不提倡使用全局变量,尽量不要在头文件中出现象 extern int value 这 类声明。
1.2 定义文件的结构
定义文件也有俩部分内容:
(1) 对一些头文件的引用。
(2) 程序的实现体(包括数据和代码)。
1.3 目录结构
如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分 别保存于不同的目录,以便于维护。
例如可将头文件保存于 include 目录,将定义文件保存于 source 目录(可以是多级 目录)。
如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声 明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。
2. 程序的版式
版式虽然不会影响程序的功能,但程序的版式追求清晰、美观,是 程序风格的重要构成因素。
可以把程序的版式比喻为“书法”。
2.1 空行
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加 清晰。
【规则 2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。
【规则 2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应 加空行分隔。
2.2 代码行
【规则 2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样 的代码容易阅读,并且方便于写注释。
【规则 2-2-2】if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论 执行语句有多少都要加{}。这样可以防止书写失误。
【建议 2-2-1】尽可能在定义变量的同时初始化该变量(就近原则)
2.3 代码行内的空格
【规则 2-3-1】关键字之后要留空格。象 const、virtual、inline、case 等关键字之后至少要留一个空格,否则无法辨析关键字。象 if、for、while 等关键字之后应留 一个空格再跟左括号‘(’,以突出关键字。
【规则 2-3-2】函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
【规则 2-3-3】‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。 【规则 2-3-4】‘,’之后要留空格,如 Function(x, y, z)。如果‘;’不是一行的结束 符号,其后要留空格,如 for (initialization; condition; update)。
【规则 2-3-5】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符, 如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
【规则 2-3-6】一元操作符如“!”、“~”、“++”、“–”、“&”(地址运算符)等前后不 加空格。
【规则 2-3-7】象“[]”、“.”、“->”这类操作符前后不加空格。
【建议 2-3-1】对于表达式比较长的 for 语句和 if 语句,为了紧凑起见可以适当地去 掉一些空格,如 for (i=0; i<10; i++)和 if ((a<=b) && (c<=d))
2.4 对齐
【规则 2-4-1】程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用 它们的语句左对齐。
【规则 2-4-2】{ }之内的代码块在‘{’右边数格处左对齐。
2.5 长行拆分
【规则 2-5-1】代码行最大长度宜控制在 70 至 80 个字符以内。代码行不要过长,否 则眼睛看不过来,也不便于打印。
【规则 2-5-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以 便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
3. 命名规则
3.1 共性规则
【规则 3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行“解码”; 标识符最好采用英文单词或其组合,便于记忆和阅读。切忌使用汉语拼音来命名。
【规则 3-1-2】程序中不要出现仅靠大小写区分的相似的标识符。
【规则 3-1-3】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的 作用域不同而不会发生语法错误,但会使人误解。
【规则 3-1-4】变量的名字应当使用“名词”或者“形容词+名词";全局函数的名字应当使用“动词”或者“动词+名词”。
【规则 3-1-5】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
【建议 3-1-6】尽量避免名字中出现数字编号,如 Value1,Value2 等,除非逻辑上的 确需要编号。
3.2 简单的 Windows 应用程序命名规则
【规则 3-2-1】类名和函数名用大写字母开头的单词组合而成
【规则 3-2-2】变量和参数用小写字母开头的单词组合而成。
【规则 3-2-3】常量全用大写的字母,用下划线分割单词。
【规则 3-2-4】静态变量加前缀 s_(表示 static)。
【规则 3-2-5】如果不得已需要全局变量,则使全局变量加前缀 g_(表示 global)。
【规则 3-2-6】类的数据成员加前缀 m_(表示 member),这样可以避免数据成员与 成员函数的参数同名。
4. 表达式和基本语句
4.1运算符
【规则 4-1-1】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免 使用默认的优先级。
4.2 复合表达式
【规则 4-2-1】不要编写太复杂的复合表达式。
4.3 for 语句的循环控制变量
【规则 4-3-1】不可在 for 循环体内修改循环变量,防止 for 循环失去控制。
【建议 4-3-1】建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法。
4.4 switch 语句
【规则 4-4-1】每个 case 语句的结尾不要忘了加 break,否则将导致多个分支重叠 (除非有意使多个分支重叠)。
【规则 4-4-2】不要忘记最后那个 default 分支。即使程序真的不需要 default 处理, 也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以 为你忘了 default 处理。
5. 常量(如宏常量)
【规则 5-1】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或 字符串。
【规则 5-2】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义 文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
【规则 5-3】如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不 应给出一些孤立的值。
(二)写出易于调试的代码
常见的coding技巧:
使用assert尽量使用const养成良好的编码风格添加必要的注释避免编码的陷阱1. 使用 assert(断言)
断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。
在运行过程中,如果 assert 的参数为假,那么程序就会中 止(一般地还会出现提示对话,说明在什么地方引发了 assert)。
2.const修饰指针变量的时候:
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。3. 一些完美代码示例
3.1 模拟实现库函数strcpy
strcpy在拷贝字符串的时候,会把源字符串中的\0也拷贝过去
返回值为目标字符串的首地址
#include<stdio.h>#include<assert.h>char* my_strcpy(char* dest, const char* src){char* ret = dest;//断言assert(src != NULL);assert(dest != NULL);while (*dest++ = *src++){;} return ret;}int main(){char arr1[20] = { 0 };char arr2[] = "hello world";printf("%s\n", my_strcpy(arr1, arr2));return 0;}
3.2 模拟实现strlen
strlen的返回值是一个无符号整形。
unsigned int my_strlen(const char* str){int count = 0;assert(str); while (*str != '\0'){count++;;str++;} return count;}int main(){char arr[] = "hello world";printf("%u\n", my_strlen(arr));return 0;}
本篇博客参考了《高质量的C/C++编程》,里面精细讲解了如何写出优秀的C/C++代码,这里附上网盘资源,感兴趣的小伙伴可以自行提取! 提取码:j6y6
结语
各位小伙伴,看到这里就是缘分嘛,希望我的这些内容可以给你带来那么一丝丝帮助,可以的话三连支持一下呗?!!! 感谢每一位走到这里的小伙伴,我们可以一起学习交流,一起进步?!!!加油?!!!