C++编程思想笔记

来源:互联网 发布:mac上itunes设置铃声 编辑:程序博客网 时间:2024/06/10 01:17

 

第1章  对象的演化

一、多态性的解释:   当处理类型层次结构时,程序员常常希望不把对象看作是某一特殊类型的成员,而把它看作基本类型成员,这样就可以编写不依赖于特殊类型的代码。在形体例子中,函数可以对一般形体进行操作,而不关心它们是圆、正方形还是三角形。所有的形体都能被绘制、擦除和移动,所以这些函数能简单地发送消息给一个形体对象,而不考虑这个对象如何处理这个消息。这样,新添类型不影响原来的代码,这是扩展面向对象程序以处理新情况的最普通的方法。

二、操作概念:OOP 程序像什么   一个写得很好的C + +程序一般要比功能相同的C 程序更简单和容易理解。程序员只会看到一些描述问题空间对象的定义(而不是计算机的描述),发送给这些对象的消息。这些消息表示了在这个空间的活动。面向对象程序设计的优点之一是通过阅读,很容易理解代码。

三、对象设计的五个阶段 

下面是描述,不是方法。它简直就是对象期望的设计出现时的观察结果。
1) 对象发现这个阶段出现在程序的最初分析期间。可以通过寻找外部因素与界线、系统中的元素副本和最小概念单元而发现对象。如果已经有了一组类库,某些对象是很明显的。类之间的共同性(暗示了基类和继承类),可以立刻出现或在设计过程的后期出现。
2) 对象装配我们在建立对象时会发现需要一些新成员,这些新成员在对象发现时期未出现过。对象的这种内部需要可能要用新类去支持它。
3) 系统构造对对象的更多要求可能出现在以后阶段。随着不断的学习,我们会改进我们的对象。与系统中其它对象通讯和互相连接的需要,可能改变已有的类或要求新类。
4) 系统扩充当我们向系统增添新的性能时,可能发现我们先前的设计不容易支持系统扩充。这时,我们可以重新构造部分系统,并很可能要增加新类。     5) 对象重用这是对类的真正的重点测试。如果某些人试图在全新的情况下重用它,他们会发现一些缺点。当我们修改一个类以适应更新的程序时,类的一般原则将变得更清楚,直到我们有了一个真正可重用的对象。

四、对象开发原则
在这些阶段中,提出考虑开发类时所需要的一些原则:
1) 让特殊问题生成一个类,然后在解其他问题时让这个类生长和成熟。
2) 记住,发现所需要的类,是设计系统的主要内容。如果已经有了那些类,这个项目就不困难了。
3) 不要强迫自己在一开始就知道每一件事情,应当不断地学习。
4) 开始编程,让一部分能够运行,这样就可以证明或反驳已生成的设计。不要害怕过程语言风格的细面条式的代码—类分割可以控制它们。坏的类不会破坏好的类。
5) 尽量保持简单。具有明显用途的不太清楚的对象比很复杂的接口好。

第2章   数据抽象

一、库的重要性

库,简单地说就是一些人已经写的代码,按某种方式包装在一起。通常,最小的包是带有扩展名如L I B 的文件和向编译器声明库中有什么的一个或多个头文件。连接器知道如何在L I B文件中搜索和提取相应的已编译的代码。但是,这只是提供库的一种方法。在跨越多种体系结构的平台上,例如U N I X ,通常,提供库的最明智的方法是用源代码,这样在新的目标机上它能被重新编译。而在微软Wi n d o w s 上,动态连接库是最明智的方法,这使得我们能够利用新发布的D D L 经常修改我们的程序,我们的库函数销售商可能已经将新D D L 发送给我们了。所以,库大概是改进效率的最重要的方法。C + +的主要设计目标之一是使库容易使用。这意味着,在C 中使用库有困难。懂得这一点就对C + +设计有了初步的了解,从而对如何使用它有了更深入的认识。

二、声明与定义

“声明”向计算机介绍名字,它说,“这个名字是什么意思”。而“定义”为这个名字分配存储空间。无论涉及到变量时还是函数时含义都一样。无论在哪种情况下,编译器都在“定义”处分配存储空间。对于变量,编译器确定这个变量占多少存储单元,并在内存中产生存放它们的空间。对于函数,编译器产生代码,并为之分配存储空间。函数的存储空间中有一个由使用不带参数表或带地址操作符的函数名产生的指针。

三、一些函数:

1、 memcpy( ):一个字节一个字节地拷贝这个变量,第一个参数是memcpy( ) 开始拷贝字节的目的地址,第二和第三个参数分别是被拷贝变量的开始地址和要拷贝的字节数。

2、 realloc( ):把已经分配而又希望重分配的存储单元首地址作为它的第一个参数(如果这个参数为零,realloc( )分配一个新块),第二个参数是这个块新的长度,如果这个长度比原来的小,这个块将不需要作拷贝,简单地告诉堆管理器剩下的空间是空闲的。如果这个长度比原来的大,在堆中没有足够的相临空间,所以要分配新块,并且要拷贝内存。

3、void *malloc( size_t size ):The malloc function allocates a memory block of at least size bytes. The block may be larger than size bytes because of space required for alignment and maintenance information!

四、动态存储分配

动态内存分配函数是标准C 库的一部分,包括malloc( )、calloc( )、
realloc( )和free( )。,C 堆管理器相当重要,它给出内存块,对它们使用free( )时就回收它们。没有对堆进行合并的工具,如果能合并就可以提供更大的空闲块。如果程序多次分配和释放堆存储,最终会导致这个堆有大量的空闲块,但没有足够大且连续的空间能满足我们对内存分配的需要。但是,如果用堆合并器移动内存块,又会使得指针保存的不是相应的值。

