404频道

学习笔记

无继承情况下的对象构造

在《Unix环境高级编程》的7.6节中提到C程序的内存空间可以分为正文段、初始化数据段、非初始化数据段、栈、堆。其中初始化数据段包含程序中需明确赋初值的变量,如C语言中的全局变量int maxcount = 99;。非初始化数据段又称为bss(block started by symbol)段,在程序开始之前,内核将此段初始化为0或空指针,如出现在函数外面的long sum[1000];,该变量没有明确赋初值,因此放到了bss段中。
而在C++语言中,将所有的全局对象当做初始化过的数据来对待,因此不会将全局变量放到bss段中。

POD数据类型

书中提到Plain ol’ data,查了下应该叫Plain Old Data,简称POD,指C风格的struct结构体定义的数据结构,其中struct结构体中只能定义常规数据类型,不可以含有自定义数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct Point
{
float x, y, z;
} Point;

Point global;

Point foobar()
{
Point local;
Point *heap = new Point();
*heap = local;
delete heap;
return local;
}

首先看全局变量global,按照常规的理解,在程序启动的时候编译器会调用Point的合成的默认构造函数来初始化global变量,在程序退出时会调用Point的合成的析构函数来销毁global变量。实际上,C++编译器会将Point看成是一个POD对象,既不会调用合成的构造函数也不会调用合成的析构函数,但C++编译器会将global当成初始化过的数据来对待,不放入BSS段。

foobar函数中的local局部变量不会自动初始化,意味着local.x中的值是不可控的,但是local变量分配了栈空间。

*heap = local;执行时仅简单执行按字节复制操作,不会产生赋值操作符,因为Point是一个POD类型。

return local;同样仅通过字节复制操作产生一个临时对象。

抽象数据类型

这次将上面的Point类型从struct变换为class

1
2
3
4
5
6
7
8
class Point
{
public:
Point(float x=0.0, float y=0.0, float z=0.0) : _x(x), _y(y), _z(z){}

private:
float _x, _y, _z;
};

在上节中的foobar函数中,各个对象的默认复制构造函数、赋值操作符和析构函数仍然不会调用,因为调用是没有意义的,因此编译器干脆就不产生。

为继承做准备

再次更改Point类,引入虚函数。

1
2
3
4
5
6
7
8
class Point
{
public:
Point(float x=0.0, float y=0.0) : _x(x), _y(y) {}
virtual float z();
private:
float _x, _y;
};

引入虚函数后,类对象就需要一个vtbl来存放虚函数的地址,类对象中需要添加vptr指针。而vptr的初始化是在对象构造的时候,因此对象初始化的时候需要调用构造函数,同时默认构造函数和赋值构造函数会自动在构造函数的最前面插入初始化vptr的代码。

继承体系下的对象构造

C++时会自动扩充类的每一个构造函数。扩充步骤如下:

  1. 如果类含有虚基类,则所有虚基类的构造函数被调用,调用顺序为从左到右,从最深到最浅。
  2. 如果类含有基类,则基类构造函数会被调用,以基类的声明顺序为顺序。
  3. 如果类对象中含有vptr,必须在初始化类的成员变量之前为vptr指定初值,使其指向vtbl。
  4. 将成员初始化列表中数据成员的初始化操作放入构造函数内部,并且按照成员在类中的声明顺序。
  5. 如果类成员变量不在构造函数的初始化列表中,但是成员变量含有默认构造函数,则默认构造函数必须被调用。

虚拟继承

本小节将学习一下引入了虚继承机制之后构造函数的生成是什么样子的。

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
class Point
{
public:
Point(float x=0.0, float y=0.0) : _x(x), _y(y) {}
virtual float z();
private:
float _x, _y;
};

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

virtual float z() {return _z;}
protected:
float _z;
};

class Vertex : virtual public Point
{
// 不是重点忽略
};

class Vertex3d : public Point3d, public Vertex
{
// 不是重点忽略
};

class PVertex : public Vertex3d
{
// 不是重点忽略
};

类之间的继承关系如下图所示,已经属于最复杂的继承模型了。
Image Title
如果要构造Vertex3d的实例,在内存中必须仅能有一个Point类型的对象,而如果在Point3d和Vertex基类中都构造一个Point实例显然是不合适的。答案是编译器会在Vertex3d的构造函数中生成Point的对象,在Point3d和Vertex的构造函数中均不会生成Point的对象。Vertex3d和Point3d的构造函数伪码如下面所示,Vertex构造函数的伪码和Point3d类似,这里就不再列出。

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
Point3d* Point3d::Point3d(Point3d *this, bool __most_derived, float x, float y, float z)
{
// 如果子类初始化基类则本构造函数不需要初始化基类
if (__most_derived != false)
{
this->Point::Point(x, y);
}
this->__vptr_Point3d = __vtbl_Point3d; // 初始化指向本类的vptr
this->__vptr_Point3d_Point = __vtbl_Point3d_Point; // 初始化指向基类的vptr
this->_z = z;
return *this;
}

