C++的类继承与类模板

2023-10-29

类继承是面向对象编程中很重要(也是很难)的内容,其能有效地提高代码复用水平,提高开发的效率。

 

目录

基本概念

公有继承

私有继承、保护继承

包含

多重继承

类模板


基本概念

  1. 继承的种类与特点

    C++中提供了几种继承,分别为公有继承(public)、私有继承(private)、保护继承(protected)。不同继承种类的特点如下:

    #公有继承(public)
    基类的保护成员、公有成员分别是派生类的保护成员、公有成员。遵循is-a-kind-of关系模型,即派生类是基类的一种(但不能反过来说基类是派生类的一种),因此基类的指针、引用可以在不显示类型转换时指向派生类。基类的公有成员可以用多级的公有继承传递。

    #私有继承(private)
    基类的保护成员、公有成员均是派生类的私有成员。遵循has-a关系模型,即派生类只是包含了基类的一部分内容,不是基类的一种。基类的成员无法用多一级的私有继承传递。

    #保护继承(protected)
    基类的保护成员、公有成员均是派生类的保护成员。也遵循has-a关系模型,但是其子类的继承比私有继承更具有“自由度”,因为基类的保护成员可以用多一级的保护继承传递。

    对于任何一种继承而言,基类的私有成员始终是基类的私有部分,也是派生类的一部分,但派生类不能直接访问(即不可见),只能通过基类的公有方法或者保护成员方法访问与修改。
     
  2. 一些关系模型

    继承的关系是基于C++继承机制的底层模型建立的。主要有is-a(-kind-of)、has-a、is-like-a、is-implemented-as、uses-a关系。

    #is-a(-kind-of)
    类A是类B的其中一种特例。例如黄种人是人类的一种。

    #has-a
    类A含有类B的部分内容。例如午餐有香蕉这种水果。

    #is-like-a
    类A像类B,与其有部分相同的特征。可以用is-a、has-a关系建立。

    #is-implemented-as
    类B被用来实现类A。例如链表可以使用节点结构实现。

    #uses-a
    类A使用了某个对象。例如我使用了这台电脑来写代码。

    其实公有继承可以用来实现除is-a以外的关系,但会带来一些编程的问题。最好还是只用来实现is-a关系。
     
  3. 提高代码复用的几种手段

    C++中提高代码复用水平的手段主要有继承(inherit)、包含(containment,也称为组合composition、层次化layering)

    #继承
    类继承使得新的类可以使用基类已经定义过的公有成员、保护成员,并在此基础上定义自己特有的部分。

    #包含
    指在一个类中含有另一个类的对象。可以通过这个包含其中的类对象的公有方法使用相应的类的接口。
     

公有继承

公有继承通过public关键字声明,基类的保护成员、公有成员分别是派生类的保护成员、公有成员,其可以使用基类的公有成员与保护成员。

派生类可以通过使用虚函数重写基类的方法。若基类不声明虚方法,程序将会根据使用的引用或者指针类型来选择(基类还是派生类)方法;若声明了虚方法,程序将会根据调用对象类型来选择方法。因此,基类的析构函数应定义为虚函数。

需要关注派生类与基类的几个特殊函数的关系,并注意派生类如何调用基类的相应方法:
#构造函数:应在创建派生类成员前,首先通过调用基类构造函数创建基类对象(以保证初始化基类成员信息);
#析构函数:首先释放派生类的成员内存,再自动调用基类的析构函数;
#显式复制构造函数(若派生类使用了new):应通过初始化成员列表,首先调用基类的复制构造函数,再复制派生类的成员信息;
#赋值运算符:通过使用作用域解析符调用基类的赋值运算符函数以给基类成员赋值,再定义派生类成员的赋值;
#友元函数:通过将派生类对象(或其他对象,若基类为其他类对象创建了转换构造函数)强制转换为基类对象,派生类可以调用基类的友元函数。
实际上,构造函数、析构函数、赋值运算符函数、友元函数均不能继承,这也是说它们特殊的原因之一。

