编 程 基 础 - baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2...

82
1 编 程 基 础 本章旨在解释编程语言的基础知识。读者将会了解数据类型、指针、作用域的规则、 程序的内存布局、参数传递的方式、编程语言的类型,以及与之相关的一些知识。 1.1 变量 在给变量下定义之前,我们首先来谈谈它与传统数学方程之间的联系。大家从小就求 解过许多数学方程,例如下面的方程: x 2 +2y-2=1 大家并不需要关注这个方程式的具体用途,而是要注意:该方程用 x y 这样的名称 name )来存放数值(value )或数据(data ),这意味这些名称是一种用来表示数据的占位符 号。同理,在计算机科学中,我们也需要用类似的办法来存放数据,这就是变量(variable 的用途。 1.2 数据类型 在上面那个方程式里面,像 x y 这样的变量既可以取整数值(例如 1020),又可以 取实数值(例如 0.235.5),还可以仅仅在 0 1 之间取值。要想解开这个方程,我们必 须确定:其中的变量可以取什么类型的数值,而这正是数据类型(data type )一词所要表达 的意思。编程语言中的数据类型,是带有预定义值(predefined value )的一组数据。如整数 integer )、浮点数( floating point unit number )、字符( character )、字符串( string )等,都是 数据类型。 Chapter 1

Upload: others

Post on 08-Jul-2020

9 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章

编 程 基 础

本章旨在解释编程语言的基础知识。读者将会了解数据类型、指针、作用域的规则、

程序的内存布局、参数传递的方式、编程语言的类型,以及与之相关的一些知识。

1.1 变量

在给变量下定义之前,我们首先来谈谈它与传统数学方程之间的联系。大家从小就求

解过许多数学方程,例如下面的方程:

x2+2y-2=1大家并不需要关注这个方程式的具体用途,而是要注意:该方程用 x 与 y 这样的名称

(name)来存放数值(value)或数据(data),这意味这些名称是一种用来表示数据的占位符

号。同理,在计算机科学中,我们也需要用类似的办法来存放数据,这就是变量(variable)的用途。

1.2 数据类型

在上面那个方程式里面,像 x 与 y 这样的变量既可以取整数值(例如 10、20),又可以

取实数值(例如 0.23、5.5),还可以仅仅在 0 与 1 之间取值。要想解开这个方程,我们必

须确定:其中的变量可以取什么类型的数值,而这正是数据类型(data type)一词所要表达

的意思。编程语言中的数据类型,是带有预定义值(predefined value)的一组数据。如整数

(integer)、浮点数(floating point unit number)、字符(character)、字符串(string)等,都是

数据类型。

Chapter 1

Page 2: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

2   程序员面试手册:概念、编程问题及面试题

计算机的内存里面装的全都是二进制的 0 和 1,然而从解决编程问题的角度来看,我们

很难直接用 0 和 1 来表示问题的解法。为此,编程语言和编译器提供了各种数据类型。如

把 2 字节合起来,就可以构成一个 integer(至于它的实际取值是什么,则要由编译器来定),

把 4 字节合起来,就可以构成一个 float,这就意味着,内存中相连的 2 字节(byte,共有

16 个二进制位(bit))可以称为一个 integer,而相连的 4 字节(共有 32 个二进制位)则可

以称为一个 float。这些数据类型,都有助于简化编程工作。从最宏观的层面上说,数据类

型可以分成两大类:

1)由系统所定义的(system-defined data)数据类型(也称为原始数据类型)。

2)由用户所定义的(user-defined)数据类型。

由系统所定义的数据类型,也叫作原始数据类型(primitive data type)。有很多编程语

言都提供了 int(整型)、float(单精度浮点数型)、char(字符型)、double(双精度浮点数型)

及 bool(布尔型)等原始数据类型。至于每一种原始数据类型到底会占多少字节,则需要由

编程语言、编译器及操作系统来决定。同一种原始数据类型,在各种编程语言里,可能会

有不同的大小。由于相同的数据类型在各种环境下所占据的字节数不固定,因此,该类型

所能取的数值种数(或者说该类型的定义域,domain)也会随之变化。

如 int 既可以用 2 字节来表示,也可以用 4 字节来表示。如果用 2 字节(16 位)来表

示,那么取值范围就是 -32 768~+32 767(或 -215~215-1);如果用 4 字节(32 位)来表示,

那么取值范围则是 -2 147 483 648~+2 147 483 647(或 -231~231-1)。其他的数据类型也可

以这样来理解。

由用户所定义的数据类型是指用户自己编写的数据类型。如果系统所定义的数据类型

无法满足需要,那么大多数编程语言都允许用户自己来定义数据类型。例如,C/C++ 语言

的结构体(structure)以及 Java 语言的类(class)就属于这种类型。

如下面这段代码,可以把很多种由系统所定义的数据类型组合成一个由用户所定义

的数据类型,并将其命名为 newType。这使得我们可以更加灵活、更加方便地处理计算机

内存。

1.3 数据结构

由上可知,当我们把数据放入变量之后,必须寻找某种机制来操作这些数据,以便解

决问题。数据结构(data structure)就是这样一种可以存储并整理数据的机制,它使得数据

能够在计算机上高效地得到运用。数据结构是用来整理并存放数据的特定格式,数组、文

件、链表、栈、队列、树、图等是通用的数据结构。

Page 3: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   3

根据元素的组织方式,数据结构可以分成以下两类。

1)线性的数据结构:元素需要按照先后顺序来访问,但并不是说所有元素,都得前后

相连地存放。例如:链表、栈、队列。

2)非线性的数据结构:其中的元素可以按照非线性的顺序来存储、访问。

1.4 抽象数据类型

在给抽象数据类型(Abstract Data Type,ADT)下定义之前,我们先来看看由系统所定

义的数据类型。在默认情况下,所有的原始数据类型(如 int、float 等)都支持如加法与减

法这样的基本运算(operation) 。原始数据类型所具备的这些运算是系统早已经实现好的,

而由用户所定义的数据类型则不是这样,因为用户需要自己定义相关的运算。用户可以在

实际用到某个类型的时候,再来实现该类型所应具备的运算。一般来说,用户在定义数据

类型时,会把这些类型所支持的运算也定义出来。1

为了简化问题的解决流程,我们把数据结构与其运算合起来称为抽象数据类型(ADT)。ADT 由两部分组成:

T 数据的声明

T 运算的声明

常用的 ADT 包括:链表、栈、队列、优先级队列、二叉树、字典、并查集(Disjoint Set,支持合并与查找操作)、哈希表、图等。例如,栈是一种采用 LIFO(Last-In-First-Out,后进先出)机制来存储对象的数据结构。最后一个插入栈中的元素,将会先被移出。该结构

的常用操作包括:创建栈、将元素推入栈、从栈里面弹出元素、查找当前的栈顶,以及确

定栈里面的元素个数等。

定义 ADT 时,不需要考虑实现细节。因为这些细节可以等真正用到相关操作时再去考

虑。不同类型的 ADT,适合解决不同类型的问题,其中某些 ADT 是专门用来解决特殊任

务的。

1.5 内存与变量

首先,我们来看看计算机怎样管理内存。内存可以看作由字节所构成的数组。其中

每个位置(location)都对应一个地址(address),这个地址也可以叫作数组的下标或索引

(index to array)。一般来说,0 号地址不能用作有效的内存位置。要强调的重点是:无论数

据放在内存中的哪个字节里,该字节的内存地址(也就是内存位置),都是一个整数。在下

面这张图中,n 的值要由系统在表示内存地址时所用的二进制位数值来确定。

 在不引发混淆的情况下,本书用运算与操作这两种说法来翻译 operation 一词。—译者注

Page 4: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

4   程序员面试手册:概念、编程问题及面试题

在读取或写入某个内存位置时,CPU 需要将其地址发送给内存控制器(memory controller),以便访问这个位置。创建变量时(例如在 C 语言中采用“ int X”这样的写法来

创建变量),编译器会分配一块连续的内存位置,其大小要由变量的大小来定。

编译器在把某块内存分配给变量 X 的时候,还会同时维护一份标记,用来把 X 这个

变量名与其内存块的首个字节所具备的地址关联起来,这种关联机制,有时称为符号表

(symbol table)。我们在访问变量 X 的时候,使用的是“X=10”这样的写法,而编译器看到

这种写法时,则可以通过刚才那份标记来确定该变量的位置,并将 10 这个值写入此位置。

变量的大小:sizeof 运算符用来查询变量的大小,也就是查询变量所占据的内存字节

数。如在某些计算机中,sizeof(X) 的结果是 4。这意味着该变量在内存中要占用连续的 4字节。如果 X 的地址是 2000,那么它所占用的那 4 内存位置就是:2000、2001、2002、2003。

变量的地址:在 C 语言中,可以通过取地址运算符 &(address-of operator)来查询变

量的地址。下面这段代码会打印出变量 X 的地址。一般来说,地址以十六进制来表示,因

为这样显得更加紧凑,而且如果地址值特别大,那么这种格式理解起来会容易一些。

1.6 指针

指针也是一种变量,用来保存其他变量的地址。

1.6.1 指针的声明

声明指针时,需要写出该指针所指向的变量类型。这意味着我们必须知道:该指针所

存放的地址对应于哪种类型的变量。声明指针所用的代码是非常简单的。下面来看几个指

Page 5: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   5

针声明的例子:

ptr1 是指向 int 变量的指针,ptr2 是指向 float 的指针,ptr3 是指向 unsigned int 的指针,

ptr4 是指向 char 的指针。最后那个 ptr5 是可以指向任意数据的指针。这种指针称为 void 指

针,该指针在操作上会受到一些限制。

1.6.2 指针的使用

前面说过,指针是用来存放地址的。这意味着,我们可以把变量的地址赋值给指针。

例如下面这段代码:

这段代码首先声明了名为 X 的整数型变量,并将其值初始化为 10。然后,创建指向 int的指针 ptr,并把 X 的地址赋给它,这就叫“令指针 ptr 指向 X”。有一种常见的指针运算

称为间接参照(indirection) ,可以用来访问它所指向的那块内存中的数据。该运算用星号

表示。这个星号与声明指针时所用的那个星号不是同一个意思。*ptr 这样的写法可以用来访

问 ptr 所指向的那块内存中的内容。我们用下面这段代码来演示指针的间接参照功能。1

首先,这段代码按照以前的办法,声明变量 X 以及指向该变量的指针 ptr。然后,打印

X 的值(这个值是 10)与 ptr 的值。由于 ptr 是个指针,因此,打印出来的内容是 ptr 所指

向的内存地址。接下来,打印 *ptr,它表示该指针所指向的那个内存位置中所存放的数值

(由于指针所指向的地址与变量 X 所占据的地址相同,因此打印出来的值,依然是 10)。

最后,我们用 *ptr=25 这样的写法来修改 ptr 所指向的那块内存中的内容。这种写法的

意思是,把值 25 赋给由 ptr 所指向的那块内存。这样做的实际效果是令 X 的值发生变化。

 也称为解引用或解除参照(dereference)。—译者注

Page 6: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

6   程序员面试手册:概念、编程问题及面试题

由于 ptr 所指向的地址正是变量 X 所在的位置,因此,修改该地址中的内存内容,就等于

修改 X 的取值。

void 指针可以指向任意类型的数据,但它有一项限制,就是无法解引用(dere-ferenced)。因为一种变量类型所占据的内存大小不一定相同。如在 32 位计算机上,int 型的

变量占 4 字节,而 short 型的变量则占 2 字节。如果想把某个地址中存放的实际数值完整地

读取出来,那么编译器必须要知道自己应该读取多少个连续的内存位置。

1.6.3 指针的操纵

指针是一种特别有用的东西,它可以做算术运算。细心的读者或许已经发现了这一点,

因为我们前面说过,指针其实就是个整数,只不过它与一般整数有一些微小的区别,使得

我们可以更直观、更简单地使用它。试试下面这段代码:

这段代码声明了一个叫作 cptr 的指针,并把地址 z 赋给它。打印出指针的内容(也

就是指针所指的地址 2),对其递增,接下来,再打印其内容。第 1 次打印出来的结果是

2,第 2 次打印出来的结果是 3,这与我们所想的完全一样。可是,下面这段代码则有所

不同:

在笔者的计算机上运行这段代码,所打印出来的 iptr 值分别是 2 与 6。为什么这个指针

在递增之后,指向了地址 6,而不是像刚才那样,指向地址 3 呢?这与变量的大小有关。int类型在笔者的计算机上占 4 字节,这意味着地址 2 中的 int 数据实际上会占用 2、3、4、5这 4 个内存位置。

要想访问下一个 int,就必须查看 6、7、8、9 这 4 个地址中的内容才行。因此,给

指向 int 的指针加 1,并不等于给该指针所代表的内存地址加 1。由于指针的加 1 操作

必须使该指针能够指向下一个 int 型的变量,因此,它要移动 4 字节才行。而在刚才的

那个例子中,由于 char 的大小恰好是 1,因此,对指向 char 的指针加 1,也就相当于

给该指针所对应的内存地址加 1。执行完这项操作之后,指针就指向了下一个 char 型的

变量。

void 指针所受的另一项限制在于,它无法做算术运算。这是因为,编译器并不清楚下

一个变量与当前所指的这个变量之间到底隔了几字节。这种指针一开始只能用来存放内存

地址,等到稍后需要访问地址中的数据时,必须先将其转换成某种特定的指针。

Page 7: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   7

1.6.4 数组与指针

数组与指针之间有着紧密的联系。由于这种联系特别强烈,因此,在绝大多数情况下,

都可以将二者视为一物。数组的名字可以理解成指向内存块起始处的指针,这块内存的大

小与数组相同。如可以像在指针之间赋值那样,直接把数组赋给指针,令该指针指向数组

的开头。

这样一来,就可以把指针想象成数组,并通过该指针来访问数组中的内容。如 ptr[2]=25这样的写法就是完全可行的。反过来说,数组本身也可以被当成指针来用,例如 *array=4 实

际上就是 array[0]=4。更宽泛地说,*(array+n) 与 array[n] 是等效的。

数组与指向其开头的指针之间只有一个区别,那就是:编译器针对前者,会保存一些

信息,用以记录存储方面的要求。

如果把 sizeof 运算符分别运用在数组与指针上,那么 sizeof(ptr) 会给出指针本身所占据

的空间(在笔者的计算机上是 4),而 sizeof array 则会给出整个数组所占据的空间(在笔者

的计算机上是 20,因为数组里面共有 10 个元素,每个元素占 2 字节)。

1.6.5 动态内存分配

在前面的几节里面,我们已经看到,指针可以用来存放其他变量的地址。此外,它还

有另一种用法,就是保存一系列内存位置所在的地址,这块内存,在编译期并不对应于某

个具体的变量名,而是要等到程序运行起来之后,再由系统做动态分配,这样的内存有时

被称为堆(heap)。

为了在运行期分配内存,C 语言提供了一种由 malloc 函数来完成的动态内存分配机制。

该函数可以按照调用方所请求的大小来分配内存,并返回一个指向这块内存的指针。与之

相对,C 语言也提供了 free 函数,用来解除(deallocate)之前分配的内存块。

free 函数以指针为参数。下面这段代码会动态地分配含有 5 个整数的数组,并把指向

该数组的指针传给 free 函数,令以前分配的内存得以释放。

本例中的 count * sizeof(int) 用来计算数组所占据的字节数。计算方法是:将数组中的元

素个数与每个元素的大小相乘。如果要分配的是整型数组,那么就与 sizeof(int) 相乘。

1.6.6 函数指针

与数据一样,可执行的代码(包括函数)也存放在内存中,因此,我们可以取得函数的

Page 8: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

8   程序员面试手册:概念、编程问题及面试题

地址,并将其赋给指针。但问题在于,这样的指针可以做什么用呢?一般来说,我们会用

函数指针来保存函数的地址,并且可以对该指针做间接参照,以便调用它所指向的那个函

数。然而,对这种指针是有一些限制的,我们只能对它做赋值与间接参照操作,而不能在

上面做算术运算。这是因为,对函数指针来说,其所指向的函数并没有顺序可言,也就是

说,函数可以存放在内存中的任意位置上,而不像普通的数据那样连续存放,使得系统可

以根据前一个数据的位置推测后一个数据的位置。下面这段代码演示了怎样创建并使用函

数指针。

首先,创建名为 fptr 的函数指针,令其可以指向那种以 int 为参数并返回 int 值的函数。

然后,令该指针指向函数 function1,并通过 fptr 指针来调用这个函数,以便打印出函数在

参数值为 5 时的取值。最后,令 fptr 指向函数 function2,并再度通过 fptr 指针来调用函数,

以打印出函数在参数值为 10 时的取值。

1.7 参数传递的方式

在讨论参数传递的方式之前,我们先看看相关的术语。

1.7.1 实际参数与形式参数

假设函数 B() 是在函数 A() 里被调用的,那么在这种情况下,A 称为主调函数(caller function),B 称为受调函数(called function、callee function,被调用的函数)。由 A 发送给

B 的参数,叫作实际参数(实参),而 B 本身所声明的参数,则称为形式参数(形参)。

在下面这个例子中,func 函数是从 main 函数中被调用的,因此,我们把 main 称为主

调函数,把 func 称为受调函数。此外,func 函数的参数 param1 与 param2 称为形式参数,

而 main 函数在调用 func 时所传入的 i 与 j 则称为实际参数。

1.7.2 参数传递的语义

参数传递有下列 3 种逻辑语义。

1)IN(输入):由主调方传给受调方。形式参数可以根据实际参数来取值,但是不能把

Page 9: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   9

值返回给实际参数。

2)OUT(输出):受调方把值写入主调方。形式参数可以把值返回给实际参数,但不能

根据实际参数来取值。

3)IN/OUT(输入 / 输出):调用方把变量值告诉受调方,受调方可以更新这些变量。

形式参数既可以根据实际参数来取值,也可以把值返回给实际参数。

1.7.3 各种编程语言所支持的参数传递方式

参数传递方式 支持该方式的语言

按值传递(pass by value) C、Pascal、Ada、Scheme、Algol68

按结果传递(pass by result) Ada

按值 - 结果传递(pass by value-result) Fortran、(某些情况下的)Ada

按引用传递(pass by reference) C(通过指针来实现)、Fortran、Pascal(用 var 来声明函数的参数)、Cobol

按名称传递(pass by name) Algol60

1.7.4 按值传递

这种传递方式采用 IN 模式的语义。形式参数相当于例程、函数、子程序范围 内的新

局部变量,它的值会根据实际参数的值来初始化。对形式参数本身所做的修改并不会返回

给调用方。1

如果采用按值传递的方式,那么程序就会像对待普通的局部变量那样,把形式参数分

