首页 > 代码库 > C++必知必会(1)

C++必知必会(1)

条款1数据抽象

抽象数据类型的用途在于将变成语言扩展到一个特定的问题领域。一般对抽象数据类型的定义需要准训以下步骤:

1.     为类型取一个描述性的名字

2.     列出类型所能执行的操作

3.     为类型设计接口

4.     实现类型

条款2多态

多态类型,从基类继承的最重要的多系就是它们的接口,而不是它们的实现。

条款3设计模式

条款4 STL

STL优秀思想体现在:容器与在容器上执行的算法之间无需彼此了解,这种戏法是通过迭代器实现的。STL容器和算法分别通过类模板和模板函数实现。

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

引用必须被初始化,一个引用即使在该引用被初始化之前已经存在的一个对象的别名。

一个指向非常量的引用是不可以用字面值或临时值进行初始化的。而一个指向常量的引用就可以。当一个指向常量的引用采用一个字面值来初始化时,该引用实际上被设置为指向采用该字面值初始化的一个临时位置。

 

引用和指针的区别:

1.不存在空引用

2.所有引用都需要初始化

3.一个引用永远指向用来对它初始化的那个对象

条款6数组形参

其实C/C++中根本不存在所谓的数组形参,因为数组在传入时,实质之传入指向其首元素的指针。数组被作为性参数会丢失边界。

void average(intary[]); 与 void average(int ary[12]);作用一样。

 

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

void average(int(&art)[12]);   //现在函数只能接受大小为12的整形数组

模板有助于代码的泛华:

template<intn>

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

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

void average(intary[], int size);

当然我们可以将这两种方法结合起来

template<intn>

inline voidaverage(int (&ary)[n])

{

average(int ary, n);

}

为保证数组边界不丢失,数组的大小必须以形参的方式显示的编码,并以单独的实参传入或者在数组内部以一个结束符值作为指示(例如用于指示“用作字符串的字符数组”的末尾’\0’)。不管数组是如何声明的,一个数组通常是通过指向其收元素的指针进行操作的。

 

由于这些原因,经常采用某种容器(vector或string)来替换数组的大多数传统用法。

多维数组是数组的数组,因此形参是一个指向数组的指针。

void process(int(*ary)[20]);    //一个指针,指向一个具有20个int元素的数组

注意第二个边界没有退化,否则无法对形参执行指针算术。

对于多维数组形参的有效处理如下:

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

{

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

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

                     {

                            a[i*m+j] = 0;              //手工计算索引

}

}

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

templat<intn, int m>

inline voidprocess(int (&ary)[n][m])

       {

process(&arr[0][0], n, m);

}

条款7常量指针和指向常量的指针

const T* pct = pt;              //一个指向const T的指针

T* const cpt = pt;              //一个const指针,指向T

可以将一个指向非常量的指针转换为一个指向常量的指针。但不能将指向常量的指针转化为指向非常量的指针。

条款8指向指针的指针

有两种情况会看到指向指针的指针:

1.     声明指针数组时,由于数组名会退化为指向其首元素的指针,所以指针数组的名字也是一个指向指针的指针。

2.     当一个函数需要改变传递给它的指针的值时。

通常,C++中秀安使用指向指针的引用作为函数的参数,而不是指向指针的指针作为参数。

 

一个常见的误解是:适用于指针的转化同样适用于指向指针的指针。事实并非如此,如下:

Circle* c = new Circle;

Shape* s = c;              //正确

 

Circle **cc = new Circle;

Shape** ss = cc;  //错误

因为Circle是一个Shape,因而一个指向Circle的指针也是一个Shape指针。然后,一个指向Circle指针的指针并不是一个指向Shape指针的指针。

当设计const同样会发生混淆。我们知道将一个指向非常量的指针转化为一个指向常量的指针是合法的,但不可以将一个指向“指向非常量的指针”的指针转化为一个“指向常量的指针”的指针。

char*  s1 = 0;

const char* s2 =s1;   //没问题

char* a[MAX];            //也就是char**

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

条款9新式转型操作符

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

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

reinterpret_cast从位的角度来对待一个对象,从而允许将一个东西看做另一个完全不同的东西。

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

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

在类X的非常量函数中,this指针的类型为X*const。也就是说,它指向非常量X的常量指针。由于this指向的对象不是常量,因此它可以被修改。而在类X的常量成员函数中,this的类型为const X* const。也就是说,是指向常量X的常量指针。由于指向的对象是常量,因此它不能被修改。

       类的非静态数据成员可以说声明为mutabl,这将允许它们的值可以被该类的常量成员函数修改,从而允许一个逻辑上为常量的成员函数被声明为常量,虽然其实现需要修改该对象。

       一个成员函数的常量版本和非常量版本可以重载。常量对象调用常量版本。

条款11编译器会在类中放东西

如果类声明了一个或多个虚函数,编译器会为该类插入一个指向虚函数表的自指针。有时使用了虚拟继承,即便类没有虚函数,其中还是有可能被插入一个虚函数表指针。

