C++:模板,string类,异常

来源:互联网 发布:电子风水罗盘软件下载 编辑:程序博客网 时间:2024/05/22 15:45

函数模板
在C++中,数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。
值(Value)和类型(Type)是数据的两个主要特征,它们在C++中都可以被参数化。
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
v在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。
一但定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说得直白一点,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替
声明类模板的语法为:
template<typename 类型参数1 , typename 类型参数2 , …> class 类名{
    //TODO:
};
类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。
一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替
在类外定义成员函数时仍然需要带上模板头,格式为:
template<typename 类型参数1 , typename 类型参数2 , …>
返回值类型 类名<类型参数1 , 类型参数2, ...>::函数名(形参列表){
    //TODO:
}
第一行是模板头,第二行是函数头,它们可以合并到一行,不过为了让代码格式更加清晰,一般是将它们分成两行。
当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。

string类
vC++大大增强了对字符串的支持,除了可以使用C风格的字符串,还可以使用内置的数据类型 string。string 类处理起字符串来会方便很多,完全可以代替C语言中的 char 数组或 char 指针。
使用 string 类需要包含头文件 <string>,下面我们逐一介绍该类的功能。
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s1;
    string s2 = "c plus plus";
    string s3 = s2;
    string s4 (5, 's');
    return 0;
}
本例介绍了几种定义 string 类型变量的方法。变量 s1 只是定义但没有初始化,编译器会将默认值赋给 s1,默认值是""(空字符串)。变量 s2 在定义的同时被初始化为"c plus plus"。与C风格的 char 字符串不同,string 类型的变量结尾没有 '\0',string 类型的本质是一个 string 类,而我们定义的变量则是一个个的 string 类的对象。变量 s3 在定义的时候直接用 s2 进行初始化,因此 s3 的内容也是"c plus plus"。变量 s4 被初始化为由 5 个 's' 字符组成的字符串,也就是 "sssss"。
从上面的代码可以看出,string 变量可以直接通过赋值操作符“=”进行赋值。string 变量也可以用C风格的字符串进行赋值,例如,s2 是用一个字符串常量进行初始化的,而 s3 则是通过 s2 变量进行初始化的。
与C风格的字符串不同,当我们需要知道字符串长度时,可以调用 string 类提供的 length() 函数。如下所示:
string s = "c plus plus";
int len = s.length();
cout<<len<<endl;
虽然C++提供了 string 类来替代C语言中的 char 数组形式的字符串,但在编程中有时必须要使用C风格的字符串,为此,string 类为我们提供了一个转换函数 c_str(),该函数能够将 string 变量转换为一个 const 字符串数组的形式,并将指向该数组的指针返回。请看下面的代码:
string a = "123";
char str[20];
strcpy_s(str,a.c_str());
cout<<str<<endl;
string 类重载了输入输出运算符,可以像对待普通变量那样对待 string 类型变量,也就是用“>>”进行输入,用“<<”进行输出。请看下面的代码:
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    cin>>s;  //输入字符串
    cout<<s<<endl;  //输出字符串
    return 0;
}
string 字符串也可以像字符串数组一样按照下标来访问其中的每一个字符。string 字符串的起始下标仍是从 0 开始。请看下面的代码:
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s1 ;
    s1 = "1234567890";
    for(int i=0, len=s1.length(); i<len; i++)
        cout<<s1[i]<<" ";
    cout<<endl;
    s1[5] = '5';
    cout<<s1<<endl;
    return 0;
}
有了 string 类,我们可以使用”+“或”+=“运算符来直接拼接字符串,非常方便,再也不需要使用C语言中的 strcat()、strcpy()、malloc() 等函数来拼接字符串了,再也不用担心空间不够会溢出了。

用”+“来拼接字符串时,运算符的两边可以都是 string 字符串,也可以是一个 string 字符串和一个C风格的字符串,还可以是一个 string 字符串和一个 char 字符。请看下面的例子:
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s1, s2, s3;
    s1 = "first";s2 = "second";
    s3 = s1 + s2;cout<< s3 <<endl;
    s2 += s1; cout<< s2 <<endl;
    s1 += "third";cout<< s1 <<endl;
    s1 += 'a';cout<< s1 <<endl;
    return 0;
}
insert() 函数可以在 string 字符串中指定的位置插入另一个字符串,它的一种原型为:
string& insert (size_t pos, const string& str);
erase() 函数可以删除 string 变量中的一个子字符串。它的一种原型为:

string& erase (size_t pos = 0, size_t len = npos);
substr() 函数用于从 string 字符串中提取子字符串,它的原型为:
string substr (size_t pos = 0, size_t len = npos) const;
pos 为要提取的子字符串的起始下标,len 为要提取的子字符串的长度。
string 类提供了几个与字符串查找有关的函数,如下所示。
find() 函数用于在 string 字符串中查找子字符串出现的位置,它其中的两种原型为:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;

rfind() 函数
rfind() 和 find() 很类似,同样是在字符串中查找子字符串,不同的是 find() 函数从第二个参数开始往后查找,而 rfind() 函数则最多查找到第二个参数处,如果到了第二个参数所指定的下标还没有找到子字符串,则返回一个无穷大值4294967295。

异常处理
编译器能够保证代码的语法是正确的,但是对逻辑错误和运行时错误却无能为力,例如除数为 0、内存分配失败、数组越界等。这些错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。