配在栈上。这种方式有时也称作按值调用(call by value)。它的好处在于,实际参数能够免

遭修改。

由于按值传递的方式通常是用复制来实现的,因此,它有 3 个缺陷:

1)程序的效率会因为分配存储空间而降低。

2)程序的效率会因为复制数值而降低。

3)为对象与数组做复制的开销是比较大的。

代码示范:在下面这段代码中,main 函数会给 func 函数传入两个值,也就是 5 和 7。func 在收到了这两个值的复制后,通过 a 与 b 这两个标识符来操作它们。此函数将修改 a的值。等到控制权交回 main 后,我们会发现,x 与 y 的值并没有改变。

 在不引发混淆的情况下,本书采用范围与作用域这两种说法来翻译 scope 一词。—译者注

Page 10: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

10   程序员面试手册:概念、编程问题及面试题

该程序的 func 函数所输出的值是:a=12,b=7 ;而 main 函数所输出的值则是:x=5,y=7。

1.7.5 按结果传递

这种方式采用 OUT 模式的语义。形式参数相当于函数范围内的新局部变量。实际参数

并不会向形式参数传递数值。等到控制权即将返回给调用方的时候,程序会把形式参数的

值写回实际参数中。

这种方式有时也称为按结果调用(call by result)。实际参数必须是变量,也就是说,可

以采用 foo(x) 及 foo(a[1]) 这样的写法来调用,但是不能采用 foo(3) 或 foo(x * y) 等写法来

调用。

1.7.6 有可能发生的参数冲突

如果主调方是以 write(p1, p1) 的形式来调用函数的,而函数的两个形式参数又具有不

同的名称,那么其中哪一个参数的值会在函数调用完毕之后写回 p1 呢?这要根据该函数向

实际参数拷贝时所遵循的顺序来定。总的来说,由于这种按结果传递的方式需要通过拷贝

来实现,因此,它有下面几个缺点:

1)程序的效率会因为分配存储空间而降低。

2)程序的效率会因为对值做复制而降低。

3)为对象与数组做复制的开销是比较大的。

4)无法用实际参数的值来给形式参数做初始化。

代码示范:由于 C 语言并不支持这种传递方式,因此,下面这段 C 语言风格的代码并

不针对特定的编程语言。

在这段代码中,main 函数使用 x 与 y 这两个变量,并将其传给 func 函数。由于采用的

是按结果传递的方式,因此,x 与 y 的值并不会复制到 func 函数的形式参数中。a 与 b 这两

个形式参数要由 func 自己来初始化。但实际上,它仅仅对 b 做了初始化,这就导致 a 的值

不可知,而 b 的值变为 5。func 函数执行完毕之后,a 与 b 的值,会分别复制回 x 与 y 中。

这段程序所输出的结果是:func 函数内的 a= 无意义的值,b=5 ;main 函数内的 x= 无

意义的值,y=5。

Page 11: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   11

1.7.7 按值 - 结果传递

这种方式采用 IN/OUT 模式的语义,它同时结合了按值传递与按结果传递这两种方式。

形式参数相当于函数范围内的新局部变量。实际参数的值会用来对形式参数做初始化。函

数在把控制权交给主调方之前,会将形式参数的值传回给实际参数。这种方式有时也称为

按值 - 结果调用(call by value-result)。

这种按值 - 结果传递的方式兼有按值传递与按结果传递的优点和缺点。一方面,与

按值传递类似,这种方式也需要给参数分配存储空间,而且要花时间去复制值。另一方

面,与按结果传递类似,这种方式在函数调用结束时,同样受困于实际参数的赋值顺序

问题。

代码示范:由于 C 语言不支持这种传递方式,因此下面这段 C 语言风格的代码并不针

对特定的编程语言。

这段代码的 main 函数使用了 x 与 y 这两个变量,并将其传给 func 函数。由于采用的

是按值 - 结果的传递方式,因此,x 与 y 的值会分别复制到 func 函数的形式参数里。于是,

a 与 b 这两个参数的值就分别是 5 和 7。接下来,func 函数会把 a 与 b 的值分别改为 10 和 5。等函数执行完毕之后,两者的

值会复制给主调函数中的 x 与 y。这种传递方式的缺点是,程序需要给每个参数分配新的

空间。

程序输出的结果是:func 函数内的 a=10,b=5;main 函数内的 x=10,y=5。

1.7.8 按引用传递(别名机制)

这种传递方式采用 IN/OUT 模式的语义。形式参数只不过是实际参数的别名而已。调

用方把参数传给受调函数之后,该函数对形式参数所做的修改会反映到调用方中。这种方

式有时也称为按引用调用(call by reference)或别名机制(aliasing)。它在时间与空间上都

很高效,然而也有着两个缺点:

1)程序会发生很多状况。

2)程序的代码不易读懂。

代码示范:在 C 语言中,当函数受到调用时,其指针参数会初始化为相应的指针值。

如果通过按引用传递的方式来调用下面这个 swapnum() 函数,那么函数执行完毕后,main函数里面的 a 值与 b 值就会互换。

Page 12: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

12   程序员面试手册:概念、编程问题及面试题

程序输出的结果是:a 的值为 20,b 的值为 10。

1.7.9 按名称传递

Algol 这样的语言并不使用按引用传递的方式来实现参数的输入与输出,而是采用另一

种更强大的机制来做,这就是按名称传递。简言之,你可以把变量当成符号,将该符号的

名字传给某个例程,使得该例程能够访问并更新此变量。如要想令 C[j] 的值翻倍,可以把

它的名称(而不是值)传给下面这个例程。

总的来说,按名称传递的效果相当于用主调方传给例程的参数表达式来替换例程代码

中的相应参数,例如,double(C[j]) 就可以理解为 C[j] := C[j] * 2。如果受调用的那个例程

里有某些变量与主调方的变量重名,那么在替换之前,必须先给这些变量起个不冲突的名

字才行。这种按名称传递的机制有下面两个特点:

1)每次访问形式参数时都会重新评估参数表达式的值。

2)例程可以修改参数表达式中所使用的变量,从而修改该表达式的值。

1.8 绑定

我们在前面说过,每个变量都是与内存中的位置相关联的。从最宏观的层面来看,绑

定(binding)就是把名称与其所含的内容关联起来的过程,例如把变量的名称与它所包含的

值关联起来。而绑定时间(binding time,绑定时机)则是指这种关联发生在什么时候。下

面列出各种时机。

T 语言设计期(language design time):将运算符与相应的运算联系起来(例如把

加号绑定到数字之间的加法运算上面)。

T 语言实现期(language implementation time):将数据类型与该类型可能取的各

种数值联系起来(例如把 int 类型与其取值范围相绑定)。

T 程序编写期(program writing time):将算法与数据结构,与其所要解决的问题

联系起来。

静态

Page 13: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   13

T 编译期(compile time):将变量与数据类型联系起来。

T 链接期(link time):安排整个程序在内存中的布局(把各模块、各程序库中的

名称确定下来)。

T 加载期(load time):选用适当的物理地址(如 C 语言中的静态变量就会在程序

加载的时候,绑定到相应的内存位置上)。

T 运行期(run time):把变量与内存位置中的值联系起来。

绑定可以分为两大类:静态绑定(static binding)与动态绑定(dynamic binding)。

1.8.1 静态绑定(前期绑定)

静态绑定发生在运行期之前,并且不会在执行过程中有所变化。这种绑定有时也叫作

前期绑定(early binding,早期绑定)。

举例:

T C 语言中的值与常量之间的绑定

T C 语言中的函数调用与函数定义之间的绑定

1.8.2 动态绑定(后期绑定)

动态绑定出现在运行期,或者会在运行期发生变化。这种绑定有时也叫作后期绑定

(late binding,晚期绑定)。

举例:

T 指针变量与内存位置之间的绑定

T 在 C++ 语言中对成员函数所做的调用与 virtual 成员函数的定义之间的绑定

1.9 作用域

作用域是程序中不会发生绑定变化(或者说不允许重新声明)的最大范围。编程语言

的作用域规则决定了它会怎样把对名称所做的引用关联到相应的对象上。总的来说,作用

域的决定规则可以分为两大类:静态的作用域判定(static scoping)与动态的作用域判定

(dynamic scoping)。

1.9.1 静态作用域

静态作用域(static scope)是以程序的物理结构来定义的。它可以由编译器确定。这意

味着,其绑定是通过对程序文本的检视而解决的。某个作用域的外围静态作用域称为其静

态祖先(static ancestors),在这些祖先中,最接近该作用域的称为静态双亲(static parent)。

如果距离目前作用域较近的某个外围作用域中有变量与更远的外围作用域中的变量重名,

那么前者就会掩盖后者。Ada 和 C++ 语言均可通过某些方式(例如 class_name::name)访问

动态

Page 14: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

14   程序员面试手册:概念、编程问题及面试题

这些变量。包括 C 与 Pascal 在内的绝大多数编译型语言(compiled language)都使用静态作

用域规则(static scope rule)。

范例 1:静态作用域规则。在多重嵌套的代码块中,优先考虑距离当前范围最近的作用域。

在对引用作解析时,我们会从当前的作用域开始逐层向外判断,直至找到适当的绑定

关系为止。

范例 2:静态作用域规则在发生多层函数调用时的运用。根据该规则,在下面这段代码

中,func2 函数的 count 变量会取与全局变量相同的值,也就是 10。

1.9.2 动态作用域

动态作用域规则一般出现于解释型的(interpreted language)语言中。这种语言通常无

法在编译期做出类型检查,因为当采用动态作用域规则时,系统未必总是可以把相关的类

型确定下来。

如果按照动态作用域规则来做绑定,那么系统会从当前被调用的函数开始,根据运行

期的控制流逐层向外判断,也就是说,会优先选取距离当前地点最近的外围活跃(active)作用域。用前一小节的范例 2 来说明,如果该例采用的是动态作用域规则,那么 func2 函

数中的 count 变量,就应该取 func1 函数中的 count 值,也就是 20。出现这种结果的原因在

于:func2 函数是从 func1 函数里得到调用的,而 func1 函数又是从 main 函数里得到调用

的。于是,程序在搜索 count 变量时,就会从 func2 开始,逐层往回查找。

我们再举一个例子。在下面这段代码中,func3 所看到的 count 变量,实际上是 func1()中的那个 count。

Page 15: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   15

func3 函数会沿着栈反向搜索,从而找到位于 func1() 之中的那个 count

如果改用静态作用域规则来判断,那么 func3 中的 count 变量就会取与全局变量相

同的值,也就是 10。

1.10 存储类别

现在谈谈 C 语言中的存储类别(storage class)。这个属性,既决定了分配给变量或对象

的存储空间究竟位于内存的哪一部分中,又决定了该空间的有效期。此外,它还决定了变

量名会在程序中的哪个范围里可见,或者说,我们可以在哪个范围里通过该名称来访问此

变量。C 语言的存储类别一共有 4 种,分别是:自动(automatic)、寄存器(register)、外部

(external)及静态(static)。

1.10.1 存储类别为 auto 的变量

这是 C 语言默认的存储类别。自动变量(auto variable)声明在利用该变量的函数中,

而声明时所用的关键字正是 auto。如,要想声明 int 型的自动变量,可以这样写:auto int number;。这种变量会在函数被调用时自动创建,并于函数退出后自动销毁。因此,对于声

明该变量的函数来说,它是个私有(private,私密)变量,或者叫作局部(local)变量。

一般来说,main 函数中的局部变量在整个程序运行其间都是正常存在的,只不过其

活跃范围仅限于 main 罢了。如果递归地调用同一个函数,那么每次递归时所创建的 auto变量都是互不相同的。此外,还可以在代码块(block)中定义自动变量,这些变量只在声

明该变量的代码块里有用。如果不对自动变量做初始化,那么它的值就是没有意义的。

举例:

输出:

注意

Page 16: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

16   程序员面试手册:概念、编程问题及面试题

1.10.2 存储类别为 extern 的变量

这种变量声明于所有函数之外。在整个程序运行期间它们始终存在,并保持活跃。这

样的变量也称作全局变量(global variable),其默认值是 0。它与局部变量不同,程序里的

每个函数都可以访问它。如果局部变量与全局变量同名,那么前者优先。

有时我们用 extern 关键字来声明这种变量。该变量从声明它的地方开始,一直到程序

末尾,均保持可见。

范例 1:number 与 length 这两个变量对于代码中的 3 个函数来说都是可用的。

范例 2 :当 function 函数引用 count 变量时,它引用的实际上是函数中的局部变量,而

不是那个同名的全局变量。

范例 3:全局变量。一旦将变量声明为全局变量,那么任何函数都可以使用它并改变其

值,因此,后面的函数所看到的值,其实是前面的函数修改之后的值。

输出:

Page 17: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   17

范例 4 :外部声明。从 main 函数的角度来看,变量 y 是尚未定义的,因此,编译器会

给出错误消息。这个问题有两种解决办法:

1)在 main 之前定义 y。2)在 main 里先把 y 声明成存储类别为 extern 的变量,然后再使用它。

范例 5:外部声明。注意,用 extern 来声明变量时,系统并不会为其分配存储空间。

范例 6:当我们把变量声明为 extern 时,编译器会搜寻该变量的初始化语句,看看它有

没有经过初始化。如果发现此变量确实作为 extern 或 static 变量得到了初始化,那么就认为

代码没有问题,否则,将显示错误消息。如:

初始化变量 i

搜索变量 i 的初始化

范例 7:编译器会在代码里寻找名为 i 且经过初始化的 static 或 extern 变量。

初始化变量 i

范例 8:编译器会在代码里面寻找名为 i 且经过初始化的 static 或 extern 变量。

初始化变量 i

Page 18: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

18   程序员面试手册:概念、编程问题及面试题

范例 9:声明了变量 i,但却没有将其初始化。

范例 10:我们可以对同一个 extern 变量声明很多次,但是只能将其初始化一次,如:

声明变量 i初始化变量 i再声明变量 i

再声明变量 i

范例 11:重复初始化导致出错。

声明变量 i初始化变量 i

初始化变量 i

范例 12:不允许编写全局形式的赋值语句。

初始化语句赋值语句

在声明变量的同时指定其值,这称为初始化(initialization)。如果指定变量值的行

为发生在其他场合,那么称为赋值(assignment)。

范例 13:

赋值语句

初始化语句

1.10.3 存储类别为 register 的变量

这些变量存放在计算机的寄存器中,它们是通过 register 关键字来声明的。例如:

register int count;。

由于访问寄存器要比访问内存快,因此,把频繁用到的变量放在寄存器里,可以提高

程序的执行速度。因为寄存器只能容纳很少的几个变量,所以我们必须谨慎地考虑:究竟

注意

Page 19: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   19

应该把哪几个变量放入其中。另一方面,如果寄存器满额,那么 C 语言会自动把 register 形式的变量转换为非 register 的形式。不要将全局变量声明为 register,因为那样做有可能会

使该变量在程序运行的过程中始终占据着寄存器。

1.10.4 存储类别为 static 的变量

静态变量的值会一直保留到程序结束时为止。它是用 static 关键字来声明的。

这种变量可能是外部变量 ,也可能是内部变量,具体情况要根据声明的地点来确定。

静态变量只在程序编译的时候初始化一次。1

1. 内部静态变量内部静态变量是指声明在函数里的静态变量。这种变量的作用域一直延续到定义它的

那个程序结束为止。此类变量与 auto 变量几乎完全相同,只不过它们在函数执行完之后依

然存在(或者说存活),直到运行完整个程序为止。即使多次调用该函数,变量的值也仍然

可以得到保留,而不会重置。如我们可以用它来统计函数的调用次数。

输出:

2. 外部静态变量外部静态变量是声明于所有函数之外的静态变量,它可以为程序中的每一个函数所使

用。外部静态变量与一般的 extern 变量类似,只不过它仅仅在定义该变量的文件之内有效,

而不像后者,可以在其他文件中被访问。

3. 静态函数静态声明也可以用来控制函数的可见范围。如果你在一个文件里面定义了某个函数,

并且想令此函数只能被该文件内的函数所访问,那么可以把这个函数声明为 static,使得其

他文件无法访问它。

1.11 存储空间的安排当我们编写并执行程序时,幕后其实发生着很多事情。首先来看计算机内部的情况。

 此处的外部变量是针对函数来说的,与针对程序文件而言的 extern 变量不是一回事。—译者注

Page 20: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

20   程序员面试手册:概念、编程问题及面试题

每个程序在运行的时候,都会与某些内存相关联,这些内存可以分成下面 3 个部分,而存

储空间的安排(storage organization)指的正是这个把值与内存位置关联起来的过程。

(1)静态段(Static Segment)如下图所示,静态存储空间可以分成两块。

1)代码段(code segment):该段用来存放程序代码,并且不会在程序执行过程中改变。

一般来说,这一部分内容是只读(read-only)且受到保护的(protected)。某些类型的常量也

有可能会放在这个静态的区域中。

2)数据段(data segment):简单地说,这一部分用来保存全局数据。除了代码之外,

程序的其他静态数据就存放于此。这一部分通常是可以编辑的(例如,全局变量与静态变量

都属于这种可编辑的数据)。它包含下列三种:

T 全局变量

T 数值与字符串值等字面常量

T 在多次调用期间,其值保持不变的局部变量(如静态变量)

(2)栈段(Stack Segment)如果编程语言支持递归,那么(从理论上来说)变量在任意时刻都可能会有很多个实

例,因此,这种情况下不能做静态分配。

如,假设函数 A() 会调用函数 B(),且前者中有一个名叫 count 的局部变量。等到函数

B() 执行完之后,如果 A() 想获取 count 的值,那么它应该能看到该变量在调用 B() 之前所

具备的值。为此,需要用一种机制来保存函数当前的状态,使它在调用完其他函数之后,

能够回到调用之前的情境(context),并使用那个时候的变量。

为了解决此类问题,程序需要使用栈来分配存储空间。调用函数时,程序会把一条新

的活动记录(activation record,也称为帧(frame))推入运行期栈(run-time stack),这条记

Page 21: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   21

录是针对该函数而言的。

每一帧都有可能占据栈里面的多个连续字节,而且其大小也不是固定的。当被调函数

把控制权返回给主调方时,这条与被调函数有关的活动记录会从栈中弹出。一般来说,活

动记录里面会保存下列信息:

T 局部变量

T 形式参数

T 与活动有关的其他附加信息

T 临时变量

T 返回地址

(3)堆段(Heap Segment)如果想动态地增加程序所使用的临时空间(如通过 C 语言中的指针来分配临时空间),

那么前面所讲的静态分配与栈分配就显得不够用了。我们需要用另一种方式来处理这些请

