第2章

学习使用Visual C++

本章主要介绍Visual C++ 2005的使用,其中的调试方法是特别需要熟练掌握的。

2.1 Visual C++安装和使用

Visual C++是Windows环境下最优秀的C++编程环境之一,它是微软公司开发的Visual Studio系列产品的一部分,具有集成开发环境(Integrated Development Environment,IDE),可以编辑C语言、C++以及C++/CLI等编程语言。VC++整合了方便的调试工具,特别是整合了微软窗口程序设计(Win32 API)、MFC、Microsoft.NET框架。目前最新的版本是Microsoft Visual C++ 2010。

2.1.1 Visual C++的版本信息

Visual C++ 1.0,集成了MFC 2.0,是Visual C++第一代版本,于1992年推出,可同时支持16位处理器与32位处理器,是Microsoft C/C++ 7.0的更新版本。

Visual C++ 1.5,集成了MFC 2.5,增加了目标文件链接嵌入(OLE)2.0和支持MFC的开放式数据库链接(ODBC)。这个版本只有16位的,也是第一个以CD-ROM为软件载体的版本。这个版本也没有所谓“标准版”。它是最后一个支持16位软件编程的软件,也是第一个支持基于x86机器的32位编程软件。

Visual C++ 2.0,集成了MFC 3.0,是第一个只发行32位的版本。

Visual C++ 4.0,集成了MFC 4.0(mfc40.dll 包含在Windows 95中),这个版本是专门为Windows 95以及Windows NT设计的。用户可以通过微软公司的订阅服务(Microsoft Subscription Service)升级至4.1和4.2版本。

Visual C++ 4.1,集成了MFC 4.1。

Visual C++ 4.2,集成了MFC 4.2,在Windows 98中自带了mfc42.dll。

Visual C++ 5.0,集成了MFC 4.21。

Visual C++ 6.0,集成了MFC 6.0(mfc42.dll),于1998发行。发行至今一直被广泛地用于大大小小的项目开发。