五、C单元的编译过程  编译每个单元--〉连接各个目标文件

1)每个独立的C 文件就是一个处理单元。就是说,编译器在每个处理单元上单独运行,而编译器在运行时只知道这个单元。这样,用包含头文件提供信息是相当重要的,因为它为编译器提供了对程序其他部分的理解。在头文件中的声明特别重要,因为无论是在哪里包含这个头文件,编译器都会知道要做什么。例如,若在一个头文件中声明void foo(float),编译器就会知道,如果我们用整型参数调用它,它会自动把i n t 转变为f l o a t 。如果没有声明,这个编译器就会简单地猜测,有一个函数存在,而不会做这个转变。

2),编译器创建一个目标文件,带有扩展名.o 或.obj 或类似的名字。必须再用连接器将这些目标文件连同必要的启动代码连接成可执行程序。在连接期间,所有的外部引用都必须确定用对应的实际地址,用这些地址替换这些外部引用。

六、头文件形式:

在C++ 中,头文件的使用变得非常清楚。它们对于每个程序开发是强制的,在它们中放入非常特殊的信息:声明。头文件告诉编译器在我们的库中哪些是可用的。因为对于C P P文件能够不要源代码而使用库(只需要对象文件或库文件),所以头文件是存放接口规范的唯一地方。头文件是库的开发者与它的用户之间的合同。它说:“这里描述的是库能做什么。”它不说如何做,因为如何做存放在C P P 文件中,开发者不需要分发这些描述“如何做”的源代码给用户。为了恰当地组织代码和写有效的头文件,有一些问题必须知道。第一个问题是将什么放进头文件中。基本规则是“只声明”,也就是说,对于编译器只需要一些信息以产生代码或创建变量分配内存。是因为,在一个项目中,头文件也许会包含在几个处理单元中,而如果内存分配不止一个地方,则连接器会产生多重定义错误。这个规则不是非常严格的。如果在头文件中定义“静态文件”的一段数据(只在文件内可视),在这个项目中将有这个数据的多个实例,编译器不会报错。基本上,不要在头文件中做在连接时会引起混淆的任何事情。(另外见Effective C++)

七、嵌套结构:简单地两次使用范围分解运算符,以指明这个嵌套struct 的名字。stack::link::initialize( ) 函数取参数并把参数赋给它的成员们。

第3章   隐藏实现
一、C++的存取控制

public 意味着在其后声明的所有成员对所有的人都可以存取。public成员就如同一般的struct成员。private关键字则意味着,除了该类型的创建者和类的内部成员函数之外,任何人都不能存取这些成员。private在设计者与用户之间筑起了一道墙。如果有人试图存取一个私有成员,就会产生一个编译错误。protected与private基本相似,只有一点不同:继承的结构可以访问protected成员,但不能访问private 成员。

二、提前引用某个类时一个值得注意的地方:解决的办法是:注意到Y :: f(X*)引用了一个X 对象的地址。这一点很关键,因为编译器知道如何传递一个地址,这一地址大小是一定的,而不管被传递的对象类型大小。如果试图传递整个对象,编译器就必须知道X 的全部定义以确定它的大小以及如何传递它,这就使程序员无法声明一个类似于Y :: g(X)的函数。