求,这种方式就是堆分配(heap allocation)。

堆分配方式用来应对需要动态分配空间的链式数据结构,以及需要动态调整大小的对

象。堆是动态地分配在内存中的一块区域。与栈类似,它也可以在运行期内增长或收缩。

然而与栈不同的地方则是:它管理起来更加复杂,而不是像栈那样后进先出(Last In First Out,LIFO)。一般来说,编程语言都同时支持堆分配与栈分配这两种内存分配方式。

如何分配栈内存与堆内存

有一种简单的办法,是在程序启动的时候,把可用内存分成两个区域,分别供栈与堆

使用。

1. 堆内存的分配方式分配堆内存的时候,系统要在堆中寻找空闲的区域。其分配方式可以分成两类:

1)隐式堆分配(implicit heap allocation):也就是自动分配堆内存。如在 Java 与 C# 语

言中,类的实例就分配在堆上面。脚本语言与函数式语言会充分利用堆内存来存放对象。

2)显式堆分配(explicit heap allocation):如果采用这种方式,那么开发者必须明确地

告诉系统应该在堆中分配内存。举例如下:

T 分配堆内存与解除分配所用的语句或函数

T malloc/free、new/delete

2. 内存碎片有的时候,内存中会出现容量比较小且无法为任何进程所使用的空间,这种空间称为

碎片(fragmentation)。它可以分成两大类。

1)内部堆碎片(internal heap fragmentation):如果分配出来的内存块的容量要比存放

对象所需的更大,那么多出来的部分就会浪费掉,这称为内部堆碎片。

Page 22: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

22   程序员面试手册:概念、编程问题及面试题

2)外部堆碎片(external heap fragmentation):如果内存中有很多小块的空间无法得到

利用,那么这些空间就叫外部堆碎片。在这些碎片中,无论哪一片内存都不能够满足程序

所提出的分配请求,也就是说,没有一片内存大到可以为程序所利用。

3. 堆分配算法一般来说,系统会用链表来维护堆中的每一块空闲内存。如果有程序发出请求,那么

可以根据下面两种办法选出应该分配给该程序的内存块。

1)最先适配法(first fit):选出首个能够满足请求的内存块。

2)最佳适配法(best fit):选出容量最小且可以满足请求的内存块。

如果需要存放的对象比算法所选出的内存块小,那么多余的空间会作为空闲内存加入

链表中。当系统将某块内存释放之后,它会把相邻的空闲内存块合并起来。

1.12 编程方式

1.12.1 无结构的编程

程序员经常会从只包含一个 main 程序的小项目开始一直往下写,等到项目变得比较

大之后,无结构的编程方式就会露出弊端。如果程序里面有很多地方都要用到同一段代码,

那么就必须将其复制很多份才行。

main 程序

1.12.2 过程式的编程

采用过程式方式来编程时,我们会把许多行代码放在一起,将其构建为例程(procedure),并且通过例程调用机制来执行这个例程。等其中的代码处理完之后,控制权会回到调用例

程的那个地方。例程可以带有参数,也可以包含子例程(subprocedure),这使得我们能够写

出更具结构且错误更少的代码。

main 程序

例程

1.12.3 模块式的编程

采用模块式方式来编程时,我们把功能上较为接近的例程,放在同一个模块里面,并

且把整个程序分成多个模块,使得这些模块之间可以通过例程调用机制来交互。每个模块

都可以有自己的数据。

Page 23: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   23

1.12.4 面向对象的编程

这种编程语言的模型的重点在于对象(object)与数据(data),而不是函数与逻辑。面

向对象的编程思维更关注我们需要操作哪些对象,而不是操作这些对象时需要什么样的逻

辑。面向对象编程(Object-oriented programming,OOP)的第一个步骤,是把需要操作的

对象确定下来,并理清它们之间的联系方式(也称为数据建模)。等这一步完成之后,我们

将该对象泛化成一整类对象,并把它所包含的各种数据定义出来,同时指出可以用来操作

这些对象的函数(也称为方法)。

面向对象编程背后的理念是,把程序当成一系列彼此交流的对象,而不是像传统的观

点那样,将其视为一组函数甚至一组计算机指令。以面向对象的观点来看,每个对象都可

以接收消息、处理数据,并给其他对象发送消息。它们是独立的小机器或行动者,各自都

扮演着某种角色,或肩负着某种职责。

一门编程语言要想成为面向对象式的语言,必须具备 3 个特征:

T 多态(polymorphism)

T 继承(inheritance) T 封装(encapsulation)

1.13 面向对象编程的基本概念

面向对象编程(OOP)有 8 个基本概念:

T 类与对象(class and object) T 封装(encapsulation) T 抽象(abstraction) T 数据隐藏(data hiding)

Page 24: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

24   程序员面试手册:概念、编程问题及面试题

T 多态(polymorphism)

T 继承(inheritance) T 动态绑定(dynamic binding) T 消息传递(message passing)

1.13.1 类与对象

对象是面向对象编程的基本单位。对象由其名称来确定,可以用来表示某个类的特定

实例。同一个对象可以有不止一个实例,这些实例都能拥有各自的数据。

对象是由数据成员及相关的成员函数(也称为方法)所组成的。有了类的名称之后,就

应该去创建该类的对象,若不创建对象,则无法使用类。

类是封装的基本单位。它由许多函数代码与数据所组成,这构成了面向对象编程的根

基。类是一种抽象数据类型(ADT),这意味着,类的定义仅仅能够提供抽象的逻辑。只有

当我们创建了类型为该类的变量之后,类里面所定义的数据和函数才能够活跃起来。

如果变量的类型是某个类,那么该变量就可以称作对象。它有其物理存在,并且可以

视为该类的实例。针对同一个类,可以创建多个对象。每个对象都有一套相似的数据,这

些数据是在类中定义的,对象也可以使用类中所定义的函数来操作这些数据。如 Cat(猫)

这个类,就定义了各种猫所能具备的特征及行为,而 Abyssinian(阿比西尼亚猫)对象,

则是特定的猫,并且拥有特定的特征。猫有毛(hair),而阿比西尼亚猫,则有着红色的毛

(ruddy hair)。

每个对象都是类的实例。它可以用来表示人、手机、椅子、学生、雇员、书、讲师、

演讲者、汽车、其他车辆或日常生活中能够看到的任意事物,具体用来表示什么,则要根

据其所属的类来定。对象的状态是由其特定实例所具备的数据值来确定的。

对象会占据内存空间,同一个类的各对象都有同一套数据项。两个对象之间,可以通

过传递消息来调用函数,进而相互沟通。

举例如下:

T 动物可以视为类,而狮子、老虎、大象、狼、奶牛等则是其对象。

T 鸟可以视为类,而麻雀、鹰、鸽子等则是其对象。

T 音乐家可以视为类,而 Himesh Reshmia、Anu Malik 及 Jatin-Lalit 等则是其对象。

1.13.2 封装

封装是一种捆绑机制,能把函数与数据捆绑成类这种紧凑的形式。

数据与函数可以是私有的(private),也可以是公开的(public)。私有的数据或函数,

只能在类中访问,而公开的数据或代码则可以从类之外访问。运用封装,可以把实现代码

中的某些复杂内容给隐藏起来。将函数代码与数据链接在一起,就可以生成对象,而这个

对象则可以表示成某个以类为其类型的变量。

Page 25: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   25

1.13.3 抽象

抽象是一种用来描述基本特征的机制,它只将重要的特征表示出来,而忽略那些不太

重要的特征。要想做好抽象,就必读充分地了解问题领域(problem domain),我们要运用

OOP 原则来实现并解决其中的问题。现在以 Vehicle 为例,来讲解什么叫抽象。

创建 Vehicle 这个类时,可向其中加入某些代码与数据。例如车辆名称、车轮数量、燃

料类型、车辆类型等属性,可以设置成该类的数据,而换挡、加速 / 减速等动作,则可以设

置成该类的函数。此时,我们并不关注车辆的具体细节,例如怎样加速、如何换挡等,而

且也不会把车辆型号及颜色等其他属性设计到这个类里。

1.13.4 数据隐藏

数据隐藏机制可以阻止用户从外部直接访问数据。OOP 语言会用 public、private 及

protected 等特殊的关键字来隐藏数据。这些访问指示符(access specifier)可以分为 3 种,

它们的用法及效果如下:

T 可以用 public、protected 及 private 关键字来声明类的成员,以便明确地加强其封装程度。

T 放置在 public 关键字后面的元素,可以为该类的全部用户所访问。

T 放置在 protected 关键字后面的元素,只能够由该类的方法来访问。

T 放置在 private 关键字后面的元素,只能够由该类的方法来访问。

T 如果将类中的数据声明为 private,那么该数据就会隐藏起来。这种私有的数据无法

直接通过对象访问。

1.13.5 多态

polymorphism(多态)这个词可以拆成 poly 与 morphism 两个部分,前者的意思是“多”,

后者的意思是“形式”。所谓“多态”,就是多种形式。

多态使得同一实体可以表现出不同的形式。简单地说,印度神话里的黑天神(Lord Krishma)就是多态的绝佳范例。从程序员的角度来看,多态意味着同一个接口可以由多种

方法来实现。

有了多态之后,我们可以用一个接口来控制一整类操作。如果想求出 3 个数的和,那

么无论输入的是 3 个整数、3 个浮点数,还是 3 个其他类型的数,我们都能用同一种接口来

计算。这个具备 3 个参数的接口,可以叫作 addition3。多态机制允许我们为其定义 3 个版

本,其中一个版本接受 3 个类型为 int 的参数,另一个版本接受 3 个类型为 double 的参数,

依此类推。编译器会根据传给 addition3 函数的数据类型,自动选出最为合适的版本。这就

叫函数多态(function polymorphism)或函数重载(function overloading)。

多态可以分为两类:

T 编译期的多态

T 运行期的多态

Page 26: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

26   程序员面试手册:概念、编程问题及面试题

1.13.6 继承

继承是从现有类中派生新类的机制。它使得原来写过的代码可以得到复用,这正是

面向对象编程的一项基本理念,新类沿袭了旧类所具备的功能。我们可以用基类(base class)—派生类(derived class)、父类(parent class)—子类(child class),以及超类(super class)—子类(sub class)等说法来称呼这一组旧类与新类。

继承机制使得我们可以用类来构建一套体系。在这套体系中,同一层的各类之间分别

具有一些特性,而它们又全都从上一层的类里继承了某些共性。这使得我们在定义类的时

候,只需要把该类所必备的属性定义出来即可(至于其他属性,则可以由下级的类来定义)。

举例:

1)在 1.13.3 节的那个例子中,Vehicle 类位于整套体系的顶端。我们可以把每辆车都

必须具备的属性放在这个类里。由此而下,可以定义新的类,如用 two wheeler 类来表示那

种只有两个轮子的 Vehicle。2)还有一个例子,就是把工学院视为上级类,而将其中的计算机系、电子系、电气系

等视为该学院的子类。至于工学院所属的大学,则可以视为该学院的上级类。

1.13.7 继承的类型

继承可以分为 5 种类型。

T 单一继承(Single Inheritance) T 分层继承(Hierarchical Inheritance) T 多级继承(Multi-Level Inheritance) T 混合继承(Hybrid Inheritance) T 多重继承(Multiple Inheritance)

1. 单一继承单一继承是最简单的继承形式。如果某类继承自另一个类,那么就叫单一继承。在下

面这张图张中,B 类只从 A 类中继承。A 是 B 的父类,B 是 A 的子类。

如果派生类只继承自一个基类,那么这种继承就是单一继承。

2. 分层继承如果多个派生类创建自同一个基类,那么这样的继承就叫分层继承。在这种继承中,

同一个类会有多个子类。下面这张图里的 B、C、D 都继承自同一个 A 类,A 是 B、C、D的父类(或基类)。

Page 27: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   27

3. 多级继承如果派生类又为另外一个类所继承,那么就形成了多级继承。

多级继承说明了面向对象技术的一项特征,就是允许派生类成为另一个类的基类。下

图中的 C 是 B 的子类,而 B 又是 A 的子类。

4. 混合继承单一继承、分层继承与多级继承之间的任意组合,都可以叫作混合继承。简单地说,

混合继承就是把单一继承与多重继承组合起来。Java 语言可以用接口来实现混合继承。

5. 多重继承如果派生类同时来自多个基类,那么就称为多重继承。Java 不支持类之间多重继承,

但是可以用接口来模拟。

由于多重继承会引发一些很难处理的问题,因此,Java 的类不能同时继承自多个超类,

不过,它可以有多个接口。

详细内容参见本章的问题及解答一节。

1.13.8 动态绑定

绑定意味着链接(linking),也就是把函数的定义与对该函数的调用链接起来。

1)如果在编译期就把函数调用与函数定义链接了起来(也就是指出了程序的控制流在

注意

Page 28: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

28   程序员面试手册:概念、编程问题及面试题

执行这次调用时,应该转向何处),那么称为静态绑定。

2)如果把链接的时机推迟到运行期,或等程序执行到这次函数调用的时候,再去做

链接,那么称为动态绑定。在这种绑定方式下,程序在调用函数时究竟会转向哪一个函数,

要等到执行这次调用时才能确定。

1.13.9 消息传递

在 C++ 与 Java 中,对象之间是通过传递消息来互动的。消息里面含有成员函数的名称

以及所要传递的参数。消息传递的格式如下:

这里的消息传递指的就是在对象上调用方法并传入参数。消息传递仅仅是一种调用

类中的方法并向其发送参数的动作而已。对象为了回应该消息,会开始执行被调用的那个

方法。

问题与解答

问题 1 下面这段程序代码会输出什么内容?

解答:8 4 4(假设使用的是 Linux GCC 编译器)。对于 C 语言来说,各种数据类型

所占据的字节数量与编译器有关。

编译器 应该输出的值

Turbo C++ 3.0 8 4 2

Turbo C++ 4.5 8 4 2

Linux GCC 8 4 4

Visual C++ 8 4 4

问题 2 下面这段程序代码会输出什么内容?

解答:4 4 7(假设使用的是 Linux GCC 编译器)。sizeof(expr) 运算符总是会返回

Page 29: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   29

整数值,该整数代表 expr 这个表达式的最终取值所占据的字节数。对于下面这个表达式

来说:

由于 0 是一个类型为整数的常量,因此,在 Linux GCC 编译器下,sizeof 运算的结果是 4。对于下面这个表达式来说:

由于在 sizeof 运算符内求值的表达式,其求值效果都仅限于该范围中,因此,variable 变量

的值在接下来的那条 printf 语句里面,依然是 7。问题 3 什么是 name mangling(名称修饰、名字重整)机制?

解答:C 语言的编译器在链接时,会把下划线添加到函数或符号前面。如 main() 函数

会链接为 _main() 符号,而通过 int counter; 语句声明的 counter 变量,则会链接为 _count。由于 C++ 有重载机制,因此,这种方式并不适合 C++ 语言的编译器。假如 C++ 也

采用添加下划线的办法来链接符号,那么重载机制就会失效。如 test() 函数有这样两个重

载版本:

假如 C++ 也采用添加下划线的办法来设定符号名,那就无法链接到正确的函数体。为此,

它采用 name mangling 机制,用序号及参数大小等额外的信息来修改函数名,以便将其解析

为独特的符号。函数名的修饰方式如下:

< 函数序号 >< 函数名称 >@< 参数大小 >

变量名的修饰方式如下:

?< 变量名称 >@@< 独特的 ID>

对于上面那个例子来说:

链接到链接到

问题 4 解释 C/C++ 中的预处理指令与宏。

解答:编写 C 程序时,需要先编译后运行。编译器本身也是个程序,它可以把源代码

转换为机器码。预处理器可以视为编译器中的一个部分,而其余的内容则可以视为另一个

部分。下面举个例子,帮助大家理解预处理器。我们通常会在 C 程序的开头写上这样一条

include 语句:

Page 30: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

30   程序员面试手册:概念、编程问题及面试题

预处理器看到这条语句之后,会把 stdio.h 文件里面的所有函数原型(function proto-type)都纳入实际的 C 程序源代码中。此时的源代码就叫经过预处理的源代码。这些代码

会输入给编译器的其余部分,以便转器化为机器码。程序运行的时候,执行的正是这些机

器码。

预处理器是 C 语言编译器里面的程序,它会在实际的编译工作开始之前,先做一些处

理,例如将头文件或宏包含进来。

预处理器提供了下列功能:

1)把头文件里的代码纳入 C 语言程序中。例如,可以这样来写:

这条语句,使得预处理器把 <stdio.h> 这个头文件里的所有函数原型都复制到当前的 C程序中。

2)宏展开(macro expansion)。格式固定的一组语句可以定义为宏。预处理器如果发现

程序用到了宏,就会以相应的语句来替换它。例如,可以定义这样一个宏:

只要程序用到 MAX,预处理器就会将其替换为 100。由于该宏所表示的数值是固定的,因

此可以称作常量宏。

3)条件编译(conditional compilation)。可以用特殊的预处理指令来判断程序是否符合某

种条件,并据此纳入或排除某一部分代码。例如,可以写出下面这种类似于 if ... else 的结构:

这就叫作条件编译,因为预处理器会根据计算机的类型把相应的代码纳入程序。如果在 main函数之前,写过这样一句:

那么预处理器就会把与 AIX 相应的代码纳入程序。若没有这条 #define 语句,则会将适用

于 Linux 的代码纳入程序。

由于预处理器可以定义并解析宏,因此也称为宏处理器(macro processor)。大家应该

明白的是,预处理器会处理那些以井号(#)开头的语句,这些语句也称为指令,因为它们

用来指示预处理应该怎样处理代码。

问题 5 下面这段代码会输出什么内容?

解答:Feb 17 2014。

Page 31: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   31

在 C/C++ 语言中,应该可以使用下面这几个预先定义好的宏名称:

__LINE__ 当前这句源代码的行号(十进制的常量)

__FILE__ 源文件所应具备的名称(字符串形式的字面量)

__DATE__ 这份源文件是在哪一天得到处理的

__TIME__ 这份源文件是在什么时间得到处理的

问题 6 下面这段代码会输出什么内容?

解答:4321。printf() 函数会返回打印出来的字符个数。例如,下面这段代码的运行结果就是 10005,由

于输出 1000 之前,还输出了 \n 字符,因此,内层的那个 printf 函数总共输出了 5 个字符。

问题 7 下面这段代码会输出什么内容?

解答:-70。XXX * 10 这个表达式会转化为 ABC-XYZ * 10,进而变成 30-10 * 10=-70。问题 8 下面这段代码会输出什么内容?

解答:4。calculator (A+4, B-2)) 会转换成 (A+4 * B-2)/(A+4-B-2)=(20+4 * 10-2)/(20+4-10-2)=58/12=4。

