首页
社区
课程
招聘
[分享]弱弱的放在这里:C/C++ for Win32编程,题目待定
发表于: 2009-1-19 21:34 12653

[分享]弱弱的放在这里:C/C++ for Win32编程,题目待定

2009-1-19 21:34
12653
我不知道在这里发是否合适,因C/C++方面经典的、好的资料铺天盖地,“早就不稀罕了”。放在这里的都是自己总结积累的,仅为图个查阅方便。当然,如有错误希望批评。如果很多人觉得垃圾,我就会直接让该贴自刎了...
    以下内容逐渐添加吧。

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

    适用人群:已经会用C和C++的初学者,想用他们开发Windows程序。倒不是因为我自认为自己水平高,而是相反... 如果有高人路过,不妨直接发个话吧...

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 0
支持
分享
最新回复 (26)
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
开发相关话题
1.引子
    对于程序编制人员来说,怎样安排他们的代码成了他们除设计具体算法外的最大的任务之一。和学生时代开发的试验品相比,所写代码还必须在执行正确的前提下(但这是个绝对的前提),保证健壮、可靠、系统收敛的,还必须尽可能的快(但不能以降低稳定为代价)。你还得保证代码看起来“漂亮”,以便让其他人看着舒服——请记住:你写的代码不是给你看的,是给别人,尤其是给开发测试人员——看的。这里的“其他人”还包括你自己:想想看,当你的程序在开发出来、运行无误很久很久后被查出有问题,你查到了有问题的部分,却已经不记得当时的思路了...挖了个坑把自己埋住。记住:如果不想给自己惹麻烦,最好别偷懒。
    现在将你觉得很上手的程序源代码翻出来,打开,将编译器的警告级别调到最高,编译...决不要忽略任何一处。错了就是错了,没有借口。
2.理想的平台
.虚处理器
  撇开编译器,现在站在程序设计者的角度想象一下,我们的程序运行起来总满足以下两点之一:1.顺序执行。代码从编码开始就有一定的逻辑顺序,尽管有被编译器和运行中处理器优化机制修改的可能,但逻辑上总是顺序一定的。2.并发执行。这没什么说的,线程和进程。说这些的根本目的是:我们必须得尽力考虑为一个“通用的处理器”编码,尽管它不存在。这样做的好处就是你不会为同时为满足不同的处理器优化而伤透脑筋,而且当硬件升级后不会碰到兼容性问题。这个问题在后面会一点点说明,并让你明白对代码安排的影响。
.虚内存(只适用于用户模式应用的开发)
    这里的虚内存和虚拟内存的概念不太一样,它是针对源程序而言的。编译后的程序会被假设使用一个理想的大内存,但却不考虑这些内存是否真的能被使用。注意虚内存的概念对驱动程序开发基本无用,只有十分有限参考价值。虚内存会被划分为几个特殊区域:代码段固定大小的块:用于存储静态可写/只读数据、动态存储区块:包括(注意栈就是栈stack,没有“堆栈”!国内的计算机资料...)。
  对于任何指针、引用、跳转,虚内存会给予其以下约定:1.NULL为空地址。指向NULL意味着没有指向任何实际对象。2.从 (void *)-1 到 (void *)(-  [对象大小]) 区域的内存为无效内存。3.除了1、2以外的地址均为可能的有效地址。4.对象被分配地址空间后,其有效起止地址范围不会跨越NULL地址值(即动态分配不会循环)。5.栈的分配在有限大小内永远成功,但很有限;堆的分配永远有失败的可能。
  注意第3点和Windows操作系统实际的地址分配方式的不同(实际上根本就不考虑系统的内存分配)。为了保证对未来的兼容性,除非你的目的就是开发某个特定系统的软件,永远不要假定系统会给你划分什么样的地址范围。例如有人认为7fffffff以上的地址不会被应用程序使用,便用它来判断是否处在系统地址空间中。这样不好(尽管事实真的可行)。实在有此需要,应当使用系统SDK提供的相关API或宏,不要自己去定义他们。
  第5点意味着,每个函数及其内部区块声明的局部对象必须尽可能的小、少,函数嵌套层数必须有界;堆分配必须检查返回合法性。强调安全的程序还要考虑分配溢出问题的处理。而“成功”的结果就是,如果超过限制,异常抛出...这种异常是会永久地破坏最深层调用栈的结构的。最好不要这样设计。
----------------------------
测试一下:
- 你有用过递归调用计算菲氏函数或画分形图案的经历吗?那现在你知道了这种设计是不好的。其调用深度动态来看无界。
- 不要以为只有空闲内存不足才会导致分配堆内存失败!内存碎片化也会导致分配失败!更不要认为Windows不会让堆内存分配失败!驱动和函数钩子也可以让分配失败!
- 指针是否只有NULL为空?其实还可以定义(T *)-1为检测值。动态内存永远分配不到这里。只要你保证定义的类型大小不止一个字节。
3.关于API的约定
    对于一个指定的API:
  如果没有特殊说明,对基本资源有分配型操作的API遵守动态内存分配的约定——即有可能失败,从而使API返回失败;对基本资源回收型操作的API遵守动态内存回收的约定——即总是会成功(注意不要对已回收的“资源”再次调用回收API,就像不要对已释放内存的指针调用内存释放函数那样)。对基本资源回收型操作的API如果违反了此约定(当然调用的时候必须合法。否则就只剩下你自己的问题了),只应该考虑以下情况:。系统不稳定或受损;。驱动级程序的有意而为,注意这个驱动应当本来就是被设计成与你的应用程序有服务或管理关系的。
  对于API的应用层,比较复杂,应该视具体情况。例如ListBox控件,对已存在的列表项执行删除的操作就可能失败,但你不该认为是“系统不稳定或受损或驱动级程序的有意而为”,它实际上是执行重分配内存块(缩小内存)操作失败所致。这其实并不是违反了约定,其实很好理解,因为ListBox控件不是基本资源。如果你需要强制他们符合约定,最好自己定义和管理相应的数据结构。
  基本资源:系统核心部分有内存、进程内线程、文件和设备(要看情况)、进程索引值等(啊...可以和操作系统原理教材涉及的概念结合起来理解)。还有一类是功能内定义的基本资源。例如窗口API中的窗口(以HWND句柄为引用标志),GDI中的设备描述(以HDC句柄为引用标志)、各种绘图资源等(位图、刷子、画笔...)。
  API的应用还有很多要注意的地方,后面会提到。
