新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 计算机科学论坛计算机技术与应用『 C/C++编程思想 』 → C++中的数组和局部静态对象 [转帖] 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 7193 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: C++中的数组和局部静态对象 [转帖] 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     enorm 帅哥哟,离线,有人找我吗?
      
      
      威望:4
      头衔:头衔
      等级:大三暑假(参加全国数模竞赛拿了一等奖)(版主)
      文章:144
      积分:854
      门派:Lilybbs.net
      注册:2005/12/1

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给enorm发送一个短消息 把enorm加入好友 查看enorm的个人资料 搜索enorm在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看enorm的博客楼主
    发贴心情 C++中的数组和局部静态对象 [转帖]

    正像以前我说过的,我已经不下五次“以为”我理解数组了,然而今天又一次发现自己无知。

        初学C的时候我把数组当成指针看。明白了一些实现机制后,我开始把数组当成一个特殊的变量; 我开始察看汇编剖析原理,理解逐步深入。 可是,明白得越多,就会发现越多的特例。 最终 —— 我陷入了看似任意且繁杂的规则的迷宫。

        还是的回到定义上来。C/C++的数组保证两件事:

        1 数组是由同类型成员组成的构造对象,占有一块连续的内存。

        2 它的名称表示它的首地址。

        仅此而已,除此之外 —— 任何内容都不属于数组的定义。

        所以,我们知道,当我们需要数组的时候,我们定义就是了。需要静态数组? C在静态区给你一个数组。 需要自动数组? C在栈上给你一个数组。它还会自动给你初始化:

        char str [] = "It's convenient to use arrays";

        于是栈上有了我们需要的、不折不扣刚好30字节的空间,当我们用str索引他的时候,他已经有需要的内容了。 这就是C/C++。他选取了最精确的定义,而隐藏繁杂的后台。一切都有可能改变,唯有概念不变。


    --------------------------------------------------------------------------------

       ANSI C++98太厚了, 一时还没法学会在里面找到自己要东西。下面是我的一些总结:

        1 首先谈谈函数内定义的普通数组,即auto数组。 auto 数组的的确确实在栈上建立的,且(可以认为)在定义的位置初始化。 所以,若你希望用一个数组实现一个颇大的正弦速查表,推荐还是改用指针 —— 系统切切实实会在栈上初始化一个结结实实的巨大 auto 数组。

         auto数组在离开作用域后,其内存就被回收了 —— 按照C的说法,你不应该继续用它了。这的确就是我们编写程序需要用到的所有知识 —— 不过,若想刨根问底,稍候,我也会谈到这是一个怎么样的内部过程。

        2 static 数组是怎么回事呢?

        C和C++中有很多 static 。 可以说,每种 static 都不是一回事。

         static 数组都存储在静态数据域中。这里要细谈的是局部 static 数组的初始化:
        局部static对象当你第一次用的时候初始化;主程序结束时,所有初始化过的局部static 对象按和构造相反的顺序析构  —— 然后全局对象才开始析构。


    --------------------------------------------------------------------------------

       下面是一个我曾研究过很长时间的一个经典问题,在此剖析一下:

        char* getHello(){
          char str[] = "hello world";
          return str;
        }

        int main(){
          std::cout << getHello() << std::endl;
        }

        这是一个错误的程序,他试图返回 auto 数组,结果是未定义的。虽然在VC7 Release 和DevC4.9下都没问题,但是在VC7 Debug模式下就会输出乱码。

        那么返回auto数组问题到底在哪里呢? 微观的看看整个指令流程罢:

        我希望大家都明白 asm/C/C++ 中函数调用是怎么一回事。函数使用一个统一的栈保存返回地址、参数和 auto 变量。 调用时往栈里堆东西, 返回时则宣布自己的东西都不要了。  正因为 auto 变量是栈上临时分配的, 所以 C/C++ 才支持递归调用 —— 若每个变量都在数据区静态引用, 那么每递归一层,原来的东西就刷掉啦~~~

        首先, getHello 已经知道自己要分配12字节的栈空间给 str。所以getHello 函数开始就把空间留好了 —— 他可能这么做:保存当前栈顶 esp 到 ebp, 并让栈顶 esp 减12(堆栈一般是向“下”增长的)。 str知道自己相对 ebp的偏移, 这都是编译期就算好了的。假如 str 不需要初始化,那么实际上定义它不会有任何额外开销。

        当执行 char str[] = "hello world" 的时候, 函数必须把栈上一片区域正确初始化。于是编译器(如VC)可以这么实现 —— 他预先在数据区放置字串 hello world\0, 然后把它拷到栈 ebp + 对应偏移的位置上。

        最后,函数返回 str 的首地址 —— 这是栈上的地址 —— 并且恢复栈顶 esp 到调用前的位置 —— 于是这段字串就在栈顶之外了。

        现在的问题是,为什么会出现乱码呢? 虽然栈顶 恢复了,但栈上的 "hello world" 仍然没有被删除阿!

        这有点像物理学的测不准原理: 你为了测量一个量,就必须改变它。道理类似 —— 为了输出这个 "hello world", 我们调用了 ostream:: operator<<。 这个函数同样可能(也可能不)使用堆栈。 假如需要的临时变量覆盖了 "hello world" ,就只能看见乱码了。

        用下面的做法,在调用函数前复制那个悬着的字符串,就可以看见正确内容了:

        int main (){
           char str[100], *pStr = str;  // buf

           char *p = getHello();
           // copy p to buf
           while( *p )   
                *pStr++ = *p++;
           *pStr = 0;

         std::cout<< str << std::endl;
        }

        更好的做法是, 直接用一个 const char 引用那个数据区的字串并返回:

        const char* getHello(){
            const char* str = "hello world";
            return str;
        }

        另外说一句, C++并没有规定,数据区一定有个 "hello world" 供 str 初始化,这只是一种可能的实现。我曾经这么认为,不过今天研究函数 和 对象数组时看到一些不同的东西。


    --------------------------------------------------------------------------------

       从数据区直接拷贝内存镜像的确是一种高效的初始化技巧 —— 如果你用 static 修饰数组,甚至可以直接用数据区的原始数据。 然而事情并不总是这么顺人心。在OOP中,我们面临一个麻烦:有些对象必须调用构造函数。

        如下面一个例子:

        class A{
            int _i;
        public:
            explicit A( int n ):_i(n) { cout<<"A("<< _i << ")"<<endl; }
            A( const A& rhs ): _i( rhs._i ) { cout<< "A(const A&"<< _i << " )"<<endl; }
            ~A( ){ cout<<"~A("<< _i << ")"<<endl; }
            A& operator= ( const A& rhs ){ _i = rhs._i; cout<<"A::operator=( const A&"<< _i << " )" <<endl; }
        };

        int main(){
            A a[] = { A(0), A(1), A(2) };
            // 当然,去掉构造函数A::A( int ) 前的 explicit就可以写成:
            // A a[] = { 0, 1, 2 };
        }

        结果并不是三个拷贝构造函数,而是三个普通的构造函数。察看汇编发现,系统以 a[0] a[1] a[2] 地址为 this 分别调用三次构造函数,而和静态区没有任何关系。(不过若是用A a[] = { 0, 1, 2 }; 则会从静态区索引0 \ 1\ 2作为参数 )


        假如我们有一个局部静态数组, 其中包含的是有构造函数的对象:

        static A a[] = { A(0), A(1), A(2) };

        那么它和一般对象数组在实现上有绝对的区别:

        static int nums[] = { 0, 1, 2 };

        后者其实什么都没有作。nums直接表示数据区串 0 1 2 的首地址。而前者,他的数据被分配在数据区,必须以成员对应地址为 this 调用三次 A( int )。

        局部静态对象还有更多奇特的属性。


    --------------------------------------------------------------------------------

      我们知道, C++有这么一句话: 对象的析构遵从和构造相反的顺序。这对是auto 对象和全局对象完全程 此,不过局部静态对象是一个特例。

        全局对象在调用 main 之前初始化, 在退出main之后析构。 局部静态对象则在“某个时刻”初始化一次且仅一次;若他初始化过,就必须且只能在退出时析构。

        那么在何时初始化呢? 唯一的答案是:“在第一次运行到其定义的时候”。 因为构造函数往往有参数 —— 系统不可能在 main 开始之前就确定所有参数。

        这种“运行时”在局部构造,最后又要求在全局析构的模式让我开始好奇 —— 系统如何知道他构造了,它又是如何析构的呢?

        下面的例子测试两个全局变量何时构造,又是按什么顺序析构的:

        void func(){
            int i;
            cin>>i;
            if( i )
               static A a( 0 );
            cin>>i;
            if( i )
               static A a( 1 );
        }

        int main(){
            func();
            func();
            cout<<"-------------"<<endl;
        }

        这个测试中, 流程是否经过 A( 0 ) 和 A( 1 ) 取决于两次调用 func() 的用户四次输入。

        1 输入 0   0  0  0 , 结果:

        -------------

        没有任何对象被构造,也没有任何对象被析构。

        2 当输入 1 1 1 1 时,结果:


        1
        A(0);
        1
        A(1);
        1
        1
        -------------
        ~A(1);
        ~A(0);

        每个静态局部对象只构造了一次,并且在退出后逆序析构。 你可以试试 0 1 1 0 这样的输入,C++仍然知道如何正确逆序析构。

        简直像魔术一样! C++ 到底如何做到这个的呢? 察看汇编可以看见VC的实现:
        1 他对每个局部 static 对象都定义了一个相关的静态变量, 监视其是否初始化过。
        2 他把初始化过的 static 对象丢到一个全局栈中, 以便程序结束时析构。


    Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=597036


       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    天亮了

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/2/22 11:05:00
     
     netfarmer 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:4
      积分:67
      门派:XML.ORG.CN
      注册:2006/2/23

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给netfarmer发送一个短消息 把netfarmer加入好友 查看netfarmer的个人资料 搜索netfarmer在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看netfarmer的博客2
    发贴心情 
    你说的是真的吗?
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/2/24 10:21:00
     
     awdesq 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:1
      积分:58
      门派:XML.ORG.CN
      注册:2006/2/27

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给awdesq发送一个短消息 把awdesq加入好友 查看awdesq的个人资料 搜索awdesq在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看awdesq的博客3
    发贴心情 
    upupuupupupupupupuu
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/2/27 15:44:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/11/26 0:46:41

    本主题贴数3,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    93.018ms