问题 9 下面这段代码会输出什么内容?

解答:7。检测完第 1 个条件,也就是 ++k<5 之后,k 的值是 6。由于该条件不成立,

因此程序不会再去检测第 2 个条件,而是会直接跳至第 3 个条件,也就是 || 符号右侧的那

Page 32: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

32   程序员面试手册:概念、编程问题及面试题

个 ++k<=8。检测完该条件之后,k 的值是 7。由于该条件为真,因此整个 if 语句为真。因

为 k 已经递增了两次,所以 printf 打印出来的值是 7。问题 10 下面这段代码会输出什么内容?

解答:Main: 6 7 Func: a=8 b=7(假设使用的是 Linux GCC)这个程序的结果取决于栈的实现方式。如果计算机按照从左到右的顺序把参数传给栈,

那么结果就是:

反之,若是从右至左,则结果为:

问题 11 下面这段代码会输出什么内容?

解答:0 10 11 20 21根据运算符的优先级,这几个表达式会分别按照下面的方式来计算:

问题 12 下面这段代码会输出什么内容?

Page 33: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   33

解答:0 1 2 3 4 -1问题 13 下面这段代码会输出什么内容?

解答 :sizeof xyz=20 sizeof abc=100。1

问题 14 C++ 与 Java 语言有哪些相似点,又有哪些区别?

解答:C++ 与 Java 的相似点及不同之处很多。

T Java 不支持 typedef 与 define,也不支持预处理器。Java 支持类,但不支持结构体

或联合体(union)。

T 所有的 C++ 程序都必须包含名为 main 的函数。

T Java 里面的所有类都继承自 Object 类。

T Java 语言的所有函数及方法都必须定义在类里。

T C++ 与 Java 都支持类级别的(静态的)方法或函数,使得开发者无需生成该类的实

例,即可调用这些方法或函数。

T 通过 Java 语言的 interface 关键字,我们可以创建出一种只包含方法声明与常量的

接口。它相当于抽象基类,其中不能够定义数据成员或方法(当然了,我们也可以

不通过接口模拟,而是直接用 Java 的类机制来定义真正的抽象基类)。C++ 语言不

支持 interface 这一概念。

T Java 不支持多重继承。在某种程度上,interface 机制实际上可以提供多重继承的某

些特性,同时又能够避免真正的多重继承所带来的某些底层问题。

T Java 不支持自动类型转换(automatic type conversion)。

T 与 C++ 不同,Java 拥有 String 类型,该类型的对象是不可变的(immutable)。用引

号括起来的字符串会自动转成 String 对象。此外,Java 还支持 StringBuffer 类型,

该类型的对象可修改。

T 与 C++ 不同,Java 语言中的数组是一种带有 length 字段的对象,该字段用来指出

数组的大小。如果访问了数组范围之外的元素,那么会抛出异常。

T Java 不支持指针,或者我们至少可以说:Java 语言不能够修改指针所含的地址,也

 由于要考虑对齐(alignment)问题,因此,结构体的总大小未必等于各成员的大小之和。—译者注

Page 34: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

34   程序员面试手册:概念、编程问题及面试题

不能够做指针运算。原来需要用指针解决的问题,在 Java 语言里,可以考虑通过

数组与字符串等类型来解决。如,在 C++ 语言里面,为了使用以 null 结尾的字符

串,我们经常会声明 char * ptr 这样的指针,令其指向字符串中的第 1 个字符;而

在 Java 语言里,则用不着这样做,因为 Java 本身就把字符串视为真正的对象。

T C++ 的范围解析运算符(::)在 Java 里面用不到。Java 用圆点符号(.)来构建完全

限定的(fully-qualified)名称。此外,由于 Java 没有指针,因此 C++ 里的 -> 指针

运算符也是用不到的。

T C++ 用范围解析运算符(scope resolution operator)来连接类名与静态成员名,以便调

用类中的静态数据成员及静态函数。Java 语言不这样做,而是改用圆点“ .”来连接。

T 与 C++ 类似,Java 也具备 int 及 float 等原始数据类型,但是这些类型的大小不会随

着平台而变化。此外,Java 里面没有无符号的整数类型。Java 语言对类型的检查与

要求要比 C++ 严格得多。

T 与 C++ 不同的是,Java 语言提供了真正的布尔类型,也就是 boolean。 T Java 的条件表达式必须是 boolean 值,而不能像 C++ 那样,采用整数值。因此,

Java 语言不允许写出 if(x+y) 这样的代码,因为这种表达式不能归结为 boolean 值。

T C++ 中的 char 类型,会映射到 8 位的 ASCII 码(或扩展 ASCII 码)上,而 Java 语

言中的 char 类型则要占据 16 个二进制位。

T 对于 C++ 语言中的各类变量及对象来说,其实例化既可以放在编译期来做,也可

以放在运行期来做。这两种方式会分别令其出现在静态内存与动态内存中。与之相

对,Java 语言要求原始类型的变量只能在编译期实例化,而对象则只能在运行期实

例化。Java 给 byte 与 short 之外的所有原始类型都提供了包装类(wrapper class),使得开发者可以根据需要,把这些数据实例化为动态内存中的对象。

T 在 C++ 语言中,如果开发者没有给原始类型的变量指定初始值,那么该变量就会含

有未定义的值。原始类型的局部变量可以在声明的同时加以初始化,但是类中的原

始类型数据成员,则无法在定义的同时设定初始值。

T Java 语言可以在定义类的同时给其中的原始类型数据成员设定初始值,此外,也可

以在构造器(constructor)里设定。如果不对这些成员做初始化,那么其值将自动设

为 0 或与 0 等价的值。

T Java 与 C++ 一样,也支持对构造器做重载。与 C++ 类似,如果开发者不提供构造

器,那么系统会给出默认的构造器;反之,如果提供了构造器,那么就不会出现默

认的构造器了。

T Java 语言的对象都是按引用传递的,因此,不需要使用 C++ 语言那样的复制构造

函数(copy constructor)(实际上,Java 程序在调用方法时,所有的参数都是按值传

递的,也就是对引用变量做一份拷贝,并将这份拷贝传给被调用的方法,使得后者

能够通过这份拷贝来访问原来那个引用变量所指向的对象,并修改其内容。然而要

Page 35: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   35

注意:接受这份拷贝的那个方法,不能令原来的引用指向其他对象)。

T Java 语言没有析构函数(destructor)。系统会在主程序之外另开一个线程,并在其

中运行垃圾回收器(garbage collector,GC),以便将无用的内存返还给操作系统。

这一机制使得 Java 与 C++ 之间出现了很多微妙且极其重要的区别。

T 与 C++ 类似,Java 也可以对函数做重载,但是不支持带有默认值的参数。

T 与 C++ 不同的是,Java 语言不支持模板(template),因此,没有泛型(generic)函

数或类 。1

T 与 C++ 不同的是,Java 语言的标准版里本身就提供了很多与各种数据结构相关

的类。

T 多线程(multithreading)是 Java 语言的标准功能。

T 尽管 Java 语言也用 private、public 及 protected 这 3 个关键字来控制访问权限,但

其解读方式则与 C++ 有很大区别。

T Java 语言没有 virtual 关键字。由于所有的非静态方法都采用动态绑定,因此无需像

C++ 那样,通过 virtual 关键字来指明这一点。

T Java 语言提供了 final 关键字,用以指出某方法不能够被覆盖(overridden)。这种方

法可以静态地绑定(编译器或许会将其内联)。

T Java 的异常处理系统在实现细节上与 C++ 有很大的区别。

T 与 C++ 不同,Java 语言不支持运算符重载(operator overloading),然而针对字符

串的 + 与 += 符号例外。如果对字符串使用这两种运算符,那么 Java 会在运算过程

中,把其他类型的数据转为字符串。

T 与 C++ 类似,Java 应用程序也可以调用以其他编程语言所写的函数。这些函数通

常称为 native method(本地方法)。但是要注意,Java applet(Java 小程序)不能调

用本地方法。

问题 15 C++ 语言的赋值与初始化有什么区别?

解答:考虑下面这段代码。

上面这段代码用变量 one 来初始化变量 two,使其成为 one 的一份拷贝。当系统创建

变量 two 时,它的值由未定义的内容立刻变成与 one 相同的内容,这中间没有过渡的步骤。

反之,如果把代码写成下面这个样子:

那就成了用变量 one 来给变量 two 赋值。系统在执行到 two=one 这条语句之前,其实已经

给变量 two 设置了一个值。这就是初始化与赋值的区别:如果创建变量的同时为其指定值,

 Java 从 J2SE 5.0 开始引入了泛型。—译者注

Page 36: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

36   程序员面试手册:概念、编程问题及面试题

那就叫初始化;反之,若令变量从旧值变为新值,则称为赋值。

如果不清楚某个语句到底是初始化还是赋值,那么可以给变量前面加上 const 声明。修

改后的这段代码若能编译,则说明是初始化,反之,就是赋值。

问题 16 解释 C++ 的重载机制。

解答:C++ 允许开发者对同一个范围内的某个函数或运算符给出多种不同的定义,这

分别称为函数重载与运算符重载。重载声明(overloaded declaration)是指其名称与同范围

内的原有声明相同,而参数及定义(或者说实现方式)有所不同的声明。

调用重载函数或运算符时,编译器会把实际参数的类型与各定义中所指定的形式参数

类型相比较,并选出最为合适的一个定义。这个用来确定最合适的重载函数或重载运算符

的过程就叫重载解析(overload resolution,或称重载裁决、重载决议)。

(1)C++ 中的函数重载

同一范围内的某个函数可以具备多种定义。这些定义必须在参数的类型或数量上有所

区别。单凭返回值的类型。是不足以实现重载的。下面这个范例会对 print() 函数做重载,

令其能够适用于很多种数据。

编译并运行上述代码之后,会产生以下结果:

(2)C++ 中的运算符重载

C++ 语言中的大部分内置运算符都可以重新定义或重载,这使得其适用范围,能够扩

大到那些由用户所定义的类型上。重载的运算符实际上是一种具备特殊名称的函数,其名

称以 operator 关键字开头,后面跟着需要重新定义的那个运算符号。与其他函数一样,这

些重载的运算符函数也具备返回值类型及参数列表。

Page 37: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   37

上面这行代码,重载了加法运算符,令其可以将两个 Box 对象加起来,并把结果以 Box对象的形式返回给调用方。大部分的重载运算符都可以像上面那样,声明为类中的成员函

数,此外,也可以声明为普通的非成员函数。假如把刚才的重载用非成员函数的形式来写,

那就要安排两个参数,用以表示参与加法运算的两个 Box 对象。

下面这个例子演示怎样以成员函数的形式来重载加法运算符。这种情况下,operator+函数是在参与运算的第一个 Box 对象上得到调用的,我们在该函数中可以像下面这样,通

过 this 访问此对象。

Page 38: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

38   程序员面试手册:概念、编程问题及面试题

编译并运行上述代码,会产生下列结果:

问题 17 重载与覆盖(override)的区别是什么?

解答如下:

方 法 重 载 方 法 覆 盖

定义 在同一个类中,编写多个名称相同的方法,这些方法在参数个数、参数类型或参数顺序上有所区别

 在子类中编写与父类方法同名的方法,使得两者的参数个数、参数类型及返回值保持一致

含义 方法重载,意味着同一个类里面会有多个名称相同但是签名不同的方法

 方法覆盖,意味着基类里面的方法,在保持签名不变的前提下,于派生类中重新做了定义

对原方法的行为的影响 方法重载,可以说增加或扩充了原方法的行为

 方法覆盖,可以说改变了原方法的行为

 重载与覆盖都可以是一种多态。多态的意思是:同一名称,多种形式

多态性  这是一种编译期的多态  这是一种运行期的多态

是否需要继承  可能需要,也可能不需要  一定需要

方法签名  各方法必须具备不同的签名 原方法与覆盖方法之间必须具备相同的签名

所涉及的方法之间的关系  这些方法都位于同一个类中 原方法与覆盖方法分别位于超类与子类中

需要满足的要求 这些方法位于同一个类中,且具备相同的名称,但是其签名有所区别

 这两个方法的名称与签名都相同,但是分别位于超类与子类中

涉及多少类  方法重载不一定涉及一个以上的类  方法覆盖至少涉及两个类

举例

 

 

 

 

问题 18 解释 C++ 的拷贝构造函数。

解答:拷贝构造函数采用已经建立好的同类对象来创建新对象的构造函数。它会在下

面这 3 种场合得到调用:

Page 39: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   39

1)用同类型的另一个对象来初始化某对象。

2)给对象制作拷贝,并将其当作参数传给某函数。

3)给对象制作拷贝,并将其当作函数的返回值,返给调用方。

对于没有定义拷贝构造函数的类来说,编译器会自动为其定义一份。如果类里带有指

针变量,并且拥有某些动态分配的内存,那么必须为其定义拷贝构造函数。该函数最为常

见的形式如下:

函数中的 obj 是指向对象的引用,拷贝构造函数要用那个对象来初始化本对象。

编译并运行上述代码,会产生下列结果:

现在我们把代码稍微修改一下,使得程序用现有的对象来创建同类型的新对象:

Page 40: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

40   程序员面试手册:概念、编程问题及面试题

编译并运行上述代码,会产生以下结果:

问题 19 什么是 C++ 的浅拷贝与深拷贝?

解答:浅拷贝(shallow copy)就是把原对象中每一个成员字段的值都拷贝到新对象里。

这对于普通的字段来说是没有问题的,但对于指针型的字段则未必成立。因为指针所指的

那些内存是动态分配的,并不会自动得到拷贝。这导致原对象与新对象中的字段,全都指

向同一块动态分配的内存,这种效果或许并不符合我们的要求。由系统默认提供的拷贝构

造函数与赋值运算符,执行的都是浅拷贝。

深拷贝(deep copy)就是不仅拷贝原对象中的每一个字段,而且还将动态分配给该字段

Page 41: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   41

的内存内容也一并拷贝过来。为此,我们必须编写拷贝构造函数,并重载赋值运算符,否

则,还是会像浅拷贝那样,使得旧对象与新对象中的指针字段都指向同一块内存。

问题 20 什么是 C++ 的对象切割?

解答:如果把派生类的对象赋给基类对象,那么前者中与基类相对应的那一部分内容,

将会拷贝到后者,而前者所特有的内容则不会得到拷贝。这就叫作对象切割(object slicing),它意味着基类对象只能访问基类的成员,而且相当于将基类成员同派生类成员相互分离。

在本例中,对象 b 具备 i 与 j 这两个字段,对象 d 具备 i、j 及 k 这 3 个字段。赋值操作

只会把 d 中的 i 和 j 拷贝到 b,而不会拷贝 k,这就使得对象 d 发生了切割。

问题 21 Java 语言有没有拷贝构造函数与赋值构造函数?

解答:Java 里面有一种与拷贝构造函数类似的写法,但这种写法并不像 C++ 的拷贝构

造函数那样,扮演特殊的角色。这种函数与其他构造函数一样,都可以通过 new 来调用。

Java 没有赋值构造函数,也没有与之类似的写法。这是因为 Java 不支持运算符重载,

它的赋值操作(=)总是执行浅拷贝。

Java 里有一个名叫 clone 的方法,可以拷贝对象。按照惯例,该方法的实现代码应该给

方法所在的那个对象做一份深拷贝,并将其返回给调用方。

问题 22 解释 C++ 的模板。

解答:C++ 的模板是泛型编程的基础,它使得开发者可以用一种与具体类型无关的方

式来编写代码。模板相当于创建泛型类或泛型函数的一套公式。程序库里的很多容器(例如

迭代器)与算法都用到了泛型机制,它们是采用模板来开发的。

每一种容器都只有一个定义,但它所能容纳的元素类型却可以有很多种,例如可以声

明 vector<int> 或 vector<string> 等。除了用来定义类,模板还可以用来定义函数。我们接

下来就要讲解模板函数。

(1)模板函数

模板函数的常见定义方式如下:

Page 42: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

42   程序员面试手册:概念、编程问题及面试题

其中的 type 是个占位符号,用来表示函数所使用的数据类型。编写函数代码时,可以

使用此符号来指代这个类型。

下面这个模板函数会返回两个值中比较大的那一个:

编译并运行上述代码,可以看到以下结果:

(2)模板类

除了可以定义模板函数,我们还可以定义模板类。它的常见形式如下:

其中的 type 是个占位符号,会在该类实例化时被指定。这种通用的数据类型可以有不

止一个,各类型之间以逗号分隔。

下面这段代码定义了 Stack<> 模板类,并实现了 push 及 pop 等泛型方法,分别用来把

元素推入栈中和从栈中弹出元素。

Page 43: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   43

编译并运行上述代码,会产生下列结果:

问题 23 解释 C++ 的 virtual 函数。

解答:C++ 在调用函数时,会把该调用与合适的函数定义相匹配,这种匹配发生在编

译期,也称为静态绑定。此外,我们还可以告诉编译器,把函数调用与函数定义之间的匹

配放到运行期去做,这称为动态绑定。如果用 virtual 关键字来声明函数,那么编译器就会

明白应对该函数做动态绑定。下面这段代码演示了静态绑定的效果。

#include<iostream>using namespace std;class A {public: void f() { cout<< "Base Class A"<< endl; }};

Page 44: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

44   程序员面试手册:概念、编程问题及面试题

class B: public A {public: void f() { cout<< "Derived Class B"<< endl; }};void g(A& arg) { arg.f();}int main() { B x; g(x);}

这段范例代码会输出:

Base Class A

执行函数 g() 时,尽管其参数所指的对象是 B 类型,但实际上被调用的却是 A 类里的

f()。这是因为,编译器在编译期只知道 g() 函数的参数所引用的那个对象其类型派生自 A类,但它并不清楚这个类型指的究竟是 A 还是 B。其实这种类型判定工作也可以放在运行

期来做。下面这个例子和前一个几乎完全一样,只不过我们在声明 A::f() 时,用了 virtual关键字而已。

这段范例代码会输出:

Derived Class B

问题 24 什么是抽象类?什么是接口?两者之间有什么区别?

解答:抽象类就是不能实例化的类,我们经常将其实现成带有一个或多个纯虚函数

(pure virtual function,或称抽象函数)的类。

纯虚函数是指必须由具体的(也就是非抽象的)派生类来覆盖的函数。声明这样的成员

函数时,我们采用“=0”这一写法来表示它是个纯虚的函数。

一般来说,抽象类会定义一段实现代码,以供具体的类继承。然而如果某个类只有纯

虚的函数,那么这样的类就成了纯抽象类(pure abstract)或接口(interface)。之所以要在

