Brief Notes of 《Effective C++》

本文为学习《Effective C++》各个条款之后的一点概要式的总结。
github博客地址

条款2 尽量以const, enum, inline替代#define

  • 宁可用编译器替代预处理器。以#define定义的记号是不会记录到符号表中的;
  • #define没有封装性可言。
  • enum hack。enum {tmp=5};对应的tmp一定在编译期就可以得到并且不会导致非必要的内存分配。

条款3 尽可能使用const

  • 调用const成员函数以实现孪生non-const成员函数。通过使用const_caststatic_cast来达到目的,优点是避免了代码重复。
  • 调用non-const成员函数实现const成员函数是错误的。因为这破坏了const的语义约束。

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

  • 如果自定义了需要实参的构造函数,则编译器不会自动生成default ctor
  • 如果class内部包含有带有&引用类型或者const常量类型,则编译器不会自动生成copy assignment;因为编译器不知道该怎么处理

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

  • 每一个带有virtual函数的class都拥有一个指向virtual table的指针,virtual table中包含了所有对应virtual函数的函数指针
  • 不要尝试继承任何标准库容器(比如std::string),因为它们都没有virtual dtor。这会导致未定义行为
  • 没有多态性质的base class也不要声明virtual dtor,比如说boost::noncopyable,virtual并无必要,且浪费空间的
  • 如果明确了一个类具有多态性质,且作为base class使用,则应该声明virtual dtor

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

  • 绝对不能让dtor吐出异常,因为很可能会造成资源泄露。对于有可能在dtor中发生的异常,应该将其吞下或者提前终止程序
  • 更合适的做法是为客户代码提供一个接口,使得客户有机会去处理可能发生的异常

条款11 在operator=中处理“自我赋值”

核心其实就是不能让指针指向一个未获取的资源;存在3类方法,各有各的优势

  • 赋值之前先比较lhs和rhs的地址是否相同,如果相同,则直接返回;
  • 先记住之前本身的资源(可以设一个pOrigin指针指向旧资源),随后拷贝一份rhs的资源,并令lhs指向新资源,最后再释放掉lhs的旧资源(即delete pOrigin)(这其实就是copy and swap的步骤...);
  • copy and swap。先拷贝rhs指向的资源,再令lhs指向的资源和这份拷贝之后的资源进行交换;

条款12 复制对象是勿忘其每一个成分

  • 自行编写copy ctor或者operator=是一项重大的责任,因为要考虑到各种细节。而也正是因为这样的原因,当自行编写时,编译器会认定你是一个足够强大的程序员,因此不会对自定义copy ctor和operator=的不好的地方做出任何警告;
  • 确保每一个成员变量都被正确拷贝;
  • 当目标是derived class时,其base class的成员变量也要被正确拷贝。这需要通过调用base class的copy ctor和operator=来实现;
  • 切记,copy ctor和operator=不能相互调用。这从语义上就行不通

条款14 在资源管理类中小心copying行为

对RAII对象执行复制,是需要万分小心的行为,因为它涉及到的资源的最佳处理方式不甚相同;常见的方式包括:

  • 禁止复制。很多情况下这是比较科学的做法,因为行为表现的像指针这样的数据类型是不应该重复进行delete的;如果不禁止复制,则必须做到对指涉到的资源也进行复制;
  • 引用计数。不多说了,就是智能指针那一套;

条款15 在资源管理类中提供对原始资源的访问

  • 诸如std::shared_ptrstd::unique_ptr都会提供get()成员函数来访问其指涉的底层资源;这不是破坏封装性,而仅仅是一种接口风格;
  • 访问底层资源的接口,一般而言就两种:①get()这样的成员函数,②隐式转换。一般来说还是①更好一点,因为更安全;

