ytZhou Blog

代码 生活 阅读 思考 运动 旅行 摄影


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

Redis源码分析(一):aeMain()函数概述

发表于 2016-05-29   |   分类于 Code   |  

缘起

临近毕业大部分事情处理的差不多,只想安静的看点东西,思考一下。找来了redis源码配合《Redis设计与实现》看了一下。大概浏览了一遍,看到main函数的最后其实就是aeMain()当中的一个大循环,对这个大循环有些兴趣。这不由得让我想起之前了一部分的Linux Kernel代码,Kernel源码最后其实也是一个大的循环,根据进程调度算法选择进程运行,模式还是比较相似。当然,Kernel源代码肯定要比这个复杂 ^_^
写下这篇博文也算是对学习过程的一个记录和思考。

对NoSQL的接触

在接触redis之前,最初接触到的比较成熟NoSQL是叫做Tokyo Cabinet的一套Key-Value存储(在这之前看过国人写的一个叫做Hash DB的Key-Value)。之前也看过它当中的hashdb的实现,Tokyo Cabinet与redis不同之处就是Tokyo Cabinet是一个持久化的Key-Value存储,每个db都会对应一个磁盘上持久化的文件。
继续本文正题。^_^

redis.c中的大循环

从下面的代码可以看到redis最后其实就是一个大的循环。只要redis服务不停止就不断调用beforeSleep()和aeProcessEvents()函数。

1
2
3
4
5
6
7
8
9
//redis.c
int main()
{

//................
aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}

1
2
3
4
5
6
7
8
9
//ae.c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}

beforesleep函数“注册”

从下面的代码可以看到aeSetBeforeSleepProc()函数将beforeSleep“注册”到server.el下。
其实就是让server.el.beforsleep这个函数指针指向beforesleep函数。

1
2
3
void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep) {
eventLoop->beforesleep = beforesleep;
}

beforeSleep(eventLoop)

beforeSleep主要有以及几个功能:

  1. 如果启动集群功能(在我自己搭建过程是standalone mode,并未开启集群模式。其实是只有一台机器,玩不了集群模式→_→),则通过clusterBeforeSleep()进行集群故障转移,状态更新等操作;
  2. 调用activeExpireCycle()处理“过期”的Key-Value对,减少内存的占用
  3. 如果在上一次循环中有客户端blocked,那么给所有的slave发送一个ACK请求;
  4. 将AOF buffer写入到磁盘当中。有趣的是,AOF buffer写入磁盘是另外的线程去完成,并未使用主进程去完成,避免主进程的阻塞和挂起,相比内存操作而言磁盘要慢一些。这一个点在后面的文章中去分析。
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
void beforeSleep(struct aeEventLoop *eventLoop) {
REDIS_NOTUSED(eventLoop);

if (server.cluster_enabled) clusterBeforeSleep();

if (server.active_expire_enabled && server.masterhost == NULL)
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);

if (server.get_ack_from_slaves) {
robj *argv[3];

argv[0] = createStringObject("REPLCONF",8);
argv[1] = createStringObject("GETACK",6);
argv[2] = createStringObject("*",1);
replicationFeedSlaves(server.slaves, server.slaveseldb, argv, 3);
decrRefCount(argv[0]);
decrRefCount(argv[1]);
decrRefCount(argv[2]);
server.get_ack_from_slaves = 0;
}

if (listLength(server.clients_waiting_acks))
processClientsWaitingReplicas();

if (listLength(server.unblocked_clients))
processUnblockedClients();

flushAppendOnlyFile(0);
}

aeProcessEvents(eventLoop, AE_ALL_EVENTS)

aeProcessEvents()函数的主要功能简单来说,就是调用epoll_wait获取状态改变的fd,判断满足要求的fd的,并调用指定的处理函数。处理完成client的请求之后转而去运行server.el.timeEventHead中的事件函数。这样就完成了一次大的aeMain()中的循环。

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
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{

int processed = 0, numevents;

if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;

if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
long now_sec, now_ms;

/* Calculate the time missing for the nearest
* timer to fire. */

aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
tvp->tv_sec --;
} else {
tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
}
if (tvp->tv_sec < 0) tvp->tv_sec = 0;
if (tvp->tv_usec < 0) tvp->tv_usec = 0;
} else {
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
tvp = NULL; /* wait forever */
}
}

numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;

if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);

return processed;
}

与aeMain()相关的数据结构

如下图所示,redis中主要的数据结构。在后续的文章中会重点分析跟aeMain()比较相关的部分是如何完成初始化,以及aeMain()中网络编程相关的部分。

本文参考

《redis设计与分析》
《Redis in Action》

END

自己常用的的Vim配置

发表于 2016-05-26   |   分类于 Linux   |  

前段时间重装机器,又重新配置环境,发觉还是之前的vim配置好用。网上的vim配置大多感觉太麻烦或者要么就是太“笨重“。个人觉得vim配合ctags看代码和写代码已经足够用了。

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
set nocompatible
set nu
syntax on
set ruler

"set cursorline " 突出显示当前行
set ruler " 打开状态栏标尺
set shiftwidth=4 " 设定 << 和 >> 命令移动时的宽度为 4
set softtabstop=4 " 使得按退格键时可以一次删掉 4 个空格
set tabstop=4 " 设定 tab 长度为 4
set incsearch " 输入搜索内容时就显示搜索结果
set hlsearch " 搜索时高亮显示被找到的文本
set autoindent
set cindent

"记录文件上次打开的位置
if has("autocmd")
au BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") | exe "normal! g'\"" | endif
endif

"vi退出时自动清屏
set term=xterm
au VimLeave * :!clear

"tags跳转时显示可选列表
map <c-]> :ts <c-r><c-w><ENTER>

END

最近遇到的Linux文件系统只读问题

发表于 2016-05-20   |   分类于 Linux   |  

最近两次遇到Linux系统中磁盘出现问题而导致不能写入和更改磁盘数据。

问题的出现

  • 问题一: 某天早上导师说实验室网站挂掉了,让我去看看。从YII输出的错误信息看应该是后端MySQL连接不上,只好登录服务去查看MySQL进程是否存在。
  • 问题二: 今天由于实验室自己的Linux主机ubuntu出现问题,通过另外一块盘备份的时候出现不能写入,在该盘挂载的目录下执行命令都会提示Input/Output error。ubuntu forums上的相关问题

问题的解决

  • 对于第一个问题登录主机之后发现MySQL进程存在。然后试着通过从shell登录MySQL,结果提示Can’t connect to local MySQL server through socket /xx/xx.sock不能创建,网上说是需要修改这儿sock的权限+R即可,但是修改之后依旧不可以,ls之后发现已经是rwx了。这就让我非常纳闷了。后面发觉整个Linux文件系统不能写入东西了,然后more /proc/mounts之后发现分区的确变成ro只读状态了。由于之前备份过数据,只好冒着一定的危险,选择万能的重启策略了。最好,重启之后手动开启了Apache和Mysql服务就完成了问题的解决。
  • 由于有了之前的经验,今天遇到这个问题网上查了资料说是可能是磁盘损坏导致。通过dmesg命令发现,这一条log:
    [ 139.666511] EXT4-fs (sdb5): mounting ext3 file system using the ext4 subsystem
    [ 139.724006] EXT4-fs (sdb5): warning: mounting unchecked fs, running e2fsck is recommended
    [ 139.725427] EXT4-fs (sdb5): mounted filesystem with ordered data mode. Opts: (null)
    [ 200.126359] EXT4-fs error (device sdb5): ext4_lookup:1441: inode #110643744: comm rm: deleted inode referenced: 110668315
    既然如此提示,那就通过e2fsck来修复sdb5这个分区,但是修复的时候没选-a自动修复参数,默然是-y交互方式,导致修复的时候一直按y按了好长时间 囧。

小记

当磁盘出现问题的时候一般会发出信号,使得文件系统变成只读模式,此时不能修改和写入数据。这个时候比较快的策略应该是dmesg查看kernel的log,然后通过mount或者/proc/mounts看已经挂载分区的状态。如果是重要数据那么选择备份之后,卸载文件系统尝试通过e2fsck/fsck命令修复分区。
涉及命令:mount/umount/df/fdisk/lsof(这个命令很强大,非常有用)/fsck/e2fsck(-a自动修复,-y交互模式)