Page 45: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   45

C++ 里用纯抽象类来模拟接口,是因为我们不能像 Java 那样,通过 interface 关键字来定

义。Java 程序可以用 interface 关键字定义一种与 C++ 中的纯抽象类相仿的结构。

在面向对象编程中,虚函数是一种可以由派生类来覆盖的函数,这两个函数的签名是

相同的,而其行为则可以有所不同,以便实现多态的效果。根据定义,Java 里的每一个非

静态方法,如果既不是 final 方法,又不是 private 方法,那么默认就是虚方法。只有那些无

法通过继承来实现多态的方法,才可以算作非虚的方法。

从设计角度来看,这样的基类主要是为了给派生类呈现一套接口,因此,是不应该实

例化的。我们只是想把许多子类的对象都用这个基类来表示,以便将其纳入同一套接口中。

这样的想法可以通过 abstract 关键字来实现,它可以把基类定义成抽象类。如果有人要制作

该类的对象,那么编译器就会报错。抽象类里既能够编写可以执行的方法,又能够编写抽

象的方法。如果某个类是从抽象类中继承下来的,那它就不能再同时继承其他的抽象类了。

interface 关键字进一步提升了抽象的程度。如果我们用这个关键字来声明某个类型,

那就不能够给其中的方法提供实现代码了。也就是说,我们只能在类型里面声明方法,而

不能为其编写代码,这些代码要留给实现本接口的类去写。接口中的所有方法都是抽象的。

同一个类可以实现多个接口。

Java 里的抽象类与接口的区别如下:

T 接口中的方法都是抽象的,不能包含实现代码;而抽象类里面的实例方法,则可以

提供默认的实现代码。这是抽象类与接口在方法上的主要区别。

T 接口里面声明的变量默认是 final 变量,而抽象类则可以包含非 final 的变量。

T Java 接口中的成员默认都是 public 成员;而抽象类则可以像其他类那样,拥有

private 及 protected 成员。

T 实现 Java 接口时,需要使用 implements 关键字;而继承抽象类时,则需使用 extends关键字。

T Java 接口本身可以继承其他的接口,然而只能继承一个;抽象类可以在继承另外一

个类的同时,实现很多接口。

T Java 类可以实现多个接口,但只能从一个抽象类里面继承。

T 接口是一种绝对抽象的结构,不能够实例化;抽象类虽然也不能实例化,但如果其

中有 main() 方法的话,那么还是可以调用的。

问题 25 memcpy 与 memmove 的区别是什么?

解答:即便来源区域和目标区域有所重叠,memmove 也依然能得出正确的结果;而

memcpy 则不能保证这一点,这或许令其开发者能够用更为迅速的办法来实现它。如果不确

定应该调用哪个方法,那么为了稳妥起见,还是调用 memmove 比较好。memmove 实现起

Page 46: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

46   程序员面试手册:概念、编程问题及面试题

来应该比较简单,只需多做一次测试,就可以正确处理内存区域重叠的情况。

可是这段代码有个问题:dptr 与 sptr 这两个指针之间的比较操作,在许多场合之下是

无法安全执行的(因为它们所指的位置不一定都在同一个对象之内),而且这种比较操作执

行起来未必就很快。

如果使用 memcpy 来复制内存,那么目标区域与来源区域之间一定不能重叠,而 mem-move 则不会施加这个限制,它的效果相当于先把来源区域复制到一块缓冲区中,然后再

把那块缓冲区的内容复制到目标区域,这样一来,就不用担心重叠的问题了。换句话说,

memcpy 在把来源区域复制到目标区域时,并不会检查两者是否重叠,而 memmove 则会先

检查,然后再正确地处理重叠与不重叠的情况。

问题 26 下面这段代码会输出什么内容?

解答:a=3 b=4sizeof 运算符会给出操作数所占据的字节数,但是它并不会把操作数的求值效果带到

sizeof 之外。因此,当程序执行完 sizeof 之后,执行期间发生在 a 上面的运算效果会消失,这

意味着,a 的值依然是 3。问题 27 对比 C++ 与 Java 的性能。

解答:为了运行编译好的 Java 程序,我们通常要在计算机上安装 Java 虚拟机(Java virtual machine,JVM),以便将程序放在这个虚拟机上执行;而要运行编译好的 C++ 程序,

则无需借助此类应用程序。早期的 Java 语言在性能上面远远不如 C++ 这样的静态编译语言。

由于那些语言与 C++ 的关系紧密,因此可以通过 C++ 编译成少量的机器指令,而 Java 语言

则会编译成很多字节码,这导致 JVM 必须用较多的机器指令,才能把这些字节码解释出来。

由于性能的优化是个特别复杂的事情,因此,我们通常很难给 C++ 与 Java 之间的性能

高低下一个概括性的结论。而且由于这两种语言的本质有很大的区别,因此,也不容易明

确地定性。由于 Java 程序极度依赖于灵活的高层抽象机制,因此,这种程序的效率本身就

Page 47: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   47

容易变得比较低,而且其优化工作也受困于某些很难突破的限制。不过,强大的 JIT 编译

器(当前某些的 JVM,已经实现了该技术)可以解决其中的一部分问题。

(1)Java 语言的某些特点会导致 Java 程序的效率本身就比较低

1)所有的对象都分配在堆上。有的函数只会用到一些比较小的对象,而这样做会降低

程序的性能,并导致堆中出现碎片,假如分配在栈上,那就几乎不会有这些问题了。目前

很多 JIT 编译器可以在某种程度上缓解该问题,它们会根据逃逸分析(escape analysis)或

逃逸侦测(escape detection)技术的检测结果,来把某些对象分配在栈上。

2)不能访问底层的细节信息,这意味着,在编译器无法优化程序时,即便开发者想自

己去优化,也是无能为力的。

(2)Java 的这种设计方式的优点

1)Java 的垃圾回收机制可以令缓存管理变得更加一致(也就是有更好的缓存一致性,

cache coherence),而不像采用 malloc/new 来分配内存时那样松散。

2)Java 语言内置了线程同步机制。

(3)C++ 照样受困于某些性能问题

1)由于指针可以指向任意地址,因此有可能导致两个类型不同的指针指向同一块内存

(这两个指针互为对方的别名 ),进而发生干扰。引入严格的别名规则(strict-aliasing rule),基本上能够解决此类问题。1

2)由于 C++ 必须先把代码生成并优化好,然后才能对其做动态链接,因此,那种跨越

多个动态模块的函数调用操作是不能内联的。

3)由于 C++ 的线程机制一般是通过程序库来支持的,因此,C++ 编译器无法做出线程

方面的优化。

问题 28 解释 C++ 的虚表(virtual table)。

解答:C++ 会采用一种特定的后期绑定机制来实现虚函数,这种机制就是虚表。它是

一张函数查找表,令系统在做动态绑定 / 后期绑定时,能够据此解析函数调用。这张表也称

为 vtable、virtual function table(虚函数表)或 virtual method table(虚方法表)。

虚表实际上是很简单的,只不过描述起来有点复杂。首先,每个采用虚函数的类以及

从采用虚函数的类里继承下来的类,都会有自己的虚表。这张表就是个静态数组,由编译

器在编译的时候设置。

凡是能够为该类对象所调用的虚函数,都对应于虚表中的某个条目。这些条目实际上

都是函数指针,分别指向该类所能访问并且距离自身最近的(most-derived,最具体的、派

生程度最大的)那个函数版本。

其次,编译器会给基类添加隐藏的指针,我们将其称为 __vptr。创建类的实例时,系

统会自动设置该指针,令其指向类的虚表。__vptr 与 this 不同,this 只不过是编译器所用的

参数而已,用来解析指向本对象的引用,而 __vptr 则是个真正的指针。

 alias each other,这种现象也称为类型双关(type punning)。—译者注

Page 48: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

48   程序员面试手册:概念、编程问题及面试题

说它是真正的指针,不仅意味着分配的类对象会比原来大,而且还意味着 __vptr 会被

派生类所继承。现在看个简单的例子:

由于总共有 3 个类,因此编译器会设置 3 张虚表,其中一张给基类 Base,另外两张分

别给派生类 D1 及 D2。编译器还会给使用了虚函数且在继承体系里最为基础的那个类添加

隐藏指针。在本例中,这个类指的是 Base。该指针是自动添加的,但我们可以想象一下添

加了这个指针之后的样子:

创建类对象之后,__vptr 就会指向该类的虚表。如果创建的对象是 Base 类型,那么它

的 __vptr 就会指向 Base 类的虚表。如果创建的对象是 D1 或 D2 类型,那么它的 _vptr 则会

指向 D1 或 D2 的虚表。

现在我们看看这些虚表是怎样填充的。由于本例只有两个虚函数,因此每张虚表里都

会出现两个条目,其中一个针对 function1(),另一个针对 function2()。刚才说过,填充虚表

的时候,每个条目都将指向该类对象所能访问且距离该类最近的那个函数版本。

Base 类的虚表很简单。由于 Base 类的对象只能访问 Base 版的 function1() 与 function2(),而无法访问定义在 D1 或 D2 里的那些版本,因此,在它的虚表中,针对 function1() 所设的条

目会指向 Base::function1(),而针对 function2() 所设的条目,则会指向 Base::function2()。

Page 49: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   49

D1 的虚表就有点复杂了。D1 类型的对象,既可以访问 D1 中的成员,又可以访问

Base 中的成员。但由于 D1 覆写了 Base 中的 function1(),因此,对于该类来说,D1 版的 function1() 函数与自身的距离,要比 Base 版的 function1() 函数更近。因此,针对

function1() 所设的那个条目会指向 D1::function1()。由于 D1 并没有覆写 function2(),因

此,那个条目所指向的函数,依然是 Base::function2()。D2 的虚表与 D1 类似,只不过针对

function1 的那个条目所指向的是 Base::function1(),而针对 function2 的那个条目指向的则

是 D2::function2()。图中每个类里面的 __vptr 都指向各自的虚表,而虚表中的每个条目,则指向当前类的

对象所能够调用且距离该类最近的那个版本。现在我们思考一下,如果创建类型为 D1 的对

象,那么会发生什么事情。

由于 cClass 是 D1 类型的对象,因此它的 __vptr 就会指向 D1 的虚表。现在,我们声明

一个基类指针,并令其指向 cClass 这个子类对象。

请注意,由于 pClass 是基类指针,因此,它只能指向 cClass 的基类部分。但同时我们

还看到,__vptr 正位于这个基类部分中,因此,pClass 是可以访问 __vptr 的。此外,我们

也知道,pClass->__vptr 所指向的是 D1 类的虚表。由此可见,即便 pClass 是个基类指针,

它也照样能访问子类 D1 的虚表。那么,调用 pClass->function1() 时,执行的究竟是哪个类

中的 function1() 呢?

首先,程序发现 function1() 是虚函数,然后,通过 pClass->__vptr 来获取 D1 的虚表;

接下来,从该表中查出应该执行的那个 function1() 版本。由于相关条目所指向的是 D1::fun-ction1(),因此,pClass->function1() 会解析到 D1::function1() 上。

有人可能会问,如果基类指针所指向的确实是个基类的对象,而不是子类 D1 的对象,

那么执行的还会是 D1::function1() 吗?这次不会了。

Page 50: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

50   程序员面试手册:概念、编程问题及面试题

在这种情况下,创建 cClass 之后,其 __vptr 会指向基类 Base 的虚表,而不是子类

D1 的虚表。于是,pClass->__vptr 自然也会指向基类的虚表。而基类虚表里面针对 fun-ction1() 所设的那个条目指向的是 Base::function1(),因此,pClass->function1() 就会解析

到 Base::function1(),这正是 Base 类型的对象所能够调用且距离 Base 自身最近的那个 fun-ction1() 版本。

有了虚表之后,即便我们通过基类指针或基类引用来调用函数,编译器及 C++ 程序也

依然能够将其解析到正确的虚函数上面。

调用虚函数要比调用非虚函数慢一些。这是因为,首先必须用 __vptr 指针来获取合适

的虚表,然后通过适当的下标来查询这张表,找出应该调用的函数版本,最后才可以真正

执行调用。这总共需要 3 步,与之相比,一般的间接函数调用只需两步,直接函数调用只

需一步。然而,在当前的计算机上,这多出来的一两步,其实花不了多少时间。

问题 29 编写函数来交换两个整数,此函数不能借助第 3 个整数(也就是说,不能使

用临时变量)。

解答:这道题有很多种解法。下面这段代码演示了这些办法。

问题 30 Java 语言的 public static void 是什么意思?

解答:public 关键字是一种访问限定符(access specifier),允许程序员控制类中的成员

是否能够为外界所见。如果在声明类的成员时给前面添加了 public 关键字,那么它就可以

为该类以外的代码所访问。反之,如果添加的是 private 关键字,则无法为该类以外的代码

所访问。换句话说,某方法是 public 的,就意味着其他对象与类型都能够看见并调用这个

方法。

Page 51: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   51

main() 方法是程序启动时所执行的方法,因为它要由类以外的代码来调用,所以必须

是 public 方法。

static 关键字使得该方法成为静态方法,也就是说,不需要制作类的实例,就可以直接

调用它。启动程序时所执行的 main() 方法必须是 static 的,因为 Java 解释器要在创建对象

之前先调用这个方法。

void 关键字会告诉编译器,这个方法没有返回值。有些方法是可以有返回值的。

问题 31 把 main 方法声明为 private,会出现什么结果?

解答:程序可以编译,但是运行的时候,会出现 Main method not public(Main 方法非

public)的错误消息。

问题 32 从 main 方法的签名里去掉 static 修饰符,会出现什么结果?

解答:程序可以编译,但在运行时会出现 NoSuchMethodError 错误。

问题 33 把 public static void 写成 static public void,会出现什么结果?

解答:程序可以正确地编译并运行。

问题 34 不给 main 方法声明 String[] 参数,会出现什么结果?

解答:程序可以编译,但在运行时会出现 NoSuchMethodError 错误。

问题 35 如果通过命令行来运行程序的时候,不提供参数,那么对于 main 方法的

String[] 参数来说,其首个元素会是什么?

解答:整个 String[] 数组都是空的,里面根本没有元素。与 C/C++ 程序不同,Java 程

序不会把自身的名称设置成字符串数组的首个元素。

问题 36 如果通过命令行来运行程序时不提供参数,那么 main 方法的 String[] 参数是

个空的数组,还是 null ?解答:是空的数组,不是 null。问题 37 在 Java 语言中,ArrayList 与 Vector 之间的重要区别是什么?

解答:Java 数组比 ArrayList 和 Vector 都快,因此,如果能提前知道元素的个数,那么

可以考虑使用数组。(数组的大小,并不能像 List 那样,随着元素个数而增长。)

ArrayList 与 Vector 都是专门的数据结构,它们在内部也使用数组。这些数据结构提

供了 add(...) 及 remove(...) 等便捷的方法,并且可以随着元素的数量而增长或收缩。此外,

ArrayList 还可以通过 indexOf(Object obj) 与 lastIndexOf(Object obj) 方法来查找元素所对应

的下标。

问题 38 讨论构造函数与析构函数,并说明它们在涉及继承的程序中的执行顺序。

解答:在 C++ 程序中,创建与删除对象并不是一件小事。每次创建类的实例时都要调

用构造函数,它的名称与类名相同,而且没有返回类型。析构函数也是这样来定义的,只

不过名称前要加上“~”符号:

Page 52: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

52   程序员面试手册:概念、编程问题及面试题

执行派生类的构造函数时,总是会调用基类的构造函数。只要创建派生类的对象,那

么基类的构造函数就会率先得到调用,然后才会执行派生类的构造函数。

(1)重要事项

1)无论在派生里面调用的是默认的构造函数,还是带有参数的构造函数,它都会执行

基类的默认构造函数。

2)如果想在调用派生类中带有参数的构造函数时,令基类里面带有参数的那个构造函

数得到调用,那么声明前者的时候,必须明确指出后者。

(2)派生类的构造函数会调用基类的默认构造函数

这段代码的输出如下:

由此可见,创建派生类的对象时,无论用的是默认构造函数,还是带有参数的构造函

数,都会调用基类的默认构造函数。

(3)在派生类的构造函数里面,调用基类中带有参数的构造函数

在派生类中声明带有参数的构造函数时,我们可以明确指出,它应该调用基类里面某

个带有参数的构造函数。

Page 53: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   53

这段代码的输出如下:

(4)执行派生类的构造函数时,为什么要调用基类的构造函数?

构造函数要执行一项特殊的任务,就是正确地将对象初始化。派生类的构造函数或许

只能访问派生类中的成员,但由于派生类会继承基类中的属性,因此,这些属性的初始化

工作,必须交由基类的构造函数来完成。由此可见,为了将类中的各个属性构建好,派生

类与基类的构造函数都必须得到调用才行。

(5)多重继承体系中的构造函数调用顺序

与单一继承类似,在多重继承的情况下,基类的构造函数也会随着派生类的构造函数

而得到调用,其顺序与继承顺序相同。

在这种情况下,程序会先执行 B 类的构造函数,然后执行 C 类的构造函数,最后执行

A 类的构造函数。

(6)C++ 中的向上转换

向上转换(upcast)是令超类的引用或指针指向子类的对象。或者也可以说:把子类的

引用或指针转为超类的引用或指针,称为向上转换。

与之相反的做法称为向下转换(downcast),也就是把超类的引用或指针转为子类的引

用或指针。我们稍后将会讨论这个话题。

(7)不会为子类所继承的函数

构造函数与析构函数不会为子类所继承,因而也不可能遭到覆盖。此外,赋值运算符

Page 54: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

54   程序员面试手册:概念、编程问题及面试题

“=”也是不能继承的,但是子类可以重载它。

(8)继承与静态函数

1)子类会继承超类的静态函数。

2)如果在子类中重新定义了静态成员函数,那么它会把超类中的各种重载版本全都隐

藏起来。

3)静态成员函数不能是虚函数。

(9)混合继承(Hybrid Inheritance)与虚类

在多重继承中,由于派生类会沿着不同的路径多次继承同一个基类,因此,会出现很

多有歧义的地方。

在这段代码中,B 类与 C 类都继承了 A 类的 show(),这导致 D 类中有两份 show() 函数。程序的 main() 函数在调用 show() 的时候会出现歧义,因为编译器不知道应该调用哪个

show() 才好。为此,我们在继承的时候,可以使用 virtual 关键字:

写了 virtual 之后,编译器就明白,随便调用哪一个 show() 都行。

(10)混合继承与构造函数的调用

实例化子类对象时,总是会调用基类的构造函数。于是,在刚才那样的混合继承体系

中,如果创建了 D 类的对象,那么其超类 B、C 及 A 的构造函数就都会得到调用。

