More Effective C++读书笔记

0 导读

  • 鼓励在至少两种编译器平台上发展代码(有哪些编译器??)

1 基础议题

条款1:仔细区别pointers和references
  • 如果这个变量总是必须代表一个对象,也就是说如果你的设计并不允许这个变量为null,那么你应该使用reference。其实char* p=0; char& rc=*pc也许能把reference强行编程null,不过C++对此没有定义
  • 另一个差异是,pointers可以被重新赋值,reference却总是执行最初获得的对象
  • 还有其他情况需要使用reference,例如实现某些操作符时,如operator[]
条款2:最好使用C++转型操作符
  • 每次转型能更精确地指明意图更好。旧式的C转型方式和C++转型之间有很大的差异,如将指向const转为指向non-const和将指向base-class转为指向derived-class之间的差距很大(比如?)
  • 旧式转型难以辨识。小括号和对象名称在C++任何地方都有可能被使用
  • 一些特点
    • const_cast由编译器贯彻执行
    • dynamic_cast执行继承体系中安全的向下转型或跨系转型动作。转型失败会以一个nulll指针(转型对象为指针)或exception(转型对象为reference)表现出来
    • reinterpret_cast几乎总是与编译平台息息相关。最常用途是转换“函数指针”类型
  • 如果编译器未支持这些新转型动作,可用传统转型方式来取代。甚至使用宏定义来仿真
条款3:绝对不要以多态(polymorphically)方式处理数组
  • 通常derived class比base class大,所以如下代码是错误不可预期的:
    void print(const BaseClass array[])
    {
      cout << array[5]; //相当于*(array+5),编译器在此认为偏移大小为sizeof(BaseClass)*5
    }
    DerivedClass array[10];
    print(array);
    
  • 通过base class指针,删除由derived class objects组成的数组,其结果未定义:在上例中,cout << array[5]改为delete [] array;(这句不停偏移并在相应区域调用析构函数导致未定义结果)
条款4:非必要不提供default constructor
  • 意思是尽量不提供default constructor(即没有参数的构造函数)
  • 无default constructor会导致无法产生其构成的数组:因为无法给数组中对象指定参数
    1. 可以使用non-heap数组使能够定义数组时提供必要的自变量:AClass array[]={AClass(para1), AClass(para2)};
    2. 使用“指针数组”而不是“对象数组”:指向对象的指针的数组(过度使用内存)
    3. (解决2中问题)先分配raw memory然后使用placement new构造对象(结束时也得手动调用析构函数并释放raw_memory)
      void* raw_memory = operator new[](10*sizeof(AClass));
      AClass p_class = static_cast<AClass>(raw_memory);
      for (int i = 0; i < 10; i++)
      new (&p_class[i]) AClass(paramemter); //这个是C++标准库中的一个placement,用于在传入的内存地址上构造
      p_class[i].~AClass();
      operator delete[](raw_memory);
      delete[] raw_memory; //不应该用这个不然结果未定义,其配套的是new[]而不是new[]();
      
  • 无defalut constructor还导致其不适用于许多template-based container classes:谨慎设计template可以消除这个问题比如标准vector容器
  • 没有default constructor还使得无论派生层次多深,派生类都必须提供构造参数。派生类设计者不期望不欣赏这么做
  • 增加default class/默认构造参数导致member function变得复杂(不断验证字段是否有意义)。因此添加无意义的default constructor也会影响效率

2 操作符

条款5:对定制的“类型转换函数”保持警觉
  • 最好不要提供任何类型转换函数:operator double();
    • 可能导致错误(非预期)的函数被调用。std::string就未含有到char*的隐式转换函数
      class Rational
      {
      operator <<();
      operator double();
      }
      Rational rat;
      cout << rat; //如果未定义operator <<,编译器想尽办法调用一个可调用的,这里是operator double。这和预期不同
      //替代方法是只创建一个double asDouble()函数
      
  • 单一自变量constructor问题
    1. 简易方法:使用C++特性explict特性:在constructor前添加explict声明。但显式转换是允许的
    2. 如果编译器不支持explict则可以:编写proxy class(以旧类的构造参数作为其构造参数),让旧类以这个新类作为构造参数,可以使得原本的构造用法不变而阻止一些隐式转换。这是因为:(隐式转换的复杂规则中一条)任何转换程序都不能内含一个以上的“用户定制”转换行为(单自变量构造函数或隐式类型转换操作符)
      class Array
      {
      Array(int size);
      }
      bool operator== (Array& lhs, Array& rhs);
      Array a(10); Array b(10);
      a == b[2]; //想a[2]==b[2]实际转化为a==static_cast<Array>(b[10])。编译器会尽可能找方法调用
      //方法2
      class Array
      {
      Array(Arraysize size);
      class ArraySize
      {
       ArraySize(int size);
      }
      }
      
条款6:区别increment/decrement操作符的前置和后置形式
  • 前置式返回一个reference,后置式返回一个const对象
    AClass& operator++(); //前置式++
    const AClass operator++(int); //后置式++。int型参数唯一目的是为了区分前置式和后置式
    
  • 后置式返回const,不然就允许i++++语法了:
    1. 这样和内建类型行为不一致
    2. 即使允许那么其也只是施加在临时对象上,没有意义
  • 为保证两者行为一致的原则:后置式和decrement操作符的实现应以前置式兄弟为基础(也即是调用)。这样就只需要维护前置式版本
条款7:千万不要重载&&,||和,操作符
  • 这些操作符具有骤死式语义``expr1 && expr2如果expr1评估为false则exper2就根本不会被计算
  • 如果编写了operator&&()重载这些操作符,就用函数调用语义替换了骤死式语音。区别:
    1. 执行函数调用,所有参数都必须评估完成,即epxr1和expr2都要计算
    2. 函数未规定评估顺序。而骤死规定了从左到右
  • 表达式如果内含逗号,那么逗号左侧会先被评估。整个逗号表达式结果以逗号右侧的值为代表。自己撰写都阿红操作符是无法模仿这些的
  • P37列出了能够重载和不能重载的操作符
      //不能重载
      . .* :: ?:
      new delete sizeof typeid
      static_cast dynamic_cast const_cast reinterpret_cast
      //可以重载
      operator new    operator delete
      opreator new[]    operator delete[]
      + - * / % ^ & | ~
      ! + < > += -= *= /= %=
      ^= &= |= << >> >>= <<= == !=
      <= >= && || ++ -- , ->* ->
      () []
    
