C/C++学习之路: 模板和异常

C/C++学习之路: 模板和异常


目录

  1. 模板
  2. 类型转换
  3. 异常

1. 模板

1. 模板概述

  1. c++提供了函数模板(function template),函数模板实际上是建立一个通用函数,其函数类型和形参类型不具体制定,用一个虚拟的类型来代表,这个通用函数就成为函数模板。
  2. 凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。
  3. 在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
    1. c++提供两种模板机制:函数模板和类模板
    2. 类属 - 类型参数化,又称参数模板
  4. 总结:
    1. 模板把函数或类要处理的数据类型参数化,表现为参数的多态性,成为类属。
    2. 模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。

2. 函数模板

  1. 用模板是为了实现泛型,可以减轻编程的工作量,增强函数的重用性。
//交换int数据
void SwapInt(int& a,int& b){
    int temp = a;
    a = b;
    b = temp;
}

//交换char数据
void SwapChar(char& a,char& b){
    char temp = a;
    a = b;
    b = temp;
}
//问题:如果我要交换double类型数据,那么还需要些一个double类型数据交换的函数
//繁琐,写的函数越多,当交换逻辑发生变化的时候,所有的函数都需要修改,无形当中增加了代码的维护难度

//如果能把类型作为参数传递进来就好了,传递int就是Int类型交换,传递char就是char类型交换
//我们有一种技术,可以实现类型的参数化---函数模板


//class 和 typename都是一样的,用哪个都可以
template<class T>
void MySwap(T& a,T& b){
    T temp = a;
    a = b;
    b = temp;
}

void test01(){

    int a = 10;
    int b = 20;
    cout << "a:" << a << " b:" << b << endl;
    //1. 这里有个需要注意点,函数模板可以自动推导参数的类型
    MySwap(a,b);
    cout << "a:" << a << " b:" << b << endl;

    char c1 = 'a';
    char c2 = 'b';
    cout << "c1:" << c1 << " c2:" << c2 << endl;
    //2. 函数模板可以自动类型推导,那么也可以显式指定类型
    MySwap<char>(c1, c2);
    cout << "c1:" << c1 << " c2:" << c2 << endl;
}

3. 函数模板和普通函数区别

  1. 函数模板不允许自动类型转化
  2. 普通函数能够自动进行类型转化
//函数模板
template<class T>
T MyPlus(T a, T b){
    T ret = a + b;
    return ret;
}

//普通函数
int MyPlus(int a,char b){
    int ret = a + b;
    return ret;
}

void test02(){

    int a = 10;
    char b = 'a';

    //调用函数模板,严格匹配类型
    MyPlus(a, a);
    MyPlus(b, b);
    //调用普通函数
    MyPlus(a, b);
    //调用普通函数  普通函数可以隐式类型转换
    MyPlus(b, a);

    //结论:
    //函数模板不允许自动类型转换,必须严格匹配类型
    //普通函数可以进行自动类型转换
}

4. 函数模板和普通函数在一起调用规则

  1. c++编译器优先考虑普通函数
  2. 可以通过空模板实参列表的语法限定编译器只能通过模板匹配
  3. 函数模板可以像普通函数那样可以被重载
  4. 如果函数模板可以产生一个更好的匹配,那么选择模板
//函数模板
template<class T>
T MyPlus(T a, T b) {
    T ret = a + b;
    return ret;
}

//普通函数
int MyPlus(int a, int b) {
    int ret = a + b;
    return ret;
}

void test03() {
    int a = 10;
    int b = 20;
    char c = 'a';
    char d = 'b';
    //如果函数模板和普通函数都能匹配,c++编译器优先考虑普通函数
    cout << MyPlus(a, b) << endl;
    //如果我必须要调用函数模板,那么怎么办?
    cout << MyPlus<>(a, b) << endl;
    //此时普通函数也可以匹配,因为普通函数可以自动类型转换
    //但是此时函数模板能够有更好的匹配
    //如果函数模板可以产生一个更好的匹配,那么选择模板
    cout << MyPlus(c, d);
}

5. 模板机制解析

  1. 编译器并不是把函数模板处理成能够处理任何类型的函数
  2. 函数模板通过具体类型产生不同的函数
  3. 编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

