select * into destTbl from srcTbl
insert into destTbl(fld1, fld2) select fld1, 5 from srcTbl
以上两句都是将 srcTbl 的数据插入到 destTbl,但两句又有区别的:
第一句(select into from)要求目标表(destTbl)不存在,因为在插入时会自动创建。
第二句(insert into select from)要求目标表(destTbl)存在,由于目标表已经存在,所以我们除了插入源表(srcTbl)的字段外,还可以插入常量,如例中的:5。
2009年5月12日星期二
2009年5月11日星期一
关于构造函数 析构函数和虚函数的关系收藏
构造函数不能声明为虚函数的原因是:
解释一:所谓虚函数就是多态情况下只执行一个,而从继承的概念来讲,总是要先构造父类对象,然后才能是子类对象,如果构造函数设为虚函数,那么当你在构造父类的构造函数时就不得不显示的调用构造,还有一个原因就是为了防错,试想如果你在子类中一不小心重写了个跟父类构造函数一样的函数,那么你的父类的构造函数将被覆盖,也即不能完成父类的构造.就会出错.
解释二:虚函数的主要意义在于被派生类继承从而产生多态. 派生类的构造函数中, 编译器会加入构造基类的代码, 如果基类的构造函数用到参数, 则派生类在其构造函数的初始化列表中必须为基类给出参数, 就是这个原因.
最近有人问构造函数能不能是虚函数: 当然不能 解释一下: 1,从存储空间角度 虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。 2,从使用角度 虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。 =: 虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。 三. 构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它 但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。四: 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数 从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数 五: 当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。 当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。 所以它使用的V P T R必须是对于这个类的V TA B L E。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内, V P T R将 保持被初始化为指向这个V TA B L E。但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的 V TA B L E,等.直到最后的构造函数结束。V P T R的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生 类顺序的另一个理由。 但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置V P T R指向它自己的 V TA B L E。如果函数调用使用虚机制,它将只产生通过它自己的V TA B L E的调用,而不是最后的V TA B L E(所有构造函数被 调用后才会有最后的V TA B L E)。
析构函数设为虚函数的作用:
解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
#include "stdafx.h"
#include "stdio.h"
class A
{
public:
A();
virtual ~A();
};
A::A()
{
}
A::~A()
{
printf("Delete class AP\n");
}
class B : public A
{
public:
B();
~B();
};
B::B()
{
}
B::~B()
{
printf("Delete class BP\n");
}
int main(int argc, char* argv[])
{
A *b=new B;
delete b;
return 0;
}
输出结果为:Delete class B
Delete class A
如果把A 的virtual 去掉:
那就变成了Delete class A
解释一:所谓虚函数就是多态情况下只执行一个,而从继承的概念来讲,总是要先构造父类对象,然后才能是子类对象,如果构造函数设为虚函数,那么当你在构造父类的构造函数时就不得不显示的调用构造,还有一个原因就是为了防错,试想如果你在子类中一不小心重写了个跟父类构造函数一样的函数,那么你的父类的构造函数将被覆盖,也即不能完成父类的构造.就会出错.
解释二:虚函数的主要意义在于被派生类继承从而产生多态. 派生类的构造函数中, 编译器会加入构造基类的代码, 如果基类的构造函数用到参数, 则派生类在其构造函数的初始化列表中必须为基类给出参数, 就是这个原因.
最近有人问构造函数能不能是虚函数: 当然不能 解释一下: 1,从存储空间角度 虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。 2,从使用角度 虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。 =: 虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。 三. 构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它 但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。四: 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数 从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数 五: 当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。 当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。 所以它使用的V P T R必须是对于这个类的V TA B L E。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内, V P T R将 保持被初始化为指向这个V TA B L E。但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的 V TA B L E,等.直到最后的构造函数结束。V P T R的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生 类顺序的另一个理由。 但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置V P T R指向它自己的 V TA B L E。如果函数调用使用虚机制,它将只产生通过它自己的V TA B L E的调用,而不是最后的V TA B L E(所有构造函数被 调用后才会有最后的V TA B L E)。
析构函数设为虚函数的作用:
解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
#include "stdafx.h"
#include "stdio.h"
class A
{
public:
A();
virtual ~A();
};
A::A()
{
}
A::~A()
{
printf("Delete class AP\n");
}
class B : public A
{
public:
B();
~B();
};
B::B()
{
}
B::~B()
{
printf("Delete class BP\n");
}
int main(int argc, char* argv[])
{
A *b=new B;
delete b;
return 0;
}
输出结果为:Delete class B
Delete class A
如果把A 的virtual 去掉:
那就变成了Delete class A
2009年5月10日星期日
C++ 的构造/析构/赋值/拷贝函数比较
构造函数、析构函数与赋值函数是每个类最基本的函数。每个类只有一个析构函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)和多个赋值函数(除了同类的赋值以外,还有其他的赋值方法)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
有几个需要注意的内容:
@ 构造函数与析构函数的另一个特别之处是没有返回值类型
@ 构造从类层次的最顶层的基类开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,在析构的时候,最低层的派生类的析构函数最开始被调用,然后调用每个基类的析构函数。
@ “缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错
下面通过例子进一步说明,
1.构造函数的初始化表
设存在两个类:
class A
{
…
A(void); // 无参数构造函数
A(const A &other); // 拷贝构造函数
A & operate =( const A &other); // 赋值函数
virtual ~A(void); //析构函数
};
class B
{
public:
B(const A &a); // B的构造函数
private:
A m_a; // 成员对象
};
下面是B的构造函数的2个实现,其中第一个的类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化;而第二个B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。
B::B(const A &a)
: m_a(a)
{
…
}
B::B(const A &a)
{
m_a = a;
…
}
2.拷贝函数和构造函数的区别
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,可以将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
3.析构函数与虚析构函数
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
@ 派生类的构造函数应在其初始化表里调用基类的构造函数
@ 基类与派生类的析构函数应该为虚(即加virtual关键字)
#include
class Base
{
public:
virtual ~Base() { cout<< "~Base" << endl ; }
};
class Derived : public Base
{
public:
virtual ~Derived() { cout<< "~Derived" << endl ; }
};
void main(void)
{
Base * pB = new Derived; // upcast
delete pB;
}
输出结果为:
~Derived
~Base
如果析构函数不为虚,那么输出结果为
~Base
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
有几个需要注意的内容:
@ 构造函数与析构函数的另一个特别之处是没有返回值类型
@ 构造从类层次的最顶层的基类开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,在析构的时候,最低层的派生类的析构函数最开始被调用,然后调用每个基类的析构函数。
@ “缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错
下面通过例子进一步说明,
1.构造函数的初始化表
设存在两个类:
class A
{
…
A(void); // 无参数构造函数
A(const A &other); // 拷贝构造函数
A & operate =( const A &other); // 赋值函数
virtual ~A(void); //析构函数
};
class B
{
public:
B(const A &a); // B的构造函数
private:
A m_a; // 成员对象
};
下面是B的构造函数的2个实现,其中第一个的类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化;而第二个B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。
B::B(const A &a)
: m_a(a)
{
…
}
B::B(const A &a)
{
m_a = a;
…
}
2.拷贝函数和构造函数的区别
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,可以将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
3.析构函数与虚析构函数
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
@ 派生类的构造函数应在其初始化表里调用基类的构造函数
@ 基类与派生类的析构函数应该为虚(即加virtual关键字)
#include
class Base
{
public:
virtual ~Base() { cout<< "~Base" << endl ; }
};
class Derived : public Base
{
public:
virtual ~Derived() { cout<< "~Derived" << endl ; }
};
void main(void)
{
Base * pB = new Derived; // upcast
delete pB;
}
输出结果为:
~Derived
~Base
如果析构函数不为虚,那么输出结果为
~Base
简介vc中的release和debug版本的区别
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
Debug 和 Release 的真正秘密,在于一组编译选项。下面列出了分别针对二者的选项(当然除此之外还有其他一些,如/Fd /Fo,但区别并不重要,通常他们也不会引起 Release 版错误,在此不讨论)
Debug 版本
参数 含义
/MDd /MLd 或 /MTd 使用 Debug runtime library (调试版本的运行时刻函数库)
/Od 关闭优化开关
/D "_DEBUG" 相当于 #define _DEBUG,打开编译调试代码开关 (主要针对assert函数)
/ZI 创建 Edit and continue(编辑继续)数据库,这样在调试过程中如果修改了源代码不需重新编译
/GZ 可以帮助捕获内存错误
/Gm 打开最小化重链接开关, 减少链接时间
Release 版本
参数 含义
/MD /ML 或 /MT 使用发布版本的运行时刻函数库
/O1 或 /O2 优化开关,使程序最小或最快
/D "NDEBUG" 关闭条件编译调试代码开关 (即不编译assert函数)
/GF 合并重复的字符串, 并将字符串常量放到只读内存, 防止被修改
实际上,Debug 和 Release 并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。
哪些情况下 Release 版会出错
有了上面的介绍,我们再来逐个对照这些选项看看 Release 版错误是怎样产生的
1、Runtime Library:链接哪种运行时刻函数库通常只对程序的性能产生影响。调试版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,因此性能不如发布版本。编译器提供的 Runtime Library 通常很稳定,不会造成 Release 版错误;倒是由于 Debug 的 Runtime Library 加强了对错误的检测,如堆内存分配,有时会出现 Debug 有错但 Release 正常的现象。应当指出的是,如果 Debug 有错,即使 Release 正常,程序肯定是有 Bug 的,只不过可能是 Release 版的某次运行没有表现出来而已。
2、优化:这是造成错误的主要原因,因为关闭优化时源程序基本上是直接翻译的,而打开优化后编译器会作出一系列假设。这类错误主要有以下几种:
1. 帧指针(Frame Pointer)省略(简称FPO):在函数调用过程中,所有调用信息(返回地址、参数)以及自动变量都是放在栈中的。若函数的声明与实现不同(参数、返回值、调用方式),就会产生错误,但 Debug 方式下,栈的访问通过 EBP 寄存器保存的地址实现,如果没有发生数组越界之类的错误(或是越界“不多”),函数通常能正常执行;Release 方式下,优化会省略 EBP 栈基址指针,这样通过一个全局指针访问栈就会造成返回地址错误是程序崩溃。
C++ 的强类型特性能检查出大多数这样的错误,但如果用了强制类型转换,就不行了。你可以在 Release 版本中强制加入/Oy-编译选项来关掉帧指针省略,以确定是否此类错误。此类错误通常有:MFC 消息响应函数书写错误。正确的应为:
afx_msg LRESULT OnMessageOwn
(WPARAM wparam, LPARAM lparam);
ON_MESSAGE 宏包含强制类型转换。防止这种错误的方法之一是重定义 ON_MESSAGE 宏,把下列代码加到 stdafx.h 中(在#include "afxwin.h"之后),函数原形错误时编译会报错。
#undef ON_MESSAGE
#define ON_MESSAGE(message, memberFxn) \
{
message, 0, 0, 0, AfxSig_lwl, \
(AFX_PMSG)(AFX_PMSGW)
(static_cast< LRESULT (AFX_MSG_CALL \
CWnd::*)(WPARAM, LPARAM) > (&memberFxn)
},
2. volatile 型变量:volatile 告诉编译器该变量可能被程序之外的未知方式修改(如系统、其他进程和线程)。优化程序为了使程序性能提高,常把一些变量放在寄存器中(类似于 register 关键字),而其他进程只能对该变量所在的内存进行修改,而寄存器中的值没变。
如果你的程序是多线程的,或者你发现某个变量的值与预期的不符而你确信已正确的设置了,则很可能遇到这样的问题。这种错误有时会表现为程序在最快优化出错而最小优化正常。把你认为可疑的变量加上 volatile 试试。
3. 变量优化:优化程序会根据变量的使用情况优化变量。例如,函数中有一个未被使用的变量,在 Debug 版中它有可能掩盖一个数组越界,而在 Release 版中,这个变量很可能被优化调,此时数组越界会破坏栈中有用的数据。当然,实际的情况会比这复杂得多。与此有关的错误有非法访问,包括数组越界、指针错误等。例如:
void fn(void)
{
int i;
i = 1;
int a[4];
{
int j;
j = 1;
}
a[-1] = 1;
//当然错误不会这么明显,例如下标是变量
a[4] = 1;
}
j 虽然在数组越界时已出了作用域,但其空间并未收回,因而 i 和 j 就会掩盖越界。而 Release 版由于 i、j 并未其很大作用可能会被优化掉,从而使栈被破坏。
3. DEBUG 与 NDEBUG :当定义了 _DEBUG 时,assert() 函数会被编译,而 NDEBUG 时不被编译。此外,TRACE() 宏的编译也受 _DEBUG 控制。
所有这些断言都只在 Debug版中才被编译,而在 Release 版中被忽略。唯一的例外是 VERIFY()。事实上,这些宏都是调用了assert()函数,只不过附加了一些与库有关的调试代码。如果你在这些宏中加入了任何程序代码,而不只是布尔表达式(例如赋值、能改变变量值的函数调用等),那么Release版都不会执行这些操作,从而造成错误。初学者很容易犯这类错误,查找的方法也很简单,因为这些宏都已在上面列出,只要利用 VC++ 的 Find in Files 功能在工程所有文件中找到用这些宏的地方再一一检查即可。另外,有些高手可能还会加入 #ifdef _DEBUG 之类的条件编译,也要注意一下。
顺便值得一提的是VERIFY()宏,这个宏允许你将程序代码放在布尔表达式里。这个宏通常用来检查 Windows API的返回值。有些人可能为这个原因而滥用VERIFY(),事实上这是危险的,因为VERIFY()违反了断言的思想,不能使程序代码和调试代码完全分离,最终可能会带来很多麻烦。因此,专家们建议尽量少用这个宏。
4. /GZ 选项:这个选项会做以下这些事:
1. 初始化内存和变量。包括用 0xCC 初始化所有自动变量,0xCD ( Cleared Data ) 初始化堆中分配的内存(即动态分配的内存,例如 new ),0xDD ( Dead Data ) 填充已被释放的堆内存(例如 delete ),0xFD( deFencde Data ) 初始化受保护的内存(debug 版在动态分配内存的前后加入保护内存以防止越界访问),其中括号中的词是微软建议的助记词。这样做的好处是这些值都很大,作为指针是不可能的(而且 32 位系统中指针很少是奇数值,在有些系统中奇数的指针会产生运行时错误),作为数值也很少遇到,而且这些值也很容易辨认,因此这很有利于在 Debug 版中发现 Release 版才会遇到的错误。要特别注意的是,很多人认为编译器会用0来初始化变量,这是错误的(而且这样很不利于查找错误)。
2. 通过函数指针调用函数时,会通过检查栈指针验证函数调用的匹配性。(防止原形不匹配)
3. 函数返回前检查栈指针,确认未被修改。(防止越界访问和原形不匹配,与第二项合在一起可大致模拟帧指针省略 FPO )通常 /GZ 选项会造成 Debug 版出错而 Release 版正常的现象,因为 Release 版中未初始化的变量是随机的,这有可能使指针指向一个有效地址而掩盖了非法访问。除此之外,/Gm/GF等选项造成错误的情况比较少,而且他们的效果显而易见,比较容易发现。
怎样“调试” Release 版的程序
遇到Debug成功但Release失败,显然是一件很沮丧的事,而且往往无从下手。如果你看了以上的分析,结合错误的具体表现,很快找出了错误,固然很好。但如果一时找不出,以下给出了一些在这种情况下的策略。
1. 前面已经提过,Debug和Release只是一组编译选项的差别,实际上并没有什么定义能区分二者。我们可以修改Release版的编译选项来缩小错误范围。如上所述,可以把Release 的选项逐个改为与之相对的Debug选项,如/MD改为/MDd、/O1改为/Od,或运行时间优化改为程序大小优化。注意,一次只改一个选项,看改哪个选项时错误消失,再对应该选项相关的错误,针对性地查找。这些选项在Project\Settings...中都可以直接通过列表选取,通常不要手动修改。由于以上的分析已相当全面,这个方法是最有效的。
2. 在编程过程中就要时常注意测试 Release 版本,以免最后代码太多,时间又很紧。
3. 在 Debug 版中使用 /W4 警告级别,这样可以从编译器获得最大限度的错误信息,比如 if( i =0 )就会引起 /W4 警告。不要忽略这些警告,通常这是你程序中的 Bug 引起的。但有时 /W4 会带来很多冗余信息,如 未使用的函数参数 警告,而很多消息处理函数都会忽略某些参数。我们可以用:
#progma warning(disable: 4702)
//禁止
//...
#progma warning(default: 4702)
//重新允许来暂时禁止某个警告,或使用
#progma warning(push, 3)
//设置警告级别为 /W3
//...
#progma warning(pop)
//重设为 /W4
来暂时改变警告级别,有时你可以只在认为可疑的那一部分代码使用 /W4。
4. 你也可以像Debug一样调试你的Release版,只要加入调试符号。在Project/Settings... 中,选中 Settings for "Win32 Release",选中 C/C++ 标签,Category 选 General,Debug Info 选 Program Database。再在 Link 标签 Project options 最后加上 "/OPT:REF" (引号不要输)。这样调试器就能使用 pdb 文件中的调试符号。
但调试时你会发现断点很难设置,变量也很难找到??这些都被优化过了。不过令人庆幸的是,Call Stack窗口仍然工作正常,即使帧指针被优化,栈信息(特别是返回地址)仍然能找到。这对定位错误很有帮助。
Debug 和 Release 的真正秘密,在于一组编译选项。下面列出了分别针对二者的选项(当然除此之外还有其他一些,如/Fd /Fo,但区别并不重要,通常他们也不会引起 Release 版错误,在此不讨论)
Debug 版本
参数 含义
/MDd /MLd 或 /MTd 使用 Debug runtime library (调试版本的运行时刻函数库)
/Od 关闭优化开关
/D "_DEBUG" 相当于 #define _DEBUG,打开编译调试代码开关 (主要针对assert函数)
/ZI 创建 Edit and continue(编辑继续)数据库,这样在调试过程中如果修改了源代码不需重新编译
/GZ 可以帮助捕获内存错误
/Gm 打开最小化重链接开关, 减少链接时间
Release 版本
参数 含义
/MD /ML 或 /MT 使用发布版本的运行时刻函数库
/O1 或 /O2 优化开关,使程序最小或最快
/D "NDEBUG" 关闭条件编译调试代码开关 (即不编译assert函数)
/GF 合并重复的字符串, 并将字符串常量放到只读内存, 防止被修改
实际上,Debug 和 Release 并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。
哪些情况下 Release 版会出错
有了上面的介绍,我们再来逐个对照这些选项看看 Release 版错误是怎样产生的
1、Runtime Library:链接哪种运行时刻函数库通常只对程序的性能产生影响。调试版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,因此性能不如发布版本。编译器提供的 Runtime Library 通常很稳定,不会造成 Release 版错误;倒是由于 Debug 的 Runtime Library 加强了对错误的检测,如堆内存分配,有时会出现 Debug 有错但 Release 正常的现象。应当指出的是,如果 Debug 有错,即使 Release 正常,程序肯定是有 Bug 的,只不过可能是 Release 版的某次运行没有表现出来而已。
2、优化:这是造成错误的主要原因,因为关闭优化时源程序基本上是直接翻译的,而打开优化后编译器会作出一系列假设。这类错误主要有以下几种:
1. 帧指针(Frame Pointer)省略(简称FPO):在函数调用过程中,所有调用信息(返回地址、参数)以及自动变量都是放在栈中的。若函数的声明与实现不同(参数、返回值、调用方式),就会产生错误,但 Debug 方式下,栈的访问通过 EBP 寄存器保存的地址实现,如果没有发生数组越界之类的错误(或是越界“不多”),函数通常能正常执行;Release 方式下,优化会省略 EBP 栈基址指针,这样通过一个全局指针访问栈就会造成返回地址错误是程序崩溃。
C++ 的强类型特性能检查出大多数这样的错误,但如果用了强制类型转换,就不行了。你可以在 Release 版本中强制加入/Oy-编译选项来关掉帧指针省略,以确定是否此类错误。此类错误通常有:MFC 消息响应函数书写错误。正确的应为:
afx_msg LRESULT OnMessageOwn
(WPARAM wparam, LPARAM lparam);
ON_MESSAGE 宏包含强制类型转换。防止这种错误的方法之一是重定义 ON_MESSAGE 宏,把下列代码加到 stdafx.h 中(在#include "afxwin.h"之后),函数原形错误时编译会报错。
#undef ON_MESSAGE
#define ON_MESSAGE(message, memberFxn) \
{
message, 0, 0, 0, AfxSig_lwl, \
(AFX_PMSG)(AFX_PMSGW)
(static_cast< LRESULT (AFX_MSG_CALL \
CWnd::*)(WPARAM, LPARAM) > (&memberFxn)
},
2. volatile 型变量:volatile 告诉编译器该变量可能被程序之外的未知方式修改(如系统、其他进程和线程)。优化程序为了使程序性能提高,常把一些变量放在寄存器中(类似于 register 关键字),而其他进程只能对该变量所在的内存进行修改,而寄存器中的值没变。
如果你的程序是多线程的,或者你发现某个变量的值与预期的不符而你确信已正确的设置了,则很可能遇到这样的问题。这种错误有时会表现为程序在最快优化出错而最小优化正常。把你认为可疑的变量加上 volatile 试试。
3. 变量优化:优化程序会根据变量的使用情况优化变量。例如,函数中有一个未被使用的变量,在 Debug 版中它有可能掩盖一个数组越界,而在 Release 版中,这个变量很可能被优化调,此时数组越界会破坏栈中有用的数据。当然,实际的情况会比这复杂得多。与此有关的错误有非法访问,包括数组越界、指针错误等。例如:
void fn(void)
{
int i;
i = 1;
int a[4];
{
int j;
j = 1;
}
a[-1] = 1;
//当然错误不会这么明显,例如下标是变量
a[4] = 1;
}
j 虽然在数组越界时已出了作用域,但其空间并未收回,因而 i 和 j 就会掩盖越界。而 Release 版由于 i、j 并未其很大作用可能会被优化掉,从而使栈被破坏。
3. DEBUG 与 NDEBUG :当定义了 _DEBUG 时,assert() 函数会被编译,而 NDEBUG 时不被编译。此外,TRACE() 宏的编译也受 _DEBUG 控制。
所有这些断言都只在 Debug版中才被编译,而在 Release 版中被忽略。唯一的例外是 VERIFY()。事实上,这些宏都是调用了assert()函数,只不过附加了一些与库有关的调试代码。如果你在这些宏中加入了任何程序代码,而不只是布尔表达式(例如赋值、能改变变量值的函数调用等),那么Release版都不会执行这些操作,从而造成错误。初学者很容易犯这类错误,查找的方法也很简单,因为这些宏都已在上面列出,只要利用 VC++ 的 Find in Files 功能在工程所有文件中找到用这些宏的地方再一一检查即可。另外,有些高手可能还会加入 #ifdef _DEBUG 之类的条件编译,也要注意一下。
顺便值得一提的是VERIFY()宏,这个宏允许你将程序代码放在布尔表达式里。这个宏通常用来检查 Windows API的返回值。有些人可能为这个原因而滥用VERIFY(),事实上这是危险的,因为VERIFY()违反了断言的思想,不能使程序代码和调试代码完全分离,最终可能会带来很多麻烦。因此,专家们建议尽量少用这个宏。
4. /GZ 选项:这个选项会做以下这些事:
1. 初始化内存和变量。包括用 0xCC 初始化所有自动变量,0xCD ( Cleared Data ) 初始化堆中分配的内存(即动态分配的内存,例如 new ),0xDD ( Dead Data ) 填充已被释放的堆内存(例如 delete ),0xFD( deFencde Data ) 初始化受保护的内存(debug 版在动态分配内存的前后加入保护内存以防止越界访问),其中括号中的词是微软建议的助记词。这样做的好处是这些值都很大,作为指针是不可能的(而且 32 位系统中指针很少是奇数值,在有些系统中奇数的指针会产生运行时错误),作为数值也很少遇到,而且这些值也很容易辨认,因此这很有利于在 Debug 版中发现 Release 版才会遇到的错误。要特别注意的是,很多人认为编译器会用0来初始化变量,这是错误的(而且这样很不利于查找错误)。
2. 通过函数指针调用函数时,会通过检查栈指针验证函数调用的匹配性。(防止原形不匹配)
3. 函数返回前检查栈指针,确认未被修改。(防止越界访问和原形不匹配,与第二项合在一起可大致模拟帧指针省略 FPO )通常 /GZ 选项会造成 Debug 版出错而 Release 版正常的现象,因为 Release 版中未初始化的变量是随机的,这有可能使指针指向一个有效地址而掩盖了非法访问。除此之外,/Gm/GF等选项造成错误的情况比较少,而且他们的效果显而易见,比较容易发现。
怎样“调试” Release 版的程序
遇到Debug成功但Release失败,显然是一件很沮丧的事,而且往往无从下手。如果你看了以上的分析,结合错误的具体表现,很快找出了错误,固然很好。但如果一时找不出,以下给出了一些在这种情况下的策略。
1. 前面已经提过,Debug和Release只是一组编译选项的差别,实际上并没有什么定义能区分二者。我们可以修改Release版的编译选项来缩小错误范围。如上所述,可以把Release 的选项逐个改为与之相对的Debug选项,如/MD改为/MDd、/O1改为/Od,或运行时间优化改为程序大小优化。注意,一次只改一个选项,看改哪个选项时错误消失,再对应该选项相关的错误,针对性地查找。这些选项在Project\Settings...中都可以直接通过列表选取,通常不要手动修改。由于以上的分析已相当全面,这个方法是最有效的。
2. 在编程过程中就要时常注意测试 Release 版本,以免最后代码太多,时间又很紧。
3. 在 Debug 版中使用 /W4 警告级别,这样可以从编译器获得最大限度的错误信息,比如 if( i =0 )就会引起 /W4 警告。不要忽略这些警告,通常这是你程序中的 Bug 引起的。但有时 /W4 会带来很多冗余信息,如 未使用的函数参数 警告,而很多消息处理函数都会忽略某些参数。我们可以用:
#progma warning(disable: 4702)
//禁止
//...
#progma warning(default: 4702)
//重新允许来暂时禁止某个警告,或使用
#progma warning(push, 3)
//设置警告级别为 /W3
//...
#progma warning(pop)
//重设为 /W4
来暂时改变警告级别,有时你可以只在认为可疑的那一部分代码使用 /W4。
4. 你也可以像Debug一样调试你的Release版,只要加入调试符号。在Project/Settings... 中,选中 Settings for "Win32 Release",选中 C/C++ 标签,Category 选 General,Debug Info 选 Program Database。再在 Link 标签 Project options 最后加上 "/OPT:REF" (引号不要输)。这样调试器就能使用 pdb 文件中的调试符号。
但调试时你会发现断点很难设置,变量也很难找到??这些都被优化过了。不过令人庆幸的是,Call Stack窗口仍然工作正常,即使帧指针被优化,栈信息(特别是返回地址)仍然能找到。这对定位错误很有帮助。
2009年5月4日星期一
Configure Codeblocks For Qt on Windows
将安装目录下\MinGW\bin内的mingw32-make文件复制并改名为make。
1.在Code::Blocks里选择Tools —> Configure tools... 添加两个新的工具
A.第一个工具,在弹出的工具编辑框中依次填入
Name: create Qt.pro file
Executable: c:\Qt\4.4.3\bin\qmake.exe (即本机上qmake的绝对路径)
Parameters: -project
Working directory: ${PROJECT_DIR}
下面的Launching options选择 Launch tool hidden with standard output redirected (这样就不会跳出命令行窗口)
B.第二个工具,在弹出的工具编辑框中依次填入
Name: create Qt makefile
Executable: c:\Qt\4.4.3\bin\qmake.exe (即本机上qmake的绝对路径)
Parameters: -makefile
Working directory: ${PROJECT_DIR}
下面的Launching options选择 Launch tool hidden with standard output redirected (这样就不会跳出命令行窗口)
4.选择Project —> Properties... —> Project settings,在"This is a custom Makefile"前打勾,这样就会启用Makefile来编译
5.转到Build targets,将output filename由bin\Debug\xxx.exe改成Debug\xxx.exe,即去掉前面的bin\
6.选择Project —> Build options... —> "Make" commands,修改两个地方
Clean project/target: $make -f $makefile $target-clean
Dist-clean project/target: $make -f $makefile $target-distclean
所有地方都设置好了,接下来只要在Build前,先在Tools下依次运行create Qt.pro file和create Qt makefile (只需在项目有文件添加和删除的情况下需要运行一遍)
1.在Code::Blocks里选择Tools —> Configure tools... 添加两个新的工具
A.第一个工具,在弹出的工具编辑框中依次填入
Name: create Qt.pro file
Executable: c:\Qt\4.4.3\bin\qmake.exe (即本机上qmake的绝对路径)
Parameters: -project
Working directory: ${PROJECT_DIR}
下面的Launching options选择 Launch tool hidden with standard output redirected (这样就不会跳出命令行窗口)
B.第二个工具,在弹出的工具编辑框中依次填入
Name: create Qt makefile
Executable: c:\Qt\4.4.3\bin\qmake.exe (即本机上qmake的绝对路径)
Parameters: -makefile
Working directory: ${PROJECT_DIR}
下面的Launching options选择 Launch tool hidden with standard output redirected (这样就不会跳出命令行窗口)
4.选择Project —> Properties... —> Project settings,在"This is a custom Makefile"前打勾,这样就会启用Makefile来编译
5.转到Build targets,将output filename由bin\Debug\xxx.exe改成Debug\xxx.exe,即去掉前面的bin\
6.选择Project —> Build options... —> "Make" commands,修改两个地方
Clean project/target: $make -f $makefile $target-clean
Dist-clean project/target: $make -f $makefile $target-distclean
所有地方都设置好了,接下来只要在Build前,先在Tools下依次运行create Qt.pro file和create Qt makefile (只需在项目有文件添加和删除的情况下需要运行一遍)
订阅:
评论 (Atom)