优秀的程序员能够从故障中恢复,或者提示用户发生了什么;不负责任的程序员放任不管,让程序崩溃。C++提供了异常机制,让我们能够捕获逻辑错误和运行时错误,并作出进一步的处理。
一个程序崩溃的例子:
#include <iostream>
using namespace std;
int main(){
    string str = "c plus plus";
    char ch1 = str[100];  //下标越界,ch1为垃圾值
    cout<<ch1<<endl;
    char ch2 = str.at(100);  //下标越界,抛出异常
    cout<<ch2<<endl;
    return 0;
}
at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与“[ ]”不同,at() 会检查下标是否越界,如果越界就抛出一个异常(错误);而“[ ]”不做检查,不管下标是多少都会照常访问。
上面的代码中,下标 100 显然超出了字符串 str 的长度。由于第 6 行代码不会检查下标越界,虽然有逻辑错误,但是程序能够正常运行。而第 8 行代码则不同,at() 函数检测到下标越界会抛出一个异常(也就是报错),这个异常本应由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,终止程序执行。
在C++中,我们可以捕获上面的异常,避免程序崩溃。捕获异常的语法为:
try{
    // 可能抛出异常的语句
}catch(异常类型){
    // 处理异常的语句
}
vtry 和 catch 都是C++中的关键字,后跟语句块,不能省略“{ }”。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被捕获。从“try”的意思可以看出,它只是“尝试”捕获异常,如果没有异常抛出,那就什么也不捕获。catch 用来处理 try 捕获到的异常;如果 try 没有捕获到异常,就不会执行 catch 中的语句。
#include <iostream>
using namespace std;
int main(){
    string str = "c plus plus";
  
    try{
        char ch1 = str[100];
        cout<<ch1<<endl;
    }catch(exception e){
        cout<<"[1]out of bound!"<<endl;
    }
try{
        char ch2 = str.at(100);
        cout<<ch2<<endl;
    }catch(exception e){
        cout<<"[2]out of bound!"<<endl;
    }
    return 0;
}
所谓抛出异常,实际上是创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来该怎么处理
异常既然是一份数据,那么就应该有数据类型。C++规定,异常类型可以是基本类型,也可以是标准库中类的类型,还可以是自定义类的类型。C++语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的类型。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
异常被捕获后,会和 catch 所能处理的类型对比,如果正好和 catch 类型匹配,或者是它的子类,那么就交给当前 catch 块处理。catch 后面的括号中给出的类型就是它所能处理的异常类型。上面例子中,catch 所能处理的异常类型是 exception,at() 函数抛出的类型是 out_of_range,out_of_range 是 exception 的子类,所以就交给这个 catch 块处理。
catch 后面的exception e可以分为两部分:exception 为异常类型,e 为 exception 类的对象。异常抛出时,系统会创建 out_of_range 对象,然后将该对象作为“实参”,像函数一样传递给“形参”e,这样,在 catch 块中就可以使用 e 了。
其实,一个 try 后面可以跟多个 catch,形式为:
try
{
    //可能抛出异常的语句
}
catch (exception_type_1)
{
    //处理异常的语句
}
catch (exception_type_2)
{
    //处理异常的语句
}
// ……
catch (exception_type_n)
{
    //处理异常的语句
}
异常被捕获时,先和 exception_type_1 作比较,如果异常类型是 exception_type_1 或其子类,那么执行当前 catch 中的代码;如果不是,再和 exception_type_2 作比较……依此类推,直到 exception_type_n。如果最终也没有找到匹配的类型,就只能交给系统处理,终止程序。
throw 是C++中的关键字,用来抛出异常。如果不使用 throw 关键字,try 就什么也捕获不到;上节提到的 at() 函数在内部也使用了 throw 关键字来抛出异常。
throw 既可以用在标准库中,也可以用在自定义的函数中,抛出我们期望的异常。throw 关键字语法为:
throw exceptionData;
exceptionData 是“异常数据”的意思,它既可以是一个普通变量,也可以是一个对象,只要能在 catch 中匹配就可以。
C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。你可以通过下面的语句来匹配所有标准异常:
try{
    //可能抛出异常的语句
}catch(exception &e){
    //处理异常的语句
}
之所以使用引用,是为了提高效率。如果不使用引用,就要经历一次对象拷贝(拷贝对象时要调用拷贝构造函数)的过程。

异常名称说 明logic_error逻辑错误。runtime_error运行时错误。bad_alloc使用 new 或 new[ ] 分配内存失败时抛出的异常。bad_typeid使用 typeid 操作一个 NULL 指针,而且该指针是带有虚函数的类,这时抛出 bad_typeid 异常。bad_cast使用 dynamic_cast 转换失败时抛出的异常。ios_base::failureio 过程中出现的异常。bad_exception这是个特殊的异常,如果函数的异常列表里声明了 bad_exception 异常,当函数内部抛出了异常列表中没有的异常时,如果调用的 unexpected() 函数中抛出了异常,不论什么类型,都会被替换为 bad_exception 类型。

异常名称说 明length_error试图生成一个超出该类型最大长度的对象时抛出该异常,例如 vector 的 resize 操作。domain_error参数的值域错误,主要用在数学函数中,例如使用一个负值调用只能操作非负数的函数。out_of_range超出有效范围。invalid_argument参数不合适。在标准库中,当利用string对象构造 bitset 时,而 string 中的字符不是 ’0’ 或 ’1’ 的时候,抛出该异常。

range_error计算结果超出了有意义的值域范围。overflow_error算术计算上溢。underflow_error算术计算下溢。