若派生类中使用了new(或new []),则需在派生类的析构函数、复制构造函数、赋值运算符函数中留意delete(或delete [])的使用,否则动态内存管理乱套。

  1. 公有继承(特点,声明、定义、调用,成员初始化列表)

    公有继承使用public关键字声明,其声明、定义、调用代码如下:
    /*声明:
    //假设基类base已于头文件base.h中定义好
    
    #include "base.h"
    class inheritance:public base
    {
    public:
    	inheritance();
    	~inheritance();
    
    private:
        ...
    };
    
    定义:同普通类成员定义。
    
    调用:同普通类调用。
    */
  2. 成员初始化列表(member initializer list)与类构造函数定义

    成员初始化列表是C++专门用于类构造函数定义的语法,不能用于其他类方法的定义。

    成员初始化列表位于函数参数列表和函数体代码块之间,其语法结构如下:
    /*
    类名::类名(argu_type1 val_1,argu_type2 val_2,...):member_1(val_1),member_2(val_2),...
    {
    
    }
    */
    
    //类c是一个声明好的类,含有a、b两个私有数据成员。
    
    c::c(int & val_a,int & val_b):a(val_a),b(val_b){}
    
    成员初始化列表是构造函数定义的方式之一,以下两种方式等效:
    /*
    类名::类名(argu_type1 val_1,argu_type2 val_2,...):member_1(val_1),member_2(val_2),...
    {
    
    }
    */
    
    //类c是一个声明好的类,含有a、b两个私有数据成员。
    
    //方式1
    c::c(int & val_a,int & val_b):a(val_a),b(val_b){}
    //方式2
    c::c(int & val_a,int & val_b)
    {
        a=val_a;
        b=val_b;
    }
    
  3. 派生类与基类的构造函数

    派生类调用构造函数的时候,必须先使用基类构造函数创建基类对象,然后再对派生类自身成员进行初始化。一般通过使用成员初始化列表与调用基类的构造函数实现,若没有调用相应类型的基类构造函数,将调用默认基类构造函数。

    派生类的构造函数几种定义方式如下:
    class base//基类
    {
    public:
    	base(int & ar_a);
    	base(base & rb);
    	~base();
    
    private:
    	int a;
    };
    
    class inheritance :public base//派生类
    {
    public:
    	inheritance(int &ar_a,int &ar_b);//方式1:直接传入所有所需的参数
        inheritance(int &ar_b,base &ar_base);//方式2:以一个基类对象为基础创建一个派生类对象
    	~inheritance();
    
    private:
    	int b;
    };
    //定义
    inheritance::inheritance(int &ar_a,int &ar_b):base(ar_a)
    {
    	ar_b=b;
    }
    
    inheritance::inheritance(int &ar_b,base &ar_base):base(ar_base)
    {
    	b=ar_b;	
    }
    由于需要在创建派生类对象前,创建一个基类对象,因此基类构造函数base()需要放在成员初始化列表冒号右侧,不能放在代码块中(否则编译器会报错)。

    需要注意的是,每种派生类的构造函数的参数列表中,涉及基类数据成员的参数类型,应与基类的构造函数的参数类型相匹配,以确保能显式调用相应参数类型的基类构造函数。例如上述代码段分别以int &ar_b、base &ar_base为参数,创建了两个派生类的构造函数。
     
  4. 派生类与基类的析构函数

    调用派生类的析构函数时,先调用派生类的析构函数,再调用基类的析构函数。这通过将基类的析构函数声明为虚方法实现(否则当使用基类指针指向派生类对象时,编译器将默认调用基类的析构函数,而不是派生类的析构函数,具体见“虚函数”部分)。顺序和调用派生类的构造函数相反。

    派生类的析构函数用于释放派生类自身的成员内存(如果派生类有使用new的话);基类的析构函数则用于将基类的成员内存释放。
     
  5. 派生类与基类的复制构造函数

    对于新增成员没有使用new初始化的派生类,可以不用再定义派生类的显式复制构造函数,因为可以会派生类的默认复制构造函数会调用基类的复制构造函数。

    对于新增成员使用new初始化的派生类,一定需要定义派生类的显式复制构造函数,因为其相较于基类有新的成员,其在定义时还需要使用成员初始化列表,通过基类的复制构造函数将基类的成员复制,再定义派生类自身的复制构造函数定义。具体代码如下:
    //baseD为基类,hasD为派生类
    #include<iostream>
    #include<cstring>//提供strcpy()、strlen()原型
    using std::strcpy;//使用using声明时,不要给函数带上括号,只写函数名即可
    using std::strlen;
    class baseD
    {
    public:
        ...
        baseD(const baseD & obj);
    private:
        char *id;
    };
    
    baseD::baseD(const baseD & obj)
    {
        id=new char[strlen(obj.id)+1];
        strcpy(id,obj.id);
    }
    
    class hasD:public baseD
    {
    public:
        ...
        hasD(const hasD & obj);
    private:
        char *location;//将新增的location成员
    };
    
    
    hasD(const hasD & obj):baseD(obj)//使用成员初始化列表,先将基类的成员复制过来。似乎也可以理解为先创建一个基类对象(毕竟复制构造函数也是构造函数)?
    {
        location=new char[strlen(obj.location)+1];
        strcpy(location,obj.location);//将新增的location成员复制过来
    }
    
  6. 派生类与基类的重载赋值运算符

    与复制构造函数一样:

    对于新增成员没用new初始化的派生类,可以不用再定义派生类的赋值运算符函数,因为派生类的默认赋值运算符会调用基类的赋值运算符函数。

    对于新增成员使用new的派生类,一定需要定义派生类的显式赋值运算符,因为其相较于基类,有新的成员并且还使用了new初始化,其在定义时还需要使用基类的赋值运算符将基类的成员赋值,再定义派生类自身的赋值运算。具体代码如下:
    //baseD为基类,hasD为派生类
    #include<iostream>
    #include<cstring>//提供strcpy()、strlen()原型
    using std::strcpy;//使用using声明时,不要给函数带上括号,只写函数名即可
    using std::strlen;
    
    class baseD
    {
    public:
        ...
        baseD & operator =(const baseD & obj);
    private:
        char *id;
    };
    
    baseD & baseD::operator =(const baseD & obj)
    {
        if(this==&obj)
            return *this;
        delete [] id;
        id=new char[strlen(obj.id)+1];
        strcpy(id,obj.id);
        return *this;
    }
    
    class hasD:public baseD
    {
    public:
        ...
        hasD & operator =(const hasD & obj);
    private:
        char *location;//将新增的location成员
    };
    
    
     hasD & hasD::operator =(const hasD & obj);
    {
        if(this==&obj)
            return *this;
        baseD::operator=(obj);//使用作用域解析运算符::表明调用的是基类的赋值运算符
        //否则将造成对派生类的赋值运算符的递归调用
        //该语句的含义“等同”于*this=obj,但实际上不能这么写,因为会造成递归调用。
        delete [] location;
        location=new char[strlen(obj.location)+1];
        strcpy(location,obj.location);//将新增的location成员赋值
        return *this;
    }
    
  7. 派生类与基类的友元函数

    派生类也可以声明自己的友元函数,其可以通过将派生类对象(或者其他对象)强制转换成基类对象,从而使用基类的友元函数

    具体代码如下:
    //baseD为基类,hasD为派生类
    #include<iostream>
    #include<cstring>//提供strcpy()、strlen()原型
    using std::strcpy;//使用using声明时,不要给函数带上括号,只写函数名即可
    using std::strlen;
    
    class baseD
    {
    public:
        ...
        friend std::ostream & operator<<(std::ostream & os,const baseD & obj);
    private:
        char *id;
    };
    
    std::ostream & operator<<(std::ostream & os,const baseD & obj);
    {
        os<<obj.id;
        return os;
    }
    
    class hasD:public baseD
    {
    public:
        ...
        friend std::ostream & operator<<(std::ostream & os,const hasD & obj);
    private:
        char *location;//将新增的location成员,其需要动态内存分配
    };
    
    
    std::ostream & operator<<(std::ostream & os,const hasD & obj)
    {
        os<<(const baseD &)obj;//将hasD对象转换为const baseD &,后调用baseD类的友元函数<<
        os<<obj.location;//使用os本身的<<函数
        return os;
    }
    
    
  8. 虚方法与多态公有继承

    虚方法使用virtual关键字声明,其用于在派生类中,重新定义基类中已经定义的成员方法(所以友元函数不能是虚函数),从而实现多态公有继承。

    通过引用或者指针调用时,虚方法具有以下特点:
    #若基类不声明虚方法,程序将会根据使用的引用或者指针类型来选择(基类还是派生类)方法
    #若声明了虚方法,程序将会根据调用对象类型来选择方法

    由于上述特点,一切需要在派生类中重写的方法,以及基类的析构函数,都应该在基类中使用虚方法。

    · 
    虚方法的声明、定义、调用代码
    虚方法的声明、定义、调用代码如下:
    /*声明:
    //基类中
    virtual 返回类型 函数名(参数列表);
    
    //派生类中
    virtual 返回类型 函数名(参数列表);//派生类声明中的virtual可以省略,但养成习惯写上也不错
    
    
    定义:
    //基类中
    返回类型 基类名::函数名(参数列表)//定义时不需要再用virtual关键字
    {
    
    }
    
    返回类型 派生类名::函数名(参数列表)
    {
        基类名::函数名(参数列表);//通过 基类名::函数名()调用基类中的同名方法。
        ...
    }
    
    */
    //一个例子
    class base
    {
    public:
    	base(int & ar_a);
    	base(base & rb);
    	~base();
    	virtual void show();
    
    private:
    	int a;
    };
    
    void base::show()
    {
    	cout<<a;
    }
    
    class inheritance :public base
    {
    public:
    	inheritance(int &ar_a,int ar_b);
    	inheritance(int &ar_b,base &ar_base);
    	~inheritance();
    	virtual void show();
    
    private:
    	int b;
    };
    
    void inheritance::show()
    {
    	base::show();
    	cout<<b;
    }
    
    在派生类的同名方法定义时,可以使用基类名::方法名()的方式调用基类中的方法。

    除了帮助实现多态以外,虚方法对于保证合理的调用析构函数也有重要意义,尤其是当使用基类指针或者引用指向派生类对象时。将基类析构函数声明为虚方法,将保证对象过期时,调用派生类的析构函数,将派生类的新增的成员内存释放,再调用基类的析构函数,将基类的成员内存释放。

    但对于基类的构造函数(因为声明为虚函数没有意义,创建派生类时肯定调用派生类的构造函数)、友元函数(因为友元不是成员函数,虚函数只能用于成员函数)、不需要在派生类中重定义的方法,都不应该写成虚函数

    · 虚函数与基类原函数的隐藏——虚函数与重载的不同
    从类的层次关系看,虚函数用于同名成员函数在派生类和基类之间的重新定义,重载则只是某一类中,同名函数的重新定义。

    (是否可以从特征标看?派生类的虚函数的函数名实际上为“派生类名::函数名”,而基类的函数名实际上为“基类名::函数名”,二者本身函数名就不同了,所以不是重载?)

    若使用了虚函数,当通过派生类调用虚函数时,将隐藏基类的函数,此时编译器可能会发出“函数被隐藏”的警告:
    //假设基类base中还有原型为void show(int &c)的虚函数
    inheritance inh=inheritance(1,2);
    inh.show();//编译器肯定允许这么干
    inh.show(3);//编译器可能不允许这么干
    

    因此,若基类中的虚函数还重载了,那么派生类中也应给每个重载版本定义虚函数,否则只定义一个版本,另外两个版本将被隐藏。

  9. 静态联编与动态联编

    · 联编(binding)、静态联编、动态联编
    将源代码中的函数调用解释为执行特定的函数代码块的过程,称为函数名联编。在编译过程中进行联编称为静态联编(static binding)/早期联编,在程序运行时进行联编称为动态联编(dynamic binding)/晚期联编。编译器只会对非虚方法使用静态联编,则对虚方法使用动态联编。

    静态联编不太灵活,但执行效率更高;动态联编灵活,但牺牲了开销,执行效果低。
     
  10. 抽象基类(abstract base class,ABC)

    C++提供了抽象基类作为更抽象的基类(如果真有使用这种更抽象的基类的必要的话)。抽象基类实际上是至少使用一个纯虚函数的接口,派生产生的类会使用常规虚函数将纯虚函数实现(即将纯虚函数覆盖)

    · 抽象基类
    C++将含有纯虚函数的类认定为抽象基类。

    抽象基类具有以下特点:
    #至少含有一个纯虚函数
    #该抽象基类不能创建类对象

    · 纯虚函数
    纯虚函数通过在函数声明时在参数列表最后写上“=0”,声明其为纯虚函数:
    /*声明:
    
    class ABC//含有一个纯虚函数的类,即成为抽象基类
    {
    public:
        ...
        virtual 返回类型 函数名(参数列表)=0;
    private:
        ....
    };
    
    */
    //示例:ABC是base_1、base_2类的抽象基类
    class ABC
    {
    public:
    	ABC();
    	~ABC();
    	virtual void show()=0;//纯虚函数的声明
    private:
    	int val;
    };
    
    class base_1:public ABC
    {
    public:
    	ABC();
    	~ABC();
    	virtual void show();//使用base_1的虚函数代替纯虚函数
    private:
    	...
    };
    
    
    class base_2:public ABC
    {
    public:
    	ABC();
    	~ABC();
    	virtual void show();//使用base_2的虚函数代替纯虚函数
    private:
    	...
    };
    
    
    
    纯虚函数具有以下特点:
    #可以不需要在ABC中给出定义,不过也可以给出定义。
    #虚函数才能成为纯虚函数,非虚函数不能成为纯虚函数(至少对于Visual Studio2012来说是这样)
     
  11. 继承与动态内存分配
    主要涉及派生类的析构函数、复制构造函数、赋值运算符与基类相应函数的关系。

    只需要记住这两点:

    #如果派生类新增成员没有使用new,则可以不需要为派生类定义显式的析构函数、复制构造函数、赋值运算符,因为派生类会调用相应的默认函数,而这些默认函数会调用基类相应的显式函数执行对应操作。

    #如果派生类新增成员使用new初始化,那么就需要为派生类定义显式的析构函数、复制构造函数、赋值运算符,因为需要派生类调用相应显式函数管理新new出来的内存。不同函数处理基类成员的方式不同:派生类的析构函数在释放了自身新增内存后自动调用基类析构函数;派生类的复制构造函数在定义时通过在初始化成员列表使用基类的复制构造函数;派生类的赋值运算符在定义时,使用作用域解析运算符表明使用基类的赋值运算符。

    本质上还是动态内存管理的那一套:有new(new [])必有delete(delete []),有新new(new [])必有新delete(delete []),new和delete是分不开的。