Vertex3d* Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, float x, float y, float z)
{
if (__most_derived != false)
{
this->Point::Point(x, y);
}
this->Point3d::Point3d(false, x, y, z);
this->Vertex::Vertex(false, x, y);
// 初始化vptr
// 用户代码
return this;
}

编译器在类的构造函数中增加了一个bool变量来判断本类是否需要初始化基类,虚基类的初始化始终在继承最底层的类构造函数中初始化。对于PVertex类来说,Point类的构造函数在该类的构造函数中调用。

vptr初始化语意学

vptr的在构造函数中的初始化时机为:在基类构造函数调用操作之后,在成员初始化列表和构造函数中显式代码之前。
构造函数的执行先后顺序为:

  1. 所有虚基类、基类的构造函数会被调用。
  2. 对象的vptr初始化,指向相关的vtbl。
  3. 在构造函数内展开成员的初始化列表。
  4. 执行显式代码。

对象复制语意学

没有一成不坏的硬件,尤其是数据放到物理硬盘中,说不定哪天硬盘闹脾气就崩掉了,硬盘不值钱,可是里面的数据值钱。下面分享下我的数据备份方案,我的原则是数据无论何时都至少留有一个备份。

博客

我的博客是放到Dropbox中的,在云端和本地均有备份,确保了博客数据的绝对安全,即使云端坏掉还有本地,本地丢了还有云端。

个人照片

由于照片都较大,放到本地硬盘很容易占满空间,而且还不经常用。除了在自己电脑上留有照片之外,选择将照片压缩并加密后按照年份放到百度云上。

代码

工作几年了,已经积攒了一些代码,有些代码时不时的会查看到。对于可以公开的自己写的代码我以后打算放到我的Github上,一方面是由于Github上可以在线浏览代码,另一方面可以向别人分享我的代码。
对于私有的代码,暂时放到了金山快盘上,没有找到可以方便浏览代码的云端。

文档

由于文档之类的资料也是经常用到,我选择了金山快盘。

大小端问题跟CPU的架构直接相关,我们常见的80x86系列CPU采用小端字节序模式。Windows平台就采用的80x86系列CPU,因此为小端字节序。
而主机之间进行网络通信时往往采用大端字节序,因此小端字节序机器在发送数据前需要进行字节序转换,在接收到数据处理处理数据之前要将网络字节序转换成本地字节序。

在Linux平台下提供了四个函数用来字节序转换:

1
2
3
4
5
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

Windows平台下也提供了相关的自己序转换函数:

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
#include <WinSock2.h>
unsigned __int64 __inline htond(
double value
);

unsigned __int32 __inline htonf(
float value
);

u_long WSAAPI htonl(
_In_ u_long hostlong
);

unsigned __int64 __inline htonll(
unsigned __int64 value
);

u_short WSAAPI htons(
_In_ u_short hostshort
);


double __inline ntohd(
unsigned __int64 value
);

float __inline ntohf(
unsigned __int32 value
);

u_long WSAAPI ntohl(
_In_ u_long netlong
);

u_long __inline ntohll(
unsigned __int64 value
);

u_short WSAAPI ntohs(
_In_ u_short netshort
);

这里有个技巧需要说明以下,比如要发送如下的结构体:

1
2
3
4
5
struct foo
{
int a;
long b;
};

为了避免每个成员都调用字节序转换函数,可以在结构体的内部定义两个方法用于转换字节序,添加字节序后的foo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct foo
{
int a;
long b;
void ntoh()
{
a = ntohl(a);
b = ntohl(b);
}
void hton()
{
a = htonl(a);
b = htonl(b);
}
};

需要特别注意的是,在发送结构体类型的数据时要注意字节对齐的问题,这里不再展开讨论,不同的平台有不同的解决办法。大体分为Winodws平台、AIX平台和GNU类平台。

近段时间写了两个通过http协议来获取指定网页的内容并将内容解析出来的程序。程序一可以解析出目前本博客的内容页面的内容、时间、访问次数参数,采用Qt类库实现;程序二可以解析出新浪博客页面的内容、时间等参数,采用Linux下的tcp相关API实现。均采用C++语言实现。

程序一

该程序采用Qt类库实现,其中Http协议的发送和接收采用Qt类库封装的类,网页内容的解析采用Qt封装的解析XML的相关类。
该程序仅能解析标准的Html语言,对于网页中的所有”<>”标签必须有结尾才行。例如本页面源码中的

1
<meta content="black" name="apple-mobile-web-app-status-bar-style" />

必须是闭合的。如果是下面这样则无法正确解析网页内容,这是由于采用的Qt类库决定的。

1
<meta content="black" name="apple-mobile-web-app-status-bar-style">

程序二

