<<C++Primer PLus 第五版>>读书笔记4(终篇)

IT书籍读书笔记 专栏收录该内容
51 篇文章 0 订阅

友元
    类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。有一些函数、成员函数或类为友元只能是由类定义,而不能从外部强加友情。因此,金光友元被授予从外部访问类的私有部分的限制,但他们并不与面向对象的编程思想相悖;相反,他们提高了公有接口的灵活性。

 

嵌套类
    在c++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class),他通过提供新的类型作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析操作符。

1.嵌套类的访问权限
    有两种访问权限适合于嵌套类。首先,嵌套类的声明位置决定了嵌套类的作用域,即他决定了程序的哪些部分可以创建这种类的对象。其次,和其他类一样,嵌套类的公有部分、保护部分和私有部分控制了对类成员的访问。在哪些地方可以使用嵌套类以及如何使用嵌套类,取决于作用域和访问控制。
    如果嵌套类是在另一个类的私有部分声明的,则只有后者知道他。派生类不能直接访问基类的私有部分。
    如果嵌套类是在另一个类中到的保护部分声明的,则他对于后者来说是课件的,但是对于外部世界则是不可见的。不过,在这种情况中,派生类将知道嵌套类,并可以直接创建这种类型的对象。
    如果嵌套类是在另一个类中的公有部分声明的,则允许后者、后者的派生类以及外部世界使用它,因为他是公有的。不过,由于嵌套类的作用域为包含它的类,因此在外部世界使用它时,必须使用类限定符。

// testNestedClass.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

class Test
{
public:
	Test( int a, int b )
	{
		m_a = a;
		m_b = b;
	}
	void show()
	{
		cout << "this class Test a value is:" << m_a << endl;
		cout << "this class Test b value is:" << m_b << endl;
	}
	void newNestedClass( double a, double b )
	{
		NestedClass( a, b ).show();
	}
	class NestedClass
	{
	public:
		NestedClass( double a, double b )
		{
			m_NestedClass_a = a;
			m_NestedClass_b = b;
		}
		void show()
		{
			cout << "this class NestedClass a value is:" << m_NestedClass_a << endl;
			cout << "this class NestedClass b value is:" << m_NestedClass_b << endl;
		}
	private:
		double m_NestedClass_a;
		double m_NestedClass_b;
	};
private:
	int m_a;
	int m_b;
};

int _tmain(int argc, _TCHAR* argv[])
{
	Test test( 7, 14 );
	test.show();

	test.newNestedClass( 10.7, 10.14 );

	Test::NestedClass nestedTest( 10.8, 10.14 );
	nestedTest.show();


	return 0;
}


 

异常
    程序又是会遇到运行阶段错误,导致程序无法正常地运行下去。例如,程序可能试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。通常,程序员都会试图预防这种意外的情况。C++异常为处理这种情况提供了一种功能强大而灵活的工具。

1.调用abort()
   对于处理( X + Y ) / ( X + Y )这类问题,处理方式之一是,如果其中一个参数等于另一个参数的负值,则调用abort()函数。abort()函数的圆形位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息,然后终止程序。他还返回一个随实现而异的值,告诉操作系统,处理失败。abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存的区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

// testException.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

double hmean( const double a, double b );

int _tmain(int argc, _TCHAR* argv[])
{
	double x, y, z;
	cout << "Enter two numbers:";
	while ( cin >> x >> y )
	{
		z = hmean( x, y );
		cout << "harmonic mean of:" << x << " and " << y << " is " << z << endl;
		cout << "Enter next set of numbers <q to quit>:";
	}
	cout << "Done." << endl;

	return 0;
}

double hmean( const double a, double b )
{
	if ( a == -b )
	{
		cout << "untenable arguments to hmean()" << endl;
		abort();
	}
	return 2.0 * a * b / ( a + b );
}

在hmean()中调用abort()函数将直接终止程序,而不是首先返回到main()。一般而言,显示的程序异常中断消息随编译器而异。为便面异常终止,程序应在调用hmean()函数之前检查x和y的值。不过,依靠程序员来执行这种检查是不安全的。

2.返回错误码
    一种比一场终止更灵活的方法是,使用函数的返回值来指出问题。例如,ostream类的get(void)成员通常返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF。对hmean()来说,这种方法不管用。任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。在这种情况下,可使用指针参数或引用参数来将值返货给调用程序,并使用函数的返回值来指出成功还是失败。istream族重载>>操作符使用了这种技术的变体。通过告知调用程序是成功了还是失败了,他将hmean()的返回值重新定义为bool函数,让返回值指出成功了还是失败了,另外还给该函数增加了第三个参数,用于提供答案。

// testException2.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "cfloat"

bool hmean( double a, double b, double* ans );

int _tmain(int argc, _TCHAR* argv[])
{
	double x, y, z;
	cout << "Enter two number:";
	while ( cin >> x >> y )
	{
		if ( hmean( x, y, &z ) )
		{
			cout << "Harmonic mean of:" << x << " and " << y << " is " << z << endl;
		}
		else
		{
			cout << "One value should not be the negative " << "of the other - try again." << endl;
		}
		cout << "Enter next set of numbers <q to quit>:";
	}
	cout << "Done." << endl;

	return 0;
}

bool hmean( double a, double b, double* ans )
{
	if ( a == -b )
	{
		*ans = DBL_MAX;
		return false;
	}
	else
	{
		*ans = 2.0 * a * b / ( a + b );
		return true;
	}
}


3.异常机制
    C++异常是针对程序运行过程中发生的异常情况(例如被0除)的一种相应。异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有3个组成部分:
      a、引发异常
      b、捕获有处理程序的异常
      c、使用try块
    程序在出现问题时将引发异常。例如,可以修改程序testException2.cpp中的hmean(),使之引发异常,而不是调用abort()函数。throw语句实际上是跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。
    程序使用异常处理程序(exceptiong handler)来捕获异常,异常处理程序位于要处理问题的程序中。catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型。然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch快。
    try块标识其中特定的异常可能被激活的代码块,他后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个有花括号括起来的代码块,表明需要注意这些代码引发的异常。

// testException3.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

double hmean( double a, double b );

int _tmain(int argc, _TCHAR* argv[])
{
	double x, y, z;
	cout << "Enter two number:";

	while ( cin >> x >> y )
	{
		try
		{
			z = hmean( x, y );
		}
		catch ( const char* s )
		{
			cout << s << endl;
			cout << "Enter a new pair of numbers:";
			continue;
		}
		cout << "Harmonic mean of:" << x << " and " << y << " is " << z << endl;
		cout << "Enter next set of numbers <q to quit>:";
	}
	cout << "Done." << endl;

	return 0;
}

double hmean( double a, double b )
{
	if ( a == -b )
	{
		throw "bad hmean() arguments:a = -b not allowed";
	}
	return 2.0 * a * b / ( a * b );
}

    这个例子里被引发的异常是字符串"bad hmean() arguments:a = -b not allowed"。异常类型可以是字符串或其他C++类型,通常为类类型。
    执行throw语句类似于执行返回语句,因为它也将终止函数的执行,但throw不是将该控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。
    catch块有点类似于函数定义,但并不是函数定义。关键字catch表明这是一个处理程序,而char* s则表明该处理程序与字符串异常匹配。s与函数参数定义及其类似,因为匹配的引发将被赋给s。另外,当异常与该处理程序匹配时,程序就爱那个执行括号中的代码。
    如果函数引发了异常,而没有try块或没有匹配的处理程序时,在默认情况下,程序最终将调用abort()函数,但可以修改这种行为。

4.将对象用作异常类型
    通常,引发异常的函数将传递一个对象。这样做的主要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch块可以根据这些信息来决定采取什么样的措施。

