C/C++
C++虚函数表解析(转)update
Jul 29th
update: 今天花了一下午仔细看了这篇文章,却怎么也琢磨不透,因为当中的说法前后存在一些矛盾,后来脑子快崩溃的时候,做了一个大胆的假设: 也许里面有些地方说错了。
果然,当我回头看这篇文章下面的评论的时候,才知道,作者确实某些地方说错了。。。但是,回过头来评价这篇文章,真的是一绝的好文,堪称一绝是无可厚非的。
现在我将其中的错误作出相应的修改,以及矛盾的说明,并以红色字说明。
original:
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,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的实例来得到虚函数表。 下面是实际例程:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << “虚函数表地址:” << (int*)(&b) << endl; //这只是对象实例的地址,而非虚函数表的地址,*(int*)(&b)才是指向虚函数表的指针,也就是虚函数表的地址,这与最前面的括号里面的注明相呼应。
cout << “虚函数表 — 第一个函数地址:” << (int*)*(int*)(&b) << endl; //而这个才是虚函数表的地址,虚函数表的第一个函数地址(函数指针)应该是*(int*)*(int*)(&b),这样下面的函数指针赋值才说得通:pFun = (Fun)*((int*)*(int*)(&b)); ,不然下面的“
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
”
的+0, +1, +2这样的函数指针偏移方式也说不通了。
// 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,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