而 B 类与 C 类都会调用其超类 A 的构造函数,这就导致 A 的构造函数将被执行两次,

我们并不想看到这种效果。把 A 设置为虚基类(virtual base class)可以避免这个问题。当

虚基类沿着多条不同的路径被具体的类(concrete class)所继承时,其构造函数只会在最

具体的那个类里面调用一次,对于本例来说,也就是只会在执行 D 类的构造函数时调用

一次。

问题 39 解释 Java 中的 HashMap 是怎样运作的。

解答:HashMap 是依靠哈希码(hash code)来运作的,当我们通过 put() 方法向其中存

储数据或通过 get() 方法从其中获取数据时,它都要计算哈希码。向 put() 方法传入键(key)与值(value)之后,为了保存数据,HashMap 需要在键对象上调用 hashCode() 方法,并根

据计算出来的哈希码以及自身的哈希函数,来确定对应的 bucket 位置,以便保存值对象。

关键在于,HashMap 是把键与值同时保存在 bucket 里面的,而非只保存值,不保存键。

Page 55: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   55

如果不了解这一点,那就无法解释 HashMap 是怎样把它所保存的对象获取出来的。

(11)如果两个不同的对象具有相同的哈希码,那会怎么样?

这是个容易引起一连串误解的地方。提问的人可能会说:既然两个对象的哈希码相同,

那么 HashMap 就认为这两个对象也相同,从而抛出异常,或拒绝保存。你可以告诉他,根

据 Java 语言的 equals() 与 hashCode() 方法所做的约定,两个不相同的对象,可以有相同的

哈希码。听到这话,有的人可能就不会再问下去了。如果他还继续问你:hashCode() 的返

回值相同,是不是就意味着 bucket 的位置也相同,进而发生冲突(collision)呢?你可以告

诉他,HashMap 在同一个 bucket 里是采用链表来存储各项数据的,因此,新来的这个值对

象,会接在原有的那些节点后面。

(12)如果两个不同的对象具有相同的哈希码,那么在获取的时候如何来区分呢?

确定对象所在的 bucket 位置之后,HashMap 会通过 equals() 方法,来把待查的键逐个

与链表中的键做比较,以便找到和前者相同的键对象,并将与该对象相关联的值对象返回

给调用方。

(13)如果 HashMap 的存储量超过了载荷因子所规定的量,会怎样?

要回答这个问题,必须先了解 HashMap 的运作细节。如果 HashMap 的存储量超过了

由负载因子(load factor)所规定的量,那么它就会扩容。如果载荷因子是 0.75,那么当占

用率超过 75% 时,HashMap 会扩容。扩容的时候,HashMap 会创建新的 bucket 数组,该

数组的大小是原来的两倍。然后,它会把原来的元素放在这个新的数组里面。这个过程叫

作 rehash,因为在确定新的 bucket 位置时,需要调用哈希函数。

问题 40 Java 的 HashMap 与 Hashtable 有什么区别?

解答:

1)HashMap 与 Hashtable 大致相同,只不过它是不同步的(non-synchronized),而且

允许 null 值。HashMap 的键对象与值对象可以是 null,但 Hashtable 不行。

2)HashMap 不保证其中的数据总是能以固定的顺序出现。

3)HashMap 是不同步的,而 Hashtable 则是同步的。

4)HashMap 的 Iterator 是一种 fail-fast 型的迭代器,它在可能出现故障时,会尽快暴

露错误,而 Hashtable 的 Enumeration 则不是。如果在迭代的时候,有其他线程绕开 Iterator的 remove() 方法,通过其他办法来向 HashMap 里面添加或删除元素,致使其结构发生变

化,那么 Iterator 就有可能抛出 ConcurrentModificationException 异常。JVM 会尽量做到这

一点,但无法完全保证。

问题 41 解释 Java 的线程机制。

解答:Java 支持多线程编程。在多线程的程序里面,有两个或两个以上的部分都能够

同时运行。每一个这样的部分就称作一条线程,每条线程都各自确定了一条执行路径(path of execution)。

前面说过,进程(process)中有一些由操作系统所分配的内存空间,而且包含一条或

Page 56: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

56   程序员面试手册:概念、编程问题及面试题

多条线程。线程不能独立存在,它必定是某个进程的一部分。如果进程中的每一条非守护

(non-daemon)线程都执行完了,那么该进程就可以结束。我们可以通过多线程技术来竭力

缩减 CPU 的空闲时间,以充分发挥其效用。

(1)创建线程

Java 语言中,有两种方式可以创建线程:

T 实现 Runnable 接口

T 继承 Thread 类

(2)通过实现 Runnable 接口来创建线程

最简单的线程创建方式就是用类来实现 Runnable 接口。为此,我们只需令该类实现名

为 run() 的方法即可。该方法的声明如下:

新线程的代码可以写在 run() 方法里面。大家要记住,run() 里面的代码与主线程中的代

码一样,也可以调用其他的方法、使用其他的类,或声明一些变量。

实现好 Runnable 接口之后,我们在类里面实例化 Thread 对象。Thread 有好几个构造

器,我们要用的是下面这个:

其中的 threadObject 参数指的是实现了 Runnable 接口的类实例,而 threadName 参数则

是新线程的名字。

创建好新线程之后,应该调用 start() 方法来启动它。这个方法是声明在 Thread 类中的:

(3)范例

下面这段代码会创建并运行新的线程:

Page 57: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   57

运行上面那段代码,会看到以下结果:

(4)通过继承 Thread 类来创建线程

还有一种办法也能创建线程,那就是继承 Thread 类,并创建该类的实例。子类必须覆

写 Thread 类的 run() 方法,这个方法是新线程的入口点。此外,还需调用 start() 方法,以

启动新线程。

范例:

如果把刚才那个例子改用继承 Thread 类的办法来写,那么就变成:

Page 58: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

58   程序员面试手册:概念、编程问题及面试题

运行上面那段代码,会看到以下结果:

下面列出 Thread 类的重要方法。

方  法 描  述

public void start() 在单独的执行路径里启动线程,并在这个 Thread 对象上调用 run()方法

public void run() 如果这个 Thread 对象是根据另一个 Runnable 来实例化的,那么就在那个 Runnable 对象上调用 run() 方法

public final void setName(String name)  修改 Thread 对象的名称。可以用 getName() 方法来获取线程的名称

public final void setPriority(int priority)  设置这个 Thread 对象的优先级。取值为 1~10

public final void setDaemon(boolean on) 如果参数为 true,那么表明该线程是守护线程(daemon thread,常驻线程)

public final void join(long millisec) 如果当前线程在执行过程中调用了另一个线程的 join 方法,那么前者就会阻塞,直到后者终止或经过了指定的毫秒数之后,才会继续

public void interrupt() 中断该线程。如果它因为某种原因而阻塞,那么调用这个方法之后,线程就会继续往下执行

public final boolean isAlive() 如果当前线程处于存活(alive)状态,那么该方法就返回 true。线程从开始执行,到运行完毕,都处于存活状态

上面那些方法必须在 Thread 对象上调用,而 Thread 类还有一些静态的方法,可以不指

明对象,直接调用。下列静态方法会在当前执行的线程上运作。

方  法 描  述

public static void yield() 使得当前正在运行的线程把执行权让给优先级相同并且等待执行的其他线程

Page 59: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   59

方  法 描  述

public static void sleep(long millisec)  使得当前正在运行的线程至少阻塞指定的毫秒数

public static boolean holdsLock(Object x) 如果与指定的那个对象有关的那把锁正在为当前的线程所持有,那么该方法就返回 true

public static Thread currentThread() 返回指向当前线程的引用,该引用所代表的线程就是调用这个方法的线程

public static void dumpStack() 打印出当前线程的 stack trace(栈跟踪、栈踪迹)信息,这在调试多线程应用程序的时候有用

范例:

下面这个 SampleThreadClassDemo 程序演示了 Thread 类中的一些方法。PrintMessage 是

个实现了 Runnable 接口的类:

下面是另外一个类,它继承了 Thread 类:

下面是主程序类,该类利用了刚才定义的那两个类:

(续)

Page 60: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

60   程序员面试手册:概念、编程问题及面试题

程序将会输出下面这样的结果。每次运行这个例子,都有可能看到不同的文字。

问题 42 解释 Java 的同步机制是怎样运作的。

解答:由于 Java 是多线程语言,可以用多条线程平行地执行程序,因此,同步(syn-chronization)是个很重要的概念。Java 代码可以用 synchronized 及 volatile 关键字实现同步。

synchronized 关键字主要提供下面这几个与并发编程有关的基本功能:

1)synchronized 关键字可以提供对共享资源的锁定能力,使得线程独占该资源,防止

其他线程过来争抢这些数据。

2)synchronized 关键字还会阻止编译器重新安排代码的顺序。如果不用 synchronized 或

volatile 关键字,那么编译器就有可能调整代码顺序,进而引发一些难于探查的并发问题。

3)synchronized 关键字确保了加锁与解锁的流程可以得到实施。线程在进入某个同步

的方法或代码块之前,必须先获取相关的锁,此时,它会直接从内存中读取数据,而不会

从缓存里读取。当该线程松开(release)这把锁时,它也会将数据直接写入内存,而不是放

在缓存里。这样做可以避免因内存不一致(memory inconsistency)而引发的错误。

(1)Java 的 synchronized 关键字

凡是位于 synchronized 范围内的代码都是线程互斥的,也就是说,同一时刻只能有一

条线程执行这些代码。synchronized 关键字可以用来修饰静态方法、非静态方法与代码块,

但是不能修饰变量。如果修饰了,那么编译的时候就会出错。

Page 61: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   61

Java 的变量虽然不能用 synchronized 修饰,但是可以用 volatile 来修饰,JVM 线程遇

到这种变量时,会直接从内存里面读取其值,而不会把它缓存起来。如果可以把同步的范

围局限在某块代码之内,那就不要将其扩展到整个方法上,因为前一种做法,只需要给最

关键的那些代码加锁,而不用把整个方法都锁起来。由于 Java 的同步机制会降低性能,因

此,只应该同步那些确实有必要加锁的代码才对。

(2)举例说明如何编写并使用 synchronized 方法(同步方法)

同步方法写起来很简单,只需把 synchronized 关键字放在方法前面即可。需要注意的

是:如果 synchronized 修饰的是静态方法,那么对应的锁就会加在代表那个类的 Class 对象

上;若修饰的不是静态方法,则会加在当前对象(也就是 this)上。

由此可见,静态与非静态的 synchronized 方法有可能会同时运行。新手在编写多线程 Java 代码时很容易犯这个错误。

上面这段代码,并没有合理地利用 Java 的同步机制,因为 getCount() 与 setCount() 方法锁住的不是同一个对象,它们有可能同时运行,进而产生错误的 count 值。getCount() 方法锁住的是 Counter.class 对象,而 setCount() 方法锁住的则是当前对象(也就是 this)。要

想写出正确的同步代码,我们可以把这两个方法都设为静态或非静态,也可以把方法前的

synchronized 去掉,改用同步的代码块来实现。

(3)举例说明如何编写并使用 synchronized 代码块(同步代码块、同步块)

同步代码块的编写方式与同步方法类似,也会用到 synchronized 关键字。但是要注意,

如果同步块想要锁定的那个对象(例如在下面这个例子中,同步块要对 Singleton.class 对象

加锁)是 null,那么程序就会抛出 NullPointerException 异常。

在实现这个 Singleton(单件、单例)时,我们采用了经典的双重检查锁机制(double checked locking,该写法详见第 3 章)。对本例来说,由于只有创建 Singleton 实例的那一部

Page 62: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

62   程序员面试手册:概念、编程问题及面试题

分代码才是最关键的,因此,只需把这几行代码用同步块包括起来就行了,而不用把整个

getInstance() 方法都设为同步方法。这样做可以提升程序的效率,使得调用该方法的线程只

会在初次创建实例时受到阻塞,而不至于在每次调用时都受到阻塞。

(4)在 Java 代码中使用 synchronized 关键字的注意事项

T Java 的 synchronized 关键字使得线程能够不受干扰地访问某一份共享资源。如果有

多个线程想在同一时刻执行某个同步方法,那么 Java 可以保证只有一个线程能够获

取到相应的锁,进而执行该方法。

T synchronized 关键字只能用来修饰方法或代码块。

T 线程进入同步方法或同步块时,必定会获取相应的锁,而当它离开同步方法或同步

块时,则会解开这把锁。无论是正常离开,还是因为错误或异常而离开,这把锁都

要解开。

T 线程进入的那个同步方法如果是类中的实例方法,那么它获取的锁就是对象级别的

锁;反之,若是静态方法,则会获取类级别的锁。

T Java 的 synchronized 代码是可以重入的(re-entrant)。这意味着,如果某个同步方

法调用另一个同步方法,而后者针对的那把锁已经被当前线程获取了,那么线程就

可以直接进入那个方法。

T 如果 synchronized 代码块想要加锁的那个对象是 null,那么程序就会抛出 Null-PointerException 异常。如果 myInstance 是 null,那么 synchronized(myInstance) 就会

抛出 NullPointerException 异常。

T synchronized 关键字的一项主要缺点就是无法实现并发读取。但可以改用下面这种

锁来实现:

ReadWriteLock.

T synchronized 关键字的一项局限在于,它只能对同一个 JVM 内的共享对象做访问

控制。如果需要在多个 JVM 所共用的文件系统或数据库之间同步,那就无法使用

synchronized 来实现了。此时需要实现一种涵盖范围更大的锁机制。

T Java 的同步机制会降低程序的效率。同步方法运行得很慢,进而会拖慢整个程序的

运行速度。因此,只应该在绝对必要的情况下才做同步,而且尽量不要把整个方法

都设为 synchronized,应该优先考虑采用同步代码块来包括其中最为关键的那部分

代码。

T 同步块要比同步方法好,因为前者可以只给最关键的那部分代码加锁,而不像后者

那样,必须对整个方法加锁,进而降低程序的效率。Singleton 类的 getInstance() 方法,很好地演示了这种用法。

T 同一个类中的静态同步方法与非静态同步方法有可能同时运行,因为它们锁住的对

象不一样。

T 如果同步代码写得不好,可能会导致死锁(deadlock)或饥饿(starvation)问题。

Page 63: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   63

T 根据 Java 语言规范,synchronized 关键字不能修饰构造器,这会导致编译错误。这

样规定其实是有道理的,因为当线程正在创建对象时,其他的线程是看不到这个对

象的,必须等创建完毕之后才能看到。

T synchronized 关键字不能修饰变量,volatile 关键字不能修饰方法。

T java.util.concurrent.locks 扩充了 synchronized 关键字,它的功能更多,例如提供了

可重入的锁及可中断的锁,因而可以用来编写更为复杂的程序。

T Java 的 synchronized 方法及 synchronized 代码块也会对内存做同步。

T 与同步有关的重要方法,例如 wait()、notify() 及 notifyAll() 等,都定义在 Object 类里。

T 编写同步代码块时,不要把锁加在非 final 的字段上面,因为这种引用可能会指向

其他对象,从而导致各线程所针对的并非同一把锁,这种情况相当于根本不加锁。

如,下面这段代码,就把锁加在了非 final 的字段上面:

如果在 Netbeans 或 IntelliJ 等 IDE 里面写出这样的代码,那么可能会收到 Synchronization on non-final field 字样的警告信息。

T 编写同步代码块时,不应该把锁加在 String 对象上,因为 String 对象是不可变

的(immutable),而这种不可变的字面量或内化(interned)之后的字符串会存放在

String 池中。如果其他代码或第三方程序库碰巧也要给 String 池中内容相同的字符

串加锁,那将导致两段完全无关的代码对同一把锁做同步,这会产生无法预料的行

为,而且会影响程序的性能。因此,编写同步代码块时,应该用 new Object() 来创

建对象,并把锁加在该对象上面。

T Java 库中的 Calendar 及 SimpleDateFormat 类并不是线程安全的(thread-safe),因

此,如果要在多线程环境下使用这些类,那就应该编写适当的同步代码。

问题 43 用 Java 线程解决生产者—消费者(producer/consumer)问题。

解答:参见第 4 章。

问题 44 C++ 的空类(empty class)对象有多大?

解答:由于每个对象都要有互不相同的地址,因此,不应该出现大小为 0 的对象(而且 C++ 标准也是这样规定的)。假如空类对象的大小是 0,那么在由这种对象所构成的数组中,

所有的对象就全都挤到数组开头去了,这导致它们的内存地址完全相同,从而违背了每个

对象都要有互不相同的地址这一要求。即便某个对象根本不占用任何空间,其对象的大小

Page 64: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

64   程序员面试手册:概念、编程问题及面试题

也依然大于 0,然而这并不会令其派生类的对象变大。

问题 45 在 C 与 C++ 中,i+++i++ 的值是什么?

解答:它的值是未定义的(undefined)。对于 C 与 C++ 的表达式来说,如果把其中的

同一个变量读取两次,而且在读的时候还修改了它的值,那结果就很难说了。代码里不应

该出现这种写法。此外,v[i]=i++; 以及 func(v[i], i++) 等写法也应该避免。

func(v[i], i++) 的结果之所以难于确定,是因为其参数没有固定的求值顺序。由于系统

想要生成更为高效的代码,因此它不打算把这个顺序规定下来。

问题 46 int main() 与 int main(void) 这两种写法,在 C 与 C++ 中,各有什么区别?

解答:在 C++ 语言中,这两种写法没有区别,它会把 func(void) 与 func() 这两种形式,

理解成同一个意思。但是在 C 语言中,如果空白的参数列表出现在编写函数原型(proto-type)的地方,那意味着该函数可以接受任意数量的参数(C++ 可不是这样);反之,若出现

在编写函数定义(definition)的地方,则意味着该函数没有参数。对于 C++ 来说,空白的

参数列表一定表示函数是没有参数的,而对于 C 语言来说,若想表达这个意思,则需在参

数列表中写上 void。

以上才是在 C 语言里面声明无参函数的正确方式,而且该方式对 C++ 同样适用。反

之,如果写成:

那么在 C 与 C++ 里就有了不同的含义。前者认为该函数可以接受任意数量的参数,而且每

个参数的类型也不加限制;但后者却认为它相当于 foo(void),也就是说,该函数不接受任

何参数。

问题 47 Java 中的 String 与 StringBuffer 有什么区别?

解答:Java 提供了 StringBuffer 与 String 这两个类,后者用来操作内容不变的字符串。

简言之,String 类型的对象用来表示那种只读的、内容不可修改的字符串;而 StringBuffer类型的对象则用来表示内容可修改的字符串。