三、句柄类(参见Effective C++,CSDN问题http://www.csdn.net/expert/topic/576/576745.xml?temp=.1663324)

在我们的编程环境中,当一个文件被修改,或它所依赖的文件包含的头文件被修改时,项目负责人需要重复编译这些文件。这意味着无论何时程序员修改了一个类,无论是修改公共的接口部分,还是私有的实现部分,他都得再次编译包含头文件的所有文件。对于一个大的项目而言,在开发初期这可能非常难以处理,因为实现部分可能需要经常改动;如果这个项目非常大,用于编译的时间过多就可能妨碍项目的完成。解决这个问题的技术有时叫句柄类(handle classes )或叫“Cheshire Cat ”[ 1 ]。有关实现的任何东西都消失了,只剩一个单一的指针“smile ”。该指针指向一个结构,该结构的定义与其所有的成员函数的定义一样出现在实现文件中。这样,只要接口部分不改变,头文件就不需变动。而实现部分可以按需要任意更动,完成后只要对实现文件进行重新编译,然后再连接到项目中。这里有个说明这一技术的简单例子。头文件中只包含公共的接口和一个简单的没有完全指定的类指针。


第4章   初始化与清除

一、空间分配

编译器更可能像C 编译器一样,在一个程序块的开头就分配所有的内存。这些对我们来说是无关紧要的,因为作为一个程序员,我们在变量定义之前总是无法得到存储空间。即使存储空间在块的一开始就被分配,构造函数也仍然要到对象的定义时才会被调用,因为标识符只有到此时才有效。这里的意思:内存空间的分配是在程序段的开头就分配,但是程序员的使用必须要等到看到这个标识符被声明时才可以,也只有到了这时他的构造函数才被调用!

第5章   函数重载与缺省参数

一、 缺省函数:在使用缺省参数时必须记住两条规则。第一,只有参数列表的后部参数才可是缺省的,也就是说,我们不可以在一个缺省参数后面又跟一个非缺省的参数。第二,一旦我们开始使用缺省参数,那么这个参数后面的所有参数都必须是缺省的。

二、占位符

多数编译器会给出一条警告信息,并认为我们犯了一个逻辑错误。用这种没有名字的参数就可以防止这种警告产生。更重要的是,如果我们开始用了一个函数参数,而后来发现不需要用它,我们可以高效地将它去掉而不会产生警告错误,而且不需要改动那些调用该函数以前版本的程序代码。


第6章   输入输出流介绍

一、操纵算子

一个称作e n d l 的操纵算子。一个操纵算子作用于流上,这种情况下,插入一新行并清空流(消除所有存储在内部流缓冲区里的还没有输出的字)。也可以只清空流:  ccout<<flush;另外有一个基本的操纵算子把基数变为o c t (八进制),d e c (十进制)或h e x (十六进制):cout<<hex<<"0x" <<i<<endl;有一个用于提取的操纵算子“跳过”空格:cin>>ws;

二、输入输出流

一个输入流(istream)或一个输出流(ostream)。有不同类型的输入流和输出流:文件输入流( ifstreams)和文件输出流( ofstreams)、char*内存的(内核格式化)输入流istrstreams和输出流(ostrstreams)、以及与标准C + +串(string)类接口的串输入流(istringstreams)和串输出流(ostringstreams)。

三、面向行的输入get,getline,read,write

有两种选择:成员函数get()或getline()。两个函数都有三个参数:指向存储结果字符的缓冲区指针、缓冲区大小(不能超过其限度)和知道什么时候停止读输入的终止符。终止符有一个经常用到的缺省值"/n"。两个函数遇到输入终止符时,都把零储存在结果加在缓冲区后面。差别虽小但极其重要:get()遇到输入流的分隔符时就停止,而不从输入流中提取分隔符。如果用同样的分隔符再调用一次get()函数,它会立即返回而不带任何输入。(要么在下一个get()说明里用一个不同的分隔符,要么用一个不同的输入函数)。getline()与其相反,它从输入流中提取分隔符,但仍没有把它储存在结果缓冲区里。

读原始字节: 如果想确切知道正在处理什么,并把字节直接移进内存中的变量、数组或结构中,可以用read()函数。第一个参数是一个指向内存目的地址的指针,第二个参数指明要读的字节数。预先将信息存储在一个文件里特别有用。

四、输入输出流缓冲(rdbuf--区的StreamBuf指针)

每一个输入输出流都包含一个指针,指向某种StreamBuf(这依赖于它是否处理标准I / O 、文件、内存等等)。我们可以直接访问StreamBuf。例如,可以向StreamBuf移进、移出原始字节,而不必通过输入输出流来格式化它们。为了允许我们访问StreamBuf,每个流对象有一个叫做Rebuf()的成员函数,这个函数返回指向对象的StreamBuf的指针。这样,我们可以为下层的StreamBuf调用任何成员函数。StreamBuf指针所做的最有兴趣的事之一是:使用“< <”操作符将其与另一个输入输出流联结。这使我们的对象中的所有字节流进“< <”左边的对象中。这意味着,如果把一个输入输出流的所有字节移到另一个输入输出流,我们不必做读入它们的一个字节或一行这样单调的工作。这是一流的方法。


五、在输入输出流中查找(tellg,tellp,seekg,seekp)

在某些情况下,可能需要移动这个流的位置,可以用两种方式处理:第一种方式是在流里绝对定位,叫流定位(StreamBuf);第二种方式像标准C 库函数fseek( )那样做,从文件的开始、结尾或当前位置移动给定数目的字节。

六、建立读/写文件

下面的代码首先建立一个有标志的ifstream,它既是一个输入文件又是一个输出文件,编译器不会让我们向ifstream写,因此,需要建立具有基本流缓冲区的输出流(ostream).这里应该注意的是:ifstream和ostream必须建立起一个关联! 然后对ostream的改变也就会改变到ifstream.                                                                     ifstream in("in.txt",ios::in|ios::out);  ostream out(in.rdbuf());

第7章   常量

一、值替代:用C 语言进行程序设计时,预处理器可以不受限制地建立宏并用它来替代值。因为预处理器只做文本替代,它既没有类型检查思想,也没有类型检查工具,所以预处理器的值替代会产生一些微小的问题,这些问题在C + +中可通过使用const而避免。C + +中的const 默认为内部连接,也就是说,const 仅在const 被定义过的文件里才是可见的,而在连接时不能被其他编译单元看到。当定义一个常量const时,必须赋一个值给它,除非用extern作了清楚的说明:
extern const bufsize ;C + +编译器通常并不为const分配存储空间,相反它把这个定义保存在它的符号表里。当const 被使用时,它在编译时会进行常量折叠.

对常量进行分配内存的两个情况:1、上面的extern 强制进行了存储空间分配 (内部连接的常量不要分配地址,而外部连接的常量需要分配地址) 2、另外还有一些情况,如取一个const的地址,也要进行存储空间分配 。对于在大量场合使用的内部数据类型,包括常量表达式,编译器都能执行常量折叠。当然,绝对不为任何const分配存储是不可能的,尤其对于复杂的结构。这种情况下,编译器建立存储,这会阻止常量折叠。这(有时会给常量分配内存)就是const为什么必须默认内部连接,即连接仅在特别编译单元内的原因;否则,由于众多的const在多个cpp文件内分配存储,容易引起连接错误,连接程序在多个对象文件里看到同样的定义就会“抱怨”了(发现同名的厂量)。然而,因为const默认内部连接,所以连接程序不会跨过编译单元连接那些定义,因此不会有冲突。(C默认const 是外部连接的)

当处理const指针时,编译器仍将努力阻止存储分配并进行常量折叠。我们可以把一个非const 对象的地址赋给一个const指针,因为也许有时不想改变某些可以改变的东西。然而,不能把一个const对象的地址赋给一个非const指针,因为这样做可能通过被赋值指针改变这个const指针。当然,总能用类型转换强制进行这样的赋值(int *a=(int*)&e强行取得地址),但是,这不是一个好的程序设计习惯,因为这样就打破了对象的const属性以及由const提供的安全性。

函数参数和返回值:用const限定函数参数及返回值是常量概念另一个容易被混淆的地方。如果以值传递对象时,对用户(函数的调用者)来讲,用const限定没有意义(它意味着传递的参数在函数里是不能被修改的),但是对函数的设计者来说,却有着一定的作用,保证某个参数不会改变。所以它其实是函数创建者的工具,而不是函数调用者的工具。为了不使调用者混淆,在函数内部用const限定参数优于在参数表里用const限定参数。如果以常量返回用户定义类型的一个对象的值,这意味着返回值不能被修改。如果传递并返回地址,const将保证该地址内容不会被改变。对于内部数据类型int,float,char等基础数据类型,不包括指针和结构、数组)来说,返回值是否是常量并没有关系,所以返回一个内部数据类型的值时,应该去掉const从而使用户程序员不混淆。处理用户定义的类型时,返回值为常量是很重要的。如果一个函数返回一个类对象的值,其值是常量,那么这个函数的返回值不能是一个左值(即它不能被赋值,也不能被修改)。返回一个内部数据类型的值时,const没有意义的原因是:编译器已经不让它成为一个左值(因为它总是一个值而不是一个变量)。仅当返回用户定义的类型对象的值时,才会出现上述问题。如果传递或返回一个指针(或一个引用),用户取指针并修改初值是可能的。如果使这个指针成为常量指针,就会阻止这类事的发生,这是非常重要的。事实上,无论什么时候传递一个地址给一个函数,我们都应该尽可能用const修饰它,如果不这样做,就使得带有指向const的指针函数不具备可用性。是否选择返回一个指向const的指针,取决于我们想让用户用它干什么.