条款8:了解各种不同意义的new和delete
  • new operator是调用new时使用的操作符,其无法被重载,其依次完成:
    1. 分配足够的内存
      • 其通过调用operator new()分配内存。我们可以重写或重载这个函数
      • operator new()通常声明为:void* operator new(size_t size);。size_t表示分配多少内存。可以加上额外参数(这中称作placement new),但第一参数类型必须是size_t
      • 我们也可以像调用任何其他函数一样调用它:void* raw_memory = operator new(sizeof(int));
    2. 调用一个constructor为刚才分配的内存设定初值:程序员没有权利做但是编译器可以
      • 程序员可以使用placement new(第二个参数为void*)传入raw memory地址达到间接调用constuctor目的。程序库中默认带有这样一个placement new
      • ps:对未使用的函数参数只定义类型而不定义参数名可以防止编译器报未使用参数的警告
    3. int* p = new int(2);还编译出了类型转换的动作,但是实际发生时间可能在调用构造函函数之前
  • new和delete要配对使用
    1. 使用new operator分配的对象应该由delete operator释放
    2. 使用operator new分配的内存应该直接调用operator delente释放
    3. 使用placement分配的内存应该手动调用析构函数p->~AClass();并调用合适方法释放(placement delete是在placement new失败时调用而不是供我们使用)
  • 如果编译器不支持operator new[](因其是较晚才加入的特性)则全局operator new会被用来为每个数组分配内存。我们就得改写全局版operator new。所以最好换编译器吧
  • 我们可以重载operator new[]和operator delete[](TODO:具体使用细节和参数含义)

3 异常

  • 异常特性的引入使得可执行文件和程序库变得更大
  • 使用异常原因:
    • exception无法被忽略。如果函数以设定状态变量或者返回错误码方式发出异常信号,无法保证调用者会检查这些错误信号。
    • C中setjmp和longjmp有类似行为,但是其调整栈的时候无法调用局部变量的destructor
条款9:利用destructors避免泄露资源
  • auto_ptr不适用于array,其内部采用单一对象的delete。更好选择是用vector取代数组
条款10:在constructors内阻止资源泄露
  • C++保证删除null指针是安全的
  • 在constructor中的异常会被抛出到产生正在产生该object的那一端。因而该object析构函数根本不会被调用(导致在constructor中为指针成员new的内存泄露)。
    • 应对上述情况:在constructor中将所有可能的exception捕捉起来,执行某种清理工作然后重新抛出excpetion让其继续传播出去
    • 在member initialization list中的non pointer data成员在constructor调用之前就已经初始化完成。construct失败时会自动被销毁
    • 如果指针成员在初始化列表中分配内存和初始化,可以单独用于一个函数进行异常的捕捉处理和抛出。这使得constructor的动作散布在函数中,造成维护上的困扰
  • 更好的方法:将指针指向对象视为资源,交给局部对象来管理(即用auto_ptr等)
条款11:禁止异常(exceptions)流出desstructors之外
  • 调用destructor的情况:1.正常状态销毁;2。excpetion传播中的stack-unwinding栈展开机制
  • 在写destructors时要假设一个exception在正在传播。否则如果desctructor抛出异常会导致C++调用terminate()函数终结程序。另一个原因:如果destructor抛出异常则意味着其执行补全清理不干净
  • 方法:在其中用try-catch语句。但要避免catch语句中仍有异常抛出,因此catch语句一般写为空
条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异
  • 不同之一:不论被捕捉的exception是以by value还是by reference方式传递(不可能by pointer因为类型不吻合??),都会发生对象的复制行为,交到catch的是那个副本。即使该对象是static属性
  • 复制行为由对象的copy constructor执行。在有继承情况下,根据“静态类型”确定copy constructor函数(即根据类型名调用而不查找虚函数表)
    //在catch (Widget& w)中
    throw ; //抛出同样的一个w
    throw w; //抛出w的一个副本
    //catch (const Widget& w)中const是不必要的:函数调用中将临时对象传递给non-const是不允许的但是exception中合法
    
    catch (Widget w); //有两个副本的构造代价
    catch (Widget& w); //只有一个副本的构造代价
    
  • throw by pointer和pass by pointer:都传递指针副本
  • 不同之二:exceptions和catch匹配中只有两种转换可以发生:1.继承架构中的类转换,即基类可以捕捉派生类的异常;2.允许从有型指针转换为无型指针
  • 不同之三:catch子句总依出现顺序做匹配尝试即first fit,而虚函数采用best fit
条款13:以by reference方式捕捉exceptions
  • catch by pointer方式:抛出指针指向一个对象。必须保证控制权离开抛出异常的函数后对象仍然催在。可设置为Golbal或static对象:常被忘记;或者指向heap object:涉及在混乱环境下是否该delete问题
    //by pointer
    throw &exception_object;
    catch (ExceptionClass* ex)
    
  • catch by value:1.两次复制问题;2.切割问题:只通过基类的拷贝构造函数进行复制,只能调用到基类中的虚函数
  • 所以用catch by reference上述问题都没有
条款14:明智运用exception specifications
void Func1() throw(); //这个函数保证不抛出异常(并不意味着函数内部真的不抛出其他异常,此时默认使程序退出)
void Func1() noexcept; //C++11中规范。throw()用法在C++17中弃用
void Func1() noexcept(true);
void Func2() throw(int, std::logic_erro); //保证只抛出int和std::logic_erro类型异常
  • 如函数抛出未列出在excetpion specification的exception,在运行期间特殊函数unexpeted()被调用。其默认行为是调用terminate()继续默认调用abort()
  • 编译器只会对exception specification做局部性检验:只检验代码中是否有抛出不合适异常的语句而不管其中调用的函数是否会抛出不合适异常
  • 避免unexpected
    1. 不应该将templates和exceptions混合使用:根本无法确定templates的参数的类型会抛出什么异常
    2. 如A函数调用B函数(无exception specifications)则A函数也不设定exception specifications
      • 需要注意Callback函数(即通过传入的函数指针调用函数),因为该函数的exception sepcifications未知。可以通过在typedef定义函数指针类型时加入throw()属性(但标准委员会认定typedef内不应该出现 exception specification,因而不广泛具有移植性,可用宏替代),然而在函数指针传递时检查exception sepcification是较晚加入的特性
    3. 处理“系统”可能抛出的exceptions。如下的方法能使得未预期的异常能传递下去,但是前提是此时exception specifications中包含相应的异常类,否则仍会调用terminate(逻辑上似乎应该导致循环抛出,什么机制保证terminate??)
      //做法1
      class UnexpectedException { };
      void ConvertUnexpected()
      {
      throw UnexpectedException();
      }
      set_unexpected(ConvertUnexpected); //设置产生未在异常规范列表中异常时调用的函数,而不是默认的unexpected()
      //做法2
      //原理:unexpected()的替代者重新抛出当前异常则该异常会被标准类型bad_exception取代
      void ConvertUnexpected()
      {
      throw ; //抛出当前异常
      }
      set_unexpected(ConvertUnexpected);
      //exception specifications中都必须含有bad_exception
      
