《Effective C++》学习总结(条款01 - 05)

introduction 导读

 

1.对高效使用C++的两类忠告 :
  • 一般性的设计策略
  • 带有具体细节的特定语言类型
2.术语:
  • 声明(declaration)与定义(definition): 声明是告诉编译器某个元素的名称和类型,但略去细节。定义则是使编译器为程序元素分配空间。
  • 二者的区别: 注意区分声明和定义式,二者最根本的区别就是是否分配内存,声明不会导致内存的分配,而定义会分配内存。在C++程序中声明可以有多次,但是定义只能有一次。因此不能将变量的定义放置于头文件中,由于头文件会被多次引用,就会导致变量在多个源文件中被重复定义,这是C++所不允许的。但是也有例外的情况,以下3种定义可以放入头文件中:
a. 类的定义。
b. const变量的定义。因为const常量的作用域仅限于定义它的文件,所以可以在多个源文件中出现它的定义。
c. inline函数。

如: extern int x; class A; 是一个对象(object)的声明式(extern 关键词用于暗示编译器去后面寻找对象的具体定义)

  • 二者的联系: 定义也是声明, 因为当定义变量时我们也向程序表明了它的类型和名字;但声明不是定义, 可以通过使用extern关键字声明变量而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern;
  • 特殊情况下声明可以充当为定义,除非是以下几种情况:

(1) 函数原型(无函数体的函数声明);

(2) 包含extern关键字并且没有初始化变量、对象或函数体,例如:

extern int i; //声明
extern int p = 123; //定义           

(3) 没有下列定义的类名声明,如Class T;

(4) 类声明中的静态数据成员。例如:

class abc{
   static const int i = 10; //常量声明式
   int ui[i];  //使用该常量
};
  • 值得注意的是,你看到的是i的声明式而非定义式。C++会要求你为所使用的任何东西提供定义式,但是如果它是class的专属常量且是static且为int类型时,可以区别对待。

但对于上述情况,如果需要取某个class专属常量的地址,或者编译器坚持要看到一个定义式来明白该常量是什么,此时必须提供额外的定义式如下:

const int abc:: i;将该式子放入一个实现文件而非头文件,由于其已在声明时获得初值10,定义时不能再设初值。

  • size_t:是一个typedef,在C++中用于计量。其定义是:“适于计量内存中可容纳的数据项目个数的无符号整数类型”
  • size_t由来: 在C++中,设计 size_t 就是为了适应多个平台的 。size_t的引入增强了程序在不同平台上的可移植性。不同系统上,定义size_t可能不一样

经测试发现,在32位系统中size_t是4字节的,在64位系统中,size_t是8字节的。在32位系统上 size_t定义为 unsigned int ,也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long ,也就是说在64位系统上是64位无符号整形。

其定义如下:

#ifdef _WIN64
	 typedef unsigned __int64   size_t;
   typedef __int64            ptrdiff_t;
	 typedef __int64            intptr_t;
#else
    typedef unsigned int       size_t;
    typedef int                ptrdiff_t;
    typedef int                intptr_t;
#endif

可以看到其实size_t就是使用了typedef别名的unsigned int

  • size_t的大小:其大小是由系统的位数决定的, 可以理解为是由你生成的程序类型决定的,只是生成的程序类型与系统的类型有一定关系。32bits的程序既可以在64bits的系统上运行,也可以在32bits的系统上运行。但是64bits的程序只能在64bits的系统上运行。然而我们编译的程序一般是32bits的,因此size_t的大小也就变成了4个字节。
3.使用关键词explicit 来声明构造函数:
  • 被声明为explicit的构造函数比non- explicit构造函数更受欢迎,因为其禁止编译器执行非预期(往往也不被期望)的类型转换。因此,习惯的遵循这样的策略
  • 除非有一个好的理由允许构造函数被用于隐式类型转换,否则将其声明为explicit。
4.复制构造函数(copy构造函数)
  • copy构造函数被用来 “以同型对象初始化自我对象”,copy assignment operator(拷贝赋值操作符)被用来 “从另一个同型对象中拷贝其值到自我对象”
  • 示例:
class Widget {
public:
  Widget();				// default构造函数
  Widget(const Wideget& rhs); 			// copy构造函数
  Widget& operator=(const Widget& rhs);		// copy assignment operator
};
Widget w1;  			// 调用default构造函数
Widget w2(w1);		// 调用copy构造函数
w1 = w2;					// 调用copy assignment operator
Widget w3 = w2;		// 调用copy构造函数,注意与上面的赋值区分,有新对象被定义,必然有构造函数被调用,而不会是赋值操作符
5.对于接口:
  • 什么是接口:接口就是规定并描述了要程序做什么(行为和功能),但不在其中完成其实现。
  • 对于C++来说,接口是通过抽象类来实现的,如果一个类中至少有一个函数被声明为纯虚函数,那么这个类就是抽象类,纯虚函数式通过在声明中使用“=0”来指定的,其示例如下:
class Box
{
   public:
      // 纯虚函数
      virtual double getVolume() = 0;
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};
  • 设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
  • 可用于实例化对象的类被称为具体类。
  • 因此,在派生类中重写纯虚函数也即对应方法在接口下的具体实现。

 

第一章 : Accustoming Yourself to C++ ——让自己习惯C++

 

条款01: 视C++为一门联邦语言

1.C++ 不单单是在C语言的基础上增加了一些面向对象的特性,可以将其视为一门联邦语言,本质上是一门多重泛型编程语言——一个同时支持过程形式(procedural),面向对象形式(OOP), 函数形式(functional),泛型形式(generic),元编程形式(metaprogramming)的语言,包括以下四个核心的次语言内容:
  • C++以C语言为语言基础,是C的超集
  • 面向对象的C++,也就是C with Classes所诉求的
  • Template C++ 也就是C++ 泛型编程的部分,也是大多数人较难掌握,缺乏经验的部分
  • STL程序库(template程序库,包括容器、迭代器、算法和函数对象)
2.请记住:C++高效编程守则视具体情况而变化,取决于你使用C++的哪一个部分。

 

条款02: 尽量以const,enum,inline替换 #define(宁可以编译器替换预处理器)

1.#define 不具有任何的封装性
  • 一旦宏被定义,就全局可见(除非在某处被#undef)。这也就意味着#define不仅不能用来定义class专属常量,也不能够提供任何封装性。
2.enum hack
  • enum hack 是枚举基础, 其理论基础是:一个属于枚举类型(enumerated type)的数值可权充ints被使用。
  • 背景是:为了防止定义的值被拷贝多份, 需要我们在类内部声明 const static 常量, 但是基于旧的编译器, 不允许static成员变量在其声明式上获得初值, 所以不得以会将初值放到定义式(也就是具体实现)里面。
  • 但是特殊情况下,例如数组声明式中,我们(编译器)坚持需要确切的知道数组的大小,我们就需要使用the enum hack 补偿法。
  • 示例:
class GamePlayer {
private:
	enum {NumTurns = 5};			 //"the enum hack" ---令NumTurns 成为5的一个记号名称
  int scores[Numturns];
};
  • 为什么要认识enum hack?
  • 第一,enum hack的行为某方面说比较像#define而不像const, 有时候这正是你想要的。 例如取一个const的地址是合法的, 但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮你实现这个约束。也就是你想使用#define的特性,而又不接受#define内存复制的问题, enum hack就是你最好的选择。
  • 第二, 纯粹为了实用主义,很多代码用了它,所以看到它,你必须认识它,而且了解为什么要这么用,知其然,知其所以然很重要。事实上,enum hack是template metaprogramming(模版元编程)的基础技术。
3.inline函数
  • 定义: 定义函数时,在返回值类型前面加上 inline 关键字,增加了inline关键字的函数被称为“内联函数”。内联函数和普通函数的区别在于:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。
  • 而上述的这一点功能上类似于宏展开, 但是比宏展开好的地方是,inline发生在编译阶段,会做类型检查,消除了宏展开可能带来的语义隐患。例如定义宏#define f(x, y) (x*y)就会在 f(x+1,y)的时候f(x,y)就变成了x+1*y,完全错误。用inline可以达到相同的意图,却不会产生错误。
  • 函数加上inline的好处: 可以节省调用的开销,而且能够便于编译器和上下文配合做优化。
  • 注意:inline只是程序员给编译器的建议,如果函数题过于复杂,编译器也会视情况忽略inline的使用
  • 总结:inline主要用在比较简单,且可能会被多次调用的函数,如简单的数学表达式,还有面向对象时读取类成员get和set等,其优点在于节省调用的开销,方便优化,能够消除宏带来的隐患。因此,对于形似函数的宏(macros),最好该用inline函数替换#define,如果涉及到inline的可见和封装性,可以将其设计在类的priavte成员中加以控制。
4.请记住:
  • 对于单纯常量,最好以const对象或enums替换#define
  • 对于形似函数的宏(macros),最好该用inline函数替换#define

 

条款03:尽可能使用const

1.const的使用范围:
  • 在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、或block scope中被声明为static的对象
  • 在classes内部修饰static和non-static成员变量
  • 面对指针,可以指出指针自身、指针所指物,或两者都是(不是)const
2.const修饰指针与常量
  • 示例:
const char * myPtr = &char_A;//指向常量的指针
char * const myPtr = &char_A;//常量的指针
const char * const myPtr = &char_A;//指向常量的常量指针
  • 常量指针(Constant Pointers)
    因为*操作符是左操作符,左操作符的优先级是从右到左,对于:int * const p
    先看const再看*,是p是一个常量类型的指针,不能修改这个指针的指向,但是这个指针所指向的地址上存储的值可以修改。
  • 指向常量的指针(Pointers to Constants)
    对于:const int *p
    先看*再看const,定义一个指针指向一个常量,不能通过指针来修改这个指针指向的值,但可以修改该指针指向的地址
  • 指向常量的常量指针(Constant Pointers to Constants)
    对于:const int* coonst p
    表示一个不能修改指向的指向常量的指针
3.令函数返回一个const类型值,能够预防很多无意义的赋值动作
4.const成员函数
  • const对象只能访问const成员函数,而非const对象可以访问任意的成员函数
  • const成员函数不能修改对象的数据成员,const对象的成员变量不可以修改(mutable修饰的数据成员除外)
5.bitwise constness 和 logical constness:成员函数是const时站在编译器的角度看问题
  • 使用关键词mutable能够解决在const成员函数内更改成员变量的需求,但不能解决所有的相关问题
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复(使用转型,条款27提及)
    也就是说,当两个函数(一个const,一个non-const除了const修饰返回类型以外,其内部实现完全相同时),为了减少代码的重复以及相应的编译,维护问题,可以通过使用casting(转型)来”运用const成员函数实现出其non-const孪生兄弟“
  • 注意:上面谈到的都是通过non-const函数调用const函数,而不能进行反向操作,因为const成员函数承诺绝不改变其对象的逻辑状态,而non-const函数并没有如此承诺。
6.请记住:
  • 将某些object声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
  • 编译器强制实施bitwise constness,但编写程序时应该使用“概念上的常量性”,而且要站在编译器的角度去看问题
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复(使用转型,条款27提及)

 

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

1.永远在使用对象之前将它初始化
2.区分在构造函数 成员初始化中的赋值(assignment)与初始化(initialization)
3.为了规范与高效,总是使用成员初值列(member initialization list),并总是在初值列中列出所有成员变量(以免遗漏)
4.当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序
5.不同编译单元内定义之non-local static对象的初始化次序问题
  • static对象的寿命从被构造出来知道整个程序结束为止
  • non-local static对象是指,非某个函数内定义的local-static对象,也即该对象是global或位于namespace作用域内,抑或是在class内或file作用域内被声明为static(总之就是不在函数内部被声明)
  • 所以真正的问题是:如果某编译单元(通常指单一源码文件加上其所#include的头文件)内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对与这个初始化次序并无明确定义(因为不可能在程序设计之初就将初始化次序定死,也不值得耗费精力去这样做)
  • 上述问题的解决方法是:reference-returning ——将每个non-local static对象搬到自己的专属函数内,使该对象在此函数内被声明为local static。这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不直接指涉这些static对象自身。
  • 基于该方法,C++即可保证函数内的local static 对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。
6.请记住:
  • 为内置型对象进行手工初始化,因为C++不保证初始化他们
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作(这样会调用default构造函数,再调用copy assignment赋值操作符)。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象

 

第二章:构造/析构/赋值运算

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

1.在你自己没有声明任何copy构造函数的情况下,编译器为你默认生成的copy构造函数在进行参数初始化时有两种方式:
  • 实例:
NameObject<int> no1 (Smallest Prime Number, 2);
NamedObject<int> no2(no1);        //调用copy构造函数(在之前类内没有声明任何copy构造函数,所以默认调用编译器为你默认生成的copy构造函数)
  • 对于有copy构造函数的类型(例子中的string):调用对应的copy构造函数,以copy的原型值为实参
  • 对于内置类型(例子中的int 内置类型):拷贝原型之内的每一个bits
2.如果你打算在一个“内含reference成员或const成员”的class内支持赋值操作(assignment),你必须自己定义copy assignment操作符
3.请记住:
  • 编译器可以暗自(silently)为class创建default构造函数,copy构造函数,copy assignment操作符,以及析构函数(编译器产出的是non-virtual的——见条款07),而且如果你自己没有声明,那么这些默认创建的函数都是public且inline的。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值