条款16 以独立语句将newed对象置入智能指针

  • 本条款在《Effective Modern C++》中也有讲述;
  • 核心的一点就是在单条语句内,编译器是有着重新编排执行顺序的自由的;
  • 因此,诸如std::shared_ptr<XXX> sp(new XXX);这样的语句应该单独成句,而不应该嵌入到其他语句中;
  • 其实现代C++的话,更好的做法是使用std::make_shared或者std::make_unique;它们使用完美转发,且很安全;

条款19 设计class犹如设计type

不多说了,在编写类代码的时候多看看本条款,思考条款中列出的问题;

条款23 宁以non-member、non-friend替换member 函数

  • 要理解这个条款,就得明确namespace的作用:①可以跨越多个源码文件;②在实现类似于utility所提供的功能时,更具有优势(因为语义更清晰);③在提供了所需功能基础上达到编译依赖最低封装性最好
  • 书中所举的例子:任务是调用class中的三个成员函数。那么方法大致为两种:①再写一个成员函数,内容就是调用那三个函数;②将新的函数放在class的外部(非成员函数),但位于同一个namespace中;
  • 基于上面所陈述的原因,使用第二个方法是更好的方式

条款24 若所有参数皆需要类型转换,请为此采用non-member函数

  • member函数的反面是non-member,而不是friend;friend在OOP中能避免则避免,因为太破坏封装性了
  • 只有当参数被置于参数列时,这个参数才是隐式类型转换的合格参与者;也就是说,当调用成员函数时,lhs实际上没有被置于参数列中,而是this

条款26 尽可能延后变量定义式的出现时间

  • 应该尽可能在要用到某个变量的时候才去定义它(这很显然嘛)
  • 关于循环体中的变量的下述两种定义方式,一般情况下,除非明确知道赋值操作的消耗小于构造加析构的时候才使用第一种;因为第一种方式扩大了变量的生命期;
// 第一种
{
  ...
  Weight tmp;
  for(int i = 0; i < N; ++i){
    tmp = Weight(i);
  }
}
// 第二种
{
  for(int i = 0; i < N; ++i){
    Weight tmp= Weight(i);
    ...
  }
}

条款29 为“异常安全”而努力是值得的

  • 所谓的异常安全函数,其实就是发生异常也不会导致资源泄露数据败坏;包括三类:
    • 基本保证:如果函数发生异常,则对应的对象不一定还能还原为调用前的状态,但至少保证还是正常可用的;
    • 强烈保证:即使函数发生异常,对象还是能够还原为原来的状态,即只有两种状态:成功调用不调用;这通常通过copy-and-swap来实现,即先将原来的对象复制一个副本,随后对副本执行相应的改变,如果执行成功,则原对象和副本执行swap;如果发生异常,原对象也未发生任何改变
    • 不抛掷(nothrow)保证:即保证函数不发生异常;这通常办不到。。。只要涉及到了动态内存的分配,都是有可能发生异常的
  • 可以看出,级别越高,其实实现是越困难的,并且带来的开销也会越高;因此,应该挑选的是现实可实施下的最高等级
  • 异常安全性是遵循木桶原理的,只要函数调用了等级较低的函数,那么它的异常安全性也会降低

条款30 透彻了解inlining的里里外外

  • inline在大多数C++程序中都是编译期行为;
  • inline仅仅是一个申请,并不保证一定会内联;
  • 是否真正内联还取决于函数的调用方式;(如果以函数指针进行调用,那么就不可能被内联了);
  • inline的优势是避免调用开销,但也存在以下问题:
    • 代码膨胀:毕竟,如果在多处都调用了该函数,那么就会有多份该函数体的副本;
    • 编译依赖:如果inline函数发生了改变,那么所有客户代码都必须重新编译;反之,如果不是内联的,那么仅仅重新链接一下就行

条款31 将文件间的编译依存关系降至最低

