笔试的范围面算是比较广吧~涉及操作系统~数据结构和算法~网络~c/c++~软件工程~唉~还是太弱了~抱着打酱油的心去带回一大瓶酱油~还是得总结总结打有准备的仗~考到线程的并发~插入排序~tcp/ip~操作系统内存分配~~加油了~要有准备要努力啊
这三天
逃了三天课,这三天来~也算是对三年来的验证吧。星期一,浙大玉泉校区参加163.com的笔试(sa反垃圾邮件工程师),星期二,笔试太累,基本一天在实验室睡觉。今天去了趟滨江。在163.com的c座大厦里就吃了一个他们的小面包和一杯水。╮(╯▽╰)╭,不管结果如何~或许都得到历练了~ 星期一,下午两点多,自个火急火燎的赶到浙大,看到玉泉校区才知道什么是读书人的地方,人家的校园,人家的读书氛围,看的像我这种二本掉渣的院校心里不是一般滋味.在里面逛了一圈后终于找到了曹光彪科技楼.晚上的宣讲会赵HR看起来很年轻,讲得很有渲染力。演讲的能力让我打心底里佩服。到场有三百多个人~大部分都是浙大~好不自卑吗~我是坐b1去的玉泉校区~然后我的笔试座位号也B1(神座啊)~八点的考试~主考官最后一个把考卷给我~算是从8:10开始的~我是9:01交的卷子~当时在场的我第三个交卷~自我感觉不错的~就赶着坐公交回下沙了~回到学校的时候已经10:30了~所以没回寝室~在实验室睡了一晚~还因此感冒了~杭州的天气还真是易变啊 星期二,因为笔试那天晚上赶着回来太累了~而且这一天就一节软件工程~所以逃了~也因HR告诉我们笔试的通知最晚在第二天的晚上告诉我们~这一天我是在焦虑中度过的~但我觉得自己的笔试很好~可以拿到面试的~晚上跟着老聂从实验室回去~吃了个饭洗了个澡~然后突然发现163.com的面试通知短信~很兴奋~跑去特地要老聂找去滨江的公交路线~一整晚睡得还可以~ 今天~怎么说呢~一切的一切或许又回归了吧~今天又逃了一天的课~早上八点跑来实验室拿简历~九点的时候去书店给天师取快递~快十点的时候坐着566前往滨江~快中午的时候到达滨江~好久都不出去了~下车后有点找不着北的感觉~找了好久才找到面试的地点~快下午一点的时候开始前往面试地点~路上认识一位从上海赶过来的哥们~他研二搞安卓的~他说饿想找吃的~最后我们一起来到了163.com大厦的C座签到,发现面试区域居然提供吃喝~163.com的研究所所在地的确很偏僻~跟我一起的哥们也这么说~他一口气吃了两个面包~我们才坐下不久我就面试开始了~面我的是个很年轻的技术哥~他首先让我自我介绍~然后他开始看我的网上简历和我的笔试卷子~边看边问~当时也是脑子一片空白~随性也跟他扯起来~我记得他让我介绍了我的两个项目~一个是在线评判系统~另一个是ftp(半成品的项目)~我糊里糊涂的也不知道怎么说了~最后依稀记得他向我问了tcpdump和smtp协议~我记得笔试的卷有两道tcp/ip的题目~我答得都挺好的~一个是tcp的三次握手过程和那七层协议~其实我看到面试官手里我的卷子上打着一个不错的分数~心里窃喜啊~最后面试官让我问他问题~我问了几个后就不问了~然后他说面试结束了让我去面试区等待HR通知~走出来的时候我问面试官怎么不问shell啊?他没回答~他过了会问我课多吗~我随性的回答了~出来后发现跟我一起来的那哥们也刚面完出来~我们彼此谈了感受~期间他又吃了两个面包~我们在等待区侯着期间认识了一位中国科技大学的研一的也搞安卓的~他还没有面~他不断地询问都面了啥~我看了他的简历~光项目他就有了一页纸了~牛人啊~然后的然后~那个女HR告诉我结果~我没有追问为何~走出来的时候也挺释然的~╮(╯▽╰)╭╮(╯▽╰)╭~ 一句话概括:面包挺好吃的,水也挺甜的~这三天也算是攒了经验值吧~结果已经不重要了~过程已经享受了~在此之前在群里也吼了好多~╮(╯▽╰)╭~回去洗个澡~睡个觉明天的太阳依旧灿烂~一个掉渣的二本院校的技术屌丝男的小逆袭?或许吧~
多线程之生产者于消费者问题
生产者线程向一缓冲区中写入数据~消费者线程从缓冲区中读取数据,由于生产者线程和消费者线程共享同一缓冲区,为了正确的读写数据,在使用缓冲队列时必须保持互至。生产者线程和消费者线程必须满足:生产者写入的缓冲区的数目不能超过缓冲区的容量,消费者线程读取的数目不能超过生产者写入的数目。初始化读写指针为0,如果读指针等于写指针。则缓冲区是空的~如果(写指针+1) % N 等于读指针,则缓冲区是满的 code
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
|
新交通规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Unix Socket Server&client
socket()函数创建一个socket,然后调用bind()函数将其与本机的地址以及一个本地端口绑定~然后利用listen()在相应的socket上监听,当accept()接收到一个连接的服务请求时,服务器将显示客户机的ip地址,并通过新的socket向客户端发送字符串”Hello,you are connected!”最后关闭socket
myserver.c: 客户端通过服务器域名获得服务器的ip,然后新建一个socket调用connect与服务器连接,连接成功后接收从服务器发送过来的数据,最后关闭socket
gethostbyname()是完成域名转换的 struct hostent gethostbyname(const char name)
server
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 |
|
client
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 |
|
Gentoo最小化安装
转载自有删减http://www.gentoo.org/doc/zh_cn/gentoo-x86-quickinstall.xml 代码 1.1: 测试机器的规格
(下述配置和耗时情况帮你粗略估计完成整个安装过程大概需要的时间)
1 2 |
|
1 2 3 4 |
|
1 2 |
|
- 快速安装指南
安装介质
从我们的镜像下载一张光盘。可以在releases/x86/current-iso/中找到最小光盘的ISO文件。最小安装光盘只适用于有网络环境的安装。本指南将使用最小光盘。
刻制光盘,用它引导机器。
从光盘启动
启动时按下F2查看有哪些引导选项。你可以启动gentoo或者gentoo-nofb,后者禁用帧缓冲。如果你用LiveCD引导,别忘了添上nox选项以防止启动X图形环境。有好几个选项可用于启用或禁用一些特性。正常情况下会检测出你的硬件并装载所有模块。如果内核无法正确引导或是在引导过程中挂机,你可能不得不尝试不同的配置。最安全的办法大概是使用nodetect选项,然后显式地载入需要的模块。
代码 2.1: 引导最小光盘
1 2 3 4 5 |
|
可选:装载模块
如果使用了nodetect选项,就要在启动后载入必要的模块。你还要配置好网络并拥有访问磁盘的权限。lspci命令能帮助你确认硬件信息。
代码 2.2: 载入必要的模块
1 2 3 4 5 6 |
|
网络配置
如果还没开启网络,可以用net-setup来配置网络。配置之前可能要先用modprobe为网卡载入支持模块。如果你有ADSL,请使用pppoe-setup和pppoe-start。要支持PPTP,首先编辑/etc/ppp/chap-secrets和/etc/ppp/options.pptp,然后使用pptp
如果是无线连接,用iwconfig设置无线连接参数,然后再次执行net-setup或者手动运行ifconfig、dhcpcd和/或route。
如果你用代理,不要忘了使用export http_proxy、ftp_proxy和RSYNC_PROXY初始化系统环境。
代码 2.3: 通过向导配置网络
1
|
|
或者,你也可以手动打开网络。下面的例子把你电脑的IP地址配置为192.168.1.10,并把网关和域名服务器设为192.168.1.1。
代码 2.4: 手动配置网络
1 2 3 |
|
安装盘允许你启动一个sshd服务,添加用户,运行irssi(一个命令行的客户端聊天工具),还可以使用links进行网上冲浪。
可选:通过ssh连接到你的新机器
最有趣的功能当然是sshd。启动这个服务,从另一台机器连过来,然后从本指南复制/粘贴命令。
代码 2.5: 启动sshd
1 2 3 4 5 6 7 8 |
|
现在设置一下安装盘的root密码,使你能通过另一台电脑连接。请注意正常情况下不推荐你允许root通过ssh连接。如果你的本地网络不太可靠,请设一个又长又复杂的密码。它只能使用一次,因为机器重启后就没掉了。
代码 2.6: 设置root密码
1 2 3 4 |
|
现在就可以在另一台电脑上打开一个终端并连接到你的新机器,在新的窗口中继续本指南接下去的内容,复制/粘贴文中的命令。
代码 2.7: 从另一台电脑连到你的新机器
1 2 3 4 5 6 7 |
|
准备磁盘
使用fdisk或者cfdisk创建分区规划。至少需要一个交换分区(类别为82)和一个Linux分区(类别为83)。下面是我们的手册选用的方案,创建包括一个/boot分区,一个交换分区和一个主分区。将/dev/sda替换为你自己的磁盘。大多数系统忽略启动标志,但有的系统需要它。使用fdisk的a命令在启动分区上设置这个标志。
代码 2.8: 创建分区
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
使用mke2fs,mke2fs -j,mkreiserfs,mkfs.xfs和mkfs.jfs建立Linux分区的文件系统。使用mkswap和swapon初始化交换分区。
代码 2.9: 创建文件系统并激活交换分区
1 2 3 4 5 6 7 8 |
|
把新建的文件系统挂载到/mnt/gentoo。如果需要其他挂载点(如/mnt/gentoo/boot),也要为它们创建目录并挂载好。
代码 2.10: 挂载文件系统
1 2 3 4 |
|
安装Stage
首先确保正确设置了日期和时间。执行date MMDDhhmmYYYY,使用UTC时间。
代码 2.11: 设定日期和UTC时间
1 2 3 4 5 6 7 |
|
接下来,从我们的镜像下载一份stage包:
代码 2.12: 下载一份stage3压缩包
1 2 3 |
|
进入/mnt/gentoo,执行tar xjpf
代码 2.13: 解开stage3压缩包
1 2 3 4 5 |
|
安装最新的Portage快照。跟下载stage3压缩包一样:从我们的列表选择一个最近的镜像,下载最新的快照并解压。
代码 2.14: 下载最新的Portage快照
1 2 3 |
|
代码 2.15: 解开Portage快照
1 2 3 4 5 |
|
切换系统
挂载/proc & /dev文件系统,拷贝/etc/resolv.conf文件,然后chroot到你的Gentoo环境。
代码 2.16: Chroot
1 2 3 4 5 6 7 |
|
设定时区
设置你的时区信息:使用/usr/share/zoneinfo中的正确条目。
代码 2.17: 设置时区
1 2 3 4 5 |
|
设定主机名和域名
在/etc/conf.d/hostname和/etc/hosts中设置主机名。以下例子中我们用mybox作为主机名,用at.myplace作为域名。可以用nano编辑这些配置文件,或者使用下面的命令:
代码 2.18: 设置主机名和域名
1 2 3 4 5 6 7 |
|
内核配置
安装一个内核源码包(通常为gentoo-sources),配置、编译并拷贝arch/i386/boot/bzImage文件到/boot。
代码 2.19: 安装内核源码包,编译和安装内核
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
配置系统
编辑/etc/fstab,用实际的分区名代替BOOT、ROOT和SWAP。记得确认一下文件系统是否与所安装的相匹配。
代码 2.20: fstab范例
1 2 3 4 5 |
|
在/etc/conf.d/net中配置网络。把net.eth0启动脚本添加到默认运行级别。如果有多张网卡,分别为它们建立符号链接到net.eth0启动脚本,并一起添加到默认运行级别。用nano编辑/etc/conf.d/net,或者使用如下命令:
代码 2.21: 配置网络
1 2 3 4 5 6 7 8 9 |
|
注意: 如果需要支持PCMCIA卡,请emerge pcmciautils。
执行passwd设置root密码。
代码 2.22: 设置root密码
1 2 3 4 |
|
编辑/etc/conf.d/clock以定义前面所使用的时区。
代码 2.23: 编辑/etc/conf.d/clock
1 2 |
|
检查系统配置,查看并编辑这些配置文件:/etc/rc.conf, /etc/conf.d/rc,/etc/conf.d/keymaps。
代码 2.24: 可选:编辑一些配置文件
1 2 3 |
|
安装系统工具
安装一个系统日志如syslog-ng和一个cron守护进程如vixie-cron,并把它们添加到默认运行级别。 注意: Cron守护进程依赖于MTA。mail-mta/ssmtp会因依赖性而被一起安装。如果你想使用一种更高级的MTA,可以现在安装它。要是现在没空,暂时先安装ssmtp,以后再卸载并安装你要的MTA。
代码 2.25: 安装系统日志和cron守护进程
1 2 3 4 5 6 7 |
|
安装必要的文件系统工具(xfsprogs,reiserfsprogs或jfsutils)和网络工具(dhcpcd或ppp)。
代码 2.26: 安装其它工具
1 2 3 4 5 |
|
配置引导程序
安装并配置grub。
代码 2.27: Emerge grub并编辑它的配置文件
1 2 3 4 5 6 |
|
代码 2.28: grub.conf范例
1 2 3 4 5 6 |
|
代码 2.29: 安装grub
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
重启
退出chroot环境,卸载所有文件系统并重启:
代码 2.30: 重启
1 2 3 4 |
|
完成安装 注意: 在我们这台测试机器上,从minimal CD启动显示boot提示符开始,到重启后显示登录提示符, 总用时为00:42:31 。没错,不到一个小时!注意这个时间还包含了stage3包、Portage快照和一些软件包的下载时间,以及配置内核所花的时间。
以root身份登录,用useradd添加一个或多个用户,作为日常使用。
代码 2.31: 从另一台电脑连到你的新机器
(清理known_hosts文件中的内容,因为你的新机器已生成一个新的
通用主机密钥)
$ nano -w ~/.ssh/known_hosts
(找出新机器的IP并删除那一行,保存并退出nano)
(使用你新机器的IP地址)
1 2 3 4 5 6 |
|
代码 2.32: 添加一个新用户
1 2 3 4 5 |
|
最后涉及的配置
开始选择最近的镜像,在/etc/make.conf中定义SYNC变量和GENTOO_MIRRORS变量,或者也可以使用mirrorselect。此时你还可以定义并行编译的进程数。
代码 2.33: 使用mirrorselect并设置MAKEOPTS
1 2 3 4 5 |
|
现在是启用或禁用一些USE标记的好时机。运行emerge -vpe world,列出所有当前已安装的软件包以及它们被启用和禁用的USE标记。编辑/etc/make.conf或使用下列命令定义USE变量:
代码 2.34: 查看当前使用的USE标记并启用或禁用一些
1 2 3 4 |
|
新版本的glibc使用/etc/locale.gen来定义语言环境。
代码 2.35: 定义语言环境
1 2 3 |
|
最后但并非最轻松的,你可能想修改/etc/make.conf中的CFLAGS变量来优化编码,以满足你的特定需要。请注意极少需要一长串的标记列表,那样甚至可能导致系统崩溃。建议通过march选项指定处理器类型并赋上-O2 -pipe。
可能你还想转到 ~x86。只有当你能够对付那些脆弱不良的ebuild和软件包时才能这么做。如果你倾向于保持系统的稳定性,请不要添加ACCEPT_KEYWORDS变量。添加FEATURES=”ccache”却是个好主意。
代码 2.36: 为make.conf作最后的修改
1 2 3 4 5 6 7 |
|
可能你会想重编译整个系统两次,以使你所作的最新配置完全生效。这要花很长的时间来完成,而只能得到微小的速度提升。你可以让系统自己随着以后新软件包的发布而逐步完成优化。不过,站在保持系统一致性的立场来看,重编译仍不失为一个好主意。请参考文档Gentoo GCC升级指南,其中讨论了如何搭建一个具有良好一致性的system和world,以及这样做的好处。
只重编译那些因你应用了新的USE标记或受此影响而需要升级的软件包,也要花费不少时间。可能还必须卸载会阻止你升级的软件包。在emerge -vpuD –newuse world的输出中寻找“[blocks B ]”,用emerge -C卸载它们。
代码 2.37: 升级软件包
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 |
|
接下来该做什么
想安装服务器应用程序还是一个桌面系统,取决于你打算让你的新Gentoo做什么。以下仅作为一个例子,统计了emerge gnome和emerge kde在~x86系统(按之前的描述安装的)上所花费的时间。两者是在同一起点开始安装的。
去看看我们的文档目录,研究一下如何安装和配置你挑选的软件。 重要: 以下只是举个例子,并不意味着就作为推荐设置。
代码 2.38: Emerge GNOME
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 |
|
开发与研发
1 2 3 4 5 6 7 |
|
C++虚函数及虚函数表解析
转自:http://www.cnblogs.com/realyan/archive/2011/07/14/2106339.html 虚函数的定义:
虚函数必须是类的非静态成员函数(且非构造函数),其访问权限是public(可以定义为private or proteceted, 但是对于多态来说,没有意义。),在基类的类定义中定义虚函数的一般形式:
virtual 函数返回值类型 虚函数名(形参表)
{ 函数体 }
虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,
可以在基类的派生类中对虚函数重新定义(形式也 是:virtual 函数返回值类型 虚函数名(形参表){ 函数体 }),在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继 承其基类的虚函数。当程序发现虚函数名前的关键字virtual后,会自动将其作为动态联编处理,即在程序运行时动态地选择合适的成员函数。
实现动态联编需要三个条件:
1、 必须把需要动态联编的行为定义为类的公共属性的虚函数。
2、 类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来。
3、 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。
定义虚函数的限制:
(1)非类的成员函数不能定义为虚函数,类的成员函数中静态 成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数 定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析 构函数。
(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名同返回值同参数个数同参数类型函数。
为什么虚函数必须是类的成员函数:
虚函数诞生的目的就是为了实现多态,在类外定义虚函数毫无实际用处。
为什么类的静态成员函数不能为虚函数:
如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(:在内存中只有一份拷贝;通过类名或对象引用访问静态成员)本身就是相矛盾的。
为什么构造函数不能为虚函数:
因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就 是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类, 其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保vtbl的构建成功呢?
注意:当基类的构造函数内部有虚函数时,会出现什么情况呢? 结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只 有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类 版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的 析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数 中,虚函数机制也是不起作用的。
C++中的虚函数的作用主要是实现了多态的机制。关于多态, 简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技 术。所谓泛型技术,说白了就是试图使用不变的代码(Or 不变的 接口)来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见
言归正传,让我们一起进入虚函数的世界。
虚函数表
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。 这样,在有虚函数的类的实例(注:抽象类即有纯虚函数的类不能被实例化。)中这个表被分配在了这个实例的内存中(注:一个类的虚函数表是静态的,也就是说 对这个类的每个实例,他的虚函数表的是固定的,不会为每个实例生成一个相应的虚函数表。),所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表 就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。在C++的标准规格说明书 中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张 虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
假设我们有这样的一个类: class Base {
public:
virtual void f() { cout << “Base::f” << endl; }
virtual void g() { cout << “Base::g” << endl; }
virtual void h() { cout << “Base::h” << endl; }
};
按照上面的说法,我们可以通过Base的实例来得到Base的虚函数表。 下面是实际例程: { … typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << “虚函数表地址:” << (int*)(&b) << endl;
cout << “虚函数表 — 第一个函数地址:” << (int)(int*)(&b) << endl; // Invoke the first virtual function
pFun = (Fun)((int)(int)(&b));
pFun(); …
}
实际运行经果如下(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3) :
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行 把&b转成int ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)((int)(int)(&b)+0); // Base::f()
(Fun)((int)(int)(&b)+1); // Base::g()
(Fun)((int)(int)(&b)+2); // Base::h()
画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。
在WinXP+VS2003下,这个值是NULL。
而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的子类虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重写任何父类的函数。那么,在派生类的实例的虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下: (overload(重载) 和 override(重写),重载就是所谓的名同而签名不同,重写就是对子类对虚函数的重新实现。)
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例的虚函数表会是下面的样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了子类虚函数表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表(子类的虚函数表)的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以用任一个父类指针来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、尝试:通过父类型的指针(指向子类对象)访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到子类的虚表中有Derive自己的虚函数,但我们根本不可能使用基类的指针来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。
但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
二、尝试:通过父类型的指针(指向子类对象)访问父类的non-public虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于子类虚函数表中,所以我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如: class Base { private: virtual void f() { cout << “Base::f” << endl; } };
class Derive : public Base{ };
typedef void(*Fun)(void);
void main()
{
Derive d;
Fun pFun = (Fun)((int)(int)(&d)+0);
pFun();
}
结束语
C++这门语言是一门Magic的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要去了解C++中那些危险的东西。不然,这是一种搬起石头砸自己脚的编程语言。
C++中的虚函数
转自:http://www.cnblogs.com/realyan/archive/2011/07/13/2105801.html
一.简介
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
class A {
public: virtual void foo()
{
cout << “A::foo() is called” << endl;
}
}; class B: public A
{
public: virtual void foo()
{
cout << “B::foo() is called” << endl;
}
};
那么,在使用的时候,我们可以: A * a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的 调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函 数。 虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public: virtual void foo();
};
class B: public A
{
virtual void foo();
}; void bar()
{
A a; a.foo(); // A::foo()被调用
}
1.1 多态 在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:void bar(A * a)
{ a->foo(); // 被调用的是A::foo() 还是B::foo()? }
因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。 这种同一代码可以产生不同效果的特点,被称为“多态”。 1.2 多态有什么用? 多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的 用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更 容易理解。 在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时 候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变 (增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。 多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层 次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态–编 译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。
1.3 如何“动态联编” 编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。 我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是 VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生 类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的 时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将 这个调用改写,针对1.1中的例子:
void bar(A * a) { a->foo(); }
会被改写为:
void bar(A * a) { (a->vptr[1])(); }
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。 虽然实际情况远非这么简单,但是基本原理大致如此。 1.4 overload和override 虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下: override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允 许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译 为“覆盖”,还贴切一些。 overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点 数作为参数。 二. 虚函数的语法 虚函数的标志是“virtual”关键字。 2.1 使用virtual关键字 考虑下面的类层次:
class A { public: virtual void foo(); }; class B: public A { public: void foo(); // 没有virtual关键字! }; class C: public B // 从B继承,不是从A继承! { public: void foo(); // 也没有virtual关键字! };
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。 2.2 纯虚函数 如下声明表示一个函数为纯虚函数:
class A { public: virtual void foo()=0; // =0标志一个虚函数为纯虚函数 };
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。 2.3 虚析构函数 析构函数也可以是虚的,甚至是纯虚的。例如:
class A { public: virtual ~A()=0; // 纯虚析构函数 };
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A {
public: A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虚析构函数
private: char * ptra; }; class B: public A { public: B() { ptrb = new char[20];}
~B() { delete[] ptrb;} private: char * ptrb; }; void foo() { A * a = new B; delete a; }
在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕? 如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。 纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。 2.4 虚构造函数? 构造函数不能是虚的。 三. 虚函数使用技巧
3.1 private的虚函数 考虑下面的例子:
class A { public:
void foo() { bar();}
private:
virtual void bar() { …}
}; class B: public A
{
private: virtual void bar() { …}
};
在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public 或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发 生B::bar()对A::bar()的override不起作用的情况。 这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。 3.2 构造函数和析构函数中的虚函数调用 一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:
class A {
public: A() { foo();} // 在这里,无论如何都是A::foo()被调用!
~A() { foo();} // 同上
virtual void foo();
}; class B: public A
{
public: virtual void foo();
}; void bar()
{
A * a = new B;
delete a;
}
如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。 3.3 多继承中的虚函数 3.4 什么时候使用虚函数 在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。 以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被 产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然 Product类一定也有多态(虚函数)。
另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。 现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。
附:C++中的虚函数和纯虚函数用法
1.虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。 2.虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class) 只有声明而没有定义。 3.虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。 4.虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。 5. 虚函数的定义形式:virtual {method body} ;纯虚函数的定义形式:virtual { } = 0; 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定 (run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。
6.如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。 以下为一个简单的虚函数和纯虚寒数的使用演示,目的是抛砖引玉!
class Virtualbase
{
public:
virtual void Demon()= 0; //prue virtual function
virtual void Base() {cout<<”this is farther class”<}; //sub class
void main() { Virtualbase* inst = new SubVirtual(); //multstate pointer
inst->Demon();
inst->Base(); // inst = new Virtualbase(); // inst->Base()
return ;
}
20个位运算知多少2
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
|