Nov 1

传送门: http://microcai.gsalex.net/2012/10/内存管理随笔

 

思考一:

任何一个程序,只要不是 helloworld 那样的简单程序,必然会用到内存管理。内存管理是写程序不可避免的过程。C程序员最大的恶魔就是野指针和内存泄漏。这是每个C程序员的噩梦。C++继承了 C的缺陷,基本上半斤八两。而且C++还可以自由重载new操作符,给内存管理更加重了复杂性。
我常常在想,写BASH脚本的时候,我们有管理过内存么?即便是java那样的语言,内存管理也是后台进行的,并不是可避免的,俗称GC。可是写脚本的时候,真的完全没用内存管理方面的困扰。

到底是哪里出了问题呢?到底为何脚本就不用管理内存?

思考二:

能不能完全不依靠malloc/free new/delete 编写出一个 c/c++程序呢?

结果是显然的,能!

但是只有对 hello world 那样的程序才有用。最近的一项小随笔项目证实了我的想法 https://github.com/microcai/hm 。在这个程序里,我没用使用任何 malloc/free new/delete 来管理内存。

为何这个程序可以不用内存管理呢? 于是在另一个随笔项目里,我依然使用了内存管理,虽然是智能指针自动管理的,但是毕竟使用了 new 操作   https://github.com/microcai/googleproxy 。我在想,凭啥这个程序就必须使用  new 了?

思考三:

程序,说到底就是一个状态机。在 hm 程序里,我采用的是“过程化”编程,外加同步多进程的IO模型。在 googleproxy 程序里,使用的是单线程异步IO的模型。

hm里,状态机的状态就是进程的状态。一步一步执行下去,状态随之切换。cpu执行到哪一行,哪一行就是当前状态。

googleproxy里,情况有了变化,因为使用了异步,所有的状态变化都是围绕一个中心进行的: boost::io_service::run() 。 在 run() 里,通过回调来通知状态的变化。也就是说,cpu执行到哪个回调,哪个就是当前状态。

区别是什么:

在不同的状态之间,我们有数据要共享! hm 多使用局部变量,不同的状态需要共享数据,通常也处于同一代码层级!可以直接引用需要的数据!

googleproxy 里,不同的状态之间,是不同的 run() 操作的回调,需要共享数据,但是上一个状态的局部变量已经消失!上一个状态的局部变量已经消失!

要跨栈域引用共享数据,唯一的办法就是将数据创建在堆上!

简单的来说,在一个程序里,所有代码的执行路径可以归纳为从 main() 开始的一个调用树。一个函数只能引用本层和上层的局部变量,无法引用子层和兄弟层和兄弟的子层的局部变量。因为这些地方的变量都是不存在的呀!

所以,只能将共享数据创建在堆上。这样才能跨过调用树的生存周期!

这也是唯一需要堆管理的理由。

如果一个变量要进入函数的时候创建在堆上,退出的时候释放,通常的做法其实就是使用栈变量,或者是使用可变长度的stl容器。

如果返回值是对象,也不要使用指针了,直接返回对象吧!别担心临时对象拷贝开销了。

只有当跨栈域共享对象的时候,才考虑使用堆吧!
记住:即便如此,也不要使用裸指针。裸指针只用来进行直接内存访问(底层编程的时候),千万别用来引用对象。使用智能指针吧!放弃对象的拷贝开销的担心 吧! 那几百个周期的拷贝操作比 new/delete 的内存管理代码的开销相比,还是低太多里。何况有了 c++11 的 Move 语义,很多时候已经没用拷贝开销里。

 

放心大胆的脚本化C++程序吧! 实在需要跨栈共享数据的时候,使用 shared_ptr 引用吧!

Oct 12

 

曾几何时,学会了C语言。安装了VC6这个神叉IDE。

然后被迫开始CPP路程。由于C++兼容C,所以一直在以C的方式写C++。然后慢慢的开始学写C++代码。写C++代码是从MFC开始的。慢慢的,我学会了用class,感觉是个比struct好用的多的结构体。再慢慢的,我学会了继承,还有... 多重继承。

MFC 就是我的导师。开始不停的向MFC学习。既然用了c++的继承,就认识到了继承的陷阱。继承后,构造函数和构析函数的执行次序,等等。还有虚继承后的各种问题和陷阱。

一一去了解。

 

在程序结构方面,开始向 MFC 看齐。喜欢把任何操作都包装到class里。设计class的时候,开始过度设计。总考虑到某天我会需要继承它。说不定还需要多重继承。等等。明明已经设计好class,却喜欢继续添加功能。永远用不到的功能。只因为我“未来可能需要在别的程序里用,可能需要继承它。”

