65.9K
CodeProject 正在变化。 阅读更多。
Home

摘自《Learning Objective-C 2.0: A Hands-On Guide to Objective-C for Mac and iOS Developers》

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2010年9月22日

CPOL

47分钟阅读

viewsIcon

27146

本章仅涵盖 C 语言的基础部分

Learning-Objective-C-2.0.jpg

Robert Clair
由 Addison-Wesley Professional 出版
ISBN-10: 0-321-71138-6
ISBN-13: 978-0-321-71138-0

Objective-C 是 C 的一个扩展。本书的大部分内容都集中在 Objective-C 为 C 增加了什么,但是要编程使用 Objective-C,你必须了解 C 的基础知识。当你做一些普通的事情,比如将两个数字相加、在代码中添加注释或使用 if 语句时,你在 C 和 Objective-C 中都以相同的方式进行。Objective-C 中非面向对象的这部分不是类似于 C类 C,它就是 C。Objective-C 2.0 目前基于 C99 标准。

本章开始为期两章的 C 语言回顾。这次回顾不是对 C 语言的完整描述;它只涵盖了语言的基本部分。位运算符、类型转换的细节、Unicode 字符、带参数的宏以及其他一些晦涩难懂的主题都没有提及。它旨在作为提醒,帮助那些 C 语言知识生疏的人,或者作为那些擅长从上下文中学习新语言的人的快速参考。下一章继续回顾 C 语言,并讨论声明变量、变量作用域以及 C 将变量存放在内存中的位置。如果你是 C/C++ 专家,你大概可以跳过本章。(不过,回顾总是有益的。我在写本章的过程中也学到了一些东西。)如果你是从 Java 或 C# 等其他类 C 语言转向 Objective-C,你可能至少应该粗略地看一下这些内容。如果你的编程经验仅限于脚本语言,或者你是一个完全的初学者,你可能会发现与本书并行阅读一本 C 语言书籍很有帮助。

注意 - 我建议每个人都阅读第 2 章“更多关于 C 变量”。根据我的经验,许多应该熟悉其中内容的人并不熟悉。

关于 C 的书有很多。Kernighan 和 Ritchie 的原著《C Programming Language》仍然是最好的书之一。1 它是大多数人用来学习 C 的书。如果你想了解 C 语言的“律师级”视角,或者探索语言的一些更深层次的角落,请查阅 Harbison 和 Steele 的《C: A Reference Manual》。2

花点时间想想你将如何学习一门新的自然语言。首先要做的是看看这门语言是如何书写的:它使用什么字母表(如果它根本使用字母表;有些语言使用象形文字)?它从左到右、从右到左还是从上到下阅读?然后你开始学习一些词语。你需要至少一个小词汇量来开始。随着词汇量的积累,你可以开始将词语组合成短语,然后开始将短语组合成完整的句子。最后,你可以将句子组合成完整的段落。

这次 C 语言回顾大致遵循相同的顺序。第一部分探讨 C 程序的结构,C 代码如何格式化,以及命名各种实体的规则和约定。接下来的部分将涵盖变量和运算符,它们大致相当于自然语言中的名词和动词,以及它们如何组合成更长的表达式和语句。最后一个主要部分将涵盖控制语句。控制语句允许程序执行比线性顺序执行语句更有趣的事情。回顾的最后一部分将涵盖 C 预处理器,它允许你在源文件发送给编译器之前对其进行一些程序化编辑,以及 printf 函数,它用于字符输出。

C 程序结构

本章首先介绍 C 程序的一些结构方面:main 函数、格式化问题、注释、名称和命名约定以及文件类型。

main 函数

所有 C 程序都有一个 main 函数。在操作系统加载 C 程序后,程序将从 main 函数的第一行代码开始执行。main 函数的标准形式如下:

int main(int argc, const char * argv[]) 
{
  // The code that does the work goes here
  return 0;
}

关键特性是:

  • 第一行的前导 int 表示 main 向操作系统返回一个整数值作为返回码。
  • 名称 main 是必需的。
  • 第一行的其余部分指的是从操作系统传递给程序的命令行参数。main 接收 argc 个参数,存储在 argv 数组中作为字符串。目前这部分不重要;只需忽略它。
  • 所有可执行代码都放在一对花括号之间。
  • return 0; 行表示将零作为返回码返回给操作系统。在 Unix 系统(包括 Mac OS X 和 iOS)中,返回码零表示“无错误”,任何其他值都表示某种错误。

如果你不关心处理命令行参数或向操作系统返回错误码(例如,在进行接下来几章的练习时),你可以使用 main 的简化形式:

int main( void ) 
{
  
}

void 表示此版本的 main 不接受任何参数。如果没有显式的 return 语句,则隐含返回值为零。

格式化

语句以分号终止。必须使用空白字符(空格、制表符或换行符)来分隔名称和关键字。C 会忽略任何额外的空白:缩进和额外的空格对编译后的可执行文件没有影响;它们可以自由使用以使你的代码更具可读性。一个语句可以跨越多行;以下三个语句是等效的:

distance = rate*time;
 
    distance     =      rate   *   time;
 
distance = 
    rate * 
        time;

注释

注释是为了程序员的理解而写的。编译器会忽略它们。

