C++基础梳理 2019.1.16(静态转换(static_cast),动态转换(dynamic_cast),常量转换(const_cast),重新解释转换(reinterpret_cast),异常

本文详细介绍了C++中的四种类型转换方法:static_cast、dynamic_cast、const_cast和reinterpret_cast,以及C++异常处理机制,包括异常抛出、捕获、栈解旋和异常接口声明等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

tip——string转char*的库函数方法:

string.c_str();

 

类型转换

 

静态转换(static_cast)

 

使用方式:

static_cast<目标类型>(原始对象)

用法:

  1. 用于父类和子类之间指针或引用的转换。
  2. 用于基本数据类型之间的转换,如把int转换成char,把char转换成int。这种转换的安全性也要开发人员来保证。
  3. 没有父子关系的自定义类型不可以转换

补充:

  • 进行行转换(把派生类的指针或引用转换成基类表示)是安全的;
  • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

 

基本数据类型转换的例子:

void test01(){
	char a = 'a';
	double d = static_cast<double>(a);
	cout << "d = " << d <<endl;
}

 

继承关系指针互相转换

class Base{};
class Child :public Base{};
class Other{};
void test02(){
	Base * base = NULL;
	Child * child = NULL;

	//把base转为 Child*类型 向下  不安全
	Child * child2 = static_cast<Child*>(base);

	//把child 转为 Base*  向上  安全
	Base * base2 = static_cast<Base*>(child);

	//转other类型 转换无效
	//Other * other = static_cast<Other*>(base);
}

 

继承关系引用相互转换

class Animal{};
class Dog : public Animal{};
class Other{};

void test03(){

	Animal ani_ref;
	Dog dog_ref;
	//继承关系指针转换
	Animal& animal01 = ani_ref;
	Dog& dog01 = dog_ref;
	//子类指针转成父类指针,安全
	Animal& animal02 = static_cast<Animal&>(dog01);
	//父类指针转成子类指针,不安全
	Dog& dog02 = static_cast<Dog&>(animal01);
}

 

 

 

动态转换(dynamic_cast)——十分严格

 

使用方式:

dynamic_cast<目标类型>(原始对象)

用法:

  1. 基础类型不可以转换
  2. dynamic_cast主要用于类层次间上行转换和下行转换;
  3. 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
  4. 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全
  5. 发生多态,则允许发生向上转换和向下转换。

dynamic_cast 如果发生了多态,那么可以让基类转为派生类 ,向下转换

原因:

创建空间大小的时候,是按照子类的大小进行创建的。

使用样例:

class Animal {
public:
	virtual void ShowName() = 0;
};
class Dog : public Animal{
	virtual void ShowName(){
		cout << "I am a dog!" << endl;
	}
};
class Other {
public:
	void PrintSomething(){
		cout << "我是其他类!" << endl;
	}
};

//普通类型转换
void test01(){

	//不支持基础数据类型
	int a = 10;
	//double a = dynamic_cast<double>(a);
}

//继承关系指针
void test02(){

	Animal* animal01 = NULL;
	Dog* dog01 = new Dog;

	//子类指针转换成父类指针 可以
	Animal* animal02 = dynamic_cast<Animal*>(dog01);
	animal02->ShowName();
	//父类指针转换成子类指针 不可以
	//Dog* dog02 = dynamic_cast<Dog*>(animal01);
}

//继承关系引用
void test03(){

	Dog dog_ref;
	Dog& dog01 = dog_ref;

	//子类引用转换成父类引用 可以
	Animal& animal02 = dynamic_cast<Animal&>(dog01);
	animal02.ShowName();
}

//无继承关系指针转换
void test04(){
	
	Animal* animal01 = NULL;
	Other* other = NULL;

	//不可以
	//Animal* animal02 = dynamic_cast<Animal*>(other);
}

 

 

 

 

常量转换(const_cast)

 

该运算符用来修改类型的const属性

 

使用方式:

  1. 加上const: const int * newP2  =  const_cast<const int *>(p2);
  2. 去掉const:            int * newp    =  const_cast<int *>(p);

用法:

  1. 常量引用被转换成非常量引用,并且仍然指向原来的对象;
  2. 常量指针被转化成非常量指针,并且仍然指向原来的对象;
  3. 不能直接非指针非引用的变量使用const_cast操作符去直接移除它的const.
//常量指针转换成非常量指针
void test01(){
	
	const int* p = NULL;
	int* np = const_cast<int*>(p);

	int* pp = NULL;
	const int* npp = const_cast<const int*>(pp);

	const int a = 10;  //不能对非指针或非引用进行转换
	//int b = const_cast<int>(a); }

//常量引用转换成非常量引用
void test02(){

int num = 10;
	int & refNum = num;

	const int& refNum2 = const_cast<const int&>(refNum);
	
}

 

 

 

重新解释转换(reinterpret_cast)——最不安全,不推荐

 

主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针.

void test06(){
	int a = 10;
	int * p = reinterpret_cast<int *>(a);

	Base * base = NULL;
	Other * other = reinterpret_cast<Other*>(base);

	//最不安全 ,不推荐
}

 

 

 

 

异常——将问题检测和问题处理相分离。

 

什么是异常处理

异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0溢出,数组下标越界,所要读取的文件不存在,空指针,内存不足等等)。

 

引入异常的原因

在C语言的世界中,对错误的处理总是围绕着两种方法:

  1. 使用整型的返回值标识错误;
  2. 使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。

C++中仍然是可以用这两种方法的。

这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。

还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。比如返回-1代表除0异常,但是10/(-10)的正确结果也是-1,这就会引发冲突,不能确定你返回的-1是正确的计算结果还是因为发生了异常。

当然,你也可以通过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂。

 

 

c++异常机制相比C语言异常处理的优势?

  • 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
  • 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
  • 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
  • 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。

 

补充——什么是跳级?

比如下图,A调用B,B调用C,在C中发生了除0异常,因此返回值为 -1。 B收到了返回值,误以为- 1 是C的正常计算结果,就将该结果加以计算并将计算结果返回给A。如果A没有发现异常,那么程序就要崩溃了,因此在B没有发现C产生了异常的情况下,允许A来处理异常,这就是跳级

如果异常没有函数来进行处理,系统会调用terminate函数,是程序中断。

 

 

C++异常捕获的小例子

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

int myDevide(int a ,int b){
	if (b == 0){
		//如果b是异常 抛出异常
		//return -1;

		throw 1; 抛出int类型异常
	}
	return a / b;
}

void test01(){
	int a = 10;
	int b = 0;

	//int ret = myDevide(a, b); //早期如果返回-1 无法区分到底是结果还是异常

	//C++中异常处理

	try //试一试
	{
		myDevide(a, b);
	}
	catch (int) //捕获异常
	{
		cout << "int类型异常捕获" << endl;
	}
}

int main(){

	test01();

	system("pause");
	return EXIT_SUCCESS;
}

 

但是类型太多了,总不能针对每种异常都写一个cache吧?可以用"..."符号来解决:

catch (...) {
	cout << "其他类型异常捕获" << endl;
}

用一用:

#include "pch.h"
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

int myDevide(int a, int b) {
	if (b == 0) {
		throw 'a';
	}
	return a / b;
}

void test01() {
	int a = 10;
	int b = 0;

	try {
		myDevide(a, b);
	}
	catch (int) {
		cout << "int类型异常捕获" << endl;
	}
	catch (...) {
		cout << "其他类型异常捕获" << endl;
	}
}

int main() {
	test01();
	system("pause");
	return EXIT_SUCCESS;
}

 

 

试一下异常的跳级处理

对于自己不想处理的异常,可以throw出去

int myDevide(int a, int b) {
	if (b == 0) {
		throw 1;
	}
	return a / b;
}

void test01() {
	int a = 10;
	int b = 0;

	try {
		myDevide(a, b);
	}
	catch (int) //捕获异常
	{
		//不想处理该异常,可以将捕获到的异常抛出
		throw;
		cout << "int类型异常捕获" << endl;
	}
}

 

完整实现代码:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

int myDevide(int a, int b) {
	if (b == 0) {
		throw 1;
	}
	return a / b;
}

void test01() {
	int a = 10;
	int b = 0;

	try //试一试
	{
		myDevide(a, b);
	}
	catch (int) //捕获异常
	{
		//不想处理该异常,可以将捕获到的异常抛出
		throw;
		cout << "int类型异常捕获" << endl;
	}
	catch (...) {
		cout << "其他类型异常捕获" << endl;
	}
}

int main() {
	try {
		test01();
	}
	catch (...) {
		cout << "main函数中的异常捕获" << endl;
	}

	system("pause");
	return EXIT_SUCCESS;
}

 

 

 

自定义异常类

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class myException //自定义异常类
{
public:
	void printError() {
		cout << "自定义的异常" << endl;
	}
};

int myDevide(int a, int b) {
	if (b == 0) {
		throw myException(); //匿名对象
	}
	return a / b;
}

void test01(){
	int a = 10;
	int b = 0;

	try {
		myDevide(a, b);
	}
	catch (int) {
		cout << "int类型异常捕获" << endl;
	}
	catch (double) {
		//如果不想处理这个异常 ,可以继续向上抛出
		throw;
		cout << "double类型异常捕获" << endl;
	}
	catch (myException e) {
		e.printError();
	}
	catch (...) {
		cout << "其他类型异常捕获" << endl;
	}

}

int main() {

	try {
		test01();
	}
	catch (char) {
		cout << "main 函数中 double类型异常捕获" << endl;
	}
	catch (...) {
		cout << "main函数中 其他类型异常捕获" << endl;
	}

	system("pause");
	return EXIT_SUCCESS;
}

 

总结:

  1. 若有异常则通过throw操作创建一个异常对象并抛出。
  2. 将可能抛出异常的程序段放到try块之中。
  3. 如果在try段执行期间没有引起异常,那么跟在try后面的catch字句就不会执行。
  4. catch子句会根据出现的先后顺序被检查,匹配的catch语句捕获并处理异常(或继续抛出异常)
  5. 如果匹配的处理未找到,则运行函数terminate将自动被调用,其缺省功能调用abort终止程序。
  6. 处理不了的异常,可以在catch的最后一个分支,使用throw,向上抛。

 

c++异常处理使得异常的引发和异常的处理不必在一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。

 

 

 

栈解旋(unwinding)

 

异常被抛出后,从进入try块起,到throw抛出异常之前,这期间在栈上构造的所有对象,都会被自动析构析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding).

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class myException //自定义异常类
{
public:
	void printError() {
		cout << "自定义的异常" << endl;
	}
};

class Person {
public:
	Person() {
		cout << "Person构造" << endl;
	}
	~Person() {
		cout << "Person析构" << endl;
	}
};

int myDevide(int a, int b) {
	if (b == 0) {
		//栈解旋
		//从try开始  到 throw 抛出异常之前  所有栈上的对象 都会被释放 这个过程称为栈解旋
		//构造和析构顺序相反
		Person p1;
		Person p2;

		throw myException(); //匿名对象
	}
	return a / b;
}

void test01()
{
	int a = 10;
	int b = 0;

	try {
		myDevide(a, b);
	}
	catch (int) {
		cout << "int类型异常捕获" << endl;
	}
	catch (double) {
		//如果不想处理这个异常 ,可以继续向上抛出
		throw;
		cout << "double类型异常捕获" << endl;
	}
	catch (myException e) {
		e.printError();
	}
	catch (...) {
		cout << "其他类型异常捕获" << endl;
	}

}

int main() {

	try {
		test01();
	}
	catch (char) {
		cout << "main 函数中 double类型异常捕获" << endl;
	}
	catch (...) {
		cout << "main函数中 其他类型异常捕获" << endl;
	}

	system("pause");
	return EXIT_SUCCESS;
}

 

 

 

异常接口声明

 

  1. 如果想抛出特定的类型异常 ,可以利用异常的接口声明
  2. void func() throw ( int) 只能抛出 int类型
  3. void func() throw() 不抛出任何类型异常
  4. 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexcepted函数会被调用,该函数默认行为调用terminate函数中断程序。
