C++对象的初始化

参考:《Effective C++》
《Effective C++》条款4学习笔记

1. 综述

C++对于对象的初始化大致分为两块:内置类型和非内置类型+局部和全局。

1.1 内置类型和非内置类型

  • 内置类型指的是类似int,short,char,double,float,long,long long等来自C语言的部分。C++对于内置类型是没有初始化的,也就是他的值是随机的。
#include <iostream>

int main(){
    int a;
    short b;
    char c;
    double d;
    std::cout<<a<<std::endl;
    std::cout<<b<<std::endl;
    std::cout<<c<<std::endl;
    std::cout<<d<<std::endl;
    return 0;
}

运行结果:

这里写图片描述

  • 非内置类型:用户自定义的类型(class、struct),STL的模板类等。它们一般通过构造函数初始化。
#include <iostream>
#include <vector>

class demo{
public:
    demo():dat(0){}
    demo(int dat):dat(dat){}
    int & get(){return dat;}
private:
    int dat;
};

int main(){
    //用户自定义的类型
    demo a;
    std::cout<<a.get()<<std::endl;
    //STL
    std::vector<int>myvector;//vector此时被初始化为空
    std::cout<<std::boolalpha<<myvector.empty()<<std::endl;
    return 0;
}

运行结果:

0
true

1.2 局部和全局

  • C++继承了C:会自动初始化全局变量,一般用0填充(自定义类型和其他类型通过构造函数初始化)
  • 但是对于内置类型的局部变量,C++和C一样,不会初始化,而是随机值
#include <iostream>

int DATA;//定义一个内置类型的全局变量

int main(){
    int data;
    std::cout<<DATA<<std::endl;//打印全局的DATA
    std::cout<<data<<std::endl;//打印局部变量的data
    return 0;
}

运行结果:

0
4291726

全局变量中的DATA被默认初始化为0,但是局部变量data确实一个随机值。


1.3 小结

所以对于内置类型的局部变量我们只能手动初始化了。


2 非内置类型的初始化

我们都明白,非内置类型的初始化的重任当落在构造函数(Constructor)身上。


2.1 构造函数的两种写法

  • 第一种
#include <iostream>
#include <string>

typedef enum{
    male,female
}gender;

class person_base{
public:
    //默认构造函数
    person_base(){
        std::cout<<"person_base default constructor called"<<std::endl;
        m_age=0;
        m_name=std::string();
        m_gender=gender::male;
        m_nationality=std::string();
    }
    //构造函数
    person_base(const unsigned int & age,const std::string & name,const gender & gen,const std::string & nationality){
        std::cout<<"person_base unique constructor calld"<<std::endl;
        m_age=age;
        m_name=name;
        m_gender=gen;
        m_nationality=nationality;
    }
    ~person_base(){}
private:
    unsigned int m_age;//年龄
    std::string m_name;//姓名
    gender m_gender;//性别
    std::string m_nationality;//国籍
};
  • 第二种
class person_base{
public:
    //默认构造函数
    person_base():m_age(0),m_name(),m_gender(male),m_nationality("unknown"){
        std::cout<<"person_base default constructor called"<<std::endl;
    }
    //构造函数
    person_base(const unsigned int & age,const std::string & name,const gender & gen,const std::string & nationality)
                :m_age(age),m_name(name),m_gender(gen),m_nationality(nationality){
        std::cout<<"person_base unique constructor calld"<<std::endl;
    }
    ~person_base(){}
private:
    unsigned int m_age;//年龄
    std::string m_name;//姓名
    gender m_gender;//性别
    std::string m_nationality;//国籍
};
  • 以上两种初始化的效果是一样的,但是二者的效率不同。前者的效率较低,后者(使用初始化列表)的效率较高。

我们使用更加简单的类说明二者的效率差。

注意将焦点放在myclass类上。

#include <iostream>
#include <string>

class demo {
public:
    demo() :dat(0) {
        std::cout << "demo default constructor" << std::endl;
    }
    demo(int d) :dat(d) {
        std::cout << "demo unary constructor" << std::endl;
    }
    demo(const demo & other) :dat(other.dat) {
        std::cout << "demo copy constructor" << std::endl;
    }
    const demo& operator=(const int & dat) {
        this->dat = dat;
        std::cout << "demo assign operator" << std::endl;
        return *this;
    }
    int getdata() { return dat; }
    const int getdata()const { return dat; }
private:
    int dat;
};

class myclass {
public:
    myclass() {
        std::cout << "myclass default constructor" << std::endl;
        m_int = 0;
        m_double = 0;
        m_demo = 0;
    }
    myclass(const int &dat, const double &db, const int& demo_dat) {
        std::cout << "myclass unique constructor" << std::endl;
        m_int = dat;
        m_double = db;
        m_demo = demo_dat;
    }
private:
    int m_int;
    double m_double;
    demo m_demo;
    //friend member
    friend std::ostream & operator<<(std::ostream & out, const myclass & other);
};
std::ostream & operator<<(std::ostream & out, const myclass & other) {
    return out << "m_int=" << other.m_int << "\nm_double="
        << other.m_double << "\nm_demo data=" << other.m_demo.getdata();
}

int main() {
    myclass obj(1, 2.2, 3);
    std::cout << obj << std::endl;
    std::cin.get();
    return 0;
}

运行结果:

demo default constructor
myclass unique constructor
demo assign operator
m_int=1
m_double=2.2
m_demo data=3

按照我们的预测,我们调用的构造函数应该只有1个。

当我们调用了myclass的构造函数,打印:myclass unique constructor

