分类目录归档:谜のC语言

第二章:对象的创建与使用 (1) (基础知识与基本语法)

可以说,从这里开始算是C++的起步了,在接触对象之前,需要了解如何在一段代码当中创建以及使用一个对象。而在这之前,也需要一些基础的知识来作为铺垫。

首先,无论是什么语言,它们最终都会通过一系列的变化,从一种我们人类易于理解的形式,变化成计算机可以执行的形式。而中间这个转化的过程就是翻译,执行翻译操作的工具叫做翻译器。翻译器分为两种,一种是解释器,一种是编译器。解释器通过读取源代码并将其转化成可执行的机器指令,一般情况下,解释器会将自身先加载到内存,然后通过读取一行或者一小段源代码,然后将其在内存中解释成为机器指令,再对其进行执行,然后再读取下一行或者一小段代码。这样会使代码的执行效率非常低下,但是从写代码到执行代码中间并不需要太多时间,而且一旦发生了错误,解释器便可以很快速的指出错误所在。所以解释器的优点在于“较好的交互性和适用于快速程序开发”。当然也有一些解释语言为了提高自身效率,会先将整个程序转化成某种中间语言,然后用更为快速的解释器来执行它。而且解释器还有一个很明显的优点就是可以一定程度上直接将源代码复制到不同的平台使用,甚至可以不用经过太多的处理,基于不同平台的解释器会将代码解释为本平台可以运行的指令并运行它。解释器的工作模式可以总结为边翻译边运行。而编译器则不同,编译器会将源代码直接编译为汇编语言或者机器指令,这个过程比较复杂,一般分多步来执行,关于这方面的知识可以参见编译原理方面的书籍。至少可以看出,这样一来程序节省了运行时的翻译所需时间,大大的提高了程序的运行效率。其工作模式可以总结为先翻译后运行。但是弊端也显而易见,首先从源代码转换到机器指令所耗费的时间将是很大的,而且修改一处将会导致所有的源码都需要重新编译,一旦发生了错误还很难发现错误的所在地,调试程序也成为了一件很难的事情。所幸的是,聪明的工程师们总能解决几乎所有可以被提出来的问题,例如C语言便采用了分段编译这样一个方法来解决第一个问题,将源代码分为一个一个区块,将每一块单独测试完成后便可以进行单独的编译,最后再使用连接器将各个区块的代码连接起来,加上可以被操作系统装载的部分指令,便生成了一个可执行程序。而想要对代码进行调试的话,便需要编译器在编译后的机器指令中间加入一些与源代码有关的信息,以便于一些源代码层的调试器来跟踪程序的执行情况,而在最后源代码可以正常工作后则可以由编译器可以产生不含这些信息的机器指令。我们常用的Visual C++可以通过在工程选项中选择生成debug版本或者release版本来控制这些信息的有无,(当然除此之外一般情况下release版本也会包含更多的优化,在编译中生成更快更短更少冗余的机器指令是编译过程中优化器所做的的工作),而在GNU C++编译器当中我们也可以通过添加debug参数来实现加入调试信息。有了这些东西很多时候会使解释性语言显得不那么的具有诱惑力,然而编译器编译后的程序的工作平台往往是局限的,想要适应更多的平台就必须使用编译器针对不同平台重新编译。这也是为了运行效率所作出的一点牺牲吧。

上一段中提到了C语言采用了分段编译来解决程序“牵一发而动全身”的问题,当然C++当中也采用了这一具有优势的特性,由其在大型工程中,很有可能一次全局的编译要耗费好几个小时甚至好几天的时间。但是在分段编译的时候,也有一些问题需要思考,首先对于一个代码块,当它要引用其他代码块或者数据块的时候,最起码要知道一些其他代码块或者数据块的必要信息,这样编译器才能为他正确的预留一个“接口”让连接器将它引用的代码块连接上来,并且提供给它正确的信息。对于一个数据块这个基本信息就是它的名字和类型组成,对于一个代码块则是它的名字还有它需要的信息类型以及执行完这个代码块之后可以得到的信息类型。当然在C和C++当中这些代码块的最小的单位为函数,而引用这些函数的动作被称为函数的调用,函数调用的时候便需要传递给函数它所需要的参数,并且读取函数执行完之后传递回来的返回值。编译器为了保证能够正确的为一个函数的调用预留接口,并且可以让编译器检查我们传入数据的正确性,于是要求我们在调用这个函数之前一定要对这个函数进行声明,这样编译器才可以获得足够的信息来避免错误。而这个函数的本体可以放置在另外的编译单元当中,而这包含了代码块的函数本体则称为函数的定义。可以由编译器对其进行单独的编译,只要正确的配置了连接器,连接器总能正确的将它们拼接起来。

