茴字的N种写法
在Visual C++世界里,对于每一种需求,几乎都存在着多种技术实现方式,这些细枝末节层层交错,从而形成了壮观的Visual C++技术脉络图。本节将实现一个目标,即将孔乙己的“茴”字输出至某个文件中,并再次读取出来。其中主要的技术点就是针对文件做一些操作,而Visual C++关于文件的操作方式就非常之多,有Windows API方式、C++标准库方式等,这些方式如图2-48所示。
以下详细介绍使用在Windows系统中以各种方式(优雅的或者鄙俗的)写出“茴”字。
图2-48 茴字的N种写法
1、 使用Windows API
API的英文全称为Application Programming Interface,Win32 API也就是Microsoft Windows 32位平台的应用程序编程接口。想一想,如果没有Win32 API,我们该如何操作文件?自己去驱动磁盘驱动器?自己去编写硬件驱动程序?不可能。Windows API介于Windows操作系统与应用程序之间,因此它是离Windows操作系统最近的函数接口,它们之间的关系如图2-49所示。
图2-49 Windows API的位置
针对文件读写这一最基础的操作,很显然,Windows会责无旁贷地提供操作函数:
HANDLE CreateFile( //创建、打开一个文件
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
BOOL WriteFile( //写文件
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
BOOL ReadFile( //读文件
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
读者根据如上函数原型,不难理解如何使用Win32 API。在Visual C++中调用Windows API,大部分情况下需要包含一个windows.h。
现在动手
接下来我们体验一下如何调用文件操作API,来完成文件的读写。
【程序 2‑11】使用Windows API输出茴字
01 #include "stdafx.h"
02 #include <windows.h>
03 #include <cstdio>
04
05 int main()
06 {
07 HANDLE hFile;
08 DWORD nBytes;
09
10 //写入文件
11 hFile = CreateFile( _T("test.out"),
12 GENERIC_WRITE,
13 FILE_SHARE_WRITE,
14 NULL,
15 CREATE_ALWAYS,
16 0,
17 NULL );
18 char msg[] = "茴香豆的茴";
19 if( hFile != INVALID_HANDLE_VALUE )
20 {
21 WriteFile( hFile, msg, sizeof(msg) - 1, &nBytes, NULL );
22 CloseHandle(hFile);
23 }
24
25 //读取文件
26 hFile = CreateFile( _T("test.out"),
27 GENERIC_READ,
28 FILE_SHARE_READ,
29 NULL,
30 OPEN_ALWAYS,
31 0,
32 NULL );
33 if( hFile != INVALID_HANDLE_VALUE )
34 {
35 char line[256] = {0};
36 BOOL bResult;
37 bResult = ReadFile(hFile,
38 line,
39 sizeof(line),
40 &nBytes,
41 NULL) ;
42
43 if (nBytes != 0 )
44 {
45 printf("%s\r\n", line);
46 }
47
48 CloseHandle(hFile);
49 }
50 }
运行该程序,读者将会发现项目目录下会生成一个test.out文件,同时控制台输出结果如图2-50所示。
图2-50 运行结果
可以看出,使用Windows API进行编程,代码的可读性并不是很高。因此一般情况下,我们推荐使用标准库或者MFC类来进行操作。
光盘导读
该项目对应于光盘中的目录“\ch02\ WinApiWriter”。
2 、使用C++标准库(stdcpp)
标准C++提供了常见的操作类和操作函数,如:针对文件处理,标准C++在<fstream>中就提供了fstream类。
一般我们提及“C++标准库(C++ standard library)”,它实际上包含一堆头文件(.h)、实现文件(.cpp)及目标库文件(.lib)等,其中包含的内容如下所示。
l 函数:函数的定义,如rand()函数用以获取随机数。
l 常量:一些常量的定义。
l 宏:一些宏的定义,如RAND_MAX。
l 类:公用类的定义,如string。
l 对象:公用对象的定义,如用以控制台输出的cout。
l 模板:C++标准库中最多的就是类模板和函数模板的定义。
不同的C++库完成对不同操作的封装,为C++程序员提供基本的操作能力。一般认为C++标准库可进行如下分类,如图2-51所示。
图2-51 C++标准库的组成
l 字符串:用以完成字符串的封装和操作。
l 输入/输出流:用以操作输入、输出流。
l 复数:用来进行复数类型的运算。
l 异常诊断:用来定义异常类和提供诊断的方法。
l C语言库:旧版的C标准库。
l 标准模板库:STL容器、泛型算法库。
l 其他工具库:包括函数对象类、内存操作类等。
C++标准库中定义的成员都包括在std(标准standard的缩写)名字空间里。所以调用库函数时别忘了对std名字空间的使用声明:
using namespace std;
现在动手
标准C++提倡使用流(stream)来操作文件,接下来我们体验如何使用文件流fstream来操作文件输入/输出。
【程序 2‑12】使用fstream输出茴字
01 #include "stdafx.h"
02 #include <iostream>
03 #include <fstream>
04
05 using namespace std;
06
07 int main()
08 {
09 //写入文件
10 ofstream out("test.out");
11 out << "茴香豆的茴";
12 out.close();
13
14 //读取文件
15 ifstream in("test.out");
16 char line[256];
17 in.getline(line, 256);
18 cout << line << endl;
19
20 return 0;
21 }
光盘导读
该项目对应于光盘中的目录“\ch02\ FstreamWriter”。
3、 使用CRT(C运行时期库)
在遥远的C语言时代,C语言就提供了丰富的函数库,如<stdio.h>,它即对应于标准输入/输出的功能库。
在C++中,这些C标准库得以保留,但C++不再赞成如下方式使用传统的C头文件:
#include <stdlib.h>
int i = rand();
C++鼓励使用<cstdlib>这样的形式替换“.h”的写法:
#include <cstdlib>
前缀c的含义在于这是一个C标准库。C标准库的内容会被C++同时置于全局名字空间和std名字空间,所以对随机函数rand()的调用也可以写成:
std::rand()
C标准库主要包括在表2-5所示的头文件中。
头文件
|
含义
|
<cassert>
|
诊断库
|
<cctype>
|
字符处理函数库
|
<cerrno>
|
错误定义
|
<cfloat>
|
浮点类型
|
<climits>
|
整型数值的尺寸定义
|
<clocale>
|
国际化库
|
<cmath>
|
数学库
|
<csetjmp>
|
跳转函数库
|
<csignal>
|
信号处理库
|
<cstdarg>
|
可变参数处理
|
<cstddef>
|
标准定义库
|
<cstdio>
|
标准输入、输出库
|
<cstdlib>
|
标准工具库
|
<cstring>
|
字符串函数库
|
<ctime>
|
时间库
|
在<cstdlib>中其实就有操作文件的函数:
FILE *fopen( //创建、打开一个文件
const char* filename,
const char* mode
);
int fclose( //关闭文件
FILE* stream
);
int fprintf( //向文件输出指定格式的文本
FILE* stream,
const char* format [, argument ]...
);
int fscanf( //从文件读取指定格式的文本
FILE* stream,
const char* format [, argument ]...
);
现在动手
接下来,我们体验如何采用CRT库函数来操作文件。
【程序 2-13】使用CRT输出茴字
01 #include "stdafx.h"
02 #include <cstdio>
03
04 int main()
05 {
06 //写入文件
07 FILE * fp = fopen("test.out", "w");
08 fprintf(fp, "茴香豆的茴");
09 fclose(fp);
10
11 //读取文件
12 fp = fopen("test.out", "r");
13 char line[256];
14 fscanf(fp, "%s", line);
15 printf("%s\r\n", line);
16 fclose(fp);
17
18 return 0;
19 }
输出结果如图2-50所示。
好奇的读者可以在项目的当前目录找到输出文件test.out,它的内容如图2-52所示。
图2-52 使用CRT库函数生成的文件
光盘导读
该项目对应于光盘中的目录“\ch02\ CrtWriter”。
4、 使用CRT库的宽字符版本
标准C++引入了宽字符wchar_t,用来表达像中文这样的宽文本,对应地,与字符(char)相关的CRT库函数基本上都有其宽字符(wchar_t)版本。
FILE *_wfopen(
const wchar_t* filename,
const wchar_t* mode
);
int fwprintf(
FILE* stream,
const wchar_t* format [, argument ]...
);
int fwscanf(
FILE* stream,
const wchar_t* format [, argument ]...
);
现在动手
接下来,我们体验如何采用CRT库函数的宽字符版本来操作文件,并输出茴字。
【程序 2-14】使用CRT的宽字符版本输出茴字
01 #include "stdafx.h"
02 #include <cstdio>
03 #include <clocale>
04
05 int main()
06 {
07 setlocale(LC_ALL, "chs");
08
09 //写入文件
10 FILE * fp = _wfopen(L"test.out", L"w,ccs=UNICODE");
11 fwprintf(fp, L"%s", L"茴香豆的茴");
12 fclose(fp);
13
14 //读取文件
15 fp = _wfopen(L"test.out", L"r,ccs=UNICODE");
16 wchar_t line[256];
17 fwscanf(fp, L"%s", line);
18 wprintf(L"%s\r\n", line);
19 fclose(fp);
20 return 0;
21 }
光盘导读
该项目对应于光盘中的目录“\ch02\ CrtWcharWriter”。
提示
注意以上代码中setlocale()的用法,它将当前的时区设定为“chs”,即采用简体中文编码方式。如果读者忘记了该行,那么像wprintf()这样的函数很有可能无法正确输出宽字符。
5 、 使用CRT库的安全版本
Visual C++为一些CRT函数提供了安全版本,即secure version,这些安全版本的函数增强了参数校验、缓冲区大小检测、格式化参数校验等功能。原先那些旧的函数在Visual C++中会被提示成“deprecated(不建议使用)”,如图2-53所示。
图2-53 不安全的CRT调用
负责任的程序员应该依照这些警告,改用安全的版本。比如,前面用到的函数都具有相应的的安全版本:
errno_t fopen_s(
FILE** pFile,
const char *filename,
const char *mode
);
int fprintf_s(
FILE *stream,
const char *format [,
argument ]...
);
int fwprintf_s(
FILE *stream,
const wchar_t *format [,
argument ]...
);
int fscanf_s(
FILE *stream,
const char *format [,
argument ]...
);
int fwscanf_s(
FILE *stream,
const wchar_t *format [,
argument ]...
);
后缀_s表明该函数是一个安全版本(secure version)。
现在动手
接下来,我们体验如何采用CRT库函数的secure版本来操作文件。
【程序 2‑15】使用CRT库的安全版本输出茴字
01 #include "stdafx.h"
02 #include <cstdio>
03
04 int main()
05 {
06 //写入文件
07 FILE * fp;
08 fopen_s(&fp, "test.out", "w");
09 fprintf_s(fp, "茴香豆的茴");
10 fclose(fp);
11
12 //读取文件
13 fopen_s(&fp, "test.out", "r");
14 char line[256];
15 fscanf_s(fp, "%s", line, 256);
16 printf_s("%s\r\n", line);
17 fclose(fp);
18
19 return 0;
20 }
光盘导读
该项目对应于光盘中的目录“\ch02\ CrtSafeWriter”。
6、 使用MFC/ATL
MFC更多的工作在于:它们将Widnows API函数包装成对象类及其成员函数。MFC的这种中间位置与标准C++很类似,只不过它仅用于Windows操作系统,MFC的位置如图2-54所示。
比如,针对文件的操作,MFC就封装了CFile类,CFile的UML类图简略如图2-55所示。
图2-54 MFC/ATL的位置 图2-55 MFC封装的CFile类
如果我们再较真一点地话,就可以通过调试等手段进入到CFile::Remove()函数的定义,来观察CFile的庐山真面目:
void PASCAL CFile::Remove(LPCTSTR lpszFileName)
{
if (!::DeleteFile((LPTSTR)lpszFileName))
CFileException::ThrowOsError((LONG)::GetLastError(),
lpszFileName);
}
原来如此!MFC提供的CFile,其Remove()函数实际上就是简单的调用一下Windows API“DeleteFile()”而已!
现在动手
使用MFC进行Windows编程,不再是一种痛苦,如下即为使用CFile操作文件的例子,为了让我们的控制台程序支持MFC,请参考2.4.2小节“让控制台程序支持MFC/ATL”。
【程序 2-16】使用CFile输出茴字
01 #include "stdafx.h"
02
03 #include <afx.h>
04
05 int main()
06 {
07 //写入文件
08 CFile file;
09 if(file.Open(_T("test.out"), CFile::modeCreate | CFile::modeWrite))
10 {
11 char line[256] = "茴香豆的茴";
12 file.Write(line, sizeof(line));
13 file.Close();
14 }
15
16 //读取文件
17 if(file.Open(_T("test.out"), CFile::modeRead))
18 {
19 char line[256];
20 if(file.Read(line, 256) != 0)
21 {
22 printf("%s\r\n", line);
23 }
24
25 file.Close();
26 }
27
28 return 0;
29 }
使用MFC类,传统的面向函数的编程接口即转换成MFC类对象的接口,这样一来,代码的安全性和可读性得以大大提高。
光盘导读
该项目对应于光盘中的目录“\ch02\ MfcFileWriter”。
7 、使用C++/CLI
前面提及,C++/CLI的目标是把C++带到CLI平台上,使C++能够在CLI平台上发挥最大的能力。通过C++/CLI中的标准扩展,C++具有了原来没有的动态编程能力及一系列的first class(一等公民)的.NET特性。
读者有时会发现术语CLI和CLR可交换使用,实际上这两者之间的区别在于:CLI是一种标准规范,而CLR却是微软对CLI的实现。当我们使用C++/CLI时,就可以通过CLI接口与CLR通信,而CLR相当于建立在操作系统之上的一个虚拟层。它们之间的调用关系如图2-56所示。
现在动手
接下来,我们使用C++/CLI来操作文件并输出茴字。
【程序 2-17】使用C++/CLI输出茴字
01 #include "stdafx.h"
02
03 using namespace System;
04 using namespace System::IO;
05
06 int main(array<System::String ^> ^args)
07 {
08 String^ path = "test.out";
09
10 //写文件
11 StreamWriter^ sw = File::CreateText(path);
12 sw->WriteLine("茴香豆的茴");
13 sw->Close();
14
15 //读文件
16 StreamReader^ sr = File::OpenText(path);
17 String^ s = "";
18 if (s = sr->ReadLine())
19 {
20 Console::WriteLine(s);
21 }
22 }
简直太简洁了,不是吗?与本例子相关的.NET Framework 类包括如下。
Console:对应于控制台输出。
File:文件类,与CFile相似。
StreamWriter:流的写入器。
StreamReader:流的读取器。
光盘导读
该项目对应于光盘中的目录“\ch02\ ClrWriter”。
8 、 该采用哪一种写法
太多的选择比没有选择更让人痛苦。综上所述,C++/CLI的使用似乎更显得时髦一些,但是由于很多原因,国内传统Visual C++的用户还是很多,我们大部分的程序员还是在使用着纯的Visual C++。因此,本书中我们尽量只介绍本地C++的用法,在本地C++语言中,如果同时又存在着几种技术实现方法,那么我们给读者的建议是:优先采用MFC和Windows API,其次再使用标准C++的类和CRT函数库。
本书在谈到C++/CLI编程时总是尽量轻描淡写,因为C++/CLI毕竟不是C++的标准语法,而且它必须和.NET搭配使用。使用C++/CLI编程,代码总是显得极其简单,但是C++/CLI不是. NET环境下最简易的编程语言,如果读者愿意在CLR编程投入更多的精力,那么笔者建议你不妨去尝试使用其他语言,如C#。在Visual C++图书中推荐C#,这似乎有失厚道,但是程序员就应该这样,什么合适就用什么,语言是工具,我们不要让偏爱蜕变成不可变通的固执。
本文出自:《
把脉VC++》