《Effective C++》-第一章-让自己习惯C++

本节介绍《Effective C++》前言以及第一章相关。
仅作为本人学习《Effective C++》的学习笔记。
送给各位的一句话:种一棵树最好的时间是十年前,其次是现在
说明:无论如何,当我们下定决心的时候,就是想要去变得更好,那就需要坚持不懈努力


前言

记录《Effective C++》的学习。


一、写在前面的话

术语:

extern int x;   //对象object声明式
std::size_t numDigits(int number);  //函数function声明式
class Widgettemplate<typename T>  //模板 template声明式
class GraphNode;

size_t 位于命名空间std内;
初始化由构造函数执行。所谓default构造函数是一个可被调用而不带任何实参者,这样的构造函数要不没有参数,要不就是每个参数都有缺省值。


class A
{
  public:
    A();
}

class B
{
  public:
    explicit B(int x = 0, bool b = true);
}

构造函数被声明为explicit,这可阻止它们被用来执行隐式类型转换(implicit type conversions),但它们仍可被用来执行显示类型转换(explicit type conversions)
拷贝构造函数 copy构造函数被用来“以同类型对象初始化自我对象”。
拷贝引用 copy assignment操作符被用来“从另一个同类型对象中拷贝其值到自我对象”。


class Widget
{
public:
    Widget(); //default构造函数
    Widget(const Widget& rhs); //copy构造函数
    Widget& operator=(const Widget& rhs); //copy assignment操作符

};

Widget w1;  //调用default构造函数
Widget w2(w1); //调用copy构造函数
w1 = w2; //调用copy assignment操作符

当你看到赋值符号时要小心,因为“=”语法也可用来调用copy构造函数:
  Widget w3 = w2;  //调用copy构造函数!
  
拷贝copy构造和拷贝copy赋值的区别:
  如果一个新对象被定义 如w3,一定会有一个构造函数被调用,不可能调用赋值操作。
  如果没有新对象被定义 如w1 = w2, 就不会有构造函数被调用,是赋值操作被调用。
  
拷贝构造函数,定义了一个对象如何以值传递(passed by value)。
例子:
bool hasAcceptableQuality(Widget w);
...
Widget aWidget;
if(hasAcceptableQuality (aWidget))
...

参数w是以传值by value的方式传递给函数hasAcceptableQuality。
所以在上述调用中aWidget被复制到w体内。
这个复制动作有Widget的copy构造函数完成。
Pass-by-value意味着“调用copy构造函数“。
一般不推荐使用by value以传值的方式传递。

构造函数ctor 析构函数dtor


二、第一章 让自己习惯C++


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

c++面向对象,多重范型编程语言,一个同时支持过程形式procedural、面向对象形式object-oriented、函数形式functional、泛型形式generic、元编程形式Metaprogramming的语言。

C++的次语言sublanguage:

  1. C.

    C++以C为基础。区块blocks ,语句statements,预处理preprocessor,内置数据类型built-in data types,数组arrays,指针pointers。但是C没有模板templates,没有异常exceptions,没有重载overloading。

  2. Object-oriented C++.面向对象

    类classes(构造函数与析构函数)、封装encapsulation、继承inherititance、多态polymorphism、virtual虚函数(动态绑定)等等。

  3. Template C++. 泛型编程部分

    templates威力强大,它们带来了崭新的编程范型programming paradigm,也就是所谓的template Metaprogramming(TMP,模板元编程)。

  4. STL. template程序库

    它对容器containers、迭代器iterators、算法algorithms、以及函数对象function objects的规约有极佳的紧密配合与协调。


pass-by-value 传值, pass-by-reference传参。
用户自定义 user-defined


条款02:尽量以const,enum,inline替换#define

2.1. 这个条款或许改为“宁可以编译器替换预处理器”比较好。

2.2. 因为或许#define 不被视为语言的一部分。

#define ASPECT_RATIO 1.653

 记号名称ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理移走了。
 于是记号ASPECT_RATIO有可能没进入记号表symbol table内。
 于是在运用这个常量但获得一个编译错误信息,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO。
 这个问题也可能出现在记号式调试器symbolic debugger中,原因相同,多使用的名称可能并未进入记号表symbol table 中。
 
