任何时候都适⽤的20个C++技巧
这些⼩技巧之所以特别,是因为这些信息通常吧不能在C++书籍或者⽹站上到。⽐如说,成员指针,即使对于⾼级程序员也是⽐较棘⼿,和易于产⽣bugs的,是应该尽量避免的问题之⼀。
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>兼容的遗留代码。
技巧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()就可以修改它的参数,从⽽引起未定义的⾏为。所以,你只能向常量对象绑定引⽤。
  同样的准则适⽤于⾃定义对象类性。只有当临时对象是常量时,你才能绑定引⽤。
void f(const int & i);
int main()
{
f(2); /* OK */
}
技巧3:奇怪的逗号分割表达式
  逗号分隔的表达式是从C继承⽽来的。你很有可能会在使⽤for-循环和while-循环的时候经常使⽤这样的表达式。然⽽,在这⽅⾯的语⾔规则还远不直观。⾸先,让我们来看看什么是逗号分隔的表达式:这种表达式可能包含⼀个或多个⽤逗号分隔的⼦表达式。例如:
if(++x, --y, d()) /*three expressions 三个表达式*/
  IF条件包含由逗号分隔的三个表达式。C++确保每表达式都被执⾏,产⽣其副作⽤。然⽽,整个表达式的值仅是最右边的表达式的结果。因此,只有d()返回true时,上述条件才为真。再举⼀个逗号表达式的例⼦:
int j=10;
int i=0;
while( ++i, --j)
{
/*只要j不为0,在循环执⾏*/
什么是编程举个例子
}
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()开始后,它就可以从⽇志⽂件中读取数据。
// 续任何时候都适⽤的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++; /*后缀*/

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。