NYC's Blog - C++ http://niyanchun.com/tag/cpp/ zh-CN 让人又爱又恨的一门语言。爱,因为你强大;恨,因为你太复杂。 Fri, 30 Oct 2015 23:27:00 +0800 Fri, 30 Oct 2015 23:27:00 +0800 重温C++——IO库 http://niyanchun.com/io-library-in-cpp.html http://niyanchun.com/io-library-in-cpp.html Fri, 30 Oct 2015 23:27:00 +0800 NYC 1. 概述

为了概览一下C++的IO库,先来张cplusplus.com网站的图片:

iostream

从这个图片我们基本上可以得到C++ IO库的所有基本信息:头文件、类、继承关系。为了更清楚的看出各个类的继承关系,我根据上面的图重新画了一个:

io

从上面两个图可以看出,C++的IO都是流式的,主要分为三大类:普通流文件流字符串(string)流。这三大类的流各有各的特点,但也有许多共性,我们先来看一些共性。

1.1 条件状态

其实所谓的条件状态很容易理解,就是标识流此刻的状态。条件状态(均为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可能造成死循环的问题分析》。

2.2 刷新缓冲区

缓冲区和IO就像孪生兄弟一样,有IO的地方,必然不能少了IO。说实话,我对C++的缓冲区还没有研究过,这里就拿C的缓冲区来说吧(我想应该差不了多少)。在标准C中,共有三种类型的缓冲:

(1)全缓冲(fully buffered)——只有填满缓冲区候才进行实际的I/O操作。这类缓冲通常用在文件操作中。

(2) 行缓冲(line-buffered)——当输入或输出中遇到换行符时才进行实际的I/O操作。键盘输入就是标准的行缓冲。

(3)无缓冲(non-buffered)——即没有缓冲。标准C的I/O库不对字符进行缓冲存储。

言归正传,这里我们的重点不是讲缓冲区的概念及类型,而是看在C++中,哪些情况可以刷新缓冲区(即数据真正的写到设备或文件):

  • 程序正常结束。当程序正常从main函数return时,所有的输出缓冲会被刷新。
  • 缓冲区满了以后,缓冲区也会被刷新。
  • 我们使用一些强制刷新缓冲区的操作。比如:(1)endl——插入换行符,然后刷新缓冲区。(2)ends——插入一个null,然后刷新缓冲区。(3)flush——什么也不加,直接刷新缓冲区。
  • 使用unitbuf操作符。该操作符告诉流没进行一次连续的输出后,就刷新缓冲区。使用nounitbuf可以复原系统默认设置。
  • 当我们把输出流和另外一个流绑定在一起的时候,后者的读或写之前都会刷新前者的缓冲区。比如标准输出流cout默认是和cin与cerr绑定在一起的,这样cin读或者cerr写都会刷新cout流。这个是很重要的,比如讲cout与cin绑定在一起就可以保证我们每次读之前输出缓冲区已经被刷新了——例如,一般我们的程序在获取输入前都会输出一段提示,如果此时不先刷新输出缓冲区的话,该缓冲区内的内容就可能被当成输入的一部分。当然,一个流一次最多只能另外绑定一个流。

最后,需要特别注意的是,当程序崩溃、异常终止时,缓冲区并不会被刷新。最常见的例子就是,我们的程序在一些异常场景下打了日志,但这些日志还处于缓冲区时程序突然异常终止了,那此时缓冲区里面的日志并不会打印出来。这是我们需要注意的。

2.3 其他

(1)所有的IO对象都是不能拷贝和赋值的

ofstream out1, out2;
out1 = out2;				// 错误:不能给IO类型赋值
ofstream print(ofstream);	// 错误:不能拷贝
out2 = print(out2);			// 错误:不能拷贝

因为IO类型不能拷贝,所以函数的参数和返回值也不能是IO类型,只能使用引用类型。

(2)读写IO对象都会改变对象的状态,所以IO的引用也不能是const的

2. 文件流

文件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 以二进制模式操作,可用于任何文件流
注意:默认情况下,输出流都是以trunc模式打开的,除非显式指定为app模式。

3. string流

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

4. sstream和strstream

上面我们介绍了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

]]>
0 http://niyanchun.com/io-library-in-cpp.html#comments http://niyanchun.com/feed/tag/cpp/
C++类中的static成员 http://niyanchun.com/static-member-in-cpp.html http://niyanchun.com/static-member-in-cpp.html Tue, 27 Oct 2015 22:57:00 +0800 NYC 本文讨论C++类中的static成员的一些特性。我们知道静态成员的特点事它只属于类本身,不属于类的任何一个对象。在类加载的时候就初始化了。那么由这一个特性我们可以得出很多其他的一些特性:

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-&gt;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();
]]>
0 http://niyanchun.com/static-member-in-cpp.html#comments http://niyanchun.com/feed/tag/cpp/
Top-Level和Low-Level const http://niyanchun.com/top-level-and-low-level-const.html http://niyanchun.com/top-level-and-low-level-const.html Sun, 25 Oct 2015 17:17:00 +0800 NYC 关于const,之前已经在多篇博客里面有介绍过了。我们知道const本身的含义很简单,就是被它修饰的对象不可以改变。但是,当他和指针或是引用一起使用的时候,我们往往比较难区分到底是指针(或引用)本身是const的,还是它所指向的对象是const的。最近读《C++ Primer》英文第五版的时候,发现了一个新的术语——Top-Level const和Low-Level const,他对于我们理解很多复杂场景下的const很有帮助。而且这个概念我在该书的中文第四版中并未见到过,不知道是原来的英文版就没有,还是中文版翻译的时候没有翻译过来。

