C++之模板、IO流、异常

C++之模板(Template)

泛型(Generic Programming),即是指具有在多种数据类型上皆可操作的含意。泛型编程的代表作品 STL 是一种高效、泛型、可交互操作的软件组件。泛型编程最初诞生于 C++中,目的是为了实现 C++的 STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的精神其实很简单:类型参数化(type parameterized),即,类型也是一种参数,也是一种静多态。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数 T。

重载函数, 固然实现了泛化的一些特性, 但是不彻底, 且有二义性(ambiguous)的存在

1、函数模板

函数模板
//在一个函数的参数表, 返回类型和函数体中使用参数化的类型。

template<typename/class 类型参数 T1, typename/class 类型参数 T2,...>返回类型 函数模板名(函数参数列表)
{
    函数模板定义体
} 

template<typename T>, 既可以与函数同行, 也可以将函数另起一行来书写。 T 即为范化的类型。其过程, 相当于经历了两次编译, 先依据实参类型, 实例化函数, 然后再将实例化的函数, 参与编译。

举一个具体的例子:

template<typename T> void myswap(T & a, T &b)
{
    T t = a;
    a = b;
    b = t;
}

函数模板默认参数

重在理解, 类型参数化, 此时的默认参数, 不再是一数值, 就是类型。

函数模板, 在调用时, 先实例化为模板函数, 然后再调用。 当然也可以设置默认类型的默认值。 由于系统强大的自动推导能力, 有时默认也没有太大的意义。

template<typename T = int>
void quickSort(T * array,int left, int right) 

函数模板的特化

Template Specialization, 函数模板的特化, 即函数模板的特性情况, 个例行为。

就是在实例化模板时, 对特定类型的实参进行特殊处理, 即实例化一个特殊的实例版本。

当以特化定义时的形参使用模板时, 将调用特化版本, 模板特化分为全特化和偏特化,函数模板的特化, 只能全特化;

比如, 我们在比较两个数的大小时:

#include <iostream>
#include <string.h>
using namespace std;

template<typename T> int compare( T &a, T &b)
{
    if(a > b) return 1;
    else if(a < b)return -1;
    else return 0;
}
//实参为两个 char*时, 比较的是指针的大小,
//而不是指针指向内容的大小, 此时就需要为该函数模板定义一个特化版本, 即特殊处理的版本:
template<> int compare < const char * >( const char* &a, const char* &b)
{
    return strcmp(a,b);
}

int main()
{
    int a = 3; int b = 5;
    cout<<compare(a,b)<<endl;
    string str1 = "abc",str2 ="abc";
    cout<<compare(str1,str2)<<endl;
    //char * 与 const char* 亦属于不匹配
    const char * p1 = "abc",*p2= "def";
    cout<<compare(p1,p2)<<endl;
    cout<<compare(p2,p1)<<endl;
    return 0;
}

2、类模板(Class Template)

template<typename T> class ClassName
{
    void func(T );
};
template<typename T> void ClassName<T>::func(T)
{ }

应用:

ClassName<int> cn; //类模板->模板类->类对象

类模板的友元

友元在.h

友元函数, 实现在.h 文件中并不多见, 但在模板中这样使用友元, 就是一种常规则用法。

友元函数的实现体也放在.h里

友元在.cpp

友元函数, 实现在.cpp 中, 需要在类模板前声明, 其前还需要类模板的声明。 类内用<>将其声明为空。

template <class ItemType>
class GenericList;

template<class ItemType>
ostream& operator <<
(ostream& outs,const GenericList<ItemType>& the_list);

template<class ItemType>
class GenericList
{ 
public:
    friend ostream&operator << <>
    (ostream& outs,const GenericList<ItemType>& the_list);
};

IO流

目前为止,cin 和 cout 充当了 scanf 和 printf 的功能。但他们并不是函数。而是类对象,那么我们就有必要了解一下,他们是哪些类的对像。

流类对象不可赋值和复制

C++对文件进行读写操作

1、 定义数据流对象指针

对文件进行读写操作首先必须要定义一个数据流对象指针,数据流对象指针有三种类型,它们分别是:

Ifstream:表示读取文件流,使用的时候必须包含头文件“ifstream”;

Ofstream:表示文件写入流,使用的时候必须包含头文件“ofstream”;

Fstream:表示文件读取/写入流,使用的时候必须包含头文件“fstream”;

2、 打开文件

打开文件可以调用两个函数,其一是使用open函数,其二是使用数据流对象的构造函数。这两个函数调用的参数基本上一致的,以open函数为例:

void open(const char * filename, ios_base::openmode mode = ios_base::in | ios_base::out);
void open(const wchar_t *_Filename, ios_base::openmode mode = ios_base::in | ios_base::out, int prot = ios_base::_Openprot);

参数filename表示文件名,如果该文件在目录中不存在,那么该函数会自动创建该文件;参数mode表示打开方式,这里打开方式有一下四种,如表1,并且这些方式可以以“|”的或的方式组合使用。

ios::in    为输入(读)而打开文件                                                   
ios::out         为输出(写)而打开文件   
ios::ate    初始位置:文件尾
ios::app    所有输出附加在文件末尾             
ios::trunc    如果文件已存在则先删除该文件
ios::binary    二进制方式
参数prot表示文件打开的属性,基本上很少用到。

3、 文件的读写操作

由于类ofstream, ifstream 和fstream 是分别从ostream, istream 和iostream 中引申而来的,所以文件的读写操作与使用控制台函数cin和cout一样,“<<”表示对文件进行写操作,“>>”表示对文件进行读操作。

根据数据流读写的状态,有4个验证函数,它们分别是:

· bad()

如果在读写过程中出错,返回 true 。例如:当我们要对一个不是打开为写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。

· fail()

除了与bad() 同样的情况下会返回 true 以外,加上格式错误时也返回true ,例如当想要读入一个整数,而获得了一个字母的时候。

· eof()

如果读文件到达文件末尾,返回true。

· good()

这是最通用的:如果调用以上任何一个函数返回true 的话,此函数返回 false 。

4、 获得或者设置流指针

获得流指针的位置有两个函数,它们是

Long tellg() 和 long tellp()这两个成员函数不用传入参数,返回pos_type 类型的值(根据ANSI-C++标准) ,就是一个整数,代表当前get 流指针的位置 (用tellg) 或 put 流指针的位置(用tellp)。

设置流指针的位置根据输入输出流指针类型不同,也有两个函数,它们是:

seekg() 和seekp()这对函数分别用来改变流指针get 和put的位置。两个函数都被重载为两种不同的原型:

seekg ( pos_type position );
seekp ( pos_type position );

使用这个原型,流指针被改变为指向从文件开始计算的一个绝对位置。要求传入的参数类型与函数 tellg 和tellp 的返回值类型相同。

seekg ( off_type offset, seekdirdirection );
seekp ( off_type offset, seekdir direction );

使用这个原型可以指定由参数direction决定的一个具体的指针开始计算的一个位移(offset)

ios::beg    从流开始位置计算的位移
ios::cur    从流指针当前位置开始计算的位移
ios::end    从流末尾处开始计算的位移

5、关闭文件

调用函数close(),可以关闭流对象所指向的文件,释放流指针之后,那么该数据流就可以指向其它的文件进行操作了。

C++二进制文件的读取和写入

用 ostream::write 成员函数写文件

ofstream 和 fstream 的 write 成员函数实际上继承自 ostream 类,原型如下:

ostream & write(char* buffer, int count);

该成员函数将内存中 buffer 所指向的 count 个字节的内容写入文件,返回值是对函数所作用的对象的引用,如 obj.write(...) 的返回值就是对 obj 的引用。

write 成员函数向文件中写入若干字节,可是调用 write 函数时并没有指定这若干字节要写入文件中的什么位置。那么,write 函数在执行过程中到底把这若干字节写到哪里呢?答案是从文件写指针指向的位置开始写入。

文件写指针是 ofstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件写指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write 函数写入 n 个字节,写指针指向的位置就向后移动 n 个字节。

