第四章 C++的类

4.1类的定义及声明

4.1.1基本概念
  • 类保留字:class、struct或union可用来声明和定义类。
  • 类的声明:由保留字class、struct或union加上类的名称构成。
  • 类的定义:包括类名的声明部分和由{ }括起来的主体两部分构成。
  • 类的实现:通常指类的函数成员的实现:即定义类的函数成员的函数体。

class 类型名;//前向声明

class 类型名{//类的定义

private:

​ 私有成员声明或定义;

protected:

​ 保护成员声明或定义;

public:

 公有成员声明或定义;

};


4.1.2定义类时应注意的问题:
  • 使用private、protected和public保留字标识主体中每一区间的访问权限,同一保留字可以多次出现;

  • 同一区间内可以有数据成员、函数成员和类型成员,习惯上按类型成员、数据成员和函数成员分开;

  • 成员在类定义体中出现的顺序可以任意,函数成员的实现既可以放在类的外面,也可以内嵌在类定义体中(此时会自动成为内联函数);但是数据成员的定义顺序与初始化顺序有关。

  • 若函数成员在类定义体外实现,则在函数返回类型和函数名之间,应使用类名和作用域运算符“::”来指明该函数成员所属的类。

  • 类的定义体花括号后要有分号作为定义体结束标志。

  • 在类定义体中允许对所数据成员定义默认值,若在构造函数的参数列表后面的“:”和函数体的“{”之间对其进行了初始化(在成员初始化列表进行初始化),则默认值无效,否则用默认值初始化;

  • 构造函数和析构函数都不能定义返回类型。

  • 如果类没有自定义的构造函数和析构函数,则C++为类生成默认的参数表无参的构造函数和析构函数。

  • 构造函数的参数表可以出现参数,因此可以重载。

  • 构造函数和析构函数:是类封装的两个特殊函数成员,都有固定类型的隐含参数this(对类A,this指针为A* const this) 。

  • 构造函数:函数名和类名相同的函数成员,用来产生对象,为对象申请资源,初始化数据成员。可在参数表显式定义参数,通过参数变化实现重载。

     class Circle{
        public:
            double radius;
        public:
            Circle() { radius = 1.0;} //缺省构造函数,将半径设为1.0
            Circle (double r) {       //带参数构造函数,用户创建Circle对象时可以
                                      //指定圆的半径r
                if( r > 0.0 ) radius = r;
                else radius = 1.0;
            }
        };
    
         Circle c1;                  //用户没指定参数,调用缺省构造函数 
         Circle c2( 5.0 ); //调用带参数构造函数,实参5.0传给形参r,将对象c2的半径设为5.0
         cout  << c1.radiuse;         //1.0
         cout  << c2.radiuse;         //5.0
  • 析构函数:函数名和类名相同且带波浪线的参数表无参函数成员。故无法通过参数变化重载析构函数。

  • 定义变量或其生命期开始时自动调用构造函数,生命期结束时自动调用析构函数。
    同一个对象仅自动构造一次。构造函数是唯一不能被显式(人工,非自动)调用的函数成员。

  • 构造函数用来为对象申请各种资源,并初始化对象的数据成员。构造函数有隐含参数this,可以在参数表定义若干参数,用于初始化数据成员。

  • 析构函数是用来毁灭对象的,析构过程是构造过程的逆过程。析构函数释放对象申请的所有资源。

  • 析构函数既能被显式调用,也能被隐式(自动)调用。由于只有一个固定类型的this,故不可能重载,只能有一个析构函数。

  • 若实例数据成员有指针,应当防止反复析构(用指针是否为空做标志)。

  • 联合也是类,可定义构造、析构以及其它函数成员。

    【示例4.1】定义字符串类型和字符串对象

    #include <alloc.h>
    #include <string.h> //如果函数同名, 可用单目::访问string.h里的全局函数
        struct STRING {
            typedef  char *CHARPTR;    //定义类型成员
            CHARPTR  s;                //定义数据成员,等价于char *s; 
            int  strlen( );            //声明函数成员,求字符串对象的的长读(有隐含this)
            STRING(CHARPTR);        //声明构造函数,无返回类型,有隐含this
            ~STRING( );                //声明析构函数,有隐含this
        };
      //必须用STRING ::限制strlen,
        int STRING::strlen( ){         //用运算符::在类体外定义
            int  k;
            for(k=0; s[k]!=0; k++);
            return k;
        }
        STRING::STRING(char *t){//在类体外定义构造函数, 必须加类名::限定,无返回类型
            int  k;
            for(k =0; t[k]!=0; k++);
            s=(char *)malloc(k+1);       //s等价于this->s
            for(k=0; (s[k]=t[k])!=0; k++);
        }
        STRING::~STRING( ) {//在类体外定义析构函数, 必须加类名::限定,无返回类型       
            free(s); 
        }
        struct  STRING  x("simple");    //struct可以省略
        void main( ){ 
            STRING  y("complex"), *z=&y; 
            int  m=y.strlen( );         //当前对象包含的字符串的长度            
            m=z->strlen( );
        }   //返回时自动调用y的析构函数
    
        #include <alloc.h>
        #include <string.h> //如果函数同名, 可用单目::访问string.h里的全局函数
        struct STRING {
            typedef  char *CHARPTR;    //定义类型成员
            CHARPTR  s;                //定义数据成员,等价于char *s; 
            int  strlen( );            //声明函数成员,求字符串对象的的长读(有隐含this)
            STRING(CHARPTR);        //声明构造函数,无返回类型,有隐含this
            ~STRING( );                //声明析构函数,有this
        };
      //另外一种实现,必须用STRING ::限制strlen, 否则递归
        int STRING::strlen( ){         //用运算符::在类体外定义
            return   ::strlen (s);     
        }
4.1.3不同对象的构造和析构
  • 构造函数在自动变量 (对象)被实例化或通过new产生对象时被自动调用一次,是唯一不能被显式调用的函数成员

  • 析构函数在变量 (对象)的生命期结束时被自动调用一次,通过new产生的对象需要用delete手动释放(调用析构)。析构函数可被显式反复调用。有些资源是不能反复析构的,例如不能反复关闭文件,因此,必要时要防止对象反复释放资源

  • 全局变量 (对象) :main执行之前由开工函数调用构造函数,main执行之后由收工函数调用析构函数。

  • 局部自动对象 (非static变量) :在当前函数内对象实例化时自动调用构造函数, 在当前函数正常返回时自动调用析构函数。

  • 局部静态对象 (static变量) :定义时自动调用构造函数, main执行之后由收工函数调用析构函数。

  • 常量对象:在当前表达式语句定义时自动调用构造函数,语句结束时自动调用析构函数

    //全局对象,mian函数之前被开工函数构造,由收工函数自动析构,全局对象在数据段里
    STRING  gs(“Global String”); 
    
    void f(){
        //局部自动变量,函数返回后自动析构,局部对象在堆栈里
        STRING x(“Auto local variable”); 
    
        //静态局部变量,由收工函数自动析构,静态局部对象在数据段里
        static STRING y(“Static local variable”); 
    
        //new出来的对象在堆里(Heap)
        STRING *p = new STRING(“ABC”);     
        delete(p); //delete new出来的对象的析构是程序员的责任
    }
    
    //C++11新标准中出现了智能指针的概念,就是要减轻程序员的必须承担的手动delete动态分配的内存(new出来的东西)的负担
    #include <string.h>
    #include <alloc.h>
    #include <iostream.h>
    struct STRING{
        char *s;       
        STRING (char *) ; 
        ~STRING ( ) ; 
    }; 
    STRING::STRING (char *t)   {  
       s= (char *) malloc  (strlen (t) +1)   ; 
        strcpy (s, t)   ; 
        cout<<"Construct: "<<s; 
    }
    STRING::~STRING ( )   {
      cout<<"Deconstruct:"<<s; 
        free (s)   ;    
    }
    void main (void)   {
        STRING s1 ("String 1\n")   ; 
        s1.~STRING ( ) ; //显式析构s1,会free(s)
    }//自动析构s1,会导致s1的s指针指向内存又释放,会出现运行时错误
    
    //最先定义的自动对象最后自动析构。可随时手动调用析构函数,但要防止反复释放资源。
    #include <string.h>
    #include <alloc.h>
    #include <iostream.h>
    struct STRING{
        char *s;       
        STRING (char *) ; 
        ~STRING ( ) ; 
    }; 
    STRING::STRING (char *t)   {  
       s= (char *) malloc  (strlen (t) +1)   ; 
        strcpy (s, t)   ; 
        cout<<"Construct: "<<s; 
    }
    STRING::~STRING ( )   {
    //防止反复释放内存
        if (s==0)    return; 
        cout<<"Deconstruct:"<<s; 
        free (s)   ;    
        s=0;  //提倡0代替NULL指针
    }
    void main (void)   {
        STRING s1 ("String 1\n")   ; 
        STRING s2 ("String 2\n")   ; 
        STRING ("Constant\n")   ; 
        cout<< "RETURN\n"; 
       // s1.~STRING ( ) ; //显式析构s1,这一句取消注释,不会出错
    }//自动析够s2,  s1
    
    

/*
显示内容:
Construct:String1
Construct:String2
Construct:Constant
Deconstruct:Constant
RETURN
Deconstruct:String2
Deconstruct:String1
最先定义的自动对象最后自动析构。可随时手动调用析构函数,但要防止反复释放资源。
*/

##### 4.1.4程序不同结束形式对对象的影响:

- **exit退出**:局部自动对象不能自动执行析构函数,故此类对象资源不能被释放。静态和全局对象在exit退出main时自动执行收工函数析构。

- **abort退出**:所有对象自动调用的析构函数都不能执行。局部和全局对象的资源都不能被释放,即abort退出main后不执行收工函数。

- **return返回**:隐式调用的析构函数得以执行。局部和全局对象的资源被释放。

- **提倡使用return**。如果用abort和exit,则要显式调用析构函数。另外,使用异常处理时,自动调用的析构函数都会执行。

  **【示例4.2】**exit和abort的正确使用方法

  ```c++
  #include <process.h>
  #include “string.cpp”              //不提倡这样include:因为string.cpp内有函数定义
  STRING x("global");                //自动调用构造函数初始化x
  void main(void){
        short error=0;
        STRING y("local");        //自动调用构造函数初始化y
        switch(error) {
        case  0: return;          //正常返回时自动析构x、y
        case  1: y.~STRING( );    //为防内存泄漏,exit退出前必须显式析构y
                      exit(1);            
        default: x.~STRING( );    //为防内存泄漏,abort退出前须显式析构x、y
          y.~STRING( ); 
          abort( );
         }
  }
4.1.5接受与删除编译自动生成的函数
  • default接受, delete:删除。

    【示例4.3】使用delete禁止构造函数以及default接受构造函数。

    struct A {
        int x=0;
        A( ) = delete;        //删除产生构造函数A( )
        A(int m): x(m) { }
        A(const A&a) = default;    //接受编译生成的拷贝构造函数A(const A&)
    };
    void main(void) {
        A x(2);            //调用程序员自定义的单参构造函数A(int)
        A y(x);            //调用编译生成的拷贝构造函数A(const A&)
        //A u;            //错误:u要调用构造函数A( ),但A( )被删除    
        A v( );            //正确:说明外部无参非成员函数v,且返回类型为A
    }//“A v( );”等价于“extern  A v( );”
    

4.2 成员访问权限及其访问

  • 封装机制规定了数据成员、函数成员和类型成员的访问权限。包括三类:

    private:私有成员,本类函数成员可以访问;派生类函数成员、其他类函数成员和普通函数都不能访问。

    protected:保护成员,本类和派生类的函数成员可以访问,其他类函数成员和普通函数都不能访问。

    public:公有成员,任何函数均可访问。

  • 类的友元不受这些限制,可以访问类的所有成员。可以在private、protected和public等任意位置说明,友元可以像类自己的函数成员一样访问类的所有成员。另外,通过强制类型转换可突破访问权限的限制。

  • 构造函数和析构函数可以定义为任何访问权限。不能访问构造函数则无法用其初始化对象。

  • 进入class定义的类时,缺省访问权限为private;进入struct和union定义的类时,缺省访问权限为public

    【示例4.5】为女性定义FEMALE类。

        class  FEMALE{        //缺省访问权限为private
            int  age;          //私有的,自己的成员和友员可访问
        public:                //访问权限改为public
            typedef char *NAME;     //公有的,都能访问
        protected:            //访问权限改为protected
            NAME nickname;  //自己的和派生类成员、友员可访问
            NAME getnickname( );
        public:                //访问权限改为public
            NAME name;        //公有的,都能访问
        };
    
    FEMALE::NAME FEMALE::getnickname( ){
        return  nickname;     //自己的函数成员访问自己的成员
    }
    void  main(void){        //main没有定义为类FEMALE的友员
        FEMALE  w; 
        FEMALE::NAME(FEMALE::*f)( ); 
        FEMALE::NAME n;
        n=w.name;                       //任何函数都能访问公有name
        n=w.nickname;                  //错误,main不得访问保护成员
        n=w.getnickname( );          //错误,main不得调用保护成员
        int  d=w.age;                 //错误,main不得访问私有age
        f=&FEMALE::getnickname;     //错误,不得取保护成员地址
    }
    

4.3 内联、匿名类及位段

  • 函数成员的内联说明:
    在类体内定义的任何函数成员都会自动内联。
    在类内或类外使用inline保留字说明函数成员。

  • 内联失败:有分支类语句、定义在使用后,取函数地址,定义(纯)虚函数。

  • 内联函数成员的作用域局限于当前代码文件。

  • 匿名类函数成员只能在类体内定义(内联)。

  • 函数局部类的函数成员只能在类体内定义(内联),某些编译器不支持局部类。

    内联:

    class COMP {
        double  r,  v; 
    public:
        COMP (double rp, double vp)   { r=rp; v=vp; } //自动内联
        inline double getr ( );      //inline保留字可以省略, 后面又定义
        double getv ( )   ; 
    }; 
    inline double COMP::getv ( )   { return v;  }    //定义内联
    void main (void)   {        
        COMP  c(3,  4)   ; 
        double  r=c.getr ( )   ;     //此时getr的函数体未定义, 内联失败
        double  v=c.getv ( )   ;     //函数定义在调用之前, 内联成功
    }
    inline double COMP::getr ( )   { return r;  }     //定义内联
    
  • 对于没有对象的匿名联合,C++兼容C的用法:
    没有对象的全局匿名联合必须定义为static,局部的匿名联合不能定义为static ;
    匿名联合内只能定义公有数据成员;
    数据成员和联合本身的作用域相同;
    数据成员共享存储空间。

    #include <iostream.h>
    static union { int x, y, z; };
    //int y=5;             //错:本作用域已定义y
    void main(void){ 
        x=3; cout<<y; //输出3
    }
    
    /*
    相当于定义:
    static int x;
    static int &y=x;
    static int &z=x;
    x,y,z作用于当前文件
    */

4.4 new和delete

  • 内存管理的区别:
    C:函数malloc、free;C++:运算符new、delete。
    内存分配:malloc为函数,参数为值表达式,分配后内存初始化为0;new为运算符,操作数为类型表达式,先底层调用malloc,然后调用构造函数
    用“new 类型表达式 {}”可使分配的内存清零,若“{}”中有数值可用于初始化。
    内存释放:free为函数,参数为指针类型值表达式,直接释放内存;delete为运算符,操作数为指针类型值表达式,先调用析构函数,然后底层调用free

  • 如为简单类型(没有构造、析构函数)分配和释放内存,则new和malloc、 delete和free没有区别,可混合使用:比如new分配的内存用free释放。

  • 无法用malloc代替new初始化

  • 注意delete的形参类型应为const void*,因为它可接受任意指针实参。

  • new <类型表达式> //后接()或{ }用于初始化或构造。{}可用于数组元素
    类型表达式:int p=new int; //等价int *p=new int(0);
    数组形式仅第一维下标可为任意表达式,其它维为常量表达式:int (
    q)[6][8]=new int[x+20][6][8];
    为对象数组分配内存时,必须调用参数表无参构造函数
    注意int *p=new int (10) ; 和int *p=new int [10] ;的区别

  • delete <指针>
    指针指向非数组的单个实体:delete p; 可能调析构函数。

  • delete [ ]<数组指针>
    指针指向任意维的数组时:delete [ ]q;
    如为对象数组,对所有对象(元素)调用析构函数,然后释放对象数组占有的内存。
    若数组元素为简单类型,则可用delete <指针>代替。

    【示例4.6】定义二维整型动态数组的类。

    #include <alloc.h>
    class  ARRAY{                    //class体的缺省访问权限为private
        int    *a, r, c;            
    public:                         //访问权限改为public
        ARRAY(int x, int y);    
        ~ARRAY( );    
    };
    ARRAY::ARRAY(int x, int y){  
        a=new int[(r=x)*(c=y)];        //可用malloc:int为简单类型
    }
    ARRAY::~ARRAY( ){                 //在析构函数里释放指针a指向的内存
        if(a){ delete [ ]a; a=0;}    //可用free(a), 也可用delete a
    }    
    ARRAY  x(3, 5);                 //开工函数构造,收工函数析构x
    void main(void){
        ARRAY  y(3, 5), *p;          //退出main时析构y
        p=new ARRAY(5, 7);               //不能用malloc,ARRAY有构造函数
        delete  p;                        //不能用free,否则未调用析构函数
    }                                //退出main时,y被自动析构
    //程序结束时,收工函数析构全局对象x
    
  • 两种内存管理的对比示意图
    图片说明
    图片说明

  • new还可以对已经析构的变量重新构造。可以减少对象的说明个数,提高内存的使用效率。(不是所有C++编译器都支持)

    STRING  x ("Hello!"), *p=&x;
    x. ~STRING ( );
    new (&x) STRING ("The World");
    new (p) STRING ("The World");
  • 这种用法可以节省内存或栈的空间。

4.5 隐含参数this

  • lthis指针是一个特殊的指针,它是普通函数成员(非静态函数成员)隐含的第一个参数,其类型是指向要调用该函数成员的对象的const指针。(A * const this)

  • 当通过对象调用函数成员时,对象的地址作为函数的第一个实参首先压栈,通过这种方式将对象地址传递给隐含参数this。

  • 构造函数和析构函数的this参数类型固定。例如A::~A()的this参数类型为A*const this;

    析构函数的this指向可写对象,但this本身是只读的

  • 注意:可用this来引用或访问调用该函数成员的普通、const或volatile对象;类的静态函数成员没有隐含的this指针;this指针不允许移动。

    class A{
        int age;
    public:    
        void setAge( int age){
            this->age = age;    //this类型:A*const this
        }
    }
    
    A a; 
    a.setAge(30); 
    
    
    

//函数setAge通过对象a被调用时,setAge函数的第一个参数是
//A*const this指针,指向调用对象a。this->age = a.age = 30

- **【示例4.7】**在二叉树中查找节点。

  ```c++
  #include <iostream.h>
  class  TREE{
      int   value; 
      TREE  *left, *right;
  public:
      TREE (int);         //this类型: TREE * const this
       ~TREE( );           //this类型: TREE * const this,析构函数不能重载
        const TREE *find(int) const; //这个const是修饰this指针,this类型: const TREE * const this
      };
  TREE::TREE(int value){         //隐含参数this指向要构造的对象
      this->value=value;        //等价于TREE::value=value
      left=right=0;             //C++提倡空指针NULL用0表示
  }
  TREE::~TREE( ){                //this指向要析构的对象
      if(left) { delete left; left=0; }
      if(right) { delete right; right=0; }
  }
  const TREE* TREE::find(int v) const {    //this指向调用对象
      if(v==value) return this;    //this指向找到的节点
      if(v<value)                 //小于时查左子树,即下次递归进入时新this=left
          return  left!=0?left->find(v):0;
      return  right!=0?right->find(v):0; //否则查右子树
  } //注意find函数返回类型必须是const TREE *
  TREE root(5);                     //收工函数将析构对象root
  void main(void){
      if(root.find(4)) cout<<“Found\n”;
  }

4.6 对象的构造与析构

  • 类的非静态数据成员可以在如下位置初始化:
    类体中进行就地初始化(称为就地初始化)(=或{}都可以),但初始化列表的效果优于就地初始化。
    成员初始化列表初始化
    可以同时就地初始化和在成员初始化列表里初始化,当二者同时使用时,并不冲突,初始化列表发生在就地初始化之后,即最终的初始化结果以初始化列表为准。
    在构造函数体内严格来说不叫初始化,叫赋值

  • 非静态数据成员初始化:

    class Person {
    private:
        int id;
    public:
        Person(int id){ this->id = id; }
    };
    
    class A {
    public:
        int i = 10;                //可以用=号初始化
        double d{ 3.14 };        //可以用{}初始化
        int j = { 0 };            //可以用 = {}来初始化
        Person p{ 100 };        //没有默认构造函数的类成员也可以就地初始化而不用出现在成员初始化列表里
        //int k(0);                //不能用()来初始化
        //Person p(100);        //不能用()初始化
    
    } a;
    //可以同时就地初始化和在成员初始化列表里初始化,当二者同时使用时,即最终的初始化结果以初始化列表为准(即使是引用和const成员)。
    class B {
    public:
        int i = 0;        //就地初始化
        int &r = i;        //引用也可以就地初始化而不用出现在成员初始化列表里 
        int j;
        const double PI = 3.14; //const成员也可以就地初始化而不用出现在成员初始化列表里
    public:
        B() :i(10),j(100),r(j),PI(6.28) { }// 成员初始化列表初始化,这时
    } b;
    
    void testB() {
        cout << b.i << “ ” << b.r << “ ” << b.PI << endl; 
        //输出:10 100 6.28  ,说明r绑定到j,const常量PI的值为6.28
    }
    //类内就地初始化
    int C = 7;            //定义整型变量C
    struct C {            //定义类型C
        C(int i) {}
    };
    //当要使用类型C时,必须加class来修饰,让编译器知道是用class C
    class C o{C}        //定义一个C类型的对象o,构造函数的实参是变量C
    
    struct D {
    //定义一个C类型的数据成员x,并用变量C作为构造函数实参,若可用()来就地初始化,
    //class C x(C);      //这时编译器很难区分这是不是一个成员函数声明:函数名x,形参和返回为C类型
    };
    
    //顺便说明,如果要声明一个函数成员:函数名x,形参类型和返回为类C类型,应该写成
    class C x(class C);
    //成员初始化列表初始化
    class A {
        int i;
        int j;
    public:
        A( ):j(0),i(0) {    //还是先初始化i, 后初始化j。初始化按定义顺序
            i = 1; j = 1;     //在函数体内不应看做初始化,而是赋值    
                }     
        };
    //成员按照在类体内定义的先后次序初始化,与出现在初始化位置列表的次序无关; 
  • 类可能会定义只读(const)引用类型的非静态(static)数据成员,在使用它们之前必须初始化;若这二种成员没有类内就地初始化,该类必须定义构造函数初始化这类成员。

  • 类A还可能定义类B类型的非静态对象成员,若B类型对象成员必须用带参数的构造函数构造,且该成员没有类内就地初始化,则A必须自定义初始化构造函数(自定义的类A的构造函数,传递实参初始化类B的非静态对象成员:缺省的无参的A( )自动只调用无参的B( ))。

  • const和引用成员初始化

    class WithReferenceAndConstMembers {
    public:
        int i = 0;                        //就地初始化
        int &r;                            //引用成员,没有就地初始化
        const double PI;                //没有就地初始化
    } ws;

    此时会引发编译报错:error C2280: “WithReferenceAndConstMembers(void)”: 尝试引用已删除的函数,因为实例化ws时无法使用编译器默认生成的构造函数,所以编译器会将这个默认的构造函数删掉。

    这时必须自己定义构造函数,没有自定义构造函数何来成员初始化列表。构造函数是否带参数取决于程序员的选择。关键是需要一个成员初始化列表

    class WithReferenceAndConstMembers {
    public:
        int i = 0;                        //就地初始化
        int &r;                            //引用成员,没有就地初始化
        const double PI;                //没有就地初始化
        WithReferenceAndConstMembers() :r(i), PI(3.14) {}  //这时r和PI必须在成员初始化列表初始化
    } ws;
  • 对象成员初始化

    class Person {
    private:
        int id;
    public:
        Person(int id){ this->id = id; }  // 注意Person没有默认构造函数,实例化对象必须带参数
    };
    class WithPersonObject {
    public:
        Person p;    //没有就地初始化,Person p{1};
    } wpo;

    同样的,此时会引发编译报错error C2280: “non_static_member_initialize::WithPersonObject::WithPersonObject(void)”: 尝试引用已删除的函数。因为数据成员“WithPersonObject::p”没有适合的 默认构造函数 或重载决策不明确,所以已隐式删除函数。

    这时必须自己定义构造函数,没有自定义构造函数何来成员初始化列表。构造函数是否带参数取决于程序员的选择。关键是需要一个成员初始化列表

    class WithPersonObject {
      public:    Person p;    
      //这时p必须在成员初始化列表初始化    
      WithPersonObject() :p(i) {}
    } wpo;
  • 构造函数的成员初始化列表在参数表的“:”后,{}之前.所有数据成员建议在此初始化,未在成员初始化列表初始化的成员也可类内就地初始化,既未在成员初始化列表初始化,也未类内就地初始化的非只读、非引用、非对象成员的值根据对象存储位置可取随机值(栈段)或0及nullptr值(数据段)。

  • 按定义顺序初始化或构造数据成员(大部分编译支持)。

  • 如未定义或生成构造函数,且类的成员都是公有的,则可以用”{ }”的形式初始化(和C的结构初始化一样)。联合仅需初始化第一个成员。

  • 对象数组的每个元素都必须初始化,默认采用无参构造函数初始化。

  • 单个参数的构造函数能自动转换单个实参值成为对象

  • 若类未自定义构造函数,编译会尝试自动生成构造函数。

  • 一旦自定义构造函数,将不能接受编译生成的构造函数,除非用default等接受。

  • 用常量对象做实参,总是优先调用参数为&&类型的构造函数;用变量等做实参,总是优先调用参数为&类型的构造函数。

  • 合成的默认构造函数:当没有自定义类的构造函数时,编译器会提供一个默认构造函数,称为合成的默认构造函数。合成的默认构造函数会按如下规则工作:

    如果为数据成员提供了类内初始值,则按类内初始值进行初始化
    否则,执行默认初始化

    但是有些场合下合成的默认构造函数会工作失败。

    合成的默认构造函数会工作是否成功,分以下情况

    1)如果为数据成员提供了类内初始值,则工作成功;
    2)**如果没有为数据成员提供类内初始值**,则:

    A:如果只包含非const、非引用内置类型数据成员,如果类的对象是全局的或局部静态的,则这些成员被默认初始化为0,如果类的对象是局部的,如果程序要访问这些成员,则编译器报错;合成的默认构造函数工作失败;如果对象是new出来的(在堆里),访问这些成员编译器不报错,但值为随机值;

    //如果非引用,非const的内置数据成员没有进行任何形式的初始化
    class A {
    public:
        int i;
    } e;      //全局对象,在数据段
    
    

