• 《C++必知必会》学习笔记


    转载:http://dsqiu.iteye.com/blog/1734640

    条款一 数据抽象

    抽象数据设计遵循步骤:(1)为类型取一个描述性的名字。(2)列出类型所能执行的操作,不要忘了初始化(构造函数),清理(析构函数),复制(复制操作)以及转换(不带explicit关键字修饰的但参数构造函数和转换操作符),而不是为数据成员提供一串get/set操作。(3)为类型设计接口。(4)实现类型,对抽象数据类型实现的改动,远比对其接口的改动要来得频繁。

    条款二 多态

    按照C++标准所言,“多态类型”就是带有虚函数的类类型。从设计角度来看,“多态对象”就是一个具有不止一种类型的对象,而“多态基类”则是一个为满足多态对象的使用需求而设计的基类。从基类基础的最重要的东西就是它们的接口,而不是它们的实现,仅仅由接口组成的基类是很少见的。

    条款三 设计模式

    从实践的角度来看,设计模式有两个重要的属性。首先,它们描述了经过验证的、成功的设计技术,这些技术可以按上下文相关的方式进行定制,一遍满足新的设计场合的要求。其次,并且可能更重要的是,在提及某个特定模式的应用时不仅包括其中用到的技术,还包括应用该模式的动因以及应用后所达到的效果。

    设计模式包括4个不可缺少的部分:

    首先,设计模式必须具有一个毫无歧义的名字。使用精确的模式名字比使用不那么精确的名字具有更明确的优势。

    其次,对模式描述时必须定义该模式所能解决的问题

    再次,对模式描述时要记述该问题的解决方案。

    最后,对模式描述时要记述将该模式应用于某个上下文的后果。

    条款四 STL

    STL三大组件:容器、算法和迭代器。STL约定并未指明具体的实现细节,但对实现指定了效率方面的约束。此外,由于STL是一个模板库,许多优化和性能调整可以在编译期进行。容器和函数对象必须通过一套标准的嵌套类型名字对其自身进行描述。容器和函数对象适配器均要求成员函数具有特定的名字并包含特定的类型信息。使用STL可以使代码变得更清晰,更易于维护,当然还有高效。

    条款五 引用时别名而非指针

    引用和指针存在三大区别:其一:不存在空引用;其二,所有引用都要初始化;其三,一个引用永远指向用来对它初始化的那个对象。

    一个指向非常量的引用时不可以用字面值或者临时值进行初始化的:

    double &d=12.3;//错误!

    swap(std::string("Hello"),std::string(", world"));//错误

    const double &cd=12.3;//OK!

    当一个指向常量的引用采用一个字面值来初始化时,该引用实际上被设置为指向“采用该字面值初始化”的一个临时位置。cd并不是直接指向字面值12.3,而是指向一个采用12.3初始化、类型为double的临时值。此外,一般说来,临时对象在创建他们的表达式的末尾被销毁(确切地说,离开作用域),然而,当这类临时对象用于初始化一个指向常量的引用时,在用于指向它们期间,这些临时对象会一直存在。

    条款六 数组形参

    数组形参是一个容易出错的地方。数组在传入时,实质上只传入指向其元素的指针。这就是“退化”,即数组退化成指向其首元素的指针。顺便提下,同样的事情也发生在函数上。一个函数类型参数会退化成一个指针,不过和数组在退化是会丢失边界这点不同,一个退化的函数具有良好的感知能力,可以保存其参数类型和返回值类型。

    由于在数组形参中数组边界被忽略了,因此通常在声明时最好将其省略。void average(int array[]);

    另一方面,如果数组边界的精确数值非常重要,并且希望函数只接受含有特定数量的元素的数组,可以考虑使用一个引用形参:

    void average(int (&array)[12]);

    现在函数智能接受大小为12的整型数组:

    int anArray[]={1,2,3};

    average(anArray);//错误,anArray是一个int[3];

    模板有助于代码的泛化:

    template<int n>

    void average(int (&arr)[n]};//让编译器帮我们推导n的值

    不过,更为传统的做法是将数组的大小明确地传入函数。

    void average_n(int arry[],int size);

    当然,可以将这两种方式结合起来;

    template<int n>

    inline void average(int (&arry)[n])

    {

    average_n(n);

    }

    从上面的讨论,应该可以清晰的获知,使用数组作为函数参数最大的问题在于,数组的大小必须以形参的方式显式编码,并以单独实参传入,或在数组内部以一个结束符值作为指示(例如用于指示“用作字符串的字符数组”只末尾的‘o’)。另一困难在于,不管数组时如何声明的,一个数组通常是通过指向其首元素的指针进行操作的。如果那个指针作为实参被传递给函数,我们前面声明引用形参的技巧将无济于事。

    int *an=new int[anArraySize];

    average(an);//错误,不可以使用int *初始化int(&)[n];

    average_n(an,anArraySize);

    由于这些原因,经常采用容器来代替数组的大多数传统的用法,并且通常应该优先考虑使用标准容器。

    从本质上说,多维数组形参并不比一维数组困难,但他们看上去更具挑战性:

    void process(int arr[10][20]);

    和一维数组的情形一样,形参不是一个数组,而是一个指向数组首元素的指针。不过,多维数组是数组的数组,因此形参是一个指向数组的指针。第二个(以及后续)的边界没有退化,否则将无法对形参执行算术。

    程序员可以取代编译器来执行索引计算:

    void process( int *a,int m,int n)

    {

    for(int i=0;i<m;i++)

    for(int j=0;j<n;j++)

    }

    同样,有时模板有助于事情更加干净利落:

    template<int n,int m>

    void process_n(int (&arr)[n][m])

    {

    process(a,n,m);

    }

    条款七 常量指针与指针常量的指针

    常量指针和指向常量的主要区分是看const修饰符修饰的指针修饰符*还是基础类型T:

    const T * p1;            T const *p2  //p1,p2都是指向常量T的指针

    T * const p3;//一个const指针

    使用一个引用通常比一个常量指针更简单:

    const T &rct= *pt;//而不是 const T * const

    T &rt=*pt;//而不是 T *const

    可以将一个指向非常常量的指针转换为一个指向常量的指针。相反的转换是非法的。

    const T act;

    const T *pct=&act;

    T *pt=pct;//报错

    *pt=at;//试图修改常量对象

    C++标准告诉我们,这样的赋值会产生未定义的结果,也就是说,不知道究竟会发生什么,不过可以肯定的是,不会发生什么好事。当然,可以利用const_cast显式执行类型转换。

    pt=const_cast<T *>(pct);

    *pt=at;

    条款八 指向指针的指针

    多级指针有通常有两种应用,第一种情形是声明一个指针数组。当然,多级指针的最常见应用情形,当一个函数需要改变传递给它的指针的值时。考虑如下函数,它将一个指针移动到指向字符串中的下一个字符:

    void scanTo(const char ** p, char c){

    while(**p && **p!=c)

    ++*p;

    }

    传递给scanTo的第一个参数是一个指向指针的指针,这意味着我们必须传递指针的地址:

    char s[]="Hello,World!";

    const char *cp=s;

    scanTo(&cp,',');

    在C++中,几乎总是首选使用指向指针的引用作为函数参数,而不是指向指针的指针:

    void scanTo(const char * &cp,char);

    一个误解:适用于指针的转换同样适用于指向指针的指针。事实并非如此。例如,

    Circle * c=new Circle; Shape * s=c;// Shape是Circle的基类

    但是

    Circle ** cc=new Circle;

    Shape **ss=cc;//错误!

    当涉及const时也会发生同样的混淆。例如,

    char *s1=0;     const char *s2=s1;//没错

    char *a[MAX];

    const char ** ps=a;//错误!

    条款九 新式转型操作符

    有四个新式转型操作符,每一个都有着特定的用途。

    const_cast操作符允许添加或移除表达式中的类型的const或volatile修饰符:

    const Person *getEmployee(){};

    Person *anEmployee=const_cast<Persion *>(getEmployee());

    上述代码就是使用const_cast来剥除getEmployee()返回类型是const修饰符。可以使用旧式转型获得同样的效果:

    anEmployee=(Person*) getEmployee();

    但使用const_cast的做法更好,因为它更丑陋,更难用,并且威力较小。

    static_cast操作符用于相对而言可跨平台移植的转型。最常见的情况是,它用于将一个继承层次结构中的基类的指针或引用,向下转型为一个派生类的指针或者引用。

    Shape *s=new Circle;

    Circle *c=static_cast<Circle *>(sp);

    如果sp指向其他类型的Shape,那么当使用cp时,很可能会得到运行期错误。

    注意,static_cast无法像const_cast那样改变类型修饰符。

    Circle *c=static_cast<Circle *>(const_cast<Shape*>(getNextShape()));

    标准并没有对reinterpret_cast的行为提供太多的保证,不过它通常的行为可以从它的名字看出来。它从位的角度来看待一个对象,从而允许将一个东西看做另一个完全不同的东西。

    这类东西在底层编码里偶尔非用不可,但它可能不具有可移植性。reinterpret_cast和static_cast将指向基类的指针向下转型为指向派生类的指针时的行为。reinterpret_cast通常只是将基类指针假装成一个派生类指针而不改变值,而static_cast则执行正确的地址操作。

    dynamic_cast通常用于执行从指向基类的指针安全地向下转型为指向派生类的指针。不同于static_cast的是,dynamic_cast仅用于对多态类型进行向下类型(也就说,被转型的表达式的类型,必须是一个指向带有虚函数的类类型的指针),并且执行运行期检查工作,来判断转型的正确性。dynamic_cast付出显著的运行期开销(static_cast通常无需付出运行期代价)。

    条款十 常量成员函数的含义

    在类X的非常量成员函数中,this指针的类型为X *const。而在类X的常量成员函数中,this的类型为const X *const。

    要明确主要常量成员函数的含义所在:

    class X {

    public :

    void modifyBuffer(int index,int value)const {

    buffer[index]=value;

    }

    private:

    int *buffer;

    }

    上述的modifyBuffer没有问题,因为它没有修改X对象,它只是修改X的buffer成员所指向的一些数据。但这种做法显然是“不道德的”。

    如果有时在常量成员函数必须要修改其对象,可以通过

    X *const aThis=const_cast<X *const>(this);  //糟糕念头,来实现,但还有更好的做法——将需要修改的类的非静态数据成员声明为mutable。

    还应注意:

    X operator +(const X &rightArg);  //左边参数是非常量

    X operator +(const X &rightArg) const;  //左边参数是常量,当然对于非常量可以转换为常量

    条款十一 编译器在类中放东西

    如果一个类声明了一个或者多个虚函数,那么编译器将会为该类的每一个对象插入一个指向虚函数表的指针,如果使用了虚继承,对象将会嵌入的指针、嵌入的偏移或其他非嵌入信息来保持对其虚基类子对象位置的跟踪。

    所以不要对类的内部结构做低级的假定。

    条款十二 赋值和初始化并不相同

    赋值和初始化时不同的操作,直截了当地说,赋值发生于赋值(左值已经被初始化)时,除此之外,遇到所有其他的复制情况均为初始化,包括声明、函数返回、参数传递、以及捕获异常的初始化。

    条款十三 复制操作

    复制构造(copy construction)和复制赋值(copy assignment)是两种不同的操作。

    条款十四 函数指针

    声明一个特定类型函数的指针:void (*fp)(int);

    函数指针的一个传统用途是实现回调(callback)。

    条款十五 指向类成员的指针并非指针

    与常规指针(包含地址)不同,一个指向成员的指针并不只想一个具体的内存位置,通常最清晰的做法是将数据成员的指针看作为一个偏移量。

    class C {

    public :

    int a;

    };

    int C::*pimC;

    C aC;

    C *pC =&aC;

    pimC=&C::a;

    aC.*pimC=0;

    int b= pc->*pimC;

    将pimC 的值设为&C::a时,实际上是将pimC设置为a在C内的偏移量,除非a是静态成员,否则在表达式&C::a中使用&不会带来一个地址,而是一个偏移量。为了访问位于哪个偏移量的数据,需要改了一个对象的地址。

    条款十六 指向类成员函数的指针并非指针

    指向类成员函数的指针跟指向数据成员的指针类似,只是在解引用的时候->*或.*必须加上括号,因为它们比()操作符优先级低。

    条款十七 处理函数和数组声明

    指向函数的指针声明和指向数组的指针声明很容易混淆,主要原因在于函数和数组修饰符优先级比指针修饰符的优先级高,因此通常需要使用圆括号。

    int *fp1();//一个返回值为int *的函数

    int (*fp1)();//一个指针,指向返回值为int的函数

    const int N=12;

    int *a1[N];//一个具有N个int*元素的数组,可以看做是 int* a1[N]

    int (*a1)[N];//一个指针,指向一个具有N个int元素的数组,(*a1)是对象类型不是指针

    理所当然,还有:

    int (**a2)[N];//一个指针,它指向一个指针,后者则指向一个具有N个int元素的数组

    int *(*a2)[N];//一个指针,指向一个具有N个int *的元素的数组

    int *(*fp3)();//一个指针,指向一个返回值为int *的函数

    巧记,一个* 表示 指针,两个 * 表示 这个指针,指向到一个指针,指针和[N]之间没有括号表示这是一个N个指针的数组,否则

    使用typedef可以简化复杂的声明语法。

    条款十八 函数对象

    函数对象类型则是重载函数调用操作符(),来创建类似于函数指针的东西。函数对象只是常规的类对象,但是可以采用标准的函数调用语法来调用它的operator()成员(此成员可以有多个重载版本)。

    class Fib{

    public :

    int operator()();

    private:

    int a0,a1;

    };

    int Fib::operator()(){

    int temp=a0;

    a0=a1; a1=temp+a0;

    retrun temp;

    }

    Fib fib;

    cout<<"next two in series"<<fib()<<' '<<fib()<<endl;

    fib()语法被编译器识别为对fib对象的operator()成员函数的调用,与fib.operator()等价。使用函数对象而不使用函数或函数指针优势在于,用于计算fibonacci数列下一个值的状态被存储于Fib对象自身之中,如果采用函数来实现计算功能,那么必须求助于全局或局部静态变量或其他一些“卑鄙”的花招,以便在函数调用之间保持状态,或者将状态信息明确地传递给函数。还有就是有别于静态数据的函数,可以拥有多个同时计算的Fib对象,这些对象的计算过程和结果不会相互干扰。

    条款十九 Command模式与好莱坞法则

    当一个函数对象作为回调时,就是一个Command模式的实例。使用函数对象代替函数指针的好处有三个:函数对象可以封装数据;函数对象可以通过虚拟成员表现出动态行为;可以处理层次结构而不用去处理较为原始的缺乏灵活性的结构(如函数指针)。

    条款二十 STL函数对象

    条款二十一 重载和重写并不相同

    重载和重写彼此之间没有关系。

    当同一个作用域内的两个或更多个函数名字相同但签名不同时,就会发生重载。函数签名由它声明的参数的数目和类型构成。

    当派生类和基类虚函数具有相同的名字和签名时,就会发生重写。一般满足对派生对象的虚拟调用。

    条款二十二 Template Method模式

    条款二十三 名字空间

    全局作用域日益变得拥挤不堪,那就使用名字空间吧。

    条款二十四 成员函数查找

    调用一个成员函数时包括三个步骤:第一步,编译器查找函数的名字;第二步,从可用的候选者中选择最佳匹配函数;第三步,检查是否具有访问该匹配函数的权限。

    一旦在内层作用域中找到匹配的,编译器就不会到外层作用域中继续查找了,即使在外层中有匹配的名字。

    条款二十五 实参相依查找

    当查找一个函数调用表达式中的函数名字时,编译器会到“包含函数调用的实参的类型的名字空间中查找。

    namespace somename 

    {

      class X { ... };

      void f(const X &);

      void g(X *);

      X operator+ (const X&, const X&);

      class String { ... };

      std::ostream operator <<(std::ostream &, const String &);

    }

    int g(somename::X *);

    void aFunc()

    {

      somename::X a;

      f(a);        // OK 调用 somename::f()

      g(&a);       // error 歧义,由于实参的类型使得somename的g也成为候选函数

      a = a + a;   // OK 调用 somename::operator+

    }

    即使g调用导致了两个候选函数参与了重载解析,::g实际上也并未重载 somename::g(),因为他们不是声明于同一个作用域中。 ADL是关于函数如何被调用的一个属性,而重载则是关于函数被如何声明的一个属性。

    somename::String name("test");

    std::out << "Hello," << name;

    第一个 operator << 极有可能是 std::basic_ostream 的一个成员函数,而第二个则是somename::operator << 的非成员函数调用。

    条款二十六 操作符函数的查找

    重载操作符后,有两种调用的方法,一种是只用到操作符,另外一种是和普通成员函数一样调用,然而两种方法的查找规则是不同的。如果使用函数调用语法,应用的是普通的查找规则,而直接使用操作符的机制则不相同。对于中缀操作符调用来说,编译器不仅会考虑成员函数,还会考虑非成员操作符,其实是一个退化形式的ADL,即当确定将哪些函数纳入重载解析考虑范围时,中缀操作符中的左参数的类的作用域和全局作用域都被考虑在内。

    成员操作符和非成员操作符并非重载,而是编译器在两个不同的地方查找候选函数。

    条款二十七 能力查询

    在大多数情况下,当一个对象出现并开始工作时,它就能够执行我们需要它做的事情,因为它的能力在其接口中已被明确的通告。

    不幸的是,我们偶尔会碰到不知道一个对象是否具有所需能力的情形。在这种情形下,我们被迫执行一个能力查询。在C++中,能力查询通常是通过对“不相关”的类型进行dynamic_cast转换而表达的。

    这种dynameic_cast用法通常称为“横向转型”,因为它试图在一个类层次结构中执行横向转换,而不是向上或向下转换。看转换是否成功。

    Shape *s=getSomeShape();

    Rollable *r=dynamic_cast<Rollable *>(s);

    如果Shape和Rollable没有共同的父类(或Shape是Rollable的子类),那么dynamic_cast就会失败返回一个空指针。

    除非找不到其他合理的方式的困难境地,最好避免对一个对象的能力进行运行期查询。

    条款二十八 指针比较的含义

    在C++中,一个对象可以有多个有效的地址,因此,指针比较不是关心地址的问题,而是关于对象同一性的问题。

    指向基类的指针可以用来指向一个派生类的对象,那么基类指针就可以和派生类的指针进行比较,这时虽然地址不同,但是所指向的对象是相同的。上面两个指针比较的时候,编译器通过将参与比较的指针值之一调整一定的偏移量来完成。

    一般而言,当我们处理指向对象的指针或引用的时候,必须小心避免丢失类型信息。一旦通过将其复制到void *,从而去掉对象中包含的地址的类型信息,编译器就别无他法,只好求助于原始地址的比较了,而这样的比较对于指向对象的指针来说,很少是正确的。

    条款二十九 虚构造函数与Prototype模式

    条款三十 Factory Mehtod模式

    条款三十一 协变返回类型

    一般来说,一个重写(多态实现)函数与被它重写的函数必须具有相同的返回类型。

    如果B是一个类类型,并且一个基类虚函数返回B*(B &),那么一个重写的派生类函数返回可以是D*(D&),其中D公有派生于B,这就是传说中的“协变返回类型”。

    条款三十二 禁止复制

    访问修饰符(包括private、protected和public)可以用于表达和执行高效的约束技术,指明一个类可以被怎样使用。

    将拷贝构造函数和拷贝赋值操作符声明为private是必不可少的,否则编译器会偷偷地将它们声明为公有、内联的成员。

    条款三十三 制造抽象基类

    抽象基类通常用于表示目标问题领域的抽象概念,创建这种类型的对象是没有什么意义的。

    我们至少声明一个纯虚函数使得一个基类成为抽象的,编译器将会确保无人能够创建该抽象基类的任何对象。

    然而有时候找不到一个可以成为纯虚函数的合理候选者,但仍然希望类的行为像个抽象基类。在这些情形下,可以通过确保类中不存在公有构造函数来模拟,把构造函数和拷贝构造函数都声明为受保护的,允许派生类的构造函数调用,又阻止创建独立的对象。

    另一种使一个类成为抽象基类的方式需要人为地将该类的一个虚函数指定为纯虚的。通常来说,析构函数是最佳候选者(不能调用析构函数,故报错)。

    当一个类没有任何虚函数并且不需要显式声明构造函数时,适合用第三种方式。在这种情形下,采用受保护的、非虚拟的析构函数,乃是最佳的实现方式。

    条款三十四 禁止或强制使用堆分配

    指明对象不应该被分配到堆(堆分配的对象必须被显式地销毁,具有自动存储区的类的局部对象和具有静态存储区的类的对象其析构函数都会自动调用)上的方式之一,是将其堆内存分配定义为不合法。把operator new和delete声明为受保护的。防止类的构造函数和析构函数的隐式的调用,又可以让派生类的构造函数和析构函数隐式的调用。还要注意阻止在堆上分配对象的数组,只要将array new和array delete声明为private并且不定义就可以。

    当然,在某些场合下,我们可能鼓励使用堆分配,为此,只要把析构函数声明为private即可,这时还要声明一个公有的销毁方法,否则创建的对象将无法销毁。

    条款三十五 placement new

    直接调用构造函数是行不通的,然而,可以通过使用placement new“哄骗”编译器调用构造函数。

    placement new是operator new的一个标准重载版本,也位于全局名字空间中,但是和我们通常所看到的operator new不同,语言命令禁止用户替换placement new,而“普通的”operator new和delete则可以被替换掉,只不过我们不应该那样做而已。

    placement new允许我们在一个特定的位置“放置对象”,起到了调用一个构造函数的效果。

    区分new操作符和命名为operator new的函数很重要,new操作符不可以被重载,所以其行为总是一样的。

    placement new是函数operator new的一个版本,它并不实际分配任何存储区,仅仅返回一个指向已经分配好空间的指针。因为没有分配空间,所以不用delete,但对象是创建了的,所以要显式销毁对象,调用析构函数。

    条款三十六 特定于类的内存管理

    我们无法对new操作符和delete操作符做什么,因为它们的行为是固定的,但是可以改变它们所调用的operator new和operator delete。

    实现方式就是在类中定义operator new和operator delete成员函数(最好同时定义两个函数),在new操作符调用时,编译器首先会在类所在的作用域中查找operator new,如果找不到,那么将会在全局作用域中去查找。

    如果在基类中定义了成员operator new 和operator delete,要确保基类的析构函数是虚拟的。如果基类的析构函数不是虚拟的,那么通过一个基类指针来删除一个派生类的对象结果是未定义的。

    成员operator new和operator delete是静态成员函数,因为这两个函数仅仅负责获取和释放对象的存储区,因此它们用不到this指针。

    一个常见的误解是以为使用new和delete操作符就意味着使用堆内存,其实并非如此。标准的operator new和delete使用的是堆,但是用户自定义的可以做任何事情。

    条款三十七 数组分配

    条款三十八 异常安全公里

    公理1:异常是同步的

    异常是同步的并且只能发生在函数调用的边界。诸如预定义类型的算术操作、预定义类型(尤其是指针)的赋值以及其他底层操作不会导致异常的发生。

    操作符重载和模板使得情形变得复杂化了,因为通常很难判定一个给定的操作是否会导致一个函数调用并可能抛出异常,因而模板内所有可能的函数调用都必须假定为就是函数调用,这包括中缀操作符、隐式转换等。

    公理2:对象的销毁时安全的

    公理3:交换操作是不会抛出异常

    交换操作在幕后它的使用很广泛,尤其是在STL实现中,所以保证异常安全是很有益处的。

    条款三十九 异常安全的函数

    只要可能,尽量少用try语句块是一个好主意。主要在这种地方使用它们:确实希望检查一个传递的异常的类型,为的是对它作一些处理。

    条款四十 RAII

    RAII是resource acquisition is initialization的缩写,意为“资源获取即初始化”,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在RAII的指导下,C++把底层的资源管理问题提升到了对象生命周期管理的更高层次。

    更多参考可以查看参考①的相关内容。

    条款四十一 new 构造函数和异常

    如果分配时采用了成员operator new ,那么,如果构造函数抛出了一个异常,则在相应的成员operator delete将会被调用,进行存储区的回收工作,这是声明成员operator new那么最好也声明operator delete的一个好理由。

    条款四十二 智能指针

    条款四十三 auto_ptr非同寻常

    条款四十四 指针算术

    条款四十五 模板术语

    template<typename T>//T是一个模板参数

    class Heap{};

    Heap<double> heap;//double是一个模板实参

    模板的特化是指把一套模板实参供应给一个模板时所得到的东西。特化可以显式进行,也可以隐式进行:

    Heap<int>//int实参显式特化Heap类模板

    print(12.3);//使用一个double实参隐式地特化print函数模板

    条款四十六 类模板显式特化

    类模板显式特化很直观,首先需要一个通用模板,也叫主模板:

    template<typename T>

    class Heap{

    public :

    void push(const T &val);

    T pop();

    bool empty() const {return h.empty()};

    private:

    std::vector<T> h;

    };

    这个实现对很多类型的值都是非常有效的,但是在处理指向字符的指针时碰了钉子。解决方法就是提供一个针对指向字符的指针的显式特化化版本:

    template<>

    class Heap<const char *>{

    public :

    void push(const char *val);

    const char * pop();

    bool empty() const {return h.empty()};

    private:

    std::vector<const char *> h;

    };

    Heap<int> h1;//使用主模板

    Heap<char *>h2;//使用主模板

    Heap<const char *>h3;//使用显式特化

    这个类模板显式特化版本其实并不是一个类模板,因为此时没有剩下任何未指定的模板参数,也就完全特化了。

    注意,C++没有显示特化的接口必须和主模板的接口完全匹配。

    条款四十七 模板局部特化

    C++目前还不支持函数模板进行局部特化,所能做的是重载它们。

    template<typename T>

    class Heap<T *>{

    public :

    void push(const T *val);

    T * pop();

    bool empty() const {return h.empty()};

    private:

    std::vector<T *> h;

    };

    Heap<std::string>h1;//主模板

    Heap<int **>h2;//使用局部特化,T是int*

    Heap<int *>h3;//使用局部特化,T是int

    这其实就是局部特化干的事,同样可以对主模板进行const T *的局部特化。

    条款四十八 类模板成员特化

    关于类模板的显式特化和局部特化有一个常见的误解:一个特化的版本会以某种方式从主模板“继承”一些东西。事实并非如此。对于主模板而言,类模板的完全特化或局部特化全是单独的实体,它们不从主模板“继承”任何接口和实现。

    现在我们要做的是只去特化主模板成员函数的一个子集:

    template<>

    void Heap<const char *>::push(const char * const & vap){

    //....

    };

    这些函数的接口必须和进行成员特化的模板的相应接口精确匹配。例如,主模板将push的声明为const T &的参数,因此针对const char *的push的特化实参类型是const char * const &。

    如果这Heap局部特化存在,那么对push的显示特化就必须符合该局部特化中的push成员的接口。

    除了成员函数外,类模板的其他成员可以被显式特化,这包括静态成员和成员模板。

    条款四十九 利用typename消除歧义

    template<typename T>

    class PtrList{

    public:

    typedef T *ElemT;

    };

    typedef PtrList<State> StateList;

    StateList::ElemT cur;

    上述代码中的嵌套名字ElemT很容易被识别为模板认可的元素类型。但是下面的情况就不然:

    template<class Cont>

    void fill(Cont &c,Cont::ElemT a[],int len){};

    这里嵌套的名字Cont::ElemT并没别识别为一个类型名字,问题在于fill上下文,编译器没有足够的信息来决定嵌套名字ElemT到底是一个类型名字,还是一个非类型的名字(如成员)。

    当然,解决方法就是显式告诉编译器这是一个类型名字:

    template<class Cont>

    void fill(Cont &c,typename Cont::ElemT a[],int len){};

    类似地,有

    typename A::B::C::E;

    等于告诉编译器,E是一个类型名字。

    条款五十 成员模板

    一个成员模板就是一个自身是模板的成员:

    template<typename T>

    class SList{

    public :

    template<typename In>SList(In begin);

    };

    template<typename T>

    template<typename In>

    SList<T>::SList(In begin){//....}

    float px[]={};

    SList<float > data(px);

    编译器会根据需要执行实参推导并实例化构造函数模板。任何非虚的成员函数都可以写成模板。

    条款五十一 利用template消除歧义

    在条款“采用typename消除歧义”就是要明确告知编译器某个嵌套的名字是一个类型名字,这样编译器才能正确地进行解析工作。对于嵌套的模板名字要使用template来告知编译器。

    template<class T>

    class AnAlloc{

    public:

    template<class Other>

    class rebind{

    public:

    typedef AnAlloc<other> other;

    };

    };

    typedef AnAlloc<int> AI;

    typedef AI::rebind<double>::other AD;//合法

    仍旧给出编译器不能识别出的例子:

    template<typename T class A=std::allocator<T>>

    class SList{

    struct Node{

    };

    typedef A::rebind<Node>::other NodeAlloc;//语法错误

    };

    而应该使用 typedef typename A::template rebind<Node>::other NodeAlloc;

    条款五十二 针对类型信息的特化

    条款五十三 嵌入类型信息

    条款五十四 traits

    条款五十五 模板的模板参数

    当模板的参数是一个模板应该具有如下形式(第二参数):

    template<typename T,template<typename> class Cont>

    class Stack;

    为了方便可以给模板的模板参数指定一个默认值:

    template<typename T,template<typename> class Cont=Deque>

    Stack <int ,List> aStack;

    上面的情况很容易跟下面的混淆:

    template<class Cont> 

    class Wrapper1;

    Wrapper1需要的是一个类型名字,而不是一个模板,此处class和template是没有区别的。

    Wrapper1<List <int>> w1;//很好,List<int>是一个类型的名字

    Wrapper1<List> w2;//错误!List是一个模板名字

    条款五十六 policy

    条款五十七 模板实参推导

    编译器根据函数调用 的实参类型推导模板实参,当然也可以显式告诉编译器模板实参究竟是什么:

    d=min<double>(12.3,4);

    与重载解决方案一样,编译器在模板实参推导期间只检查函数实参的类型,对可能的返回类型不作检查。

    条款五十八 重载函数模板

    当面临各种不同的模板和非模板候选者时,挑选一个正确的函数是一个复杂的过程。

    条款五十九 SFINAE

    条款六十 泛型算法

    条款六十一 只实例化要用的东西

    在C和C++中,如果没有调用一个已声明的函数(或试图获取其地址),那么就不需要定义它。对于类模板的成员函数来说也大致相同,如果实际并没有调用一个模板的成员函数,那么该成员就不会实例化。

    下面我们定义一个 operator ==的实现:

    template<typename T,int n>

    bool Array<T,n>::operator ==(const Array &c)const{

    return this->a==c->a;

    }

    这里a是class Array的数据成员T  a;

    此时,如果a没有定义operator ==操作,相应的Array对象调用operator ==就会报错。

    条款六十二 包含哨位

    C++头文件基本上是使用预处理技术来防止头文件内容在一个编译单元多次出现:

    #ifndef HDR1_H

    #define HDR1_H

    //头文件内容

    #endif

    这个过程是比较耗时的,在某些情况下,采用冗余的包含哨位可以相当显著地加快编译速度:

    #ifndef HDR1_H

    #include"hdr1.h"

    #endif

    条款六十三 可选关键字

    当重写纯虚函数时,virtual关键字同样是可选的。

    当声明operator new、operator delete、array new以及array delete成员时,static关键字是可选的,因为这些函数是隐式静态的。

    参考:

    ①RAII:http://www.cppblog.com/aaxron/archive/2011/03/22/142475.html

  • 相关阅读:
    Hadoop综合大作业
    hive基本操作与应用
    理解MapReduce计算构架
    熟悉HBase基本操作
    Hadoop综合大作业
    hive基本操作与应用
    理解MapReduce计算构架
    熟悉HBase基本操作
    熟悉常用的HDFS操作
    爬虫大作业
  • 原文地址:https://www.cnblogs.com/lihonglin2016/p/4991098.html
Copyright © 2020-2023  润新知