一、参考:

  本文主要参考《C++编程调试秘笈》一书。

  在编写C++代码时,我们不应该自己捕捉缺陷,而是由编译器和可执行代码为我们做这些事情,该书便提供了这样的一个思考。作者以“调试器友好”的方式编写了一些方便安全检查时所需的宏代码并针对C++代码中最为常见的各种错误制定了一些规则,并用代码实现,使之很容易在运行时捕捉,或者尽可能地在编译时就捕捉缺陷。


二、C++缺陷来源 

  在C语言中为了追求简单和速度,产生高效的编译代码,有时候并未考虑一些方便用户的特性,就会产生一些比较明显的问题,比如垃圾回收,越界检查,缓冲区溢出等等

  1. 程序员可以创建一定长度的数组,并可用一个超出数组边界的索引值访问元素

  2. 滥用最多的是指针运算,程序员可以把指针运算所产生的任何值作为内存地址进行访问,不管该内存是否有效还是能否被访问,如解引用NULL指针strlen(NULL)将会导致程序崩溃

  3. 程序员在运行时使用calloc()malloc()函数动态分配内存并使用free()函数负责释放内存。但是如果忘了销毁,产生了内存泄露(分配内存后并未被释放,最终消耗完系统空间),或者不小心销毁了多次,产生内存悬挂(释放对象后没有将指针置为NULL而之后又解引用了它,未定义的指针解引用是非常严重的)等灾难性的问题

  4. sprintf()和某些字符串函数在写入缓冲区时,它们可能会改写越过缓冲区尾部的内存,从而导致不可预料的程序行为;相比对应的安全版本会安静地在缓冲区结束时截断,但很可能不是我们所期望的结果,建议多使用C++的stringstringstream
    关于C的字符串函数和C++ 的string 、stringstream孰优孰劣还是有争论的,有空的话可以分析分析

  当然C++语言中也存在一些问题

    1. 友元和多重继承并不是个很好的思路
    2. 混用了newdelete,其中一个带方括号和一个不带方括号,
      一定要使用正确的形式:
A* p_object=new A();
A* p_array=new A[size];
delete p_object;
delete []p_array;

  读完这本书,感触还是蛮深的,比如说C++早期的时候主要侧重在面向对象的特性方面的设计,后来陆续引入模板、异常处理、名字空间,到现在的C++11引入类型推导、lambda函数、标准程序库的变更(无序散列表、正则表达式、线程支持等),体会就是:

  • 语言的设计也是会演化的,它源于不断发展中实际的需求,设计什么样的特性是有舍有得的。
  • 设计思想和特性决定了它能做什么事,不能做什么事,有怎么的好处也有相应的缺陷。
  • 任何语言都不是silver bullet ,你不能单纯说它好坏. 只有当认识清楚语言背后的设计思想、演化史,了解各自的特性和缺点,就不会出现遇到具体问题而直接掉入编程语言的坑了

  觉得需要深入了解的主题:

  • Unix哲学编程艺术(Unix的设计思想是很值得思考和借鉴的)
  • C++语言的设计和演化、Java语言的演化设计史(虚拟机、设计模式,对比Java和C++的不同点)
  • 计算机程序的构造和解释,里面解释函数式编程语言它是如何工作的(表示一直不理解)
  • Python、Go这两种语言它有着怎样不同的设计

三、何时捕捉陷阱   

  在编译时诊断错误,有如下规则:

  1. 禁止隐式类型转换:关键字explicit声明一个接受一个参数的构造函数,并禁止使用转换操作符
  2. 用不同的类表示不同的数据类型
  3. 不要使用单纯功能的枚举创建整形常量,而是用它们创建新类型

  为什么呢,下面将一一解释

  A. 假设我们有两个类A和B,并有一个期望接受一个B类型的参数的函数:

void doSomething(const B& b)

  但是我们不小心向它提供了A类型的对象:

A a(input);
doSomething(a);

  某些情况,这样的代码可通过编译,原因是它有可能平静的进行隐式类型转换:A转换成B。它可能通过以下两种方式发生

  1. B类接受含A类型的参数构造函数,它可以隐式地把A转换为B
class B {
public:
B(const A& a);
}

   2.A类具有一个可以将其转换为B的操作符,以明确的方式提供了转换方法