// testException4.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "math.h"
#pragma warning( disable:4290 )

class CBad_hmean
{
public:
	CBad_hmean( double a = 0, double b = 0 ) : v1( a ), v2 ( b ){}
	void mesg() const;
private:
	double v1;
	double v2;
};

inline void CBad_hmean::mesg() const
{
	cout << "hmean (" << v1 << "," << v2 << "):" << "invalid arguments:a = -b" << endl;
}

class CBad_gmean
{
public:
	CBad_gmean( double a = 0, double b = 0 ) : v1( a ), v2 ( b ){}
	void mesg() const;
public:
	double v1;
	double v2;
};

inline void CBad_gmean::mesg() const
{
	cout << "gmean() arguments should be >= 0" << endl;
}

double hmean( double a, double b ) throw( CBad_hmean );
double gmean( double a, double b ) throw( CBad_gmean );


int _tmain(int argc, _TCHAR* argv[])
{
	double x, y, z;
	cout << "Enter two number:";
	while( cin >> x >> y )
	{
		try
		{
			z = hmean( x, y );
			cout << "Harmonic mean of:" << x << " and " << y << " is " << z << endl;
			cout << "Geometric mean of:" << x << " and " << y << " is " << gmean( x, y) << endl;
		}
		catch ( CBad_hmean& bg )
		{
			bg.mesg();
			cout << "try again" << endl;
			continue;
		}
		catch ( CBad_gmean& hg )
		{
			hg.mesg();
			cout << "Values used:" << hg.v1 << ", " << hg.v2 << endl;
			cout << "Sorry, you don't get to play any more." << endl;
			break;
		}
	}
	cout << "Done." << endl;

	return 0;
}

double hmean( double a, double b ) throw( CBad_hmean )
{
	if ( a == -b )
	{
		throw CBad_hmean( a, b );
	}
	return ( 2.0 * a * b ) / ( a * b );
}

double gmean( double a, double b ) throw( CBad_gmean )
{
	if ( a < 0 || b < 0 )
	{
		throw CBad_gmean( a, b );
	}
	return ( sqrt( a * b ) );
}

首先CBad_hmean异常处理程序使用了一条continue语句,而CBad_gmean异常处理程序使用了一条break语句。因此,如果用户给函数hmean()提供的参数不正确,将导致程序跳过循环中余下的代码,进入下一次循环,而用户给函数gmean()提供的参数不正确时将结束循环。其次,异常类CBad_gmean和CBad_hmean使用的技术不同,具体来说,CBad_gmean使用的是公有数据和一个公有方法,该方法返回一个C风格字符串。

5.堆栈解退
    假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这就会涉及到堆栈解退(unwinding the stack)。
    c++通过通过将信息放在堆栈中来处理函数调用。具体来说,程序将调用函数的指令的地址(返回地址)放到堆栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到堆栈中。在堆栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到堆栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到对战中,依此类推。当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同事堆栈顶端的元素被释放。因此,函数通常都返回到调用它的函数,依此类推,同事每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。
    现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放堆栈中的内存,但不会在释放堆栈的第一个返回地址后停止,而是继续释放堆栈,知道找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为堆栈解退。引发机制的一个非常重要的特性是,和函数返回一样,对于堆栈中的自动类对象,类的析构函数将被调用。不过,函数返回仅仅处理该函数放在堆栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在堆栈中的对象。如果没有堆栈解退这种特性,则引发异常后,对于中间函数调用放在堆栈中的自动类对象,其析构函数将不会被调用。

// testException5.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "math.h"
#pragma warning( disable:4290 )
#pragma warning( disable:4996 )

class CBad_hmean
{
public:
	CBad_hmean( double a = 0, double b = 0 ) : v1( a ), v2 ( b ){}
	void mesg() const;
private:
	double v1;
	double v2;
};

inline void CBad_hmean::mesg() const
{
	cout << "hmean (" << v1 << "," << v2 << "):" << "invalid arguments:a = -b" << endl;
}

class CBad_gmean
{
public:
	CBad_gmean( double a = 0, double b = 0 ) : v1( a ), v2 ( b ){}
	void mesg() const;
public:
	double v1;
	double v2;
};

inline void CBad_gmean::mesg() const
{
	cout << "gmean() arguments should be >= 0" << endl;
}

class demo
{
public:
	demo( const char* str )
	{
		strcpy( word, str );
		cout << "demo " << word << " created" << endl;
	}
	~demo()
	{
		cout << "demo " << word << " destoryed" << endl;
	}
	void show() const
	{
		cout << "demo " << word << " lives ! " << endl;
	}
private:
	char word[ 40 ];
};

double hmean( double a, double b ) throw( CBad_hmean );
double gmean( double a, double b ) throw( CBad_gmean );
double means( double a, double b ) throw( CBad_hmean, CBad_gmean );

int _tmain(int argc, _TCHAR* argv[])
{
	double x, y, z;
	demo d1("found in main()");
	cout << "Enter two numbers:";

	while ( cin >> x >> y )
	{
		try
		{
			z = means( x, y );
			cout << "The mean mean of:" << x << " and " << y << " is " << z << endl;
			cout << "Enter next pair:";
		}
		catch ( CBad_hmean& bg )
		{
			bg.mesg();
			cout << "Try again." << endl;
			continue;
		}
		catch ( CBad_gmean& hg )
		{
			hg.mesg();
			cout << "Values used:" << hg.v1 << ", " << hg.v2 << endl;
			cout << "Sorry, you don't get to play any more." << endl;
			break;
		}
	}
	d1.show();
	cout << "Done." << endl;

	return 0;
}

double hmean( double a, double b ) throw( CBad_hmean )
{
	if ( a == -b )
	{
		throw CBad_hmean( a, b );
	}
	return ( 2.0 * a * b ) / ( a * b );
}

double gmean( double a, double b ) throw( CBad_gmean )
{
	if ( a < 0 || b < 0 )
	{
		throw CBad_gmean( a, b );
	}
	return ( sqrt( a * b ) );
}

double means( double a, double b) throw( CBad_hmean, CBad_gmean )
{
	double am, hm, gm;
	demo d2( "found in means()" );
	am = ( a + b ) / 2.0;
	try
	{
		hm = hmean( a, b );
		gm = gmean( a, b );
	}
	catch ( CBad_hmean& bg )
	{
		bg.mesg();
		cout << "Caught in means()." << endl;
		throw;
	}
	d2.show();
	return ( am + hm + gm ) / 3.0;
}

    在mean()可以引发CBad_hmean和CBad_gmean异常,但只有在means()中显式地引发的CBad_hmean异常被CBad_hmean异常处理程序重新引发。然而,异常规范中不仅要包含函数本身引发的异常,还应包含该函数调用的其他函数引发的异常,依此类推。因此,由于means()调用了gmean(),因此它应宣称自己还可以传递gmean()引发的异常。
    如果省略异常规范中的CBad_gmean&,当gmean()引发这种异常时将是一个意料外的异常。直接传递出来在main中捕获。

6.其他异常特性
    虽然throw-catch机制类似于函数参数和函数返回机制,但还是有些不同之处。其中之一是函数fun()中的返回语句将控制权返回到调用func()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。例如5的程序中,当函数hmeans()引发异常时,控制权将传递给函数means();然而,当gmean()引发异常时,控制权将向上传递到main()。
    另一个不同之处是,引发异常时编译器总创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

catch ( CBad_hmean& bg )
{
	bg.mesg();
	cout << "Try again." << endl;
	continue;
}