void testA() {
static A se; //局部静态对象,在数据段
A o; //局部对象,在堆栈段,只要不访问对象成员,编译器不报错
cout << e.i << endl; //对e.i执行了默认初始化,为0
cout << se.i << endl; //对se.i执行了默认初始化,为0
//cout << o.i << endl; //一旦访问局部对象的未初始化数据成员,编译器就报错:使用了未初始化的局部变量“o”。因为o.i初始化失败

  A *p = new A;              //p指向堆里new出来的对象
  cout << p->i << endl;    //编译器不报错,但i的值是一个随机值-842150451
  delete p;

}



  B:如果包含了const、引用数据成员,一旦实例化对象,**编译器会报错;合成的默认构造函数工作失败;**

  ```c++
  class B {
  public:
      int i = 0;
      int &ri;
  const int c;
  }; //只要不实例化对象,编译器不报错

  B ob; //只要实例化对象,编译器立刻报错

编译器报错信息(VS2017):

error C2280: “synthesized_default_constructor::B::B(void)”: 尝试引用已删除的函数

note: 编译器已在此处生成“synthesized_default_constructor::B::B”

note: “synthesized_default_constructor::B::B(void)”: 因为

“synthesized_default_constructor::B”有一个未初始化的常量限定的数据成员“synthesized_default_constructor::B::c”,所以已隐式删除函数

note: 参见“synthesized_default_constructor::B::c”的声明

C: 如果包含了其他class类型成员且这个类型没有默认构造函数,那么编译器无法默认初始化该成员;编译器会报错;合成的默认构造函数工作失败;

  //Person自定义构造函数,因此编译器不再提供合成的构造函数
  //更糟糕的是Person自定义的构造函数是带参数的,这就意味着Person类无法默认初始化,即该类对象没有默认值
  class Person {
  public:
      int i;
  public:
      Person(int i) { this->i = i; }
  };

  class C {
  public:
      Person p; //包含一个Person类型成员,且这个类没有默认构造函数
  };

  C oc; //只要实例化对象,编译器立刻报错,报错信息如下(VS2017)

图片说明

​ 任何情况下,只要合成的默认构造函数工作失败,编译器会报错。工作失败的原因就是有成员没有初始值,而且也不能默认初始化该成员。因此,我们一定要保证类的所有数据成员或者有初始值,或者能默认初始化。

​ 对于const、引用成员,只能就地初始化和在成员初始化列表里初始化,不能在构造函数体内被赋值。

​ 当没有自定义类的构造函数时,编译器会提供一个合成的默认构造函数。但自定义了构造函数后,能否要求编译器仍然提供合成的默认构造函数呢?可以,使用=default

  class PersonWithoutDefaultConstructor {
  public:
      int i = 100;    //就地初始化
  public:
      //自定义带参数构造函数,因此没有默认构造函数
      PersonWithoutDefaultConstructor(int i) { this->i = i; }
  };
  //PersonWithoutDefaultConstructor p; //编译报错:没有合适的默认构造函数可用

  class PersonWithDefaultConstructor {
  public:
      int i = 100;//就地初始化
  public:
      //自定义带参数构造函数
      PersonWithDefaultConstructor(int i) { this->i = i; }

      //要求编译器提供合成的默认构造函数
      PersonWithDefaultConstructor() = default; //=default出现在类内部,这时默认构造函数是内联的
  };
  PersonWithDefaultConstructor p; //这时p可以执行默认构造了
  class PersonWithDefaultConstructor {
  public:
      int i = 100;    //就地初始化
  public:
      //自定义带参数构造函数
      PersonWithDefaultConstructor(int i) { this->i = i; }

      PersonWithDefaultConstructor();
  };
  PersonWithDefaultConstructor::PersonWithDefaultConstructor() = default;
  //=default也可以作为实现出现在类外部,这时默认构造函数不是内联的
  • 隐式的类型转换和显式构造函数

    ​ 如果构造函数只接受一个实参时,实际上它定义了转换为此类类型的转换机制,这种构造函数称为转换构造函数。如果想抑制这种隐式转换,必须将转换构造函数声明为explicit

    ​ explicit只对接受一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换。

    ​ 只能在类内声明构造函数时使用explicit关键字

    class Integer {
    public:
        int value;
    public:
        //隐式地提供了从int到Integer的转换功能
        Integer(int value): value(value){}
    };
    class Age {
    public:
        Integer age{ 0 };
    public:
        //隐式地提供了从Integer到Age的转换功能
        Age(Integer i) :age(i) {}
    };
    void test() {
    // 隐式地将int转换成Integer
    Integer integer = 25; // 对于转换构造函数(单参数),可以用=初始化;也可以Integer b(35)
    Age age = integer;     //隐式地将Integer转换成Age
    
    //但编译器只能自动进行一次转换
    //Age a = 100; //编译报错:不存在int到Age的转换构造函数。如果成立,就有二次隐式转换:int->Integer->Age
    //只能这样
    Age a = Integer(100);     //先显式地int->Integer,再隐式地Integer->Age
    }
    //有时想抑制这种隐式转换,这时必须把构造函数声明为explicit
    class ExplicitInteger {
    public:
        int value;
    public:
        explicit ExplicitInteger(int value) : value(value) {}
    };
    class ExplicitAge {
    public:
        Integer age{ 0 };
    public:
        ExplicitAge(Integer i) :age(i) {}
        ExplicitAge() = default;
    };
    void test_explicit() {
    //编译报错:不存在从int到ExplicitInteger的构造函数, 这种用=来初始化的形式不再支持
    //ExplicitInteger i = 10; 
    
    //编译也报错:赋值列表初始化不能使用标记为explicit的构造函数
    //ExplicitInteger j = { 10 }; 
    
    ExplicitInteger k(0);    //只能以这种形式
    
    ExplicitInteger l{ 0 };    //或以这种形式
    }
    class A {
    public:
        int i;
        int j;
    public:
        A(int i, int j = 0) {}  // 同样是转换构造函数
    };
    A a = 0;                      //这样初始化也成立
    class A {
    public:
        int i;
        int j;
    public:
        explicit A(int i, int j = 0) {}  // 同样是转换构造函数
    };
    //A a = 0;      //这样就不成立成立
  • 【示例4.8】包含只读、引用及对象成员的类。

    class A{
            int  a;
        public:    
            A(int x) { a=x;}     //重载构造函数,自动内联
            A( ){ a=0; }        //重载构造函数,自动内联
        };
    class B{
            const  int  b;         //b没有就地初始化
            int  c, &d, e, f;    //b,d,g,h都没有就地初始化,故只能在构造函数体前初始化
            A    g, h;             //数据成员按定义顺序b, c, d, e, f, g, h初始化
    public:                     //类B构造函数体前未出现h,故h用A( )初始化
        B(int y): d(c), c(y), g(y) ,b(y), e(y){//自动内联
            c+=y;            f=y; 
        }//f被赋值为y
    };
    void main(void){
        int x(5);                         //int x=5等价于int x(5)
        A a(x), y=5;              //A y=5等价于A y(5),请和上一行比较
        A *p=new A[3]{ 1, A(2)};          //初始化的元素为A(1), A(2), A(0)
        B b(7), z=(7,8);                //B z=(7,8)等价于B z(8),等号右边必单值
        delete [ ]p;            //防止内存泄漏:new产生的所有对象必须用delete
    }                                    //故(7,8)为C的扩号表达式,(7,8)=8