class A{
public:
//转换操作符operator type():type可以是基本数据类型,类,结构体
operator B() const;
}

  所以针对上述问题,对于所有接受一个参数的构造函数用关键字explicit声明,并且不建议用转换操作符,这是值得推荐的做法。

  一般而言,隐式转换的所有可能性都是不好的思路,还记得深入计算机系统第二章讲过FreeBSD开源系统曾出现的getpeername的安全漏洞么,这是由于无符号数和有符号数间的不匹配造成了隐式类型转换。不过我们还可以用另外一个方法进行转换

class A{
public:
B asB() const;
} A a(input);
doSomething(a.asB()); // 显式转换

  B. 定义两个枚举,分别表示一周中的某天及月份,这些常量都是整数。假设我们有一个期望接受一周中的某天作为参数的函数

enum {SUN1,MON=1,TUE,WED,THU,FRI,SAT};
enum {JAN=1,FEB,...,DEC}; void func(int day_of_week);

  因而下面调用将不会产生任何警告的情况下通过编译:func(JAN);

  所以捕捉此类缺陷的办法就是创建新类型的枚举,直接限定了新类型的枚举范围,这样就可以在编译时判断是否有错误。

typedef enum {SUN1,MON=1,TUE,WED,THU,FRI,SAT} DayofWeek;
typedef enum {JAN=1,FEB,...,DEC} Month;

四、在运行时遇见错误如何处理

  我们把精力集中在运行时的一类错误--缺陷。为了捕捉缺陷专门编写的一段代码称为安全检查,当其失败时,就表示发现了缺陷,那如何处理呢,这里作者提供这样的一个思路

  定义一个SCPP_ASSERT宏,永久性的安全检查,用来捕捉运行时错误,并提供与错误有关的具体信息

#scpp_assert.h
#define SCPP_ASSERT(condition,msg) \
if(!(condition)) { \
std:ostringstream s; \
s << msg; \
SCPP_AssertErrorHandler(__FILE__,__LINE__,s.str().c_str()); \
} #scpp_assert.cpp
void SCPP_AssertErrorHandler(const char *file_name,
unsigned line_no,
const char *msg){
//此处适合插入断点,合适情况下还可向一个日志文件写入相同的信息
#ifdef SCPP_THROW_EXCEPTION_ON_BUG
throw scpp::ScppAssertFailedException(file_name,
line_no,msg);
#else
cerr << msg << "in file "<<file_name <<
" #" <<line_no <<endl<<flush;
exit(1);
#endif
} #scpp.h
#ifdef SCPP_THROW_EXCEPTION_ON_BUG
#include<exception> namespace scpp {
class ScppAssertFailedException :public std::exception {
private:
std::string what_;
public:
ScppAssertFailedException(const char *file_name, unsigned line_no,
const char *msg);
virtual void const char* getwhat() const throw() { return what_.c_str();}
virtual ~ScppAssertFailedException() throw() {}
} } #scpp_assert.cpp
#ifdef SCPP_THROW_EXCEPTION_ON_BUG
namespace scpp {
ScppAssertFailedException::ScppAssertFailedException(const char *file_name,
unsigned line_no,
const char *msg) {
ostringstream s;
s << "SCPP Assertion failed with message " << msg <<" in file " <<file_name << " # " << line_no;
what_=s.str();
}
}
#endif

  我们可以看到该宏接受一个条件和一条错误信息。条件为真不执行任何事情,为假时错误信息会输出到ostringstream中,并且错误处理函数将被调用。这里有两个问题:

  • 问:为什么要调用scpp_assert.cpp文件中一个单独AssertErrorHandler函数,而不是在scpp_assert.h文件的宏中执行相同的操作
    答:调试器更擅长对函数而不是宏进行逐步调试

  • 问:为什么AssertErrorHandler函数向我们提供了两种选择机会,要么终止程序,要么抛出一个异常
    答:在最常见的情况下我们发现第一个缺陷时默认采取的办法是终止程序,修补缺陷并再次开始,这时候将打印出错误信息并终止程序,即对应没有定义的SCPP_THROW_EXCEPTION_ON_BUG符号。
    那么定义了该符号的情况呢,在某些情况下,有部分安全检查必须保留在代码中,即使是在产品模式下。假设有一个持续依次处理大量请求的程序在处理某个请求时安全检查失败,终止程序并不是理想的选择,应该采取的办法是抛出一个异常,包含详细的错误信息并把错误信息记录在某日志文件中,可能还需要发送邮件或警报,宣布对当前请求的处理失败,同时继续处理发送其他的请求。因而在scpp_assert.h声明了一个异常类

  • 问:什么时候编写安全检查?
    答:如果我们的想法是等我们编码好后再回过头来添加安全检查,这个计划可能永远不糊实施。
    较好的建议是从一开始编写新函数新类新功能时等具体的代码前就应该为它所有的输入编写好安全检查和测试。
    可以看出编写安全检查并不困难,它不仅让你更明确你所要做的工作,更重要的是它会在以后的测试阶段得到足够的回报,这要比你以后回过头来调试代码要方便得多。

  注:要养成这样的习惯,单元测试也是类似的思路:编码的同时编写好安全检查和测试,更明确的办法是当我们开始编写具体的代码前为它的所有输入编写安全检查

  如下类似的代码用来测试:

#include <iostream>
#include "scpp_assert.h" using namespace std;
int main(int argc,char *argv[]) {
cout << "Hello,SCPP_ASSERT" << endl; try {
double price=100.0 ; //合理价格
SCPP_ASSERT(0< price && price <=1e6,"Stock price " <<price <<" is out of range "); //条件成立时不执行 price=-1;
SCPP_ASSERT(0< price && price <=1e6,"Stock price " <<price <<" is out of range "); //条件不成立时执行并捕获异常
} catch (const exception& ex) {
cerr << "Exception caught in " << _FILE_ << " # "<< _LINE_ << ". "<< endl;
cerr << ex.what() << endl;
}
return 0;
} //在SCPP_ASSERT宏中也可使用任何类的对象,只要它定义了<< 操作符,设计和测试如下:
/* Test :
*MyClass obj(inputs);
*SCPP_ASSERT(obj.IsValid(),"Object "<< obj <<" is invalid.");
*/
class MyClass {
public:
bool IsValid() const ; //对象状态有效即返回true
//Implement constructors 、destructors
private:
int data;
friend std::ostream operator << (std::ostream& os ,const MyClass& obj);
}
inline std::ostream operator << (std::ostream& os ,const MyClass& obj) {
//执行一些任务,按被人理解的格式显示对象
os << obj.data;
return os;
}
/*
* Output :
* Hello,SCPP_ASSERT
* Exception caught in xxx.cpp #13 .
* SCPP assertion failed with message 'Stock price -1 is out of range ' in file xxx.cpp #13
*/

  问:什么时候使用它
  答:我们意识到代码中可能含有大量的安全检查,有些是永久性的,有些是临时性的。为了保持C++代码执行的高效性和有效性,在不同运行阶段执行不同的策略:

    • 在Debug模式,打开测试安全检查,对错误进行调试
    • 在Release模式,打开测试安全检查,快速调试(考虑到1的安全检查会较慢)
    • 在Release模式下关闭安全检查,发布产品

  代码实现如下:

#scpp_assert.h
#ifdef _DEBUG
#define SCPP_TEST_ASSERT_ON
#endif #ifdef SCPP_TEST_ASSERT_ON
#define SCPP_TEST_ASSERT(condition,msg) SCPP_ASSERT(condition,msg)
#else
#define SCPP_TEST_ASSERT(condition,msg)

  可以看到SCPP_ASSERT是永久性的安全检查,SCPP_TEST_ASSERT可以在编译时打开

  下面分别就索引越界、编写一致的比较操作符、未初始化变量指针操作、内存泄露等缺陷进行一一处理,用于在代码进入产品阶段前捕捉各类缺陷。


