2004年09月26日

为了更好的理解这一问题的解决方法,我们需要先介绍一下.NET Framework的安全机制。然后再结合我们的实际问题来讨论解决方案。

  为了解决安全问题,.NET Framework提供了一种称为代码访问安全性的安全机制。代码访问安全性允许根据代码的来源和代码的标识等属性将代码设置为不同级别的信任代码,同时还详细定义了不同级别的对代码的信任,从而可以详细的对代码设置各自的权限而不是将最大权限赋给所有的代码。使用代码访问安全性,可以减小恶意代码或各种错误的代码带来的严重的系统安全性问题的可能性。您可以设置允许代码执行的一组操作,同样可以设置永远不允许代码执行的一组操作。

  实现代码访问安全性的基础就是JIT(运行时编译)和IL(中间代码)。所以所有以公共语言运行库为目标的托管代码都会受益于代码访问安全性。非托管代码则无法完全使用代码访问安全性。
下面我们将介绍一下代码访问安全性实现的各种功能:

  代码访问安全性是控制代码对受保护资源和操作的访问权限的一种机制。在 .NET Framework中,代码访问安全性执行下列功能:

  · 定义权限和权限集,它们表示访问各种系统资源的权限。

  · 使管理员能够通过将权限集与代码组关联来配置安全策略。

  · 使代码能够请求运行所需权限以及其他一些有用的权限,以及指定代码绝对不能拥有哪些权限。

  · 根据代码请求的权限和安全策略允许的操作,向加载的每个程序集授予权限。

  · 使代码能够要求其调用方拥有特定的权限。

  · 使代码能够要求其调用方拥有数字签名,从而只允许特定组织或特定站点的调用方来调用受保护的代码。

  · 通过将调用堆栈上每个调用方所授予的权限与调用方必须拥有的权限相比较,加强运行时对代码的限制。

  为了确定是否已授予代码相应的权限,.NET运行库的安全系统将遍历整个调用堆栈,将每个调用方所授予的权限与目前要求的权限相比较。如果调用堆栈中的任何调用方没有要求的权限,则会引发安全性异常,并会拒绝访问和相应的操作。堆栈步旨在防止引诱攻击;在这种攻击中,受信程度较低的代码调用高度信任的代码,并使用高度信任的代码执行未经授权的操作。在运行时要求所有调用方都拥有权限将影响性能,但对防止代码遭受攻击至关重要。若要优化性能,可以使代码执行较少的堆栈步;但是,任何时候这样做时均必须确保不会暴露安全缺陷。

  还存在另外一种代码访问安全性的常见用途,即应用程序将控件从网络 Web 站点直接下载到客户端,这种方式的代码安全性也是可以在客户端进行设置的,根据签名等数据权限证书来确定是不是可以允许下载的控件运行。这种方法类似于ActiveX的安全性设置,但是比之在设置权限更加详细和强大。同JAVA APPLET的沙箱安全机制相比,.NET 的客户端控件可以在本地简单设置后访问客户端的各种资源。由于这一方面的用途不是我们的重点,所以我们在这里就不再更详细的讨论其用途及其实现原理了。

  下面我们就谈谈如何应用这一安全特性来解决ASP.NET中存在的系统安全漏洞。由于我们介绍的系统是共享主机,所以有其特殊性,即系统管理员无法事先给所有的代码赋予相应的权限,因为每个用户都可能有各种权限要求,并且这些要求特殊权力的代码在使用中都可能出现的,所以在权限管理上随时都有各种要求。
因此在权限设置方面,不仅仅是管理员设置,也包括了各个共享主机用户的权限请求,这也正是安全代码机制的一个重要部分。

  请求权限是您让运行库知道代码执行有哪些操作权限的方法。通过将属性(声明式语法)放到代码的程序级范围来为程序集请求权限。

  请求内置权限的代码示例:

//The attribute is placed on the assembly level.
using System.Security.Permissions;
[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]

  将此段代码放在程序的开始部分(namespace声明之前),在编译时就会将请求的权限存储在程序集清单中。加载时,运行库检查权限请求,并应用安全策略规则来确定授予程序集哪些权限。

  虽然我们编写的大部分代码都没有请求权限,其实不管是共享主机形式还是独立服务器形式都应该请求权限,这是因为请求权限有助于确保只将代码需要的权限授予代码。如果没有授予代码额外权限,即使某些恶意代码想利用您的代码来进行安全性破坏,它也无法操作没有赋给您自己代码相应权限的额外系统资源。您只应该请求代码需要的那些权限,而不应请求更多权限。

  代码请求权限之后,系统管理员可以使用”权限查看”工具 (Permview.exe,位于您的.NET Framework的目录的bin目录下) 来检查您的程序集并根据其他条件来设置安全策略以决定是否给您的代码所请求的相应权限。如果您不显式地在代码中请求应用程序需要的权限,那么管理员将很难管理您的应用程序。在权限管理严格的主机上,将无法实现您的代码所要求的功能。

  请求权限会通知运行库应用程序正常运行需要哪些权限,或具体不需要哪些权限。在.NET Framework安装后的默认状态下,所有代码都是FullTrust(完全信任)的。这时是不需要申请任何权限的,但是管理员一旦修改了代码安全,我们使用的磁盘访问就要受到限制了,这是就需要申请相应的权限了。我们上边介绍的文件管理代码就需要具有本地硬盘读写操作的能力,则应用程序必须拥有 FileIOPermission。如果代码不请求 FileIOPermission,在本地安全设置不允许应用程序拥有此权限的主机上,在应用程序尝试磁盘操作时就会引发安全性异常。即使应用程序能够处理此异常,也不会允许它操作磁盘。当然,如果您的代码不访问受保护的资源或执行受保护的操作,则不必请求任何权限。例如,如果代码只根据向它传递的输入来计算结果而不使用任何资源,则不必请求权限。如果您的代码访问受保护的资源但未请求必要的权限,则仍可能允许它执行,但如果它尝试访问某种资源而它又没有必要的权限,则可能在执行过程中失败。

现在绝大多数的虚拟主机都禁用了 ASP 的标准组件:FileSystemObject,因为这个组件为 ASP 提供了强大的文件系统访问能力,可以对服务器硬盘上的任何文件进行读、写、复制、删除、改名等操作(当然,这是指在使用默认设置的 Windows NT / 2000 下才能做到)。但是禁止此组件后,引起的后果就是所有利用这个组件的 ASP 将无法运行,无法满足客户的需求。
  如何既允许 FileSystemObject 组件,又不影响服务器的安全性(即:不同虚拟主机用户之间不能使用该组件读写别人的文件)呢?这里介绍本人在实验中获得的一种方法,下文以 Windows 2000 Server 为例来说明。
  在服务器上打开资源管理器,用鼠标右键点击各个硬盘分区或卷的盘符,在弹出菜单中选择“属性”,选择“安全”选项卡,此时就可以看到有哪些帐号可以访问这个分区(卷)及访问权限。默认安装后,出现的是“Everyone”具有完全控制的权限。点“添加”,将“Administrators”、“Backup Operators”、“Power Users”、“Users”等几个组添加进去,并给予“完全控制”或相应的权限,注意,不要给“Guests”组、“IUSR_机器名”这几个帐号任何权限。然后将“Everyone”组从列表中删除,这样,就只有授权的组和用户才能访问此硬盘分区了,而 ASP 执行时,是以“IUSR_机器名”的身份访问硬盘的,这里没给该用户帐号权限,ASP 也就不能读写硬盘上的文件了。
  下面要做的就是给每个虚拟主机用户设置一个单独的用户帐号,然后再给每个帐号分配一个允许其完全控制的目录。
  如下图所示,打开“计算机管理”→“本地用户和组”→“用户”,在右栏中点击鼠标右键,在弹出的菜单中选择“新用户”:

  在弹出的“新用户”对话框中根据实际需要输入“用户名”、“全名”、“描述”、“密码”、“确认密码”,并将“用户下次登录时须更改密码”前的对号去掉,选中“用户不能更改密码”和“密码永不过期”。本例是给第一虚拟主机的用户建立一个匿名访问 Internet 信息服务的内置帐号“IUSR_VHOST1”,即:所有客户端使用 http://xxx.xxx.xxxx/ 访问此虚拟主机时,都是以这个身份来访问的。输入完成后点“创建”即可。可以根据实际需要,创建多个用户,创建完毕后点“关闭”:

现在新建立的用户已经出现在帐号列表中了,在列表中双击该帐号,以便进一步进行设置:

在弹出的“IUSR_VHOST1”(即刚才创建的新帐号)属性对话框中点“隶属于”选项卡:

刚建立的帐号默认是属于“Users”组,选中该组,点“删除”:

  现在出现的是如下图所示,此时再点“添加”:

在弹出的“选择 组”对话框中找到“Guests”,点“添加”,此组就会出现在下方的文本框中,然后点“确定”:

  出现的就是如下图所示的内容,点“确定”关闭此对话框:

  打开“Internet 信息服务”,开始对虚拟主机进行设置,本例中的以对“第一虚拟主机”设置为例进行说明,右击该主机名,在弹出的菜单中选择“属性”:

  弹出一个“第一虚拟主机 属性”的对话框,从对话框中可以看到该虚拟主机用户的使用的是“F:\VHOST1”这个文件夹:

  暂时先不管刚才的“第一虚拟主机 属性”对话框,切换到“资源管理器”,找到“F:\VHOST1”这个文件夹,右击,选“属性”→“安全”选项卡,此时可以看到该文件夹的默认安全设置是“Everyone”完全控制(视不同情况显示的内容不完全一样),首先将最将下的“允许将来自父系的可继承权限传播给该对象”前面的对号去掉:

此时会弹出如下图所示的“安全”警告,点“删除”:

 此时安全选项卡中的所有组和用户都将被清空(如果没有清空,请使用“删除”将其清空),然后点“添加”按钮。

  将如图中所示的“Administrator”及在前面所创建的新帐号“IUSR_VHOST1”添加进来,将给予完全控制的权限,还可以根据实际需要添加其他组或用户,但一定不要将“Guests”组、“IUSR_机器名”这些匿名访问的帐号添加上去!

  再切换到前面打开的“第一虚拟主机 属性”的对话框,打开“目录安全性”选项卡,点匿名访问和验证控制的“编辑”:

  在弹出的“验证方法”对方框(如下图所示),点“编辑”:

  弹出了“匿名用户帐号”,默认的就是“IUSR_机器名”,点“浏览”:

  在“选择 用户”对话框中找到前面创建的新帐号“IUSR_VHOST1”,双击:

 此时匿名用户名就改过来了,在密码框中输入前面创建时,为该帐号设置的密码:

  再确定一遍密码:

  OK,完成了,点确定关闭这些对话框。
  经此设置后,“第一虚拟主机”的用户,使用 ASP 的 FileSystemObject 组件也只能访问自己的目录:F:\VHOST1 下的内容,当试图访问其他内容时,会出现诸如“没有权限”、“硬盘未准备好”、“500 服务器内部错误”等出错提示了。
  另:如果该用户需要读取硬盘的分区容量及硬盘的序列号,那这样的设置将使其无法读取。如果要允许其读取这些和整个分区有关的内容,请右键点击该硬盘的分区(卷),选择“属性”→“安全”,将这个用户的帐号添加到列表中,并至少给予“读取”权限。由于该卷下的子目录都已经设置为“禁止将来自父系的可继承权限传播给该对象”,所以不会影响下面的子目录的权限设置。

此方法可以从根本上杜绝FSO的虚拟主机问题,昨天晚上我也试用了这一方法很方便的解决了FSO的问题。

在Microsoft .NET Framework Configuration中可以设置所有关于.NET Framework的属性。
  点击我的电脑,打开下拉菜单,我们可以看到程序集缓存、已配置程序集、远程处理服务、运行库安全策略、应用程序等五项。运行库安全策略设置是我们这篇文章的重点。
   
    我们可以先查看一下程序集缓存,在这里我们可以看到所有的全局程序集缓存,全局程序集缓存中存储了专门指定给由计算机中若干应用程序共享的程序集。在这里我们可以发现我们可以使用的所有的程序集,同时也可以添加和删除某些程序集。详细操作请参见.NET Framework SDK文档。
   
    我们在这里主要讨论的是运行库安全策略。在此策略中,按层次结构由高到低分为四个级别,即:企业、计算机、用户、应用程序。在计算权限授予时,运行库从该层次结构的顶部开始,然后向下进行计算。较低的策略级别不能对在较高级别上授予的权限进行增加,但是可以使权限减少。这就是说如果我们将计算机策略设置为较小的权限时,可以不必更改企业策略就可以使设置的权限生效,也就是说权限检查的顺序是从低级别到高级别,只有在低级别中不存在的设置才会检查上一级的设置。默认情况下,用户策略和应用程序域策略的限制性小于计算机策略和企业级策略。大部分默认策略存在于计算机级别。所以我们需要将默认安装的主机的权限在计算机级别上进行修改,修改的内容根据主机是不是共享主机,主机应用的其他不明代码的可能性来设置。如果是我们讨论的共享主机的话,在计算机级别上就尽量将权限设的小一些,为了避免我们讨论的文件系统安全问题,一定要注意权限中的本地磁盘访问权限。
   
    我们打开计算机策略设置可以发现几个默认的代码组、权限集和策略程序集。
   
    根据需要,我们可以添加代码组和自定义的权限集。
   
    在添加代码组的时候可以选择几种条件,主要的条件类型:默认为All Code、应用程序目录、哈希、强名称、作者、站点等。
   
    对于我们所要讨论的共享主机,我们需要将My_Computer_Zone下的All Code的权限更改为不能进行磁盘读写,在更改之前,我们需要先定义一个权限集。这一权限集的作用就是将我们需要点击权限集,右键快捷菜单中选择新建,会出现一个创建权限集的窗口,这里需要给我们新建的权限集命名。下一步就是将单个权限分配给权限集。如下图所示。

在这里我们可以给这个新建的权限集赋予一个的系统权限,如上图所示,可用的权限包括:目录服务、DNS、事件日志、环境变量、文件IO、OLEDB数据库操作、注册表等等。我们主要要说明的是文件IO操作,其他的权限操作可以根据自己的需求来设置。这里我们就不再说明了。
   
    在文件IO的权限设置中我们可以自定义针对每一个目录的权限,这里包括读、写、追加、路径盘等操作,在这里我们可以将我们需要的目录权限添加到列表中。因为我们是利用这一权限使所有没有配置权限的代码不可以进行文件IO操作,所以我们不强文件IO添加到分配的权限中。
   
    新建了这一权限集后,我们更改一下默认设置,即将All Code的权限设置为此新建的权限集,也就是说所有没有在此定义代码都不能访问文件IO系统。
   
    这里需要注意一件事情,因为Microsoft .NET Framework Configuration本身也需要文件IO权限,如果没有单独分配给Configuration一个文件IO操作权限的话,那么您就不能再次使用Configuration来设置权限了,只能重新安装.NET Framework了。所以我们需要将FullTrust权限分配给Configuration所使用的Dll,即mscorcfg.dll。在添加时,成员条件可以选择强名称,使用”导入”,到winnt/window .net/framework/versionnumber/下选择mscorcfg.dll。如果需要运行其他配置程序,还需要设置相应的权限,这些系统程序一般都在系统程序集缓存中。
   
    这样我们就完成了一个简单的设置,可以防止任何未经验证的代码访问文件IO系统。这样就从根本上防止了磁盘恶意操作。
   
    如果您今后需要利用这一功能或者有共享主机用户需要使用文件IO功能,那么您可以在Microsoft .NET Framework Configuration中将其加入代码,如果不能使其使用其他功能,可以仅仅设置一个只具有文件IO权限的权限集。如果是共享主机用户您还可以给他分配直接到其所使用的目录的全部读写权限,对于他的日志文档,您可以将读功能分配给用户。通过上边新建权限集时我们可以发现:权限集可以规定到每一个目录的读写权限,所以可以将用户锁定于其可以使用的目录中。当然对于共享主机提供商来说,最好的方法就是自己实现这些功能,然后配置权限系统使用户使用共享主机提供商的程序来实现他们的正常操作,而避免了恶意文件操作。
   
    需要注意的是如果分配给每一个单独的程序相应的权限时,我们最好使用强名称这一方式或者其他的可验证方式,强名称由程序集的标识–其简单文本名称、版本号和区域性信息(如果提供)–加上公钥和数字签名组成。这就需要我们使用Sn.exe 来设置密钥、签名和签名验证。强名称保证了程序是开发人员开发的并且没有被改动。
   
    在进行上面的设置之后,管理员可以根据用户的各种需求来设置不同的代码集和权限集。
   
    我们已经简单的介绍了一下ASP.NET中关于文件IO系统的漏洞的防治方法,这一方法有些繁琐,但是却可以从根本上杜绝一些漏洞,由于.NET的JIT(运行时编译)和IL(中间语言),.NET可以在程序编译时检查程序的安全性设置,所以能从根本上防止一些非法访问。.NET的代码安全性的内容很全面,我们讨论的只是很少的一部分,更多的功能需要大家共同来探索、学习。

2004年09月10日

    经常会碰到有人问如何保证程序只运行一个实例,原来我也零碎的给过两三个方法,今天干脆来个大总结,希望对大家在做程序设计的时候有所帮助。

    一个程序只运行一个实例(或限制实例数量)通常可以采用如下方法:

1)FindWindow 之<窗口标题>
    通过查找窗口标题来确定上一实例是否正在运行,不适合窗口标题动态变化的程序。

2)FindWindow 之<任务栏按纽标题>
    通过查找任务栏按纽标题来确定上一实例是否正在运行,不适合按纽标题动态变化的程序(如Winamp)。通常情况下,该方法还是优先考虑,因为按纽标题是一般是固定的。

3)Window Property
    将某个数据(可以是字符串或句柄)通过SetProp加入到指定窗口的property list,程序运行时枚举窗口并检查该数据是否存在来确定上一实例是否正在运行。

4)全局Atom
    将某个特定字符串通过GlobalAddAtom加入全局原子表(Global Atom Table),程序运行时检查该串是否存在来确定上一实例是否正在运行。该方法有个局限,就是程序终止前必须显式调用GlobalDeleteAtom来释放atom,否则该atom不会自动释放,如果程序运行时意外终结了,那么下一个实例就无法正常执行。早期版本的realplayer就存在这个现象,不知道是不是采用了该方法。

5)Mutex/Event/Semaphore
    通过互斥对象/信号量/事件等线程同步对象来确定实例是否存在,在NT下要注意权限问题(SID)。

6)DLL全局共享区域
    VC下的DLL工程可以通过下面代码来建立一个进程间共享数据段:
    #pragma data_seg(“.share”)
    //shared for all processes that attach to the dll
    DWORD dllgs_dwRunCount = 1; //一定要在这里对变量进行初始化,否则工夫白做!
    #pragma data_seg()
    #pragma comment(linker,”/section:.share,rws”)

    导出3个函数,分别为:
    DWORD IncRunCount(void); //运行计数器加1,返回计数器结果
    DWORD DecRunCount(void); //运行计数器减1,返回计数器结果
    DWORD GetRunCount(void); //取当前运行计数器

    由于DLL全局共享段在映射到各个进程地址空间时仅会被初始化一次,并且是在首次被windows加载时,所以利用该共享段数据就能对程序实例进行可靠计数。

7)内存映射文件(File Mapping)
    通过把程序实例信息(如窗口句柄、计数器等等)放置到跨进程的内存映射文件,同样可以控制程序实例运行的数量,道理与DLL全局共享区域类似。

8)其它
    曾经见过有人通过注册表、磁盘文件等途径来处理实例控制问题,但由于这些参考对象均为非易失性资源,在碰到程序非正常结束且没有清除实例标识时相当麻烦,真正使用起来具有很大的局限性。

    总结:前面三种方法适用于拥有窗体的程序,而后面几种则没有这个限制,但相对而言后者实现起来较复杂。不管采用哪种方法,参考对象均必须具有可共享、跨进程、易失性、重启自复位等必要性质。

写在前面:

  《C程序设计》可以说是一本再基础不过的编程书了,但每读一遍的感觉却都是不同的,可以说,每读一遍,都会有很多新的收获。真所谓老书再读,回味无穷啊!此笔记是《C程序设计》谭浩强编著,清华大学出版社出版。除了将书中的重点知识点记下来外,也加入了我对知识点的理解,我想这一点是读书笔记的重要性所在。

