20个实用的小技巧
这些小技巧之所以特别,是因为这些信息通常吧不能在C++书籍或者网站上找到。比如说,成员指针,即使对于高级程序员也是比较棘手,和易于产生bugs的,是应该尽量避免的问题之一。
What makes these tips special is that the information they provide usually cannot be found in C++ books or Web sites. For example, pointers to members are one of the most evasive, tricky, and bug-prone issues for even advanced users.
====================================
Page 1: Introduction 介绍
接下来的这几条技巧主要集中于实用技术和一些晦涩知识上;它们与特殊的平台、编程领域、或编译器无关。因此,它们适用于所有的C++程序员。本人把这些技巧划分为五大类:编码风格、内存管理、性能提升、面向对象的设计,和标准模板库(STL)五方面的一般准则。
The following tips are a collection of general hands-on techniques and recondite pieces of knowledge not associated with a specific platform, programming domain, or compiler. As such, they can be of use to all C++ programmers. I grouped the tips into five major categories: general guidelines for coding style, memory management, performance enhancement, object-oriented design, and the Standard Template Library (STL).
====================================
First Four: Guidelines for Better Coding Style 较好编程风格所要遵循的一些准则
在这个类别中,所涉及的技巧是各级C++的程序员均会经常提及的问题。举个例子,我很惊讶的发现,有很多具有一定经验的程序员仍旧不知道.h是一种过时的标准头文件标识方式,不会正确的应用名空间,不了解在向临时对象绑定引用时所要遵循的准则。这些问题以及一些其他问题将在这里进行讨论。首先,我们先解释过时的头文件命名符号<xxx.h>与现代的符合标准的<xxx>头文件命名符号之间的区别。接下来,我们探究一些由于编译器限制以及相关的语言规则深奥性质所带来的C++“阴暗角落”;这点往往有许多程序员混淆不清。例如,用逗号分隔的表达式,对右值绑定引用的规则等。最后,我们将学习如何在程序的启动之前调用某个函数。
技巧1:用<iostream.h>还是<iostream>?这不是一个问题!
很多的C++程序员依旧使用<iostream.h>,而非最新的、标准编译生成的<iostream>库。这两个库之间有什么区别呢?首先,针对用.h作为标准头文件的标识符这一问题,五年前就已经不被推荐使用了。在新的代码中再使用这种不被认同的表示方式绝不是一个好主意。在功能方面,<iostream>包括模板化的IO类,它同时支持窄字符和宽字符;而<iostream.h>却只支持以char为导向的流。第三,在C++的iostream接口标准规格在许多微妙的方面发生了变化。所以,<iostream>的接口与实现与<iostream.h>存在着一定得差异。最后,,<iostream>组件声明于std命名空间中,而<iostream.h>组件是全局性的。
因为二者之间存在着这些重大分歧,你不能在同一程序中混合使用两个库。作为一条准则:使用<iostream>代替<iostream.h>,除非你处理了那些只与<iostream.h>兼容的遗留代码。
Tip 1: <iostream.h> or <iostream>?
Many C++ programmers still use <iostream.h> instead of the newer, standard compliant <iostream> library. What are the differences between the two? First, the .h notation of standard header files was deprecated more than five years ago. Using deprecated features in new code is never a good idea. In terms of functionality, <iostream> contains a set of templatized I/O classes which support both narrow and wide characters, as opposed to <iostream.h> which only supports char-oriented streams. Third, the C++ standard specification of iostream's interface was changed in many subtle aspects. Consequently, the interfaces and implementation of <iostream> differ from those of <iostream.h>. Finally, <iostream> components are declared in namespace std whereas <iostream.h> components are global.
Because of these substantial differences, you cannot mix the two libraries in one program. As a rule, use <iostream> unless you're dealing with legacy code that is only compatible with <iostream.h>.
===================================================
===================================================
技巧2:左值的引用,要注意!
左值和右值是C++编程的基本概念。从本质上说,右值是一种不能出现在赋值表达式左侧的表达式。相较而言,左值是一个你可以写入数值的对象或者内存块。引用可以指向左值,也可以是右值。但是,由于对右值的语言限制,所以你必须了解在向右值绑定引用时所要遵循的规则。
只要引用绑定的对象是一个const类型,那么就可以对右值绑定引用。这一规则背后的理由很简单:你不能试图去改变一个右值,常量的引用保证了程序不会通过引用去改变右值。在下面的例子中,函数f()输入一个const int的引用:
void f(const int & i);
int main()
{
f(2); /* OK */
}
程序将右值2作为一个参数传入函数f()。在实时运行中,C++会生成一个类型为int值为2的临时对象,并将其与引用i绑定。临时对象和他的引用存在与函数f()从触发到返回整个过程;函数返回后,他们被立即销毁。注意,如果我们声明引用i时没有使用const标识符,函数f()就可以修改它的参数,从而引起未定义的行为。所以,你只能向常量对象绑定引用。
同样的准则适用于自定义对象类性。只有当临时对象是常量时,你才能绑定引用。
Tip 2: Binding a Reference to an Rvalue
Rvalues and lvalues are a fundamental concept of C++ programming. In essence, an rvalue is an expression that cannot appear on the left-hand side of an assignment expression. By contrast, an lvalue refers to an object (in its wider sense), or a chunk of memory, to which you can write a value. References can be bound to both rvalues and lvalues. However, due to the language's restrictions regarding rvalues, you have to be aware of the restrictions on binding references to rvalues, too.
Binding a reference to an rvalue is allowed as long as the reference is bound to a const type. The rationale behind this rule is straightforward: you can't change an rvalue, and only a reference to const ensures that the program doesn't modify an rvalue through its reference. In the following example, the function f() takes a reference to const int:
void f(const int & i);
int main()
{
f(2); /* OK */
}
The program passes the rvalue 2 as an argument to f(). At runtime, C++ creates a temporary object of type int with the value 2 and binds it to the reference i. The temporary and its reference exist from the moment f() is invoked until it returns; they are destroyed immediately afterwards. Note that had we declared the reference i without the const qualifier, the function f() could have modified its argument, thereby causing undefined behavior. For this reason, you may only bind references to const objects.
===================================================
===================================================
技巧3:奇怪的逗号分割表达式
逗号分隔的表达式是从C继承而来的。你很有可能会在使用for-循环和while-循环的时候经常使用这样的表达式。然而,在这方面的语言规则还远不直观。首先,让我们来看看什么是逗号分隔的表达式:这种表达式可能包含一个或多个用逗号分隔的子表达式。例如:
if(++x, --y, cin.good()) /*three expressions 三个表达式*/
IF条件包含由逗号分隔的三个表达式。C++确保每表达式都被执行,产生其副作用。然而,整个表达式的值仅是最右边的表达式的结果。因此,只有cin.good()返回true时,上述条件才为真。再举一个逗号表达式的例子:
int j=10;
int i=0;
while( ++i, --j)
{
/*只要j不为0,在循环执行*/
}
Tip 3: Comma-Separated Expressions
Comma-separated expressions were inherited from C. It's likely that you use such expressions in for- and while-loops rather often. Yet, the language rules in this regard are far from being intuitive. First, let's see what a comma separated expression is. An expression may consist of one or more sub-expressions separated by commas. For example:
if(++x, --y, cin.good()) /*three expressions */
The if condition contains three expressions separated by commas. C++ ensures that each of the expressions is evaluated and its side effects take place. However, the value of an entire comma-separated expression is only the result of the rightmost expression. Therefore, the if condition above evaluates as true only if cin.good() returns true. Here's another example of a comma expression:
int j=10;
int i=0;
while( ++i, --j)
{
/*if (j!=0) loop*/
}
===================================================
===================================================
技巧4:如何在程序启动前调用函数?
某些应用程序需要在调用主要程序之前开始启动功能。例如,polling(轮询),billing(***),和logger(日志记录)等函数必须在调用实际的程序之前开始。最简单的实现这一目标的方式是调用一个全局对象的构造函数。因为从概念上说,全局对象是在程序开始之构造的,这个函数会在main()开始之前返回。例如:
class Logger
{
public:
Logger()
{
activate_log();
}
};
Logger log; /*global instance*/
int main()
{
record * prec=read_log();
//.. application code
}
全局对象log在main()开始之前完成构造。在构造过程中,log触发了函数activate_log()。当main()开始后,它就可以从日志文件中读取数据。
Tip 4: Calling a Function Before Program's Startup
Certain applications need to invoke startup functions that run before the main program starts. For example, polling, billing, and logger functions must be invoked before the actual program begins. The easiest way to achieve this is by calling these functions from a constructor of a global object. Because global objects are conceptually constructed before the program's outset, these functions will run before main() starts. For example:
class Logger
{
public:
Logger()
{
activate_log();
}
};
Logger log; /*global instance*/
int main()
{
record * prec=read_log();
//.. application code
}
The global object log is constructed before main() starts. During its construction, log invokes the function activate_log(). Thus, when main() starts, it can read data from the log file.
// 续 任何时候都适用的20个C++技巧 <5-8> 内存管理
===================================================
===================================================
===================================================
毫无疑问,内存管理是在C++编程中最复杂和最易出错的问题之一。能够直接地访问原始内存,动态地分配内存空间,以及C++的高效性决定它必须有一些非常严格的规则;如果你不遵守将难以避免那些内存相关的错误或者程序运行时的崩溃。
指针是访问内存的主要手段。 C++可以分为两个主要类别:指向数据的指针和指向函数的指针。第二大类又可以分为两个子类类:普通函数指针和成员函数指针。在下面的技巧中,我们将深入探讨这些问题,并学习一些方法,简化指针的使用,同时隐藏它们的笨拙语法。
指向函数的指针很可能是C++中一种最不具可读性的语法结构。唯一的可读性更差的似乎只有成员指针。第一个技巧会教你如何提高普通的函数指针的可读性。这是你理解C++成员指针的前提。接下来,我们将学习如何避免内存碎片,并告诉你其可怕的后果。最后,我们讨论delete和delete []的正确使用方法;它常常会是众多错误和误解的来源。
===================================================
===================================================
技巧5:函数指针的繁琐语法?!见鬼去吧!!
你能告诉我下面定义的含义么?
void (*p[10]) (void (*)());
p是“一个包含10个函数指针的数组,这些函数返回为空,其参数为{【(另外一个无参数返回为空)的函数】的指针}。”如此繁琐的语法几乎难以辨认,难道不是吗?解决之道在何方?你可以通过typedef来合理地大大地去简化这些声明。首先,声明一个无参数、返回空的函数的指针的typedef,如下所示:
typedef void (*pfv)();
接下来 声明另一个typedef,一个指向参数为pfv返回为空的函数的指针:
typedef void (*pf_taking_pfv) (pfv);
现在,再去声明一个含有10个这样指针的数组就变得轻而易举,不费吹灰之力了:
pf_taking_pfv p[10]; /*等同于void (*p[10]) (void (*)()); 但更具可读性*/
===================================================
===================================================
技巧6:函数指针的枝枝节节
类有两类成员:函数成员和数据成员。同样,也就有两种类别的成员指针:成员函数指针和数据成员指针。后者不太常见,因为,一般来说,类是没有公共数据成员的。当使用传统C代码的时候,数据成员指针才是有用的,因为传统C代码中包含的结构体或类是具有公开数据成员的。
在C++中,成员指针是最为复杂的语法结构之一;可是,这却是一个非常强大而重要的特性。它们可以使您在不知道这个函数的名字的前提下调用一个对象的成员函数。这是非常方便的回调实现。同样的,你可以使用一个数据成员指针来监测和修改数据成员的值,而不必知道它的名字。
指向数据成员的指针
虽然成员指针的语法可能会显得有点混乱,但是它与普通指针的形式比较一致和类似,只需要在星号之前加上类名::即可。例如,如果一个普通的整形指针如下所示:
int * pi;
那么,你就可以按照下面的方式来定义一个指向类A的整形成员变量的指针:
int A::*pmi; /* pmi is a pointer to an int member of A*/
你需要按照这样的方式初始化成员指针:
class A
{
public:
int num;
int x;
};
int A::*pmi = & A::num; /* 1 */
标号1的语句声明了一个指向类A的整形成员的指针,它用A类对象中的成员变量num的地址实现了初始化。使用pmi和内置操作符.*,你可以监测和修改任何一个A类型的对象中的num的值。
A a1, a2;
int n=a1.*pmi; /* copy a1.num to n */
a1.*pmi=5; /* assign the value 5 to a1.num */
a2.*pmi=6; /* assign the value 6 to a2.num */
如果有一个指向A的指针,你必须使用->*操作符:
A * pa=new A;
int n=pa->*pmi;
pa->*pmi=5;
成员函数指针
它由成员函数的返回类型,类名加上::,指针名称,函数的参数列表几部分组成。例如,类型A的一个成员函数返回一个int,无参数,那么其函数指针应该定义如下(注意两对括号是必不可少的):
class A
{
public:
int func ();
};
int (A::*pmf) ();
换句话说,pmf是一个指向类A的成员函数的指针,类A的成员函数返回int指针,无参数。事实上,一个成员函数指针看起来和普通的函数指针相似,除了它包含函数名加上::操作符。您可以使用.*操作符来调用pmf指向的成员函数:
pmf=&A::func;
A a;
(a.*pmf)(); /* invoke a.func() */
如果有的是一个对象的指针,你必须使用->*操作符:
A *pa=&a;
(pa->*pmf)(); /*calls pa->func() */
成员函数指针遵循多态性。因此,如果你通过这样一个指针调用虚成员函数,则会实现动态调用。但是需要注意的是,你不能将成员函数指针指向一个类的构造函数或者析构函数的地址。
===================================================
===================================================
技巧7:内存碎片,No!!No!!No!!
通常而言,应用程序是不受内存泄露影响的;但如果应用程序运行很长一段时间,频繁的分配和释放内存则会导致其性能逐渐下降。最终,程序崩溃。这是为什么呢?因为经常性的动态内存分配和释放会造成堆碎片,尤其是应用程序分配的是很小的内存块。支离破碎的堆空间可能有许多空闲块,但这些块既小又不连续的。为了证明这一点,请参看一下下面的堆空间表示。0表示空闲内存块,1表示使用中的内存块:
100101010000101010110
上述堆是高度分散。如果分配一个包含五个单位(即五个0)内存块,这将是不可能的,尽管系统总共中还有12个空闲空间单位。这是因为可用内存是不连续的。另一方面,下面的堆可用内存空间虽然少,但它却不是支离破碎的:
1111111111000000
你能做些什么来避免这样的堆碎片呢?首先,尽可能少的使用动态内存。在大多数情况下,可以使用静态或自动储存,或者使用STL容器。其次,尽量分配和重新分配大块的内存块而不是小的。例如,不要为一个单一的对象分配内存,而是一次分配一个对象数组。当然,你也可以将使用自定义的内存池作为最后的手段。
===================================================
===================================================
技巧8:对于Delete 和 Delete [],你要区分清楚
在程序员当中流传有一个众所周知的传说:对于内置类型,使用delete 代替delete []来释放内存是完全可以的。例如,
int *p=new int[10];
delete p; /*bad; should be: delete[] p*/
这是完全错误的方式。在C++标准明确指出,使用delete来释放任何类型的动态分配的数组,将会导致未定义行为。事实上,在某些平台上应用程序使用delete而非delete[]但是不死机可以归结为纯粹的运气:例如,针对内置数据类型,Visual C++通过调用free()同时实现了delete[]和delete。但是,我们并不能保证的Visual C++未来版本中将仍然坚持这么做。此外,也不会保证这个代码将适用于其他的编译器。总括来说,使用delete来代替delete [],或者用delete []来代替delete都很危险,应当尽量去避免。
// 续 任何时候都适用的20个C++技巧 <9-11> 性能的提高
===================================================
===================================================
===================================================
Nine to 11: Performance Enhancements
下面所列出的是三个相当简单但又不是很常见的技术,在不牺牲程序可读性、不修改程序设计的前提下,提高程序的性能。例如,程序员往往不太清楚,只是简单的对数据成员进行重新排序就可以大大减少它的大小。这种优化可以提高性能,如果应用程序使用到了这些对象的数组,效果尤其明显。此外,我们还将学习前缀和后缀操作符之间的差异;在重载操作符中,这是一个很重要的问题。最后,我们将学习一些消除临时对象的创建的方法。
===================================================
===================================================
技巧9:类成员对齐方式的优化
只需改变类成员的声明顺序就可以改变这个类的大小:
struct A
{
bool a;
int b;
bool c;
}; /*sizeof (A) == 12*/
在我的机器上,sizeof (A) 等于12。结果看起来非常的出乎意料,因为A的成员大小之和仅仅是6个字节,多余的6个字节来自何方呢?编译器在每个bool类型成员的后面插入了各插入三个填充字节,使得它四字节边界对齐。你可以按照下面的方式重新组织数据成员减少A的大小:
struct B
{
bool a;
bool c;
int b;
}; // sizeof (B) == 8
这次编译器只是在成员c的后面插入两个填充字节。因为b占4字节,它自然就word边界对齐,而不需要额外的填充字节。
===================================================
===================================================
技巧10:明确前缀后缀操作符之间的差异
内置的++操作符可以放在操作数的两边:
int n=0;
++n; /*前缀*/
n++; /*后缀*/
你一定知道前缀操作符首先改变操作数,然后再使用它的值。比如:
int n=0, m=0;
n = ++m; /*first increment m, then assign its value to n*/
cout << n << m; /* display 1 1*/
在这个例子中,在赋值之后,n等于1;因为它是在将m赋予n之前完成的自增操作。
int n=0, m=0;
n = m++; /*first assign m's value to n, then increment m*/
cout << n << m; /*display 0 1*/
在这个例子中,赋值之后,n等于0;因为它是先将m赋予n,之后m再加1。
为了更好的理解前缀操作符和后缀操作符之间的区别,我们可以查看这些操作的反汇编代码。即使你不了解汇编语言,你也可以很清楚地看到二者之间的区别,注意inc指令出现的位置:
/*disassembly of the expression: m=n++;*/
mov ecx, [ebp-0x04] /*store n's value in ecx register*/
mov [ebp-0x08], ecx /*assign value in ecx to m*/
inc dword ptr [ebp-0x04] /*increment n*/
/*disassembly of the expression: m=++n;*/
inc dword ptr [ebp-0x04] /*increment n;*/
mov eax, [ebp-0x04] /*store n's value in eax register*/
mov [ebp-0x08], eax /*assign value in eax to m*/
注:前缀操作符、后缀操作符与性能之间的联系是什么?原文作者没有说明。所以有了亚历山大同志 的疑问。在此做一下说明:当应用内置类型的操作时,前缀和后缀操作符的性能区别通常可以忽略。然而,对于用户自定义类型,后缀操作符的效率要低于前缀操作符,这是因为在运行操作符之间编译器需要建立一个临时的对象
===================================================
===================================================
技巧11:尽量消除临时对象
在一些情况下,C++会“背着你”创建一些临时对象。一个临时对象的开销可能很大,因为它的构造和析构函数肯定会被调用。但是,在大多数情况下,您可以防止临时对象的创建。在下面的例子,一个临时对象被创建:
Complex x, y, z;
x=y+z; /* temporary created */
表达式y+z;将会导致一个Complex类型的临时对象的产生,这个临时对象保存着相加的结果。之后,这个临时对象被赋予x,随后销毁。临时对象的生成可以用两种方式加以避免:
Complex y,z;
Complex x=y+z; /* initialization instead of assignment */
在上面的例子中,y和z相加的结果直接用于对象x的构造,所以就避免了起中介作用的临时对象。或者你可以用+=代替+,同样可以达到相同的效果:
/* instead of x = y+z; */
x=y;
x+=z;
虽然采用+=的这个版本不太优雅,它只有两个成员函数调用:赋值操作和+=操作。相较而言,使用+操作符则产生三次成员函数调用:临时对象的构造、对于x的拷贝构造,以及临时对象的析构!
// 续 任何时候都适用的20个C++技巧 <12-13> Object-oriented ===================================================
===================================================
===================================================
12 and 13: Object-oriented Design
虽然C++支持多种非常有用的编程范式,如procedural programming、functional programming、generic programming,以及object-oriented programming。其中object-oriented programming(面向对象编程)无疑是使用最为广泛和重要的范例。下面的两个小技巧将帮助你更好的应用面向对象的设计和实现。首先,我将解释一下虚析构函数在类继承中的重要性。另外一个小技巧将阐述如何去处理嵌套类:将嵌套类声明为其包含类的友元。
技巧12:为什么没有虚析构函数的类继承是危险的?
如果一个类的析构函数是非虚的,那么就意味着它不会作为基类来使用(这种类就是我们所熟知的“实体类”)。std::string,std::complex,以及std::vector都是实体类。为什么不推荐继承这些类呢?当你使用公共继承时,你就会在基类与派生类之间创建一种is-a的关系。因此,基类的指针和引用实际上可以指向一个派生的对象。由于析构函数不是虚的,所以当您删除这样一个对象时,C++将不会调用整个析构链。例如:
class A
{
public:
~A() // non virtual
{
// ...
}
};
class B: public A /* 不好; A 没有虚析构函数*/
{
public:
~B()
{
// ...
}
};
int main()
{
A * p = new B; /*貌似没什么问题*/
delete p; /*问题出现, B的析构未被调用*/
}
没有调用对象的析构所带来的结果是不确定的。因此,你不应该公开继承这样的类。尤其是,不要继承STL容器和std::string。
===================================================
===================================================
技巧13:将嵌套类声明为其包含类的友元
当作为其包含类的友元来声明一个嵌套类时,你应当将友元声明放于嵌套类声明的后面,而不是前面:
class A
{
private:
int i;
public:
class B /*先定义嵌套类*/
{
public:
B(A & a) { a.i=0;};
};
friend class B;/*友元声明*/
};
如果你把友元声明放置于嵌套类声明之前,编译器将丢弃友元声明,因为编译器还没有见过它,不知道它是什么玩意儿。
注:关于嵌套类定义
A nested class is any class whose declaration occurs within the body of another class or interface. A top level class is a class that is not a nested class.
一个 class A 如果定义在了另一个 class B 或 interface B 里,那么这个 class A 就是 nested class,class B 或 interface B 则被称为 enclosing class。至于 class A 是定义在了 class B 或 interface B 的什么地方,例如 method 和 constructor,则是没有限制的。
// 续 任何时候都适用的20个C++技巧 <14-20> STL and Generic Programming
===================================================
===================================================
===================================================
标准模板库和通用编程
标准模板库(STL)给C++程序员编写代码的方式带来了革命性的影响。这样的代码重用将生产力水平提升到了更高的水平,节省了大量的时间,避免了重复性的劳动。然而,STL是一个具有特殊术语和复杂规则的、比较全面的框架,如果你想更好的去应用它,那么你只能去掌握它,“知己知彼方能百战不殆”吗。为了更深入地了解STL某些方面的情况,这大类中将包含6个小技巧。
第一个技巧将介绍一下STL的基本组成和一些关键术语。接下来的小技巧则集中于模板定义这一方面。正如你所知,模板是STL容器和算法最基础的“建筑材料”。接下来的三个小技巧将依次描述如何使用标准库中应用最为广泛的容器 - vector,学习如何在vector中存储对象指针,避免常见陷阱,以及如何将vector当做一个内置的数组来使用。第五个提示将会告诉你如何使用vector来模仿多维数组。最后的提示将介绍一个非常重要的问题:auto_ptr和STL容器之间的一些问题。
技巧14:非常有用的STL术语
接下来所讲述的是STL中一些非常关键的条款。也许在你阅读标准模板库(STL)文献或文档的时候,您遇到过它们。
Container 容器
容器是一个对象,它将对象作为元素来存储。通常情况下,它是作为类模板来实现,其成员函数包括遍历元素,存储元素和删除元素。std::list和std::vector就是两种典型的容器类。
Genericity 泛型
泛型就是通用,或者说是类型独立。上面对于容器类的定义是非常宽松的,因为它适用于字符串,数组,结构体,或者是对象。一个真正的容器是不局限于某一种或着某些特定的数据类型的。相反,它可以存储任何内置类型或者用户自定义类型。这样的容器就被认为是通用的。请注意,string只能包含字符。泛型也许是STL的最重要的特征。第三个技巧将给出函数对象的标准基类。因为函数对象是通用编程重的一个重要部分。在设计实现过程中,坚持标准规范将会省去你的很多的困难。
Algorithm 算法
算法就是对一个对象序列所采取的某些操作。例如std::sort()排序,std::copy()复制,和std::remove()删除。STL中的算法都是将其作为函数模板来实现的,这些函数的参数都是对象迭代器。
Adaptor 适配器
适配器是一个非常特殊的对象,它可以插入到一个现有的类或函数中来改变它的行为。例如,将一个特殊的适配器插入到std::sort()算法中,你就可以控制排序是降序还是升序。 STL中定义了多种类型的序列适配器,它可以将一个容器类变换成一个具有更严格接口的不同容器。例如,堆栈(stack)就可以由queue<>和适配器来组成,适配器提供了必要的push()和pop()操作。
O(h) Big Oh Notation
O(h)是一个表示算法性能的特殊符号,在STL规范当中用于表示标准库算法和容器操作的最低性能极限。任何其他的实现可能会提供更好的性能,但绝不是更坏的。O(h)可以帮助您去评估一个算法或者某个特定类型容器的某个操作的效率。std::find()算法遍历序列中的元素,在最坏的情况下,其性能可以表示为:
T(n) = O(n). /* 线性复杂度 */
Iterator 迭代器
迭代器是一种可以当做通用指针来使用的对象。迭代器可以用于元素遍历,元素添加和元素删除。 STL定义了五个种主要的迭代器:
输入迭代器和输出迭代器 input iterators and output iterators
前向迭代器 forward iterators
双向迭代器 bidirectional iterators
随机访问迭代器 random access iterators
请注意,上述迭代器列表并不具有继承关系,它只是描述了迭代器种类和接口。下面的迭代器类是上面类的超集。例如,双向迭代器不仅提供了前向迭代器的所有功能,还包括一些附加功能。这里将对这些类别做简要介绍:
输入迭代器允许迭代器前行,并提供只读访问。
输出迭代器允许迭代器前行,并提供只写访问。
前向迭代器支持读取和写入权限,但只允许一个方向上的遍历。
双向迭代器允许用户在两个方向遍历序列。
随机访问迭代器支持迭代器的随机跳跃,以及“指针算术”操作,例如:
string::iterator it = s.begin();char c = *(it+5); /* assign sixth char to c*/
===================================================
===================================================
技巧15:模板定义的位置在哪里?是.cpp文件吗?
通常情况下,你会在.h文件中声明函数和类,而将它们的定义放置在一个单独的.cpp文件中。但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。这就是为什么所有的STL头文件都包含模板定义的原因。
另外一个方法就是使用关键字“export”!你可以在.h文件中,声明模板类和模板函数;在.cpp文件中,使用关键字export来定义具体的模板类对象和模板函数;然后在其他用户代码文件中,包含声明头文件后,就可以使用该这些对象和函数了。例如:
// output.h - 声明头文件
template<class T> void output (const T& t);
// out.cpp - 定义代码文件
#include <****>
export template<class T> void output (const T& t) {std::cerr << t;}
//main.cpp:用户代码文件
#include "output.h"
void main() // 使用output()
{
output(4);
output("Hello");
}
某种程度上,这有点类似于为了访问其他编译单元(如另一代码文件)中普通类型的变量或对象而采用的关键字extern。
但是,这里还有一个不得不说的问题:并非所有的编译器都支持export关键字(我们最熟悉、最常用的两款编译器VS 和 GCC就是不支持export的典型代表)。对于这种不确定,最好的方法就是采用解决方案一:声明定义放在一起,虽然这在某种程度上破坏了C++编程的优雅性。
分离编译模式(Separate Compilation Model)允许在一处翻译单元(Translation Unit)中定义(define)函数、类型、类对象等,在另一处翻译单元引用它们。编译器(Compiler)处理完所有翻译单元后,链接器(Linker)接下来处理所有指向 extern 符号的引用,从而生成单一可执行文件。该模式使得 C++ 代码编写得称心而优雅。
然而该模式却驯不服模板(Template)。标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。
以优雅著称的 C++ 是不能容忍有此“败家玩意儿”好好活着的。标准 C++ 为此制定了“模板分离编译模式(Separation Model)”及 export 关键字。然而由于 template 语义本身的特殊性使得 export 在表现的时候性能很次。编译器不得不像 .net 和 java 所做的那样,为模板实体生成一个“中间伪代码(IPC,intermediate pseudo - code)”,使得其它翻译单元在实例化时可找到定义体;而在遇到实例化时,根据指定的 typename 实参再将此 IPC 重新编译一遍,从而达到“分离编译”的目的。因此,该标准受到了几乎所有知名编译器供应商的强烈抵制。
谁支持 export 呢?Comeau C/C++ 和 Intel 7.x 编译器支持。而以“百分百支持 ISO ”著称的 VS 和 GCC 却对此视而不见。真不知道这两大编译器“百分百支持”的是哪个版本的 ISO。在 VS 2008 中,export 关键字在 IDE 中被标蓝,表示 VS IDE 认识它,而编译时,会用警告友情提示你“不支持该关键字”,而配套的 MSDN 9 中的 C++ keywords 页则根本查不到该关键字;而在 VS 2010 中,就没那么客气了,尽管 IDE 中仍然会将之标蓝,但却会直截了当地报错。
===================================================
===================================================
技巧16:函数对象的标准基
为了简化编写函数对象的过程,标准库提供了两个类模板,作为用户自定义函数对象的基类:std::unary_function和std::binary_function。两者都声明在头文件<functional>中。正如名字所显示的那样,unary_function被用作是接受一个参数的函数的对象的基类,而binary_function是接受两个参数的函数的对象的基类。这些基类定义如下:
template < class Arg, class Res > struct
unary_function
{
typedef Arg argument_type;
typedef Res result_type;
};
template < class Arg, class Arg2, class Res >
struct binary_function
{
typedef Arg first_argument_type;
typedef Arg2 second_argument_type;
typedef Res result_type;
};
这些模板并不提供任何实质性的功能。他们只是确保其派生函数对象的参数和返回值有统一的类型名称。在下面的例子中,is_vowel继承自unary_function,接受一个参数:
template < class T >
class is_vowel: public unary_function< T, bool >
{
public:
bool operator ()(T t) const
{
if ((t=='a')||(t=='e')||(t=='i')||(t=='o')||(t=='u'))
return true;
return false;
}
};
===================================================
===================================================
技巧17:如何在STL容器中存储动态分配的对象?
假设你需要在同一容器中存储不同类型的对象。通常情况下,您可以通过存储储动态分配对象的指针来达到这一点。然而,除了使用指针外,也可以按照下面的方式将元素插入到容器中:
class Base {};
class Derived : public Base{};
std::vector <Base *> v;
v.push_back(new Derived);
v.push_back(new Base);
如果按照这种方式,那么存储的对象只能通过其容器来访问。请记住,应按照下面的方式删除分配的对象:
delete v[0];
delete v[1];
===================================================
===================================================
技巧18:将向量当作数组使用
假设你有一个整型向量vector<int> v,和一个以int*为参数的函数。为了获得向量v内部数组的地址,并将它传给函数,你必须使用表达式&v[0]或者是&*v.front()。举个例子:
void func(const int arr[], size_t length );
int main()
{
vector <int> vi;
//.. fill vi
func(&vi[0], vi.size());
}
只要你遵守线面的几条规则,你用&vi[0]和&*v.front()作为其内部数组地址就会很安全放心:
(1)fun()不应访问超出数组范围的元素。
(2)向量中的元素必须是连续的。虽然C++标准中并没有做这样的规定,但是据我所知,没有一个vector的实现不是使用连续内存的。
===================================================
===================================================
技巧19:动态多维数组和向量的恩恩怨怨
你可以按照以下方式手动分配多维数组:
int (*ppi)[5] = new int[4][5]; /*parentheses required*/
/*fill array..*/
ppi[0][0] = 65;
ppi[0][1] = 66;
ppi[0][2] = 67;
//..
delete [] ppi;
然而,这种编码风格是非常枯燥的,而且容易出错。你必须用圆括号括起ppi,以确保这个声明能被编译器正确解析;同时你也必须手动地删除你所分配的内存。更糟糕的是,你会在不经意间碰上令人头疼的缓冲区溢出。而使用向量的向量来模拟多维数组则是一个更好的选择:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <vector <int> > v; /*two dimensions*/
v.push_back(vector <int>()); /*create v[0]*/
v.push_back(vector <int>()); /*create v[1]*/
v[0].push_back(15); /*assign v[0][0]*/
v[1].push_back(16); /*assign v[1][0]*/
}
因为vector重载了操作符[],你可以向使用内置的二维数组一样使用[][]:
cout << v[0][0];
cout << v[1][0];
用向量的向量模拟多维数组主要有两个优点:向量会自动的按照需要来分配内存。其次,它自己负责释放分配的内存,而你则不必担心潜在的内存泄漏。
===================================================
===================================================
技巧20:为什么你不应该在STL容器中存储auto_ptr对象?
在C++标准中,一个STL元素必须是可以“拷贝构造”和“赋值”。这个条款意味着,对于一个给定类,“拷贝”和“赋值”是其非常便利顺手的操作。特别是,当你将它复制到目标对象时,原始对象的状态是不会改变的。
但是,这是不适用auto_ptr的。因为auto_ptr的从一个拷贝到另一个或赋值到另一个对象时会使得原始对象产生预期变动之外的变化。具体说来就是,会将原来的对象的指针转移到目标对象上,从而使原来的指针变为空指针,试想一下,如果照下面代码所示来做,会出现什么结果呢:
std::vector <auto_ptr <Foo> > vf;/*a vector of auto_ptr's*/
// ..fill vf
int g()
{
std::auto_ptr <Foo> temp=vf[0]; /*vf[0] becomes null*/
}
当temp被初始化时,vf[0]的指针变成空。任何对该元素的调用都将导致程序的运行崩溃。在您从容器中复制元素时,这种情况极有可能会发生。注意,即使您的代码中没有执行任何显式的拷贝操作或赋值操作,许多算法(例如std::swap()和std::random_shuffle()等)都会创建一个或多个元素的临时副本。此外,该容器的某些成员函数也会创建一个或多个元素的临时副本,这就会抵消原有的元素指针。以致任何对容器中元素的后续操作都将带来的不确定的后果。
可能Visual C++用户会说,我在STL容器中使用auto_ptr时,从来没有遇到过任何问题。这是因为在Visual C++中auto_ptr的实现已经过时,而且依赖在一个过时规范上。如果厂商决定赶上当前的最新ANSI/ISO C++标准,并相应地改变它的标准库,在STL容器中使用auto_ptr将会导致严重故障。
总结来说,你是不应该在STL容器中使用auto_ptr的。你可以使用普通指针或其他智能指针类,但绝不是auto_ptr指针。<<任何时候都适用的20个C++技巧>>这个小系列到此就结束了,虽然关于C++的技巧还很多,还值得我们去总结。如果遇到,我会第一时间拿出来与大家分享! 谢谢各位博友!!
版权注明:copy from other's