bg指向CBad_hmean的拷贝而不是CBad_hmean&本身。这是件好事,因为函数mesg()执行完毕后,CBad_hmean将不复存在。顺便说一句,将引发异常和创建对象组合在一起将更简单:
throw CBad_hmean();
在这里使用引用有一个重要特征:基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,他将与任何派生类对象匹配。
    假设有一个异常类层次结构,并要分别处理不同的异常类型,则使用基类引用将能够捕获任何异常对象,而使用派生对象只能捕获它所属类及从这个类派生而来的类的对象。引发的异常对象将被第一个与之匹配的catch块捕获。这意味着catch块的排列顺序应该与派生顺序相反。
    通过正确地排列catch块的顺序,能够在如何处理异常方面有选择的余地。然而,有时候可能不知道会发生哪些异常。例如,编写了一个调用另一个函数的函数,并不知道被调用你的函数可能引发哪些异常。在这种情况下,仍能够捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常:
catch(...) {}

7.exception类
    c++异常的主要目的是为设计容错程序提供语音级支持,即异常使得在程序设计中包含错误处理功能更容易,以免事后采取一些严格的错误处理方式。异常的灵活性和相对方便性激励着程序员在条件允许的情况下载程序设计中加入错误处理功能。简而言之,异常是这样一种特性:类似于类,可以更变自己的编程方式。
    为支持该语言,exception头文件(以前为exception.h或except.h)定义了exception类,c++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception类用作基类。有一个名为what()的序列成员函数,他返回一个字符串,该字符串的特征随实现而异。然后,由于这是一个虚方法,因此可以在从exception派生而来的类中重新定义它:

// testException6.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "math.h"
#include "exception"
#pragma warning( disable:4290 )

class CBad_hmean : public exception
{
public:
	CBad_hmean( double a = 0, double b = 0 ) : v1( a ), v2 ( b ){}
	const char* what() const; 
private:
	double v1;
	double v2;
};

inline const char* CBad_hmean::what() const
{
	return ("invalid arguments:a = -b");
}

double hmean( double a, double b ) throw( CBad_hmean );

int _tmain(int argc, _TCHAR* argv[])
{	
	double x, y, z;
	cout << "Enter two number:";
	while( cin >> x >> y )
	{
		try
		{
			z = hmean( x, y );
			cout << "Harmonic mean of:" << x << " and " << y << " is " << z << endl;
		}
		catch ( exception& e )
		{
			cout << e.what() << endl;
			cout << "try again" << endl;
			continue;
		}
	}
	cout << "Done." << endl;

	return 0;
}

double hmean( double a, double b ) throw( CBad_hmean )
{
	if ( a == -b )
	{
		throw CBad_hmean( a, b );
	}
	return ( 2.0 * a * b ) / ( a * b );
}

头文件exception提供了bad_exception,供函数unexpected()使用。
a、stdexcept异常类
    头文件stdexcept定义了其他几个异常类。首先,该文件定义了logic_error和runtime_error类,他们都是以公有方式从exception派生而来的:

class logic_error
	: public _XSTD exception
	{	// base of all logic-error exceptions
public:
	explicit __CLR_OR_THIS_CALL logic_error(const string& _Message)
		: _Str(_Message)
		{	// construct from message string
		}

	virtual __CLR_OR_THIS_CALL ~logic_error() _THROW0()
		{	// destroy the object
		}

	virtual const char *__CLR_OR_THIS_CALL what() const _THROW0()
		{	// return pointer to message string
		return (_Str.c_str());
		}

 #if !_HAS_EXCEPTIONS
protected:
	virtual void __CLR_OR_THIS_CALL _Doraise() const
		{	// perform class-specific exception handling
		_RAISE(*this);
		}
 #endif /* _HAS_EXCEPTIONS */

private:
	string _Str;	// the stored message string
	};

		// CLASS domain_error
class domain_error
	: public logic_error
	{	// base of all domain-error exceptions
public:
	explicit __CLR_OR_THIS_CALL domain_error(const string& _Message)
		: logic_error(_Message)
		{	// construct from message string
		}

	virtual __CLR_OR_THIS_CALL ~domain_error() _THROW0()
		{	// destroy the object
		}

 #if !_HAS_EXCEPTIONS
protected:
	virtual void __CLR_OR_THIS_CALL _Doraise() const
		{	// perform class-specific exception handling
		_RAISE(*this);
		}
 #endif /* _HAS_EXCEPTIONS */

	};

这些类的构造函数接受一个string对象作为参数,该参数提供了方法what()以C风格字符串方式返回的字符数据。
    这两个类被用作两个派生系列的基类。异常类系列logic_error描述了典型的逻辑错误。总体而言,通过合理的编程可以避免这种错误,但实际上这些错误还是可能发生的。每个类的名称指出了他用于报告的错误类型:
domain_error
invalid_argument
length_error
out_of_bounds
用到的时候再看吧
b、bad_alloc异常和new
    对于处理使用new时可能出现的内存分配问题,c++提供了两种可供选择的方式。第一种方式(一度是唯一的方式)是,让new在无法满足内存请求时返回一个空指针;第二种方式是,让new引发bad_alloc异常。头文件new(以前名为new.h)中包含了bad_alloc类的声明,他是从exception类公有派生而来的。实现智能提供一种方式,但也可以使用编译器开关或其他一些方法。

8.异常何时会迷失方向
    异常被引发后,在两种情况下,会导致问题。首先,如果他是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则成为意外异常。在默认情况下,这将导致程序异常终止。如果异常不是在函数中引发的(或者函数没有异常规范),则他必须被捕获。如果没有被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被床位未捕获异常。在默认情况下,这将导致程序异常终止。不过,可以修改程序对意外异常和未捕获异常的反应。
    未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。在默认情况下,terminate()调用abort()函数。可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为。为此,可调用set_terminate()函数。set_terminate()和terminate()都是在头文件exception中声明的:

// testException8.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "math.h"
#include "exception"

void myQuit()
{
	cout << "Terminating due to uncaught exception" << endl;
	exit( EXIT_FAILURE );
}

void myExceptio()
{
	double a = 0.0;
	throw a;
};

int _tmain(int argc, _TCHAR* argv[])
{
	set_terminate( myQuit );
	try
	{
		myExceptio();
	}
	catch ( int e )
	{
		cout << "this value is:" << e << endl;
	}
	
	cout << "Done." << endl;

	return 0;
}

    原则上,异常规范应包含函数调用的其他函数引发的异常。但如果函数引发了其异常规范中没有的异常,程序将调用unexpected()函数。这个函数将调用terminate(),后则在默认情况下将调用abort()。正如有一个科用于修改terminate()行为的set_terminate()函数一样,也有一个可用于修改unexpected()的行为的set_unexpected()函数。
    不过,与提供给set_terminate()函数的行为相比,提供给set_unexpected()函数的行为受到更严格的限制。具体地说,unexpected_handler函数可以:
a、通过调用terminate()(默认行为)、abort()或exit()来终止程序。
b、引发异常
    引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范:
      如果新引发的异常与原来的异常规范匹配,则程序从那里开始进行正常处理,即需找与新引发的异常匹配的catch块。基本上,这种方法将用预期的异常取代意外异常。
      如果新引发的异常与原来的异常规范不匹配,且异常规范中没有包括bad_exception类型,则程序将调用terminate()。bad_exception是从exception派生而来的。
      如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了bad_exception类型,则不匹配的异常将被bad_exception异常所取代。
    简而言之,如果要捕获所有的异常(不管是预期的异常还是意外异常),则可以这样做:

// testException9.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "math.h"
#include "exception"

void myUnexcepted()
{
	throw bad_exception();
}

void myException() throw( double )
{
	double a = 0.0;
	throw a;
}

int _tmain(int argc, _TCHAR* argv[])
{
	set_unexpected( myUnexcepted );

	try
	{
		myException();
	}
	catch ( bad_exception* e )
	{
		cout << e->what() << endl;
	}

	return 0;
}

 

RTTI
    RTTI是运行阶段类型识别(Runtime Type Identification)的简称。RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。
1.用途
    假设有一个类层次结构,其中的类都是从同一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。这样便可以调用这样的函数:在处理一些消息后,选择一个类,并创建这种类型的对象,然后返回他的地址,而该地址可以被赋给基类指针。如何知道指针指向的是哪种对象呢?
    在回答这个问题之前,先考虑为何要知道类型。可能希望调用类方法的正确版本,在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,则并不真正需要知道对象的类型。但派生对象可能包含不是继承而来的方法,这种情况下,只有某些类型的对象可以使用该方法。也可能是出于调试的目的,想跟踪生成的对象的类型。对于后两种情况,RTTI提供了解决方案。

2.RTTI的工作原理
    c++有3个支持RTTI的元素:
a、如果可能的话dynamic_cast操作符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该操作符返回0----空指针。

b、typeid操作符返回一个指向对象的类型的值
    typeid操作符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参数:
1)类名
2)结果为对象的表达式
    typeid操作符返回一个对type_info对象的引用,其中type_info是在头文件typeinfo中定义的一个类。type_info类冲在了==和!=操作符,以便可以使用这些操作符来对类型进行比较。例如,如果pg指向的是一个Magnificent对象,则表达式
typeid( Magnificent ) == typeid( *pg )
的结果将为bool值true,否则为false。如果pg是一个空指针,程序将引发bad_typeid异常。

// testrtti.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "ctime"

class Grand
{
public:
	Grand( int h = 0 ) : hold( h ) { }
	virtual void Speak() const
	{
		cout << "I am a grand class!" << endl;
	}
	virtual int Value() const
	{
		return hold;
	}
private:
	int hold;
};

class Superb : public Grand
{
public:
	Superb( int  h = 0 ) : Grand( h ){ }
	void Speak() const
	{
		cout << "I am a superb class!!" << endl;
	}
	virtual void Say() const
	{
		cout << "I hold the superb value of " << Value() << "!" << endl;
	}
};

class Magnificent : public Superb
{
public:
	Magnificent( int h = 0, char cv = 'A' ) : ch( cv ), Superb( h ){ }
	void Speak() const
	{
		cout << "I am a magnificent class!!" << endl;
	}
	virtual void Say() const
	{
		cout << "I hold the character:" << ch << " and the integer" << Value() << "!" << endl;
	}
private:
	char ch;
};

Grand* GetOne();

int _tmain(int argc, _TCHAR* argv[])
{
	srand( time( 0 ) );
	Grand* pg;
	Superb* ps;
	for ( int i = 0; i < 5; i++ )
	{
		pg = GetOne();
		cout << "Now processing type " << typeid( *pg ).name() << "." << endl;
		pg->Speak();
		if ( ps = dynamic_cast<Superb *>( pg ) )
		{
			ps->Say();
		}
		if ( typeid( Magnificent ) == typeid( *pg ) )
		{
			cout << "Yes, your are really magnificent." << endl;
		}
	}
	return 0;
}

Grand* GetOne()
{
	Grand* p;
	switch ( rand() % 3 )
	{
	case 0:
		p = new Grand( rand() % 100 ); break;
	case 1:
		p = new Superb( rand() % 100 ); break;
	case 2:
		p = new Magnificent( rand() % 100, 'A' + rand() % 26 ); break;
	}
	return p;
}

c、type_info结构存储了有关特定类型的信息

只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。

 

类型转换操作符
1.dynamic_cast
该操作符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换

2.const_cast
const_cast操作符用于执行只有一种用途的类型转换,即改变值为const或volatile,其句法与dynamic_cast操作符相同
如果类型的其他方面也被修改,则上述类型转换将出错。也就是说,除了const或volatile特征(有或无)可以不同外,type_name和expression的类型必须相同。假设High和Low是两个类:
High bar;
const High* pbar = &bar;
...
High* pb = const_cast<Hight* >(pbar);
const Low* pl = const_cast<const Low* >(pbar);
第一个类型转换使得*pb成为一个可用于修改bar对象值的指针,它删除const标签。第二个类型转换是非法的,因为它同时尝试将类型从const Hight*为const Low*。

提供该操作符的原因是,有时候可能需要这样一个值,他在大多是时候是常量,而有时又是可以修改的。在这种亲看下,可以将这个值声明为const,并在需要修改它的时候,使用const_cast。这也可以通过通用类型转换来实现,但通用转换也可能同时改变类型。

// testconstcast.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

void change( const int* pt, int n )
{
	int* pc;
	if ( n < 0 )
	{
		pc = const_cast<int *>( pt );
		*pc = 100;
	}
}

int _tmain(int argc, _TCHAR* argv[])
{
	int popl = 38383;
	const int pop2 = 2000;

	cout << "pop1, pop2:" << popl << ", " << pop2 << endl;
	change( &popl, -1 );
	change( &pop2, -1 );

	cout << "pop1, pop2:" << popl << ", " << pop2 << endl;

	return 0;
}

在这个程序中,调用change()时,修改了pop1,但没有修改pop2(这里使用的是编译器生成pop2的一个临时拷贝,并将地址赋给pc,但正如前面指出的,c++标准指出,在这种情况下,上述行为是不确定的)

3.static_cast
static_cast操作符的句法与其他类型转换操作符相同,static_cast< type_name >( expression )仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的,否则将出错。假设High是Low的基类,而Pond是一个无关的类,则从High到Low的转换、从Low到High的转换都是合法的,而从Low到Pond的转换是不允许的:
High bar;
Low blow;
...
High* pb = static_cast<High *>( &blow );
Low* p1 = static_cast<Low *>( &bar );
Pond* pmer = static_cast<Pond *>( &blow );
第一种转换是合法的,因为向上转换可以显示地进行。第二种转换是从基类指针到派生类指针,在不进行显示类型转换的情况下,将无法进行。但由于无需进行类型转换,便可以进行另一个方向的类型转换,因此使用static_cast来进行向下转换是合法的。

4.reinterpret_cast
reinterpret_cast操作符用于天生危险的类型转换,他不允许删除const,但会后自行其他令人生厌的操作。有时程序员必须做一些依赖于实现的、令人生厌的操作,使用reinterpret_cast操作符可以简化对这种行为的跟踪工作。
struct dat { short a; short b };
long value = 0xA224B118;
dat* pd = reinterpret_cast< dat* >( &value );
cout << pd->a;
    通常,这样的转换适用于依赖实现的底层编程技术,是不可抑制的。然而reinterpret_cast操作符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针类型表示的转型,但不能将指针转换为更小的整型或浮点型。另一个限制是,不能将函数指针转换为数据指针,反之亦然。
    在c++中,普通类型转换也收到限制。基本上,可以执行其他类型转换可执行的操作,加上一些组合,如static_cast或reinterpret_cast后跟const_cast,但不能执行其他转换。因此:
char ch = char ( &d );
    在c语言中式允许的,但在c++中通常不允许,因为对于大多数c++实现,char类型都太小,不能存储指针。

 