第一章 概述 第二章 数据类型、运算符与表达式
第三章 最简单的c程序设计 第四章 逻辑运算和判断选取控制
第五章 循环控制 第六章 数组
第七章 函数 第八章 预编译处理
第九章 指针 第十章 结构体与共用体
第十一章 位运算 第十二章 文件

第一章 概述

1. C语言的特点

①语言简洁、紧凑,使用方便、灵活。共有32个关键字,9种控制语句。
②运算符丰富,公有34种运算符。
③数据结构丰富,数据类型有:整型、实型、字符型、数组、指针、结构体、共用体等。
④具有结构化的控制语句(如if…else、while、do…while、switch、for)
⑤语法限制不太严格,程序设计自由度大。
⑥允许直接访问物理地址,能进行位(bit)操作,可以直接对硬件操作。
⑦生成目标代码质量高,程序执行效率高。
⑧可移植性好。

2. C语言的用途

C虽不擅长科学计算和管理领域,但对操作系统和系统实用程序以及对硬件进行操作方面,C有明显的优势。现在很多大型应用软件也用C编写。

Top of Page

第二章 数据类型、运算符与表达式

1. C的数据类型

C的数据类型包括:整型、字符型、实型或浮点型(单精度和双精度)、枚举类型、数组类型、结构体类型、共用体类型、指针类型和空类型。

2. 常量与变量

常量其值不可改变,符号常量名通常用大写。变量其值可以改变,变量名只能由字母、数字和下划线组成,且第一个字符必须为字母或下划线。否则为不合法的变量名。变量在编译时为其分配相应存储单元。

3. 整型数据

整型常量的表示方法:十进制不用说了,八进制以0开头,如0123,十六进制以0x开头,如0×1e。
整型变量分为:基本型(int)、短整型(short int)、长整型(long int)和无符号型。不同机器上各类数据所占内存字节数不同,一般int型为2个字节,long型为4个字节。

4. 实型数据

实型常量表示形式:十进制形式由数字和小数点组成(必须有小数点),如:0.12、.123、123.、0.0等。指数形式如123e3代表123×10的三次方。
实型变量分为单精度(float)和双精度(double)两类。在一般系统中float型占4字节,7位有效数字,double型占8字节,15~16位有效数字。

5. 字符型数据

字符变量用单引号括起来,如’a',’b'等。还有一些是特殊的字符常量,如’\n’,'\t’等。分别代表换行和横向跳格。
字符变量以char 来定义,一个变量只能存放一个字符常量。
字符串常量是由双引号括起来的字符序列。这里一定要注意’a'和”a”的不同,前者为字符常量,后者为字符串常量,c规定:每个字符串的结尾加一个结束标志’\0′,实际上”a”包含两个字符:’a'和’\0′。

6. 数值型数据间的混合运算

整型、字符型、实型数据间可以混合运算,运算时不同类型数据要转换成同一类型再运算,转换规则:
char,short -> int -> unsigned -> long -> double <- float

7. 运算符和表达式

c运算符包括:
算数运算符( + - * / % )
关系运算符( > < == >= <= != )
逻辑运算符( ! && || )
位运算符( << >> ~ | ^ & )
赋值运算符( = )
条件运算符( ? : )
逗号运算符( , )
指针运算符( * & )
求字节数( sizeof )
强制类型转换(类型)
分量运算符( . -> )
下标运算符( [ ] )
其它运算符( 如函数调用运算符( ) )
自增自减运算符( ++ — )注意:++i和i++的不同之处,++i使用i之前先使i加1,i++使用i之后,使i加1。
逗号表达式的求解过程:先求解表达式1,再求解表达式2,整个表达式的值是表达式2的值。

Top of Page

第三章 最简单的c程序设计

1.c的9种控制语句:

if() ~ else~
for()~
while()~
do~while()
continue
break
switch
goto
return
程序的三种基本结构:顺序结构,选择结构,循环结构

2.数据输出

c语言不提供输入输出语句,输入输出操作是由c的库函数完成。但要包含头文件stdio.h。
putchar( ) 向终端输出一个字符
printf( )的格式字符:
① d格式符 用来输出十进制整数
%d 按整型数据的实际长度输出
%md 使输出长度为m,如果数据长度小于m,则左补空格,如果大于m,则输出实际长度
%ld 输出长整型数据
② o格式符 以八进制形式输出整数
③ x格式符 以十六进制形式输出整数
④ u格式符 用来输出unsigned型数据,以十进制形式输出
⑤ c格式符 用来输出一个字符
⑥ s格式符 输出一个字符串
%s 输出实际长度字符串
%ms 输出的串占m列,如果串长度小于m,左补空格,如果大于m,实际输出
%-ms输出的串占m列,如果串长度小于m,右补空格,
%m.ns 输出占m列,但只取字符串中左端n个字符并靠右对齐
%-m.ns m、n含义同上,靠左对齐,如果n>m,则m自动取n值
⑦ f格式符 以小数形式输出实数
%f 整数部分全部输出,小数部分输出6位
%m.nf 输出数据共占m列,其中有n位小数。如果数值长度小于m,左补空格
%-m.nf 同上,右补空格
⑧ e格式符 以指数形式输出实数
%e 系统指定6位小数,5位指数(e+002 )
⑨ g格式符 输出实数,根据数值大小,自动选f格式或e格式

3.数据输入

getchar( ) 从终端输入一个字符
scanf( 格式控制,地址列表) 标准C scanf中不使用%u,对于unsigned型数据,以%d或%o或%x输入。%后的*,用来跳过它相应的数据。输入数据时不能规定精度如scanf( “%7.2f”, &a );是不合法的。

Top of Page

第四章 逻辑运算和判断选取控制

1. 关系运算符:

c提供6种关系运算符(> < <= >= == != )前四种优先级高于后两种。

2. If语句

C提供了三种形式的if语句
If(表达式) 语句
If(表达式) 语句1 else 语句2
If(表达式1) 语句1
Else if(表达式2) 语句2

else 语句n

3. 条件运算符

(a>b)?a:b 条件为真,表达式取值a,否则取值b

4. Switch语句

Switch(表达式)
{
case 常量表达式1:语句1; break;
case 常量表达式2:语句2; break;
   …
case 常量表达式n:语句n; break;
  default :语句n+1;
}

Top of Page

第五章 循环控制

1. 几种循环语句

goto语句(现已很少使用)
while语句 先判断表达式后执行语句
do-while语句 先执行语句后判断表达式
for语句

2. Break语句和continue语句

Break语句用于跳出循环,continue用于结束本次循环。

Top of Page

第六章 数组

1. 一维数组

c规定只有静态存储(static)和外部存储(extern)数组才能初始化。给数组初始化时可以不指定数组长度。

2. 二维数组

3. 字符数组

部分字符串处理函数
puts(字符数组) 将一个字符串输出到终端。
gets(字符数组) 从终端输入一个字符串到字符数组,并且得到一个函数值,为该字符数组的首地址
strcat(字符数组1,字符数组2) 连接两个字符数组中的字符串,数组1必须足够大。
Strcpy(字符数组1,字符串2)  将字符串2拷贝到字符数组1中。
Strcmp(字符串1,字符串2) 比较字符串,相等返回0,字符串1>字符串2,返回正数,小于返回负数。
Strlen(字符数组) 求字符串长度。
Strlwr( 字符串) 将字符串中的大写字母转换成小写
Strupr( 字符串)  将字符串中的小写字母转换成大写
以上是一些比较常用的字符串处理函数。

Top of Page

第七章 函数

1. 关于形参和实参的说明

① 在函数被调用之前,形参不占内存
② 实参可以是常量、变量或表达式
③ 必须指定形参的类型
④ 实参与形参类型应一致
⑤ 实参对形参的数据传递是”值传递”,即单向传递

2. 函数返回值

如果想让函数返回一个值,在函数中就要用return语句来获得,在定义函数时也要对函数值指定类型,如果不指定,默认返回整型。

3. 函数调用

1)注意在函数调用时实参和形参的个数、类型应一一对应。对实参表求值的顺序是不确定的,有的系统按自左至右,有的系统则按自右至左的顺序。这一点要注意。
2)函数调用的方式:函数语句,函数表达式,函数参数
3)如果主调函数和被调函数在同一文件中,并且主调函数在前,那么一般要在主调函数中对被调函数进行说明。除非:(1)被调函数的返回值类型为整型或字符型(2)被调函数出现在主调函数之前。
4)对函数的说明和定义是不同的,定义是指对函数功能的确立,包括指定函数名,函数值类型,形参及其类型、函数体等。说明则只是对已定义的函数返回值类型进行说明,只包括函数名、函数类型以及一个空的括弧,不包括形参和函数体。
5)c语言允许函数的递归调用(在调用一个函数的过程中又出现直接或间接的调用该函数本身)。

4. 数组作为函数参数

1)数组元素作为函数参数 和一般变量相同
2)数组名作参数应该在主调和被调函数分别定义数组,形参数组的大小可以不定义。注意:数组名作参数,不是单向传递。
3)多维数组作参数,在被调函数中对形参数组定义时可以省略第一维的大小说明,但不能省略第二维或更高维的说明。

5. 局部变量和全局变量

从变量作用域角度分,变量可分为局部变量和全局变量。
1)内部变量(局部变量)
在一个函数内定义,只在函数范围内有效的变量。
   2)外部变量(全局变量)
在函数外定义,可以为本文件其它函数所共用,有效范围从定义变量的位置开始
     到本文件结束。建议尽量少使用全局变量,因为它在程序全部执行过程中都占用
     资源,而且使函数的通用性降低了。如果在定义外部变量之前的函数要想使用该
     外部变量,则应在该函数中用extern作外部变量说明。

6. 动态存储变量与静态存储变量