下面的程序从键盘输入几名学生的姓名和年龄(输入时,在单独的一行中按 Ctrl+Z 键再按回车键以结束输入。假设学生姓名中都没有空格),并以二进制文件形式存储,成为一个学生记录文件 students.dat。

例子,用二进制文件保存学生记录:

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
public:
    char szName[20];
    int age;
};
int main()
{
    CStudent s;
    ofstream outFile("students.dat", ios::out | ios::binary);
    while (cin >> s.szName >> s.age)
        outFile.write((char*)&s, sizeof(s));
    outFile.close();
    return 0;
}

输入:

Tom 60↙

Jack 80↙

Jane 40↙

^Z↙

则形成的 students.dat 为 72 字节,用“记事本”程序打开呈现乱码:

Tom烫烫烫烫烫烫烫烫 Jack烫烫烫烫烫烫烫? Jane烫烫烫烫烫烫烫?

第 13 行指定文件的打开模式是 ios::out|ios::binary,即以二进制写模式打开。在 Windows平台中,用二进制模式打开是必要的,否则可能出错

第 15 行将 s 对象写入文件。s 的地址就是要写入文件的内存缓冲区的地址。但是 &s 不是 char * 类型,因此要进行强制类型转换。

第 16 行,文件使用完毕一定要关闭,否则程序结束后文件的内容可能不完整。

用 istream::read 成员函数读文件

ifstream 和 fstream 的 read 成员函数实际上继承自 istream 类,原型如下:

istream & read(char* buffer, int count);

该成员函数从文件中读取 count 个字节的内容,存放到 buffer 所指向的内存缓冲区中,返回值是对函数所作用的对象的引用。

如果想知道一共成功读取了多少个字节(读到文件尾时,未必能读取 count 个字节),可以在 read 函数执行后立即调用文件流对象的 gcount 成员函数,其返回值就是最近一次 read 函数执行时成功读取的字节数。gcount 是 istream 类的成员函数,原型如下:

int gcount();

read 成员函数从文件读指针指向的位置开始读取若干字节。文件读指针是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以ios::app 方式打开,则指向文件末尾),用 read 函数读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read 函数,就能将整个文件的内容读取出来。

下面的程序将前面创建的学生记录文件 students.dat 的内容读出并显示。

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
    public:
        char szName[20];
        int age;
};
int main()
{
    CStudent s;       
    ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开
    if(!inFile) {
        cout << "error" <<endl;
        return 0;
    }
    while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束
        int readedBytes = inFile.gcount(); //看刚才读了多少字节
        cout << s.szName << " " << s.age << endl;   
    }
    inFile.close();
    return 0;
}

程序的输出结果是:

Tom 60

Jack 80

Jane 40

第 18 行,判断文件是否已经读完的方法和 while(cin>>n) 类似,归根到底都是因为 istream 类重载了 bool 强制类型转换运算符。

第 19 行只是演示 gcount 函数的用法,删除该行对程序运行结果没有影响。

思考题:关于 students.dat 的两个程序中,如果 CStudent 类的 szName 的定义不是“char szName[20] ”而是“string szName”,是否可以?为什么?

用文件流类的 put 和 get 成员函数读写文件

可以用 ifstream 和 fstream 类的 get 成员函数(继承自 istream 类)从文件中一次读取一个字节,也可以用 ofstream 和 fstream 类的 put 成员函数(继承自 ostream 类) 向文件中一次写入一个字节。

例题:编写一个 mycopy 程序,实现文件复制的功能。用法是在“命令提示符”窗口输入:

mycopy 源文件名 目标文件名

就能将源文件复制到目标文件。例如:

mycopy src.dat dest.dat

即将 src.dat 复制到 dest.dat。如果 dest.dat 原本就存在,则原来的文件会被覆盖。

解题的基本思路是每次从源文件读取一个字节,然后写入目标文件。程序如下:

#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[])
{
    if (argc != 3) {
        cout << "File name missing!" << endl;
        return 0;
    }
    ifstream inFile(argv[l], ios::binary | ios::in);  //以二进制读模式打开文件
    if (!inFile) {
        cout << "Source file open error." << endl;
        return 0;
    }
    ofstream outFile(argv[2], ios::binary | ios::out);  //以二进制写模式打开文件
    if (!outFile) {
        cout << "New file open error." << endl;
        inFile.close();  //打开的文件一定要关闭
        return 0;
    }
    char c;
    while (inFile.get(c))  //每次读取一个字符
        outFile.put(c);  //每次写入一个字符
    outFile.close();
    inFile.close();
    return 0;
}

文件存放于磁盘中,磁盘的访问速度远远低于内存。如果每次读一个字节或写一个字节都要访问磁盘,那么文件的读写速度就会慢得不可忍受。因此,操作系统在接收到读文件的请求时,哪怕只要读一个字节,也会把一片数据(通常至少是 512 个字节,因为磁盘的一个扇区是 512 B)都读取到一个操作系统自行管理的内存缓冲区中,当要读下一个字节时,就不需要访问磁盘,直接从该缓冲区中读取就可以了。

操作系统在接收到写文件的请求时,也是先把要写入的数据在一个内存缓冲区中保存起来,等缓冲区满后,再将缓冲区的内容全部写入磁盘。关闭文件的操作就能确保内存缓冲区中的数据被写入磁盘。

尽管如此,要连续读写文件时,像 mycopy 程序那样一个字节一个字节地读写,还是不如一次读写一片内存区域快。每次读写的字节数最好是 512 的整数倍

异常

1, 把可能发生异常的语句放在 try 语句声当中。 try 不影响原有语句的执行流程。

2, 若未发生异常, catch 子语句并不起作用。 程序会流转到 catch 子句的后面执行。

3, 若 try 块中发生异常, 则通过 throw 抛出异常。 throw 抛出异常后, 程序立即离开本函数, 转到上一级函数。 所以 triangleArea 函数中的 return 语句不会执行。

4, throw 抛出数据, 类型不限。 既可以是基本数据类型, 也可以是构造数据类型。

5,程序流转到 main 函数以后, try 语句块中 抛出进行匹配。匹配成功,执行 catch语句, catch 语句执行完毕后。 继续执行后面的语句。

6, 如无匹配, 系统调用 terminate 终止程序。

语法:

try{
    被检查可能抛出异常的语句
} catch(异常信息类型 [变量名]){
    进行异常处理的语句
}

1, 被检语句必须放在 try 块中, 否则不起作用。

2, try catch 中花括号不可省。

3, 一个 try-catch 结构中, 只能有一个 try 块, catch 块却可以有多个。 以便与不同的类型信息匹配。

4, throw 抛出的类型, 既可以是系统预定义的标准类型也可以是自定义类型。 从抛出到 catch 是一次复制拷贝的过程。 如果有自定义类型, 要考虑自定义类型的拷贝问题。

5, 异常匹配, 不作类型转化。 如果 catch 语句没有匹配异常类型信息, 就可以用(...)表示可以捕获任何异常类型的信息。

6.try-catch 结构可以与 throw 在同一个函数中, 也可以不在同一个函数中, throw抛出异常后, 先在本函数内寻找与之匹配的 catch 块, 找不到与之匹配的就转到上一层, 如果上一层也没有, 则转到更上一层的 catch 块。 如果最终找不到与之匹配的 catch 块, 系统则会调有系统函数 terminate 使程序终止。

throw应该包含在try块中。

//通过声明的方式, 告知, 调用方, 如何处理
void func() throw(char)
{
    throw 'a';
} 
//什么都没有写的情况-> 上抛
//写点什么,处理自己可以处理的部分 , 若无匹配上抛
void foo()
{
    try
    {
        func();
    }catch(int i)
    {
        cout<<"foo() catch "<<i<<endl;
    }catch(...) //若无匹配, 写日志上抛
    {
        cout<<"log throw up"<<endl;
        throw;
    }
} 
int main()
{
    try{
        foo();
    }catch(int i){
        cout<<"main() catch int "<<i<<endl;
    }catch(double i){
        cout<<"main() catch double "<<i<<endl;
    } 
    return 0;
}

栈自旋

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

Last modification:August 8th, 2019 at 03:47 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment