effect C++笔记

视C++为一个语言联邦

C
Object-Oriented C++
Template C++
STL
对内置(也就是C-like)类型而言pass-by-value通常比pass-by-reference高效,但当你从C part of C++移往Object-OrientedC++,由于用户自定义(user-defined)构造函数和析构函数的存在, pass-by-reference-to-const往往更好。

尽量以const,enum,inline替换#define

对于单纯常量,最好以const对象或enums替换#defines

使用const创建class专属常量

通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(integral type,例如ints,chars,bools),则需特殊处理。只要不取它们的地址,你可以声明并使用它们而无须提供定义式。但如果你取某个class专属常量的地址,或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:

const int GamePlayer::NumTurns;

请把这个式子放进一个实现文件而非头文件。由于 class 常量已在声明时获得初值(例如先前声明NumTurns时为它设初值5),因此定义时不可以再设初值。

可以使用const创建一个class的专属常量,但却不能使用#define创建一个专属常量

为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员(member):

class GamePlayer
{
private:
      static const int Num=5;//常量声明式
      int scores[Num];//使用该常量
      ...
};

通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(integral type,例如ints,chars,bools),则需特殊处理。只要不取它们的地址,你可以声明并使用它们而无须提供定义式。但如果你取某个class专属常量的地址,或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:

const int GamePlayer::Num;//Num的定义式,下面告诉你为什么没赋值

请把这个式子放进一个实现文件而非头文件。由于 class 常量已在声明时获得初值(例如先前声明NumTurns时为它设初值5),因此定义时不可以再设初值。
如果你的编译器不支持上述语法,你可以将初值放在定义式:

class ConstEstimate{
private:
      static const int Num;
};//位于头文件内

const int Num=5;//位于实现文件内

使用enum创建类内常量

如果你的编译器不允许使用static const创建类内常量,那么可以使用enum来代替:

class GamePlayer{
      enum {Num=5};
      int scores[Num];
};

需要注意的是,enum常量是不能被取址的。

对于形似函数的宏(macros),最好改用inline函数替换#defines

使用# define创建一个比较大小的宏:

#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))

需要注意的是所有实参都需要加上小括号

以上函数有着很多缺点,但优点是不需要调用函数的开销。

使用inline内联函数,不仅可以获得#define的效率,还可以获得函数编译和检查。
因此,对于形似函数的宏(macros),最好改用inline函数替换#defines。

内联函数inline和函数的区别
内联函数会进行副本替换,会存在多个副本占用多份内存,而函数只有一个地址只占用一份内存,但函数调用会产生压栈,速度会降低,而内联函数是直接将代码嵌入到调用处,省去了压栈寻址的过程,这是典型的空间和时间转换的问题。

const 语法虽然变化多端,但并不莫测高深。如果关键字 const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

以下两种方式是相同的,都表明指针所指向的内容不可以被改变

void f1(const Widget* pw);
void f2(Widget const* pw);

尽可能使用const

将某些东西声明为 const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体

如果关键字 const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

以下写法是等价的,都是限制指针所指内容是常量的:

void f1(const int * pw);
void f2(int const * pw);

使用const修饰函数返回值时,如const引用,则可以既获得返回值传递的高效性,也可以获得返回值的常量性。

编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)

const成员函数
const修饰成员函数,作用是限制对象内成员函数不修改任何成员变量,需要注意的是,在定义和实现中都需要写上const
此时函数内部如果发生变量被改变的行为都会导致编译错误

得知哪个函数可以改动对象内容而哪个函数不行,很是重要。
C++定义,bitwise constness是指成员函数不改变任何成员变量。
但是如果成员函数改变了指针所指物,编译器却也能通过编译,这个问题引出了logical constness概念。
因此以上叙述的是C++定义的成员函数是只要成员函数不更改成员变量,就是const成员函数,而logical constness认为,如果返回值可以被更改,则不属于const成员函数,因为这种方式虽然不是通过成员函数更改了成员变量,但最终还是通过返回值更改了成员变量,这是不被认为是const成员函数的。
如果const成员函数希望能更改某些成员变量,那么这个成员变量应该用mutable进行修饰。

当 const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

Class TextBlock{
Public:
	…
	const char & operator[] (std::size_t position) const
	{
		…
		…
		…
		return text[positon];
	}
	char& operator[] (std::size_t position)
	{
		return cosnt_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
	}
	…
};

这里共有两次转型:第一次用来为*this添加 const(这使接下来调用 operator[]时得以调用 const版本),第二次则是从const operator[]的返回值中移除const。

更值得了解的是,反向做法——令 const版本调用 non-const 版本以避免重复——并不是你该做的事。

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

为内置型对象进行手工初始化,因为C++不保证初始化它们。

为了减少初始化造成的消耗,c语言会允许变量未初始化。
因此array不保证初始化,而vector却可以保证初始化

构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

