C 编程语言是一种流行且广泛使用的编程语言,用于创建计算机程序。世界各地的程序员都青睐 C 语言,因为它为程序员提供了最大的控制权和效率。
如果您是程序员,或者您对成为程序员感兴趣,学习 C 语言有以下几个好处:
广告
在本文中,我们将从头开始,带您了解整个语言,并向您展示如何成为一名 C 程序员。一旦掌握了 C 语言,您将惊叹于自己可以创造出各种不同的东西!
C 编程语言是一种流行且广泛使用的编程语言,用于创建计算机程序。世界各地的程序员都青睐 C 语言,因为它为程序员提供了最大的控制权和效率。
如果您是程序员,或者您对成为程序员感兴趣,学习 C 语言有以下几个好处:
广告
在本文中,我们将从头开始,带您了解整个语言,并向您展示如何成为一名 C 程序员。一旦掌握了 C 语言,您将惊叹于自己可以创造出各种不同的东西!
C 是一种计算机编程语言。这意味着您可以使用 C 来创建计算机要遵循的指令列表。C 是目前使用的数千种编程语言之一。C 语言已经存在了几十年,并因其为程序员提供了最大的控制权和效率而获得了广泛的认可。C 是一种易于学习的语言。它的风格比其他一些语言更晦涩一些,但您很快就能掌握它。
C 是一种所谓的编译语言。这意味着一旦您编写了 C 程序,就必须通过 C 编译器将其转换为计算机可以运行(执行)的可执行文件。C 程序是人类可读的形式,而编译器生成的可执行文件是机器可读和可执行的形式。这意味着要编写和运行 C 程序,您必须能够访问 C 编译器。如果您正在使用 UNIX 机器(例如,如果您正在主机的 UNIX 计算机上用 C 编写 CGI 脚本,或者您是一名学生在实验室的 UNIX 机器上工作),C 编译器是免费提供的。它被称为“cc”或“gcc”,并且可以在命令行上使用。如果您是学生,那么学校很可能会为您提供一个编译器——查清楚学校正在使用什么并学习它。如果您在家里的 Windows 机器上工作,您将需要下载一个免费的 C 编译器或购买一个商业编译器。一个广泛使用的商业编译器是微软的 Visual C++ 环境(它编译 C 和 C++ 程序)。不幸的是,这个程序要花费几百美元。如果您没有几百美元来购买商业编译器,那么您可以使用网上免费提供的编译器之一。请访问 http://delorie.com/djgpp/ 作为您的搜索起点。
广告
我们将从一个极其简单的 C 程序开始,然后逐步深入。我将假定您在这些示例中使用 UNIX 命令行和 gcc 作为您的环境;如果不是,所有代码仍然可以正常工作——您只需理解并使用您可用的任何编译器即可。
我们开始吧!
让我们从最简单的 C 程序开始,用它来理解 C 的基础知识和 C 编译过程。将以下程序输入到标准文本编辑器中(UNIX 上的 vi 或 emacs,Windows 上的记事本或 Macintosh 上的 TeachText)。然后将程序保存到一个名为 samp.c 的文件中。如果您省略 .c,编译时可能会出现某种错误,因此请务必记住 .c。另外,请确保您的编辑器不会自动在文件名后附加一些额外的字符(例如 .txt)。这是第一个程序
#include <stdio.h> int main() { printf("This is output from my first program!\n"); return 0; }
执行时,该程序会指示计算机打印出“这是我的第一个程序的输出!”这一行——然后程序退出。没有比这更简单的了!
广告
要编译此代码,请执行以下步骤:
运行程序时,您应该会看到输出“这是我的第一个程序的输出!”。以下是您编译程序时发生的情况:
如果您程序输入错误,它要么无法编译,要么无法运行。如果程序无法编译或运行不正确,请再次编辑并检查您在哪里打错了字。修正错误,然后重试。
让我们逐步分析这个程序,看看不同行的作用(点击这里在新窗口中打开程序)
广告
作为一名程序员,您会经常希望您的程序“记住”某个值。例如,如果您的程序向用户请求一个值,或者它计算出一个值,您会希望将其存储在某个地方,以便以后使用。您的程序记住事物的方式是使用变量。例如
int b;
这一行表示:“我想创建一个名为 b 的空间,它能够容纳一个整数值。”一个变量有一个名称(在这个例子中是 b)和一个类型(在这个例子中是 int,一个整数)。您可以通过以下方式将值存储在 b 中:
广告
b = 5;
您可以通过以下方式使用 b 中的值:
printf("%d", b);
在 C 语言中,变量有几种标准类型:
我们将在后续内容中看到这些其他类型的示例。
printf 语句允许您将输出发送到标准输出。 对我们而言,标准输出通常是屏幕(尽管您可以将标准输出重定向到文本文件或另一个命令)。
这是另一个程序,将帮助您了解更多关于 printf 的信息
广告
#include <stdio.h> int main() { int a, b, c; a = 5; b = 7; c = a + b; printf("%d + %d = %d\n", a, b, c); return 0; }
将此程序输入到一个文件中并保存为 add.c。使用命令 gcc add.c -o add 编译它,然后通过输入 add(或 ./add)来运行它。您将看到输出行“5 + 7 = 12”。
以下是此程序中不同行的解释:
前面的程序很好,但如果它从用户那里读取 5 和 7 的值而不是使用常量,会更好。请尝试这个程序:
#include <stdio.h> int main() { int a, b, c; printf("Enter the first value:"); scanf("%d", &a); printf("Enter the second value:"); scanf("%d", &b); c = a + b; printf("%d + %d = %d\n", a, b, c); return 0; }
以下是您执行此程序时它的工作方式:
广告
进行更改,然后编译并运行程序以确保其正常工作。请注意,scanf 使用与 printf 相同的格式字符串(输入 man scanf 获取更多信息)。另请注意 a 和 b 前面的 &。这是 C 语言中的地址运算符:它返回变量的地址(这在我们讨论指针之前可能无法理解)。您必须在 scanf 中对任何 char、int 或 float 类型的变量以及结构类型(我们很快会讲到)使用 & 运算符。如果您省略 & 运算符,运行程序时将会出错。尝试一下,以便您可以看到那种运行时错误是什么样子。
让我们看一些变体来完全理解 printf。这是最简单的 printf 语句
printf("Hello");
这个 printf 调用有一个格式字符串,告诉 printf 将单词“Hello”发送到标准输出。与此对比:
printf("Hello\n");
两者之间的区别在于,第二个版本将单词“Hello”后面跟着一个回车符发送到标准输出。
以下一行显示了如何使用 printf 输出变量的值。
printf("%d", b);
%d 是一个占位符,当 printf 语句执行时,它将被变量 b 的值替换。通常,您会希望将值嵌入到其他文字中。一种实现方式如下:
printf("The temperature is "); printf("%d", b); printf(" degrees\n");
一个更简单的方法是这样说:
printf("The temperature is %d degrees\n", b);
您还可以在一个 printf 语句中使用多个 %d 占位符
printf("%d + %d = %d\n", a, b, c);
在 printf 语句中,格式字符串中运算符的数量必须与后面变量的数量和类型完全对应,这一点极其重要。例如,如果格式字符串包含三个 %d 运算符,那么它后面必须紧跟三个参数,并且它们的类型和顺序必须与运算符指定的完全相同。
您可以通过使用不同的占位符来用 printf 打印所有常见的 C 类型
您可以在 UNIX 机器上通过输入 man 3 printf 来了解更多关于 printf 的细微差别。您使用的任何其他 C 编译器可能都会附带包含 printf 描述的手册或帮助文件。
scanf 函数允许您从标准输入接收输入,对我们而言,标准输入通常是键盘。scanf 函数可以做很多不同的事情,但由于它不能很好地处理人为错误,因此可能不可靠。但对于简单的程序来说,它足够好用且易于使用。
scanf 最简单的应用看起来像这样
广告
scanf("%d", &b);
程序将读取用户在键盘上输入的整数值(%d 用于整数,printf 也是如此,因此 b 必须声明为 int 类型)并将其值放入 b 中。
scanf 函数使用与 printf 相同的占位符
您必须在 scanf 中使用的变量前面加上 &。原因在您了解指针后就会清楚。很容易忘记 & 符号,而当您忘记它时,您的程序在运行时几乎总是会崩溃。
一般来说,最好按此处所示使用 scanf——从键盘读取单个值。使用多个 scanf 调用来读取多个值。在任何实际程序中,您将改用 gets 或 fgets 函数来一次读取一行文本。然后您将“解析”该行以读取其值。您这样做的原因是,您可以检测输入中的错误并根据需要处理它们。
printf 和 scanf 函数需要一些练习才能完全理解,但一旦掌握,它们就极其有用。
修改此程序,使其接受三个值而不是两个,并将这三个值相加
#include <stdio.h> int main() { int a, b, c; printf("Enter the first value:"); scanf("%d", &a); printf("Enter the second value:"); scanf("%d", &b); c = a + b; printf("%d + %d = %d\n", a, b, c); return 0; }
您也可以删除上述程序第一行中的变量 b,看看当您忘记声明变量时编译器会做什么。删除一个分号,看看会发生什么。省略一个大括号。删除 main 函数旁边的一个括号。单独制造每个错误,然后通过编译器运行程序,看看会发生什么。通过模拟这些错误,您可以了解不同的编译器错误,这将在您真正犯错时更容易找到您的输入错误。
在 C 语言中,if 语句和 while 循环都依赖于布尔表达式的概念。这是一个演示 if 语句的简单 C 程序
#include int main() { int b; printf("请输入一个值:"); scanf("%d", &b); if (b < 0) printf("该值为负数\n"); return 0; }
广告
该程序接受用户输入的一个数字。然后它使用 if 语句测试该数字是否小于 0。如果是,程序会打印一条消息。否则,程序不输出。程序中的 (b < 0) 部分是布尔表达式。C 语言会评估此表达式,以决定是否打印消息。如果布尔表达式评估为真,则 C 语言会执行紧跟在 if 语句后的单行(或紧跟在 if 语句后大括号内的代码块)。如果布尔表达式为假,则 C 语言会跳过紧跟在 if 语句后的行或代码块。
这是一个稍微复杂一些的例子
#include <stdio.h> int main() { int b; printf("Enter a value:"); scanf("%d", &b); if (b < 0) printf("The value is negative\n"); return 0; }
在这个例子中,else if 和 else 部分也对零值和正值进行了评估。
这是一个更复杂的布尔表达式
if ((x==y) && (j>k)) z=1; else q=10;
这个语句表示:“如果变量 x 中的值等于变量 y 中的值,并且如果变量 j 中的值大于变量 k 中的值,那么将变量 z 设置为 1,否则将变量 q 设置为 10。”您将在 C 程序中始终使用这样的 if 语句来做出决策。通常,您做出的大多数决策都将像第一个示例一样简单;但偶尔,事情会变得更复杂。
请注意,C 语言使用 == 来测试相等性,而使用 = 来赋值给变量。C 语言中的 && 表示布尔与运算。
以下是 C 语言中所有的布尔运算符
equality == less than < Greater than > <= <= >= >= inequality != and && or || not !
您会发现 while 语句和 if 语句一样易于使用。例如
while (a < b) { printf("%d\n", a); a = a + 1; }
这会导致大括号内的两行代码重复执行,直到 a 大于或等于 b。while 语句的通常工作方式如右图所示。
C 也提供 do-while 结构
#include <stdio.h> int main() { int a; printf("Enter a number:"); scanf("%d", &a); if (a) { printf("The value is True\n"); } return 0; }
C 语言中的 for 循环只是表达 while 语句的一种简写方式。例如,假设您在 C 语言中有以下代码
x=1; while (x<10) { blah blah blah x++; /* x++ is the same as saying x=x+1 */ }
您可以将其转换为 for 循环,如下所示
for(x=1; x<10; x++) { blah blah blah }
请注意,while 循环包含一个初始化步骤 (x=1)、一个测试步骤 (x<10) 和一个递增步骤 (x++)。for 循环允许您将所有这三个部分放在一行上,但您可以在这三个部分中放置任何内容。例如,假设您有以下循环
a=1; b=6; while (a < b) { a++; printf("%d\n",a); }
您也可以将其放入 for 语句中
for (a=1,b=6; a < b; a++,printf("%d\n",a));
这有点令人困惑,但这是可能的。逗号运算符允许您在 for 循环的初始化和递增部分(但不能在测试部分)中分隔几个不同的语句。许多 C 程序员喜欢将大量信息打包到一行 C 代码中;但很多人认为这使得代码更难理解,所以他们会将其拆分。
假设您想创建一个程序,打印一个华氏温度到摄氏温度转换表。这很容易通过 for 循环或 while 循环实现
#include <stdio.h> int main() { int a; a = 0; while (a <= 100) { printf("%4d degrees F = %4d degrees C\n", a, (a - 32) * 5 / 9); a = a + 10; } return 0; }
如果您运行此程序,它将生成一个从 0 华氏度开始,到 100 华氏度结束的数值表。输出将如下所示
广告
0 degrees F = -17 degrees C 10 degrees F = -12 degrees C 20 degrees F = -6 degrees C 30 degrees F = -1 degrees C 40 degrees F = 4 degrees C 50 degrees F = 10 degrees C 60 degrees F = 15 degrees C 70 degrees F = 21 degrees C 80 degrees F = 26 degrees C 90 degrees F = 32 degrees C 100 degrees F = 37 degrees C
表中的值以 10 度为增量。您可以看到,您可以轻松更改程序生成的表的起始值、结束值或增量值。
如果您希望值更精确,可以改用浮点值
#include <stdio.h> int main() { float a; a = 0; while (a <= 100) { printf("%6.2f degrees F = %6.2f degrees C\n", a, (a - 32.0) * 5.0 / 9.0); a = a + 10; } return 0; }
您可以看到 a 的声明已更改为 float 类型,并且 printf 语句中的 %f 符号取代了 %d 符号。此外,%f 符号还应用了一些格式化:该值将以小数点前六位、小数点后两位的格式打印。
现在假设我们想修改程序,使温度 98.6 插入到表格中的正确位置。也就是说,我们希望表格每隔 10 度递增,但我们也希望表格包含一个额外的 98.6 华氏度行,因为那是人类的正常体温。以下程序实现了这个目标
#include <stdio.h> int main() { float a; a = 0; while (a <= 100) { if (a > 98.6) { printf("%6.2f degrees F = %6.2f degrees C\n", 98.6, (98.6 - 32.0) * 5.0 / 9.0); } printf("%6.2f degrees F = %6.2f degrees C\n", a, (a - 32.0) * 5.0 / 9.0); a = a + 10; } return 0; }
如果结束值是 100,这个程序就能正常工作,但如果您把结束值改为 200,您会发现程序存在一个错误。它打印 98.6 度的行太多次了。我们可以用几种不同的方法来解决这个问题。这是一种方法
#include <stdio.h> int main() { float a, b; a = 0; b = -1; while (a <= 100) { if ((a > 98.6) && (b < 98.6)) { printf("%6.2f degrees F = %6.2f degrees C\n", 98.6, (98.6 - 32.0) * 5.0 / 9.0); } printf("%6.2f degrees F = %6.2f degrees C\n", a, (a - 32.0) * 5.0 / 9.0); b = a; a = a + 10; } return 0; }
在本节中,我们将创建一个小型 C 程序,它能生成 10 个随机数并对其进行排序。为此,我们将使用一种称为数组的新变量排列方式。
数组允许您声明和使用相同类型的值的集合。例如,您可能希望创建一个包含五个整数的集合。一种方法是直接声明五个整数
广告
int a, b, c, d, e;
这可以,但如果您需要一千个整数怎么办?一个更简单的方法是声明一个包含五个整数的数组
int a[5];
这个数组中的五个独立整数通过索引访问。在 C 语言中,所有数组都从索引零开始,到 n-1 结束。因此,int a[5]; 包含五个元素。例如
int a[5]; a[0] = 12; a[1] = 9; a[2] = 14; a[3] = 5; a[4] = 1;
数组索引的一个优点是您可以使用循环来操作索引。例如,以下代码将数组中的所有值初始化为 0
int a[5]; int i; for (i=0; i<5; i++) a[i] = 0;
以下代码按顺序初始化数组中的值,然后将其打印出来
#include <stdio.h> int main() { int a[5]; int i; for (i=0; i<5; i++) a[i] = i; for (i=0; i<5; i++) printf("a[%d] = %d\n", i, a[i]); }
数组在 C 语言中一直被使用。要了解其常见用法,请启动编辑器并输入以下代码
#include <stdio.h> #define MAX 10 int a[MAX]; int rand_seed=10; /* from K&R - returns random number between 0 and 32767.*/ int rand() { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } int main() { int i,t,x,y; /* fill array */ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } /* more stuff will go here in a minute */ return 0; }
此代码包含几个新概念。#define 行声明了一个名为 MAX 的常量并将其设置为 10。常量名通常以全大写形式书写,以便在代码中显而易见。行 int a[MAX]; 向您展示了如何在 C 语言中声明一个整数数组。请注意,由于数组声明的位置,它对整个程序是全局的。
行 int rand_seed=10 也声明了一个全局变量,这次名为 rand_seed,每次程序开始时都会将其初始化为 10。这个值是后面随机数代码的起始种子。在真正的随机数生成器中,种子应该初始化为一个随机值,例如系统时间。在这里,每次运行程序时,rand 函数都会产生相同的值。
行 int rand() 是一个函数声明。rand 函数不接受任何参数,并返回一个整数值。我们稍后将学习更多关于函数的内容。接下来的四行实现了 rand 函数。我们暂时忽略它们。
主函数是正常的。声明了四个局部整数变量,并使用 for 循环将数组填充了 10 个随机值。请注意,数组 a 包含 10 个独立的整数。您使用方括号指向数组中的特定整数。因此 a[0] 指的是数组中的第一个整数,a[1] 指的是第二个,依此类推。以 /* 开头并以 */ 结尾的行被称为注释。编译器会完全忽略这一行。您可以在注释中为自己或其他程序员放置备注。
现在,在 more stuff ... 注释的位置添加以下代码
/* bubble sort the array */ for (x=0; x < MAX-1; x++) for (y=0; y < MAX-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } /* print sorted array */ printf("--------------------\n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]);
这段代码排序随机值并按排序顺序打印它们。每次运行它,您都会得到相同的值。如果您想更改排序的值,请在每次运行程序时更改 rand_seed 的值。
真正理解这段代码在做什么的唯一简单方法是“手动”执行它。也就是说,假设 MAX 为 4 以使其更易于管理,拿出一张纸,假装您是计算机。在纸上画出数组,并向数组中放入四个随机的、未排序的值。执行代码排序部分的每一行,并精确地画出发生了什么。您会发现,每次内循环执行时,数组中较大的值都会被推向数组底部,而较小的值则会向上冒泡。
C 语言中有三种标准变量类型
int 是一个 4 字节的整数值。float 是一个 4 字节的浮点值。char 是一个 1 字节的单字符(如“a”或“3”)。字符串声明为字符数组。
广告
有许多派生类型
C 语言中的运算符与大多数语言中的运算符类似
+ - addition - - subtraction / - division * - multiplication % - mod
如果两个操作数都是整数,/ 运算符执行整数除法;否则执行浮点除法。例如
void main() { float a; a=10/3; printf("%f\n",a); }
这段代码会打印出一个浮点值,因为 a 被声明为 float 类型,但 a 的值将是 3.0,因为代码执行了整数除法。
C 语言中的运算符优先级也与大多数其他语言类似。除法和乘法优先于加法和减法。计算 5+3*4 的结果是 17,而不是 32,因为 * 运算符在 C 语言中的优先级高于 +。您可以使用括号来改变正常的优先级顺序:(5+3)*4 是 32。5+3 首先被计算,因为它在括号中。我们稍后会深入探讨优先级——一旦引入指针,它在 C 语言中会变得有些复杂。
C 允许您即时执行类型转换。在使用指针时,您尤其经常这样做。类型转换也发生在某些类型的赋值操作期间。例如,在上面的代码中,整数值被自动转换为浮点数。
在 C 语言中,您可以通过将类型名放在括号中并将其置于要更改的值前面来进行类型转换。因此,在上面的代码中,将行 a=10/3; 替换为 a=(float)10/3; 会产生 3.33333 作为结果,因为在除法之前 10 被转换为浮点值。
您可以使用 typedef 语句在 C 语言中声明命名的、用户定义的类型。以下示例显示了在 C 代码中经常出现的一种类型
#define TRUE 1 #define FALSE 0 typedef int boolean; void main() { boolean b; b=FALSE; blah blah blah }
这段代码允许您在 C 程序中声明布尔类型。
如果您不喜欢用“float”来表示实数,您可以这样说
typedef float real;
然后稍后这样说
real r1,r2,r3;
您可以在 C 程序的任何地方放置 typedef 语句,只要它们在使用之前出现即可。
C 语言中的结构体允许您将变量组合成一个包。这是一个例子
struct rec { int a,b,c; float d,e,f; }; struct rec r;
如这里所示,每当您想声明 rec 类型的结构体时,都必须写 struct rec。这一行很容易忘记,您会因为不经意地漏掉 struct 而收到许多编译器错误。您可以将代码压缩成这种形式
struct rec { int a,b,c; float d,e,f; } r;
其中 rec 的类型声明和变量 r 在同一语句中声明。或者,您可以为结构体名称创建一个 typedef 语句。例如,如果您不喜欢每次声明记录时都写 struct rec r,您可以这样写
typedef struct rec rec_type;
然后通过以下方式声明 rec_type 类型的记录
rec_type r;
您使用句点访问结构体的字段,例如 r.a=5;。
您可以通过在常规声明后插入数组大小来声明数组,如下所示
int a[10]; /* array of integers */ char s[100]; /* array of characters (a C string) */ float f[20]; /* array of reals */ struct rec r[50]; /* array of records */
Long Way Short Way i=i+1; i++; i=i-1; i--; i=i+3; i += 3; i=i*j; i *= j;
大多数语言都允许您创建某种函数。函数允许您将一个长程序分解为命名部分,以便这些部分可以在整个程序中重用。函数接受参数并返回结果。C 函数可以接受无限数量的参数。一般来说,C 不关心您在程序中放置函数的顺序,只要在函数被调用之前,编译器知道函数名即可。
我们已经简单讨论了函数。前面看到的 rand 函数是函数中最简单的了。它不接受任何参数,并返回一个整数结果
广告
int rand() /* from K&R - produces a random number between 0 and 32767.*/ { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; }
行 int rand() 向程序的其余部分声明了 rand 函数,并指定 rand 不接受任何参数并返回一个整数结果。此函数没有局部变量,但如果它需要局部变量,它们将放在开头的 { 下方(C 允许您在任何 { 之后声明变量——它们存在直到程序到达匹配的 },然后它们消失。因此,函数的局部变量在函数中到达匹配的 } 时立即消失。当它们存在时,局部变量存在于系统栈上。)请注意,第一行中 () 之后没有 ;。如果您不小心添加了一个,您将收到一连串毫无意义的编译器错误消息。还要注意,即使没有参数,您也必须使用 ()。它们告诉编译器您正在声明一个函数,而不仅仅是声明一个 int。
return 语句对于任何返回结果的函数都很重要。它指定函数将返回的值,并导致函数立即退出。这意味着您可以在函数中放置多个 return 语句,以提供多个退出点。如果您不在函数中放置 return 语句,函数将在到达 } 时返回,并返回一个随机值(如果您未能返回特定值,许多编译器会警告您)。在 C 语言中,函数可以返回任何类型的值:int、float、char、struct 等。
有几种正确调用 rand 函数的方法。例如:x=rand();。在此语句中,变量 x 被赋值为 rand 返回的值。请注意,即使没有传递参数,您也*必须*在函数调用中使用 ()。否则,x 将获得 rand 函数的内存地址,这通常不是您想要的。
您也可以这样调用 rand
if (rand() > 100)
或者这样
rand();
在后一种情况下,函数被调用,但 rand 返回的值被丢弃。您可能永远不想对 rand 这样做,但许多函数通过函数名返回某种错误代码,如果您不关心错误代码(例如,因为您知道不可能出错),则可以以这种方式丢弃它。
如果您打算不返回任何内容,函数可以使用 void 返回类型。例如
void print_header() { printf("Program Number 1\n"); printf("by Marshall Brain\n"); printf("Version 1.0, released 12/26/91\n"); }
此函数不返回值。您可以使用以下语句调用它
print_header();
调用中必须包含 ()。如果省略,函数将不会被调用,即使它在许多系统上可以正确编译。
C 函数可以接受任何类型的参数。例如
int fact(int i) { int j,k; j=1; for (k=2; k<=i; k++) j=j*k; return j; }
返回 i 的阶乘,其中 i 作为整数参数传入。多个参数用逗号分隔
int add (int i, int j) { return i+j; }
C 语言多年来不断发展。您有时会看到像 add 这样的函数以“旧式”编写,如下所示
int add(i,j) int i; int j; { return i+j; }
能够阅读旧式风格编写的代码很重要。它的执行方式没有区别;它只是一种不同的表示法。您应该使用“新风格”(称为 ANSI C),其中类型作为参数列表的一部分声明,除非您知道您将把代码发送给只能访问“旧风格”(非 ANSI)编译器的用户。
现在,为程序中的所有函数使用函数原型被认为是良好的编程习惯。原型在函数实际声明之前向程序的其余部分声明函数名、其参数及其返回类型。要理解函数原型为何有用,请输入以下代码并运行它
#include <stdio.h> void main() { printf("%d\n",add(3)); } int add(int i, int j) { return i+j; }
这段代码在许多编译器上编译时不会给出警告,尽管 add 期望两个参数却只接收到一个。这是因为许多 C 编译器不检查参数的类型或数量是否匹配。您可能会浪费大量时间调试因意外传递过多或过少参数而导致的代码。上面的代码编译正确,但它产生了错误的答案。
广告
为了解决这个问题,C 语言允许您将函数原型放在程序的开头(实际上是任何位置)。如果您这样做,C 语言会检查所有参数列表的类型和数量。尝试编译以下代码
#include <stdio.h> int add (int,int); /* function prototype for add */ void main() { printf("%d\n",add(3)); } int add(int i, int j) { return i+j; }
原型导致编译器在 printf 语句处标记错误。
在程序开头为每个函数放置一个原型。它们可以为您节省大量的调试时间,并且还能解决在使用函数之前未声明它们时编译所遇到的问题。例如,以下代码将无法编译
#include <stdio.h> void main() { printf("%d\n",add(3)); } float add(int i, int j) { return i+j; }
您可能会问,为什么当 add 返回 int 时可以编译,而返回 float 时却不能呢?因为旧的 C 编译器默认返回 int 值。使用原型将解决这个问题。“旧式”(非 ANSI)编译器允许原型,但原型的参数列表必须为空。旧式编译器不对参数列表进行错误检查。
库在 C 语言中非常重要,因为 C 语言只支持它所需的最基本功能。C 甚至不包含从键盘读取和写入屏幕的 I/O 函数。任何超出基本语言的功能都必须由程序员编写。由此产生的代码块通常被放置在库中,以便于重复使用。我们已经看到了标准 I/O 库,即 stdio 库:标准库存在用于标准 I/O、数学函数、字符串处理、时间操作等。您可以在自己的程序中使用库,将程序拆分为模块。这使得它们更容易理解、测试和调试,并且还可以重用您编写的其他程序中的代码。
您可以轻松创建自己的库。举个例子,我们将从本系列之前的一篇文章中提取一些代码,并将其中的两个函数制作成一个库。以下是我们将要开始使用的代码
广告
#include <stdio.h> #define MAX 10 int a[MAX]; int rand_seed=10; int rand() /* from K&R - produces a random number between 0 and 32767.*/ { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } void main() { int i,t,x,y; /* fill array */ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } /* bubble sort the array */ for (x=0; x < MAX-1; x++) for (y=0; y < MAX-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } /* print sorted array */ printf("--------------------\n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]); }
这段代码用随机数填充一个数组,使用冒泡排序对其进行排序,然后显示排序后的列表。
取出冒泡排序代码,并利用您在上一篇文章中学到的知识,将其制作成一个函数。由于数组 a 和常量 MAX 都是全局已知的,因此您创建的函数不需要任何参数,也不需要返回结果。但是,您应该为 x、y 和 t 使用局部变量。
一旦您测试了函数以确保其正常工作,请将元素数量作为参数传入,而不是使用 MAX
#include <stdio.h> #define MAX 10 int a[MAX]; int rand_seed=10; /* from K&R - returns random number between 0 and 32767.*/ int rand() { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } void bubble_sort(int m) { int x,y,t; for (x=0; x < m-1; x++) for (y=0; y < m-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } } void main() { int i,t,x,y; /* fill array */ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } bubble_sort(MAX); /* print sorted array */ printf("--------------------\n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]); }
您还可以通过将 a 作为参数传入,进一步泛化 bubble_sort 函数
bubble_sort(int m, int a[])
这一行表示:“接受任意大小的整数数组 a 作为参数。” bubble_sort 函数的主体部分无需更改。要调用 bubble_sort,请将调用更改为
bubble_sort(MAX, a);
请注意,即使排序会改变 a,函数调用中也没有使用 &a。理解指针后,原因就会清楚。
由于之前的程序中的 rand 和 bubble_sort 函数很有用,您可能希望在您编写的其他程序中重复使用它们。您可以将它们放入一个实用程序库中,以便于重复使用。
每个库都由两部分组成:一个头文件和实际的代码文件。头文件通常以 .h 为后缀,包含使用该库的程序需要了解的库信息。一般来说,头文件包含常量和类型,以及库中可用函数的原型。输入以下头文件并将其保存到名为 util.h 的文件中。
广告
/* util.h */ extern int rand(); extern void bubble_sort(int, int []);
这两行是函数原型。C 语言中的“extern”关键字表示稍后将被链接进来的函数。如果您使用的是旧式编译器,请从 bubble_sort 的参数列表中移除参数。
将以下代码输入到名为 util.c 的文件中。
/* util.c */ #include "util.h" int rand_seed=10; /* from K&R - produces a random number between 0 and 32767.*/ int rand() { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } void bubble_sort(int m,int a[]) { int x,y,t; for (x=0; x < m-1; x++) for (y=0; y < m-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } }
请注意,该文件包含其自己的头文件(util.h),并且它使用引号而不是符号 < 和>,后者仅用于系统库。如您所见,这看起来像普通的 C 代码。请注意,变量 rand_seed,因为它不在头文件中,所以使用此库的程序无法看到或修改它。这称为信息隐藏。在 int 前面添加 static 关键字可以完全强制实现这种隐藏。
在名为 main.c 的文件中输入以下主程序。
#include <stdio.h> #include "util.h" #define MAX 10 int a[MAX]; void main() { int i,t,x,y; /* fill array */ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } bubble_sort(MAX,a); /* print sorted array */ printf("--------------------\n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]); }
此代码包含了实用程序库。使用库的主要好处是主程序中的代码会大大缩短。
要编译库,请在命令行中输入以下内容(假设您使用 UNIX)(如果您的系统使用 cc,请将 gcc 替换为 cc)
gcc -c -g util.c
-c 使编译器为库生成一个目标文件。目标文件包含库的机器码。它必须链接到一个包含主函数的程序文件才能执行。机器码位于一个名为 util.o 的单独文件中。
要编译主程序,请输入以下内容
gcc -c -g main.c
这一行创建了一个名为 main.o 的文件,其中包含主程序的机器代码。要创建包含整个程序机器代码的最终可执行文件,请键入以下内容链接两个目标文件
gcc -o main main.o util.o
这将 main.o 和 util.o 链接起来,形成一个名为 main 的可执行文件。要运行它,请键入 main。
Makefile 让使用库变得更容易一些。您将在下一页了解 Makefile。
重复输入所有 gcc 命令会很繁琐,特别是当您对代码进行大量更改并且代码包含多个库时。make 工具解决了这个问题。您可以使用以下 makefile 替换上面的编译序列
main: main.o util.o gcc -o main main.o util.o main.o: main.c util.h gcc -c -g main.c util.o: util.c util.h gcc -c -g util.c
将其输入到名为 makefile 的文件中,然后输入 make 以构建可执行文件。请注意,您*必须*在所有 gcc 行前面加上一个制表符。(八个空格不足以替代——必须是一个制表符。所有其他行都必须左对齐。)
广告
这个 makefile 包含两种类型的行。左对齐的行是依赖行。以制表符开头的行是可执行行,可以包含任何有效的 UNIX 命令。依赖行表示某个文件依赖于其他文件集。例如,main.o: main.c util.h 表示文件 main.o 依赖于文件 main.c 和 util.h。如果这两个文件中的任何一个发生变化,则应执行以下可执行行以重新创建 main.o。
请注意,整个 makefile 生成的最终可执行文件是 main,位于 makefile 的第 1 行。makefile 的最终结果应始终放在第 1 行,在本 makefile 中表示文件 main 依赖于 main.o 和 util.o。如果其中任何一个发生变化,则执行行 gcc -o main main.o util.o 以重新创建 main。
可以在依赖行下方放置多行要执行的命令——它们都必须以制表符开头。一个大型程序可能包含多个库和一个主程序。Makefile 会自动重新编译所有因更改而需要重新编译的内容。
如果您不在 UNIX 机器上工作,您的编译器几乎肯定具有与 makefile 等效的功能。请阅读您的编译器文档以了解如何使用它。
现在您明白为什么在之前的程序中一直包含 stdio.h 了。它只是一个标准库,很久以前由某人创建,并提供给其他程序员,以使他们的工作更轻松。
C 语言中的文本文件简单明了,易于理解。C 语言中所有的文本文件函数和类型都来自于 stdio 库。
当您在 C 程序中需要文本 I/O,并且只需要一个输入信息源和一个输出信息接收器时,您可以依赖 stdin(标准输入)和 stdout(标准输出)。然后您可以使用命令行上的输入和输出重定向,将不同的信息流通过程序传递。在 <stdio.h> 中有六个不同的 I/O 命令可以与 stdin 和 stdout 一起使用
广告
stdin 和 stdout 的优点是它们易于使用。同样,重定向 I/O 的能力也非常强大。例如,您可能想创建一个从 stdin 读取并计算字符数的程序
#include <stdio.h> #include <string.h> void main() { char s[1000]; int count=0; while (gets(s)) count += strlen(s); printf("%d\n",count); }
输入此代码并运行它。它会等待来自 stdin 的输入,所以输入几行。完成后,按 CTRL-D 信号文件结束 (eof)。gets 函数会读取一行,直到检测到 eof,然后返回 0,从而使 while 循环结束。当您按下 CTRL-D 时,您会在 stdout(屏幕)上看到字符计数。(使用 man gets 或您的编译器文档了解更多关于 gets 函数的信息。)
现在,假设您想计算文件中的字符。如果您将程序编译为名为 xxx 的可执行文件,您可以输入以下内容
xxx < filename
将使用名为 filename 的文件内容作为输入,而不是从键盘接收输入。您可以使用管道实现相同的结果
cat < filename | xxx
您还可以将输出重定向到文件
xxx < filename > out
此命令将程序生成的字符计数放置到名为 out 的文本文件中。
有时,您需要直接使用文本文件。例如,您可能需要打开一个特定文件并从中读取或写入。您可能希望管理多个输入或输出流,或者创建一个像文本编辑器那样的程序,能够按命令保存和召回数据或配置文件。在这种情况下,请使用 stdio 中的文本文件函数
您使用 fopen 来打开文件。它以指定模式打开文件(最常见的三种模式是 r、w 和 a,分别代表读取、写入和追加)。然后它返回一个文件指针,您可以使用该指针访问文件。例如,假设您想打开一个文件并在其中写入数字 1 到 10。您可以使用以下代码
#include <stdio.h> #define MAX 10 int main() { FILE *f; int x; f=fopen("out","w"); if (!f) return 1; for(x=1; x<=MAX; x++) fprintf(f,"%d\n",x); fclose(f); return 0; }
这里的 fopen 语句以 w 模式打开一个名为 out 的文件。这是一种破坏性写入模式,意味着如果 out 不存在,它将被创建;但如果它存在,它将被销毁,并在其位置创建一个新文件。fopen 命令返回一个指向文件的指针,该指针存储在变量 f 中。此变量用于引用文件。如果文件由于某种原因无法打开,f 将包含 NULL。
广告
fprintf 语句应该看起来很熟悉:它就像 printf,但使用文件指针作为它的第一个参数。fclose 语句在您完成后关闭文件。
要读取文件,请以 r 模式打开它。一般来说,不建议使用 fscanf 进行读取:除非文件格式完美,否则 fscanf 将无法正确处理。相反,使用 fgets 读取每一行,然后解析出您需要的部分。
以下代码演示了读取文件并将其内容输出到屏幕的过程
广告
#include <stdio.h> int main() { FILE *f; char s[1000]; f=fopen("infile","r"); if (!f) return 1; while (fgets(s,1000,f)!=NULL) printf("%s",s); fclose(f); return 0; }
fgets 语句在文件结束标记处返回 NULL 值。它读取一行(在此例中最多 1,000 个字符),然后将其打印到 stdout。请注意,printf 语句的格式字符串中不包含 \n,因为 fgets 会在其读取的每行末尾添加 \n。因此,如果一行溢出 fgets 第二个参数中指定的最大行长,您可以判断该行是否不完整。
C 语言中指针无处不在,因此如果您想充分利用 C 语言,就必须对指针有非常好的理解。它们必须让您感到*舒适*。本节以及接下来的几节的目标是帮助您全面理解指针以及 C 语言如何使用它们。对于大多数人来说,完全适应指针需要一点时间和练习,但一旦您掌握了它们,您就是一名合格的 C 程序员了。
C 语言以三种不同的方式使用指针
广告
在某些情况下,C 程序员也使用指针,因为它们可以使代码稍微更高效。您会发现,一旦您完全适应了指针,您就会倾向于一直使用它们。
我们将从指针及其相关概念的基本介绍开始本次讨论,然后转向上面描述的三种技术。特别是对于这篇文章,您会希望阅读两遍。第一次阅读可以学习所有概念。第二次阅读可以尝试将这些概念在您的脑海中整合成一个整体。当您第二次通读这些材料后,它将变得非常有意义。
想象一下,您想创建一个文本编辑器——一个让您编辑普通 ASCII 文本文件的程序,就像 UNIX 上的“vi”或 Windows 上的“记事本”一样。文本编辑器是人们相当普遍会创建的东西,因为如果您仔细想想,文本编辑器可能是程序员最常用的软件。文本编辑器是程序员与计算机的紧密联系——您在那里输入所有的想法,然后操作它们。显然,对于您经常使用且如此密切协作的任何东西,您都希望它恰到好处。因此,许多程序员创建自己的编辑器并根据自己的个人工作风格和偏好进行定制。
所以有一天您坐下来开始编写您的编辑器。在考虑了想要的功能之后,您开始思考编辑器的“数据结构”。也就是说,您开始思考如何将您正在编辑的文档存储在内存中,以便您可以在程序中操作它。您需要一种方法,将您输入的信息以一种可以快速方便地操作的形式存储起来。您认为一种方法是基于字符行来组织数据。根据我们目前所讨论的内容,您目前唯一可用的就是数组。您会想:“嗯,典型的一行有 80 个字符长,一个典型的文件不超过 1,000 行。”因此,您声明了一个二维数组,像这样
广告
char doc[1000][80];
此声明请求一个包含 1,000 行 80 字符的数组。这个数组的总大小为 80,000 个字符。
然而,当您更深入地思考您的编辑器及其数据结构时,您可能会意识到三件事
假设您一次最多打开 10 个文件,最大行长度为 1,000 个字符,最大文件大小为 50,000 行。您的声明现在看起来像这样
char doc[50000][1000][10];
这看起来似乎不无道理,直到您拿出计算器,将 50,000 乘以 1,000 再乘以 10,然后意识到这个数组包含了 5 亿个字符!现在大多数计算机都会对这么大的数组感到吃力。它们根本没有足够的内存,甚至没有足够的虚拟内存空间来支持这么大的数组。如果用户试图在即使是最大的多用户系统上同时运行三四个这个程序的副本,那将对系统设施造成严峻的压力。
即使计算机能接受如此巨大的数组请求,您也会发现这是极大的空间浪费。声明一个 5 亿字符的数组似乎很奇怪,因为在绝大多数情况下,您运行这个编辑器只是为了查看最多消耗 4,000 或 5,000 字节的 100 行文件。数组的问题在于,您必须从一开始就声明它在每个维度上都具有最大大小。 这些最大大小常常相乘形成非常大的数字。而且,如果您碰巧需要编辑一个包含 2,000 字符行的奇特文件,您就束手无策了。您无法真正预测和处理文本文件的最大行长,因为从技术上讲,这个数字是无限的。
指针旨在解决这个问题。通过指针,您可以创建动态数据结构。您不必预先在数组中声明最坏情况下的内存消耗,而是在程序运行时从堆中分配内存。这样您就可以使用文档所需的精确内存量,而不会浪费。此外,当您关闭文档时,可以将内存返回到堆中,以便程序的其他部分可以使用它。通过指针,内存可以在程序运行时回收。
顺便说一句,如果您阅读了之前的讨论,并且您有一个大问题是:“字节到底是什么?”,那么文章位和字节的工作原理将帮助您理解这些概念,以及“兆”、“千兆”和“太拉”等术语。去看看,然后回来。
要理解指针,将其与普通变量进行比较会有帮助。
“普通变量”是内存中一个可以存储值的位置。例如,当您将变量 i 声明为整数时,会为其分配四个字节的内存。在您的程序中,您通过名称 i 来引用内存中的该位置。在机器层面,该位置有一个内存地址。该地址处的四个字节对您(程序员)来说就是 i,这四个字节可以存储一个整数值。
广告
指针则不同。指针是一个指向另一个变量的变量。这意味着指针存储了另一个变量的内存地址。换句话说,指针不以传统意义上的方式存储值;相反,它存储了另一个变量的地址。指针通过持有另一个变量的地址副本,从而“指向”该变量。
因为指针存储的是地址而不是值,所以它由两部分组成。指针本身存储地址。该地址指向一个值。所以有指针,还有被指针指向的值。这个事实在您熟悉它之前可能会有点令人困惑,但一旦您熟悉了它,它就会变得极其强大。
以下示例代码展示了一个典型的指针
#include <stdio.h> int main() { int i,j; int *p; /* a pointer to an integer */ p = &i; *p=5; j=i; printf("%d %d %d\n", i, j, *p); return 0; }
这个程序中的第一个声明声明了两个普通的整数变量 i 和 j。行 int *p 声明了一个名为 p 的指针。这一行要求编译器声明一个变量 p,它是一个指向整数的指针。* 表示正在声明一个指针而不是一个普通变量。您可以创建指向任何类型数据的指针:浮点数、结构体、字符等等。只需使用 * 来表示您想要一个指针而不是普通变量。
行 p = &i; 对您来说肯定很陌生。在 C 语言中,& 被称为地址运算符。表达式 &i 的意思是:“变量 i 的内存地址。”因此,表达式 p = &i; 的意思是:“将 i 的地址赋值给 p。”一旦您执行此语句,p 就“指向” i。在此之前,p 包含一个随机、未知的地址,使用它很可能会导致分段错误或类似的程序崩溃。
了解正在发生的事情的一个好方法是画一张图。在声明了i、j和p之后,世界看起来就像上面的图片。
在这张图中,变量i、j和p已经声明,但它们都没有初始化。因此,这两个整数变量被画成包含问号的方框——在程序执行的这一点上,它们可能包含任何值。指针被画成一个圆圈,以区别于存储值的普通变量,随机的箭头表示它此时可以指向任何地方。
在执行行p = &i;之后,p被初始化并指向i,像这样
一旦p指向i,内存位置i就有了两个名称。它仍然被称为i,但现在也被称为*p。这就是C语言描述指针变量两个部分的方式:p是存储地址的位置,而*p是该地址指向的位置。因此,*p=5意味着p指向的位置应该设置为5,像这样
因为位置*p也是i,所以i也取值为5。因此,j=i;将j设置为5,并且printf语句会输出5 5 5。
指针的主要特点是它的两部分性质。指针本身持有一个地址。指针也指向特定类型的值——即指针所持地址处的值。在这种情况下,指针本身是p。所指向的值是*p。
如果你了解内存地址在计算机硬件中的工作原理,之前的讨论会更清晰一些。如果你还没有读过,现在是阅读位和字节如何工作的好时机,以便充分理解位、字节和字。
所有计算机都有内存,也称为RAM(随机存取存储器)。例如,你的电脑现在可能安装了16、32或64兆字节的RAM。RAM存储着你的电脑当前正在运行的程序以及它们正在操作的数据(它们的变量和数据结构)。内存可以简单地看作是一个字节数组。在这个数组中,每个内存位置都有自己的地址——第一个字节的地址是0,然后是1、2、3,依此类推。内存地址的作用就像普通数组的索引。计算机可以随时访问内存中的任何地址(因此得名“随机存取存储器”)。它还可以根据需要将字节组合起来,形成更大的变量、数组和结构。例如,一个浮点变量在内存中占用4个连续的字节。你可能会在程序中进行以下全局声明
广告
float f;
这条语句表示:“声明一个名为f的内存位置,它可以存储一个浮点值。”当程序运行时,计算机在内存中的某个地方为变量f保留空间。该位置在内存空间中有一个固定地址,像这样
当你想到变量f时,计算机想到的是内存中的一个特定地址(例如,248,440)。因此,当你创建这样一条语句时
f = 3.14;
编译器可能会将其翻译为:“将值3.14加载到内存位置248,440。”计算机总是以地址和这些地址处的值来思考内存。
顺便说一下,你的计算机处理内存的方式有几个有趣的副作用。例如,假设你在程序中包含以下代码
int i, s[4], t[4], u=0; for (i=0; i<=4; i++) { s[i] = i; t[i] =i; } printf("s:t\n"); for (i=0; i<=4; i++) printf("%d:%d\n", s[i], t[i]); printf("u = %d\n", u);
你从程序中看到的输出可能看起来像这样
s:t 1:5 2:2 3:3 4:4 5:5 u = 5
为什么t[0]和u不正确?如果你仔细查看代码,你会发现for循环写到了每个数组的末尾之后一个元素。在内存中,数组彼此相邻放置,如下所示
因此,当你尝试写入不存在的s[4]时,系统会转而写入t[0],因为t[0]是s[4]应该在的位置。当你写入t[4]时,你实际上是在写入u。就计算机而言,s[4]只是一个地址,它可以写入其中。然而,正如你所看到的,即使计算机执行了程序,它也不是正确或有效的。程序在运行过程中破坏了数组t。如果你执行以下语句,会导致更严重的后果
s[1000000] = 5;
位置s[1000000]很可能超出了你程序的内存空间。换句话说,你正在写入不属于你程序拥有的内存。在具有受保护内存空间(UNIX、Windows 98/NT)的系统上,这种语句会导致系统终止程序的执行。然而,在其他系统(Windows 3.1、Mac)上,系统并不知道你在做什么。你最终会损坏另一个应用程序中的代码或变量。这种违规行为的影响范围从没有任何影响到完全的系统崩溃。在内存中,i、s、t和u都紧密地放置在特定地址。因此,如果你写入超出变量边界的地方,计算机将按照你说的做,但最终会破坏另一个内存位置。
由于C和C++在你访问数组元素时不会执行任何类型的范围检查,因此作为程序员,你必须自己仔细注意数组范围并保持在数组的适当边界内。无意中读写超出数组边界的情况总是会导致程序行为错误。
另一个例子,请尝试以下操作
#include <stdio.h> int main() { int i,j; int *p; /* a pointer to an integer */ printf("%d %d\n", p, &i); p = &i; printf("%d %d\n", p, &i); return 0; }
这段代码告诉编译器打印出p中持有的地址,以及i的地址。变量p最初会包含某个随机值或0。i的地址通常是一个很大的值。例如,当我运行这段代码时,我收到了以下输出
0 2147478276 2147478276 2147478276
这意味着i的地址是2147478276。一旦语句p = &i;执行,p就包含了i的地址。也试试这个
#include <stdio.h> void main() { int *p; /* a pointer to an integer */ printf("%d\n",*p); }
这段代码告诉编译器打印出p指向的值。然而,p尚未初始化;它包含地址0或某个随机地址。在大多数情况下,会导致段错误(或其它运行时错误),这意味着你使用了指向无效内存区域的指针。几乎总是,未初始化的指针或错误的指针地址是导致段错误的原因。
说了这么多,我们现在可以从全新的角度来看待指针了。以这个程序为例
#include <stdio.h> int main() { int i; int *p; /* a pointer to an integer */ p = &i; *p=5; printf("%d %d\n", i, *p); return 0; }
这是正在发生的事情
变量i占用4字节内存。指针p也占用4字节(在当今大多数机器上,指针占用4字节内存。当今大多数CPU上的内存地址是32位长的,尽管有向64位寻址发展的趋势)。i的位置有一个特定地址,在本例中是248,440。一旦你说p = &i;,指针p就保存了该地址。因此,变量*p和i是等价的。
指针p字面上保存了i的地址。当你在程序中说类似这样的话时
printf("%d", p);
输出的是变量i的实际地址。
这是C语言一个很酷的方面:任意数量的指针都可以指向同一个地址。例如,你可以将p、q和r声明为整数指针,并将它们都设置为指向i,如下所示
int i; int *p, *q, *r; p = &i; q = &i; r = p;
请注意,在这段代码中,r指向p所指向的同一个对象,即i。你可以将指针相互赋值,赋值时地址会从右侧复制到左侧。执行上述代码后,情况会是这样
广告
变量i现在有四个名称:i、*p、*q和*r。指向(并因此可以指向)同一地址的指针数量没有限制。
Bug #1 - 未初始化指针
制造指针bug最简单的方法之一是尝试引用指针的值,即使该指针尚未初始化且未指向有效地址。例如
广告
int *p; *p = 12;
当你声明指针p时,它未经初始化,指向内存中的一个随机位置。它可能指向系统堆栈、全局变量、程序的代码空间,或操作系统。当你输入*p=12;时,程序将尝试将12写入p指向的任何随机位置。程序可能立即崩溃,或者等待半小时后崩溃,或者它可能在程序的另一部分悄悄地损坏数据而你永远不会意识到。这使得这种错误非常难以追踪。请确保在解引用所有指针之前,将它们初始化为有效地址。
Bug #2 - 无效指针引用
当指针未指向有效内存块却被引用其值时,就会发生无效指针引用。
制造这个错误的一种方法是,当q未初始化时,执行p=q;。指针p也将变得未初始化,并且对*p的任何引用都是无效的指针引用。
避免这个bug的唯一方法是画出程序每一步的图示,并确保所有指针都指向某个地方。无效指针引用导致程序莫名其妙地崩溃,原因与Bug #1中给出的一样。
Bug #3 - 零指针引用
当指向零的指针在试图引用内存块的语句中使用时,就会发生零指针引用。例如,如果p是一个指向整数的指针,以下代码是无效的
p = 0; *p = 12;
p没有指向任何内存块。因此,试图从该块读取或写入任何内容是无效的零指针引用。将指针指向零有很好的、有效的原因,我们将在后面的文章中看到。然而,解引用这样的指针是无效的。
所有这些bug对包含它们的程序都是致命的。你必须仔细检查你的代码,以防止这些bug的发生。最好的方法是逐步绘制代码执行的图示。
大多数C程序员最初使用指针来实现函数中的可变参数。你实际上已经在scanf函数中使用了可变参数——这就是为什么你必须在与scanf一起使用的变量上使用&(地址运算符)的原因。现在你理解了指针,你就能明白到底发生了什么。
为了理解可变参数的工作原理,让我们看看如何在C语言中实现一个swap函数。要实现一个交换函数,你希望传入两个变量,并让函数交换它们的值。这里有一个实现尝试——输入并执行以下代码,看看会发生什么
广告
#include <stdio.h> void swap(int i, int j) { int t; t=i; i=j; j=t; } void main() { int a,b; a=5; b=10; printf("%d %d\n", a, b); swap(a,b); printf("%d %d\n", a, b); }
当你执行这个程序时,你会发现没有发生交换。a和b的值被传递给swap函数,并且swap函数确实交换了它们,但是当函数返回时,什么都没有发生。
要使此函数正常工作,你可以使用指针,如下所示
#include <stdio.h> void swap(int *i, int *j) { int t; t = *i; *i = *j; *j = t; } void main() { int a,b; a=5; b=10; printf("%d %d\n",a,b); swap(&a,&b); printf("%d %d\n",a,b); }
要了解这段代码的作用,请将其打印出来,画出两个整数a和b,并在其中输入5和10。现在画出两个指针i和j,以及整数t。当调用swap时,它被传递了a和b的地址。因此,i指向a(从i画一个箭头到a),j指向b(从b画另一个箭头到j)。一旦指针被函数调用初始化,*i就是a的另一个名称,而*j是b的另一个名称。现在运行swap中的代码。当代码使用*i和*j时,它实际上指的是a和b。当函数完成时,a和b已经交换。
假设你在调用swap函数时不小心忘记了&,并且swap行不小心看起来像这样:swap(a, b);。这会导致段错误。当你省略&时,传递的是a的值而不是它的地址。因此,i指向内存中的一个无效位置,当使用*i时系统会崩溃。
这也是为什么如果你忘记在传递给scanf的变量上加上&,scanf会崩溃的原因。scanf函数使用指针将其读取的值写回到你传递的变量中。如果没有&,scanf将收到一个错误的地址并崩溃。
可变参数是C语言中指针最常见的用途之一。现在你明白正在发生什么了!
动态数据结构是根据需要通过从称为堆的地方分配和释放内存来增长和缩减的数据结构。它们在C语言中极其重要,因为它们允许程序员精确控制内存消耗。
动态数据结构根据需要从堆中分配内存块,并使用指针将这些块链接在一起形成某种数据结构。当数据结构不再需要某个内存块时,它会将其返回给堆以供重用。这种回收利用方式使得内存使用效率非常高。
广告
要完全理解动态数据结构,我们需要从堆开始。
如今,一台典型的个人电脑或工作站通常安装有16到64兆字节的RAM。通过使用一种称为虚拟内存的技术,系统可以将内存片段在机器硬盘上换入换出,为CPU营造出拥有更多内存的假象,例如200到500兆字节。虽然这种假象对于CPU来说是完整的,但从用户的角度来看,有时会极大地拖慢速度。尽管有这个缺点,虚拟内存是一种以廉价方式“增加”机器RAM容量的极其有用的技术。为了本次讨论,我们假设一台典型的计算机总内存空间为50兆字节(无论该内存是实实在在的RAM还是虚拟内存)。
机器上的操作系统负责管理50兆字节的内存空间。操作系统以几种不同的方式使用该空间,如下所示。
广告
当然,这是一种理想化,但基本原则是正确的。如你所见,内存中存储着机器上当前运行的不同应用程序的可执行代码,以及操作系统本身的可执行代码。每个应用程序都有一部分相关的全局变量。这些变量也占用内存。最后,每个应用程序都使用一个名为栈的内存区域,它保存着任何函数使用的所有局部变量和参数。栈还记住函数调用的顺序,以便函数正确返回。每次调用函数时,其局部变量和参数都会被“压入”栈中。当函数返回时,这些局部变量和参数都会被“弹出”。因此,程序的栈大小在程序运行时不断波动,但它有一个最大尺寸。
当程序执行完毕时,操作系统会将其可执行代码空间、全局变量和栈空间从内存中卸载。新的程序可以在以后使用该空间。通过这种方式,计算机系统中的内存不断地被“回收”和重用,随着程序的执行和完成。
通常,在任何给定时刻,计算机总内存空间可能有多达50%的未使用部分。操作系统拥有并管理这些未使用内存,它们统称为堆。堆极其重要,因为它可以通过C语言的malloc(内存分配)和free函数在程序执行期间供应用程序使用。堆允许程序在执行过程中需要时才精确地分配内存,而不是通过特定大小的数组声明预先分配。
假设你想在应用程序执行期间分配一定量的内存。你可以随时调用malloc函数,它会从堆中请求一个内存块。操作系统会为你的程序保留一个内存块,你可以随意使用它。当你使用完该内存块时,通过调用free函数将其返回给操作系统进行回收。然后其他应用程序可以在以后保留它供自己使用。
例如,以下代码演示了堆的最简单用法
广告
int main() { int *p; p = (int *)malloc(sizeof(int)); if (p == 0) { printf("ERROR: Out of memory\n"); return 1; } *p = 5; printf("%d\n", *p); free(p); return 0; }
此程序的第一行调用malloc函数。此函数执行三项操作
程序接下来通过行if (p == 0)(也可以写成if (p == NULL)甚至if (!p))检查指针p,以确保分配请求成功。如果分配失败(如果p为零),程序终止。如果分配成功,程序则将内存块初始化为值5,打印出该值,并在程序终止前调用free函数将内存返回给堆。
这段代码与之前将p设置为现有整数i地址的代码实际上没有区别。唯一的区别在于,对于变量i,内存作为程序预分配内存空间的一部分存在,并且有两个名称:i和*p。对于从堆中分配的内存,该块只有一个名称*p,并在程序执行期间分配。两个常见问题
以下两个程序展示了指针的两种不同有效用法,并试图区分指针的使用和指针值的用法
void main() { int *p, *q; p = (int *)malloc(sizeof(int)); q = p; *p = 10; printf("%d\n", *q); *q = 20; printf("%d\n", *q); }
这段代码的最终输出将是第4行的10和第6行的20。
以下代码略有不同
void main() { int *p, *q; p = (int *)malloc(sizeof(int)); q = (int *)malloc(sizeof(int)); *p = 10; *q = 20; *p = *q; printf("%d\n", *p); }
这段代码的最终输出将是第6行的20。
请注意,编译器将允许*p = *q,因为*p和*q都是整数。这条语句的意思是:“将q指向的整数值移动到p指向的整数值中。”该语句移动的是值。编译器也允许p = q,因为p和q都是指针,并且都指向相同类型(如果s是指向字符的指针,则不允许p = s,因为它们指向不同类型)。语句p = q的意思是:“让p指向q所指向的相同内存块。”换句话说,q所指向的地址被移动到p中,因此它们都指向同一个内存块。这条语句移动的是地址。
从所有这些例子中,你可以看到有四种不同的方式来初始化指针。当声明一个指针时,如int *p,它在程序中处于未初始化状态。它可能指向任何地方,因此解引用它是一个错误。指针变量的初始化涉及将其指向内存中一个已知的位置。
任何指针都可以设置为指向零。然而,当p指向零时,它并不指向一个内存块。指针只是包含地址零,这个值可以用作一个标记。你可以在诸如以下语句中使用它
if (p == 0){ ...}
或
while (p != 0){ ...}
系统也识别零值,如果你碰巧解引用一个零指针,系统将生成错误消息。例如,在以下代码中
p = 0;*p = 5;
程序通常会崩溃。指针p不指向内存块,它指向零,因此不能给*p赋值。当我们讨论链表时,零指针将用作一个标志。
malloc命令用于分配一块内存。当不再需要内存块时,也可以释放它。当内存块被释放后,它可以被后续的malloc命令重新使用,从而使系统能够回收内存。用于释放内存的命令称为free,它接受一个指针作为参数。free命令执行两项操作
free语句只是将指针返回到其原始的未初始化状态,并使该内存块在堆上再次可用。
以下示例展示了如何使用堆。它分配一个整数块,填充它,写入它,然后释放它
#include <stdio.h> int main() { int *p; p = (int *)malloc (sizeof(int)); *p=10; printf("%d\n",*p); free(p); return 0; }
这段代码实际上只用于演示C语言中分配、释放和使用内存块的过程。malloc行分配指定大小的内存块——在本例中是sizeof(int)字节(4字节)。C语言中的sizeof命令返回任何类型的大小(以字节为单位)。这段代码也可以简单地写成malloc(4),因为在大多数机器上sizeof(int)等于4字节。然而,使用sizeof使代码更具可移植性和可读性。
malloc函数返回一个指向已分配内存块的指针。这个指针是通用的。不进行类型转换就使用该指针通常会导致编译器发出类型警告。(int *)类型转换将malloc返回的通用指针转换为“指向整数的指针”,这正是p所期望的。C语言中的free语句将内存块返回给堆以供重用。
第二个例子演示了与前一个例子相同的功能,但它使用结构体而不是整数。在C语言中,代码看起来像这样
#include <stdio.h> struct rec { int i; float f; char c; }; int main() { struct rec *p; p=(struct rec *) malloc (sizeof(struct rec)); (*p).i=10; (*p).f=3.14; (*p).c='a'; printf("%d %f %c\n",(*p).i,(*p).f,(*p).c); free(p); return 0; }
注意以下行
(*p).i=10;
许多人想知道为什么以下代码不起作用
*p.i=10;
答案与C语言中运算符的优先级有关。计算5+3*4的结果是17,而不是32,因为在大多数计算机语言中,*运算符的优先级高于+。在C语言中,.运算符的优先级高于*,所以括号强制了正确的优先级。
大多数人厌倦了总是输入(*p).i,因此C语言提供了一种简写符号。以下两条语句完全等效,但第二条更容易输入
(*p).i=10; p->i=10;
在阅读他人代码时,你看到第二种写法的频率会高于第一种。
通常,你会以比之前一些示例中所示更复杂的方式使用指针。例如,创建和使用普通整数比创建和使用指向整数的指针要容易得多。在本节中,将探讨一些更常见和高级的指针使用方法。
在C语言中创建指针类型是可能、合法且有益的,如下所示
广告
typedef int *IntPointer; ... IntPointer p;
这与以下说法相同
int *p;
这种技术将在后续页面的许多示例中使用。该技术通常使数据声明更易于阅读和理解,也使得在结构中包含指针或在函数中传递指针参数变得更容易。
在C语言中,几乎可以为任何类型创建指针,包括用户自定义类型。创建指向结构体的指针非常常见。示例如下
typedef struct { char name[21]; char city[21]; char state[3]; } Rec; typedef Rec *RecPointer; RecPointer r; r = (RecPointer)malloc(sizeof(Rec));
指针r是一个指向结构体的指针。请注意,r是一个指针,因此像任何其他指针一样占用四个字节的内存。然而,malloc语句从堆中分配了45字节的内存。*r是一个结构体,就像任何其他Rec类型的结构体一样。以下代码显示了指针变量的典型用法
广告
strcpy((*r).name, "Leigh"); strcpy((*r).city, "Raleigh"); strcpy((*r).state, "NC"); printf("%s\n", (*r).city); free(r);
你可以像处理普通结构体变量一样处理*r,但必须小心C语言中运算符的优先级。如果你省略了*r周围的括号,代码将无法编译,因为“.”运算符的优先级高于“*”运算符。由于在使用指向结构体的指针时输入这么多括号会很麻烦,C语言包含了一种做同样事情的简写符号
strcpy(r->name, "Leigh");
r->符号与(*r).完全等价,但字符数减少了两个。
还可以创建指向数组的指针,如下所示
int *p; int i; p = (int *)malloc(sizeof(int[10])); for (i=0; i<10; i++) p[i] = 0; free(p);
或
int *p; int i; p = (int *)malloc(sizeof(int[10])); for (i=0; i<10; i++) *(p+i) = 0; free(p);
请注意,当你创建一个指向整数数组的指针时,你只是创建一个普通的int指针。对malloc的调用会分配你想要的任何大小的数组,并且指针指向该数组的第一个元素。你可以使用普通数组索引来遍历p指向的数组,也可以使用指针算术来实现。C语言认为这两种形式是等价的。
这种特殊的技术在处理字符串时非常有用。它允许你精确地分配足够的存储空间来容纳特定大小的字符串。
有时,通过声明一个指针数组,可以节省大量空间,或者解决某些内存密集型问题。在下面的示例代码中,声明了一个包含10个指向结构体的指针数组,而不是声明一个结构体数组。如果创建的是结构体数组,则需要243 * 10 = 2,430字节的数组空间。使用指针数组可以使数组占用最小的空间,直到实际记录通过malloc语句分配。下面的代码只是分配一个记录,在其中放置一个值,并处理该记录以演示该过程
typedef struct { char s1[81]; char s2[81]; char s3[81]; } Rec; Rec *a[10]; a[0] = (Rec *)malloc(sizeof(Rec)); strcpy(a[0]->s1, "hello"); free(a[0]);
结构体可以包含指针,如下所示
typedef struct { char name[21]; char city[21]; char phone[21]; char *comment; } Addr; Addr s; char comm[100]; gets(s.name, 20); gets(s.city, 20); gets(s.phone, 20); gets(comm, 100); s.comment = (char *)malloc(sizeof(char[strlen(comm)+1])); strcpy(s.comment, comm);
当只有部分记录的注释字段中实际包含注释时,这种技术非常有用。如果记录没有注释,那么注释字段将只包含一个指针(4字节)。那些有注释的记录则根据用户输入的字符串长度精确分配足够的空间来存储注释字符串。
创建指向指针的指针是可能且通常很有用的。这种技术有时被称为句柄,在某些情况下很有用,例如操作系统希望能够随意移动堆上的内存块。以下示例演示了一个指向指针的指针
int **p; int *q; p = (int **)malloc(sizeof(int *)); *p = (int *)malloc(sizeof(int)); **p = 12; q = *p; printf("%d\n", *q); free(q); free(p);
Windows和Mac OS使用这种结构来允许堆上的内存紧凑。程序管理指针p,而操作系统管理指针*p。由于操作系统管理*p,所以*p指向的内存块(**p)可以被移动,并且*p可以被改变以反映移动而不会影响使用p的程序。指向指针的指针也经常在C语言中用于处理函数中的指针参数。
广告
以下示例使用了上一节中的Addr记录
typedef struct { char name[21]; char city[21]; char phone[21]; char *comment; } Addr; Addr *s; char comm[100]; s = (Addr *)malloc(sizeof(Addr)); gets(s->name, 20); gets(s->city, 20); gets( s->phone, 20); gets(comm, 100); s->comment = (char *)malloc(sizeof(char[strlen(comm)+1])); strcpy(s->comment, comm);
指针s指向一个结构体,该结构体包含一个指向字符串的指针。
在这个例子中,如果你不小心,很容易创建丢失的内存块。例如,这是AP示例的一个不同版本。
s = (Addr *)malloc(sizeof(Addr)); gets(comm, 100); s->comment = (char *)malloc(sizeof(char[strlen(comm)+1])); strcpy(s->comment, comm); free(s);
这段代码创建了一个丢失的内存块,因为包含指向字符串的指针的结构体在字符串块被释放之前就被处理掉了,如图所示。
最后,可以创建能够指向相同结构体的结构体,这种能力可以用于将一整串相同记录链接在一起,形成一种称为链表的数据结构。
typedef struct { char name[21]; char city[21]; char state[21]; Addr *next; } Addr; Addr *first;
编译器会允许你这样做,并且只要有一些经验,就可以创建像左侧所示的结构。
动态数据结构的一个很好例子是一个简单的栈库,它使用动态列表并包含init、clear、push和pop函数。该库的头文件如下所示
/* Stack Library - This library offers the minimal stack operations for a stack of integers (easily changeable) */ typedef int stack_data; extern void stack_init(); /* Initializes this library. Call first before calling anything. */ extern void stack_clear(); /* Clears the stack of all entries. */ extern int stack_empty(); /* Returns 1 if the stack is empty, 0 otherwise. */ extern void stack_push(stack_data d); /* Pushes the value d onto the stack. */ extern stack_data stack_pop(); /* Returns the top element of the stack, and removes that element. Returns garbage if the stack is empty. */
库的代码文件如下
广告
#include "stack.h" #include <stdio.h> /* Stack Library - This library offers the minimal stack operations for a stack of integers */ struct stack_rec { stack_data data; struct stack_rec *next; }; struct stack_rec *top=NULL; void stack_init() /* Initializes this library. Call before calling anything else. */ { top=NULL; } void stack_clear() /* Clears the stack of all entries. */ { stack_data x; while (!stack_empty()) x=stack_pop(); } int stack_empty() /* Returns 1 if the stack is empty, 0 otherwise. */ { if (top==NULL) return(1); else return(0); } void stack_push(stack_data d) /* Pushes the value d onto the stack. */ { struct stack_rec *temp; temp= (struct stack_rec *)malloc(sizeof(struct stack_rec)); temp->data=d; temp->next=top; top=temp; } stack_data stack_pop() /* Returns the top element of the stack, and removes that element. Returns garbage if the stack is empty. */ { struct stack_rec *temp; stack_data d=0; if (top!=NULL) { d=top->data; temp=top; top=top->next; free(temp); } return(d); }
注意这个库是如何实践信息隐藏的:只看到头文件的人无法判断栈是使用数组、指针、文件还是以其他方式实现的。还要注意C语言使用NULL。NULL定义在stdio.h中,所以当您使用指针时,几乎总是需要包含stdio.h。NULL与零相同。
向栈库添加dup、count和add函数,分别用于复制栈顶元素、返回栈中元素的数量以及将栈顶的两个元素相加。
数组和指针在C语言中密切相关。要有效使用数组,你必须知道如何将指针与它们一起使用。完全理解两者之间的关系可能需要几天时间的学习和实验,但这非常值得付出努力。
让我们从C语言中数组的一个简单示例开始
广告
#define MAX 10 int main() { int a[MAX]; int b[MAX]; int i; for(i=0; i<MAX; i++) a[i]=i; b=a; return 0; }
输入这段代码并尝试编译。你会发现C语言不会编译它。如果你想将a复制到b,你必须输入类似下面的内容
for (i=0; i<MAX; i++) b[i]=a[i];
或者,更简洁地说
for (i=0; i<MAX; b[i]=a[i], i++);
更好的是,使用string.h中的memcpy工具。
C语言中的数组不同寻常之处在于,变量a和b在技术上并非数组本身。相反,它们是数组的永久指针。a和b分别永久指向它们各自数组的第一个元素——它们分别持有a[0]和b[0]的地址。由于它们是永久指针,你不能改变它们的地址。因此,语句a=b;不起作用。
因为a和b是指针,你可以用指针和数组做一些有趣的事情。例如,以下代码是可行的
#define MAX 10 void main() { int a[MAX]; int i; int *p; p=a; for(i=0; i<MAX; i++) a[i]=i; printf("%d\n",*p); }
语句p=a;之所以有效,是因为a是一个指针。严格来说,a指向实际数组的第0个元素的地址。这个元素是一个整数,所以a是一个指向单个整数的指针。因此,将p声明为指向整数的指针并将其设置为等于a是可行的。另一种表达同样意思的方法是将p=a;替换为p=&a[0];。由于a包含a[0]的地址,所以a和&a[0]含义相同。
现在p指向a的第0个元素,你可以对它做一些相当奇特的事情。变量a是一个永久指针,不能被改变,但p不受此限制。C语言实际上鼓励你使用指针算术来移动它。例如,如果你输入p++;,编译器知道p指向一个整数,所以这条语句会使p递增适当的字节数,以将其移动到数组的下一个元素。如果p指向一个100字节长的结构体数组,p++;会将p移动100字节。C语言会处理元素大小的细节。
你也可以使用指针将数组a复制到b。以下代码可以替换(for i=0; i<MAX; a[i]=b[i], i++);
p=a; q=b; for (i=0; i<MAX; i++) { *q = *p; q++; p++; }
你可以把这段代码缩写如下
p=a; q=b; for (i=0; i<MAX; i++) *q++ = *p++;
你还可以进一步缩写为
for (p=a,q=b,i=0; i<MAX; *q++ = *p++, i++);
如果你使用指针p或q超出了数组a或b的末尾会怎样?C语言并不在意——它会漫不经心地递增p和q,毫不犹豫地将数据复制到其他变量上。在C语言中对数组进行索引时需要小心,因为C语言假定你清楚自己在做什么。
你可以通过两种不同的方式将数组(如a或b)传递给函数。设想一个函数dump,它接受一个整数数组作为参数,并将数组内容打印到标准输出。编写dump有两种方式
void dump(int a[],int nia) { int i; for (i=0; i<nia; i++) printf("%d\n",a[i]); }
或
void dump(int *p,int nia) { int i; for (i=0; i<nia; i++) printf("%d\n",*p++); }
nia(number_in_array)变量是必需的,以便知道数组的大小。请注意,传递给函数的只是数组的指针,而不是数组的内容。另请注意,C函数可以接受可变大小的数组作为参数。
C语言中的字符串与指针在很大程度上是交织在一起的。你必须熟悉前面文章中涵盖的指针概念,才能有效地使用C字符串。然而,一旦你习惯了它们,你通常可以非常高效地执行字符串操作。
C语言中的字符串本质上是一个字符数组。以下行声明了一个最多可容纳99个字符的字符串数组。
广告
char str[100];
它按你预期的方式存储字符:str[0]是字符串的第一个字符,str[1]是第二个字符,依此类推。但为什么一个100元素的数组不能容纳多达100个字符呢?因为C语言使用空终止字符串,这意味着任何字符串的末尾都由ASCII值0(空字符)标记,在C语言中也表示为'\0'。
空终止符与许多其他语言处理字符串的方式非常不同。例如,在Pascal中,每个字符串都由一个字符数组组成,带有一个长度字节来计数数组中存储的字符数量。这种结构使得Pascal在查询字符串长度时具有明显优势。Pascal可以简单地返回长度字节,而C语言必须计数字符直到找到'\0'。这个事实使得C语言在某些情况下比Pascal慢得多,但在其他情况下又更快,我们将在下面的例子中看到。
由于C语言本身不提供对字符串的显式支持,所有字符串处理函数都在库中实现。字符串I/O操作(gets、puts等)在<stdio.h>中实现,而一套相当简单的字符串操作函数则在<string.h>(在某些系统上是<strings.h>)中实现。
字符串不是C语言原生类型的事实迫使你编写一些相当迂回的代码。例如,假设你想将一个字符串赋值给另一个字符串;也就是说,你想将一个字符串的内容复制到另一个字符串。在C语言中,正如我们在上一篇文章中看到的,你不能简单地将一个数组赋值给另一个。你必须逐个元素地复制它。字符串库(<string.h>或<strings.h>)包含一个名为strcpy的函数来完成此任务。以下是在普通C程序中发现的非常常见的代码片段
char s[100]; strcpy(s, "hello");
这两行代码执行后,下图显示了s的内容
上图显示了带有字符的数组。下图显示了字符的等效ASCII码值,这也是C语言实际思考字符串的方式(作为包含整数值的字节数组)。有关ASCII码的讨论,请参阅位和字节如何工作。
以下代码展示了如何在C语言中使用strcpy
#include <string.h> int main() { char s1[100],s2[100]; strcpy(s1,"hello"); /* copy "hello" into s1 */ strcpy(s2,s1); /* copy s1 into s2 */ return 0; }
在C语言中初始化字符串时,都会使用strcpy。你可以使用字符串库中的strcmp函数来比较两个字符串。它返回一个整数,表示比较结果。零表示两个字符串相等,负值表示s1小于s2,正值表示s1大于s2。
#include <stdio.h> #include <string.h> int main() { char s1[100],s2[100]; gets(s1); gets(s2); if (strcmp(s1,s2)==0) printf("equal\n"); else if (strcmp(s1,s2)<0) printf("s1 less than s2\n"); else printf("s1 greater than s2\n"); return 0; }
字符串库中的其他常用函数包括返回字符串长度的strlen和连接两个字符串的strcat。字符串库还包含许多其他函数,你可以通过阅读man手册来浏览它们。
为了帮助你开始构建字符串函数,并帮助你理解其他程序员的代码(每个人似乎都有自己的一套用于程序特殊目的的字符串函数),我们将看两个例子:strlen和strcpy。下面是一个严格意义上像Pascal风格的strlen版本
int strlen(char s[]) { int x; x=0; while (s[x] != '\0') x=x+1; return(x); }
大多数C程序员都避开这种方法,因为它看起来效率低下。相反,他们通常使用基于指针的方法
int strlen(char *s) { int x=0; while (*s != '\0') { x++; s++; } return(x); }
你可以将这段代码缩写如下
int strlen(char *s) { int x=0; while (*s++) x++; return(x); }
我想真正的C语言专家甚至可以把这段代码写得更短。
当我在一台MicroVAX上使用gcc编译这三段代码,不进行优化,并在一个120字符的字符串上分别运行20,000次时,第一段代码耗时12.3秒,第二段12.3秒,第三段12.9秒。这意味着什么?对我来说,这意味着你应该以最容易理解的方式编写代码。指针通常会产生更快的代码,但上面的strlen代码表明并非总是如此。
我们可以对strcpy进行同样的演变
strcpy(char s1[],char s2[]) { int x; for (x=0; x<=strlen(s2); x++) s1[x]=s2[x]; }
请注意,这里的<=在for循环中很重要,因为代码随后会复制'\0'。务必复制'\0'。如果你省略它,稍后会发生严重的bug,因为字符串没有结束符,因此长度未知。另请注意,这段代码效率非常低,因为每次for循环都会调用strlen。为了解决这个问题,你可以使用以下代码
strcpy(char s1[],char s2[]) { int x,len; len=strlen(s2); for (x=0; x<=len; x++) s1[x]=s2[x]; }
指针版本类似。
strcpy(char *s1,char *s2) { while (*s2 != '\0') { *s1 = *s2; s1++; s2++; } }
你可以进一步压缩这段代码
strcpy(char *s1,char *s2) { while (*s2) *s1++ = *s2++; }
如果你愿意,你甚至可以说while (*s1++ = *s2++);。strcpy的第一个版本复制一个120字符的字符串10,000次需要415秒,第二个版本需要14.5秒,第三个版本9.8秒,第四个版本10.3秒。正如你所看到的,指针在这里提供了显著的性能提升。
字符串库中strcpy函数的原型表明它被设计为返回一个指向字符串的指针
char *strcpy(char *s1,char *s2)
大多数字符串函数都返回一个字符串指针作为结果,strcpy返回s1的值作为其结果。
将指针与字符串结合使用有时可以显著提高速度,如果你稍加思考,就可以利用这些优势。例如,假设你想从字符串中删除开头的空白。你可能会倾向于将字符移到空白上方以删除它们。在C语言中,你可以完全避免这种移动
#include <stdio.h> #include <string.h> int main() { char s[100],*p; gets(s); p=s; while (*p==' ') p++; printf("%s\n",p); return 0; }
这比移动技术快得多,尤其是对于长字符串。
随着你的进步和阅读其他代码,你还会学到许多其他关于字符串的技巧。实践是关键。
假设你创建并运行以下两个代码片段
Fragment 1 { char *s; s="hello"; printf("%s\n",s); } Fragment 2 { char s[100]; strcpy(s,"hello"); printf("%s\n",s); }
这两个代码片段产生相同的输出,但它们的内部行为却截然不同。在片段2中,你不能说s="hello";。要理解这些差异,你必须了解C语言中字符串常量表的工作原理。
广告
当你的程序被编译时,编译器会生成目标代码文件,其中包含你的机器代码以及程序中声明的所有字符串常量的表格。在片段1中,语句s="hello";使s指向字符串常量表中字符串hello的地址。由于这个字符串在字符串常量表中,因此在技术上是可执行代码的一部分,你不能修改它。你只能指向它并以只读方式使用它。
在片段2中,字符串hello也存在于常量表中,因此你可以将其复制到名为s的字符数组中。由于s不是指针,语句s="hello";在片段2中将不起作用。它甚至无法编译。
关于使用malloc处理字符串的特别说明
假设你编写以下程序
int main() { char *s; s=(char *) malloc (100); s="hello"; free(s); return 0; }
它编译正常,但在运行时会在free行给出段错误。malloc行分配了一个100字节长的内存块并使s指向它,但现在s="hello";这一行是一个问题。它语法上是正确的,因为s是一个指针;然而,当s="hello";执行时,s会指向字符串常量表中的字符串,而之前分配的内存块则被孤立了。由于s指向字符串常量表,该字符串不能被修改;free失败,因为它不能释放可执行区域中的内存块。
正确的代码如下
int main() { char *s; s=(char *) malloc (100); strcpy(s,"hello"); free(s); return 0; }
C语言包含许多运算符,并且由于运算符优先级的运作方式,多个运算符之间的交互可能会变得令人困惑。
x=5+3*6;
广告
X接收到的值是23,而不是48,因为在C语言中,乘法和除法的优先级高于加法和减法。
char *a[10];
a是一个指向10个字符数组的单个指针,还是一个包含10个指向字符的指针的数组?除非你了解C语言中的优先级约定,否则无从得知。同样,在E.11中我们看到,由于优先级问题,诸如*p.i = 10;这样的语句是无效的。相反,必须使用(*p).i = 10;的形式来强制正确的优先级。
以下来自Kernighan和Ritchie的《C程序设计语言》的表格展示了C语言中的优先级层次结构。最上面一行具有最高优先级。
Operators Associativity ( [ - . Left to right ! - ++ -{- + * & (type-cast) sizeof Right to left (in the above line, +, - and * are the unary forms) * / % Left to right + - Left to right << >> Left to right < <= > >= Left to right == != Left to right & Left to right ^ Left to right | Left to right && Left to right || Left to right ?: Left to right = += -= *= /= %= &= ^= |= <<= >>= Right to left , Left to right
使用此表,你可以看到char *a[10];是一个包含10个指向字符的指针的数组。你也可以看出为什么(*p).i要正确处理时需要括号。经过一些练习,你将记住这张表的大部分内容,但偶尔仍然会遇到由于微妙的优先级问题导致代码无法正常工作的情况。
C语言提供了一种相当简单的机制来检索用户输入的命令行参数。它将一个argv参数传递给程序中的main函数。argv结构出现在相当多的高级库调用中,因此理解它们对任何C程序员都很有用。
输入并编译以下代码
广告
#include <stdio.h> int main(int argc, char *argv[]) { int x; printf("%d\n",argc); for (x=0; x<argc; x++) printf("%s\n",argv[x]); return 0; }
在这段代码中,主程序接受两个参数:argv和argc。argv参数是一个指向字符串的指针数组,其中包含程序在UNIX命令行被调用时输入的参数。argc整数包含参数的数量。这段特定的代码会打印出命令行参数。要尝试此功能,请将代码编译成名为aaa的可执行文件,然后输入aaa xxx yyy zzz。代码将逐行打印出命令行参数xxx、yyy和zzz。
char *argv[]行是一个指向字符串的指针数组。换句话说,数组的每个元素都是一个指针,每个指针都指向一个字符串(严格来说,是字符串的第一个字符)。因此,argv[0]指向一个包含命令行上第一个参数(程序名称)的字符串,argv[1]指向下一个参数,依此类推。argc变量告诉你数组中有多少个指针是有效的。你会发现前面的代码只是打印出argv指向的每个有效字符串。
因为argv的存在,你可以相当容易地让你的程序响应用户输入的命令行参数。例如,你可以让你的程序检测程序名后的第一个参数是否为help,然后将帮助文件输出到stdout。文件名也可以作为参数传入并在你的fopen语句中使用。
二进制文件与结构体数组非常相似,只是结构体位于磁盘文件中而不是内存数组中。由于二进制文件中的结构体在磁盘上,你可以创建非常大的结构体集合(仅受可用磁盘空间限制)。它们也是永久性的且始终可用。唯一的缺点是磁盘访问时间带来的缓慢。
二进制文件有两个与文本文件不同的特点
广告
二进制文件通常比文本文件具有更快的读写时间,因为记录的二进制图像直接从内存存储到磁盘(反之亦然)。在文本文件中,所有内容都必须在文本之间来回转换,这需要时间。
C语言非常清晰地支持结构体文件概念。一旦打开文件,你可以读取一个结构体,写入一个结构体,或者在文件中定位到任何结构体。这种文件概念支持文件指针的概念。当文件被打开时,指针指向记录0(文件中的第一个记录)。任何读取操作会读取当前指向的结构体并将指针向下移动一个结构体。任何写入操作会写入当前指向的结构体并将指针向下移动一个结构体。Seek将指针移动到请求的记录。
请记住,C语言将磁盘文件中的所有内容都视为从磁盘读取到内存或从内存写入磁盘的字节块。C语言使用文件指针,但它可以指向文件中的任何字节位置。因此,你必须跟踪各项事务。
以下程序演示了这些概念
#include <stdio.h> /* random record description - could be anything */ struct rec { int x,y,z; }; /* writes and then reads 10 arbitrary records from the file "junk". */ int main() { int i,j; FILE *f; struct rec r; /* create the file of 10 records */ f=fopen("junk","w"); if (!f) return 1; for (i=1;i<=10; i++) { r.x=i; fwrite(&r,sizeof(struct rec),1,f); } fclose(f); /* read the 10 records */ f=fopen("junk","r"); if (!f) return 1; for (i=1;i<=10; i++) { fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); } fclose(f); printf("\n"); /* use fseek to read the 10 records in reverse order */ f=fopen("junk","r"); if (!f) return 1; for (i=9; i>=0; i--) { fseek(f,sizeof(struct rec)*i,SEEK_SET); fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); } fclose(f); printf("\n"); /* use fseek to read every other record */ f=fopen("junk","r"); if (!f) return 1; fseek(f,0,SEEK_SET); for (i=0;i<5; i++) { fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); fseek(f,sizeof(struct rec),SEEK_CUR); } fclose(f); printf("\n"); /* use fseek to read 4th record, change it, and write it back */ f=fopen("junk","r+"); if (!f) return 1; fseek(f,sizeof(struct rec)*3,SEEK_SET); fread(&r,sizeof(struct rec),1,f); r.x=100; fseek(f,sizeof(struct rec)*3,SEEK_SET); fwrite(&r,sizeof(struct rec),1,f); fclose(f); printf("\n"); /* read the 10 records to insure 4th record was changed */ f=fopen("junk","r"); if (!f) return 1; for (i=1;i<=10; i++) { fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); } fclose(f); return 0; }
在这个程序中,使用了结构体描述rec,但你可以使用任何你想要的结构体描述。你可以看到fopen和fclose的工作方式与文本文件完全相同。
这里的新函数是fread、fwrite和fseek。fread函数接受四个参数
因此,行fread(&r,sizeof(struct rec),1,f);表示从文件f(从文件指针的当前位置)读取12字节(rec的大小)到内存地址&r。请求一个12字节的块。通过将1改为100,同样可以轻松地从磁盘读取100个块到内存中的数组。
fwrite函数的工作方式相同,但将字节块从内存移动到文件。fseek函数将文件指针移动到文件中的某个字节。通常,你会以sizeof(struct rec)的增量移动指针,以使指针保持在记录边界。在进行查找时,你可以使用三个选项
SEEK_SET将指针从文件开头(从文件中的字节0开始)向下移动x个字节。SEEK_CUR将指针从当前指针位置向下移动x个字节。SEEK_END将指针从文件末尾移动(因此必须使用负偏移量)。
上述代码中出现了几种不同的选项。特别注意以r+模式打开文件的部分。这会以读写模式打开文件,允许更改记录。代码会定位到一条记录,读取它,然后更改一个字段;之后,它会回溯,因为读取操作移动了指针,并将更改写回。
请复制/粘贴以下文本以正确引用此十万个为什么.com文章