//可抛出所有类型异常
void TestFunction01(){
	throw 10;
}

//只能抛出int char char*类型异常
void TestFunction02() throw(int,char,char*){
	string exception = "error!";
	throw exception;
}

//不能抛出任何类型异常
void TestFunction03() throw(){
	throw 10;
}

int main(){

	try{
		//TestFunction01();
		//TestFunction02();
		//TestFunction03();
	}
	catch (...){
		cout << "捕获异常!" << endl;
	}
	
	system("pause");
	return EXIT_SUCCESS;
}

 

 

 

异常变量的生命周期

 

  1. 如果  MyException e,会多开销一份数据 ,调用拷贝构造
  2. 如果 MyExcepiton *e   ,不提前释放对象 new ,自己管理delete
  3. 推荐  MyException &e  容易些 ,而且就一份数据
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class MyException {
public:
	MyException() {
		cout << "MyException的默认构造" << endl;
	}
	MyException(const MyException & e) {
		cout << "MyException的拷贝构造" << endl;
	}
	~MyException() {
		cout << "MyException的析构调用" << endl;
	}
};

void doWork() {
	throw MyException();
}

void test01() {
	try {
		doWork();
	}
	catch (MyException e) //MyException e,会多开销一份数据
	{
		cout << "捕获异常" << endl;
	}
}

int main() {
	test01();
	system("pause");
	return EXIT_SUCCESS;
}

调用拷贝构造是因为catch的参数是值传递。

catch (MyException e)

为了提高效率,可以改成引用传递

catch (MyException &e)

但是传引用,在catch函数中不能对传入的e进行操作,因为在栈上创建的e已经被析构了。那么就改成在堆上创建e,然后catch传入一个指向e的指针

void doWork() {
	throw new MyException();
}