END

(译)POSIX Threads Programming(POSIX线程编程)

发表于 2016-05-08   |   分类于 Code   |  

本文翻译自POSIX Threads Programming,原作者是:Blaise Barney, Lawrence Livermore National Laboratory
翻译缘由:某次面试面试官的问题,让我一直在思考如何高效的使用线程及其锁,思考很长一段时间没有答案,缺乏实际的负载和场景让我能够真切体会和验证。网上关于锁性能和线程的问题并不多,往往只是简单的介绍编程接口,并在假设的场景下对锁的使用进行介绍,在IBM开发者社区看到一些文章,但是也是比较浅显,没有太直接深入。偶然的机会找到这篇文章,觉得还不错,因此将它翻译出来,一是自己能够从中学习到一些东西和加深理解,另外一点顺道巩固英语(雅思囧)。

目录

  1. 摘要
  2. 线程概述
    2.1 线程是什么?
    2.2 Pthreas是什么?
    2.3 是什么使用线程?
    2.4 线程编程的设计
  3. Pthreads API
  4. 编译线程程序
  5. 线程管理
    5.1 创建和终止线程
    5.2 传递参数给线程
    5.3 线程结合(Joing)和分离

摘要

在共享内存多处理器架构下,线程可以用来实现并行。硬件厂商会以他们自己私有的方式实现线程,这使得可移植性成为开发人员关心的问题。对于UNIX系统而言,IEEE POSIX 1003.1c标准将c语言线程编程接口标准化了,这个标准的具体实现就是POSIX threads或者Pthreads.

这份材料首先介绍了使用线程需要了解的概念、出发点(动机)和设计方面的考虑。然后,对于三类主要的Pthreads API例程进行了介绍:线程管理,互斥量(Mutex Variables)和条件变量(Condition Variables)。通过示例代码的演示,线程编程新手可以快速学会如何使用这些接口。这份材料包含了对LLNL细节以及MPI和线程混合编程的讨论。作为练习,许多示例代码(c语言实现)也包括在内。

适合的读者/先决条件:这份材料主要是针对那些刚刚接触并行编程的人。阅读这份材料需要对C中并行编程有个基本的了解。对于那些对并行编程并不了解的人,可以参考EC3500: Introduction to Parallel Computing。

Pthreads概览

线程是什么?

  • 严格的来说,一个线程被定义为由操作系统调度的一个独立的指令流。这意味着什么?
  • 对于开发人员而言,或许对线程最好的描述是:一个独立于它的主程序(main program)运行的“程序”(procedure)。
  • 在深入一步,想象一下一个主程序(a.out)包含多个程序(procedures),然后这些程序可以被操作系统同时或者独立的运行,这就是对多线程程序的描述。(译者注:其实可以理解为一个程序有多条不同的执行路径在同时执行。)
  • 这是怎么做到的呢?
  • 在理解线程之前,首先需要理解UNIX进程。一个进程有操作系统创建,这需要一定的开销(译者注:需要加载可执行文件,构建运行时环境,创建内存空间地址及其映射,打开三个标准的输入输出等等。具体可以参考《程序员的自我修养》)。进程包含了程序的资源以及执行状态,有:
    • 线程ID,线程组ID,用户ID,以及用户组ID
    • 环境
    • 工作目录
    • 程序执行
    • 寄存器
    • 栈
    • 堆
    • 文件描述符
    • 信号执行函数
    • 共享库
    • 进程间通信的工具(比如消息队列,pipes,信号量或者是内存共享)