条款15:了解异常处理的成本
  1. 为了在运行期处理exception程序必须做大量记录工作。只要程序任意部分使用exception则整个程序就必须支持(如果不使用exceptions并让编译器知道,则其可以适度完成某种性能优化)
  2. 使用try-catch语句使相应段代码大约膨胀5%~10%执行速度也大约下降这个数(只是未抛异常情况),所以应避免非必要的try语句块。一个exception specification通常招致try语句块相同成本
  3. 和正常函数返回相比,抛出exception导致的函数返回,速度可能慢3个数量级。所以exception的出现应该是罕见的
  • 结论:对try语句块和exception sepcifications的使用限制于非用不可地点,并在真正异常情况才抛出exceptions(作者说由于是实现和标准出现较晚等原因成本很难精确估计,因而上述数据来自小道消息和一些测试结果)

4 效率

  • 既要写高效的程序,也不要忽视算法的天生效率
条款16:谨记80-20法则
  • 基本重点:软件的整体性能几乎总是由其构成要素(代码)的一小部分决定
  • “程序的性能特质倾向于高度的非直觉性”。可行之道是完全根据观察或实验识别出造成你心痛的拿20%代码,而识别之道就是借助某个程序分析器
条款17:考虑使用lazy evaluation(缓式评估)
  • 延缓运算直到运算结果不得不被需要为止
  • 三种方式
    1. Reference Counting(引用计数):类似于写时拷贝
    2. 区分读和写:在同一函数如operator中区分读和写来延缓写入(如何做??条款30:proxy classes)
    3. Lazy Fetching(缓式取出):不预取来自数据库等的全部数据,每次只取出需要的
    4. Lazy Expression Evaluation(表达式缓式评估):先只记录运算操作直到必须得到结果才运算。矩阵运算中只计算需要的位置的值:APL
  • 做法原则:先实现一个class使用直接易懂的eager evaluation策略,但在分析报告指出瓶颈在此时,用另一个lazy evaluation的class替代
  • PS:在const member function中改变成员变量的两种方法
    1. mutable关键字:意思是该字段可以在任何member function,甚至const member function中被修改
    2. “冒牌this”法:产生一个pointer-to-non-const指向this指向的对象
      //在类的一个const成员函数中:
      void AClass::Func() const
      {
      AClass* fake_this = const_cast<AClass* const>(this);
      fake_this->a_private_member = value;
      }
      //首先在类中本就可以通过本类指针访问任何私有成员变量(无论指向为哪一个对象)
      //其次,const函数被翻译为Func(const AClass* this)使得我们无法修改私有成员
      
条款18:分期摊还预期的计算成本
  • over-eager evaluation(超急评估):超前进度地做“要求以外”的更多工作。
  • 方法
    1. Caching缓存
    2. Prefetch预取
  • 两种方法都是“空间可以交换时间”。但少数情况下对象变大会降低软件性能(换页增加,击中率降低)。因而还是要利用分析器profiler
  • PS:1995C++标准化时规定STL iterator必须支持“->”操作符。然而某些STL实现品可能尚无法满所以(*it)仍然是最具移植性的写法
条款19:了解临时对象的来源
  • C++真正的临时对象是不可见的。只要产生一个non-heap object而没有命名就产生。通常发生于:1.隐式类型转换;2.函数返回对象时
  • 隐式类型转换:只有1.当对象以by value方式传递,2.或当对象被传递给一个reference-to-non-const参数才会发生隐式类型转换
    • C++语言禁止在隐式转换中为non-const reference对象产生临时对象的原因:如允许,则修改只会发生在临时对象上
  • 返回对象时:返回值优化,见条款20
条款20:协助完成“返回值优化(RVO)”
  • 有些函数硬是得返回对象,我们需要做的是努力找出某种方法以降低被返回对象的成本
  • 可某种特殊写法撰写函数使在返回对象时能让编译器消除临时对象的成本:返回constructor argument取代对象
    const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
      //通常写法
      //Rational result(...);
      //return result;
      return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
    }
    Rational a = 10;
    Rational b(1, 2);
    Rational c = a * b;
    //编译器得以消除operator*中临时对象和被其返回的临时对象:将return表达式定义的对象构造于c的内存
    //因为命名对象是不能被消除的所以c和变量result不能被消除。还可以将函数inline消除掉operator*的额外开销(条款33)
    //但是1996年ANSI宣布命名对象和匿名对象都可以通过return value optimization优化去除,所以两个版本都能导出相同的优化目标代码
    
条款21:利用重载技术(overload)避免隐式类型转换(implicit type conversions)
  • 意思即写多个重载函数来调用避免编译器利用隐式类型转换调用不合适的函数。前提是:使用重载函数后,程序的整体效率可获得重大改善
  • 每个“重载操作符”必须获得至少一个“用户定制类型”的自变量:不然就改变了预先定义的操作符意义
条款22:考虑以操作符复合形式(op+)取代其独身形式(op)
  • 意思即:比如通常在实现operator+时导致返回时产生临时对象,而operator+=不会(就在本对象上改变)
  1. 一般而言,复合操作符比其对应的独身版本效率高:独身版本必须返回一个新对象
  2. 如果同时提供某个操作符的复合形式和独身形式,便允许用户在效率与便利性之间做取舍
    result = a + b + c; //效率低:可能用到3个临时对象
    result +=a; result +=b; result +=c; //效率高:不需要临时对象
    
  3. 独身形式操作符上的效率
    template<class T>
    const T operator+ (const T& lhs, const T& rhs)
    {
     return T(lhs) += rhs;
     //通常写为
     //T result(lhs);
     //return result += rhs;
    }
    //通常写法含有命名对象reslt意味着返回值优化不可能被实施而未注释写法总是适用,比较有机会
    //可能会产生一个临时对象(和通常写法产生result一样),但是匿名对象总是比命名对象更容易被消除
    //PS:某些编译器会把T(lhs)视为一个转型动作移除lhs常量性然后把rhs加到lhs并返回一个reference指向修改后的lhs,因此请先检测编译器行为
    
