《Effective C++》学习笔记(条款04:确定对象被使用前已被初始化)

最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!

1 内置类型的初始化

C++中内置类型继承于C,而C中是没有初值的,定义变量时一定需要初始化,不然它的初值一般都是个垃圾值。

int x;//未初始化
cout<<x<<endl;//输出的是一个垃圾值

//初始化方式
int a = 10;	//对int进行手工初始化
const char* str = "Hello";	//对指针进行手工初始化
double d;
cin>>d;		//以读取输入流的方式完成初始化

注:如果定义在全局区的变量没有被用户初始化的话,编译器会自动将其初始化。int 初值为 0,bool 为 false
首先看一下C++中的几个存储区:

  • 栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量、const修饰的局部变量的值等,其操作方式类似于数据结构中的栈。
  • 堆区:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
  • 全局区:全局变量、const 修饰的全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后有系统释放
  • 常量区:常量字符串就是放在这里的。 程序结束后由系统释放
  • 程序代码区:存放函数体的二进制代码。

2 类的初始化

类的初始化工作落到构造函数的身上,确保每一个构造函数都将对象的每一个成员初始化。重要的是,别混淆了赋值初始化

class PhoneNumber{
    ...
};

class ABEntry{
public:
    ABEntry(const string &name, const string &address,const list<PhoneNumber> &phones);
    ABEntry();//无参构造
private:
    string theName;					//自定义类型(不是用户自定义,是std)
    string theAddress;					//自定义类型
    list<PhoneNumber> thePhones;	//自定义类型
    int numTimesConsulted;			//内置类型
};
//第一版:在构造函数执行体内进行赋值操作
ABEntry::ABEntry(const string &name, const string &address,const list<PhoneNumber> &phones)
{
	theName = name;       //这些都是赋值
    theAddress = address; //而非初始化
    thePhones = phones;
    numTimesConsulted = 0;
}

//第二版:使用初始化列表
ABEntry::ABEntry(const string &name, const string &address,const list<PhoneNumber> &phones):
				theName(name),
                theAddress(address),
                thePhones(phones),
                numTimesConsulted(0)//这些都是初始化
{
	//构造函数本体无需任何动作
}

ABEntry::ABEntry():theName(),theAddress(),thePhones(),numTimesConsulted(0)
{
    //对于无参构造函数,也可以使用初始化列表,因为theName(),theAddress(),thePhones()会调用它们的默认构造函数
}

第一版和第二版的最终结果相同,但第二版的效率较高。

第一版:首先调用非内置类型成员变量 theNametheAddressthePhones 的默认构造函数赋初值,然后立刻对它们进行赋新值。这个方法中非内置类型默认构造函数赋初值这动作浪费了。

第二版:初始化列表中为各个成员变量的实参被拿去作为各成员变量的构造函数的实参。即 theName(name) 表示 theNamename 作为参数调用了拷贝构造。

注:对于内置类型(如 numTimesConsulted)在第一版和第二版中的效率是一样的,但为了初始化列表的一致性,一律用初始化列表。加上如果成员变量是 constreference ,它们一定需要初值,不能被赋值。为避免需要思考什么时候用初始化列表,什么时候不用,还不如都用初始化列表来得方便。

3 成员变量初始化的顺序

基类更早于其派生类被初始化,而 class 内的成员变量总是以其声明的顺序被初始化。如在 ABEntry 类中,初始化顺序从前到后为 theNametheAddressthePhonesnumTimesConsulted,即使它们在初始化列表中的顺序声明顺序不同,还是按照声明顺序为准,但一般初始化列表的顺序都是和它的声明顺序一致。

  1. 初始化非局部静态对象
  • 静态对象
    • 局部静态对象
      • 函数内的静态对象
    • 非局部静态对象
      • 全局对象
      • 定义命名空间作用域内的对象
      • class 内、 file 作用域内被声明的 static 对象

在两个源代码文件中分别包含至少一个非局部静态对象,当不同源代码文件中的非局部静态对象有所依赖时,直接使用这些对象是由风险的,因为它们的初始化顺序是不确定的

//a.cpp
class Server{
    ...
public:
    int clientNum;
};
extern Server server;//在全局范围声明外部对象server,供本文件外使用(即在其它文件内知道有server这个全局变量)
//b.cpp
class Client{
    Client()
    {
        m_num = server.clientNum;
    }
public:
    int m_num;
}

Client client;//定义全局变量client对象,自动调用了Client类的构造函数,而构造函数内需要server的成员变量 clientNum 的值,但我们不能保证server是否已经被初始化了。

解决方法:非局部静态对象变为局部静态对象

使用一个函数,只用来定义一个局部静态变量并返回它的引用。因为C++规定在本地范围(函数范围)内定义某静态对象时,当此函数被调用,该静态变量一定会被初始化。

//a.cpp
class Server{...};

Server& server(){                         //将直接的声明改为一个函数
    static Server server;
    return server;
}

//b.cpp
class Client{...};

Client::client(){                        //客户端构造函数通过函数访问服务器数据
    number = server().number;
}

Client& client(){                        //同样将客户端的声明改为一个函数
    static Client client;
    return client;
}

Note:

  • 为内置类型进行手工初始化
  • 构造函数最好使用初始化列表,而不要在构造函数的实现体内使用赋值操作。初始化列表的顺序最好与成员变量的声明顺序一致
  • 当不同源代码文件中的有所依赖时,最好将非局部静态对象变为局部静态对象

条款05:了解C++隐式提供并调用哪些函数

你可能感兴趣的:(Effective,C++,学习笔记,c++)