在构造函数中,一定要使用成员初始化列表列出所有成员变量,即使有些变量不需要赋初值,以免某些变量被忘记赋值。
使用成员初始化列表的原因是,如果不使用成员初始化列表,而是在构造函数中赋值,将会面临的过程是,首先调用了默认构造函数,其次调用了赋值运算符,而如果使用成员初始化列表,将会只调用一次复制构造函数。

许多class拥有多个构造函数,这时如果每个构造函数都去列表初始化一次成员变量,则会造成代码冗余,解决这个问题的方法可以是,将内置类型的初始化用赋值方式代替,并封装到函数内,然后在每个构造函数的代码块内调用该函数。因为对于内置变量而言,这两种方式的消耗是差不多的。

//.h文件
class People{
public:
      People(const string& name,const string& dress);
};

//.c文件
People(const string& name,const string& dress) //低效写法,会先调用默认构造函数再调用赋值运算符
{
      name="XiaoMing";
      adress="TianJin";
}
People(const string& name,const string& dress)//成员初始化列表
:name("XiaoMing"), adress(TianJin)
{
}

C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes更早于其derived classes被初始化(见条款12),而class的成员变量总是以其声明次序被初始化。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。

为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-localstatic对象。

static的生命周期
从被构造开始,直到程序结束为止。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。
程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。

C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。
幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。Design Patterns迷哥迷姊们想必认出来了,这是Singleton模式的一个常见实现手法。

为避免在对象初始化之前过早地使用它们,你需要做三件事。第一,手工初始化内置型non-member对象。第二,使用成员初值列(memberinitialization lists)对付对象的所有成分。最后,在“初始化次序不确定性”(这对不同编译单元所定义的non-local static对象是一种折磨)氛围下加强你的设计。

了解C++默默编写并调用哪些函数

如果你声明一个类却什么都没做,C++会默默帮你生成如下内容:
默认构造函数,拷贝构造函数,复制构造函数,赋值运算符,析构函数。
所有这些函数都是public且是inline的。

如果成员函数被声明为引用类型或const类型,而类内又没有声明赋值运算符,
那么编译器将拒绝编译,除非你定义了赋值运算符。

class temp
{
public:
      ....
private:
      int& Num;
      const string Name;
};

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

如果不想让类拥有复制构造函数和赋值运算符,可以将复制构造函数和赋值运算符声明为private,并不去实现他们。
更好的做法是,制作一个Uncopyable基类,使用时直接继承基类就可以:

class Uncopyable{
protected:
      Uncopyable(){} //允许继承类构造和析构
      ~Uncopyable(){}
private:
      Uncopyable(const Uncopyable&)//阻止复制构造函数
      Uncopyable& operator=(const Uncopyable &);
};

使用时:

class HomeForSale:Uncopyable{

};

继承类构造函数
写子类的构造函数时,必须要看看父类的构造函数能否被自动调用;若不能被自动调用,应该显示调用。

class A
{
    int a;
public:
    A(int a)
    {
        this->a = a;
    }
};

class B:public A
{
    int b;
public:
    B(int b):A(10)
    {
        this->b = b;
    }
};

为多态基类声明virtual析构函数

作为基类的class一定要有虚析构函数,这样在销毁基类的时候会继续调用继承类的虚构函数。
不作为基类的class一定不要带有虚析构函数,因为这样会使对象的大小多出50%~100%,因为会多出一个vptr

欲实现出virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。

注意:
STL容器都不带虚析构函数的,因此不适合做基类。
比如以下这样会导致继承类指针没有被释放:

class SpecialStirng:std::String{
};
SpecialString* pss=new SpecialString("abc");
string* ps;
ps=pss;
delete ps;//此时pss没有被释放,造成内存泄漏

条款08:别让异常逃离析构函数

析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序

C++允许在析构函数中抛出异常,但不建议这样做,因为STL容器内包含的对象,如果每个对象都会抛出异常,在同时出现两个异常的情况时,则会导致unexception 行为。
如果确实在析构函数中发生了异常,导致程序不能再继续运行,可以采取两种方式:

如果close抛出异常就结束程序。通常通过调用abort完成

DBConn::~DBConn(){
      try{
            db.close();
      }
      Cath(){
            std::abort();//如果close()发生异常,调用abort终止程序,并记录
      }
}

吞下因调用close而发生的异常

DBConn::~DBConn(){
      try{
            db.close();
      }
      Cath(){
      }
}

如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作

应该提供一个函数供客户处理异常:

class DBConn{
public:
      close(){  //供客户使用的新函数
      db.clsose();
      closed=true;
      }
~DBConn(){
            if(!closed){    //关闭连接,如果客户没有调用close
                  try{db.closed();}
                  catch(){};                  
            }
      }
private:
      DBConnection db.close();
      bool closed;    
};

条款09:绝不在构造和析构过程中调用virtual函数

posted @ 2020-12-26 22:50  多弗朗强哥  阅读(348)  评论(0编辑  收藏  举报