PS:本文所说的对象指所有类型的对象,而不局限与复合类型的对象。

  • Top-Level const——对象本身是一个常量;
  • Low-Level const——指针指向的或引用引用的对象是const。

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*);  // 新函数,与上面的函数为不同的函数
]]>
4 http://niyanchun.com/top-level-and-low-level-const.html#comments http://niyanchun.com/feed/tag/cpp/
C/C++中如何选择基本数据类型 http://niyanchun.com/decide-whick-type-to-use.html http://niyanchun.com/decide-whick-type-to-use.html Mon, 19 Oct 2015 23:18:00 +0800 NYC 《C++ Primer》中对于如何选择我们要使用的类型给出了四条建议:

  1. 如果你知道变量不可能是负值的话,就使用无符号类型。
  2. 在整数运算中尽量选择int类型,而不是short,因为short一般太小了;在实际中,long一般都是和int是一样长的,所以如果你的数据超出了int能表示的范围,就直接使用long long,而不是long。
  3. 在算术表达式中不要使用不带符号修饰符的char类型,因为char类型在有的机器上面是unsigned char,在有的上面是signed char;也不要在算术表达式中使用bool类型,该类型只用来表示真或假。
  4. 涉及浮点数运算的场合使用double而不是float,因为float能表示的精度太小了。但双精度与单精度的计算速度往往差不了多少,在有些机器上面,双精度反而比单精度计算更快。

PS:这里给出的四条建议都很常规,需要注意的是第3条:

  • C或C标准中并没有明确规定char代表signed char还是unsigned char,具体是哪一种是有系统或者编译器决定的。所以我们使用的时候尽量避免使用不带符号修饰符的char类型。
  • bool类型只有true—1和false—0两个值。如果我们将一个非0(正数或负数,不论是整数还是非整数)的数赋给bool类型,那其值就会转换为true,0转换为false。反之,如果我们在算术表达式中将bool型赋给其他类型的变量,那也只会将0或1赋给变量,这往往都不是我们所预期的。所以在算术表达式中不要使用bool类型。
]]>
0 http://niyanchun.com/decide-whick-type-to-use.html#comments http://niyanchun.com/feed/tag/cpp/
关于《C++ Primer》中cin.fail可能造成死循环的问题分析 http://niyanchun.com/cin-fail-infinite-loop.html http://niyanchun.com/cin-fail-infinite-loop.html Wed, 29 Jul 2015 22:49:00 +0800 NYC 在《C++ Primer》一书中有这样一个例子(如果是中文第四版,就在P248):

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.
The current value of the flags is overwritten: All bits are replaced by those in