这两者在性能方面的主要区别是,StringBuffer 能够比 String 更快地将字符串连接起来。

操作 String 时,经常需要连接字符串。我们通常会这样写:

Page 65: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   65

如果改用 StringBuffer 来连接,那么代码就变成:

有些开发者总以为第 1 种写法要比第 2 种快,他们觉得,用 append 方法来连接字符串其开

销要比用 + 运算符更大。

问题 48 Java 的 == 与 equals() 方法有什么区别?对象的浅对比与深对比有什么区别?

解答:如果两个引用都指向内存中的同一个对象,那么 == 返回 true,这就是浅对比

(shallow comparison)。而 equals() 方法的返回值则取决于该方法所在的类是怎样在对象的

属性之间做比较的。

equals() 方法可以执行深对比(deep comparison),也就是判断两个引用所指的对象是否

在逻辑上相等,而不是像 == 那样,仅仅判断它们是否指向内存中的同一个位置。如果在用

户自定义的类对象上调用 equals() 方法,而该类又没有覆写这个方法,那么调用的实际上是

继承自 Object 类的那个 equals()。该方法还是会像 == 运算符那样做浅层的对比,也就是判

断这两个引用有没有指向内存中的同一个对象。

问题 49 什么是序列化?如何将类中的字段排除在序列化范围之外,或者说,什么是

transient 变量?

解答:序列化(serialization)是一种读取或写入对象的过程,它能把对象的状态保存成

一系列字节,随后可以根据这些字节,把以前的对象重新构建出来。如果想把某个类的对

象标注成可以序列化的对象,那么就令该类实现 java.io.Serializable 接口。这是个仅仅起到

标记作用的接口(marker interface),它会告诉系统,该类的对象或许可以接受序列化处理,

从而保存成某种形式(通常是文件形式)的持久化数据。

transient(瞬时、暂态)变量不参与序列化。在对象的序列化过程中,凡是标注为 tran-sient 的字段,都不会纳入字节流。例如,文件句柄(file handle)、数据库连接,以及系统线

程等,就可以标注为 transient,因为这些对象只在当时的那个情境中才有意义,它们不应该

参与序列化。

问题 50 Java 中的 final、finally 与 finalize() 有什么区别?

解答:final 关键字用来声明固定不变的字段。此外,也可以用来声明类,使得该类不

能为其他类所继承;或用来声明方法,使得该方法不能为其他方法所覆写。finally 是处理

异常的时候用的。编写异常处理代码时,可以提供 finally 块。无论 try 块中发生了什么情

况(除非是调用 System.exit(0)),程序都会执行 finally 块里的代码,从而能够在此完成清理

工作,并释放相关的资源,例如关闭文件,断开与数据库之间的连接,终止对数据库的查

询等。

finalize() 方法或许对垃圾回收有所帮助。该方法可能会在垃圾回收器将要丢弃这个对

象时得到调用,使得对象能够完成一些清理工作。但是像文件句柄、socket(套接字),以

及数据库连接等非内存型的资源(non-memory resource),则不应该在这里释放。由于这些

Page 66: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

66   程序员面试手册:概念、编程问题及面试题

资源在 Java 中的名额有限,因此使用完之后要尽早释放,而不能等到垃圾回收器去调用

finalize() 方法的时候再释放,因为我们无法确定它究竟什么在时候调用这个方法。

问题 51 谈谈你对 Java 垃圾回收器的了解。

解答:Java 每次创建对象时,都会将其放在堆中,这是内存中的一块区域。Java 的堆

是一种可以在其上回收垃圾的堆(garbage collectable heap)。垃圾回收工作由系统来负责,

用户无法保证它一定能够得以执行。垃圾回收器或许在内存较为紧张的情况下运行,以便

丢弃那些不可达的对象(unreachable object),从而释放其所占据的内存。这个垃圾回收器

运行在优先级较低的守护线程(也就是后台线程)中,我们可以调用 System.gc() 方法来请

求它回收垃圾,但是无法强迫它必须这么做。

如果没有其他东西能够引用某个对象,那么这个对象就没有必要继续存在下去了,因

为既然不能引用它,那就没有办法用它来做事情。这样的对象可以视为不可达的对象,垃

圾回收器会把它们找出来。Java 系统可以定期回收这些不可达的对象,释放它们所占用的

内存,以便留给将来那些可达的对象(reachable object)使用。

问题 52 什么是菱形继承问题? Java 里面有这样的问题吗?如果有,如何避免?

解答:下面这张图解释了菱形继承问题(diamond problem,也称钻石形的继承问题)。

图中的 B 类与 C 类,都继承自同一个类,也就是 A。而 D 类又通过多重继承机制,同时继

承了 B 与 C。于是,它们四者之间就构成了菱形的继承关系,这就是菱形继承问题。

上面这张图所表示的继承方式的问题在于,如果实例化 D 类的对象,并且在它上面调

用 A 类所定义的方法,那么就会产生歧义,因为无法判断它想要调用的方法指的是从 B 类

继承下来的那个方法,还是从 C 类继承下来的那个方法。

(1)Java 没有多重继承

然而 Java 并不支持多重继承,因此,不会出现图中所示的菱形继承问题。C++ 支持多

重继承,因此可能会遇到这个问题,其详情可参考 C++ 中的菱形继承。

(2)Java 采用接口来模拟多重继承

Java 的接口可以用来模拟多重继承。尽管这种实现多个接口的做法看上去与多重继承

有点类似,但接口中的方法,只有一份实现代码,而不像多重继承那样,会通过多条路径

得到多份实现代码。这意味着 Java 不会遭遇菱形继承问题,也就是说,编译器不可能遇见

不知道该调用哪一个方法的情况。

问题 53 声明变量与定义变量有什么区别?

解答:声明变量就是仅仅提到变量的类型与名字,但并不做初始化;而定义变量则意

味着声明的同时,还做了初始化。例如,String s; 只是声明了变量 s 而已;但 String s=new

Page 67: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   67

String("abcd"); 与 String s="abcd";,就是给变量 s 下了定义。

问题 54 什么是序列化?

解答:序列化是一种将对象转为字节流,以保存其状态的机制。

问题 55 怎样把对象序列化到文件中?

解答:如果想要把某个实例序列化,那么就应该令该实例所在的类实现 Serializable 接

口。然后,把实例传给与 FileOutputStream 相连的 ObjectOutputStream 对象,这样就可以

把它保存到文件里了。

问题 56 解释 JDBC。

解答:JDBC 是 Java DataBase Connectivity(Java 数据库连接)的简称,这是一套可供 Java 程序执行 SQL 语句的 API,它可以和那些兼容于 SQL 的数据库相交互。由于所有的关

系型 DBMS(DataBase Management System,数据库管理系统)几乎都支持 SQL,而 Java本身也能够运行在绝大多数的平台上,因此,JDBC 使得同一个 Java 数据库应用程序可以

在各种平台上面运行,并与各种 DBMS 相交互。

JDBC 与 ODBC(Open DataBase Connectivity)类似,但它是专门设计给 Java 程序用的,

而不像后者那样,独立于具体的编程语言。ODBC 是一套访问数据库的标准方法,由 SQL Access Group 在 1992 年提出,其目标是令任何一款应用程序,都能在支持 ODBC 的数据库

管理系统上处理数据。

为此,ODBC 在应用程序与 DBMS 之间引入了名为数据库驱动程序(database driver)的中间层,旨在将应用程序所发出的数据查询请求,转译为 DBMS 所能理解的信息。要想

实现此效果,应用程序与 DBMS 就必须兼容 ODBC,也就是说,应用程序要能发出符合

ODBC 标准的命令,而 DBMS 也要能理解并回应这些命令。

(1)创建 JDBC 应用程序

下面将要讨论的这个 JDBC 应用程序,需要分 6 个步骤来构建。

1)引入 JDBC 包。

这一步是要把数据库编程所需的包引入程序,以便使用其中与 JDBC 有关的类。一般

来说,只引入 java.sql.* 就可以了。

2)注册 JDBC 驱动程序。

这一步是要初始化驱动程序,以便开启程序与数据库之间的通信渠道。我们用下面这

行代码来完成这项任务。

3)开启数据库连接。

这一步是要用 DriverManager.getConnection() 方法来创建 Connection 对象,以便表示程

序与数据库之间的物理连接。

Page 68: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

68   程序员面试手册:概念、编程问题及面试题

4)执行查询。

这一步是要把 SQL 查询语句构建成 Statement 或 PreparedStatement 类型的对象,并将

其提交给数据库。

如果要执行的不是查询语句,而是更新(UPDATE)、插入(INSERT)或删除(DELETE)语句,那就改用下面这样的写法:

5)从表示查询结果的集合中提取数据。

这一步是要把数据库所返回的数据提取出来。我们可以用 ResultSet.getXXX() 形式的

方法,从表示查询结果的集合中获取相关数据。

6)清理运行环境。

用完数据库之后,应该尽快清理数据库资源,而不要等到 JVM 回收垃圾的时候再去

清理。

(2)以范例程序来演示 JDBC 的用法

我们把这 6 个步骤整合为下面这个范例程序。这可以视为一个模板,以供将来编写 JDBC代码时参考。

Page 69: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   69

问题 57 在 C/C++ 中,const int *、const int * const 与 int const * 有什么区别?

解答:按照从右至左的顺序解读。

Page 70: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

70   程序员面试手册:概念、编程问题及面试题

int* 是个指针,它指向 int

int const * 是个指针,它指向 const int

int * const 是个 const 指针,它指向 int

int const * const 是个 const 指针,它指向 const int

例如,对于下面这 3 项指针声明来说:

我们可以按照从右至左的顺序,分别解读每个指针的含义。

1)const Item * ptr 是个名为 ptr 的指针,它指向 Item 对象,这个 Item 对象是 const 的。

也就是说,不能通过 ptr 来改变这个 Item 对象。

2)Item * const ptr 是个名为 ptr 的 const 指针,它指向 Item 对象。也就是说,可以通

过 ptr 来改变 Item 对象,但 ptr 本身是不能改变的。

3)const Item * const ptr 是个名为 ptr 的 const 指针,它指向 Item 对象,这个 Item 对象,

同样是 const 的。也就是说,既不能改变 ptr 本身,也不能通过 ptr 来改变这个 Item 对象。

指针符号(*)左边的那个 const,可以写在指针所指的类型左侧,也可以写在其右侧,

如下:

问题 58 .dll 与 .lib 有什么区别?

解答:.dll 是一种能够为可执行程序所共用的函数库。看看 Windows 系统的 system32文件夹你就会发现,其中有很多这样的文件。程序创建 dll 时,通常还会创建一份 lib 文件,

以供 *.exe 程序来解析 dll 所声明的符号。

.lib 是一种与程序静态链接的函数库,它们并不为其他程序所共享。*.lib 文件中的代码

会出现在与之相链接的程序里。如果 X.exe 与 Y.exe 这两个程序都与 Z.lib 相链接,那么 X与 Y 里会各自包含一段 Z.lib 的代码。

至于如何创建 dll 与 lib 文件,则要依照你所使用的编译器来定。每个编译器都有不同

的创建办法。

问题 59 大端序与小端序的区别是什么?

解答:大端序(big endian)与小端序(little endian)指的是在多字节的数据里面,究竟

是权重最大的那个字节存放在最低的内存地址上面,还是存放在最高的内存地址上面。

如果计算机的硬件先把多字节数据里面权重最小的那个字节,存放在最低的内存地址

上面,那么这种硬件就是小端序的硬件。这意味着,在这个多字节的整数中,最小的那一

端率先得到存储,其后每个内存地址上面所保存的字节,其权重都比前一个大。这种小端

在前的字节序可以理解成:把权重最小的字节,放在最低的内存地址上面。

Intel/AMD x86、Digital VAX 以及 Digital Alpha 等架构的计算机,都采用小端序来处理

Page 71: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   71

标量(scalar)。

如果计算机的硬件先把多字节数据里面权重最大的那个字节,存放在最低的内存地址

上面,那么这种硬件就是大端序的硬件。这意味着,在这个多字节的整数中,最大的那一

端率先得到存储,其后每个内存地址上面所保存的字节,其权重都比前一个小。这种大端

在前的字节序,可以理解成:把权重最大的字节,放在最低的内存地址上面。

IBM mainframe(IBM 大型机)、Motorola 680x0 系列、Sun SPARC 及 PowerPC 等架构的

计算机,以及绝大部分采用 RISC(精简指令集)架构的计算机,都采用大端序来处理标量。

以 4 字节整数为例,来说明大端序与小端序的区别。

下面考虑 0x44332211 这个 4 字节的整数。所谓小端,指的是权重最小的那个字节,也

就是 0x11,所谓大端,指的则是权重最大的那个字节,也就是 0x44。这个数字里的 4 字节

在大端序与小端序这两种存储方式之下,分别如下排列:

内存地址该地址上的字节值

(按照大端序来存储)该地址上的字节值

(按照小端序来存储)

104 11 44

103 22 33

102 33 22

101 44 11

下面这段代码,可以判断出你的计算机采用哪种端序:

问题 60 解释 C++ 的 STL 与容器。

解答:STL(Standard Template Library,标准模板库)是一套 C++ 模板类,其中有很多

模板化的通用类及通用函数,可以实现出各种常见的算法与数据结构,例如数组、列表、队

列、栈等。容器(container)是一种能够保存其他对象的对象,它所保存那些对象也称为元

素(element)。容器是以模板类的形式实现的,因为这样更加灵活,便于保存用户所需的各种

元素。

容器会管理其元素所占据的存储空间,还会提供成员函数,以供用户直接访问,或通

过迭代器。(一种与指针类似的对象,可以引用容器中的元素来间接访问这些元素。)

编程中常见的数据结构几乎都能找到对应的容器,例如动态数组可以用 vector 容器来

Page 72: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

72   程序员面试手册:概念、编程问题及面试题

表示,队列可以用 queue 来表示,栈可以用 stack 来表示,堆可以用 priority_queue 来表示,

链表可以用 list 来表示,树可以用 set 来表示,关联数组可以用 map 来表示……

许多容器都有相同的成员函数,并提供类似的功能。但我们在决定应该使用哪种容器时,

通常不仅要考虑容器所提供的功能,而且还要顾及某些成员函数的效率(或者说复杂度)。

对于线性容器(linear container)来说,这尤其重要,因为有些线性容器擅长插入或删除元

素,而另一些线性容器的强项则在于可以更加迅速地访问其元素。我们需要在两者之间权衡。

容 器 类

序列

● vector:序列形式的容器,用来表示容量可变的数组 ● deque :可以在头部或尾部插入 / 删除元素的数组,其首、尾两端都可以推入(push)或弹出(pop)元素

● list:由变量、结构体或对象所组成的链表,是一串可以任意修改的数据项

关联式的容器

● set(不允许有重复元素)、multiset(可以有重复元素):一组无序的数据项 ● map(其中的键必须各不相同)、multimap(可以有重复的键):以平衡二叉树(balanced

binary tree)结构来存放的一组键值对

容器适配器

● stack(LIFO,后进先出):只能在一端推入或弹出元素的一系列数据项 ● queue(FIFO,先进先出):可以在两端推入或弹出元素的一系列数据项 ● priority_queue:能够返回权限最高的元素

字符串 ● string:可以容纳并操作一系列字符 ● rope:可以容纳、管理并操作多个 string

二进制位 ● bitset:可以用更为直观的方式来保存并操作二进制位

数据操作 / 实用工具

● iterator:是个 STL 类,其对象可以用来表示 STL 容器中的位置。声明的时候应指出这是针对哪一种容器类的 iterator

● algorithm :是一些例行的流程,用来对容器中的元素执行查找(find)、计数(count)、排列(sort)、搜索(search)等操作

● auto_ptr:是个 STL 类,用来管理内存指针并防止内存泄漏

范例代码:

Page 73: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   73

问题 61 解释 C++ 的智能指针。

解答:C++ 的指针与对象是很强大的。C++ 的指针用得很广,然而对于内置的那种指

针来说,如果使用不当,可能产生无法预料的结果。内置的指针刚刚创建出来的时候,并

不会自动设为 NULL,将这种指针以 pointer == NULL 的形式与 NULL 做比较之后,开发

者会误认为它是个有效的指针,并对其解引用,从而导致未定义的行为。

其实我们只需要在创建完指针之后,提醒自己将其设为 NULL,就可以避免这个问题

了。由此来看,它并不是特别严重。但还有一些问题就不是这么简单了。如果你调用的那

个函数返回了一个指针,那么你就要思考,这个指针所指的内存是不是通过 new 或 malloc分配在堆上面的?如果是的话,那就必须有人负责删掉这块内存,否则将导致内存泄漏。

这个问题只有当开发者查阅了函数文档之后才能明白。

在多线程环境下,也会出现一些与指针有关的问题。例如,两条线程经常会同时使用

同一份数据,因此也就有可能用到同一个指针。如果其中一方删除了指针所指的内存数据,

而另外一方还按照原来的方式使用这个指针,那么会出现什么情况呢?

此外,如果使用了异常处理机制,那么还会遇到一个相当困难的问题,也就是如何保

证程序在发生异常之后,总是能够把原来分配的那些内存释放掉。例如,下面这段代码:

如果 Foo 抛出了异常,那应该怎么办?你必须把 Foo(或 Foo 调用的函数)所抛出的每

个异常都捕获下来,这样才能保证 ptr 所指向的内存可以得到释放,否则,将发生内存泄漏。

由上述问题可见,我们必须寻找比内置指针更好的办法才行。智能指针(smart pointer)正是

这样的一种解决办法。

智能指针的理念是用对象把内置的指针包裹起来。这些智能指针要么指向某个对象,

要么等于 NULL,也就是说,它们总能得到初始化,而且绝对不会指向已经释放的内存。

1)智能指针如果不是 NULL,那么必须指向有效的内存。

2)智能指针要带有引用计数,而且要将其所指的内存合理地释放掉(这意味着,它能

够安全地应对异常,同时又不会发生内存泄漏)。

3)智能指针用起来应该与内置的指针差不多。

由于智能指针用起来与内置的指针相似,因此,必须以非侵入式(non-intrusive)的方

式来实现。假如用侵入式的办法去做,那就需要设计公共基类。所有想要用该指针来指代

Page 74: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

74   程序员面试手册:概念、编程问题及面试题

的类,都必须继承那个基类,因为只有继承了那个基类,这些类的对象才能够获得引用计

数的能力,从而可以被智能指针所引用。用这种办法来实现智能指针,其适用范围仅局限

于由用户所定义的类型,而无法涵盖 int 与 float 这样的内置类型。反之,非侵入式的方法

则可以同时涵盖这两个方面,因为无论是使用内置的类型,还是使用由用户所定义的类型,