“声明”是一个很有意思的概念,它会告诉编译器有这样一个名字的数据块或者函数,它是什么什么样子的,你可以在接下来的代码当中根据这些信息预留出它的接口,并让连接器将它的函数体连接上去,接下来的代码便可以正确运行。而“定义”则是告诉编译器下边花括号当中的代码块即是这个名字,或者请在程序运行至这个位置时为这个名字的变量分配空间。在C和C++当中,我们可以在很多地方进行相同的声明,但是定义只能出现在一个地方,如果一个函数或者变量具有多个定义的话,连接器将会报告出错误。

以下为一些详细的使用方法,几乎全部摘自书上。

首先声明一个函数:

double func1(int, float);

其中int表示函数返回值类型,func1为函数名称,可以是除了保留字以外的任何字符串,括号当中是函数的参数表,依次为函数输入参数的类型。当然函数声明的时候也是可以给参数命名的只不过这里的命名对于编译器来书毫无意义,它会将其忽略掉,不过给它命名会给程序员阅读代码带来很多便利。正如下面的声明和上面的声明意义完全相同。

double func1(int a, float b);

当然值得注意的一点是在C和C++当中空的参数列表具有不同的意思,对于C语言来说一个空的参数列表意味着这个函数可以匹配任意的参量表,而对于C++来说这意味着这个函数不能接受任何参数。当然如果需要在C语言当中表示这个函数不能接受任何参数的话需要在括号内加入void,而在C++当中表示这个函数可以接受任何参数的话需要在括号内加入省略号。示例如下:

int func2();

int func2(void);

int func2(…);

而函数的定义也不是特别复杂,定义一个函数即是把函数声明部分的分号替换成为花括号就好:

double func1(int a, float b) {}

花括号中间为函数的主体部分,值得注意的是,如果函数当中使用到了参数表中的某个参数,那么这个参数必须具有一个名字,即标识符,而如果始终没有用到某个参数,则这个参数可以只写它的参数类型,这样的参数称为占位符。一个用途是函数更新后参数需求减少时可以进行占位而不必让编译器弹出警告。

声明一个变量则稍稍有点复杂,这是一个耐人寻味的问题,因为当我们声明一个变量的时候,编译器便获取到了足够的信息可以对它进行内存分配,所以如果我们单独写出一行类似于:

int a;

这样的代码之后,编译器默认这是一条声明和定义。但是如果我们不想在这个地方定义变量的话,我们需要向编译器特别说明。通过在前边加上extern关键字来实现这一点:

extern int a;

这样就可以区分定义和声明一个变量了,当然函数也可以用相同的方法区分,不过会显得多此一举。

以上便是一些关于C++的基础的知识以及基础的语法,通过了解本章后半部分关于库及模板的介绍之后,便可以构建一个简单的C++程序了。我将它分为两部分来叙述。

第一章:对象导言

最近听了别人的安利,对Think in C++这本书产生了一定的兴趣,刚开始读的时候,觉得甚是晦涩,因为它满篇的专业名词让我无所适从。但是庆幸的是,度过了第一章之后,后边的内容便变得平易近人,细细读来发现确实写的非常的精彩。在第一遍读完之后,觉得读的不是很细致,于是反过头又读一遍,觉得确实错过了很多东西。如今是第三遍翻到开头的部分,决心将它再读一遍。并且这次想要把读的时候想到的读到的一些有意思的东西记录下来,也希望能帮到一些人,更希望自己在之后看到自己当时所写,会有更深的理解和感触。

