404频道

学习笔记

Image Title

前段时间在家看书学习,难得的学习的好时机。

楼下有一个卖饭的小摊及其猖狂,不仅占用了人行道来炒菜,而且还在马路上摆了一溜桌子供客人吃饭,不仅占用了人行道,连车行道都给占用了。这些也就罢了,对我影响都不算太大,更可气的是每天中午和晚上吃饭的时候会开着大音响放着恼人的音乐,我不想惹麻烦,我忍。

今天中午我刚开始看书,看到难处需要精心思考,恼人的音乐又开始了,我实在忍不住了,TMD,维权。打市长热线12345投诉,市长热线让我打110投诉。继续打110投诉,然后跟110说了下具体情况后,说给相应的派出所去处理。派出所的小片警立刻就给我回电话了,说外放音乐正常经营范围,只要不在晚上或清晨放音乐就不算违规,他们管不着,建议我去下面跟卖饭的商量,好一个商量。然后我又说,他们非法占道经营,小片警又说这个归城管管,让我给城管打电话,好一个给城管打电话。好一个推卸责任,这些把我给惹毛了。

挂断电话后,寻思这个理不太对,然后继续给市长热线12345打电话,告诉情况后,市长热线的妹子告诉我说这个事情我给你处理,好一个我给你处理,这才是为人民服务的态度,鼓掌。

这是第二次机会接触小片警,每次都是让我失望,绝望,恨之入骨。第一次接触小片警我甚至kill him的心都有了。不一心想着为人民服务,却是一心想着推卸责任,处处刁难市民并从中谋取私利,对市民爱理不理,这就是小片警在我心中的形象,很难改变。越是权利小的小兵,架子越大,这也就决定了永远是个小兵的身份。

如果没有市长热线那这件扰民的事情也就不了了之了,因为投诉110都不管用了,作为市民已经没有可以维权的机构了。还好有市长热线的存在给市民多了一个维权的途径。

上周五打的电话,这个周一给我回复电话问我饭馆在哪一次,周二又打电话问我饭馆在哪,然后周三终于给处理了,下班途中派出所给我电话回复说:“已经处理好了,让小饭馆的音响声音调小了,以后如果再有这种情况可以继续打电话”。等我回家一看,果然音响不见了,世界一下子清净了,zf终于替我办事了。

也许是因为我的事情不是很紧急的原因,整个处理流程过于慢了,等了足足五天的时间才处理好。

当大家的权益受到损害时,请大家多给市长热线打电话维护自己的权益。

Image Title

得知老家三老爷家的三叔去世了,原本还在看代码的我收到消息之后立刻无法平静了,只好出去走走散散心,回家之后依然感觉莫名的胸闷,玩游戏分散分散精力,游戏过后依然胸闷。意料之中的失眠,中途醒了好几次。总感觉消息不是真实的,总感觉昨晚在梦中,真希望一觉醒来之后什么都没有发生。

三叔42岁,正值壮年,在家附近的号称有一万员工的炼钢厂打工,在整个市也算是很大的企业了。工作中意外丧命。总觉得这样的事情不会发生在我身边,客观事实是发生了。

听家人说,三叔小时候调皮爬到树上掏鸟窝从树上掉下来把一个肩膀都磕到身体里了,大家都觉得肯定好不了,在镇上医院住院打吊瓶打够了自己偷偷跑回学校,后来居然奇迹般的好了,而且还没留下任何痕迹。大家都说三叔命大,谁知三叔小时候躲过了一劫却没有躲过这一劫,这难道就是天命?三叔一生勤俭节约,人忠厚老实,到头来却落得如此下场,谁说上帝是公平的,谁说好人有好报,这都是胡扯。

临近三叔出事的前天,我做了一个很不好的梦,梦的内容我已经记不起来了。回家后听家里人说很多人都做了不好的梦,甚至连平常不怎么做梦的都会被梦惊醒。这绝对不是巧合,很明显已经超出了当前科学的范畴。

记得最后一次跟三叔接触还是在过年的时候,三叔到我家来转转,聊了几句,现在还记忆犹新。再上一次见面就是在去年夏天的一个下午,约着三婶去火车站接三叔家的弟弟和我爸,正巧在三叔家的门口碰到三叔,估计是要去上夜班。

每年过年我们一大家20多人就会团聚在一起,男人一桌,女人一桌,还有我们小孩一桌,其乐融融。最近两年过年三叔是唯一缺席的,由于工作的原因,三叔正巧在过年的时候上夜班。总觉得少了三叔过年的时候是个遗憾,现在看来以后过年要永远遗憾下去了。

企业在追求经济效益的同时,往往会忽略员工的安全。员工伤亡事件屡见不鲜,却很难得到企业的重视。相比人类的伤亡,企业的经济效益显得那么苍白无力。听说钢厂每年总会出些事故,但是事故的赔偿是从所有员工的工资中扣除的,而不是工厂承担,这也是工厂对安全问题不够重视的原因,反正出了事掉血的是员工。

现在村中的人大部分出去在外面打工,农忙时回家忙几天。在此提醒相亲们一定要注意人身安全,没有了安全保障赚再多的钱都白搭。

谨以此文献给为工作而献身的三叔。

Image Title