C++调试总结的更多相关文章

  1. C# Web应用调试开启外部访问

    在用C#开发Web应用时有个痛点,就是本机用VS开启Web应用调试时外部机器无法访问此Web应用.这里将会介绍如何通过设置允许局域网和外网机器访问本机的Web应用. 目录 1. 设置内网访问 2. 设 ...

  2. NodeJs之调试

    关于调试 当我们只专注于前端的时候,我们习惯性F12,这会给我们带来安全与舒心的感觉. 但是当我们使用NodeJs来开发后台的时候,我想噩梦来了. 但是也别泰国担心,NodeJs的调试是很不方便!这是 ...

  3. 微信公众号开发之VS远程调试

    目录 (一)微信公众号开发之VS远程调试 (二)微信公众号开发之基础梳理 (三)微信公众号开发之自动消息回复和自定义菜单 前言 微信公众平台消息接口的工作原理大概可以这样理解:从用户端到公众号端一个流 ...

  4. 写出易调试的SQL(修订版)

    h4 { background: #698B22 !important; color: #FFFFFF; font-family: "微软雅黑", "宋体", ...

  5. tomcat开发远程调试端口以及利用eclipse进行远程调试

    一.tomcat开发远程调试端口 方法1 WIN系统 在catalina.bat里:  SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compi ...

  6. Hawk 4.7 单步调试

    单步调试的意义 已经编写的工作流,可能会因为某些外界环境的变化而出错,此时需要排除错误,我们可以使用单步调试. 单步调试的本质,相当于只使用前n个模块,这样就能看到每个步骤下,流的改变. 例子 还是上 ...

  7. Visual Studio 2012远程调试中遇到的问题

    有的时候开发环境没问题的代码在生产环境中会某些开发环境无法重现的问题,或者需要对生产环境代码进行远程调试该怎么办? Vs已经提供给开发者远程调试的工具 下面简单讲讲该怎么用,前期准备:1.本地登录账户 ...

  8. iOS逆向工程之Hopper+LLDB调试第三方App

    LLDB是Low Level Debugger的简称,在iOS开发的调试中LLDB是经常使用的,LLDB是Xcode内置的动态调试工具.使用LLDB可以动态的调试你的应用程序,如果你不做其他的额外处理 ...

  9. 写出易调试的SQL

    h4 { background: #698B22 !important; color: #FFFFFF; font-family: "微软雅黑", "宋体", ...

  10. linux应用调试技术之GDB和GDBServer

    1.调试原理 GDB调试是应用程序在开发板上运行,然后在PC机上对开发板上得应用程序进行调试,PC机运行GDB,开发板上运行GDBServer.在应用程序调试的时候,pc机上的gdb向开发板上的GDB ...

随机推荐

  1. python 字典和列表嵌套用法

    python中字典和列表的使用,在数据处理中应该是最常用的,这两个熟练后基本可以应付大部分场景了.不过网上的基础教程只告诉你列表.字典是什么,如何使用,很少做组合说明. 刚好工作中采集promethe ...

  2. 【.NET 与树莓派】TM1638 模块的按键扫描

    上一篇水文中,老周马马虎虎地介绍 TM1638 的数码管驱动,这个模块除了驱动 LED 数码管,还有一个功能:按键扫描.记得前面的水文中老周写过一个 16 个按键的模块.那个是我们自己写代码去完成键扫 ...

  3. C语言:C99 中的37个关键字

    一.数据类型关键字(12个): 1.char [tʃɑ:]:声明字符型变量或函数 2.double [ˈdʌbəl] :声明双精度变量或函数 3.enum :声明枚举类型 4.float [fləut ...

  4. 17、linux root用户密码找回

    17.1.救援模式: 光盘模式启动(第一启动项) 删除/mnt/sysimage/etc/passwd root的密码,halt重启. 改为硬盘启动模式,无密码进入root,为root新建密码 17. ...

  5. mui 移动端网页双击事件处理

    使用场景:需要在动态生成li列表数据中添加双击事件 定义事件 : var date1=null; function listdb(myKey) { var date2 = new Date(); if ...

  6. Redis 底层数据结构之链表

    文章参考:<Redis设计与实现>黄建宏 链表 链表提供了高效的节点重排能力,以及可以顺序访问,也可以通过增删节点灵活调整链表长度,Redis中的列表.发布订阅.慢查询.监视器等功能均用到 ...

  7. bugku--cookie欺骗

    打开题目一看,是一串的东西,再看了一下filename发现不对劲了,明显是base64编码,拿去解码一下, 发现是这个,说明是filename,是需要解析的哪个文件名,把index.php编码一下,试 ...

  8. tableview折叠动效

    缘起于看见书旗小说的列表有点击折叠的动效,觉得十分炫酷.想了三分钟,不知道怎么写.晚上百度了下,知道了大致流程,于是自己实现了下,发现不少坑,于是写下这篇博文 实现原理: 1 tableview ce ...

  9. STM32笔记三

    1.单片机有两种存储器,程序存储器用来存储编写的程序,数据存储器用来存储单片机工作时的临时数据.内部存储器分为工作寄存器区.位寻址区.数据缓存区和特殊功能寄存器区. 2.位:数据存储的最小单位.在计算 ...

  10. .NET Core/.NET5/.NET6 开源项目汇总11:WPF组件库1

    系列目录     [已更新最新开发文章,点击查看详细] WPF(Windows Presentation Foundation)是微软推出的基于Windows 的用户界面框架,属于.NET Frame ...