因为跨平台的内存布局不同的原因,不要使用memcpy这样的标准内存块复制函数,而应该使用对象的初始化或赋值操作。

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

赋值发生于赋值时,除此之外,所遇到其他的复制情形均为初始化,包括声明、函数返回、参数传递以及捕获异常中的初始化。

初始化操纵:先分配内存用于容乃字符串的复制,然后执行复制操作

赋值有一点像析构动作后跟一个构造动作。

条款13复制操作

复制构造函数和赋值函数总是成对的声明。

复制构造函数被声明为X (const X &),而复制赋值操作应被声明为&operat = (const X &)。

条款14函数指针

可以声明一个指向特定类型函数的指针:

void (*fp)(int);     //指向函数的指针

它表示fp是一个指向返回值为void的函数的指针。就像指向数据的指针一样,指向函数的指针也可以是空,否则它就应该指向一个具有适当类型的函数。

extern void h(int);

fp = h;    //正确

fp = &h; //正确

将一个函数的地址初始化或赋值给一个函数的指针时,无需显式地取得函数地址,编译器知道隐式地获取函数的地址,因此在这种情况下&操作符时可有可无的。

类似地,为了调用函数指针所指向的函数而对指针进行解引用操作也是不必要的,因为编译器可以帮你解引用。如下:

(*fp)(12);      //显示解引用

fp(12);          //隐式地解引用,结果相同

和void*指针可以指向任何类型的数据不同,不存在可以指向任何类型函数的通用指针。还要注意,非静态成员函数的地址不是一个指针,因此不可以将一个函数指针指向一个非静态成员函数。

函数指针的一个传统用途是实现回调。一个回调就是一个可能的动作,这个动作在初始化阶段设置,以便对将来可能发生的事件做出反应时而被调用。

一个函数指针指向内联函数时合法的,然而通过函数指针调用内联函数将不会导致内联式的函数调用(也就是不会将代码展开),因为编译器通常无法在编译器精确的确定会调用什么函数。因此在调用点,编译器别无他法,只好生成间接、非内联的函数调用代码。

条款15指向类成员的指针并非指针

一个常规指针包含一个地址。如果解引用,就会得到位于该地址的对象。

与常规指针不同,一个指向成员的指针并不指向具体的内存位置。它指向的是一个类的特定成员,而不是指向一个特定对象里的特定成员。通常最清晰的做法是将指向数据成员的指针看做是一个偏移量。但C++标准没有这样规定,只是大多数编译器都将指向数据成员的指针实现为一个整数,其中包含被指向的成员的偏移量,另外加上1(加1是为了让值0可以表示一个空的数据成员指针。)这个偏移量告诉你,一个特定成员的位置距离对象的起点有多少字节。

一般来说,在C++中存在从指向派生类的指针到指向任何公有基类的预定义转换。在指向类成员的指针的情况下恰恰相反:存在从指向基类成员的指针到指向公有派生类成员的指针的隐式转换,但不存在从指向派生类成员的指针到指向其任何一个基类成员的指针的转换。这个逆变性看起来有违直觉,不过,如果回忆指向数据成员的指针并非一个对象的指针,而是对象内的一个偏移量,就会明白了。

class Shape{

       Point center_;

};

class Circle : public{

       double radius;    

};

因为一个Circle也是一个Shape,所以一个Circle对象内包含一个Shape子对象。因而,Shape内的任何偏移量在Circle内也是一个有效的偏移量。

PointCircle::*loc = &Shape::center_;    //ok,从基类到派生类的转换

然而,一个Shape未必是一个Circle,因此一个Circle的成员的偏移量在Shape内未必是一个有效的偏移量。

doubleShape::*extent = &Circle::radius_;   //错误!从派生类到基类的转换

 

16指向成员函数的指针并非指针

和指向常规函数的指针不同,指向成员函数的指针可以指向一个常量成员函数。

和指向数据成员的指针的情形一样,为了对一个指向成员函数的桌子很进行解引用,需要一个对象或一个指向对象的指针。对于指向数据成员的指针的情形,为了访问该成员,需要将对象的地址和成员的偏移量相加。对于指向成员函数的指针的情形,需要将地相的地址用作this指针的值,进行函数调用,以及其他用途。

和数据成员的指针一样,指向成员函数的指针也表现出一种逆变性,即存在从指向基类成员函数的指针到指向派生类成员函数指针的预定义转换,反之则不然。

class B{

       public:

              void bset(int val){ bval_ = val;}

       private:

              int bval_;

};

class D : publicB{

              public:

                     voiddset(int val){ dval _ = val;}

private:

       int dval_;

};

 

B b;

D d;

void (B::*f1)(int)= &D::dset;    //错误!不存在这种反向逆转

(b.*f1)(12);          //哎呀!访问不存在的dval成员!

void (D::*f2)(int)= &B::bset;    //OK,存在这种转换

(d.*f2)(11);          //OK,设置继承来的bval_数据成员