该程序的Http协议部分采用Linux的tcp协议api实现,解析网页直接采用搜索字符串的方式实现,较上一种方式要底层,仅能运行在Linux系统下运行。

相关下载

程序一和二的下载链接

本文的安装环境为ubuntu13.04。为了以后便于查阅,本文将相关插件的使用放到了文章的开始部分。这里不作插件的相关介绍,相关介绍看文章底部的参考文章。

插件使用

本插件快捷键会跟随下文安装内容一块同步。

ctags

在源码目录执行ctags -R可生成ctags文件。该文件在源码修改后并不会改变,需要重新生成ctags文件。
ctrl+]:转到函数定义处。
ctrl+T:回到执行ctrl+]的地方。

taglist

:TlistOpen:打开taglist窗口
:TlistClose:关闭taglist窗口。
:TlistToggle:在打开和关闭间切换。

NERD tree

:NERDTree:打开窗口。

winmanager

wm:打开和关闭taglist和NERD tree窗口。

a.vim

:A:在新Buffer中切换到c/h文件
:AS:横向分割窗口并打开c/h文件
:AV:纵向分割窗口并打开c/h文件
:AT:新建一个标签页并打开c/h文件
F12:代替:A命令

MiniBufExplorer

<Tab>:向前循环切换到每个buffer名上
<S-Tab>:向后循环切换到每个buffer名上
<Enter>:在打开光标所在的buffer
d:删除光标所在的buffer

插件安装

安装ctags

执行: sudo apt-get install ctags

安装taglist

  1. 下载页面:http://www.vim.org/scripts/script.php?script_id=273。下载后得到taglist_46.zip文件。
  2. 执行unzip taglist_46.zip解压文件。
  3. 将解压出的文件复制到~/.vim目录下。sudo cp ~/tmp/ ~/.vim/
  4. 在~/.vimrc文件中添加如下:
    1
    2
    3
    let Tlist_Show_One_File = 1            "不同时显示多个文件的tag,只显示当前文件的
    let Tlist_Exit_OnlyWindow = 1 "如果taglist窗口是最后一个窗口,则退出vim
    let Tlist_Use_Right_Window = 1 "在右侧窗口中显示
    参考网址:http://www.cnblogs.com/mo-beifeng/archive/2011/11/22/2259356.html

安装文件浏览器NERD tree

  1. 下载页面:http://www.vim.org/scripts/script.php?script_id=1658。
  2. 将下载后的nerdtree.zip文件解压到~/.vim目录下。

安装winmanager

  1. 下载页面:http://www.vim.org/scripts/script.php?script_id=95
  2. 将下载后的winmanager.zip文件解压到~/.vim目录下
  3. 修改.vimrc文件,添加:
    1
    2
    let g:winManagerWindowLayout='FileExplorer|TagList'
    nmap wm :WMToggle<cr>
    这样利用winmanager工具将taglist和NERD tree工具整合到了一个块,输入wm可以打开和关闭窗口。

安装cscope

  1. 下载页面:http://cscope.sourceforge.net,下载后得到文件cscope-15.8a.tar.gz。
  2. ./configure
  3. make。可能会出现错误,执行如下命令:
    1
    2
    3
    apt-get install libncurses-dev
    sudo apt-get install flex
    sudo apt-get install byacc
    然后执行make clean后重新make。
  4. sudo make install

安装在h/c文件之间切换插件a.vim

  1. 下载页面:http://www.vim.org/scripts/script.php?script_id=31。
  2. 将下载的a.vim文件复制到~/.vim/plugin文件夹下。
  3. 在~/.vimrc文件中添加nnoremap <silent> <F12> :A<CR>
  4. 下面内容为快捷键列表:
    :A switches to the header file corresponding to the current file being edited (or vise versa)
    :AS splits and switches
    :AV vertical splits and switches
    :AT new tab and switches
    :AN cycles through matches
    :IH switches to file under cursor
    :IHS splits and switches
    :IHV vertical splits and switches
    :IHT new tab and switches
    :IHN cycles through matches
    ih switches to file under cursor
    is switches to the alternate file of file under cursor (e.g. on <foo.h> switches to foo.cpp)
    ihn cycles through matches

安装快速浏览和操作Buffer

  1. 下载页面:http://www.vim.org/scripts/script.php?script_id=159
  2. 将下载的 minibufexpl.vim文件丢到 ~/.vim/plugin 文件夹中即可
  3. 在~/.vimrc文件中增加如下行:
    1
    2
    3
    let g:miniBufExplMapCTabSwitchBufs = 1
    let g:miniBufExplMapWindowNavVim = 1
    let g:miniBufExplMapWindowNavArrows = 1
  4. 快捷键:
    向前循环切换到每个buffer名上
    向后循环切换到每个buffer名上
    在打开光标所在的buffer
    d 删除光标所在的buffer

参考文章

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类对象的除该成员外的最后位置。

虚拟继承

对象成员的效率

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

指向数据成员的指针

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

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

0%