state

; If

state

is

goodbit

(which is zero) all error flags are cleared.
In the case that no stream buffer is associated with the stream when this function is called, the

badbit

flag is automatically set (no matter the value for that bit passed in argument

state

).
Note that changing the

state

may 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对象都管理一个缓冲区,输入时先把内容输入到缓冲区中,当缓冲区被刷新时将内容写入到真是的输出设备或者文件,缓冲区被刷新有以下几种情况:

  • 程序正常结束,作为main函数返回的一部分,将清空所有的缓冲区
  • 在一些不确定的情况下缓冲区可能已经满了,因此在写下一个值之前会对缓存区进行刷新
  • 用操纵符显示的进行刷新:(1)endl:刷新流,输出内容换行(2)flush:刷新流,但不在输出中添加任何形式的字符(3)ends:在缓冲区中插入空字符null并刷新
  • 在每次输出操作执行完后,用unitbuf操作符设置流的内部状态,从而清空缓冲区
  • 将输入流与输出流关联。

当我们输入一个错误的输入(比如,此例中输入一个非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

n

characters have been extracted, or one compares equal to

delim

.
The function also stops extracting characters if the end-of-file is reached. If this is reached prematurely (before either extracting

n

characters or finding

delim

), the function sets the

eofbit

flag.
Internally, the function accesses the input sequence by first constructing a

sentry

object (with

noskipws

set to true). Then (if

good

), it extracts characters from its associated stream buffer object as if calling its member functions

sbumpc

or

sgetc

, and finally destroys the

sentry

object 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

n

characters have been extracted, or one compares equal to

delim

.
The function also stops extracting characters if the end-of-file is reached. If this is reached prematurely (before either extracting

n

characters or finding

delim

), the function sets the

eofbit

flag.
Internally, the function accesses the input sequence by first constructing a

sentry

object (with

noskipws

set to true). Then (if

good

), it extracts characters from its associated stream buffer object as if calling its member functions

sbumpc

or

sgetc

, and finally destroys the

sentry

object 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 for sync() that can't be done with ignore is a replacement for system ("PAUSE");:

cin.sync(); //discard unread characters (0 if none) cin.get(); //wait for input

With cin.ignore() and cin.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里面比较重要的东西。

]]>
0 http://niyanchun.com/cin-fail-infinite-loop.html#comments http://niyanchun.com/feed/tag/cpp/
C++标准I/O返回值问题 http://niyanchun.com/cpp-io-return-value.html http://niyanchun.com/cpp-io-return-value.html Mon, 27 Jul 2015 22:40:00 +0800 NYC 这里以标准输入流cin为例。

首先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

]]>
0 http://niyanchun.com/cpp-io-return-value.html#comments http://niyanchun.com/feed/tag/cpp/
C++的const成员函数和this指针以及mutable关键字 http://niyanchun.com/const-this-pointer-mutable-keyword-in-cpp.html http://niyanchun.com/const-this-pointer-mutable-keyword-in-cpp.html Fri, 10 Jul 2015 00:11:00 +0800 NYC 成员函数是类的一部分,其定义与普通函数一样,没有特殊之处。但是需要注意以下一些特点:

  • 编译器隐式的将在类内部定义的成员函数当做内联函数(不管有没有使用inline关键字)。
  • 在成员函数的声明或定义处指定inline关键字都是合法的。
  • 类的成员函数可以访问该类的private成员。
  • 使用类的对象来调用类的成员函数,如果是静态成员函数,还可以使用类名加::符号来调用。
  • 成员函数可以重载。