智能指针都会自己想办法来记录引用计数,而不需要开发者对类型做出修改。

有了智能指针程序员就不用担心内存管理问题了,也就是说,我们终于可以告别 delete 了。由于这些指针能够安全地复制、运行在多线程环境中,并且能够安全地应对异常状

况,因此,绝不会指向已经释放的内存。如果运用得当,还可以避免循环引用(circular references)。总之,它们用起来和内置的指针几乎完全相同(而且还有很多内置指针所不具

备的好处)。

问题 62 解释 C++ 的 auto_ptr 概念。

解答:C++ 标准库提供了一种名为 auto_ptr 的智能指针。这种指针称为自动指针

(automatic pointer),它拥有其所指向的那个对象。这意味着指针在析构的时候,会把该对

象所占据的内存也一起释放掉。这种指针本身并不负责分配内存,也不会统计与其对象所

占据的那块内存有关的引用计数。

auto_ptr 类重载了 * 与 -> 运算符,使得用户能够像普通的指针那样去访问动态对象。如

果想获取它所封装的那个原始指针,那么可以调用 get 方法。如:

我们要注意:auto_ptr 构造函数的声明中是写有 throw() 字样的,这说明它有可能在构

造的时候抛出异常。然而对于上面这个例子来说,如果构造 auto_ptr 时出现了异常,那就

要用相当复杂的代码才能把这些异常处理好,从而失去了使用智能指针的意义。智能指针

还提供 release 方法,用来与它所管理的内存对象脱离关系,并把原始的指针返回给调用

者。如果我们要在某一段很关键的代码中手工管理内存,那可以在程序即将进入这段代码

时,通过 release 方法,把智能指针里面的原始指针获取出来。下面我们来演示 release 的用

法。假设要编写这样一个函数:该函数动态地分配某个对象,并且把指向该对象的普通指针

返回给调用方。此外,函数还要为对象做很多初始化工作,如果这些工作任务在执行的时候

出错,那么函数会从不同的地方退出。这样的函数用自动指针写起来会比用内置指针更加

简洁。

Page 75: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   75

自动指针是不能复制的。如果想复制一份新的 auto_ptr 实例,那么原来那个实例中的

对象就会移交给新的指针,从而导致原有的指针失效。因此,将这样的指针与 STL 集合搭

配起来使用是很危险的,这会违背 STL 集合所应有的语义。

此外,如果两个自动指针都持有指向同一个对象的引用,那么程序的行为就是未定义

的(不过一般来说,会直接崩溃)。

根据 C++11 标准,应该使用 std::unique_ptr 来代替 std::auto_ptr。

问题 63 Java 中的序列化与反序列化是什么意思?

解答:把 Java 对象转为 Stream 的过程叫作序列化(serialization)。转成 Stream 之后,就

可以存到文件里面,或沿着网络发送出去了。此外,也可以在 socket 连接中使用。要接受序

列化处理的对象,其所属的类应该实现 Serializable 接口,我们用 java.io.ObjectOutputStream 把这样的对象写到文件或其他 OutputStream 对象里面。将序列化之后的流数据转回 Java 对

象的过程叫作反序列化(deserialization)。

问题 64 为什么说 Java 不是纯粹的面向对象编程语言?

解答:之所以说 Java 不是纯粹的面向对象语言,是因为它还支持 int、byte、short 及 long等原始类型。笔者认为,这些类型很有可能是为了简化编程工作而设立的。其实 Java 本来可

以完全不提供原始类型,而是以相应的类来表示这些类型,但那样做不会带来太大的好处。

Java 没有废弃原始类型,但它同时提供了对应的包装类,例如 Integer、Long 等。这些

类与原始类型相比多了一些方法。

问题 65 PATH 与 classpath 变量有什么区别?

解答:PATH 环境变量可以供操作系统来确定可执行文件的位置。因此,我们在安装 Java 或其他可执行程序时,会把相关的目录位置添加到 PATH 变量中,以便操作系统找到

这些程序。

classpath 变量,是专门给那个名叫 java 的可执行文件使用的,java 可以用它来确定类

文件的位置。运行 java 应用程序时,可以通过 classpath 指定目录、ZIP 文件及 JAR 文件等,

以供 Java 系统在其中寻找相应的类。

注意

Page 76: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

76   程序员面试手册:概念、编程问题及面试题

问题 66 一个 Java 源文件里可以有多个 public 类吗?

解答:不能。一个 Java 源文件里不能有两个或两个以上的 public 类。如果那些类不是

public 的,那么可以放在同一个源文件里。

问题 67 final 关键字是什么意思?

解答:final 关键字可以用来修饰类,以防其他类继承这个类。例如,String 类就是

final 类,我们无法继承它。

final 也可以用来修饰方法,以防止子类覆写该方法。

final 还可以用来修饰变量,使得该变量只能赋一次值。尽管如此,但变量所表示的那

个对象其状态依然能够发生变化。例如,当我们把某个对象赋给 final 变量之后,就不能再

将其他对象赋给这个变量了,然而该变量所指的对象本身还是可以发生变化的。

Java 接口中的变量默认具有 final 及 static 的特征。

问题 68 static 关键字是什么意思?

解答:static 可以用来修饰类中的变量,使得该类的所有对象都共用这个变量。

此外,也可以用来修饰方法。static 方法只能使用类中的 static 变量,而且也只能调用

类中的 static 方法。

问题 69 Java 中的 finally 及 finalize 是什么意思?

解答:finally 块是与 try-catch 结构搭配起来使用的,即便 try-catch 块里的代码抛出异

常,finally 块中的代码也依然能够得到执行。我们通常在其中释放由 try 块所创建的资源。

finalize() 是 Object 类中的特殊方法,可以在子类里面覆写。这个方法,通常由垃圾回

收器在回收对象时调用。我们之所以覆写该方法,通常是想在 Java 系统回收对象时,释放

其中的系统资源。

问题 70 能不能把类声明成 static ?

解答:顶级类(top-level class)不能声明成 static,但是内部类(inner class)可以。声

明成 static 的内部类,又称作静态嵌套类(static nested class)。

这些类其实与顶级类是相似的,之所以写成内部类,只不过是便于封装而已。

问题 71 什么是 static import ?解答:如果想使用类中的静态变量或静态方法,那我们通常会引入该类,然后通过类

名来访问相关的方法与变量。

同样的功能,还可以改用 static import 来实现。这样引入的静态方法与变量,看上去就

和本类自身的方法与变量一样,可以不加修饰,直接使用。

使用这样的引入方式,可能会带来混乱,因此应该尽量少用。滥用 static import 会使程

Page 77: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   77

序变得难以阅读和维护。

问题 72 Java 中的 Annotation 是什么意思?

解答:Java 中的 Annotation 可以用来注解(或者说标注)代码,以便提供与之有关的

信息。它们不会给受到注解的代码带来直接的影响。这个功能是从 Java 5 开始出现的。注

解是一种内嵌在程序里面的元数据(metadata),用来描述该程序本身。

这些注解可以由编译器或解析工具来解析。我们可以指定注解是仅在编译期有效,还是

可以持续到运行期。Java 内置的注解包括 @Override、@Deprecated 及 @SuppressWarnings 等。

问题 73 什么是匿名内部类?

解答:没有名字的内部类称为匿名内部类(anonymous inner class)。这些类都是用一

行代码来定义并实例化的。匿名内部类总会继承某个类或实现某个接口。由于它没有名字,

因此无法为其定义构造器。我们只能在定义匿名内部类的那个地方访问它。

问题 74 Java 中的 Classloader 是什么意思?

解答:Java 中的 Classloader 是一种程序,可以在我们想要访问某个类的时候,把相应

的字节码载入内存。我们也可以通过继承 ClassLoader 类并覆写 loadClass(String name) 方法

来自己编写 classloader。问题 75 this 关键字是什么意思?

解答:this 关键字是指向当前对象的引用。我们通常用它来确保代码使用的是对象中的

变量,而不是同名的局部变量。

此外,还可以在构造器里用 this 关键字调用其他版本的构造器。

问题 76 System 类是做什么用的?

解答:System 类属于 Java 的核心类(core class)。我们可以很方便地通过该类调用

System.out.print() 方法,来打印调试所需的 log 信息。

System 类是个 final 类,其中的方法都是 static 方法,我们不能继承这个类,也不能通

过覆写其中的方法来改变其行为。System 类没有提供 public 构造器,因此无法实例化,正

因为如此,它的所有方法都是 static 方法。

Page 78: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

78   程序员面试手册:概念、编程问题及面试题

System 类里面有一些工具方法,提供了复制数组、获取当前时间,以及读取环境变量

等功能。

问题 77 instanceof 关键字是什么意思?

解答:instanceof 关键字可以用来判断对象是否属于某个类,但尽量要少用。下面这段

代码演示了它的用法:

由于 str 的运行时类型是 String,因此,第 1 个 if 语句的值为 true,而第 2 个则为 false。问题 78 Java 里面可以使用不带 catch 块的 try 结构吗?

解答:可以。try-finally 形式的 try 结构,就不需要 catch 块。

问题 79 super 关键字是做什么用的?

解答:如果子类覆写了超类的方法,那么可以用 super 关键字访问那个方法。

子类的构造器可以用 super 关键字来调用超类的构造器,但必须把它写在构造器的第 1 行。

下面这段代码,演示了怎样在子类里面使用 super 关键字。

问题 80 解释 Java Servlet 与 JavaServer Pages(JSP)。

解答:JavaServer Page(JSP)是一种支持动态内容的网页开发技术,编程者可以利

用特殊的 JSP 标记,将 Java 代码插入 HTML 页面。这些标记大多以 <% 开头,并以 %>结尾。

Page 79: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   79

JSP 组件是一种 Java Servlet,用来充当 Java Web Application 中的用户界面。Web 开发者

编写 JSP 的时候,可以混用 HTML 或 XHTML 代码及 XML 元素,并嵌入 JSP 动作与命令。

利用 JSP 技术,我们可以把用户输入网页表单的内容获取过来,并据此向其展示数据

库或其他数据源中的记录,此外,还可以动态地创建 Web 页面。

JSP 标记有很多用途,例如获取数据库里的信息,注册用户的配置信息(user pre-ferences,用户偏好),访问 JavaBeans 组件,传递页面控制权,以及在请求与页面之间共享

信息等。

用 JSP 写出来的程序通常也具备与 CGI(Common Gateway Interface,通用网关接口)

程序类似的功能,并且还有几个优于 CGI 的地方:

T JSP 的性能比 GCI 好很多,因为 JSP 可以把动态内容直接嵌入 HTML 页面,而不

像 CGI 那样,要用单独的文件来写。

T JSP 总是先经过编译,然后才由服务器处理,而不像 CGI/Perl 那样,每次遇到页面

请求,都需要由服务器端载入解释器及目标脚本。

T JSP 页面是基于 Java Servlet API 构建的,因此与 Servlet 一样,也可以访问各种功

能强大的企业级 Java API,例如 JDBC、JNDI、EJB、JAXP 等。

T JSP 页面可以与 Servlet 结合起来处理业务逻辑,这种处理方式受到 Java Servlet 模板引擎的支持。

JSP 是 Java EE 的一部分。由于 Java EE 是完整的企业级应用程序平台,因此,这意味

着 JSP 不仅能编写简单的应用程序,而且还能编写极为复杂、要求极高的应用程序。

(1)JSP 的工作原理

Web 服务器(例如 Apache)需要用 JSP 引擎(也就是 JSP 容器)来处理 JSP 页面。这种

容器(例如 WebLogic)负责把针对 JSP 页面的请求拦截下来。下面这部分内容,用 Apache来解释 JSP 的工作原理,Apache 内置了开发 JSP 所需的容器。

JSP 容器与 Web 服务器相配合,以提供 JSP 所需的运行时环境及其他服务。

Web 服务器会按照下列步骤,使用 JSP 创建出 Web 页面:

1)与访问普通页面时一样,浏览器会给 Web 服务器发送 HTTP 请求。

2)Web 服务器发现这是个针对 JSP 页面的 HTTP 请求,于是将其转发给 JSP 引擎。发

Page 80: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

80   程序员面试手册:概念、编程问题及面试题

出这种请求时所用的 URL 或 JSP 网址,通常以 .jsp 而非 .html 结尾。

3)JSP 引擎将磁盘中的 JSP 页面加载进来,并将其转换成 Servlet 内容。转换过程很简

单,只需把模板中的文本变为 println() 语句,并把 JSP 元素变为对应的 Java 代码,以实现

页面中的动态行为就可以了。

4)JSP 引擎会将 Servlet 编译成可执行的 class,并把原始的请求转发给 Servlet 引擎。

5)这个 Servlet 引擎是 Web 服务器中的一部分,用以加载并执行 Servlet class。执行期

间,Servlet 会产生 HTML 格式的输出信息,Servlet 引擎把这些信息放在 HTTP 响应消息里

面,传给 Web 服务器。

6)Web 服务器以静态 HTML 的形式将 HTTP 响应消息转发给浏览器。

7)浏览器会像处理静态页面那样处理 HTTP 响应消息中的这些动态生成的 HTML页面。

(2)JSP 的生命期

JSP 的生命期可以认为是 JSP 从创建到销毁的整个过程,该过程与 Servlet 的生命期类

似,只不过多出来了一个把 JSP 编译成 Servlet 的步骤而已。JSP 要经历下面这几个阶段:

T 编译

T 初始化

T 执行

T 清理

1)编译:浏览器针对 JSP 发出请求之后,JSP 引擎首先会判断是否需要编译这个 JSP 页面。如果页面从来没有被编译过,或是上次编译之后所有变动,那么 JSP 引擎就会编译

该页面。编译的过程可以分成 3 小步:

T 解析 JSP T 将 JSP 变为 Servlet T 编译 Servlet

2)初始化:容器加载 JSP 的时候,先调用 jspInit() 方法,然后再开始处理请求。如果

要想执行特定的 JSP 初始化工作,那就覆写 jspInit() 方法。

一般来说,初始化只执行一次,而且与 init 方法类似,我们在 jspInit 方法中,通常也

可以初始化数据库连接、开启文件,或创建查找表。

3)执行:在 JSP 的生命期里面,JSP 与 HTTP 请求之间的全部交互都发生于该阶段,

直到 JSP 销毁为止。JSP 引擎在前面两个阶段已经收到了由浏览器发来的请求,然后据此加

载并初始化了相应的 JSP 页面,现在,它会调用 JSP 的 _jspService() 方法。该方法的两个

参数分别为 HttpServletRequest 及 HttpServletResponse 类型。

Page 81: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

第 1 章 编 程 基 础   81

每处理一次请求,就要调用一次 _jspSerive() 方法,以便生成针对该请求的应答。这个

方法也负责对 GET、POST 及 DELETE 等全部 7 种 HTTP 请求方式给出应答。

4)清理:容器开始移除某个 JSP 的时候,这个 JSP 就进入了生命期的销毁阶段。此时 jspDestroy() 方法会得到调用,该方法相当于 Servlet 的 destroy() 方法。如果需要做一些清

理,例如释放数据库连接或关闭文件,那么就覆写 jspDestroy() 方法。该方法的形式如下。

jspInit()、_jspService() 以及 jspDestroy(),合起来称为 JSP 的生命期方法。

问题 81 什么是 JavaBeans ?

解答:JavaBeans 是 Oracle 提供的面向对象编程接口,可以用来构建可复用的应用程序

与程序块,这些内容称为组件,它们能够部署在由主流的操作系统平台所运作的网络里面。

与 Java applet 一样,JavaBeans 组件(这种组件也称为 Bean)可以给互联网页面或其他应用

程序提供交互功能,例如,可以根据用户或浏览器的特点,在页面中提供利率计算功能或

其他一些内容。

从用户的角度来看,组件可以指某个用来操作的按钮,或按下该按钮后所启动的某个

小型计算机程序。而从开发者的角度来看,按钮组件与计算器组件其实可以分开创建,而

且各自都能够与其他应用程序或情境中的组件相结合。

使用组件或 Bean 时,Bean 的属性(如视窗的背景颜色)对其他 Bean 来说是可见的,那

些没有见过它的 Bean,可以由此来与这个 Bean 动态地交互,以了解对方的属性。

Bean 是用 Oracle 的 Beans Development Kit(BDK)开发的,可以运行在主流操作系统

的各种应用程序环境(也称为容器)中,例如运行在浏览器以及文字处理软件中。

构建 JavaBeans 组件时,需要用 Oracle 的 Java 语言来编程,并编写一些描述组件属性

的 JavaBeans 语句,以说明用户界面具备哪些特征,以及触发什么事件可以令该 Bean 与同

一容器或网络里的其他 Bean 相互交流等。

企业版的 JavaBeans,通常称为 EJB。

问题 82 什么是 Hibernate 框架?

解答:Hibernate 是针对 Java 语言的对象关系映射库,这种框架可以把面向对象的领域

模型映射到传统的关系型数据库上面。

配置 XML 文件或使用 Java 注解,就可以将 Java 的类映射成数据库中的表格。

Hibernate 的主要功能是把 Java 类映射成数据库表格,并把 Java 数据类型映射成 SQL数据类型。此外,Hibernate 也提供了查询及获取数据的功能。它会生成 SQL 调用,使得开

发者不用手工处理结果集并转换对象。

Page 82: 编 程 基 础 - Baiduimages.china-pub.com/ebook7500001-7505000/7502217/ch01.pdf2 程序员面试手册:概念、编程问题及面试题 ... 示,那么取值范围就是-32 768~+32

82   程序员面试手册:概念、编程问题及面试题

问题 83 什么是 Java Struts ?

解答:Struts 是最流行的 Java Web 应用程序开发框架,它是由 Apache 所启动的开源项

目。这套框架基于 MVC(Model—View—Controller,模型—视图—控制器)架构。

问题 84 解释 Java Spring 框架。

解答:Spring 是一个开发 Java 应用程序的框架,它提供了丰富的基础设施(infrastruc-ture),从而令开发者可以专心制作应用程序。

有了 Spring 之后,我们可以用普通的 Java 对象(Plain Old Java Object,POJO)来构建

应用程序,并将企业服务顺畅地运用到这些对象上。这种做法适用于 Java SE 编程模型,有

时也能够完全或部分地适用于 Java EE。

Spring 框架的优势如下:

1)可以在数据库事务(database transaction)中执行 Java 方法,而不必借助相关的事

务 API。2)可以像调用 Java 本地方法(local method)那样调用远程例程(remote procedure),

而不必借助远程 API。3)可以将 Java 本地方法当作管理操作(management operation)来用,而不必借助 JMX

API。4)可以将 Java 本地方法当作消息处理程序来用,而不必借助 JMS API。