美文网首页c++
C++简明教程(三)

C++简明教程(三)

作者: leifuuu | 来源:发表于2019-08-07 00:34 被阅读0次

    本文内容来自菜鸟教程, C++教程,该篇内容仅作为笔记使用

    继承

    基类 & 派生类

    一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。

    定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

    class derived-class: access-specifier base-class
    

    访问修饰符 access-specifier 是 public、protectedprivate 其中的一个,base-class 是之前定义过的某个类的名称。

    如果未使用访问修饰符 access-specifier,则默认为 private。

    // 派生类
    class Rectangle: public Shape
    

    访问控制和继承

    派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

    我们可以根据访问权限总结出不同的访问类型,如下所示:

    访问 public protected private
    同一个类 yes yes yes
    派生类 yes yes no
    外部的类 yes no no

    一个派生类继承了所有的基类方法,但下列情况除外:

    • 基类的构造函数、析构函数和拷贝构造函数。
    • 基类的重载运算符。
    • 基类的友元函数。

    继承类型

    当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。

    我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

    • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
    • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
    • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

    多继承

    多继承即一个子类可以有多个父类,它继承了多个父类的特性。

    C++ 类可以从多个类继承成员,语法如下:

    class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
    {
    <派生类类体>
    };
    

    重载运算符和重载函数

    C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

    重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。

    当您调用一个重载函数重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策

    函数重载

    在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。

    下面的实例中,同名函数 print() 被用于输出不同的数据类型:

    #include <iostream>
    using namespace std;
     
    class printData
    {
       public:
          void print(int i) {
            cout << "整数为: " << i << endl;
          }
     
          void print(double  f) {
            cout << "浮点数为: " << f << endl;
          }
     
          void print(char c[]) {
            cout << "字符串为: " << c << endl;
          }
    };
     
    int main(void)
    {
       printData pd;
     
       // 输出整数
       pd.print(5);
       // 输出浮点数
       pd.print(500.263);
       // 输出字符串
       char c[] = "Hello C++";
       pd.print(c);
     
       return 0;
    }
    

    运算符重载

    您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。

    重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

    Box operator+(const Box&);
    

    声明加法运算符用于把两个 Box 对象相加,返回最终的 Box 对象。大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。

    如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数,如下所示:

    Box operator+(const Box&, const Box&);
    

    下面的实例使用成员函数演示了运算符重载的概念。在这里,对象作为参数进行传递,对象的属性使用 this 运算符进行访问,如下所示:

    #include <iostream>
    using namespace std;
     
    class Box
    {
       public:
     
          double getVolume(void)
          {
             return length * breadth * height;
          }
          void setLength( double len )
          {
              length = len;
          }
     
          void setBreadth( double bre )
          {
              breadth = bre;
          }
     
          void setHeight( double hei )
          {
              height = hei;
          }
          // 重载 + 运算符,用于把两个 Box 对象相加
          Box operator+(const Box& b)
          {
             Box box;
             box.length = this->length + b.length;
             box.breadth = this->breadth + b.breadth;
             box.height = this->height + b.height;
             return box;
          }
       private:
          double length;      // 长度
          double breadth;     // 宽度
          double height;      // 高度
    };
    // 程序的主函数
    int main( )
    {
       Box Box1;                // 声明 Box1,类型为 Box
       Box Box2;                // 声明 Box2,类型为 Box
       Box Box3;                // 声明 Box3,类型为 Box
       double volume = 0.0;     // 把体积存储在该变量中
     
       // Box1 详述
       Box1.setLength(6.0); 
       Box1.setBreadth(7.0); 
       Box1.setHeight(5.0);
     
       // Box2 详述
       Box2.setLength(12.0); 
       Box2.setBreadth(13.0); 
       Box2.setHeight(10.0);
     
     
       // 把两个对象相加,得到 Box3
       Box3 = Box1 + Box2;
     
       // Box3 的体积
       volume = Box3.getVolume();
       cout << "Volume of Box3 : " << volume <<endl;
     
       return 0;
    }
    

    可重载运算符/不可重载运算符

    可重载的运算符列表

    双目算术运算符 + (加),-(减),*(乘),/(除),% (取模)
    关系运算符 ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于)
    逻辑运算符 ||(逻辑或),&&(逻辑与),!(逻辑非)
    单目运算符 + (正),-(负),*(指针),&(取地址)
    自增自减运算符 ++(自增),--(自减)
    位运算符 | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
    赋值运算符 =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
    空间申请与释放 new, delete, new[ ] , delete[]
    其他运算符 ()(函数调用),->(成员访问),,(逗号),[](下标)

    不可重载的运算符列表

    • .:成员访问运算符
    • .\*, ->\*:成员指针访问运算符
    • :::域运算符
    • sizeof:长度运算符
    • ?::条件运算符
    • #: 预处理符号

    运算符重载实例

    下面提供了各种运算符重载的实例,帮助您更好地理解重载的概念。

    序号 运算符和实例
    1 一元运算符重载
    2 二元运算符重载
    3 关系运算符重载
    4 输入/输出运算符重载
    5 ++ 和 -- 运算符重载
    6 赋值运算符重载
    7 函数调用运算符 () 重载
    8 下标运算符[] 重载
    9 类成员访问运算符 -> 重载

    值得注意的是:

    1、运算重载符不可以改变语法结构。

    2、运算重载符不可以改变操作数的个数。

    3、运算重载符不可以改变优先级。

    4、运算重载符不可以改变结合性。

    类重载、覆盖、重定义之间的区别:

    重载指的是函数具有的不同的参数列表,而函数名相同的函数。重载要求参数列表必须不同,比如参数的类型不同、参数的个数不同、参数的顺序不同。如果仅仅是函数的返回值不同是没办法重载的,因为重载要求参数列表必须不同。(发生在同一个类里)

    覆盖是存在类中,子类重写从基类继承过来的函数。被重写的函数不能是static的。必须是virtual的。但是函数名、返回值、参数列表都必须和基类相同(发生在基类和子类)

    重定义也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。(发生在基类和子类)

    一元运算符重载

    一元运算符只对一个操作数进行操作,下面是一元运算符的实例:

    一元运算符通常出现在它们所操作的对象的左边,比如 !obj、-obj 和 ++obj,但有时它们也可以作为后缀,比如 obj++ 或 obj--。

    下面的实例演示了如何重载一元减运算符( - )。

    // 重载负运算符( - )
    Distance operator- ()  
    {
        feet = -feet;
        inches = -inches;
        return Distance(feet, inches);
    }
    

    二元运算符重载

    二元运算符需要两个参数,下面是二元运算符的实例。我们平常使用的加运算符( + )、减运算符( - )、乘运算符( * )和除运算符( / )都属于二元运算符。就像加(+)运算符。

    下面的实例演示了如何重载加运算符( + )。类似地,您也可以尝试重载减运算符( - )和除运算符( / )。

    // 重载 + 运算符,用于把两个 Box 对象相加
    Box operator+(const Box& b)
    {
        Box box;
        box.length = this->length + b.length;
        box.breadth = this->breadth + b.breadth;
        box.height = this->height + b.height;
        return box;
    }
    
    

    关系运算符重载

    C++ 语言支持各种关系运算符( < 、 > 、 <= 、 >= 、 == 等等),它们可用于比较 C++ 内置的数据类型。

    您可以重载任何一个关系运算符,重载后的关系运算符可用于比较类的对象。

    下面的实例演示了如何重载 < 运算符,类似地,您也可以尝试重载其他的关系运算符。

    // 重载小于运算符( < )
    bool operator <(const Distance& d)
    {
        if(feet < d.feet)
        {
            return true;
        }
        if(feet == d.feet && inches < d.inches)
        {
            return true;
        }
        return false;
    }
    

    输入/输出运算符重载

    C++ 能够使用流提取运算符 >> 和流插入运算符 << 来输入和输出内置的数据类型。您可以重载流提取运算符和流插入运算符来操作对象等用户自定义的数据类型。

    在这里,有一点很重要,我们需要把运算符重载函数声明为类的友元函数,这样我们就能不用创建对象而直接调用函数。

    #include <iostream>
    using namespace std;
     
    class Distance
    {
       private:
          int feet;             // 0 到无穷
          int inches;           // 0 到 12
       public:
          // 所需的构造函数
          Distance(){
             feet = 0;
             inches = 0;
          }
          Distance(int f, int i){
             feet = f;
             inches = i;
          }
          friend ostream &operator<<( ostream &output, 
                                           const Distance &D )
          { 
             output << "F : " << D.feet << " I : " << D.inches;
             return output;            
          }
     
          friend istream &operator>>( istream  &input, Distance &D )
          { 
             input >> D.feet >> D.inches;
             return input;            
          }
    };
    int main()
    {
       Distance D1(11, 10), D2(5, 11), D3;
     
       cout << "Enter the value of object : " << endl;
       cin >> D3;
       cout << "First Distance : " << D1 << endl;
       cout << "Second Distance :" << D2 << endl;
       cout << "Third Distance :" << D3 << endl;
     
     
       return 0;
    }
    

    惯上人们是使用 cin>>cout<< 的,得使用友元函数来重载运算符,如果使用成员函数来重载会出现 d1<<cout; 这种不自然的代码。

    下面这个实例展示了如果运用成员函数来重载会出现的情况d1<<cout;

    #include <iostream>
    using namespace std;
     
    class Distance
    {
        private:
            int feet;             // 0 到无穷
            int inches;           // 0 到 12
        public:
            // 所需的构造函数
            Distance(){
                feet = 0;
                inches = 0;
            }
            Distance(int f, int i){
                feet = f;
                inches = i;
            }
            ostream& operator<<( ostream & os)
            {
            os<<"英寸:"<<feet<<"\n英尺:"<<inches;
            return os;
        }
    };
    int main ()
    {
        Distance d1(20,18);
        d1<<cout;//相当于d1.operator<<(cout)
    }
    

    ++ 和 -- 运算符重载

    递增运算符( ++ )和递减运算符( -- )是 C++ 语言中两个重要的一元运算符。

    下面的实例演示了如何重载递增运算符( ++ ),包括前缀和后缀两种用法。类似地,您也可以尝试重载递减运算符( -- )。

    #include <iostream>
    using namespace std;
     
    class Time
    {
       private:
          int hours;             // 0 到 23
          int minutes;           // 0 到 59
       public:
          // 所需的构造函数
         
          // 重载前缀递增运算符( ++ )
          Time operator++ ()  
          {
          
          }
          // 重载后缀递增运算符( ++ )
          Time operator++( int )         // 括号中插入 int 表示后缀 
          {
             
          }
    };
    
    

    注意,int 在 括号内是为了向编译器说明这是一个后缀形式,而不是表示整数。

    前缀形式重载调用 Check operator ++ () ,后缀形式重载调用 operator ++ (int)。

    递增和递减运算符重载

    1、递增和递减一般是改变对象的状态,所以一般是重载为成员函数。

    2、重载递增递减,一定要和指针的递增递减区分开。因为这里的重载操作的是对象,而不是指针(由于指针是内置类型,指针的递增递减是无法重载的),所以一般情况的递增递减是操作对象内部的成员变量。

    3、递增和递减分为前置和后置情况,a = ++b;(前置), a = b++;(后置)。因为符号一样,所以给后置版本加一个int形参作为区分,这个形参是0,但是在函数体中是用不到的,只是为了区分前置后置。例如:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string>
    #include <iostream>
    
    using namespace std;
    
    // 例如这里的重载递增就是为了增加pos的值
    class ca
    {
    public:    
        int pos;
    
        //前置递增就是增加当前对象的pos的值,并且返回当前对象
        ca operator++(){
            pos++;
            return *this;
        }
    
        //后置递增就是增加当前对象的pos的值,并且返回增加pos之前的该对象
        ca operator++(int){
            ca ret = *this;
            ++*this;        //这个会调用上面的函数,其实这里可以换成pos++;
            return ret;
        }
    };
    

    赋值运算符重载

    就像其他运算符一样,您可以重载赋值运算符( = ),用于创建一个对象,比如拷贝构造函数。

    Distance(int f, int i){
        feet = f;
        inches = i;
    }
    
    void operator=(const Distance &D )
    { 
        feet = D.feet;
        inches = D.inches;
    }
    

    函数调用运算符 () 重载

    函数调用运算符 () 可以被重载用于类的对象。当重载 () 时,您不是创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数。

    Distance(int f, int i){
        feet = f;
        inches = i;
    }
    // 重载函数调用运算符
    Distance operator()(int a, int b, int c)
    {
        Distance D;
        // 进行随机计算
        D.feet = a + c + 10;
        D.inches = b + c + 100 ;
        return D;
    }
    
    D2 = D1(10, 10, 10); // invoke operator() 调用
    

    下标运算符 [] 重载

    下标操作符 [] 通常用于访问数组元素。重载该运算符用于增强操作 C++ 数组的功能。

    int& operator[](int i)
    {
        if( i > SIZE )
        {
            cout << "索引超过最大值" <<endl; 
            // 返回第一个元素
            return arr[0];
        }
        return arr[i];
    }
    

    类成员访问运算符->重载

    类成员访问运算符(->)可以被重载,但它较为麻烦。它被定义用于为一个类赋予"指针"行为。运算符->必须是一个成员函数。如果使用了->运算符,返回类型必须是指针或者是类的对象。

    运算符 ->通常与指针引用运算符 * 结合使用,用于实现"智能指针"的功能。这些指针是行为与正常指针相似的对象,唯一不同的是,当您通过指针访问对象时,它们会执行其他的任务。比如,当指针销毁时,或者当指针指向另一个对象时,会自动删除对象。

    间接引用运算符 ->可被定义为一个一元后缀运算符。也就是说,给出一个类:

    class Ptr{
       //...
       X * operator->();
    };
    

    Ptr 的对象可用于访问类 X 的成员,使用方式与指针的用法十分相似。例如:

    void f(Ptr p )
    {
       p->m = 10 ; // (p.operator->())->m = 10
    }
    

    语句 p->m被解释为(p.operator->())->m。同样地,下面的实例演示了如何重载类成员访问运算符->

    #include <iostream>
    #include <vector>
    using namespace std;
     
    // 假设一个实际的类
    class Obj {
       static int i, j;
    public:
       void f() const { cout << i++ << endl; }
       void g() const { cout << j++ << endl; }
    };
     
    // 静态成员定义
    int Obj::i = 10;
    int Obj::j = 12;
     
    // 为上面的类实现一个容器
    class ObjContainer {
       vector<Obj*> a;
    public:
       void add(Obj* obj)
       { 
          a.push_back(obj);  // 调用向量的标准方法
       }
       friend class SmartPointer;
    };
     
    // 实现智能指针,用于访问类 Obj 的成员
    class SmartPointer {
       ObjContainer oc;
       int index;
    public:
       SmartPointer(ObjContainer& objc)
       { 
           oc = objc;
           index = 0;
       }
       // 返回值表示列表结束
       bool operator++() // 前缀版本
       { 
         if(index >= oc.a.size() - 1) return false;
         if(oc.a[++index] == 0) return false;
         return true;
       }
       bool operator++(int) // 后缀版本
       { 
          return operator++();
       }
       // 重载运算符 ->
       Obj* operator->() const 
       {
         if(!oc.a[index])
         {
            cout << "Zero value";
            return (Obj*)0;
         }
         return oc.a[index];
       }
    };
     
    int main() {
       const int sz = 10;
       Obj o[sz];
       ObjContainer oc;
       for(int i = 0; i < sz; i++)
       {
           oc.add(&o[i]);
       }
       SmartPointer sp(oc); // 创建一个迭代器
       do {
          sp->f(); // 智能指针调用
          sp->g();
       } while(sp++);
       return 0;
    }
    

    多态

    多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

    C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

    #include <iostream> 
    using namespace std;
     
    class Shape {
       protected:
          int width, height;
       public:
          Shape( int a=0, int b=0)
          {
             width = a;
             height = b;
          }
          int area()
          {
             cout << "Parent class area :" <<endl;
             return 0;
          }
    };
    class Rectangle: public Shape{
       public:
          Rectangle( int a=0, int b=0):Shape(a, b) { }
          int area ()
          { 
             cout << "Rectangle class area :" <<endl;
             return (width * height); 
          }
    };
    class Triangle: public Shape{
       public:
          Triangle( int a=0, int b=0):Shape(a, b) { }
          int area ()
          { 
             cout << "Triangle class area :" <<endl;
             return (width * height / 2); 
          }
    };
    // 程序的主函数
    int main( )
    {
       Shape *shape;
       Rectangle rec(10,7);
       Triangle  tri(10,5);
     
       // 存储矩形的地址
       shape = &rec;
       // 调用矩形的求面积函数 area
       shape->area();
     
       // 存储三角形的地址
       shape = &tri;
       // 调用三角形的求面积函数 area
       shape->area();
       
       return 0;
    }
    

    当上面的代码被编译和执行时,它会产生下列结果:

    Parent class area
    Parent class area
    

    导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

    但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:

    class Shape {
       protected:
          int width, height;
       public:
          Shape( int a=0, int b=0)
          {
             width = a;
             height = b;
          }
          virtual int area()
          {
             cout << "Parent class area :" <<endl;
             return 0;
          }
    };
    
    

    修改后,当编译和执行前面的实例代码时,它会产生以下结果:

    此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

    正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

    虚函数

    虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

    我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

    virtual int area()
    {
        cout << "Parent class area :" <<endl;
        return 0;
    }
    

    纯虚函数

    纯虚函数指的是只定义函数原型的成员函数

    您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

    我们可以把基类中的虚函数 area() 改写如下:

    class Shape {
       protected:
          int width, height;
       public:
          Shape( int a=0, int b=0)
          {
             width = a;
             height = b;
          }
          // pure virtual function
          virtual int area() = 0;
    };
    

    = 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

    数据抽象

    数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

    数据抽象是一种依赖于接口和实现分离的编程(设计)技术。

    就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。

    在 C++ 中,我们使用来定义我们自己的抽象数据类型(ADT)。

    数据抽象的好处

    数据抽象有两个重要的优势:

    • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
    • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

    如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。

    如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。

    实例

    class Adder{
       public:
          // 构造函数
          Adder(int i = 0)
          {
            total = i;
          }
          // 对外的接口
          void addNum(int number)
          {
              total += number;
          }
          // 对外的接口
          int getTotal()
          {
              return total;
          };
       private:
          // 对外隐藏的数据
          int total;
    };
    

    设计策略

    抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。

    在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

    接口(抽象类)

    接口

    c++中接口也是一种特殊的类,需要满足:
     (1) 类中没有定义任何成员变量
     (2) 类中所有成员函数都是公有且都是纯虚函数

    接口描述了类的行为和功能,而不需要完成类的特定实现。

    抽象类

    抽象类是指 含有纯虚函数成员的类

    其实,在c++中并没有抽象类的概念,要实现抽象类则需要通过纯虚函数实现。

    如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。

    纯虚函数是通过在声明中使用 "= 0" 来指定的,如下所示:

    class Box
    {
       public:
          // 纯虚函数
          virtual double getVolume() = 0;
       private:
          double length;      // 长度
          double breadth;     // 宽度
          double height;      // 高度
    };
    

    文件和流

    到目前为止,我们已经使用了 iostream 标准库,它提供了 cincout 方法分别用于从标准输入读取流和向标准输出写入流。

    本教程介绍如何从文件读取流和向文件写入流。这就需要用到 C++ 中另一个标准库 fstream,它定义了三个新的数据类型:

    数据类型 描述
    ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
    ifstream 该数据类型表示输入文件流,用于从文件读取信息。
    fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

    要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件 <iostream> 和 <fstream>。

    打开文件

    在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstreamfstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。

    下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

    void open(const char *filename, ios::openmode mode);
    

    在这里,open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。

    模式标志 描述
    ios::app 追加模式。所有写入都追加到文件末尾。
    ios::ate 文件打开后定位到文件末尾。
    ios::in 打开文件用于读取。
    ios::out 打开文件用于写入。
    ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

    您可以把以上两种或两种以上的模式结合使用。例如,如果您想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么您可以使用下面的语法:

    ofstream outfile;
    outfile.open("file.dat", ios::out | ios::trunc );
    

    类似地,您如果想要打开一个文件用于读写,可以使用下面的语法:

    ifstream  afile;
    afile.open("file.dat", ios::out | ios::in );
    

    关闭文件

    当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。

    下面是 close() 函数的标准语法,close() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

    void close();
    

    写入文件

    在 C++ 编程中,我们使用流插入运算符( << )向文件写入信息,就像使用该运算符输出信息到屏幕上一样。唯一不同的是,在这里您使用的是 ofstreamfstream 对象,而不是 cout 对象。

    读取文件

    在 C++ 编程中,我们使用流提取运算符( >> )从文件读取信息,就像使用该运算符从键盘输入信息一样。唯一不同的是,在这里您使用的是 ifstreamfstream 对象,而不是 cin 对象。

    读取 & 写入实例

    下面的 C++ 程序以读写模式打开一个文件。在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上:

    实例

    下面的 C++ 程序以读写模式打开一个文件。在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上:

    #include <fstream>
    #include <iostream>
    using namespace std;
     
    int main ()
    {
        
       char data[100];
     
       // 以写模式打开文件
       ofstream outfile;
       outfile.open("afile.dat");
     
       cout << "Writing to the file" << endl;
       cout << "Enter your name: "; 
       cin.getline(data, 100);
     
       // 向文件写入用户输入的数据
       outfile << data << endl;
     
       cout << "Enter your age: "; 
       cin >> data;
       cin.ignore();
       
       // 再次向文件写入用户输入的数据
       outfile << data << endl;
     
       // 关闭打开的文件
       outfile.close();
     
       // 以读模式打开文件
       ifstream infile; 
       infile.open("afile.dat"); 
     
       cout << "Reading from the file" << endl; 
       infile >> data; 
     
       // 在屏幕上写入数据
       cout << data << endl;
       
       // 再次从文件读取数据,并显示它
       infile >> data; 
       cout << data << endl; 
     
       // 关闭打开的文件
       infile.close();
     
       return 0;
    }
    

    当上面的代码被编译和执行时,它会产生下列输入和输出:

    $./a.out
    Writing to the file
    Enter your name: Zara
    Enter your age: 9
    Reading from the file
    Zara
    9
    

    上面的实例中使用了 cin 对象的附加函数,比如 getline()函数从外部读取一行,ignore() 函数会忽略掉之前读语句留下的多余字符。

    文件位置指针

    istreamostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg("seek get")和关于 ostream 的 seekp("seek put")。

    seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。

    查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。

    文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。

    下面是关于定位 "get" 文件位置指针的实例:

    // 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
    fileObject.seekg( n );
     
    // 把文件的读指针从 fileObject 当前位置向后移 n 个字节
    fileObject.seekg( n, ios::cur );
     
    // 把文件的读指针从 fileObject 末尾往回移 n 个字节
    fileObject.seekg( n, ios::end );
     
    // 定位到 fileObject 的末尾
    fileObject.seekg( 0, ios::end );
    

    异常处理

    异常是程序在执行期间产生的问题。

    异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

    抛出异常

    可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。

    以下是尝试除以零时抛出异常的实例:

    double division(int a, int b)
    {
       if( b == 0 )
       {
          throw "Division by zero condition!";
       }
       return (a/b);
    }
    

    捕获异常

    catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。

    try
    {
       // 保护代码
    }catch( ExceptionName e )
    {
      // 处理 ExceptionName 异常的代码
    }
    

    上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...

    实例

    抛出一个除以零的异常,并在 catch 块中捕获该异常。

    #include <iostream>
    using namespace std;
     
    double division(int a, int b)
    {
       if( b == 0 )
       {
          throw "Division by zero condition!";
       }
       return (a/b);
    }
     
    int main ()
    {
       int x = 50;
       int y = 0;
       double z = 0;
     
       try {
         z = division(x, y);
         cout << z << endl;
       }catch (const char* msg) { // 
         cerr << msg << endl;
       }
     
       return 0;
    }
    

    由于我们抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*。当上面的代码被编译和执行时,它会产生下列结果:

    Division by zero condition!
    

    标准的异常

    C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

    image

    下表是对上面层次结构中出现的每个异常的说明:

    异常 描述
    std::exception 该异常是所有标准 C++ 异常的父类。
    std::bad_alloc 该异常可以通过 new 抛出。
    std::bad_cast 该异常可以通过 dynamic_cast 抛出。
    std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
    std::bad_typeid 该异常可以通过 typeid 抛出。
    std::logic_error 理论上可以通过读取代码来检测到的异常。
    std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
    std::invalid_argument 当使用了无效的参数时,会抛出该异常。
    std::length_error 当创建了太长的 std::string 时,会抛出该异常。
    std::out_of_range 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator
    std::runtime_error 理论上不可以通过读取代码来检测到的异常。
    std::overflow_error 当发生数学上溢时,会抛出该异常。
    std::range_error 当尝试存储超出范围的值时,会抛出该异常。
    std::underflow_error 当发生数学下溢时,会抛出该异常。

    定义新的异常

    可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:

    #include <iostream>
    #include <exception>
    using namespace std;
     
    struct MyException : public exception
    {
      const char * what () const throw ()
      {
        return "C++ Exception";
      }
    };
     
    int main()
    {
      try
      {
        throw MyException();
      }
      catch(MyException& e)
      {
        std::cout << "MyException caught" << std::endl;
        std::cout << e.what() << std::endl;
      }
      catch(std::exception& e)
      {
        //其他的错误
      }
    }
    

    在这里,what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。

    动态内存

    了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的内存分为两个部分:

    • 栈:在函数内部声明的所有变量都将占用栈内存。
    • 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。

    在 C++ 中,可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。

    如果不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。

    new 和 delete 运算符

    下面是使用 new 运算符来为任意的数据类型动态分配内存的通用语法:

    new data-type;
    

    在这里,data-type 可以是包括数组在内的任意内置的数据类型,也可以是包括类或结构在内的用户自定义的任何数据类型。让我们先来看下内置的数据类型。

    例如,我们可以定义一个指向 double 类型的指针,然后请求内存,该内存在执行时被分配。我们可以按照下面的语句使用 new 运算符来完成这点:

    double* pvalue  = NULL; // 初始化为 null 的指针
    pvalue  = new double;   // 为变量请求内存
    

    如果自由存储区已被用完,可能无法成功分配内存。所以建议检查 new 运算符是否返回 NULL 指针,并采取以下适当的操作:

    double* pvalue  = NULL;
    if( !(pvalue  = new double ))
    {
       cout << "Error: out of memory." <<endl;
       exit(1);
    }
    

    malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。

    在任何时候,当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 操作符释放它所占用的内存,如下所示:

    delete pvalue;        // 释放 pvalue 所指向的内存
    

    实例

    #include <iostream>
    using namespace std;
     
    int main ()
    {
       double* pvalue  = NULL; // 初始化为 null 的指针
       pvalue  = new double;   // 为变量请求内存
     
       *pvalue = 29494.99;     // 在分配的地址存储值
       cout << "Value of pvalue : " << *pvalue << endl;
     
       delete pvalue;         // 释放内存
     
       return 0;
    }
    

    数组的动态内存分配

    假设我们要为一个字符数组(一个有 20 个字符的字符串)分配内存,我们可以使用上面实例中的语法来为数组动态地分配内存,如下所示:

    char* pvalue  = NULL;   // 初始化为 null 的指针
    pvalue  = new char[20]; // 为变量请求内存
    

    要删除我们刚才创建的数组,语句如下:

    delete [] pvalue;        // 删除 pvalue 所指向的数组
    

    下面是 new 操作符的通用语法,可以为多维数组分配内存,如下所示:

    一维数组

    // 动态分配,数组长度为 m
    int *array=new int [m];
     
    //释放内存
    delete [] array;
    

    二维数组

    int **array
    // 假定数组第一维长度为 m, 第二维长度为 n
    // 动态分配空间
    array = new int *[m];
    for( int i=0; i<m; i++ )
    {
        array[i] = new int [n]  ;
    }
    //释放
    for( int i=0; i<m; i++ )
    {
        delete [] arrary[i];
    }
    delete [] array;
    

    二维数组实例

    #include <iostream>
    using namespace std;
     
    int main()
    {
        int **p;   
        int i,j;   //p[4][8] 
        //开始分配4行8列的二维数据   
        p = new int *[4];
        for(i=0;i<4;i++){
            p[i]=new int [8];
        }
     
        for(i=0; i<4; i++){
            for(j=0; j<8; j++){
                p[i][j] = j*i;
            }
        }   
        //打印数据   
        for(i=0; i<4; i++){
            for(j=0; j<8; j++)     
            {   
                if(j==0) cout<<endl;   
                cout<<p[i][j]<<"\t";   
            }
        }   
        //开始释放申请的堆   
        for(i=0; i<4; i++){
            delete [] p[i];   
        }
        delete [] p;   
        return 0;
    }
    

    对象的动态内存分配

    对象与简单的数据类型没有什么不同。例如,请看下面的代码,我们将使用一个对象数组来理清这一概念:

    #include <iostream>
    using namespace std;
     
    class Box
    {
       public:
          Box() { 
             cout << "调用构造函数!" <<endl; 
          }
          ~Box() { 
             cout << "调用析构函数!" <<endl; 
          }
    };
     
    int main( )
    {
       Box* myBoxArray = new Box[4];
     
       delete [] myBoxArray; // 删除数组
       return 0;
    }
    

    如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。

    命名空间

    假设这样一种情况,当一个班上有两个名叫 Zara 的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的家庭住址,或者他们父母的名字等等。

    同样的情况也出现在 C++ 应用程序中。例如,您可能会写一个名为 xyz() 的函数,在另一个可用的库中也存在一个相同的函数 xyz()。这样,编译器就无法判断您所使用的是哪一个 xyz() 函数。

    因此,引入了命名空间这个概念,专门用于解决上面的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

    定义命名空间

    命名空间的定义使用关键字 namespace,后跟命名空间的名称,如下所示:

    namespace namespace_name {
       // 代码声明
    }
    

    为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称,如下所示:

    namespace_name::code;  // code 可以是变量或函数
    

    让我们来看看命名空间如何为变量或函数等实体定义范围:

    #include <iostream>
    using namespace std;
     
    // 第一个命名空间
    namespace first_space{
       void func(){
          cout << "Inside first_space" << endl;
       }
    }
    // 第二个命名空间
    namespace second_space{
       void func(){
          cout << "Inside second_space" << endl;
       }
    }
    int main ()
    {
     
       // 调用第一个命名空间中的函数
       first_space::func();
       
       // 调用第二个命名空间中的函数
       second_space::func(); 
     
       return 0;
    }
    

    using 指令

    您可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。

    #include <iostream>
    using namespace std;
    using std::cout; // 用来指定命名空间中的特定项目
    

    不连续的命名空间

    个命名空间的各个组成部分可以分散在多个文件中

    下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:

    namespace namespace_name {
       // 代码声明
    }
    

    嵌套的命名空间

    命名空间可以嵌套,您可以在一个命名空间中定义另一个命名空间,如下所示:

    namespace namespace_name1 {
       // 代码声明
       namespace namespace_name2 {
          // 代码声明
       }
    }
    

    可以通过使用 :: 运算符来访问嵌套的命名空间中的成员:

    // 访问 namespace_name2 中的成员
    using namespace namespace_name1::namespace_name2;
     
    // 访问 namespace:name1 中的成员
    using namespace namespace_name1;
    

    实例

    #include <iostream>
    using namespace std;
     
    // 第一个命名空间
    namespace first_space{
       void func(){
          cout << "Inside first_space" << endl;
       }
       // 第二个命名空间
       namespace second_space{
          void func(){
             cout << "Inside second_space" << endl;
          }
       }
    }
    using namespace first_space::second_space;
    int main ()
    {
     
       // 调用第二个命名空间中的函数
       func();
       
       return 0;
    }
    

    模板

    模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

    模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

    每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int>vector <string>

    您可以使用模板来定义函数和类,接下来让我们一起来看看如何使用。

    函数模板

    模板函数定义的一般形式如下所示:

    template <class type> ret-type func-name(parameter list)
    {
       // 函数的主体
    }
    

    在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

    #include <iostream>
    #include <string>
     
    using namespace std;
     
    template <typename T>
    inline T const& Max (T const& a, T const& b) 
    { 
        return a < b ? b:a; 
    } 
    int main ()
    {
     
        int i = 39;
        int j = 20;
        cout << "Max(i, j): " << Max(i, j) << endl; 
     
        double f1 = 13.5; 
        double f2 = 20.7; 
        cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
     
        string s1 = "Hello"; 
        string s2 = "World"; 
        cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
     
       return 0;
    }
    

    类模板

    也可以定义类模板。泛型类声明的一般形式如下所示:

    template <class type> class class-name {
    .
    .
    .
    }
    

    在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。可以使用一个逗号分隔的列表来定义多个泛型数据类型。

    下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

    #include <iostream>
    #include <vector>
    #include <cstdlib>
    #include <string>
    #include <stdexcept>
     
    using namespace std;
     
    template <class T>
    class Stack { 
      private: 
        vector<T> elems;     // 元素 
     
      public: 
        void push(T const&);  // 入栈
        void pop();               // 出栈
        T top() const;            // 返回栈顶元素
        bool empty() const{       // 如果为空则返回真。
            return elems.empty(); 
        } 
    }; 
     
    template <class T>
    void Stack<T>::push (T const& elem) 
    { 
        // 追加传入元素的副本
        elems.push_back(elem);    
    } 
     
    template <class T>
    void Stack<T>::pop () 
    { 
        if (elems.empty()) { 
            throw out_of_range("Stack<>::pop(): empty stack"); 
        }
        // 删除最后一个元素
        elems.pop_back();         
    } 
     
    template <class T>
    T Stack<T>::top () const 
    { 
        if (elems.empty()) { 
            throw out_of_range("Stack<>::top(): empty stack"); 
        }
        // 返回最后一个元素的副本 
        return elems.back();      
    } 
     
    int main() 
    { 
        try { 
            Stack<int>         intStack;  // int 类型的栈 
            Stack<string> stringStack;    // string 类型的栈 
     
            // 操作 int 类型的栈 
            intStack.push(7); 
            cout << intStack.top() <<endl; 
     
            // 操作 string 类型的栈 
            stringStack.push("hello"); 
            cout << stringStack.top() << std::endl; 
            stringStack.pop(); 
            stringStack.pop(); 
        } 
        catch (exception const& ex) { 
            cerr << "Exception: " << ex.what() <<endl; 
            return -1;
        } 
    }
    

    预处理器

    预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。

    所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。

    我们已经看到,之前所有的实例中都有 #include 指令。这个宏用于把头文件包含到源文件中。

    C++ 还支持很多预处理指令,比如 #include、#define、#if、#else、#line 等,让我们一起看看这些重要指令。

    #define 预处理

    #define 预处理指令用于创建符号常量。该符号常量通常称为,指令的一般形式是:

    #define macro-name replacement-text 
    
    // 实例
    #define PI 3.14159
    

    参数宏

    可以使用 #define 来定义一个带有参数的宏,如下所示:

    #define MIN(a,b) (a<b ? a : b)
    

    条件编译

    可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。

    条件预处理器的结构与 if 选择结构很像。请看下面这段预处理器的代码:

    #ifdef NULL
       #define NULL 0
    #endif
    

    可以只在调试时进行编译,调试开关可以使用一个宏来实现,如下所示:

    #ifdef DEBUG
       cerr <<"Variable x = " << x << endl;
    #endif
    

    如果在指令 #ifdef DEBUG 之前已经定义了符号常量 DEBUG,则会对程序中的 cerr 语句进行编译。您可以使用 #if 0 语句注释掉程序的一部分,如下所示:

    #if 0
       // 不进行编译的代码
    #endif
    

    实例

    #include <iostream>
    using namespace std;
    #define DEBUG
     
    #define MIN(a,b) (((a)<(b)) ? a : b)
     
    int main ()
    {
       int i, j;
       i = 100;
       j = 30;
    #ifdef DEBUG
       cerr <<"Trace: Inside main function" << endl;
    #endif
     
    #if 0
       /* 这是注释部分 */
       cout << MKSTR(HELLO C++) << endl;
    #endif
     
       cout <<"The minimum is " << MIN(i, j) << endl;
     
    #ifdef DEBUG
       cerr <<"Trace: Coming out of main function" << endl;
    #endif
        return 0;
    }
    

    # 和 ## 运算符

    # 和 ## 预处理运算符在 C++ 和 ANSI/ISO C 中都是可用的。

    # 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。

    #define MKSTR( x ) #x // 定义在这里
    
    cout << MKSTR(HELLO C++) << endl;
    
    // 上一句被转换成了
    cout << "HELLO C++" << endl; // 被转换成了引号引起来的字符串
    

    ## 运算符用于连接两个令牌。下面是一个实例:

    #define CONCAT( x, y )  x ## y
    

    当 CONCAT 出现在程序中时,它的参数会被连接起来,并用来取代宏。例如,程序中 CONCAT(HELLO, C++) 会被替换为 "HELLO C++",如下面实例所示。

    预定义宏

    描述
    __LINE__ 这会在程序编译时包含当前行号。
    __FILE__ 这会在程序编译时包含当前文件名。
    __DATE__ 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
    __TIME__ 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。

    信号处理

    信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。

    有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal>中。

    信号 描述
    SIGABRT 程序的异常终止,如调用 abort
    SIGFPE 错误的算术运算,比如除以零或导致溢出的操作。
    SIGILL 检测非法指令。
    SIGINT 接收到交互注意信号。
    SIGSEGV 非法访问内存。
    SIGTERM 发送到程序的终止请求。

    signal() 函数

    C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:

    void (*signal (int sig, void (*func)(int)))(int); 
    

    这个函数接收两个参数:

    第一个参数是一个整数,代表了信号的编号;

    第二个参数是一个指向信号处理函数的指针。

    让我们编写一个简单的 C++ 程序,使用 signal() 函数捕获 SIGINT 信号。不管您想在程序中捕获什么信号,您都必须使用 signal 函数来注册信号,并将其与信号处理程序相关联。看看下面的实例:

    #include <iostream>
    #include <csignal>
    #include <unistd.h>
     
    using namespace std;
     
    void signalHandler( int signum )
    {
        cout << "Interrupt signal (" << signum << ") received.\n";
     
        // 清理并关闭
        // 终止程序  
     
       exit(signum);  
     
    }
     
    int main ()
    {
        // 注册信号 SIGINT 和信号处理程序
        signal(SIGINT, signalHandler);  
     
        while(1){
           cout << "Going to sleep...." << endl;
           sleep(1);
        }
     
        return 0;
    }
    

    当上面的代码被编译和执行时,它会产生下列结果:

    Going to sleep....
    Going to sleep....
    Going to sleep....
    

    现在,按 Ctrl+C 来中断程序,您会看到程序捕获信号,程序打印如下内容并退出:

    Going to sleep....
    Going to sleep....
    Going to sleep....
    Interrupt signal (2) received.
    

    raise() 函数

    可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:

    int raise (signal sig);
    

    在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。以下是我们使用 raise() 函数内部生成信号的实例:

    #include <iostream>
    #include <csignal>
    #include <unistd.h>
     
    using namespace std;
     
    void signalHandler( int signum )
    {
        cout << "Interrupt signal (" << signum << ") received.\n";
     
        // 清理并关闭
        // 终止程序 
     
       exit(signum);  
     
    }
     
    int main ()
    {
        int i = 0;
        // 注册信号 SIGINT 和信号处理程序
        signal(SIGINT, signalHandler);  
     
        while(++i){
           cout << "Going to sleep...." << endl;
           if( i == 3 ){
              raise( SIGINT);
           }
           sleep(1);
        }
     
        return 0;
    }
    

    当上面的代码被编译和执行时,它会产生下列结果,并会自动退出:

    Going to sleep....
    Going to sleep....
    Going to sleep....
    Interrupt signal (2) received.
    

    多线程

    多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程

    • 基于进程的多任务处理是程序的并发执行。
    • 基于线程的多任务处理是同一程序的片段的并发执行。

    多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。

    本教程假设您使用的是 Linux 操作系统,我们要使用 POSIX 编写多线程 C++ 程序。POSIX Threads 或 Pthreads 提供的 API 可在多种类 Unix POSIX 系统上可用,比如 FreeBSD、NetBSD、GNU/Linux、Mac OS X 和 Solaris。

    创建线程

    下面的程序,我们可以用它来创建一个 POSIX 线程:

    #include <pthread.h>
    pthread_create (thread, attr, start_routine, arg) 
    

    在这里,pthread_create 创建一个新的线程,并让它可执行。下面是关于参数的说明:

    参数 描述
    thread 指向线程标识符指针。
    attr 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
    start_routine 线程运行函数起始地址,一旦线程被创建就会执行。
    arg 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。

    创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败。

    终止线程

    使用下面的程序,我们可以用它来终止一个 POSIX 线程:

    #include <pthread.h>
    pthread_exit (status) 
    

    在这里,pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。

    如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。

    实例

    以下简单的实例代码使用 pthread_create() 函数创建了 5 个线程,每个线程输出"Hello Runoob!":

    #include <iostream>
    // 必须的头文件
    #include <pthread.h>
     
    using namespace std;
     
    #define NUM_THREADS 5
     
    // 线程的运行函数
    void* say_hello(void* args)
    {
        cout << "Hello Runoob!" << endl;
        return 0;
    }
     
    int main()
    {
        // 定义线程的 id 变量,多个变量使用数组
        pthread_t tids[NUM_THREADS];
        for(int i = 0; i < NUM_THREADS; ++i)
        {
            //参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数
            int ret = pthread_create(&tids[i], NULL, say_hello, NULL);
            if (ret != 0)
            {
               cout << "pthread_create error: error_code=" << ret << endl;
            }
        }
        //等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来;
        pthread_exit(NULL);
    }
    

    使用 -lpthread 库编译下面的程序:

    $ g++ test.cpp -lpthread -o test.o
    

    现在,执行程序,将产生下列结果:

    $ ./test.o
    Hello Runoob!
    Hello Runoob!
    Hello Runoob!
    Hello Runoob!
    Hello Runoob!
    

    以下简单的实例代码使用 pthread_create() 函数创建了 5 个线程,并接收传入的参数。每个线程打印一个 "Hello Runoob!" 消息,并输出接收的参数,然后调用 pthread_exit() 终止线程。

    //文件名:test.cpp
     
    #include <iostream>
    #include <cstdlib>
    #include <pthread.h>
     
    using namespace std;
     
    #define NUM_THREADS     5
     
    void *PrintHello(void *threadid)
    {  
       // 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取
       int tid = *((int*)threadid);
       cout << "Hello Runoob! 线程 ID, " << tid << endl;
       pthread_exit(NULL);
    }
     
    int main ()
    {
       pthread_t threads[NUM_THREADS];
       int indexes[NUM_THREADS];// 用数组来保存i的值
       int rc;
       int i;
       for( i=0; i < NUM_THREADS; i++ ){      
          cout << "main() : 创建线程, " << i << endl;
          indexes[i] = i; //先保存i的值
          // 传入的时候必须强制转换为void* 类型,即无类型指针        
          rc = pthread_create(&threads[i], NULL, 
                              PrintHello, (void *)&(indexes[i]));
          if (rc){
             cout << "Error:无法创建线程," << rc << endl;
             exit(-1);
          }
       }
       pthread_exit(NULL);
    }
    

    现在编译并执行程序,将产生下列结果:

    $ g++ test.cpp -lpthread -o test.o
    $ ./test.o
    main() : 创建线程, 0
    main() : 创建线程, 1
    Hello Runoob! 线程 ID, 0
    main() : 创建线程, Hello Runoob! 线程 ID, 21
    
    main() : 创建线程, 3
    Hello Runoob! 线程 ID, 2
    main() : 创建线程, 4
    Hello Runoob! 线程 ID, 3
    Hello Runoob! 线程 ID, 4
    

    向线程传递参数

    这个实例演示了如何通过结构传递多个参数。您可以在线程回调中传递任意的数据类型,因为它指向 void,如下面的实例所示:

    #include <iostream>
    #include <cstdlib>
    #include <pthread.h>
     
    using namespace std;
     
    #define NUM_THREADS     5
     
    struct thread_data{
       int  thread_id;
       char *message;
    };
     
    void *PrintHello(void *threadarg)
    {
       struct thread_data *my_data;
     
       my_data = (struct thread_data *) threadarg;
     
       cout << "Thread ID : " << my_data->thread_id ;
       cout << " Message : " << my_data->message << endl;
     
       pthread_exit(NULL);
    }
     
    int main ()
    {
       pthread_t threads[NUM_THREADS];
       struct thread_data td[NUM_THREADS];
       int rc;
       int i;
     
       for( i=0; i < NUM_THREADS; i++ ){
          cout <<"main() : creating thread, " << i << endl;
          td[i].thread_id = i;
          td[i].message = (char*)"This is message";
          rc = pthread_create(&threads[i], NULL,
                              PrintHello, (void *)&td[i]);
          if (rc){
             cout << "Error:unable to create thread," << rc << endl;
             exit(-1);
          }
       }
       pthread_exit(NULL);
    }
    

    当上面的代码被编译和执行时,它会产生下列结果:

    $ g++ -Wno-write-strings test.cpp -lpthread -o test.o
    $ ./test.o
    main() : creating thread, 0
    main() : creating thread, 1
    Thread ID : 0 Message : This is message
    main() : creating thread, Thread ID : 21
     Message : This is message
    main() : creating thread, 3
    Thread ID : 2 Message : This is message
    main() : creating thread, 4
    Thread ID : 3 Message : This is message
    Thread ID : 4 Message : This is message
    

    连接和分离线程

    我们可以使用以下两个函数来连接或分离线程:

    pthread_join (threadid, status) 
    pthread_detach (threadid) 
    

    pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。

    只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连接。

    这个实例演示了如何使用 pthread_join() 函数来等待线程的完成。

    #include <iostream>
    #include <cstdlib>
    #include <pthread.h>
    #include <unistd.h>
     
    using namespace std;
     
    #define NUM_THREADS     5
     
    void *wait(void *t)
    {
       int i;
       long tid;
     
       tid = (long)t;
     
       sleep(1);
       cout << "Sleeping in thread " << endl;
       cout << "Thread with id : " << tid << "  ...exiting " << endl;
       pthread_exit(NULL);
    }
     
    int main ()
    {
       int rc;
       int i;
       pthread_t threads[NUM_THREADS];
       pthread_attr_t attr;
       void *status;
     
       // 初始化并设置线程为可连接的(joinable)
       pthread_attr_init(&attr);
       pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
     
       for( i=0; i < NUM_THREADS; i++ ){
          cout << "main() : creating thread, " << i << endl;
          rc = pthread_create(&threads[i], NULL, wait, (void *)&i );
          if (rc){
             cout << "Error:unable to create thread," << rc << endl;
             exit(-1);
          }
       }
     
       // 删除属性,并等待其他线程
       pthread_attr_destroy(&attr);
       for( i=0; i < NUM_THREADS; i++ ){
          rc = pthread_join(threads[i], &status);
          if (rc){
             cout << "Error:unable to join," << rc << endl;
             exit(-1);
          }
          cout << "Main: completed thread id :" << i ;
          cout << "  exiting with status :" << status << endl;
       }
     
       cout << "Main: program exiting." << endl;
       pthread_exit(NULL);
    }
    

    更多实例参考:http://www.runoob.com/w3cnote/cpp-multithread-demo.html

    c++ 11 之后有了标准的线程库

    #include <thread>
    

    Web 编程

    xxx

    STL 教程

    C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。

    C++ 标准模板库的核心包括以下三个组件:

    组件 描述
    容器(Containers) 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。
    算法(Algorithms) 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。
    迭代器(iterators) 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。

    这三个组件都带有丰富的预定义函数,帮助我们通过简单的方式处理复杂的任务。

    下面的程序演示了向量容器(一个 C++ 标准的模板),它与数组十分相似,唯一不同的是,向量在需要扩展大小的时候,会自动处理它自己的存储需求:

    标准库

    C++有用的资源

    以下资源包含了 C++ 有关的网站、书籍和文章。请使用它们来进一步学习 C++ 的知识。

    C++ 有用的网站

    • C++ Programming Language Tutorials − C++ 编程语言教程。
    • C++ Programming − 这本书涵盖了 C++ 语言编程、软件交互设计、C++ 语言的现实生活应用。
    • C++ FAQ − C++ 常见问题
    • Free Country − Free Country 提供了免费的 C++ 源代码和 C++ 库,这些源代码和库涵盖了压缩、存档、游戏编程、标准模板库和 GUI 编程等 C++ 编程领域。
    • C and C++ Users Group − C 和 C++ 的用户团体提供了免费的涵盖各种编程领域 C++ 项目的源代码,包括 AI、动画、编译器、数据库、调试、加密、游戏、图形、GUI、语言工具、系统编程等。

    C++ 有用的书籍

    相关文章

      网友评论

        本文标题:C++简明教程(三)

        本文链接:https://www.haomeiwen.com/subject/mzxndctx.html