UNIX进程
包含线程的UNIX进程

  • 线程使用并且与进程的这些资源一同存在,还能够成为操作系统调度和运行的实体。这是因为线程中仅仅包含独立运行需要的资源。
  • 线程能够成为独立的执行和调度单位,是因为线程自己维护了:
    • 栈指针
    • 寄存器
    • 调度的属性(比如策略或者优先级)
    • 包含处理和阻塞的信号集
    • 线程特定的数据
  • 总结,对于一个UNIX环境下的线程:
    • 存在于某个特定的进程之内,并且使用这个特定进程的资源
    • 拥有自己独立的控制流只要他的父进程存在并且操作系统支持
    • 仅仅包含独立调度所需的资源
    • 与其他线程共享父进程的资源
    • 父进程消亡时,线程也消亡
    • 轻量级。因为创建进程需要完成更多资源分配,而线程仅仅包含很少的资源。
  • 因为同一个进程中的线程共享资源:
    • 一个线程修改共享的资源能够被其他线程知道
    • 两个指针有相同的值并指向相同的数据
    • 能够读写相同的内存地址,因此需要开发者指定同步方式

Pthreads是什么?

  • 一直以来,硬件厂商以他们自己的方式实现了线程。不同厂商之间的实现基本是不同的,这就使得开发者很难使用线程开发出基于移植性的程序。
  • 为了能够有效利用线程带来的好处,需要一份编程标准:
    • 对于UNIX系统,这些接口由IEEE POSIX 1003.1c标准指定(1995)
    • POSIX或者Pthreads就是对这个标准的实现
    • 现在大多数的硬件厂商除了他们私有的API外,都已经提供Pthreads
  • POSIX标准在不断的演变和修改,包括Pthreads规范
  • 一些有用的链接
    • standards.ieee.org/findstds/standard/1003.1-2008.html
    • www.opengroup.org/austin/papers/posix_faq.html
    • www.unix.org/version3/ieee_std.html
  • Pthreads被定义为一组C语言编程接口和调用,通过pthread.h头文件和一个线程库实现——在某些实现中,这个线程库或许是其他库的一部分,比如libc.

为什么使用线程?

->轻量级:

  • 与创建进程相比,线程创建仅仅更少开销,对线程的管理需要更少的系统资源。
  • 比如,下面的表格比较了fork()和pthread_create()两个子例程创建50, 000个进程和线程的开销。使用time命令测试所需的时间,以秒为单位,不对优化参数进行编译。
    注意:通过将系统时间和用户时间相加得到真实的运行时间是不合理的,因为对于SMP系统中多CPUs/cores是在同一时间处理用一个问题。充其量而言,这些结果接近于在本地机器过去和现在运行的结果。




Foo
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
//==============================================================================
//C Code for fork() creation test
//==============================================================================
#include <stdio.h>
#include <stdlib.h>
#define NFORKS 50000

void do_nothing() {
int i;
i= 0;
}