void test01() {
	try {
		doWork();
	}
	catch (MyException *e){		
		cout << "捕获异常" << endl;
		delete e; //靠自觉 释放对象
	}
}

一定要记得自己new的东西,自己delete

 

注意!!!如果是栈上创建的e,那么在catch函数中就不要再对e进行任何操作,因为那是野指针,这种操作是很危险的

class MyException {
public:
	MyException() {
		cout << "MyException的默认构造" << endl;
	}
	MyException(const MyException & e) {
		cout << "MyException的拷贝构造" << endl;
	}
	~MyException() {
		cout << "MyException的析构调用" << endl;
	}

	void printError() {
		this->m_A = 100;
		cout << "error" << m_A << endl;
	}
	int m_A;
};

void doWork() {
	throw  MyException();
}

void test01() {
	try {
		doWork();
	}
	catch (MyException &e) {
                //操作野指针
		e->printError();
		e->printError();
		e->printError(); //指向非法内存空间,不应该这么做
		cout << "捕获异常" << endl;
	}
}

 

 

 

异常的多态使用

  1. 利用多态来实现 printError同一个接口调用
  2. 抛出不同的错误对象,提示不同错误
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

//异常基类
class BaseException {
public:
	virtual void printError() {}
};

class  NullPointerException :public BaseException {
public:
	virtual void printError() {
		cout << "空指针异常" << endl;
	}
};

class OutofRangeException :public BaseException {
public:
	virtual void printError() {
		cout << "越界异常" << endl;
	}
};

void doWork() {
	//throw NullPointerException();
	throw OutofRangeException();
}

void test01() {
	try {
		doWork();
	}
	catch (BaseException & e) {
		e.printError();
	}
}

int main() {
	test01();
	system("pause");
	return EXIT_SUCCESS;
}

 

 

 

C++标准异常库

 

标准库介绍

标准异常类的成员:

① 在上述继承体系中,每个类都有提供了构造函数、复制构造函数、和赋值操作符重载。

② logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述

③ 所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。

 

标准异常类的具体描述:

异常名称

描述

exception

所有标准异常类的父类

bad_alloc

当operator new and operator new[],请求分配内存失败时

bad_exception

这是个特殊的异常,如果函数的异常抛出列表里声明了bad_exception异常,当函数内部抛出了异常抛出列表中没有的异常,这是调用的unexpected函数中若抛出异常,不论什么类型,都会被替换为bad_exception类型

bad_typeid

使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常

bad_cast

使用dynamic_cast转换引用失败的时候

ios_base::failure

io操作过程出现错误

logic_error

逻辑错误,可以在运行前检测的错误

runtime_error    

运行时错误,仅在运行时才可以检测的错误

logic_error的子类:

异常名称

描述

length_error

试图生成一个超出该类型最大长度的对象时,例如vector的resize操作

domain_error

参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负数的函数

out_of_range

超出有效范围

 

invalid_argument

参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常

runtime_error的子类:

异常名称

描述

range_error

计算结果超出了有意义的值域范围

overflow_error

算术计算上溢

underflow_error

算术计算下溢

invalid_argument

参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常

 

标准库异常函数使用要包含头文件

#include<stdexcept>

 

out_of_range的使用例子:

#include<iostream>
#include<stdexcept>
using namespace std;

class Person {
public:
	Person(int age) {
		if (age < 0 || age > 150) {
			throw out_of_range("年龄应该在0-150岁之间!");
		}
	}
public:
	int mAge;
};

int main() {

	try {
		Person p(151);
	}
	catch (out_of_range& ex) {
		cout << ex.what() << endl;
	}

	system("pause");
	return EXIT_SUCCESS;
}

 

 

 

编写自己的异常类

原因:

① 标准库中的异常是有限的;

② 在自己的异常类中,可以添加自己的信息。(标准库中的异常类值允许设置一个用来描述异常的字符串)。

如何写:

① 建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时。

② 当继承标准异常类时,应该重载父类的what函数和虚析构函数

③ 因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的拷贝构造函数

 

先来看看系统的异常基类长啥样(取自VS)

class exception
{
public:

    exception() noexcept
        : _Data()
    {
    }

    explicit exception(char const* const _Message) noexcept
        : _Data()
    {
        __std_exception_data _InitData = { _Message, true };
        __std_exception_copy(&_InitData, &_Data);
    }

    exception(char const* const _Message, int) noexcept
        : _Data()
    {
        _Data._What = _Message;
    }

    exception(exception const& _Other) noexcept
        : _Data()
    {
        __std_exception_copy(&_Other._Data, &_Data);
    }

    exception& operator=(exception const& _Other) noexcept
    {
        if (this == &_Other)
        {
            return *this;
        }

        __std_exception_destroy(&_Data);
        __std_exception_copy(&_Other._Data, &_Data);
        return *this;
    }