C++中降低文件间的编译依赖,主要就是两种手段:handle class以及interface class

  • 如果客户代码所使用的的头文件中,直接包含的是要使用的class的具体实现(包括各个函数定义),那么就形成了依赖关系;
  • 所谓依赖关系,就是指,只要一个class改变了一点点实现,那么所有使用它的客户代码都需要重新编译;
  • handle class
    • 所谓的handle class,实际上意味着一个负责声明的class和一个负责具体实现的class(假设为class Widgetclass WidgetImpl);两者的接口全部一致,而客户代码使用的是class Widget
    • class Widget中不对任何方法进行具体实现,只声明类接口;且涉及到非基本类型的自定义类型成员变量(比如此处的class WidgetImpl),都使用前置声明(智能)指针来进行指涉;
    • 标准库组件无需也不应该被前置声明;直接#include就行;
    • 这样一来,class Widget的头文件中不会#include任何其他的头文件(除了标准库);而这,也就杜绝了客户代码对除了class Widget头文件之外的文件产生任何依赖
    • 至于class Widget的接口实现,则在其.cpp文件中去#include "WidgetImpl",然后调用class WidgetImpl的接口即可;
  • interface class
    • 即类似于Java中的interface,不过实现方式是定义成虚基类;面向派生谱系的多态技术;

条款33 避免遮掩继承而来的名称

  • C++应对派生谱系中的函数调用,归根结底就是以名称为准进行匹配;
  • 无论是变量还是函数,是重载还是重写,是否是虚函数,甚至也无论函数的参数列表是什么形式,都没有任何关系;编译器只要在当前的域中找到了对应的名称,就直接结束匹配;
  • 这意味着:如果base class中定义了一组重载函数,而后又在derived class中定义了一个同名的函数,那么当用derived class类型(或引用、指针)来调用这个名称的函数时,基类的重载函数统统被覆盖
  • 克服这个问题的方法:在派生类中加入using声明:
class Base{
public:
    // 重载函数
    void f(int);
    void f();

};

class Derived : public Base {
public:
    using Base::f; // OK,基类的重载函数不会被覆盖了
    void f(int, int);
};

  • 如何实现仅继承部分基类接口?很简单,使用private继承+转接函数
    • 所谓的转接函数就是派生类中的公共接口,但这些公共接口只是去调用基类的函数;
    • 基类因为被private继承了,所以其所有接口也就被隐藏了;

条款34 区分接口继承和实现继承

  • 在继承谱系中,虚函数,纯虚函数,普通函数之间的根本区别就是对待接口继承实现继承的方式不同;
  • 纯虚函数:只继承接口
  • 虚函数:继承接口和一份缺省实现
  • 普通函数:继承接口和一份强制实现======》
    • 这意味着任何derived class都不应该重新定义base class中的普通函数;
    • 条款36就是在陈述这一点;本质上就是因为普通函数实施的是静态绑定,相同的对象会因为其指针或引用的类型的不同而执行不同的函数体(有可能是基类的函数体,也可能是派生类的函数体);这造成了不确定性(另一方面,单个基类指针,即使指向不同类型的派生类,其调用普通函数时,也只会执行基类函数体,造成了程序错误);
  • 注:C++的虚函数模型在二进制兼容性(ABI)方面的负面影响是极大的。如果一个程序会设计为一个动态库,客户代码对其进行加载调用,如果后续动态库进行了升级,在某个类中加入了新的虚函数,那么如果客户代码不重新编译的话,会直接调用不同的函数,造成错误,因为客户代码在编译结束以后,就直接以虚表指针加偏移的形式去调用函数,而动态库的各个函数的偏移可能在升级之后就完全改变了。

条款37 绝不重新定义继承而来的缺省参数值

  • 虽然虚函数实行的是动态绑定,但虚函数(实际上是任何函数)中的参数缺省值却是静态绑定的;
  • 这意味着函数的参数缺省值不应该被重新定义;理由还是一样的,这会因为指针或引用的类型不同而造成不确定性;
  • 如果需要为虚函数定义参数缺省值,则更好的做法是:
    • 定义一个普通函数,有缺省值;
    • 实际的虚函数变为private,且无缺省值;
    • 使用普通函数去调用虚函数;
    • 这样就避免了代码在派生谱系中的依赖性;