我们预测的结果应该是,下面这样:
myclass unique constructor <-调用了myclass的构造函数
demo unary constructor <-此时为初始化,调用的应该是构造函数
m_int=1
m_double=2.2
m_demo data=3

但是,我们发现运行结果和我们的猜想大相径庭。

这是怎么回事呢?
答:“C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。”——《Effective C++》

  • 所以,在进入myclass的构造函数体初始化demo之前,demo已经调用了默认的构造函数初始化了
  • 随后的初始化中,我们又在函数体中调用=赋值操作,这就造成了不必要的重复调用。这就是为什么第一种构造函数的写法不被推荐的理由。
  • 对于内置类型的int和double不受这种规则的影响 ( 因为C++本来就不会初始化它们,编译器只是直接划了一块内存给它们。它们的值就是内存中原来那些垃圾二进制。所以,对于内置类型,两种构造函数的写法的效率是相当的 ) 。
  • 使用初始化列表则不会发生上述的情况。(这也是我们为什么需要在初始化列表中才能初始化类中的引用或是const变量)
  • 所以,第二种(使用初始化列表)的构造函数的写法效率会高于前者。

  • 所以,我们一般会在初始化列表中,将类中所有的变量都列出来,这样就可以保证变量不会被遗漏了
  • 对于内置类型,我们建议同样列在初始化列表中,因为这样可以使初始化看起来整齐一致,程序员也就没有必要记忆和判断:何种变量必需在初始化列表中初始化而何种没有必要那么做。
  • 当然有时也会遇到初始化列表特别长的情况。此时我们可以将“赋值表现的和初始化一样好”的变量,放在一个私有的函数,供构造函数调用。下面是例子:
class test{
public:
    test():dat(0){}
    test(int data):dat(data){}
private:
    int dat;
};

class myclass:public test{
public:
    myclass(const int &data,const char &ch,const double& db,int data_test):test(data_test){
        init(data,ch,db);
    }
private:
    //将内置类型的初始化些写成函数,供构造函调用
    void init(const int &data,const char &ch,const double &db){
        m_int=data;
        m_char=ch;
        m_double=db;
    }
    int m_int;
    char m_char;
    double m_double;
    test m_t;
};

所以,我们将上面的代码修改优化后就是下面这一段:

#include <iostream>
#include <string>

class demo {
public:
    demo() :dat(0) {
        std::cout << "demo default constructor" << std::endl;
    }
    demo(int d) :dat(d) {
        std::cout << "demo unary constructor" << std::endl;
    }
    demo(const demo & other) :dat(other.dat) {
        std::cout << "demo copy constructor" << std::endl;
    }
    const demo& operator=(const int & dat) {
        this->dat = dat;
        std::cout << "demo assign operator" << std::endl;
        return *this;
    }
    int getdata() { return dat; }
    const int getdata()const { return dat; }
private:
    int dat;
};

class myclass {
public:
    myclass() :m_int(0), m_double(0), m_demo(0) {
        std::cout << "myclass default constructor" << std::endl;
    }
    myclass(const int &dat, const double &db, const int& demo_dat)
        :m_int(dat), m_double(db), m_demo(demo_dat) {
        std::cout << "myclass unique constructor" << std::endl;
    }
private:
    int m_int;
    double m_double;
    demo m_demo;
    //friend member
    friend std::ostream & operator<<(std::ostream & out, const myclass & other);
};
std::ostream & operator<<(std::ostream & out, const myclass & other) {
    return out << "m_int=" << other.m_int << "\nm_double="
        << other.m_double << "\nm_demo data=" << other.m_demo.getdata();
}

int main() {
    myclass obj(1, 2.2, 3);
    std::cout << obj << std::endl;
    std::cin.get();
    return 0;
}

运行结果:

demo unary constructor
myclass unique constructor
m_int=1
m_double=2.2
m_demo data=3


3. 继承中构造函数的调用顺序

继承中,构造函数的调用总是从基类开始的。
在多继承的情况下,基类构造函数是按照继承列表从左到右的顺序初始化的。

#include <iostream>

class baseA {
public:
    baseA() { std::cout << "baseA constructor" << std::endl; }
};

class baseB {
public:
    baseB() { std::cout << "baseB constructor" << std::endl; }
};

class myclassA :public baseA,public baseB{
public:
    myclassA() { std::cout << "myclassA constructor" << std::endl; }
};

class myclassB :public baseB, public baseA {
public:
    myclassB() { std::cout << "myclassB constructor" << std::endl; }
};

int main() {
    myclassA a;
    std::cout << '\n';
    myclassB b;
    std::cin.get();
    return 0;
}

运行结果:

baseA constructor
baseB constructor
myclassA constructor

baseB constructor
baseA constructor
myclassB constructor


4. 类中成员的初始化顺序

类中成员的初始化顺序是:按照成员声明的顺序来初始化的。
由此,我们得知:初始化列表的顺序并不是成员的初始化顺序。
所以,我们 有时 会受到成员初始化顺序的困扰。

#include <iostream>

class myclass {
public:
    myclass(int dat) :b(dat), a(b) {}
    int a, b;
};

int main() {
    myclass my(100);
    std::cout << "a=" << my.a << std::endl;
    std::cout << "b=" << my.b << std::endl;
    std::cin.get();
    return 0;
}

运行结果:

a=-858993460
b=100

  • 我们可以看到,a此时是一个非常随机的值。
  • 原因就是:因为a比b的声明要早。所以,a的初始化早于b(尽管此时的初始化列表中b写在前),b此时没有初始化,所以会将一个随机值传递给a。
  • 这就是一个陷阱,不注意就会掉进去
  • 所以,我们的做法是:初始化列表中的顺序和声明的顺序保持一致

5. 转载请注明出处

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值