解决办法:
    以一个常量替换上述的宏#define;
    const double AspectRatio = 1.653;   //大写名称通常用于宏,因此这里改变名称写法。
    
   作为一个语言常量,AspectRatio肯定会被编译器看到,当然会被进入记号表内。
   此外,对于浮点常量floating point constant 而言,使用常量可能比使用#define 导致较小量的码。
   因为预处理器”盲目地将宏名称ASPECT_RATIO替换为1.653“,可能导致目标码object code出现多份1.653,
   若改用常量AspectRatio绝不会出现相同情况。


2.3. 以常量替换 #define 的两种情况:

2.3.1.第一个是定义常量指针constant pointers.

由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。

例如若要在头文件内定义一个常量的(不变的)char* based字符串,必须写两次const:
  const char* const authorName = "Scott Meyers";
  
  string对象通常比其前辈char*-based合宜。所以上述的authorName往往定义为这样更好些:
    const std::string authorName("Scott Meyers");


2.3.2.第二个是class专属常量。

为了将常量的作用域scope 限制于class内,必须让常量作为class的一个成员member;而为了确保常量至多(最多)只有一份实体,必须将常量声明为一个static成员:

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

然而NumTurns 的声明式而不是定义式。
通常C++要求对所使用的任何东西都提供一个定义式,
但是如果它是个class专属常量又是static且为整数类型 integral type,例如ints, chars, bools.需要进行特殊处理。

只要不取它们的地址,可以声明并使用它们,不用提供定义式。
如果要取某个class专属常量的地址,必须另外提供定义式:
    const int GamePlayer::NumTurns; //NumTurns的定义
    这个式子放入实现文件,而不是头文件中,由于class常量已经在声明时获得初值。因此在定义时不可以再设初值。 
    

请注意,我们无法利用#define创建一个class专属常量,因为#defines并不重视作用域(scope)。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味#defines不仅不能够用来定义class专属常量,也不能够提供任何封装性,也就是说没有所谓private#define这样的东西。而当然const成员变量是可以被封装的,NumTurns就是。


旧式编译器也许不支持上述语法,它们不允许static成员在其声明式上获得初值。此外所谓的"in-class初值设定”也只允许对整数常量进行。如果你的编译器不支持上述语法,你可以将初值放在定义式


class CostEstimate
{
private:
  static const double FudgeFactor; //static class常量声明
  ...                             //位于头文件内
};
const double                          //static class 常量定义
    CostEstimate::FudgeFactor = 1.35; //位于实现文件内

2.4. 唯一例外是当你在class编译期间需要一个class常量值

例如在上述的GamePlayer::scores的数组声明式中(是的,编译器坚持必须在编译期间知道数组的大小)。这时候万一你的编译器(错误地)不允许"static整数型class常量”完成"in class初值设定”,可改用所谓的"the enum hack"补偿做法。其理论基础是:“一个属于枚举类型(enumerated type)的数值可权充ints被使用”,于是GamePlayer可定义如下:

class GamePlayer 
{
private:
  enum{NumTurns = 5};  //"the enum hack"--令NumTurns成为5的一个记号名称。
  
  int scores[NumTurns];
 };


第一,enum hack的行为某方面说比较像#define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。
如果你不想让别人获得一个pointer或 reference指向你的某个整数常量,enum可以帮助你实现这个约束。
第二,实用主义。事实上"enum hack"是template metaprogramming(模板元编程)的基础技术。


2.5.另一个常见的#define误用情况是以它实现宏(macros).

宏看起来像函数,但不会招致函数调用(function call)带来的额外开销。下面这个宏夹带着宏实参,调用函数f:

//以a和b的较大值调用f
#define CALL_WITH_MAX(a,b)  f((a)>(b) ? (a) : (b))
这般长相的宏有着太多缺点,光是想到它们就让人痛苦不堪。会在调用的时候可能遭遇麻烦。
--------------------------------------
纵使所有实参加上小括号,也会出现神奇的事情:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加二次
CALL_WITH_MAX(++a, b+10); //a被累加一次
在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”
---------------------------------------
解决方法:
 利用template inline函数:
   template<typename T>
   inline void callWithMax(const T& a, const T& b)
   {//由于不知道T是什么,所以采用pass-by-refenrence-to-const。
     f(a > b ? a : b);
   }
  这个template产出一整群函数,每个函数都接受两个同型对象,并以其中较大者调用f。
  这里不需要在函数本体中为参数加上括号,也不需要操心参数被核算(求,值)多次……等等。
  此外由于callWithMax是个真正的函数,它遵守作用域(scope)和访问规则。
  例如你绝对可以写出一个"class内的private inline函数”。一般而言 宏 无法完成此事。