从变量值存在的时间(生存期)角度来分,可分为静态存储变量和动态存储变量。静态存储指在程序运行期间给变量分配固定的存储空间,动态存储指程序运行期间根据需要动态的给变量分配存储空间。
C语言中,变量的存储方法分为两大类:静态存储类和动态存储类,具体包括:自动的(auto),静态的(static),寄存器的(register),外部的(extern)。
1) 局部变量的存储方式
函数中的局部变量如不作专门说明,都之auto的,即动态存储的,auto可以省略。局部变量也可以定义为static的,这时它在函数内值是不变的。静态局部变量如不赋初值,编译时系统自动赋值为0,动态局部变量如不赋初值,则它的值是个不确定的值。C规定,只有在定义全局变量和局部静态变量时才能对数组赋初值。为提高执行效率,c允许将局部变量值放在寄存器中,这种变量叫register变量,要用register说明。但只有局部动态变量和形式参数可以作为register变量,其它不行。
2) 全局变量的存储方式
全局变量在函数外部定义,编译时分配在静态存储区,可以在程序中各个函数所引用。多个文件的情况如何引用全局变量呢?假如在一个文件定义全局变量,在别的文件引用,就要在此文件中用extern对全局变量说明,但如果全局变量定义时用static的话,此全局变量就只能在本文件中引用了,而不能被其它文件引用。
3) 存储类别小结
从作用域角度分,有局部变量和全局变量
局部变量:自动变量,即动态局部变量(离开函数,值就消失)
     静态局部变量(离开函数,值仍保留)
 寄存器变量(离开函数,值就消失)
 (形参可定义为自动变量和寄存器变量)
全局变量:静态全局变量(只限本文件引用)
 全局变量(允许其它文件引用)
从存在的时间分,有静态存储和动态存储
动态存储:自动变量(本函数内有效)
 寄存器变量(本函数内有效)
 形参
静态存储:静态局部变量(函数内有效)
 静态全局变量(本文件内有效)
 全局变量(其它文件可引用)
从变量值存放的位置分
静态存储区:静态局部变量
  静态全局变量
全局变量
动态存储区:自动变量和形参
寄存器内:寄存器变量

7. 内部函数和外部函数

内部函数:只能被本文件中的其它函数调用,定义时前加static,内部函数又称静态函数。
外部函数:可以被其它文件调用,定义时前加extern,如果省略,则隐含为外部函数,在需要调用此函数的文件中,一般要用extern说明。

Top of Page

第八章 预编译处理

 

c编译系统在对程序进行通常的编译之前,先进行预处理。c提供的预处理功能主要有以下三种:1)宏定义 2)文件包含 3)条件编译

1. 宏定义

不带参数的宏定义
用一个指定的标识符来代表一个字符串,形式:#define 标识符 字符串
几点说明:
1) 宏名一般用大写
2) 宏定义不作语法检查,只有在编译被宏展开后的源程序时才会报错
3) 宏定义不是c语句,不在行末加分号
4) 宏名有效范围为定义到本源文件结束
5) 可以用#undef命令终止宏定义的作用域
6) 在宏定义时,可以引用已定义的宏名

带参数的宏定义
定义形式:#define 宏名(参数表) 字符串
这和函数有些类似,但他们是不同的:
1) 函数调用时,先求实参表达式值,再代入形参,而宏只是简单替换,并不求值
2) 函数调用是在程序运行时分配内存的,而宏展开时并不分配内存,也没有返回值的概念
3) 对函数中的实参和形参都要定义类型,而且要求一致,宏名无类型,其参数也没有类型。
4) 函数只有一个返回值,而宏可以得到几个结果
5) 宏替换不占运行时间,只占编译时间,而函数调用占运行时间

2. 文件包含处理

#include “文件1″ 就是将文件1的全部内容复制插入到#include位置,作为一个源文件进行编译。
在#include命令中,文件名可以用” “也可以用< >,假如现在file1.c中包含file2.h文件,” “表示系统先在file1.c所在目录中找file2.h,如果找不到,再按系统指定的标准方式检索目录,< >表示系统直接按指定的标准方式检索目录。所以用” “保险一点。

3. 条件编译

条件编译指不对整个程序都编译,而是编译满足条件的那部分。条件编译有以下几种形式:
1)#ifdef 标识符
  程序段1
  #else
程序段2
#endif
它的作用:当标识符在前面已经被定义过(一般用#define),则对程序段1编译,否则对程序段2编译。
2)#ifndef 标识符
程序段1
#else
程序段2
#endif
它的作用和#ifdef相反,当标识符没被定义过,对程序段1编译,否则对程序段2编译。
3)#if 表达式
程序段1
  #else
 程序段2
#endif
它的作用:当表达式值为真(非0)时,对程序段1编译,否则对程序段2编译。

Top of Page

第九章 指针

  指针说白了就是地址。指针变量就是用来存放指针(地址)的变量。

1. 变量的指针和指向变量的指针变量

读起来很拗口,说白了就是变量的地址和用来存放变量地址的地址变量。因为一个变量在编译的时候系统要为它分配一个地址,假如再用一个变量来存放这个地址,那么这个变量就叫做指向变量的指针变量,也就是用来存放变量地址的这么一个变量。所谓”指向”就是指存放××的地址,如指向变量的指针变量,”指向”就是指用来存放变量的地址,再如指向数组的指针变量,”指向”就是指存放数组的地址。只要理解了这个,指针也就不难了。另外,还有指向字符串的指针变量,指向函数的指针变量,指向指针的指针变量等。

1) 指针变量的定义
形式:类型标识符 *标识符 如:int *pointer;
要注意两点:*表示pointer是个指针变量,在用这个变量的时候不能写成*pointer, *pointer是pointer指向的变量。一个指针变量只能指向同一个类型的变量。如上面
pointer只能指向int型变量。

2)指针变量的引用
两个有关的运算符:
& 取地址运算符 &a 就代表变量a的地址
* 指针运算符  *a 就代表变量a的值

2. 数组的指针和指向数组的指针变量

数组的指针指数组的起始地址,数组元素的指针指数组元素的地址。
1)指向数组元素的指针变量的定义与赋值
定义和指向变量的指针变量定义相同,c规定数组名代表数组的首地址,即第一个数组元素地址。
2)通过指针引用数组元素
我们通常引用数组元素的形式是a[i],如果用指针可以这样引用,*(a+i),或定义一个指针变量p,将数组a的首地址赋给p,p=a;然后用*(p+i)引用。
注意:指针变量p指向数组a首地址,则p++指向数组a的下一元素地址,即a[1]的地址。
3)数组名作函数参数
形参数组和实参数组之间并不是值传递,而是共用同一段地址,所以在函数调用过程中如果形参的值发生变化,则实参的值也跟着变化。
4)指向多维数组的指针和指针变量
以二维数组为居多。假设定义了一个二维数组a[3][4],那么
a代表整个二维数组的首地址,也代表第0行的首地址,同时也是第0行第0列的元素的首地址。a +0和a[0]代表第0行首地址,a+1和a[1]代表第一行的首地址。
假设a是一个数组的首地址,那么如果a是一维的,a+I代表第I个元素的地址,如果a是二维的,则a+I代表第I行的首地址。
那么第一行第二列的元素地址如何表示呢?a[1]+2或&a[1][2]或*(a+1)+2。
我们只要记住:在二维数组中a代表整个数组的首地址,a[I]代表第I行的首地址,a[I]与*(a+I)等价就行了。只要运用熟练了就没什么复杂的了。
5)指向由m个整数组成的一维数组的指针变量
如:int (*p)[4],p是一个指向包含4个元素的一维数组,如果p先指向a[0],则p+1指向a[1],即p的增值是以一维数组的长度为单位的,这里是4,举个例子:
假设a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23},p先指向a[0]也就是数组a的首地址,那么p+1就是a[1]的首地址即元素9的地址,因为在定义p时int (*p)[4],定义一维数组长度为4,所以p+1就等于加了一个一维数组的长度4。

3. 字符串的指针和指向字符串的指针变量

1)字符串的表示形式
c中字符串有两种表示形式:一种是数组,一种是字符指针
char string[]=”I love c!”;
char *str=”I love c!”;
其实指针形式也是在内存中开辟了一个数组,只不过数组的首地址存放在字符指针变量str中,千万不要认为str是一个字符串变量。
2)字符串指针作函数参数
实际上字符串指针就是数组的首地址。
3)字符指针变量与字符数组的区别
① 字符数组由若干元素组成,每个元素存放一个字符,而字符指针变量只存放字符串的首地址,不是整个字符串
② 对数组初始化要用static,对指针变量不用。
③ 对字符数组赋值,只能对各个元素赋值,不能象下面这样:
char str[14];
str=”I love c!”;
对指针变量可以,
char *str;
str=”I love c!”;
注意:此时赋给str的不是字符,而是字符串首地址。
④ 数组在定义和编译时分配内存单元,而指针变量定义后最好将其初始化,否则指针变量的值会指向一个不确定的内存段,将会破坏程序。如:
char *a;
scanf( “%s”, a );这种方法是很危险的,应该这样:
char *a, str[10];
a = str;
scanf( “%s”, a );这样字符指针就指向了一个确定的内存段。
⑤ 指针变量的值是可以改变的,而字符数组名所代表的字符串首地址却是不能改变的。

4. 函数的指针和指向函数的指针变量

一个函数在编译时被分配一个入口地址,这个入口地址就称为函数的指针。函数名代表函数的入口地址,这一点和数组一样。我们可以用一个指针变量来存放这个入口地址,然后通过该指针变量调用函数。如:假设有一个求两者较大的函数如下:int max( int x, int y );
当我们调用这个函数时可以这样:
int c;
c=max( a, b );这是通常调用方法,其实我们可以定义一个函数指针,通过指针来调用,如:
int (*p)(); //注意指向函数指针变量的定义形式
p=max; //此句就是将函数的入口地址赋给函数指针变量p
c=(*p)( a, b );
有些朋友可能对(*p)()不大理解,其实它的意思就是定义一个指向函数的指针变量p,p不是固定指向哪个函数的,而是专门用来存放函数入口地址的变量。在程序中把哪个函数的入口地址赋给它,它就指向哪个函数。但要注意,p不能象指向变量的指针变量一样进行p++,p-等无意义的操作。
既然p是一个指针变量,那么就可以作为函数的参数进行传递。其实函数的指针变量最常用的用途之一就是作为函数参数传递到其它函数。这也是c语言中应用的比较深入的部分了。

5. 返回指针值的函数