这段时间,中国大部分的地区都是高温不降,有些地区甚至出现了鸡蛋自然孵化的现象。媒体报道高温热死人的现象也是时有发生,虽说媒体的话不可信,但至少从一个侧面反映了天气的确是较往年同期高出一些,开始渐渐超出人类身体可以承受的温度,开始悄悄的打破往年同期的高温纪录。

最近几年气候变化无常,跟人类的活动绝对脱不了干系。冬天雾霾可以持续一周不散,春天一个月干旱,夏季雨天可以持续一个月不变,秋季如蝉的生命般短暂。在北方已经生活了二十多年的我,这些现象在小时候是极少碰到的,现在却是极其频繁。记得小时候雨天过后时常会在天边挂上一道弯弯的彩虹,记得最后一次见彩虹是在小学一二年纪的时候,自此之后彩虹仅存在了我的脑海里。对于现在的大部分中国人而言,彩虹仅存在于永恒的记忆中和孩子们的画中。

人类近几百年来正在肆无忌惮的向地球母亲索要不该属于人类自己的东西,人类已经占有了迄今为止地球上对人类有价值的且可以占有的所有资源,人类仍然在忘形的开发并破坏着地球上生物赖以生存的家园。

拿中国的三峡大坝举例,从能源的角度考虑的确是有利的。但是从地球生态的角度考虑肯定是有害的。人类的存在时间相对地球是短暂的,地球每一处地形存在就有它存在的理由,已经经过了无数年的实践验证说明地形存在的正确性。可恶的中国ZF,可恨的脑残砖家居然能够利用理论来论证修建三峡的必要及正确性,TMD没学过实践是检验真理的唯一标准。在没有对地球有充足的了解之前不要利用有限的理论来推断并指导实践,因为往往实践之后就再也回不了头,就比如三峡大坝。谁敢说近几年的西南大旱、特大地震跟三峡拖得了干系,可以灾难发生了又有哪个砖家可以站出来声称我可以对这个灾难负责呢?

我从小就一直在担忧一个问题如果再过几十年后几百年后地球上的煤炭、石油等不可再生资源被人类用过了人类该何去何从,我时长为此而忧虑不已。也许这有点杞人忧天,肯定有人会站出来说到那时候随着人类科技的发展早就发现了新能源了,这是谁给的自信?谁这么大胆敢预言人类几十年后一定可以发现新能源?何况不可再生资源中蕴藏着的价值肯定不仅是燃烧带来的能量这点价值,假如几十年后人类已经将石油资源消耗殆尽了,却发现石油中蕴藏着巨大的能量,估计那时候我们只有哭的份了,楚人一炬,可怜焦土。

人类不过是地球上几百万种生物中的一种,如果硬要从广义公平的角度来考虑,人类在生物界占有的太多了,人类已经把该占有的不该占有的全部占为己有,贪婪的本性暴露无遗。很难想象几十年过后我们人类的家园已经成为了什么样子,四处可见的是拔地而起的高楼,柏油路横一条竖一条,无论在地球的哪个角落都能找到人类留下的痕迹。地球该随着人类的发展何去何从,我不敢想象,我能做到的仅仅是节约点力所能及的资源,仅此而已。

成员函数的各种调用方式

非静态成员函数的调用方式

在C++中必须要保证类的非静态成员函数必须和非成员函数的执行效率一致,在编译的过程中,编译期已经将类的非静态成员函数编译为了非成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class  Test
{
public:
int sum() const
{
return a + b;
}

int add()
{
return a;
}

int add(int size)
{
return a + size;
}
public:
int a;
int b;
};

在上述Test类中,编译器会在编译阶段对类中的成员函数做一些转换。下面列出了编译器可能会做出的变换,不同的编译器实现不太一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sum_TestFv(const Test * const this)
{
return this->a + this->b;
}

int add_TestFv(Test * const this)
{
return this->a;
}

int add_Testi(int size, Test * const this)
{
return this->a + size;
}

通过上面的变换可以总结出如下规律:

  • 将成员函数重新改写为一个外部函数,并且对函数的名字进行处理,使在程序中唯一。一种可能的处理办法就是将函数名更改为:函数名_类名_函数参数。这样即解决了类之间函数名相同的问题,又解决了类之间函数重载的问题。
  • 在函数的参数末尾添加额外的this指针参数。对于const函数添加的this指针为双const类型,对于非const函数则添加的this指针为指向的内容可变的const指针。
  • 在函数内对成员函数的存取采用this指针来实现。

虚成员函数

虚函数如果是通过指针类型访问,需要在运行时动态决定指针指向的类型,因此需要访问虚函数表才能够获取正确的虚函数地址。访问虚函数的方式为(*ptr->vptr[i])(ptr),其中i代表要调用的虚函数在虚函数表中的索引,最后一个ptr代表要调用虚函数的编译器添加的this指针参数。

关于虚成员函数的更详细问题会在下一个节中进行讨论。

静态成员函数

静态成员函数中没有this指针,可以理解成带类作用域的全局函数,执行效率跟全局函数一致。

虚成员函数

这部分内容是本书的核心内容,可以参考陈皓的博客相关文章,已经对C++中的虚成员函数和虚成员变量进行了说明。

单一继承下的虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Point
{
public:
virtual ~Point(){}
virtual Point& mult(float) = 0;
float x() const {return _x;}
virtual float y() const {return 0;}
virtual float z() const {return 0;}
protected:
Point(float x=0.0) {_x = x;}
float _x;
};