介绍完成员函数的基本概念,我们就来开始今天的正文:const成员函数和this指针以及mutable关键字。

1. this指针

每个成员函数(除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指针的用途不止于此,这里不再介绍。

2. const成员函数

我们看到,上面举的第一个例子中的两个成员函数后面都加了const,这种成员函数叫const成员函数(也称为常量成员函数。没有const的成为为普通成员函数或非const成员函数。),而那个const其实是修饰this形参的。这里有两个非常重要的区别:

  • 在普通的非const成员函数中,this的类型是一个指向类类型的const指针(指向const对象的指针,指针本身不能再改变,即指针初始化时指向的是什么就是什么,后面不能再指向其他对象),也就是说我们可以改变const所指向的值,但是不能改变this所保存的地址。这是非常合理的,也就是说我们可以通过this指针修改类的某些成员的值,但我们不能改变this指向该对象的特性,否则就失去了this的含义。
  • 在const成员函数中,this的类型是一个指向const类类型对象的const指针,即我们既不能改变this所指向的对象(不论该对象是否是const的),也不能改变this所保存的地址。不能从const成员函数返回指向类对象的普通引用,const成员函数只能返回*this作为一个const引用。

另外,需要注意两点:

  • const对象、指向const对象的指针或引用只能用于调用其const成员函数,不能调用非const成员函数。
  • 非const对象、非指向const对象的指针或引用既可以调用const成员函数,也可以调用非const成员函数。

上面的东西非常的抽象,我们看个例子:继续前面的例子,现在我们定义了一个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嵌入到长表达式中去调用就不会有问题了。

3. mutable关键字

我们有时可能希望可以在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也带来了许多混乱。

]]>
3 http://niyanchun.com/const-this-pointer-mutable-keyword-in-cpp.html#comments http://niyanchun.com/feed/tag/cpp/
重温C++的指针和引用 http://niyanchun.com/references-and-pointer-in-cpp.html http://niyanchun.com/references-and-pointer-in-cpp.html Sun, 28 Jun 2015 19:49:00 +0800 NYC C++和C一个不同的地方就是引入了引用(reference)这个概念,而且引用很容易和指针混淆。

1. 指针

C++中保留了指针,但其使用与C中是完全一样的,没有新增特性。所以这里不再赘述,只说一些使用时的注意点。更详细的内容可以看我的另外一片博客《解读C指针》。

(1)每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。

(2)一个有效的指针必然是下面三种状态之一:

  • 保存一个特定对象的地址;
  • 指向某个对象后面的另一对象;
  • 0(即NULL)值。

(3)对指针赋值或初始化只能使用以下四中类型的值:

  • 0值常量表达式,例如在编译时可获得0值的整形const对象或字面值常量0;
  • 类型匹配的对象的地址;
  • 另一对象之后的下一地址;
  • 同类型的另一个有效指针。

(4)void*指针是一种特殊的指针类型,它可以保存任何类型对象的地址,但void*只表明该指针与一地址值相关,但并不清楚存储在此地址上的对象的类型。另外,void*指针只支持几种有限的操作:

  • 与另一个指针进行比较;
  • 向函数传递void*指针或从函数返回void*指针;
  • 给另一个void*指针赋值。
  • 不允许使用void*指针操作它所指向的对象。

(5)指向const对象的指针(指针指向的对象是const的,但指针本身不是const的)的一些特性:

  • 指向const对象的指针也必须具有const特性,即不能将一个const对象的地址赋给一个非const对象的指针;
  • 允许把非const对象的地址赋给指向const对象的指针;
  • 不能使用void*指针保存const对象的地址,而必须用const void *类型的之后怎保存const对象的地址。

也就是说,指向const对象的指针既可以指向const对象的地址,也可以指向非const对象的地址,但指向非const对象的指针只能指向非const对象的地址。

