为了概览一下C++的IO库,先来张cplusplus.com网站的图片:
从这个图片我们基本上可以得到C++ IO库的所有基本信息:头文件、类、继承关系。为了更清楚的看出各个类的继承关系,我根据上面的图重新画了一个:
从上面两个图可以看出,C++的IO都是流式的,主要分为三大类:普通流、文件流、字符串(string)流。这三大类的流各有各的特点,但也有许多共性,我们先来看一些共性。
其实所谓的条件状态很容易理解,就是标识流此刻的状态。条件状态(均为ios_base::iostate类型,实质是一种枚举类型,定义在ios_base.h头文件中)共有四种:
状态 | 条件状态检测函数 | 状态说明 |
badbit | s.bad() | 用于指出被破坏的流,不可恢复 |
eofbit | s.eof() | 用于指出流已经到达文件结束符 |
failbit | s.fail() | 用于指出失败的IO操作 |
goodbit | s.good() | 用于指出流是OK的 |
除了上面四个条件状态检测函数,还有几个操作条件状态的函数:
函数名 | 函数作用 |
s.clear() | 将流s的状态设置为goodbit状态 |
s.clear(flag) | 将流s的状态设置为flag状态,flag的类型是ios_base::iostate类型 |
s.setstate(flag) | 给流s添加指定条件 |
s.rdstate() | 返回流s的当前条件,返回值类型为ios_base::iostate类型 |
关于条件状态的使用,看下面几个代码片段:
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
using namespace std;
int main()
{
int ival;
while (cin >> ival, !cin.eof())
{
if (cin.bad())
throw runtime_error("IO stream corrupted");
if(cin.fail())
{
cerr << "bad data, try again" << endl;
cin.clear(istream::failbit);
continue;
}
cout << "ival:" << ival << endl;
}
return 0;
}
这段代码是《C++ Primer》第四版上面的一个例子,该段代码的作用是循环不断读入cin,直到到达文件结束符或者发生不可恢复的读取错误为止。但是这个例子是有问题的,具体见我之前的博客《关于<C++ Primer>中cin.fail可能造成死循环的问题分析》。
缓冲区和IO就像孪生兄弟一样,有IO的地方,必然不能少了IO。说实话,我对C++的缓冲区还没有研究过,这里就拿C的缓冲区来说吧(我想应该差不了多少)。在标准C中,共有三种类型的缓冲:
(1)全缓冲(fully buffered)——只有填满缓冲区候才进行实际的I/O操作。这类缓冲通常用在文件操作中。
(2) 行缓冲(line-buffered)——当输入或输出中遇到换行符时才进行实际的I/O操作。键盘输入就是标准的行缓冲。
(3)无缓冲(non-buffered)——即没有缓冲。标准C的I/O库不对字符进行缓冲存储。
言归正传,这里我们的重点不是讲缓冲区的概念及类型,而是看在C++中,哪些情况可以刷新缓冲区(即数据真正的写到设备或文件):
最后,需要特别注意的是,当程序崩溃、异常终止时,缓冲区并不会被刷新。最常见的例子就是,我们的程序在一些异常场景下打了日志,但这些日志还处于缓冲区时程序突然异常终止了,那此时缓冲区里面的日志并不会打印出来。这是我们需要注意的。
(1)所有的IO对象都是不能拷贝和赋值的:
ofstream out1, out2;
out1 = out2; // 错误:不能给IO类型赋值
ofstream print(ofstream); // 错误:不能拷贝
out2 = print(out2); // 错误:不能拷贝
因为IO类型不能拷贝,所以函数的参数和返回值也不能是IO类型,只能使用引用类型。
(2)读写IO对象都会改变对象的状态,所以IO的引用也不能是const的。
文件IO类型都定义在fstream头文件中,共有三类:ifstream(读文件)、ofstream(写文件)、fstream(读写文件)。fstream将ifstream和ofstream的功能合并在一起,所以这里直接介绍fstream。由上面的继承关系可以看出fstream继承自iostream,所以它拥有iostream的特性,同时它还有自己特有(其他IO类型没有)的一些操作:
fstream fstrm | 创建一个未和任何文件绑定的文件流 |
fstream fstrm(s) | 创建一个和文件s绑定的文件流,s可以是string或char*;打开文件的模式取决于默认值 |
fstrean fstrm(s, mode) | 和上面的一样,但指定打开文件的模式 |
fstrm.open(s) fstrm.open(s,mode) |
两个函数都是打开文件s,并将其与文件流fstrm绑定在一起。前者使用流默认的文件模式,后者使用指定的模式,返回void |
fstrm.close( ) | 关闭fstrm流绑定的文件,返回void。当fstrm被析构时,也会自动调用close |
fstrm.is_open( ) | 检查与文件流fstrm绑定的文件是否成功打开或者有没有被关闭 |
C++中的文件模式有以下几种:
in | 用于打开一个输入流,ifstream流的默认值,ofstream不能使用该模式 |
out | 用于打开一个输出流,oftream流的默认值,ifstream不能使用该模式 |
app | 以追加写的方式打开一个输出流 |
ate | 打开文件后,立刻定位到文件尾,可用于任何文件流 |
trunc | 截断文件,即清空已有内容,仅用于输出流,且不可和app同时使用 |
binary | 以二进制模式操作,可用于任何文件流 |
sstream头文件中定义了三种用于操作“内存IO”的类型:istringstream(读string)、ostringstream(写string)、stringstream(读写string)。这里我们要注意一下,虽然这里我们把string流划分到IO去了,但其实它们的操作对象其实都是在内存中的string对象,并非平常说的那种IO。和文件流一样,string流继承自iostream,所以它也有iostream的特性,也有它自己特有的一些操作,这里以stringstream为例:
sstream strm | 创建一个未和任何string类型绑定的stringstream对象。sstream是sstream头文件中定义的一种类型 |
sstream strm(s) | 创建一个sstream流,该流与s的拷贝绑定 |
strm.str( ) | 返回与strm流绑定的string |
strm.str(s) | 拷贝string s到流strm,返回void |
上面我们介绍了sstream头文件里面包含的string流,其实还有一个strstream流,在strstream头文件中定义。它和sstream特别像,但是sstream是基于string实现的,而strstream是基于char*实现的。而strstream已经逐渐被废弃了,我们最好也不要再使用了,因为它非常的不安全。gcc的strstream头文件中是这样说的(其实是它包含的backward_warning.h中说的):
This file includes at least one deprecated or antiquated header which
may be removed without further notice at a future date. Please use a
non-deprecated interface with equivalent functionality instead. For a
listing of replacement headers and interfaces, consult the file
backward_warning.h. To disable this warning use -Wno-deprecated.
/*
A list of valid replacements is as follows:Use: Instead of:
<sstream>, basic_stringbuf <strstream>, strstreambuf
<sstream>, basic_istringstream <strstream>, istrstream
<sstream>, basic_ostringstream <strstream>, ostrstream
<sstream>, basic_stringstream <strstream>, strstream
<unordered_set>, unordered_set <ext/hash_set>, hash_set
<unordered_set>, unordered_multiset <ext/hash_set>, hash_multiset
<unordered_map>, unordered_map <ext/hash_map>, hash_map
<unordered_map>, unordered_multimap <ext/hash_map>, hash_multimap
<functional>, bind <functional>, binder1st
<functional>, bind <functional>, binder2nd
<functional>, bind <functional>, bind1st
<functional>, bind <functional>, bind2nd
<memory>, unique_ptr <memory>, auto_ptr
*/
这里列举出了旧的C++中存在的一些类型,后面会逐渐废弃的类型,后续我们也要避免使用。这里举一个例子:
/*
* main.cpp
*
* Created on: 2015年10月26日
* Author: Allan
*/
#include <iostream>
#include <sstream>
#include <string>
#include <strstream>
using namespace std;
string int2string_v1(int i)
{
ostringstream os;
os << i;
return os.str();
}
string int2string_v2(int i)
{
ostrstream os;
os << i;
return os.str();
}
int main()
{
cout << int2string_v1(10) << endl;
cout << int2string_v2(10) << endl;
return 0;
}
上面的这段代码分别使用sstream的string流和strstream流实现了同一个功能:将整数转为string。但是,程序运行后,我们会发现v1版本工作正常,但v2版本有些问题。这是因为sstream的str成员函数返回string,而strstream的str成员函数返回char*,但是返回的这个char*并没有包含字符串结束符’’,这就是为什么说strstream类型不安全。刚才我们介绍过ends,它会给缓冲后面加一个null,所以如果你一定要使用strstream,那ends对你就非常重要了。比如将上面的int2string_v2版本里os << i;改为os << i << ends;就可以正常工作了。但考虑到各方面因素,还是不推荐使用strstream。
]]>1, 调用方式方面的特点。我们调用静态成员的时候,不需要依赖于一个具体的类对象(当然也可以用对象去调用静态成员),可以直接用“类名::静态成员”这种方式去调用。
2, 没有this指针。我们知道C++的每个类都包含一个隐藏的成员函数——this指针,用来指向当前对象。而且这个this指针默认就是const的(top-level的const,有关top-level和low-level const的可以看我的博文《Top-Level和Low-Level const》),即这个this里面就是存的这个对象的地址,不可以改变。而静态变量不属于任何一个对象,所以它没有this指针。
3, 不能将静态成员函数声明为const的。首先我们看下边的一个成员函数的声明:
class A
{
public:
...
string get_something() const
{
return this->some_member;
}
...
}
我们注意到里面的成员函数get_something的参数列表后面有一个const。那么这个const代表什么含义呢?其实就是修饰隐藏的this指针的。前面已经说了,this指针默认是top-level的const,即它本身不可以改变。但是有时候,我们也不想让成员函数可以通过this指针改变它指向的对象,即我们也想让她同时是low-level const的,这时候我们就在参数列表后面加一个const就可以了。更多信息可以参见我的博文《C++的const成员函数和this指针以及mutable关键字》。因为这个const是修饰this指针的,而静态成员没有this指针,所以我们不能将静态成员函数声明为const。
4, 对于static成员函数,如果我们在类内部声明,然后在类外部实现,那只需要在声明的时候加上static关键字就可以了,在定义的时候不必且不能再加static关键字,否则会报错。我个人觉得这个有点苛刻了。
5, 不使用类的构造函数。我们知道类的构造函数实在初始化对象的时候使用的,既然静态成员不属于任何一个变量,自然也就不使用类的构造函数了。而且对于静态成员的初始化我们一定要小心——我们不能在类中初始化静态成员,因为我们一般是将类写在头文件中的,而头文件可能被多个源文件包含。也就是说,如果在类中初始化静态变量,那可能造成重复初始化的错误。一般最好的方式是在实现类的成员函数的cpp文件中去初始化静态成员。
6, 可以用不完全类型(incomplete type,比如只有前向声明,还没有具体实现的类)定义静态成员,但不可以定义非静态成员(指针和引用例外)。
当然,这里只是列举出了一部分static的特性,由于static成员属于类,而不属于对象这个大的特点导致它还有很多区别于非静态成员的特点,我们使用的时候需要注意。
最后,举一个《C++ Primer》上面的例子:假设有一个Account类,用来表示银行账户,那么每个账户都会有自己的所有者,以及账户所存储的金额,但是利率每个账户应该都是一样的。那么这个时候,我们就可以将利率定义为静态变量。看下面的代码:
// Account.h
/*
* Account.h
*
* Created on: 2015年10月26日
* Author: Allan
*/
#ifndef ACCOUNT_H_
#define ACCOUNT_H_
#include <iostream>
#include <string>
class Account
{
public:
Account() = default; // C++11新特性
~Account();
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
static double initRate() { return 0.053; };
private:
std::string owner;
double amount = 0;
static double interestRate;
};
#endif /* ACCOUNT_H_ */
// Account.cpp
/*
* Account.cpp
*
* Created on: 2015年10月26日
* Author: Allan
*/
#include "Account.h"
/*
* static 关键字只需要在类中声明的时候使用即可
*/
void Account::rate(double newRate)
{
interestRate = newRate;
}
double Account::interestRate = initRate();
]]>PS:本文所说的对象指所有类型的对象,而不局限与复合类型的对象。
top-level const可以出现在任何对象类型中,比如基本类型、指针类型、类类型等。而对于low-level const,只出现在基于基本类型的复合类型中,比如指针和引用。所以,对于所有的基本类型里面的const都是top-level的。但这里需要注意的是指针,它的top-level const和low-level const是相互独立的。其实这个我已经在之前的好几篇博客中分析过了,就是所谓的常量指针(对应low-level const)和指针常量(对应top-level const)的区分。这里再简单复习一下:
常量指针——指向常量(const)的指针;指针常量——指针本身是常量。
const char* p1; //常量指针,指向常量的指针;const is low-level
char const* p2; //同上
char* const p3; //指针常量,指针是常量;const is top-level
区分方法:const在*
左边的为常量指针,const在*
右边的为指针常量。现在我们也可以说,*
号左边的const是low-level的,*
号右边的const是top-level的。
思考:为什么引用和指针的作用比较相似,但是在top-level const和low-level const的概念上,它却不像指针,反而和其他类型一样,只有low-level const。是的,引用中的const永远是low-level的。为什么呢?——因为引用不是对象(references are not objects)。是的,引用不是对象,而top-level const是修饰对象的。
int i = 0;
int *const p1 = &i; // 我们不能改变p1本身的值,即p1永远只能指向i,因为const是top-level的
const int ci = 42; // 我们不能改变ci的值,因为const是top-level的。基本类型永远是top-level的
const int *p2 = &ci; // 我们不能改变p2(指向的值),因为const是low-level的(但可以改变p2本身的值)
const int *const p3 = p2; // *右边的const是top-level的,即p3本身的值不可变;*号左边的const是low-level的,即p3指向的对象是const的
const int &r = ci; // 引用中的const永远是low-level的
const和指针的混合使用已经够复杂的了,那为什么还要引入这两个概念呢?而它们又有什么作用呢?继续看…
top-level还是low-level const在我们拷贝一个对象时会有些区别——top-level的const在对象拷贝中会被丢弃(The distinction between top-level and low-level matters when we copy an object. When we copy an object, top-level consts are ignored)。
i = ci; // 我们把ci的值拷贝给了i,ci中的top-level const被丢弃了
p2 = p3; // 因为p2和p3指向的类型是一致的,所以我们这样拷贝赋值没有问题,但是需注意的是在拷贝的过程中,p3中的top-level const被丢弃了
但是拷贝的过程中,top-level const永远不会被丢弃。因为在程序设计的很多场景中,都设计到对象的拷贝,所以这个概念还是比较重要的。举一个函数重载的例子,我们知道函数重载是依靠参数列表来区分不同函数的,而函数传参的过程就是一个对象拷贝的过程。因为top-level的const在拷贝中会被丢弃,所以我们是不能依靠top-level的const来重载函数,但是可以使用low-level的const来重载:
// 依靠top-level const不能重载函数
Record lookup(Phone);
Record lookup(const Phone); // 重复定义,与上面函数定义相同
Record lookup(Phone *);
Record lookup(Phone* const); // 重复定义,与上面函数定义相同
// 以来low-level const可以重载函数
Record lookup(Account&);
Record lookup(const Account&); // 新函数,与上面的函数为不同的函数
Record lookup(Account*);
Record lookup(const Account*); // 新函数,与上面的函数为不同的函数
]]>PS:这里给出的四条建议都很常规,需要注意的是第3条:
int ival;
while (cin >> ival, !cin.eof())
{
if (cin.bad())
throw runtime_error("IO stream corrupted");
if(cin.fail())
{
cerr << "bad data, try again";
cin.clear(istream::failbit);
continue;
}
}
该段程序要实现的功能是循环不断读入cin,直到到达文件结束符或者发生不可恢复的读取错误为止。但该例程是有问题的,一旦走入if(cin.fail()) 分支,将陷入死循环。主要是有两方面的问题,下面逐一讨论。
问题1:对cin.clear的理解
对于C++的IO流,有一个条件状态(condition state)的概念。条件状态用于标记给定的IO对象(流)是否处于可用状态,或者是碰到了哪种特定的错误。目前,有四种枚举状态:
状态 | 条件状态检测函数 | 状态说明 |
badbit | s.bad() | 用于指出被破坏的流,不可恢复 |
eofbit | s.eof() | 用于指出流已经到达文件结束符 |
failbit | s.fail() | 用于指出失败的IO操作 |
goodbit | s.good() | 用于指出流是OK的 |
同时,提供了几个修改流状态的API:
函数名 | 函数作用 |
s.clear() | 将流s的状态设置为goodbit状态 |
s.clear(flag) | 将流s的状态设置为flag状态,flag的类型是ios_base::iostate类型 |
s.setstate(flag) | 给流s添加指定条件 |
s.rdstate() | 返回流s的当前条件,返回值类型为ios_base::iostate类型 |
这里需要重点注意的是clear成员函数。先看cpluscplus.com网站的说明:
public member function
<ios> <iostream>
std::ios::clear
void clear (iostate state = goodbit);Set error state flags
Sets a new value for the stream's internal error state flags.
state
The current value of the flags is overwritten: All bits are replaced by those in; If
stateis
goodbit(which is zero) all error flags are cleared.
badbit
In the case that no stream buffer is associated with the stream when this function is called, theflag is automatically set (no matter the value for that bit passed in argument
state).
state
Note that changing themay throw an exception, depending on the latest settings passed to member
exceptions.
The current state can be obtained with member function rdstate.
而《C Primer》中文版一书中对于s.clear()的解释为:将流所有状态值都重设为有效状态;对于s.clear(flag)的解释为:将流s中某个指定条件状态设置为有效。显然,后者的解释还讲的通,但前者的解释就是有问题的。但是,书中对于clear函数的描述还是有些模糊。其实,该函数的作用与它的名字大相径庭。从上面的英文解释中可以看出,该函数用于设置流内部的错误状态。而且默认值为goodbit。也就是说,s.clear()就相当于s.clear(goodbit),作用是将流s的状态设置为goodbit。而s.clear(flag)的作用就是讲流s的状态设置为flag。
这样,我们就发现了上述程序的第一个错误之处:cin.clear(istream::failbit); 当流出现错误时,程序的本意是提醒用户,然后恢复流状态。但是,这里使用cin.clear(istream::failbit); 语句并不是恢复流状态,而是将流设置为failbit状态,即错误状态,这样下一次循环又进入出错分支。所以应该将原代码中的cin.clear(istream::failbit); 改为cin.clear()或者cin.clear(istream::goodbit).然后我们再看另外一个问题。
问题2:缓冲区的问题
每一个IO对象都管理一个缓冲区,输入时先把内容输入到缓冲区中,当缓冲区被刷新时将内容写入到真是的输出设备或者文件,缓冲区被刷新有以下几种情况:
当我们输入一个错误的输入(比如,此例中输入一个非int值),流状态被设置为failbit,然后走进if(cin.fail()) 分支。虽然我们用clear清除了错误状态。但之前输入的错误值仍然留在缓冲区里面,且等到continue后,又被cin读入,所以陷入了死循环。
所以,我们要在clear之后,再将错误的缓冲区清空。类似于C程序,我们可以读取缓冲区中的值,然后将其丢弃。C++的IO提供了两个可以成员函数可以使用:sync和ignore:
sync:
public member function
<istream> <iostream>
std::istream::ignore
istream& ignore (streamsize n = 1, int delim = EOF);Extract and discard characters
Extracts characters from the input sequence and discards them, until either
ncharacters have been extracted, or one compares equal to
delim.
n
The function also stops extracting characters if the end-of-file is reached. If this is reached prematurely (before either extractingcharacters or finding
delim), the function sets the
eofbitflag.
sentry
Internally, the function accesses the input sequence by first constructing aobject (with
noskipwsset to
goodtrue
). Then (if), it extracts characters from its associated stream buffer object as if calling its member functions
sbumpcor
sgetc, and finally destroys the
sentryobject before returning.
ignore:
public member function
<istream> <iostream>
std::istream::ignore
istream& ignore (streamsize n = 1, int delim = EOF);Extract and discard characters
Extracts characters from the input sequence and discards them, until either
ncharacters have been extracted, or one compares equal to
delim.
n
The function also stops extracting characters if the end-of-file is reached. If this is reached prematurely (before either extractingcharacters or finding
delim), the function sets the
eofbitflag.
sentry
Internally, the function accesses the input sequence by first constructing aobject (with
noskipwsset to
goodtrue
). Then (if), it extracts characters from its associated stream buffer object as if calling its member functions
sbumpcor
sgetc, and finally destroys the
sentryobject before returning.
上面是cplusplus.com网站的说明,下面还有两个精简的:
cin.ignore
discards characters, up to the number specified, or until the delimiter is reached (if included). If you call it with no arguments, it discards one character from the input buffer.For example,
cin.ignore (80, 'n')
would ignore either 80 characters, or as many as it finds until it hits a newline.
cin.sync
discards all unread characters from the input buffer. However, it is not guaranteed to do so in each implementation. Therefore,ignore
is a better choice if you want consistency.
cin.sync()
would just clear out what's left. The only use I can think of forsync()
that can't be done withignore
is a replacement forsystem ("PAUSE");
:cin.sync(); //discard unread characters (0 if none) cin.get(); //wait for input
With
cin.ignore()
andcin.get()
, this could be a bit of a mixture:cin.ignore (std::numeric_limits<</span>std::streamsize>::max(),'n'); //wait for newline//cin.get()
If there was a newline left over, just putting
ignore
will seem to skip it. However, putting both will wait for two inputs if there is no newline. Discarding anything that's not read solves that problem, but again, isn't consistent.
从上面的解释可以看出,二者各有利弊。因为我们没法确定错误输入的内容、长度,所以如果使用ignore来丢弃缓冲区的数据的话,ignore的参数指定将是一个问题。似乎,sync是一个更好的选择。但是sync也有问题,即不同平台实现可能有差异,移植性差。所以具体选会哪种,需要根据具体场景决定。
另外一个需要注意的问题是,不管是sync还是ignore,都一定要在恢复流状态为goodbit之后再使用,否则两个函数都不会起作用。具体可参加函数实现。
综上,修改后的程序代码为:
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
int ival;
while (cin >> ival, !cin.eof())
{
if (cin.bad())
throw runtime_error("IO stream corrupted");
if(cin.fail())
{
cerr << "bad data, try again" << endl;
cin.clear(istream::goodbit); // 或 cin.clear();
cin.sync(); // 或 cin.ignore();
continue;
}
}
return 0;
}
遗留问题:上述代码我使用MinGW(GCC版本为5.1.0)在Windows下编译发现cin.sync()和cin.ignore()都是OK的。但是在Ubuntu 14.04(GCC版本为4.8.2)下面编译,发现使用cin.sync还是会有死循环的问题。但是二者sync的实现是相同的,问题原因还不清楚。
本文讨论的这个例子虽然是一个小例子,但是涉及的知识却是在C++ IO里面比较重要的东西。
]]>首先cin是一个对象,不会“返回”值,>>和<<才是方法,具有返回值。>>和<<操作符的运算顺序是从左向右,所以下面两种语句描述其实是一致的:
cin >> a >> b >> c; (((cin >> a) >> b) >> c)
操作cin >> a的意义:调用istream的operator>>方法读取数据并存入变量a中。那么>>或者<<的返回值是什么呢?这里说的返回值并不是指读入变量中的值,而是返回赋给左值的数据,在这里,>>返回的是cin,追踪源码可以发现这一点:
istream& operator>> (istream& is, char& ch ); istream& operator>> (istream& is, signed char& ch ); istream& operator>> (istream& is, unsigned char& ch ); istream& operator>> (istream& is, char* str ); istream& operator>> (istream& is, signed char* str ); istream& operator>> (istream& is, unsigned char* str );
当然,也可以测试如下:
int main() { int a; if ((cin >> a) == cin) { cout << "Equal" << endl; } else { cout << "Not equal" << endl; } return 0; }
为什么可以使用cin作为真值判定条件?
cin可以被如下使用:
if (cin) {} if (cin >> a >> b) {} while (cin >> a) {}
上面说到了>>返回值是cin,所以上面的真值判定等同于:
if (cin) {} if (cin) {} while (cin) {}
如果cin的状态OK则为真,如果cin遇到eof或者发生错误则返回false,为什么可以使用cin作为真值判定条件呢?
首先看cin是如何定义的:
extern istream cin;
这样一个值怎么可以作为if的真值判定条件呢?这是因为所有派生自ios的类都可以被强制转换为一个void *指针,如果设置了错误标志位则指针被置为NULL,否则为非NULL。测试代码如下:
#include <iostream> #include <fstream> #include <string> using namespace std; int main() { cout << "test1:" << endl; int a; if (cin >> a) { cout << "True" << endl; } else { cout << "False" << endl; } cout << "test2:" << endl; ifstream is1, is2; is1.open("test.txt"); is2.open("test.txt", ifstream::app); if ((void *)is1 == NULL) cerr << "Error open file" << endl; else cout << "success: is1=" << is1 << endl; if ((void *)is2 == NULL) cerr << "Error open file" << endl; else cout << "success: is2=" << is2 << endl; return 0; }
编译后执行结果:
allan@ubuntu:Temp$ ./a.out test1: a # 输入a,不是整数,所以cin出错,应该为NULL False test2: Error open file # 文件不存在,打开失败 success: is2=0x7ffffe090dd0 # 文件不存在,创建文件;打开成功 allan@ubuntu:Temp$ ./a.out test1: 1 True test2: Error open file success: is2=0x7fffcf46f320
参考自:http://www.cnblogs.com/alex-tech/archive/2012/03/27/2420197.html
]]>介绍完成员函数的基本概念,我们就来开始今天的正文:const成员函数和this指针以及mutable关键字。
每个成员函数(除static成员函数)都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象的地址,所以形参this是指向类对象的指针。因为static成员函数是类的组成部分,但不属于任何一个类对象,所以static成员函数没有this指针。在成员函数中,不必显式的使用this指针来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀的引用,都被假定为通过指针this实现的引用。看个例子:
class A { public: bool is_same(const A &inst) const { return m_id == inst.m_id; } bool is_same_ext(const A &inst) const { return this->m_id == inst.m_id; } private: int m_id; };
该例子中,is_same和is_same_ext其实是一模一样的。
NB:由于this指针是隐式定义的,因此不需要也不能在函数的形参表中包含this指针,但可以在函数体中显式的使用this指针。
那this指针有什么用呢?这里举个例子,看下面代码:我们定义了一个Screen类,其中move成员函数用于将光标移动到某个位置,set函数用于设置光标处的值。我们分别定义了两个版本。
class Screen { public: typedef std::string::size_type index; public: void move_dumm(index r, index c); void set_dumm(char); Screen& move(index r, index c); Screen& set(char); private: std::string contents; index cursor; index height, width; }; /* 实现 */ void Screen::set_dumm(char c) { contents[cursor] = c; } Screen& Screen::set(char c) { contents[cursor] = c; return *this; } void Screen::move_dumm(index r, index c) { index row = r * width; cursor = row + c; } Screen& Screen::move(index r, index c) { index row = r * width; cursor = row + c; return *this; }
我们对比以下上面代码中的set与set_dumm,move和move_dumm,其实他们实现的功能是一样的,但是不带dumm后缀的都返回了this的引用,这样有什么好处呢?好处就是我们可以将一些操作连接成一个序列,在一个表达式中实现:
Screen myScreen; myScreen.move(4, 0).set('#'); //先将光标移到(4,0)位置,再将该处的值设置为'#'
其实上面这个语句等价于:
myScreen.move(4, 0); myScreen.set('#'); 或者 myScreen.move_dumm(4, 0); myScreen.set_dumm('#');
显然,不带dumm后缀的实现比不带的用起来更灵活。当然this指针的用途不止于此,这里不再介绍。
我们看到,上面举的第一个例子中的两个成员函数后面都加了const,这种成员函数叫const成员函数(也称为常量成员函数。没有const的成为为普通成员函数或非const成员函数。),而那个const其实是修饰this形参的。这里有两个非常重要的区别:
另外,需要注意两点:
上面的东西非常的抽象,我们看个例子:继续前面的例子,现在我们定义了一个display常量成员函数,用于在给定的ostream上打印屏幕的内容contens。因为打印contents不会改变对象,所以我们将display函数定义为const。这样根据前面const成员函数的定义,display内部的this指针将是一个const Screen*型的const,同时,它返回的类型也必须是const Screen&,正如下面代码中实现的那样。代码如下:
class Screen { public: typedef std::string::size_type index; public: Screen& move(index r, index c); Screen& set(char); const Screen& display(std::ostream &os) const { do_display(os); return *this; } private: void do_display(std::ostream &os) const { os << contents; } private: std::string contents; index cursor; index height, width; };
但这样定义是有问题的,比如下面的使用:
myScreen.display(cout).set('#');
这样使用是错误的,原因在于这个表达式实在由display返回的对象上运行set。但对象是const的(因为display是const成员函数,只能将this指针作为const引用返回),我们不能在const对象上面调用非const成员函数set。那如何解决这个问题呢?
可以使用基于const的重载:我们定义两个display操作,一个是const,另一个不是const,基于成员函数是否为const,可以重载一个成员函数;同样的,基于一个指针形参是否指向const,也可以重载一个函数。const对象只能使用const成员,非const对象可以使用任一成员,但非const版本是一个更好的匹配。下面是代码实现:
class Screen { public: typedef std::string::size_type index; public: Screen& move(index r, index c); Screen& set(char); const Screen& display(std::ostream &os) const { do_display(os); return *this; } Screen& display(std::ostream &os) { do_display(os); return *this; } private: void do_display(std::ostream &os) const { os << contents; } private: std::string contents; index cursor; index height, width; };
我们只是再重载实现了一个非const版本的display,这样将display嵌入到长表达式中去调用就不会有问题了。
我们有时可能希望可以在const成员函数中改变对象的数据成员,那么我们就可以在数据类型声明前增加mutable关键字,这样我们变将这个数据成员声明为了可变数据成员。可变数据成员永远都不能为const,甚至当它是const对象的成员时也是如此。比如我们给Screen添加了一个新的可变数据成员access_ctr,用来跟踪调用Screen成员函数的频繁程度:
/*之前代码省略 */ private: void do_display(std::ostream &os) const { ++access_ctr; // 等效于:++this->access_ctr os << contents; } private: mutable size_t access_ctr; /* 之后代码省略*/
虽然do_display是const成员函数,但我们仍然可以在它里面改变数据成员access_ctr的值。
个人观点:虽然C和C++中引入const带来了许多方便和增强了许多功能,但不得不说,const也带来了许多混乱。
]]>C++中保留了指针,但其使用与C中是完全一样的,没有新增特性。所以这里不再赘述,只说一些使用时的注意点。更详细的内容可以看我的另外一片博客《解读C指针》。
(1)每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。
(2)一个有效的指针必然是下面三种状态之一:
(3)对指针赋值或初始化只能使用以下四中类型的值:
(4)void*指针是一种特殊的指针类型,它可以保存任何类型对象的地址,但void*只表明该指针与一地址值相关,但并不清楚存储在此地址上的对象的类型。另外,void*指针只支持几种有限的操作:
(5)指向const对象的指针(指针指向的对象是const的,但指针本身不是const的)的一些特性:
也就是说,指向const对象的指针既可以指向const对象的地址,也可以指向非const对象的地址,但指向非const对象的指针只能指向非const对象的地址。
引用就是对象的另一个名字,通过在变量名前添加“&”符号来定义。和指针一样,引用也只能绑定到和引用定义类型相同的对象上面。但如果对象之间支持隐式转换,也可以关联。比如可以将一个int类型的引用绑定到一个浮点数变量上面,但此时该引用绑定的对象已经被隐式转换了,比如:
float fval = 100.5; int &refval = fval;
此时,refval的值为100,而不是100.5。
const引用(指向const对象的引用):非const引用只能绑定到与该引用类型的对象;但const引用则可以绑定到不同但相关的类型的对象或绑定到右值。这一点和const指针类似。
const int ival4 = 8192; const double fval4 = 8192.1; //int &refval3 = ival4; //错误:refval3是非const引用,不能绑定到const对象ival4 const int &refval4 = ival4; const int &refval5 = fval4; // int类型的const引用可以绑定到相关类型(可隐式转换)的变量,但是值会被转换为引用类型的值,比如这里会将fva4的值由8192.1转换为int型8192,然后再绑定 //int &refval6 = 10; // 错误,非const引用不能绑定到右值 const int &refval7 = 10; //const引用可以绑定到右值
(1)定义与访问对象的方式不一样
指针通过在变量名前加“*”定义,而引用通过在变量名前加"&"来定义;访问关联的对象时,指针需要解引用(加*)才可以使用,而引用直接使用变量名访问。
int ival = 1024; int *pval = &ival; // 定义一个指针,并使它指向ival int &refval = ival; // 定义一个引用,并把它绑定到ival上 cout << "ival = " << ival << endl; // 指针访问对象时需要家*解引用才可以访问 cout << "*pval = " << *pval << endl; // 引用访问它绑定的变量时直接访问,因为它只是对象的另一个名字 cout << "refval = " << refval << endl;
(2)指针定义的时候可以不用初始化(虽然这不是一个好习惯),但引用必须在定义时就初始化。
(3)可以定义指针的指针,但是不能定义引用的引用。
指针的应用范围很广,但是引用在实际使用中往往大多用于函数参数传递的场合。所以我们这里专门来介绍以下函数参数传递,其中也会体现出指针与引用的联系与区别。
关于函数参数传递,有一条金科玉律:形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值;如果形参为引用类型,则它只是实参的别名。
非引用形参表示对应实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。
函数的形参是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。但因为指针传递的是地址,所以我们依旧可以通过这个地址来改变该地址保存的值。这点是指针与普通非引用形参的区别,也是与引用形参的一个相似点(只是效果相同,机制并不同)。但是对于指针形参,我们需要特别注意一个问题:指针形参是指向const类型还是非const类型,因为这将影响函数调用所使用的实参。看下面两个函数:
void func1(int *p) { } void func2(const int *p) { }
我们既可以用int*也可以用const int*类型的实参调用func2函数;但仅能用int*类型的实参调用func1函数。这个差别来源于前面介绍的指针的初始化规则:可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象。这样也是为了防止改变const对象的值而引起错误。
在调用函数时,如果该函数使用非引用的非const形参,则既可以给函数传递const实参,也可以传递非const的实参。这种行为源于之前介绍的const对象的标准初始化规则。因为初始化复制了初始化式的值,所以可以用const对象初始化非const对象;反过来,也可以用非const对象初始化const对象。而且,即使我们将函数形参指定为const,编译器还是会将其声明为普通类型,所以,下面两个定义是相同的:
void func(const int i) {} void func(int i) {}
这样是为了兼容C,因为在C中,具有const形参或非const形参的函数并无区别。
复制实参并不是在所有情况下都适合的,不适宜复制实参的情况包括:
针对上述几种情况,有效的解决方法是将形参定义为引用或指针类型。
与所有引用一样,引用形参直接关联到其所绑定的对象,而并非这些对象的副本。
这里举一个简单的例子:
void swap_error(int v1, int v2) { int temp = v2; v2 = v1; v1 = temp; } void swap(int &v1, int &v2) { int temp = v2; v2 = v1; v1 = temp; }
第一个swap_error函数显然是实现不了交换的功能的,这里不再啰嗦。而第二个函数swap是可以的,是因为我们的参数都是引用类型的,所以v1、v2并不是实参的拷贝,而只是实参的另一个名字,对v1和v2的任何修改就是对实参的修改。这是引用形参最基本的用法——利用引用形参修改实参的值。除了这个,我们再介绍几个其他用法。
函数只能返回单个值,但有些时候,函数有不止一个的内容需要返回。例如,定义一个find_val函数,在一个整形vector对象的元素里面搜索某个特定值。如果找到满足要求的元素,则返回指向该元素的迭代器;否则返回一个迭代器,指向该vector对象的end操作返回的元素。此外,如果该值出现了不止一次,我们还希望函数可以返回其出现的次数。这时我们可以实现如下:
vector<int>::const_iterator find_val ( vector<int>::const_iterator beg, vector<int>::const_iterator end, int value, vector<int>::size_type &occurs ) { vector<int>::const_iterator res_iter = end; occurs = 0; for(; beg != end; ++beg) if( *beg == value ) { if (res_iter == end) res_iter = beg; ++occurs; } return res_iter; }
这个很容易理解,直接看一段代码:
bool is_shorter(const string &s1, const string &s2) { return s1.size() < s2.size(); }
这里我们想比较两个string对象的长度,显然我们只需访问每个string对象的size,而不必修改这些对象。如果此时直接传非引用、非指针形参,则会导致复制操作。
NB:如果使用引用形参的唯一目的是避免复制实参,则应该将形参定义为const引用。
如果函数具有普通的非const引用形参,则不能通过const对象进行调用。因为,此时函数可以修改传递进来的对象,这样就违背了实参const特性。但是定义为const引用的形参,却可以通过非const对象调用。这个之前也已经介绍过。看下面一个例子:
string::size_type find_char(string &s, char c) { string::size_type i = 0; while (i != s.size() && s[i] != c) ++i; return i; } // 第一处调用 ... if (find_char("Hello world", 'o')) ... // 第二处调用 bool is_sentence(const string &s) { return (find_char(s, '.') == s.size() - 1); }
我们定义了一个在string对象中查找特定字符的函数find_char,但由于它的引用形参是非const引用,所以是不能用字面值或const引用或可以产生右值的表达式调用的,所以后面的两处调用都是错误的。
所以,应该将不需要修改的引用形参定义为const引用,普通的非const引用形参在使用时太不灵活——这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
之前我们写了交换两个整数的swap函数,现在我们编写一个实现两个指针交换的函数。我们知道用*定义指针,用&定义引用。现在问题在于如何将这两个操作符结合起来获取指向指针的引用。这里给出一个例子:
void ptrswap(int *&p1, int *&p2) { int *temp = p2; p2 = p1; p1 = temp; }
形参int *&p1 的定义应从右至左理解:p1是一个引用,与指向int型对象的指针相关联。也就是说,p1只是传递进ptrswap函数的任意指针的别名。比如说,交换前p1指向i,p2指向j,则交换后p1指向j,p2指向i。
至此,函数参数传递就介绍完了。最后再总结以下比较容易混淆的点:我们会发现不论是const指针还是const引用都比非const指针和非const引用有“较强的能力”——可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象;如果函数具有普通的非const引用形参,则不能通过const对象进行调用,但是定义为const引用的形参,却可以通过非const对象调用。所以,除我们要使用形参来修改实参的值的场景外,在函数参数传递过程中,应该定义为const指针或const引用。
个人观点:虽然大多数场景,指针和引用可是实现相同的功能,但是在C++的函数参数传递中还是优先使用引用而非指针。毕竟指针会直接操作内存,使用不当就会踩内容;而引用实现为对象的一个别名,不会直接操作内存,比较安全。
]]>输出流: std::cout << "print to stream" << std::endl;
输入流: std::cin >> v1;
endl是一个特殊值,将它写入输出流中时,具有换行的效果,并刷新与设备关联的缓冲区。
声明(declaration)用于向程序表明变量的类型和名字。我们使用extern关键字声明变量而不定义它。
定义(definition)用于为变量分配存储空间,还可以为变量指定初始值。
变量与声明的一些区别:
下面我们看几个例子:
extern int i; // 声明但不定义i int i; // 声明并且定义i extern double pi = 3.14; // 虽然使用了extern关键字,但因为初始化了变量,分配了存储空间,所以依旧是定义,而不是声明
NB:任何在多个文件中使用的变量都需要有与定义分离的声明。这样,一个文件含有变量的定义,使用该变量的其他文件则包含该变量的声明(而不是定义)。
在声明和定义的问题上,C和C++是一样的。
C++中的枚举与C中的也是一样的,使用关键字enum定义,气候是一个可选的枚举类型名,和一个用花括号括起来,用逗号分开的枚举成员列表。枚举有如下特点:
在C++中用class和struct关键字定义类的唯一差别在于默认的访问级别:默认情况下,struct的成员为public,而class的成员为private。
因为头文件包含在多个源文件中,所以头文件用于声明而不用于定义,即不应该包含变量或函数的定义。除下面三个例外:
(1)定义类;
(2)定义inline函数(其实inline函数只应该定义在头文件中,不应该出现在源文件中);
(3)定义值在编译时就已经知道的const对象;
其实,上面三个例外项都有一个特点:可能会在很多地方使用,但每个地方(的定义)都是一样的,可以看成是其他地方(比如源文件中)的使用都是从定义它的头文件处获得的一个拷贝,不能修改。
]]>头文件:<io.h>
函数原型:int _access(const char *pathname, int mode);
参数:pathname 为文件路径或目录路径 mode 为访问权限(在不同系统中可能用不能的宏定义重新定义)
返回值:如果文件具有指定的访问权限,则函数返回0;如果文件不存在或者不能访问指定的权限,则返回-1.
备注:当pathname为文件时,_access函数判断文件是否存在,并判断文件是否可以用mode值指定的模式进行访问。当pathname为目录时,_access只判断指定目录是否存在,在Windows NT和Windows 2000中,所有的目录都只有读写权限。
mode的值和含义如下所示:
00——只检查文件是否存在
02——写权限
04——读权限
06——读写权限
对应的还有_access的宽字符版本,用法相同。
例子:
#include <io.h> #include <stdio.h> #include <stdlib.h> int main() { if ((_access("IsExist.txt", 0)) != -1) { printf("File IsExist.txt exists.n"); if ((_access("IsExist.txt", 2)) != -1) printf("File IsExist.txt does not have write permission.n"); } return 0; }]]>