我们知道,一个函数可以带回一个整型值、字符值、实型值等,函数还可以带回一个指针型的数据,即地址。这种函数的定义形式如下:
类型标识符 *函数名(参数表) 如:int *a(x,y)返回一个指向整型的指针
使用这种函数的时候要注意:在调用时要先定义一个适当的指针来接收函数的返回值。这个适当的指针其类型应为函数返回指针所指向的类型。
这样的函数比较难于理解,其实只要把它当做一般的函数来处理就容易了。当我们觉得指针难于理解的时候,就把它暂时当做整型来看,就好理解多了。

6. 指针数组

指针数组无疑就是数组元素为指针,定义形式为: 类型标识 *数组名[数组长度]
如:int *p[4],千万不要写成int (*p)[4],这是指向一维数组的指针变量。指针数组多用于存放若干个字符串的首地址,注意一点,在定义指针数组时初始化,如下:
static char *name[]={“Li jing”,”Wang mi”,”Xu shang”};
不要以为数组中存放的是字符串,它存放的是字符串首地址,这一点一定要注意。

7. 指向指针的指针

说的明白一点,将一个指针再用一个变量来存放,那么这个变量就是指向指针的指针。定义如:char * *p;

8. 指针数组作main()函数的参数

函数形式为
main( int argc, char *argv[] ){}
main函数的参数是从命令行得到的,argc指命令行参数个数,注意命令名也算一个参数,命令行参数都是字符串,他们的首地址构成一个指针数组argv。Main函数的形参用argc和argv只是一个习惯,也可以定义成别的名字。

9. 指针小结

1)有关指针的数据类型

定  义 含   义
Int I; 定义一个整型变量I
Int *p; P为指向整型数据的指针变量
Int a[n]; 定义整型数组a,它有n个元素
Int *p[n]; 定义指针数组p,它有n个指向整型的指针元素
Int (*p)[n]; P为指向含有n个元素的一维数组的指针变量
Int f(); F为返回整型值的函数
Int *p(); P为返回值为指针的函数,该指针指向整型数据
Int (*p)(); P为指向函数的指针,该函数返回一个整型值
Int **p; 定义一个指向指针的指针变量

 

2)ANSI新增了一种void *指针类型,即定义一个指针变量,但不指向任何数据类型,等用到的时候再强制转换类型。如:
char *p1;
void *p2;
p1 = (char *)p2;
也可以将一个函数定义成void *型,如:
void *fun( ch1, ch2 )
表示函数fun返回一个地址,它指向空类型,如果需要用到此地址,也要对其强制转换。如(假设p1为char型):
p1=(char *)fun( c1,c2 );

指针应该说是c语言中比较重要的概念,也是c语言的精华,它有很多优点,但用不好也会带来严重性的错误,这就需要我们多用,多练,慢慢的积累经验。

Top of Page

第十章 结构体与共用体

1. 定义

结构体定义的一般形式:
struct 结构体名{
成员列表
};
定义一个结构体变量可以这样定义:struct 结构体名 结构体变量名;

2. 结构体变量的引用

在引用结构体变量时应注意以下规则:
1)不能将结构体变量作为一个整体输入输出,只能对变量当中的各个成员输入输出。新标准C允许将一个结构体变量直接赋值给另一个具有相同结构的结构体变量。

3. 结构体变量的初始化

如:
struct student
{long int num;
char name[20];
char sex;
char addr[20];
}a={89031,”Li Lin”,’M',”123 Beijing Road” };

4. 结构体数组

struct student stu[4];
定义了一个数组stu,其元素为struct student类型,数组有4个元素。注意数组各元素在内存中是连续存放的。
在定义结构体数组时,数组元素个数可以不指定。编译时,系统会根据给出初值的结构体常量的个数来确定数组元素的个数。

5. 指向结构体变量的指针

因为结构体变量在内存中是连续存放各成员的,因此我们可以将结构体变量在内存中的起始地址存放到一个变量中,那么这个变量就是指向结构体变量的指针。
注意将结构体变量的首地址赋给指针变量的形式:
struct student stu_1;
struct student *p;
p=&stu_1; //要加取地址符 而指向函数和指向字符串的指针不用
在对引用结构体变量中的成员时,有三种方式:
以上面的结构体为例:设p为指向此结构体变量的指针,即p=&a;
1) a.num
2) (*p).num
3) p->num

6. 指向结构体数组的指针

struct student *p;
struct student stu[4];
p=stu;
则p为指向结构体数组的指针变量。这里应注意p++,p指向stu[0],p++则指向stu[1]。P指向的是数组中一个元素的首地址,而不能让p指向元素中的某一成员,如p=&stu[I].name是不对的。

7. 用指向结构体的指针作函数参数

虽然ANSI C允许用整个结构体作为函数参数,但要将全部成员值一个一个传递,开销大。所以用指针作参数,能提高运行效率。
Struct student stu;
用整个结构体作为参数调用形式:
fun( stu );
而且被调函数fun中也要定义成结构体变量,struct student stu;
用指针作参数调用形式:
fun( &stu );
被调函数fun中定义成指针变量,struct student *p;

8. 用指针处理链表

链表是一种重要的数据结构,原因就在于它可以动态的进行存储分配。链表都有一个头指针,用来存放整个链表的首地址。链表的定义形式如下:
struct node{
int num;

struct node *next;
};
next用来存放下一节点的地址。
如何进行动态的开辟和释放存储单元呢?c提供了以下有关函数:
1) malloc(size) 在内存的动态存储区开辟一个长度为size的连续空间。成功返回空间首地址,失败返回0;
2) calloc(n,size) 在内存的动态存储区开辟n个长度为size的连续空间。成功返回空间首地址,失败返回0;
3) free(ptr) 释放由ptr指向的内存区。Ptr是最近调用一次调用malloc和calloc时返回的值。
  上面函数中,n和size为整型,ptr为字符指针。

9. 共用体

定义形式:
union 共用体名
{
成员列表
}变量列表;
共用体和结构体类似,只是有一点不同,结构体中个成员的起始地址不同,结构体变量在内存中的长度为各成员长度之和;而共用体中个成员的起始地址相同,共用体变量所占的内存长度为最长的成员的长度。
共用体类型数据的特点:
1) 同一个内存段可以存放几种不同类型的成员
2) 共用体变量中起作用的成员是最后一次存放的成员
3) 不能对共用体变量名赋值,不能在定义时初始化。
4) 不能把共用体变量作为函数参数
5) 共用体类型可以出现在结构体定义中,反之也可,也可以定义共用体数组。
另外,结构体名可以作为参数,而共用体名不可以。
这两中数据结构在不同场合中各有所用。

10. 枚举类型

定义形式如下:举个例子
enum weekday{sun,mon,tue,wed,thu,fri,sat};
enum weekday workday,week_end; //定义枚举变量
workday和week_end被定义成枚举类型,他们的值只能为sun到sat之一。
也可以直接定义枚举变量,这一点与结构体相同
enum weekday{sun,mon,tue,wed,thu,fri,sat}wordday,week_end;
注意:枚举元素是作为常量存在的,他们是有值的,c在编译时使他们的值按顺序为0,1,2…
如:上面的定义中,sun的值为0,mon的值为1
另外:虽然枚举元素有值,但不能将一个整数直接赋给一个枚举变量。应进行强制类型转换,如:
workday=(enum weekday)2;它相当于把tue赋给了workday。

11. 用typedef定义类型

typedef的作用就是能够让你定义一个自己喜欢的数据类型名来代替已有的数据类型名。如:
typedef int INT;那么我就可以用INT来定义整型变量了。作用和int一样。
Typedef用于结构体定义,如:
Typedef struct{
Int day;
Int month;
Int year;
}DATE;
DATE birthday;
DATE *p;等等
用typedef有利于程序的通用与移植。

Top of Page

第十一章 位运算

1)概述

所谓位运算是指进行二进制位的运算。在系统软件中,常要处理二进制位的问题。
c提供的位运算符有:
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
>> 右移
&对于将一个单元清零、取一个数中的某些指定位以及保留指定位有很大用途。
|常被用来将一个数的某些位置1。
^判断两个位值,不同为1,相同为0。常用来使特定位翻转等。
~常用来配合其它位运算符使用的,常用来设置屏蔽字。
<<将一个数的各二进制位全部左移,高位左移后溢出,舍弃不起作用。左移一位相当于该数乘2,左移n位相当于乘2n。左移比乘法运算要快的多。
>>右移时,要注意符号问题。对无符号数,右移时左边高位移入0,对于有符号数,如果原来符号位为0(正数),则左边移入0;如果符号位为1(负数),则左边移入0还是1要取决于系统。移入0的称为”逻辑右移”,移入1的称为”算数右移”。

2)位段

将一个字节分为几段来存放几个信息。所谓位段是以位为单位定义长度的结构体类型中的成员。如:
struct packed-data{
unsigned a:2;
unsigned b:6;
unsigned c:4;
unsigned d:4;
int I;
}data;
其中a,b,c,d分别占2位,6位,4位,4位。I为整型,占4 个字节。
对于位段成员的引用如下:
data.a = 2;等,但要注意赋值时,不要超出位段定义的范围。如位段成员a定义为2位,最大值为3,即(11)2,所以data.a=5;就会取5的两个低位进行赋值,就得不到想要的值了。
关于位段的定义和引用,有几点重要说明:
①若某一个段要从另一个字开始存放,可以定义:
unsigned a:1;
unsigned b:2;
unsigned :0;
unsigned c:3; (另一单元)
使用长度为0的位段,作用就是使下一个位段从下一个存储单元开始存放。
②一个位段必须存放在用一个存储单元中,不能跨两个单元。
③可以定义无名位段。如:
unsigned a:1;
unsigned :2; (这两位空间不用)
unsigned b:3;
④位段的长度不能大于存储单元的长度,也不能定义位段数组。

Top of Page

第十二章 文件

1) 概述