class Point2d : public Point
{
public:
Point2d(float x=0.0, float y=0.0) : Point(x), _y(y) {}
~Point2d(){}

Point2d& mult(float){return *this;}
float y() const {return _y;}
protected:
float _y;
};

class Point3d : public Point2d
{
public:
Point3d(float x=0.0, float y=0.0, float z=0.0) : Point2d(x, y), _z(z) {}
~Point3d(){}

Point3d & mult(float) {return *this;}
float z() const {return _z;}
protected:
float _z;
};

三个类对应的虚函数表会转化成下图

Image Title

通过图中可以看出每个函数在虚函数表中的位置无论在基类还是在子类中位置总是固定的。图中的Point的实例应该是不存在的,因为类中含有纯虚函数mult。

要想调用ptr->z()就变得非常容易,可以在编译器就可以确定虚函数的调用。虽然ptr所指向的对象在编译器并不能确定,但是编译器可以将其转化成为(*ptr->vptr[4])(ptr)。因为z()函数总是在虚函数表中的第四个位置,唯一需要在执行期确定的就是ptr所指的对象的实际类型。

多重继承下的虚函数

避免重复造轮子,参考上面博文。

虚拟继承下的虚函数

避免重复造轮子,参考上面博文。

函数的效率

非成员函数、静态成员函数、非静态成员函数都被转换成为了完全相同的形式。
inline函数的执行效率最高。
虚函数的效率最低。

指向成员函数的指针

这里学习到一个新的语法,之前没有接触过。即指向类成员函数的指针及使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
class Point
{
public:
virtual ~Point() {}
float x() {return _x;}
public:
Point(float x=0.0)
{
_x = x;
}
float _x;
};

int main()
{
Point point(1.0);
float (Point::*p)(); // 定义指向成员函数的指针
p = &Point::x; // 为指向成员函数的指针赋值
cout << (point.*p)(); // 调用指向类成员函数的指针
return 1;
}

如果成员函数的指针并不用于虚函数、多重继承、虚基类等情况,则成员函数的指针效率跟非成员函数指针的效率一致。

指向虚成员函数的指针

书中对于函数取地址的语法在gcc和vs2008下我试验不成功,语法错误。

多重继承下指向成员函数的指针

依赖于编译器的实现,用到的情况比较少,没仔细看。

指向成员函数指针的效率

在引入了虚函数、多重继承、虚基类等情况后,指向成员函数的指针效率有所下降。

内联函数

内联只是一个请求,编译器并不一定会将函数内联的展开。

形式参数

内联时每一个形参都会被对应的实参取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline int min(int i, int j)
{
return i < j ? i : j;
}

inline int bar()
{
int minval;
int val1 = 1024;
int val2 = 2048;
minval = min(val1, val2);
minval = min(1024, 2048);
minval = min(foo(), bar() + 1);
}

minval=min(val1, val2)会被内联展开成minval = val1 < val2 ? val1 : val2
minval = min(1024, 2048)会被扩展为minval = 1024
minval = min(foo(), bar() + 1)需要引入临时对象,被扩展为

1
2
3
int t1;
int t2;
minval = (t1 = foo()) , (t2 = bar() + 1), t1 < t2 ? t1 : t2;

局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
inline int min(int i, int j)
{
int minval = i < j ? i : j;
return minval;
}

inline int bar()
{
int minval;
int val1 = 1024;
int val2 = 2048;
minval = min(val1, val2);
}

在内联函数中引入局部变量,内联函数在内联的时候局部变量会拥有一个唯一的名称。代码中的minval = min(val1, val2)会被内联为:

1
2
int _min_lv_minval;
minval = (_min_lv_minval = val1 < val2 ? val1 : val2), _min_lv_minval;

内联函数可以代替C语言中的#define宏定义,但是当内联函数调用次数过多,会产生大量的扩展代码,使程序的大小变大。

1
2
3
4
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};

对于上述代码我在vs2008中的实验结果,X的大小为1,Y和Z的大小为4,A的大小为8。X的大小为1是因为编译器给空类了1字节的空间。Y和Z的大小为4是因为内部包含一个vbptr(指向虚基类的指针)占用了4个字节。A的大小包含了两个vbptr,分别指向虚基类的指针X。利用cl main.cpp /d1reportSingleClassLayoutA 命令可以查看对象的内存布局,利用vs2008调试界面查看对象的内存布局往往是不全的,不推荐采用此种方式。下面为A的类布局。
Image Title

在64位的linux的g++下测试X、Y、Z、A的大小分别为1、8、8、16,这是因为指针的大小为8个字节。

一个类占用的空间比类本身非静态数据成员空间大的原因有如下两点:

  • 编译器自动加上额外的数据成员,用来支持某些语言特性,例如virtual特性。
  • 内存边界调整的需要

3.1 数据成员的绑定

味同嚼蜡的章节。

3.2 数据成员的布局

数据成员在内存中的布局顺序跟数据成员在类中的声明顺序是一致的,而且现在的编译器都不关心数据成员在类中是public、protected还是private的。

为了内存对齐,编译器在变量之间插入了空白字节,不同的编译器内存对齐的原则并不一致。

为了实现虚函数机制,编译器插入了vptr成员变量。

以上这些内容,本章节并没有展开详解。

3.3 数据成员变量的存取