私有继承、保护继承

私有继承描述的是has-a关系(和包含是一样的),不是is-a关系模型,这使得其继承行为和公有继承很不同。

私有继承和公有继承最大的不同就是基类的公有成员、保护成员都成为了私有派生类的私有部分,以及其描述的是has-a关系。但在派生类的构造函数、析构函数、友元函数上,这两者调用基类相应函数的方式相近。

  1. 和基类接口与实现关系

    · 继承接口
    在类外,能通过派生类对象调用基类公有方法。
    · 继承实现
    指在类作用域内,能调用基类方法。

    私有继承只继承实现,不继承基类的接口,这表示派生类可以调用基类的方法,但是不能通过私有派生类对象调用基类方法。
  2. 子对象

    子对象(subobject)指的是派生类通过继承或者包含添加进类的成员。即来自基类的成员。
     
  3. 私有继承的声明、定义、调用

    私有继承通过关键字private声明,表明基类的公有成员、保护成员均成为了派生类的私有成员,但派生类的成员函数可以使用它。其声明、定义、调用的代码如下:
    //base是基类,PvtIhrt是私有派生类
    class base
    {
    public:
    	base(int & ar_a);
    	base(base & rb);
    	~base();
    	virtual void show();
    
    private:
    	int a;
    };
    
    class PvtIhrt:private base
    {
    public:
    	PvtIhrt();
    	~PvtIhrt();
        
    private:
        int c;
    };
    
    //定义同普通类定义
    //调用同普通类调用
  4. 私有派生类与基类的构造函数

    和公有继承一样,私有派生类的构造函数也需要先创建基类对象,再对新增成员进行初始化,这个过程也通过使用成员初始化列表实现:
    //base是基类,PvtIhrt是私有派生类
    class base
    {
    public:
    	base(const int & val_a){a=val_a}//构造函数写成内联函数
    	base(base & rb);
    	~base();
    	virtual void show();
    
    private:
    	int a;
    };
    
    class PvtIhrt:private base
    {
    public:
    	PvtIhrt(const int & val_a,const int & val_c);
    	~PvtIhrt();
        
    private:
        int c;
    };
    
    //私有继承的构造函数
    PvtIhrt::PvtIhrt(const int & val_a,const int & val_c):base(val_a)
    {
    	c=val_c;
    }
  5. 私有派生类与基类的析构函数

    和公有继承一样,私有派生类的析构函数也是先释放新增成员的内存,再自动调用基类的析构函数释放基类成员的内存。因此也需要将基类的析构函数写成虚函数。

    具体代码略。
     
  6. 私有派生类与基类的成员函数

    私有派生类通过类名与作用域解析运算符::来访问基类的成员函数(而包含则是使用包含的类成员来访问),这点和公有派生类调用基类的赋值运算符函数一样。

    私有派生类成员函数定义、调用代码如下:
    //base是基类,PvtIhrt是私有派生类
    class base
    {
    public:
        ...
        int getA();
    private:
    	int a;
    };
    
    int base::getA()
    {
    	return a;
    }
    
    
    class PvtIhrt:private base
    {
    public:
        ...
        int sumVal();
        
    private:
        int c;
    };
    
    int PvtIhrt::sumVal()
    {
    	int val_a=base::getA();
    	return val_a+c;
    }
    
  7. 私有派生类访问基类的成员

    在私有派生类的成员方法中要访问基类的成员或者返回基类本身,需要将派生类强制转换成基类。具体代码如下:
    //base是基类,PvtIhrt是私有派生类
    class base
    {
    public:
        ...
    private:
    	int a;
    };
    
    ostream & operator<<(ostream & os,const base & obj_pvt)
    {
        os<<obj_base.a;
    	return os;
    }
    
    
    class PvtIhrt:private base
    {
    public:
        ...
        base getBase();
        
    private:
        int c;
    };
    
    base PvtIhrt::getBase()
    {
    	return (base)*this;
    }
    
  8. 私有派生类与基类的友元函数

    和公有继承一样,私有派生类也通过强制类型转换来调用基类的友元函数。具体代码如下:
    //base是基类,PvtIhrt是私有派生类
    class base
    {
    public:
        ...
        friend ostream & operator<<(ostream & os,const PvtIhrt & obj_pvt);
    private:
    	int a;
    };
    
    ostream & operator<<(ostream & os,const base & obj_pvt)
    {
        os<<obj_base.a;
    	return os;
    }
    
    
    class PvtIhrt:private base
    {
    public:
        ...
        friend ostream & operator<<(ostream & os,const PvtIhrt & obj_pvt);
        
    private:
        int c;
    };
    
    ostream & operator<<(ostream & os,const PvtIhrt & obj_pvt)
    {
    	os<<(const base &)obj_pvt;
    	os<<obj_pvt.c;
    	return os;
    }
    
  9. 保护继承

    保护继承指的是基类的保护成员、公有成员均是派生类的保护成员,在派生类继续派生处第三代类时,第三代类可以直接使用基类的公有方法(而私有派生派生出的第三代类则不能再使用基类的方法,若试图访问编译器会报错)。保护继承也遵循has-a关系模型,

    保护继承通过protected关键字声明。
     
  10. 使用using声明重定义访问权限

    一般而言,在私有继承中,基类的公有方法、保护方法都会变成私有派生类的私有部分,在类外不能访问。实际上,可以在私有派生类的公有部分使用using声明,使特定基类方法变成私有派生类的公有成员,从而可以通过私有类对象访问基类方法。using实际上为继承开了一个口。

    具体代码如下:
     
    //base是基类,PvtIhrt是私有派生类
    class base
    {
    public:
        ...
        int getA();
    private:
    	int a;
    };
    
    int base::getA()
    {
    	return a;
    }
    
    
    class PvtIhrt:private base
    {
    public:
        ...
        using base::getA;
        
    private:
        int c;
    };
    
    //user.cpp
    //objPvt是一个PvtIhrt对象
    int a=objPvt.getA();
    