c语言将文件看成一个字符的序列,分为ASCII文件(文本文件)和二进制文件。即一个c文件就是一个字节流或二进制流。
ASCII文件每一个字节放一个ASCII码,代表一个字符,输出与字符一一对应,便于逐个处理字符,但占用空间较多。二进制文件按内存中的存储形式原样输出到磁盘上,节省空间,由于输出与字符不对应,不能直接输出字符形式,一般用于保存中间结果。目前c对文件的处理只有缓冲文件系统一种方法,即无论是从程序到磁盘文件还是从磁盘文件到程序,数据都要先经过缓冲区,待缓冲区充满后,才集中发送。

2) 文件夹类型指针

在缓冲文件系统中,关键的概念是文件指针。因为每个被使用的文件都在内存中开辟一个缓冲区,来存放文件有关信息。这些信息保存在一个结构体变量中,该结构体类型是由系统定义的,取名为FILE,在stdio.h中定义。
FILE *fp;
定义了一个文件指针变量fp,以后对文件的操作都是通过fp进行的。

3) 文件的打开与关闭

在对文件读写之前,要先打开文件。
打开文件的函数为:fopen(),调用方式为:
FILE *fp;
fp=fopen( filename,使用文件方式 );
fopen()失败返回一个空指针NULL,成功则返回一个指向”filename”的文件指针,赋给fp,这样fp就和打开的文件联系在一起了。或者说,fp指向了”filename”。
文件使用方式:r,w,a,rb,wb,ab,r+,w+,a+,rb+,wb+,ab+,具体含义要记住。

4)文件的关闭

为了防止数据丢失,程序结束前,务必将打开的文件关闭,即将文件指针与文件脱钩。用fclose(文件指针)函数关闭文件,执行函数后,先将缓冲区中的数据送到磁盘文件,然后释放文件指针。成功返回0,失败返回非0。

5)文件的读写

文件打开后,就可以对其读写了,常用的文件读写函数有:
①fputc和fgetc
fputc将一个字符写到文件,形式为fputc( ch, fp );将字符ch写入fp所指向的文件。成功返回该字符,失败返回EOF,EOF在stdio.h中定义为符号常量-1。
fgetc从指定文件读入一个字符,该文件必须是以读或读写方式打开的。调用形式为ch=fgetc(fp);从fp指向的文件读入一个字符赋给ch,当文件结束时,fgetc返回一个EOF,我们可以用函数feof(fp)来判断是否已到文件尾,返回1表示已到文件尾,否则返回0。这个函数适用于文本文件和二进制文件。
②fread和fwrite函数
可以读写一组数据。调用形式如下:
fread( buffer, size, count, fp );
fwrite( buffer, size, count, fp );
buffer为一个指针,对fread来讲,是指从文件读出数据的存放地址,对fwrite来讲,是要写入文件的数据的地址。
size 要读写的字节数
count 要进行读写多少个size字节的数据项(书上这么说)其实就是读写的次数
fp 文件指针
这两个函数返回值成功为1,失败为非1,一般用于二进制文件的读写。
注意:有些c编译系统不具备这两个函数。
③fprintf()和fscanf()函数
格式化输出和输入函数,与printf()和scanf()作用相似,只有一点不同,fprintf()和fscanf()的读写对象不是终端而是磁盘文件。调用方式:
fprintf(文件指针,格式字符串,输出列表);
fscanf(文件指针,格式字符串,输出列表);
④fgets()和fputs()函数
作用是读写一个字符串,如:
fgets(str,n,fp);
意为从fp指向的文件读出n-1个字符,存放到str中,成功返回str的首地址。
fputs( “China”, fp );
把字符串China写入fp指向的文件。成功返回0,失败为非0。

6)文件的定位

文件中有一个位置指针,指向当前读写的位置,如果要强制改变位置指针的位置,可以用有关函数:
①rewind 使位置指针重新返回文件的开头
②fseek()
fseek()函数可以任意改变位置指针的位置,以实现随机读写文件。调用形式:
fseek( 文件指针类型,位移量,起始点 );
起始点有以下三个值:
SEEK_SET或0 文件开始
SEEK_CUR或1 文件当前位置
SEEK_END或2 文件末尾
位移量指以起始点为基点,移动的字节数(正数向文件尾移动,负数向文件头移动),一般位移量用long型数据,以避免大于64K的文件出错。Fseek()函数一般用于二进制文件,因为文本文件要进行字符转换,计算时会发生混乱。
Fseek( fp, 100L, 0 ); 将位置指针从文件头向文件尾移动100个字节处。
Fseek( fp, 50L, 1 ); 将指针从当前位置向文件尾移动50个字节处。
Fseek( fp, -10L, 2 ); 将指针从文件尾向文件头移动10个字节处。
③ftell()
得到流式文件位置指针的当前位置,成功返回相对于文件头的位移量,失败返回-1L。

Top of Page

  另外,由于ANSI C不使用非缓冲文件系统,而其它C系统还用到非缓冲文件系统,所以对于这一章节只是略微的看了一下,不至于以后见到这样的程序不认识,呵呵。这一节主要讲了几个文件读写的有关函数,看了也没做笔记。如果关心的话,自己看一下吧。
  至此,这本语言类的基础书又温习了一遍,由于工作太忙,花了我半个月的时间。不过总的来说,收获还是很大的,有很多以前没有发现的新东西,也有很多以前理解较浅显的东西,这次加深了理解。其实,看完了一章后,最好将书后的习题一一解答,因为这是对这章知识点的考查。同时,动手编一下小程序,也能提高自己的编程能力。

#include
#include
#include
#define NUM 24

char calc(int,double);
void exchange(int,int);
void addform(int,int);
float operate(int,int,int);
void show(int,int,int);
void item(int,int,int,char*);

int a[4];
double b[4];
char form[40]=”";
char need;
char ok=0;

main()
{
?int i,k1,k2;
?float tp1,tp2;
?cputs(“\n”);
?textattr(0×1e);
loop:
?cputs(“Please input 4 numbers:(0 to exit)\n\r”);
?for(i=0;i<4;i++)
?{
? cscanf(“%d”,a+i);
? if(!a[i])
? {
?? textattr(0×0f);
?? exit(0);
? }
? b[i]=(double)a[i];
?}
?cputs(“\n”);
?if(calc(4,NUM))
?{
? ok=1;
? cprintf(“%s=%d\n\r”,form,NUM);
?}
?for(i=1;i<4;i++)
?{
? exchange(1,i);
? for(k1=0;k1<3;k1++)
?? for(k2=0;k2<3;k2++)
?? {
??? tp1=operate(0,1,k1);
??? tp2=operate(2,3,k2);
??? if(fabs(tp1+tp2-NUM)<1e-3) show(k1,k2,0);
??? if(fabs(tp1-tp2-NUM)<1e-3) show(k1,k2,1);
??? if(fabs(tp2-tp1-NUM)<1e-3) show(k1,k2,2);
?? }
? exchange(1,i);
?}
?if(!ok) cprintf(“No way to get %d!\n\r”,NUM);
?goto loop;
}

