c++类成员的初始化

来源:互联网 发布:新网域名如何解析 编辑:程序博客网 时间:2024/06/05 18:18

c++类成员的初始化

总结下c++类成员初始化的几种方式:

  1.构造函数初始化(一般构造函数,拷贝构造函数,移动构造函数)

  2.把数据成员声明为一个类内初始值

类内初始值

  在C++98的标准里只有整型的静态常量才可以使用类内初始值。

    class Cls{        static const int a = 123;              //ok        static const double b = 1.23;          //编译器报错          static const int d = 1.24;        //ok        static int e = 123;                    //编译器报错    }

  关于类static初始化有几点值得注意:所有static数据成员都可以在类外声明;在类外定义的static成员必须省略static符号,static符号只能在类内出现。

    Class Cls{        static const int a;        static int b;    }    static const Cls::a = 123;       //编译器报错    const int Cls::a = 123;          //ok    static int Cls::b = 123;         //编译器报错    int cls::b = 123;                //ok

  在C++11的标准中对静态成员的规则不变,当时允许我们为类的数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。

另外需要注意的是当我们提供一个类内初始值的时候,必须以符号=或者花括号表示。

  然而,在我实践的时候还是碰到了一点问题。对于内置类型,都是可以使用类内初始值进行初始化的。但是对于其他类型g++支持类内初始值,vs编译器不支持。

    Class Cls{        int a = 123;        //ok        double b = 1.23;    //ok        string str1 = "helo world";         //ok        strint str2("hello world");        //编译器报错        vector<int> vec1 = {1, 2, 3}        //vs报错,g++通过        vector<int> vec2{1, 2, 3}          //vs报错, g++通过

一般构造函数

  构造函数的任务就是初始化类对象的数据成员,无论合适只要类对象被创建,就会执行构造函数。构造函数我们可以分为三类:一般构造函数(重载构造函数),拷贝构造函数,移动构造函数。在自己没有定义构造函数的情况下,编译器都会帮助我们合成,除了一些特殊情况:

  1. 我们自己定义了构造函数。

  2. 类中包含一个其他类类型成员,且这个成员的类型没有默认构造函数。

有几点需要注意:

  1. 构造函数不能被声明为const(如果声明为const,构造函数就无意义了)

  2. 成员初始化顺序和他们被声明的顺序相同

  3. 当我们对一个数据成员既使用了类内初始化,又使用了构造函数初始化的时候,数据成员的初始化值以构造函数为准。

    Class cls{        public:            int member2;            //首先初始化member2,然后初始化member1            Cls(int a = 3,int b = 2):member1(a),member2(b){}            int print(){ cout<<member<<endl; }        private:            int member1=0;        }        Cls c;        c.print();                  //结果为3

  C++11加入了一种特殊的构造函数:委托构造函数

    class Cls{    public:        Cls(int a, double b):member1(a), member2(b){}        Cls():Cls(2,2.4){}         //委托构造函数    private:        int member1;        double member2;    }

拷贝构造函数

   当我们没有定义自己的拷贝构造函数和拷贝运算符的时候编译器会为我们自动合成(如果可以合成的话)。即使我们定义了一般构造函数。我们可以举个编译器无法合成拷贝运算符的例子:
  

    class Cls1{    public:        Cls1() = default;        Cls1(const Cls1&) = delete;    }    class Cls2{    public:        Cls2() = default;        Cls1 c1;    }    Cls2 cls1;    Cls2 cls2(cls1);             //错误编译器无法为我们合成一个拷贝构造函数,因为我们无法拷贝Cls2中的成员c1。

  
  分辨拷贝初始化和直接初始化:

    string dots(10, '.');       //直接初始化    string s(dots);             //直接初始化    string s2 = dots;           //拷贝初始化    string str = "hello world"; //拷贝初始化    string nines = string(10, '9');  //拷贝初始化

  当我们使用=定义变量的时候会发生拷贝初始化。拷贝初始化是一种浅复制,我个人理解为当初始化发生浅复制的时候即为拷贝初始化。不过我们需要区分的两个不同概念:赋值和拷贝初始化。

    string str1;     str1 = "hello world";             //赋值    string nines;    nines1 = string(10, '9');         //赋值

  下面讲下拷贝构造函数的一般形式:

    class Cls{    public:        Cls();               //默认构造函数        Cls(const Cls&);     //拷贝构造函数        ...    }

  当我们没有定义拷贝构造函数也没有定义拷贝运算符(operator=)的时候,编译器会为我们合成一个拷贝构造函数(除了无法合成构造函数的情况)

    class Cls{    public:        Cls();    private:        int member1;        double member2;    }    //合成的拷贝构造函数    Cls::Cls(const Cls& sur):        member1(sur.member1),        member2(sur.member2){ }

移动构造函数

  移动构造函数不同与拷贝构造函数,移动构造函数是深复制,拷贝构造函数参数传递的是左值引用(&),移动构造函数传递的是右值引用(&&)。相比拷贝构造函数,移动构造函数从给定对象“窃取”资源。它通常不会分配任何资源。因此,移动构造函数通常不会抛出任何异常,我们应该将此事通知标准库。所以,通常我们将移动构造函数声明为noexcept。(noexcept出现在参数列表和冒号之间。如果将一个函数声明为noexcept,声明和定义处都应该有noexcept)。

  特别的,我们使用移动构造函数通常要保证移后源对象被销毁是无害的。意味着源对象必须不再指向被移动的资源,这些资源所有权已经属于新创建的对象。

Class Cls{    public:        Cls(Cls &&s) noexcept;    private:        int member1;        double member2;        vector<int> vec;        }    //相当于合成的移动构造函数。注意,定义处也要加上noexcept    Cls::Cls(Cls &&s) noexcept:        member1(s.member1),        member2(s.member2),        vec(vec){ }

  需要注意下编译器合成移动控制函数的条件:只有当一个类没有定义任何自己版本的拷贝控制运算符(包含析构函数),且类的每个非static数据成员可以移动时,编译器才会合成移动构造函数。

初始化顺序

   我们为什么要关心初始化的顺序?因为很有可能你就会被初始化顺序给坑了。举个例子:

    //正确的初始化顺序    const int num = 10;    int arr[num];       //错误的初始化顺序    const arr[num];                    const int num = 10;

  当然这个例子可能过于做作,几乎所有的程序员都不会犯这样的错误。当时如果这样的问题隐蔽点呢。

    class Cls{        int member2;        int member1;    public:        Cls() = default;        Cls(int a):member1(a),member2( 2*member1){} //错误    }

  这个例子的初始化是错误的。因为 一个类的成员初始化顺序是按照成员在类中的声明顺序的。member2比member1先声明,因此member2会比member1先初始化,即使在构造函数中我们把member1排在前面。更要命的是我们这样做编译器是允许的。而得到的member2却是未定义的。

  甚至还有种更加隐蔽的初始化顺序错误。

    //file1.c    class Cls_file1{    public:        static const int cnt = 10;        ...    }    //file2.c    class Cls_file2{    public:        static const  int num = cnt;        ...    }

  可以看到这次我们有两个文件,文件2中的num初始化依赖于文件1中的cnt的值。那么如果是文件1中的cnt先初始化,貌似就没有什么问题。如果是文件2的num先初始化那么就会有麻烦了。然而这个问题是没有答案的,c++标准并没有规定他们的初始化顺序。

  最后从初始化角度解释下构造函数为什么不可以声明为虚函数的。虚函数的实现是依赖编译器生成一张虚函数表,然后创建一个虚拟指针_vptr指向虚函数表。通过这种方法查找我们实际调用的函数的。_vptr指针是在创建类的时候生成的,如果我们将构造函数声明为虚函数那就违反了因果定律。

  以上就是我个人对于C++类成员初始化的理解,如有错误,欢迎指正。

1 0
原创粉丝点击