C 支持两种注释风格:

  • 在两个正斜杠(//)之后、行尾之前的所有内容都是注释。例如:
  • // This is a comment.
  • 被 /* 和 */ 包围的任何内容也是注释:
  • /* This is the other style of comment */

这种注释可以跨越多行。例如:

/* This is
      a longer
          comment. */

它可用于在开发过程中暂时“注释掉”代码块。

这种风格的注释不能嵌套:

/*  /* WRONG - won't compile */ */

然而,以下是合法的:

/*
    // OK - can nest end of line comment
*/

变量和函数名

C 中的变量和函数名由字母、数字和下划线字符(_)组成:

  • 第一个字符必须是下划线或字母。
  • 名称区分大小写:bandersnatch 和 Bandersnatch 是不同的名称。
  • 名称中间不能有任何空白。

以下是一些合法的名称:

j
taxesForYear2010
bananas_per_bunch
bananasPerBunch

这些名称是不合法的:

2010YearTaxes
rock&roll
bananas per bunch

命名约定

为了方便你自己以及任何可能阅读你代码的人,你应该为变量和函数使用描述性的名称。bpb 易于输入,但一年后你可能会费力地去理解它;而 bananas_per_bunch 则一目了然。

许多纯 C 程序使用下划线分隔长变量名和函数名中的单词的约定:

apples_per_basket

Objective-C 程序员通常使用驼峰式命名(CamelCase)来命名变量。驼峰式命名使用大写字母来标记名称中后续单词的开头:

applesPerBasket

以下划线开头的名称传统上用于旨在私有或内部使用的变量和函数:

_privateVariable
_leaveMeAlone

然而,这是一种约定;C 没有强制机制来保持变量或函数的私有性。

文件

纯 C 程序的代码放在一个或多个具有.c 文件名扩展名的文件中:

ACProgram.c

注意 - Mac OS X 文件名不区分大小写。文件系统会记住你为文件命名的格式,但它会将myfile.cMYFILE.cMyFile.c 视为同一个文件名。

使用 Objective-C 对象(从第 3 章“面向对象编程入门”开始介绍的内容)的代码放在一个或多个具有 .m 文件名扩展名的文件中:

AnObjectiveCProgram.m

注意 - 因为 C 是 Objective-C 的一个 proper subset,所以将一个纯 C 程序放在 .m 文件中是没问题的。

对于定义和实现 Objective-C 类(在第 3 章中讨论)的文件,有一些命名约定,但 C 语言本身并没有对文件名扩展名之前的这部分名称有任何正式规则。将包含一个会计程序代码的文件命名为 foo.m 是愚蠢的,但并非非法。

MyFlightToRio.m

C 程序也使用头文件。头文件通常包含许多.c 和 .m 文件共享的各种定义。它们的内容通过使用 #include 或 #import 预处理器指令合并到其他文件中。(参见本章后面的预处理器。)头文件具有.h 文件名扩展名,如下所示:

AHeaderFile.h

注意 - 这个主题超出了本书的范围,但有可能在同一个程序中混合 Objective-C 和 C++ 代码。结果称为 Objective-C++。Objective-C++ 代码必须放在具有 .mm 文件名扩展名的文件中。

AnObjectiveCPlusPlusProgram.mm

有关更多信息,请参阅 Using C++ With Objective-C

变量

变量是程序中某个内存字节的名称。当你给变量赋值时,你实际上是在将该值存储在那些字节中。计算机语言中的变量就像自然语言中的名词。它们代表你程序问题空间中的项目或数量。

C 要求你通过声明来告知编译器你将要使用的任何变量。变量声明的形式如下:

variabletype name;

C 允许在单个声明中声明多个变量:

variabletype name1, name2, name3;

变量声明会使编译器为该变量保留存储空间(内存)。变量的值是其内存位置的内容。下一章将更详细地介绍变量声明。它将涵盖变量声明的位置、变量在内存中的创建位置以及不同类别的变量的生存期。

整数类型

C 提供了以下类型来存储整数:char、short、int、long 和 long long。表 1.1 显示了 32 位和 64 位 Mac OS X 可执行文件中整数类型的大小。(32 位和 64 位可执行文件在附录 C,“32 位和 64 位”中讨论。)

char 类型之所以命名为char,是因为它最初 intended 用于存储字符;但它经常被用作 8 位整数类型。

表 1.1 整数类型的大小

类型 32 位 64 位
char 1 字节 1 字节
short 2 字节 2 字节
int 4 字节 4 字节
long 4 字节 8 字节
long long 8 字节 8 字节

整数类型可以声明为 unsigned(无符号):

unsigned char a;
unsigned short b;
unsigned int c;
unsigned long d;
unsigned long long e;

单独使用时,unsigned 被理解为 unsigned int。

unsigned a;  // a is an unsigned int

无符号变量的位模式始终被解释为正数。如果你给一个无符号变量赋一个负数,结果将是一个非常大的正数。这几乎总是一个错误。

浮点类型

C 的浮点类型是 float、double 和 long double。在 32 位和 64 位可执行文件中,浮点类型的大小是相同的。

float aFloat;   // floats are 4 bytes
double aDouble; // doubles are 8 bytes
long double aLongDouble;  // long doubles are 16 bytes

浮点值始终是有符号的。

真值

普通表达式通常用于真值。求值为零的表达式被视为假,求值不为零的表达式被视为真(参见以下边栏)。

_Bool、bool 和 BOOL - C 的早期版本没有定义布尔类型。普通表达式(现在仍然如此)用于布尔值(真值)。如正文中所示,求值为零的表达式被视为假,求值不为零的表达式被视为真。大多数 C 代码仍以此方式编写。

C99,即 C 的当前标准,引入了 _Bool 类型。_Bool 是一个整数类型,只有两个允许值:0 和 1。将任何非零值赋给 _Bool 都会得到 1。

_Bool  b = 35;   // b is now 1

如果你在源代​​码文件中包含 stdbool.h 文件,你可以使用 bool 作为 _Bool 的别名,并使用布尔常量 true 和 false。(true 和 false 分别定义为 1 和 0。)

#include <stdbool.h>
bool b = true;

你很少会在 Objective-C 代码中看到 _Bool 或 bool,因为 Objective-C 定义了自己的布尔类型 BOOL。BOOL 在第 3 章中介绍。

初始化

变量可以在声明时进行初始化:

int a = 9;
 
int b = 2*4;
 
float c = 3.14159;
 
char d = 'a';

用单引号括起来的字符是字符常量。它在数值上等于该字符的编码值。这里,变量 d 的数值为 97,这是字符 'a' 的 ASCII 值。

指针

指针是一个值为内存地址的变量。它“指向”内存中的一个位置。

通过在声明中在变量名前加上 * 来声明一个变量为指针。以下代码声明 pointerVar 为指向内存中存储整数的位置的变量:

int *pointerVar;

一元 & 运算符(“地址”运算符)用于获取变量的地址,以便可以将其存储在指针变量中。以下代码将指针变量 b 的值设置为整数变量 a 的地址:

1 int a = 9;
2 
3 int *b;
4 
5 b = &a;

现在让我们逐行分析那个例子:

  • 第 1 行将 a 声明为 int 变量。编译器为 a 分配了四字节存储空间,并将其初始化为 9。
  • 第 3 行声明 b 为 int 指针。
  • 第 5 行使用 & 运算符获取 a 的地址,然后将 a 的地址赋给 b。

图 1.1 说明了该过程。(假设编译器已将 a 定位在内存地址 1048880。)图中的箭头显示了指针的概念。

01LOC01.jpg

图 1.1 指针变量

一元 * 运算符(称为“内容”或“解引用”运算符)用于通过使用指向该位置的指针变量来设置或检索内存位置的内容。一种思考方式是,将表达式 *pointerVar 视为 pointerVar 的内容中存储的任何内存位置的别名,另一个名称。表达式 *pointerVar 可用于设置或检索该内存位置的内容。在以下代码中,b 被设置为 a 的地址,因此 *b 成为 a 的别名:

int a;
int c;
int *b;
 
a = 9;
 
b = &a;
 
c = *b;  // c is now 9
 
*b = 10; // a is now 10

指针在 C 中用于引用动态分配的内存(第 2 章)。指针也用于避免复制大块内存,例如数组和结构(本章后面讨论),从程序的某一部分到另一部分。例如,与其将一个大的结构传递给一个函数,不如将指向该结构的指针传递给函数。然后函数使用该指针访问该结构。正如你稍后将在书中看到的,Objective-C 对象总是通过指针引用的。

通用指针

声明为 void 指针的变量是通用指针:

void *genericPointer;

通用指针可以设置为指向任何变量类型的地址:

int a = 9;
void *genericPointer;
genericPointer = &a;

然而,尝试从通用指针获取值是错误的,因为编译器不知道如何解释通用指针所指示地址处的字节:

int a = 9;
int b;
void *genericPointer;
genericPointer = &a;
b = *genericPointer;  // WRONG - won't compile

要通过 void* 指针获取值,你必须将其强制转换为指向已知类型的指针:

int a = 9;
int b;
void *genericPointer;
genericPointer = &a;
b = *((int*) genericPointer) ;  // OK - b is now 9

cast 运算符 (int*) 强制编译器将 genericPointer 视为指向整数的指针。(参见本章后面的转换和类型转换。)

C 不会检查指针变量是否指向有效内存区域。指针的错误使用可能比 C 编程的任何其他方面导致更多崩溃。

数组

C 数组是通过将数组中的元素数量(包含在方括号 [] 中)添加到声明中,在类型和数组名称之后来声明的:

int a[100];

通过在数组名称后面的 [] 中放置元素的索引来访问数组的单个元素:

a[6] = 9;

索引是基于零的。在前面的示例中,合法的索引范围是 0 到 99。C 数组的访问两端都没有边界检查。C 会毫不犹豫地让你这样做:

int a[100];
a[200] = 25;
a[-100] = 30;

使用超出数组范围的索引会让你破坏属于其他变量的内存,从而导致崩溃或数据损坏。利用这种缺乏检查的特性是恶意软件的支柱之一。

括号表示法只是指针算术的一种更友好的语法。数组名(不带数组括号)是指向数组开头的指针变量。 这两行是完全等价的:

a[6] = 9;
 
*(a + 6) = 9;

在编译使用指针算术的表达式时,编译器会考虑指针指向的类型的大小。如果 a 是 int 数组,则表达式 *(a + 2) 指的是数组 a 开头之后 8 个字节(一个 int 的大小)的内存中 4 个字节(一个 int 的大小)的内容。但是,如果 a 是 char 数组,则表达式 *(a + 2) 指的是数组 a 开头之后 2 个字节(一个 char 的大小)的内存中 1 个字节(一个 char 的大小)的内容。

多维数组

多维数组声明如下:

int b[4][10];

多维数组按行线性存储在内存中。这里,b[0][0] 是第一个元素,b[0][1] 是第二个元素,b[1][0] 是第十一个元素。

使用指针表示法

b[i][j]

可以写成

*(b + i*10 + j)

字符串

C 字符串是终止于零字节的一维字节数组(char 类型)。常量 C 字符串通过将字符串的字符放在双引号 ("") 之间来编码:

"A constant string"

当编译器在内存中创建常量字符串时,它会自动在其末尾添加零字节。但如果你声明一个将用于保存字符串的 char 数组,在确定需要多少空间时,你必须记住包含零字节。以下代码行将常量字符串 "Hello" 的五个字符及其终止零字节复制到数组 aString:

char aString[6] = "Hello";

与其他数组一样,表示字符串的数组也没有边界检查。溢出用于程序输入的字符串缓冲区是黑客最喜欢的技巧。

char* 类型的变量可以用常量字符串进行初始化。你可以将此类变量设置为指向不同的字符串,但不能用它来修改常量字符串:

char *aString = "Hello"; 
 
aString = "World"; 
 
aString[4] = 'q';  // WRONG - causes a crash

第一行将 aString 指向常量字符串 "Hello"。第二行将 aString 改为指向常量字符串 "World"。第三行会导致崩溃,因为常量字符串存储在受保护的只读内存区域。

结构体

结构体将一组相关的变量分组,以便可以将它们作为一个整体来引用。以下是一个结构声明的例子:

struct dailyTemperatures
  {
    float high; 
    float low; 
    int   year;
    int   dayOfYear;
};

结构中的单个变量称为成员变量,或简称成员。关键字 struct 后面的名称是结构标签。结构标签标识结构。它可以用来声明类型为该结构的变量:

struct dailyTemperatures today;
 
struct dailyTemperatures *todayPtr;

在前面的例子中,today 是一个 dailyTemperatures 结构,而 todayPtr 是一个指向 dailyTemperatures 结构的指针。

点运算符(.)用于从结构变量访问结构的单个成员。箭头运算符(->)用于从指向结构的变量访问结构成员:

todayPtr = &today; 
 
today.high = 68.0; 
 
todayPtr->high = 68.0;

最后两句话是相同的。

结构体可以包含其他结构体作为成员。之前的例子可以这样写:

struct hiLow
{    
    float high;
    float low;
};  
 
struct dailyTemperatures
{
    struct hiLow tempExtremes; 
    int   year;
    int   dayOfYear;
};

设置今天的最高温度将如下所示:

struct dailyTemperatures today;
today.tempExtremes.high = 68.0;

注意 - 编译器可以自由地在结构体中插入填充,以强制结构体成员对齐在内存中的特定边界。你不应该尝试通过计算结构体成员相对于结构体开头的偏移量来访问它们,或者做任何其他依赖于结构体二进制布局的事情。

typedef

typedef 声明提供了一种为变量类型创建别名的方法:

typedef float Temperature;

现在可以使用 Temperature 来声明变量,就好像它是一种内置类型一样:

Temperature high, low;

typedef 只是为类型提供备用名称。在这里,high 和 low 仍然是 float。typedef 这个词在谈论 C 代码时经常用作动词,例如“Temperature 被 typedef 为 float。”

枚举常量

enum 语句允许你定义一组整数常量:

enum woodwind { oboe, flute, clarinet, bassoon };

上一条语句的结果是 oboe、flute、clarinet 和 bassoon 的值分别为 0、1、2 和 3。

如果你不喜欢从零开始按顺序计数,你可以自己分配常量的值。任何没有赋值的常量的值都比前一个常量高一:

enum woodwind { oboe=100, flute=150, clarinet, bassoon=200 };

前面的语句使得 oboe、flute、clarinet 和 bassoon 现在分别是 100、150、151 和 200。

关键字 enum 后面的名称称为枚举标签。枚举标签是可选的。枚举标签可用于声明变量:

enum woodwind soloist;
soloist = oboe;

枚举对于定义多个常量以及帮助你的代码自我记录很有用,但它们不是独立的类型,它们不会得到编译器的太多支持。声明 enum woodwind soloist;表明你的意图是 soloist 应该限于 oboe、flute、clarinet 或 bassoon 中的一个,但不幸的是,编译器并没有强制执行这个限制。编译器将 soloist 视为 int,它允许你将任何整数值赋给 soloist 而不产生警告:

enum woodwind { oboe, flute, clarinet, bassoon };
enum woodwind soloist;
soloist  = 5280;  // No complaint from the compiler!

注意 - 枚举常量与变量名占用相同的命名空间。你不能拥有一个同名的变量和一个枚举常量。

运算符

运算符就像动词。它们会使你的变量发生一些事情。

算术运算符

C 有用于加、减、乘、除的常用二元运算符 +、-、*、/。

注意 - 如果除法运算符(/)的两个操作数都是整数类型,C 会执行整数除法。整数除法会截断除法的结果。7 / 3 的值为 2。

余数运算符

余数或运算符(%)计算整数除法的余数。以下表达式的结果是 1:

int a = 7;
int b = 3;
int c = a%b;  // c is now 1

余数运算符的两个操作数都必须是整数类型。

增量和减量运算符

C 提供了用于增量和减量变量的运算符:

a++;
 
++a;

这两行都使 a 的值加 1。但是,如果这两个表达式作为更大表达式的一部分使用,它们之间存在区别。前缀版本 ++a 在进行任何其他评估之前会增加 a 的值。是增加后的值用于表达式的其余部分。后缀版本 a++ 在进行其他评估之后会增加 a 的值。原始值用于表达式的其余部分。以下示例说明了这一点:

int a = 9;
int b;
b = a++; // postfix increment
 
int c = 9;
int d;
d = ++c; // prefix increment

后缀运算符在变量的初始值已被用于表达式的其余部分后,才增量该变量。在此示例的代码执行后,b 的值为 9,a 的值为 10。前缀运算符在变量的值被用于表达式的其余部分之前就增量它。在该示例中,c 和 d 的值均为 10。

运算符的减量版本 a-- 和 --a 以类似的方式工作。

依赖于运算符前缀和后缀版本之间差异的代码,除了其创建者之外,很可能让其他人感到困惑。

优先级

以下表达式等于 18 还是 22?

2 * 7 + 4

答案似乎是模棱两可的,因为它取决于你先做加法还是先做乘法。C 通过制定一个规则来解决这种歧义,即它先执行乘法和除法,然后再执行加法和减法;所以表达式的值是 18。技术上来说,这意味着乘法和除法的优先级高于加法和减法。

如果你需要先做加法,可以通过使用括号来指定:

2 * (7 + 4)

编译器将尊重你的请求,并安排在乘法之前执行加法。

注意 - C 为其所有运算符定义了一个复杂的优先级表(参见 http://en.wikipedia.org/wiki/Order_of_operations)。但是,通过使用括号指定你想要的求值顺序,比试图记住运算符优先级要容易得多。

取反

一元减号(-)将算术值变为其负值:

int a = 9;
int b;
b  = -a;  // b is now -9

比较

C 还提供了用于比较的运算符。比较的值是一个真值。以下表达式在为真时求值为 1,在为假时求值为 0:

a > b // true, if a is greater than b 
 
a < b // true, if a is less than b
 
a >= b // true, if a is greater than or equal to b 
 
a <= b // true, if a is less than or equal to b
 
a == b // true, if a is equal to b
 
a != b // true, if a is not equal to b

注意 - 与任何语言一样,由于舍入错误,测试浮点相等性是危险的,并且这种比较很可能产生不正确的结果。

逻辑运算符

AND 和 OR 的逻辑运算符形式如下:

expression1 && expression2  // Logical AND operator
 
expression1 || expression2  // Logical OR operator

C 使用短路求值。表达式从左到右求值,并且一旦可以推断出整个表达式的真值,求值就会停止。如果在 AND 表达式中,expression1 求值为假,则整个表达式的值为假,并且不求值 expression2。同样,如果在 OR 表达式中,expression1 求值为真,则整个表达式为真,并且不求值 expression2。短路求值对第二个表达式具有任何副作用的后果很有趣。在以下示例中,如果 b 大于或等于 a,则不调用函数 CheckSomething()(if 语句在本章后面介绍):

if (b < a && CheckSomething())
  {
    ...
  }

逻辑非

一元感叹号(!)是逻辑非运算符。执行以下代码行后,如果 expression 为真(非零),则 a 的值为 0;如果 expression 为假(零),则 a 的值为 1:

a = ! expression;

赋值运算符

C 提供了基本的赋值运算符:

a = b;

将 b 的值赋给 a。当然,a 必须是能够被赋值的东西。可以被赋值的实体称为左值(因为它们可以出现在赋值运算符的侧)。以下是一些左值的例子:

 /* set up */
float a;
float b[100]
float *c;
struct dailyTemperatures today;
struct dailyTemperatures *todayPtr;
c = &a;
todayPtr = &today;
 
/* legal lvalues */
a = 76;
b[0] = 76;
*c = 76;
today.high = 76;
todayPtr->high = 76;

有些东西不是左值。你不能给数组名、函数返回值或任何不引用内存位置的表达式赋值:

float a[100];
int x;
 
a = 76; // WRONG
x*x = 76; // WRONG 
GetTodaysHigh() = 76; // WRONG

转换和类型转换

如果赋值的左右两边变量类型不同,则右边的类型将被转换为左边的类型。从较短类型到较长类型或从整数类型到浮点类型的转换通常不会有问题。反过来,从较长类型到较短类型可能会导致有效数字丢失、截断或完全胡说八道。例如:

int a = 14;
float b;
b = a;  // OK, b is now 14.0
 
float c = 12.5;
int d;
d = c;  // Truncation, d is now 12
 
char e = 128;
int f;
f = e;  // OK, f is now 128
 
int g = 333;
char h;
h = g;  // Nonsense, h is now 77

你可以通过使用类型转换来强制编译器将变量的值转换为不同的类型。在以下示例的最后一行中,(float) 类型转换强制编译器将 a 和 b 转换为 float 并执行浮点除法:

int a = 6;
int b = 4;
float c, d;
 
c = a / b;  // c is equal to 1.0 because integer division truncates
 
d = (float)a / (float)b; // Floating-point division, d is equal to 1.5 

你可以将指针从一种类型转换为另一种类型的指针。强制转换指针可能是一项风险操作,有可能破坏你的内存,但它是解引用传递给 void* 类型指针的唯一方法。成功地强制转换指针需要你了解指针“真正”指向的实体类型。

其他赋值运算符

C 还提供了一些将算术和赋值组合在一起的简写运算符:

a += b; 
 
a -= b; 
 
a *= b; 
 
a /= b;

它们分别等同于以下内容:

a = a + b;
 
a = a - b; 
 
a = a * b;
 
a = a / b;

表达式和语句

C 中的表达式和语句相当于自然语言中的短语和句子。

表达式

最简单的表达式就是单个常量或变量:

14
bananasPerBunch

每个表达式都有一个。作为常量的表达式的值就是该常量本身:14 的值是 14。变量表达式的值是该变量当前持有的值:bananasPerBunch 的值是它上次被初始化或赋值时所获得的值。

表达式可以组合创建其他表达式。以下也都是表达式:

j + 14
a < b
distance = rate * time

算术或逻辑表达式的值就是你通过算术或逻辑运算得到的结果。赋值表达式的值是赋给被赋值变量的值。

函数调用也是表达式:

SomeFunction()

函数调用表达式的值是函数的返回值。

求值表达式

当编译器遇到一个表达式时,它会生成二进制代码来求值该表达式并找到其值。对于原始表达式,没有什么可做的:它们的值就是它们本身。对于更复杂的表达式,编译器会生成执行指定的算术、逻辑、函数调用和赋值的二进制代码。

求值表达式可能会导致副作用。最常见的副作用是由于赋值而导致变量的值发生变化,或者由于函数调用而执行函数中的代码。

表达式在其值在各种控制结构中使用,以确定程序的流程(参见程序流程)。在其他情况下,表达式可能仅为求值它们引起的副作用而求值。通常,赋值表达式的目的是进行赋值。在极少数情况下,值和副作用都很重要。

语句

当你在表达式末尾添加分号 (;) 时,它就变成了一个语句。这类似于在自然语言的短语末尾添加句号,使其成为一个句子。语句的执行完成,直到编译该语句所产生的机器语言指令全部执行完毕,并且所有对该语句影响的内存位置的更改都已完成。

复合语句

你可以在任何可以使用单个语句的地方使用一对花括号括起来的语句序列:

{
  timeDelta = time2 — time1;
  distanceDelta = distance2 — distance1;
  averageSpeed = distanceDelta / timeDelta;
}

闭合括号后面没有分号。这样的组称为复合语句。复合语句非常普遍地与本章后面部分介绍的控制语句一起使用。

注意 - (block)一词用作复合语句的同义词在 C 文献中非常普遍,并且可以追溯到 C 语言的开端。不幸的是,Apple 将这个名字用于其在 C 中添加的闭包(参见第 16 章“块”)。为了避免混淆,本书的其余部分使用稍微有些别扭的名称复合语句

程序流程

程序中的语句按顺序执行,除非被 for、while、do-while、if、switch 或 goto 语句或函数调用指示更改顺序。

  • if 语句根据表达式的真值有条件地执行代码。
  • for、while 和 do-while 语句用于形成循环。在循环中,相同的语句或一组语句会重复执行,直到满足某个条件。
  • switch 语句根据整数表达式的算术值选择要执行的一组语句。
  • goto 语句是无条件跳转到带标签的语句。
  • 函数调用是跳转到函数体中的代码。当函数返回时,程序从函数调用之后的点继续执行。

这些控制语句将在接下来的部分中更详细地介绍。

注意 - 在阅读下一部分时,请记住,每次提到语句时,你都可以使用复合语句。

if

if 语句根据表达式的真值有条件地执行代码。它具有以下形式:

if ( expression )
 
  statement 

如果 expression 求值为真(非零),则执行 statement;否则,执行将继续到 if 语句之后的下一个语句。if 语句可以通过添加 else 部分来扩展:

if ( expression )
 
  statement1 
 
else
 
  statement2

如果 expression 求值为真(非零),则执行 statement1;否则,执行 statement2

if 语句还可以通过添加 else if 部分来扩展,如下所示:

if ( expression1 )
 
  statement1
 
else if ( expression2 )
 
  statement2
 
else if ( expression3 )
 
  statement3
 
... 
 
else
 
  statementN

表达式按顺序求值。当表达式求值为非零时,将执行相应的语句,并且执行将继续到 if 语句之后的下一个语句。如果所有表达式都为假,则执行 else 子句之后的语句。(与简单 if 语句一样,else 子句是可选的,可以省略。)

条件表达式

条件表达式由三个子表达式组成,形式如下:

expression1 ? expression2 : expression3

当求值条件表达式时,会为 expression1 的真值而求值。如果为真,则求值 expression2,并且整个表达式的值就是 expression2 的值。不求值 expression3

如果 expression1 求值为假,则求值 expression3,并且条件表达式的值就是 expression3 的值。不求值 expression2

条件表达式通常用作简单 if 语句的简写。例如:

a = ( b > 0 ) ? c : d;

等同于

if ( b > 0 )
 
  a = c;
 
else
 
  a = d;

while

while 语句用于形成循环,如下所示:

while ( expression ) statement

当 while 语句执行时,会求值 expression。如果求值为真,则执行 statement,然后再次求值条件。此序列重复进行,直到 expression 求值为假,此时执行将继续到 while 之后的下一个语句。

你偶尔会看到这种结构:

while ( 1 )
  {
    ...
  }

从 while 的角度来看,前面的语句是一个无限循环。大概,循环体中的某处会检查一个条件,并在该条件满足时跳出循环。

do-while

do-while 语句与 while 类似,区别在于测试发生在语句之后而不是之前:

do statement while ( expression );

这样做的结果是,无论 expression 的值如何,statement 总是被执行一次。程序逻辑要求循环体至少执行一次,即使条件为假的情况并不常见。因此,do-while 语句在实践中很少遇到。

for

for 语句是最通用的循环结构。它具有以下形式:

for (expression1; expression2; expression3) statement

当 for 语句执行时,会发生以下顺序:

  1. expression1 在循环开始前求值一次。
  2. expression2 为其真值而求值。
  3. 如果 expression2 为真,则执行 statement;否则,循环结束,执行将继续到循环之后的下一个语句。
  4. expression3 被求值。
  5. 步骤 2、3 和 4 重复进行,直到 expression2 变为假。

expression1expression3 仅为它们的副作用而求值。它们的值被丢弃。它们通常用于初始化和递增循环计数器变量:

int j;
 
for ( j=0; j < 10; j++ )
  {
    // Something that needs doing 10 times
  }

任何表达式都可以省略(分号必须保留)。如果省略了 expression2,则该循环是无限循环,类似于 while( 1 )。

for ( i=0; ; i++ )
  {
    ...
    // Check something and exit if the condition is met
  } 

注意 - 当你使用循环迭代数组元素时,请记住数组索引从零到数组元素数量减一。

int j;
int a[25];

for (j=0; j < 25; j++ )
  {
    // Do something with a[j]
  }

将前面示例中的 for 语句写成 for (j=1; j <= 25; j++) 是一个常见的错误。

break

break 语句用于跳出循环或 switch 语句。

int j;
for (j=0;  j < 100; j++ )
 {
    ...
 
    if ( someConditionMet ) break; //Execution continues after the loop
 }

执行将继续到包含的 while、do、for 或 switch 语句之后的下一个语句。当在嵌套循环中使用时,break 只会跳出最内层的循环。编写一个未被循环或 switch 包围的 break 语句会导致编译器错误:

error: break statement not within loop or switch

continue

continue 用于 while、do 或 for 循环内部,以放弃当前循环迭代的执行。例如:

int j;
for (j=0;  j < 100; j++ )
 {
    ...
 
    if ( doneWithIteration ) continue; // Skip to the next iteration
    ...
 }

当 continue 语句执行时,控制权将传递到循环的下一次迭代。在 while 或 do 循环中,将为下一次迭代求值控制表达式。在 for 循环中,先求值迭代(第三个)表达式,然后求值控制(第二个)表达式。编写一个未被循环包围的 continue 语句会导致编译器错误。

逗号表达式

逗号表达式由两个或多个由逗号分隔的表达式组成:

expression1, expression2, ..., expressionN

表达式按从左到右的顺序求值,整个表达式的值是最右侧子表达式的值。

逗号运算符的主要用途是在 for 语句中初始化和更新多个循环变量。随着以下示例中的循环迭代,j 从 0 变为 MAX-1,k 从 MAX-1 变为 0:

for ( j=0, k=MAX-1; j < MAX; j++, k--)
  {
    // Do something
  }

当逗号表达式用于 for 循环时,只有子表达式求值的副作用(在前面的示例中初始化和递增或递减 j 和 k)是重要的。逗号表达式的值将被丢弃。

switch

switch 根据整数表达式的值分支到不同的语句。switch 语句的形式如下:

switch ( integer_expression )
  {
     case value1: 
       statement 
       break;
 
     case value2: 
       statement 
       break;
 
     ... 
 
     default:
       statement 
       break;
}

与 C 的其余部分略有不同,每个 case 可以有多个语句,而无需复合语句。

value1, value2, ... 必须是整数、字符常量或求值为整数的常量表达式。(换句话说,它们必须在编译时可归结为整数。)不允许重复的具有相同整数的 case。

当 switch 语句执行时,会求值 integer_expression,然后 switch 将结果与整数 case 标签进行比较。如果找到匹配项,执行将跳转到匹配 case 标签后的语句。执行将按顺序继续,直到遇到 break 语句或 switch 的结尾。break 会导致执行跳转到 switch 后面的第一个语句。

break 语句不是每个 case 都必需的。如果省略 break,执行将“掉落”到下一个 case。如果你在现有代码中看到 break 被省略,可能是错误(很容易犯)或有意为之(如果编码者想要一个 case 和后面的 case 执行相同的代码)。

如果 integer_expression 与任何 case 标签都不匹配,执行将跳转到可选的 default: 标签之后的语句(如果存在)。如果没有匹配项也没有 default:,则 switch 不执行任何操作,执行将继续到 switch 之后的第一个语句。

goto

C 提供了 goto 语句:

goto label;

当执行 goto 时,控制权将无条件转移到标记为 label 的语句:

label: statement
  • 标签不是可执行语句;它们只是标记代码中的一个点。
  • 命名标签的规则与命名变量和函数的规则相同。
  • 标签始终以冒号结尾。

滥用 goto 语句可能导致混乱、难以理解的代码(通常称为意大利面条式代码)。通常的样板建议是“不要使用 goto 语句。”尽管如此,goto 语句在某些情况下仍然有用,例如跳出嵌套循环(break 语句只跳出最内层的循环):

for ( i=0; i < MAX_I; i++ )
  for ( j=0; j < MAX_J; j++ )
     {
        ...
       if ( finished ) goto moreStuff;
    
     }
 
moreStuff:  statement     // more statements

注意 - 是否使用 goto 语句是计算机科学中持续时间最长的争论之一。有关争论的摘要,请参阅 http://david.tribble.com/text/goto.html

函数

函数通常具有以下形式:

returnType functionName( arg1Type arg1, ..., argNType argN )
{
 statements
}

一个简单函数的例子如下:

float salesTax( float purchasePrice, float taxRate )
{
  float tax = purchasePrice * taxRate;
  return tax;
}

通过在函数名称后跟一个用括号括起来的表达式列表(对应于函数的每个参数)来调用函数。每个表达式的类型必须与相应函数参数的声明类型匹配。以下示例显示了一个简单的函数调用:

float carPrice = 20000.00;
float stateTaxRate = 0.05;
 
float carSalesTax = salesTax( carPrice, stateTaxRate );

当执行函数调用行时,控制权将跳转到函数体中的第一条语句。执行将继续,直到遇到 return 语句或到达函数末尾。然后执行返回到调用上下文。调用上下文中的函数表达式的值是 return 语句设置的值。

注意 - 函数不要求有任何参数或返回值。不返回值的函数类型为 void:

void FunctionThatReturnsNothing( int arg1 )

你可以从不返回值的函数中省略 return 语句。

不接受任何参数的函数通过使用空的括号作为参数列表来表示:

int FunctionWithNoArguments()

函数有时仅为它们的副作用而执行。此函数打印销售税,但不改变程序的任何状态:

void printSalesTax ( float purchasePrice, float taxRate )
{
  float tax = purchasePrice * taxRate; 
 
  printf( "The sales tax is: %f.2\n", tax ); 
 
}

C 函数是传值调用。当调用函数时,调用语句的参数列表中的表达式将被求值,并且它们的将被传递给函数。函数不能直接更改调用上下文中任何变量的值。此函数对调用上下文中的任何内容都没有影响:

void salesTax( float purchasePrice, float taxRate, float carSalesTax)
{
  // Changes the local variable calculateTax but not the value of
  // the variable in the calling context 
 
     carSalesTax = purchasePrice * taxRate; 
     return;
}

要更改调用上下文中变量的值,必须传递指向该变量的指针,并使用该指针来操作变量的值:

void salesTax( float purchasePrice, float taxRate, float *carSalesTax)
{
  *carSalesTax = purchasePrice * taxRate; // this will work 
 
  return;
}

注意 - 上面的例子仍然是传值调用。指向调用上下文中变量的指针的被传递给函数。然后函数使用该指针(它不更改该指针)来设置它所指向的变量的值。

声明函数

当你调用一个函数时,编译器需要知道函数的参数类型和返回值。它使用这些信息来设置函数与其调用者之间的通信。如果函数代码出现在函数调用之前(在源文件代码中),则无需做任何其他事情。如果函数在函数调用之后或在另一个文件中编码,则必须在使用函数之前声明它。

函数声明会重复函数的第一行,并在末尾添加一个分号:

void printSalesTax ( float purchasePrice, float taxRate );

将函数声明放在头文件中是一种常见做法。然后,包含(参见下一节)该头文件的任何使用该函数的源文件都会包含它。

注意 - 忘记声明函数可能导致难以察觉的错误。如果你调用一个在另一个文件中(或在同一个文件中函数调用之后)编码的函数,并且你没有声明该函数,那么编译器和链接器都不会抱怨。但是,函数会接收浮点参数的垃圾,并且如果函数的返回类型是浮点类型,则返回垃圾。

预处理器

当 C(和 Objective-C)代码文件被编译时,它们在被发送给编译器本身之前,首先会经过一个名为预处理器的初始程序。以 # 字符开头的行是预处理器的指令。使用预处理器指令,你可以:

  • 在指定位置将一个文件的文本导入到一个或多个其他文件中。
  • 创建定义的常量。
  • 有条件地编译代码(根据条件编译或省略语句块)。

包含文件

以下行:

#include "HeaderFile.h"

导致预处理器在 #include 行的位置将 HeaderFile.h 文件的文本插入正在编译的文件中。效果与使用文本编辑器将 HeaderFile.h 中的文本复制并粘贴到正在编译的文件中一样。

如果包含的文件名用引号 ("") 括起来:

#include "HeaderFile.h"

预处理器将在与正在编译的文件相同的目录中查找 HeaderFile.h,然后在你可以作为编译器参数提供的位置列表中查找,最后在一系列系统位置中查找。

如果包含的文件用尖括号 (<>) 括起来:

#include <HeaderFile.h>

预处理器将仅在标准系统位置查找包含的文件。

注意 - 在 Objective-C 中,#include 被 #import 超越,后者产生相同的结果,但它阻止了命名文件被导入多次。如果预处理器遇到同一头文件的后续 #import 指令,它们将被忽略。

#define

#define 用于文本替换。#define 最常见的用途是定义常量,例如:

#define MAX_VOLUME 11

预处理器将在正在编译的文件中将 MAX_VOLUME 的每个实例替换为 11。可以通过在定义中除了最后一行之外的所有行末尾放置反斜杠 (\) 来将 #define 继续到多行。

注意 - 如果这样做,\ 必须是行上的最后一个字符。在 \ 后面加上其他内容(例如以 "//" 开头的注释)会导致错误。

一个常用的模式是将 #define 放在一个头文件中,然后该头文件被各种源文件包含。然后,你可以通过更改头文件中单个定义的值来更改所有源文件中常量的值。定义的常量传统的 C 命名约定是使用全大写字母。传统的 Apple 命名约定是以 k 开头,然后其余部分使用驼峰式命名:

#define kMaximumVolume 11

你会在同一代码中遇到这两种风格。

条件编译

预处理器允许进行条件编译:

#if condition 
 
  statements
 
#else 
 
  otherStatements
 
#endif

在这里,condition 必须是一个在编译时可以为其求值真值的常量表达式。如果 condition 求值为真(非零),则编译 statements,但不编译 otherStatements。如果 condition 为假,则跳过 statements 并编译 otherStatements

需要 #endif,但 #else 和替代代码是可选的。条件编译块也可以以 #ifdef 指令开始:

#ifdef name
 
  statements
 
#endif

行为与上一个示例相同,只是 #ifdef 的真值取决于 name 是否已被 #define 定义。

#if 的一个用途是在调试期间轻松删除和替换代码块:

#if 1 
  statements
#endif

通过将 1 改为 0,可以为测试暂时省略 statements。然后可以通过将 0 改回 1 来恢复它们。

#if 和 #ifdef 指令可以嵌套,如下所示:

#if 0 
#if 1
statements
#endif
#endif

在前面的示例中,编译器会忽略 #if 0 和匹配的 #endif 之间的所有代码,包括其他编译器指令。statements 不会被编译。

如果你需要禁用和重新启用多个语句块,你可以像这样编码每个块:

#if _DEBUG
 
  statements
 
#endif

定义常量 _DEBUG 可以在头文件中添加或删除,或者通过使用编译器的一个 —D 标志来添加或删除。

printf

输入和输出(I/O)不是 C 语言的一部分。字符和二进制 I/O 由 C 标准 I/O 库中的函数处理。

注意 - 标准 I/O 库是 C 环境提供的函数库集之一。

要使用标准 I/O 库中的函数,你必须在程序中包含该库的头文件:

#include <stdio.h>

这里介绍的唯一函数是 printf,它将格式化字符串打印到你的终端窗口(或如果你正在使用 Xcode,则打印到 Xcode 控制台窗口)。printf 函数接受可变数量的参数。printf 的第一个参数是格式字符串。任何剩余的参数是数量,它们根据格式字符串的指定方式被打印出来:

printf( formatString, argument1, argument2, ... argumentN );

格式字符串由普通字符和转换说明符组成:

  • 格式字符串中非 % 的普通字符会原样发送到输出。
  • 转换说明符以百分号 (%) 开头。% 后面的字母指示说明符预期的参数类型。
  • 每个转换说明符按顺序消耗格式字符串后面的一个参数。参数被转换为表示参数值的字符,并将这些字符发送到输出。

本书中使用的唯一转换说明符是 %d(用于 char 和 int)、%f(用于 float 和 double)以及 %s(用于 C 字符串)。C 字符串的类型是 char*。

这是一个简单的例子:

int myInt = 9;
float myFloat = 3.145926;
char* myString = "a C string";
 
printf( "This is an Integer: %d, a float: %f, and a string: %s.\n",
    myInt, myFloat, myString );

注意 - \n 是换行符。它会推进输出,以便任何后续输出都显示在新的一行上。

前面示例的结果是:

This is an Integer: 9, a float: 3.145926, and a string: a C string.

如果格式字符串后面的参数数量与转换说明符的数量不匹配,printf 将会忽略多余的参数,或者为多余的说明符打印垃圾。

注意 - 本书仅将 printf 用于记录和调试非对象变量,而不是用于显示经过精心设计的程序的输出,因此本节仅对格式字符串和转换说明符进行了粗略介绍。

printf 处理大量类型,并且可以非常精细地控制输出的外观。可以通过 Unix man 命令获取关于可用转换说明符类型以及如何控制格式细节的完整讨论。要查看它们,请在终端窗口中键入以下内容:

man 3 printf 

注意 - Foundation 框架提供了 NSLog,这是另一个日志记录函数。它类似于 printf,但增加了打印对象变量的功能。它还会将程序名称、日期以及小时、分钟、秒和毫秒的时间添加到输出中。如果你只想知道一个或两个变量的值,这些额外的信息可能会在视觉上分散注意力,所以本书在某些情况下使用 printf,而在不需要 NSLog 的额外功能时使用。NSLog 在第 3 章中介绍。

使用 gcc 和 gdb

当你为 Mac OS X 或 iOS 编写程序时,你应该使用 Apple 的集成开发环境(IDE)Xcode 来编写和编译你的程序。你将在第 4 章“你的第一个 Objective-C 程序”中学习如何设置一个简单的 Xcode 项目。但是,对于本章和下一章练习所需的简单 C 程序,你可能会发现更容易在自己喜欢的文本编辑器中编写程序,然后使用 GNU 编译器 gcc 从命令行进行编译和运行。为此,你需要:

  1. 一个终端窗口。你可以使用 Mac OS X 自带的 Terminal 应用程序(/Applications/Terminal)。如果你来自其他 Unix 环境,并且习惯使用 xterms,你可能更喜欢下载并使用 iTerm,一个类似 xterm 的 Mac OS X 原生终端应用程序。(http://iterm.sourceforge.net/)。
  2. 一个文本编辑器。Mac OS X 自带 viemacs,或者你可以使用其他编辑器(如果你有的话)。
  3. 命令行工具。这些可能未安装在你的系统上。要检查,请在命令提示符下键入 which gcc。如果响应是 /usr/bin/gcc,则一切正常。但是,如果没有响应,或者响应是 gcc: Command not found.,你将不得不从安装磁盘或从下载的 Xcode 磁盘映像安装命令行工具。(你可以在 Mac Dev Center 网页 http://developer.apple.com/mac/ 上找到当前版本开发工具的链接。)启动安装过程,当到达 Custom Install 阶段时,确保选中UNIX Dev Support框,如图 1.2 所示。继续安装。

01LOC02.jpg

图 1.2 安装命令行工具

现在你就可以编译了。如果你的源文件名为 MyCProgram.c,你可以通过在命令提示符下键入以下内容来编译它:

gcc -o MyCProgram MyCProgram.c

-o 标志允许你为最终可执行文件命名。如果编译器抱怨你犯了一个或两个错误,请回去修复它们,然后重试。当你的程序成功编译后,你可以通过在命令提示符下键入可执行文件名来运行它:

MyCProgram

如果你想使用 gdb(GNU 调试器)来调试你的程序,你必须在编译时使用 -g 标志:

gcc -g -o MyCProgram MyCProgram.c

-g 标志使 gcc 将 gdb 的调试信息附加到最终的可执行文件中。要使用 gdb 调试程序,请键入 gdb,后跟可执行文件名:

gdb MyCProgram

gdb 的文档可在 GNU 网站 https://gnu.ac.cn/software/gdb/ 或 Apple 网站 http://developer.apple.com/mac/library/documentation/DeveloperTools/gdb/gdb/gdb_toc.html 上找到。此外,还有许多网站提供 gdb 的使用说明。搜索“gdb tutorial”。

摘要

本章回顾了 C 语言的基本部分。回顾将在第 2 章继续,该章将介绍 C 程序的内存布局、变量声明、变量作用域和生命周期以及内存的动态分配。第 3 章将开始本书的真正内容:面向对象编程和 Objective-C 的面向对象部分。

练习

  1. 编写一个函数,该函数返回两个浮点数的平均值。编写一个小程序来测试你的函数并记录输出。接下来,将函数放在一个单独的源文件中,但在包含 main 例程的文件中“忘记”声明该函数。会发生什么?现在将函数声明添加到包含 main 程序的那个文件中,并验证声明是否解决了问题。
  2. 编写另一个平均函数,但这次尝试将结果通过函数参数之一返回。你的函数应该声明为:
  3. void average( float a, float b, float average )

    编写一个小型测试程序,并验证你的函数不起作用。你无法通过设置函数参数的值来影响调用上下文中的变量。

    现在更改函数及其调用,以传递指向调用上下文中变量的指针。验证函数是否可以使用该指针来修改调用上下文中的变量。

  4. 假设你有一个函数 int FlipCoin(),它随机返回 1 表示正面或 0 表示反面。解释以下代码片段如何工作:
  5. int flipResult;
    if ( flipResult = FlipCoin() )
      printf("Heads is represented by %d\n", flipResult );
    else
      printf("Tails is represented by %d\n", flipResult );

    正如你将在第 6 章“类和对象”中看到的,与前面示例中的 if 条件类似,它用于初始化 Objective-C 对象。

  6. 单位矩阵是一个方形数字数组,对角线上为 1(行号等于列号的元素),其他位置为零。2x2 单位矩阵看起来像这样:
  7. Matrix.gif

    编写一个计算并存储 4x4 单位矩阵的程序。当你的程序完成矩阵计算后,它应该将结果输出为一个格式精美的方形数组。

  8. 斐波那契数(http://en.wikipedia.org/wiki/Fibonacci_number)是一个出现在自然界和数学中许多地方的数字序列。前两个斐波那契数定义为 0 和 1。第 n 个斐波那契数是前两个斐波那契数的和:
  9. Fn = Fn-1 + Fn-2

    编写一个计算并存储前 20 个斐波那契数的程序。计算完数字后,你的程序应该逐行输出它们以及它们的索引。输出行应类似于:

    Fibonacci Number 2 is: 1

    使用 #define 来控制你的程序产生的斐波那契数的数量,以便可以轻松更改。

  10. 重写你上一个练习中的程序,使用 while 循环而不是 for 循环。
  11. 如果要求你计算前 75 个斐波那契数怎么办?如果你使用 int 来存储数字,会有一个问题。你会发现第 47 个斐波那契数太大了,无法装入 int。你该如何解决这个问题?
  12. 根据 iPhone App Store 中可用的账单计算器数量判断,相当一部分人已经忘记了如何进行乘法。帮助那些不会乘法但又买不起 iPhone 的人。编写一个程序,计算 10 美元到 50 美元之间所有账单的 15% 小费。(为简洁起见,按 0.50 美元的增量计算。)同时显示账单和小费。
  13. 现在让小费计算器看起来更专业。添加一列用于 20% 的小费(Objective-C 程序员在优雅的场所用餐)。在每列上方放置正确的标题,并使用一对嵌套循环,以便在每 10 美元增量后输出一个空行。
  14. 使用转换说明符 %.2f 而不是 %f 将检查和小费输出限制为两位小数。在格式字符串中使用 %% 将导致 printf 输出一个单独的 % 字符。

  15. 定义一个结构体来保存矩形。通过定义一个保存点坐标的结构体,以及另一个保存宽度和高度的表示大小的结构体来做到这一点。你的矩形结构体应包含一个表示矩形左下角的点和一个大小。 (Cocoa 框架定义了类似这样的结构体,但现在请自己创建一个。)
  16. 高效计算机图形学的基本原则之一是“如果不需要,就不要绘制。”图形程序通常会为每个图形对象保留一个边界矩形。当需要将图形绘制到屏幕上时,程序会将图形的边界矩形与表示窗口的矩形进行比较。如果矩形之间没有重叠,程序就可以跳过绘制图形的尝试。总的来说,这通常是划算的;比较矩形比绘制图形要便宜得多。
  17. 编写一个函数,它接受两个矩形结构参数。(使用你在上一个练习中定义的结构。)如果两个矩形之间存在非零重叠,则你的函数应返回 1,否则返回 0。编写一个测试程序,创建一些矩形并验证你的函数是否正常工作。

  1. Brian W. Kernighan 和 Dennis M. Ritchie,《C Programming Language,第二版》。(Englewood Cliffs: Prentice Hall, 1988)。

  2. Samuel P. Harbison 和 Guy L. Steele,《C: A Reference Manual, Fifth Edition.》(Upper Saddle River: Prentice Hall, 2002)。

© . All rights reserved.