4.GC的困惑。
  使用垃圾收集器可以从理论上根除堆分配管理中的常见经典问题。这些问题是菜鸟的家常便饭,而遗憾的是老鸟也不能保证不范。
  垃圾收集器成立的条件是基于一个基本事实:所有的计算机程序,除了操作系统的最底层代码,都是以函数的形式存在的。于是就意味着在任意时刻,对任何有效对象的任何索引都是以某个函数栈的某个索引符号为根节点的(当然,除了操作系统的最底层以外)。因为索引不能被创建,只能从另一个有效的索引拷贝出来,而栈又是后进先出的...于是垃圾收集器诞生...
  现在的问题是,对于使用.NET开发Windows程序的人来说,GC对于开发基本资源的自动回收机制却不利。很多资源是基于线程的。一个线程创建的资源很多只能在该线程内销毁,而当前的GC,都运行于一个区别于主线程的独立的线程内,且释放也不及时(并不是无用的时候就释放)。如果想在GC里析构他们基本上是不可能的。除了C++x新标准,个人很看好D语言(它的GC可是线程内的),但还是不敢现在投入正式使用。
[以后再修改]
2009-1-19 22:00
0
雪    币: 590
活跃值: (177)
能力值: ( LV9,RANK:680 )
在线值:
发帖
回帖
粉丝
3
我是小鸟  有学习资料我绝对支持
2009-1-20 10:38
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
以下内容不是强制的标准,只是一般化的建议

C/C++基本变量——整数:
    已经知道的,不说了...大家都知道的,可以问问,查查资料。这里放些附加建议:
  首先,计算机里没有真正的整数,其取值范围在数学里都可看成是一些连续大小、数量有限的整数的集合。
  其实整数可以分3类(以下简称“大类”):
   1.通用型整数,像 int、long、short、char、unsigned int、unsigned long ...。
特点:标准字节的宽度 | 用于大小有限,又不强调最大值,或者最大值总保证满足实际需要的场合。
| 平台无关。不同的编译器,同样类型的整数,不同编译器厂商规定的实际字节的宽度可能不同,具体由编译器厂商决定。
| 不要假定某个类型的具体位宽(除了char/unsigned char),不要假定最大值的具体数值。例如,不要以为int就是32位(尽管现在事实如此)。
   2.固定字节宽度整数,像 DWORD、WORD、BYTE、LONGLONG、__int32、...。
特点:指定字节宽度 | 用于强调固定宽度的场合。
| 编译器无关。平台无关 | 意义很明确:例如DWORD就是4字节。
   3.指针型整数,像 size_t 等。
特点:宽度等于指针宽度 | 常用于取得指针的比较结果、指针取值、通用混合类型变量的传递(VC.NET之前版本不支持)。
| 编译器无关。平台相关 | 不要假定具体位宽,不要假定最大值的具体数值。例如,不要以为size_t等于int位宽(尽管win32中如此)。

  不要自己定义这些整数类型,尽量使用编译器厂商提供的定义。例如你可以在厂商提供的头文件里看到 typedef unsigned long DWORD,不表示你就可以用unsigned long取代DWORD,反过来也不可以。

以下公式成立:
sizeof(DWORD) == 4
sizeof(WORD) == 2
sizeof(BYTE) == 1
sizeof(ULONGLONG) == 8
sizeof(char) == 1
sizeof(unsigned char) == 1
sizeof(long) >= sizeof(int) >= sizeof(short) >= sizeof(char)
sizeof(long) > sizeof(short)
sizeof(int) > sizeof(char)
sizeof(void*) >= sizeof(int)
sizeof(size_t) == sizeof(void*)
sizeof(wchar_t) >= 2

由上还可推出:
sizeof(long) > sizeof(char)
sizeof(size_t) >= sizeof(int)
sizeof(char) == sizeof(BYTE)
sizeof(unsigned char) == sizeof(BYTE)
sizeof(int) >= 2
sizeof(int) >= sizeof(WORD)
sizeof(size_t) >= sizeof(WORD)

还有,所有的指针型整数,同一编译环境中字节宽度都是一样的,不管字面意思是什么。另外指针型整数不是指针,而是整数。标准里给了几种:
size_t, uintptr_t, intptr_t等。

好吧,现在给出有关的无符号整数的赋值规范:
1.赋值给另一个属于不同大类的,必须检查按照你的实际应用要求(注意这种要求不应该依赖于平台)的取值范围是否包含于目标整数的取值范围。该范围必须是符合上述不等式的。如果你是对的,还必须在赋值时进行强制类型转换。
2.同一大类里或不同大类间有确定上述不等式关系的,小整数赋给大整数可行,反之则有截断问题。
3.如果某大整数按照你的实际应用要求(注意这种要求不应该依赖于平台)的取值范围恒包含于目标小整数的取值范围,则赋值可行,但必须进行强制类型转换。

另外微软定义的 LRESULT、LPARAM、WPARAM等满足:
sizeof(LRESULT) >= sizeof(DWORD)(但不要以为sizeof(size_t) >= sizeof(DWORD)),因为LRESULT被微软定义能容纳指针值、LONG、int、DWORD中的任何一种。有sizeof(LRESULT) >= sizeof(void*)