    virtual ~exception() noexcept
    {
        __std_exception_destroy(&_Data);
    }

    virtual char const* what() const
    {
        return _Data._What ? _Data._What : "Unknown exception";
    }

private:

    __std_exception_data _Data;
};

 

string转char*的库函数方法:

string.c_str();

这就是一个string类型的字符串了

string("我自己的年龄越界异常")

 

完整代码:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <string>
using namespace std;

//继承基类 exception
class MyOutOfRangeException :public exception {
public:
	MyOutOfRangeException(string errorInfo) {
		this->m_ErrorInfo = errorInfo;
	}
	//重写父类的虚析构函数和what函数
	virtual  ~MyOutOfRangeException() {}
	virtual const char *  what() const {
		//返回 错误信息
		//string 转 char *     .c_str()
		return this->m_ErrorInfo.c_str();
	}
	string m_ErrorInfo;
};

class Person {
public:
	Person(string name, int age) {
		this->m_Name = name;
		//年龄做检测
		if (age < 0 || age > 200) {
			throw MyOutOfRangeException(string("我自己的年龄越界异常"));
		}
	}
	string m_Name;
	int m_Age;
};

void test01() {
	try {
		Person p("张三", 300);
	}
	catch (MyOutOfRangeException & e) {
		cout << e.what() << endl;
	}
}

int main() {
	test01();
	system("pause");
	return EXIT_SUCCESS;
}

 

总结:

  1. 自己的异常类 需要继承于  exception
  2. 重写 父类的 虚析构和 what函数
  3. 内部维护以错误信息——字符串
  4. 构造时候传入错误信息字符串,what返回这个字符串
  5. string 转 char *的方法——   .c_str();

 

 

 

 

标准输入输出流

 

C++输入输出包含以下三个方面的内容:

  • 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O。
  • 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O。
  • 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O。

 

C++编译系统提供了用于输入输出的iostream类库。iostream这个单词是由3个部 分组成的,即i-o-stream,意为输入输出流。在iostream类库中包含许多用于输入输出的 类。常用的见表

ios是抽象基类,由它派生出istream类和ostream类,两个类名中第1个字母i和o分别代表输入(input)和输出(output)。 istream类支持输入操作,ostream类支持输出操作, iostream类支持输入输出操作。iostream类是从istream类和ostream类通过多重继承而派生的类。其继承层次见上图表示。

C++对文件的输入输出需要用ifstrcam和ofstream类,两个类名中第1个字母i和o分别代表输入和输出,第2个字母f代表文件 (file)。ifstream支持对文件的输入操作, ofstream支持对文件的输出操作。类ifstream继承了类istream,类ofstream继承了类ostream,类fstream继承了 类iostream。见图

http://c.biancheng.net/cpp/uploads/allimg/140527/1-14052GP142452.png

I/O类库中还有其他一些类,但是对于一般用户来说,以上这些已能满足需要了。

 

与iostream类库有关的头文件

iostream类库中不同的类的声明被放在不同的头文件中,用户在自己的程序中用#include命令包含了有关的头文件就相当于在本程序中声明了所需 要用到的类。可以换 —种说法:头文件是程序与类库的接口,iostream类库的接口分别由不同的头文件来实现。常用的有

  • iostream  包含了对输入输出流进行操作所需的基本信息。
  • fstream  用于用户管理的文件的I/O操作。
  • strstream  用于字符串流I/O。
  • stdiostream  用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序。
  • iomanip  在使用格式化I/O时应包含此头文件。

 

在iostream头文件中定义的流对象

在 iostream 头文件中定义的类有 ios,istream,ostream,iostream,istream 等。

在iostream头文件中不仅定义了有关的类,还定义了4种流对象,

对象

含义

对应设备

对应的类

c语言中相应的标准文件

cin

标准输入流

键盘

istream_withassign

stdin

cout

标准输出流

屏幕

ostream_withassign

stdout

cerr

标准错误流

屏幕

ostream_withassign

stderr

clog

标准错误流

屏幕

ostream_withassign

stderr

 

在iostream头文件中定义以上4个流对象用以下的形式(以cout为例):

 ostream cout ( stdout);

  在定义cout为ostream流类对象时,把标准输出设备stdout作为参数,这样它就与标准输出设备(显示器)联系起来,如果有
    cout <<3;
就会在显示器的屏幕上输出3。

 

在iostream头文件中重载运算符

“<<”和“>>”本来在C++中是被定义为左位移运算符和右位移运算符的,由于在iostream头文件中对它们进行了重载, 使它们能用作标准类型数据的输入和输出运算符。所以,在用它们的程序中必须用#include命令把iostream包含到程序中。