string对象
1.string版本的getline()函数从输入中读取字符,并将其存储到模板string中,知道发生下列3中情况之一:
a、达到文件尾,在这种情况下,输入流的eofbit将被设置,这意味着方法fail()和eof()都将会发true
b、遇到分界字符(默认为\n),在这种情况下,将分界字符从输入流中删除,单步存储他
c、读取的字符数达到最大允许值(string::npos和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的failbit,这意味着方法fail()将返回true

    string版本的operator>>()函数的行为与此类似,只是它不断读取,知道遇到空白字符将其留在输入对了中,而不是不断读取,直到遇到分解字符并将其丢弃。空白字符指的是空格、换行符和制表符,更普遍的说,是任何将其作为参数来调用isspace()时,该函数返回true的字符

// teststring.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "string"
#include "fstream"


int _tmain(int argc, _TCHAR* argv[])
{
	ifstream fin;
	fin.open( "tobuy.txt" );
	if ( false == fin.is_open() )
	{
		cerr << "Can't open file . Done." << endl;
		exit( EXIT_FAILURE );
	}
	string item;
	int count = 0;

	getline( fin, item, ':' );
	while ( fin )
	{
		++count;
		cout << count << ":" << item << endl;
		getline( fin, item, ':');
	}
	cout << "Done." << endl;
	fin.close();

	return 0;
}

这是一个从文件中读取字符串的简短范例,他假设文件中包含冒号字符分隔的字符串,并使用指定分界符的getline()方法。然后,显示字符串并给他们编号,每个字符串占一行。
这里值得注意的是,指定为分界字符后,换行符将被视为常规字符。

2.使用字符串
    string除了可以获取长度和比较之外,还提供了相关的方法:rfind()、find_first_of()等,他们的重载函数特征标都与find()方法相同。rfind()方法查找子字符串或字符最后一次出现的位置。

3.length()与size()
    这两个函数完成相同的任务,length()成员来自较早版本的string类,而size()则是为提供STL兼容性而添加的

4.string的自动调整大小功能
    下面的程序中,将一个字母附加到字符串末尾时将发生什么呢?不能仅仅将已有的字符串加大,因为相邻的内存可能被占用了。因此,可能需要分配一个新的内存块,并将原来的内容复制到新的内存单元中。如果执行大量这样的操作,效率将非常低,因此很多c++实现分配一个比实际字符串大的内存块,为字符串提供了增大空间。然后,如果字符串不断增大,超过了内存块的大小,程序将分配一个大小为原来两倍的新内存块,以提供足够的增大空间,避免不断地分配新的内存块。方法capacity()返回当前分配给字符串的内存块的大小,而reserve()方法能够请求内存块的最小长度

// teststring2.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "string"

int _tmain(int argc, _TCHAR* argv[])
{
	string szEmpty;
	string szSmall = "bit";
	string szLarger = "Elephants are a girl's best friend";
	cout << "Size:" << endl;
	cout << "\tempty:" << szEmpty.size() << endl;
	cout << "\tsmall:" << szSmall.size() << endl;
	cout << "\tlarger:" << szLarger.size() << endl;

	cout << "Capacities:" << endl;
	cout << "\tempty:" << szEmpty.capacity() << endl;
	cout << "\tsmall:" << szSmall.capacity() << endl;
	cout << "\tlarger:" << szLarger.capacity() << endl;

	szEmpty.reserve( 50 );
	cout << "Capacities after empty.reserve( 50 ):" << szEmpty.capacity() << endl;

	return 0;
}

5.string转到C风格字符串
    如果现在有string对象,但需要C风格字符串,比如,可能想打开一个其名称存储在string对象中的文库:
string fileName;
cout << "Enter file name:";
cin >> fileName;
ofstream fout;
不幸的是,open()方法要求使用一个C风格字符串作为参数;幸运的是,c_str()方法返回一个指向C风格字符串的指针,该C风格字符串的内容与用于调用c_str()方的string对象相同。因此可以这样做:
fout.open( fileName.c_str() );

6.重载C函数以使用string对象
    可以使用重载的==操作符来比较string对象。不过,在某些情况下,==操作符执行相等比较时的区分大小写特征会带来问题。通常,比较两个字符串是否相等时不区分大小写。例如,程序可能将用户的输入与常数值进行比较,而用户输入时的大小写可能与常量不完全相同。可以这么做,很多C库都提供了stricmp()或_stricmp()函数,能够执行不区分大小写的比较(不过,该函数不属于C标准,因此不一定普通适用)。通过创建该函数的重载版本,可以避免上述问题

// teststring3.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "string"

inline bool stricmp( const string& strA, const string& strB )
{
	return stricmp( strA.c_str(), strB.c_str() ) == 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	string strA;
	cin >> strA;
	string strB = "apple";

	cout << stricmp( strA, strB ) << endl;
	cout << "Done." << endl;

	return 0;
}


 

auto_ptr类
    auto_ptr是一个模板类,用于管理动态内存分配的用法。常规指针在函数结束或者程序结束后,并不能自动释放所占用的内存,如果他是对象,则可以在对象过期时,让他的析构函数删除被指向的内存。这正是auto_ptr背后的思想

1.使用auto_ptr
    auto_ptr模板定义了类似指针的对象,可以就爱那个new获得(直接或间接)的地址赋给这种对象。当auto_ptr对象过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给auto_ptr对象后,无须记住稍后释放这些内存。在auto_ptr对象过期时,这些内存将自动被释放。
    要创建auto_ptr对象,必须包含头文件memory,该文件包括auto_ptr模板。然后使用通常的模板句法来实例化所需类型的指针。
    auto_ptr是一种智能指针(samrt pointer)----类似于指针,但特性比指针更多。auto_ptr类被定义为在很多方面与常规指针类似。例如,如果ps是一个auto_ptr,则可以对他执行解除引用操作( *ps )和递增操作( ++ps ),用它来访问结构成员( ps->puffIndex ),将它赋给指向相同类型的常规指针。还可以将auto_ptr赋给另一个同类型的auto_ptr,但将引起一个问题,请看2。

2.有关auto_ptr的注意事项
auto_ptr并不是万灵丹。例如,下面的代码:
auto_ptr<int> pi( new int [200]);
    对于new和new[],必须相应地使用delete和delete[]。auto_ptr模板使用的是delete,而不是delete[],因此它只能与new一起使用,而不能与new[]一起使用。没有适用于动态数组的auto_ptr等同位语。可以复制头文件memory中的auto_ptr模板,将它重命名为auto_arr_ptr,然后对其进行修改,使之使用delete[],而不是delete。在这种情况下,可能希望添加对[]操作符的支持。

3.有关auto_ptr赋值的情况
auto_ptr<string> ps( new string("I reigned lonely as a cloud.") );
auto_ptr<string> vocation;
vocation = ps;
上面的代码,如果ps和vocation是常规指针,则两个指针将指向同一个string对象,其中的一个是另一个的拷贝。这是不可接受的,因为ps和vocation都过期时,程序将试图删除同一个对象两次。要避免这种问题,方法有很多种:
a、定义赋值操作符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的拷贝。
b、建立所有权概念,对于特定的对象,只能有一个智能指针可拥有它。智能指针的构造函数只能删除该智能指针拥有的对象。并使复制操作转让所有权。这就是用于auto_ptr的策略。
c、创建智能更高的指针,跟踪引用特定对象的智能指针数。这被称为引用计数。例如,赋值时,计数将+1;指针过期时,计数将-1.仅当最后一个指针过期时,delete才被调用。
当然,同样的策略也适用于赋值构造函数

 

智能指针
    c++库中auto_ptr对象是一种只能指针。智能指针是这样一种类,即其对象的特征类似于指针。例如,只能指针可以存储new分配的内存地址,也可以被解除引用。引用智能指针是一个类对象,因此他可以修改和扩充简单指针的行为。例如,智能指针可以建立引用计数,这样多个对象可共享由智能指针跟踪的同一值。当使用该值的对象数为0时,智能指针将删除这个值。智能指针可以提供内存的使用效率,帮助防止内存泄露,但并不要求用户熟悉新的编程技术。

 

函数对象
    很多STL算法都使用函数对象----也叫函数符(functor)。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()操作符的类对象(即定义了函数operator()()的类)。
    在STL中的for_each( x.beging(), x.end(), ShowReview )函数的第三个参数是常规函数,也可以使函数符。实际上,这提出了一个问题:如何声明第三个参数呢?不能把它声明为函数指针。STL通过使用模板解决了这个问题。for_each()的圆形看上去就像这样:
template< class InputIterator, class Function >
Function for_each( InputIterator first, InputIterator last, Function f );
ShowReview()的原型如下:
vodi ShowReview( const Review7 )
这样标识符ShowReview的类型键位void( * )( const Review& ),这也是赋给模板承诺书Function的类型。对于不同的函数调用,Function参数可以表示具有重载的()操作符的类类型。最终for_each()代码将具有一个使用f(...)的表达式。

// testforeach.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "string"
#include "vector"
#include "algorithm"

void show( const int a )
{
	cout << "current value is:" << a << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	vector<int> test;
	for ( int i = 0; i < 10; i++ )
	{
		test.push_back( i );
	}
	for_each( test.begin(), test.end(), show );

	return 0;
}


 

流、缓冲区和iostream文件
    管理流和缓冲区的工作有点复杂,不过iostream文件中包含了一些专门设计用来实现、管理流和缓冲区的类。最新版本的c++ i/o定义了一些类模板,以支持char和wchar_t数据。通过使用typedef攻击,c++使得这些模板char具体化能够模仿传统的非模板i/o实现。下面是其中的一些类
a、streambuf类为缓冲区提供了内存,并提供了用于填充缓存区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法
b、ios_base类表示流的一般特征,如是否可读取、是二进制流还是文本流等
c、ios类基于ios_base,其中包括了一个指向streambuf对象的指针成员
d、ostream类是从ios类派生而来的,提供了输出方法
e、isteram类是从ios类派生而来的,提供了输入方法
f、iostream类是基于isteram和ostream类的,因此继承了输入方法和输出方法
要使用这些工具,必须使用适当的类对象。例如,使用ostream对象(如cout)来处理输出。创建这样的对象将打开一个流,自动创建缓冲区,并将其与流关联起来,同时使得能够使用类成员函数。

    c++的iostream类库管理了很多细节。例如,在程序中包含iostream文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流):