这本书的第一章,可以算是对它两卷书所有内容的总结概括,信息量还是非常大的。首先,在这一章中,介绍了面向对象的基本概念。在我看来,面向对象,可以说是对脑力的一个解放,它将之前一直困扰着程序员的按照计算机的结构去思考问题,一举简化成为按照要解决的问题去思考问题,从某种程度上省去了程序员耗费在将实际问题转化为计算机模型中问题的脑力,降低了开发要求和开发成本。在实际写程序时最直观的感受就是面向过程的C语言大多忙于按照机器的思路去构造一个函数来解决某些问题,而C++则是引入对象的概念,如果是对象的使用者,则只用考虑如何去如何利用这个对象以及它的特性去做一些事情,而不是沿着机器的思路去思考它。而如果是对象的创建者,则只用考虑去如何描述一个实际的东西以及它的功能。也就是说,面向对象更加贴近于“如何解决一个问题”而不是“如何使用计算机”。可以说这是前人不断努力封装才得到的东西。

说到封装,C++是一个十分注重封装的语言。可以说,封装能力代表了代码有可能的复用能力,每一个经过深思熟虑和细心调试的代码块,都可以被当做一个整体封装起来,拿到别的地方去使用,这一点C++做的相当出色,因为它包含有详细的边界控制和多种代码复用的方法。对于一类对象或者实例,它们可以被描述为一个类,一个类具有其相应的接口以及内部数据,而C++在类的构建者和用户之间筑起了一堵高高的墙,即类的创建者可以自己控制类中数据及方法的显隐性,创建者可能不希望它的用户修改其中一些重要的东西,不希望其调用其中的一些函数方法,希望它们可以完全由自己掌控。而且这堵墙后边的实现方法其实也与用户毫无关系,用户只需要知道怎么去使用创建者留给自己的接口就好,这样一旦创建者对自己的类进行改进的时候,也可以减少使用者代码的修改量甚至使用者根本不需要更新代码。

关于代码的复用性,C++当中提供了很多种方法,首先通过一个类去构建多个相同类型的不同对象可以勉强当做是一种代码的复用。而如果将他人封装好的类生成对象嵌入到自己的类当中,即成员对象,也算得上是一种很好的代码复用。自然还有一种便是直接对他人封装好的类进行继承,我觉得扩展这个词可能比继承更为形象一点,就是通过改变或者增加或者减少基类的接口,来形成一个派生类,这个派生类则可以更好的去适应更为实际的问题,同时又不必为每一个派生类去单独设计所有代码,其中基础的部分可以进行封装并单独维护。而C++的虚函数及一些特性则会保证基类以及派生类之间相互转换,调用时的一些问题,详细的会在靠后的章节中来专门讲,这里不再赘述。

根据书中提到的Alan Kay对第一个成功的面向对象语言Smalltalk的总结,即1.万物皆对象 2.程序就是一组对象,对象之间相互通信通知做什么 3.每个对象都有自己的由其他对象构成的存储区 4.每个对象都有一个类型 5.一个特定类型的所有对象都能接收相同的消息。这些描述也符合C++当中对象的特点,可以说总结的非常的精炼。而在C++当中,类最原本的概念也就是将函数作为成员整合到struct当中去,使其不仅包含数据成员,同时包含成员函数,以构成最面向对象当中最基本的类。数据成员作为对象的描述,而成员函数也充当对象的接口,以及对象内部的运作工具。

总而言之,C++提供了一套完整而且又易于使用的面向对象体系,在这个体系当中我们可以尽力去研究我们需要解决的问题的本身,而不必花费太多脑力去将其转换为计算机的思路。虽然C++相较于C语言做了很多的封装,而且也提供了更丰富的库,但是在实际使用当中C++代码的运行性能并不逊色于C语言,这可能也是C++最为可贵的地方,而且相较于C语言代码,用书中的原话:一个写的好的C++程序一般比等价的C程序简单的多和更容易理解。而且根据我自己的感受来看,熟练的掌握C++当中的标准函数库将会给代码的开发带来前所未有的便利。最后,再次翻开这本书,便向C++又迈进了一步,参透一门语言的思想远比学会怎么去用它难得多,我想我还欠些火候。