有了consts、enums和inlines,我们对预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演控制编译的重要角色。

对于单纯常量,最好以const对象或enums替换#defines。
对于形似函数的宏(macros),最好改用inline函数替换#defines。


条款03:尽可能使用const

3.1. 变量

    1. const的一件奇妙事情是,它允许你指定一个语义约束(也就是指定一个“不该被改动”的对象),而编译器会强制实施这项约束。它允许你告诉编译器和其他程序员某值应该保持不变。只要这(某值保持不变)是事实,你就该确实说出来,因为说出来可以获得编译器的襄助,确保这条约束不被违反。
      关键字const多才多艺。你可以用它在classes外部修饰global 或namespace(见条款2)作用域中的常量,或修饰文件、函数、或区块作用域(block scope)中被声明为static的对象。你也可以用它修饰classes内部的staticnon-static成员变量。

3.2. 面对指针

    1. 面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不)是const
char greeting[] = "Hello"; 
char* p = greeting;                      // non-const pointer, non-const data
const char* p = greeting;                // non-const pointer, const data
char* const p = greeting;                // const pointer, non-const data
const char* const p = greeting;          // const pointer, const data
----------

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

------------
常量指针 指向常量的指针;  
指针常量 指针常量的值是指针,这个值因为是常量,所以不能被赋值。

如果被指物是常量,有些程序员会将关键字const写在类型之前,有些人会把它写在类型之后、星号之前。两种写法的意义相同,所以下列两个函数接受的参数类型是一样的:

void f1(const Widget* pw);          //f1获得一个指针,指向一个常量的(不变的)Widget对象.
void f2(Widget const * pw);         //f2也是

3.3.STL选代器

    1. STL选代器系以指针为根据塑模出来,所以选代器的作用就像个T*指针。声明选代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个选代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果你希望选代器所指的东西不可被改动(即希望STL模拟一个const T*指针),你需要的是const_iterator
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();      //iter的作用像个T* const 
*iter = 10;             //没问题,改变iter所指物
++iter;                 //错误!iter是const

std::vector<int>::const_iterator cIter = vec.begin();    //iter的作用像个const T* 
*cIter = 10;           //错误!cIter是const
++cIter;               //没问题,改变cIter



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

4.1. 永远在使用对象之前先将它初始化

    1. 永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。
int x = 0;  //对int进行手工初始化
const char* text = "A C-style string"; //对指针进行手工初始化
                                      //(亦见条款3)
double d;
std::cin >> d;  //以读取inputstream的方式完成初始化.

4.2. 确保每一个构造函数都将对象的每一个成员初始化

    1. 至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化

4.3. 别混淆了赋值assignment和初始化initialization

    1. 别混淆了赋值assignment和初始化initialization
class PhoneNumber {...}
class ABEntry
{
//ABEntry ="Address Book Entry"
public:
  ABEntry(const std::string&name, const std::string&address, 
          const std::list<PhoneNumber>&phones);
          
private:
  std::string theName;
  std::string theAddress;
  std::list<PhoneNumber> thePhones;
  int numTimesConsulted;

};

ABEntry::ABEntry(const std::string& name, const std::string& address,
                const std::list<PhoneNumber>& phones)
{
    theName = name;       //这些都是赋值(assignments)
    theAddress = address; //非初始化(initializations)
    thePhones = phones;
    numTimesConsulted = 0;
 }

这会导致ABEntry对象带有你期望(你指定)的值,但不是最佳做法。

C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前

ABEntry构造函数内,theNametheAddressthePhones都不是被初始化,而是被赋值。
初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。
但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。


4.4. 成员初值列

  1. ABEntry构造函数的一个较佳语法是,使用所谓的member initialization list(成员初值列)替换赋值动作:
ABEntry::ABEntry(const std::string& name, const std::string& address,
                const std::list<PhoneNumber>& phones)
    : theName(name), 
    theAddress(address),
    thePhones(phones),
    numTimesConsulted(0)   //现在,这些都是初始化(initialization)
{
    //现在,构造函数本体不必有任何动作
}

这个构造函数和上一个的最终结果相同,但通常效率较高。

基于赋值的那个版本(本例第一版本)首先调用default构造函数theNametheAddressthePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。

成员初值列(member initialization list)的做法(本例第二版本)避免了这一问题,
因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。

本例中的theNamename为初值进行copy构造,
theAddressaddress为初值进行copy构造,
thePhonesphones为初值进行copy构造。


4.5. 总是使用成员初值列

    1. 对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。
      对于内置型对象如numTimesConsulted,其初始化赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。
      同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可。假设ABEntry有一个无参数构造函数,我们可将它实现如下:
ABEntry::ABEntry( )
    : theName(name),  //调用theName的default构造函数
    theAddress(address), //为theAddress做类似动作
    thePhones(phones),  //为thePhones做类似动作
    numTimesConsulted(0)   //记得将numTimesConsulted显示初始化为0
{
    //现在,构造函数本体不必有任何动作
}

由于编译器会为用户自定义类型(user-defined types)之成员变量自动调用default构造函数–如果那些成员变量在“成员初值列”中没有被指定初值的话,因而引发某些程序员过度夸张地采用以上写法。
那是可理解的,但请立下一个规则,规定总是在初值列中列出所有成员变量,以免还得记住哪些成员变量(如果它们在初值列中被遗漏的话)可以无需初值。

举个例子,由于numTimesConsulted属于内置类型,如果成员初值列(member initialization list)遗漏了它,它就没有初值,因而可能开启“不明确行为”的潘多拉盒子。

有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初值列。是的,如果成员变量是constreferences,它们就一定需要初值,不能被赋值(见条款5)。
为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。


4.6. 成员初值列 真正初始化

    1. 成员初值列 真正初始化
  • 许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。

  • 如果这种classes存在许多成员变量和/或base classes,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。

这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。

这种做法在“成员变量的初值系由文件或数据库读入”时特别有用。

然而,比起经由赋值操作完成的“伪初始化”(pseudo-initialization),通过成员初值列(member initialization list)完成的“真正初始化”通常更加可取。


4.7. 成员初始化次序

    1. 成员初始化次序

    C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes更早于其derived classes被初始化(见条款12),而class的成员变量总是以其声明次序被初始化。

    ABEntry,其theName成员永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。为避免你或你的检阅者迷惑,并避免某些可能存在的晦涩错误,当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序。

译注: 上述所谓晦涩错误,指的是两个成员变量的初始化带有次序性。 \red{上述所谓晦涩错误,指的是两个成员变量的初始化带有次序性。} 上述所谓晦涩错误,指的是两个成员变量的初始化带有次序性。
例如初始化 a r r a y 时需要指定大小,因此代表大小的那个成员变量必须先有初值。 \red{例如初始化array时需要指定大小,因此代表大小的那个成员变量必须先有初值。} 例如初始化array时需要指定大小,因此代表大小的那个成员变量必须先有初值。


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

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

所谓static对象,其寿命从被构造出来直到程序结束为止,因此stackheap-based对象都被排除。

这种对象包括 global 对象、定义于 namespace 作用域内的对象、在 classes 内、在函数内、以及在 file 作用域内被声明为static的对象。

函数内的 static 对象称为 local static 对象(因为它们对函数而言是local),其他 static 对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

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

涉及至少两个源码文件,每一个内含至少一个non-local static对象(也就是说该对象是global或位于namespace作用域内,亦或在class内或file作用域内被声明为static)。

真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。

实例:

假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机(local)。由于这个class使世界看起来像个单一文件系统,你可能会产出一个特殊对象,位于global 或namespace作用域内,象征单一文件系统:

class FileSystem          //来自你的程序库
{
public:
  ...
  std::size_t numDisks() const;  //众多成员函数之一
  ...
};
extern FileSystem tfs;        //预备给客户使用的对象;
                  			//tfs 代表"the file system"

总结

学如逆水行舟,不进则退!!!

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值