当前位置 博文首页 > 技术杂谈:C/C++ 踩过的坑和防御式编程

    技术杂谈:C/C++ 踩过的坑和防御式编程

    作者:[db:作者] 时间:2021-06-14 21:42



    本文来自作者?林奇思妙想??GitChat?上分享 「C/C++ 踩过的坑和防御式编程」,阅读原文查看交流实录。

    文末高能

    编辑 | 哈比

    相信你或多或少地用过或者了解过 C/C++,尽管今天越来越少地人直接使用它,但今天软件世界大多数软件都构筑于它,包括编译器和操作系统。因此掌握一些 C/C++ 技能的重要性不言而喻。

    这场 Chat 本人将从小处入手,以亲身踩过的坑作为示例,讲述一下 C++ 的常见的坑,以及其防御方法——防御式编程。主要内容包括:

    • C/C++ 基础知识简介

    • C/C++ 常见问题复现示例

    • 内存泄露问题排查

    • 防御式编程理论

    • 防御式编程实践


    大家好,我是林奇思妙想。很高兴能够和各位在GitChat上交流一些平常用 C/C++, 写 C/C++ 的经验分享。

    为了表明这是一场严肃的、有深度的交流,我们先不走偏,先回顾一下经典教材上关于 C/C++ 的基础知识。尽量保持简单,点到为止,希望你没有被吓走。作为一枚典型猿族,我就话不多说,直接带领大家入坑(笑~)。

    注:所有的程序示例在 MS VS2017 下调试的。

    C/C++ 基础知识简介

    我们说到 C/C++ 一般指两部分:

    • C++ 兼容 C 的部分

    • C++ 独有部分

    先看一下 C/C++ 共有部分(有基础的同学可以略过这一部分 , 或者可以跟我一起温习)。

    C/C++ 共有部分

    常量

    常量是指运行时值不能改动的一类 “变量”,他们的值是编译进目标程序中的。

    1)立即数

    如字面常量 12, 123.5f, “abc”.

    2)常量对象

    const int SIZE_A = 11;
    const Mat MAT_A(12,22,-1);

    变量在运行时占有内存地址空间,且它的值可以在运行时被更改

    变量

    1)普通值变量

    float a = 9.1f; Mat mat_a(1,2,-1);

    2)指针变量 p_a

    int *p_a = &a;

    表达式

    表达式是指能被编译器编译为指令的语句,通常以 “;” 结束。

    表达式分以下几种:

    1)赋值

    int a = 4;

    2)逗号

    a = 9, ?b = 11;

    3)判断

    if (a > b) { do_something(); }

    4)循环

    for(int i=0; i<MAX; i++) { do_something(); }

    5)函数调用

    do_something();

    其它数据结构

    结构体等

    struct st_a { int x; int y; };

    通常是 public 的,即没有封装性。

    C++ 独有

    类是 C++ 所独有,也称对象。通常我们用一个类来表示一类客观事物的层次、继承关系。

    如下图所示:


    图 0.1 类的继承关系

    1)继承

    继承就如 0.1 所示,从父类到子类,越来越具体。

    2)封装

    封装是指对于某一子类,可以控制哪些信息能被外部看到,如控制我们能获取手机的大小,颜色等属性,对用户隐藏手机串号等信息。

    3)多态

    多态是 C++ 精华所在,但也是 C++ 的难点所在。一些教材常常用高大上的描述把人搞昏。譬如我手边这本书里面是这样讲多态的(C++ 程序设计教程——钱能版):

    “ 多态是基于类的层次结构的,当指针飘忽不定地可能指向类层次中的上下不同对象时,以指针间访的形式实施的操作便是表现多态的条件。”

    这段文字真是一骑绝尘,不食人间烟火,难免把人带到小黑屋子——关着。

    用通俗的话说,多态是指多个子类有一个共有操作,我们在父类中定义一个统一的抽象虚接口,然后各个子类分别实现。

    这样子,运行时,依据子类是什么,动态选择子类的方法。 这样子描述,我们又不可避免地走入了经典教材的巨梗——不懂啊! 不如直接看如下代码吧。

    如下代码所示:

    // code start/* B is base class, A -- C is sub-class */class B {public: ? ? ? ? ? ?virtual void do_sth() = 0; };class A : public B {public: ? ? ? ? ? ?void do_sth() ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?cout << "- A do_sth()\n"; ? ? ? ? ? ?} };class C : public B {public: ? ? ? ? ? ?void do_sth() ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?cout << "- C do_sth()\n"; ? ? ? ? ? ?} };void do_sth(B *id_b) { ? ? ? ? ? ?id_b->do_sth(); }int main() { ? ? ? ? ? ?A* id_a = new A(); ? ? ? ? ? ?C* id_c = new C(); ? ? ? ? ? ?do_sth(id_a); ? ? ? ? ? ?do_sth(id_c); ? ? ? ? ? ?return 0; }// code end

    函数运行输出结果是:

    A do_sth() C do_sth()

    注意其中的粗体代码。

    看 “ void do_sth(B *id_b)” , ?我们用的基类指针作为函数的接口参数,但是 “do_sth(id_a); ” 传递参数时,我们传的是 A 或者 C 对象的指针! 多态使得调用的接口一致,更利于抽象和简化。

    C/C++ 常见问题复现示例

    如果算上最新接触 C 到现在,已经有 9 年。写过无数的 C/C++ 代码。有些坑是自己挖的,有些则是语言层面上的 “陷阱” 。

    坑 1

    立即数左移越出范围。先看一段代码:

    assert((1 << ?3) == pow(2, ?3)); assert((1 << 30) == pow(2, 30)); assert((1 << 62) == pow(2, 62));

    先不运行,猜测一下问题在哪一行?

    运行结果如下:

    Assertion failed: (1 << 62) == pow(2, 62), file

    我们再进一步看一下他们的值:

    cout << pow(2, 62) << endl; cout << (1 << 62) << endl;

    4.61169e+18 0

    可知前者是对的,后者是错的,在 C 语言中,左移结果最大是 32 位。

    为了验证,我们再看一下:

    cout << pow(2, 62) << endl; cout << (1 << 62) << endl; cout << (4.61169e+18) << endl;

    运行结果是:

    4.61169e+18 0 4.61169e+18

    符合我们的预期。

    坑 2

    sprintf() 越界问题。

    ? ? ? ?char buf[10]; ? ? ? ?float x = 1/3.0f; ? ? ? ?sprintf(buf, "cols = %f", x); ? ? ? ?printf(buf);

    运行后,buf 会越界,出现地址异常!正确的做法是给 buf 一个更大的地址。 但是这类栈溢出在大型的工程中,防不胜防。其实可以在 C++ 中,考虑用更一种更安全的方式。

    ? ? ? ? ? ?float f = 1 / 3.0f; ? ? ? ? ? ?ostringstream ss; ? ? ? ? ? ?ss << "num is " << f << endl; ? ? ? ? ? ?cout << ss.str();