由于引用的语法(看起来像值传递)的原因,传递一个临时对象给带有一个引用的函数是可能的,但不能传递一个临时对象给带有一个指针的函数----因为它必须清楚地带有地址。所以,通过引用传递会产生一个在C 中不会出现的新情形:一个总是常量的临时变量,它的地址可以被传递给一个函数。这就是为什么临时变量通过引用被传递给一个函数时,这个函数的参数一定是常量(const)引用。在函数参数中使用常量引用特别重要。这是因为我们的函数也许会接受临时的对象,这个临时对象是由另一个函数的返回值创立或由函数使用者显式地创立的。临时对象总是不变的,因此如果不使用常量引用,参数将不会被编译器接受。 f(1)

二、集合:const可以用于集合(数组) const int a={1,2,3,4},但编译器不能把一个集合存放在它的符号表里,所以必须分配内存。在这种情况下,const 意味着“不能改变的一块存储”。然而,其值在编译时不能被使用,因为编译器在编译时不需要知道存储的内容,只有在运行的时候才知道(虽然早已确定它的值,但是不知道它的存放位置阿)!

三、类里的const 和enum

起初读者可能认为合乎逻辑的选择是把一个const放在类里。但这不会产生预期的结果。在一个类里,const恢复它在C 中的一部分意思。它在每个类对象里分配存储并代表一个值,这个值一旦被初始化以后就不能改变。在一个类里使用const的意思是“在这个对象寿命期内,这是一个常量”。然而,对这个常量来讲,每个不同的对象可以含一个不同的值。在一个类里建立一个const 时,不能给它初值。这个初始化工作必须发生在构造函数里,并且,要在构造函数的某个特别的地方。因为const必须在建立它的地方被初始化,所以在构造函数的主体里,const必须已初始化了,否则,就只有等待,直到在构造函数主体以后的某个地方给它初始化,这意味着过一会儿才给const初始化。

四、const 对象和成员函数

编译器强调对象为const的,因此它必须保证对象的数据成员在对象寿命期内不被改变。可以很容易地保证公有数据不被改变,但是怎么知道哪个成员函数会改变数据?又怎么知道哪个成员函数对于const对象来说是“安全”的呢?如果声明一个成员函数为const函数,则等于告诉编译器可以为一个const对象调用这个函数。一个没有被特别声明为const的成员函数被看成是将要修改对象中数据成员的函数,而且编译器不允许为一个const对象调用这个函数。。仅仅声明一个函数在类定义里是const的,不能保证成员函数也如此定义,所以编译器迫使程序员在定义函数时要重申const说明。(const已成为函数识别符的一部分,所以编译器和连接程序都要检查const)。

五、 volatile

volatile的语法与const是一样的,但是volatile的意思是“在编译器认识的范围外,这个数据可以被改变”。不知何故,环境正在改变数据(可能通过多任务处理),所以,volatile告诉编译器不要擅自做出有关数据的任何假定—在优化期间这是特别重要的。如果编译器说:“我已经把数据读进寄存器,而且再没有与寄存器接触”。一般情况下,它不需要再读这个数据。但是,如果数据是volatile修饰的,编译器不能作出这样的假定,因为可能被其他进程改变了,它必须重读这个数据而不是优化这个代码。

第8章   内联函数

一、内联函数和编译器

为了理解内联何时有效,应该先理解编译器遇到一个内联函数时将做什么。对于任何函数,编译器在它的符号表里放入函数类型(即包括名字和参数类型的函数原型及函数的返回类型)。另外,编译器看到内联函数和内联函数的分析没有错误时,函数的代码也被放入符号表。代码是以源程序形式存放还是以编译过的汇编指令形式存放取决于编译器。调用一个内联函数时,编译器首先确保调用正确,即所有的参数类型必须是正确类型或编译器必须能够将类型转换为正确类型,并且返回值在目标表达式里应该是正确类型或可改变为正确类型。当然,编译器对任何类型函数都是这样做的,这与预处理器显著不同,因为预处理器不能检查类型和进行转换。假如所有的函数类型信息符合调用的上下文的话,内联函数代码就会直接替换函数调用,消除了调用的开销。假如内联函数也是成员函数,对象的地址(this)就会被放入合适的地方,这当然也是预处理器不能执行的。

二、标志粘贴

标志粘贴在写代码时是非常有用的。它让我们设两个标识符并把它们粘贴在一起自动产生一个新的标识符。

#define  FIELD(A) char* A##_string
main()
{
  FIELD(a);
 
  a_string ="aaaa";
  cout<<a_string<<endl;
}

 

第09章  命名控制

一、来自C 语言中的静态成员

在C 和C + +中,static都有两种基本的含义,并且这两种含义经常是互相有冲突的:1) 在固定的地址上分配,也就是说对象是在一个特殊的静态数据区上创建的,而不是每次函数调用时在堆栈上产生的。这也是静态存储的概念。
2) 对一个特定的编译单位来说是本地的(就像我们在后面将要看到的,这在C++中包括类的范围)。这里static控制名字的可见性,所以这个名字在这个单元或类之外是不可见的。这也描述了连接的概念,它决定连接器将看到哪些名字。


二、静态对象的析构函数

静态对象的析构函数(包括静态存储的所有对象,不仅仅是上例中的局部静态变量)在程序从main() 块中退出时,或者标准的C 库函数exit()被调用时才被调用。多数情况下main()函数的结尾也是调用exit()来结束程序的。这意味着在析构函数内部使用exit()是很危险的,因为这可能陷入一个死循环中。但如果用标准的C 库函数abort()来退出程序,静态对象的析构函数并不会被调用。

三、控制连接

一般情况下,在文件范围内的所有名字(既不嵌套在类或函数中的名字)对程序中的所有编译单元来说都是可见的。这就是所谓的外部连接,因为在连接时这个名字对连接器来说是可见的,外部的编译单元、全局变量和普通函数都有外部连接。内部连接的一个好处是这个名字可以放在一个头文件中而不用担心连接时发生冲突。

那些通常放在头文件里的名字,像常量、内联函数(inline function ),在缺省情况下都是内部连接的(当然常量只有在C + +中缺省情况下是内部连接的,在C 中它缺省为外部连接)。注意连接只引用那些在连接/装载期间有地址的成员,因此类声明和局部变量并没有连接。

四、静态成员函数

像静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全体服务而不是为一个类的部分对象服务。这样就不需要定义一个全局函数,减少了全局或局部名字空间的占用,把这个函数移到了类的内部。当产生一个静态成员函数时,也就表达了与一个特定类的联系。静态成员函数不能访问一般的数据成员,它只能访问静态数据成员,也只能调用其他的静态成员函数。通常,当前对象的地址(this)是被隐含地传递到被调用的函数的。但一个静态成员函数没有this ,所以它无法访问一般的成员函数。这样使用静态成员函数在速度上可以比全局函数有少许的增长,它不仅没有传递this所需的额外的花费,而且还有使函数在类内的好处。因为静态成员对象的初始化方法,我们可以把上述类的一个静态数据成员放到那个类的内部。

五、静态初始化的依赖因素

有三种方法来处理这一问题:
1) 不用它,避免初始化时的互相依赖。这是最好的解决方法。
2) 如果实在要用,就把那些关键的静态对象的定义放在一个文件中,这样我们只要让它们在文件中顺序正确就可以保证它们正确的初始化。
3) 如果我们确信把静态对象放在几个编译单元中是不可避免的(比方在编写一个库时,我们无法控制那些使用该库的程序员)这时我们可用由Jerry Schwarz 在创建iostream库(因为cin ,cout和cerr的定义是在不同的文件中)时提供的一种技术。这一技术要求在库头文件中加上一个额外的类。这个类负责库中静态对象的动态初始化。(类似于Singleton)


六、转换连接指定

如果C + +中编写一个程序需要用到C 库,那该怎么办呢?如果这样声明一个C 函数:
float f(int a,char b);  C++的编译器就会将这个名字变成像_f_int_int之类的东西以支持函数重载(和类型安全连接)。然而,C编译器编译的库一般不做这样的转换,所以它的内部名为_f。这样,连接器将无法解决我们C++对f()的调用。C++中提供了一个连接转换指定,它是通过重载extern关键字来实现的。extern后跟一个字
符串来指定我们想声明的函数的连接类型,后面是函数声明。                extern "C" float f(int a,char b);

这就告诉编译器f()是C连接,这样就不会转换函数名。标准的连接类型指定符有“C”和“C++”两种,但编译器开发商可选择用同样的方法支持其他语言。如果我们有一组转换连接的声明,可以把它们放在花括号:                                    extern "C" {  float f(int a); float g(int b);}

 