包含

在一个类中含有其他类的成员,称为包含(containment)/组合(composition)/层次化(layering)。和私有继承一样,包含描述的也是has-a关系,因此二者有很多相近的地方,由于包含中含有类成员,二者在调用基类成员时也存在较大的不同。

  1. 和基类接口、实现的关系

    包含和私有继承一样,描述的都是has-a关系,has-a关系在与基类接口关系上表现为只继承基类的实现,不继承基类的接口。
     
  2. 声明、定义、调用

    包含的声明很简单,只需要在类定义中直接声明其他类的成员即可,这样即可通过这些成员来调用其他类的方法/接口。其声明、定义、调用代码如下:
     
    /*声明
    
    class A
    {
        ...
    };
    
    class B
    {
    public:
        ...
    private:
        ...
        A 对象名;
    
    };
    
    
    //定义同普通类
    //调用同普通类
    
    */
  3. 包含类的构造函数

    包含类的构造函数需要各自对包含的类进行初始化,这可以通过使用成员初始化列表或者单独调用各类的构造函数来实现。具体代码如下:
  4. 包含和私有继承的不同

    由于包含含有具体类成员,而私有继承没有具体类成员,其具有以下不同:
    #包含类中含有具体的类成员,私有继承只有类名,没有类成员名。
    #在定义构造函数时,包含通过类成员名直接初始化,私有继承则通过类名来初始化
    #在使用基类的成员方法时,包含直接通过类对象调用,私有继承则通过类名与作用域解析运算符调用;
    #在使用基类的友元函数时,包含不需要进行强制类型转换,私有继承则需要进行强制类型转换;


多重继承

多重继承(Multiple Inheritance,MI)指从多个基类继承。多重继承可以使用公有继承、私有继承,若全都使用公有继承,则描述的是is-a关系;若全使用私有继承,则描述的是has-a关系。当然,也可以即使用公有继承,又使用私有继承,只是这样关系就比较复杂。

