参考:《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 constructorbaseB 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。
- 这就是一个陷阱,不注意就会掉进去
- 所以,我们的做法是:初始化列表中的顺序和声明的顺序保持一致