第10章   引用和拷贝构造函数

一、使用引用时有一定的规则:
1) 当引用被创建时,它必须被初始化。(指针则可以在任何时候被初始化。)2) 一旦一个引用被初始化为指向一个对象,它就不能被改变为对另一个对象的引用。(指针则可以在任何时候指向另一个对象。)3) 不可能有N U L L 引用。必须确保引用是和一块合法的存储单元关连。

二、参数传递准则

当给函数传递参数时,人们习惯上应该是通过常量引用来传递。虽然最初看起来似乎仅出于对效率的考虑(通常在设计和装配程序时并不考虑效率),但像本章以后部分介绍的,这里将会存在很多的危险。(常量引用的使用不仅仅提高效率,而且避免了拷贝构造函数的带来的麻烦)拷贝构造函数需要通过值来传递对象,但这并不总是可行的。这种简单习惯可以大大提高效率:传值方式需要调用构造函数和析构函数,然而如果不想改变参数,则可通过常量引用传递,它仅需要将地址压栈。事实上,只有一种情况不适合用传递地址方式,这就是当传值是唯一安全的途径,否则将会破坏对象时(而不是修改外部对象,这不是调用者通常期望的)  (疑问,什么时候出现这种情况,必须要传值呢?)

三、函数调用栈的框架

当编译器为函数调用产生代码时,它首先把所有的参数压栈,然后调用函数。在函数内部,产生代码,向下移动栈指针为函数局部变量提供存储单元。(在这里“下”是相对的,在压栈时,机器栈指针可能增加也可能减小。)在汇编语言CALL 中,CPU 把程序代码中的函数调用指令的地址压栈,所以汇编语言RETURN 可以使用这个地址返回到调用点。当然,这个地址是非常神圣的,因为没有它程序将迷失方向。这儿提供一个在C ALL 后栈框架的样子,此时在函数中已为局部变量分配了存储单元。 图1 函数参数
返回地址
局部变量
 对于函数返回值的存放位置的思考:1)栈:如果调用函数试图从普通函数中返回堆栈中的值将会发生什么。因为不能触及堆栈返回地址以上任何部分,所以函数必须在返回地址下将值压栈。但当汇编语言RETURN执行时,堆栈指针必须指向返回地址(或正好位于它下面,这取决于机器。),所以恰好在RETURN语句之前,函数必须将堆栈指针向上移动,以便清除所有局部变量。如果我们试图从堆栈中的返回地址下返回数值,因为中断可能此时发生,此时是我们最易被攻击的时候。这个时候ISR将向下移动堆栈指针,保存返回地址和局部变量,这样就会覆盖掉我们的返回值。(你的ISR已经移动了,但是值还是放在下面的内存中,已经归还给程序进程了。因为这是突然来了另外一个函数调用时,就覆盖掉你刚才的内容了)。2)全局数据区:我们的下一个想法可能是在全局数据区域返回数值,但这不可行。重入意味着任何函数可以中断任何其他的函数,包括与之相同的函数。因此,如果把返回值放在全局区域,我们可能又返回到相同的函数中,这将重写返回值。对于递归也是同样道理。3)寄存器:寄存器,问题是当寄存器没有足够大用于存放返回值时该怎么做。答案是把返回值的地址像一个函数参数一样压栈,让函数直接把返回值信息拷贝到目的地。这样做不仅解决了问题,而且效率更高。程序在调用的时候会将你的返回值的地址压栈,然后呢在被调用的函数中就把值直接放到某个内存区,而把内存地址记录下来。在返回的时候,再将该内存内容拷贝给你的返回值赋给的变量。


第11章 运算符重载

一、语法

函数参数表中参数的个数取决于两个因素:
1) 运算符是一元的(一个参数)还是二元的(两个参数)。
2) 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数
(对于一元没有参数,对于二元是一个参数—对象变为左侧参数)。

二、自增和自减

重载的++和--号运算符出现了两难选择的局面,这是因为希望根据它们出现在它们作用的对象前面(前缀)还是后面(后缀)来调用不同的函数。解决是很简单的,但一些人在开始时却发现它们容易令人混淆。例如当编译器看到++a(先自增)时,它就调用operator++(a) ;但当编译器看到a++时,它就调用operator++(a,int)。即编译器通过调用不同的函数区别这两种形式。

四、参数和返回值

1) 对于任何函数参数,如果仅需要从参数中读而不改变它,缺省地应当按const引用来传递它。普通算术运算符(像+和-号等)和布尔运算符不会改变参数,所以以const引用传递是使用的主要方式。当函数是一个类成员的时候,就转换为const成员函数。只是对于会改变左侧参数的赋值运算符(operator-assignment,像+=)和运算符'=',左侧参数才不是常量(constant),但因为参数将被改变,所以参数仍然按地址传递。

2) 应该选择的返回值取决于运算符所期望的类型。(可以对参数和返回值做任何想做的事)如果运算符的效果是产生一个新值,将需要产生一个作为返回值的新对象。例如,integer::operator+必须生成一个操作数之和的integer 对象。这个对象作为一个const通过传值方式返回,所以作为一个左值结果不会被改变。

