常量字符串存储与销毁引发的误解

来源:互联网 发布:中国乐队那么少 知乎 编辑:程序博客网 时间:2024/05/19 23:16

前言:这是笔者从论坛看到一个网友的探讨帖,觉得有趣,就拿来分析了一下。

原帖地址:http://topic.csdn.net/u/20110302/22/a4df98e6-a15c-41d9-8ae2-b24964fc5ab8.html

这篇文章是探索一些C++编译的知识,内容比较枯燥,适宜真正的程序员来看,并且不能太浮躁,否则,是吸收不了这篇文章的营养的,就不要浪费时间继续往下看了。当然,鉴于笔者水平有限,这篇文章的分析也可能有某些地方有错误,恳请大家留言指教。

附:测试环境 Windows Xp  ;  VS2008

 

奇怪的代码如下:

#include <iostream>
using namespace std;
char* strcpy_(char* strDest,const char* strSrc)
{
   
if (strDest==NULL || strSrc == NULL)
    {
       
return 0;
    }
   
char *strDestCopy = strDest;
   
while ((*strDest++ = *strSrc++) != '/0')
    {
        ;
    }
    cout
<<strSrc<<endl;// adf
    cout<<strDestCopy<<endl;// 123
    return strDestCopy;
}

int main()
{
   
char a[10]="adf";
    strcpy_(a,
"123");
   
return 0;
}

 

说明如下:

这段代码的目的是自己写一个字符串拷贝函数,事实上,也完成了这个功能。

 

奇怪的地方:

在字符串拷贝函数内部,加了两行输出语句,如下:

    cout<<strSrc<<endl;
    cout<<strDestCopy<<endl;

在看下文之前,试着猜一下这里的输出结果吧,如果你能很明确的说出结果和原因,那么,足以说明,你是一个学习非常深入的程序员,喜欢探索,遇到问题,必找到本质的原因,这一行还是很适合你的。

 

正常分析认为:

strSrc经过递增操作,已经从指向常量字符串"123”变更为原strSrc地址后的第4个字符,如果此时输出strSrc的内容,结果应该是不可预期的。

strDestCopy仍指向原strDest的起始位置,而strDest刚刚经过赋值,所以此时输出内容应该为赋值后的值,即字符串“123”。

 

实验验证:

(1)Debug版本

无论如何运行,运行结果总是输出如下:

adf

123

让人奇怪的是,应该为不可预期的strSrc却总是显示输出为“adf”,真是让人觉得非常奇怪。

(2)Release版本

 **(不可预期,每次结果都是一个随机的字符串)

123

 

很明显,Release版本是符合预期的,但是Debug版本却一反常态,与预期不一样,为什么呢?

 

笔者也觉得很奇怪,为什么呢???

 

首先让我们一道来看看这个程序的汇编源码::(希望本文的读者也有着一定的汇编基础,否则可能比较难以理解)

Debug模式的主程序的汇编源码如下 :

char a[10] = "adf";
00401118  mov         eax,dword ptr [string "adf" (404240h)]
0040111D  mov         dword ptr [ebp-14h],eax
//上面两行汇编代码已经将静态存储区中的字符串“adf”拷贝入地址a为首的内存中,其实就是栈

00401120  xor         eax,eax
00401122  mov         dword ptr [ebp-10h],eax
00401125  mov         word ptr [ebp-0Ch],ax
 strcpy_(a, "123");
00401129  push        offset string "123" (40423Ch)
0040112E  lea         eax,[ebp-14h]
00401131  push        eax 
00401132  call        strcpy_ (401000h)
00401137  add         esp,8
 return 0;
0040113A  xor         eax,eax

 

从这段代码中,我们很轻易的就看出了常量字符串“adf”的地址为0x00404240,而常量字符串“123”的地址为0x0040423C,他们都是存储在静态存储区中的,虽然字符串“adf”已经被拷贝到栈中,但是静态存储区中的内存也并未释放。很明显,如果一个指针指向常量字符串“123”,经过递增操作,那么这个指针的值必然是0x0040423C+4,也就是常量字符串“adf”的地址0x00404240,所以,从这里,我们就清楚为什么Debug版本的输出是那样的了。

进入内存查看窗口,验证一下:

0x0040423C  31 32 33 00 61 64 66 00 00 00 00 00 66 00 3a 00 5c 00 64 00 64  123.adf.....f.:./.d.d
0x00404251  00 5c 00 76 00 63 00 74 00 6f 00 6f 00 6c 00 73 00 5c 00 63 00  ./.v.c.t.o.o.l.s./.c.
0x00404266  72 00 74 00 5f 00 62 00 6c 00 64 00 5c 00 73 00 65 00 6c 00 66  r.t._.b.l.d./.s.e.l.f
0x0040427B  00 5f 00 78 00 38 00 36 00 5c 00 63 00 72 00 74 00 5c 00 73 00  ._.x.8.6./.c.r.t./.s.

 

那么为什么会是这样的情况呢?因为常量字符串的分配在静态存储区中,在程序启动时,这些需要定义的常量字符串其实已经在静态存储区中按照一定的规则被分配了内存,而且这些常量字符串不会随着程序的关闭被释放,一般直到下一次被覆盖为止,否则一般不会进行释放,所以,在Debug模式下运行这个程序,输出就是那样一个奇怪的结果了。

 

好了,我们接着分析一下Release模式:

 

 Release模式的主程序的汇编源码如下:

 

 char a[10] = "adf";
0040106E  xor         eax,eax
 strcpy_(a, "123");
00401070  lea         ecx,[esp]
00401073  mov         dword ptr [esp],666461h //在寄存器中定义了字符串"adf",这个字符串的ascII码即:0x66 0x64 0x61,并将这个字符串拷贝入以a为首地址的内存中,其实就是栈,esp是栈顶指针,默认段基址为ss

0040107A  mov         dword ptr [esp+4],eax //给上述字符串补充结尾的0字符
0040107E  mov         word ptr [esp+8],ax
00401083  call        strcpy_ (401000h)
 return 0;

 

从上述汇编代码中,我们可以看出,由于是Release模式,编译器做了一些优化,并没有将这些字符串常量定义到静态存储区中,再将静态存储区中的字符串拷贝进首地址为a的内存中,而是直接将其这些字符串常量放到CPU的寄存器中,然后就进行拷贝,放到首地址为a的内存中。也就是说,整个过程并未让静态存储区参与。

那么,此时我们再往下看,跟踪进入函数:strcpy_,相关汇编源码如下:

char* strcpy_(char* strDest,const char* strSrc)
{
 if (strDest==NULL || strSrc == NULL)
 {
  return 0;
 }
 char *strDestCopy = strDest;
0040100A  mov         esi,ecx
0040100C  lea         esp,[esp]
 while ((*strDest++ = *strSrc++) != '/0')
00401010  mov         al,byte ptr [edx]
00401012  mov         byte ptr [ecx],al
00401014  inc         ecx 
00401015  inc         edx 
00401016  test        al,al
00401018  jne         strcpy_+10h (401010h)
 {
  ;
 }
 cout<<strSrc<<endl;// adf
0040101A  mov         eax,dword ptr [__imp_std::endl (402038h)]
0040101F  mov         ecx,dword ptr [__imp_std::cout (402044h)]
00401025  push        eax 
00401026  push        edx 
00401027  push        ecx 
00401028  call        std::operator<<<std::char_traits<char> > (4011D0h)
0040102D  add         esp,8
00401030  mov         ecx,eax
00401032  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402040h)]
........

}

通过寄存器查看,得知strSrc未递增时的内存地址为:0x00402134,我们来看看这个地址的内存情况:

0x00402134  31 32 33 00 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  123.H................
0x00402149  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  .....................
0x0040215E  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  .....................
0x00402173  00 00 30 40 00 10 22 40 00 03 00 00 00 52 53 44 53 34 67 91 b9  ..0@.."@.....RSDS4g..

很明显,由于一开始的编译器优化,常量字符串“adf”并未定义在静态存储区中,也就是说不会出现在字符串“123”的后面了,所以strSrc经过三次递增后的输出是随机的,不可知的。

 

以上画线部分是对Debug版本和Release版本的分析。

 

总结:

这个问题出现的本质原因在于编译器对堆、栈和静态存储区的使用策略,以及VS开发环境对Debug版本和Release版本的处理方式不同。

Debug版本中,整个程序需要使用的常量字符串都被依次定义在静态存储区中了,于是就出现了这个奇怪的现象。

Release版本运行结果不一样,是因为这个版本并未将所有的常量字符串都依次定义在静态存储区中,如本例中看到的,从CPU寄存器直接拷贝到栈中,而不是先在静态存储区中定义这个字符串,再拷贝到栈中。

 

累了,休息。。。。

 

 

原创粉丝点击