条款23:考虑使用其他程序库
  • 很容易出现“两个程序库提供类似机能却有相当不同的性能表现”。比如iostream相对于stdio库,其具有类型安全(基本等价于内存安全)并且可扩充,但效率如后者。不过有时候前者更快:iostreams在编译期就决定其操作数的类型,而stdio在运行期才解析其格式字符串
    cout << setw(10) << setprecision(5) << setiosflags(ios::showpoint) << setiosflags(ios::fixed)  << d;
    //等价于printf("%10.5f", d);
    
条款24:了解virtual functions、multiple inheritance、virtual base classes、runtime type identificcation的成本
  • 虚函数的成本
    1. 必须为每个拥有虚函数的class耗费一个vtbl(virtual table)空间
      • vtble位置:1.暴力做法:在每个需要vtbl的目标文件都产生一个vtbl副本最后由连接器剥除重复副本;2.探勘式做法:vtbl在内含其第一个non-inline、non-pure虚函数定义式的目标文件中;但当虚函数全为inline时会产生很多副本,虽然大部分用该方法的编译器会允许手动控制vtbl产生不过最好避免虚函数为inline。目前编译器通常都会忽略虚函数的inline指示
    2. 每个拥有虚函数的对象内付出“一个额外指针vptr”的代价:不同编译器会放在不同位置(g++和vc++放在哪??)
    3. 事实上等于放弃了inline:虚函数本身并不构成性能上的瓶颈(即使用虚函数会变慢函数调用但是不会成为瓶颈)
      • 虚函数仍然可以被声明为inline并通过编译。但是当前编译器是否真的inline是不一定的。g++上测试是inline函数需要和声明放在同一个文件中(否则提示inline找不到)但是virtual函数并没有inline(意味着会产生多个函数实现副本,而不是像普通函数的定义写到.h文件时产生多重定义的编译错误)
  • virtual base classes可能导致另外成本,因为实现做法常常利用指针
  • 运行时期类型辨识(runtime type identification,RTTI)成本:一个class一份type_info对象以及vtbl的一个指针空间指向type_info
    • PS:含有虚函数的类下运行时RTTI,否则在编译期RTTI返回静态类型
  • 了解该条款内的详细实现手法:Inside the C++ Object Model

5 技术

  • 描述C++程序员常常遭遇的一些问题的解决方法
条款25:将constructor和non-member functions虚化
  • virtual constructor是某种函数,可以按照其获得的输入产生不同各类型的对象。其实就是类中的BaseClass* Create();函数(根据情况可以产生各种不同的派生类对象)
    • 其中一种特别的是virtual copy constructor:即类中有个virtual DerivedClassOne* Clone();函数
      class DerivedClassOne : public BaseClass
      {
        virtual DerivedClassOne* Clone()
        { return new DerivedClassOne(*this); }
      }
      
    • 上述实现手法说明一规则:当derived class重新定义其base class的一个虚函数时不再需要一定得声明与原本相同的返回类型。规则是:一虚、二容、四同
  • 将non-member Functions的行为虚化:让其视某个自变量虚化
    • 例:为类实现operator<<,可以在base class和derived class中声明virtual operator<<,但是该实现方法使得其使用方法为object<<cout。于是可在类中实现virtual print成员函数,然后全局实现operator<<(ostream, BaseClass&)(其中调用print)利用多态自动产生不同的输出结果
条款26:限制某个class所能产生的对象数量
产生零个或一个
  • 允许零个:构造函数声明为private
  • 产生唯一对象:constructor声明为private;全局TheClass()声明为类的friend;TheClass()含一个static类对象。当然可以把TheClass()设为static成员函数,或者把两者放到一个namespace中
    1. 要用函数static而不是类static:1.只有当函数第一次调用才初始化,从未被使用就不会;2.确切地知道初始化时机:C++对同一编译单元中statics初始化顺序有保证而不同编译单元未说明
    2. 不要产生含local static对象的inline non-member function:inline是内部连接的(不和其他编译单元交互,其名称不会被带到目标文件中)所以会产生多个函数/static副本。但据注释,1996年ANSI将inline默认连接改为外部连接故该问题已经解决,不过最好还是验证编译器
  • 产生一个对象:类中static变量记录数目;constructor检验数量超过则抛出异常;private copy constructor
不同的对象构造状态
  • 对象可在3种不同状态下:1.它自己;2.派生物的base部分;3.内嵌于较大的对象中
  • 如果只希望1状态存在:可将所有constructor设为private以禁止派生;声明static AClass* MakeObject()
允许对象生生灭灭
  • 前述用函数static产生唯一对象的方法的缺点是不允许对象被destory后重新产生(时间上每个时刻仍只有一个对象)。于是可将禁止派生方法和类个数技术方法结合起来。这种方法也能产生任意固定数目个对象
一个用来计算对象个数的Base Class
  • 用奇异模板模式来使得对每个derived class都有各自独立的static元素;让构造析构函数成为protected只让派生类调用;派生类实现方法如上即专用MakeObjective()成员函数产生
    template<class BeingCounted>
    class Counted{ }
    class Widget : private Counted<Widget>
    { }
    
  • 继承时使用private继承:1.除了Widget其他不需要知道任何Counted的内容;2.privated继承下编译器不自动将derived class转化为base class,故不可能通过base class*删除派生类对象,故不需要virtual destructor,故可省去虚函数的相关空间浪费
    • 此时如果想让base class中部分函数能被使用,可在derived class中使用using声明该函数
条款27:要求(或禁止)对象产生于heap之中
要求对象产生于heap之中
  • 比较好的方法:让destructor成为private,而constructor仍为public;需要另外定义一个Destory()成员函数并在其中调用delete this
  • 另一个方法:让所有constructors声明为private,但去缺点是要将所有的都如此因为编译器产生的函数总为public
  • 允许继承:将destructor声明为protected(这不保证子类的base成分一定在heap中)
