• UNICODE编程资料(转贴)

                                          

     1. 如何取得一个既包含单字节字符又包含双字节字符的字符串的字符个数?
    可以调用Microsoft Visual C++的运行期库包含函数_mbslen来操作多字节(既包括单字节也包括双字节)字符串。
    调用strlen函数,无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。

    2. 如何对DBCS(双字节字符集)字符串进行操作?
    函数 描述
    PTSTR CharNext ( LPCTSTR ); 返回字符串中下一个字符的地址
    PTSTR CharPrev ( LPCTSTR, LPCTSTR ); 返回字符串中上一个字符的地址
    BOOL IsDBCSLeadByte( BYTE ); 如果该字节是DBCS字符的第一个字节,则返回非0值

    3. 为什么要使用Unicode?

    (1) 可以很容易地在不同语言之间进行数据交换。
    (2) 使你能够分配支持所有语言的单个二进制.exe文件或DLL文件。
    (3) 提高应用程序的运行效率。
    Windows 2000是使用Unicode从头进行开发的,如果调用任何一个Windows函数并给它传递一个ANSI字符串,那么系统首先要将字符串转换成 Unicode,然后将Unicode字符串传递给操作系统。如果希望函数返回ANSI字符串,系统就会首先将Unicode字符串转换成ANSI字符串,然后将结果返回给你的应用程序。进行这些字符串的转换需要占用系统的时间和内存。通过从头开始用Unicode来开发应用程序,就能够使你的应用程序更加有效地运行。
    Windows CE 本身就是使用Unicode的一种操作系统,完全不支持ANSI Windows函数
    Windows 98 只支持ANSI,只能为ANSI开发应用程序。
    Microsoft公司将COM从16位Windows转换成Win32时,公司决定需要字符串的所有COM接口方法都只能接受Unicode字符串。

    4. 如何编写Unicode源代码?

    Microsoft 公司为Unicode设计了WindowsAPI,这样,可以尽量减少代码的影响。实际上,可以编写单个源代码文件,以便使用或者不使用Unicode来对它进行编译。只需要定义两个宏(UNICODE和_UNICODE),就可以修改然后重新编译该源文件。
    _UNICODE宏用于C运行期头文件,而UNICODE宏则用于Windows头文件。当编译源代码模块时,通常必须同时定义这两个宏。

    5. Windows定义的Unicode数据类型有哪些?

    数据类型 说明
    WCHAR Unicode字符
    PWSTR 指向Unicode字符串的指针
    PCWSTR 指向一个恒定的Unicode字符串的指针
    对应的ANSI数据类型为CHAR,LPSTR和LPCSTR。
    ANSI/Unicode通用数据类型为TCHAR,PTSTR,LPCTSTR。

    6. 如何对Unicode进行操作?

    字符集 特性 实例
    ANSI 操作函数以str开头 strcpy
    Unicode 操作函数以wcs开头 wcscpy
    MBCS 操作函数以_mbs开头 _mbscpy
    ANSI/Unicode 操作函数以_tcs开头 _tcscpy(C运行期库)
    ANSI/Unicode 操作函数以lstr开头 lstrcpy(Windows函数)
    所有新的和未过时的函数在Windows2000中都同时拥有ANSI和Unicode两个版本。ANSI版本函数结尾以A表示;Unicode版本函数结尾以W表示。Windows会如下定义:
    #ifdef UNICODE
    #define CreateWindowEx CreateWindowExW
    #else
    #define CreateWindowEx CreateWindowExA
    #endif // !UNICODE

    7. 如何表示Unicode字符串常量?

    字符集 实例
    ANSI "string"
    Unicode L"string"
    ANSI/Unicode T("string")或_TEXT("string")if( szError[0] == _TEXT(‘J') ){ }

    8. 为什么应当尽量使用操作系统函数?

    这将有助于稍稍提高应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程Explorer.exe所使用。由于这些函数使用得很多,因此,在应用程序运行时,它们可能已经被装入RAM。
    如:StrCat,StrChr,StrCmp和StrCpy等。

    9. 如何编写符合ANSI和Unicode的应用程序?

    (1) 将文本串视为字符数组,而不是chars数组或字节数组。
    (2) 将通用数据类型(如TCHAR和PTSTR)用于文本字符和字符串。
    (3) 将显式数据类型(如BYTE和PBYTE)用于字节、字节指针和数据缓存。
    (4) 将TEXT宏用于原义字符和字符串。
    (5) 执行全局性替换(例如用PTSTR替换PSTR)。
    (6)修改字符串运算问题。例如函数通常希望在字符中传递一个缓存的大小,而不是字节。这意味着不应该传递sizeof(szBuffer),而应该传递(sizeof(szBuffer)/sizeof(TCHAR)。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用
    malloc(nCharacters *sizeof(TCHAR)),而不是调用malloc(nCharacters)。

    10. 如何对字符串进行有选择的比较?

    通过调用CompareString来实现。
    标志 含义
    NORM_IGNORECASE 忽略字母的大小写
    NORM_IGNOREKANATYPE 不区分平假名与片假名字符
    NORM_IGNORENONSPACE 忽略无间隔字符
    NORM_IGNORESYMBOLS 忽略符号
    NORM_IGNOREWIDTH 不区分单字节字符与作为双字节字符的同一个字符
    SORT_STRINGSORT 将标点符号作为普通符号来处理

    11. 如何判断一个文本文件是ANSI还是Unicode?

    判断如果文本文件的开头两个字节是0xFF和0xFE,那么就是Unicode,否则是ANSI。

    12. 如何判断一段字符串是ANSI还是Unicode?

    用IsTextUnicode进行判断。IsTextUnicode使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此 IsTextUnicode有可能返回不正确的结果。

    13. 如何在Unicode与ANSI之间转换字符串?

    Windows函数MultiByteToWideChar用于将多字节字符串转换成宽字符串;函数WideCharToMultiByte将宽字符串转换成等价的多字节字符串。

  • "顺,不妄喜;逆,不惶馁;安,不奢逸;危,不惊惧;胸有激雷而面如平湖者,可拜上将军"
  • 李阳的一三五法(发音、口语、听力同时突破)

    1. 大量收集TOFEL听力全真试题。
    2. 查字典、注音标。
    3. 看中文并口泽成中文。
    4. 反复听并体会五大发音秘诀语调和停顿。
    5. 三最法(最快速、最清晰、最大声)地读并模彷多次。
    6. 一口气读。
    7. 流利、自然地复述。

    用这个方法时注意:

    1.英语发音不准的人,是无法体会五大发音秘诀,这不能单靠反复模彷就能突破的,因为有些人连辨音和修正能力也没有,我就是这样的一个人。最好有一位教师帮你一一修正。请参考世界知识出版社出版社,<新东方学校出国考试丛书――听力的弦外之音>。这里面有很详细讲述五大发音秘诀、语调起伏、语气和音变等问题。
    2.三最法中最快速和一口气读容易忽略语调和停顿。例如:下雨天留客天天留我不留。把它很读得很快是没有人知道你说什么。最大声很容易损坏嗓子。
    3.必须想像语言环境。
    4.我用这个方法后,变得有点狂,目空一切,这不利与人交流。

    改进方法:

    1.最清晰、从慢到快地反复模彷并注意语调、停顿和五大发音秘诀。
    2.两个人反复对话并不断改造对话内容。这样练出来的效果会比大喊的效果来得更自然、更流利、更富感情。
    3.记住:一山还有一山高。

    钟道隆的逆向法(语音、语法、听力、口语同时突破)

    这法是针对新闻听力。

    1.购买新闻听力教材BBC、VOA 、CNN 或SPECIAL ENGLISH
    2.利用复读机,不许翻书,把每篇文章听懂。
    3.逐句把原文听写出来。
    4.对比原文、分析错误(语音、拼写、词汇、语法等)
    5.将错误听出来。
    6.边听边译成中文,并与译文对比。
    7.将单词、短语、设法反复将其听懂。
    8.模彷。
    9.不看书,用新学的单词复述新闻内容。

    用这个方法时注意:

    1.这个方法很费时,但很快见效(三天左右,但要每天练习十小时以上),一定要有耐心。
    2.平常要多看英文报纸、多听中英文广播。
    3.这个方法能有效地提高你的辨音能力,特别是对连读、略读、动词第三身、过去分词、名词复读。
    4.复读机最好是买步步高的BK-782,保真效果很理想。
    5.注意新闻用词,写作特点,可参考钟道隆的<逆向法巧学英语>一书。

    <学习的革命>一书中的磁带辅导阅读方案(听力、阅读同时突破)

    1. 使用中山大学出版的CRAZY ENGLISH。
    2. 边看书,边听边阅读。
    3. 查字典,(单词、短语、习语)并注在书本上。
    4. 反复边看书,边听边阅读。
    5. 边看中译,边听磁带。

    这个方法对四级、初、中级水平或语感不好的人有极大的帮助,能在八周内提高一年半的阅读水平。还有<同伴指导原则>和<音乐辅导方案>,这是两个很有创新性的方法,能在数周提高一到两年的水平,=缺点是没有这个条件。

    以上的方法是在传统教育下学了十几年英文了, 还是没学好,还是不能用英文自由交流的“哑吧们“的灵丹妙药。

    以下是我对英语的一些促成方法。

    原则:

    1. 学外语不用Step by step, 是可以跑的。老师们所说的一步一步地学,是指要达到文学欣赏、创作等境界。我所说可以跑的,是指要达到普通的听说读写的技能。
    2. 练听必练说,练读必练写。语言能力是听说阅写,发音,语法,语气等的集合,是不可分割的。
    3. 语言必须是与人共享的。

    想一想你还是小孩时,你是怎样学母语的,是不是一开始你爸妈就给你讲解语法,强迫你做语法练习?当然不是,而是先听说,后读写。
    语感是来自听觉的,但当中涉及一个辨音能力的问题,即所听到的与原来别人发音的差距,你无法完全知道自已的辨音是否对的,因此你必需把它说出来,人家听懂你的话,代表你的辨音正确,同时可以避免中式英文,多听收音广播,多看原版电影、电视,多听TOEFL真题,多和老外聊天。老外说什么,你就跟着说什么,就像鹦鹉一样。注意的是听说是同时进行的,就像小时候你爸妈教你说话一样。
    不知小时候上学时,老师是否强迫你们背了很多中文单词?否则为什么今天很多人老是拿着英语单词书硬哽下去。当中很多人忽略了理解能力的重要性,误以为单词是阅读的核心,试想想,一个英语单词可能有数十个中文解释,你可否单靠记忆或运气就挑对解释?阅读能力是需要长时间的培养,並不是整天泡在单词书里就可以达到的,挑选合适的书,例如原版的专业书,报纸,杂志,试着直接从这些书报中获取信息与知识,这是你的理解能力最好的训练,但挑书有一个原则,就是 “由浅入深,循序渐进”。很多人在阅读时找不住文章的核心内容、主旨、哪些是重要的、哪些是可弃不读的。练习写作就可以帮你解决这一问题,写文章时,你必须考虑到文章的布局、文体、中心思想等问题,这不就正是阅读所碰到的问题吗?因此你的理解能力和阅读速度就大大提高,因为你已懂得找重点和分辨出哪些是可弃不读。要想在英语有更多的长进,读写是不可少的。

    至于语法,连读等语音问题其实是不用刻意去学的,只要你在练习听说读写时多加注意,适当时候强化一下就成了。

    步骤:

    1. 找一个语音很好的人,给你一一改正你的糟透的发音,我当时用了三个月的时音改正发音。
    2. 用李阳的方法,突破开口难这一关。
    3. 改良了的方法,除了上述所说的,还有
    (1)读故事,然后讲故事。
    (2)看见什么东西,就用英语自言自语说个不停。
    (3)找一个partner,用英语跟她胡说八道。
    (4)参加英语演讲比赛。等等。
    4. 准备考四级而疯狂的做了一个多月题,迅速突破语法和阅读。
    5. 用了磁带辅导方案半年,和读了15本简易本小说,不做一题模拟题,轻松考过六级。
    6. 看了很多China daily, 21st century.
    7. 大三时,开始帮同班同学补四、六级,我把有关学英语的方法的书通读一篇,又向外语专业的学生、老师请教,思维上飞跃的突破。
    8.决心参加写作比赛,利用电脑软件,着迷900英语系列――读写通,每星期坚持写一篇,并找外语专业的同学修改,然后再过两三天后,自己再作第二次修改,再找老师或老外修改第三篇。或有时候模彷范文,先写再对照,后背范文。开始时写150字,后来写400-1000字的文章。半年内达到外语专业学生的大三水平。英语到了这阶段,好像停滞不前。反思数天,原因有:
    (1) 阅读量不够
    (2) 从小到大,只依赖字典中的中文解释,使对在不同的文章中单词的理解有误差。
    (3) 以前Chinadaily, 21st century读多,反而看不懂国外的报纸,因为写稿的人大多是英语专业的人,他们看了很多的文学原著,相对国外的报纸、杂志,由于没有多大的机会接触,使他们的文章用词过份大词小用、死拼硬溱、不准。句子千篇一律,刻意造成像英语那样“多枝共干“即一个句了,共有一个主语或宾语,中间加进了定语、状语从句等类似情况。使文章生硬、表达不清。
    (4) 大一大二时完全忽略语法,语法忘了七七八八。
    (5) 电影英语的对白并不是如我们所发的音一样,虽然用词很简单,但那些语气语调,连读音变,让我大吃一惊。
    9. 探索了一段时间,从<学习的革命>一书拿来的idea,采用了以下的新方法:
    (1) 背单词,买了ARCO公司的preparation for the TOEFL CD-ROM. 里面有350条TOEFL常用的词汇,而且全部是英英解释,各条词汇都配了例句和纯正的美国发音。反复背诵模彷后,再通过光盘里的两个单词游戏来强化我的记忆。这使我以后可以不依赖中文解释。
    (2) 强化阅读:先用钟道隆的逆向法三天,跟着从 www.yahoo.com 收集了大量的新闻,包括World, Business, Science, Tech, Politics. 以三天为一单位,三天内只读同一类新闻,如world. 必须使用Microsoft 的Bookshelf99 和金山词霸3。当遇到新单词,用Bookshelf99找出英英解释,并把这解释朗读数遍,再用金山词霸把单词的发音读出和了解一下中文解释。查字典的时间在一秒左右,这可大大增加阅读的速度和兴趣。再www.abcnews.go.com 寻找并观看即时在线新闻。这样就可以把地名、人名等专有名词的听力完全突破。把自已当成新闻报导员,用刚才所学的英语单词、短句、习语,用自已的语言作新闻报导。有空还可以自已写社论,并从internet里找一些社论,与自已的作个对比。这是一个配合电脑,听说读写完全突破的方法。
    (3)用改良了李阳的方法,大量收集全真TOEFL听力试题,并疯狂突破。但使用疯狂方法不能因而变狂,自以为是,否则外语专业的高人前辈不会给你指导。(英语听力突破掌上宝,和TOEFL的模拟题不能用作练作材料,因为这类书的录音磁带忽略了该场境对话应有的语气和感情。)
    (4) ARCO公司的preparation for the TOEFL CD-ROM里有大量的语法训练和详细的解释,把这些练习完成后,语法又过了关。
    (5) 电影英语:把中山大学出版的<CRAZY ENGLISH>和其出版的电影英语对白系列,经过边看原文,边听,对照中文解释,模彷,背诵精采对白等步骤。确定那些语气语调,连读音变的句子你是无法听懂的,跑到外语系找老师、老外帮你听一听,并跟他们学到底是如何发音的。平常还要经常看英语电视节目(如Start TV, Start Sport, VCD等),用在电视学来的东西,到学校里的外语角跟老师、老外、其他高人前辈谈天说笑。一旦突破了英语节目的听力,你的英语就如鱼得水。
    10. 今年大四,跟着就是去挑战TOEFL和英语专业八级的考试,阅读原版的专业书,以英语来学习。大四下学期,用以上所述的方法,突破日语、德语(我想只是皮毛而已)。

    这方法关键是要能形成一个学习团体,与人共学,互相促成,一个人是无法成功的,两三个人一起听新闻,然后互相补充,以英语说出来,写作互相批改。并须配合电脑、SOFTWARE、INTERNET,电视,VCD,复读机,书本教材和老师、老外、高人前辈的指点等,才能在有限的时间内促成英语的听说读写的基本技能,学英语其实只要两三年的时间就可以,一般人要达到精通听说读写只需4000学时,为什么却要我们苦学十多年却不得其道?

  • 2005-11-24

    非常好的blog

    http://BugEyes.blog.edu.cn

  • 突然对lisp很感兴趣,发誓要剥去她神秘的外衣(in short time)

    google了一下,先把相关网站记录一下,呵呵.

    中文blog  http://blog.csdn.net/windoze/

    http://daiyuwen.freeshell.org/gb/lisp.html

    foreigner: http://www.paulgraham.com/lisp.html

  • // silent.cpp : Defines the entry point for the console application.
    //

    #include "stdafx.h"


    int main(int argc, char* argv[])
    {
     STARTUPINFO si;
     PROCESS_INFORMATION pi;
        char temp[100];
     ZeroMemory(temp,100);
     ZeroMemory( &si, sizeof(si) );
        si.cb = sizeof(si);
        ZeroMemory( &pi, sizeof(pi) );
     if (argc<2)
      return 1;

     for ( int i=1;i<argc;i++)
     {
       
        strcat(temp,argv[i]);
        strcat(temp," ");
     }
        printf("argv is %s\n",temp);
     CreateProcess(NULL,temp,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
     DWORD x=GetLastError();

     DEBUG_EVENT DebugEv;                   // debugging event information
     DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation
    // WaitForSingleObject(pi.hProcess,INFINITE);
     Sleep(5000);
     DebugActiveProcess(pi.dwProcessId);
    #if 1
     for(;;)
     {
     
     // Wait for a debugging event to occur. The second parameter indicates
     // that the function does not return until a debugging event occurs.
     
      WaitForDebugEvent(&DebugEv, INFINITE);
      dwContinueStatus = DBG_CONTINUE;
     // Process the debugging event code.
     
      switch (DebugEv.dwDebugEventCode)
      {
       case EXCEPTION_DEBUG_EVENT:
       // Process the exception code. When handling
       // exceptions, remember to set the continuation
       // status parameter (dwContinueStatus). This value
       // is used by the ContinueDebugEvent function.
     
        switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode)
        {
         case EXCEPTION_ACCESS_VIOLATION:
         // First chance: Pass this on to the system.
         // Last chance: Display an appropriate error.
    //      printf("%d 0x%x Access Violation!\n",DebugEv.u.Exception.ExceptionRecord.ExceptionInformation[0],DebugEv.u.Exception.ExceptionRecord.ExceptionInformation[1]);

    //这里行为????????
          dwContinueStatus=DBG_EXCEPTION_NOT_HANDLED;
          if(DebugEv.u.Exception.dwFirstChance==0 )
          {
          printf("%d 0x%x Access Violation!\n",DebugEv.u.Exception.ExceptionRecord.ExceptionInformation[0],DebugEv.u.Exception.ExceptionRecord.ExceptionInformation[1]);
           exit(0);
          }

          break;
     
         case EXCEPTION_BREAKPOINT:
         // First chance: Display the current
         // instruction and register values.
          break;
     
         case EXCEPTION_DATATYPE_MISALIGNMENT:
         // First chance: Pass this on to the system.
         // Last chance: Display an appropriate error.
          break;
     
         case EXCEPTION_SINGLE_STEP:
         // First chance: Update the display of the
         // current instruction and register values.
          break;
     
         case DBG_CONTROL_C:
         // First chance: Pass this on to the system.
         // Last chance: Display an appropriate error.
          break;
     
         default:
         // Handle other exceptions.
          break;
        }
     
       case CREATE_THREAD_DEBUG_EVENT:
       // As needed, examine or change the thread's registers
       // with the GetThreadContext and SetThreadContext functions;
       // and suspend and resume thread execution with the
       // SuspendThread and ResumeThread functions.
        break;

       case CREATE_PROCESS_DEBUG_EVENT:
       // As needed, examine or change the registers of the
       // process's initial thread with the GetThreadContext and
       // SetThreadContext functions; read from and write to the
       // process's virtual memory with the ReadProcessMemory and
       // WriteProcessMemory functions; and suspend and resume
       // thread execution with the SuspendThread and ResumeThread
       // functions. Be sure to close the handle to the process image
       // file with CloseHandle.
        break;
     
       case EXIT_THREAD_DEBUG_EVENT:
       // Display the thread's exit code.
        break;
     
       case EXIT_PROCESS_DEBUG_EVENT:
       // Display the process's exit code.
        exit(0);
        break;
     
       case LOAD_DLL_DEBUG_EVENT:
       // Read the debugging information included in the newly
       // loaded DLL. Be sure to close the handle to the loaded DLL
       // with CloseHandle.
        break;
     
       case UNLOAD_DLL_DEBUG_EVENT:
       // Display a message that the DLL has been unloaded.
        break;
     
       case OUTPUT_DEBUG_STRING_EVENT:
       // Display the output debugging string.
        break;
     
      }
     
     // Resume executing the thread that reported the debugging event.
     
     ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
     
     }
    #endif
     return 0;
    }

  • C++中的虚函数(virtual function)
    1.简介
        虚函数是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约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
    2. 虚函数的语法
        虚函数的标志是“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. 虚函数使用技巧 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<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。

        现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。

    4.参考资料
    [1] 深度探索C++对象模型,Stanley B.Lippman,侯捷译

    [2] Design Patterns, Elements of Reusable Object-Oriented Software, GOF

  • 对函数调用方式的研究


    函数的调用约定主要是声明以下信息:
    ?    参数的入栈顺序;
    ?    校正栈顶者;
    ?    参数传递方式;
    ?    可否不定参数等。

    在 VC++6.0中支持__stdcall、__cdecl、pascal、__fastcall等调用约定,在这里我们研究一下这几种调用约定的具体行为。

    编译环境:[VC++6.0]
    编译方式:[DEBUG]
    代码如下:

    // TestCall.cpp : Defines the entry point for the console application.
    //

    #include "stdafx.h"

    int __stdcall test_stdcall(char para1, char para2)
    {
        para1 = para2;
        return 0;

    }

    int __cdecl test_cdecl(char para, ...)
    {
        char    p = '\n';

        va_list marker;

        va_start( marker, para );
       
     
        while( p != '\0' )
        {
            p = va_arg( marker, char);
            printf("%c\n", p);

        }

        va_end( marker );

        return 0;

    }

    int pascal test_pascal(char para1, char para2)
    {
        return 0;

    }

    int __fastcall test_fastcall(char para1, char para2, char para3, char para4)
    {
        para1 = (char)1;
        para2 = (char)2;
        para3 = (char)3;
        para4 = (char)4;

        return 0;

    }
    __declspec(naked) void __stdcall test_naked(char para1, char para2)
    {
        __asm
        {
            push    ebp
            mov     ebp, esp

            push    eax

            mov     al,byte ptr [ebp + 0Ch]
            xchg    byte ptr [ebp + 8],al
           
            pop     eax
            pop     ebp
            ret     8
        }

    //    return ;

    }
    struct sa{
      double a;
      char b;
      int  c;
     // char d;
      int f;
     
    };
    struct bita
    {
     int a:8;
     int b:8;
     int c:8;
     int d:8;
    };
    int main(int argc, char* argv[])
    {
     sa sa;
     sa.a=1.0;
     sa.b='A';
        sa.c=4;
    // sa.d='D';
     sa.f=5;

     printf("sa is %d\n",sizeof(sa));
        bita bita;
     bita.a=1;
     bita.b=2;
     bita.c=3;
     bita.d=4;
     test_stdcall( 'a', 'b' );

        test_cdecl('c','d','e','f','g' ,'h' ,'\0');

        test_pascal( 'e', 'f' );

        test_fastcall( 'g', 'h', 'i', 'j' );

        test_naked( 'k', 'l');

        return 0;
    }

    ;*********************************************************************************************************

    首先调用的是test_stdcall( \'a\', \'b\' ),这个函数被声明为 __stdcall 的调用方式,其调用代码如下:

    00401138   |.  6A 62                push 62     ;   \'b\'
    0040113A   |.  6A 61                push 61     ;   \'a\'
    0040113C   |.  E8 D3FEFFFF          call TestCall.00401014

    在这里可以清楚地看到,__stdcall的参数传递使用栈方式,入栈顺序是先 \'b\' 后 \'a\' ,所以入栈顺序是从右往左。
    其返回值用 eax 传递。

    然后看看函数体内的代码:

    00401050   /> \\55                   push ebp
    00401051   |.  8BEC                 mov ebp,esp
    00401053   |.  83EC 40              sub esp,40
    00401056   |.  53                   push ebx
    00401057   |.  56                   push esi
    00401058   |.  57                   push edi
    00401059   |.  8D7D C0              lea edi,dword ptr ss:[ebp-40]
    0040105C   |.  B9 10000000          mov ecx,10
    00401061   |.  B8 CCCCCCCC          mov eax,CCCCCCCC
    00401066   |.  F3:AB                rep stos dword ptr es:[edi] ; 以上是保存现场和分配局部变量空间

    00401068   |.  8A45 0C              mov al,byte ptr ss:[ebp+C]
    0040106B   |.  8845 08              mov byte ptr ss:[ebp+8],al  ; para1 = para2;

    0040106E   |.  33C0                 xor eax,eax                 ; return 0;

    00401070   |.  5F                   pop edi
    00401071   |.  5E                   pop esi
    00401072   |.  5B                   pop ebx
    00401073   |.  8BE5                 mov esp,ebp
    00401075   |.  5D                   pop ebp                     ; 恢复现场
    00401076   \\.  C2 0800              retn 8                      ; 校正栈顶

    __stdcall在函数体内完成了栈顶的校正。

    ;*********************************************************************************************************


    然后调用了test_cdecl( \'c\',\'d\' ),这个函数被声明为 __cdecl 的调用方式,其调用代码如下:
    00401141   |.  6A 64                push 64
    00401143   |.  6A 63                push 63
    00401145   |.  E8 BBFEFFFF          call TestCall.00401005

    一样, __cdecl 的参数传递使用栈方式,入栈顺序是从右往左。
    其返回值用 eax 传递。
    不过 __cdecl 方式有个特点,他支持可变的参数个数。

    来看看函数体内的代码:

    0040B6A0   /> \\55                   push ebp
    0040B6A1   |.  8BEC                 mov ebp,esp
    0040B6A3   |.  83EC 48              sub esp,48
    0040B6A6   |.  53                   push ebx
    0040B6A7   |.  56                   push esi
    0040B6A8   |.  57                   push edi
    0040B6A9   |.  8D7D B8              lea edi,dword ptr ss:[ebp-48]
    0040B6AC   |.  B9 12000000          mov ecx,12
    0040B6B1   |.  B8 CCCCCCCC          mov eax,CCCCCCCC
    0040B6B6   |.  F3:AB                rep stos dword ptr es:[edi]     ; 以上是保存现场和分配局部变量空间

                                        ; char    p = \'\\n\';
    0040B6B8   |.  C645 FC 0A           mov byte ptr ss:[ebp-4],0A

                                        ; va_start( marker, para );
    0040B6BC   |.  8D45 0C              lea eax,dword ptr ss:[ebp+C]
    0040B6BF   |.  8945 F8              mov dword ptr ss:[ebp-8],eax

                                        ; while( p != \'\\0\' )
    0040B6C2   |>  0FBE4D FC            /movsx ecx,byte ptr ss:[ebp-4]
    0040B6C6   |.  85C9                 |test ecx,ecx
    0040B6C8   |.  74 26                |je short TestCall.0040B6F0
                                        |
                                        |; p = va_arg( marker, char) // 返回当前参数,并使参数指针指向下一个参数
    0040B6CA   |.  8B55 F8              |mov edx,dword ptr ss:[ebp-8]
    0040B6CD   |.  83C2 04              |add edx,4
    0040B6D0   |.  8955 F8              |mov dword ptr ss:[ebp-8],edx
    0040B6D3   |.  8B45 F8              |mov eax,dword ptr ss:[ebp-8]
    0040B6D6   |.  8A48 FC              |mov cl,byte ptr ds:[eax-4]
    0040B6D9   |.  884D FC              |mov byte ptr ss:[ebp-4],cl
                                        |
                                        |; printf("%c\\n", p);
    0040B6DC   |.  0FBE55 FC            |movsx edx,byte ptr ss:[ebp-4]
    0040B6E0   |.  52                   |push edx                                      ; /Arg2
    0040B6E1   |.  68 0CF14100          |push TestCall.0041F10C                        ; |Arg1 = 0041F10C ASCII "%c
    "

    0040B6E6   |.  E8 45020000          |call TestCall.0040B930                        ; \\TestCall.0040B930
                                        |
    0040B6EB   |.  83C4 08              |add esp,8  ; printf 声明为 __cdecl , 由调用者校正栈顶
    0040B6EE   |.^ EB D2                \\jmp short TestCall.0040B6C2

    0040B6F0   |>  C745 F8 00000000     mov dword ptr ss:[ebp-8],0      ; va_end( marker );

    0040B6F7   |.  33C0                 xor eax,eax ; return 0;

    0040B6F9   |.  5F                   pop edi
    0040B6FA   |.  5E                   pop esi
    0040B6FB   |.  5B                   pop ebx
    0040B6FC   |.  83C4 48              add esp,48
    0040B6FF   |.  3BEC                 cmp ebp,esp
    0040B701   |.  E8 7A5AFFFF          call TestCall.00401180
    0040B706   |.  8BE5                 mov esp,ebp
    0040B708   |.  5D                   pop ebp     ; 恢复现场
    0040B709   \\.  C3                   retn


    由于支持可变的参数个数,函数无法校正栈顶,所以这个活就留给了调用者:
    0040114A   |.  83C4 08              add esp,8   ; 校正栈顶

    可变参数的使用很危险,如果不知道怎样结束的话,va_arg宏会执行到出现内存访问错误为止,
    而且对参数的类型控制和识别也很麻烦。

    ;*********************************************************************************************************


    看看test_pascal( \'e\', \'f\' ),这个函数被声明为 pascal 的调用方式,其调用代码如下:
    0040114D   |.  6A 66                push 66
    0040114F   |.  6A 65                push 65
    00401151   |.  E8 B9FEFFFF          call TestCall.0040100F

    和 __stdcall 一样, pascal 的参数传递使用栈方式,入栈顺序是从右往左,其返回值用 eax 传递。

    按照 MASM32 的约定来看, pascal 的参数入栈顺序是从左往右才对啊,奇怪。

    然后看看函数体内的代码:

    004010B0   /> \\55                   push ebp
    004010B1   |.  8BEC                 mov ebp,esp
    004010B3   |.  83EC 40              sub esp,40
    004010B6   |.  53                   push ebx
    004010B7   |.  56                   push esi
    004010B8   |.  57                   push edi
    004010B9   |.  8D7D C0              lea edi,dword ptr ss:[ebp-40]
    004010BC   |.  B9 10000000          mov ecx,10
    004010C1   |.  B8 CCCCCCCC          mov eax,CCCCCCCC
    004010C6   |.  F3:AB                rep stos dword ptr es:[edi] ; 以上是保存现场和分配局部变量空间

    004010C8   |.  33C0                 xor eax,eax ; return 0;

    004010CA   |.  5F                   pop edi
    004010CB   |.  5E                   pop esi
    004010CC   |.  5B                   pop ebx
    004010CD   |.  8BE5                 mov esp,ebp
    004010CF   |.  5D                   pop ebp     ; 恢复现场
    004010D0   \\.  C2 0800              retn 8      ; 校正栈顶

    如果定义了<windows.h>, pascal 和 __stdcall 没有什么区别,
    如果没有定义<windows.h>程序一编译就会报错。

    可能是为了兼容才让 pascal 约定存在的。


    ;*********************************************************************************************************

    接着是test_fastcall( \'g\', \'h\', \'i\', \'j\' ),这个函数被声明为 __fastcall 的调用方式,其调用代码如下:
    00401156   |.  6A 6A                push 6A
    00401158   |.  6A 69                push 69
    0040115A   |.  B2 68                mov dl,68
    0040115C   |?  B1 67                mov cl,67
    0040115E   |?  E8 C0FEFFFF          call TestCall.00401023

    这个有意思,是通过寄存器方式传递参数的,不过只能有两个寄存器参与, ecxedx ,其余的还是用栈了,要珍惜啊,呵呵。
    嗯,用寄存器的确是比访问内存要快得多。
    其返回值用 eax 传递。


    看看函数体内的代码:

    004010E0   /> \\55                   push ebp
    004010E1   |.  8BEC                 mov ebp,esp
    004010E3   |.  83EC 48              sub esp,48
    004010E6   |.  53                   push ebx
    004010E7   |.  56                   push esi
    004010E8   |.  57                   push edi
    004010E9   |.  51                   push ecx
    004010EA   |.  8D7D B8              lea edi,dword ptr ss:[ebp-48]
    004010ED   |.  B9 12000000          mov ecx,12
    004010F2   |.  B8 CCCCCCCC          mov eax,CCCCCCCC
    004010F7   |.  F3:AB                rep stos dword ptr es:[edi] ; 以上是保存现场和分配局部变量空间

    004010F9   |.  59                   pop ecx                     ; 获得第一个参数\'g\'
    004010FA   |.  8855 F8              mov byte ptr ss:[ebp-8],dl  ; 获得第二个参数\'h\'
    004010FD   |.  884D FC              mov byte ptr ss:[ebp-4],cl  ; 获得第一个参数\'g\'
    00401100   |.  C645 FC 01           mov byte ptr ss:[ebp-4],1   ; para1 = (char)1;
    00401104   |.  C645 F8 02           mov byte ptr ss:[ebp-8],2   ; para2 = (char)2;
    00401108   |.  C645 08 03           mov byte ptr ss:[ebp+8],3   ; para3 = (char)3;
    0040110C   |.  C645 0C 04           mov byte ptr ss:[ebp+C],4   ; para4 = (char)4;

    00401110   |.  33C0                 xor eax,eax                 ; return 0;

    00401112   |.  5F                   pop edi
    00401113   |.  5E                   pop esi
    00401114   |.  5B                   pop ebx
    00401115   |.  8BE5                 mov esp,ebp
    00401117   |.  5D                   pop ebp                     ; 恢复现场
    00401118   \\.  C2 0800              retn 8                      ; 校正栈顶

    开始就把寄存器参数保存在 [ebp - 4] 和 [ebp - 8] 的局部变量里,其余的参数还是在栈底。
    在函数体内完成了栈顶的校正。

    ;*********************************************************************************************************

    最后试一下 __declspec(naked) 的感觉,这个不是函数的调用约定,而是指定编译器的处理方式。
    其调用代码如下:
    00401163   |.  6A 6C                push 6C
    00401165   |.  6A 6B                push 6B
    00401167   |.  E8 B2FEFFFF          call TestCall.0040101E

    这里使用的是 __stdcall 方式,在前面已经研究过了。

    函数体内的代码:

    0040B5D0   /> \\55                   push ebp
    0040B5D1   |.  8BEC                 mov ebp,esp
    0040B5D3   |.  50                   push eax
    0040B5D4   |.  8A45 0C              mov al,byte ptr ss:[ebp+C]
    0040B5D7   |.  8645 08              xchg byte ptr ss:[ebp+8],al
    0040B5DA   |.  58                   pop eax
    0040B5DB   |.  5D                   pop ebp
    0040B5DC   \\.  C2 0800              retn 8

    和我写的比较一下看看:

    __declspec(naked) void __stdcall test_naked(char para1, char para2)
    {
        __asm
        {
            push    ebp
            mov     ebp, esp

            push    eax

            mov     al,byte ptr [ebp + 0Ch]
            xchg    byte ptr [ebp + 8],al
            
            pop     eax
            pop     ebp
            ret     8
        }

    }

    完全是一样的,只不过没有我写的好看:)。

    这种处理方式高度自由,编译器不帮你生成保存现场和分配局部变量空间的代码,一切得靠自己的双手来解决,包括返回值的处理。
    可能在底层的控制方面会有作用,编译器不会碍事嘛。

    ;*********************************************************************************************************

  •  Calling Convertion,不同的函数调用方式往往会导致混合编程中相互调用的出错。C++默认使用的是的__cdecl方式,这样生成的DLL如果在 DELPHI中使用stdcall方式调用是会出错的,下文对各种主要的函数调用方式做了一个比较详细的解释。



    函数调用的不同方式

    Visual C/C++编译器使用若干种方式来调用内部和外部的函数。理解这些不同的方式有助于我们来调试程序和链接我们的代码。以下文字解释了不同的函数调用方式下,参数的传递,函数返回值的返回等。同时,也讨论了无参函数的调用。

    Visual C/C++ 支持若干种不同的函数调用转换。所有的参数再传递时被设置为32位大小。除了8byte的结构体返回在EDX:EAX寄存器对上,其他的返回值也被设置为 32位,并且返回在EAX寄存器中。大的结构体则以一个指向隐藏的返回结构体指针保存在EAX寄存器中。参数是从右向左压入堆栈中。如果寄存器ESI, EDI,EBX,EBP寄存器在函数中使用,编译器产生prolog和epilog代码来保存和恢复它们。

    Visual C/C++ 编译器支持以下的调用转换:
    关键字               堆栈清除者    参数传递
    __cdecl              调用者       从右向左的顺序压入堆栈内
    __stdcall            被调用者     从右向左的顺序压入堆栈内
    __fastcall           被调用者     保存在寄存器中,然后其他的压入堆栈内
    thiscall(非关键字)    被调用者     压入堆栈,this指针保存在ECX寄存器内

    调用方式解析:
    void    calltype MyFunc( char c, short s, int i, double f );
    calltype代表不同的调用方式

    __cdecl调用:
    在VC中的编译器选项是/Gd。这是C和C++程序的缺省函数调用转换方式。由于堆栈是由调用者清除的,所以它能调用vararg函数。以__cdecl 调用方式编译的程序比__stdcall大,因为以__stdcall方式编译的程序需要在每个函数调用中包含堆栈清空的代码。
    以C方式修饰的函数名是_MyFunc。
    __cdecl调用方式的示例图:


    __stdcall调用:
    在VC中的编译器选项是/Gz。
    以C方式修饰的函数名是_MyFunc@20。20是参数的字节数(5×4bytes)。C++方式修饰的名称是私有的,各个编译器可能不同。
     __stdcall and thiscall 调用方式的示例图:

     __fastcall调用:
     在VC中的编译器选项是/Gr。
    以C方式修饰的函数名是@MyFunc@20。C++方式修饰的名称是私有的,各个编译器可能不同。
    __fastcall调用方式的示例图:

    thiscall调用:
    这是无参C++成员函数的默认调用方式。调用者负责清空堆栈,this指针第一个入栈。因为没有对应的关键字,所以thiscall方式的调用不可在程序中被显式定义。所有的函数参数都压入堆栈。这种调用方式只应用于C++,所以没有C方式的命名修饰。

  •  1,寻找WinMain人口:
    在安装目录下找到MFC文件夹下的SRC文件夹,SRC下是MFC源代码。
    路径:MFC|SRC|APPMODUL.CPP:
    _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
     LPTSTR lpCmdLine, int nCmdShow)
    {
     // call shared/exported WinMain
     return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
    }
    注意:(#define _tWinMain   WinMain)

    2,对于全局对象或全局变量来说,在程序运行即WINMAIN函数加载的时候,已经为全局对象或全局变量分配了内存和赋初值。
    所以:CTEApp theApp;->CTEApp ::CTEApp(){}->_tWinMain(){}
    说明:每一个MFC程序,有且只有一个从WinApp类派生的类(应用程序类),也只有一个从应用程序类所事例化的对象,表示应用程序本身。在WIN32程序当中,表示应用程序是通过WINMAIN入口函数来表示的(通过一个应用程序的一个事例号这一个标识来表示的)。在基于MFC应用程序中,是通过产生一个应用程序对象,用它来唯一的表示了应用程序。

    3,通过构造应用程序对象过程中调用基类CWinApp的构造函数,在CWinApp的构造函数中对程序包括运行时一些初始化工作完成了。
    CWinApp构造函数:MFC|SRC|APPCORE.CPP
    CWinApp::CWinApp(LPCTSTR lpszAppName){...}//带参数,而CTEApp构造函数没有显式向父类传参,难道CWinApp()有默认参数?见下:
    (在CWinApp类定义中, CWinApp(LPCTSTR lpszAppName = NULL); )
    注意:CWinApp()函数中:
    pThreadState->m_pCurrentWinThread = this;
    pModuleState->m_pCurrentWinApp = this
    (this指向的是派生类CTEApp对象,即theApp)
    调试:CWinApp::CWinApp();->CTEApp theApp;(->CTEApp ::CTEApp())->CWinApp::CWinApp()->CTEApp ::CTEApp()->_tWinMain(){}

    4,_tWinMain函数中通过调用AfxWinMain()函数来完成它要完成的功能。(Afx*前缀代表这是应用程序框架函数,是一些全局函数,应用程序框架是一套辅助生成应用程序的框架模型,把一些类做一些有机的集成,我们可根据这些类函数来设计自己的应用程序)。
    AfxWinMain()函数路径:MFC|SRC|WINMAIN.CPP:
    在AfxWinMain()函数中:
    CWinApp* pApp = AfxGetApp();
    说明:pApp存储的是指向WinApp派生类对象(theApp)的指针。
    //_AFXWIN_INLINE CWinApp* AFXAPI AfxGetApp()
    // { return afxCurrentWinApp; }

    调用pThread->InitInstance()
    说明:pThread也指向theApp,由于基类中virtual BOOL InitApplication()定义为虚函数,所以调用pThread->InitInstance()时候,调用的是派生类CTEApp的InitInstance()函数。

    nReturnCode = pThread->Run();
    说明:pThread->Run()完成了消息循环。

    5,注册窗口类:AfxEndDeferRegisterClass();
    AfxEndDeferRegisterClass()函数所在文件:MFC|SRC|APPCORE.CPP
    BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister){...}
    说明:设计窗口类:在MFC中事先设计好了几种缺省的窗口类,根据不同的应用程序的选择,调用AfxEndDeferRegisterClass()函数注册所选择的窗口类。
    调试:CWinApp::CWinApp();->CTEApp theApp;(->CTEApp ::CTEApp())->CWinApp::CWinApp()->CTEApp ::CTEApp()->_tWinMain(){}//进入程序
    ->AfxWinMain();->pApp->InitApplication();->pThread->InitInstance()//父类InitInstance虚函数;->CTEApp::InitInstance()//子类实现函数;->AfxEndDeferRegisterClass(LONG fToRegister)//注册所选择的窗口类(出于文档管理,注册提前,正常的应在PreCreateWindow中进行注册)//之后进入创建窗口阶段(以下再不做调试)

    6,PreCreateWindow()://主要是注册窗口类
    BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
    {
     if( !CFrameWnd::PreCreateWindow(cs) )
      return FALSE;
     return TRUE;
    }
    说明:
    CFrameWnd::PreCreateWindow()函数所在文件:MFC|SRC|WINFRM.CPP
    BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs)
    {
     if (cs.lpszClass == NULL)
     {
      VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
       //判断AFX_WNDFRAMEORVIEW_REG型号窗口类是否注册,如果没有注册则注册
      cs.lpszClass = _afxWndFrameOrView;  // COLOR_WINDOW background
       //把注册后的窗口类名赋给cs.lpszClass
     }

     if ((cs.style & FWS_ADDTOTITLE) && afxData.bWin4)
      cs.style |= FWS_PREFIXTITLE;

     if (afxData.bWin4)
      cs.dwExStyle |= WS_EX_CLIENTEDGE;

     return TRUE;
    }

    其中:
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs);//PreCreateWindow()是个虚函数,如果子类有则调用子类的。
    #define VERIFY(f)          ASSERT(f)
    #define AfxDeferRegisterClass(fClass) AfxEndDeferRegisterClass(fClass)
    define AFX_WNDFRAMEORVIEW_REG          0x00008
    const TCHAR _afxWndFrameOrView[] = AFX_WNDFRAMEORVIEW;//WINCORE.CPP文件中,定义为全局数组。
    //#define AFX_WNDFRAMEORVIEW  AFX_WNDCLASS("FrameOrView")

    7,创建窗口:
    Create()函数路径:MFC|SRC|WINFRM.CPP:
    CFrameWnd::Create(...){
     ...
     CreateEx(...);//从父类继承来的,调用CWnd::CreateEx().
     ...
    }

    CWnd::CreateEx()函数路径:MFC|SRC|WINCORE.CPP
    BOOL CWnd::CreateEx(...){
     ...
     if (!PreCreateWindow(cs))//虚函数,如果子类有调用子类的。
     {
      PostNcDestroy();
      return FALSE;
     }
     ...
     HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
      cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
      cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);

     ...
    }
    说明:CreateWindowEx()函数与CREATESTRUCT结构体参数的对应关系,使我们在创建窗口之前通过可PreCreateWindow(cs)修改cs结构体成员来修改所要的窗口外观。PreCreateWindow(cs))//是虚函数,如果子类有调用子类的。
    HWND CreateWindowEx(
      DWORD dwExStyle,     
      LPCTSTR lpClassName, 
      LPCTSTR lpWindowName,
      DWORD dwStyle,       
      int x,               
      int y,               
      int nWidth,          
      int nHeight,         
      HWND hWndParent,     
      HMENU hMenu,         
      HINSTANCE hInstance, 
      LPVOID lpParam       
    );
    typedef struct tagCREATESTRUCT { // cs
        LPVOID    lpCreateParams;
        HINSTANCE hInstance;
        HMENU     hMenu;
        HWND      hwndParent;
        int       cy;
        int       cx;
        int       y;
        int       x;
        LONG      style;
        LPCTSTR   lpszName;
        LPCTSTR   lpszClass;
        DWORD     dwExStyle;
    } CREATESTRUCT;

    8,显示和更新窗口:
    CTEApp类,TEApp.cpp中
    m_pMainWnd->ShowWindow(SW_SHOW);//显示窗口,m_pMainWnd指向框架窗口
    m_pMainWnd->UpdateWindow();//更新窗口
    说明:
    class CTEApp : public CWinApp{...}
    class CWinApp : public CWinThread{...}
    class CWinThread : public CCmdTarget
    {
     ...
    public:
     CWnd* m_pMainWnd;
     ...
    ...
    }

    9,消息循环:
    int AFXAPI AfxWinMain()
    { ...
     // Perform specific initializations
     if (!pThread->InitInstance()){...}
     //完成窗口初始化工作,完成窗口的注册,完成窗口的创建,显示和更新。
     nReturnCode = pThread->Run();
     //继承基类Run()方法,调用CWinThread::Run()来完成消息循环
     ...
    }
    ////////////////////////////////////////////////////////////////
    CWinThread::Run()方法路径:MFC|SRC|THRDCORE.CPP
    int CWinThread::Run()
    { ...
      // phase2: pump messages while available
      do//消息循环
      {
       // pump message, but quit on WM_QUIT
       if (!PumpMessage())//取消息并处理
        return ExitInstance();
       ...
      } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
     ...
    }
    说明:
    BOOL PeekMessage(,,,,)函数说明
    The PeekMessage function checks a thread message queue for a message and places the message (if any) in the specified structure.
    If a message is available, the return value is nonzero.
    If no messages are available, the return value is zero.

    /////////////////////////////////////////////////////////////
    BOOL CWinThread::PumpMessage()
    {
     ...
     if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))//取消息
     {...}
     ...
     // process this message
     if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
     {
      ::TranslateMessage(&m_msgCur);//进行消息(如键盘消息)转换
      ::DispatchMessage(&m_msgCur);//分派消息到窗口的回调函数处理(实际上分派的消息经过消息映射,交由消息响应函数进行处理。)
     }
     return TRUE;
    }

    9,文档与视结构:
    可以认为View类窗口是CMainFram类窗口的子窗口。
    DOCument类是文档类。
    DOC-VIEW结构将数据本身与它的显示分离开。
    文档类:数据的存储,加载
    视类:数据的显示,修改

    10,文档类,视类,框架类的有机结合:
    在CTEApp类CTEApp::InitInstance()函数中通过文档模板将文档类,视类,框架类的有机组织一起。
    ...
    CSingleDocTemplate* pDocTemplate;
    pDocTemplate = new CSingleDocTemplate(
     IDR_MAINFRAME,
     RUNTIME_CLASS(CTEDoc),
     RUNTIME_CLASS(CMainFrame),       // main SDI frame window
     RUNTIME_CLASS(CTEView));
    AddDocTemplate(pDocTemplate);//增加到模板

  • 1, c语言中,结构体struct中不能包括函数的,而在C++中struct中可以包括函数。
    2,C++中结构体和类可以通用,区别主要表现在访问控制方面:struct中默认是public,而 class中默认的是private。
    3,构造函数最重要的作用是创建对象的本身,C++中每个类可以拥有多个构造函数,但必须至少有一个构造函数,当一个类中没有显式提供任何构造函数,C++编辑器自动提供一个默认的不带参数的构造函数,这个默认的构造函数只负责构造对象,不做任何初始化工作。但在一个类中只要自己定义一个构造函数,不管带参不带参,编辑器不再提供默认的不带参的构造函数了。构造函数没有返回值。
    4,析构函数当一个对象生命周期结束时候被调用来回收对象占用的内存空间。一个类只需有一个析构函数。析构函数没有返回值也不的带参数。
    5,析构函数的作用与构造函数相反,对象超出起作用范围对应的内存空间被系统收回,或被程序用delete删除的时候,对象的析构函数被调用。
    6,函数的重载条件:函数的参数类型、个数不同,才能构成函数的重载。重载是发生在同一个类中。
    7,类是抽象的,不占用具体物理内存,只有对象是实例化的,是占用具体物理内存的。
    8,this指针是隐含指针,指向对象本身(this指针不是指向类的),代表了对象的地址。所有的对象调用的成员函数都是同一代码段,但每个对象都有自己的数据成员。当对象通过调用它的成员函数来访问它的数据成员的时候,成员函数除了接收实参外,还接收了对象的地址,这个地址被一个隐藏的形参this所获取,通过这个this指针可以访问对象的数据成员和成员函数。
    9,对象中public属性的成员在外部和子类中都可以被访问;protected属性的成员在外部不能被访问,在子类中是可以访问的;private属性在子类中和外部都不能被访问。
    10,类的继承访问特性:(public,protected,private)
     a)基类中private属性成员,子类无论采用那种继承方式都不能访问。
     b)采用public继承,基类中的public,protected属性的成员访问特性在子类中仍然保持一致。
     c)采用protected继承,基类中的public,protected属性成员访问特性在子类中变为protected.
     d)采用provate继承,基类中的public,protected属性成员访问特性在子类中变为provate.
    11,子类和基类的构造函数或析构函数调用顺序:
     当调用子类的构造函数时候先调用基类的构造函数(如果没有指明,则调用基类却省那个不带参数的构造函数;如果要指明则在子类构造函数名后加":基类名(参数)")。析构函数则相反,先调用子类析构函数,后调用基类的析构函数。
    12,函数的覆盖:
     函数的覆盖是发生在发生父类和子类之间的。(函数的重载是发生在同一个类中)
     当子类中重写了父类的某些成员函数后,子类中的成员函数覆盖了父类的对应同名成员函数。
    13,用父类指针访问子类对象成员时候,只能访问子类从父类继承来的那部分。(这时候外部不可以访问父类中保护和私有的部分,子类中不可访问父类私有部分。)
    14,多态性:在基类的的成员函数前加virturl变成虚函数,当用子类对象调用该功能的成员函数时候,子类有的就调用子类的,子类没有的就调用基类的。
     当C++编译器在编译的时候,发现被调用的成员函数在基类中定义的是虚函数,这个时候C++就会采用迟绑定技术(late binding),在运行的时候,依据对象的类型来确定调用的哪个函数,子类有调用子类的,子类没有的就调用基类的。
     如果基类中的成员函数不是虚函数,则这时候的绑定是早期绑定,在编译的时候就已经确定该调用哪个函数。
    15,纯虚函数:在类中定义时 eg: virtual void f1()=0;
     纯虚函数没有函数体,含有纯虚函数的类叫做抽象类,抽象类不能实例化对象。当子类从抽象类的基类中派生出来时候,如果没有实现基类中的纯虚函数,则子类也是个抽象类,也不能实例化对象。
     纯虚函数被标名为不具体实现的虚成员函数,纯虚函数可以让类只具有操作的名称而不具有具体的操作的内容,让派生类在继承的时候再给出具体的定义。如果派生类没有给出基类的纯虚函数的具体定义的时候,派生类也为一个抽象类,也不能实例化对象。
    16,引用:变量的别名。引用需要在定义的时候用一变量或对象初始化自己。引用一旦在定义的时候初始化,就维系在一个特定的变量或对象上。
     引用不占用物理内存(与定义引用的目标共用同一内存)。指针变量需要占用物理内存,用来存储地址。
    1, c语言中,结构体struct中不能包括函数的,而在C++中struct中可以包括函数。
    2,C++中结构体和类可以通用,区别主要表现在访问控制方面:struct中默认是public,而 class中默认的是private。
    3,构造函数最重要的作用是创建对象的本身,C++中每个类可以拥有多个构造函数,但必须至少有一个构造函数,当一个类中没有显式提供任何构造函数,C++编辑器自动提供一个默认的不带参数的构造函数,这个默认的构造函数只负责构造对象,不做任何初始化工作。但在一个类中只要自己定义一个构造函数,不管带参不带参,编辑器不再提供默认的不带参的构造函数了。构造函数没有返回值。
    4,析构函数当一个对象生命周期结束时候被调用来回收对象占用的内存空间。一个类只需有一个析构函数。析构函数没有返回值也不的带参数。
    5,析构函数的作用与构造函数相反,对象超出起作用范围对应的内存空间被系统收回,或被程序用delete删除的时候,对象的析构函数被调用。
    6,函数的重载条件:函数的参数类型、个数不同,才能构成函数的重载。重载是发生在同一个类中。
    7,类是抽象的,不占用具体物理内存,只有对象是实例化的,是占用具体物理内存的。
    8,this指针是隐含指针,指向对象本身(this指针不是指向类的),代表了对象的地址。所有的对象调用的成员函数都是同一代码段,但每个对象都有自己的数据成员。当对象通过调用它的成员函数来访问它的数据成员的时候,成员函数除了接收实参外,还接收了对象的地址,这个地址被一个隐藏的形参this所获取,通过这个this指针可以访问对象的数据成员和成员函数。
    9,对象中public属性的成员在外部和子类中都可以被访问;protected属性的成员在外部不能被访问,在子类中是可以访问的;private属性在子类中和外部都不能被访问。
    10,类的继承访问特性:(public,protected,private)
     a)基类中private属性成员,子类无论采用那种继承方式都不能访问。
     b)采用public继承,基类中的public,protected属性的成员访问特性在子类中仍然保持一致。
     c)采用protected继承,基类中的public,protected属性成员访问特性在子类中变为protected.
     d)采用provate继承,基类中的public,protected属性成员访问特性在子类中变为provate.
    11,子类和基类的构造函数或析构函数调用顺序:
     当调用子类的构造函数时候先调用基类的构造函数(如果没有指明,则调用基类却省那个不带参数的构造函数;如果要指明则在子类构造函数名后加":基类名(参数)")。析构函数则相反,先调用子类析构函数,后调用基类的析构函数。
    12,函数的覆盖:
     函数的覆盖是发生在发生父类和子类之间的。(函数的重载是发生在同一个类中)
     当子类中重写了父类的某些成员函数后,子类中的成员函数覆盖了父类的对应同名成员函数。
    13,用父类指针访问子类对象成员时候,只能访问子类从父类继承来的那部分。(这时候外部不可以访问父类中保护和私有的部分,子类中不可访问父类私有部分。)
    14,多态性:在基类的的成员函数前加virturl变成虚函数,当用子类对象调用该功能的成员函数时候,子类有的就调用子类的,子类没有的就调用基类的。
     当C++编译器在编译的时候,发现被调用的成员函数在基类中定义的是虚函数,这个时候C++就会采用迟绑定技术(late binding),在运行的时候,依据对象的类型来确定调用的哪个函数,子类有调用子类的,子类没有的就调用基类的。
     如果基类中的成员函数不是虚函数,则这时候的绑定是早期绑定,在编译的时候就已经确定该调用哪个函数。
    15,纯虚函数:在类中定义时 eg: virtual void f1()=0;
     纯虚函数没有函数体,含有纯虚函数的类叫做抽象类,抽象类不能实例化对象。当子类从抽象类的基类中派生出来时候,如果没有实现基类中的纯虚函数,则子类也是个抽象类,也不能实例化对象。
     纯虚函数被标名为不具体实现的虚成员函数,纯虚函数可以让类只具有操作的名称而不具有具体的操作的内容,让派生类在继承的时候再给出具体的定义。如果派生类没有给出基类的纯虚函数的具体定义的时候,派生类也为一个抽象类,也不能实例化对象。
    16,引用:变量的别名。引用需要在定义的时候用一变量或对象初始化自己。引用一旦在定义的时候初始化,就维系在一个特定的变量或对象上。
     引用不占用物理内存(与定义引用的目标共用同一内存)。指针变量需要占用物理内存,用来存储地址。
  • 该指令允许有选择性的修改编译器的警告消息的行为
    指令格式如下:
    #pragma warning( warning-specifier : warning-number-list [; warning-specifier : warning-number-list...]
    #pragma warning( push[ ,n ] )
    #pragma warning( pop )

    主要用到的警告表示有如下几个:

    once:只显示一次(警告/错误等)消息
    default:重置编译器的警告行为到默认状态
    1,2,3,4:四个警告级别
    disable:禁止指定的警告信息
    error:将指定的警告信息作为错误报告

    如果大家对上面的解释不是很理解,可以参考一下下面的例子及说明
     
    #pragma warning( disable : 4507 34; once : 4385; error : 164 ) 
    等价于: 
    #pragma warning(disable:4507 34)  // 不显示4507和34号警告信息 
    #pragma warning(once:4385)        // 4385号警告信息仅报告一次 
    #pragma warning(error:164)        // 把164号警告信息作为一个错误。 
    同时这个pragma warning 也支持如下格式: 
    #pragma warning( push [ ,n ] ) 
    #pragma warning( pop ) 
    这里n代表一个警告等级(1---4)。 
    #pragma warning( push )保存所有警告信息的现有的警告状态。 
    #pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告 
    等级设定为n。  
    #pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作的 
    一切改动取消。例如: 
    #pragma warning( push ) 
    #pragma warning( disable : 4705 ) 
    #pragma warning( disable : 4706 ) 
    #pragma warning( disable : 4707 ) 
    #pragma warning( pop )

    在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)

    在使用标准C++进行编程的时候经常会得到很多的警告信息,而这些警告信息都是不必要的提示,
    所以我们可以使用#pragma warning(disable:4786)来禁止该类型的警告

    在vc中使用ADO的时候也会得到不必要的警告信息,这个时候我们可以通过
    #pragma warning(disable:4146)来消除该类型的警告信息

  • pragma指令简介
    在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作.
    下面介绍了一下该指令的一些常用参数,希望对大家有所帮助!
    一. message 参数。

    message
    它能够在编译信息输出窗 
    口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为: 

    #pragma message("消息文本") 

    当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。 
    当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条
    指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法 
    #ifdef _X86 
    #pragma message("_X86 macro activated!") 
    #endif 
    当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示"_ 
    X86 macro activated!"。我们就不会因为不记得自己定义的一些特定的宏而抓耳挠腮了 
     


       
    二. 另一个使用得比较多的#pragma参数是code_seg。格式如: 

    #pragma code_seg( [ [ { push | pop}, ] [ identifier, ] ] [ "segment-name" [, "segment-class" ] )
    该指令用来指定函数在.obj文件中存放的节,观察OBJ文件可以使用VC自带的dumpbin命令行程序,函数在.obj文件中默认的存放节
    为.text节
    如果code_seg没有带参数的话,则函数存放在.text节中
    push (可选参数) 将一个记录放到内部编译器的堆栈中,可选参数可以为一个标识符或者节名
    pop(可选参数) 将一个记录从堆栈顶端弹出,该记录可以为一个标识符或者节名
    identifier (可选参数) 当使用push指令时,为压入堆栈的记录指派的一个标识符,当该标识符被删除的时候和其相关的堆栈中的记录将被弹出堆栈
    "segment-name" (可选参数) 表示函数存放的节名
    例如:
    //默认情况下,函数被存放在.text节中
    void func1() {                  // stored in .text
    }

    //将函数存放在.my_data1节中
    #pragma code_seg(".my_data1")
    void func2() {                  // stored in my_data1
    }

    //r1为标识符,将函数放入.my_data2节中
    #pragma code_seg(push, r1, ".my_data2")
    void func3() {                  // stored in my_data2
    }

    int main() {
    }
     


    三. #pragma once (比较常用) 

    这是一个比较常用的指令,只要在头文件的最开始加入这条指令就能够保证头文件被编译一次


       
    四. #pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。

    BCB可以预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。  
    有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。你可以用#pragma startup指定编译优先级,
    如果使用了#pragma package(smart_init) ,BCB就会根据优先级的大小先后编译。  
        


    五. #pragma warning指令

    该指令允许有选择性的修改编译器的警告消息的行为


    指令格式如下:
    #pragma warning( warning-specifier : warning-number-list [; warning-specifier : warning-number-list...]
    #pragma warning( push[ ,n ] )
    #pragma warning( pop )

    主要用到的警告表示有如下几个:

    once:只显示一次(警告/错误等)消息
    default:重置编译器的警告行为到默认状态
    1,2,3,4:四个警告级别
    disable:禁止指定的警告信息
    error:将指定的警告信息作为错误报告

    如果大家对上面的解释不是很理解,可以参考一下下面的例子及说明
     
    #pragma warning( disable : 4507 34; once : 4385; error : 164 ) 
    等价于: 
    #pragma warning(disable:4507 34)  // 不显示4507和34号警告信息 
    #pragma warning(once:4385)        // 4385号警告信息仅报告一次 
    #pragma warning(error:164)        // 把164号警告信息作为一个错误。 
    同时这个pragma warning 也支持如下格式: 
    #pragma warning( push [ ,n ] ) 
    #pragma warning( pop ) 
    这里n代表一个警告等级(1---4)。 
    #pragma warning( push )保存所有警告信息的现有的警告状态。 
    #pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告 
    等级设定为n。  
    #pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作的 
    一切改动取消。例如: 
    #pragma warning( push ) 
    #pragma warning( disable : 4705 ) 
    #pragma warning( disable : 4706 ) 
    #pragma warning( disable : 4707 ) 
    #pragma warning( pop )

    在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)

    在使用标准C++进行编程的时候经常会得到很多的警告信息,而这些警告信息都是不必要的提示,
    所以我们可以使用#pragma warning(disable:4786)来禁止该类型的警告

    在vc中使用ADO的时候也会得到不必要的警告信息,这个时候我们可以通过
    #pragma warning(disable:4146)来消除该类型的警告信息

     


    六. pragma comment(...)
    该指令的格式为
    #pragma comment( "comment-type" [, commentstring] )
     

    该指令将一个注释记录放入一个对象文件或可执行文件中,
    comment-type(注释类型):可以指定为五种预定义的标识符的其中一种
    五种预定义的标识符为:

    compiler:将编译器的版本号和名称放入目标文件中,本条注释记录将被编译器忽略
             如果你为该记录类型提供了commentstring参数,编译器将会产生一个警告
    例如:#pragma comment( compiler )

    exestr:将commentstring参数放入目标文件中,在链接的时候这个字符串将被放入到可执行文件中,
           当操作系统加载可执行文件的时候,该参数字符串不会被加载到内存中.但是,该字符串可以被
           dumpbin之类的程序查找出并打印出来,你可以用这个标识符将版本号码之类的信息嵌入到可
           执行文件中!

    lib:这是一个非常常用的关键字,用来将一个库文件链接到目标文件中


    常用的lib关键字,可以帮我们连入一个库文件。 
    例如:
    #pragma comment(lib, "user32.lib") 
    该指令用来将user32.lib库文件加入到本工程中


    linker:将一个链接选项放入目标文件中,你可以使用这个指令来代替由命令行传入的或者在开发环境中
           设置的链接选项,你可以指定/include选项来强制包含某个对象,例如:
           #pragma comment(linker, "/include:__mySymbol")

    你可以在程序中设置下列链接选项

    /DEFAULTLIB
    /EXPORT
    /INCLUDE
    /MERGE
    /SECTION
    这些选项在这里就不一一说明了,详细信息请看msdn!

    user:将一般的注释信息放入目标文件中commentstring参数包含注释的文本信息,这个注释记录将被链接器忽略
    例如:
    #pragma comment( user, "Compiled on " __DATE__ " at " __TIME__ )

  • 链表与数组的区别
    A 从逻辑结构来看
    A-1. 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当     数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。

    A-2. 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、     删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)


    B 从内存存储来看
    B-1. (静态)数组从栈中分配空间, 对于程序员方便快速,但是自由度小
    B-2. 链表从堆中分配空间, 自由度大但是申请管理比较麻烦.

    堆和栈的区别 

    solost 2004 1009 发表 

    一、预备知识程序的内存分配
    一个由c/C++编译的程序占用的内存分为以下几个部分
    1栈区(stack—   编译器(Compiler)自动分配释放 ,存放函数的参数值局部变量的值等。其操作方式类似于数据结构中的栈。
    2堆区(heap —   一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
    3全局区(静态区)static全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 
    4文字常量区  — 常量字符串就是放在这里的。 程序结束后由系统释放
    5程序代码区存放函数体的二进制代码

    二、例子程序
    这是一个前辈写的,非常详细
    //main.cpp
    int a = 0;
    全局初始化区
    char *p1;
    全局未初始化区
    main()
    {
    int b;

    char s[] = "abc";

    char *p2;

    char *p3 = "123456"; 123456\0
    在常量区,p3在栈上。
    static int c =0
     全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);
    分配得来得1020字节的区域就在堆区。
    strcpy(p1, "123456"); 123456\0
    放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
    }


    二、堆和栈的理论知识
    2.1
    申请方式
    stack:
    由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
    heap:
    需要程序员自己申请,并指明大小,在cmalloc函数
    p1 = (char *)malloc(10);
    C++中用new运算符
    p2 = (char *)malloc(10);
    但是注意p1p2本身是在栈中的。


    2.2
    申请后系统的响应 
    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
    会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    2.3
    申请大小的限制 
    栈:在Windows, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。


    2.4
    申请效率的比较: 
    栈由系统自动分配,速度较快。但程序员是无法控制的。
    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
    另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

    2.5
    堆和栈中的存储内容 
    栈: 在函数调用时,(1) 第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址(2) 然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,(3) 然后是函数中的局部变量。 注意: 静态变量是不入栈的。 
    当本次函数调用结束后,(1) 局部变量先出栈,(2) 然后是参数,(3) 最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行
    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 

    2.6
    存取效率的比较 
    char s1[] = "aaaaaaaaaaaaaaa";
    char *s2 = "bbbbbbbbbbbbbbbbb";
    aaaaaaaaaaa
    是在运行时刻赋值的;
    bbbbbbbbbbb是在编译时就确定的;
    但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
    比如:
    #include
    void main()
    {
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
    a = c[1];
    a = p[1];
    return;
    }
    对应的汇编代码
    10: a = c[1];
    00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
    0040106A 88 4D FC mov byte ptr [ebp-4],cl
    11: a = p[1];
    0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
    00401070 8A 42 01 mov al,byte ptr [edx+1]
    00401073 88 45 FC mov byte ptr [ebp-4],al
    第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。


    2.7
    小结: 
    堆和栈的区别可以用如下的比喻来看出:
    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 
    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 


    深度优先搜索与广度优先搜索算法有何区别呢?
      通常深度优先搜索法不全部保留结点,扩展完的结点从数据库中弹出删去,这样,一般在数据库中存储的结点数就是深度值,因此它占用空间较少。所以,当搜索树的结点较多,用其它方法易产生内存溢出时,深度优先搜索不失为一种有效的求解方法。
      广度优先搜索算法,一般需存储产生的所有结点,占用的存储空间要比深度优先搜索大得多,因此,程序设计中,必须考虑溢出和节省内存空间的问题。但广度优先搜索法一般无回溯操作,即入栈和出栈的操作,所以运行速度比深度优先搜索要快些

  • 2005-10-30

    sizeof

    sizeof

    关键字:sizeof,字节对齐,多继承,虚拟继承,成员函数指针

    前向声明:

    sizeof,一个其貌不扬的家伙,引无数菜鸟竟折腰,小虾我当初也没少犯迷糊,秉着“
    辛苦我一个,幸福千万人”的伟大思想,我决定将其尽可能详细的总结一下。
    但当我总结的时候才发现,这个问题既可以简单,又可以复杂,所以本文有的地方并不
    适合初学者,甚至都没有必要大作文章。但如果你想“知其然,更知其所以然”的话,
    那么这篇文章对你或许有所帮助。
    菜鸟我对C++的掌握尚未深入,其中不乏错误,欢迎各位指正啊

    1. 定义:
    sizeof是何方神圣sizeof乃C/C++中的一个操作符(operator)是也,简单的说其作
    用就是返回一个对象或者类型所占的内存字节数。
    MSDN上的解释为:
    The sizeof keyword gives the amount of storage, in bytes, associated with a
    variable or a type (including aggregate types).
    This keyword returns a value of type size_t.
    其返回值类型为size_t,在头文件stddef.h中定义。这是一个依赖于编译系统的值,一
    般定义为
    typedef unsigned int size_t;
    世上编译器林林总总,但作为一个规范,它们都会保证char、signed char和unsigned
    char的sizeof值为1,毕竟char是我们编程能用的最小数据类型。
    2. 语法:
    sizeof有三种语法形式,如下:
    1) sizeof( object ); // sizeof( 对象 );
    2) sizeof( type_name ); // sizeof( 类型 );
    3) sizeof object; // sizeof 对象;
    所以,
    int i;
    sizeof( i ); // ok
    sizeof i; // ok
    sizeof( int ); // ok
    sizeof int; // error
    既然写法3可以用写法1代替,为求形式统一以及减少我们大脑的负担,第3种写法,忘
    掉它吧!
    实际上,sizeof计算对象的大小也是转换成对对象类型的计算,也就是说,同种类型的
    不同对象其sizeof值都是一致的。这里,对象可以进一步延伸至表达式,即sizeof可以
    对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,一般不会对表达式
    进行计算。如:
    sizeof( 2 );// 2的类型为int,所以等价于 sizeof( int );
    sizeof( 2 + 3.14 ); // 3.14的类型为double,2也会被提升成double类型,所以等价
    于 sizeof( double );
    sizeof也可以对一个函数调用求值,其结果是函数返回类型的大小,函数并不会被调用
    ,我们来看一个完整的例子:
    char foo()
    {
    printf("foo() has been called.\n");
    return 'a';
    }
    int main()
    {
    size_t sz = sizeof( foo() ); // foo() 的返回值类型为char,所以sz = sizeof(
    char ),foo()并不会被调用
    printf("sizeof( foo() ) = %d\n", sz);
    }
    C99标准规定,函数、不能确定类型的表达式以及位域(bit-field)成员不能被计算s
    izeof值,即下面这些写法都是错误的:
    sizeof( foo );// error
    void foo2() { }
    sizeof( foo2() );// error
    struct S
    {
    unsigned int f1 : 1;
    unsigned int f2 : 5;
    unsigned int f3 : 12;
    };
    sizeof( S.f1 );// error
    3. sizeof的常量性
    sizeof的计算发生在编译时刻,所以它可以被当作常量表达式使用,如:
    char ary[ sizeof( int ) * 10 ]; // ok
    最新的C99标准规定sizeof也可以在运行时刻进行计算,如下面的程序在Dev-C++中可以
    正确执行:
    int n;
    n = 10; // n动态赋值
    char ary[n]; // C99也支持数组的动态定义
    printf("%d\n", sizeof(ary)); // ok. 输出10
    但在没有完全实现C99标准的编译器中就行不通了,上面的代码在VC6中就通不过编译。
    所以我们最好还是认为sizeof是在编译期执行的,这样不会带来错误,让程序的可移植
    性强些。
    4. 基本数据类型的sizeof
    这里的基本数据类型指short、int、long、float、double这样的简单内置数据类型,
    由于它们都是和系统相关的,所以在不同的系统下取值可能不同,这务必引起我们的注
    意,尽量不要在这方面给自己程序的移植造成麻烦。
    一般的,在32位编译环境中,sizeof(int)的取值为4。
    5. 指针变量的sizeof
    学过数据结构的你应该知道指针是一个很重要的概念,它记录了另一个对象的地址。既
    然是来存放地址的,那么它当然等于计算机内部地址总线的宽度。所以在32位计算机中
    ,一个指针变量的返回值必定是4(注意结果是以字节为单位),可以预计,在将来的6
    4位系统中指针变量的sizeof结果为8。
    char* pc = "abc";
    int* pi;
    string* ps;
    char** ppc = &pc;
    void (*pf)();// 函数指针
    sizeof( pc ); // 结果为4
    sizeof( pi ); // 结果为4
    sizeof( ps ); // 结果为4
    sizeof( ppc ); // 结果为4
    sizeof( pf );// 结果为4
    指针变量的sizeof值与指针所指的对象没有任何关系,正是由于所有的指针变量所占内
    存大小相等,所以MFC消息处理函数使用两个参数WPARAM、LPARAM就能传递各种复杂的消
    息结构(使用指向结构体的指针)。
    6. 数组的sizeof
    数组的sizeof值等于数组所占用的内存字节数,如:
    char a1[] = "abc";
    int a2[3];
    sizeof( a1 ); // 结果为4,字符 末尾还存在一个NULL终止符
    sizeof( a2 ); // 结果为3*4=12(依赖于int)
    一些朋友刚开始时把sizeof当作了求数组元素的个数,现在,你应该知道这是不对的,
    那么应该怎么求数组元素的个数呢Easy,通常有下面两种写法:
    int c1 = sizeof( a1 ) / sizeof( char ); // 总长度/单个元素的长度
    int c2 = sizeof( a1 ) / sizeof( a1[0] ); // 总长度/第一个元素的长度
    写到这里,提一问,下面的c3,c4值应该是多少呢
    void foo3(char a3[3])
    {
    int c3 = sizeof( a3 ); // c3 ==
    }
    void foo4(char a4[])
    {
    int c4 = sizeof( a4 ); // c4 ==
    }
    也许当你试图回答c4的值时已经意识到c3答错了,是的,c3!=3。这里函数参数a3已不
    再是数组类型,而是蜕变成指针,相当于char* a3,为什么仔细想想就不难明白,我
    们调用函数foo1时,程序会在栈上分配一个大小为3的数组吗不会!数组是“传址”的
    ,调用者只需将实参的地址传递过去,所以a3自然为指针类型(char*),c3的值也就为
    4。
    7. 结构体的sizeof
    这是初学者问得最多的一个问题,所以这里有必要多费点笔墨。让我们先看一个结构体

    struct S1
    {
    char c;
    int i;
    };
    问sizeof(s1)等于多少聪明的你开始思考了,char占1个字节,int占4个字节,那么
    加起来就应该是5。是这样吗你在你机器上试过了吗也许你是对的,但很可能你是错
    的!VC6中按默认设置得到的结果为8。
    Why为什么受伤的总是我
    请不要沮丧,我们来好好琢磨一下sizeof的定义——sizeof的结果等于对象或者类型所
    占的内存字节数,好吧,那就让我们来看看S1的内存分配情况:
    S1 s1 = { 'a', 0xFFFFFFFF };
    定义上面的变量后,加上断点,运行程序,观察s1所在的内存,你发现了什么
    以我的VC6.0为例,s1的地址为0x0012FF78,其数据内容如下:
    0012FF78: 61 CC CC CC FF FF FF FF
    发现了什么怎么中间夹杂了3个字节的CC看看MSDN上的说明:
    When applied to a structure type or variable, sizeof returns the actual siz
    e, which may include padding bytes inserted for alignment.
    原来如此,这就是传说中的字节对齐啊!一个重要的话题出现了。
    为什么需要字节对齐计算机组成原理教导我们这样有助于加快计算机的取数速度,否
    则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数
    据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,
    让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个
    数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
    让我们交换一下S1中char与int的位置:
    struct S2
    {
    int i;
    char c;
    };
    看看sizeof(S2)的结果为多少,怎么还是8再看看内存,原来成员c后面仍然有3个填
    充字节,这又是为什么啊别着急,下面总结规律。

    字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
    1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
    2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,
    如有需要编译器会在成员之间加上填充字节(internal adding);
    3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最
    末一个成员之后加上填充字节(trailing padding)。
    对于上面的准则,有几点需要说明:
    1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢因为有
    了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。想想为什么。

    结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也
    在stddef.h中定义,如下:
    #define offsetof(s,m) (size_t)&(((s *)0)->m)
    例如,想要获得S2中c的偏移量,方法为
    size_t pos = offsetof(S2, c);// pos等于4
    2) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型
    ,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型
    ,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子
    成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将
    复合类型作为整体看待。
    这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以
    VC6为例,以后不再说明):
    struct S3
    {
    char c1;
    S1 s;
    char c2
    };
    S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,
    所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整
    除,整个sizeof(S3)的值也应该被4整除。
    c1的偏移量为0,s的偏移量呢这时s是一个整体,它作为结构体变量也满足前面三个
    准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需
    要了,所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补
    上3个填充字节。最后得到sizeof(S3)的值为16。
    通过上面的叙述,我们可以得到一个公式:
    结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:

    sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( tr
    ailing padding )

    到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有
    一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。它是用来调整结构
    体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma pack实现,也可以
    直接修改/Zp编译开关。#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐
    数,其取值为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么
    该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,
    公式如下:
    offsetof( item ) = min( n, sizeof( item ) )
    再看示例:
    #pragma pack(push) // 将当前pack设置压栈保存
    #pragma pack(2)// 必须在结构体定义之前使用
    struct S1
    {
    char c;
    int i;
    };
    struct S3
    {
    char c1;
    S1 s;
    char c2
    };
    #pragma pack(pop) // 恢复先前的pack设置
    计算sizeof(S1)时,min(2, sizeof(i))的值为2,所以i的偏移量为2,加上sizeof(i)
    等于6,能够被2整除,所以整个S1的大小为6。
    同样,对于sizeof(S3),s的偏移量为2,c2的偏移量为8,加上sizeof(c2)等于9,不能
    被2整除,添加一个填充字节,所以sizeof(S3)等于10。
    现在,朋友们可以轻松的出一口气了,:)
    还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不
    占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢于是
    ,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占
    位了。如下:
    struct S5 { };
    sizeof( S5 ); // 结果为1

    8. 含位域结构体的sizeof
    前面已经说过,位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构
    体的sizeof,只是考虑到其特殊性而将其专门列了出来。
    C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,
    允许其它类型类型的存在。
    使用位域的主要目的是压缩存储,其大致规则为:
    1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字
    段将紧邻前一个字段存储,直到不能容纳为止;
    2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字
    段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
    3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方
    式,Dev-C++采取压缩方式;
    4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
    5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

    还是让我们来看看例子。
    示例1:
    struct BF1
    {
    char f1 : 3;
    char f2 : 4;
    char f3 : 5;
    };
    其内存布局为:
    |_f1__|__f2__|_|____f3___|____|
    |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
    0 3 7 8 1316
    位域类型为char,第1个字节仅能容纳下f1和f2,所以f2被压缩到第1个字节中,而f3只
    能从下一个字节开始。因此sizeof(BF1)的结果为2。
    示例2:
    struct BF2
    {
    char f1 : 3;
    short f2 : 4;
    char f3 : 5;
    };
    由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2。
    示例3:
    struct BF3
    {
    char f1 : 3;
    char f2;
    char f3 : 5;
    };
    非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。
    9. 联合体的sizeof
    结构体在内存组织上是顺序式的,联合体则是重叠式,各成员共享一段内存,所以整个
    联合体的sizeof也就是每个成员sizeof的最大值。结构体的成员也可以是复合类型,这
    里,复合类型成员是被作为整体考虑的。
    所以,下面例子中,U的sizeof值等于sizeof(s)。
    union U
    {
    int i;
    char c;
    S1 s;
    };

  • 2005-10-30

    c位域

    位域

    有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:
    struct 位域结构名
    { 位域列表 };
    其中位域列表的形式为: 类型说明符 位域名:位域长度
    例如:
    struct bs
    {
    int a:8;
    int b:2;
    int c:6;
    };
    位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:
    struct bs
    {
    int a:8;
    int b:2;
    int c:6;
    }data;

    说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:

    1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
    struct bs
    {
    unsigned a:4
    unsigned :0 /*空域*/
    unsigned b:4 /*从下一单元开始存放*/
    unsigned c:4
    }

    在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

    2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

    3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
    struct k
    {
    int a:1
    int :2 /*该2位不能使用*/
    int b:3
    int c:2
    };

    从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。

    二、位域的使用位域的使用和结构成员的使用相同,其一般形式为: 位域变量名·位域名 位域允许用各种格式输出。
    main(){
    struct bs
    {
    unsigned a:1;
    unsigned b:3;
    unsigned c:4;
    } bit,*pbit;
    bit.a=1;
    bit.b=7;
    bit.c=15;
    printf("%d,%d,%d\n",bit.a,bit.b,bit.c);
    pbit=&bit;
    pbit->a=0;
    pbit->b&=3;
    pbit->c|=1;
    printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);
    }

    上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。
    程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=", 该行相当于: pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算"|=", 相当于: pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。
  •  

    __cdecl

     

     

    __stdcall

     

    CC++程序的缺省调用规范

     

    为了使用这种调用规范,需要你明确的加上__stdcall(或WINAPI)文字。即return-type __stdcall function-name[(argument-list)]

     

     

    调用函数(Callee)返回,由调用者(Caller)调整堆栈。

     

    调用者

        // call function

        // adjust stack

     

    被调用函数

        // do work

        // return

     

    调用函数(Callee)返回,由调用函数(Callee)调整堆栈。图示:

     

    调用者

        // call function

     

    被调用函数

        // do work

        // adjust stack

        // return

     

    因为每个调用的地方都需要生成一段调整堆栈的代码,所以最后生成的文件较大。

     

     

    因为调整堆栈的代码只存在在一个地方(被调用函数的代码内),所以最后生成的文件较小。

     

    函数的参数个数可变(就像printf函数一样),因为只有调用者才知道它传给被调用函数几个参数,才能在调用结束时适当地调整堆栈。

     

     

    函数的参数个数不能是可变的。

     

    对于定义在C程序文件中的输出函数,函数名会保持原样,不会被修饰。

    对于定义在C++程序文件中的输出函数,函数名会被修饰, MSDNUnderscore character (_) is prefixed to names. 我实际测试(VC4VC6)下来发现好像不是那么简单。

    可通过在前面加上extern “C”以去除函数名修饰。也可通过.def文件去除函数名修饰。

     

    不论是C程序文件中的输出函数还是C++程序文件中的输出函数,函数名都会被修饰。

    对于定义在C程序文件中的输出函数,An underscore (_) is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list.

    对于定义在C++程序文件中的输出函数,好像更复杂,和__cdecl的情况类似。

    好像只能通过.def文件去除函数名修饰。

     

     

    _beginthread需要__cdecl的线程函数地址

     

     

    _beginthreadexCreateThread需要__stdcall的线程函数地址

     

     

    两者的参数传递顺序都是从右向左。

    为了让VB可以调用,需要用__stdcall调用规范来定义C/C++函数。请参看Microsoft KB153586 文章:How To Call C Functions That Use the _cdecl Calling Convention(没有怎么看明白)。

    当你LoadLibrary一个DLL文件后, 把GetProcAddress取得的函数地址传给上面三个线程生成函数时,请务必确认实际定义在DLL文件的输出函数符合调用规范要求。否则,编译成Release版后运行,可能会破坏堆栈,程序行为不可预测。

    VC中的相关编译开关:/Gd /Gr /Gz。另外,VC6中新增加的 /GZ 编译开关可以帮你检查堆栈问题。

    我也是初学者,若有不对的地方、可以补充的地方,请指教。谢谢。 

  • 仔细想想地位卑贱的类型转换功能(cast),其在程序设计中的地位就象goto语句一样令人鄙视。但是它还不是无法令人忍受,因为当在某些紧要的关头,类型转换还是必需的,这时它是一个必需品。

      不过C风格的类型转换并不代表所有的类型转换功能。

      一来它们过于粗鲁,能允许你在任何类型之间进行转换。不过如果要进行更精确的类型转换,这会是一个优点。在这些类型转换中存在着巨大的不同,例如把一个指向const对象的指针(pointer-to-const-object)转换成指向非const对象的指针(pointer-to-non-const-object)(即一个仅仅去除const的类型转换),把一个指向基类的指针转换成指向子类的指针(即完全改变对象类型)。传统的C风格的类型转换不对上述两种转换进行区分。(这一点也不令人惊讶,因为C风格的类型转换是为C语言设计的,而不是为C++语言设计的)。

      二来C风格的类型转换在程序语句中难以识别。在语法上,类型转换由圆括号和标识符组成,而这些可以用在C++中的任何地方。这使得回答象这样一个最基本的有关类型转换的问题变得很困难:“在这个程序中是否使用了类型转换?”。这是因为人工阅读很可能忽略了类型转换的语句,而利用象grep的工具程序也不能从语句构成上区分出它们来。

      C++通过引进四个新的类型转换操作符克服了C风格类型转换的缺点,这四个操作符是, static_cast, const_cast, dynamic_cast, 和reinterpret_cast。在大多数情况下,对于这些操作符你只需要知道原来你习惯于这样写,

    (type) expression

      而现在你总应该这样写:

    static_cast<type>(expression)

      例如,假设你想把一个int转换成double,以便让包含int类型变量的表达式产生出浮点数值的结果。如果用C风格的类型转换,你能这样写:

    int firstNumber, secondNumber;
    ...
    double result = ((double)firstNumber)/secondNumber;

      如果用上述新的类型转换方法,你应该这样写:

    double result = static_cast<double>(firstNumber)/secondNumber;

      这样的类型转换不论是对人工还是对程序都很容易识别。

       static_cast在功能上基本上与C风格的类型转换一样强大,含义也一样。它也有功能上限制。例如,你不能用static_cast象用C风格的类型转换一样把struct转换成int类型或者把double类型转换成指针类型,另外,static_cast不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。
    其它新的C++类型转换操作符被用在需要更多限制的地方。const_cast用于类型转换掉表达式的const或volatileness属性。通过使用const_cast,你向人们和编译器强调你通过类型转换想做的只是改变一些东西的constness或者volatileness属性。这个含义被编译器所约束。如果你试图使用const_cast来完成修改constness 或者volatileness属性之外的事情,你的类型转换将被拒绝。下面是一些例子:

    class Widget { ... };
    class SpecialWidget: public Widget { ... };
    void update(SpecialWidget *psw);
    SpecialWidget sw; // sw 是一个非const 对象。
    const SpecialWidget& csw = sw; // csw 是sw的一个引用
    // 它是一个const 对象
    update(&csw); // 错误!不能传递一个const SpecialWidget* 变量
    // 给一个处理SpecialWidget*类型变量的函数
    update(const_cast<SpecialWidget*>(&csw));
    // 正确,csw的const被显示地转换掉(
    // csw和sw两个变量值在update
    //函数中能被更新)
    update((SpecialWidget*)&csw);
    // 同上,但用了一个更难识别
    //的C风格的类型转换
    Widget *pw = new SpecialWidget;
    update(pw); // 错误!pw的类型是Widget*,但是
    // update函数处理的是SpecialWidget*类型
    update(const_cast<SpecialWidget*>(pw));
    // 错误!const_cast仅能被用在影响
    // constness or volatileness的地方上。,
    // 不能用在向继承子类进行类型转换。

      到目前为止,const_cast最普通的用途就是转换掉对象的const属性。

      第二种特殊的类型转换符是dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时):

    Widget *pw;
    ...
    update(dynamic_cast<SpecialWidget*>(pw));
    // 正确,传递给update函数一个指针
    // 是指向变量类型为SpecialWidget的pw的指针
    // 如果pw确实指向一个对象,
    // 否则传递过去的将使空指针。
    void updateViaRef(SpecialWidget& rsw);
    updateViaRef(dynamic_cast<SpecialWidget&>(*pw));
    //正确。传递给updateViaRef函数
    // SpecialWidget pw 指针,如果pw
    // 确实指向了某个对象
    // 否则将抛出异常
    dynamic_casts在帮助你浏览继承层次上是有限制的。它不能被用于缺乏虚函数的类型上(参见条款M24),也不能用它来转换掉constness:
    int firstNumber, secondNumber;
    ...
    double result = dynamic_cast<double>(firstNumber)/secondNumber;
    // 错误!没有继承关系
    const SpecialWidget sw;
    ...
    update(dynamic_cast<SpecialWidget*>(&sw));
    // 错误! dynamic_cast不能转换
    // 掉const。

      如你想在没有继承关系的类型中进行转换,你可能想到static_cast。如果是为了去除const,你总得用const_cast。

      这四个类型转换符中的最后一个是reinterpret_cast。使用这个操作符的类型转换,其的转换结果几乎都是执行期定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。

      reinterpret_casts的最普通的用途就是在函数指针类型之间进行转换。例如,假设你有一个函数指针数组:

    typedef void (*FuncPtr)(); // FuncPtr is 一个指向函数
    // 的指针,该函数没有参数
    // 返回值类型为void
    FuncPtr funcPtrArray[10]; // funcPtrArray 是一个能容纳
    // 10个FuncPtrs指针的数组

      让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:

    int doSomething();

      你不能不经过类型转换而直接去做,因为doSomething函数对于funcPtrArray数组来说有一个错误的类型。在FuncPtrArray数组里的函数返回值是void类型,而doSomething函数返回值是int类型。

    funcPtrArray[0] = &doSomething; // 错误!类型不匹配
    reinterpret_cast可以让你迫使编译器以你的方法去看待它们:
    funcPtrArray[0] = // this compiles
    reinterpret_cast<FuncPtr>(&doSomething);

      转换函数指针的代码是不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果(参见条款M31),所以你应该避免转换函数指针类型,除非你处于着背水一战和尖刀架喉的危急时刻。一把锋利的刀。一把非常锋利的刀。

      如果你使用的编译器缺乏对新的类型转换方式的支持,你可以用传统的类型转换方法代替static_cast, const_cast, 以及reinterpret_cast。也可以用下面的宏替换来模拟新的类型转换语法:

    #define static_cast(TYPE,EXPR) ((TYPE)(EXPR))
    #define const_cast(TYPE,EXPR) ((TYPE)(EXPR))
    #define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR))

      你可以象这样使用使用:

    double result = static_cast(double, firstNumber)/secondNumber;
    update(const_cast(SpecialWidget*, &sw));
    funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);

      这些模拟不会象真实的操作符一样安全,但是当你的编译器可以支持新的的类型转换时,它们可以简化你把代码升级的过程。

      没有一个容易的方法来模拟dynamic_cast的操作,但是很多函数库提供了函数,安全地在派生类与基类之间进行类型转换。如果你没有这些函数而你有必须进行这样的类型转换,你也可以回到C风格的类型转换方法上,但是这样的话你将不能获知类型转换是否失败。当然,你也可以定义一个宏来模拟dynamic_cast的功能,就象模拟其它的类型转换一样:

    #define dynamic_cast(TYPE,EXPR) (TYPE)(EXPR)

      请记住,这个模拟并不能完全实现dynamic_cast的功能,它没有办法知道转换是否失败。

      我知道,是的,我知道,新的类型转换操作符不是很美观而且用键盘键入也很麻烦。如果你发现它们看上去实在令人讨厌,C风格的类型转换还可以继续使用并且合法。然而,正是因为新的类型转换符缺乏美感才能使它弥补了在含义精确性和可辨认性上的缺点。并且,使用新类型转换符的程序更容易被解析(不论是对人工还是对于工具程序),它们允许编译器检测出原来不能发现的错误。这些都是放弃C风格类型转换方法的强有力的理由。还有第三个理由:也许让类型转换符不美观和键入麻烦是一件好事。

  • 2005-10-29

    c++虚函数

    先整一段网上流传很广的代码,我又加了一点在上面

    #include<iostream.h>
    class A
    {
    public:
    A();
    ~A();
    virtual void foo() { cout << "A::Vfoo() is called" << endl;}
    void function() {...}
    };
    class B: public A
    {
    public:
    B();
    ~B();
    virtual void foo() { cout << "B::Vfoo() is called" << endl;}
    void function() { ...};
    void main()
    {
    A * a = new B;
    a->foo(); //B的被调用
    a->function();//A的被调用
    }

    这个地球人都知道了,分析如下:

    首先,virtual 是有传递性的。在你的代码中的体现就是:在B的定义中去除foo这个函数前面的那个virtual的话,该函数还是被认为是虚拟函数的;
    其次,每个类如果具有虚拟函数(这些虚拟函数有的是自己定义的,有的是通过继承得到的),那么,编译器在存储该类的定义时,会用一个指针(名叫vptr)指向一个数组X。该数组X中的元素的类型是函数指针类型。每一个元素指向一个虚拟函数。这个数组X的学名叫虚拟函数表。这张虚拟函数表的构建方法是:这张虚拟函数表包含基类的虚拟函数,也包含派生类的虚拟函数表。但是,如果派生类中有重写了基类的虚拟函数的话,那么虚拟函数表中对应的元素指向的是派生类的,而不是指向基类的。对于你的代码而言,本来X中存放的函数指针指向了A中的foo,但是因为B重写了这个虚拟函数,于是,编译器将这个指针重新定向,让它指向B中的foo;
    然后,如果在程序中出现一个类对象(假设为a),a调用了它的一个成员函数,而该函数是虚拟函数。那么,编译器就会先判断a的实际类型,从而获得正确的vptr,然后在它的虚拟函数表中寻找到相对应的函数指针,最后通过这个函数指针调用正确的虚拟函数。对于你的代码,a的实际类型是B,所以a的虚拟函数表中的函数指针指向的函数是B中的foo;
    还有,每个类只有一张虚拟函数表,所有的对象共用这张表。

    编译器对于
    a->foo();的调用形式如下

    int * vptr = ( int * ) * ( int * )a;
    int foo_offset = 0;
    int * foo_addr = ( int * ) * ( vptr + foo_offset );

    typedef void ( * PFunc )();
    PFunc pFunc = ( PFunc ) foo_addr;

    pFunc();

    也可以这样折腾:

    void (*func)(A*);
     A* p=new B;//p指向类B的对象,因为B没有数据成员,那么只有虚函数表的地址
     int i;
     memcpy(&i,p,4);//将p指向的内存拷贝4字节到i指向的内存中,就是拷贝vptr
     memcpy(&func,reinterpret_cast<int*>(i),4);//将i转换为指针就是vptr
              //将vptr指向的内存就是虚函数表vptl,拷贝4字节到函数指针func的内存空间
              //注意是&func,就是指针的地址,指向指针本身拥有的4字节空间
     func(p);//现在func就是B中func的地址,func()编译器调用时为func(this)
              //现在this指针只能自己加所以开始要定义为void (*func)(A*)        
     delete p;

    再多弄一下,代码如下:
    #include "stdafx.h"
    #include "stdio.h"
    #include "iostream.h"
    class A // 大小为12 字节
    {
    private:
     int i;  //可以被自类继承,但不能被访问
    protected:
     int j; //可以被子类继承和访问
    public:
     A(){cout << "in the A constructor function"<<endl;i=111;j=222;}
     ~A(){cout << "in the A distructor function"<<endl;}
     virtual void foo() { cout << "A::Vfoo() is called" << endl;}
     void function() {cout << "A:function() is called"<<endl;}
     virtual void hoo(){cout << "A::Vhoo() is called"<<endl;}
    };
    class C
    {
    virtual void foo() { cout << "C::Vfoo() is called" << endl;}
    virtual void coo() {}
    };
    class B: public A,public C //大小为16个字节,两个vptr(A,C),i,j
    {

    //不能访问i
    public:
     B(){cout<<"in the B constructor function"<<endl;/*j=20;*/}
     
     void foo() { cout << "B::Vfoo() is called" << endl;}
     void function() { cout << "B:function() is called"<<endl;}
     virtual second(){cout << "B:second() is called"<<endl;}
     ~B(){cout <<"in the B distructor function"<<endl;}
    };
    void main()
    {
     A * pa = new B();
     A a;
        B b;
     printf("a is %d\n",sizeof(a));
     printf("b is %d\n",sizeof(b));
     printf("pa size is %d\n",sizeof(pa));
     pa->foo();
     pa->function ();

     ///////////////////////////////
    int * vptr = ( int * ) (* ( int * )pa);
    int foo_offset = 0;
    int * foo_addr = ( int * ) * ( vptr + foo_offset );

    typedef void ( * PFunc )();
    PFunc pFunc = ( PFunc ) foo_addr;

    pFunc();

     //pa->second();
     return ;
    }

  • 要求: 算法中使用的内存数量是一个常数, 即不能因为链表长度的增减使用的内存也增减. 

    解析:其实就好象两个人在环形的跑道上跑步,速度快的必定可以超过速度慢的一圈最后相遇。(这个模型可以扩展为一个数组中只有两个相同的数,如何快速找出)


     flw 回复于:2004-09-20 13:18:10
    用两个指针,一个的步长为 1,另外一个的为 2,从表头开始一起往前走,如果相遇,表明有环路,否则就是没有了。


    win_hate 回复于:2004-09-20 18:50:29
    这个问题应该有很长历史了,《C 专家编程》的附录,程序员工作面试的秘密就提到过这个问题。在这

    里我试图对这个问题做一个数学的分析。

    如果单链表里存在重复的点,则该链表中包含一个环,事实上,可以用下面的图来表示

    这使我想起 Pollard 的 "rho" 算法,事实上本问题与 "rho" 算法有一个共同点,寻找一个碰撞。

    从这个图我们可以看到,如果一个单链表里出现了重复的点,则从表头开始走,无论以什么步调,必定

    会落到环中。所以我们可以肯定,如果以某个步调走,碰到了NULL,则该链表无重合点。

    尝试用两个指针,以不同的步调前进,如果他们能相遇,必定是在环中。假定指针 p 以步调 f 前进,q

    以步调 g 前进,g>f。则 q 先进入链表的环。有一种情况很特殊,就是:在 p 刚进入环的时候就与 q 

    相遇。这是一个小概率事件,我们排除它,不考虑这种情况。可以认为:

    p 进入环的时候,偏移为 a, 而同时 q 的偏移为 b, 环的长度为 n。(参考下面的图)


    往后, p, q 就在圈内打转,它们在x 步后重合的条件为:

    fx + a = gx + b (mod n)
    (f-g)x = b-a (mod n)

    上式有解等价于  (f-g, n) | (b-a)。 

    但是,我们在事前不知道 n, 不知道 b-a, 所以唯一能确保 (f-g, n) | (b-a) 成立的是 f-g =1。

    只要 f-g = 1, 我们就能一定能检测出重合的情况,这是一个充分条件。

    而一旦 p 刚进入环时与 q 不等,(f-g, n) | (b-a) 就成为检测重合的必要条件。前面一些朋友说 f,g 

    互素或 f, g 不同即可的观点是错误的。从 (f-g, n) | (b-a) 这个条件应该能找到反例。但这个我就

    留给有兴趣的朋友了。

    f = 1, g = 2 未必是最好的,因为如果 "rho" 的尾巴很长,p 要花费很多工夫才能进入环。此外,虽

    然步调大的时候,可能要跑好几个圈才能覆盖整个环,甚至在很多情况下不能覆盖整个环,但它跑一圈

    的时间也相应减少,足以抵消。可惜的是,分析最优的选择,超出了我的能力范围。









     yangtou 回复于:2004-09-20 19:28:07
    不需要考虑细节情况,就可以判断可能性
    x=pn/(a-b) > m/b
    只要改变p的值就可以找到合适的x,x>m/b所以只要ab不等就必定相遇

    另外如果m较小比如0,由x=pn/(a-b)(不考虑m/b)
    则x只决定于a-b(只要改变p就可以得到x的合适的最小值,而p是决定于a-b的)
    T=(au+bu+av+w)x,在(a-b)一定的情况下,ab取值越小T越小

  • C++中extern “C”含义深层探索


    作者:宋宝华              出处:PConline

      1.引言

      C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。

      2.从标准头文件说起

      某企业曾经给出如下的一道面试题:

      面试题
      为什么标准头文件都有类似以下的结构?




    #ifndef __INCvxWorksh
    #define __INCvxWorksh
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*...*/
    #ifdef __cplusplus
    }
    #endif
    #endif /* __INCvxWorksh */

      分析
      显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。

      那么
    #ifdef __cplusplus
    extern "C" {
    #endif
    #ifdef __cplusplus
    }
    #endif

      的作用又是什么呢?我们将在下文一一道来。
      3.深层揭密extern "C"

      extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。

      被extern "C"限定的函数或变量是extern类型的;

      extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
      extern int a;

      仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。

      通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

      与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

      被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;

      未加extern “C”声明时的编译方式

      首先看看C++中对类似C的函数是怎样编译的。

      作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
    void foo( int x, int y );

      该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。

      _foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
      同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

      未加extern "C"声明时的连接方式

      假设在C++中,模块A的头文件如下:
    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    int foo( int x, int y );
    #endif

      在模块B中引用该函数:
    // 模块B实现文件 moduleB.cpp
    #include "moduleA.h"
    foo(2,3);

      实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

      加extern "C"声明后的编译和连接方式

      加extern "C"声明后,模块A的头文件变为:
    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    extern "C" int foo( int x, int y );
    #endif

      在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

      (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

      (2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

      如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

      所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):
      实现C++与C及其它语言的混合编程。
      明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
      4.extern "C"的惯用法

      (1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
    extern "C"
    {
    #include "cExample.h"
    }

      而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

      笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
    /* c语言头文件:cExample.h */
    #ifndef C_EXAMPLE_H
    #define C_EXAMPLE_H
    extern int add(int x,int y);
    #endif
    /* c语言实现文件:cExample.c */
    #include "cExample.h"
    int add( int x, int y )
    {
    return x + y;
    }
    // c++实现文件,调用add:cppFile.cpp
    extern "C"
    {
    #include "cExample.h"
    }
    int main(int argc, char* argv[])
    {
    add(2,3);
    return 0;
    }

      如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。

      (2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
      笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
    //C++头文件 cppExample.h
    #ifndef CPP_EXAMPLE_H
    #define CPP_EXAMPLE_H
    extern "C" int add( int x, int y );
    #endif
    //C++实现文件 cppExample.cpp
    #include "cppExample.h"
    int add( int x, int y )
    {
    return x + y;
    }
    /* C实现文件 cFile.c
    /* 这样会编译出错:#include "cExample.h" */
    extern int add( int x, int y );
    int main( int argc, char* argv[] )
    {
    add( 2, 3 );
    return 0;
    }

      如果深入理解了第3节中所阐述的extern "C"在编译和连接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。对第4节给出的示例代码,需要特别留意各个细节。

      欢迎与作者联系沟通。联系方式:
      Email: 21cnbao@21cn.com
      MSN: barrysong80@hotmail.com

  • 2005-10-11

    内存管理内幕

    内存管理内幕[转载]- -

                                          

    动态分配的选择、折衷和实现

    级别: 中级

    Jonathan Bartlett
    技术总监, New Media Worx
    2004 年 11 月 29 日

    本文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。

    为什么必须管理内存
    内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。

    追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。

    不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求:

    • 确定您是否有足够的内存来处理数据。
    • 从可用的内存中获取一部分内存。
    • 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。

    实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。

    C 风格的内存分配程序
    C 编程语言提供了两个函数来满足我们的三个需求:

    • malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
    • free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。

    物理内存和虚拟内存
    要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存

    只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。

    在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。)

    基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:

    • brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
    • mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。

    如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。

    实现一个简单的分配程序
    如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc()free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 mallocfree 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。

    要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。

    在大部分操作系统中,内存分配由以下两个简单的函数来处理:

    • void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
    • void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。

    malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:

    清单 1. 我们的简单分配程序的全局变量
    
    
            
    int has_initialized = 0;
    
    void *managed_memory_start;
    
    void *last_valid_address;
    
          

    如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:

    清单 2. 分配程序初始化函数
    
    
            
    /* Include the sbrk function */
    
    #include <unistd.h>
    
    void malloc_init()
    
    {
    
    	/* grab the last valid address from the OS */
    
    	last_valid_address = sbrk(0);
    
    
    	/* we don't have any memory to manage yet, so
    	 *just set the beginning to be last_valid_address
    	 */
    
    	managed_memory_start = last_valid_address;
    
    	/* Okay, we're initialized and ready to go */
    
     	has_initialized = 1;
    
    }
    
          

    现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:

    清单 3. 内存控制块结构定义
    
    
            
    struct mem_control_block {
    
    	int is_available;
    
    	int size;
    
    };
    
          

    现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。

    在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:

    清单 4. 解除分配函数
    
    
            
    void free(void *firstbyte) {
    
    	struct mem_control_block *mcb;
    
    	/* Backup from the given pointer to find the
    	 * mem_control_block
    	 */
    
    	mcb = firstbyte - sizeof(struct mem_control_block);
    
    	/* Mark the block as being available */
    
    	mcb->is_available = 1;
    
    	/* That's It!  We're done. */
    
    	return;
    }
    
          

    如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:

    清单 5. 主分配程序的伪代码
    
    
            
    
    1. If our allocator has not been initialized, initialize it.
    
    2. Add sizeof(struct mem_control_block) to the size requested.
    
    3. start at managed_memory_start.
    
    4. Are we at last_valid address?
    
    5. If we are:
    
       A. We didn't find any existing space that was large enough
          -- ask the operating system for more and return that.
    
    6. Otherwise:
    
       A. Is the current space available (check is_available from
          the mem_control_block)?
    
       B. If it is:
    
          i)   Is it large enough (check "size" from the
               mem_control_block)?
    
          ii)  If so:
    
               a. Mark it as unavailable
    
               b. Move past mem_control_block and return the
                  pointer
    
          iii) Otherwise:
    
               a. Move forward "size" bytes
    
               b. Go back go step 4
    
       C. Otherwise:
    
          i)   Move forward "size" bytes
    
          ii)  Go back to step 4
    
          

    我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:

    清单 6. 主分配程序
    
    
            
    void *malloc(long numbytes) {
    
    	/* Holds where we are looking in memory */
    
    	void *current_location;
    
    	/* This is the same as current_location, but cast to a
    	 * memory_control_block
    	 */
    
    	struct mem_control_block *current_location_mcb;
    
    	/* This is the memory location we will return.  It will
    	 * be set to 0 until we find something suitable
    	 */
    
    	void *memory_location;
    
    	/* Initialize if we haven't already done so */
    
    	if(! has_initialized) 	{
    
    		malloc_init();
    
    	}
    
    	/* The memory we search for has to include the memory
    	 * control block, but the users of malloc don't need
    	 * to know this, so we'll just add it in for them.
    	 */
    
    	numbytes = numbytes + sizeof(struct mem_control_block);
    
    	/* Set memory_location to 0 until we find a suitable
    	 * location
    	 */
    
    	memory_location = 0;
    
    	/* Begin searching at the start of managed memory */
    
    	current_location = managed_memory_start;
    
    	/* Keep going until we have searched all allocated space */
    
    	while(current_location != last_valid_address)
    
    	{
    
    		/* current_location and current_location_mcb point
    		 * to the same address.  However, current_location_mcb
    		 * is of the correct type, so we can use it as a struct.
    		 * current_location is a void pointer so we can use it
    		 * to calculate addresses.
    		 */
    
    		current_location_mcb =
    
    			(struct mem_control_block *)current_location;
    
    		if(current_location_mcb->is_available)
    
    		{
    
    			if(current_location_mcb->size >= numbytes)
    
    			{
    
    				/* Woohoo!  We've found an open,
    				 * appropriately-size location.
    				 */
    
    				/* It is no longer available */
    
    				current_location_mcb->is_available = 0;
    
    				/* We own it */
    
    				memory_location = current_location;
    
    				/* Leave the loop */
    
    				break;
    
    			}
    
    		}
    
    		/* If we made it here, it's because the Current memory
    		 * block not suitable; move to the next one
    		 */
    
    		current_location = current_location +
    
    			current_location_mcb->size;
    
    	}
    
    	/* If we still don't have a valid location, we'll
    	 * have to ask the operating system for more memory
    	 */
    
    	if(! memory_location)
    
    	{
    
    		/* Move the program break numbytes further */
    
    		sbrk(numbytes);
    
    		/* The new memory will be where the last valid
    		 * address left off
    		 */
    
    		memory_location = last_valid_address;
    
    		/* We'll move the last valid address forward
    		 * numbytes
    		 */
    
    		last_valid_address = last_valid_address + numbytes;
    
    		/* We need to initialize the mem_control_block */
    
    		current_location_mcb = memory_location;
    
    		current_location_mcb->is_available = 0;
    
    		current_location_mcb->size = numbytes;
    
    	}
    
    	/* Now, no matter what (well, except for error conditions),
    	 * memory_location has the address of the memory, including
    	 * the mem_control_block
    	 */
    
    	/* Move the pointer past the mem_control_block */
    
    	memory_location = memory_location + sizeof(struct mem_control_block);
    
    	/* Return the pointer */
    
    	return memory_location;
    
     }
    
          

    这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。

    运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc()free() 才是最主要的函数):

    清单 7. 编译分配程序
    
    
    
            
    gcc -shared -fpic malloc.c -o malloc.so
    
          

    该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。

    在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下:

    清单 8. 替换您的标准的 malloc
    
    
            
    LD_PRELOAD=/path/to/malloc.so
    
    export LD_PRELOAD
    
          

    LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。

    如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。

    我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括:

    • 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。
    • 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。
    • 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。
    • 它没有实现很多其他的内存函数,比如 realloc()
    • 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。
    • 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。
    • 分配程序不是线程安全的。
    • 分配程序不能将空闲空间拼合为更大的内存块。
    • 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。
    • 我确信还有很多其他问题。这就是为什么它只是一个例子!

    其他 malloc 实现
    malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:

    • 分配的速度。
    • 回收的速度。
    • 有线程的环境的行为。
    • 内存将要被用光时的行为。
    • 局部缓存。
    • 簿记(Bookkeeping)内存开销。
    • 虚拟内存环境中的行为。
    • 小的或者大的对象。
    • 实时保证。

    每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。

    还有其他许多分配程序可以使用。其中包括:

    • Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
    • BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。
    • Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。

    众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。

    在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。

    基于 malloc() 的内存管理的缺点
    不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。

    因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。

    半自动内存管理策略

    引用计数
    引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。

    在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。

    这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。

    要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。

    一个示例引用计数函数集可能看起来如下所示:

    清单 9. 基本的引用计数函数
    
    
            
    /* Structure Definitions*/
    
    /* Base structure that holds a refcount */
    
    struct refcountedstruct
    
    {
    
    	int refcount;
    
    }
    
    /* All refcounted structures must mirror struct
     * refcountedstruct for their first variables
     */
    
    /* Refcount maintenance functions */
    
    /* Increase reference count */
    
    void REF(void *data)
    
    {
    
    	struct refcountedstruct *rstruct;
    
    	rstruct = (struct refcountedstruct *) data;
    
    	rstruct->refcount++;
    
    }
    
    /* Decrease reference count */
    
    void UNREF(void *data)
    
    {
    
    	struct refcountedstruct *rstruct;
    
    	rstruct = (struct refcountedstruct *) data;
    
    	rstruct->refcount--;
    
    	/* Free the structure if there are no more users */
    
    	if(rstruct->refcount == 0)
    
    	{
    
    		free(rstruct);
    
    	}
    
    }
    
          

    REFUNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。

    当使用 REFUNREF 时,您需要遵守这些指针的分配规则:

    • UNREF 分配前左端指针(left-hand-side pointer)指向的值。
    • REF 分配后左端指针(left-hand-side pointer)指向的值。

    在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则:

    • 在函数的起始处 REF 每一个指针。
    • 在函数的结束处 UNREF 第一个指针。

    以下是一个使用引用计数的生动的代码示例:

    清单 10. 使用引用计数的示例
    
    
            
    /* EXAMPLES OF USAGE */
    
    
    /* Data type to be refcounted */
    
    struct mydata
    
    {
    
    	int refcount; /* same as refcountedstruct */
    
    	int datafield1; /* Fields specific to this struct */
    
    	int datafield2;
    
    	/* other declarations would go here as appropriate */
    
    };
    
    
    /* Use the functions in code */
    
    void dosomething(struct mydata *data)
    
    {
    
    	REF(data);
    
    	/* Process data */
    
    	/* when we are through */
    
    	UNREF(data);
    
    }
    
    
    struct mydata *globalvar1;
    
    /* Note that in this one, we don't decrease the
     * refcount since we are maintaining the reference
     * past the end of the function call through the
     * global variable
     */
    
    void storesomething(struct mydata *data)
    
    {
    
    	REF(data); /* passed as a parameter */
    
    	globalvar1 = data;
    
    	REF(data); /* ref because of Assignment */
    
    	UNREF(data); /* Function finished */
    
    }
    
          

    由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 mallocfree 等低层的分配程序来实际地分配和释放它们的内存。

    在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处:

    • 实现简单。
    • 易于使用。
    • 由于引用是数据结构的一部分,所以它有一个好的缓存位置。

    不过,它也有其不足之处:

    • 要求您永远不要忘记调用引用计数函数。
    • 无法释放作为循环数据结构的一部分的结构。
    • 减缓几乎每一个指针的分配。
    • 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 trysetjmp()/ longjmp())时,您必须采取其他方法。
    • 需要额外的内存来处理引用。
    • 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。
    • 在多线程环境中更慢也更难以使用。

    C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的“Smart Pointers”那一章。

    内存池
    内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。

    在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。

    要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。

    下面的假想代码列表展示了如何使用 obstack:

    清单 11. obstack 的示例代码
    
    
            
    #include <obstack.h>
    
    #include <stdlib.h>
    
    /* Example code listing for using obstacks */
    
    /* Used for obstack macros (xmalloc is
       a malloc function that exits if memory
       is exhausted */
    
    #define obstack_chunk_alloc xmalloc
    
    #define obstack_chunk_free free
    
    /* Pools */
    
    /* Only permanent allocations should go in this pool */
    
    struct obstack *global_pool;
    
    /* This pool is for per-connection data */
    
    struct obstack *connection_pool;
    
    /* This pool is for per-request data */
    
    struct obstack *request_pool;
    
    void allocation_failed()
    
    {
    
    	exit(1);
    
    }
    
    int main()
    
    {
    
    	/* Initialize Pools */
    
    	global_pool = (struct obstack *)
    
    		xmalloc (sizeof (struct obstack));
    
    	obstack_init(global_pool);
    
    	connection_pool = (struct obstack *)
    
    		xmalloc (sizeof (struct obstack));
    
    	obstack_init(connection_pool);
    
    	request_pool = (struct obstack *)
    
    		xmalloc (sizeof (struct obstack));
    
    	obstack_init(request_pool);
    
    	/* Set the error handling function */
    
    	obstack_alloc_failed_handler = &allocation_failed;
    
    	/* Server main loop */
    
    	while(1)
    
    	{
    
    		wait_for_connection();
    
    		/* We are in a connection */
    
    		while(more_requests_available())
    
    		{
    
    			/* Handle request */
    
    			handle_request();
    
    			/* Free all of the memory allocated
    
    			 * in the request pool
    
    			 */
    
    			obstack_free(request_pool, NULL);
    
    		}
    
    		/* We're finished with the connection, time
    
    		 * to free that pool
    
    		 */
    
    		obstack_free(connection_pool, NULL);
    
    	}
    
    }
    
    int handle_request()
    
    {
    
    	/* Be sure that all object allocations are allocated
    	 * from the request pool
    	 */
    
    	int bytes_i_need = 400;
    
    	void *data1 = obstack_alloc(request_pool, bytes_i_need);
    
    	/* Do stuff to process the request */
    
    	/* return */
    
    	return 0;
    
    }
    
          

    基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free()NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。

    使用池式内存分配的益处如下所示:

    • 应用程序可以简单地管理内存。
    • 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。
    • 可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。
    • 有非常易于使用的标准实现。

    池式内存的缺点是:

    • 内存池只适用于操作可以分阶段的程序。
    • 内存池通常不能与第三方库很好地合作。
    • 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。
    • 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。

    垃圾收集
    垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。

    收集器的类型

    • 复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。
    • 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。
    • 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。
    • 保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。

    Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。

    垃圾收集的一些优点:

    • 您永远不必担心内存的双重释放或者对象的生命周期。
    • 使用某些收集器,您可以使用与常规分配相同的 API。

    其缺点包括:

    • 使用大部分收集器时,您都无法干涉何时释放内存。
    • 在多数情况下,垃圾收集比其他形式的内存管理更慢。
    • 垃圾收集错误引发的缺陷难于调试。
    • 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。

    结束语
    一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。

    表 1. 内存分配策略的对比

    策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好
    定制分配程序 取决于实现 取决于实现 取决于实现 很难 取决于实现 取决于实现
    简单分配程序内存使用少时较快很快容易
    GNU malloc 容易
    Hoard 容易
    引用计数 N/A N/A 非常好 是(取决于 malloc 实现) 取决于实现
    非常快 极好 是(取决于 malloc 实现) 取决于实现
    垃圾收集 中(进行收集时慢) 几乎不
    增量垃圾收集 几乎不
    增量保守垃圾收集 容易 几乎不

  • 2005-10-10

    malloc实现

    /* Take from the IBM development formum*/

    /* Include the sbrk function */
    #include <unistd.h>

    int has_initialized = 0;
    void *managed_memory_start;
    void *last_valid_address;

    void malloc_init()
    {
    /* grab the last valid address from the OS */ 
    last_valid_address = sbrk(0);    

    /* we don't have any memory to manage yet, so
      *just set the beginning to be last_valid_address
      */ 
    managed_memory_start = last_valid_address;    

    /* Okay, we're initialized and ready to go */
      has_initialized = 1;  
    }

    struct mem_control_block {
    int is_available;
    int size;
    };

    void free(void *firstbyte) {
    struct mem_control_block *mcb; 

    /* Backup from the given pointer to find the
      * mem_control_block
      */
    mcb = firstbyte - sizeof(struct mem_control_block);  
    /* Mark the block as being available */
    mcb->is_available = 1;   
    /* That's It!  We're done. */
    return;  


    void *malloc(long numbytes) {
    /* Holds where we are looking in memory */
    void *current_location;

    /* This is the same as current_location, but cast to a
      * memory_control_block
      */
    struct mem_control_block *current_location_mcb; 

    /* This is the memory location we will return.  It will
      * be set to 0 until we find something suitable
      */ 
    void *memory_location; 

    /* Initialize if we haven't already done so */
    if(! has_initialized)  {
      malloc_init();
    }

    /* The memory we search for has to include the memory
      * control block, but the user of malloc doesn't need
      * to know this, so we'll just add it in for them.
      */
    numbytes = numbytes + sizeof(struct mem_control_block); 

    /* Set memory_location to 0 until we find a suitable
      * location
      */
    memory_location = 0; 

    /* Begin searching at the start of managed memory */
    current_location = managed_memory_start; 

    /* Keep going until we have searched all allocated space */
    while(current_location != last_valid_address) 
    {
      /* current_location and current_location_mcb point
       * to the same address.  However, current_location_mcb
       * is of the correct type so we can use it as a struct.
       * current_location is a void pointer so we can use it
       * to calculate addresses.
       */
      current_location_mcb =
       (struct mem_control_block *)current_location;

      if(current_location_mcb->is_available)
      {
       if(current_location_mcb->size >= numbytes)
       {
        /* Woohoo!  We've found an open,
         * appropriately-size location. 
         */

        /* It is no longer available */
        current_location_mcb->is_available = 0;

        /* We own it */
        memory_location = current_location;

        /* Leave the loop */
        break;
       }
      }

      /* If we made it here, it's because the Current memory
       * block not suitable, move to the next one
       */
      current_location = current_location +
       current_location_mcb->size;
    }

    /* If we still don't have a valid location, we'll
      * have to ask the operating system for more memory
      */
    if(! memory_location)
    {
      /* Move the program break numbytes further */
      sbrk(numbytes);

      /* The new memory will be where the last valid
       * address left off
       */
      memory_location = last_valid_address;

      /* We'll move the last valid address forward
       * numbytes
       */
      last_valid_address = last_valid_address + numbytes;

      /* We need to initialize the mem_control_block */
      current_location_mcb = memory_location;
      current_location_mcb->is_available = 0;
      current_location_mcb->size = numbytes;
    }

    /* Now, no matter what (well, except for error conditions),
      * memory_location has the address of the memory, including
      * the mem_control_block
      */

    /* Move the pointer past the mem_control_block */
    memory_location = memory_location + sizeof(struct mem_control_block);

    /* Return the pointer */
    return memory_location;
    }

  •  上一章费那么多唇舌讨论C语言的声明,其实目的都是为了这一章,期望读者通过对C语言声明形式的详细了解,树立声明嵌套的观念,因为C语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既著名又常用的方法。不过,右左法则其实并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的。右左法则的英文原文是这样说的:

    The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.


    这段英文的翻译如下:

    右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。

            笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个。

            现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:

    int (*func)(int *p);

    首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是int。

    int (*func)(int *p, int (*f)(int*));

    func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。

    int (*func[5])(int *p);

    func右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。


    int (*(*func)[5])(int *p);

    func被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下,就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。

    int (*(*func)(int *p))[5];

    func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。

    要注意有些复杂指针声明是非法的,例如:

    int func(void) [5];

    func是一个返回值为具有5个int元素的数组的函数。但C语言的函数返回值不能为数组,这是因为如果允许函数返回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C语言的数组名是一个右值,它不能作为左值来接收另一个数组,因此函数返回值不能为数组。

    int func[5](void);

    func是一个具有5个元素的数组,这个数组的元素都是函数。这也是非法的,因为数组的元素除了类型必须一样外,每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空间通常是不相同的。

            作为练习,下面列几个复杂指针声明给读者自己来解析,答案放在第十章里。

    int (*(*func)[5][6])[7][8];

    int (*(*(*func)(int *))[5])(int *);

    int (*(*func[7][8][9])(int*))[5];

            实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性,例如对于声明:

    int (*(*func)(int *p))[5];

    可以这样分解:

    typedef  int (*PARA)[5];
    typedef PARA (*func)(int *);

    这样就容易看得多了。
  •                    来源:天极网  作者网易郑兰

            概述

      C语言中有一种长度不确定的参数,形如:"…",它主要用在参数个数不确定的函数中,我们最容易想到的例子是printf函数。

      原型:

    int printf( const char *format [, argument]... );

      使用例:

    printf("Enjoy yourself everyday!\n");
    printf("The value is %d!\n", value);

      这种可变参数可以说是C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。

      注意:在C++中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。

      问题:printf的实现

      请问,如何自己实现printf函数,如何处理其中的可变参数问题? 答案与分析:

      在标准C语言中定义了一个头文件<stdarg.h>专门用来对付可变参数列表,它包含了一组宏,和一个va_list的typedef声明。一个典型实现如下:

    typedef char* va_list;
    #define va_start(list) list = (char*)&va_alist
    #define va_end(list)
    #define va_arg(list, mode)\
    ((mode*) (list += sizeof(mode)))[-1]
    自己实现printf:
    #include <stdarg.h>
    int printf(char* format, …)
    {
    va_list ap;
    va_start(ap, format);
    int n = vprintf(format, ap);
    va_end(ap);
    return n;
    }

      问题:运行时才确定的参数

      有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?

      答案与分析:

      目前没有"正规"的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是main(),它的原型是:

    int main(int argc,char *argv[]);

      函数的参数是argc和argv。

      深入想一下,"只能在运行时确定参数形式",也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个void *类型的参数,用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是main函数中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。

      虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。

      问题:可变长参数的传递

      有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现?

      答案与分析:

      目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数的参数为va_list类型,同时在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。看如下所示:

    void subfunc (char *fmt, va_list argp)
    {
    ...
    arg = va_arg (fmt, argp); /* 从argp中逐一取出所要的参数 */
    ...
    }

    void mainfunc (char *fmt, ...)
    {
    va_list argp;
    va_start (argp, fmt); /* 将可变长参数转换为va_list */
    subfunc (fmt, argp); /* 将va_list传递给子函数 */
    va_end (argp);
    ...
    }

      问题:可变长参数中类型为函数指针

      我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?

      答案与分析:

      这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下:

    #define va_arg(argp, type) \
    (*(type *)(((argp) += sizeof(type)) - sizeof(type)))

      其中,argp的类型是char *。

      如果你想用va_arg从可变参数列表中提取出函数指针类型的参数,例如

    int (*)(),则va_arg(argp, int (*)())被扩展为:
    (*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))

      显然,(int (*)() *)是无意义的。

      解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如:

    typedef int (*funcptr)();

      这时候再调用va_arg(argp, funcptr)将被扩展为:

    (* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))

      这样就可以通过编译检查了。

      问题:可变长参数的获取

      有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:

    va_arg (argp, float);

      这样做可以吗?

      答案与分析:

      不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。

      问题:定义可变长参数的一个限制

      为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?

    int f (...)
    {
    ...
    }

      答案与分析:

      不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。

      这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。

  • [转帖]可变参数学习笔记
    前言:本文在很大程度上改编自网友kevintz的"C语言中可变参数的用法"一文,在行文之前先向这位前辈表示真诚的敬意和感谢。
    一、什么是可变参数
    我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: 
    int printf( const char* format, ...); 
    它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点"…"做参数占位符),实际调用时可以有以下的形式: 
        printf("%d",i); 
        printf("%s",s); 
        printf("the number is %d ,string is:%s", i, s);    
    以上这些东西已为大家所熟悉。但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助.

    二、写一个简单的可变参数的C函数 
    先看例子程序。该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定. 在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值.
    函数代码如下:
    //示例代码1:可变参数函数的使用
    #include "stdio.h"
    #include "stdarg.h"
    void simple_va_fun(int start, ...) 

        va_list arg_ptr; 
        int nArgValue =start;
        int nArgCout=0;     //可变参数的数目
        va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
        do {
           ++nArgCout;
            printf("the %d th arg: %d\n",nArgCout,nArgValue);     //输出各参数的值
            nArgValue = va_arg(arg_ptr,int);                    //得到下一个可变参数的值
        } while(nArgValue != -1);                
        return; 
    }
    int main(int argc, char* argv[])
    {
        simple_va_fun(100,-1); 
        simple_va_fun(100,200,-1); 
        return 0;
    }

    下面解释一下这些代码

    从这个函数的实现可以看到,我们使用可变参数应该有以下步骤: 

    ⑴由于在程序中将用到以下这些宏: 
        void va_start( va_list arg_ptr, prev_param ); 
        type va_arg( va_list arg_ptr, type ); 
        void va_end( va_list arg_ptr ); 
    va在这里是variable-argument(可变参数)的意思. 
    这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.

    ⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。

    ⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数. 

    ⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。

    ⑸设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完这几个宏的内部实现机制后,自然就会明白。

    (二)可变参数在编译器中的处理 
    我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装目录下的\vc98\include\stdarg.h)
        typedef char *  va_list;
        #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
        #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
        #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
        #define va_end(ap)      ( ap = (va_list)0 )

    下面我们解释这些代码的含义:

    1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的

    2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。

    3、va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。 

    这里要知道两个事情:
        ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
        (2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
    |--------------------------|
    |  最后一个可变参数             |   ->高内存地址处
    |--------------------------|
    |--------------------------|
    |  第N个可变参数              |     ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
    |                               |     即第N个可变参数的地址。
    |--------------- |     
    |--------------------------|
    |  第一个可变参数               |     ->va_start(arg_ptr,start)后arg_ptr所指的地方
    |                               |     即第一个可变参数的地址
    |--------------- |     
    |------------------------ --|
    |                               |
    |  最后一个固定参数             |    -> start的起始地址
    |-------------- -|       .................
    |-------------------------- |
    |                               |  
    |--------------- |  -> 低内存地址处

    (4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
    因此,现在再来看va_arg()的实现就应该心中有数了:
        #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    这个宏做了两个事情,
       ①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
       ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。

    (5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

    (三)可变参数在编程中要注意的问题 
    因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢, 可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能 地识别不同参数的个数和类型. 有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数 printf是从固定参数format字符串来分析出参数的类型,再调用va_arg 的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 例如,在C的经典教材《the c programming language》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。

    (四)小结: 
    1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
    2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
    ⑴在固定参数中设标志-- printf函数就是用这个办法。后面也有例子。
    ⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.
    无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
    3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
    ①函数栈的生长方向
    ②参数的入栈顺序
    ③CPU的对齐方式
    ④内存地址的表达方式
    结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。
    4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。下面就是一个例子

    (五)扩展--自己实现简单的可变参数的函数。
    下面是一个简单的printf函数的实现,参考了<The C Programming Language>中的156页的例子,读者可以结合书上的代码与本文参照。
    #include "stdio.h"
    #include "stdlib.h"
    void myprintf(char* fmt, ...)        //一个简单的类似于printf的实现,//参数必须都是int 类型

        char* pArg=NULL;               //等价于原来的va_list 
        char c;
        pArg = (char*) &fmt;    //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
        pArg += sizeof(fmt);         //等价于原来的va_start          
       do
        {
            c =*fmt;
            if (c != '%')
            {
                putchar(c);            //照原样输出字符
            }
            else
    {
    //按格式字符输出数据
                switch(*++fmt) 
    {
                case 'd':
                    printf("%d",*((int*)pArg));           
                    break;
                case 'x':
                    printf("%#x",*((int*)pArg));
                    break;
                default:
                    break;
                } 
                pArg += sizeof(int);               //等价于原来的va_arg
            }
            ++fmt;
        }while (*fmt != '\0'); 
        pArg = NULL;                               //等价于va_end
        return; }
    int main(int argc, char* argv[])
    {
        int i = 1234;
        int j = 5678;
        myprintf("the first test:i=%d\n",i,j); 
        myprintf("the secend test:i=%d; %x;j=%d;\n",i,0xabcd,j); 
        system("pause");
        return 0;
    }
    在intel+win2k+vc6的机器执行结果如下:
    the first test:i=1234
    the secend test:i=1234; 0xabcd;j=5678;