Effective C++ (条款4到6)

条款四:确定对象被使用前已先被初始化

1. 内置类型

C++中的内置基本类型,比如int,double,float等,初值都是垃圾值,即声明int i,i的初值是一个垃圾值。本书建议的最佳处理方法是:永远在使用对象之前将之初始化。比如:

 int x = 0;
 const char* test = “hello world”;
 double d; cin >> d;

2. STL

C++提供了丰富的容器,比如vector,list,deque,map和set等,这些容器已经写好了构造函数,所以总会自动初始化成默认值,程序员可以直接使用,比如:

vector<int> vt; vt.push_back(3);

3. 自定义类

C++在类中有专门初始化成员变量的构造函数,程序员可以写出合适的构造函数,比如:

 class A
 {
 private:
          int a;
          double b;
          string text;

 public:
          A():a(0), b(0), text("hello world"){} //构造函数
 };

当声明
A obj;
时,obj的成员变量a,b和text就已经获得了初值,分别是0,0和hello world。

这里注意一下,有些C++的初学者喜欢这样写:

 class A

 {
 private:
          int a;
          double b;
          string text;
 public:
          A()
          {
                    a = 0;
                    b = 0;
                   text = "hello world";
          }

 };

效果虽然和上一个例子一样,都获得了指定的初值,但执行的效率却不如上个例子。上一个例子中使用了成员初始化列表的方式,即在冒号后面逐一初始化,但本例却在函数体内进行了初始化。事实上,本例其实不能严格称为“初始化”,因为在进入构造函数的函数体时,这些成员变量已经被初始化了,a和b初始化成垃圾值,string因为是STL,调用默认的构造函数初始化为空字符串,在函数体内进行的操作实为“赋值”,也就是用新值覆盖旧值。这也正是说它的执行效率不高的原因,既进行了初始化,又在之后进行了赋值,不像上一个例子,只有初始化,一步到位。在有些特殊情况下,比如成员变量是const的或者是reference的,进行初始化后值就不可以改变了,这时只能用初始化列表,不能在函数体内赋值。

所以,应该尽可能地采用有冒号的成员初始化列表。注意这里的用词“尽可能地”,表示也不是所有情况都用这个初始化列表,当想构造的内容复杂,或者已经模块化为函数了,这时不能用初始化列表的方式,就采用在函数体内赋值的方式为好。

这里还有一个问题要注意下,当使用初始化列表时,初始成员变量的顺序与列表排列的顺序没有关系,只取决于声明这些成员变量的顺序,还是那个例子,将之改成:

 class A
 {
 private:
       int a;
       double b;
       string text;

 public:
        A():b(0), a(0), text("hello world"){}
 };

虽然初始化列表的顺序是b在先,但编译器只会根据声明变量时的先后顺序,所以还是a被先初始化,想要用以下这种方式初始化的同学要当心:

  class A
  {
  private:
           int a;
           double b;
           string text;

 public:
          A():b(0), a(b), text("hello world"){}
};

这一定不会得到你想要的结果的,因为a会首先初始化,而这时b尚未初始化,所以会有bug。本书中建议,为了避免这种顺序不一致的晦涩错误,“当你在成员初值列中条列各个成员时,最好总是以声明次序为次序”。

4. 不同编译单元内定义的non-local static 对象

这里先解释一下名词,static是静态的意思,表示这个变量不由栈分配,而存储在特有的全局变量/静态变量区域中,具有长寿命的特点(从被构造出来,直到程序结束时,才会由系统释放资源);而non-local则是说这个对象是全局的,而不是函数内的静态变量,是说它的作用范围广。

比如在文件1中定义了
int a = 1;
而在文件2中又会去使用:

extern int a;
int b = a *3;

可以看到文件1应在文件2之后执行,这样a才能获得初值,否则b得到的将是垃圾值,但事实上C++对于不同文件执行的相对次序并无明确定义,这样b究竟得到的是垃圾值还是3就不能确定。

解决这个问题是方法是不要使变量有全局的作用域,可以在文件1中定义:

 int& GetA()
 {
          static int a = 1;
          return a;
 }

而在文件2中调用
int b = GetA();
这样就一定保证a的初始化在先了。
总结一下:

  1. 为内置型对象进行手工初始化,因为C++不保证初始化它们;
  2. 构造函数最好使用成员初始化列表(实际初始化顺序不与列表的排列顺序有关,只取决于类中的声明顺序),而不要在构造函数体内使用赋值操作;
  3. 未避免“跨编译单元的初始化次序”问题,请用local static代替non-local static对象。

条款五:了解C++默默编写并调用哪些函数

一个常见的面试题就是问“一个空类占几个字节”,想当然的是0个字节,但事实上类要区分不同的对象,比如:
EmptyClass obj1;
EmptyClass obj2;
即便是空类,也要能识别obj1和obj2的不同,所以空类仍然要占字节数,一般占一个字节
还有一个针对空类的问题是“一个空类里面有什么”,就是想问编译器为这个空类自动生成了哪些成员函数。
很容易想到的是生成了默认的构造函数和析构函数,事实上还有拷贝构造函数和赋值运算符,所以,总共生成了四个成员函数。具体地说,就是你表面上写了 Class EmptyClass{};
但实际编译器为你加了四个成员函数,所以看起来像这样:

Class EmtpyClass
{
public:
//构造函数
EmtpyClass(){}
// 析构函数
~EmptyClass(){}
// 拷贝构造函数
EmptyClass(const EmptyClass& obj){ }
// 赋值运算符重载
EmptyClass& operator= (const EmptyClass& obj)
{ };

赋值运算符要返回自身*this,是因为考虑到可以出现连等的情况,比如obj1 = obj2 = obj3,另外,这里都使用了自身类的引用,即EmptyClass&,这里的引用是必须要加的,这是因为:

(1) 引用修饰形参时,可以避免实参对形参的拷贝,一方面可以节省空间和时间资源,更为重要的是若实参对形参拷贝了,又会调用一次拷贝构造函数,这样拷贝构造函数就会一遍又一遍的被调用,造成无穷递归。

(2) 引用修饰返回值时,可以使返回的对象原地修改。比如(a=b) ++,这样返回的a对象还可以进行自增操作,如果不加引用,则因为生成的是原对象的拷贝,所以这样的自增操作并不使a本体自增。

对初学者而言,还要注意区分什么时候调用的是赋值运算符,什么时候调用的是拷贝构造函数。比如:

EmptyClass a(b); // 调用的是拷贝构造函数
EmptyClass a = b; // 调用的是拷贝构造函数
EmptyClass a;
 a = b; // 调用的是赋值运算符

这里注意一下第二个和第三个例子,同样是等号,但却调用了不同的成员函数,重要的区别就要看是不是在这句话中新产生一个对象,第二个例子新产生一个对象,所以调用的是拷贝构造,第三个例子a在“=”前已经诞生了,所以调用的是赋值运算符。
本书中还讲到了一个特殊的情况,就是成员变量是const的,或者是引用,比如:

class SampleClass
{
private:
         const int var1;
         double& var2;
};

这时候编译器会报错,告诉你无法提供合适的构造函数,因为对于const变量以及reference,需要在声明的时候初始化,而编译器提供的默认构造函数显然无法做到这点。可以改成下面这样:

class SampleClass
{
private:
         const int var1;
         double& var2;
public:
         SampleClass(const int a = 0, double b = 0):var1(a), var2(b){}
};

编译器不会报错了,但是如果像这样:
SampleClass obj1;
SampleClass obj2;
obj2 = obj1;
编译器会提示“operator =”函数在“SampleClass”中不可用,这说明编译器同样没有为SampleClass生成赋值运算符,因为var1和var2在初始化后,值就不能再改变了。但:
SampleClass obj1;
SampleClass obj2(obj1);
却是可以编译通过的,这是因为编译器可以生成默认的拷贝构造函数。 这种生成方式并不会破坏const和reference的特性。
综上,编译器总是尽量地去生成这四个成员函数,但如果成员变量出现了const和reference,则编译器会拒绝生成默认的构造函数和赋值运算符重载函数

条款六:若不想使用编译器自动生成的函数,就该明确拒绝

地产中介商卖的是房子,有这样一个类:
class HomeForSale
{…}
每个房子都是不一样的,所以希望这样:

 HomeForSale house1;
 HomeForSale house2;
 HomeForSale house3(house1); // house3企图与house1相同,不能通过这种情况
house2 = house1; // house2也不应该与house1相同,也不能通过这种情况

我们希望最后这两句话不能通过编译,那么怎样在C++中实现呢?一种想当然的做法,就是不去写拷贝构造函数和赋值运算符。但由上一个条款可以知道,这样做是行不通的,C++编译器会我们生成四个默认的成员函数,其中就有拷贝构造函数和赋值运算符,所以不写是不能解决问题的。如果写了呢?这确实是一个突破口,自己写了成员函数,编译器就不会生成默认的了,但不能写在public里,因为public的函数都是可以在外部被调用的,一种好的方法就将这些函数写在private或者protected里面,大概像这样:

class HomeForSale
{
private:
         HomeForSale(const HomeForSale& house){...}
         HomeForSale& operator= (const HomeForSale& house){...}
};

好了,这样做编译器就不会为我们生成默认的了,同时因为是在private访问权限下定义的,所以也不会担心在外部被调用了,下面这两句话是无法通过编译的:

 HomeForSale house3(house1); // 无法通过编译
 house2 = house1; // 无法通过编译

有了上述class定义,当用户企图拷贝HomeForSale对象时,编译器会阻挠。但是如果你不慎在成员函数或者友元函数中使用了它们,这时候链接器将会阻挠。
所以这样还不能保证绝对安全。

将连接期间的错误移至编译期间是可能的。
只要将拷贝构造函数和拷贝赋值运算符声明为private,只不过不是在类HomeForSale本身,而是在一个专门为了阻止拷贝动作而设计的基类中。
这个基类非常简单:

class Uncopyable
{
    Uncopyable(){}
    ~Uncopyable(){}

private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator = (const Uncopyable&);
};

那么为了阻止HomeForSale对象被拷贝,我们唯一要做的就是继承Uncopyable:

class HomeForSale : private Uncopyable
{
public:
    ...

private:
    ...
        HomeForSale(const HomeForSale&); //只有声明
    HomeForSale& operator=(const HomeForSale&);
};

这样HomeForSale就不能被拷贝了。这里的继承可以不是public,比如private。
一句话总结一下:为了驳回编译器自动提供的机能(比如可拷贝),可将相应的成员函数声明为private且不予实现。还有一种方法,继承Uncopyable也行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值