2. 引用

引用就是对象的另一个名字,通过在变量名前添加“&”符号来定义。和指针一样,引用也只能绑定到和引用定义类型相同的对象上面。但如果对象之间支持隐式转换,也可以关联。比如可以将一个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引用可以绑定到右值

3. 引用和指针的区别

(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)可以定义指针的指针,但是不能定义引用的引用。

4. 函数参数传递

指针的应用范围很广,但是引用在实际使用中往往大多用于函数参数传递的场合。所以我们这里专门来介绍以下函数参数传递,其中也会体现出指针与引用的联系与区别。

关于函数参数传递,有一条金科玉律:形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值;如果形参为引用类型,则它只是实参的别名。

4.1 非引用形参

非引用形参表示对应实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。

4.1.1 指针形参

函数的形参是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。但因为指针传递的是地址,所以我们依旧可以通过这个地址来改变该地址保存的值。这点是指针与普通非引用形参的区别,也是与引用形参的一个相似点(只是效果相同,机制并不同)。但是对于指针形参,我们需要特别注意一个问题:指针形参是指向const类型还是非const类型,因为这将影响函数调用所使用的实参。看下面两个函数:

void func1(int *p) { }
void func2(const int *p) { }

我们既可以用int*也可以用const int*类型的实参调用func2函数;但仅能用int*类型的实参调用func1函数。这个差别来源于前面介绍的指针的初始化规则:可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象。这样也是为了防止改变const对象的值而引起错误。

4.1.2 const形参

在调用函数时,如果该函数使用非引用的非const形参,则既可以给函数传递const实参,也可以传递非const的实参。这种行为源于之前介绍的const对象的标准初始化规则。因为初始化复制了初始化式的值,所以可以用const对象初始化非const对象;反过来,也可以用非const对象初始化const对象。而且,即使我们将函数形参指定为const,编译器还是会将其声明为普通类型,所以,下面两个定义是相同的:

void func(const int i) {}
void func(int i) {}

这样是为了兼容C,因为在C中,具有const形参或非const形参的函数并无区别。

4.1.3 复制实参的局限性

复制实参并不是在所有情况下都适合的,不适宜复制实参的情况包括:

  • 当需要在函数中修改实参的值时。
  • 当需要以大型对象作为实参传递时。对实际应用而言,复制对象所付出的时间和存储空间代价往往过大。
  • 当没有办法实现对象的复制时。

针对上述几种情况,有效的解决方法是将形参定义为引用或指针类型。

4.2 引用形参

与所有引用一样,引用形参直接关联到其所绑定的对象,而并非这些对象的副本。

4.2.1 利用引用形参修改实参的值

这里举一个简单的例子:

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的任何修改就是对实参的修改。这是引用形参最基本的用法——利用引用形参修改实参的值。除了这个,我们再介绍几个其他用法。

4.2.2 使用引用形参返回额外信息

函数只能返回单个值,但有些时候,函数有不止一个的内容需要返回。例如,定义一个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;
}

4.2.3 利用const引用避免复制

这个很容易理解,直接看一段代码:

bool is_shorter(const string &s1, const string &s2)
{
    return s1.size() < s2.size();
}

这里我们想比较两个string对象的长度,显然我们只需访问每个string对象的size,而不必修改这些对象。如果此时直接传非引用、非指针形参,则会导致复制操作。

NB:如果使用引用形参的唯一目的是避免复制实参,则应该将形参定义为const引用。

4.2.4 更灵活的指向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对象初始化,也不能用字面值或产生右值的表达式实参初始化。

4.2.5 传递指向指针的引用