条款38 通过复合塑膜出has-a或“根据某物实现”

  • 关键就是理解复合(Composition)二字;复合包含应用域和实现域两种关系;
  • 应用域:即把一个class作为组件;比如说class People的一个组件是class PhoneNumber;这就是所谓的has-a关系;
  • 实现域:即某个class需要通过另一个class进行实现,但两者并不存在完美的继承关系;比如说通过一个std::vector<int>来实现一个class Stack<int>;这就是所谓的Is-implemented-int-terms-of关系

条款39 明智而审慎地使用private继承

  • private继承并不具备“软件设计”层面的意义,其仅仅是一种“软件实现”的技术;
  • 条款38中已经阐述过"Is-implemented-in-terms-of"关系,事实上,private继承也是这种意义;
  • “private继承”和“复合”的区别就在于:
    • 一般情况下,能使用复合就使用复合;
    • 只有当明确是Is-implemented-in-terms-of关系的同时,需要重写基类的虚函数或者访问protect变量时,才使用private继承;因为这是复合无法做到的;
    • EBO(empty-base-optimization):C++中一个空类的size不等于0,而是1;而继承一个空类不会加大size;这就是private的另一个优势;

条款40 明智而审慎地使用多重继承

  • 总的来说,多重继承还是有用的,但却是也存在很多的限制;
  • 条款中所涉及的“虚继承”概念是比较重要的:
    • 多重继承很可能会发生所谓的菱形继承:即某一个基类和某一个派生类之间存在多条继承路径;
    • 如果使用非虚继承的话,派生类将会保存同一个基类的多个副本;但实际上一份副本就足够了;这造成了空间浪费;更糟糕的则是因为多份副本导致的命名冲突;
    • 虚继承是解决这个问题的唯一方法;它使得派生类可以只保留基类的一份副本;
    • 但虚继承也有自己的缺点:最突出的就是加大了运行时消耗;因为采取虚继承的话,class的size和内存模型就只能在运行期才能知晓了;(C++中虚函数、虚继承内存模型 - 知乎 (zhihu.com)

条款41 了解隐式接口和编译器多态

  • 基于模板的泛型编程其实也隐含着“接口”的概念,但是是隐式的。这和派生谱系中的接口机制有很大不同;
  • 隐式接口是基于:必须满足模板代码中隐含的一组约束。比如书中给出的例子:if(w.size() > 10 && w != someNastyWidget){...}w的类型为typename T,那么就必须满足:if中给出的表达式能够转换为bool类型。
  • 所谓的编译器多态就是:编译器根据隐式接口去决定需要(生成)调用哪一个重载函数以及具现化模板。

条款42 了解typename的双重意义

  • 当用于模板参数的时候,typaname和class没有区别;
  • 如果某个名称是嵌套从属名称(nested-dependent-names),即它的性质(是变量名还是类型名)需要由模板参数来决定,那么如果它确实是一个类型名的话,就需要加上typename;(因为编译器不知道它到底是什么东西);
  • 萃取器:即traits,通过模板以及模板偏特化技术,将传递进去的类型的一些相关特征给萃取出来。比如说typename std::iterator_traits<iteT>::value_type表示的就是iteT类型的迭代器所指涉的元素类型;萃取器的优势在于任何类型的迭代器(甚至是原生指针)都能萃取出想要的特征;

条款43 学习处理模板化基类的名称

  • 模板化基类(templatized-base-class):也就是说继承来的基类是一个模板,其具体是哪一个类暂时无法确定;
  • 当模板化类继承自一个模板化基类时,编译器就默认基类中的所有名称是无法得知的;除非显式指出
  • 编译器之所以这样做,是因为由于模板偏特化以及全特化的存在,使得模板化基类不一定会拥有模板中所写的所用名称;
  • 显示指出的方法有3类:使用this->name;使用using BaseClass<T>::name;;显式调用BaseClass<T>::name;其中,第3种方法会丧失动态绑定特性,因此不是很推荐;

条款44 将与参数无关的代码抽离templates

  • 如果模板类中的某些函数与模板参数没有关系,那么多个具现化的实体类则会拥有相同的函数体,这无疑使得目标码变得冗余;
  • 更好的做法是将这些与模板参数无关的代码抽离出来,变成基类代码或者其他,然后不同的模板的具现化class去共同调用这些相同的代码(此时这些代码就只有一份实体了);
  • 当然,这样也会存在一定问题。简而言之,谁好谁坏,还是得由具体的运行环境去决定;

条款45 运用成员函数模板接受所有兼容类型

比如对于如下的一个模板类,很多时候,我们可能需要使用TmpDemo<int>去初始化一个tmpDemo<double>对象。这完全是合理的,但问题是,在模板编程的世界里,TmpDemo<int>TmpDemo<double>是完全没有任何关系的。或者可以直接在模板类中定义这样一个构造函数,但如果遭遇了其他的需求呢?比如说int变为了char,又或者,现在的typename是一个继承谱系中的各种类型。显然,单一的成员函数是解决不了问题的。

template <typename T>
class TmpDemo{
  // ...
};
  • 成员模板函数是解决这个问题的唯一方法;在成员函数中再声明typename,来让编译器来处理各种需求;
  • 泛化构造函数是成员模板函数的一种,它解决的是通过TmpDemo<U>来初始化TmpDemo<T>的问题;
  • 即使声明了泛化构造函数,也还是要去自定义拷贝构造函数,这一点需要注意;

条款46 需要类型转换时请为模板定义非成员函数

  • 该条款和条款24的思想是一致的,也就是当函数的所有参数都涉及隐式转换时,它最好是一个非成员函数(因为this是无法转换的);
  • 和条款24的不同之处在于,本条款涉及到的是模板类;即,某个函数的各个参数是模板类型;
  • 很显然,这种函数也需要定义为非成员函数;
  • 不同之处在于:因为涉及到了模板,那么在进行函数模板的模板参数推导时,绝对无法进行隐式转换,比如说对于如下的代码,直接调用int ans = addFunc(tmp, 3);是无法通过编译的,因为这涉及到了从3TmpDemo<T>(3)的隐式转换;而这在函数模板参数推导中是绝对禁止的;
template <typename T>
class TmpDemo{
public:
  TmpDemo(const T& num){value = num;}
private:
  T num;
};

template <typename T>
const T addFunc(const TmpDemo<T> &t1, const TmpDemo<T> &t2){
  return t1.num * t2.num;
}

TmpDemo<int> tmp(2);
  • 解决方法就是把非成员函数定义在模板类的内部,并声明为friend。因为模板类会将typename信息进行硬编码,就可以直接进行转换了。

条款49 了解new-handler的行为

  • new-handler:一个函数指针类型typedef void (*new_handler) ( );,并对应一个global的函数指针,由用户通过new_handler std::set_new_handler(new_handler p)填充其值(可能会有系统默认值);当new无法分配出足够的空间时,系统就会在抛出异常之前先调用这个函数;
  • 通常情况下,拥有以下几种行为的new-handler是更好的:
    • ①可以使得下一次调用new时有更大概率成功;这可以通过预先分配一块大内存,随后每次调用new-handler时归还部分内存;
    • ②安装其他new-handler和卸载本地的new-handler:各个class有可能会定义自己的new-handler,因此最好的做法是new不同的class的时候,调用各自不同的new-handler,并在调用完毕后将new-handler进行恢复;
    • ③抛出std::bad_alloc或者直接退出exit()std::abort()
  • 如何实现方式②?答:自定义operator new以及使用基于CRTP(curiously recursive template pattern)的模板技术
    • 为一个需要设置new-handler的class自定义一个operator new和set_new_handler,而在operator new内部的流程就是:先调用std::set_new_handler设置自己的new-handler,随后调用系统的new,再之后就是恢复new-handler到系统原本的值了;
    • 由于设置恢复完全适配于一个RAII,因此更优秀的做法便是再设置一个资源管理类,在构造函数内保存之前的new-handler,并在析构函数内恢复之前的new-handler;
    • 接下来就是考虑这样一个问题了,如果不同的class都需要自定义new-handler的话,而又由于自定义new-handler其实是一套完全一致的流程,除了各自的new-handler不一样;因此CRTP就派上用场了,以下代码就是完整的实例。
class HandleHolder{
public:
    HandleHolder(const HandleHoldr &) = delete; // 禁止拷贝
    HandleHolder &operator=(const HandleHolder &) = delete;

    HandleHolder(std::new_handler p): oldHandler(p) {}
    ~HandleHolder(){std::set_new_handler(oldHandler);}
private:
    std::new_handler oldHandler;
};

template <typename T>
class NewHandlerHelper{ // 此处没有定义自己的set_new_handler了,感觉没有必要
public:
    NewhandlerHelper(std::new_handler p): myHandler(p) {}
    static void *operator new(size_t size) throw(std::bad_alloc){ // 每个class对应一个operator new
        HandleHolder tmp(std::set_new_handler(myHandler)); // std::set_new_handler会返回之前的new-handler
        return ::operator new(size);
        // tmp被析构,new-handler也就得以恢复
    }

private:
    static std::new_handler myHandler; // 每个class对应一个new_handler
};

template <typename T>
std::new_handler NewHandlerHelper<T>::myHandler = nullptr; // static变量要记得初始化

class Widget : public NewHandlerHelper<Widget> { // 自己继承自己,虽然看起来很奇怪,但实际上是行得通的;本质上只是让不同的class拥有不同的myHandler
    /**
     * ...
     * Widge只要在构造函数处给NewHandlerHelper提供自己的new-handler即可
     * ...
     */
};

条款52 写了placement new也要写placement delete

  • 当代码中使用new表达式之后,发生了两件事情:
    • ①调用void *operator new(size_t size)来获取一块原始内存(raw memory);
    • ②调用class的ctor以构造对应的对象
  • 因为有两个步骤的存在,因此,如果在第2个阶段发生了异常,就有可能产生内存泄漏;
  • 为了避免可能的内存泄漏,当发生上述情况时,由系统来负责回收对应的内存;
  • 这就引出了一个问题,系统如何知道应该调用哪一个版本的delete呢?系统的原则是,使用和operator new参数列表一致的operator delete
  • 因此就有了本条条款的原则:定义了一个placement new,就需要定义对应的placement delete;所谓的placement就是参数列表除了size_t以外还包括其他的参数;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 让自己习惯C++ 条款01:视C++为一个语言联邦 C++可视为: C:以C为基础。 面向对象的C++:添加面向对...
    Cloudox_阅读 3,041评论 0 3
  • 这本书属于“想提高必看之书”,相见恨晚,建议所有C++程序员都看看,没事也可以拿出来翻翻。大家也可以浏览下面的笔记...
    拉普拉斯妖kk阅读 707评论 0 1
  • 20190228暂停学习,后续继续 第一部分让自己习惯C++ 条款1:视C++为一个语言联邦 C++最初的名称C ...
    去年匆匆今年匆匆阅读 388评论 0 1
  • 第一章 从 C 到 C++ 条款1 C++是语言联邦这条意思是C++支持面向过程、面向对象、泛型编程、函数编程、元...
    linanwx阅读 282评论 0 0
  • 条款1:视C++为一个语言联邦 1. C 用于C的基本特性。 2. 面向对象 封装、继承、多态、动态绑定、虚函数表...
    小张同学_loveZY阅读 169评论 0 0