6. 模板的局限性

  1. 假设有如下模板函数:
	template<class T>
	void f(T a, T b)
	{}
  1. 如果代码实现时定义了赋值操作 a = b,但是T为数组,这种假设就不成立了
  2. 同样,如果里面的语句为判断语句 if(a>b),但T如果是结构体,该假设也不成立,另外如果是传入的数组,数组名为地址,因此它比较的是地址,而这也不是我们所希望的操作。
  3. 总之,编写的模板函数很可能无法处理某些类型,另一方面,有时候通用化是有意义的,但C++语法不允许这样做。为了解决这种问题,可以提供模板的重载,为这些特定的类型提供具体化的模板。

7. 类模板

  1. 类模板和函数模板的定义和使用类似,有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同。
  2. 类模板用于实现类所需数据的类型参数化
1. 类模板做函数参数
template<class NameType, class AgeType>
class Person {
public:
    Person(NameType name, AgeType age) {
        this->mName = name;
        this->mAge = age;
    }

    void showPerson() {
        cout << "name: " << this->mName << " age: " << this->mAge << endl;
    }

public:
    NameType mName;
    AgeType mAge;
};

void test01() {
    //Person P1("德玛西亚",18); // 类模板不能进行类型自动推导 
    Person<string, int> P1("德玛西亚", 18);
    P1.showPerson();
}
2. 类模板派生普通类
template<class T>
class MyClass {
public:
    MyClass(T property) {
        this->mProperty = property;
    }

public:
    T mProperty;
};

//子类实例化的时候需要具体化的父类,子类需要知道父类的具体类型是什么样的
//这样c++编译器才能知道给子类分配多少内存

//普通派生类
class SubClass : public MyClass<int> {
public:
    SubClass(int b) : MyClass<int>(20) {
        this->mB = b;
    }

public:
    int mB;
};
3. 类模板派生类模板
template<class T>
class Base {
    T m;
};

template<class T>
class Child2 : public Base<double> { //继承类模板的时候,必须要确定基类的大小
public:
    T mParam;
};

void test2() {
    Child2<int> d2;
}
4. 类模板类内实现
template<class NameType, class AgeType>
class Person {
public:
    Person(NameType name, AgeType age) {
        this->mName = name;
        this->mAge = age;
    }

    void showPerson() {
        cout << "name: " << this->mName << " age: " << this->mAge << endl;
    }

public:
    NameType mName;
    AgeType mAge;
};

void test01() {
    //Person P1("德玛西亚",18); // 类模板不能进行类型自动推导 
    Person<string, int> P1("德玛西亚", 18);
    P1.showPerson();
}
5. 类模板类外实现
template<class T1, class T2>
class Person {
public:
    Person(T1 name, T2 age);

    void showPerson();

public:
    T1 mName;
    T2 mAge;
};

//类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
    this->mName = name;
    this->mAge = age;
}

template<class T1, class T2>
void Person<T1, T2>::showPerson() {
    cout << "Name:" << this->mName << " Age:" << this->mAge << endl;
}

void test4() {
    Person<string, int> p("Obama", 20);
    p.showPerson();
}
6. 模板类遇到友元函数
template<class T1, class T2>
class Person;

//告诉编译器这个函数模板是存在
template<class T1, class T2>
void PrintPerson2(Person<T1, T2> &p);

//友元函数在类内实现
template<class T1, class T2>
class Person {
    //1. 友元函数在类内实现
    friend void PrintPerson(Person<T1, T2> &p) {
        cout << "Name:" << p.mName << " Age:" << p.mAge << endl;
    }

    //2.友元函数类外实现
    //告诉编译器这个函数模板是存在
    friend void PrintPerson2<>(Person<T1, T2> &p);

    //3. 类模板碰到友元函数模板
    template<class U1, class U2>
    friend void PrintPerson(Person<U1, U2> &p);

public:
    Person(T1 name, T2 age) {
        this->mName = name;
        this->mAge = age;

    }

    void showPerson() {
        cout << "Name:" << this->mName << " Age:" << this->mAge << endl;
    }

private:
    T1 mName;
    T2 mAge;
};

void test01() {
    Person<string, int> p("Jerry", 20);
    PrintPerson(p);
}


// 类模板碰到友元函数
//友元函数类外实现  加上<>空参数列表,告诉编译去匹配函数模板
template<class T1, class T2>
void PrintPerson2(Person<T1, T2> &p) {
    cout << "Name2:" << p.mName << " Age2:" << p.mAge << endl;
}