int main(int argc, char *argv[]) {
int pid, j, status;

for (j=0; j<NFORKS; j++) {

/*** error handling ***/
if ((pid = fork()) < 0 ) {
printf ("fork failed with error code= %d\n", pid);
exit(0);
}

/*** this is the child of the fork ***/
else if (pid ==0) {
do_nothing();
exit(0);
}

/*** this is the parent of the fork ***/
else {
waitpid(pid, status, 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
//==============================================================================
//C Code for pthread_create() test
//==============================================================================
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NTHREADS 50000

void *do_nothing(void *null) {
int i;
i=0;
pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
int rc, i, j, detachstate;
pthread_t tid;
pthread_attr_t attr;

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

for (j=0; j<NTHREADS; j++) {
rc = pthread_create(&tid, &attr, do_nothing, NULL);
if (rc) {
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}

/* Wait for the thread */
rc = pthread_join(tid, NULL);
if (rc) {
printf("ERROR; return code from pthread_join() is %d\n", rc);
exit(-1);
}
}

pthread_attr_destroy(&attr);
pthread_exit(NULL);

}
END

为什么使用锁?自己的一些思考,以及死锁调试的方法

发表于 2016-05-07   |   分类于 Code   |  

锁和并发是一个非常大话题,往往在实际中大家都有点敬而远之,而网上的资料,特别是某度查出来的东西就是千篇一律互相Copy,很难看到有自己的思考在里面,很少会去讲为什么使用锁。如何高效的使用锁。
某次面试面试官直接上来就说给我讲讲你满意的项目,然后就讲了之前基于Fuse写的数据去重文件。在讲的过程当中提到了使用了互斥锁pthread_mutex_lock,这个时候面试官打断了我,问到为什么要使用锁?在什么样的真实场景下需要使用锁?当时讲了两个应用同时读写一个文件,出现数据不一致。面试官强调了一下,给个实际的例子。当时没给出很实际的例子,后来面试官说我那样使用锁并不高效。
在面试过程中当中发现面试官其实非常强调合理和高效的使用锁,二面面试官当时也给出了问题,在高并发和高查询下,如何在不使用的锁的情况,完成数据更新的情况并保证不打断查询。当时给的解决方案是先将数据更新到另外一个地方,然后再将查询指向那个更新的地方,因为修改指针其实非常快。如果非要死扣细节的话,最佳应该是配合使用voilate关键字。

为什么使用锁

锁的性能

自己调试死锁的一些方法

待续

END

基本数据类型,结构体,类的sizeof大小

发表于 2016-05-06   |   分类于 Code   |  

sizeof

sizeof在C/C++作为操作符,而不是函数。sizeof操作符以字节的形式给出其操作数的大小。

字节对齐(Data structure alignment)

Wikipedia上关于字节对齐的解释:Data structure alignment is the way data is arranged and accessed in computer memory. It consists of two separate but related issues: data alignment and data structure padding. When a modern computer reads from or writes to a memory address, it will do this in word sized chunks (e.g. 4 byte chunks on a 32-bit system) or larger. Data alignment means putting the data at a memory address equal to some multiple of the word size, which increases the system’s performance due to the way the CPU handles memory. To align the data, it may be necessary to insert some meaningless bytes between the end of the last data structure and the start of the next, which is data structure padding.
从这段解释我们可以知道字节对齐的原因是:

  • 加快CPU访问内存中数据的速度;如果将数据存放在偶地址开始那么读出32bit变量时,仅仅需要一个周期,反之放在奇数地址之上需要两个周期才能读出。
  • 可以简化CPU和内存之间的接口。

每个特定的编译器都有自己默认的对齐系数,可以通过编译命令#pragma pack(n)指定对齐系数,其中n=1,2,4,8,16.

实例

结构体大小

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
//struct.cc
#include <iostream>

using namespace std;

struct stu1 {
int i;
char c;
int j;
};

struct stu2 {
char c;
int i;
char a;
};

struct stu3 {
char c;
char a;
int i;
};

struct stu4 {
int arr[0];
};

struct stu5 {
char *c;
};

struct stu6 {
char *c;
int i;
};

struct stu7 {
double i;
char c;
};

struct stu8 {
struct stu7 stu7Ojbect;
double i;
};

int main (int argc, char *argv[])
{

cout << "int = " << sizeof(int) << ", float = " << sizeof(float)
<< ", long = " << sizeof(long) << ", double = " << sizeof(double)
<< ", long long = " << sizeof (long long) << endl;
cout << "the size of stu1 is " << sizeof(stu1) << endl;
cout << "the size of stu2 is " << sizeof(stu2) << endl;
cout << "the size of stu3 is " << sizeof(stu3) << endl;
cout << "the size of stu4 is " << sizeof(stu4) << endl;
cout << "the size of stu5 is " << sizeof(stu5) << endl;
cout << "the size of stu6 is " << sizeof(stu6) << endl;
cout << "the size of stu7 is " << sizeof(stu7) << endl;
cout << "the size of stu8 is " << sizeof(stu8) << endl;

return 0;
}

在linux 64bit上以32位方式进行编译,结果如下:

1
2
3
4
5
6
7
8
9
10
11
$g++ -m struct.cc -o struct
$./struct
int = 4, float = 4, long = 4, double = 8, long long = 8
the size of stu1 is 12
the size of stu2 is 12
the size of stu3 is 8
the size of stu4 is 0
the size of stu5 is 4
the size of stu6 is 8
the size of stu7 is 12
the size of stu8 is 20

在linux 64bit上编译,结果如下:

1
2
3
4
5
6
7
8
9
10
11
$g++ struct.cc -o struct
$./struct
int = 4, float = 4, long = 8, double = 8, long long = 8
the size of stu1 is 12
the size of stu2 is 12
the size of stu3 is 8
the size of stu4 is 0
the size of stu5 is 8
the size of stu6 is 16
the size of stu7 is 16
the size of stu8 is 24

从上面的代码和运行结果中我们可以得出结论:

  • 一个结构体中对齐方式是按照最长的基本类型为基准的。
    • 比如,struct stu1中最长的是int,因此按照4字节对齐;而struct stu6最长是int *,在64位平台下按照8字节对齐。
    • 按照4字节对齐时,不到4字节时,补齐另外的字节,比如struct stu2。8字节对齐时,依次类推。
  • 需要注意的是柔性数组struct stu4中长度为0的数组并不占用空间,即sizeof(struct stu4)值为0,柔性数组主要用于实现动态的数组或者空间管理,在Linux Kernel以及redis的底层sds实现中均有体现。

类的大小

类的大小稍微复杂一些,需要考虑继承和虚函数问题,但是搞懂了虚函数问题,这个问题其实非常容易理解。关于虚函数的原理,可以参考皓叔的这篇博文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
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
#include <iostream>

using namespace std;

class nullClass
{
public:
nullClass();
~nullClass();
};

class nullClassVirtual
{
public:
nullClassVirtual();
virtual ~nullClassVirtual();//虚函数需要占据一个指针的大小,64bits机器上是8字节
};

class staticClass {
public:
static int i;
};

class staticConstClass {
static const int i;
};

class Base
{
public:
Base();
virtual ~Base(); //虚函数需要占据一个指针大小,64bits机器上是8字节
void set_num (int num)
{

a = num;
}
private:
int a;
char *p;
char c;
};

class Drive : public Base
{
public:
Drive() : Base(){}
~Drive(){};
};

int main (int argv, char *argc[])
{

cout << "the size of pointer is " << sizeof(int *) << endl;
cout << "the size of nullClass is " << sizeof(nullClass) << endl;
cout << "the size of nullClassVirtual " << sizeof(nullClassVirtual) << endl;
cout << "the size of staticClass " << sizeof(staticClass) << endl;
cout << "the size fo staticConstClass " << sizeof(staticConstClass) << endl;
cout << "the size of Base is " << sizeof(Base) << endl;
cout << "the size of Drive is " << sizeof(Drive) << endl;

return 0;
}

在Linux 64bit的机器上分别以32位和64位的方式编译和运行

1
2
3
4
5
6
7
8
9
$g++ -m32 class.cc -o class
$./class
the size of pointer is 4
the size of nullClass is 1
the size of nullClassVirtual 4
the size of staticClass 1
the size fo staticConstClass 1
the size of Base is 16
the size of Drive is 16

  • 首先空类的大小1,这是因为在实例化的过程中每个实例在内存中都得有一个独一无二的地址,为了达到这个目的,编译器会给空类隐含加一个字节。详见stakcoverflow: Why is the size of an empty class in C++ not zero?
  • 对于空类,但是析构函数是virtual类型,该函数在基本的虚函数表中占用一项,因此szieof(nuullClassVirtual)大小为4.
  • 对于类中包含static/ static const成员的类,该静态成员不影响类的大小,该成员之友一个实例存在,无论类是否被实例化。
  • 对于Base类,大小为4(虚函数) + 4(int a) + 4(char *p) + 4(char c,虽然char大小为1,但是此处按照4字节对齐) = 16
  • 对于Drive类,继续了基类,因此大小至少为16,但是除此之外Drive类中不包含其他成员。

Linux 64bit下编译

1
2
3
4
5
6
7
8
9
$g++ class.cc -o class
$./class
the size of pointer is 8
the size of nullClass is 1
the size of nullClassVirtual 8
the size of staticClass 1
the size fo staticConstClass 1
the size of Base is 32
the size of Drive is 32

END

Hello World

发表于 2016-05-06   |   分类于 Hexo   |  

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

END
ytZhou

ytZhou

7 日志
3 分类
9 标签
RSS
github lofter
© 2015 - 2016 ytZhou
由 Hexo 强力驱动
主题 - NexT.Pisces