#include <iostream>
  1. >>a表示将数据放入a对象中。
  2. <<a表示将a对象中存储的数据拿出。

 

 

标准I/O流

标准I/O对象:cin,cout,cerr,clog

 

cout流对象

cout是console output的缩写,意为在控制台(终端显示器)的输出。强调几点。

1)   cout不是C++预定义的关键字,它是ostream流类的对象,在iostream中定义。 顾名思义,流是流动的数据,cout流是流向显示器的数据。cout流中的数据是用流插入     运算符“<<”顺序加入的。如果有:

cout<<"I "<<"study C++ "<<"very hard. << “hello world !";

按顺序将字符串"I ", "study C++ ", "very hard."插人到cout流中,cout就将它们送到显示器,在显示器上输出字符串"I study C++ very hard."。cout流是容纳数据的载体,它并不是一个运算符。人们关心的是cout流中的内容,也就是向显示器输出什么。


2)    用“cout<<”输出基本类型的数据时,可以不必考虑数据是什么类型,系统会判断数据的类型,并根据其类型选择调用与之匹配的运算符重载函数。这个过程都是自动的,用户不必干预。如果在C语言中用prinf函数输出不同类型的数据,必须分别指定相应    的输出格式符,十分麻烦,而且容易出错。C++的I/O机制对用户来说,显然是方便而安全的。

 

  1. cout流在内存中对应开辟了一个缓冲区,用来存放流中的数据,当向cout流插人一个endl时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符, 并刷新流(清空缓冲区)。注意如果插人一个换行符”\n“(如cout<<a<<"\n"),则只输出和换行,而不刷新cout 流(但并不是所有编译系统都体现出这一区别)。
  2.  在iostream中只对"<<"和">>"运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出进行重载。如果用户声明了新的类型,并希望用"<<"和">>"运算符对其进行输入输出,按照重运算符重载来做。

 

cerr流对象

cerr流对象是标准错误流,cerr流已被指定为与显示器关联。cerr的作用是向标准错误设备(standard error device)输出有关出错信息。cerr与标准输出流cout的作用和用法差不多。但有一点不同:cout流通常是传送到显示器输出,但也可以被重定向输出到磁盘文件,而cerr流中的信息只能在显示器输出。当调试程序时,往往不希望程序运行时的出错信息被送到其他文件,而要求在显示器上及时输出,这时 应该用cerr。cerr流中的信息是用户根据需要指定的。

 

clog流对象

clog流对象也是标准错误流,它是console log的缩写。它的作用和cerr相同,都是在终端显示器上显示出错信息。区别:cerr是不经过缓冲区,直接向显示器上输出有关信息,而clog中的信息存放在缓冲区中,缓冲区满后或遇endl时向显示器输出。

 

标准输入流

标准输入流对象cin,重点掌握的函数

cin.get() //一次只能读取一个字符
cin.get(一个参数) //读一个字符
cin.get(两个参数) //可以读字符串
cin.getline()
cin.ignore()
cin.peek()
cin.putback()

 

 

cin.get(一个参数)的测试——输入as

void test01(){
	char c = cin.get();
	cout << "c = " << c << endl;

	c = cin.get();
	cout << "c = " << c << endl;

	c = cin.get();
	cout << "c = " << c << endl;

	c = cin.get();
	cout << "c = " << c << endl;
}

因为输入的有换行符,因此输出的如上图所示。

 

 

cin.get(两个参数) //可以读字符串——输入 hello world

cin.get(两个参数)读取字符串时,不会把换行符拿走,遗留在缓冲区中

void test02()
{
	//cin.get(两个参数) //可以读字符串
	char buf[1024];
	cin.get(buf, 1024);

	char c = cin.get();

	if (c == '\n'){
		cout << "换行还在缓冲区" << endl;
	}else{
		cout << "换行不在缓冲区" << endl;
	}

	cout << buf << endl;
}

 

 

cin.getline()测试

    //cin.getline 把换行符读取,并且扔掉

void test03()
{
	char buf[1024];
	cin.getline(buf, 1024);

	char c = cin.get();
	if (c == '\n')
	{
		cout << "换行还在缓冲区" << endl;
	}
	else
	{
		cout << "换行不在缓冲区" << endl;
	}
}

 

 

cin.ignore()测试

没有参数 代表忽略一个字符 ,带参数N,代表忽略N个字符

void test04()
{
	cin.ignore(2); //没有参数 代表忽略一个字符 ,带参数N,代表忽略N个字符

	char c = cin.get();

	cout << "c = " << c << endl;

}

 

 

cin.peek() 测试

