shine 日志 - shine - 卡博博客站 - powered by X-Space

发布新日志

  • VB与Access连接的方法

    2007-04-19 04:16:00

    VB与Access连接的方法(Ado连接)

    一、建立数据库

      因为在Visual Basic 6.0中有的数据库连接方式不支持Access 2000版本格式的数据库,为了便于说明问题,本文所提的数据库以Access 97版本数据库为例。

      在Microsoft Access 97中建立一个数据库,如:ssgl.mdb,并设置密码,如:“1234”,再将数据库文件和VB中创建的工程文件放在同一目录下。

      如果用户的计算机上只有Access 2000的话,可以先在Access 2000中建立ssgl.mdb数据库,并设置密码,再用Access 2000中的“数据库实用工具”将数据库转换成Access 97版本的格式。

      当然也可以直接在Visual Basic 6.0集成开发环境中通过“可视化数据管理器”来创建数据库,再到Access 97中设置密码。

      通过对数据库文件设置密码,一般情况下,非法用户就不能用常规的手段打开数据库了,对数据库中的信息起到了一定的安全和保密作用。 

      二、连接加密的Access数据库

      在Visual Basic 6.0中,要建立与数据库的连接,可采用的技术手段很多,如:数据控件、数据对象、数据环境设计器等。开发人员可以根据自身的条件和用户的需求进行选择。

      限于篇幅,下面只介绍加密的Access数据库与没有加密的Access数据库在连接时的不同之处。关于没有加密的数据库的连接及访问的方法读者可以参阅其它资料。

      1、使用控件

      ① Data控件

      Data控件是Visual Basic 6.0中的一个内置数据控件,可以通过设置Data控件的connect、DatabaseName、RecordSource属性实现对数据库的连接和访问。 通过Data控件连接加密的数据库的方法有两种:

      一种方法是在设计状态时,在“属性窗口”中将Data控件的connect属性的缺省值”Access”改为”; pwd=1234”即可,其它属性的设置方法与没有加密的Access数据库的连接相同。

      另一种方法是在运行时,通过代码对connect属性赋值来实现。如:

    Data1.connect=”; pwd=1234”

    Data1.DatabaseName=APP.path + “\ssgl.mdb” 

      其中,”1234”为Access数据库文件ssgl.mdb的密码,下同。

      ②Adodc控件

      Adodc控件是一个ActiveX控件,它使用Microsoft ActiveX Data Objects(ADO)创建到数据库的连接。使用Adodc控件之前,要先将Adodc控件添加到控件工具箱中。方法如下:在VB 6.0种选择“工程”菜单,再点击“部件”菜单项,在弹出的“部件”对话框中选中“Microsoft ADO Data Control 6.0(OLEDB)”选项即可。

      通过Adodc控件连接加密的数据库的方法也有两种:

      一种方法是在设计状态时,在“属性窗口”中,对Adodc控件的ConnectionString属性设置一个有效的连接字符串,并在连接字符串后增加上”; Jet OLEDB: DataBase password=1234”,再设置Adodc控件的CommandType、RecordSource的属性就可以创建到加密的数据库的连接了。

      另一种方法是在运行时,通过代码动态地设置ConnectionString、CommandType和RecordSource属性来创建连接。 只要在ConnectionString属性的有效连接字符串后增加上”; Jet OLEDB: DataBase password=1234”即可。

      2、使用数据对象

      ① DAO数据对象

      要能正确引用DAO数据对象来建立与数据库的连接,应先在VB集成开发环境中选择“工程”菜单,再点击“引用”菜单项,在弹出的“引用”对话框选择“Microsoft DAO 3.51 Object Library”选项来添加DAO数据对象类型库。

      接下来就可用如下代码来建立到加密的Access数据库ssgl.mdb的连接。

    Dim db AS DataBase

    Set db=OpenDataBase(App.path + “\ssgl.mdb” , False , False , ”  pwd=1234”) 

      ② ADO数据对象

      ADO是Microsoft推出的处理关系数据库和非关系数据库中信息的最新技术,也是Microsoft推崇的用于数据连接和访问的技术。在VB 6.0中,Adodc控件、ADO数据对象及DataEnvironment(数据环境设计器)都采用的是ADO技术,因而它们处理加密的Access数据库的方法类似。

      要能正确引用ADO数据对象,应在VB 6.0集成开发环境中选择“工程”菜单,再点击“引用”菜单项,在弹出的“引用”对话框中选中“Microsoft ActiveX Data Objects 2.1 Library”选项来添加ADO数据对象类型库。

      可用如下代码来建立到加密的Access数据库ssgl.mdb的连接。

    Dim cnn AS ADODB.Connection

    Dim rst AS ADODB.Recordset

    Set cnn=New ADODB.Connection

    Cnn.Provider= ”Microsoft.Jet.OLEDB.3.51”

    Cnn.ConnectionString= ”Data Source=” & App.path & ”\ssgl.mdb;” & _

    ” Jet OLEDB: Database password=1234”

    cnn.Open 

      ③ 使用DataEnvironment(数据环境设计器)

      有两种方法可以通过DataEnvironment连接到加密的Access数据库:

      一种方法是在设计状态时,在DataEnvironment的connection对象的ConnectionSource属性的有效连接字符串后加上” 

    Jet OLEDB: Database password=1234” 

      另一种方法是在DataEnvironment_Initialize()事件中编写如下代码:

    Private sub DataEnvironment_Initialize( )

    Dim strconn AS string

    Strconn=” Provider=Microsoft.Jet.OLEDB.3.51;” & _

    ”Data Source=” & App.path & “\ssgl.mdb;” & _

    ”; Jet OLEDB: Database password=1234”

    DataEnvironment1.connection1.connectionstring=strconn

    End sub

  • VB中recordset的用法

    2007-04-19 04:14:00

    Set Rs = Server.CreateObject("ADODB.Recordset")
    Rs.Open Source, ActiveConnection, CursorType, LockType, Options
    参数
    Source 选择性参数:
      此 Variant 是为一个有效的 Command 对象变量名称、SQL 陈述式、数据表名称、已存的过程调用,或是一个保存的 Recordset 的檔名。
    ActiveConnection 选择性参数:
      不是 Variant 得到一个有效的 Connection 对象变量名称,就是 String 包含 ConnectionString 参数。
    CursorType 选择性参数:
      此 CursorTypeEnum 值决定提供者在开启 Recordset 时应使用的指标类型。其可以是下列其中一种常数。

    常数说明
    adOpenForwardOnly:开启一个顺向数据指针。(预设)
    AdOpenKeyset:开启一个索引键集 (keyset-type) 数据指针。
    AdOpenDynamic:开启一个动态数据指针。
    AdOpenStatic:开启一个静态数据指针。
    LockType 选择性参数:
      此 LockTypeEnum 值决定提供者在开启 Recordset 时应使用何种锁定 (同时性)。其可以是下列其中一种常数。

    常数说明
    adLockReadOnly:只读,数据无法变更。(预设)
    AdLockPessimistic:悲观锁定,提供者会进行必要的动作以确保能顺利编辑数据录,其方法通常是在编辑时立即在数据源处锁定数据录。
    AdLockOptimistic:乐观锁定,提供者使用乐观性锁定,当您呼叫 Update 方法时,仅锁定数据录。
    AdLockBatchOptimistic:乐观批次更新,此为批次更新模式所需,与实时更新模式相反。

    Options 选择性参数:
      一个 Long 值,表示提供者在 Source 自变量代表 Command 对象以外的东西时应如何评估它,否则 Recordset 应从前次储存的档案还原。它可以是下列其中一种常数。

    常数说明
    adCmdText:提供者会将 Source 评估为指令的文字定义。
    AdCmdTable:ADO 会产生一个 SQL 查询,从 Source 中指定的数据表传回所有数据列。
    AdCmdTableDirect:提供者会从 Source 中指定的数据表传回所有数据列。
    AdCmdStoredProc:提供者会将 Source 评估为一个预存程序。
    AdCmdUnknown:Source 自变量中未知的指令类型。
    AdCommandFile:保留的 (已储存的) Recordset 会从 Source 中指定的档案还原。
    AdExecuteAsync:Source 作异步执行。
    AdFetchAsync:表示在 CacheSize 属性中指定的初始数量被抓取后,剩下的数据列就会被异步地抓取。


    应用函数
    RecordSet.BOF            判断指标是否超过最前面
    RecordSet.EOF             判断指标是否超过最后面
    RecordSet.MoveFirst       将数据录指针移至第一笔
    RecordSet.MoveLast       将数据录指针移至最后一笔
    RecordSet.MoveNext       将数据指针往后移一笔
    RecordSet.MovePrevious       将数据指针往前移一笔
    RecordSet.Fields.Count       传回Recordset中的字段数
    RecordSet(i).Name             传回Recordset中第i个字段的名称
    RecordSet.RecordCount       传回Recordset中资料录的笔数
    RecordSet("字段名称")       传回指定字段名称的数据内容
    RecordSet(i)                   传回RecordSet中的第i个字段数据
    RecordSet.Fields(i).DefinedSize             传回RecordSet中的第i个字段数据域位长度
    RecordSet.Fields(i).Type             传回RecordSet中的第i个字段数据域位数据型别
    RecordSet.BookMark             传回设定的书签以储存现在纪录的位置。RecordSet.AbsolutePostition 将指标移至RecordSet中的某一笔数据上
    RecordSet.PageSize             设定每页显示的资料笔数
    RecordSet.PageCount             传回分页后的总页数
    RecordSet.AbsolutePage             传回目前所在的页数
    RecordSet.AddNew             新增数据至数据表中
    RecordSet.Update             更新目前这笔资料
    RecordSet.Delete             删除目前这笔资料
    RecordSet.Find             寻找数据值
    RecordSet.GetRows             可将Recordset中的数据储存至数组中
    RecordSet.Sort             可将Recordset中的数据排序

  • bn架设

    2007-03-31 05:30:00

    本手册架设的服务器可以支持《魔兽争霸3-冰封王座》1.20和《星际争霸》1.13-----暗黑的架设稍后放出

    需要用到的东西:
    1.wamp5_1.6.1(http://www.wampserver.com/ 包括Apache 2.0.55,mysql 5.0.18-nt ,php PHP 5.1.2 ,phpMyAdmin 2.7.0-pl2、SQLiteManager 1.1.3)

    2.PvPGN-1.8.0rc2-1-Win32和pvpgn-support-1.0(http://developer.berlios.de/projects/pvpgn/ 下载support files 1.0解压到files目录下)

    3.BNetEditor(方便修改和添加BN服务器)

    4.w3l Loader(封闭式BN引导程序)



    数据库的安装
    1.下载并安装wamp5_1.6.1,在安装好以后会自动跳出测试页。(很好的数据库套件,不过注意,默认的root是没有密码的!为了安全起见,记得要更改MYSQL的root的密码。)

    2.在phpMyAdmin的主页面建一个pvpgn的库----------“创建一个新的数据库”在它下面输入新的数据库的名字(比如pvpgn),点击下面的“创建”即可。完成后(会进入表创建页面,不去管他,点击上面说的“主目录”回到phpMyAdmin的主页面),你可以点击主页面的“数据库”链接看看那个库是不是已经建立好了

    3.给PGPGN创建一个自己的用户--------点击“权限”---〉“添加新用户”,进入新用户设置页面,用户名,你自己取一个。“主机”如果你的PVPGN和MYSQL在同一台主机,那就选“本地”,否则就选“任意主机”吧,如果你的PGPGN是固定IP的,也可以选择“使用文本域”,然后在后面的框里输入IP地址,密码,你自己设一个吧。下面的“全局权限”里什么都不用给,不过一般建议给一个“CREATE TEMPORARY TABLES”,让它可以在需要的时候创建临时库,点击最下面的执行,用户就添加完毕了

    4.给新添加的用户操作数据库的权利---------回到用户设置页面,用户设置页面的中部有一个“按数据库指定权限”,在下面的“在下列数据库添加权限:”下拉,选择刚才建的那个PVPGN的库,然后页面会自动跳到PVPGN的库的授权页面(注意看清楚了,页面的最上面的提示信息现在是“用户 '*****'@'localhost' - 数据库 pvpgn ”,表明是在对PVPGN这个库授权),这里就可以给他全部权限了(全部打钩),下面的表可以不用指定的。
    这里注意,到现在为止你还是用ROOT用户登录的,所以请登出。然后再用你的用户名和密码登入,就可以看见你设定的登录用户了在管理数据库了。



    PvPGN-1.8.0rc2-1-Win32的安装
    1.下载PvPGN-1.8.0rc2-1-Win32,解压到任意盘符(建议建立到D:\)

    2.从D:\wamp\mysql\bin文件夹下复制libmySQL.dll到pvpgn目录

    3.到D:\pvpgn-1.8.0rc2\conf下,修改bnetd.conf文件,找到storage_path = cdb:dir=var/userscdb;clan=var/clanscdb;default=conf/bnetd_default_user.cdb,在这行前加#号注释掉它。
    在底下添加storage_path = sql:mode=@@@@@;host=127.0.0.1;name=******;user=******;pass=******;default=0
    这里要注意“@@@@@”为你使用的数据库类型(我们这里应该填mysql);“******”分别为你刚才在MYSQL里面创建的那个数据库名、用户名、用户密码。
    找到w3routeaddr = "0.0.0.0:6200",将他改成你的IP地址(局域网/Internet只需在这里修改IP就行了)
    好了,到这里就设置完毕。第一次运行PGPGN.exe的时候,程序会自动在PVPGN这个库里建立需要的数据表的,我们就不用管了。



    bnetd.conf文件的修改和优化:

    注意:在bnetd.conf文件设置中,有3项必须修改:

    ################################################
    # Tracking server info #
    #----------------------------------------------------------------------------#
    # Set track=0 to disable tracking. Any other number will set number
    # of seconds between sending tracking packets. This is OFF by default.
    #track = 0
    track = 60
    # 10 minutes
    注意,令track = 0,否则有严重的track问题.


    ################################################
    # war3 ladder textual output #
    #-----------------------------------------------------------------------------#
    # this is for all the guys, that want Warcraft 3 ladder, but don't want their
    # server to run with MySQL support.
    # For each ladder (solo, team, ffa, at) a corresponing file is created,
    # so it's easy to build your ladder pages with them
    # the following value determines, at which rate, these files are created
    # set to 0 if you don't want or need these files

    war3_ladder_update_secs = 300

    # jfro's latest ladder is based on XML... so we can switch to XML output of ladder
    # on demand
    XML_output_ladder = false

    ladder排行榜的刷新时间,默认5分钟(300秒),如果你想快速更新可以适当改小一点。据说开启会严重影响性能,如果你不需要这个文件,可以设置=0


    ####################################################
    # server status textual output #
    #-----------------------------------------------------------------------------#
    # This is for writing status of the server in an attempt to see number of user
    # on line actually, and games/chans.
    # This is store in file var\status\warcraft3.dat as a *.ini format.
    # Shouldn't be so hard in php to create dynamic website using this content.
    # the following value determines, at which rate, these files are created
    # set to 0 if you don't want or need these files

    war3_output_update_secs = 60

    # jfro's latest ladder is based on XML... so we can switch to XML output of ladder
    # on demand. Maybe we should set update interval bigger cause XML output version
    # is much more verbose than the standard output
    XML_status_output_ladder = false

    注意,令output_update_secs = 0,否则严重影响性能.



    如何实现Ladder战网排行榜

    如果要实现Ladder,就必须要有pvpgn-stats。最新的版本是pvpgnstats2.44汉化版。

    PVPGN服务器架设好以后,在pvpgn数据库中,建立pvpgnstats的表文件
    在解压出来的pvpgnstats\SQL Files\zion文件夹下有2个sql文件,看pvpgn的表头类型,使用相应的sql文件。在pvpgn数据库中,选中sql查询建立表

    1.没有表头的,对应使用的sql文件为:bnet.sql

    2.对于PVPGN 1.80以后的版本,可能带有表头 pvpgn_ (可以在PVPGN的配置文件中查看,也可以在PVPGN
    数据库看到该表头)
    如果有,则这里也要做相应的修改$db_prefix = "pvpgn_";
    对应使用的sql文件为:pvpgn_bnet.sql

    3,修改pvpgnstats下的config.inc.php文件
    $site_name = "PvPGN server";
    $db_type = "mysql"; //数据库类型
    $db_host = "127.0.0.1"; //数据库IP地址,一般设置本地。
    $db_port = 3306; /* 3306 is the most common MySQL port */默认端口
    $db_database = "pvpgn"; //显示排行数据库名
    $db_user = ""; //pvpgn数据库用户名
    $db_pass = ""; //pvpgn数据库名对应密码(默认是没密码的,建议加上)
    $homepage = "http://127.0.0.1/"; //首页
    $ladderroot = "http://127.0.0.1/bbs/pvpgnstats2/"; //pvpgnstats路径,这里改在BBS下
    $pvpgn_dir = "d:/pvpgn-1.8.0rc2"; //pvpgn的路径
    $d2ladder_file = "d:/pvpgn-1.8.0rc2/var/ladders/ladder.D2DV"; //ladder.d2dv目录


    从战网里面进入排行榜:D:\pvpgn-1.8.0rc2\conf\anongame_infos.conf文件,将里面的网址改为你的排行榜地址(注意替换的时候选对编码格式,不然PVPGN服务器程序会出错!推荐用UltraEdit-32做修改,会提示你选择正确编码的。)



    修改PVPGN里面的conf信息,修改新闻,频道以及其他一些需要中文的地方

    新闻 改news.txt
    每日消息 改 bnmotd.txt

    要使用中文必须这样:
    用UltraEdit-32打开上述文件输入中文,然后选择utra edit32 的【文件】->【转换】->【unicode/ascii/utf-8转utf-8(ASCII编制)】然后保存就可以了。

    如果以后要编辑这个文件,则必须先选择utra edit32 的【文件】->【转换】->【UTF-8转unicode(I)】这样看起来是乱码的东西才会变成中文,修改后按前面的方法保存就可以了。



    如何进入架设好的封闭式BN战网:

    1.下载解压BNetEditor.zip和w3l Loader.rar到魔兽安装目录下并覆盖,启动BNetEditor.exe,添加服务器名****,服务器地址****。可以进行一下测试,看是否正常。

    2.运行W3L.exe进入游戏,选择BN.net。OK,恭喜你~~~~(不过记住:自己建的BN,自己是不能做主机的,要别的游戏玩家建立游戏你进才行。)



    没有固定IP的战网架设方法:

    用VNN或者花生壳之类的软件,然后在bnetd.conf里面将w3routeaddr = "0.0.0.0:6200"设置为你申请的虚拟域名

    用户添加战网IP的时候,把你的虚拟域名作为IP添加
  • 各种排序算法小结- -(转贴)

    2006-11-04 05:41:00

    排序算法是一种基本并且常用的算法。由于实际工作中处理的数量巨大,所以排序算法
    对算法本身的速度要求很高。
    而一般我们所谓的算法的性能主要是指算法的复杂度,一般用O方法来表示。在后面我将
    给出详细的说明。

    对于排序的算法我想先做一点简单的介绍,也是给这篇文章理一个提纲。
    我将按照算法的复杂度,从简单到难来分析算法。
    第一部分是简单排序算法,后面你将看到他们的共同点是算法复杂度为O(N*N)(因为没有
    使用word,所以无法打出上标和下标)。
    第二部分是高级排序算法,复杂度为O(Log2(N))。这里我们只介绍一种算法。另外还有几种
    算法因为涉及树与堆的概念,所以这里不于讨论。
    第三部分类似动脑筋。这里的两种算法并不是最好的(甚至有最慢的),但是算法本身比较
    奇特,值得参考(编程的角度)。同时也可以让我们从另外的角度来认识这个问题。
    第四部分是我送给大家的一个餐后的甜点——一个基于模板的通用快速排序。由于是模板函数
    可以对任何数据类型排序(抱歉,里面使用了一些论坛专家的呢称)。

    现在,让我们开始吧:

    一、简单排序算法
    由于程序比较简单,所以没有加什么注释。所有的程序都给出了完整的运行代码,并在我的VC环境
    下运行通过。因为没有涉及MFC和WINDOWS的内容,所以在BORLAND C++的平台上应该也不会有什么
    问题的。在代码的后面给出了运行过程示意,希望对理解有帮助。

    1.冒泡法:
    这是最原始,也是众所周知的最慢的算法了。他的名字的由来因为它的工作看来象是冒泡:
    #include <iostream.h>

    void BubbleSort(int* pData,int Count)
    {
    int iTemp;
    for(int i=1;i<Count;i++)
    {
    for(int j=Count-1;j>=i;j--)
    {
    if(pData[j]<pData[j-1])
    {
    iTemp = pData[j-1];
    pData[j-1] = pData[j];
    pData[j] = iTemp;
    }
    }
    }
    }

    void main()
    {
    int data[] = ;
    BubbleSort(data,7);
    for (int i=0;i<7;i++)
    cout<<data<<" ";
    cout<<"\n";
    }

    倒序(最糟情况)
    第一轮:10,9,8,7->10,9,7,8->10,7,9,8->7,10,9,8(交换3次)
    第二轮:7,10,9,8->7,10,8,9->7,8,10,9(交换2次)
    第一轮:7,8,10,9->7,8,9,10(交换1次)
    循环次数:6次
    交换次数:6次

    其他:
    第一轮:8,10,7,9->8,10,7,9->8,7,10,9->7,8,10,9(交换2次)
    第二轮:7,8,10,9->7,8,10,9->7,8,10,9(交换0次)
    第一轮:7,8,10,9->7,8,9,10(交换1次)
    循环次数:6次
    交换次数:3次

    上面我们给出了程序段,现在我们分析它:这里,影响我们算法性能的主要部分是循环和交换,
    显然,次数越多,性能就越差。从上面的程序我们可以看出循环的次数是固定的,为1+2+...+n-1。
    写成公式就是1/2*(n-1)*n。
    现在注意,我们给出O方法的定义:

    若存在一常量K和起点n0,使当n>=n0时,有f(n)<=K*g(n),则f(n) = O(g(n))。(呵呵,不要说没
    学好数学呀,对于编程数学是非常重要的!!!)

    现在我们来看1/2*(n-1)*n,当K=1/2,n0=1,g(n)=n*n时,1/2*(n-1)*n<=1/2*n*n=K*g(n)。所以f(n)
    =O(g(n))=O(n*n)。所以我们程序循环的复杂度为O(n*n)。
    再看交换。从程序后面所跟的表可以看到,两种情况的循环相同,交换不同。其实交换本身同数据源的
    有序程度有极大的关系,当数据处于倒序的情况时,交换次数同循环一样(每次循环判断都会交换),
    复杂度为O(n*n)。当数据为正序,将不会有交换。复杂度为O(0)。乱序时处于中间状态。正是由于这样的
    原因,我们通常都是通过循环次数来对比算法。


    2.交换法:
    交换法的程序最清晰简单,每次用当前的元素一一的同其后的元素比较并交换。
    #include <iostream.h>
    void ExchangeSort(int* pData,int Count)
    {
    int iTemp;
    for(int i=0;i<Count-1;i++)
    {
    for(int j=i+1;j<Count;j++)
    {
    if(pData[j]<pData)
    {
    iTemp = pData;
    pData = pData[j];
    pData[j] = iTemp;
    }
    }
    }
    }

    void main()
    {
    int data[] = ;
    ExchangeSort(data,7);
    for (int i=0;i<7;i++)
    cout<<data<<" ";
    cout<<"\n";
    }
    倒序(最糟情况)
    第一轮:10,9,8,7->9,10,8,7->8,10,9,7->7,10,9,8(交换3次)
    第二轮:7,10,9,8->7,9,10,8->7,8,10,9(交换2次)
    第一轮:7,8,10,9->7,8,9,10(交换1次)
    循环次数:6次
    交换次数:6次

    其他:
    第一轮:8,10,7,9->8,10,7,9->7,10,8,9->7,10,8,9(交换1次)
    第二轮:7,10,8,9->7,8,10,9->7,8,10,9(交换1次)
    第一轮:7,8,10,9->7,8,9,10(交换1次)
    循环次数:6次
    交换次数:3次

    从运行的表格来看,交换几乎和冒泡一样糟。事实确实如此。循环次数和冒泡一样
    也是1/2*(n-1)*n,所以算法的复杂度仍然是O(n*n)。由于我们无法给出所有的情况,所以
    只能直接告诉大家他们在交换上面也是一样的糟糕(在某些情况下稍好,在某些情况下稍差)。

    3.选择法:
    现在我们终于可以看到一点希望:选择法,这种方法提高了一点性能(某些情况下)
    这种方法类似我们人为的排序习惯:从数据中选择最小的同第一个值交换,在从省下的部分中
    选择最小的与第二个交换,这样往复下去。
    #include <iostream.h>
    void SelectSort(int* pData,int Count)
    {
    int iTemp;
    int iPos;
    for(int i=0;i<Count-1;i++)
    {
    iTemp = pData;
    iPos = i;
    for(int j=i+1;j<Count;j++)
    {
    if(pData[j]<iTemp)
    {
    iTemp = pData[j];
    iPos = j;
    }
    }
    pData[iPos] = pData;
    pData = iTemp;
    }
    }

    void main()
    {
    int data[] = ;
    SelectSort(data,7);
    for (int i=0;i<7;i++)
    cout<<data<<" ";
    cout<<"\n";
    }
    倒序(最糟情况)
    第一轮:10,9,8,7->(iTemp=9)10,9,8,7->(iTemp=8)10,9,8,7->(iTemp=7)7,9,8,10(交换1次)
    第二轮:7,9,8,10->7,9,8,10(iTemp=8)->(iTemp=8)7,8,9,10(交换1次)
    第一轮:7,8,9,10->(iTemp=9)7,8,9,10(交换0次)
    循环次数:6次
    交换次数:2次

    其他:
    第一轮:8,10,7,9->(iTemp=8)8,10,7,9->(iTemp=7)8,10,7,9->(iTemp=7)7,10,8,9(交换1次)
    第二轮:7,10,8,9->(iTemp=8)7,10,8,9->(iTemp=8)7,8,10,9(交换1次)
    第一轮:7,8,10,9->(iTemp=9)7,8,9,10(交换1次)
    循环次数:6次
    交换次数:3次
    遗憾的是算法需要的循环次数依然是1/2*(n-1)*n。所以算法复杂度为O(n*n)。
    我们来看他的交换。由于每次外层循环只产生一次交换(只有一个最小值)。所以f(n)<=n
    所以我们有f(n)=O(n)。所以,在数据较乱的时候,可以减少一定的交换次数。


    4.插入法:
    插入法较为复杂,它的基本工作原理是抽出牌,在前面的牌中寻找相应的位置插入,然后继续下一张
    #include <iostream.h>
    void InsertSort(int* pData,int Count)
    {
    int iTemp;
    int iPos;
    for(int i=1;i<Count;i++)
    {
    iTemp = pData;
    iPos = i-1;
    while((iPos>=0) && (iTemp<pData[iPos]))
    {
    pData[iPos+1] = pData[iPos];
    iPos--;
    }
    pData[iPos+1] = iTemp;
    }
    }

    void main()
    {
    int data[] = ;
    InsertSort(data,7);
    for (int i=0;i<7;i++)
    cout<<data<<" ";
    cout<<"\n";
    }

    倒序(最糟情况)
    第一轮:10,9,8,7->9,10,8,7(交换1次)(循环1次)
    第二轮:9,10,8,7->8,9,10,7(交换1次)(循环2次)
    第一轮:8,9,10,7->7,8,9,10(交换1次)(循环3次)
    循环次数:6次
    交换次数:3次

    其他:
    第一轮:8,10,7,9->8,10,7,9(交换0次)(循环1次)
    第二轮:8,10,7,9->7,8,10,9(交换1次)(循环2次)
    第一轮:7,8,10,9->7,8,9,10(交换1次)(循环1次)
    循环次数:4次
    交换次数:2次

    上面结尾的行为分析事实上造成了一种假象,让我们认为这种算法是简单算法中最好的,其实不是,
    因为其循环次数虽然并不固定,我们仍可以使用O方法。从上面的结果可以看出,循环的次数f(n)<=
    1/2*n*(n-1)<=1/2*n*n。所以其复杂度仍为O(n*n)(这里说明一下,其实如果不是为了展示这些简单
    排序的不同,交换次数仍然可以这样推导)。现在看交换,从外观上看,交换次数是O(n)(推导类似
    选择法),但我们每次要进行与内层循环相同次数的‘=’操作。正常的一次交换我们需要三次‘=’
    而这里显然多了一些,所以我们浪费了时间。

    最终,我个人认为,在简单排序算法中,选择法是最好的。


    二、高级排序算法:
    高级排序算法中我们将只介绍这一种,同时也是目前我所知道(我看过的资料中)的最快的。
    它的工作看起来仍然象一个二叉树。首先我们选择一个中间值middle程序中我们使用数组中间值,然后
    把比它小的放在左边,大的放在右边(具体的实现是从两边找,找到一对后交换)。然后对两边分别使
    用这个过程(最容易的方法——递归)。

    1.快速排序:
    #include <iostream.h>

    void run(int* pData,int left,int right)
    {
    int i,j;
    int middle,iTemp;
    i = left;
    j = right;
    middle = pData[(left+right)/2]; //求中间值
    do{
    while((pData<middle) && (i<right))//从左扫描大于中值的数
    i++;
    while((pData[j]>middle) && (j>left))//从右扫描大于中值的数
    j--;
    if(i<=j)//找到了一对值
    {
    //交换
    iTemp = pData;
    pData = pData[j];
    pData[j] = iTemp;
    i++;
    j--;
    }
    }while(i<=j);//如果两边扫描的下标交错,就停止(完成一次)

    //当左边部分有值(left<j),递归左半边
    if(left<j)
    run(pData,left,j);
    //当右边部分有值(right>i),递归右半边
    if(right>i)
    run(pData,i,right);
    }

    void QuickSort(int* pData,int Count)
    {
    run(pData,0,Count-1);
    }

    void main()
    {
    int data[] = ;
    QuickSort(data,7);
    for (int i=0;i<7;i++)
    cout<<data<<" ";
    cout<<"\n";
    }

    这里我没有给出行为的分析,因为这个很简单,我们直接来分析算法:首先我们考虑最理想的情况
    1.数组的大小是2的幂,这样分下去始终可以被2整除。假设为2的k次方,即k=log2(n)。
    2.每次我们选择的值刚好是中间值,这样,数组才可以被等分。
    第一层递归,循环n次,第二层循环2*(n/2)......
    所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n
    所以算法复杂度为O(log2(n)*n)
    其他的情况只会比这种情况差,最差的情况是每次选择到的middle都是最小值或最大值,那么他将变
    成交换法(由于使用了递归,情况更糟)。但是你认为这种情况发生的几率有多大??呵呵,你完全
    不必担心这个问题。实践证明,大多数的情况,快速排序总是最好的。
    如果你担心这个问题,你可以使用堆排序,这是一种稳定的O(log2(n)*n)算法,但是通常情况下速度要慢
    于快速排序(因为要重组堆)。

    三、其他排序
    1.双向冒泡:
    通常的冒泡是单向的,而这里是双向的,也就是说还要进行反向的工作。
    代码看起来复杂,仔细理一下就明白了,是一个来回震荡的方式。
    写这段代码的作者认为这样可以在冒泡的基础上减少一些交换(我不这么认为,也许我错了)。
    反正我认为这是一段有趣的代码,值得一看。
    #include <iostream.h>
    void Bubble2Sort(int* pData,int Count)
    {
    int iTemp;
    int left = 1;
    int right =Count -1;
    int t;
    do
    {
    //正向的部分
    for(int i=right;i>=left;i--)
    {
    if(pData<pData[i-1])
    {
    iTemp = pData;
    pData = pData[i-1];
    pData[i-1] = iTemp;
    t = i;
    }
    }
    left = t+1;

    //反向的部分
    for(i=left;i<right+1;i++)
    {
    if(pData<pData[i-1])
    {
    iTemp = pData;
    pData = pData[i-1];
    pData[i-1] = iTemp;
    t = i;
    }
    }
    right = t-1;
    }while(left<=right);
    }

    void main()
    {
    int data[] = ;
    Bubble2Sort(data,7);
    for (int i=0;i<7;i++)
    cout<<data<<" ";
    cout<<"\n";
    }


    2.SHELL排序
    这个排序非常复杂,看了程序就知道了。
    首先需要一个递减的步长,这里我们使用的是9、5、3、1(最后的步长必须是1)。
    工作原理是首先对相隔9-1个元素的所有内容排序,然后再使用同样的方法对相隔5-1个元素的排序
    以次类推。
    #include <iostream.h>
    void ShellSort(int* pData,int Count)
    {
    int step[4];
    step[0] = 9;
    step[1] = 5;
    step[2] = 3;
    step[3] = 1;

    int iTemp;
    int k,s,w;
    for(int i=0;i<4;i++)
    {
    k = step;
    s = -k;
    for(int j=k;j<Count;j++)
    {
    iTemp = pData[j];
    w = j-k;//求上step个元素的下标
    if(s ==0)
    {
    s = -k;
    s++;
    pData[s] = iTemp;
    }
    while((iTemp<pData[w]) && (w>=0) && (w<=Count))
    {
    pData[w+k] = pData[w];
    w = w-k;
    }
    pData[w+k] = iTemp;
    }
    }
    }

    void main()
    {
    int data[] = ;
    ShellSort(data,12);
    for (int i=0;i<12;i++)
    cout<<data<<" ";
    cout<<"\n";
    }
    呵呵,程序看起来有些头疼。不过也不是很难,把s==0的块去掉就轻松多了,这里是避免使用0
    步长造成程序异常而写的代码。这个代码我认为很值得一看。
    这个算法的得名是因为其发明者的名字D.L.SHELL。依照参考资料上的说法:“由于复杂的数学原因
    避免使用2的幂次步长,它能降低算法效率。”另外算法的复杂度为n的1.2次幂。同样因为非常复杂并
    “超出本书讨论范围”的原因(我也不知道过程),我们只有结果了。


    四、基于模板的通用排序:
    这个程序我想就没有分析的必要了,大家看一下就可以了。不明白可以在论坛上问。
    MyData.h文件
    ///////////////////////////////////////////////////////
    class CMyData
    {
    public:
    CMyData(int Index,char* strData);
    CMyData();
    virtual ~CMyData();

    int m_iIndex;
    int GetDataSize(){ return m_iDataSize; };
    const char* GetData(){ return m_strDatamember; };
    //这里重载了操作符:
    CMyData& operator =(CMyData &SrcData);
    bool operator <(CMyData& data );
    bool operator >(CMyData& data );

    private:
    char* m_strDatamember;
    int m_iDataSize;
    };
    ////////////////////////////////////////////////////////

    MyData.cpp文件
    ////////////////////////////////////////////////////////
    CMyData::CMyData():
    m_iIndex(0),
    m_iDataSize(0),
    m_strDatamember(NULL)
    {
    }

    CMyData::~CMyData()
    {
    if(m_strDatamember != NULL)
    delete[] m_strDatamember;
    m_strDatamember = NULL;
    }

    CMyData::CMyData(int Index,char* strData):
    m_iIndex(Index),
    m_iDataSize(0),
    m_strDatamember(NULL)
    {
    m_iDataSize = strlen(strData);
    m_strDatamember = new char[m_iDataSize+1];
    strcpy(m_strDatamember,strData);
    }

    CMyData& CMyData::operator =(CMyData &SrcData)
    {
    m_iIndex = SrcData.m_iIndex;
    m_iDataSize = SrcData.GetDataSize();
    m_strDatamember = new char[m_iDataSize+1];
    strcpy(m_strDatamember,SrcData.GetData());
    return *this;
    }

    bool CMyData::operator <(CMyData& data )
    {
    return m_iIndex<data.m_iIndex;
    }

    bool CMyData::operator >(CMyData& data )
    {
    return m_iIndex>data.m_iIndex;
    }
    ///////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    //主程序部分
    #include <iostream.h>
    #include "MyData.h"

    template <class T>
    void run(T* pData,int left,int right)
    {
    int i,j;
    T middle,iTemp;
    i = left;
    j = right;
    //下面的比较都调用我们重载的操作符函数
    middle = pData[(left+right)/2]; //求中间值
    do{
    while((pData<middle) && (i<right))//从左扫描大于中值的数
    i++;
    while((pData[j]>middle) && (j>left))//从右扫描大于中值的数
    j--;
    if(i<=j)//找到了一对值
    {
    //交换
    iTemp = pData;
    pData = pData[j];
    pData[j] = iTemp;
    i++;
    j--;
    }
    }while(i<=j);//如果两边扫描的下标交错,就停止(完成一次)

    //当左边部分有值(left<j),递归左半边
    if(left<j)
    run(pData,left,j);
    //当右边部分有值(right>i),递归右半边
    if(right>i)
    run(pData,i,right);
    }

    template <class T>
    void QuickSort(T* pData,int Count)
    {
    run(pData,0,Count-1);
    }

    void main()
    {
    CMyData data[] = {
    CMyData(8,"xulion"),
    CMyData(7,"sanzoo"),
    CMyData(6,"wangjun"),
    CMyData(5,"VCKBASE"),
    CMyData(4,"jacky2000"),
    CMyData(3,"cwally"),
    CMyData(2,"VCUSER"),
    CMyData(1,"isdong")
    };
    QuickSort(data,8);
    for (int i=0;i<8;i++)
    cout<<data.m_iIndex<<" "<<data.GetData()<<"\n";
    cout<<"\n";
    }
    ----------------------------------
    以上系转贴。

  • 堆和栈的区别

    2006-11-04 05:38:00

    堆和栈的区别 (转贴)

    非本人作也!因非常经典,所以收归旗下,与众人阅之!原作者不祥!

    堆和栈的区别
    一、预备知识—程序的内存分配
    一个由c/C++编译的程序占用的内存分为以下几个部分
    1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    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); 
    分配得来得10和20字节的区域就在堆区。 
    strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 


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


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

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


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

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

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



    windows进程中的内存结构


    在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。 

    接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。 

    首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码: 

    #include <stdio.h> 

    int g1=0, g2=0, g3=0; 

    int main() 

    static int s1=0, s2=0, s3=0; 
    int v1=0, v2=0, v3=0; 

    //打印出各个变量的内存地址 

    printf("0x%08x\n",&v1); //打印各本地变量的内存地址 
    printf("0x%08x\n",&v2); 
    printf("0x%08x\n\n",&v3); 
    printf("0x%08x\n",&g1); //打印各全局变量的内存地址 
    printf("0x%08x\n",&g2); 
    printf("0x%08x\n\n",&g3); 
    printf("0x%08x\n",&s1); //打印各静态变量的内存地址 
    printf("0x%08x\n",&s2); 
    printf("0x%08x\n\n",&s3); 
    return 0; 

    编译后的执行结果是: 

    0x0012ff78 
    0x0012ff7c 
    0x0012ff80 

    0x004068d0 
    0x004068d4 
    0x004068d8 

    0x004068dc 
    0x004068e0 
    0x004068e4 

    输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈 (stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。 


    ├———————┤低端内存区域 
    │ …… │ 
    ├———————┤ 
    │ 动态数据区 │ 
    ├———————┤ 
    │ …… │ 
    ├———————┤ 
    │ 代码区 │ 
    ├———————┤ 
    │ 静态数据区 │ 
    ├———————┤ 
    │ …… │ 
    ├———————┤高端内存区域 


    堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码: 

    #include <stdio.h> 

    void __stdcall func(int param1,int param2,int param3) 

    int var1=param1; 
    int var2=param2; 
    int var3=param3; 
    printf("0x%08x\n",?m1); //打印出各个变量的内存地址 
    printf("0x%08x\n",?m2); 
    printf("0x%08x\n\n",?m3); 
    printf("0x%08x\n",&var1); 
    printf("0x%08x\n",&var2); 
    printf("0x%08x\n\n",&var3); 
    return; 

    int main() 

    func(1,2,3); 
    return 0; 

    编译后的执行结果是: 

    0x0012ff78 
    0x0012ff7c 
    0x0012ff80 

    0x0012ff68 
    0x0012ff6c 
    0x0012ff70 


    ├———————┤<—函数执行时的栈顶(ESP)、低端内存区域 
    │ …… │ 
    ├———————┤ 
    │ var 1 │ 
    ├———————┤ 
    │ var 2 │ 
    ├———————┤ 
    │ var 3 │ 
    ├———————┤ 
    │ RET │ 
    ├———————┤<—“__cdecl”函数返回后的栈顶(ESP) 
    │ parameter 1 │ 
    ├———————┤ 
    │ parameter 2 │ 
    ├———————┤ 
    │ parameter 3 │ 
    ├———————┤<—“__stdcall”函数返回后的栈顶(ESP) 
    │ …… │ 
    ├———————┤<—栈底(基地址 EBP)、高端内存区域 


    上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码: 

    ;--------------func 函数的汇编代码------------------- 

    :00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间 
    :00401003 8B442410 mov eax, dword ptr [esp+10] 
    :00401007 8B4C2414 mov ecx, dword ptr [esp+14] 
    :0040100B 8B542418 mov edx, dword ptr [esp+18] 
    :0040100F 89442400 mov dword ptr [esp], eax 
    :00401013 8D442410 lea eax, dword ptr [esp+10] 
    :00401017 894C2404 mov dword ptr [esp+04], ecx 

    ……………………(省略若干代码) 

    :00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间 
    :00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间 
    ;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复 

    ;-------------------函数结束------------------------- 


    ;--------------主程序调用func函数的代码-------------- 

    :00401080 6A03 push 00000003 //压入参数param3 
    :00401082 6A02 push 00000002 //压入参数param2 
    :00401084 6A01 push 00000001 //压入参数param1 
    :00401086 E875FFFFFF call 00401000 //调用func函数 
    ;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C” 

    聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码: 

    #include <stdio.h> 
    #include <string.h> 

    void __stdcall func() 

    char lpBuff[8]="\0"; 
    strcat(lpBuff,"AAAAAAAAAAA"); 
    return; 

    int main() 

    func(); 
    return 0; 

    编译后执行一下回怎么样?哈,“"0x00414141"指令引用的"0x00000000"内存。该内存不能为"read"。”,“非法操作”喽! "41"就是"A"的16进制的ASCII码了,那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节,算进结尾的\0,那 strcat最多只能写入7个"A",但程序实际写入了11个"A"外加1个\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要着个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。 


    ├———————┤<—低端内存区域 
    │ …… │ 
    ├———————┤<—由exploit填入数据的开始 
    │ │ 
    │ buffer │<—填入无用的数据 
    │ │ 
    ├———————┤ 
    │ RET │<—指向shellcode,或NOP指令的范围 
    ├———————┤ 
    │ NOP │ 
    │ …… │<—填入的NOP指令,是RET可指向的范围 
    │ NOP │ 
    ├———————┤ 
    │ │ 
    │ shellcode │ 
    │ │ 
    ├———————┤<—由exploit填入数据的结束 
    │ …… │ 
    ├———————┤<—高端内存区域 


    windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码: 

    #include <stdio.h> 
    #include <iostream.h> 
    #include <windows.h> 

    void func() 

    char *buffer=new char[128]; 
    char bufflocal[128]; 
    static char buffstatic[128]; 
    printf("0x%08x\n",buffer); //打印堆中变量的内存地址 
    printf("0x%08x\n",bufflocal); //打印本地变量的内存地址 
    printf("0x%08x\n",buffstatic); //打印静态变量的内存地址 

    void main() 

    func(); 
    return; 

    程序执行结果为: 

    0x004107d0 
    0x0012ff04 
    0x004068c0 

    可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数: 

    HeapAlloc 在堆中申请内存空间 
    HeapCreate 创建一个新的堆对象 
    HeapDestroy 销毁一个堆对象 
    HeapFree 释放申请的内存 
    HeapWalk 枚举堆对象的所有内存块 
    GetProcessHeap 取得进程的默认堆对象 
    GetProcessHeaps 取得进程所有的堆对象 
    LocalAlloc 
    GlobalAlloc 

    当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间: 

    HANDLE hHeap=GetProcessHeap(); 
    char *buff=HeapAlloc(hHeap,0,8); 

    其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧: 

    #pragma comment(linker,"/entry:main") //定义程序的入口 
    #include <windows.h> 

    _CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf 
    /*--------------------------------------------------------------------------- 
    写到这里,我们顺便来复习一下前面所讲的知识: 
    (*注)printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。 
    由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是 __stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。 
    ---------------------------------------------------------------------------*/ 
    void main() 

    HANDLE hHeap=GetProcessHeap(); 
    char *buff=HeapAlloc(hHeap,0,0x10); 
    char *buff2=HeapAlloc(hHeap,0,0x10); 
    HMODULE hMsvcrt=LoadLibrary("msvcrt.dll"); 
    printf=(void *)GetProcAddress(hMsvcrt,"printf"); 
    printf("0x%08x\n",hHeap); 
    printf("0x%08x\n",buff); 
    printf("0x%08x\n\n",buff2); 

    执行结果为: 

    0x00130000 
    0x00133100 
    0x00133118 

    hHeap 的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。 

    最后来说说内存中的数据对齐。所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86 CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果: 

    #include <stdio.h> 

    int main() 

    int a; 
    char b; 
    int c; 
    printf("0x%08x\n",&a); 
    printf("0x%08x\n",&b); 
    printf("0x%08x\n",&c); 
    return 0; 

    这是用VC编译后的执行结果: 
    0x0012ff7c 
    0x0012ff7b 
    0x0012ff80 
    变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。 

    这是用Dev-C++编译后的执行结果: 
    0x0022ff7c 
    0x0022ff7b 
    0x0022ff74 
    变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。 

    这是用lcc编译后的执行结果: 
    0x0012ff6c 
    0x0012ff6b 
    0x0012ff64 
    变量在内存中的顺序:同上。 

    三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存哦。 


    基础知识: 
    堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针,EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从 EIP寄存器中读取下一条指令的内存地址,然后继续执行。 


    参考:《Windows下的HEAP溢出及其利用》by: isno 
    《windows核心编程》by: Jeffrey Richter 



    摘要: 讨论常见的堆性能问题以及如何防范它们。(共 9 页)

    前言
    您是否是动态分配的 C/C++ 对象忠实且幸运的用户?您是否在模块间的往返通信中频繁地使用了“自动化”?您的程序是否因堆分配而运行起来很慢?不仅仅您遇到这样的问题。几乎所有项目迟早都会遇到堆问题。大家都想说,“我的代码真正好,只是堆太慢”。那只是部分正确。更深入理解堆及其用法、以及会发生什么问题,是很有用的。

    什么是堆?
    (如果您已经知道什么是堆,可以跳到“什么是常见的堆性能问题?”部分)

    在程序中,使用堆来动态分配和释放对象。在下列情况下,调用堆操作: 

    事先不知道程序所需对象的数量和大小。


    对象太大而不适合堆栈分配程序。
    堆使用了在运行时分配给代码和堆栈的内存之外的部分内存。下图给出了堆分配程序的不同层。

    GlobalAlloc/GlobalFree:Microsoft Win32 堆调用,这些调用直接与每个进程的默认堆进行对话。

    LocalAlloc/LocalFree:Win32 堆调用(为了与 Microsoft Windows NT 兼容),这些调用直接与每个进程的默认堆进行对话。

    COM 的 IMalloc 分配程序(或 CoTaskMemAlloc / CoTaskMemFree):函数使用每个进程的默认堆。自动化程序使用“组件对象模型 (COM)”的分配程序,而申请的程序使用每个进程堆。

    C/C ++ 运行时 (CRT) 分配程序:提供了 malloc() 和 free() 以及 new 和 delete 操作符。如  Microsoft Visual Basic 和 Java 等语言也提供了新的操作符并使用垃圾收集来代替堆。CRT 创建自己的私有堆,驻留在  Win32 堆的顶部。

    Windows NT 中,Win32 堆是 Windows NT 运行时分配程序周围的薄层。所有 API 转发它们的请求给 NTDLL。

    Windows NT 运行时分配程序提供 Windows NT 内的核心堆分配程序。它由具有 128 个大小从 8 到 1,024 字节的空闲列表的前端分配程序组成。后端分配程序使用虚拟内存来保留和提交页。

    在图表的底部是“虚拟内存分配程序”,操作系统使用它来保留和提交页。所有分配程序使用虚拟内存进行数据的存取。

    分配和释放块不就那么简单吗?为何花费这么长时间?

    堆实现的注意事项
    传统上,操作系统和运行时库是与堆的实现共存的。在一个进程的开始,操作系统创建一个默认堆,叫做“进程堆”。如果没有其他堆可使用,则块的分配使用“进程堆”。语言运行时也能在进程内创建单独的堆。(例如,C 运行时创建它自己的堆。)除这些专用的堆外,应用程序或许多已载入的动态链接库 (DLL) 之一可以创建和使用单独的堆。Win32 提供一整套 API 来创建和使用私有堆。有关堆函数(英文)的详尽指导,请参见 MSDN。

    当应用程序或 DLL 创建私有堆时,这些堆存在于进程空间,并且在进程内是可访问的。从给定堆分配的数据将在同一个堆上释放。(不能从一个堆分配而在另一个堆释放。)

    在所有虚拟内存系统中,堆驻留在操作系统的“虚拟内存管理器”的顶部。语言运行时堆也驻留在虚拟内存顶部。某些情况下,这些堆是操作系统堆中的层,而语言运行时堆则通过大块的分配来执行自己的内存管理。不使用操作系统堆,而使用虚拟内存函数更利于堆的分配和块的使用。

    典型的堆实现由前、后端分配程序组成。前端分配程序维持固定大小块的空闲列表。对于一次分配调用,堆尝试从前端列表找到一个自由块。如果失败,堆被迫从后端(保留和提交虚拟内存)分配一个大块来满足请求。通用的实现有每块分配的开销,这将耗费执行周期,也减少了可使用的存储空间。

    Knowledge Base  文章 Q10758,“用 calloc() 和 malloc() 管理内存” (搜索文章编号), 包含了有关这些主题的更多背景知识。另外,有关堆实现和设计的详细讨论也可在下列著作中找到:“Dynamic Storage Allocation:  A Survey and Critical Review”,作者 Paul R. Wilson、Mark S. Johnstone、 Michael Neely 和 David Boles; “International Workshop on Memory Management”, 作者 Kinross, Scotland, UK,  1995 年 9 月(http://www.cs.utexas.edu/users/oops/papers.html)(英文)。

    Windows NT  的实现(Windows NT 版本 4.0 和更新版本) 使用了 127 个大小从 8 到 1,024 字节的 8 字节对齐块空闲列表和一个“大块”列表。“大块”列表(空闲列表[0]) 保存大于 1,024 字节的块。空闲列表容纳了用双向链表链接在一起的对象。默认情况下,“进程堆”执行收集操作。(收集是将相邻空闲块合并成一个大块的操作。)收集耗费了额外的周期,但减少了堆块的内部碎片。

    单一全局锁保护堆,防止多线程式的使用。(请参见“Server Performance and Scalability Killers”中的第一个注意事项, George Reilly 所著,在 “MSDN Online Web Workshop”上(站点:http://msdn.microsoft.com/workshop/server/iis/tencom.asp(英文)。)单一全局锁本质上是用来保护堆数据结构,防止跨多线程的随机存取。若堆操作太频繁,单一全局锁会对性能有不利的影响。

    什么是常见的堆性能问题?
    以下是您使用堆时会遇到的最常见问题: 

    分配操作造成的速度减慢。光分配就耗费很长时间。最可能导致运行速度减慢原因是空闲列表没有块,所以运行时分配程序代码会耗费周期寻找较大的空闲块,或从后端分配程序分配新块。


    释放操作造成的速度减慢。释放操作耗费较多周期,主要是启用了收集操作。收集期间,每个释放操作“查找”它的相邻块,取出它们并构造成较大块,然后再把此较大块插入空闲列表。在查找期间,内存可能会随机碰到,从而导致高速缓存不能命中,性能降低。


    堆竞争造成的速度减慢。当两个或多个线程同时访问数据,而且一个线程继续进行之前必须等待另一个线程完成时就发生竞争。竞争总是导致麻烦;这也是目前多处理器系统遇到的最大问题。当大量使用内存块的应用程序或 DLL 以多线程方式运行(或运行于多处理器系统上)时将导致速度减慢。单一锁定的使用—常用的解决方案—意味着使用堆的所有操作是序列化的。当等待锁定时序列化会引起线程切换上下文。可以想象交叉路口闪烁的红灯处走走停停导致的速度减慢。 
    竞争通常会导致线程和进程的上下文切换。上下文切换的开销是很大的,但开销更大的是数据从处理器高速缓存中丢失,以及后来线程复活时的数据重建。

    堆破坏造成的速度减慢。造成堆破坏的原因是应用程序对堆块的不正确使用。通常情形包括释放已释放的堆块或使用已释放的堆块,以及块的越界重写等明显问题。(破坏不在本文讨论范围之内。有关内存重写和泄漏等其他细节,请参见 Microsoft Visual C++(R) 调试文档 。)


    频繁的分配和重分配造成的速度减慢。这是使用脚本语言时非常普遍的现象。如字符串被反复分配,随重分配增长和释放。不要这样做,如果可能,尽量分配大字符串和使用缓冲区。另一种方法就是尽量少用连接操作。
    竞争是在分配和释放操作中导致速度减慢的问题。理想情况下,希望使用没有竞争和快速分配/释放的堆。可惜,现在还没有这样的通用堆,也许将来会有。

    在所有的服务器系统中(如 IIS、MSProxy、DatabaseStacks、网络服务器、 Exchange 和其他), 堆锁定实在是个大瓶颈。处理器数越多,竞争就越会恶化。

    尽量减少堆的使用
    现在您明白使用堆时存在的问题了,难道您不想拥有能解决这些问题的超级魔棒吗?我可希望有。但没有魔法能使堆运行加快—因此不要期望在产品出货之前的最后一星期能够大为改观。如果提前规划堆策略,情况将会大大好转。调整使用堆的方法,减少对堆的操作是提高性能的良方。

    如何减少使用堆操作?通过利用数据结构内的位置可减少堆操作的次数。请考虑下列实例:

    struct ObjectA {
       // objectA 的数据 
    }

    struct ObjectB {
       // objectB 的数据 
    }

    // 同时使用 objectA 和 objectB

    //
    // 使用指针 
    //
    struct ObjectB {
       struct ObjectA * pObjA;
       // objectB 的数据 
    }

    //
    // 使用嵌入
    //
    struct ObjectB {
       struct ObjectA pObjA;
       // objectB 的数据 
    }

    //
    // 集合 – 在另一对象内使用 objectA 和 objectB
    //

    struct ObjectX {
       struct ObjectA  objA;
       struct ObjectB  objB;
    }

    避免使用指针关联两个数据结构。如果使用指针关联两个数据结构,前面实例中的对象 A 和 B 将被分别分配和释放。这会增加额外开销—我们要避免这种做法。


    把带指针的子对象嵌入父对象。当对象中有指针时,则意味着对象中有动态元素(百分之八十)和没有引用的新位置。嵌入增加了位置从而减少了进一步分配/释放的需求。这将提高应用程序的性能。


    合并小对象形成大对象(聚合)。聚合减少分配和释放的块的数量。如果有几个开发者,各自开发设计的不同部分,则最终会有许多小对象需要合并。集成的挑战就是要找到正确的聚合边界。


    内联缓冲区能够满足百分之八十的需要(aka 80-20 规则)。个别情况下,需要内存缓冲区来保存字符串/二进制数据,但事先不知道总字节数。估计并内联一个大小能满足百分之八十需要的缓冲区。对剩余的百分之二十,可以分配一个新的缓冲区和指向这个缓冲区的指针。这样,就减少分配和释放调用并增加数据的位置空间,从根本上提高代码的性能。


    在块中分配对象(块化)。块化是以组的方式一次分配多个对象的方法。如果对列表的项连续跟踪,例如对一个 {名称,值} 对的列表,有两种选择:选择一是为每一个“名称-值”对分配一个节点;选择二是分配一个能容纳(如五个)“名称-值”对的结构。例如,一般情况下,如果存储四对,就可减少节点的数量,如果需要额外的空间数量,则使用附加的链表指针。 
    块化是友好的处理器高速缓存,特别是对于 L1-高速缓存,因为它提供了增加的位置 —不用说对于块分配,很多数据块会在同一个虚拟页中。

    正确使用 _amblksiz。C 运行时 (CRT) 有它的自定义前端分配程序,该分配程序从后端(Win32 堆)分配大小为 _amblksiz 的块。将 _amblksiz 设置为较高的值能潜在地减少对后端的调用次数。这只对广泛使用 CRT 的程序适用。
    使用上述技术将获得的好处会因对象类型、大小及工作量而有所不同。但总能在性能和可升缩性方面有所收获。另一方面,代码会有点特殊,但如果经过深思熟虑,代码还是很容易管理的。

    其他提高性能的技术
    下面是一些提高速度的技术: 

    使用 Windows NT5 堆 
    由于几个同事的努力和辛勤工作,1998 年初 Microsoft Windows(R) 2000 中有了几个重大改进:

    改进了堆代码内的锁定。堆代码对每堆一个锁。全局锁保护堆数据结构,防止多线程式的使用。但不幸的是,在高通信量的情况下,堆仍受困于全局锁,导致高竞争和低性能。Windows 2000 中,锁内代码的临界区将竞争的可能性减到最小,从而提高了可伸缩性。


    使用 “Lookaside”列表。堆数据结构对块的所有空闲项使用了大小在 8 到 1,024 字节(以 8-字节递增)的快速高速缓存。快速高速缓存最初保护在全局锁内。现在,使用 lookaside 列表来访问这些快速高速缓存空闲列表。这些列表不要求锁定,而是使用 64 位的互锁操作,因此提高了性能。


    内部数据结构算法也得到改进。
    这些改进避免了对分配高速缓存的需求,但不排除其他的优化。使用  Windows NT5 堆评估您的代码;它对小于 1,024 字节 (1 KB) 的块(来自前端分配程序的块)是最佳的。GlobalAlloc () 和 LocalAlloc() 建立在同一堆上,是存取每个进程堆的通用机制。如果希望获得高的局部性能,则使用 Heap(R) API 来存取每个进程堆,或为分配操作创建自己的堆。如果需要对大块操作,也可以直接使用 VirtualAlloc() / VirtualFree() 操作。

    上述改进已在 Windows 2000 beta 2 和 Windows NT 4.0 SP4 中使用。改进后,堆锁的竞争率显著降低。这使所有  Win32 堆的直接用户受益。CRT 堆建立于 Win32 堆的顶部,但它使用自己的小块堆,因而不能从 Windows NT 改进中受益。(Visual C++ 版本 6.0 也有改进的堆分配程序。)

    使用分配高速缓存 
    分配高速缓存允许高速缓存分配的块,以便将来重用。这能够减少对进程堆(或全局堆)的分配/释放调用的次数,也允许最大限度的重用曾经分配的块。另外,分配高速缓存允许收集统计信息,以便较好地理解对象在较高层次上的使用。

    典型地,自定义堆分配程序在进程堆的顶部实现。自定义堆分配程序与系统堆的行为很相似。主要的差别是它在进程堆的顶部为分配的对象提供高速缓存。高速缓存设计成一套固定大小(如 32 字节、64 字节、128 字节等)。这一个很好的策略,但这种自定义堆分配程序丢失与分配和释放的对象相关的“语义信息”。 

    与自定义堆分配程序相反,“分配高速缓存”作为每类分配高速缓存来实现。除能够提供自定义堆分配程序的所有好处之外,它们还能够保留大量语义信息。每个分配高速缓存处理程序与一个目标二进制对象关联。它能够使用一套参数进行初始化,这些参数表示并发级别、对象大小和保持在空闲列表中的元素的数量等。分配高速缓存处理程序对象维持自己的私有空闲实体池(不超过指定的阀值)并使用私有保护锁。合在一起,分配高速缓存和私有锁减少了与主系统堆的通信量,因而提供了增加的并发、最大限度的重用和较高的可伸缩性。

    需要使用清理程序来定期检查所有分配高速缓存处理程序的活动情况并回收未用的资源。如果发现没有活动,将释放分配对象的池,从而提高性能。

    可以审核每个分配/释放活动。第一级信息包括对象、分配和释放调用的总数。通过查看它们的统计信息可以得出各个对象之间的语义关系。利用以上介绍的许多技术之一,这种关系可以用来减少内存分配。

    分配高速缓存也起到了调试助手的作用,帮助您跟踪没有完全清除的对象数量。通过查看动态堆栈返回踪迹和除没有清除的对象之外的签名,甚至能够找到确切的失败的调用者。

    MP 堆 
    MP  堆是对多处理器友好的分布式分配的程序包,在 Win32 SDK(Windows NT 4.0 和更新版本)中可以得到。最初由 JVert 实现,此处堆抽象建立在 Win32 堆程序包的顶部。MP 堆创建多个 Win32 堆,并试图将分配调用分布到不同堆,以减少在所有单一锁上的竞争。

    本程序包是好的步骤 —一种改进的 MP-友好的自定义堆分配程序。但是,它不提供语义信息和缺乏统计功能。通常将 MP 堆作为 SDK 库来使用。如果使用这个 SDK 创建可重用组件,您将大大受益。但是,如果在每个 DLL 中建立这个 SDK 库,将增加工作设置。

    重新思考算法和数据结构 
    要在多处理器机器上伸缩,则算法、实现、数据结构和硬件必须动态伸缩。请看最经常分配和释放的数据结构。试问,“我能用不同的数据结构完成此工作吗?”例如,如果在应用程序初始化时加载了只读项的列表,这个列表不必是线性链接的列表。如果是动态分配的数组就非常好。动态分配的数组将减少内存中的堆块和碎片,从而增强性能。

    减少需要的小对象的数量减少堆分配程序的负载。例如,我们在服务器的关键处理路径上使用五个不同的对象,每个对象单独分配和释放。一起高速缓存这些对象,把堆调用从五个减少到一个,显著减少了堆的负载,特别当每秒钟处理 1,000 个以上的请求时。

    如果大量使用“Automation”结构,请考虑从主线代码中删除“Automation BSTR”,或至少避免重复的 BSTR 操作。(BSTR 连接导致过多的重分配和分配/释放操作。)

    摘要
    对所有平台往往都存在堆实现,因此有巨大的开销。每个单独代码都有特定的要求,但设计能采用本文讨论的基本理论来减少堆之间的相互作用。 

    评价您的代码中堆的使用。


    改进您的代码,以使用较少的堆调用:分析关键路径和固定数据结构。


    在实现自定义的包装程序之前使用量化堆调用成本的方法。


    如果对性能不满意,请要求 OS 组改进堆。更多这类请求意味着对改进堆的更多关注。


    要求 C 运行时组针对 OS 所提供的堆制作小巧的分配包装程序。随着 OS 堆的改进,C 运行时堆调用的成本将减小。


    操作系统(Windows NT 家族)正在不断改进堆。请随时关注和利用这些改进。
    Murali Krishnan  是 Internet Information Server (IIS) 组的首席软件设计工程师。从 1.0 版本开始他就设计 IIS,并成功发行了 1.0 版本到 4.0 版本。Murali 组织并领导 IIS 性能组三年 (1995-1998), 从一开始就影响 IIS 性能。他拥有威斯康星州 Madison 大学的 M.S.和印度 Anna 大学的 B.S.。工作之外,他喜欢阅读、打排球和家庭烹饪。

  • java变量的存储

    2006-11-04 05:33:00

     (1)Registers(寄存器):在CPU内部,是最快的存储场所,但程序员无法直接控制。  
              (2)Stack(栈):位于一般的RAM(随机访问内存),速度仅次于Registers。存放基本类型的数据和对象的reference,但对象本身不存放在stack中,而是存放在Heap中。  
              (3)Heap(堆):也位于RAM,存放用new产生的数据。他比stack好在编译器不需要知道实际在heap中存储数据的大小,也不知道这个空间需要分配多长时间,弹性好,但分配空间的速度比Stack慢很多。  
              (4)Static   storage(静态存储空间):位于RAM,存放在对象中用static定义的静态成员,它是“程序执行期间”一直存在的数据,Java对象本身是不会被分配在这里的。  
              (5)Constant   storage(常量存储空间):存放常量。  
              (6)NON-RAM存储空间:硬盘等永久存储空间,实现对象的持久化存储。  
       
      java编程思想里面都有的
     
  • c位域

    2006-11-04 05:30:00

    有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有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行用指针方式输出了这三个域的值。
  • sizeof

    2006-11-04 05:28:00

    关键字: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;
    };

  • 孙鑫VC++讲座笔记-(2)C++

    2006-11-04 05:26:00

     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,引用:变量的别名。引用需要在定义的时候用一变量或对象初始化自己。引用一旦在定义的时候初始化,就维系在一个特定的变量或对象上。
     引用不占用物理内存(与定义引用的目标共用同一内存)。指针变量需要占用物理内存,用来存储地址。
  • C++中的虚函数(virtual function)

    2006-11-04 05:25:00

    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

  • C各种变量的存储机制、作用域规则以及初始化

    2006-11-04 05:19:00

    本文只作为个人防止忘记,作为基础资料来查阅所用。

    一、变量类型

    externel和internal简介:

    internel 用于描述定义在函数内部的函数变元和变量。外部变量在函数外部定义,故可以在很多函数中使用。由于C语言不允许在一个函数中定义其他函数,因此函数本身是外部的。缺省情况下,外部变量和函数具有如下性质:所有通过名字对外部变量和函数的引用都是引用同一个对象(即外部链接)。

    由于外部变量是可以全局访问的,这就为在函数之间交换数据提供了一种可以代替函数变元与返回值的方法。任何函数都可以用名字来访问外部变量,只要这个名字在某个地方已经做了说明。但是使用太多的外部变量,会导致对代码结构产生不好的影响,而且可能会使程序在各个函数之间产生太多的数据联系。

    外部变量的用途还表现在他们比内部变量有更大的作用域和更长的生存周期。

    1、自动变量(auto)

    自动变量只能在函数内部使用,当其所在函数开始调用时开始存在,当函数退出时消失。

    作用域规则:说明该自动变量的函数。对于函数参数也是如此,函数参数可看作局部变量。

    2、静态变量(static)

    存储机制:静态存储。

    static说明适用于外部变量和函数时,用于把这些对象的作用域限定为被编译源文件的剩余部分。

    static说明适用于内部变量时,和自动变量一样只能在该函数内部使用,但是与自动变量不同的是,不管其所在函数是否被调用,它都是一直存在的。即内部静态变量是一种只能在某一函数内部使用的但一直占据存储空间的变量。

    3、寄存器变量(register)

    register说明用于提醒编译程序所说明的变量在程序中使用频率较高。其思想是,将寄存器变量放在机器的寄存器中,这样使程序更小、执行速度更快。但编译程序可以忽略此选项。

    寄存器说明只适用于自动变量以及函数的形式参数。

    4、外部变量(extern)

    外部变量是永久保存的,他们的值在从一次函数调用到下次函数调用之间保持不变。因此如果两个函数必须共享某些数据,而且这两个函数有互不调用对方,那么把这些共享数据作为外部变量表示,比用函数变元更好。

    作用域规则:从其说明处开始,一直到其所在的被编译的文件的末尾。函数的作用域也是如此。另外,如果一个外部变量在定义之前就要被用到,或者这个外部变量定义在与所要使用的它的源文件不相同的源文件中,那么要在相应的变量说明中强制性使用关键字extern。

    将对外部变量的说明与定义严格区分开来非常重要,变量说明用于通报变量的性质(主要是变量的类型),而变量定义则除此以外还引起存储分配。

    例:如果在函数的外部有如下说明:

    int sp;

    double val[MAX_PATH];

    则这两个说明定义了两个外部变量sp和val,并为之分配了存储空间,同时也用作源文件其他部分使用的说明。

    而:

    extern int sp;

    extern double val[MAX_PATH];

    则:为源文件剩余部分说明了两个外部变量,但是这两个说明并没有建立变量或为他们分配存储空间。

    在一个源程序的所有源文件中对一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern说明来访问它。在外部变量的定义中必须指定数组的大小,但在extern说明中,则不一定要指定数组的大小。

    外部变量的初始化只能出现在其定义中。

    二、变量的初始化

    在没有显式初始化的情况下,外部变量和静态变量都被初始化为0。而自动变量和寄存器变量的初始值则没有定义。

    在定义纯量变量时,可以通过在所定义的变量名后面加一个“=”和一个表达式进行初始化。如:

    int i = 1;

    对于外部变量和局部变量,初始化符必须是常量表达式,初始化只作一次(从概念上讲是在程序开始执行之前进行初始化)。

    对于自动变量和寄存器变量,则在每当进入函数或分程序时进行初始化。初始化符不一定限定为常量,它可以是任何表达式,甚至可以包含函数调用。实际上,自动变量的初始化部分就是赋值语句的缩写。

    数组的初始化:

    也是通过说明中初始化符来完成的,数组初始化符用“{}”括住,并用逗号分隔。

    如果初始化符序列中的初始化符的个数比数组元素少时,那么对于没有得到初始化的数组元素在该数组为自动变量、外部变量、静态变量时都被初始化为0。



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

  • 我对C语言变量的一些认识

    2006-11-04 05:17:00

          从本质上讲,变量是程序中用来存放信息的一块空间。“这块空间”一词,还要加两个定语。一是所存放的内容是可以(通过代码来)改变的;二是它的地址是可以访问的(否则就无法用代码来访问它)。这就引出一连串与变量有关的许多问题。

           变量有两个属性。一是它的数据类型,就是这个空间能用来存放哪种类型的数据;二是它的时空属性。本文只涉及及后者。时就是变量的生命期,空就是变量的作用域。

           C中有两类变量:在任何函数外面定义的外部变量和在某函数内定义的自动变量。

           尽管在大多数C书里大谈特谈全局变量,但是,事实上,在C标准里,从没定义过全局变量。C的创建者这么安排,是有用意的。这体现了C创建者的一个重要思想:尽一切可能限制变量的作用域。他们想限制的还有变量的生命期。这一思想贯穿了整个C标准,也成为后来才出现的结构化程序设计思想的重要内容之一。

           外部变量是源文件级变量。就是说,它的作用域仅限于定义它的那个源文件。在源文件A里定义的一个外部变量,你要在源文件B里访问它,必须在文件B的开头用关键字extern声明它。注意定义和声明两个词,在经典的C书里,是很小心地区别开的。定义的实质是创建,本质上就是要申请一块存储空间。而声明不引发存储分配。这里的声明,只是使源文件A里的某变量的作用域延伸到源文件B里。

           在一个工程里,外部变量必须在一个源文件里定义,在其它也需访问它的每个源文件里要extern声明它。

           在工程里,如果你在某源文件里定义一个变量,并在其它所有源文件里声明它,这个变量就成了全局变量。因此,在C里,全局变量是靠你自己做出来的。