3) 所有赋值运算符改变左值。为了使得赋值结果用于链式表达式(像A=B=C),应该能够返回一个刚刚改变了的左值的引用。但这个引用应该是const还是nonconst呢?虽然我们是从左向右读表达式A=B=C ,但编译器是从右向左分析这个表达式,所以并非一定要返回一个nonconst值来支持链式赋值。然而人们有时希望能够对刚刚赋值的对象进行运算,例如(A=B).foo(),这是B 赋值给A后调用foo( )。因此所有赋值运算符的返回值对于左值应该是nonconst 引用。(为了便于级联,所以虽然A已经在函数中改变了,但是还是需要返回一个引用;这里应该返回nonconst,因为foo作为一个member function,并非一定是const函数,所以如果A=B返回的值是const,那么将不能调用foo函数)

4) 对于逻辑运算符,人们希望至少得到一个int返回值,最好是bool返回值。(在大多数编译器支持C++内置bool类型之前开发的库函数使用int或typedef 等价物)

5) 因为有前缀和后缀版本,所以自增和自减运算符出现了两难局面。两个版本都改变对象,所以不能把这个对象看作一个const。因此,前缀版本返回这个对象被改变后的值。这样,用前缀版本我们只需返回*this作为一个引用。因为后缀版本返回改变之前的值,所以被迫创建一个代表这个值的单个对象并返回它。因此,如果想保持我们的本意,对于后缀必须通过传值方式返回。(注意,我们经常会发现自增和自减运算返回一个int值或bool值,例如用来指示是否有一个循环子(iterator)在表的结尾)。现在问题是:这些应该按const被返回还是按nonconst被返回?如果允许对象被改变,一些人写了表达式(++A).foo(),则foo()作用在A上。但对于表达式(A++).foo(),foo()作用在通过后缀运算符++号返回的临时对象上。临时对象自动定为const,所以被编译器标记。但为了一致性,使两者都是const更有意义,就像这儿所做的。因为想给自增和自减运算符赋予各种意思,所以它们需要就事论事考虑。也就是说都返回const数据

1. 按const通过传值方式返回
按const通过传值方式返回,开始看起来有些微妙,所以值得多加解释。我们来考虑二元运算符+号。假设在一个表达式像f(A+B)中使用它,A+B的结果变为一个临时对象,这个对象用于f()调用。因为它是临时的,自动被定为const,所以无论使返回值为const还是不这样做都没有影响。

2. 返回效率
当为通过传值方式返回而创建一个新对象时,要注意使用的形式。例如用运算符+号:return integer (left.i + right.i) ;一开始看起来像是一个“对一个构造函数的调用”,但其实并非如此。这是临时对象语法,它是这样陈述的:“创建一个临时对象并返回它”。因为这个原因,我们可能认为如果创建一个命名的本地对象并返回它结果将会是一样的。其实不然。如果像下面这样表示,将发生三件事。首先,tmp对象被创建,与此同时它的构造函数被调用。然后,拷贝构造函数把tmp拷贝到返回值外部存储单元里。最后,当tmp在作用域的结尾时调用析构函数。integer tmp(left.i + right.i) ;  return tmp ;  相反,“返回临时对象”的方法是完全不同的。看这样情况时,编译器明白对创建的对象没有其他需求,只是返回它,所以编译器直接地把这个对象创建在返回值外面的内存单元。因为不是真正创建一个局部对象,所以仅需要单个的普通构造函数调用(不需要拷贝构造函数),并且不会调用析构函数。因此,这种方法不需要什么花费,效率是非常高的。

五、灵巧(smart)指针

如果想为类包装一个指针以使得这个指针安全, 或是在一个普通的循环子(iterator)的用法中,则这样做特别有用。循环子是一个对象,这个对象可以作用于其他对象的包容器或集合上,每次选择它们中的一个,而不用提供对包容器实现的直接访问。(在类函数里经常发现包容器和循环子。)灵巧指针必须是成员函数。它有一个附加的非典型的内容:它必须返回一个对象(或对象的引用),这个对象也有一个灵巧指针或指针,可用于选择这个灵巧指针所指向的内容.

六、 

基本规则              运算符                          建议使用                      
         所有的一元运算符              成员
           = ( ) [] ->           必须是成员
 += -= /= *= ^= &= |= %= >>= <<=             成员
        所有其他二元运算符             非成员


 七、拷贝构造函数和运算符=

foo B ;
foo A = B ;  //  定义了对象A 。一个新对象先前不存在,现在正被创建。因为我们现在知道了C++编译器关于对象初始化是如何保护的,所以知道在对象被定义的地方构造函数总是必须被调用。A是从现有的foo对象创建的,所以只有一个选择:拷贝构造函数。所以虽然这里只包括一个‘=’,但拷贝构造函数仍被调用。
A = B ;      //  在‘=’左侧有一个以前初始化了的对象。很清楚,不用为一个已经存在的对象调用构造函数。在这种情况下,为A调用foo::operator=,把foo::operator=右侧的任何东西作为参数。当然这里的赋值符也可以重载

 

第13章  继承和组合

一、继承和组合

组合:简单地创建一个包含已存在的类对象的新类,这称为组合,因为这个新类是由已存在类的对象组合的。对于新类的public接口函数,包含对嵌入对象的使用,但不必模仿这个嵌入对象的接口。

继承:创建一个新类作为一个已存在类的类型,采取这个已存在类的形式,对它增加代码,但不修改它。

二、构造函数和析构函数的次序

构造在类层次的最根处开始,而在每一层,首先调用基类构造函数,然后调用成员对象构造函数。调用析构函数则严格按照构造函数相反的次序—这是很重要的,因为要考虑潜在的相关性。对于成员对象,构造函数调用的次序完全不受在构造函数
的初始化表达式表中次序的影响。该次序是由成员对象在类中声明的次序所决定的。