数据成员包括静态数据成员和非静态数据成员。

静态数据成员变量放在静态存储区,不会造成任何空间或执行时间上的浪费。

对于非静态数据成员,无论成员变量是struct数据成员、类数据成员、单一继承、多重继承情况下执行效率完全一样。执行效率较静态数据成员变量稍低。

1
2
3
4
5
6
7
8
class  Test
{
public:
int a;
int b;
int c;
};
Test test;

在上述例子中要想读取test.c的位置,编译器需要执行类似这样的操作:&test + &Test::c,可以看出对类中变量的存取成本多了一个算数运算。

对于虚拟继承的情况由于需要在运行期才能决定存取操作,需要一些额外的成本,在下文讨论。

3.4 继承与数据成员

如果类中不包含继承机制,则数据成员的布局和struct中数据成员的布局是一致的。

本节将从单一继承但不包含虚函数、单一继承包含虚函数、多重继承、虚拟继承四个方面讨论数据成员变量。陈浩有几篇博文对此进行了详细的解释,比书上内容要易懂和全面,这几篇文章必看。

单一继承且不包含虚函数

书中举例解释了为什么类继承时类成员之间的填补空白会比单个类时要多,下图的内存布局图中Concrete3继承自Concrete2,Concrete2继承自Concrete1。Concrete3类占用的空间大小为:bit1占用的1个字节+3个字节的空白,bit2占用的1字节+3字节的空白,bit3占用的1字节+3字节空白。如果Concrete3不继承自任何对象,而是包含bit1、bit2、bit3三个变量,占用的空间大小为1+1+1+1=4。

Image Title

之所以编译器在继承机制中会作如此处理,是为了在继承机制中对象之间的默认按比特复制操作更方便。例如:

1
2
Concrete1 *p1 = new Concrete1, *p2 = new Concrete2;
*p2 = *p1; // 此时编译器只需要按比特复制就可以了

单一继承包含虚函数

假设有如下类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Base
{
public:
Base()
{
printf("Base\n");
}

virtual ~Base()
{
printf("~Base\n");
}

virtual void foo() {}
int base_x;
};

class Derived : public Base
{
public:
Derived()
{
printf("Derived\n");
}

~Derived()
{
printf("~Derived\n");
}

void foo() {}

int derived_y;
};

则Derived类的对象模型如下,通过图可以非常清晰的理解单一继承包含虚函数的情况:

单一继承包含虚函数的对象模型

多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Base1
{
public:
Base1()
{
printf("Base1\n");
}
virtual ~Base1()
{
printf("~Base1\n");
}
virtual void base1_virtual_func() {}
int base1_x;
};

class Base2
{
public:
Base2()
{
printf("Base2\n");
}
virtual ~Base2()
{
printf("~Base2\n");
}

void base2_not_virtual_func() {}
int base2_x;
};

class Derived : public Base1, public Base2
{
public:
Derived()
{
printf("Derived\n");
}

~Derived()
{
printf("~Derived\n");
}

void derived_func() {}

void base1_virtual_func() {}

int derived_y;
};

则Vertex3d类的对象模型如下,同样通过图可以非常清晰的理解多重继承的情况:

多重继承

重复继承

书中并没有涉及到重复继承,重复继承是指某个基类被间接重复继承了多次,属于重复继承和钻石级多重虚拟继承的过渡情况,有必要说明一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Base
{
public:
virtual void base_virtual_func() {}
void base_func() {}
int base_x;
};

class Base1 : public Base
{
public:
Base1()
{
printf("Base1\n");
}
virtual ~Base1()
{
printf("~Base1\n");
}

virtual void base_virtual_func() {}
virtual void base1_virtual_func() {}
int base1_x;
};

class Base2 : public Base
{
public:
Base2()
{
printf("Base2\n");
}
virtual ~Base2()
{
printf("~Base2\n");
}

virtual void base_virtual_func() {}
void base2_not_virtual_func() {}
int base2_x;
};

class Derived : public Base1, public Base2
{
public:
Derived()
{
printf("Derived\n");
}

~Derived()
{
printf("~Derived\n");
}

void derived_func() {}

void base1_virtual_func() {}

int derived_y;
};

通过下图的对象模型可以看出,重复继承的类Base在Derived的实例中存在两份,要想直接更改Base类中的base_x变量的值,不能通过derived.base_x = 1直接赋值的方式,需要调用derived.Base1::base_x = 1的方式来更改,更改后的效果仅更改了Base1对象对应的Base类实例中的base_x的值。

重复继承

钻石型多重虚拟继承

该种方式的继承已经是所有继承中最为复杂的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Base
{
public:
virtual void base_virtual_func() {} // 虚基类最好是不再提供虚函数
void base_func() {}
int base_x;
};

class Base1 : virtual public Base
{
public:
Base1()
{
printf("Base1\n");
}
virtual ~Base1()
{
printf("~Base1\n");
}

virtual void base_virtual_func() {}
virtual void base1_virtual_func() {}
int base1_x;
};

class Base2 : virtual public Base
{
public:
Base2()
{
printf("Base2\n");
}
virtual ~Base2()
{
printf("~Base2\n");
}

//virtual void base_virtual_func() {} // 由于是虚拟继承,不再能重复重载父类的虚函数了
void base2_not_virtual_func() {}
int base2_x;
};

class Derived : public Base1, public Base2
{
public:
Derived()
{
printf("Derived\n");
}

~Derived()
{
printf("~Derived\n");
}

void derived_func() {}

void base1_virtual_func() {}

int derived_y;
};

在下图标出的区域中,我认为Base应该是不存在的,这里只是vs2013为了展示的考虑而添加上的。虚拟继承基类Base位于Derived类对象的除该成员外的最后位置。

虚拟继承

对象成员的效率

作者经过试验测试,继承下的类成员读写效率跟读写普通变量效率相差不大,虚拟继承对程序的读写效率有影响。这跟理论上相差不大。

指向数据成员的指针

小技巧:可以通过&类名::变量名的语法来获取类成员变量在类对象中的位置,即相对于类对象起始地址的偏移量。

书中后面的内容个人感觉没有必要看了。

2.1 默认构造函数的生成

只有当编译器需要默认构造函数的时候才会合成默认构造函数,并不是类只要没有定义默认构造函数编译器就会合成默认构造函数,而是只有以下四种情况编译器会生成默认构造函数。编译器合成的默认构造函数仅会处理类的基类对象和类中的数据成员对象,对于类中的普通类型的非静态数据成员并不会作任何处理。比如类中一个指针类型的数据成员,编译器合成的默认构造函数不会对该指针作任何处理,该指针就是一个野指针。

带有默认构造函数的类成员对象

一个类没有定义任何构造函数,该类中包含了一个带有默认构造函数(包括了合成的默认构造函数和定义的默认构造函数)的类成员对象,那么编译器需要为此类合成一个默认构造函数,合成默认构造函数的时机为该构造函数被调用时。合成的默认构造函数默认为内联函数,如果不适合使用内联函数,就合成explicit static的构造函数。

默认构造函数、复制构造函数和赋值操作符的生成都是inline。inline函数有静态链接,不会被当前文件之外的文件看到。如果函数过于复杂不适合生成inline函数,会生成一个explicit non_inline static实体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Dopey
{
public:
Dopey();
};

class Sneezy
{
public:
Sneezy(int);
Sneezy();
};

class Bashful
{
public:
Bashful();
};

class Snow_White
{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;

private:
int mumble;
};

void foo()
{
Snow_White snow_white;
}

在上述例子中,foo()中需要调用Bashful的构造函数,编译器会为Bar类生成内联的默认构造函数。Bashful类会生成类似于下面的默认构造函数。

1
2
3
4
5
6
inline Bar::Bar()
{
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy();
bashful.Bashful::Bashful();
}

默认构造函数的生成原则为:如果类A中包含了一个或一个以上的类成员对象,那么类A的默认构造函数必须调用每一个类成员的默认构造函数。但是不会初始化普通类型的变量,因此在上例中必须手动初始化mumble变量。在编译器合成的默认构造函数中类成员变量的默认构造函数的调用次序为成员变量在类中的声明顺序,该顺序和类成员的构造函数初始化列表顺序是一致的。

如果Snow_White类定义了如下的默认构造函数,则编译器会自动在定义的构造函数中增加调用类成员变量的代码,调用类成员变量相应构造函数的顺序仍然和类成员变量在类中的声明顺序一致。

从中可以看出类成员变量的构造函数的调用要早于类构造函数的调用,这一点是在很多面试题中经常会见到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义的默认构造函数,包含了类成员变量sneezy的初始化列表
Snow_White::Snow_White() : sneezy(1024)
{
mumble = 2048;
}

// 编译器扩张后的默认构造函数
Snow_White::Snow_White() : sneezy(1024)
{
dopey.Dopey::Dopey(); // 调用默认构造函数
sneezy.Sneezy::Sneezy(1024); // 自动调用合适的构造函数
bashful.Bashful::Bashful();
mumble = 2048;
}

带有默认构造函数的基类

在继承机制中,一个没有构造函数的子类继承自带有默认构造函数的基类,则子类的构造函数会被合成,并且会调用基类的默认构造函数。若子类没有定义默认构造函数,却定义了多个带参数的构造函数,编译器会扩张所有自定义的构造函数,将调用基类默认构造函数的代码添加到子类的构造函数的最前面。

从这里可以看出继承机制中,首先构造基类,后构造子类,这点也是面试题中经常遇到的。

带有虚函数的类

为了实现虚函数或虚继承机制,编译器需要为每一个类对象设定vptr(指向虚函数表的指针)的初始值,使其指向一个vtbl(虚函数表)的地址。如果类包含构造函数则编译器会生成一些代码来完成此工作;如果类没有任何构造函数,则编译器会在合成的默认构造函数中添加代码来完成此工作。

带有虚基类的类

需要维护内部指针来实现虚继承。

2.2 复制构造函数的生成

复制构造函数被调用有三种情况:

  • 明确的一个对象的内容作为另外一个对象的初始值。如X xx = x或X xx(x)。
  • 对象作为参数传递给函数时。
  • 类对象作为函数返回值时。

合成复制构造函数的情况

如果一个类没有提供显式的复制构造函数,同默认构造函数一样,只有编译器认为需要合成复制构造函数时,编译器才会合成一个。那么问题来了,什么时候编译器才合成复制构造函数呢?书中给出的答案为当一个类不展现出_bitwise copy semantics_1的时候。具体来说有以下四种情况,跟类的默认构造函数的合成基本一致。

  • 当类内包含一个类成员变量且类成员变量声明了复制构造函数。
  • 当类继承的基类有复制构造函数(复制构造函数可以是显示声明或合成的)
  • 一个类中包含了一个或多个虚函数
  • 类继承自一个或多个虚基类

其中前面两种情况必须将成员变量或基类的复制构造函数的调用插入到合成的复制构造函数中,因此不是按照按比特复制的。第三和第四点分别用下面两小节来说明。

重新设定虚函数表的指针

当编译器需要在类对象中设定一个指向虚函数表的指针时,该类就不能再使用按位复制的复制构造函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ZooAnimal
{
public:
ZooAnimal();
virtual ~ZooAnimal();

virtual void animate();
virtual void draw();
};

class Bear : public ZooAnimal
{
public:
Bear();
void animate();
void draw();
virtual void dance();
};
void foo()
{
// yogi的vptr指向Bear的虚函数表
Bear yogi;
// franny的vptr指向ZooAnimal的虚函数表
ZooAnimal franny = yogi;
draw(yogi); // 调用Bear::draw()
draw(franny); // 调用ZooAnimal::draw()
}

Image Title

合成出来的ZooAnimal的复制构造函数会明确设定对象的vptr指向ZooAnimal的虚函数表,而不是从右值中复制过来的值。

处理virtual base class subobjects

虚基类的存在需要特别处理,一个类对象如果以另外一个类对象作为初始值,而后者有一个virtual base class subobjects,也会使按比特复制的复制构造函数失效。

每一个编译器都必须让派生的类对象的virtual base class subobjects位置在执行期准备完毕。按比特复制的复制构造函数可能会破坏virtual base class subobjects的位置,因此编译器必须在自己合成出来的复制构造函数中修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ZooAnimal
{
public:
ZooAnimal();
virtual ~ZooAnimal();

virtual void animate();
virtual void draw();
};

class Raccoon : public virtual ZooAnimal
{
public:
Raccoon();
Raccoon(int val);
};

class RedPanda : public Raccoon
{
public:
RedPanda();
RedPanda(int val);
};

文章的内容没有完全理解,虚继承机制使用较少,可以暂时不用理解。

2.3 程序转化语意学

本节涉及到了编译器优化的相关细节,由于较容易理解,可以直接看书上内容,对工作帮助不大。包括类对象的初始化优化,函数参数的初始化优化,函数返回值的初始化优化,使用者层面的优化和编译器层面的优化。

如果不是上节指定的四种情况,不需要显示的声明复制构造函数,因为显示的声明的复制构造函数往往效率不如编译器合成的复制构造函数效率高。编译器合成的复制构造函数利用memcpy()或memset()函数来合成,效率最高。

2.4 类成员的初始化列表

说到类成员的初始化列表必然想起一个经常出现的面试题:成员初始化列表的顺序是按照成员变量在类中声明的顺序。如果成员初始化列表的顺序和成员变量在类中声明的顺序不一致时某些编译器会提示警告。编译器将成员初始化列表的代码插入到构造函数的最开始位置,优先级跟调用类类型的成员变量的默认构造函数是一致的,都是跟类类型成员变量在类中的声明次序相关。

类成员初始化必须使用成员初始化列表的四种方式:

  • 初始化一个引用类型的成员变量
  • 初始化一个const的成员变量
  • 调用基类的构造函数,且基类的构造函数采用成员初始化列表的方式
  • 调用类成员的构造函数,且类成员的构造函数采用成员初始化列表的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
class  Word
{
public:
Word()
{
_name = 0;
_cnt = 0;
}

private:
String _name;
int _cnt;
};

此例子在构造函数中对成员变量进行测试,编译器对构造函数的扩张方式可能会生成如下的伪码:

1
2
3
4
5
6
7
8
Word::Word()
{
_name.String::String();
String temp = String(0);
_name.String::operator=(temp);
temp.String::~String();
_cnt = 0;
}

构造函数中生成了一个临时性的String对象,这浪费了一部分开销。如果将构造函数该成如下的定义方式:

1
2
3
4
Word() : _name(0)
{
_cnt = 0;
}

即将其中的类成员变量更改为成员初始化列表的方式来初始化,编译器会自动将构造函数扩张为如下方式,这样减少了临时对象,提供了程序效率。

1
2
3
4
5
Word::Word()
{
_name.String::String(0);
_cnt = 0;
}

引申

下面例子是对本章内容的一个简单概况,也是面试题中经常碰到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class A
{
public:
A()
{
printf("A\n");
}

~A()
{
printf("~A\n");
}
};

class B
{
public:
B(int n)
{
printf("B_%d\n", n);
}
~B()
{
printf("~B\n");
}
};

class Base
{
public:
Base()
{
printf("Base\n");
}

virtual ~Base()
{
printf("~Base\n");
}
};

class Derived : public Base
{
public:
Derived() : _m(1), _b(_m)
{
printf("Derived\n");
}

~Derived()
{
printf("~Derived\n");
}

int _m;

// 下面两个类类型的成员遍历的构造函数的调用次序跟在类中的声明次序是相关的
B _b; // 类类型的类成员变量,初始化列表中包含该变量
A _a; // 类类型的类成员变量
};