void test05()
{
	//输入as  偷看一眼 a,然后再放回缓冲区 缓冲区中还是as
	char c = cin.peek();

	cout << "c = " << c << endl;

	c = cin.get();

	cout << "c = " << c << endl;

}

 

 

cin.putback()

把读取到的东西放回缓冲区

void test06()
{
	char c = cin.get();
	cin.putback(c);

	char buf[1024];

	cin.getline(buf, 1024);
	cout << buf << endl;
}

 

总结:

  1. cin.get 缓冲区中读取一个字符
  2. cin.get(两个参数) 不读换行符
  3. cin.getline () 读取换行 并且扔掉
  4. cin.ignore 忽略 (N) N代表忽略字符数
  5. cin.peek 偷窥   偷看1个字符然后放回去
  6. cin.putback  放回 把字符放回缓冲区

 

 

输入流案例

 

判断用户输入的是数字还是字符串

//案例1  判断用户的是字符串 还是数字?
void test07()
{
	cout << "请输入一串数字或者字符串" << endl;

	//偷窥
	char c = cin.peek();

	if (c >= '0' && c <= '9')
	{
		int num;
		cin >> num;

		cout << "您输入是数字 :数字为" << num << endl;
	}
	else
	{
		char buf[1024];
		cin >> buf;

		cout << "您输入是字符串 :字符串为" << buf << endl;
	}
}

 

 

让用户输入 1 到 10 的数字 ,如果输入有误 重新输入 

缓冲区有一个标志位,如果你输入的类型与标志位不符,则缓冲区中就一直是那个类型不符的值。因此需要对缓冲区进行清空处理。

cin.fail() //看标志位  0正常 1不正常
cin.clear()//重置标志位
cin.syne() //清空缓冲区

完整测试案例

//案例2 让用户输入 1 到 10 的数字 ,如果输入有误 重新输入 
void test08(){
	int num;
	cout << "请输入一个1到10的数字:" << endl;

	while (true){
		cin >> num;
		if (num > 0 && num <= 10){
			cout << "输入的数字为" << num << endl;
			break;
		}
		cout << "对不起,请重新输入" << endl;
		//重置标志位
		cin.clear();
		cin.sync(); //清空缓冲区
		cout << "标志位: " << cin.fail() << endl; //标志位 0 正常的  1 不正常
	}
}

如果上面的代码没有充值标志位和清空缓冲区操作,当你输入的不是int类型的值,而是一个字符a时, 则会程序如下的运行结果

 

 

 

标准输出流

cout.flush() //刷新缓冲区 Linux下有效
cout.put() //向缓冲区写字符
cout.write() //从buffer中写num个字节到当前输出流中。

 

//cout.flush 刷新缓冲区,linux下有效
void test01(){	
	cout << "hello world";
	//刷新缓冲区
	cout.flush(); 
}

//cout.put 输出一个字符
void test02(){
	
	cout.put('a');
	//链式编程
	cout.put('h').put('e').put('l');
}

//cout.write 输出字符串 buf,输出多少个
void test03(){
	
	//char* str = "hello world!";
	//cout.write(str, strlen(str));
	char* str = "*************";
	for (int i = 1; i <= strlen(str); i ++){
		cout.write(str, i);
		cout << endl;
	}

	for (int i = strlen(str); i > 0; i --){
		cout.write(str, i);
		cout << endl;
	}

}

 

 

格式化输出

在输出数据时,为简便起见,往往不指定输出的格式,由系统根据数据的类型采取默认的格式,但有时希望数据按指定的格式输出,如要求以十六进制或八进制形式输出一个整数,对输出的小数只保留两位小数等。有两种方法可以达到此目的。

1)使用控制符的方法;

2)使用流对象的有关成员函数。

 

使用流对象的有关成员函数

通过调用流对象cout中用于控制输出格式的成员函数来控制输出格式。用于控制输出格式的常用的成员函数如下:

流成员函数setf和控制符setiosflags括号中的参数表示格式状态,它是通过格式标志来指定的。格式标志在类ios中被定义为枚举值。因此在引用这些格式标志时要在前面加上类名ios和域运算符“::”。格式标志见表13.5。

void test02()
{
	//通过流成员函数

	int number = 99;
	cout.width(20);//输出18个空格,后面跟着99
	cout.fill('*');//把空格换成*
	cout.setf(ios::left); //设置输出格式状态,输入内容左对齐
	cout.unsetf(ios::dec); //卸载十进制
	cout.setf(ios::hex); //安装16进制
	cout.setf(ios::showbase); // 强制输出整数基数  0  0x
	cout.unsetf(ios::hex);
	cout.setf(ios::oct);
	cout << number << endl;

}

 

控制符格式化输出

如果想使用控制符,则需要引入头文件

