404频道

学习笔记

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成员。

Image Title

今天下午新办的工资银行卡办下来了,原先工资卡用的招商银行的金卡,现在新办了个农村信用社的普通卡,有种从城市的大马路走到了胡同里的城中村的感觉。于是大家高高兴兴的蹦蹦跳跳的屁颠屁颠的去领来的新的工资卡。为什么大家会高兴呢?这也是本文的因子,因为大家都已经有两个月没发工资了,新卡到说明工资在不久的将来也会到,大家当然高兴啦。

可是深入想一下,大家真的应该高兴吗?工资本来就是公司单方面拖欠大家的,给大家造成的损失员工宽宏大量没有跟公司计较也就罢了。工资关系到民生,直接影响了员工每个人的生活。要是搁到法国估计一天不发工资,员工早就集体罢工了吧。换卡势必会造成大家的麻烦,原来的银行卡可能就要销户,新的银行是否足够方便。我们却是心宽体胖,公司的错既往不咎,只要看到发工资的希望我照样努力为公司奉献。

这就好比是在中国假期几天高速路不收费,有车一族就喜出望外,“在中国真幸福,可以赚到国家便宜了”。殊不知,世界已建成14万公里收费公路,10万公里在我国。不知道了解这个之后,有车一族还笑得出来吗?我估计连哭的心态都有了。

那大家应该是什么心态去领工资卡呢?平常心就好。至少不至于表现出屁颠屁颠的心态吧。

中国两千多年的封建统治对人民的思想影响是深远的,人们心中早就建立起了人分三六九等的等级观念。而且这种等级观念影响是深远的,悄无声息的深入到我们所谓的“社会主义国家”内部的各个角落。电视里整天的宫廷剧,格格与皇阿玛齐飞,太监共丫鬟一色,这些可是典型的民权不公,民生不平。我们可是就靠这些文化垃圾养大的,心里怎么会没有了奴隶的思想呢?百姓心中的怕官、傍官的思想及考官的行为直接复制到了现在不曾改变,奴隶的心态也就不会改变。

好在现代文明来了,互联网时代来了。西方的文明渐渐深入,我们有的救。打倒xx,打倒xx,期待来一场轰轰烈烈的革新吧。

如果说奴隶的心态为“给你点阳光你就灿烂,给你点洪水你就泛滥”,期待我们的心态变成“没有阳光也要比比灿烂,没有洪水也要试图泛滥”。

题图:《被释放的姜戈》电影海报

Image Title

这是来自一个保守主义者的geeker的傻X的吐槽。

最近股市暴跌,我却在思考为何有股票这个东西。我从未入市且对股市一点兴趣都没有甚至是反感。我所认识的股市游戏规则就是价位低的时候大股东入市,价位高的时候大股东抛售,大股东一吐一吞钱到手了,股民被掏空了。很多股民却用侥幸心理来入市,有赔有赚,出市的时候裤子都赔进去了。

搞不懂那么多经济学专家、博士、硕士、学士,却搞不定一个经济学。若是真搞不懂,索性就不要去研究,反正学了也白学,浪费这个资源误人歧途干啥。一个砖家说熊,另一个砖家说牛,相互对掐有意思吗?

为什么搞股市这个东西,这是嫌经济学不够复杂吗?股市难道能推动人类社会的进步?股市是企业融资的一种手段,而能上市的公司往往是相对不太缺钱的,而最缺钱的是小型创业公司。这也就早就了很多公司把上市套现作为了一个目标,这不明摆着投机取巧,一夜暴富。传销是集合了底层的力量资金而构成了金字塔,企业通过上市手段获取到了股民的资本来运作而发展自身,只不过金字塔只有两层罢了。

假如没有股市,很多上班族就会坐在办公桌上安心工作,而不用时时刻刻关心着像过山车一样的死难看的折线,也不用设个老板键提心吊胆的担心自己的boss悄悄走到自己的面前。难道安安心心全身心投入工作不是更好?

假如没有股市,或许就少了一个行业,一部分人就可以全身心投入到其他行业,带动其他行业的发展,推动历史进步,岂不快哉!

假如没有股市,就不会有人赚发了之后,别人也总想着不劳而获,间接助长了人们投机取巧的气焰。

假如没有股市,就不会因此而发家,当然这是少数。郭美美的母亲也不会用4万块钱赚到100万,也不会有郭美美的今天,也不会有红十字会的今天,当然这只是个笑话。没有股市郭照样可以炫富,因为人家本来就不是靠的股市。没有股市红十字会照样会没落,因为他们的本质就是那样。

假如没有股市,就不会有人因为股市而跳楼的新闻,就不会有人将养老钱都搭进去了,就不会…

请不要职责我,因为我是一个傻X,永远不要和傻子讲道理,否则你也会变成一个傻X!

0%