Visual C++ .NET 2002(也称Visual C++ 7.0),于2002年发行,集成了MFC 7.0(mfc70. dll),支持链接时代码生成和调试执行时检查。这个版本还集成了Managed Extension for C++,以及一个全新的用户界面(与Visual Basic和Visual C#共享)。这也是Visual C++ 6.0仍然被广泛使用的一个主要原因。

Visual C++ .NET 2003(也称Visual C++ 7.1),集成了MFC 7.1(mfc71.dll),于2003年发行,是对Visual C++ .NET 2002的一次重大升级。eMbedded Visual C++,用于Windows CE操作系统。Visual C++作为一个独立的开发环境被Microsoft Visual Studio 2005所替代。

Visual C++ 2005(也称Visual C++ 8.0),集成了MFC 8.0(mfc80.dll),于2005年11月发布。这个版本引进了对C++/CLI语言和OpenMP的支持。本教材采用这个版本作为编程环境。

Visual C++ 2008(也称Visual C++ 9.0),于2007年11月发布。这个版本支持.NET 3.5,集成了MFC9.0(mfc90.dll)。

2.1.2 创建Win32应用程序

Windows支持两种类型的应用程序:一种是基于图形用户界面(Graphic User Interface,GUI)的窗口应用程序;另一种是基于控制台用户界面(Console User Interface,CUI)的控制台应用程序。由于Windows的兼容性,部分DOS程序可以在Windows下运行,所以有些程序员习惯地把控制台应用程序称为DOS应用程序,从本质上讲这是错误的。CUI应用程序仍然是Windows应用程序,它一样可以使用所有的Win32 API,甚至可以创建窗口。

如何创建Windows应用程序呢?打开Visual Studio 2005,通过起始页选择创建项目,也可以通过菜单“文件”→“新建”→“项目”,或者直接使用快捷方式(按Ctrl+Shift+N键)创建项目,会弹出如图2-1所示的对话框。

图2-1 “新建项目”对话框

如图2-1所示,在“项目类型”中,选择Visual C++下的Win32项目,并且输入你想要的位置以及项目名称,然后单击“确定”按钮,会弹出“Win32应用程序向导”对话框(如图2-2所示)。选择创建解决方案的目录选项,会为解决方案中的每一个项目创建单独的目录。在本书中,基本上一个解决方案只包含一个项目。也可以根据个人的喜好选不选该选项。

图2-2 Win32应用程序向导的第一步

单击“下一步”按钮,会进入Win32应用程序向导的第二步,如图2-3所示。当然,如果全部使用默认设置,则直接单击“完成”按钮。单击“下一步”按钮,对下述选项可以进行设置。

图2-3 Win32应用程序向导第二步

1.控制台应用程序

计算机用户经常把控制台应用程序当做一种“遗物”,但是实际上很多系统管理员以及一些高手还是很喜欢它。最起码,对于那些使用Visual Studio学习C/C++语言的初学者来讲,控制台应用程序是必须经历的。在本书的学习过程中,对于STL的学习以及部分算法的设计、开发工具的使用,仍然可以使用简单、直观的控制台应用程序。可以在“新建项目”对话框中直接选择“Win32控制台应用程序”。

2.DLL与静态库

DLL是动态链接库(Dynamic Link Library)的缩写,它与静态库(Static Library)在学习过程中经常是一起出现的。有关这二者的编程,在后面会有专门的介绍,请参见第5章。

3.Windows应用程序

这种应用程序类型是本书的主角。如果不选择“空项目”,直接单击“完成”按钮,应用程序向导帮助我们完成了一个最基本、最全面的Windows应用程序,如图2-4所示。

图2-4 Visual Studio创建的标准的Windows应用程序

单击工具栏中的按钮,或者选择菜单“调试”→“启动调试”,或者直接按F5键就会编译、链接并且执行当前的应用程序,应该出现如图2-5所示的第一个Windows应用程序的运行效果。

图2-5 第一个Windows应用程序的运行效果

2.2 Win32控制台应用程序设计

在“新建项目”对话框中,首先选择“Win32控制台应用程序”,输入项目名称并选择好项目位置后,单击“确定”按钮。在弹出的应用程序向导中,可以看到如图2-6所示的选项。

图2-6 创建控制台应用程序

我们不做任何修改,单击“完成”按钮创建项目。Visual Studio会自动创建项目,并打开以项目名称命名的cpp文件,内容如下(其中,ch03con是项目名称)。

//ch03con.cpp:定义控制台应用程序的入口点
//
#include "stdafx.h"
int _tmain(int argc,_TCHAR* argv[])
{
    return 0;
}

2.2.1 预编译头文件

在学习C/C++编程语言时,我们已知道头文件的概念。在C语言家族程序中,头文件被大量使用。一般而言,每个C++/C程序通常由头文件(header files)和定义文件(definition files,也就是我们通常说的.c文件或者.cpp文件)组成。头文件作为一种包含功能函数、数据接口声明的载体文件,用于保存程序的声明(declaration),而定义文件用于保存程序的实现(implementation)。

Visual C++创建项目时,会自动创建预编译头文件stdafx.h。在编译其他文件之前,Visual C++会首先编译此文件。根据项目类型,该文件包含了项目所需的一些系统头文件,比如在创建控制台应用程序时会包含stdio.h,而在创建Win32项目时会包含windows.h。在自己的头文件中包括stdafx.h就相当于包含了那些系统头文件。

所谓头文件预编译,就是把一个工程(Project)中使用的一些标准头文件(如windows.h、afxwin.h)预先编译,以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度、节省时间。预编译的结果以工程名命名,后缀是“pch”(Pre-Compiled Header的缩写)。stdafx.h这个头文件名是可以在项目属性里指定的。

在默认情况下,Visual C++要求使用预编译头文件,因此,所有的CPP文件的第一条语句都必须是#include "stdafx.h"。很多C/C++程序员在学习编程语言时没有使用Visual C++,在新建文件时就没有包含stdafx.h,此时编译器会报“意外的文件结尾”这样类似的错误,所以需要特别小心。

2.2.2 Unicode编码

main函数是C/C++程序的主函数(或入口函数)。_tmain是Visual C++为了支持Unicode所使用的main的别名,换言之,_tmain()不过是Unicode版本的main()。要使用_tmain,则必须包含tchar.h。而在控制台应用程序的stdafx.h中,已经包含了tchar.h。什么是Unicode呢?

要了解Unicode,首先要从ASCII码讲起。ASCII码用8位表示一个字符,因此最多能够表示256个字符,包括大小写字母、数字以及少数特殊字符,如标点符号、货币符号等。对于大多数拉丁语言来说,这些字符已经够用。但是,许多亚洲和东方语言所用的字符远远不止256个字符。人们为了突破ASCII码字符数的限制,试图用一种简单的方法来针对超过256个字符的语言编写计算机程序。于是,Unicode应运而生。Unicode通过用双字节来表示一个字符,从而在更大范围内将数字代码映射到多种语言的字符集。

从Windows NT开始,Windows的所有版本都完全使用Unicode来构建。在调用Win32 API函数时,即使你传入一个ANSI字符串,函数首先也会把字符串转换为Unicode,再把结果传给操作系统。如果希望函数返回ANSI字符串,那么操作系统也会首先计算Unicode版本的结果再转换为ANSI字符串。尽管这些都由系统自动完成,但需要时间和内存上的消耗。

作为软件开发人员,如何熟练、有效地使用Unicode呢?如果你正在用Visual C++编写程序,Unicode兼容性意味着你的程序是否具有国际化特征,也就是说你的应用程序是针对本地市场还是国际市场。一旦你做出了决定,那么就得在代码中实现具体细节。好在Visual C++提供了很多内建功能来支持Unicode,在创建工程时就可以利用Visual C++提供的这些功能。在Visual Studio 2005中,项目属性的默认设置使用Unicode字符集。Win32 SDK包含一些数据类型遵循Unicode编码规则,MFC以宏的形式提供了将一般文本转换成Unicode数据类型的途径。开发人员只需要稍微改变一下编写代码的习惯便可以轻松编写支持Unicode的应用。

C程序员一般是用char关键字像下面这样来声明一个字符串数组以及字符串复制函数的原型:

char str[100];
void strcpy(char *out,char *in);

为了将上面的声明改成支持双字节的Unicode字符集,可以用下面的方法:

wchar_t str[100];
void wcscpy(wchar_t *out,wchar_t *in);

对于字符串常量,则使用L“Hello,World”的方式把一个ANSI字符串转换成Unicode字符串。

2.2.3 TCHAR

为了使代码同时兼容ASCII码和Unicode码,微软公司还提供了通用字符类型TCHAR。本书也将使用通用类型。通用字符类型的含义是,如果在项目属性中选择“Unicode字符集”,则TCHAR代表WCHAR,或者在项目属性中选择“多字节字符集”,则TCHAR代表char。

喜欢刨根问底的读者可以在一个使用了TCHAR的项目中,在TCHAR上单击鼠标右键,选择“转到定义”,然后仔细查看随后打开的winnt.h中的内容。这里需要C语言中条件预处理命令的知识。特别要注意的是,对于winnt.h这样由系统提供的头文件,我们可以打开它,但是千万不要修改它!

在使用通用类型的前提下,程序员只需要注意以下3点。

  • 凡是用关键字char的地方都用TCHAR取代;
  • 凡是用char *的地方都用LPTSTR取代;
  • 凡是定义在双引号中的字符串常量都用TEXT宏或者_T宏重写。

大多数人在学习C语言时,就习惯使用了C-Run Time库的许多字符串处理函数。由于微软公司建议使用通用字符类型,所以给出如表2-1所示的对照表,方便读者使用。

表2-1 常用字符串函数的TCHAR版本

建议初学者通过MSDN了解各种常用TCHAR版本的函数。例如,我们希望使用如下语句在控制台应用程序中输出“Hello,World!”:

printf("%s\n","Hello,World!");

通过MSDN查找printf的使用帮助,可以知道对应的TCHAR版本是_tprintf,则对应的代码应该是:

_tprintf(_T("%s"),_T("Hello,World! "));

特别要注意的是,由于C运行时库(C-Run Time,CRT)对的Unicode支持不好,使用_tprintf输出Unicode会有问题,需要在程序入口处设置如下本地属性:

#include <locale.h>
int _tmain(int argc,_TCHAR* argv[])
{
    //****** 设置本地属性*********
    setlocale(LC_ALL,"CHS");
    //……其他代码
}

2.2.4 Debug和Release

如图2-7所示,在Visual Studio的标准工具栏中,有一个解决方案配置下拉列表,默认的两种配置方案是Debug和Release。从字面上的意思来讲,它们分别代表调试版本和发行版本。程序员通常先在调试版本下编写代码并进行调试,最后才在发行版本中生成应用程序交付客户使用。一些初学者总觉得Debug版本没有意义。为什么不直接在Release版本下编写代码呢?这是因为在Debug版本中包含了大量的调试信息和保护机制,而有一些错误并非每一次运行都会导致程序崩溃,因此很有可能在Release版本下就无法发现这些bug。

图2-7 Debug和Release

我们来看下面这两行代码:

TCHAR text[10],*bugs=_T("The code has bug!");
_tcscpy(text,bugs);

错误很明显,也很典型,字符串bugs包含的字符超出了字符数组text的容量,此时把bugs复制到text中,会导致数组下标越界。在编译和链接阶段,这个程序不会报错。但是在Release版本下运行的时候,这段代码并不一定每次都报错。这是因为很有可能text数组后面的内存空间并未分配,所以即使非法使用也没有太大的关系。但毕竟这是一个隐患。然而在Debug版本下,由于采取了保护机制,程序的运行反而会报错。

从本质来讲,Debug和Release并没有区别,它们只是Visual C++预定义的两组编译选项的集合的名字。如果我们愿意,完全可以把Debug和Release的行为颠倒过来。当然我们还可以使用Visual C++提供的配置管理器定义自己的一组编译选项。不过在习惯上,我们更愿意使用Visual C++已经定义好的名称。

在默认的配置方案中,调试版本包含了大量的调试信息,所以要比发行版本大很多(甚至达到数兆字节)。两种版本通常使用不同的库,在使用MFC的情况下,调试版本使用MFC42D.dll,而发行版本使用MFC42.dll。调试版本允许对源代码进行调试,没有对速度进行优化,并采用了一些保护机制以帮助发现错误(例如MFC中提供了很多诊断用的宏),而发行版本正好相反。二者最本质的区别在于编译的时候使用了不同的选项,具体内容请参考MSDN。此外,Debug版本和Release版本在初始化变量时所做的操作是不同的,Debug版本将每个字节都赋成0xCC,而Release版本下如果不赋初值则会近似于随机数。

对于一个初级的程序员,特别要注意的是,Debug版本和Release版本必须单独配置!例如在配置引入库的时候,必须在Debug和Release两个版本下单独进行配置。

2.2.5 基本的调试方法

无论你学习的第一门编程语言是什么,在最初的编程阶段出错是难免的。一般会犯两种类型的错误:语法错误和逻辑错误。在学习初期,语法错误很多,并且由于代码量很小,不会有什么逻辑错误。语法错误比逻辑错误要简单得多,Visual C++集成的编译器会告诉你是否存在语法错误以及错误的位置和类型。随着编码越来越熟练、项目越做越大,语法错误越来越少,逻辑错误却越来越多。而逻辑错误却是很难跟踪和解决的。

应用程序的调试过程会相当复杂和困难。通常,一个调试程序应该具备至少4种功能:跟踪、断点、查看变量和更改数值。其中最基本和最重要的就是跟踪。跟踪功能使你能够在运行应用程序时,确认当前执行的代码的位置。在Visual C++中,有多种方式对程序的执行进行跟踪,如图2-8所示。

图2-8 基本的调试方法

按F5键表示启动调试,如果此时程序运行不会中断并且代码中没有设置断点,则程序会自动一直运行至结束。还可以使用F9键在光标所在行设置/取消断点。

进入调试状态后,Visual Studio的窗口布局发生变化,会出现自动窗口、局部变量窗口、监视窗口这三个窗口。这三个窗口的目的都是为了让程序员观察代码中变量的值,只是具体观察哪些变量在不同的窗口中有不同的选择。其中监视窗口可以由程序员自己添加所关心的变量。在具体的调试过程中,在跟踪程序运行的同时观察变量的值是调试的最基本的方法。

2.3 良好的编程习惯

2.3.1 使用正确的代码格式

(1)正确地使用缩进方式有助于程序员阅读和理解代码。选择你觉得缩进不好的代码,然后按快捷键Ctrl+K、Ctrl+F,编辑器会自动帮你调整缩进。

(2)适当地使用空白行可以让代码更加清晰。

(3)一定要写注释。

(4)千万不要删除一些暂时不用的代码,可以先把它们作为注释保留下来。按Ctrl+K、Ctrl+C键可以把当前行或者选中的内容自动转为注释,而按Ctrl+K、Ctrl+U键则可以取消注释。

2.3.2 采用匈牙利命名法

匈牙利命名法是一种编程时的命名规范。基本原则是:变量名=属性+类型+对象描述,其中每一对象的名称都要求有明确含义,可以取对象名字全称或名字的一部分。命名要基于容易记忆容易理解的原则。保证名字的连贯性是非常重要的。

举例来说,表单的名称为form,那么在匈牙利命名法中可以简写为frm,当表单变量名称为Switchboard时,变量全称应该为frmSwitchboard。这样,可以很容易从变量名看出Switchboard是一个表单。同样,如果此变量类型为标签,那么就应命名成lblSwitchboard。可以看出,匈牙利命名法非常便于记忆,而且使变量名非常清晰易懂,从而增强了代码的可读性,方便各程序员之间相互交流代码。据说这种命名法是一位叫Charles Simonyi的匈牙利程序员发明的,后来他在微软公司工作了几年,于是这种命名法就通过微软公司的各种产品和文档资料向世界传播开了。现在,大部分程序员不管自己使用什么软件进行开发,或多或少都使用了这种命名法。这种命名法的出发点是把变量名按属性+类型+对象描述的顺序组合起来,以使程序员做变量时对变量的类型和其他属性有直观的了解,表2-2给出匈牙利命名法变量命名规则。

表2-2 匈牙利命名法变量命名规则

不需要牢记这些命名规则,但是了解这些规则对于我们阅读代码用途很大。良好的编程习惯是成为程序员的第一步。使用规范的命名方式,并且使所有人都能看懂,是非常有必要的。例如,我们见到一个gbFlag变量,可很容易地理解,它是全局的布尔型的状态标志变量。

不要使用拼音来作为变量名,更要命的是使用拼音的缩写作为变量名。编程的时候打开一个电子词典软件,即使使用的单词不是很恰当也没有关系。注意单复数。如果由几个单词构成变量名,可以让每个单词的首字母大写,例如,allBullets——所有的子弹。

使用大家都看得懂、猜得到的单词缩写。例如bitmap可以缩写成bmp,source缩写成src,等等。

2.4 Win32 API中的常见数据类型

为了方便读者,在表2-3中罗列了本书中用到的Win32 API中的常见数据类型。希望读者能够尽量使用这些类型,例如UINT,而不是使用C语言中的unsigned int。

表2-3 Win32 API中的常见数据类型

这些类型定义是有迹可寻的,前面是“LP”的就是LongPiont长指针类型的,而“H”开头的大部分是Handle句柄类型的,“U”是Unsign无符号型。

2.5 习题

(1)ASCII编码和Unicode编码有什么区别?使用字符类型TCHAR有什么好处?

(2)VC创建项目时自动创建的预编译头文件stdafx.h有什么作用?

(3)回忆之前在学习C语言的字符串处理函数时所写的各种程序,把它们改写成TCHAR版本的。

(4)在第(3)题的项目中,自学使用Visual Studio的编辑菜单,并记忆其中常用的快捷方式。

(5)创建一个Win32控制台应用程序,随机产生10~100的加减法,由用户输入结果,并计算最终成绩。

(6)在完成上面的各种程序时,请务必有意地使用各种调试方法。