当多重继承的直接基类来自于同一个祖先时,必须将直接基类声明为虚基类,以避免出现多个祖先子对象的冲突。多重继承也可以混合继承非虚基类与虚基类,但当它们有交叉继承的关系时,也还是会出现多个祖先子对象的冲突,此时需要做出适当的调整。虚基类改变了C++解释二义性的方式,即派生类的方法优先于直接或间接基类的同名方法,不使用类名限定时将直接调用派生类的方法,这称为虚二义性规则。

  1. 声明、定义、调用

    多重继承的声明很简单,只需要把继承类型和基类写明就可以:
    /*声明
    
    class A
    {
    public:
        A(int val_a);
        ~A();
    private:
        int a;
    };
    
    class B
    {
    public:
        B();
        B();
    private:
        int b;
        ...
    };
    
    class C:public A,public B//多重公有继承
    {
        ...
    };
    
    
    class D:private A,private B//多重私有继承
    {
        ...
    };
    
    //定义同普通类
    //调用同普通类
    
    */
    其定义则较为复杂,尤其是当两个基类均来自同一个抽象基类时,此时第三代派生类就具有了两个抽象基类的值,需要使用虚基类来解决这个问题。
     
  2. 虚基类


    · 声明、定义、调用
    虚基类通过在第二代类继承抽象基类时使用virtual关键字来声明。其声明、定义、调用代码如下:
    class ABC
    {
    public:
    	ABC();
    	~ABC();
    	virtual void show()=0;//纯虚函数的声明
    private:
    	int val;
    };
    
    class A:public virtual ABC//virtual与public的顺序可以调整,见下方
    {
    public:
    	A();
    	~A();
    private:
    	int a;
    };
    
    class B:virtual public ABC
    {
    public:
    	B();
    	~B();
    
    private:
    	int b;
    };
    
    class AB: public A,public B
    {
    public:
    	AB();
    	~AB();
    
    private:
    	int ab;
    };
    在上方的代码中,通过在类A、B声明时使用virtual关键字表明两个第二代类是虚基类后,第三代类AB此时就只具有一套抽象基类ABC的数据成员(或者说只有一个ABC的副本)。


    · 使用虚基类时的构造函数定义
    对于一般的A->B->C继承关系,定义C的构造函数时,需要调用B(),再初始化C的成员,而B()再调用A(),后初始化B的成员,也就是说,C构造函数的参数允许通过直接基类B传递给间接基类A。

    但是对于直接基类是虚基类的派生类而言,不允许这样的参数传递,此时派生类的构造函数的定义代码如下:
    class ABC
    {
    public:
    	ABC(const int & val_val):val(val_val){}
    	~ABC();
    	virtual void show()=0;//纯虚函数的声明
    private:
    	int val;
    };
    
    class A:virtual public ABC
    {
    public:
    	A(int val_val,int val_a):ABC(val_val),a(val_a){}
    	A(const ABC & objABC,int val_a):ABC(objABC),a(val_a){}
    	~A();
    private:
    	int a;
    };
    
    class B:virtual public ABC
    {
    public:
    	B(int val_val,int val_b):ABC(val_val),b(val_b){}
    	B(const ABC & objABC,int val_b):ABC(objABC),b(val_b){}
    	~B();
    
    private:
    	int b;
    };
    
    class AB:public A,public B
    {
    public:
    	AB(const ABC & objABC,int val_a,
            int val_b,int val_ab):ABC(objABC),A(objABC,val_a),B(objABC,val_b){}
    	~AB();
    
    private:
    	int ab;
    };
    在上述代码段中,若类A、B均不是虚基类,那么此时ABC就是间接的非虚基类,在AB()的初始化成员列表中使用ABC(objABC)就是非法的。对于虚基类而言,AB()必须显式调用ABC(objABC),否则只写A(objABC,val_a),B(objABC,val_b)将调用ABC的默认构造函数。

    · 非虚基类与虚基类的混合继承
    多重继承实际上还支持同时继承虚基类与非虚基类。

    若类X继承了n个虚基类,m个非虚基类,那么X将有1份包括所有虚途径的基类子对象和m个非虚途径的子对象(其中含有1份祖先子对象,m个非虚基类的子对象)。若这些虚基类和非虚基类还有部分交叉关系,如A是B、C的虚基类,A又是V、W的非虚基类,B、C、V、W则是X的基类,那么此时X将有3份A的子对象,1份来自B、C,2份分别来自V、W。
     
  3. 支配(即同名成员的优先级)

    虚基类还改变了C++解析二义性的方式。


    若派生类继承了多个类,且这些基类中有同名的方法,为了避免二义性,则需要通过类名与作用域解析运算符来限定使用的是哪个类的方法。若派生类继承了多个虚基类,且派生类中也有同名方法,那么派生类的方法优先于直接或者间接基类的同名方法被调用,此时不使用类名也不会导致二义性,这被称为虚二义性规则。例如
    class A
    {
    public:
        hhh();
        ...
    private:
        ...
    };
    
    class B
    {
    public:
        hhh();
    private:
        ....
    };
    
    class C:virtual public A
    {
    public:
        ...
    private:
        ...
    };
    
    class D:public B
    {
    public:
        ...
    private:
        hhh();
    };
    
    class E:public C,public D
    {
    public:
        hhh()
    private:
    
    };
    类E的hhh()优先级高于类D的hhh();类D的hhh()优先级高于类B的hhh(),即使类D的hhh()是私有方法。如果要在E中使用类D的hhh(),那么就需要使用D::hhh()来限定。


类模板

类模板实际上是由其他类型、泛型Type和使用泛型的函数模板组成的。