注意这些公式给正确进行强制类型转换的提示。要把不等式看成真正的不等式,不然你的代码在不同硬件平台间移植时很可能会出问题(例如从win32移植到win64)。如果你的软件代码量很大,你的麻烦就大了。

注意特殊情况的运用。例如下面是有问题的代码:
int a;
LPCTSTR pc = "1", pd = "2";

a = pd - pc // Here. the 'a' should be a size_t typed integer.

但下面的代码却可以:
int b = (int)sizeof(LARG_INTEGER); // That is right because the LARG_INTEGER always takes 8 bytes, which is small and will not be extended in future.

----------------------------
试一下:
- Win32SDK句柄的类型绝大部分为指针类型,少部分为指针型整数,极少部分为通用型整数。如果你有将通用型整数传递指针的例子、习惯,还是改改吧。另外建议还在使用VC6的同志们:如果软件自身的源代码类化的不彻底,还是赶快把VC6扔进垃圾桶吧...
- 如果你的软件用文件API传递内容于内存中(如用ReadFile\WriteFile),如果是动态的、不定长的块,好好检查源代码(注意DWORD/ULONGLONG与size_t的转化问题,不要懒得去考虑,或者“特殊情况”一番)。

C++结构体和类:
  -复合式数据类型的大小:
很久前看过有计算机考题如下:
struct tcs
{
    char a, b, c;
};
则sizeof(tcs) == ______
编程时发现实际情况与答案不同:
如果你写了一个这样的结构体,编译运行,调试时应该会显示结果为4。这是因为有对齐到WORD边界的规定(答案就是4)。
现在在结构体上下加几句,结果如下:
#pragma pack(push)
#pragma pack(1)
struct tcs
{
    char a, b, c;
};
#pragma pack(pop)
编译运行,调试时会显示结果为3。(答案?...)
如果自己定义这种结构体指针,还要加关键字UNALIGNED:
#pragma pack(push)
#pragma pack(1)
typedef struct _tcs
{
    char a, b, c;
} tcs, UNALIGNED * ptcs;
#pragma pack(pop)
现在回到原问题,我们知道一个char占用一个字节,8位:
现在在结构体上下加几句,结果如下:
#pragma pack(push)
#pragma pack(1)
struct tcs
{
    char a: 8;
    char b: 8;
    char c: 8;
};
#pragma pack(pop)
编译运行,调试时又会显示结果为4。($@$%$#?...)
还要注意代码sizeof(tcs::a)只会导致编译期错误。不能求解位域成员的sizeof运算。

所以,看来还是“实际一点”吧...
  - 注意虚类和多态类还会附加有隐含成员,不要认为结构的大小等于成员大小加对齐字节之和。这一点,论坛的各高手们都很清楚了。
  - 注意方法和静态属性成员是不参与决定结构本身的大小的。
  - C++支持编译期的成员权限关键字。但要注意标准给出了可选的成员分组重排规则。是否支持,支持到什么程度,由编译器厂商自己决定。我们一定要注意该规则带来的可能性影响(现在微软的编译器不支持,我们不但不该不当回事,反而更要注意):
struct FH
{
public:
  BYTE Knw[3];
public:
  DWORD cbSize;
  DWORD mm1;
  BYTE trl;
};
有些编译器会把它修改成(微软的编译器还不支持):
struct FH
{
public:
  DWORD cbSize;
  DWORD mm1;
  BYTE trl;
public:
  BYTE Knw[3];
};
源代码上是看不到这种修改的。

  - 我们该认为结构体和类的用途是不同的,而且结构体最好最好不要带虚成员,不要从虚类、多态类继承。虽然C++标准认为定义一个结构,写struct还是class都一样。

[以后再修改,我要离线了...]
2009-1-20 22:04
0
雪    币: 375
活跃值: (12)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
5
建议:

弄个电子书配点图 。
然后很多人就会支持你~
2009-1-20 23:10
0
雪    币: 2362
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
我比较支持123456
2009-1-20 23:53
0
雪    币: 342
活跃值: (55)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
7
绝对支持...
2009-1-21 00:08
0
雪    币: 325
活跃值: (97)
能力值: ( LV13,RANK:530 )
在线值:
发帖
回帖
粉丝
8
MSDN上面不是有嘛,总喜欢去买点书之类的看啊?
这是VS系列开发包的合法权益啊,不懂得貌似可以找微软客户支持吧。

up as second floor. fuck 'mspy' which failed to work properly with IE editbox control once again.
2009-1-22 03:10
0
雪    币: 231
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
  有人需要看的,支持!
2009-1-23 20:30
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
一是为查阅方便,还有就是因为很多人不见得把质量重视起来,只认为能运行得到结果就可以。(重读该句时发现会引起歧义...昏...现已更正)
还有,放帖子这里的目的不是为出问题后寻求帮助,而是起预防针的作用,最好在着手编码前就知道的。我在MSDN论坛就有注册的,各种问题问过很多,也帮人解决过一些问题。
打预防同时,还要看很多书如《Win32汇编语言程序设计》、《高质量程序设计指南C/C++》、《加密与解密》...,当然MSDN光盘是不能少的。
另外我不想贴重复的内容,你可以看书的,还要看这里干吗,没必要的啦,所以就贴一般书里没写的。

关于电子书,因为电脑不在身边,现在还要找工作,只有U盘带的,没法打包,有些还写在别的地方,只好凭记忆了(自己总结+资料汇集,完整度绝大部分),慢慢整理。
2009-1-25 00:01
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
以下内容也不是强制的标准,仍只是一般化的建议

C/C++算数运算:
    已经知道的,不说了...大家都知道的,可以问问,查查资料。这里又放些附加建议:
1.基本算数运算:
  既然计算机里没有真正的整数,那么计算机里也没有真正的算数运算。
    取值范围:
  设a和b是两个占一样位宽的无符号整数,这种整数可取到最大值MAX:
    如果f1为一元运算,f2为二元运算,则先将计算机中的整数和运算扩展到 {0}UZ+ 域

      f1(a) = fz1(az) % MAXz,
         f2(a, b) = fz2(az, bz) % MAXz.
      其中MAXz=R(MAX) + 1.
    这些东西计算机系的应该知道的啦。即“有溢出可能的运算”或有穷域的取值问题

还有隐式转换、强制类型转换等。因为可查,略过。
    书写代码时很多书里建议......(这里主要谈括号的问题)。对于默认顺序,个人认为有些建议没有必要:
已知的先乘除,后加减;先算数,后逻辑。初中、高中的东西。很多建议将算数加括号,多此一举。自己该补课去。但如果代码很长那就是另外一回事了。还有,位运算一定要加括号。因为它们不是基本代数运算。
    加、乘、与、或运算也不满足交换率(尤其是对其他线程数据的读写运算)。
    如果某一小块代码区域集中了大量的代数运算,而上面这类问题对结果有很大影响,建议用类将整数包装一下(但不要太复杂,影响性能),以便能正确地处理进位截断问题。
    如果表达式中有超过一处使用同一变量,该变量是同时被多个线程存储的,要用本地临时变量拷贝取代表达式里所有的那个变量,以避免计算出莫名其妙的值。记住不要依赖编译器优化机制的隐含修改。
2.比较运算:
- 比较符号的左右两边数据类型必须相同。如果不同,代码照常编译通过。但是数据类型一定是相等的,只是通过了隐式转换,这种转换有时会让你晕倒...
正确的代码
WORD a, b, c

...
if((WORD)(a+b) >= c) // Never thought that '(WORD)()' be unnecessary.
...
- 比较必须完整,不然会留下隐患
正确的代码
unsinged int a, b, c

...
if(a > c - b || b > c) // We need 'if(a+b>c)' actually.
...
- 比较中尤其要考虑进位截断问题
例子同上。注意那个a+b>c为什么要用a>c-b表示而不直接写
- 注意指针指向对象对比较的影响,因为指针有可能为空
正确的代码
if(p == NULL)
{
...
}
else if(*p >= n)
{
...
}
else
{
...
}

不要写成
if(p != NULL && *p >= n)
{
...
}
else
{
...
}

因为这样的代码执行顺序受编译器及其优化影响很大,顺序不当,引发访问空指针异常。
- 注意多线程对比较的影响
正确的代码
int volatile a; // It will be written by another thread.

switch(a)
{
case 0:;
...
break;
case 1:;
...
break;
default;
}

不等同于(下面的代码先不考虑效率问题)
if(a == 0)
{
...
}
else if(a == 1)
{
...
}
else
{
...
}

应该建立一个本地临时变量,将a拷贝,然后用那个本地临时变量完成比较。

[以后再修改,我要离线了...]
2009-1-25 02:20
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
以下内容也不是强制的标准,仍只是一般化的建议

C/C++程序流程:
    还是那句话...。这里再次放些附加建议:
- goto语句的争议
    从前很多人都喜欢用goto,结果程序结构常常变得难以理解,现在却相反——坚决反对goto,甚至有人要取消它。这就叫“goto恐惧症”...
    用goto即有好处又有坏处,看具体情况了。
    如:
for(y = 0; y < 256; y++)
{
    for(x = 0; x < 256; x++)
    {
        if(clr == 0x000000)
            goto J_BlkPixelsFnd; // Here.
    }
}
J_BlkPixelsFnd:;
...
上面的程序段中,深绿色可以改成:
{
    y = 256; x = 256;
    break;
}
来避免使用goto,但程序段如果很大还要让观众分析一阵,容易让人不知所云。
    又如:
设有一字符串指针pt,如果字符串非空,则执行程序段St1,否则执行St2:
if(pt != NULL)
{
    if(*pt != '\0')
    {
        // St1 is in here.
    }
    else
        goto
J_EmptyStr;
}
else
{
J_EmptyStr:;
    // St2 is in here.
}
如果去掉goto就要重写一次St2。如果St2很长,就会让看代码的人很费劲。这反而违反了“不要让程序难以让人理解”的初衷。还有,这些重复的代码是会增加代码段长度的,从而使程序运行时占用更多的内存(注意我们不要把两个if合并。理由前面已经提过)。
    goto在编译后就变成了jmp xxxx指令,很多编译器还会做一些附加修改。但要注意,不同的编译器修改行为不同(甚至不做任何修改),我们不该让编译器决定这些。
    一些让我们不能使用goto的情况:
  1. 不要让goto跨过本地变量的创建声明或回收区域:
不好:
...
{
    int a = s;
    CT ob;

    ...
    if(s != 0)
        goto J_Err;
}
...
J_Err:;
...
    这样的代码运行时会导致栈不平衡,还会让析构函数被跳过。注意break和continue语句不会导致这些错误。
但下面的代码是可以的:
...
if(b != 0)
    goto J_Errs;
...
{
    int a = s;
    CT ob;

    ...
}
...
J_Errs:;
...
强烈反对写出这样的代码:
if(c >= 2)
{
    int a = 0;

    ...
        goto J_OthRes;
    ...
}
else
{
    int b = 0;

    ...
J_OthRes:;
    ...
}
你不会想知道编译器会把它改成什么的。相信我。
2. 不要在try和catch块的内外、块间跳转(理由同上)。但跳过整个try-catch块却是可以的。
3. 可以在switchers里面使用goto,像这样:
switch(a)
{
case FROGS:;
    ...
    if(r != 0)
        goto J_SEARCHALL;
    ...
    break;
case FREEKS:;
    ...
    goto J_SEARCHALL;
case NERDS:;
    ...
    if(s != 0)
        goto J_SEARCHALL;
    ...
    break;
J_SEARCHALL:;
    // Hey! Do something now:
    ...
    break;
default:;
    ...
}
原因只是goto语句及其目标地址都位于整个switcher内,所以看到以上代码后可不要有什么非份之想...
也就是说,又要提下面的话了:
4. 不要用goto跳进switcher或从switcher里跳出。后一种情况,应该用break。(注意:如果switcher外面包着循环,需要的话用continue也没问题)
注意对于switcher,编译器可能会为优化执行加入跳转表的数据结构。这样就和第一种情况相同。
- 循环语句
1.结构化的循环的完整结构如下:
// Pre-procees:
...
// Enter the cycle:
for(;;)
{
    // Do somting:
    ...
    if(...)
    {
        ...
        break; // Or a 'continue'.
    }
    // Do somting:
    ...
    if(...)
    {
        ...
        break; // Or a 'continue'.
    }
    // Do somting:
    ...
    if(...)
    {
        ...
        break; // Or a 'continue'.
    }
        ......
    // The 'if-break/continue's have ended.
    // looping ...
   ...
}
// Process after looping:
...
    对以上内容的说明如下:
1.这是循环的完全版,其中if的位置可能出现在任何地方,而不限于上面给出的位置。
2.你可能将预处理和后处理部分不当作循环的一部分,但是按逻辑来说,他们就是循环的一部分。注意写代码时分块清楚。
3.他们在各种情况下才被简化成简单的for、while、do-while结构。对于嵌套,情况类似。
2.使用指针的循环结构:
比如复位内存块,从pFrom开始n个int整数,设n << MAX_INT - 1:
int *p0, * p2;

for(p0 = pFrom, p2 = pFrom + n; p0 < p2; p0++)
    *p0 = 0;
上面代码存在有争议的问题:
按程序结构来说,它是运行最快的。以下代码就比较慢:
int *p0, ns;

for(p0 = pFrom, ns = 0; ns < n; ns++)
    *(p0 + ns) = 0;
但按虚内存的概念来看,第一种代码有潜在的安全问题,因为有可能pFrom被分配到可以使pFrom + n == NULL成立。
不过标准让new操作符分配的内存块大小比实际大小多出一个提交的数据结构大小(malloc仅多给一字节),来避免这种问题(不表示你可以用到那个地址,改写会让DEBUG版抛出异常)。
我们最好用第二种代码,虽然这样会慢一些。
[题外话:这里的ns声明是否正确呢?是否要改成size_t?其实不用。注意pFrom开始的n个字节是已经分配到的。不然代码就运行不到这里来了,这里一定要有n << MAX_INT - 1的保证]
再按虚内存的概念,不要依赖操作系统的内存分配机制来保证pFrom + n == NULL不成立。
3.注意循环内部的变量赋值对循环次数的影响。相信有经验的人这种错误会很少。这里就强调对数据结构对象调用写方法时要注意对循环的影响。使用泛型的尤其要注意。没办法,你得自己研究每种这样的调用会产生的影响了。
2009-1-26 19:04
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
<<接上贴
- 函数
- 前面提过的:成员函数(方法)是不参与决定结构本身的大小的,但函数指针成员则参与。这很好理解:因为函数指针就是一个指针变量。
- 如果有结构体或类指针/引用作为参数,未经说明就不要将无同步保护的多线程共享结构地址传入该函数。
- 如果你想自己定义并编写好用的函数:
1.明确函数的行为、目的、参数及返回值、使用方法及调用时必须满足的前提条件、禁忌、是否要公开到用户库文件中、是否申请了堆、栈开销多大、参数表是否可以再简单些灵活些、性能及其是否胜任请求、是否便于维护升级等。
2.如果函数是对外公开的,应增加完善的参数内部合法性检查,再进入正式处理过程。
3.调用回调函数的函数如果不是对外公开的,对结构体或类作参数的,最好传入结构指针或引用而不要传值以提高性能(当然还要考虑其他情况);如果是对外公开的,对结构体或类作参数的,如果不想让回调函数修改里面的内容,可以传入值拷贝(仍然要考虑其他情况),或者在调用回调函数前后进行保存与恢复工作(注意性能损失问题的处理)。
4.回调函数vs虚函数:
回调函数也可以实现类似C++的自动对象管理:

进入函数-〉创建临时对象并进行预处理-〉调用回调函数,将临时对象作为参数传给它-〉分析临时对象中的内容,得到并提交需要的结果-〉删除临时对象,回收资源-〉从函数返回。

上述过程相当于有一个C++对象的构造析构过程。你甚至就可以用一个C++对象来完成临时对象的创建-回收工作。
和虚函数比,回调函数的灵活性更大:它兼容C;而回调函数可以在运行时按要求变动,不像虚函数那样被类对象绑定。但这样维护的代价也更大了(虚函数可在运行期间被自动识别)。

不要过分迷恋回调函数的自动对象管理应用:注意对象是被调用回调函数的函数指定的,已经指定就不能更改,不像C++那样可以在同一作用域内随意安排对象声明。
- 使用第三方的函数、类问题:
这个问题放在后面,和使用API一起说。注意这个“第三方”还包括你自己,只要你定义了对外公开的函数。

[以后再修改,我要离线了...]
2009-1-26 19:05
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
逐渐转入Windows程序开发话题了...

上面几贴的内容会很有用。后面说的假定你已经知道这些东西了。
这是一个复杂的系统,需要大量的人做不同的工作...

一些基本问题先交代一下

    如果想让你的程序兼容性很好,代码的可移植性很关键。不经过任何修改,可以保证兼容于还没诞生的Windows;不经过任何修改,只需重新编译就可以运行于32位及64位平台上,还有各种Intel、AMD、Alpha平台。
如果你现在还选择C++/C这种“麻烦”的开发工具,“对技术的求知欲”不该是你唯一的理由。你想程序同时满足占内存小、快速运行、容易更快的使用系统API而不用坐等编程工具提供商升级他们的导出库,这不是大多数高级编程语言能够做到的。他们要么运行缓慢,将大量的时间耗费在和业务完全无关的检查、压栈出栈,使用大量的内存来完成同样与业务无关的逻辑相互作用和高级模块化上。计算机越来越快、软件却越来越慢、越来越大,而业务在其中的比例不可能很大,却可能很小...
(注意这和提高软件本身的健壮性是不同的,我们要避免的是昂贵而无用的各种检查、资源消耗行为,但提高软件质量的大量技术却不可少、越完善越好。虽然这两点间的区别有时很模糊)

- 使用windows api的基本要求
1.保证使用的api被修改的可能性微乎其微。
    windows也是一个软件,和其他软件一样会不断升级。很多新的api被加入——尽管经过精心设计,不足依然可能存在。于是,有些api被废弃,还有些被修改或重新定位。大部分的api接口形式不变,但却被改变了行为。api的行为被修订的原因,主要有系统安全、系统内部结构、驱动及操控方式的改变等。无论怎样,以下解决方案总是有效的:
1。不要假定api会执行在其描述文档未提及的实际行为。
2。你最好有预言windows升级后对你调用的api的影响的能力,这种能力越强越好。尽量不要使用可能被改变或丢弃的功能。这样你的程序就具有较强的向后兼容性。不注意该点,当新版操作系统发布后,你的程序在其上很可能运行不正常、功能受限或根本不能运行。
  例如用过vista的,一定受过UAC的“折磨”...
3。尽量使用各操作系统都具有的api及其通用的功能。这样你的程序就具有较强的向前兼容性。如果不这样,你就得做好放弃完全兼容的准备,你的程序在某些旧操作系统上运行不正常、功能受限或根本不能运行。
2.在遵循第一点的前提下,严格按照api说明文挡的要求使用该api。
例如,根据GetClientRect的描述文档,我们应该知道:
下面的代码
RECT rc;

GetClientRect(hWnd, &rc);
// Uses 'rc':
iW = rc.right - rc.left;
...


就要改成
RECT rc;

if(!GetClientRect(hWnd, &rc))
{
// Reports error:
...
return; // or throws an exception to a catcher.
}
// Uses 'rc':
iW = rc.right - rc.left;
...


3.考虑各种可能。如果有多个api可供选择以执行一样的功能,尽量使用不易被废弃、执行快、行为简单、可靠的api。举几个例子说明:
  用GetAncestor(hWnd, GA_PARENT)就比用GetParent(hWnd)好。原因可以通过比较这两个api的说明文档分析得到。
  用CreateWindowEx创建工具条比用CreateToolbar好。虽然后者简单,但已被废弃(我也不想这样,但没办法...)。
  用GetWindowLongPtr比用GetWindowLong好。通过查看导出表,可发现你的程序在32位编译版里只包含有GetWindowLong(这说明32位编译时GetWindowLongPtr是一个宏),但你的程序不用修改就可编译为64位版,照样运行正确。再则,LONG_PTR和LONG属于不同的两类整数。
4.对文档保留一些看法,当文档描述含糊时。因为有很多因素影响api的行为,文档含糊是因为很多细节问题不好交代,或者和很多具体情况交织在一起。一般情况下你可以采用纠错法编程,但如果有些特殊情况下的行为细节对你很重要,你得设法找到详细的官方文档(我必须警告你,它们可能很长,看起来要花上一些时间)来搞清楚。
  考虑到某些文档可能丢失,放一些常用API的说明如以后几贴。

[以后再修改,我要离线了...]
2009-1-26 19:06
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
DllMain(说明:这不是一个API,但用对它很重要)
  详见http://msdn.microsoft.com/en-us/library/ms682583.aspx,(你在自己的MSDN光盘上也可找到。后同,略)需要强调的是Remarks中的
"The entry-point function should perform only simple initialization or termination tasks. It must not call the LoadLibrary or LoadLibraryEx function (or a function that calls these functions), ..."
"Calling functions that require DLLs other than Kernel32.dll may result in problems that are difficult to diagnose. For example, calling User, Shell, and COM functions can cause access violation errors, because some functions load other system components. Conversely, calling functions such as these during termination can cause access violation errors because the corresponding component may already have been unloaded or uninitialized."
当然,如果你还想干点儿别的,全看遍...

以下没特殊说明的,都假设你传入的参数都符合各API说明文档的要求。
InterlockedXXX APIs
即使失败时也不会返回失败。失败的原因是待同步检查的数据没有对齐到指针型整数的边界。

CloseHandle
是否成功仅依赖于句柄对应的驱动程序的行为。还有句柄必须有效的前提。

SetWindowLongPtr/GetWindowLongPtr
微软保证从窗口线程调用这两个API操纵已存在的合法内部字段时函数不会失败。对其他任何情况则不提供这种保证。

[以后再修改,我要离线了...]
2009-1-26 19:07
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
[占位贴5,内容暂时保留,以后再修改]
2009-1-26 19:07
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
[占位贴6,内容暂时保留,以后再修改]
2009-1-26 19:08
0
雪    币: 2362
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
强烈支持
继续继续
2009-1-26 20:18
0
雪    币: 2134
活跃值: (14)
能力值: (RANK:170 )
在线值:
发帖
回帖
粉丝
19
好文留名,期待下文
2009-1-26 21:06
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
好文要靠前啊! 楼主从事的是一项伟大而又艰巨的工作
2009-1-26 23:57
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
内存管理
    很多编程书都谈过这个问题。只关心API的可见罗云斌的《Win32汇编语言程序设计》;关心质量的典型,可见林瑞的《高质量程序设计指南C/C++》;如果用的C++,就应该知道各种经典的对象管理技术,以及用模板包装常规对象如句柄的方法(例如C++ for COM中的ATL),还可以用类似垃圾收集器的原理管理对象...。
    这里主要说明的是对Win32编程有直接关系的内容,这些内容有部分都可以在不同的资料上被找到,只是比较零散而已。
    不知道妥不妥,我把栈内存管理也放在这里?

- 不论用C还是C++,对象都会被自动从内存清除,而C++还要在清除前调用析构函数。现在的Windows应用程序占内存都比较大,除了考虑代码量、堆消耗、静态数据段外,减少栈消耗也很重要。但发现大量的程序根本不考虑这一点。
比如下面的代码:
...
CEgg o1;

o1.ShowDesc();
o1.Draw();

CBird o2;

o2.Jump();
o2.Fly();
...


就可以改成:
...
{
CEgg o1;

o1.ShowDesc();
o1.Draw();
}
{
CBird o2;

o2.Jump();
o2.Fly();
}
...

因为在本例中o1和o2没有被同时使用的问题,用不着同时占着内存,然后一起释放。
- 编写Windows程序可能会遇到不要用std库的问题(微软建议过),如果你非要在Win32代码中调用new而不能确定std库是否能用,建议一例(当然,如果能更结构化一些,最好不要搞成这样):
  如果你想这样干:
int * ptr;

ptr = newint [n];
// uses ptr:
...
  你可将代码写成:
int * ptr;

try
{
ptr = new int [n]; // The std::bad_alloc may be thrown from the line.
if(ptr == NULL)
throw(ptr);
}
catch(...)
{
// Recovers from error status.
}
// uses ptr:
...


- 有关堆内存管理API的补充:
  1.不要以为当系统空闲内存很多时,分配内存就一定成功。要知道你的虚拟内存对整个进程是固定的,当内存碎片化时,可能就没有“空档”能放进你所申请的那块内存。而对于不同的进程,内存视图完全不同,空闲内存会被合并。
  2.当申请了一块可移动块,不要以为对它的锁定操作就一定成功。系统的设计者可以决定分配操作不考虑锁定后的状态而仅考虑有足够数量的空闲内存即可,剩下的道理和1类似。而且,并不保证所有的Windows都是这样设计或者都不是这样设计。研究过内核代码的可能同意或不同意2的观点。问题是如果你是Windows的设计人,你能保证下一个、下一个、...Windows版本也都沿用一样的策略来管理可移动块吗?

[以后再修改,我要离线了...]
2009-2-2 21:15
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
多线程
    编译原理、计算机原理的基本内容,我不想重复了。放上些内容,主要是关于理论与实际间的差距。
- 线程间共享变量应该在声明时加入volatile关键字
    如果不加会怎样?看以下代码:
intptr_t a = 1; // Reset by another thread.

...
while(a)
{// Looping until a equal to zero.
    Sleep(0);
}
优化后可能变成:
intptr_t a = 1; // Reset by another thread.

...
register intptr_t xa = a;

while(xa)
{ // Looping until a equals to zero.
    Sleep(0);
}
结果在发布版中,程序运行到这里后就死循环。

- 关于“不需要同步”的线程间访问编程
    例如你想在一个线程里拷贝文件,而在另一个线程里获取已拷贝的字节数,从而得到进度。
    不要以为你将共享变量加上volatile关键字就完事了。这里不想多说,有现成的文章:http://msdn.microsoft.com/en-us/library/ms686355(VS.85).aspx,(你在自己的MSDN光盘上也可找到)
否则在多处理器、多核系统上运行,拷贝文件的进度条直到完成拷贝,还停留在0%位置。

- 使用互斥、信号、事件同步对象时调用WaitForSingleObject/WaitForMultipleObject等等API时,还有进入同步保护区时,API里面做了内存分配动作。这样你就应当想到不要假定只有要么等待或超时、要么通过的几种状态。还应考虑申请失败的可能。采用纠错法编码吧。如果你很介意,不希望某个地方出现同步失败,就应该用InterlockedXXX APIs(自旋锁原子操作),创建自己的同步锁(注意:该方法不适用于解决跨进程的线程同步问题)。

- 不要以为只有你在编译原理里面学过的几种同步源,事实上,在Windows中,堆内存管理、设备、窗口消息、事件等等很多都事先考虑了同步访问问题。例如,假如你从窗口线程外用SendMessage向窗口发消息时,就不用考虑窗口资源是否可能被从不同的线程同时访问的问题。因为窗口管理程序已经将所有请求串行化了。

[以后再修改,我要离线了...]
2009-2-5 20:26
0
雪    币: 218
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
对象管理
    先说一些题外话...
  其实并没有“Win64编程”这一说法。只有64位Windows、64位平台、(应用程序的)64位版本。Win32PlatformSDK自从7.0版发布以来就做了修订。支持32/64位一体化代码。微软保证:没有特殊说明的API在32位和64位上都有相同的形式(仅针对库文件而言)。64位的API也叫Win32API(的64位版本)。没有Win64API一说。
  当然不是所有的API两平台都有,例如AWE memory APIs就只有32位版本。

    聪明的Windows设计者们并没有因为对象化管理就提供了完全面向对象的编程接口。相反,Windows提供的API很多是基于C的。尽管.NET已经发布并升级了多次,设计者甚至提出了用Windows SDK(基于.net的WinFX开发包)取代Win32PlatformSDK成为核心主件,但似乎并没有放弃C接口的API作为系统API的意思。毕竟让用户能够随便修改系统级的保护数据成员可不是个好主意。而且放弃了能包装成多种编程语言的能力会让微软冒着“无法灵活地扩展编程接口、仅支持自家编程语言”,从而失去部分市场的风险。

    有一点比较重要:对字符串尽量不要用strxxx一族的标准库函数。尽量使用multi-bytes或Unicode处理字符串。(multi-bytes != chars)

- 如果你要提供给别人编程接口,使用C++可以做到高于C的性能(如果你开发的是用户模式应用)
    不要以为C比C++快。当前的C++编译器已经比较完善。事实上,C++的性能很大程度上取决于用户的编程风格。这里提到的是指在完善编程的情况下能达到的。
例如我们创建一个对象,然后处理该对象并得到想要的结果。这里假设不考虑对象管理问题:
C:
LpTagAlian pobj;

pobj = CreateAlian();
if(!pobj)
return(FALSE);
fbRes = AlyAlian(&oResult, pobj);
...
DeleteAlian(pobj);

首先在这里CreateAlian分配一个TagAlian所需内存,然后经过适当处理形成一个有效的TagAlian对象。
然后调用AlyAlian。要命的是因为AlyAlian要公开给用户(例如作为DLL的导出函数),因此AlyAlian内部必须在作所需处理前先检查第二个参数中内容的有效性(除了检查pobj!=NULL外)。
C++:
CAlian obj;

obj.Create();
if(!obj.IsValid())
return(FALSE);
fbRes = obj.Aly(oResult);
...

考虑到有效性基于obj对象的私有属性成员,Aly内部就可以不检查该属性成员指向的堆内的实际对象的有效性。注意这样编码的缺点是,用户可能修改库文件来编程访问私有成员。

    另外,通过上述分析可知,用C++包装已有API是无法像这样提高性能的(反而有所降低)。因为原有的“额外”检查并没有减少。

- 对象的连续引用问题
>相信你不会写出类似下面的代码:
LPDATA lpDEntry = GetData(hWndThis);

if(!lpDEntry)
return(0);
delete lpDEntry; // Frees memory referenced by lpDEntry;
lpDEntry->DoSomething(); // Quotes the 'object' that referenced by lpDEntry!
lpDEntry->msgpost = 1; // Writes 1 to the 'object' that has been deleted!
...


>但是现在我们把它改一下形式,成为:
LPDATA lpDEntry = GetData(hWndThis);

if(!lpDEntry)
return(0);
Foo(lpDEntry);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
...

其中函数Foo的内容为:
void Foo(LPDATA lpDEntry)
{
delete lpDEntry; // Frees memory referenced by lpDEntry;
}


>然后我们可以干得再隐蔽些,成为:
LPDATA lpDEntry = GetData(hWndThis);
LPFOO lpFuncPrcss;

if(!lpDEntry)
return(0);
lpFuncPrcss = SelectFunc(userrequest);
(*lpFuncPrcss)(lpDEntry);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
...

其中LPFOO被声明为:
typedef void (* LPFOO)(LPDATA);
SelectFunc为一个函数,视传入参数的值决定具体的返回函数,其中包括Foo。

>然而,我们还可以干得更隐蔽:
LPDATA lpDEntry = GetData(hWndThis);
LPFOO lpFuncPrcss;

if(!lpDEntry)
return(0);
SendMessage(hWndParent, WM_COMMAND, thisID, hWndThis);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
...

不要忘了其他人在hWndParent的过程响应函数里可以干任何事情,包括调用Foo,如果hWndParent的过程响应函数是那个人编写的话。

>现在即使其他人调用不到Foo也一样糟糕,如果你在hWndThis过程函数里删除*lpDEntry,Foo这个函数甚至根本不存在。

>让我们把问题更简单化些:
设以下代码是你写的hWndThis窗口的过程响应函数中的几行:
RECT rc;

DestroyWindow(hWndThis);
if(!GetClientRect(hWndThis, &rc))
return(0);
if(rc.right - rc.left >= 16)
UseMyFrame(hWndThis, ...

你当然不会这样写,但有可能DestroyWindow处换成了一个SendMessage(hWndParent)。

这样的Win32 Users API还真不少:SendMessage系列、SetFocus、SetCapture、SetWindowPos、GetMessage、PeekMessage、...,及它们的调用者们(如MoveWindow调用了SetWindowPos)。

>杜绝此类故障的方法:
    注意在C++等面向对象程序中可看不到类似在成员函数内部删除自己的例子(违反规则的程序不能被编译或运行时引起bug),因为设计决定了非静态成员函数就是对象的组成部分(如果这个类可以创建对象的话)。
    但是你却可以把回调函数看作对象(严格地说不属于任何对象)的静态成员函数(其实根本就是全局的)。问题是很多回调函数被从某个函数内部激发,各种可能事件都会发生。

1.最简单但无本质帮助的方法就是把禁忌编程规范写到你的公开给用户(用你开发的接口开发的人)的文档中
    例如微软就让开发人员不要对DialogBox调用DestroyWindow而应当用EndDialog(其实只是设置退出标志)来结束模态对话框。
    这样做的坏处就是对有嵌套的用户回调调用可能的用户程序码中,很容易因考虑不周和系统版本升级等因素引发跨越多极的这种故障。而且让用户很难调试跟踪出问题代码来。
2.避免在有内部的用户回调调用可能的函数其后使用对象。如果非要,该点无效。
3.避免这样线性地编程,将能延迟或提前的处理过程尽量分离掉
例如把第一例改成
LPDATA lpDEntry = GetData(hWndThis);
LPFOO lpFuncPrcss;

if(!lpDEntry)
return(0);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
SendMessage(hWndParent, WM_COMMAND, thisID, hWndThis);
...

但如果DoSomething和msgpost的处理想要依赖于SendMessage的结果,该点就无效。
(>>接下贴)
2009-2-14 23:07
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
不错,支持一下子。
2009-2-15 22:08
0
雪    币: 362
活跃值: (25)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
25
楼主写了这么多,一定要顶一下。
2009-2-16 09:35
0
游客
登录 | 注册 方可回帖
返回
//