a、cin对象对应于标准输入流。在默认情况下,这个流被关联到标准输入设备(通常为键盘)。wcin对象与此类似,但处理的是wchar_t类型
b、cout对象与标准输出流相对应。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。wcout
c、cerr对象与标准错误流相对应,可用于显示错误信息。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流没有被缓存,这意味着信息将被直接发送给屏幕,而不会等到缓存区填满或新的换行符。wcerr
d、clog对象也对应着标准错误流。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流被缓存。wclog

 

使用cout进行输出
    要在屏幕上显示数字-2.34,需要将5个字符(-、2、.、3和4),而不是这个值的64位内部浮点表示发送到屏幕上。因此,ostream类最重要的任务之一是将数值类型(如int或float)转换为以文本形式表示的字符流。也就是说,ostream类将数据内部表示(二进制位模式)转换为由字符字节组成的输出流。为执行这些转换任务,ostream类提供了多个类方法。

1.其他ostream方法----put()
    除了各种operator<<()函数外,ostream类还提供了put()方法和write()方法, 前者用于显示字符,后者用于显示字符串。
    最初,put()方法的原型为:
ostream& put( char );
    当前标准与此相同,但被模板化,以适用于wchar_t。可以用类方法表示法来调用它:
cout.put('A');
其中,cout是调用方法的对象,put()是类成员函数。和<<操作符函数一样,该函数也返回一个指向调用对象的引用,因此可以用它将拼接除数:
cout.put('t').puit('a');
函数调用cout.put('t')返回cout,cout然后被用作puit('a')调用的调用对象

// testforeach.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{

	cout << 'w' << endl;
	cout.put( 'A' ) << endl;
	cout.put( 65 ) << endl;
	cout.put( 97 ) << endl;

	return 0;
}


2.write()
    write()方法显示整个字符串,其模板原型:
basic_ostream< charT, traits >& write( const char_type* s, streamsize n );
write()的第一个参数提供了要显示的字符串的地址,第二个参数指出要显示多少个字符。使用cout调用write()时,将调用char具体化,因此返回类型为ostream&。

// testforeach.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
	const char* state = "zeng2";
	int len = strlen( state );
	cout << "Increasing loop index:" << endl;
	int i;
	for ( i = 0; i <= len; i++ )
	{
		cout.write( state, i );
		cout << endl;
	}
	cout << "Decreasing loop index:" << endl;
	for ( i = len; i > 0; i-- )
	{
		cout.write( state, i ) << endl;
	}
	cout << "Exceeding string length:" << endl;
	cout.write( state, len + 5 ) << endl;

	return 0;
}

write()方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符,即使超过了字符串的边界!

3.修改显示时使用的进制系统
    ostream类是从ios类派生而来的,而后者是从ios_base类派生而来的。ios_base类存储了描述格式状态的信息。例如,一个类成员中某些位决定了使用的计数系统,而另一个成员决定了字段宽度。通过使用控制符(manipulator),可以控制显示整数时使用的计数系统。通过使用ios_base的成员函数,可以控制字段宽度和小数位数。由于ios_base类是ostream的间接基类,因此可以将其方法用于ostream对象,如cout
    例如hex(cout);将cout对象的计数系统格式状态设置为十六进制。完成上述设置后,程序将以十六进制形式打印整数值,知道将格式状态设置为其他选项为止。
    虽然控制符实际上市函数,但他们通常的使用方式为:
cout << hex;

// testcout.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
	cout << "Enter an integer:";
	int n;
	cin >> n;

	cout << "n    n * n" << endl;
	cout << n << "    " << n * n << "( decimal )" << endl;

	// set to hex mode
	cout << hex;
	cout << n << "    ";
	cout << n * n << "( hexadecimal )" << endl;

	// set to octal mode
	cout << oct << n << "    " << n * n  << "( octal )" << endl;

	// alternative way to call a manipulator
	dec( cout );
	cout << n << "    " << n * n << "( decimal )" << endl;

	return 0;
}


4.调整字段宽度
    int width();
    int width( int i );
第一种格式返回字段宽度的当前设置;第二种格式将字段宽度设置为i个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后恢复宽度值时使用。
    width()方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。例如:
    cout << '#';
    cout.width( 12 );
    cout << 12 << "#" << 24 << "#" << endl;
    由于width()是成员函数,因此必须使用对象(这里为cout)来调用它。

// testcout2.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
	int w = cout.width( 30 );
	cout << "default field width = " << w << ":" << endl;
	cout.width( 5 );
	cout << "N" << ':';
	cout.width( 8 );
	cout << "N * N" << ':';

	for ( long i = 1; i <= 100; i *= 10 )
	{
		cout.width( 5 );
		cout << i << ':';
		cout.width( 8 );
		cout << "i * i" << ':' << endl;
	}

	return 0;
}

 

5.填充字符
    在默认情况下,cout用空格填充字段中未被使用的部分,可以用fill()成员函数来改变填充字符。例如:cout.fill( '*' )

// testcout3.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
	cout.fill( '*' );
	const char* staff[ 2 ] = { "Waldo Whipsnade", "Wilmarie Wooper" };
	long bonus[ 2 ] = { 900, 1350 };

	for ( int i = 0; i < 2; i++ )
	{
		cout << staff[ i ] << ":$";
		cout.width( 7 );
		cout << bonus[ i ] << endl;
	}

	return 0;
}


6.设置浮点数的显示精度
    浮点数精度的含义取决于输出模式。在默认模式下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。precision()成员函数使得能够选择其他值。例如:
cout.precision( 2 );
将cout的精度设置为2.和width()的情况不同,但与fill()类似,新的精度设置将一直有效,知道被创新设置

// testcout4.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
	float pricel = 20.40;
	float pricel2 = 1.9 + 8.0 / 9.0;
	cout << "\Furry Friends\" is $" << pricel << "!" << endl;
	cout << "\Fiery Friends\" is $" << pricel2 << "!" << endl;

	cout.precision( 2 );
	cout << "\Furry Friends\" is $" << pricel << "!" << endl;
	cout << "\Fiery Friends\" is $" << pricel2 << "!" << endl;
	
	return 0;
}


7.打印末尾的0和小数点
    对于有些输出(如价格或栏中的数字),保留末尾的0将更加美观。iostream系列类没有提供专门用于完成这项任务的函数,但ios_base类提供了一个setf()函数(用于set标记),能够控制多种格式化特征。这个类还定义了多个常量,可用作该函数的参数。例如,下面的调用:
cout.setf( ios_base::showpoint );
    showpoint仅仅是ios_base类中定义的一个常量,如果使用默认精度(6位)时,cout不会将2.00显示为2,而是将它显示为2.000000,但是结合6中的precision()成员函数设定浮点数的显示精度,就可以更改这个默认精度

// testcout5.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
	double a = 2.0f;
	cout << "this double variable:" << a << endl;

	cout.precision( 3 );
	cout.setf( ios_base::showpoint );
	cout << "this double variable:" << a << endl;

	return 0;
}

 

8.在谈setf()
    setf()方法控制了小数点被显示时其他几个格式选项。ios_base类有一个受保护的数据成员,其中的各位(这里叫做标记)分别控制着格式化的各个方面,例如计数系统、是否显示末尾的0等。打开一个标记成为设置标记(或位),并意味着相应的位被设置为1.位标记是编程开关,相当于设置DIP开关以配置计算机硬件。例如,hex、dec和oct控制符调整控制计数系统的3个标记位。
ios_base::boolalpha----输入和输出bool值,可以为true或false
ios_base::showbase----对于输出,使用c++基数前缀(0,0x)
ios_base::showpoint----显示末尾的小数点
ios_base::uppercase----对于16进制输出,使用大写字母,E表示法
ios_base::showpos----在正数前面加上+
    当做了这些修改之后,直到被覆盖为止。

9.标准控制符
    使用setf()不是进行格式化的、对用户最为友好的方法,c++提供了多个控制符,能够调用setf(),并自动提供正确的参数。比如前面的dec、hex和oct,这些控制符的工作方式都与hex相似,例如:
cou << left << fixed;
打开左对齐和定点选项

10.iomanip头文件
    使用iostream工具来设置一些格式值(如字段宽度)不太方便。为简化工作,c++在iomanip头文件中提供了其他一些控制符,它们能够提供前面的服务,但表示起来更方便。3个最常用的龙支付分别是setprecision()、setfill()和setw(),它们分别用来设置精度、填充字符和字段宽度。与前面讨论的控制符不同的是,这3个控制符带参数。setprecision()控制符接受一个指定精度的整数参数;setfill()控制符接受一个指定填充字符的char参数;setw()控制符接受一个指定字段宽度的整数参数。由于它们都是控制符,因此可以用cout语句连接起来。这样,setw()控制符在显示多列值时尤其方便。

// testcout6.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "iomanip"
#include "math.h"

int _tmain(int argc, _TCHAR* argv[])
{
	// use new standard manipulators
	cout << showpoint << fixed << right;

	// use iomanip mainpulators
	cout << setw( 6 ) << "N" << setw( 14 ) << "square root" << setw( 15 ) << "fourth root" << endl;

	double root;
	for ( int i = 0; i <= 100; i++ )
	{
		root = sqrt( double( i ) );
		cout << setw( 6 ) << setfill( '.' ) << i << setfill( ' ' )
			 << setw( 12 ) << setprecision( 3 ) << root 
			 << setw( 14 ) << setprecision( 4 ) << sqrt( root ) << endl;
	}

	return 0;
}

该程序生成的是几乎完全对齐的列。

 

cin>>如何检查输入
    不同版本的抽取操作符查看输入流的方法是相同的。他们跳过空白(空格、换行符和制表符),直到遇到非空白字符。即使对于单字符模式(参数类型为char、unsigned char或signed char),情况也是如此,但对于c语言的字符输入函数,情况并非如此。在单字符模式下,>>操作符将读取该字符,将它放置到指定的位置。在其他模式下,>>操作符将读取一个指定类型的数据。也就是说,他读取从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容。

// testcin1.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
	cout << "Enter number:";

	int sum = 0;
	int input;
	while ( cin >> input )
	{
		sum += input;
	}

	cout << "Last value entered = " << input << endl;
	cout << "Sum = " << sum << endl;

	return 0;
}

输入有时可能没有满足程序的期望。例如,假设输入的是Zcar,那么cin将不会获取输入的值

如果输入的是
Enter numbers:200 10 -50 -123Z 60
由于输入被缓冲。因此通过键盘输入的第二行在用户按下回车键之前,不会被发送给程序。但是,循环在字符Z处停止了对输入的处理,因此他不与任何一种浮点格式匹配。输入与预期格式不匹配发过来将导致表达式cin >> input的结果为false,因此while循环被终止

 

流状态
1.设置流状态
clear()和setstate()很相似。他们都重置状态,但采取的方式不同。clear()方法将状态设置为他的参数。
clear()
将使用默认参数0,这将清除全部3个状态位( eofbit、badbit和failbit ),同样,下面的调用:
clear( eofbit );
将状态设置为eofbit;也就是说eofbit将被设置,另外两个状态为被清除。
而setstate()方法只影响其参数中已设置的位。因此下面的调用:
setstate( eofbit );
将设置eofbit,而不会影响其他为。因此,如果failbit被设置,则仍将被设置

2.I/O异常
    假设某个输入函数设置了eofbit,在默认情况下,不会导致异常被引发。但可以使用exceptions()方法来控制异常如何被处理。
    exceptions()方法返回一个位字段,他包含3位,分别对应于eofbit、badbit和failbit。修改流状态涉及到clear()或setstate(),这都将使用clear()。修改流状态后,clear()方法将当前的流状态与exceptions()返回的值进行比较。如果在返回值中某一位被设置,而当前状态中的对应为也被设置,则clear()将引发ios_base::failure异常。如果两个值都设置了badbit,将发生这种情况。如果exceptions()返回goodbit,则不会引发任何异常。ios_base::failure异常是从exception类派生而来的,因此包含一个what()方法

// testcin2.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "exception"

int _tmain(int argc, _TCHAR* argv[])
{
	// have failbit cause an exception to be throw
	cin.exceptions( ios_base::failbit );
	cout << "Enter numbers:";
	int sum = 0;
	int input;
	try
	{
		while ( cin >> input )
		{
			sum += input;
		}
	}
	catch ( ios_base::failure* bf )
	{
		cout << bf->what() << endl;
		cout << "O! the horror! " << endl;
	}
	
	cout << "Last value entered = " << input << endl;
	cout << "Sum = " << sum << endl;

	return 0;
}


 

其他的istream类方法
1.单字符输入
    在使用char参数或没有参数的情况下,get()方法读取下一个输入字符,即使该字符是空格、制表符或换行符。get( char& ch )版本将输入字符赋给其参数,而get( void )版本将输入字符转换为整型(通常是int),并将其返回。
    get( void )一般用在文本读取中比如:while( ( ch = cin.get() ) != EOF ){}
    get( char& ch )一般用在输入中