void test02() {
    Person<string, int> p("Jerry", 20);
    PrintPerson2(p);   //不写可以编译通过,写了之后,会找PrintPerson2的普通函数调用,因为写了普通函数PrintPerson2的声明	
}

int main() {

    //test01();
    test02();
    system("pause");
    return EXIT_SUCCESS;
}

2. 类型转换

  1. 类型转换(cast)是将一种数据类型转换成另一种数据类型。
  2. 例如,如果将一个整型值赋给一个浮点类型的变量,编译器会暗地里将其转换成浮点类型。
  3. 转换是非常有用的,但是它也会带来一些问题,比如在转换指针时,我们很可能将其转换成一个比它更大的类型,但这可能会破坏其他的数据。
  4. 所以应该小心类型转换,因为转换也就相当于对编译器说:忘记类型检查,把它看做其他的类型。
  5. 一般情况下,尽量少的去使用类型转换,除非用来解决非常特殊的问题。
  6. 标准c++提供了一个显示的转换的语法,来替代旧的C风格的类型转换。
  7. 使用C风格的强制转换可以把想要的任何东西转换成我们需要的类型。那为什么还需要一个新的C++类型的强制转换呢?
  8. 新类型的强制转换可以提供更好的控制强制转换过程,允许控制各种不同种类的强制转换。C++风格的强制转换其他的好处是,它们能更清晰的表明它们要干什么。程序员只要扫一眼这样的代码,就能立即知道一个强制转换的目的。

1. 静态转换(static_cast)

  1. 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
  2. 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
  3. 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  4. 用于基本数据类型之间的转换,如把int转换成char,把char转换成int。这种转换的安全性也要开发人员来保证。
class Animal {
};

class Dog : public Animal {
};

class Other {
};

//基础数据类型转换
void test01() {
    char a = 'a';
    double b = static_cast<double>(a);
}

//继承关系指针互相转换
void test02() {
    //继承关系指针转换
    Animal *animal01 = NULL;
    Dog *dog01 = NULL;
    //子类指针转成父类指针,安全
    Animal *animal02 = static_cast<Animal *>(dog01);
    //父类指针转成子类指针,不安全
    Dog *dog02 = static_cast<Dog *>(animal01);
}

//继承关系引用相互转换
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);
}

//无继承关系指针转换
void test04() {

    Animal *animal01 = NULL;
    Other *other01 = NULL;

    //转换失败
    //Animal* animal02 = static_cast<Animal*>(other01);
}

2. 动态转换

  1. dynamic_cast主要用于类层次间的上行转换和下行转换;
  2. 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
  3. 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_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);
}

3. 常量转换(const_cast)

  1. const_cast运算符用来修改类型的const属性。
  2. 常量指针被转化成非常量指针,并且仍然指向原来的对象;
  3. 常量引用被转换成非常量引用,并且仍然指向原来的对象;
  4. 注意:不能直接对非指针和非引用的变量使用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);
}

4. 重新解释转换(reinterpret_cast)

  1. 这是最不安全的一种转换机制,最有可能出问题。
  2. 主要用于将一种数据类型从一种类型转换为另一种类型。
  3. 它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针。

3. 异常

1. 异常基本概念

  1. 异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0溢出,数组下标越界,所要读取的文件不存在,空指针,内存不足等等)
  2. c++异常机制相比C语言异常处理的优势?
    1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
    2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
    3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
  3. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
//如果判断返回值,那么返回值是错误码还是结果?
//如果不判断返回值,那么b==0时候,程序结果已经不正确
//A写的代码
int A_MyDivide(int a, int b) {
    if (b == 0) {
        return -1;
    }

    return a / b;
}

//B写的代码
int B_MyDivide(int a, int b) {

    int ba = a + 100;
    int bb = b;

    int ret = A_MyDivide(ba, bb);  //由于B没有处理异常,导致B结果运算错误

    return ret;
}

//C写的代码
int C_MyDivide() {

    int a = 10;
    int b = 0;

    int ret = B_MyDivide(a, b); //更严重的是,由于B没有继续抛出异常,导致C的代码没有办法捕获异常
    if (ret == -1) {
        return -1;
    } else {
        return ret;
    }
}

//所以,我们希望:
//1.异常应该捕获,如果你捕获,可以,那么异常必须继续抛给上层函数,你不处理,不代表你的上层不处理
//2.这个例子,异常没有捕获的结果就是运行结果错的一塌糊涂,结果未知,未知的结果程序没有必要执行下去

2. 异常语法