int main()
{
// 调用基类的构造函数->调用子类类类型成员变量的构造函数->调用子类的构造函数
Derived derived;
return 0;
// 根据栈的特点,类析构的次序跟构造是相反的
}

上述代码执行的结果为:

1
2
3
4
5
6
7
8
Base
B_1
A
Derived
~Derived
~A
~B
~Base

总结

本章讲述了合成的默认构造函数、合成的复制构造函数和构造函数的成员初始化列表。其中如果类没有定义默认构造函数,只有在文中提到的四种情况下编译器才会合成默认构造函数。合成的复制构造函数在需要的时候编译器就会生成,默认是按对象比特复制的方式实现,有四种情况下编译器是不按照比特复制的方式。


[1] bitwise copy semantics书中翻译为“位逐次拷贝”,就是按照内存中的字节进行复制类,感觉翻译不如不翻译好理解。

C++对象模型是深入了解C++必须掌握知识,而《深度探索C++对象模型》一书基本是理解C++对象模型的必须之作。可惜本书看起来更像是作者Stanley B.Lippman的随笔,语言诙谐,跟作者的另外一本经典之作《C++ Primer》有着天壤之别,侯捷的翻译也是晦涩难懂,跟侯捷翻译的其他作品也有一定差距(这两位大师还真是凑到一块了),所以这本书看起来还是很吃力的。这里挑选文中重点记录笔记,忽略扯淡部分,以备忘。

C++的额外开销

C++相比C语言多出了封装、继承和多态等特性,新特性的增加必然会以牺牲一部分性能为代价。额外开销主要是由virtual引起的,包括:

  • 虚函数机制:需要在运行期间动态绑定。
  • 虚基类:多次出现在继承中的基类有一个单一且被共享的实体。

C++对象模型

C++类成员变量包括:静态成员变量和非静态成员变量。成员函数包括:静态成员函数、非静态成员函数和虚函数。

单一类的对象模型

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
public:
Point(float xval);
virtual ~Point();
float x() const; /* 非静态成员函数 */
static int PointCount(); /* 静态成员函数 */

protected:
virtual ostream& print(ostream &os) const; /* 虚函数 */
float _x; /* 非静态成员变量 */
static int _point_count; /* 静态成员函数 */
}

该类的c++对象模型如下图:
Image Title

通过图中可以看出:

  • 非静态数据成员直接放到了类的对象里面。
  • 静态数据成员放到所有的类对象的外面,即静态存储区。
  • 静态和非静态的成员函数放在类对象之外,即代码区。
  • 如果类中存在虚函数,则会产生一个虚函数表(vtbl),表中的表项存储了指向虚函数的指针。在类对象的开始位置添加一个指向虚函数表的指针(vptr)。vptr的赋值由类的构造函数和赋值运算符自动完成。
  • 虚函数表的第一项指向用来作为动态类型识别用的type_info对象。

C++支持的编程范式(programming paradigms)

  • 程序模型:通俗的理解成C语言的面向过程编程方式。
  • 抽象数据类型模型:通过类封装成为了一种新的数据类型,该数据类型有别于基本数据类型。
  • 面向对象模型:利用封装、继承和多态的特性。

C++支持多态的方式

  • 隐含的转换操作,例如通过父类的指针指向子类的对象。shape *ps = new circle();
  • 通过虚函数机制。
  • 通过dynamic_cast强制类型转换。如if (circle *pc = dynamic_cast<circle*>(ps))

类对象的内存构成

  • 非静态数据成员。
  • 由于内存对齐而添加到非静态数据成员中的空白。
  • 为了支持虚机制(包括:虚函数和虚继承)而额外占用的内存。

利用工具查看对象模型

查看C++类的对象模型有两种比较简便的方式,一种是使用Virtual Studio在调试模式下查看对象的组成,但是往往显示的对象不全;另外一种是通过Virtual Studio中cl命令来静态查看。

本文选择使用cl工具的方式,cl命令位于C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin目录下,为了方便使用可以将该变量添加到Path环境变量中。在命令行中执行cl命令,提示“计算机中丢失mspdb80.dll”,该文件位于C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE目录下,将msobj80.dll,mspdb80.dll,mspdbcore.dll,mspdbsrv.exe四个文件复制到C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin目录下即可。

通过cl xxx.cpp /d1reportSingleClassLayoutXX命令即可查看文件中类的对象模型,其中该命令最后的XX需要更换为要查看的类名,中间没有空格。

执行上述命令时提示:无法打开文件“LIBCMT.lib”。该文件位于C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\lib目录下,将该目录添加到环境变量lib中。重新打开命令行执行cl提示:无法打开文件“kernel32.lib”,将C:\Program Files\Microsoft SDKs\Windows\v6.0A\Lib添加lib环境变量中。

本程序将讲解java调用C语言写的二进制文件,并将二进制文件中的内容利用Java读出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

union data
{
int inter;
char ch;
};

struct Test
{
int length;
char arr[20];

void toBigEndian()
{
union data c;
c.inter = 1;
if(c.ch == 1)
{
// 小端
unsigned char temp;
unsigned char *tempData = (unsigned char *)&length;
for (int i=0; i < sizeof(int) / 2; i++)
{
temp = tempData[i];
tempData[i] = tempData[sizeof(int) - i - 1];
tempData[sizeof(int) - i - 1] = temp;
}
}
}
};