之前我们写了交换两个整数的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++的函数参数传递中还是优先使用引用而非指针。毕竟指针会直接操作内存,使用不当就会踩内容;而引用实现为对象的一个别名,不会直接操作内存,比较安全。

]]>
0 http://niyanchun.com/references-and-pointer-in-cpp.html#comments http://niyanchun.com/feed/tag/cpp/
重温C++的一些基础概念 http://niyanchun.com/basic-concepts-in-cpp.html http://niyanchun.com/basic-concepts-in-cpp.html Fri, 26 Jun 2015 22:41:00 +0800 NYC 好久都没有使用C++写过代码了,一直都在用C或者一些脚本语言。最近似乎因为工作需要,又要用C++了,觉得有必要再翻一遍《C++ Primer》,重温一下C++中有但C中没有的特性了。

1. 输出流与输入流

输出流: std::cout << "print to stream" << std::endl;

输入流: std::cin >> v1;

endl是一个特殊值,将它写入输出流中时,具有换行的效果,并刷新与设备关联的缓冲区。

2. 声明和定义

声明(declaration)用于向程序表明变量的类型和名字。我们使用extern关键字声明变量而不定义它。

定义(definition)用于为变量分配存储空间,还可以为变量指定初始值。

变量与声明的一些区别:

  • 声明不会分配空间,定义会分配空间;
  • 一个程序中,变量可以有多个(相同的)声明,但有且只能有一个定义。

下面我们看几个例子:

extern int i;	// 声明但不定义i
int i;          // 声明并且定义i

extern double pi = 3.14;    // 虽然使用了extern关键字,但因为初始化了变量,分配了存储空间,所以依旧是定义,而不是声明

 NB:任何在多个文件中使用的变量都需要有与定义分离的声明。这样,一个文件含有变量的定义,使用该变量的其他文件则包含该变量的声明(而不是定义)。

在声明和定义的问题上,C和C++是一样的。

3. 枚举

C++中的枚举与C中的也是一样的,使用关键字enum定义,气候是一个可选的枚举类型名,和一个用花括号括起来,用逗号分开的枚举成员列表。枚举有如下特点:

  • 默认地,第一个枚举成员赋值为0,后面的每个枚举成员比前面的大1.
  • 枚举成员是常量,所以不能改变枚举成员的值。
  • 枚举成员值可以是不唯一的。
  • 每个enum都定义一种唯一的类型。所以枚举类型的对象的初始化或赋值,只能通过其枚举成员或统一枚举类型的其他对象来进行。

4. struct和class

在C++中用class和struct关键字定义类的唯一差别在于默认的访问级别:默认情况下,struct的成员为public,而class的成员为private。

5. 关于头文件

因为头文件包含在多个源文件中,所以头文件用于声明而不用于定义,即不应该包含变量或函数的定义。除下面三个例外

(1)定义类;

(2)定义inline函数(其实inline函数只应该定义在头文件中,不应该出现在源文件中);

(3)定义值在编译时就已经知道的const对象;

其实,上面三个例外项都有一个特点:可能会在很多地方使用,但每个地方(的定义)都是一样的,可以看成是其他地方(比如源文件中)的使用都是从定义它的头文件处获得的一个拷贝,不能修改。

]]>
0 http://niyanchun.com/basic-concepts-in-cpp.html#comments http://niyanchun.com/feed/tag/cpp/
C++中的_access函数 http://niyanchun.com/access-in-cplusplus.html http://niyanchun.com/access-in-cplusplus.html Wed, 28 May 2014 08:15:00 +0800 NYC 今天读代码看到一个access函数,当时没明白怎么回事。man了一下才知道该函数主要用于文件读取方面——判断文件是否存在,并判断文件是否可写。Linux下,该函数为access,位于头文件<unistd.h>中,而在标准C++中,该函数为_access,位于头文件<io.h>中,两者的使用方法基本相同,只是在一些参数方面可能会有一些不同的宏定义。下面是标准C++为例做一下总结:

头文件:<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;
}
]]>
0 http://niyanchun.com/access-in-cplusplus.html#comments http://niyanchun.com/feed/tag/cpp/