20个实用的小技巧

这些小技巧之因此特别,是由于这些信息一般吧不能在C++书籍或者网站上找到。好比说,成员指针,即便对于高级程序员也是比较棘手,和易于产生bugs的,是应该尽可能避免的问题之一。                                                    java

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.                                                                                                                  ios

====================================
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  较好编程风格所要遵循的一些准则
express

在这个类别中,所涉及的技巧是各级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>组件是全局性的。sass

  由于两者之间存在着这些重大分歧,你不能在同一程序中混合使用两个库。做为一条准则:使用<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.
app

  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

相关文章
相关标签/搜索