#include <iomanip> //使用控制符的头文件

C++提供了在输入输出流中使用的控制符(有的书中称为操纵符)。

//控制符的方式显示
void test03() {

	int number = 99;
	cout << setw(20)  //20个空格
		<< setfill('~') //空格换成~
		<< setiosflags(ios::showbase) //基数
		<< setiosflags(ios::left) //左对齐
		<< hex // 十六进制
		<< number
		<< endl;
}

 

 

 

 

文件读写

输入输出是以系统指定的标准设备(输入设备为键盘,输出设备为显示器)为对象的。在实际应用中,常以磁盘文件作为对象。即从磁盘文件读取数据,将数据输出到磁盘文件。

和文件有关系的输入输出类主要在fstream.h这个头文件中被定义,在这个头文件中主要被定义了三个类,由这三个类控制对文件的各种输入输出操作,他们分别是ifstream、ofstream、fstream,其中fstream类是由iostream类派生而来,他们之间的继承关系见下图所示:

由于文件设备并不像显示器屏幕与键盘那样是标准默认设备,所以它在fstream头文件中是没有像cout那样预先定义的全局对象,所以我们必须自己定义一个该类的对象。ifstream类,它是从istream类派生的,用来支持从磁盘文件的输入。ofstream类,它是从ostream类派生的,用来支持向磁盘文件的输出。

fstream类,它是从iostream类派生的,用来支持对磁盘文件的输入输出。

 

 

打开文件的方式

//以输出的方式打开文件 如果想配合使用,要用 |
ofstream ofs("./test.txt", ios::out | ios::trunc);

另一种打开文件的方式

//后期指定打开方式
ofstream ofs;
ofs.open("./test.txt", ios::out | ios::trunc);

 

写文件的例子:

  1. ofstream  ofs  
  2. open                指定打开方式
  3. isopen             判断是否打开成功
  4. ofs << “数据”   写入数据
  5. ofs.close         关闭文件
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//文件读写头文件
#include <fstream>

//写文件
void test01()
{
	//以输出的方式打开文件 如果想配合使用,要用 |
	//ofstream ofs("./test.txt", ios::out | ios::trunc);
	//后期指定打开方式	
	ofstream ofs;
	ofs.open("./test.txt", ios::out | ios::trunc);

	//判断是否打开成功
	if ( !ofs.is_open())
	{
		cout << "打开失败" << endl;
	}

	ofs << "姓名:abc" << endl;
	ofs << "年龄:100" << endl;
	ofs << "性别:男" << endl;

	ofs.close();
}

 

 

读文件的例子

  1. ifstream  ifs
  2.  ios::in       指定打开方式
  3. isopen            判断是否打开成功
  4.  三种方式读取数据
//读文件
void test02()
{
	ifstream ifs;
	ifs.open("./test.txt", ios::in);

	//判断是否打开成功
	if (!ifs.is_open())
	{
		cout << "打开失败" << endl;
	}

	//第一种方式
	//char buf[1024];

	//while (ifs >>buf) //按行读取
	//{
	//	cout << buf << endl;
	//}

	//第二种方式
	//char buf2[1024];

	//while (!ifs.eof()) //eof读到文件尾
	//{
	//	ifs.getline(buf2, sizeof(buf2));
	//	cout << buf2 << endl;
	//}

	//第三种 不推荐 按单个字符读取
	char c;
	while (  (c = ifs.get() ) != EOF) // EOF文件尾
	{
		cout << c;
	}

	ifs.close();

}

 

 

完整例子:

class Person{
public:
	Person(char* name,int age){
		strcpy(this->mName, name);
		this->mAge = age;
	}
public:
	char mName[64];
	int mAge;
};

int main(){

	char* fileName = "person.txt";
	//二进制模式读写文件
	//创建文件对象输出流
	ofstream osm(fileName, ios::out | ios::binary);

	Person p1("John",33);
	Person p2("Edward", 34);

	//Person对象写入文件
	osm.write((const char*)&p1,sizeof(Person));
	osm.write((const char*)&p2, sizeof(Person));

	//关闭文件输出流
	osm.close();

	//从文件中读取对象数组
	ifstream ism(fileName, ios::in | ios::binary);
	if (!ism){
		cout << "打开失败!" << endl;
	}
	
	Person p3;
	Person p4;

	ism.read((char*)&p3, sizeof(Person));
	ism.read((char*)&p4, sizeof(Person));

	cout << "Name:" << p3.mName << " Age:" << p3.mAge << endl;
	cout << "Age:" << p4.mName << " Age:" << p4.mAge << endl;

	//关闭文件输入流
	ism.close();

	system("pause");
	return EXIT_SUCCESS;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值