C++的字符串分割函数
Jul 16th
定义一个数据结构,其中成员有int len, char* internal_buf, char** strings。其中len是被分割后的字符串的个数,internal_buf用于保存原字符串。char** strings是字符串指针(或者你可以看做是指向字符数组的字符串指针,指针指向的类型是字符串,而不是单一的字符,char*strings[]),每个字符串指针用于保存被分割后的每个字符串的首地址。
#define STRING_TERMINATER '\0'
/*必须用完后调用free_sc_exlode_t释放内存,否则内存泄露*/
typedef struct sc_exlode_struct
{
char** strings;
int len;
char* internal_buf;
} sc_exlode_t;
void free_sc_exlode_t(sc_exlode_t *string_array)
{
if(NULL == string_array)
{
return;
}
free(string_array->internal_buf);
free(string_array->strings);
free(string_array);
}
void split_string(char delimiter, char *string ,sc_exlode_t *string_array)
{
int count = 1;
char *pchar, **ptr;
if ( NULL != string_array)
{
memset(string_array, 0, sizeof(sc_exlode_t));
}
if(NULL == string || NULL == string_array)
{
return;
}
string_array->internal_buf = malloc(strlen(string) + 1);
strcpy(string_array->internal_buf,string);
if(NULL == string_array->internal_buf)
{
return;
}
pchar = string;
while(STRING_TERMINATER != *pchar) //计算原字符串可以分割成的字符串个数
{
if (delimiter == *pchar)
{
count++;
}
pchar++;
}
string_array->strings = (char**)malloc(count*sizeof(char*));
if(NULL == string_array->strings)
{
return;
}
string_array->len = count;
ptr = string_array->strings;
*ptr = string_array->internal_buf;
pchar = string_array->internal_buf;
while(STRING_TERMINATER != *pchar)
{
if (delimiter == *pchar) //遇到指定字符时走此分支
{
ptr++;
*ptr = pchar+1;
*pchar = STRING_TERMINATER;
pchar++;
//每个ptr所指向的字符串末尾应该为'\0',
//这样才能结束,所以将';'置为 '\0',
//并让下一个字符串的首个字符地址,
//即*ptr,指向‘;’后面的新字符串
//的首地址。
}
}
int main(void)
{
sc_exlode_t *string_array = malloc(sizeof(sc_exlode_t));
char *ip_range = "10.11.111.0;192.168.0.1;192.168.0.101";
split_string(';', ip_range, string_array);
printf("The spilited result: \n");
for(int i=0;ilen;i++)
{
printf("%s\n",string_array->strings[i]);
}
free_sc_exlode_t(string_array);
return 0;
}
C++/C中指针与数组的差别
Jul 15th
C++/C中指针与数组的差别简述:
C++/C 程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以
为两者是等价的。
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着
(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改
变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来
操作动态内存。指针远比数组灵活,但也更危险。
C/C++宏定义中的## 连接符与# 符
Jun 26th
##连接符号由两个井号组成,其功能是在带参数的宏定义中将两个子串(token)联接起来,从而形成一个新的子串。但它不可以是第一个或者最后一个子串。所谓的子串(token)就是指编译器能够识别的最小语法单元。具体的定义在编译原理里有详尽的解释,但不知道也无所谓。同时值得注意的是#符是把传递过来的参数当成字符串进行替代。下面来看看它们是怎样工作的。这是MSDN上的一个例子。
假设程序中已经定义了这样一个带参数的宏:
#define paster( n ) printf( “token” #n ” = %d”, token##n )
同时又定义了一个整形变量:
int token9 = 9;
现在在主程序中以下面的方式调用这个宏:
paster( 9 );
那么在编译时,上面的这句话被扩展为:
printf( “token” “9″ ” = %d”, token9 );
注意到在这个例子中,paster(9);中的这个”9”被原封不动的当成了一个字符串,与”token”连接在了一起,从而成为了token9。而#n也被”9”所替代。
可想而知,上面程序运行的结果就是在屏幕上打印出token9=9
include的含义(C/C++摘记)
May 13th
最近开始工作了,涉及的是开源C项目,现在正是一个学习过程。本以为信心满满,以为自己C/C++的基础相当扎实,步入工作才发现,菜得不行。
这种菜,是在阅读项目源代码的过程体现出来的。因为实在是看到了太多的陌生的变量声明或函数声明的方式,看得我真的沉迷了,同时也自卑了。
同时也复习了一遍指针,感觉上理解的层次更高了一层,额,应该说往更底层走了,涉及汇编内容了,还有编译一个程序的过程有了新的认识,对于内存的分配有也了更深刻的理解。
虽然让同事说我指针没学好,呵呵,还是虚心接受吧,还好,遇到的是好同事,它们愿意给我讲解,我很高兴。还有一点,我发现,在使用课本上的一些术语与同事进行沟通的时候,发现会互相误解。。。到底是课本太肤浅还是我学得太肤浅导致我的表达不准确呢?随他去吧。
近日,搜罗了很多自己遇到的疑问的回答,并已进行mark,在这里能够进行一些摘抄的笔记,加油!
下面转自一帖子,原址不便放出,希望对大家有所帮助,对于重要话语,我进行摘录,对一些重要句子进行高亮处理。
#include用于在源代码中引用其他的源代码文件
#include有两种写法:
#include <stdio.h> // 使用<>时,编译器仅在系统目录下搜索被引用的文件
#include “stdio.h” // 使用”"时,编译器除了在系统目录下搜索,还会在当前目录下搜索引用的文件
系统目录包括哪些?
一个是编译器内定的目录,这些目录是内建在编译器内部的,不可改变(如果要追究的话,可以看这个文件gcc/gcc/collect2.c),一般为:
/usr/include
/usr/local/include
/usr/lib/gcc-lib/i386-linux/2.95.2/include
/usr/lib/gcc-lib/i386-linux/2.95.2/../../../../include/g++-3
/usr/lib/gcc-lib/i386-linux/2.95.2/../../../../i386-linux/include
可以增加
-nostdinc参数阻止gcc搜索内建路径。
还有一个就是通过-I参数指定的路径
#include的工作原理非常简单,就是将引用文件的内容在#include处展开。
举个例子,有两个文件test.h和main.c
/* test.h */
#ifndef TEST_H
#define TEST_H
#define TEST_STR “This is a test string”
#endif
/* main.c */
#include “test.h”
extern int printf (const char*, …);
int main (int argc, char* argv[])
{
printf(“%s”, TEST_STR);
return 0;
}
那么在编译main.c的时候,#include “test.h”语句被编译器展开,编译器最终看到的main.c应该是这个样子的:
#ifndef TEST_H
#define TEST_H
#define TEST_STR “This is a test string”
#endif
extern int printf (const char*, …);
int main (int argc, char* argv[])
{
printf(“%s”, TEST_STR);
return 0;
}
OK,到这里#include的基本工作原理就清楚了,那么我们为什么要使用#include?
编译器是个傻子,它在碰到不认识的符号的时候就会抱怨,然后罢工。因此,如果我们在源代码中引用了一个外部的对象的时候,会在引用之前增加一个该对象的声明,让编译器先和这个符号熟悉一下,就像extern int printf (const char*, …);
于是,在每个引用到了printf的C文件中,我们都需要加上这么一个声明。一个函数就凑合了,多了怎么办?
对,把这些函数声明都放到stdio.h文件中,这样我只需在每个C文件中写一行#include <stdio.h>,就等同于声明了n个函数。这个偷懒的方法真不错!
由于include可以让我们少写很多代码,这样大家都喜欢include,不光在c文件中include,还在h文件中include。include是可以递归的。这样有时貌似只引用了一个文件,但其实引用了很多个文件。编译器貌似在编译一个十几行代码的C文件,但由于C文件中的头文件被展开,编译器实际上是在编译一个几千行的源文件,编译器真累的不行。所以大家在写源代码的时候务必也照顾一下编译器的感受,不需要的头文件就不要引用了。
同时,由于include可以递归,我们在包含一个头文件的时候往往不能判断出到底包含了那些文件,文件的重复包含无法避免。重复包含头文件是有害的,有时候编译器会向你抱怨某个数据类型或数据结构被重复定义。为了避免头文件被重复包含,我们往往使用下面的技巧:
#ifndef TEST_H
#define TEST_H
/* your file goes here */
#endif
我们看看它的工作原理,如果代码如下
#include “test.h”
#include “test.h”
extern int printf (const char*, …);
int main (int argc, char* argv[])
{
printf(“%s”, TEST_STR);
return 0;
}
那么编译时会被展开为:
#ifndef TEST_H /* 这里TEST_H还没有被define,条件为true,下面的代码会被编译到 */
#define TEST_H /* 这里define了,TEST_H */
#define TEST_STR “This is a test string”
#endif /* #ifndef TEST_H 条件结束 */
#ifndef TEST_H /* 这里TEST_H在上面被define,条件为false,下面的代码会被跳过 */
#define TEST_H /* 这里不会被编译 */
#define TEST_STR “This is a test string” /* 这里不会被编译 */
#endif /* 这里不会被编译, #ifndef TEST_H 条件结束 */
extern int printf (const char*, …);
int main (int argc, char* argv[])
{
printf(“%s”, TEST_STR);
return 0;
}
OK,最后我们来说说c文件和h文件。
对编译器来说,它并不区分c文件和h文件,编译器会一视同仁的认为是源代码文件,即包含源代码的文本文件。
程序员们则约定,将编译器编译的对象命名为.c文件,将上面说的,用来偷懒给别人引用的文件命名为.h文件。
由于h文件是用来被引用的,且往往不能确定被多少人引用,因此,我们在h文件中放的是不会参数数据实体的,比如声明和宏。而产生数据实体的定义则被放在c文件中。试想,如果我们在一个h文件中定义一个变量a,那么凡是引用了这个h文件的c文件中都会定义一个a,编译器到无所谓,为每个c编译出来的o文件分配一个a,而链接器却会向你抱怨a太多了,都不知道用哪个好。