int main()
{
Test test;
memset(&test, 0, sizeof(Test));
test.length = 0x12345678;
strcpy(test.arr, "hello world");
test.toBigEndian();
FILE *file = fopen("test.txt", "w+");
fwrite(&test, sizeof(Test), 1, file);
fclose(file);
return 1;
}

本例子中的C程序将一个包含int变量和char数组的结构体写入文件中。

其中需要考虑到机器的大小端问题,java程序采用的大端字节序,因此这里将C的结构体在写入文件时转换成大端字节序。在将结构体写入到文件时,将其中的int类型变量转换成大端字节序,如果机器本身即为大端字节序则不需要转换字节序。

java端读取的文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


public class Main {

public static int byte2int(byte[] res) {
int targets = (res[3] & 0xff) | (res[2] << 8) | (res[1] << 16) | (res[0] << 24);
return targets;
}

/**
* @param args
*/
public static void main(String[] args) {
File file = new File("test.txt");
if (!file.exists()) {
System.out.println("文件不存在");
return;
}

byte[] data = new byte[50];

try {
FileInputStream fis = new FileInputStream(file);
int size = fis.read(data);
System.out.println("读取到" + size + "个字节的数据");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

// 转换完成的int值
int value = byte2int(data);
System.out.printf("%x\n", value);


StringBuffer sb = new StringBuffer();
for (int i=4; i<24; i++) {
System.out.print((char)data[i]);
}
}
}

Image Title

药是什么?药是人类文明的发展过程中不断克服自身疾病的必然产物。谁让外星人造人的时候没有把人类制造的那么完美,要是人类除了生老死之外没有病这个状态,或许药物就不会产生。

现实生活中,过度依赖药物的人比比皆是,尤其是在中国。在中国,药物已经用到了满天飞的程度了。得个小感冒会去药店买药,医生除了推荐你感冒药之外,肯定会推荐你消炎药。以至于大众普遍认为,有炎症必须吃消炎药。感冒烧到38度,医院医生会推荐你打针。感冒烧到38.5度,医生会推荐你挂上三天吊瓶,美其名曰挂吊瓶好的快,如果患者的心理能起到恢复快的作用的话,那么挂吊瓶应该是管用的。

我没去过西方国家,但我猜测在西方国家,医药分离的占多数。得病了想吃个消炎药解解馋可没那么容易,消炎药岂能是想吃就吃,没有医生的处方药店怎敢卖给患者,又不是口香糖。想打个针过过瘾更是难了,喜欢花钱让针扎着屁股玩的活中国人比较喜欢。要想挂个吊瓶数滴答玩,除非烧到了40度一星期高烧不退,否则还是自己在家挂瓶自来水数着玩吧。以上有杜撰的成分,但是话激理不歪,你懂得。

要知道中国较西方的文明程度还有差距。西方发明的西药在西方都谨慎使用了,反倒在中国大行其道。谁家没一抽屉瓶瓶罐罐,这可都是毒药啊。除了可以拉动GDP外,有理由怀疑中国担心未来人口老龄化加剧给经济发展造成压力而采取的措施。

是时候阐述本文的观点了,药物不是万能的,能少用尽量少用。

中药讲究是药三分毒,换成西药是药五分毒应该毫不夸张吧。当时治好了我们身体的病症,感觉一身轻松了,但是后患无穷。人类本身就具备一定的修复能力,而且修复能力很强。在人类自我修复能力许可的范围内非要硬用药物加速治疗,那造成的必然结果就是身体下次不干了,有药物可以对抗疾病,那用身体的修复能力干嘛。久而久之,身体就再也扛不住疾病了。

吃西药多了往往治的病好了,却带来了其他疾病。比如感冒了吃消炎药,吃着吃着把胃给吃坏了。治感冒的医生才不会管你的其他身体部位情况,反正胃吃坏了找不到他。这也是医院分那么多科室的一个弊端,只从局部看问题,不能从整体上看问题,所以看到的问题总是片面的。

解决药物依赖的问题从三方面着手解决。一方面要从心理上战胜自己,不要迷信药物,更不能依赖药物。另一方面,多运动,生命在于运动,真理中的真理。第三,眼光放长远,多看其他国家,多了解长寿的人是怎么生存的。

真心希望过度依赖药物的人可以走出这个误区。

本文摘录《C/C++程序员面试宝典》一书中我认为需要注意的地方。

数组指针与指针数组的区别

数组指针即指向数组的指针,定义数组指针的代码如int (*ap)[2],定义了一个指向包含两个元素的数组的数组指针。
如果数组的每一个元素都是一个指针,则该数组为指针数组。实例代码如下:

1
char *chararr[] = {"C", "C++", "Java"};

int (*ap)[2]int *ap[2]的区别就是前一个是数组指针,后一个是指针数组。因为”[]”的优先级高于”*”,决定了这两个表达式的不同。

public、protected、private

修饰成员变量或成员函数

  • public:可以被该类中的函数、子类的函数、友元函数和该类的对象访问。
  • protected:可以被该类中的函数、子类的函数和友元函数访问,不能被该类的对象访问。
  • private:只能由该类中的函数或友元函数访问。
  • 默认为private权限。

用在继承中

  • public:基类成员保持自己的访问级别,基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
  • protected:基类的public和protected成员在派生类中未protected成员。
  • private:基类的所有成员在派生类中为private成员。
0%