任何一个程序,我都为它设计对象,全局的程序对象又有子对象,子又有子,子子孙孙无穷尽也~

成员变量一多,构造函数里成员变量初始化代码就变多。然后设计构造函数重载。写更多的用不到的构造函数。

我没有意识到,我已经犯了过度设计的毛病了

 

而c++另一个强大的功能“模板”我却一无所知。当我发现了c++还有模板功能的时候,我只是简单的尝试了一下,发现模板无法在VC下很好的被自动完成。又听说模板会导致代码膨胀,诸如此类。我只小用一下模板。就丢弃了模板。继续我过度设计的c++之路。

某一天,我开始Linux之路。

当我开始写出第一个GTK程序的时候,发现了C之美。C可以写出和C++一样的对象代码。而且没有了 C++的许多陷阱。

也正因为C,我开始摒弃了过度设计。所设计的API皆以现阶段够用为限。未来怎么样,未来再添加好了。没有了c++的包袱,再也不设计class了。都是简单的函数。函数完成一个“过程”。而不再是操作一个“对象”。让我把精力放到了“过程”本身,而不会再去设计“漂亮的对象”。

之后阅读到了Linus对C++的一篇讽刺邮件。对比自身,多多感触。c++确实是个容易写出糟糕代码的语言。

从此发誓不再用c++,只以C写代码。

 

再一次拾起C++,是因为c++11的发布,有了一些新的语法糖。我虽然不再打算写c++代码,但是没打算不再读别人的c++代码。如果别人用c++11写了新的代码,我确看不懂,不是损失是什么。我决定看一看c++11。

c++11最令我着迷的新特性就是lambda。还有新的for语法。当然,boost::bind 和 boost::function 进入了 std。成为的标准的一部分。我对 std::function 的印象只限于“更好的函数指针替代”。

lambda 一用,发现非常不错。好多麻烦的 static 函数都可以被匿名的 lambda取代了。我喜欢这种让名称空间更干净的感觉。

我开始纠结lambda了。动摇了。如果只用c++兼容C的那部分,然后lambda,就是带lambda的C嘛!这样自欺欺人的开始用C++。

参与了朋友的一个小项目。他是个重度c++粉。但是因为他的关系,我开始真正的拿起了boost。他说,没有boost他就没办法写代码。

这里有个小插曲,我在Linux下一直用eclipse编程,但是也因为boost把eclipse搞崩溃了,重拾了KDevelop。结果发现KDevelop已经不再是我第一次使用的时候(Kdevelop 3.? 的样子,好多年前的尝试。)那样使用正则匹配的自动完成了,真正的和eclipse一样是通过语法解析获得的自动完成列表。这导致KDevelop的自动完成功能异常强大,和 eclipse一样智能。所不同的是,eclipse在解析boost的代码的时候会时不时的崩溃,而且eclipse花了很久很久的假死状态解析boost头文件。毕竟是java开发的,速度还是不行啊!但是KDevelop完成boost头文件的解析只需要几秒钟。第一次打开cpp文件的时候,KDevelop在后台花了及秒钟解析了boost头文件,然后打代码的时候boost的所有功能就全部都能提示出来了。和eclipse一样的自动完成,速度快了几百倍,我马上切换到了KDevelop,并卸载了eclipse。

其实之前也尝试过boost,都被假死状态的eclipse弄怕了。所以关了eclipse的index功能没了boost的语法提升瞎写过。

但是换到KDevelop后那畅快淋淋的感觉,打出 boost:: 后那完善的提示,我马上开始全身心的试用boost。

就是这一试用,让我重回了c++的怀抱。重回c++后,我发现了c++正确的用法:

  • class只能作为基础,构建比int/long/char要高级点的对象。绝不能用来构造程序本身。像MFC那样搞个 CMyApp 绝对是对c++的侮辱。也就是说,程序主体仍是C语言式的。
  • 可以用class构建string,构建bigint,用重载为bigint编写大数乘法... 这才是 class 和运算符重载的意义。绝对不能肆意滥用。
  • 必要的时候编写模板代替宏。
  • 模板更好的用法是使用boost这样现成的模板库。
  • 设计回调函数,使用 std::function/boost::function。这样可以使用 std::bind/boost::bind 绑定任意多的参数给回调函数用,避免C里的void*user_data设计。
  • 虚回调函数绝对是个傻逼设计。迫使用户使用继承并进行虚成员函数重载。