多C多美丽
Visual C++程序调试方法入门[zz]
之 发表于 2008-04-08 12:29:13
调试是一个程序员最基本的技能,其重要性甚至超过学习一门语言。不会调试的程序员就意味着他即使会一门语言,却不能编制出任何好的软件。
这里我简要的根据自己的经验列出调试中比较常用的技巧,希望对大家有用。
本文约定,在选择菜单时,通过/表示分级菜单,例如File/Open表示顶级菜单File的子菜单Open。
设置
为了调试一个程序,首先必须使程序中包含调试信息。一般情况下,一个从AppWizard创建的工程中包含的Debug Configuration自动包含调试信息,但是是不是Debug版本并不是程序包含调试信息的决定因素,程序设计者可以在任意的Configuration中增加调试信息,包括Release版本。
为了增加调试信息,可以按照下述步骤进行:
- 打开Project settings对话框(可以通过快捷键ALT+F7打开,也可以通过IDE菜单Project/Settings打开)
- 选择C/C++页,Category中选择general ,则出现一个Debug Info下拉列表框,可供选择的调试信息 方式包括:
命令行 Project settings 说明 无 None 没有调试信息 /Zd Line Numbers Only 目标文件或者可执行文件中只包含全局和导出符号以及代码行信息,不包含符号调试信息 /Z7 C 7.0- Compatible 目标文件或者可执行文件中包含行号和所有符号调试信息,包括变量名及类型,函数及原型等 /Zi Program Database 创建一个程序库(PDB),包括类型信息和符号调试信息。 /ZI Program Database for Edit and Continue 除了前面/Zi的功能外,这个选项允许对代码进行调试过程中的修改和继续执行。这个选项同时使#pragma设置的优化功能无效
- 选择Link页,选中复选框"Generate Debug Info",这个选项将使连接器把调试信息写进可执行文件和DLL
- 如果C/C++页中设置了Program Database以上的选项,则Link incrementally可以选择。选中这个选项,将使程序可以在上一次编译的基础上被编译(即增量编译),而不必每次都从头开始编译。
断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是 最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。
设置断点:可以通过下述方法设置一个断点。首先把光标移动到需要设置断点的代码行上,然后
- 按F9快捷键
- 弹出Breakpoints对话框,方法是按快捷键CTRL+B或ALT+F9,或者通过菜单Edit/Breakpoints打开。打开后点击Break at编辑框的右侧的箭头,选择 合适的位置信息。一般情况下,直接选择line xxx就足够了,如果想设置不是当前位置的断点,可以选择Advanced,然后填写函数、行号和可执行文件信息。
条件断点:可以为断点设置一个条件,这样的断点称为条件断点。对于新加的断点,可以单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就 被中断。底下设置包括“观察数组或者结构的元素个数”,似乎可以设置一个指针所指向的内存区的大小,但是我设置一个比较的值但是改动 范围之外的内存区似乎也导致断点起效。最后一个设置可以让程序先执行多少次然后才到达断点。
数据断点:数据断点只能在Breakpoints对话框中设置。选择“Data”页,就显示了设置数据断点的对话框。在编辑框中输入一个表达式,当这个 表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成,例如:在编辑框中输入 g_bFlag这个全局变量的名字,那么当程序中有g_bFlag= !g_bFlag时,程序就将停在这个语句处。
消息断点:VC也支持对Windows消息进行截获。他有两种方式进行截获:窗口消息处理函数和特定消息中断。
在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在上面那个对话框中写入消息处理函数的名字,那么 每次消息被这个函数处理,断点就到达(我觉得如果采用普通断点在这个函数中截获,效果应该一样)。如果在底下的下拉 列表框选择一个消息,则每次这种消息到达,程序就中断。
值
Watch
VC支持查看变量、表达式和内存的值。所有这些观察都必须是在断点中断的情况下进行。
观看变量的值最简单,当断点到达时,把光标移动到这个变量上,停留一会就可以看到变量的值。
VC提供一种被成为Watch的机制来观看变量和表达式的值。在断点状态下,在变量上单击右键,选择Quick Watch, 就弹出一个对话框,显示这个变量的值。
单击Debug工具条上的Watch按钮,就出现一个Watch视图(Watch1,Watch2,Watch3,Watch4),在该视图中输入变量或者表达式,就可以观察 变量或者表达式的值。注意:这个表达式不能有副作用,例如++运算符绝对禁止用于这个表达式中,因为这个运算符将修改变量的值,导致 软件的逻辑被破坏。
Memory
由于指针指向的数组,Watch只能显示第一个元素的值。为了显示数组的后续内容,或者要显示一片内存的内容,可以使用memory功能。在 Debug工具条上点memory按钮,就弹出一个对话框,在其中输入地址,就可以显示该地址指向的内存的内容。
Varibles
Debug工具条上的Varibles按钮弹出一个框,显示所有当前执行上下文中可见的变量的值。特别是当前指令涉及的变量,以红色显示。
寄存器
Debug工具条上的Reigsters按钮弹出一个框,显示当前的所有寄存器的值。
进程控制
VC允许被中断的程序继续运行、单步运行和运行到指定光标处,分别对应快捷键F5、F10/F11和CTRL+F10。各个快捷键功能如下:
| 快捷键 | 说明 |
| F5 | 继续运行 |
| F10 | 单步,如果涉及到子函数,不进入子函数内部 |
| F11 | 单步,如果涉及到子函数,进入子函数内部 |
| CTRL+F10 | 运行到当前光标处。 |
Call Stack
调用堆栈反映了当前断点处函数是被那些函数按照什么顺序调用的。单击Debug工具条上的Call stack就显示Call Stack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中去。
其他调试手段
系统提供一系列特殊的函数或者宏来处理Debug版本相关的信息,如下:
| 宏名/函数名 | 说明 |
| TRACE | 使用方法和printf完全一致,他在output框中输出调试信息 |
| ASSERT | 它接收一个表达式,如果这个表达式为TRUE,则无动作,否则中断当前程序执行。对于系统中出现这个宏 导致的中断,应该认为你的函数调用未能满足系统的调用此函数的前提条件。例如,对于一个还没有创建的窗口调用SetWindowText等。 |
| VERIFY | 和ASSERT功能类似,所不同的是,在Release版本中,ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值。 |
关注
一个好的程序员不应该把所有的判断交给编译器和调试器,应该在程序中自己加以程序保护和错误定位,具体措施包括:
- 对于所有有返回值的函数,都应该检查返回值,除非你确信这个函数调用绝对不会出错,或者不关心它是否出错。
- 一些函数返回错误,需要用其他函数获得错误的具体信息。例如accept返回INVALID_SOCKET表示accept失败,为了查明 具体的失败原因,应该立刻用WSAGetLastError获得错误码,并针对性的解决问题。
- 有些函数通过异常机制抛出错误,应该用TRY-CATCH语句来检查错误
- 程序员对于能处理的错误,应该自己在底层处理,对于不能处理的,应该报告给用户让他们决定怎么处理。如果程序出了异常, 却不对返回值和其他机制返回的错误信息进行判断,只能是加大了找错误的难度。
对于.c文件,由于不能包含stdafx.h,因此可以通过Project settings把它的预编译头设置为“不使用”,方法是:
- 弹出Project settings对话框
- 选择C/C++
- Category选择Precompilation Header
- 选择不使用预编译头。
字节对齐详解[zz]
之 发表于 2008-04-01 21:45:04
一.什么是字节对齐,为什么要对齐?
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
二.字节对齐对程序的影响:
先让我们看几个例子吧(32bit,x86环境,gcc编译器):
设结构体如下定义:
struct A
{
int a;
char b;
short c;
};
struct B
{
char b;
int a;
short c;
};
现在已知32位机器上各种数据类型的长度如下:
char:1(有符号无符号同)
short:2(有符号无符号同)
int:4(有符号无符号同)
long:4(有符号无符号同)
float:4 double:8
那么上面两个结构大小如何呢?
结果是:
sizeof(strcut A)值为8
sizeof(struct B)的值却是12
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个,B也一样;按理说A,B大小应该都是7字节。
之所以出现上面的结果是因为编译器要对数据成员在空间上进行对齐。上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认对齐设置呢,当然可以.例如:
#pragma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。
修改对齐值为1:
#pragma pack (1) /*指定按1字节对齐*/
struct D
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。
后面我们再讲解#pragma pack()的作用.
三.编译器是按照什么样的原则进行对齐的?
先让我们看四个重要的基本概念:
1.数据类型自身的对齐值:
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
2.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
3.指定对齐值:#pragma pack (value)时的指定对齐值value。
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B
{
char b;
int a;
short c;
};
假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为 2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B
共有12个字节,sizeof(struct B)=12;其实如果就这一个就来说它已将满足字节对齐了, 因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍.其实诸如:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了.
同理,分析上面例子C:
#pragma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续
字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、0x0007中,符合 0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C 只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.
四.如何修改编译器的默认对齐值?
1.在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct
Member Alignment中修改,默认是8字节。
2.在编码时,可以这样动态修改:#pragma pack .注意:是pragma而不是progma.
五.针对字节对齐,我们在编程中如何考虑?
如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照 类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做
法是显式的插入reserved成员:
struct A{
char a;
char reserved[3];//使用空间换时间
int b;
}
reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.
六.字节对齐可能带来的隐患:
代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;
p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.
七.如何查找与字节对齐方面的问题:
如果出现对齐或者赋值问题首先查看
1. 编译器的big little端设置
2. 看这种体系本身是否支持非对齐访问
3. 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。
八.相关文章:转自http://blog.csdn.net/goodluckyxl/archive/2005/10/17/506827.aspx
ARM下的对齐处理
from DUI0067D_ADS1_2_CompLib
3.13 type qulifiers
有部分摘自ARM编译器文档对齐部分
对齐的使用:
1.__align(num)
这个用于修改最高级别对象的字节边界。在汇编中使用LDRD或者STRD时
就要用到此命令__align(8)进行修饰限制。来保证数据对象是相应对齐。
这个修饰对象的命令最大是8个字节限制,可以让2字节的对象进行4字节
对齐,但是不能让4字节的对象2字节对齐。
__align是存储类修改,他只修饰最高级类型对象不能用于结构或者函数对象。
2.__packed
__packed是进行一字节对齐
1.不能对packed的对象进行对齐
2.所有对象的读写访问都进行非对齐访问
3.float及包含float的结构联合及未用__packed的对象将不能字节对齐
4.__packed对局部整形变量无影响
5.强制由unpacked对象向packed对象转化是未定义,整形指针可以合法定
义为packed。
__packed int* p; //__packed int 则没有意义
6.对齐或非对齐读写访问带来问题
__packed struct STRUCT_TEST
{
char a;
int b;
char c;
} ; //定义如下结构此时b的起始地址一定是不对齐的
//在栈中访问b可能有问题,因为栈上数据肯定是对齐访问[from CL]
//将下面变量定义成全局静态不在栈上
static char* p;
static struct STRUCT_TEST a;
void Main()
{
__packed int* q; //此时定义成__packed来修饰当前q指向为非对齐的数据地址下面的访问则可以
p = (char*)&a;
q = (int*)(p+1);
*q = 0x87654321;
/*
得到赋值的汇编指令很清楚
ldr r5,0x20001590 ; = #0x12345678
[0xe1a00005] mov r0,r5
[0xeb0000b0] bl __rt_uwrite4 //在此处调用一个写4byte的操作函数
[0xe5c10000] strb r0,[r1,#0] //函数进行4次strb操作然后返回保证了数据正确的访问
[0xe1a02420] mov r2,r0,lsr #8
[0xe5c12001] strb r2,[r1,#1]
[0xe1a02820] mov r2,r0,lsr #16
[0xe5c12002] strb r2,[r1,#2]
[0xe1a02c20] mov r2,r0,lsr #24
[0xe5c12003] strb r2,[r1,#3]
[0xe1a0f00e] mov pc,r14
*/
/*
如果q没有加__packed修饰则汇编出来指令是这样直接会导致奇地址处访问失败
[0xe59f2018] ldr r2,0x20001594 ; = #0x87654321
[0xe5812000] str r2,[r1,#0]
*/
//这样可以很清楚的看到非对齐访问是如何产生错误的
//以及如何消除非对齐访问带来问题
//也可以看到非对齐访问和对齐访问的指令差异导致效率问题
}
C语言-预处理程序[zz]
之 发表于 2008-04-01 10:27:05
我们可以在C源程序中插入传给编译程序的各中指令,这些指令被称为预处理器指令,它们扩充了程序设计的环境。现把常用的预处理命令总结如下:
1. 预处理程序
按照ANSI标准的定义,预处理程序应该处理以下指令:
#if
#ifdef
#ifndef
#else
#elif
#endif
#define
#undef
#line
#error
#pragma
#include
显然,上述所有的12个预处理指令都以符号#开始,,每条预处理指令必须独占一行。
2. #define
#define指令定义一个标识符和一个串(也就是字符集),在源程序中发现该标识符时,都用该串替换之。这种标识符称为宏名字,相应的替换称为宏代换。一般形式如下:
#define macro-name char-sequence
这种语句不用分号结尾。宏名字和串之间可以有多个空白符,但串开始后只能以新行终止。
例如:我们使用LEFT代表1,用RIGHT代表0,我们使用两个#define指令:
#define LEFT 1
#define RIGHT 0
每当在源程序中遇到LEFT或RIGHT时,编译程序都用1或0替换。
定义一个宏名字之后,可以在其他宏定义中使用,例如:
#define ONE 1
#define TWO ONE+ONE
#define THREE ONE+TWO
宏代换就是用相关的串替代标识符。因此,如果希望定义一条标准错误信息时,可以如下定义:
#define ERROR_MS “Standard error on input \n”
如果一个串长于一行,可在行尾用反斜线”\”续行,如下:
#define LONG_STRING “This is a very very long \
String that is used as an example”
3. #error
#error指令强制编译程序停止编译,它主要用于程序调试。#error指令的一般形式是:
#error error-message
注意,宏串error-message不用双引号包围。遇到#error指令时,错误信息被显示,可能同时还显示编译程序作者预先定义的其他内容。
4. #include
程序中的#include指令要求编译程序读入另一个源文件。被读入文件的名字必须用双引号(“”)或一对尖括号(<>)包围,例如:
#include “stdio.h”
#include <stdio.h>
都使C编译程序读入并编译头文件以用于I/O系统库函数。
包含文件中可以包含其他#include指令,称为嵌套包含。允许的最大嵌套深度随编译器而变。
文件名被双括号或尖括号包围决定了对指定文件的搜索方式。文件名被尖括号包围时,搜索按编译程序作者的定义进行,一般用于搜索某些专门放置包含文件的特殊目录。当文件名被双括号包围时,搜索按编译程序实时的规定进行,一般搜索当前目录。如未发现,再按尖括号包围时的办法重新搜索一次。
通常,绝大多数程序员使用尖括号包围标准的头文件,双引号用于包围与当前程序相关的文件名。
5. 条件编译指令
若干编译指令允许程序员有选择的编译程序源代码的不同部分,这种过程称为条件编译。
5.1#if、#else、#elif #endif
条件编译指令中最常用的或许是#if,#else,#elif和#endif。这些指令允许程序员根据常数表达式的结果有条件的包围部分代码。
#if的一般形式是:
#if constant-expression
Statement sequence
#endif
如#if后的常数表达式为真,则#if和#endif中间的代码被编译,否则忽略该代码段。#endif标记#if块的结束。
#else指令的作用与C语言的else相似,#if指令失败时它可以作为备选指令。例如:
#include <stdio.h>
#define MAX 100
Int main(void)
{
#if MAX>99
printf(“Compiled for array greater than 99.\n”);
#else
printf(“Complied for small array.\n”);
#endif
return 0;
}
注意,#else既是标记#if块的结束,也标记#else块的开始。因为每个#if只能写一个#endif匹配。
#elif指令的意思是“否则,如果”,为多重编译选择建立一条if-else-if(如果-否则-如果链)。如果#if表达式为真,该代码块被编译,不测试其他#elif表达式。否则,序列中的下一块被测试,如果成功则编译之。一般形式如下:
#if expression
Statement sequence
#elif expression1
Statement sequence
#elif expression2
Statement sequence
.
.
.
#elif expression
Statement sequence
#endif
5.2#ifdef和#ifndef
条件编译的另一个方法是使用编译指令#ifdef和#ifndef,分别表示“如果已定义”和“如果未定义”。#ifdef的一般形式如下:
#ifdef macro-name
Statement sequence
#endif
如果macro-name原先已经被一个#define语句定义,则编译其中的代码块。
#ifndef的一般形式是:
#ifndef macro-name
Statement sequence
#endif
如果macro-name当前未被#define语句定义,则编译其中的代码块。
#ifdef和#ifndef都可以使用#else或#elif语句。
#inlucde <stdio.h>
#define T 10
Int main(void)
{
#ifdef t
Printf(“Hi T\n”);
#else
Printf(“Hi anyone\n”);
#endif
#ifndef M
Printf(“M Not Defined\n”);
#endif
Return 0;
}
6. #undef
#undef指令删除前面定义的宏名字。也就是说,它“不定义”宏。一般形式为:
#undef macro-name
7. 使用defined
除#ifdef之外,还有另外一种确定是否定义宏名字的方法,即可以将#if指令与defined编译时操作符一起使用。defined操作符的一般形式如下:
defined macro-name
如果macro-name是当前定义的,则表达式为真,否则为假。
例如,确定宏MY是否定义,可以使用下列两种预处理命令之一:
#if defined MY
或
#ifdef MY
也可以在defined之前加上感叹号”!”来反转相应的条件。例如,只有在DEBUG未定义的情况下才编译。
#if !defined DEBUG
Printf(“Final Version!\n”);
#endif
使用defined的一个原因是,它允许由#elif语句确定的宏名字存在。
8. #line
#line指令改变__LINE__和__FILE__的内容。__LINE__和__FILE__都是编译程序中预定义的标识符。标识符__LINE__的内容是当前被编译代码行的行号,__FILE__的内容是当前被编译源文件的文件名。#line的一般形式是:
#line number “filename”
其中,number是正整数并变成__LINE__的新值;可选的“filename”是合法文件标识符并变成__FILE__的新值。#line主要用于调试和特殊应用。
9. #pragma
#pragma是编译程序实现时定义的指令,它允许由此向编译程序传入各种指令。例如,一个编译程序可能具有支持跟踪程序执行的选项,此时可以用#pragma语句选择该功能。编译程序忽略其不支持的#pragma选项。#pragma提高C源程序对编译程序的可移植性。
10. 预处理操作符#和##
有两个预处理操作符:#和##,它们可以在#define中使用。
操作符#通常称为字符串化的操作符,它把其后的串变成用双引号包围的串。例如:
#include <stdio.h>
#define mkstr(s) #s
int main(void)
{
Printf(mkstr(I like C));
Return 0;
}
预处理程序把以下的语句:
Printf(mkstr(I like C));
变成
Printf(“I like C”);
操作符##把两个标记拼在一起,形成一个新标记。例如:
#include <stdio.h>
#define concat(a,a) a##b
int main(void)
{
Int xy = 10;
Printf(“%d”,concat(x,y));
Return 0;
}
预处理程序把以下语句:
Printf(“%d”,concat(x,y));
变成
Printf(“%d”,xy);
操作符#和##主要作用是允许预处理程序对付某些特殊情况,多数程序中并不需要。
11. 预定义宏
C规范了5个固有的预定义宏,它们是:
__LINE__
__FILE__
__DATE__
__TIME__
__STDC__
__LINE__和__FILE__包含正在编译的程序的行号和文件名。
__DATE__和内容形如month/day/year(月/日/年)的串,代表源文件翻译成目标码的日期。
__TIME__中的串代表源代码编译成目标码的时间,形如hour:minute:second(时:分:秒)
如果__STDC__的内容是十进制常数1,则表示编译程序的实现符合标准C。
优化--C程序员之终极标靶【zz】
之 发表于 2008-03-24 20:47:04
介绍:
最简单的优化方法是借助prof工具判断程序的瓶颈在哪里,你必须判断出
程序的那些部分消耗了大量资源.
一旦你判断出瓶颈(比如说执行上万次的循环),你所做的第一件事就是重
新设计程序,减低循环次数.当然,现在绝大多数优化编译器可以做到这一
点,(不过最好还是自己来--东楼),但是记住,当以下情况出现时,优化是
在浪费时间:
1)程序只写了一部分
2)程序还没有测试通过
3)看起来已经足够快了
还要注意的就是判断程序的用途,如果仅仅为了得到一份报告而写的仅运
行一次的程序,用户往往在午餐前运行程序,这时,程序只要在他们回来之
前运行完就可以了,如果程序调用其他的程序,而且其他程序都比较慢,那
么优不优化效果都差不多,但是,如果是GUI图形用户界面程序(比如鼠标光
标显示程序),那么一点点的延迟都会遭到用户的投诉
完成优化后,带上所有的优化命令编译,然后用你实际使用的数据测试它,
如果做不到这一点,请小心选择你的测试数据,程序员多半倾向于按照程序
的要求给输入数据,但用户可不这么干.
如果你已经完成了所有优化,但是程序仍然看起来不快,注意一下你的操作
系统,很多多任务操作系统按时间片来划分用户资源,如果给你的资源太少,
那和你的系统管理员联系吧.
1.选择一个更好的算法:
应该熟悉算法语言,知道各种算法的优缺点,一般很多计算机资料文本上有
介绍,应该能够看得懂算法描述.
这里是一些明显可以通用的替换
慢的算法 替换成
顺序查找 二分法查找或乱序查找
插入排序或冒泡排序 快速排序,合并排序,根(radix)排序
还要选择一种合适的数据结构(记着,你的程序所干的唯一一件事就是在计
算机里搬数,把一堆数从一个地方提出来,处理一下,甩到另一个地方,那么
按什么方式搬数有多重要你应该知道了吧--东楼),比如你在一堆随机存放
的数中使用了大量的插入和删除指令,那使用链表要快得多.如果你要做二
分法查找,那提前排下序非常重要.
2.写一些清晰,可读性好并且简单的代码
一个人容易看得懂的程序同样也容易被编译器读懂.一个大而复杂的表达式
往往会把编译器脑袋都弄大,为了防止自己发疯,编译器往往放弃对这段代
码的优化.但绝对不会向你报告,出于维护自己面子起见,东楼发现所有的编
译器都只会向你报告它优化了多少,而决不会报告它干不了的有多少,东楼
就亲眼见到一个瓜编译器因为一个表达式弄昏了头,把整个模块的优化都放
弃了,回来居然还恬不知耻的报告优化非常顺利,整个儿一个报喜不报忧.
适当的时候尽量减小每个函数的代码量(这时候对代码要抠一点,懂吗?--东
楼),不过也别走极端,别为了优化把一个函数写成10页纸的一堆函数,那编
译器倒高兴了,可人发疯了.
优化后,赶快找一台快点的机器看看效果吧(满足一下虚荣心,嬉嘻!)
3.透视你的程序
一个程序写出来,凭直觉就应该感觉出哪些地方快,哪些地方慢,(就是,东楼
的程序就是全部凭直觉优化的(...反正吹牛不上税,嘻嘻)),一般说来,最快
的运算就是分配一块内存,给指针赋值,还有就是两个整数的加法运算,别的
都有点慢,最慢的就要数打开文件啦,打开新的进程啦,读写一大块内存啦,
寻找啦,排序啦等等,别看这帮虾子指令都只要几个微秒,可成百上千的杀将
过来,东楼可受不了.一定不能让这帮虾子进循环,干了它.
这是经常犯的一个错误:
if (x != 0) x = 0;
程序的原意是当x等于0时,节约时间不执行赋值操作,可你别忘了,赋值语
句才是最快的,那还不如直接写成下面的语句更来劲.
x = 0;
还有就是一些神勇的大虾,非得等到编译器把代码输出成汇编语言级然后
拿着计算器一行行加汇编指令的个数和周期数,才算优化完成了,不过可
别忘了,最后一次优化不是obj代码级的,而是由link程序完成的,这没多
大用.
4.理解你的编译程序选项
许多编译程序有几级优化选项,注意使用最优化的一项,特别注意gcc,优化
选项非常多,小心使用,别弄得适得其反.
通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,精
简指令,(如DJGPP的-O3),但少数情况下,这会影响程序的正确性,这是你只
有改自己的程序啦.不过也有这种情况,就是一些参数会影响优化的程序,
但是不会影响普通程序,这时只有具体情况具体分析了.
5.内联(内嵌)
gcc(使用-finline-functions参数),还有一些别的编译器可以在最高级优
化中内联一些小的函数.K&C编译器则只有在库函数是用汇编写成的时候才
内联,C++编译器普遍支持内联函数.
不过把C函数写成宏也能达到加速的作用,不过必须是在程序完全除错之后,
因为绝大多数除错程序不支持宏除错.
宏内联的例子:
旧代码:
int foo(a, b)
{
a = a - b;
b++;
a = a * b;
return a;
}
新代码:
#define foo(a, b) (((a)-(b)) * ((b)+1))
注意最外层括号是必须的,因为当宏在表达式中展开时,你不知道表达式里
还有没有比乘法级别更高的运算.
一些警告:
1.无限制地使用宏可以使代码爆炸,程序会很快消耗完你所有的资源,包
括物理内存,最后系统要么崩溃,要么把你的代码放到虚拟内存(磁盘上)
中去,那你再怎么优化也没用了
2.C的宏每次调用都要对参数赋值,如果参数很多很复杂,那光赋值就要消
耗大量的CPU时间,效果还不如不用宏
3.因为宏允许包含很复杂的表达式,所以编译程序会非常辛苦,为了使自
己不至于完全发疯,一般编译程序对宏能包含的字符数都有一个限制,注
意别犯规.
4.一旦用了宏,prof程序也跟着糊涂起来了,这是它说的话可信度可不高
6.循环展开
这是经典的速度优化,但许多编译程序(如gcc -funroll-loops)能自动完成
这个事,所以现在你自己来优化这个显得效果不明显.(这里说一句,云风工
作室的云风朋友曾来信和东楼专门探讨过这个问题,他根据自己在DJGPP的
经验认定循环展开无效,东楼猜测可能就是因为gcc在编译时自动进行了展
开,所以手工展开已经没多大效果了.但这个方法总是对的).
旧代码:
for (i = 0; i < 100; i++)
{
do_stuff(i);
}
新代码:
for (i = 0; i < 100; )
{
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
}
可以看出,新代码里比较指令由100次降低为10次,循环时间节约了90%.
不过注意:对于中间变量或结果被更改的循环,编译程序往往拒绝展开,(怕
担责任呗),这时候就需要你自己来做展开工作了.
还有一点请注意,在有内部指令cache的CPU上(如MMX芯片),因为循环展开的
代码很大,往往cache溢出,这时展开的代码会频繁地在CPU 的cache和内存
之间调来调去,又因为cache速度很高,所以此时循环展开反而会变慢.还有
就是循环展开会影响矢量运算优化.
7.循环嵌套
把相关循环放到一个循环里,也会加快速度.
旧代码:
for (i = 0; i < MAX; i++) /* initialize 2d array to 0's */
for (j = 0; j < MAX; j++)
a[i][j] = 0.0;
for (i = 0; i < MAX; i++) /* put 1's along the diagonal */
a[i][i] = 1.0;
新代码:
for (i = 0; i < MAX; i++) /* initialize 2d array to 0's */
{
for (j = 0; j < MAX; j++)
a[i][j] = 0.0;
a[i][i] = 1.0; /* put 1's along the diagonal */
}
8.循环转置
有些机器对JNZ(为0转移)有特别的指令处理,速度非常快,如果你的循环对方向
不敏感,可以由大向小循环
旧代码:
for (i = 1; i <= MAX; i++)
{
...
}
新代码:
i = MAX+1;
while (--i)
{
...
}
不过千万注意,如果指针操作使用了i值,这种方法可能引起指针索引超界的严重
错误(i = MAX+1;).当然你可以通过对i做加减运算来纠正,但是这样加速的作用
就没有了除非类似于以下情况
旧代码:
char a[MAX+5];
for (i = 1; i <= MAX; i++)
{
*(a+i+5)=0;
}
新代码:
i = MAX+1;
while (--i)
{
*(a+i+4)=0;
}
9.减小运算强度
采用运算量更小的表达式替换原来的表达式,下面是一个经典例子:
旧代码:
x = w % 8;
y = pow(x, 2.0);
z = y * 33;
for (i = 0; i < MAX; i++)
{
h = 14 * i;
printf("%d", h);
}
新代码:
x = w & 7; /* 位操作比求余运算快 */
y = x * x; /* 乘法比平方运算快 */
z = (y << 5) + y; /* 位移乘法比乘法快 */
for (i = h = 0; i < MAX; i++)
{
h += 14; /* 加法比乘法快 */
printf("%d", h);
}
10.循环不变计算
对于一些不需要循环变量参加运算的计算任务可以把它们放到循环外面,现在许
多编译器还是能自己干这件事,不过对于中间使用了变量的算式它们就不敢动了,
所以很多情况下你还得自己干.
那位大哥说了,不就是把没必要的表达式拿出来嘛,这话咱可得商量商量,这里的
计算任务可不是仅仅表达式那么简单,什么调用函数啦,指针运算啦,数组访问啦,
总之,凡是你相让计算机干的事都算计算任务.
对于那些在循环中调用的函数,也不能让它们轻松了,把它扒光了看看,凡是没必
要执行多次的操作通通提出来,放到一个init函数里,循环前调用.另外尽量减少
喂食次数,没必要的话尽量不给它传参,需要循环变量的话让它自己建立一个静
态循环变量自己累加,速度会快一点.
还有就是结构体访问,东楼的经验,凡是在循环里对一个结构体的两个以上的元
素执行了访问,就有必要建立中间变量了(结构这样,那C++的对象呢?想想看),看
下面的例子:
旧代码:
total =
a->b->c[4]->aardvark +
a->b->c[4]->baboon +
a->b->c[4]->cheetah +
a->b->c[4]->dog;
新代码:
struct animals * temp = a->b->c[4];
total =
temp->aardvark +
temp->baboon +
temp->cheetah +
temp->dog;
一些老的C语言编译器不做聚合优化,而符合ANSI规范的新的编译器可以自动完
成这个优化,看例子:
float a, b, c, d, f, g;
...
a = b / c * d;
f = b * g / c;
这种写法当然要得,但是没有优化
float a, b, c, d, f, g;
...
a = b / c * d;
f = b / c * g;
如果这么写的话,一个符合ANSI规范的新的编译器可以只计算b/c一次,然后将结
果代入第二个式子,节约了一次除法运算.
11.公用代码块
一些公用处理模块,为了满足各种不同的调用需要,往往在内部采用了大量的
if-then-else结构,这样很不好,判断语句如果太复杂,会消耗大量的时间的,应
该尽量减少公用代码块的使用.(任何情况下,空间优化和时间优化都是对立的--
东楼).
当然,如果仅仅是一个(3==x)之类的简单判断,适当使用一下,也还是允许的.记
住,优化永远是追求一种平衡,而不是走极端.
12.采用递归
与LISP之类的语言不同,C语言一开始就病态地喜欢用重复代码循环,许多C程序
员(包括东楼)都是除非算法要求,坚决不用递归.事实上,C编译器们对优化递归
调用一点都不反感,相反,它们还很喜欢干这件事.只有在递归函数需要传递大量
参数,可能造成瓶颈的时候,才应该使用循环代码,其他时候,还是用递归好些.
13.查表(游戏程序员必修课)
一个聪明的游戏大虾,基本上不会在自己的主循环里搞什么运算工作,绝对是先
计算好了,再到循环里查表.(东楼每一次写游戏,基本上都有一大堆表格).看下
面的例子:
旧代码:
long factorial(int i)
{
if (i == 0)
return 1;
else
return i * factorial(i - 1);
}
新代码:
static long factorial_table[] =
{1, 1, 2, 6, 24, 120, 720 /* etc */};
long factorial(int i)
{
return factorial_table[i];
}
如果表很大,不好写,就写一个init函数,在循环外临时生成表格.
14.变量
在最内层循环避免使用全局变量和静态变量,除非你能确定它在循环周期中不会
动态变化,大多数编译器们优化变量仅有置成寄存器变量一招,而对于动态变量,
它们干脆放弃对整个表达式的优化.
尽量避免把一个变量地址传递给另一个函数,虽然这个还很常用.C语言的编译器
们总是先假定每一个函数的变量都是内部变量,这是由它的机制决定的,在这种
情况下,它们的优化完成得最好,但是,一旦一个变量有可能被别的函数改变,这
帮兄弟就再也不敢把变量放到寄存器里了,严重影响速度.看例子:
a = b();
c(&d);
因为d的地址被c函数使用,有可能被改变,编译器不敢把它长时间的放在寄存器
里,一旦运行到c(&d),编译器就把它丢回内存,如果在循环里,会造成N次频繁的
在内存和寄存器之间读写d的动作,众所周知,CPU在系统总线上的读写速度可是
慢得可以.比如你的赛杨300,CPU主频300,总线速度最多66M,为了一个总线读,
CPU可能要等4-5个周期,得..得..得..想起来都打颤.
C语言基本功教程系列(3) - 快速的函数调用【zz】
之 发表于 2008-03-24 20:43:31
首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用const来定义参数类型
=========不好的风格==========
void function(char * ServerName)
{
// 内部不允许对ServerName的内容进行修改
}
=========好的风格===========
void function(const char * ServerName)
{
// 内部不允许对ServerName的内容进行修改
}
为什么这么做呢? 举个简单的例子: 在团队开发中程序员A写好了displayFunction,传了一个数据结构给displayFunction做图象显示,然后在接下来的程序中对数据进行计算。A认为displayFunction不会对数据进行修改,所以在以后的数据运算中,没有进行一致性检测。过了几天程序员B被派过来优化A的程序,因为不知道不能改数据,结果改了下,在displayFunction中改变了数据结构的内容,当时测试通过。但是在产品发布的Alpha测试阶段,用real data的时候出了问题。我想通宵debug去差这么点个小问题,不是很值得吧。只要稍微留点心,就可以避免了
==================分割线==================
下面谈谈函数的调用问题。我们都知道,在调用的一个函数的时候,传给函数的参数是要压到栈里,然后才能被函数访问。我们来看一下函数调用的汇编代码.(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02)
=======printf("%s%d%d%d%d",haha,m,n,p,i);======
00401000 push ecx
00401001 push ebx
00401002 mov ebx, dword ptr [esp+04]
00401003 push ebp
00401004 mov ebp, dword ptr [esp+08]
00401005 push esi
00401006 push edi
00401007 mov edi, dword ptr [esp+10]
00401008 xor esi, esi
00401009 push esi
0040100A push edi
0040100B push ebx
0040100C push ebp
0040100D push 00408040
0040100E push 004060FC
0040100F call 00401054
我的天哪,这是多少代码,只不过为了把参数push到栈里就用了15条。看我们看看另一段代码
===========printf("%s",haha);============
00401010 push 00408040
00401011 push 004060FC
00401012 call 00401054
现在我不用说大家都明白了吧。传递给函数的参数越少越好,最好就是一个指针,指向一个structure。这就是为什么大部分的directX的函数就是一个指针的大structure传过去。里边的参数好几十个。当然了 void fucntion(void)是最快的函数调用,也可以用inline来优化关键循环内的函数。不过在每一个frame的执行代码中,有成百上千个函数,不可能所有的都inline吧。所有能快点就快点喽。当然了,传递structure的reference也是同样的效果,只要不把structure当参数就好。
============错误的方式===========
void function(struct OneStructure Parameter);
============正确的方式===========
void function(struct OneStructure & Parameter);
or
void function(struct OneStructure * pParameter);
==================分割线==================
这个例子不是很好,因为降低了代码的可读性,不过做为参考。。。。
很多人喜欢写代码的时候这么写:
char szName[] = "Aear";
int length;
length = strlen(szName);
if(length > 0) // 这行的效率不考虑
{
// do something
}
粗一看没什么问题,不过如果length在以后用不到的话,那么就浪费了。因为length占用了内存,而且浪费了cpu资源。让我们看带汇编代码(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02)
length = strlen(szName);
if(length > 0) {...}
0040101F sub eax, edx
00401021 mov dword ptr [esp+4], eax // 把返回值存到length中
00401025 je 00401039 // 判断跳转
========更快速的写法的代码========
if(strlen(szName)) {...}
0040101F sub eax, edx
00401021 mov esi, eax //把返回值放在个临时寄存器中
00401023 je 00401037
大家都知道寄存器之间进行数据操作是非常快的,而且是稳定的一个cpu clock cycle,至于00401021 mov dword ptr [esp+4], eax 到底要花多少个clock cycle,那只有天知道了。因为这种从内存中读数据的指令,最少也是2个clock cycle,即使在L2 cache中,也不会比 mov esi, eax 快,而且浪费了栈空间。
==================再分割下吧,虽然不是很喜欢==================
最后说说一种类告诉的分枝判断参数传递。在有些情况下,我们经常要传很多参数,比如pixel shader等等,这些函数根据参数的设置,进行不同的操作。举个例子:
struct Parameter{
bool bDrawWater;
bool bDrawSkybox;
bool bDrawTerrain;
bool bDrawSepcialEffects;
} DrawParamter;
void DrawEnvironment( struct Parameter * pPara)
{
if(pPara->bDrawWater) {....};
if(pPara->bDrawSkybox) {....};
if(pPara->bDrawTerrain) {....};
if(pPara->bDrawSpecialEffects) {....};
}
对于这样的代码,还有更快速, 更节省内存的方法,那就是位操作。
const static UINT32 DRAW_WATER_FLAG = 1;
const static UINT32 DRAW_SKYBOX_FLAG = 1 << 1;
const static UINT32 DRAW_TERRAIN_FLAG = 1 << 2;
const static UINT32 DRAW_SPECIALEFFECTS_FLAG = 1 << 3;
void DrawEnvironment(UINT32 DrawFlag)
{
//注意了,这里不需要 pPara->,也就是节省了内存访问,速度至少提高了1到2个clock cycle
if( DrawFlag & DRAW_WATER_FLAG ) {.....};
if( DrawFlag & DRAW_SKYBOX_FLAG) {.....};
//甚至还可以进行各种不同组合的判断,比如
if( DrawFlag & (DRAW_WATER_FLAG | DRAW_SKYBOX_FLAG) ) {....};
}
在调用的时候,代码更加简洁明了:
DrawEnvironment( DRAW_WATER_FLAG | DRAW_TERRAIN_FLAG );
节选:C语言基本功教程系列(2)【zz】
之 发表于 2008-03-24 20:42:02
if语句很简单,相信大家都会,但是确有很多值得注意的。 首先来说一下code style的问题。
=========不好的风格===========
if( (x +4-y * 25) > 10 || y > 1023 || GetSomething())
{
....
}
=========好的风格============
if( (x +4-y * 25) > 10
|| y > 1023
|| GetSomething() )
{
....
}
相信大家能看出来第2段代码的时候要比第1段代码容易读的多。
if语句虽然简单,但是涉及到CPU的branch prediction的问题。简单的说, CPU有个指令缓存,会预先把一部分代码读到缓存中等待稍后执行。当CPU遇到 if语句的时候, 会把条件判断为true的那段代码读到缓存中,然后对if(条件判断)中的条件判断语句进行运算。如果运算结果是false,那么CPU就会重新从内存中载入false的代码,在这期间大部分CPU时间会被浪费点。
所以在写if语句的时候,一定要把最容易成立的条件放在最前面进行判断。 比如:
======错误的写法=======
if( (float)rand() / RAND_MAX < 0.2 ) //只有20%的可能运行if部分
{
// 被读入到指令缓存的部分。
}
======正确的写法=======
if( (float)rand() / RAND_MAX > 0.2 ) //有80%的可能运行if部分。
{
// 被读入到指令缓存的部分。
}
if语句另外一个需要注意的地方是在进行多重条件判断的时候,要安排好顺序。比如:
if ( (float)rand() / RAND_MAX < 0.4
&& (float)rand() / RAND_MAX < 0.3
&& (float)rand() / RAND_MAX < 0.2 )
{
......
}
根据C语言的规则(这点不同于Pascal),如果第一个条件(rand() / RAND_MAX < 0.4)不成立,那么就不会运行第2和第3个条件,而直接跳转。 所以应该把最难成立的条件放在第一的位置上,正确的代码为:
if ( (float)rand() / RAND_MAX < 0.2 // 只有%20的可能
&& (float)rand() / RAND_MAX < 0.3
&& (float)rand() / RAND_MAX < 0.4 )
{
......
}
由于编译器并无法计算和统计每种条件成立的可能性,只能靠大家手动的调整来提高代码的效率。
最后是if有一种技术叫做binary branch,举个简单的例子,代码如下:
int x;
if( x == 1)
{
}
else if( x == 2)
{
}
else if( x == 3)
{
}
else if( x == 4)
{
}
对付这段代码,可以用switch来解决,也可以用binary branch,修改后的代码如下:
if( x <= 2)
{
if( x == 1)
{...}
else
{...}
}
else
{
if( x == 3)
{...}
else
{...}
}
如果判断的情况复杂一点,编译器就没有优化的能力,需要考大家自己动手啦。