2.采用哪种单字符输入形式
    假设可以选择>>、get( char& ch )或get( void ),应使用哪一个呢?首先,应确定是否希望跳过空白。如果跳过空白更方便,则使用抽取操作符>>。例如,提供菜单选项时,跳过空白更为方便:
    如果希望程序检查每个字符,请使用get()方法,例如,计算字数的程序可以使用空格来判断单纯合适结束。在get()方法中,get( char& ch )的接口更加。
    get( void )的主要优点是:他与标准c语言中的getchar()函数极其类似,这意味着可以用过包含iostream(而不是stdil.h),并用cin.get()替换所有的getchar(),用cout.put( ch )替换所有的putchar( ch ),来讲c程序转换为c++程序

3.字符串输入:getline()、get()和ignore()
    getline()成员函数和get()的字符串读取版本都读取字符串,他们的函数特征相同:
istream& get( char*, int, char );
istream& get( char*, int );
istream& getline( char*, int, char );
istream& getline( char*, int );
第一个参数是用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大1(额外的一个字符用于存储结尾的空字符,以便将输入存储为一个字符串)。第三个参数指定用作分界符的字符,只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。例如:
char line[ 50 ];
cin.get( line, 50 );
将字符输入读取到字符数组line中。cin.get()函数将在到底第49个字符或遇到换行符(默认情况)后停止将输入读取到数组中。get()和geiline()之间的主要区别在于,get()将换行符留在输入流中,这样接下来的输入操作首先看到的将是换行符,而getline()抽取并丢弃输入流中的换行符

// testget.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

const int Limit = 255;

int _tmain(int argc, _TCHAR* argv[])
{
	char input[ Limit ];

	cout << "Enter a string for getline() processing:" << endl;
	cin.getline( input, Limit, '#' );
	cout << "Here is your input:" << endl;
	cout << input << "\nDone with phase 1" << endl;

	char ch;
	cin.get( ch );
	cout << "The next input character is:" << ch << endl;

	if ( '\n' != ch )
	{
		cin.ignore( Limit, '\n' );
	}

	cout << "Enter a string for get() processing:" << endl;
	cin.get( input, Limit, '#' );
	cout << "Here is your input:" << endl;
	cout << input << "\nDone with phase 1" << endl;

	cin.get( ch );
	cout << "The next input character is:" << ch << endl;


	return 0;
}

get()和getline()之间的主要区别在于,get()将换行符留在输入流中,这样接下来的输入操作首先看到的是换行符,而getline()抽取并丢弃输入流中的换行符

4.read()、peek()、gcount()和putback()
read()函数读取指定数目的字符,并将他们存储在指定的位置中
char gross[ 144 ];
cin.read( gross, 144 );
从标准输入中读取144个字符,并将他们存储在gross数组中。与getline()和get()不同的是,read()不会在输入后加上空值字符,因此不能就爱那个输入转换为字符串。read()方法不是专为键盘输入设计的,它最常与ostream write()函数结合使用,来完成文件输入和输出。

peek()函数返回输入中的下一个字符,但不抽取输入流中的字符。也就是说,它使得能够查看下一个字符。假设要读取输入,直到遇到换行符或据点,则可以用peek()查看输入流中的下一个字符,以此来判断是否继续读取:
char great_input[ 80 ];
char ch;
int i = 0;
while( ( ch = cin.peek() != '.' && ch != '\n' )
    cin.get( great_input[ i++ ] );
great_input[ i ] = '\0';

 

文件输入和输出

1.简单的文件I/O
    c++ I/O类软件包处理文件输入和输出的方式与处理标准输入和输出的方式非常相似。要写入文件,需要创建一个ofstream对象,并使用ostream方法,如<<插入操作符或write()。要读取文件,需要创建一个ifstream对象,并使用istream方法,如>>抽取操作符或get()。不过,与标准输入和输出相比,文件的管理更为浮躁。例如,必须将新打开的文件和流关联起来。可以以只读模式、只写模式或读写模式打开文件。写文件时,可能想创建新文件、取代就文件或添加到旧文件中,还可能想在文件中来回移动。为帮助处理这些任务,c++在头文件fstream中定义了多个新类,其中包括用于文件输入的ifstream类和用于文件输出的ofstream类。c++还定义了一个fstream类,用于同步文件I/O,这些类都是从头文件iostream中的类派生而来的。

注意:以默认模式打开文件进行输出将自动把文件的长度截短为0,这相当于删除已有的内容

// testfileoperator.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "fstream"
#include "string"


int _tmain(int argc, _TCHAR* argv[])
{
	string fileName;

	
	cout << "Enter name for new file:";
	cin >> fileName;

	// create output stream object for new file call it fout
	ofstream fout( fileName.c_str() );

	fout << "For you eyes only! " << endl;
	cout << "Enter your secret number:";
	float secret;
	cin >> secret;
	fout << "Your secret number is:" << secret << endl;
	fout.close();

	// create input stream object for new file and call it fin
	ifstream fin( fileName.c_str() );
	cout << "\nHere are the contents of:" << fileName << endl;
	char ch;
	while ( fin.get( ch ) )
	{
		cout << ch;
	}
	cout << "Done." << endl;
	fin.close();

	return 0;
}

 

2.流状态检查和is_open()
    在试图打开一个不存在的文件进行输入时,将设置failbit位。但是c++提供了一种更好的检查文件是否被打开的方法----is_open()方法:
if( !fin.is_open() )
{...}

3.打开多个文件
    程序可能需要打开多个文件。打开多个文件的策略取决于他们将被如何使用。如果需要同时打开两个文件,则必须为每个文件创建一个流。例如,将两个排序后的文件拼接成第三个文件的程序,需要为两个输入文件创建两个ifstream对象,并为输出文件创建一个ofstream对象。可以同门师打开的文件数取决于操作系统,但通常为20个左右。

4.文件模式
    文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用open()方法),都可以提供指定文件模式的第二个参数:
ifstream fin( "test.dat", model1 );
ofstream fout();
fout.open( "test.dat", model2 );
    ios_base类定义了一个openmode类型,用于表示模式;与fmtflags和iostate类型一样,他也是一种bitmask类型。可以选择ios_base类中定义的多个变量来指定模式。
ios_base::in----打开文件,以便读取
ios_base::out----打开文件,以便写入
ios_base::ate----打开文件,并移到文件尾
ios_base::app----追加到文件尾
ios_base::trun----如果文件存在,则截短文件
ios_base::binary----二进制文件

// testfileoperator1.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
#include "fstream"
#include "string"

const char* fileName = "test.dat";

int _tmain(int argc, _TCHAR* argv[])
{
	char ch;

	// show initial contens
	ifstream fin;
	fin.open( fileName );

	if ( fin.is_open() )
	{
		cout << "Here are the current contents of the " << fileName << endl;
		while ( fin.get(ch) )
		{
			cout << ch;
		}
		fin.close();
	}
	cout << "In the first fin.is_open()" << endl;

	// add new names
	ofstream fout( fileName, ios::out | ios::app );
	if ( !fout.is_open() )
	{
		cerr << "Can't open " << fileName << "file for output." << endl;
		exit( EXIT_FAILURE );
	}

	cout << "Enter guest names(enter a blank line to quit:)" << endl;
	string name;
	while ( getline( cin, name ) && name.size() > 0 )
	{
		fout << name << endl;
	}
	fout.close();
	
	// show revesed file
	fin.clear();
	fin.open( fileName );
	if ( fin.is_open() )
	{
		cout << "Here are the current contents of the " << fileName << endl;
		while ( fin.get(ch) )
		{
			cout << ch;
		}
		fin.close();
	}
	cout << "In the second fin.is_open()" << endl;
	cout << "Done." << endl;


	return 0;
}





 

  • 2
    点赞
  • 2
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值