三、非自动继承的函数

构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。所以,在整个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承。operator= 也不能被继承,因为它完成类似于构造函数的活动。

四、组合与继承的选择

组合通常在希望新类内部有已存在类性能时使用,而不希望已存在类作为它的接口。这就是说,嵌入一个计划用于实现新类性能的对象,而新类的用户看到的是新定义的接口而不是来自老类的接口。

因为正在由一个已存在的类做一个新类,并且希望这个新类与已存在的类有严格相同的接口(希望增加任何我们想要加入的其他成员函数),所以能在已经用过这个已存在类的任何地方使用这个新类,这就是必须使用继承的地方。

五、对私有继承成员公有化


第14章  多态和虚函数

C++如何实现晚捆绑

编译器对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数地址.在每个带有虚函数的类中,编译器秘密地置一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。

 

如果没有数据成员, C + +编译器会强制这个对象是非零长度,因为每个对象必须有一个互相区别的地址。如果我们想象在一个零长度对象的数组中索引,我们就能理解这一点。一个“哑”成员被插入到对象中,否则这个对象就有零长度。

 

C + +对此提供了一种机制,称为纯虚函数。下面是它的声明语法:
virtual void x() = 0;
这样做,等于告诉编译器在V TA B L E中为函数保留一个间隔,但在这个特定间隔中不放地址。只要有一个函数在类中被声明为纯虚函数,则V TA B L E就是不完全的。包含有纯虚函数的类称为纯抽象基类。如果一个类的V TA B L E是不完全的,当某人试图创建这个类的对象时,编译器做什么呢?由于它不能安全地创建一个纯抽象类的对象,所以如果我们试图制造一个纯抽象类的对象,编译器就发出一个出错信息。这样,编译器就保证了抽象类的纯洁性,我们就不用担心误用它了。

纯虚函数防止产生V TA B L E,但这并不意味着我们不希望对其他函数产生函数体。我们常常希望调用一个函数的基类版本,即便它是虚拟的。把公共代码放在尽可能靠近我们的类层次根的地方,这是很好的想法。这不仅节省了代码空间,而且能允许使改变的传播变得容易。

对象切片:

当多态地处理对象时,传地址与传值有明显的不同。所有在这里已经看到的例子和将会看到的例子都是传地址的,而不是传值的。这是因为地址都有相同的长度,传派生类型(它通常稍大一些)对象的地址和传基类(它通常小一点)对象的地址是相同的.如果使用对象而不是使用地址或引用进行向上映射,发生的事情会使我们吃惊:这个对象被“切片”,直到所剩下来的是适合于目的的子对象。

#include <iostream>
using namespace std;

class base
{public:
    base(int  j)
 {
  cout<<"base()"<<endl;
  i=j;
 }
 base (base & b)
 { cout<<"base(base&)"<<endl;
   i=100;
 }
 virtual  int sum() {return i;};
private :
 int i;
};

class derive :public base
{
public:
 derive(int m,int k):base(k)
 {
    cout<<"derive()"<<endl;
    j=m;
 }
 int sum()
 {
  return(base::sum()+j);
 }
private:
 int j;
};

void call( base b)
{
 cout<< b.sum();
}

main()
{

 base   a(1);
 derive b(1,2);
 call(b);

}

 

三、构造函数调用次序
所有基类构造函数总是在继承类构造函数中被调用。因为构造函数有一项专门的工作:确保对象被正确的建立。派生类只访问它自己的成员,而不访问基类的成员,只有基类构造函数能恰当地初始化它自己的成员。如果不在构造函数初始化表达式表中显式地调用基类构造函数,它就调用缺省构造函数。如果没有缺省构造函数,编译器将报告出错。当继承时,我们必须完全知道基类和能访问基类的任何public和protected成员。这也就是说,当我们在派生类中时,必须能肯定基类的所有成员都是
有效的。在通常的成员函数中,构造已经发生,所以这个对象的所有部分的成员都已经建立。然而,在构造函数内,必须想办法保证所有我们的成员都已经建立。保证它的唯一方法是让基类构造函数首先被调用。这样,当我们在派生类构造函数中时,在基类中我们能访问的所有成员都已经被初始化了。在构造函数中,“必须知道所有成员对象是有效的”也是下面做法的理由:只要可能,我们应当在这个构造函数初始化表达式表中初始化所有的成员对象(放在合成的类中的对象)。只要我们遵从这个做法,我们就能保证所有基类成员和当前对象的成员对象已经被初始化。

第15章  模板和包容器

一、包容器所有权问题

一个包容器所包含的指针所指向的对象都仅仅用于包容器本身。在这种情况下,所有权问题简单而直观:该包容器拥有这些指针所指向的对象。由于通常大多数都是上述情况,因此把包容器完全拥有包容器内的指针所指向的对象的情况定义为缺省情形。处理所有权问题的最好方法是由用户程序员来选择。这常常用构造函数的一个参数来完成,它缺省地指明所有权(对于典型理想化的简单程序)。另外还有读取和设置函数用来查看和修正包容器的所有权。假若包容器内有删除对象的函数,包容器所有权的状态会影响删除,所以我们还可以找到在删除函数中控制析构的选项。我们可以对在包容器中的每一个成员添加所有权信息,这样,每个位置都知道它是否需要被销毁,这是一个引用记数变量,在这里是包容器而不是对象知道所指对象的引用数。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/jinfeng_wang/archive/2002/06/17/18841.aspx

原创粉丝点击