译自Deep C (and C++) by Olve Maudal and Jon Jagger,本身半桶水不到,如果哪位网友发现有错,留言指出吧:)
第二位候选者表现不错,那么,相比大多数程序员,他还有什么潜力没有被挖掘呢?
可以从以下几个角度去考察:
有关平台的问题—32位与64位的编程经验;
内存对齐;
CPU以及内存优化;
C语言的精髓;
接下来,主要分享一下以下相关内容:
内存模型;
优化;
C语言之精髓;
内存模型:
静态存储区(static storage):如果一个对象的标识符被声明为具有内部链接或是外部链接,或是存储类型说明符是static,那么这个对象具有静态生存期。这个对象的生命周期是整个程序的运行周期。
PS:内部链接,也就是编译单元内可见,是需要使用static来修饰的,连接程序不可见;外部链接,是指别的编译单元可见,也就是链接程序可见。我这里还不太清楚为什么需要三种情况来说明。
int* immortal(void) { static int storage = 42; return &storage; }
自动存储区(automatic storage):如果一个对象没有被指明是内部链接还是外部链接,并且也没有static修饰,那么,这个对象具有自动生存期,也称之为本地生存期。一般使用auto说明符来修饰,只在块内的变量声明中允许使用,这样是默认的情况,因此,很少看到auto说明符。简单地说,自动存储区的变量,在一对{}之间有效。
int* zombie(void) { auto int storage = 42; return &storage; }
分配的存储区域(allocated storage):调用calloc函数,malloc函数,realloc函数分配的内存,称之为分配的存储区域。他们的作用域(生命周期会是更好的术语吗?)在分配和释放之间。
int* finite(void) { int* ptr = malloc(sizeof(int*)); *ptr = 42; return ptr; }
优化相关:
一般来说,编译的时候,你都应该打开优化选项。强制编译器更努力的去发现更多的潜在的问题。
上面,同样地代码,打开优化选项的编译器得到了警告信息:a 没有初始化。
C语言的精髓:
C语言的精髓体现在很多方面,但其本质在于一种社区情感(communitysentiment),这种社区情感建立在C语言的基本原则之上。
C语言原理简介:
1、 相信程序员;
2、 保持语言简单精炼;
3、 对每一种操作,仅提供一种方法;(译者注:?)
4、 尽可能的快,但不保证兼容性;
5、 保持概念上的简单;
6、 不阻止程序员做他们需要做的事。
现在来考察一下我们的候选者关于C++的知识:)
你:1到10分,你觉得你对C++的理解可以打几分?
第一个候选者:我觉得我可以打8到9分。
第二个候选者:4分,最多也就5分了。我还需要多加学习C++。
这时,C++之父Bjarne Stroustrup在远方传来声音:我觉得我可以打7分。(OH,MY GOD!!)
那么,下面的代码段,会输出什么?
#include <iostream> struct X { int a; char b; int c; }; int main(void) { std::cout << sizeof(X) << std::endl; }
第二个候选者:这个结构体是一个朴素的结构体(POD:plain old data),C++标准保证在使用POD的时候,和C语言没有任何区别。因此,在你的机器上(64位机器,运行在32位兼容模式下),我觉得会输出12.
顺便说一下,使用func(void)而不是用func()显得有点诡异,因为C++中,void是默认情况,这个相对于C语言的默认是任意多的参数,是不一样的。这个规则同样适用于main函数。当然,这不会带来什么伤害。但这样的代码,看起来就像是顽固的C程序员在痛苦的学习C++的时候所写的。下面的代码,看起来更像C++:
#include <iostream> struct X { int a; char b; int c; }; int main() { std::cout << sizeof(X) << std::endl; }
第一个候选者:这个程序会打印12.
你:好。如果我添加一个成员函数,会怎么样?比如:
#include <iostream> struct X { int a; char b; int c; void set_value(int v) { a = v; } }; int main() { std::cout << sizeof(X) << std::endl; }
第一个候选者:啊?C++中可以这样做吗?我觉得你应该使用类(class)。
你:C++中,class和struct有什么区别?
候选者:在一个class中,你可以有成员函数,但是我不认为在struct中可以拥有成员函数。莫非可以?难道是默认的访问权限不同?(Is it the default visibility that is different?)
不管怎样,现在程序会输出16.因为,会有一个指针指向这个成员函数。
你:真的?如果我多增加两个函数呢?比如:
#include <iostream> struct X { int a; char b; int c; void set_value(int v) { a = v; } int get_value() { return a; } void increase_value() { a++; } }; int main() { std::cout << sizeof(X) << std::endl; }
第一个候选者:我觉得对打印24,多了两个指针?
你:在我的机器上,打印的值比24小。
候选者:啊!对了,当然,这个struct有一个函数指针的表,因此他仅仅需要一个指向这个表的指针!我确实对此有一个很深的理解,我差点忘记了,呵呵。
你:事实上,在我的机器上,这段代码输出了12.
候选者心里犯嘀咕:哦?可能是某些诡异的优化措施在捣鬼,可能是因为这些函数永远不会被调用。
你对第二个候选者说:你怎么想的?
第二个候选者:在你的机器上?我觉得还是12?
你:好,为什么?
候选者:因为以这种方式来增加成员函数,不会增加struct的所占内存的大小。对象对他的函数一无所知,反过来,是函数知道他具体属于哪一个对象。如果你把这写成C语言的形式,就会变得明朗起来了。
你:你是指这样的?
struct X { int a; char b; int c; }; void set_value(struct X* this, int v) { this->a = v; } int get_value(struct X* this) { return this->a; } void increase_value(struct X* this) { this->a++; }
第二个候选者:恩。就想这样的。现在很明显很看出,类似这样的函数是不会增加类型和对象的内存大小的。
你:那么现在呢?
#include <iostream> struct X { int a; char b; int c; virtual void set_value(int v) { a = v; } int get_value() { return a; } void increase_value() { a++; } }; int main() { std::cout << sizeof(X) << std::endl; }
//注意改变:第一个成员函数变成了虚函数。
第二个候选者:类型所占用的内存大小很有可能会增加。C++标准没有详细说明虚类(virtual class)和重载(overriding)具体如何实现。但是一般都是维护一个虚函数表,因此你需要一个指针指向这个虚函数表。所以,这种情况下会增加8字节。这个程序是输出20吗?
你:我运行这段程序的时候,得到了24.
候选者:别担心。极有可能是某些额外的填充,以便对齐指针类型(之前说的内存对齐问题)。
你:不错。再改一下代码。
#include <iostream> struct X { int a; char b; int c; virtual void set_value(int v) { a = v; } virtual int get_value() { return a; } virtual void increase_value() { a++; } }; int main() { std::cout << sizeof(X) << std::endl; }
现在会发生什么?
第二个候选者:依旧打印24.每一个类,只有一个虚函数表指针的。
你:恩。什么是虚函数表?
候选者:在C++中,一般使用虚函数表技术来支持多态性。它基本上就是函数调用的跳转表(jump table),依靠虚函数表,在继承体系中,你可以实现函数的重载。
让我们来看看另一段代码:
#include "B.hpp" class A { public: A(int sz) { sz_ = sz; v = new B[sz_]; } ~A() { delete v; } //... private: //... B* v; int sz_; };
看看这段代码。假设我是一名资深的C++程序员,现在要加入你的团队。我向你提交了这么个代码段。请从学术的层面,尽可能详细轻柔的给我讲解这段代码可能存在的陷阱,尽可能的跟我说说一些C++的处理事情的方式。
第一个候选者:这是一段比较差的代码。这是你的代码?首先,不要使用两个空格来表示缩进。还有class A后面的大括号要另起一行。sz_?我从来没见过如此命名的。你应该参照GoF标准_sz或且微软标准m_sz来命名。(GoF标准?)
你:还有呢?
候选者:恩?你是不是觉得在释放一个数组对象的时候,应该使用delete []来取代delete?说真的,我的经验告诉我,没必要。现代的编译器可以很好的处理这个事情。
你:好?有考虑过C++的“rule of three“原则吗?你需要支持或是不允许复制这一类对象吗?
PS:
(来自维奇百科http://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming))
The rule of three (also known asthe Law of The Big Three or The Big Three) is a rule of thumb in C++ that claimsthat if a class defines one of the following itshould probably explicitly define all three:
§ destructor
§ copy constructor
§ assignment operator
也就是说,在C++中,如果需要显式定义析构函数、拷贝构造函数、赋值操作符中的一个,那么通常也会需要显式定义余下的两个。
第一个候选者:恩。无所谓了。听都没听说过tree-rule。当然,如果用户要拷贝这一类对象的话,会出现问题。但是,这也许就是C++的本质,给程序员无穷尽的噩梦。
顺便说一下,我想你应该知道哎C++中所有的析构函数都应该定义为virtual函数。我在一些书上看到过这个原则,这主要是为了防止在析构子类对象时候出现内存泄露。
你心里嘀咕:或是类似的玩意。Another ice cream perhaps?(我还是没搞明白这到底哪门情感)
令人愉悦的第二个候选者登场了:)
候选者:哦,我该从何说起呢?先关注一些比较重要的东西吧。
首先是析构函数。如果你使用了操作符new[],那么你就应该使用操作符delete[]进行析构。使用操作符delete[]的话,在数组中的每一个对象的析构函数被调用以后,所占用的内存会被释放。例如,如果像上面的代码那样写的话,B类的构造函数会被执行sz次,但是析构函数仅仅被调用1次。这个时候,如果B类的构造函数动态分配了内存,那么就是造成内存泄漏。
接下类,会谈到“rule of three”。如果你需要析构函数,那么你可能要么实现要么显式禁止拷贝构造函数和赋值操作符。由编译器生成的这两者中任何一个,很大可能不能正常工作。
还有一个小问题,但是也很重要。通常使用成员初始化列表来初始化一个对象。在上面的例子中,还体现不出来这样做的重要性。但是当成员对象比较复杂的时候,相比让对象隐式地使用默认值来初始化成员,然后在进行赋值操作来说,使用初始化列表显式初始化成员更为合理。
先把代码修改一下:)然后再进一步阐述问题。
你改善了一下代码,如下:
#include "B.hpp" class A { public: A(int sz) { sz_ = sz; v = new B[sz_]; } ~A() { delete[] v; } //... private: A(const A&); A& operator=(const A&); //... B* v; int sz_; };
这个时候,这位候选者(第二个)说:好多了。
你进一步改进,如下:
#include "B.hpp" class A { public: A(int sz) { sz_ = sz; v = new B[sz_]; } virtual ~A() { delete[] v; } //... private: A(const A&); A& operator=(const A&); //... B* v; int sz_; };
第二位候选者忙说道:别着急,耐心点。
接着他说:在这样的一个类中,定义一个virtual的析构函数,有什么意义?这里没有虚函数,因此,如果以此作为基类,派生出一个类,有点不可理喻。我知道是有一些程序员把非虚类作为基类来设计继承体系,但是我真的觉得他们误解了面向对象技术的一个关键点。我建议你析构函数的virtual说明符去掉。virtual这个关键字,用在析构函数上的时候,他有这么个作用:指示这个class是否被设计成一个基类。存在virtual,那么表明这个class应该作为一个基类,那么这个class应该是一个virtual class。
还是改一下初始化列表的问题吧:)
于是代码被你修改为如下:
#include "B.hpp" class A { public: A(int sz):sz_(sz), v(new B[sz_]) { } ~A() { delete[] v; } //... private: A(const A&); A& operator=(const A&); //... B* v; int sz_; };
第二个候选者说:恩,有了初始化列表。但是,你有没有注意到由此有产生了新的问题?
你编译的时候使用了-Wall选项吗?你应该使用-Wextra、-pedantic还有-Weffc++选项。如果没有警告出现,你可能没有注意到这里发生的错误。但是如果你提高了警告级别,你会发现问题不少。
一个不错的经验法则是:总是按照成员被定义的顺序来书写初始化列表,也就是说,成员按照自己被定义的顺序来呗初始化。在这个例子中,当v(new B[sz_])执行的时候,sz_还没有被定义。然后,sz_被初始化为sz。
事实上,C++代码中,类似的事情太常见了。
你于是把代码修改为:
#include "B.hpp" class A { public: A(int sz):v(new B[sz]), sz_(sz) { } ~A() { delete[] v; } //... private: A(const A&); A& operator=(const A&); //... B* v; int sz_; };
第二个候选者:现在好多了。还有什么需要改进的吗?接下来我会提到一些小问题。。。
在C++代码中,看到一个光秃秃的指针,不是一个好的迹象。很多好的C++程序员都会尽可能的避免这样使用指针。当然,例子中的v看起来有点像STL中的vector,或且差不多类似于此的东西。
对于你的私有变量,你貌似使用了一些不同的命名约定。在此,我的看法是,只要这些变量是私有的,你爱怎么命名就怎么命名。你可以使得你的变量全部以_作为后缀,或且遵循微软命名规范,m_作为前缀。但是,请你不要使用_作为前缀来命名你的变量,以免和C语言保留的命名规范、Posix以及编译器的命名规则相混淆:)
(待续)