判断某个对象是否位于heap内
  • 一种不好但算是可用的方法:重写类中opeator new并在分配前在类中static变量中设置heap标志,然后在构造函数中检测标志并复位
    1. 问题一。分配数组时仍然这么写operator new[]会失效,只会分配一次内存但会多次调用构造函数
    2. 问题二。AClass* p = new AClass(*new AClass);这段代码代码中的四个动作(分配内存和构造函数)可能会被编译器按照某种顺序编译导致出现运行错误
  • 一没有移植性的通过判断地址位置的方法:在判断是否在heap的函数OnHeap()函数中比较地址和函数内部变量(在栈中),如果小于则在heap中。其假定Heap在Stack的内存低地址且stacka向低地址增长。但是对象可能分配在3个地方:栈、堆、static对象(包括global和namespace中的)。如何排布是看系统的,而在许多系统中static在heap低地址方向
  • 不只没绝对具移植性甚至没有颇具移植性的办法决定对象是否位于heap内。如果需要绝对确定,那么最好重新设计软件避免是否在heap中的判断
  • 判断“指针的删除动作”(对一对象调用delete)是否安全比判断“指针是否指向heap内的对象”简单一些;因为并非所有指向heap内的指针都可以被安全删除
    • 一个可能的解决方法:写全局的operator new和delete函数并在其中把指针加入到一个集合中。问题:1.极端不愿意在全局空间定义任何东西;2.效率问题:不必为所有heap簿记;3.不可能实现总有效的查找函数IsSafeToDelete():涉及多重继承时对象拥有多个地址
    • abstract mixin base class抽象混合式基类:完全满足需求,提供机能又不污染全局空间
        class HeapTracked
        {
        public:
            virtual ~HeapTracked() = 0;
            static void* operator new(size_t size);
            static void operator delete(void* ptr);
            bool IsOnHeap() const;
        private:
            static list<void*> addresses;
        };
        //IsOnHeap()实现开始需要:
        const void* raw_address = dynamic_cast<const void*>(this); //之后进行查找list,略
      
      • dynamic_cast转换为void*会获得一个指针指向原指针所指对象(此为HeapTraked)的内存起始处;而此处IsOnHeap()只会处理HeapTracked对象。不过dynamic_cast要求指针所指对象至少有一个虚函数
禁止对象产生于heap之中
  • 对象三种状态:1.直接实例化;2.derived class对象的base成分;3.内嵌于其他对象中
  • 禁止直接实例化在heap中:将operatro new设置为private,最好将operator delete和new放在同一层级。数组同理
    • 当派生类不定义public operator new时仍然有用。而派生类有属于自己的public operator new则需要另觅方法阻止派生类中base成分。同样地我们也没有办法阻止含有该类对象作为成员的对象产生于heap中;这和判断是否在heap中的问题是一个问题的两面
条款28:Smart Pointers(智能指针)
  • 利用对象而非明白调用某函数来开始和结束运转记录,在面对exceptions时是一种比较稳健的做法
  • 函数特化模板里,派生类不能使用基类模板,即不存在所谓“隐式转换”。模板函数调用不一定需要显示指定参数(Func())
Smart Pointers的构造、赋值、析构
  • smart pointer有责任在destroctor销毁时删除对象,前提是对象为动态分配所得(意味着其实也可让其指向非动态分配对象,但这样做没有意义)
  • auto_ptr不一定得指向T对象还可能指向T派生类对象导致不可能确定其类型auto_ptr间的赋值用重新new一对象并复制出来不合适,并且此会导致性能问题
  • 一个更有弹性的解法:auto_ptr被复制或被赋值则转移对象拥有权。
    • by value方式传递auto_ptr非常糟糕因此STL容器绝对不应该防止auto_ptr
    • 只有当确定要转移拥有权给函数时才应该by value方式传递auto_ptr,但实际上很少想要这么做,pass-by-referenct-to-const才是适当的
实现Dereferencing Operators(解引用操作符)
  • 返回reference而不是对象:1.其可能指向派生类;2.返回reference效率高
  • 解引用null指针是个无定义的行为
  • operator->只可能返回两种东西:1.dumb pointer;2.smart pointer
测试Smart Pointers是否为Null
  • 可以添加一个IsNull()成员函数
  • 可以提供一个operator void*()(如果为Null返回零否则返回非零值)隐式类型转换操作符。但隐式类型转换非常危险:
      SmartPtr<Apple> pa; SmartPtr<Orange> po;
      if (pa == po) { } //可以通过编译,虽然两者都没有提供operator==但都可以隐式转换比较。
      //暗示:虽然一个对象不能两次隐式转换但是一次函数调用中可以存在多个变量的隐式转换??
    
  • 差强人意但把不同类型比较机会降到最低的方法:重载operator !()(是Null返回true)。会改变其用法比如if(!p)对,if(p == 0)错误。但存在if(!pa == !pb)唯一风险可以通过编译
将Smart Pointers转换为Dumb Pointers
  • 实现operator T*()成员函数来提供隐式转换
    • 但允许client直接使用dumb pointers往往导致灾难
    • 隐式转换往往不能满足所有使用dumb pointer的代码的需求,因为不能使用一次以上的隐式转换(如果原有代码中需要如此)
    • 如果提供隐式转换就打开了难缠的bug门户,比如double free:
        SmartPtr<AClass> p = new AClass;
        delete p; //这竟然能编译通过
      
  • 结论:除非不得已不要提供对dumb pointers的隐式转换操作符
Smart Pointers和“与继承有关的”类型转换
  • derived class智能指针转换为base class智能指针,一个观念上简单但实现复杂的方法:为每一个smart pointer实现隐式类型转换操作符(使用特化模板类)。缺点:1.为每一个SmartPtr class实例加入,这完美避开了template的意义和优点;2.必须加入许多转换操作符,尤其在继承体系复杂时
  • 扩充性质:将nonvirtual member function声明为templates
      template<class T> class SmartPtr
      {
          template<class NewType>
          operator SmartPtr<NewType>() { return SmartPtr<NewType>(member_ptr); }
      };
    
    • 由于模板的原理,这里真正的决定因素是是否可以用T 构造出一个Smart对象,所以只要能够将NewType 转化为T* 都适用
    • C++理念是对任何转换函数的调用动作一样对待。因此可能导致模棱两可的行为:三层继承并有一个函数针对三个类进行重载,使用dumb pointer则会调用直接基类为参数的函数,而在这template方法下顶层和直接base class具有一样优先权
    • 缺点(似乎都不算太大缺点):1.支持member template的编译器不多(到现在应该很多了吧??);2.该方法涵盖的技术比较多,可能导致维护困难
    • smart pointers虽然smart却不是pointers,不可能完全和指针一样