char calc(int n,double f)
{
?int i;
?if(n>1)
?{
? for(i=0;i? {
?? exchange(i,n-1);
?? if(calc(n-1,b[n-1]+f))
?? {
??? addform(0,n-1);
??? exchange(i,n-1);
??? need=2;
??? return 1;
?? }
?? if(calc(n-1,b[n-1]-f))
?? {
??? addform(1,n-1);
??? exchange(i,n-1);
??? need=2;
??? return 1;
?? }
?? if(calc(n-1,f-b[n-1]))
?? {
??? addform(2,n-1);
??? exchange(i,n-1);
??? need=2;
??? return 1;
?? }
?? if(calc(n-1,f*b[n-1]))
?? {
??? addform(3,n-1);
??? exchange(i,n-1);
??? need=1;
??? return 1;
?? }
?? if(fabs(f)>0.01)
?? {
??? if(calc(n-1,b[n-1]/f))
??? {
???? addform(4,n-1);
???? exchange(i,n-1);
???? need=1;
???? return 1;
??? }
?? }
?? if(calc(n-1,f/b[n-1]))
?? {
??? addform(5,n-1);
??? exchange(i,n-1);
??? need=1;
??? return 1;
?? }
?? exchange(i,n-1);
? }
? return 0;
?}
?else
?{
? if(fabs(b[0]-f)<0.001)
? {
?? itoa(a[0],form,10);
?? need=0;
?? return 1;
? }
? else return 0;
?}
}

void exchange(int i,int j)
{
?double tpf;
?int tpi;
?tpf=b[i];
?b[i]=b[j];
?b[j]=tpf;
?tpi=a[i];
?a[i]=a[j];
?a[j]=tpi;
}

void addform(int s,int k)
{
?char str[40];
?itoa(a[k],str,10);
?switch(s)
?{
? case 0: strcat(form,”-”);
?? strcat(form,str);
?? break;
? case 1: if(need==2) strcat(str,”-(“);
?? else strcat(str,”-”);
?? strcat(str,form);
?? if(need==2) strcat(str,”)”);
?? strcpy(form,str);
?? break;
? case 2: strcat(form,”+”);
?? strcat(form,str);
?? break;
? case 3: if(need==2) strcat(form,”)?);
?? else strcat(form,”?);
?? strcat(form,str);
?? if(need==2) strcpy(str,”(“);
?? else strcpy(str,”");
?? strcat(str,form);
?? strcpy(form,str);
?? break;
? case 4: if(need)? strcat(str,”?”);
?? else????? strcat(str,”?);
?? strcat(str,form);
?? if(need)? strcat(str,”)”);
?? strcpy(form,str);
?? break;
? case 5: if(need==2) strcat(str,”*(“);
?? else??????? strcat(str,”*”);
?? strcat(str,form);
?? if(need==2) strcat(str,”)”);
?? strcpy(form,str);
?? break;
?}
}

float operate(int i,int j,int k)
{
?float v;
?switch(k)
?{
? case 0: v=b[i]*b[j];
?? break;
? case 1: v=b[i]/b[j];
?? break;
? case 2: v=b[j]/b[i];
?? break;
?}
?return v;
}

void show(int k1,int k2,int s)
{
?char str[40];
?item(0,1,k1,str);
?item(2,3,k2,form);
?switch(s)
?{
? case 0: strcat(form,”+”);
?? strcat(form,str);
?? break;
? case 1: strcat(str,”-”);
?? strcat(str,form);
?? strcpy(form,str);
?? break;
? case 2: strcat(form,”-”);
?? strcat(form,str);
?? break;
?}
?cprintf(“%s=%d\n\r”,form,NUM);
?ok=1;
}

void item(int i,int j,int k,char *str)
{
?char s1[5],s2[5];
?itoa(a[i],s1,10);
?itoa(a[j],s2,10);
?switch(k)
?{
? case 0: strcat(s1,”*”);
?? strcat(s1,s2);
?? strcpy(str,s1);
?? break;
? case 1: strcat(s1,”?);
?? strcat(s1,s2);
?? strcpy(str,s1);
?? break;
? case 2: strcat(s2,”?);
?? strcat(s2,s1);
?? strcpy(str,s2);
?? break;
?}
}

2004年08月27日

        在2005年即将推出的Visual C++2005版本中,实现了绝大部分的C++/CLI规范。这也意味着Version 1中的语法定义的过时,很多程序员刚刚适应了__gc等等语法定义形式,又要被迫作一次转换。诚然最新的语法定义是向着好的方向转化,也让人们觉得舒服很多,但是对于企业来讲又要对员工进行一次筛选和再培训。但是这将会不同程度的耽误企业的项目进度。

        在未来的Beta2或者正式版中,微软将在集成开发环境中加入一个源代码转换(翻译)工具,称为“mscfront”他将可以将你现有的VC++.NET代码自动转换成C++/CLI代码。这解决了企业的后顾之忧。然而还是认为应当尽快地熟悉C++/CLI规范。

想要真正掌握委托与事件最好的方法便是你自己来实现它们使用以前的纯C++

简介

类型安全机制的实现原来采用的是C风格的回调(callback)函数,而.NET Framework引入了委托和事件来替代原来的方式;它们被广泛地使用。我们在这里尝试使用标准C++来实现与之类似的功能,这样我们不但可以对这些概念有一个更好的认识,而且同时还能够体验C++的一些有趣的技术。

C#中的委托与事件关键字

首先我们来看一个简单的C#程序(下面的代码略有删节)。执行程序的输出结果如下显示:

SimpleDelegateFunction called from Ob1,

string=Event fired!

Event fired!(Ob1): 3:49:46 PM on

Friday, May 10, 2002

Event fired!(Ob1): 1056318417

SimpleDelegateFunction called from Ob2,

string=Event fired!

Event fired!(Ob2): 3:49:46 PM on

Friday, May 10, 2002

Event fired!(Ob2): 1056318417

所有这些都源于这样一行代码:dae.FirePrintString(“Event fired!”);

在利用C++来实现这些功能时,我模仿了C#的语法并完全按照功能的要求进行开发。

namespace DelegatesAndEvents

{

             class DelegatesAndEvents

             {

                           public delegate void PrintString(string s);

                           public event PrintString MyPrintString;

                           public void FirePrintString(string s)

                           {

                                         if (MyPrintString != null)MyPrintString(s);

                           }

             }

 

             class TestDelegatesAndEvents

             {

                           [STAThread]

                           static void Main(string[] args)

                           {

                                         DelegatesAndEvents dae =new DelegatesAndEvents();

                                         MyDelegates d = new MyDelegates();

                                         d.Name = “Ob1″;

                                         dae.MyPrintString +=new DelegatesAndEvents.PrintString(d.SimpleDelegateFunction);

                                         // … more code similar to the

                                         // above few lines …

                                         dae.FirePrintString(“Event fired!”);

                           }

             }

             class MyDelegates

             {

                           // … “Name” property omitted…

                           public void SimpleDelegateFunction(string s)

                           {

                                         Console.WriteLine(“SimpleDelegateFunction called from {0}, string={1}”, m_name, s);

                           }

                           // … more methods …

             }

}

 

 

C++中的类型安全函数指针

对于老式方法的批判之一便是它们不是类型安全的[1]。下面的代码证明了这个观点:

typedef size_t (*FUNC)(const char*);

void printSize(const char* str) {

FUNC f = strlen;

(void) printf(“%s is %ld chars\n”, str, f(str));

}

void crashAndBurn(const char* str) {

FUNC f = reinterpret_cast(strcat);

f(str);

}

 

 

 

代码在[2]中可以找到。当然,在你使用reinterpret_cast的时候,你可能会遇到麻烦。如果你将强制转换(cast)去掉,C++编译器将报错,而相对来说更为安全的static_cast也不能够完成转换。这个例子也有点像比较苹果和橙子,因为在C#中万事万物皆对象,而reinterpret_cast就相当于一种解决方式。下面的这个C++程序示例将会采取使用成员函数指针的方法来避免使用reinterpret_cast

struct Object { };

struct Str : public Object {

size_t Len(const char* str) {

return strlen(str);

}

char* Cat(char* s1, const char* s2) {

return strcat(s1, s2);

}

};

 

typedef size_t (Object::*FUNC)(const char*);

void printSize(const char* s) {

Str str;

FUNC f = static_cast(&Str::Len);

(void) printf(“%s is %ld chars\n”, s, (str.*f)(s));

}

void crashAndBurn(const char* s) {

Str str;

FUNC f = static_cast(&Str::Cat);

(str.*f)(s);

}

 

static_cast运算符将转化Str::Len函数指针,因为Str是由Object派生来的,但是Str::Cat是类型安全的,它不能被转换,因为函数签名是不匹配的。

成员函数指针的工作机制与常规的函数指针是非常相似的;唯一不同(除了更为复杂的语法外)的是你需要一个用来调用成员函数的类的实例。当然,我们也可以使用->*运算符来用指向类实例的指针完成对成员函数的调用。

Str* pStr = new Str();

FUNC f = static_cast(&Str::Len);

(void) printf(“%s is %ld chars\n”, s, (str->*f)(s));

delete pStr;

 

 

只要所有的类是从基类Object派生来的(C#中就是这样),你就可以使用C++来创建类型安全的成员函数指针。

创建一个委托类

拥有类型安全成员函数指针是我们效仿.NET功能的第一部。尽管如此,单独的成员函数指针是毫无用处的你总是需要一个类的实例;委托对象同时保持在两边,使得调用成员函数非常方便。我们接着上面的例子续写下面的代码:

struct StrLen_Delegate

{

typedef size_t (Str::*MF_T)(const char*);

MF_T m_method;

Object& m_pTarget;

 

StrLen_Delegate(Object& o, const MF_T& mf) :

m_pTarget(&o), m_method(mf) {}

 

MF_T Method() const {

return m_method;

}

Object& Target() const {

return *m_pTarget;

}

 

size_t Invoke(const char* s) {

(m_pTarget.*m_method)(s);

}

};

 

void printSize2(const char* s) {

Str str;

StrLen_Delegate d(str, &Str::Len);

(void) printf(“%s is %ld chars\n”, s,

d.Invoke(s));

}

 

 

有了委托类,调用成员函数变得更为简单。使用运算符代替Invoke来给这个类创建一个仿函数将使调用降为仅有d(s);为了清晰以及和.NET规定匹配,我使用Invoke。需要注意的是,类的实例是一个对象(Object而不是Str。只要签名匹配,从Object派生来的任何一个类的成员函数指针将允许被用于创建委托。

这个类在这个例子中使用能够工作得非常好,但是它不是非常灵活;我们必须为每一个可能的成员函数签名写一个新的委托类。.NET使用由公用语言运行时(Common Language Runtime)维护的rich type信息来解决这个问题。但这在C++中不是一个非常可行的办法,但是可以采用模板来完成类似的功能。我们不用将Invoke函数的参数设为const char* s而是将类型指定为模板参数:

template <typename ARG1>

struct StrLen_Delegate

{

typedef size_t (Str::*MF_T)(ARG1);

// … as above …

size_t Invoke(ARG1 v1) {

(m_pTarget.*m_method)(v1);

}

};

 

 

这样效果就好很多了,但是Invoke函数将只作用于单参数的成员函数。并且,委托也仅仅关心类的实例以及成员函数指针;它不是真正关心成员函数指针的细节。最后,我们很方便地就能够为成员函数指针产生一个typedef作为模版参数使用。由于一切都是由Object类派生出来的,这些细节也可以被移动到Object当中:

struct Object

{

template <typename ARG1>

struct void1_T {

typedef void (Object::*mf_t)(ARG1);

};

 

template <typename ARG1, typename ARG2>

void Invoke(void1_T::mf_t mf, ARG1 v1, ARG2) const {

(this->*mf)(v1);

}

};

template <typename CLASS>

class ObjectT : public Object {};

typedef  ObjectT<void> VoidType;

 

这个Object基类包含了一个typedef对应每一个成员函数签名;我使用了void返回类型来简化了很多需要做的工作。Typedef可以参照如下方式使用:

typedef Object::void1_T<std::string>::mf_t StringMF_t;

我们使用了std::string类型的参数和void返回类型就能够非常容易地为成员函数指针创建typedef

程序根据附加的参数对于Invoke是跟踪计数的。这是非常必要的,因为对于所有的Invoke方法必须有同样数目的参数;重载决策基于第一个参数成员函数指针的类型,来完成。需要注意的是大部分的.NET Framework将在委托中使用EventArgs对象来避免上述的复杂情况。你可以通过从EventArgs派生来添加额外的参数而不需要给委托添加签名。

最后,ObjectT模版提供了一个简单的方法用来产生唯一类型,每一个类型最终是从Object派生来的。这就确保了类型安全。

基于上面所有的内容,委托类现在就应当是如下所示的样子:

template <typename MF_T>

class DelegateT_ : public ObjectT

{

MF_T m_method;

Object* m_pTarget;

 

protected:

DelegateT_() : m_pTarget(NULL), m_method(NULL) {}

DelegateT_(Object& o, const MF_T& mf) :

m_pTarget(&o), m_method(mf) {}

 

public:

维护委托集

C#中,DelegateEvent关键字成对出现用来创建一列委托,就像上面的第一个例子:

new DelegatesAndEvents.PrintString(d.SimpleDelegateFunction);

创建一个新的类似于我的C++实现的委托对象:

StrLen_Delegate d(str, &Str::Len);

MyPrintString对象是一个拥有重载运算符+=的事件,这是用来添加委托的。在C++中我们也可以模仿这个功能来完成类似的工作。C#中的Delegate关键字创建了一个MultiCastDelegate对象(详见[3])。你会注意到我将上面的委托类命名为DelegateT_(尾随的下划线说明这个名字是保留的)。严格地说,名字_DelegateT是为这个程序实现而保留的(__DelegateT也是一样的)因为下划线后跟随着一个大写字母。_delegateT也可以(仅有一个被小写字母尾随其后的下划线),但是我偏向于避免所有的由于前下划线所可能导致的潜在错误(阅读我写的代码的人很可能抓不到我的所有规则)也不愿意采用后划线代替它。保留DelegateT_是因为完成效仿.NET功能的委托类是从多播委托MultiCastDelegate)类派生来的。

Delegate对象可以很容易地被存储在标准C++容器中。我将使用list,因为它与.NET的工作机制是最接近的。依据你个人的需要,也可以使用vector或者deque。使用集(set)来提供不论委托被附加入几次,仅仅调用一次的有趣的特性。MultiCastDelegate的第一部分如下所示:

template <typename MF_T, typename ARG1 = VoidType,

typename ARG2 = VoidType>

class MulticastDelegateT : public DelegateT_

{

typedef DelegateT_ Delegate;

typedef std::list Delegates_t;

protected:

MulticastDelegateT() {}

public:

MulticastDelegateT(Object& o, const MF_T& mf) :

Delegate(o, mf) {}

?

MulticastDelegateT& operator+=(const Delegate& d) {

m_delegates.push_back(d);

return *this;

}

?

private:

Delegates_t m_delegates;

};

?

这里使用了list和几个typedef来存储委托集。它需要从DelegateT_派生而来,因为下面我将从MultiCastDelegateT派生出DelegateT作为真正的委托类。

而后激发所有被存储的委托上的一个C#循环中的事件并调用每一个。因为我使用的是标准容器,使迭代器将很方便:

void operator()(ARG1 v1 = VoidType(),

ARG2 v2 = VoidType()) const {

for (Delegates_t::const_iterator it = m_delegates.begin();

it != m_delegates.end(); ++it)

(it->Target()).Invoke(it->Method(), v1, v2);

}

?

即使你很适应标准C++容器,这可能也是你不熟悉的一行代码:只在一个模版类中就可以使用迭代器调用成员函数!对迭代器取反引用,我们可以清楚地看到发生了什么:

const Delegate& d = *it;

d.Invoke(d.Method(), v1, v2);

?

如果你对迭代器还不是很适应,你可以指出一个就像数组一样的deque

for (int i=0; i<m_delegates.size(); i++)

Delegate d = m_delegates[i];

在这里,你可以为DelegateT_ 类添加下面的模板成员函数:

template <typename ARG1, typename ARG2>

void Invoke_(ARG1 v1 = ARG1(), ARG2 v2 = ARG2()) const {

this->Invoke(m_method, v1, v2);

}

这样就避免了MultiCastDelegateT::Invoke方法一定要将成员函数指针传递给Object::Invoke:

d.Invoke_(v1, v2);

尽管如此,这将需要每一个参数都有一个默认构造函数,但事实却不见得如此。并且,由于MultiCastDelegateT是真正的委托基类,看上去并没有太大的必要调用Object::Invoke 路径即使由于这个原因代码显得更为复杂。(这也会在Visual C++.NET中导致可怕的内部编译器错误)。

实际的委托类现在仅仅是MultiCastDelegateT的一个简单的包装:

template <typename MF_T, typename ARG1 = VoidType,

typename ARG2 = VoidType>

struct DelegateT :

public MulticastDelegateT

{

DelegateT(Object& o, const MF_T& mf) :

MulticastDelegateT(o, mf) {}

DelegateT() {}

typedef DelegateT Event;

};

?

它的主要功能是提供事件typedef

将他们集成起来

现在你可以用C++编写实现C#例子当中的DelegatesAndEvents类了:

class DelegatesAndEvents

{

// C#: public delegate void PrintString(string s);

typedef DelegateTstd::string>::mf_t,

std::string> PrintString_;

public:

template <typename OBJECT>

static PrintString_ PrintString(OBJECT& o,

void (OBJECT::*mf)(std::string)) {

return PrintString_(o,

static_caststd::string>::mf_t>(mf));

}

// C#: public event PrintString MyPrintString;

PrintString_::Event MyPrintString;

?

void FirePrintString(std::string s) {

MyPrintString(s);

}

};

这样的语法看上去着实令人恐怖,如果你愿意,可以用一些灵巧的宏来简化它。但最近宏的名声不太好,并且我们进行的这个主题关键是要了解细节。无论怎样,你都应当感谢C#编译器为你做的工作。

第一行代码创建一个成员函数指针私有的typedef名称为PrintString_。参数类型std::string需要列两次,这太糟了,但是这正是由于Visual C++不支持局部模版特化造成的。static方法为创建你自己的类型的委托提供了一个方便的方法,允许你这样来写你的代码:

DelegatesAndEvents::PrintString_

myDelegate =

DelegatesAndEvents::PrintString(d,

&MyDelegates::SimpleDelegateFunction);

?

这与上面的C#代码是类似的。

而后,我们使用来自DelegateT_Event typedef创建事件。请注意这一系列的typedef是如何允许C++代码至少是有C#代码一些类似之处的。最后,有一个方法触发事件,这与C#尤其相同。(由于你采用的是标准容器,所以不必担心NULL列表。)

使用委托和事件的客户端的代码就很明了,而且也很类似于C#代码(同样这些代码也是略有缩减的):

struct MyDelegates : public ObjectT<MyDelegates>

{

// … Name omitted…

void SimpleDelegateFunction(std::string s)

{

printf(“SimpleDelegateFunction called from %s,

string=%s\n”, m_name.c_str(), s.c_str());

}

// … more methods …

};

void CppStyle()

{

DelegatesAndEvents dae;

?

MyDelegates d;

d.Name() = “Obj1″;

?

dae.MyPrintString += DelegatesAndEvents::PrintString

(d, &MyDelegates::SimpleDelegateFunction);

// … more code similar to the above few lines …

?

dae.FirePrintString(“Event fired!”);

}

请注意MultiCastDelegateT::operator+=是如何被调用来为委托列表添加每一个由静态方法DelegatesAndEvents::PrintString返回的委托的。

托管C++

由于委托和事件是.NET框架的一部分,所有的.NET支持的语言都可以使用它们。我所描述的基于模版的实现是专门针对C++的。Microsoft采用了不同的方法在C++中将这个功能公开对于标准C++的扩展称为托管C++。也许你并不感到太吃惊,在托管C++中编写这个例子与最初的代码是那么相似:

public __gc struct DelegatesAndEvents {

__event void MyPrintString(String* s);

void FirePrintString(String* s) {

MyPrintString(s);

}

};

?

__gc struct MyDelegates

{

String* Name;

void SimpleDelegateFunction(String* s) {

Console::WriteLine

(“SimpleDelegateFunction called from {0} string={1}”,

Name, s);

}

};

?

void ManagedCpp()

{

DelegatesAndEvents* dae = new DelegatesAndEvents();

?

MyDelegates* d = new MyDelegates();

d->Name = “Obj1″;

__hook(&DelegatesAndEvents::MyPrintString, dae,

&MyDelegates::SimpleDelegateFunction, d);

?

dae->FirePrintString(S”Event fired!”);

}

?

关键字__gc标志着这个类是被垃圾回收机制控制的(托管的);我们不需要调用delete函数。仅仅一个__event关键字就完成了我们上面代码的大部分功能。需要注意的是托管C++使用__hook关键字来替代上面讨论的操作符+=。你会发觉使用-Fx标记[4]调用(托管)C++编译器编译上述代码和检查产生的结果文件.mrg非常有趣。在编译器级加入新功能而不是编写模板显然要容易得多了。

结论

通过使用极为高级的C++技巧,我已经向大家展示了用C++为简单的样例代码实现委托与事件是可行的。这个实现主要考虑基于.NET框架。更为一流和纯粹的C++解决方案可以使用C++标准库中的适配器和联编程序。

参考文献

[1] Jeffrey Richter. “An Introduction to Delegates,” MSDN Magazine, April 2001.

< http://msdn.microsoft.com/msdnmag/issues/01/04/net/default.aspx >.

[2] Richard Grimes. “.NET Delegates: Making Asynchronous Method Calls in the .NET Environment,” MSDN Magazine, August 2001.

.

[3] Jeffrey Richter. “Delegates, Part 2,” MSDN Magazine, June 2001.

< http://msdn.microsoft.com/msdnmag/issues/01/06/net/default.aspx>

[4] Bobby Schmidt. “The Red Pill,” April 23, 2002.

作者简介

J. Daniel Smith 是密歇根州Novi的一位持有Autodesk认证的软件工程师。他从加尔文学院取得了理学学士,并在密歇根州立大学取得了计算机科学的理学硕士学位。你可以通过 cuj@jdanielsmith.org 与他取得联系。

译者注:

译注1Type-safe:按照2003年微软官方提供的术语表翻译为类型安全

译注2overload resolution: 按照2003年微软官方提供的术语表翻译为重载决策

译注3原文中所列参考文献的地址已经失效,译文中提供的是在本文翻译截稿时所示参考的最新有效链接,为尊重原著者特此说明。

译注4destructor一词按照简体中文常用译法译为反引用

译注5关于文中采用的reinterpret_cast。事实上,reinterpret_cast在这里是通不过的。因为我们不可能对成员函数指针进行所谓的类型转换。这个例子实际上是在比较对象,转换的也是对象,而不是对象的成员。而这个示例却将reinterpret_cast作为解决的方式,即直接比较的是对象的成员,而不考虑对象。也就是说,试图转换对象的成员。而失去类型转换的真正意图。为什么作者在这里用了reinterpret_cast,意为重新意义上的强制转换。这种转换并不是基于类型或者是对象的,更谈不上类型安全了。委托的本质上讲是函数指针,不过,它需要首先进行类型检查。我们说委托对象的存在,只是为了类型检查,真正有意义的还是其方法。所以reinterpret_cast相当于一种解决方式。

MF_T Method() const {

return m_method;

}

Object& Target() const {

return *m_pTarget;

}

};

 

模板参数现在就是一个typedef成员函数指针(生成方法如上所示),而Invoke方法继承于Object基类。