int A_MyDivide(int a, int b) {
    if (b == 0) {
        throw 0;
    }

    return a / b;
}

//B写的代码 B写代码比较粗心,忘记处理异常
int B_MyDivide(int a, int b) {

    int ba = a;
    int bb = b;

    int ret = A_MyDivide(ba, bb) + 100;  //由于B没有处理异常,导致B结果运算错误

    return ret;
}

//C写的代码
int C_MyDivide() {

    int a = 10;
    int b = 0;

    int ret = 0;

//没有处理异常,程序直接中断执行
#if 1
    ret = B_MyDivide(a, b);

//处理异常
#else
    try{
        ret = B_MyDivide(a, b); //更严重的是,由于B没有继续抛出异常,导致C的代码没有办法捕获异常
    }
    catch (int e){
        cout << "C_MyDivide Call B_MyDivide 除数为:" << e << endl;
    }
#endif

    return ret;
}

int main() {

    C_MyDivide();

    system("pause");
    return EXIT_SUCCESS;
}
  1. 若有异常则通过throw操作创建一个异常对象并抛出。
  2. 将可能抛出异常的程序段放到try块之中。
  3. 如果在try段执行期间没有引起异常,那么跟在try后面的catch字句就不会执行。
  4. catch子句会根据出现的先后顺序被检查,匹配的catch语句捕获并处理异常(或继续抛出异常)
  5. 如果匹配的处理未找到,则运行函数terminate将自动被调用,其缺省功能调用abort终止程序。
  6. 处理不了的异常,可以在catch的最后一个分支,使用throw,向上抛。
  7. c++异常处理使得异常的引发和异常的处理不必在一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。
1. 异常严格类型匹配
  1. 异常机制和函数机制互不干涉,但是捕捉方式是通过严格类型匹配。
void TestFunction() {

    cout << "开始抛出异常..." << endl;
    //throw 10; //抛出int类型异常
    //throw 'a'; //抛出char类型异常
    //throw "abcd"; //抛出char*类型异常
    string ex = "string exception!";
    throw ex;

}

int main() {

    try {
        TestFunction();
    }
    catch (int) {
        cout << "抛出Int类型异常!" << endl;
    }
    catch (char) {
        cout << "抛出Char类型异常!" << endl;
    }
    catch (char *) {
        cout << "抛出Char*类型异常!" << endl;
    }
    catch (string) {
        cout << "抛出string类型异常!" << endl;
    }
        //捕获所有异常
    catch (...) {
        cout << "抛出其他类型异常!" << endl;
    }


    system("pause");
    return EXIT_SUCCESS;
}
2. 栈解旋(unwinding)
  1. 异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding).
class Person {
public:
    Person(string name) {
        mName = name;
        cout << mName << "对象被创建!" << endl;
    }

    ~Person() {
        cout << mName << "对象被析构!" << endl;
    }

public:
    string mName;
};

void TestFunction() {

    Person p1("aaa");
    Person p2("bbb");
    Person p3("ccc");

    //抛出异常
    throw 10;
}

int main() {

    try {
        TestFunction();
    }
    catch (...) {
        cout << "异常被捕获!" << endl;
    }

    system("pause");
    return EXIT_SUCCESS;
}
3. 异常接口声明
  1. 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类型的异常。
  2. 如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()
  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;
}
4. 异常变量生命周期
  1. throw的异常是有类型的,可以是数字、字符串、类对象。
  2. throw的异常是有类型的,catch需严格匹配异常类型。
class MyException {
public:
    MyException() {
        cout << "异常变量构造" << endl;
    };

    MyException(const MyException &e) {
        cout << "拷贝构造" << endl;
    }

    ~MyException() {
        cout << "异常变量析构" << endl;
    }
};

void DoWork() {
    throw new MyException(); //test1 2都用 throw MyExecption();
}

void test01() {
    try {
        DoWork();
    }
    catch (MyException e) {
        cout << "捕获 异常" << endl;
    }
}

void test02() {
    try {
        DoWork();
    }
    catch (MyException &e) {
        cout << "捕获 异常" << endl;
    }
}

void test03() {
    try {
        DoWork();
    }
    catch (MyException *e) {
        cout << "捕获 异常" << endl;
        delete e;
    }
}
5. 异常的多态使用
//异常基类
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();
}

void test() {
    try {
        doWork();
    }
    catch (BaseException &ex) {
        ex.printError();
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值