Practical cpp

try-throw-catch 三段式

这是异常抛出的方式,简单的用法便是在外部使用的try-catch形式:

//Code-1
int main()
{
    int get_num;
    std::cin >> get_num;
    try
    {
        if(get_num < 0)
            throw get_num
        //Something happen
    }
    catch(int e) //此处括号内的参数的类型和抛出的类型需要一致
    {
        std::cout << "The number " << get_num << " is negative"
                  <std::endl;
    }
    //...
}
  • 这是最简单的使用,在三段式中,try充当的是正常代码块,而catch则是对于抛出异常的处理。catch 可以不止一个,但一定要合理,实际来说,三段式能少用尽量少用,我们一般是这么使用三段式的:

    • 在函数中使用三段式,例如函数调用里throw一个异常
    • 在调用函数的地方的附近使用三段式获取一个异常
    • 异常被抛出后一定要有接收的catch,否则会默认调用unexcept(),从而紧接着默认调用terminate()终止程序。

      //Code-2
      //head.h
      #include <...> //各种必要的头文件 
      class ExceptionText{
      public:
              ExceptionTest(){}
              ExceptionTest(std::string err_mes): message(err_mes)    {}
              //调用接口
              std::string show_message(){
                  return message;
              }
      private:
              std::string message;
      };
      
      class ExceptionNumber{
      };
      

      这个类的作用在于抛出异常的时候,使用该类作为抛出对象的类型,throw可以抛出任意类型的异常。

      //Code-3
      //run.cpp
      #include <...>
      void AddFunction();
      void TheMainFunction(double args);
      
      int main()
      {
          int SomeThingYouWant = 0;
          std::cin >> SomeThingYouWant; 
          try
          {
              TheMainFunction(SomeThingYouWant);
          }
          catch(ExceptionText t)
          {
              std::cout << e.show_message();
              std::cout << std::endl;
          }
          catch(ExceptionNumber n) // 抛出类型可以只定义类型,而不加任何具体实现
          {
              std::cout << "The number is not match the logic"
                    << std::endl;
          }
          catch(...) //这是默认的catch,...并不代表无限的参数,而是默认的意思,就像switch中的default.
          {
              std::cout << "There has some thing bad happen to us";
          }
          return 0;
      }
      
  • 在三段式中,可以有很多个catch,并且可以拥有一个默认的catch,在上述代码中,只要抛出类型不是ExceptionTextExceptionNumber,抛出就会被默认catch所获取。

    //Code-4
    void TheMainFunction(double args)
    {
        double result;
        if(args < 0)
            throw ExceptionText("The number is negative");
        else if(args == 0)
            throw ExceptionText("The number is Zero");
        result = (args-1) / args;
    
        while(result > 0)
            result -= 0.01;
        if(result < 0)
            throw ExceptionNumber;
        else
            throw 0; //只要是ExceptionTextExceptionNumber之外的任意类型都可以。    
    }
    

    在TheMainFunction的定义里,可以添加一些限制条件,来限制抛出的类型:

    void TheMainFunction(double) throw(ExceptionText, ExceptionNumber)
    //...
    

    这样抛出类型就只有ExceptionTextExceptionNumber两种了。

  • 函数的级别的 try-catch

    • 所谓函数级别就是,使用在函数身上的 try

      //Code-5
      class BaseText{
          string plantText;
      public:
          class BaseException{}; //定义一个空类,来定义抛出类型
                                 //类似Java里的 nested 语法
          BaseTest(const char* msg = nullptr): plantText(msg){}
      //...
      class ExceptionText : public BaseText{
      public:
          ExceptionText(const char* msgError = nullptr) 
          throw(BaseException) //限定抛出类型只能是BaseException
          try : BaseText(msgError)
          {
              //这里是正常的构造函数的函数体
          }catch(BaseException&)
          {
              std::cout << "In the catch body!" << std::endl;
          }
      //...
      
    • 语法上, try 关键字紧跟在函数参数列表之后,如果是构造函数,则在初始化列表之前, tryblock 中就是正常的函数体。

  • 对于抛出类型的限定,尽量使用它。并且在获取异常的时候,使用引用接收它。这不仅考虑到效率,而且是因为考虑到了继承类的上切问题(例如一个继承了 logic_error 的异常类对象,使用 logic_error 进行按值传递,会导致信息丢失)。

  • 可以在构造函数中抛出异常(移动构造函数不要),并额外设置一个标志去检查它,但不要再析构函数中抛出异常!
  • 但是在 C++11之后,一个函数如果要限定其没有抛出,则不再使用 throw(),而使用新关键字 noexcept

    //Code-6
    //void noException() throw() { //C++98以前
    void noException() noexcept(true){ //C++11以后
                                       //此时 noexcept作为一个操作符存在
        for(volatile int i = 0;i < 3;++i)
            if(i == 2)
                throw "Thorw!But You will Never See those";
    }
    

    可以将该操作符用于模板,以此来决定一个函数是否为一个可抛出异常 的函数

虽然三段式看起来十分实用,但是我们依然应该尽可能的减少他们的使用频率。

标准 C++ 异常

  • 比起自己定义的异常处理,标准库自带的要更加快速
  • 头文件 <exception> <stdexcept>,后者 include 前者
  • 异常类 exception <- logic_errorruntime_error
    • 后两者继承自前者,通常用法是构造后两者的对象,设置错误信息,并调用其接口 what(),其中 what() 接口继承自类 exception, 但是由于 exception 的构造函数并没有提供接口让用户设置错误信息,故总是用其两个继承类
    • 具体用法有两种:
      1. 继承法,通过让自己的类继承类 logic_error 或者 runtime_error(或者其派生类) 来实现使用它们的目的
      2. 成员法,通过让自己的类拥有一个嵌套类,专门用来抛出错误信息,类中拥有 logic_error 或者 runtime_error(或者其派生类)的成员,并在必要时设置构造这个类对象
    • 异常处理参考网址,维基百科(无须翻墙)
    • 补充上述 logic_error 的 派生类
      1. bad_castdynamic_cast 失败之后,抛出的异常,与(RTTI机制有关) -> #include <typeinfo>
      2. bad_typeid 也与 RTTI 机制有关 -> #include <typeinfo>
    • 补充 runtime_error 的 派生类
      1. bad_alloc 如果 new 分配内存失败,则会抛出这个异常 -> #include <new>
    • 其余补充
    • 补充的继承类不再 <stdexcept> 中声明定义而在各自的头文件中声明定义。
    • C++11 之后的标准弃用了许多异常类,也重新有了许多的类。

模板

  • 模板的三种用法:

    1. 用于待定类型的占位:
    2. 用于定义维数
    3. 将模板类当成模板参数进行传递

      //Code-7
      template<typename tpOfPara, //通常的用法
               typename<typename secT, 
                        typename = std::allocator<secT>> class tpClName, //传递模板类作为模板参数
               std::size_t consDim = 100> //使用无类型的模板参数,且使用了默认参数
          class ClDefByMe{
              tpClName<tpOfPara> priMem_1;
              tpOfPara            priMem_2;
              int                testMem[consDim];
          pubic:
              explict ClDefByMe(const tpClName<tpOfPara>&, 
                                const tpOfPara&);
      //...
      ClDefByMe<std::string, std::vector> tmp1;
      
    • 用于类的时候,模板可以使用默认参数,但是用在函数的时候却不行。
    • 在第二句中,为了传递特定的模板容器 vector,其声明中包含了一个默认模板参数,在此处需要显式的写出,否则无法编译通过,allocator<memory> 中声明定义
  • 关键字 typename

    • C++11之后, typename 也可以用在模板参数中,之前只能使用 class 关键字,这两个关键字的效果一致
    • 在模板类的定义中,我们可以使用 typename 关键字来获取模板参数中的内嵌类型

      //Code-8
      class clFirst{
          int mem1;
      public:
          class nestcl{
              int mem2;
          public:
              void showMem() {std::cout << "mem2 = " << std::endl;}
          };
      }; //用于传递的类
      template<typename T>
          class ClDefByMe{
              typename T::nestcl tmpMem;
          public:
              void show(){ tmpMem.showMem(); }
          };
      //...
      ClDefByMe<clFirst> tmp;
      tmp.show();
      
      • 此时如果引用过来的不是内嵌类型,而是别名(typedef)的话,亦是可以。
      • 如果T::nestcl是类型 T的静态类型成员 就无需添加关键字 typename
      • 对于 模板函数 而言,这一性质同样起作用,常用来声明定义某种类型(STL容器)的 迭代器(Iterator),方便使用.
  • template 关键字的另类用处

    • 当在模板函数中调用一个模板类的成员函数,且该成员函数亦是模板的时候,需要使用template 关键字消除编译器的判定错误(将模板参数的左尖括号 < 当成小于运算符)。

      //Code-9
      //引用Thinking in C++ 例子
      //bitset例子
      template<typename charT, size_t N>
          basic_string<charT> bitsetToString(const bitset<N>& bs)
          {    
              return bs. template to_string<charT, 
                                            char_traits<charT>,
                                            allocator<charT>>();
          }
      //此时template关键字必不可少, -> 操作符也是如此
      //bitset
      
    • template 关键字不可少的原因在于,当模板函数被解析(parsed)之后,编译器会认为 to_string 右边的尖括号 < 是小于运算符,我们需要这个关键字来告诉编译器这是模板的语法。
  • 模板成员函数

    • 在模板类中声明定义模板成员函数,相当于嵌套。

      //Code-10
      template<typename typOne>
          class ClDefByMe{
          //...
          public:
              template<typename typTwo> 
                  ClDefByMe(const ClDefByMe<typTwo> &);
          ...
      //如果类外定义
      template <typename typOne>
          template<typename typTwo>
              ClDefByMe<typOne>::ClDefByMe(const ClDefByMe<typTwo> &)
              {
                  //...定义
              }
      
    • 模板类中的模板成员函数的模板参数不必和类一样。
    • 模板成员可以用在嵌套类上。

      //Code-11
      template<typename typOne>
          class ClDefByMe{
          //...
          public:
              template<typename typThr>
                  class inner{
                  //...
                  public:
                      void lookFunc();
                  };
          ...
      template<typename typOne> //定义 lookFunc
          template<typename typThr>
              ClDefByMe<typOne>::inner<typThr>::lookFunc()
              {
                  //...函数体
              }
      ...
      int main()
      {
          clDefByMe<int>::inner<long> tmpObj;
          tmpObj.lookFunc();
          return 0;
      }    
      
  • 但是,模板成员函数不可是 虚函数 (virtual) ,不过能够重载,重载的时候,可以让常规函数和模板函数混搭(即不一定需要模板重载模板,普通重载普通)。

    • 在选择哪个版本的函数方面,优先选择非模板函数,可以显式地强制调用模板函数版本通过在调用函数名后面,括号的前面,添加空参数的模板尖括号。
  • 模板函数常常可以省略模板参数,而交由编译器来推断所使用的参数类型,也可以自己显式传递。

    • 例如上方的 Code-9 中的模板函数 bitsetToString 的定义可以改成: return bs.to_string(); 产生的效果与此前代码一致。
    • 这种有编译器干的事情里,如果出现了需要类型转换的情况,则会失败。
    • 在操作数组的时候,不妨使用 数组的引用 来代替单一指针,因为这样可以让编译器帮我们推断数组的维数,而不自自己显式传递,例如( arrType (&arr)[9][10] )
    • 如果需要传递一个模板函数的地址,也可以借助编译器自动推断来省略显式的模板参数传递:

      //Code-12
      void knowType(void (*innerFunp)(int *)) {}
      template<typename T>
          void unkwType(T *) {} //一个返回值类型为 void, 参数为 T* 的函数
      //使用
      knowType(&unkwType<int>); //显式
      knowType(&unkwType); //编译器推断
      

      但是这种情况下在使用模板推断的时候,需要注意,如果该函数有重载版本的情况下,需要显式的指明需要哪个函数,否则会发生 推断错误 ,因为编译器没办法知道需要选择哪个版本的函数,即使明显是需要选择某个版本.

      解决方法可以是,类型转换(static_cast),或者重新写一个函数将某个特定的重载版本包裹起来.

  • 对于一个模板而言,它能够为不同类型生成不同的代码,同样,也可以人为先预设一些样板,就像一个建设网页的框架,总有人做好了模板,你可以使用别人的模板,也可以选择使用自己做模板

    • 比如 STL容器 vector 有一个生成好的特殊的版本 vector<bool>,优化了其的空间利用率。
    • 生成显式特殊化(Explicit Specialization)的方法是,在定义好基本模板之后

      //Code-13
      template<typename T, typename U>
          class tmplClass{
              T memOne;
              U memTwo;
          public:
              T getFstMem() const { return memOne; }
          };
      template<> //完全特殊化的语法
          class tmplClass<int, long>{ //显式的定义了一个 <int, long> 的版本
              int memOne;
              long memTwo;
          public:
              int getFstMem() const { return memOne; }
          };
      

      同样也支持有局部特殊化

      //Code-14
      template<typename U> // 部分特殊化的语法
          class tmplClass<float, U>{ //只给了第一个参数的类型
          ...                           //给的参数可以随意调整顺序。
      template<typename T, typename U>
          class tmplClass<T, U*> { //这也是局部特殊化
      

      此时,如果是在类外定义接口函数,那么需要注意的是,完全特殊化情况下,如果已经完全特殊化一个模板类,则在实现其特殊化类的接口函数时,不需要重新再写一遍 template<> 但是情况就需要:

      //Code-15
      //假设只有普通模板类
      template<typename T, typename U>
          class tmplClass{
              T memOne;
              U memTwo;
          public:
              T getFstMem() const; //想要类外实现一个特殊化的这个接口函数
          };
      //实现:
      template<>
          int tmplClass<int, long>::getFstMem() const
          {
              return memOne;
          }
      

      如果此时已经存在一个完全特殊化的模板类的时候:

      //Code-16
      //这个假设在已有一个普通的模板类为前提之上。
      template<> //完全特殊化的语法
          class tmplClass<int, long>{ //显式的定义了一个 <int, long> 的版本
              int memOne;
              long memTwo;
          public:
              int getFstMem() const; //定义它,只需要和普通类一样即可
          };
      //实现:
      int tmplClass<int, long>::getFstMem() const
      {
          return memOne;
      }
      
  • 使用模板会导致一个问题,就是 代码膨胀,对于每一个具体类型的实现都会有一段新的代码产生,而编译器对此的措施就是,只有用到的才会生成对应的代码,否则就不生成。

    • 例如,对于一个模板类而言,其成员函数只有被使用了,才会产生相应的代码,否则就当成摆设。
    • 其次,可以人为的避免过大的代码膨胀,例如使用之前提到的,完全/部分特殊化的方法。
      1. 当拥有一个基本的模板类时,我们可以在考虑后续使用的基础上,为其生成一个 完全特殊化 的类
      2. 使用这个类作为一个基类,产生一个继承体系,继承体系中的各个派生类便是我们真正会生成的类,派生类使用 部分特殊化 实现,继承之前的 完全特殊化 生的类
      3. 如此能够利用已经生成的代码,而不需要反复生成同样的代码。
      4. 继承 完全特殊化 类的 部分特殊化 类,需要对每一个接口函数重新定义(可以直接调用基类定义好的函数,通过域解析作用符::),因为继承的时候采用的是 private 继承。
      5. 最明显的用处便是当不同的类型是指针的时候,用 void* 生成完全特殊化基类,其他类型指针做派生类的对象。
  • 模板中的友元

    • 在模板类中使用友元函数

      1. 最简洁的写法便是将友元函数作为类内(in-class)函数声明且定义
      2. 否则,对于需要在类外进行定义的友元函数,只有将其声明为模板,且需要声明两次,定义一次,才能生效:顺序(1.模板类声明,2.模板友元函数声明,3.模板类定义,4.类定义的同时使用友元修饰符friend 重新声明该函数,5.类外定义该友元函数)

        //Code-17
        //非类内(in-class)写法
        template<typename T> class templClass;// 1.
        template<typename T> void show(const templClass<T>&);// 2.
        
        template<typename T> // 3。
            class templClass{
                 T x;
              public:
                templClass<T>(const T& para):x(para){}
                friend void show<>(const templClass<T> & fo); //4.
            };
        //类外定义友元函数
        template<typename T> // 5.
            void show(const templClass<T> & fo)
            {
                cout << fo.x << endl;
            }
        

        .

        //Code-18
        //类内(in-class)写法
        template<typename T> 
            class templClass{
                 T x;
              public:
                templClass<T>(const T& para):x(para){}
                friend void show<>(const templClass<T> & fo)
                {
                    cout << fo.x << endl;
                }
            };
        

转载注明: www.wushxin.top/2015/07/21/Practical-cpp.htm