c++中的引用计数
(1) 引子
本文谈一谈C++中的隐式计数。隐式计数是一个计数器,因为他的储存空间没有显示的体现在程序代码中,故称之为“隐式”,而“计数”是说该存储空间的功能。这么一说,你首先想到的可能是C++中的new []和delete []操作符,不错,用new分配一个数组时,正是使用了“隐式计数”,才使得delete该数组指针时,能够获取到数组元素的个数,请看下面的代码,或许你并不陌生:
struct Test
{
Test() { cout < < "Test()" < < endl; }
~Test() { cout < < "~Test()" < < endl; }
};
int main()
{
Test *p = new Test[2];
*((int *)p - 1) = 1;
delete[] p;
return 0;
}
看看输出吧,构造函数被调用两次,析构函数被调用一次(在VC6.0和gcc中均是如此),毫无疑问,(int *)p - 1的位置就是一个计数器,它记录了数组数组元素的个数。
(2) 示例1——string中的隐式计数
我们再来看看下面关于std::string的例子:
int main()
{
string s1("Hello");
string s2 = s1;
char *p = const_cast <char *>(s2.c_str());
p[0] = 'M';
cout < < s1 < < endl;
return 0;
}
上面的程序输出什么?是“Hello”吗?答案是不一定(我在gcc 3.4.6中是Mello)。这实际上取决于string类的实现,如果string采用了copy on write的机制,那么s1和s2实际上共享同一段内存,因此上面的代码也修改了s1的内容。当然,这都是const_cast惹的祸,常规代码对s1和 s2都是能正常工作的。
上面提到了copy on write的共享机制,那么,程序又是如何决定什么时候删除该共享内存呢?答案是引用计数,如果你有兴趣,你可以查看p指针的前4个字节,发现其值为1,而再添加一个“string s3 = s1;”后,其值变为2,毫无疑问,这正是一个引用计数。
3) 示例2——static中的隐式计数
再看一个关于static局部变量的例子。我们知道,如果函数中的static变量在定义时赋初值,那么只有在第一次调用该函数时,初始化才被执行,以后的调用都不再执行。那么,编译器又是怎么实现的呢?答案很简单,就是一个标志位,程序初始化时该标志位为0,函数中初始化相关的代码则演变为:检查标志位,如果是0,则对变量进行初始化,然后将标志位置1,否则跳过初始化步骤。这个标志位实际上就是一个隐式的计数器,虽然它只是一个0-1计数。
知道了编译器的这个“内幕”,你可以用下面的代码轻易的绕过初始化检查,让函数中的static变量在每次被调用时都被初始化(代码有些BT,不适者勿看)。
void Test(int initVal)
{
static int i = initVal;
cout < < i < < endl;
++i;
}
int FindAddress()
{
unsigned char *addr = (unsigned char *)&Test;
// There is only one instruction in Test: jmp realAddr
if (*addr == 0xe9)
{
addr = addr + *(int *)(addr + 1) + 5;
}
// Look forward at most 100 bytes for instruction "and eax 1"
for (int i = 0; i < 64; i++)
{
#ifdef WIN32
if (memcmp(addr + i, "/x83/xe0/x01", 3) == 0)
{
return *(int *)(addr + i - 4);
}
#else
if (addr[i+0] == 0x80 && addr[i+1] == 0x3d && addr[i+6] == 0x00)
{
return *(int *)(addr + i + 2);
}
#endif
}
return 0;
}
int main()
{
cout < < "before modify: " < < endl;
Test(0);
Test(100);
try
{
int flagAddress = FindAddress();
if (flagAddress)
{
cout < < "After modify: " < < endl;
*reinterpret_cast <int *>(flagAddress) = 0;
Test(1000);
}
else
{
cout < < "Can not find the flag address" < < endl;
}
}
catch (...)
{
cout < < "There is some bug in program" < < endl;
}
return 0;
}
代码的讲解我就不说了,注意的是FindAddress函数,其中尝试查找某种特征的指令。
(4) 总结
上面举了几个“隐式计数”的例子,它们可能是编译器为了在程序中实现代码面上的功能,而在其中添加的额外数据结构,也可能是库源码中为你所不熟悉的数据结构(如string中的隐式计数)。了解这些深层次的实现,对查排错误或是提高效率都不无裨益。
很少写文章,写这篇文章的目的,只是希望下次你在用到C++的某个比较“怪异”的特性时,也能有自己的发现, :)。
(1) 引子
本文谈一谈C++中的隐式计数。隐式计数是一个计数器,因为他的储存空间没有显示的体现在程序代码中,故称之为“隐式”,而“计数”是说该存储空间的功能。这么一说,你首先想到的可能是C++中的new []和delete []操作符,不错,用new分配一个数组时,正是使用了“隐式计数”,才使得delete该数组指针时,能够获取到数组元素的个数,请看下面的代码,或许你并不陌生:
struct Test
{
Test() { cout < < "Test()" < < endl; }
~Test() { cout < < "~Test()" < < endl; }
};
int main()
{
Test *p = new Test[2];
*((int *)p - 1) = 1;
delete[] p;
return 0;
}
看看输出吧,构造函数被调用两次,析构函数被调用一次(在VC6.0和gcc中均是如此),毫无疑问,(int *)p - 1的位置就是一个计数器,它记录了数组数组元素的个数。
(2) 示例1——string中的隐式计数
我们再来看看下面关于std::string的例子:
int main()
{
string s1("Hello");
string s2 = s1;
char *p = const_cast <char *>(s2.c_str());
p[0] = 'M';
cout < < s1 < < endl;
return 0;
}
上面的程序输出什么?是“Hello”吗?答案是不一定(我在gcc 3.4.6中是Mello)。这实际上取决于string类的实现,如果string采用了copy on write的机制,那么s1和s2实际上共享同一段内存,因此上面的代码也修改了s1的内容。当然,这都是const_cast惹的祸,常规代码对s1和 s2都是能正常工作的。
上面提到了copy on write的共享机制,那么,程序又是如何决定什么时候删除该共享内存呢?答案是引用计数,如果你有兴趣,你可以查看p指针的前4个字节,发现其值为1,而再添加一个“string s3 = s1;”后,其值变为2,毫无疑问,这正是一个引用计数。
3) 示例2——static中的隐式计数
再看一个关于static局部变量的例子。我们知道,如果函数中的static变量在定义时赋初值,那么只有在第一次调用该函数时,初始化才被执行,以后的调用都不再执行。那么,编译器又是怎么实现的呢?答案很简单,就是一个标志位,程序初始化时该标志位为0,函数中初始化相关的代码则演变为:检查标志位,如果是0,则对变量进行初始化,然后将标志位置1,否则跳过初始化步骤。这个标志位实际上就是一个隐式的计数器,虽然它只是一个0-1计数。
知道了编译器的这个“内幕”,你可以用下面的代码轻易的绕过初始化检查,让函数中的static变量在每次被调用时都被初始化(代码有些BT,不适者勿看)。
void Test(int initVal)
{
static int i = initVal;
cout < < i < < endl;
++i;
}
int FindAddress()
{
unsigned char *addr = (unsigned char *)&Test;
// There is only one instruction in Test: jmp realAddr
if (*addr == 0xe9)
{
addr = addr + *(int *)(addr + 1) + 5;
}
// Look forward at most 100 bytes for instruction "and eax 1"
for (int i = 0; i < 64; i++)
{
#ifdef WIN32
if (memcmp(addr + i, "/x83/xe0/x01", 3) == 0)
{
return *(int *)(addr + i - 4);
}
#else
if (addr[i+0] == 0x80 && addr[i+1] == 0x3d && addr[i+6] == 0x00)
{
return *(int *)(addr + i + 2);
}
#endif
}
return 0;
}
int main()
{
cout < < "before modify: " < < endl;
Test(0);
Test(100);
try
{
int flagAddress = FindAddress();
if (flagAddress)
{
cout < < "After modify: " < < endl;
*reinterpret_cast <int *>(flagAddress) = 0;
Test(1000);
}
else
{
cout < < "Can not find the flag address" < < endl;
}
}
catch (...)
{
cout < < "There is some bug in program" < < endl;
}
return 0;
}
代码的讲解我就不说了,注意的是FindAddress函数,其中尝试查找某种特征的指令。
(4) 总结
上面举了几个“隐式计数”的例子,它们可能是编译器为了在程序中实现代码面上的功能,而在其中添加的额外数据结构,也可能是库源码中为你所不熟悉的数据结构(如string中的隐式计数)。了解这些深层次的实现,对查排错误或是提高效率都不无裨益。
很少写文章,写这篇文章的目的,只是希望下次你在用到C++的某个比较“怪异”的特性时,也能有自己的发现, :)。