Smart Pointers与const
  • SmartPtr不能转化为SmartPtr因为它们是完全不同的类型,而dumb pointer却可以
  • 由于const转换的单向性,和继承体系对象转换单向性相似,可以利用这种性质:令每一个smart pointer-to-T class公开继承一个对应的smart pointer-to-const-T class
      template<class T>
      class SmartPtrToConst
      {
      protected:
          union //为避免base和derived class中都存在同一指针副本
          {
              const T* ConstPointee;
              T* pointee;
          }
      }
      template<T>
      class SmartPtr: public SmartPtrToConst<T>
      { }
    
条款29:Reference counting(引用计数)
  • 动机:1.简化heap object周边的簿记工作;2.实现一种常识:避免重复
Reference Counting的实现
  • 将一个struct嵌套放进一个class的private内能方便让class所有members处理又能禁止任何其他人访问
  • 该部分是实现:String类中定义一struct StringValue内含引用计数和char,String中含StringValue Value。实现了拷贝构造赋值函数等,较为常规操作
Copy-on-Write(写时才复制)
  • (该部分都以string对象的实现为例)不幸的是,C++编译器无法告诉我们operator[]被用于
    读或写,所以我们必须悲观地假设non-const operator[]的所有调用都用于写
Pointers,References以及Copy-on-Wrie
  • 问题:
      String s1= "hello"; //自己实现的String采用CoW
      char* p = &s[1];
      String s2 = s1; //此时s1和s2共享同一区域
      *p = 'x'; //bug出现了
    
  • 至少的解决方法:1.忽略假装不存在,为大多数实现reference-counted字符串的库采用;2.在文件中说明;3.为每一个实值对象加上是否可以共享的标志变量。C++库采用2和3结合的做法non-const operator[]返回的reference在下一次调用可能修改字符串的函数前保证有效
一个Reference-Counting(引用计数)基类
  • 即把实值类的计数部分的功能剥离出一个基类,让嵌套的实值类public继承它来自动使用计数功能。需把析构函数声明为纯虚函数(需要抽象类但其他函数不能为纯需)并定义纯虚函数(必须被定义否则无法编译通过);拷贝构造和默认构造函数对RefCount的初都设置为0(但提供函数增减)让其创建者设置初值1(其创建时就应该是全新的);operator=只返回*this(不存在赋值情况,并且即使存在那么这样可以使得=左右两边不变)
自动操作Reference Count(引用次数)
  • 实现一个智能指针templateRCPtr{},String中用该对象指向StringValue对象。在智能指针中对引用计数进行加减和复制操作
  • 前提:1.之只能指针需要StringValue必须执行深层复制(deep copy)因为其含有char*成员;2.T必须是T而不能是其派生类,因为智能指针在复制(因为此时为不共享标志)时按照类型T进行new
把所有努力放在一起
  • 结构简述(—→表示指针指向):String{ RCPtr[智能指针]—(ptr)→StringValue[继承自RCObject]—(ptr)→HeapValue }
将Reference Counting加到既有的Classes身上
  • 场景描述:当前库已经有Widget类作为实体,现在用该技术来减少Widget实体的重复数量
  • 不好的方法:RCWidget{ RCPtr—→Widget[修改为继承自RCOjbect] }。这样需要修改Widget
  • 修改后:RCWidget{ RCIPtr—→CountHolder[继承自RCOjbect]—→Widget }
评估
  • 对象的实值需要更多内存,有时候得执行更多代码。Reference counting是个优化技术,适用前提是:对象常常共享实值
  • 使用其改善效率的最适当时机:1.相对多数的对象共享相对少量的实值;2.对象实值的产生或销毁成本很高,或它们使用许多内存
  • 应该严谨地分析程序确定其是否是瓶颈
  • PS:提到的一个词,工业水平的垃圾回收器(garbage collectors)
条款30:Proxy classes(替身类、代理类)
实现二维数组
  • 场景:实现二维数组类实现tmp[2][3]用法:operator[][]是无法通过编译的
  • 一个方法:
      template<class T>
      class Array2D
      {
      public:
          //此类并不存在用户心中,被用来代表其他对象,被称为proxy class
          class Array1D
          {
          public:
              T& operator [](int index);
              const T& operator [](int index) const;
          };
          Array1D operator [](int index);
          const Array1D operator [](int index);
      }
    
区分operaor[]的读写动作
  • 用proxy class来区分operator[]的读写动作
  • 是否调用const member function只以“调用函数的对象是否为const”为基准,因而可能只通过类中重载两个const和non-const operator[]来区分读写
  • 只要将所要的处理动作延缓至知道operator[]的返回结果将如何被使用为止。proxy class可以如此
  • proxy class区分左值运用和右值运用:

      class String
      {
      public:
          class CharProxy
          {
          public:
              CharProxy(String& str, int index);
              CharProxy& operator=(const CharProxy& rhs); //左值运用
              CharProxy& operator=(char c);
              operator char() const; //右值运用
    
          private:
              String& theString;
              ing charIndex;
          };
          friend class CharProxy;
          const CharProxy operator[](int index) const;
          CharProxy operator[](int index);
      };
      //如下用法都有效
      cout << s1[5]; //隐式转换为char
      s2[5] = 'x'; //调用参数为char的operator=
      s1[3] = s2[8]; //
    
限制
  1. char* p = &s1[1];问题,无法通过编译:可以重载取地址操作符
  2. 不能使用intArray[5] += 5;等,除非为proxy class一一定义这些函数但是工作量不小。同样地在其他member function中,为了让proxies模仿其代表的对象的行为,必须将适用于真实对象的每一个函数重载(我们不可以重载operate .)
  3. 隐式类型转换:运用用户定制转换函数的次数只限一次。实际上隐式类型转换有许多问题,比较好的设计是将constructor声明为explicit禁止隐式转换
评估
  • proxy class允许我们完成某些困难的行为:1.多维数组;2.左/右值区分;3.压制隐式转换
  • 缺点:作为函数返回值时需要被产生和销毁代价;增加软件系统复杂度;往往造成class语义的改变因为其行为和真正对象有细微差异
  • 许多情况下,proxies可完美取代其所代表的真正对象
条款31:让函数根据一个以上的对象类型来决定如何虚化
  • 问题背景:有GameObject派生出多个具体类型,多个具体类型之间碰撞效果不同,如何判断并处理(因为都是根据GameObject类型的指针来取得派生类对象)
  • multi-method:是一种“根据你所希望的多个参数而虚化”的函数。message dispatch(消息分派):一个虚函数的调用动作;上述需求可以称为double-dispatching
虚函数+RTTI(运行时期类型辨识)
  • 基本的粗暴的方法:在每个派生类的碰撞函数中利用type_id判断类型并进行相应的处理,未定义的类型抛出异常
只使用虚函数
  • 基本想法:将double-dispatching以两个singel dispatches(即两个分离的虚函数调用)实现出来

      class GameOjbect
      {
          virtual void collide(GameObject& otherObject) = 0;
          virtual void collide(SpaceShip& otherObject) = 0;
          virtual void collide(SpaceStation& otherObject) = 0;
          //略去所有其他派生类作为参数的重载函数
      };
    
      class SpaceShip : public GameObject
      {
          virtual void collide(GameObject& otherObject);
          virtual void collide(SpaceStation& otherObject);
          //略去所有其他派生类作为参数的重载函数
      };
      void SpaceShip::collide(GameObject& otherObject)
      {
          otherObject.collide(*this); //*this类型是SpaceShip
      }
    
    • 编译器是根据函数获得的自变量的静态类型来决定调用哪一组函数。*this的类型是静态知道的(因为虚函数隐藏的第一个参数是本类型的指针)为SpaceShip
  • 如需在程序中实现double-dispatching,最好修改设计消除此需求。如不能那么虚函数法比RTTI法安全一些。但对头文件权力不足会束缚系统扩充性
自行仿真虚函数表格(Virtual Function Tables)
class GameObject
{
public:
    virtual void collide(GameObject& otherObject) = 0;
};
class SpaceShip : public GameObject
{
    typedef void (SpaceShip::* HitFunctionPtr)(GameObject);

    virtual void collide(GameObject& otherObject)
    {
        HitFunctonPtr hfp = lookup(otherObject); //之后调用hfp函数,如果查找失败抛出异常
    }

    static HitFunctionPtr lookup(const GameObject& whatWeHit)
    {
        static map<string, HitFunctionPtr> collisionMap
        //find(typeid(whatWeHit).name());
        //然后根据typeid返回的类名进行查找(然而C++ standard并未明定type_info::name的返回值,编译器行为不同,比如有的返回“class SpaceShip”)
    }

    //如下函数处理SpaceShip和其他的碰撞
    virtual void hitSpaceShit(SpaceShip& otherObject);
    virtual void hitSpaceStation(SpaceStation& otherObject);
    //略去其他函数

    //
};
将自行仿真的虚函数表格(Virtual Function Tables)初始化
  • 问题一:初始化代码只应该执行一次,通过如下方法保证
      //initializeCollisionMap()是SpaceShip类中的private static membetfunction
      SpaceShip::lookup(const GameObject& whatWeHit)
      {
          static map<string, HitFunctionPtr> = initializeCollizionMap();
          //该初始化函数只会被调用一次。g++在未关闭返回值优化时直接调用一次copy constructor(不然就直接在这个对象身上构造),因为是构造因而逻辑上只应该调用一次
          //略去其他处理
      }
    
    • 书中考虑到初始化中的多次复制问题(实际上很可能会被返回值优化掉),可以用static auto_ptr指向map方法来解决这个问题
  • 问题二:以不同类型作为参数(这些类型具有继承关系)的函数指针之间不存在隐式转换。比如一种函数指针的参数只有派生类,不能转换为另一种参数是基类的函数指针(即使其他方面完全相同)
    • 不能通过reinterpret_cast将派生类参数函数指针转换为基类参数函数指针存入,然后在找到后调用:在多重继承或虚拟基类情况下,一个对象存在多个基地址(很可能基地址和派生类地址不一致),编译器认为需要基地址并传入而真正需要的是派生地址。最终导致运行错误
      //撞击函数都按照如下方式声明,并在其中做类型转换
      virtual void hitSpaceStation(GameObject& spaceStation)
      {
        SpaceStation& station = dynamic_cast<SpaceStation>(spaceStation); //然后进行处理,略
      }
      
使用“非成员(Non-Member)函数”的碰撞处理函数
  • 可以解决:

    1. 前述方法,在增加函数都需要重新编译,这种情况甚至不如双虚函数法
    2. 如果两个不同类型物体碰撞,到底哪一个class应该负责处理:前述方法具有任意性

      namespace //匿名的命名空间
      {
      //主要的碰撞处理函数:spaceShip碰撞spaceStation
      void shipStation(GameOjbect& spaceShip, GameObject& spaceStation)
      
      //次要的碰撞处理函数:spaceStation碰撞spaceShip
      void stationShip(GameObject& spaceStation, GameOjbect& spaceShip)
      {
       shipStation(spaceShip, spaceStation);
      }
      
      //函数指针按照如下定义
      type void (* HitFunctionPtr)(GameObject&, GameObject&);
      //存储类名到指针的映射的map改为如下定义:撞击这:被撞者 -> 处理函数
      typedef map< pair<string, string>, HitFunctionPtr > HitMap;
      //相应地lookup函数用法更改为:
      lookup(typeid(object1).name(), typeid(object2).name());
      //还包括lookup(),initializeCollisionMap()等的声明
      } //命名空间结束
      //实现部分也应该放在同一编译单元的匿名namespace中
      
  • 匿名namespace内的每样东西对其所在编译单元是私有的,效果就像在头文件中将函数声明为static
“继承”+“自行仿真的虚函数表格”
  • 当前方法的前提:在调用碰撞处理函数时不发生inheritance-based类型转换。比如SpaceShip的派生类和SpaceStation(其直接继承自GameObject)无法找到碰撞处理函数
  • 唯一可用的方法是回到“双虚函数调用”机制
将自行仿真的虚函数表格初始化(再度讨论)
  • 可创建class CollisionMap作为单例放置存储函数指针的map,并提供addEntry和removeEntry以支持动态增加删除碰撞处理函数
  • 要确保map条目在撞击发生之前就被加入其中
    1. 方法之一是在GameObject的派生类的构造函数中进行检查
    2. 另一种做法:产生RegisterCollisionFunction class,在其构造时获得构造函数参数并传入给addEntry。于是可以利用该类型的全局变量来自动注册需要的函数:
       RegisterCollisionFunction cf1("SpaceShip", "SpaceStation", &shipStation);
      
  • 还没完美办法实现double-dispatch,但该法或许可以让我们轻松一些

6 杂项讨论

条款32:在未来时态下发展程序
  • 就是接受事情总会改变的事实并准备应变之道,办法之一是C++“本身”(而不是注释和说明文件)来表现各种规范
  • 请为每一个class处理assignment和copy construction动作,即使没有人使用
  • 努力让classes的操作符和函数拥有自然的语法和直观的语义
  • 让classes容易被正确使用不易被误用,接受客户会犯错的事实
  • 努力写出可移植代码
  • 设计代码使“系统改变带来的冲击”得以局部化。尽量采用封装,避免以RTTI作为设计基础家呢人导致if-then-else
  • 等等等等
