当前位置 博文首页 > 技术杂谈:C/C++ 踩过的坑和防御式编程
本文来自作者?林奇思妙想?在?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++ 共有部分(有基础的同学可以略过这一部分 , 或者可以跟我一起温习)。
常量
常量是指运行时值不能改动的一类 “变量”,他们的值是编译进目标程序中的。
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++ 所独有,也称对象。通常我们用一个类来表示一类客观事物的层次、继承关系。
如下图所示:
图 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 到现在,已经有 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();