类模板的使用和普通类差不多(正如函数模板使用起来和普通函数差不多),可以作为参数、返回类型。类模板和函数模板都是用template来声明的,二者有很多相似的地方,但也有不同,可以对照。

  1. 声明、定义、调用

    · 声明、定义、调用
    类模板的声明、定义都很简单,和普通类的声明、定义相近,只是书写看起来很麻烦:需要使用类型参数Type和函数模板的格式代替普通类的类型、类名、方法定义。其中,类型参数Type可以是任意参数,如整型、浮点数、指针,甚至是类模板生成的对象等

    其声明、定义、调用的代码如下:
     
    template<typename T> class TC//声明是一个模板类
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
    	~TC(){}
    	T getValT();//表示返回类型为T的函数
    private:
    	T t;
    	int a;
    
    };
    
    
    //调用代码
    
    #include...
    
    TC<double> TCDb=TC(3.0,2);
    在上述代码中,创建了一个double类型实例化的类对象TCDb,后即可通过该对象进行具体函数的调用。和调用函数模板一样,调用模板类也需要实例化/具体化。


    · 类模板的非类型参数
    类模板声明时,还可以设置非类型参数,这常用于指定类模板中的特定数目的大小,例如在数组/栈模板中。例如:
    template<typename T,int n> class Stack//n即为栈的大小
    {
    public:
    	Stack();
    	~Stack();
    
    private:
    
    };
    但是需要指出的是,非类型参数(表达式参数)只能是整型、枚举或者指针,不能是浮点数(但可以是浮点数的指针)。

    · 类模板的多类型参数
    类模板允许使用多个类型参数,以表示不同类型,例如希望能返回一对不同类型的值时,可以写这么一个模板(实际上C++的STL提供了相应的模板pair):
    //声明
    template<typename T1, typename T2> class Pair
    {
    public:
        Pair(T1 val_t1,T2 val_t2);
        ...
    
    private:
        T1 t1;
        T2 t2;
    };
    
    //定义
    template<typename T1, typename T2>
    Pair<T1,T2>::Pair()
    {
        t1=val_t1;
        t2=val_t2;
    }
    
    
    //调用
    
    #include ...
    Pair<string,int> PStrInt=Pair<string,int>(...);
    需要强调的是,需要使用Pair<string,int>来调用构造函数。

    · 类模板的默认类型参数
    类模板也允许对类型参数使用默认参数,用法同类的默认参数:
    //声明
    template<typename T1, typename T2=int> class Pair
    {
    public:
        Pair(T1 val_t1,T2 val_t2);
        ...
    
    private:
        T1 t1;
        T2 t2;
    };
    
    
    当不提供T2类型的参数时,将默认使用int作为T2的类型值。
  2. 具体化

    与函数模板一样,类模板也有实例化(instantiation)与具体化(specification)的过程(二者统称为具体化)。

    · 实例化(instantiation)
    类模板的实例化指的是根据传入的类型与类模板,生成具体的类定义。分为隐式实例化、显式实例化两种,显式实例化用于声明一个具体类型的类,且该声明必须位于声明类模板的命名空间中隐式实例化则用于声明具体类的对象(但未创建具体类定义与对象)。二者的共同点在于均未生成具体类定义和对象。

    其调用代码如下:
    //TC.h
    template<typename T> class TC//声明是一个模板类
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
        TC();//默认构造函数
    	~TC(){}
    	T getValT();//表示返回类型为T的函数
    private:
    	T t;
    	int a;
    
    };
    
    template class TC<float>;//生成一个float类型的显式实例化,但还未创建对象
    
    //定义代码
    ...
    
    //user.cpp,调用代码
    
    #include...
    
    TC<double> TCDb;//隐式实例化,此时只声明了对象及使用的类型,但还未创建具体类定义、类对象
    TCDb=new TC<double>;//此时才生成类对象
    
    显式实例化的意义在于直接告诉编译器该生成的类型是什么,而隐式实例化则需要编译器根据输入的参数类型判断该生成什么类型的类。理论上来说,显式实例化的编译效率会更高一些(因为编译器明确知道了类型?),但实际上显式实例化用的不是很多。


    · 具体化(specification)
    和函数模板一样,类模板的具体化则是处理特定类型而对部分方法做出特定的定义,即定制。其声明、定义的代码如下:
    /*声明:
    template <> class 类名<特定类型>
    {
        ...
    }
    
    */
    
    //示例
    
    template<typename T> class TC//通用模板
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
    	TC();
    	~TC(){}
    	T getValT();
    private:
    	T t;
    	int a;
    
    };
    template class TC<int>;//显式实例化
    
    template<> class TC<const char*>//显式具体化,为const char*类型特别定义一个模板
    {
    
    };
  3. 将模板作为成员
    类模板也可以作为类或者类模板中的成员,需要注意的是,部分编译器比较老,不支持嵌套类模板。

    部分支持嵌套类的编译器允许在类中定义,部分则只允许在类外定义嵌套类模板。对于后者,需要使用缩进表示嵌套类其定义格式如下:
    template<typename T> class TC
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
    	TC();
    	~TC(){}
    	T getValT();
    private:
    	T t;
    	int a;
    	template<class U> class nested
    	{
    	public:
    		nested();
    		~nested();
    
    	private:
    
    
    	};
    
    };
    
    template<typename T> class TC
    		template<class U> class nested
    		nested::nested()
    		{
    
    		}
  4. 将类模板作为类模板参数
    类模板甚至可以作为另一个类模板的参数,表明A类模板的参数类型为类模板B。声明A类对象时,传入的参数应该符合类模板B的声明,这样才能与A模板声明匹配。

    声明代码如下:
    /*声明:
    template<template<class 泛型名> class 模板参数名> class 类名{};
    
    */
    
    //示例
    template<template<class T> class tempClass> class TCAP
    {
    private:
        Thing<int> s1;
        Thing<double> s2;
    public:
        Crab() {};
        // assumes the thing class has push() and pop() members
        bool push(int a, double x) { return s1.push && s2.push(x); }
        bool pop(int & a, double & x){ return s1.pop(a) && s2.pop(x); }
    };
    声明代码中,第二个template<class T> class表明这是模板类型,tempClass则是参数。实际上,就是把普通类模板的声明中的class 换成了template<class T> class,其他都一样。

    需要强调的是,使用B类模板作为A模板参数时,声明模板后若使用了B类对象的成员,那么在实际调用生成A类模板对象,作为参数的模板需要满足B模板的要求,这样才能和A模板声明匹配,例如上文中的代码,要求传入的模板需要含有push()、pop()这两个方法,并且方法的返回值是bool类型。
     
  5. 模板与友元函数

    类模板也可以有自己的友元函数。按照是否使用函数模板,分为非模板友元与模板友元,模板友元又根据是否使用模板的泛型,分为约束(bound)模板友元与非约束(unbound)模板友元。非模板友元是所有类模板实例化的友元;约束模板友元是各模板实例化各自的友元;非约束模板友元则是所有类模板实例化的友元。

    · 非模板友元
    非模板友元不是一个函数模板若非模板友元没有使用类模板作为参数,那么其将是所有模板的实例化的友元。若其使用了类模板作为参数(即约束非模板友元),其则是某个实例化的友元,并且需要为使用到的类型提供显式具体化友元函数

    具体声明、定义代码如下:
    template<typename T> class TC
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
    	TC();
    	~TC(){}
    	T getValT();
    	friend void count();//是所有实例化的友元
    	friend void show(TC<T> & objTC);//是每个实例化各自的友元
    private:
    	T t;
    	int a;
    };
    
    void count()
    {
    	...
    }
    
    void show(TC<int> & objTC)//TC<int>类的友元
    {
    	...
    }
    
    void show(TC<double> & objTC)//TC<double>类的友元
    {
    	...
    }
    count()为所有实例化的友元,show()则是每个具体化的友元。


    · 约束(bound)模板友元

    约束模板友元也可以使得该友元是每个实例化各自的友元:其将友元声明为函数模板,同时利用类模板的泛型T,生成T的具体化。

    约束模板友元的声明分为三个步骤:
    ①在类声明前声明函数模板;
    ②声明友元函数,并声明类型的泛型T的
    具体化这里用显式实例似乎更明确)
    ③为模板友元提供定义。


    具体代码如下:
    template<class funcT> void tempCount();
    template<typename T> class TC
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
    	TC();
    	~TC(){}
    	...
    	friend void tempCount<T>();//声明约束模板友元,是模板函数的具体化
    private:
    	T t;
    	int a;
    };
    
    template<class funcT> void tempCount()//用函数模板自身的泛型定义
    {
        ...
    }
    类模板中的tempCount<T>表明这是函数模板tempCount的实例化。对于函数模板而言,其定义应该使用函数模板自身的泛型funcT。


    · 非约束(unbound)模板友元
    非约束模板友元使用与类模板不同的参数类型,
    因此每个非约束模板友元的具体化是所有类实例化的友元,所以可以访问所有实例化的成员。

    由于不需要在类模板声明中显式实例化,非约束模板友元在类模板内声明,其代码如下:
    template<typename T> class TC
    {
    public:
    	TC(T val_t,int val_a):t(val_t),a(val_a){}
    	TC();
    	~TC(){}
    	T getValT();
    	...
    	friend template<class funcT> void tempShow();//声明非约束模板友元
    private:
    	T t;
    	int a;
    };
    
    
    template<class funcT> void tempShow()
    {
        ...
    }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++的类继承与类模板 的相关文章

  • Directory.Delete 之后 Directory.Exists 有时返回 true ?

    我有非常奇怪的行为 我有 Directory Delete tempFolder true if Directory Exists tempFolder 有时 Directory Exists 返回 true 为什么 可能是资源管理器打开了
  • 如何将 protobuf-net 与不可变值类型一起使用?

    假设我有一个像这样的不可变值类型 Serializable DataContract public struct MyValueType ISerializable private readonly int x private readon
  • 提交后禁用按钮

    当用户提交付款表单并且发布表单的代码导致 Firefox 中出现重复发布时 我试图禁用按钮 去掉代码就不会出现这个问题 在firefox以外的任何浏览器中也不会出现这个问题 知道如何防止双重帖子吗 System Text StringBui
  • MVC 在布局代码之前执行视图代码并破坏我的脚本顺序

    我正在尝试将所有 javascript 包含内容移至页面底部 我正在将 MVC 与 Razor 一起使用 我编写了一个辅助方法来注册脚本 它按注册顺序保留脚本 并排除重复的内容 Html RegisterScript scripts som
  • ClickOnce 应用程序错误:部署和应用程序没有匹配的安全区域

    我在 IE 中使用 FireFox 和 Chrome 的 ClickOnce 应用程序时遇到问题 它工作正常 异常的详细信息是 PLATFORM VERSION INFO Windows 6 1 7600 0 Win32NT Common
  • 复制 std::function 的成本有多高?

    While std function是可移动的 但在某些情况下不可能或不方便 复制它会受到重大处罚吗 它是否可能取决于捕获变量的大小 如果它是使用 lambda 表达式创建的 它依赖于实现吗 std function通常被实现为值语义 小缓
  • C中的malloc内存分配方案

    我在 C 中尝试使用 malloc 发现 malloc 在分配了一些内存后浪费了一些空间 下面是我用来测试 malloc 的一段代码 include
  • 如何区分用户点击链接和页面自动重定向?

    拥有 C WebBrowser control http msdn microsoft com en us library system windows forms webbrowser aspx在我的 WinForms 应用程序中 并意识
  • 使用接口有什么好处?

    使用接口有什么用 我听说它用来代替多重继承 并且还可以用它来完成数据隐藏 还有其他优点吗 哪些地方使用了接口 程序员如何识别需要该接口 有什么区别explicit interface implementation and implicit
  • 将 Word 文档另存为图像

    我正在使用下面的代码将 Word 文档转换为图像文件 但是图片显得太大 内容不适合 有没有办法渲染图片或将图片保存到合适的尺寸 private void btnConvert Click object sender EventArgs e
  • 为什么调用非 const 成员函数而不是 const 成员函数?

    为了我的目的 我尝试包装一些类似于 Qt 共享数据指针的东西 经过测试 我发现当应该调用 const 函数时 会选择它的非 const 版本 我正在使用 C 0x 选项进行编译 这是一个最小的代码 struct Data int x con
  • 是否有实用的理由使用“if (0 == p)”而不是“if (!p)”?

    我倾向于使用逻辑非运算符来编写 if 语句 if p some code 我周围的一些人倾向于使用显式比较 因此代码如下所示 if FOO p some code 其中 FOO 是其中之一false FALSE 0 0 0 NULL etc
  • DbContext 和 ObjectContext 有什么区别

    From MSDN 表示工作单元和存储库模式的组合 使您能够查询数据库并将更改分组在一起 然后将这些更改作为一个单元写回存储 DbContext在概念上类似于ObjectContext 我虽然DbContext只处理与数据库的连接以及针对数
  • 如何在 32 位或 64 位配置中以编程方式运行任何 CPU .NET 可执行文件?

    我有一个可在 32 位和 64 位处理器上运行的 C 应用程序 我试图枚举给定系统上所有进程的模块 当尝试从 64 位应用程序枚举 32 位进程模块时 这会出现问题 Windows 或 NET 禁止它 我认为如果我可以从应用程序内部重新启动
  • 使用自定义堆的类似 malloc 的函数

    如果我希望使用自定义预分配堆构造类似 malloc 的功能 那么 C 中最好的方法是什么 我的具体问题是 我有一个可映射 类似内存 的设备 已将其放入我的地址空间中 但我需要获得一种更灵活的方式来使用该内存来存储将随着时间的推移分配和释放的
  • C# HashSet 只读解决方法

    这是示例代码 static class Store private static List
  • 调用堆栈中的“外部代码”是什么意思?

    我在 Visual Studio 中调用一个方法 并尝试通过检查调用堆栈来调试它 其中一些行标记为 外部代码 这到底是什么意思 方法来自 dll已被处决 外部代码 意味着该dll没有可用的调试信息 你能做的就是在Call Stack窗口中单
  • WebSocket安全连接自签名证书

    目标是一个与用户电脑上安装的 C 应用程序交换信息的 Web 应用程序 客户端应用程序是 websocket 服务器 浏览器是 websocket 客户端 最后 用户浏览器中的 websocket 客户端通过 Angular 持久创建 并且
  • 使用 .NET Process.Start 运行时挂起进程 - 出了什么问题?

    我在 svn exe 周围编写了一个快速而肮脏的包装器来检索一些内容并对其执行某些操作 但对于某些输入 它偶尔会重复挂起并且无法完成 例如 一个调用是 svn list svn list http myserver 84 svn Docum
  • 当从finally中抛出异常时,Catch块不会被评估

    出现这个问题的原因是之前在 NET 4 0 中运行的代码在 NET 4 5 中因未处理的异常而失败 部分原因是 try finallys 如果您想了解详细信息 请阅读更多内容微软连接 https connect microsoft com