条款33:将非尾端类(non-leaf classes)设计为抽象类(abstract classes)
  • 背景:Animal派生出Lizard和Chicken两个类,为了支持Animal*指向对象(实际为派生类对象)之间正确的赋值
  • 为了保证同类型之间正确赋值:声明virtual Lizard& operator=(const Animal& rhs);重载基类中的对应虚函数(其返回Animal&)。该规则强迫在每一个class为此虚函数生命完全相同的参数类型:Animal
  • 又导致了异型赋值问题:

      //异型赋值问题
      Animal* pAnimal1 = new Lizard; Animal* pAnimal2 = new Chicken;
      *pAnimal1 = *pAnimal2; //operator=声明为virtual后可以通过编译了。而C++强制类型检查通常会不通过
    
      //方法:用dynamic_cast
      Lizard& Lizard::operator =(const Animal& rhs)
      {
          const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs); //如果rhs不是Lizard则向外传播std::bad_cast异常
          //略去其他处理
      }
      //改进:在Lizard形成两个重载函数,避免liz1=liz2同型赋值时dymaic_cast的成本
      virtual Lizard& operator =(const Animal& rhs);
      Lizard& operator=(const Lizard& rhs); //可通过virtual operator=中dynamic_cast后调用该函数来简化代码
    
  • 问题:1.移植性:某些编译器仍未支持dynamic_cast;2.每次执行都要准备捕捉bad_cast异常
    • 解决方案:在编译期就阻止这么做。最简单办法让operator=成为animal的private(且非虚函数)
    • 该解决方案问题:1.Animal是具体类而其对象间却不能赋值;2.使无法正确实现出Lizard等的assignment操作符,因为其有必要调用base class的assignment操作符(可通过将Animal中assignment声明为protected解决)
  • 最简单的办法就是消除Animal对象相互赋值的需要:让Animal成为抽象类
    • 没有任何可以作为纯虚函数时可以令destructor为纯虚,但是必须要destructor的实现码
    • 另一个好处:降低“企图以多态方式对待数组”的机会,即用Animal类型做数组而以其他方式乱用
    • 具体类用抽象类替代,最有意义的在设计层面:强迫识别出有用的抽象性质;比如C1继承自C2应该改为产生抽象类A让C1和C2继承
  • 面向对象的设计目标是辨识出一些有用的抽象性并强迫它们成为抽象类
    • 有用的抽象性:本身有用(为其产生对象有用),对一个或多个derived classes也有用
    • 具体基类转抽象基类原因:只有在原有具体类被当作基类使用(该类被复用),才强迫导入一个新的抽象类
    • 记住,只有当你有能力设计某种class,可以继承自它而它不需要任何改变时,才能够从“封包抽象类”中获益
  • 怎么做:日后当发现有“从具体封包类继承下来”的需要时,才补上一个抽象类就好
    • 一般法则:继承体系中非尾端类应该是抽象类
条款34:如何在同一个程序中结合C++和C
  • 在尝试这么做之前,请确定C++和C的编译器产生兼容的目标文件:.o文件的gcc和g++版本不兼容,添加extern C描述后可以
Name Mangling(名字重整)
  • 这是一种程序,在C++编译器中为程序的每个函数编译出独一无二的名称
  • 重载不兼容于大部分连接器因为连接器往往要求函数名不同;C++编译器通过名称重整来妥协;而在C中函数名称并未被重整。因而在连接两种目标文件时导致函数不存在错误
  • 让C++编译器不重整某些函数名称及其它用途

      extern "C" //意味着这个函数有C linkage
                  //注意:不存在extern "Pascal"之类的用法
      void FunctionName();
    
      //其他场景1:在assemble写的函数中压抑名字重整
      extern "C" void FunctionByAssembleLang();
      //其他场景2:在C++中写被用于C++语言外的函数
      extern "C" void UsedByOtherLang(); //将被其他语言以UsedByOtherLang名字调用
      //批量使用:使一组函数避免名字重整
      extern "C"
      {
          void Function1();
          void Function2();
      }
    
  • 预处理器符号__cplusplus只针对C++才有定义,可用此来使得extern “C”的运用简化,减轻同时被C和C++使用的头文件的维护工作(通过使用#ifdef __cplusplus)
  • 没有所谓的name mangling标准算法
Statics的初始化
  • static initialization:static class对象、全局对象、namespace内的对象以及文件范围内的对象,其constructors总是在main之前就获得执行。通过此产生的对象的destructors必须在发生于main之后的static destruction过程被调用(详细可阅读《程序员的自我修养》)
  • 由于普遍的是,上述两个过程常常被放置在main函数内首尾(甚至是inline),意味着没有main函数就不会初始化,因而只要负责撰写C++软件的任何一部分都应该尝试写main
    • 如果能够尽量在C++中撰写main(因为C++中很有可能使用static对象)。为了兼容,可以将C中main重命名为extern “C” int realMain();然后在C++的main中调用
动态内存分配
  • 即两种各语言下如何释放(delete还是free,需要和new及malloc配对)以及该由谁释放等问题。结论是:为降低移植问题,尽量避免调用标准程序库外或大部分平台上未稳定的函数
数据结构的兼容性
  • 没有任何具有移植性的做法可以将对象或member functions指针传递给C函数
  • struct不具有虚函数的时候来做双向交流,应该是安全的,勉强可以用
条款35:让自己习惯于标准C++语言
  • 习惯和使用C++的Template、标准库等特性
  • 可用class template作为其他template的自变量
  • C++标准程序库两个特性
    1. 标准程序库中每一样东西几乎都是template:strings可由char构成,也可由Unicode chars等构成
       typedef basic_string<char> string;
       template<class charT,
               class traits = string_char_traits<charT>, //模板函数作为默认参数,template也可用默认参数
               class Allocator = allocator>
       class basic_string;
      
    2. 所有成为都位于namespace std中
Standard Template Library(STL)
  • 3个基本概念:container、iterator、algorithm
  • C++中迭代经常使用end:end指针可以对其他类型的container带来泛型效果。list及其他所有STL container都提供begin和end返回iterator
  • C++标准程序库远比C函数丰富许多,但花费在它身上的学习时间绝对值得

7 推荐读物

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值