随机推荐

  • SQL增删改查语句学习

    删除语句 语法 DELETE FROM 表名 WHERE 条件 例如 DELETE FROM studentchose WHERE sc id 202046 DELETE FROM studentchose WHERE sc id 2020
  • 基于springboot的校园疫情防控系统【毕业设计,源码,论文】

    想要源码或其他毕设项目 可以私信 摘 要 随着信息技术和网络技术的飞速发展 人类已进入全新信息化时代 传统管理技术已无法高效 便捷地管理信息 为了迎合时代需求 优化管理效率 各种各样的管理系统应运而生 各行各业相继进入信息管理时代 校园疫情
  • Linux下远程git服务器拉取代码发布jar包脚本

    一 yum安装 在Linux上是有yum安装Git 非常简单 只需要一行命令 yum y install git 二 maven安装 参考https www jianshu com p 51e4e84e02cd 三 编写脚本 脚本步骤如下
  • Echarts 地图使用,以及tooltip轮播使用

    一 首先下载Echarts npm install echarts save 二 引入Echarts 全局引入main js中 echarts 4 0 import echarts from echarts Vue prototype ec
  • sql注入-union select

    什么是SQL注入 SQL注入 Sql Injection 是一种将SQL语句插入或添加到应用 用户 的输入参数中的攻击 这些参数传递给后台的SQL数据库服务器加以解析并执行 哪里存在SQL注入 GET POST HTTP头部注入 Cooki
  • 第一次使用Arduino IDE(mac os) 配置合宙ESP32C3(9.9包邮)且烧录代码的历程

    目录 Arduino 配置ESP32 1 Arduino 请更新至最新版 2 科学上网 3 添加开发板管理URL 配置 1 连接开发板 2 Arduino IDE 的配置 3 烧录代码 Arduino 配置ESP32 1 Arduino 请
  • java实现pdf上传、下载、在线预览、删除、修改等功能

    资源下载 pdf上传 下载 在线预览 删除 修改功能源码 最近闲来无事 做了一个pdf的小功能 以供各位大神参考 下面是效果展示图 功能主页 点击上传PDF按钮 上传文件之后 在线预览 开发环境 jdk 1 8 mysql 5 7 开发工具
  • 蓝牙 bluetooth-之一

    蓝牙profile的作用 蓝牙子系统应用程序的交互通过蓝牙profile实现 profile有些文献将其解释为子协议 似乎不是很准确 我依然以profile称呼它 蓝牙profile定义了蓝牙子系统分层结构中的每一层需要具有的功能和特性 G
  • loss.backward() Segmentation fault 段错误

    在运行一个非常简单的深度学习程序的时候 发现运行一段时间会报错 段错误 经过定位发现是执行loss backward 的时候出现的问题 而源码明显是没有什么问题的 具体排查可以这样 gdb args python train py 然后发现
  • CDI(Weld)基础<2> JSF+CDI+JPA项目示例

    2019独角兽企业重金招聘Python工程师标准 gt gt gt CDI可以理解为Spring 但其中的一些功能比spring更强大 灵活 本章是个简单的项目示例 推荐有一定基础的看 1 JPA定义 MVC M Entity public
  • dom型xss ---(waf绕过)

    目录 1 漏洞源码 2 进行绕过 2 1 使用老方法进行绕过 注入失败原因 2 2 分析注入方式 2 3 使用svg进行绕过 方式一 2 3 1 了解什么是dom树 以及dom树的构建 2 3 3 分析img方法失败的原因 可以在页面上添加
  • 二阶段提交java_分布式事务(一)两阶段提交及JTA

    分布式事务 分布式事务简介 分布式事务是指会涉及到操作多个数据库 或者提供事务语义的系统 如JMS 的事务 其实就是将对同一数据库事务的概念扩大到了对多个数据库的事务 目的是为了保证分布式系统中事务操作的原子性 分布式事务处理的关键是必须有
  • 【原生js实现】吃掉病毒,还森林一片祥和

    嗨 大家好 我是法医 一只治疗系前端码猿 与代码对话 倾听它们心底的呼声 期待着大家的点赞 与关注 游戏开始前 请容我感叹一声 掘金活动真多鸭 快卷不过来了 哈哈 其实最近也一直在加班 眼看游戏投稿时间快过去了 当初真想做个小游戏出来参加活
  • 基础编程练习 7-26 单词长度 (15 分)

    这个题的测试用例只卡在了空句子那一个 题目没有明确给出只有一个 的时候 什么也不输出直接结束 C include
  • sublime text中换行符替换成空(mac版)

    sublime text做字符串处理 需要将 xxx xxx xxx 改造为 xxx xxx xxx 1 alt command f 调用替换界面 2 shift command enter 在find what 输入 换行符 3 repl
  • Sqlite 嵌入式数据库移植和使用

    1 源代码的获取 sqlite是一个功能强大 体积小运算速度快的嵌入式数据库 采用了全C语言封装 并提供了八十多个命令接口 可移植性强 使用方便 下载地址 http sqlite org download html sqlite源代码 sq
  • 【Educoder python 作业答案】国防科技大学《大学计算机基础》Python入门-绘制炸弹轨迹 ※

    Educoder python 作业答案 国防科技大学 大学计算机基础 Python入门 绘制炸弹轨迹 第1关 绘制一个坐标点 第2关 绘制n个坐标点 第3关 绘制一条轨迹 第4关 更简单的绘制一条轨迹 第5关 绘制多条轨迹 第1关 绘制一
  • 接口传参时,不写字段,这种格式http://localhost:9000/findData/1 取参

    GetMapping findData id public List
  • 7.GDB与文件IO

    1 GDB 什么是 GDB 调试 1 1 GDB 准备工作 gdb 是一个 shell 指令 必须带有 g 的参数 程序才将调试信息添加到文件中 g g a cpp o a out 先为文件添加调试信息 打开所有的 warning 选项 g
  • C++的类继承与类模板

    类继承是面向对象编程中很重要 也是很难 的内容 其能有效地提高代码复用水平 提高开发的效率 目录 基本概念 公有继承 私有继承 保护继承 包含 多重继承 类模板 基本概念 继承的种类与特点 C 中提供了几种继承 分别为公有继承 public