指针和地址
地址
地址是内存中一个特定位置的标识符。每个内存位置都有一个唯一的地址,用于存储数据。这些地址通常表示为十六进制数。
- 物理地址:硬件层次上的实际内存地址。
- 逻辑地址:程序运行时使用的地址,由操作系统管理。
例如,在某个特定内存位置存储一个整数值42,那么这个内存位置就有一个特定的地址,比如0x7ffc1234。
指针
指针是一种变量,用于存储另一个变量的地址。指针允许间接访问和操作存储在内存中不同位置的数据。
在C语言中,指针的声明方式是使用星号(*)。例如:
int x = 10; // 定义一个整数变量x
int *p; // 定义一个指向整数的指针p
p = &x; // 将变量x的地址赋给指针p
在这个例子中:
int *p
声明了一个指向整数的指针。&x
获取变量x的地址。p = &x
将x的地址赋给指针p。
现在,p
指向变量x
,可以通过*p
访问x
的值:
printf("%d\n", *p); // 输出10
指针的操作
- 访问值:通过解引用(dereference)指针,可以访问它所指向的变量的值。例如,
*p
获取指针p
指向的变量的值。 - 修改值:通过解引用指针,可以修改它所指向的变量的值。例如,
*p = 20
将修改变量x
的值为20。 - 指针运算:可以对指针进行加减操作,从而访问数组等连续内存块。例如,
p+1
指向下一个内存位置(通常是下一个元素)。
指针的应用
- 数组和字符串:指针用于遍历和操作数组和字符串。
- 动态内存分配:通过
malloc
等函数动态分配内存,并使用指针访问和管理这些内存。 - 函数参数:通过传递指针,可以在函数中修改外部变量的值,实现更高效的数据传递。
void increment(int *p) {
(*p)++;
}
int main() {
int a = 10;
increment(&a); // 传递变量a的地址
printf("%d\n", a); // 输出11
return 0;
}
在这个例子中,increment
函数接受一个指针参数,并通过解引用来修改实际变量的值。
指针和函数参数
int getch(void);
void ungetch(int);
/* getint: get next integer from input into *pn */
int getint(int *pn) {
int c, sign;
// 跳过空白字符
while (isspace(c = getch()))
;
// 检查是否是数字、EOF、'+' 或 '-'
if (!isdigit(c) && c != EOF && c != '+' && c != '-') {
ungetch(c); // 不是数字
return 0;
}
sign = (c == '-') ? -1 : 1;
// 检查正负号后获取下一个字符
if (c == '+' || c == '-')
c = getch();
// 读取数字部分
for (*pn = 0; isdigit(c); c = getch())
*pn = 10 * *pn + (c - '0');
*pn *= sign;
// 如果读取到的字符不是EOF,则将其放回输入流
if (c != EOF)
ungetch(c);
return c;
}
更多例子
*ip = *ip + 10;
y = *ip + 1;
*ip += 1;
++*ip;
(*ip)++;
指针与函数参数
指针可以作为函数参数传递,这样可以在函数内部修改外部变量的值,避免值传递带来的开销。
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y); // 输出:x = 20, y = 10
return 0;
}
举例:从字符串中读取整数
#include <stdio.h>
#include <ctype.h>
// 函数定义:获取整数并存储到指针变量 pn 所指向的位置
int getint(int *pn) {
int c, sign, state;
// 跳过空白字符
while (isspace(c = getchar()))
;
// 如果第一个非空白字符不是数字、不是EOF、不是加号或减号,则返回0
if (!isdigit(c) && c != EOF && c != '+' && c != '-') {
return 0;
}
// 确定符号
sign = (c == '-') ? -1 : 1;
// 如果字符是加号或减号,则读取下一个字符
if (c == '+' || c == '-') {
if (!isdigit(c = getchar()))
return getint(pn); // 递归调用自身,直到找到数字字符为止
}
// 循环读取数字字符,计算整数值
for (*pn = 0; isdigit(c); c = getchar())
*pn = 10 * *pn + (c - '0');
*pn *= sign; // 将符号应用到整数值上
return c; // 返回下一个非数字字符
}
练习
- 编写getfloat(double *pn)。
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int getfloat(double *pn) {
int c, sign;
double power;
power = 1.0; // 初始化幂,用于处理小数部分
while (isspace(c = getchar()))
; // 跳过空白字符
// 检查第一个非空白字符是否是数字、'+' 或 '-'
if (!isdigit(c) && c != EOF && c != '+' && c != '-') {
return 0; // 如果不是数字或符号,则返回 0 表示失败
}
sign = (c == '-') ? -1 : 1; // 确定数字的符号
if (c == '+' || c == '-') {
// 如果是 '+' 或 '-',则读取下一个字符,并检查其是否为数字,如果不是则递归调用 getfloat 函数
if (!isdigit(c = getchar()))
return getfloat(pn);
}
// 处理整数部分
for (*pn = 0.0; isdigit(c); c = getchar())
*pn = 10.0 * *pn + (c - '0'); // 构建整数部分的浮点数值
// 处理小数部分
if (c == '.') {
for (c = getchar(); isdigit(c); c = getchar()) {
*pn = 10.0 * *pn + (c - '0'); // 构建小数部分的浮点数值
power *= 10.0; // 更新幂
}
}
// 应用符号和幂到浮点数值
*pn = *pn * sign / power;
return c; // 返回最后读取的字符(可能是空白字符或 EOF)
}
指针和数组
相同之处
-
访问数组元素的方式:
- 指针可以像数组一样通过下标来访问数组元素。例如,
array[i]
等价于*(array + i)
,其中array
是一个指向数组首元素的指针。
- 指针可以像数组一样通过下标来访问数组元素。例如,
-
数组名作为指针:
- 数组名在表达式中会被转换成一个指向其第一个元素的指针。因此,对于数组
int arr[10]
,arr
相当于指向arr[0]
的指针。
- 数组名在表达式中会被转换成一个指向其第一个元素的指针。因此,对于数组
-
遍历数组:
- 使用指针可以遍历数组。通过递增指针,可以访问数组的每一个元素。
不同之处
-
存储方式:
- 数组:数组是一块连续的内存区域。数组名是该块内存的首地址,且数组的大小在编译时确定,不能更改。
- 指针:指针是一个变量,存储的是内存地址。指针本身可以指向任意位置,并且可以在运行时改变所指向的位置。
-
内存分配:
- 数组:数组的内存是在声明时一次性分配的,例如
int arr[10];
会在栈上分配10个int
类型的空间。 - 指针:指针在声明时并不分配所指向的内存,需要手动分配,例如通过
malloc
或calloc
,如int *ptr = malloc(10 * sizeof(int));
。
- 数组:数组的内存是在声明时一次性分配的,例如
-
大小(Sizeof操作符):
- 数组:
sizeof(array)
返回整个数组的字节大小。例如,对于int arr[10];
,sizeof(arr)
返回40(假设int
占4字节)。 - 指针:
sizeof(pointer)
返回指针变量本身的大小,而不是它指向的内存大小。例如,对于int *ptr;
,sizeof(ptr)
通常返回8(在64位系统上)。
- 数组:
-
指针运算:
- 指针:指针可以进行算术运算,如递增、递减等。例如,
ptr++
会使指针指向下一个元素。 - 数组名:数组名是一个常量指针,不能进行运算。例如,
arr++
是非法的。
- 指针:指针可以进行算术运算,如递增、递减等。例如,
-
函数参数传递:
- 数组:数组作为函数参数传递时,实际上传递的是指向数组首元素的指针。这意味着在函数中无法获取数组的大小。
- 指针:指针作为函数参数传递时,直接传递的是指针变量的值(即地址),可以指向任何数据类型或内存区域。
这个示例展示了如何通过数组和指针分别访问和操作内存,凸显了它们之间的异同。
地址运算
如果 p 是指向某个数组元素的指针,那么 p++ 会将 p 增加,使其指向下一个元素,而 p+=i 会将其增加 i,使其指向当前指向位置之后的第 i 个元素。
举例:内存分配和回收。
#define ALLOCSIZE 10000 /* size of available space */
static char allocbuf[ALLOCSIZE]; /* storage for alloc */
static char *allocp = allocbuf; /* next free position */
char *alloc(int n) /* return pointer to n characters */
{
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
allocp += n;
return allocp - n; /* old p */
} else { /* not enough room */
return 0;
}
}
void afree(char *p) /* free storage pointed to by p */
{
if (p >= allocbuf && p < allocbuf + ALLOCSIZE) {
allocp = p;
}
}
像下面的测试:
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
和
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
展示了指针运算的几个重要方面。首先,在某些情况下指针可以进行比较。如果 p 和 q 指向同一个数组的成员,那么像 ==、!=、<、>= 等关系运算是有效的。例如,如果 p 指向数组中较早的元素而 q 指向较晚的元素,那么 p < q
为真。任何指针都可以与零进行相等或不等的比较。但对于不指向同一个数组成员的指针,进行算术运算或比较是未定义的行为。(有一个例外:可以使用指向数组末尾第一个元素的地址进行指针运算。)
其次,我们已经注意到指针和整数可以进行加减运算。构造 p + n
表示 p 当前指向的对象之后第 n 个对象的地址。无论 p 指向什么类型的对象,这都是正确的;n 的缩放根据 p 所指向对象的大小来确定,这由 p 的声明决定。例如,如果一个 int 是四个字节,那么 n 将被缩放四倍。
指针减法也是有效的:如果 p 和 q 指向同一个数组的元素,并且 p < q,那么 q - p + 1
是从 p 到 q 包括 q 在内的元素数量。这个事实可以用来写另一个版本的 strlen
:
/* strlen: return length of string s */
int strlen(char *s)
{
char *p = s;
while (*p != '\0')
p++;
return p - s;
}
这个函数通过移动指针 p
来计算字符串 s
的长度。p - s
给出了从 s
到 p
(不包括 \0
)的字符数。
字符指针和函数
下面的定义之间有一个重要的区别:
char amessage[] = "now is the time"; /* 一个数组 */
char *pmessage = "now is the time"; /* 一个指针 */
amessage
是一个数组,它的大小正好能够容纳初始化它的字符序列和终止符 '\0'
。数组中的单个字符可以被修改,但 amessage
将始终指向同一个存储空间。另一方面,pmessage
是一个指针,初始化为指向一个字符串常量;这个指针随后可以被修改为指向其他地方,但如果尝试修改字符串内容,其结果是未定义的。
举例:编写函数strcpy(s, t),将字符串拷贝到s
之前的版本
/* strcpy: copy t to s; array subscript version */
void strcpy(char *s, char *t)
{
int i;
i = 0;
while ((s[i] = t[i]) != '\0')
i++;
}
对比之下,这里是一个使用指针版本的 strcpy:
/* strcpy: 将 t 复制到 s; 指针版本 */
void strcpy(char *s, char *t)
{
int i;
i = 0;
while ((*s = *t) != '\0') {
s++;
t++;
}
}
由于参数是按值传递的,strcpy 可以以任何方式使用参数 s 和 t。在这里,它们是方便的初始化指针,沿着数组逐个字符前进,直到 t 终止符 ‘\0’ 被复制到 s 中。
实际上,strcpy 不会像我们上面展示的那样编写。经验丰富的 C 程序员会更喜欢这样写:
/* strcpy: 将 t 复制到 s; 指针版本 2 */
void strcpy(char *s, char *t)
{
while ((*s++ = *t++) != '\0')
;
}
这将 s 和 t 的递增操作移到了循环的测试部分。*t++ 的值是 t 在递增之前指向的字符;后缀 ++ 直到这个字符被获取之后才改变 t。同样,这个字符在 s 递增之前被存储到旧的位置。这个字符也是与 ‘\0’ 比较以控制循环的值。其净效果是字符从 t 复制到 s,包括终止符 ‘\0’。
作为最终的简化,注意与 ‘\0’ 的比较是多余的,因为问题只是表达式是否为零。所以函数很可能会写成这样:
/* strcpy: 将 t 复制到 s; 指针版本 3 */
void strcpy(char *s, char *t)
{
while (*s++ = *t++)
;
}
第二个我们将要讨论的例程是 strcmp(s, t)
,它用于比较字符串 s 和 t。如果 s 在字典序上小于、等于或大于 t,函数分别返回负值、零或正值。返回值是通过减去 s 和 t 在第一个不同位置的字符值得到的。
/* strcmp: 如果 s < t 返回负值,s == t 返回 0,s > t 返回正值 */
int strcmp(char *s, char *t) {
int i;
for (i = 0; s[i] == t[i]; i++)
if (s[i] == '\0')
return 0;
return s[i] - t[i];
}
这是 strcmp
的指针版本:
/* strcmp: 如果 s < t 返回负值,s == t 返回 0,s > t 返回正值 */
int strcmp(char *s, char *t) {
for ( ; *s == *t; s++, t++)
if (*s == '\0')
return 0;
return *s - *t;
}
由于 ++
和 --
可以是前缀或后缀操作符,其他与 *
和 ++
、--
的组合也会出现,尽管不那么常见。例如:
*--p
会在获取 p
指向的字符之前先递减 p
。实际上,下面这对表达式:
*p++ = val; /* 将 val 推入栈 */
val = *--p; /* 将栈顶元素弹出到 val */
练习
练习 5-3. 编写一个用指针实现的函数 strcat(s, t), 将字符串 t 复制到 s 的末尾。
#include <stdio.h>
void strcat(char *s, char *t);
int main() {
char s[100] = "Hello ";
char t[] = "world!";
strcat(s, t);
printf("%s\n", s);
}
void strcat(char *s, char *t) {
while (*s)
s++;
while(*s++ = *t++)
;
}
练习 5-4. 编写函数 strend(s, t),如果字符串 t 出现在字符串 s 的末尾,则返回 1,否则返回 0。
#include <stdio.h>
// 声明函数 strend,参数为两个字符指针
int strend(char *s, char *t);
int main() {
// 初始化字符数组 s 和 t
char s[100] = "Hello";
char t[] = "";
// 调用 strend 函数并输出返回值
printf("%d\n", strend(s, t));
}
// 定义函数 strend,判断字符串 t 是否出现在字符串 s 的末尾
int strend(char *s, char *t) {
int i, j;
i = j = 0;
// 遍历字符串 s,计算其长度
while (*s) {
s++;
i++;
}
// 遍历字符串 t,计算其长度
while (*t) {
t++;
j++;
}
// 如果 t 的长度大于 s 的长度或者 t 为空字符串,返回 0
if (i < j || j == 0)
return 0;
// 从字符串 s 和 t 的末尾开始比较字符
for (i = 0; i < j && *--s == *--t; i++)
;
// 如果比较的字符数等于 t 的长度,则返回 1,否则返回 0
return i == j;
}
指针数组;指向指针的指针
由于指针本身也是变量,所以它们可以像其他变量一样存储在数组中。
举例:将输入的字符串读入到指针数组中
#include <stdio.h>
#include <string.h>
// 分配内存函数,返回一个指向大小为 n 的内存区域的指针
char *alloc(int n);
// 获取一行输入的函数,参数 s 为存储输入的字符串指针,lim 为限制的最大字符数
int getLine(char *s, int lim);
// 输出所有行的函数,参数 lineptrs 是存储行指针的数组,lim 为行数
void writelines(char **lineptrs, int lim);
#define MAXLINES 10 // 最大行数
#define LINELEN 100 // 每行的最大字符数
char *lineptrs[MAXLINES]; // 存储行指针的数组
int main() {
char s[LINELEN], *p; // s 用于存储输入的行,p 用于指向分配的内存
int i, j;
j = i = 0;
// 循环获取每一行输入
while ((i = getLine(s, LINELEN)) > 0) {
// 如果当前行数未超过最大行数且成功分配内存
if (j < MAXLINES && (p = alloc(i + 1)) != NULL) {
strcpy(p, s); // 将输入的行复制到分配的内存
lineptrs[j++] = p; // 将内存指针存储到行指针数组中
}
else
break; // 如果条件不满足,则退出循环
}
writelines(lineptrs, j); // 输出所有行
}
#define MAXLEN 100000 // 最大内存池大小
char allocbuf[MAXLEN]; // 内存池
char *allocp = allocbuf; // 指向内存池的指针
// 分配内存函数的实现
char *alloc(int n) {
// 检查内存池是否有足够的空间
if (allocp + n <= allocbuf + MAXLEN) {
allocp += n; // 移动指针
return allocp - n; // 返回分配的内存起始地址
}
return NULL; // 如果空间不足,返回 NULL
}
// 获取一行输入的函数实现
int getLine(char *s, int lim) {
int i;
// 循环读取字符,直到达到限制或遇到 EOF 或换行符
for (i = 0; i < lim && (*s = getchar()) != EOF && *s != '\n'; s++, i++)
;
// 如果遇到换行符,处理换行符
if (*s == '\n') {
i++;
s++;
}
*s = '\0'; // 添加字符串结束符
return i; // 返回读取的字符数
}
// 输出所有行的函数实现
void writelines(char **lineptrs, int lim) {
int i;
// 循环输出每一行
for (i = 0; i < lim; i++)
printf("%s", lineptrs[i]);
}
多维数组
C 语言提供了矩形的多维数组,尽管在实际中,它们的使用远远少于指针数组。在本节中,我们将展示它们的一些特性。
考虑日期转换的问题,从月份中的某一天转换为一年中的第几天,反之亦然。例如,3月1日是非闰年的第60天,也是闰年的第61天。我们定义两个函数来进行转换:day_of_year
将月和日转换为一年中的第几天,month_day
将一年中的第几天转换为月和日。由于后一个函数需要计算两个值,月和日的参数将是指针:month_day(1988, 60, &m, &d)
将 m 设置为2,d 设置为29(2月29日)。
static char daytab[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
/* day_of_year: 根据月和日设置一年中的第几天 */
int day_of_year(int year, int month, int day)
{
int i, leap;
leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
for (i = 1; i < month; i++)
day += daytab[leap][i];
return day;
}
/* month_day: 根据一年中的第几天设置月和日 */
void month_day(int year, int yearday, int *pmonth, int *pday)
{
int i, leap;
leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
for (i = 1; yearday > daytab[leap][i]; i++)
yearday -= daytab[leap][i];
*pmonth = i;
*pday = yearday;
}
如果要将二维数组传递给函数,函数中的参数声明必须包括列数;行数无关紧要,因为传递的是指向行数组的指针,其中每行是一个包含13个整数的数组。在这种情况下,它是一个指向包含13个整数的数组的指针。因此,如果要将数组 daytab
传递给函数 f
,f
的声明应为:
f(int daytab[2][13]) { ... }
它也可以是:
f(int daytab[][13]) { ... }
因为行数无关紧要,或者可以是:
f(int (*daytab)[13]) { ... }
指针数组的初始化
考虑编写一个函数 month_name(n)
,它返回一个指向包含第 n 个月名称的字符字符串的指针。这是一个使用内部静态数组的理想应用。month_name
包含一个私有的字符字符串数组,并在调用时返回指向正确字符串的指针。本节展示了如何初始化该数组。其语法与之前的初始化类似:
/* month_name: 返回第 n 个月的名称 */
char *month_name(int n)
{
static char *name[] = {
"Illegal month",
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
};
return (n < 1 || n > 12) ? name[0] : name[n];
}
name
的声明是一个字符指针数组,这与排序示例中的 lineptr
相同。初始化器是一个字符字符串列表;每个字符串都被分配到数组中的相应位置。第 i 个字符串的字符被放置在某处,并将指向它们的指针存储在 name[i]
中。由于未指定数组 name
的大小,编译器会计算初始化器的数量并填入正确的数量。
指针与多维数组
刚接触 C 语言的人有时会混淆二维数组和指针数组之间的区别,例如上面示例中的 name
。给定如下定义:
int a[10][20];
int *b[10];
那么 a[3][4]
和 b[3][4]
在语法上都是对单个 int
的合法引用。但是 a
是一个真正的二维数组:分配了 200 个 int
大小的位置,并使用常规的矩形下标计算 20 * row + col
来找到元素 a[row][col]
。然而,对于 b
,定义只分配了 10 个指针并且没有初始化;初始化必须显式地完成,可以是静态的,也可以是通过代码进行的。假设 b
的每个元素确实指向一个二十元素的数组,那么将会分配 200 个 int
,加上十个指针单元。指针数组的重要优势在于数组的行可以有不同的长度。也就是说,b
的每个元素不一定都指向一个二十元素的向量;有些可能指向两个元素,有些可能指向五十个,还有一些可能完全不指向任何元素。
尽管我们以整数为例来讨论,但指针数组最常见的用途是存储长度不同的字符串,就像 month_name
函数中那样。对比指针数组的声明和示意图:
char *name[] = { "Illegal month", "Jan", "Feb", "Mar" };
与二维数组的声明和示意图:
char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" };
练习
- 用指针重写
day_of_year
和month_day
,而不是使用索引。
#include <stdio.h>
static int month_day[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
static int *daytab[] = {month_day[0], month_day[1]};
/* day_of_year: set day of year from month & day */
int day_of_year(int year, int month, int day)
{
int i, leap;
leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
for (i = 1; i < month; i++)
day += *(*(daytab + leap) + i);
return day;
}
/* month_day: set month, day from day of year */
void month_day_func(int year, int yearday, int *pmonth, int *pday)
{
int i, leap;
leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
for (i = 1; yearday > *(*(daytab + leap) + i); i++)
yearday -= *(*(daytab + leap) + i);
*pmonth = i;
*pday = yearday;
}
int main() {
int year = 2023, month = 6, day = 29;
printf("Day of year: %d\n", day_of_year(year, month, day));
int yearday = 180;
int pmonth, pday;
month_day_func(year, yearday, &pmonth, &pday);
printf("Month: %d, Day: %d\n", pmonth, pday);
return 0;
}
命令行参数
在支持 C 语言的环境中,有一种方法可以在程序开始执行时将命令行参数或参数传递给程序。当调用 main
函数时,它会带有两个参数。第一个参数(通常称为 argc
,表示参数计数)是程序调用时的命令行参数的数量;第二个参数(称为 argv
,表示参数向量)是一个指向字符字符串数组的指针,这些字符串包含每个参数。我们通常使用多级指针来操作这些字符字符串。最简单的例子是 echo
程序,它在一行中回显其命令行参数,用空格分隔。即,命令:
echo hello, world
打印输出:
hello, world
按照惯例,argv[0]
是调用程序的名称,因此 argc
至少为 1。如果 argc
为 1,则在程序名称之后没有命令行参数。在上述示例中,argc
为 3,argv[0]
、argv[1]
和 argv[2]
分别是 “echo”、“hello,” 和 “world”。第一个可选参数是 argv[1]
,最后一个是 argv[argc-1]
;此外,标准要求 argv[argc]
是一个空指针。
echo
的第一个版本将 argv
视为字符指针数组:
#include <stdio.h>
/* echo command-line arguments; 1st version */
int main(int argc, char *argv[])
{
int i;
for (i = 1; i < argc; i++)
printf("%s%s", argv[i], (i < argc-1) ? " " : "");
printf("\n");
return 0;
}
由于 argv
是指向指针数组的指针,我们可以操作指针而不是索引数组。下一个变体基于递增 argv
,它是指向指向字符的指针,同时 argc
递减:
#include <stdio.h>
/* echo command-line arguments; 2nd version */
int main(int argc, char *argv[])
{
while (--argc > 0)
printf("%s%s", *++argv, (argc > 1) ? " " : "");
printf("\n");
return 0;
}
由于 argv
是指向参数字符串数组起始位置的指针,递增 1 (++argv
) 会使其指向原始的 argv[1]
而不是 argv[0]
。每次递增都会使其指向下一个参数;*argv
然后是指向该参数的指针。同时,argc
递减;当其变为零时,没有参数需要打印。或者,我们可以将 printf
语句写为:
printf((argc > 1) ? "%s " : "%s", *++argv);
这表明 printf
的格式参数也可以是一个表达式。作为第二个例子,让我们对第 4.1 节中的模式查找程序进行一些增强。如果你还记得,我们将搜索模式深深地嵌入到程序中,这是显然不令人满意的安排。借鉴 UNIX 程序 grep
,让我们增强程序,以便匹配的模式由命令行上的第一个参数指定。
#include <stdio.h>
#include <string.h>
#define MAXLINE 1000
int getline(char *line, int max);
/* find: print lines that match pattern from 1st arg */
int main(int argc, char *argv[])
{
char line[MAXLINE];
int found = 0;
if (argc != 2)
printf("Usage: find pattern\n");
else
while (getline(line, MAXLINE) > 0)
if (strstr(line, argv[1]) != NULL) {
printf("%s", line);
found++;
}
return found;
}
标准库函数 strstr(s, t)
返回指向字符串 s
中第一次出现的字符串 t
的指针,如果没有则返回 NULL。它在 <string.h>
中声明。现在可以扩展这个模型以进一步说明指针构造。假设我们想允许两个可选参数。一个表示“打印所有与模式不匹配的行”;第二个表示“在每行前面加上其行号”。在 UNIX 系统上的 C 程序中,常见的约定是以减号开头的参数引入可选标志或参数。如果我们选择 -x
(表示“排除”)来表示反转,-n
(“数字”)来请求行号,那么命令:
find -x -n pattern
将打印不匹配模式的每一行,前面加上其行号。可选参数应以任何顺序允许,并且程序的其余部分应独立于我们提供的参数数量。此外,如果选项参数可以组合在一起,用户会觉得方便,例如:
find -nx pattern
这是程序:
#include <stdio.h>
#include <string.h>
#define MAXLINE 1000
int getline(char *line, int max);
/* find: print lines that match pattern from 1st arg */
int main(int argc, char *argv[])
{
char line[MAXLINE];
long lineno = 0;
int c, except = 0, number = 0, found = 0;
while (--argc > 0 && (*++argv)[0] == '-')
while (c = *++argv[0])
switch (c) {
case 'x':
except = 1;
break;
case 'n':
number = 1;
break;
default:
printf("find: illegal option %c\n", c);
argc = 0;
found = -1;
break;
}
if (argc != 1)
printf("Usage: find -x -n pattern\n");
else
while (getline(line, MAXLINE) > 0) {
lineno++;
if ((strstr(line, *argv) != NULL) != except) {
if (number)
printf("%ld:", lineno);
printf("%s", line);
found++;
}
}
return found;
}
argc
在每个可选参数之前递减,argv
递增。在循环结束时,如果没有错误,argc
表示剩余未处理的参数数量,argv
指向第一个这些参数。因此,argc
应该为 1,*argv
应该指向模式。注意,*++argv
是指向参数字符串的指针,因此 (*++argv)[0]
是其第一个字符。(另一种有效的形式是 **++argv
。)由于 []
比 *
和 ++
绑定得更紧,因此需要括号;否则表达式将被解释为 *++(argv[0])
。事实上,这就是我们在内部循环中使用的,在那里任务是遍历特定的参数字符串。在内部循环中,表达式 *++argv[0]
递增指针 argv[0]
!很少使用比这些更复杂的指针表达式;在这种情况下,将它们分成两到三个步骤会更直观。
练习
- 编写程序
expr
,从命令行求值逆波兰表达式,其中每个操作符或操作数是一个单独的参数。例如:
expr 2 3 4 + *
计算 2 * (3+4)
。
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define NUMBER 0 // 表示数字的常量
#define ERROR 1 // 表示错误的常量
// 函数声明
int read(char *s);
int isNumber(char *s);
double atoF(char *s);
void push(double n);
double pop();
int main(int argc, char *argv[]) {
double op2; // 存储操作数的变量
int n = 6; // 参数数量
// 处理命令行参数
while (--n > 0) {
switch (read(*++argv)) {
case NUMBER:
push(atoF(*argv)); // 将数字转换为浮点数并压入栈
break;
case '+':
push(pop() + pop()); // 执行加法操作
break;
case '-':
op2 = pop();
push(pop() - op2); // 执行减法操作
break;
case '*':
push(pop() * pop()); // 执行乘法操作
break;
case '/':
op2 = pop();
push(pop() / op2); // 执行除法操作
break;
default:
printf("error: unsupported operation %s\n", *argv); // 输出错误信息
exit(1);
}
}
printf("%.2f\n", pop()); // 输出计算结果
return 0;
}
// 读取参数并确定其类型
int read(char *s) {
if (isdigit(*s))
return isNumber(s); // 检查是否为数字
else {
if (strlen(s) == 1)
return *s; // 返回单个字符
else {
if (*s == '+' || *s == '-')
return isNumber(s); // 检查带符号的数字
else
return ERROR; // 返回错误
}
}
}
// 检查字符串是否为数字
int isNumber(char *s) {
if (*s == '+' || *s == '-')
s++;
while (isdigit(*s))
s++;
if (*s == '.') {
s++;
while (isdigit(*s))
s++;
}
if (*s == '\0')
return NUMBER; // 是有效数字
else
return ERROR; // 不是有效数字
}
// 将字符串转换为浮点数
double atoF(char *s) {
int sign;
double power, n;
power = 1.0;
sign = 1;
if (*s == '+' || *s == '-') {
sign = (*s == '-') ? -1 : 1;
s++;
}
for (n = 0.0; isdigit(*s); s++)
n = n * 10 + (*s - '0');
if (*s == '.')
for (s++; isdigit(*s); s++, power *= 10.0)
n = n * 10 + (*s - '0');
return n * sign / power; // 返回转换后的浮点数
}
#define MAXLEN 100 // 栈的最大长度
double stack[MAXLEN];
double *pstack = stack; // 栈指针
// 将数字压入栈
void push(double n) {
if (++pstack < stack + MAXLEN)
*pstack = n;
else {
printf("error: stack overflow\n");
exit(1);
}
}
// 从栈中弹出数字
double pop() {
if (pstack >= stack)
return *pstack--;
else {
printf("error: stack is empty\n");
exit(1);
}
}
执行
MacBook-Air 5.10 % ./expr 1 2 -4.2 + "*"
-2.20
- 编写程序
tail
,打印输入的最后 n 行。默认情况下,n 设为 10,但可以通过可选参数更改为:
tail -n
打印最后 n 行。无论输入或 n 的值多么不合理,程序都应表现合理。编写程序,使其最佳地利用可用存储;行应如第 5.6 节的排序程序中那样存储,而不是固定大小的二维数组。
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
// 分配内存函数,返回一个指向大小为 n 的内存区域的指针
char *alloc(int n);
// 获取一行输入的函数,参数 s 为存储输入的字符串指针,lim 为限制的最大字符数
int getLine(char *s, int lim);
// 输出所有行的函数,参数 lineptrs 是存储行指针的数组,n 为要输出的行数,lim 为总行数
void writelines(char **lineptrs, int n, int lim);
#define MAXLINES 10 // 最大行数
#define LINELEN 100 // 每行的最大字符数
char *lineptrs[MAXLINES]; // 存储行指针的数组
int main(int argc, char *argv[]) {
char s[LINELEN], *p; // s 用于存储输入的行,p 用于指向分配的内存
int i, j, n, c;
if (argc == 1)
n = 10; // 默认输出最后 10 行
else {
while (--argc > 0 && (*++argv)[0] == '-') {
while (c = *++argv[0])
switch (c) {
case 'n':
if (argc > 1)
n = atoi(*(argv + 1));
else {
printf("Usage: tail -n integer\n");
exit(1);
}
break;
default:
printf("error: unknown parameter -%c\n", c);
exit(1);
}
}
}
j = i = 0;
// 循环获取每一行输入
while ((i = getLine(s, LINELEN)) > 0) {
// 如果当前行数未超过最大行数且成功分配内存
if (j < MAXLINES && (p = alloc(i + 1)) != NULL) {
strcpy(p, s); // 将输入的行复制到分配的内存
lineptrs[j++] = p; // 将内存指针存储到行指针数组中
}
else
break; // 如果条件不满足,则退出循环
}
writelines(lineptrs, n, j); // 输出最后 n 行
}
#define MAXLEN 100000 // 最大内存池大小
char allocbuf[MAXLEN]; // 内存池
char *allocp = allocbuf; // 指向内存池的指针
// 分配内存函数的实现
char *alloc(int n) {
// 检查内存池是否有足够的空间
if (allocp + n <= allocbuf + MAXLEN) {
allocp += n; // 移动指针
return allocp - n; // 返回分配的内存起始地址
}
return NULL; // 如果空间不足,返回 NULL
}
// 获取一行输入的函数实现
int getLine(char *s, int lim) {
int i;
// 循环读取字符,直到达到限制或遇到 EOF 或换行符
for (i = 0; i < lim && (*s = getchar()) != EOF && *s != '\n'; s++, i++)
;
// 如果遇到换行符,处理换行符
if (*s == '\n') {
i++;
s++;
}
*s = '\0'; // 添加字符串结束符
return i; // 返回读取的字符数
}
// 输出所有行的函数实现
void writelines(char **lineptrs, int n, int lim) {
char **end, **start;
if (n > lim)
start = lineptrs; // 如果要输出的行数大于总行数,则从第一行开始
else
start = lineptrs + lim - n; // 否则从倒数第 n 行开始
// 循环输出每一行
for (end = lineptrs + lim; start < end; start++)
printf("%s", *start);
}
指向函数的指针
在C语言中,函数指针是一种指向函数的指针。它允许程序在运行时动态调用函数,这使得编写更灵活和可重用的代码成为可能。函数指针的声明、赋值和调用与普通指针类似,但需要注意一些特定的语法。
函数指针的声明
函数指针的声明语法如下:
return_type (*pointer_name)(parameter_types);
例如,声明一个指向返回类型为 int
,参数类型为 int
和 char
的函数指针:
int (*func_ptr)(int, char);
函数指针的赋值
可以将函数的地址赋值给函数指针。例如,假设有一个函数 add
,其原型如下:
int add(int a, char b);
我们可以将函数 add
的地址赋给函数指针 func_ptr
:
func_ptr = add;
使用函数指针调用函数
使用函数指针调用函数与直接调用函数类似,但需要使用指针名和参数列表。例如:
int result = func_ptr(10, 'a');
示例代码
下面是一个完整的示例,展示了如何声明、赋值和调用函数指针:
#include <stdio.h>
// 一个简单的函数,返回两个整数的和
int add(int a, int b) {
return a + b;
}
// 一个简单的函数,返回两个整数的差
int subtract(int a, int b) {
return a - b;
}
// 函数指针示例
int main() {
// 声明一个函数指针
int (*operation)(int, int);
// 将函数指针指向函数 add
operation = add;
printf("Addition of 3 and 4: %d\n", operation(3, 4));
// 将函数指针指向函数 subtract
operation = subtract;
printf("Subtraction of 7 and 2: %d\n", operation(7, 2));
return 0;
}
函数指针数组
函数指针数组是一组指向不同函数的指针。这在需要根据某种条件选择和调用不同的函数时特别有用。例如:
#include <stdio.h>
// 定义三个简单的数学运算函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int main() {
// 声明并初始化函数指针数组
int (*operations[3])(int, int) = {add, subtract, multiply};
// 使用函数指针数组调用不同的函数
printf("Addition of 5 and 2: %d\n", operations[0](5, 2));
printf("Subtraction of 5 and 2: %d\n", operations[1](5, 2));
printf("Multiplication of 5 and 2: %d\n", operations[2](5, 2));
return 0;
}
高阶函数
在C语言中,函数指针还可以用作函数参数。这使得我们可以编写更通用的代码。例如:
#include <stdio.h>
// 定义两个简单的函数
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// 定义一个高阶函数,接受一个函数指针作为参数
int compute(int (*operation)(int, int), int x, int y) {
return operation(x, y);
}
int main() {
int result;
// 调用高阶函数 compute,传递不同的函数指针
result = compute(add, 10, 5);
printf("Addition: %d\n", result);
result = compute(multiply, 10, 5);
printf("Multiplication: %d\n", result);
return 0;
}
举例:字符串排序
排序既可以依照数字,也可以依照字符,因此可以穿入不同函数进行不同排序。
#include <stdio.h>
#include <string.h>
#define MAXLINES 5000 /* 最大的排序行数 */
char *lineptr[MAXLINES]; /* 指向文本行的指针数组 */
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *));
int numcmp(char *, char *);
/* 排序输入行 */
int main(int argc, char *argv[]) {
int nlines; /* 读取的输入行数 */
int numeric = 0; /* 如果是数字排序则为1 */
if (argc > 1 && strcmp(argv[1], "-n") == 0)
numeric = 1;
if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
qsort((void**) lineptr, 0, nlines-1,
(int (*)(void*,void*))(numeric ? numcmp : strcmp));
writelines(lineptr, nlines);
return 0;
} else {
printf("输入太多,无法排序\n");
return 1;
}
}
在调用 qsort
时,strcmp
和 numcmp
是函数的地址。因为它们被认为是函数,所以不需要使用 &
,就像在数组名称前不需要 &
一样。我们编写的 qsort
可以处理任何数据类型,而不仅仅是字符字符串。根据函数原型,qsort
需要一个指针数组、两个整数和一个带有两个指针参数的函数。泛型指针类型 void *
用于指针参数。任何指针都可以转换为 void *
并且再转换回来而不丢失信息,因此我们可以通过将参数转换为 void *
来调用 qsort
。对函数参数的详细转换确保了比较函数的参数类型匹配。虽然这些转换通常对实际表示没有影响,但它们确保了编译器的一致性。
/* qsort: 将 v[left] 到 v[right] 排序为递增顺序 */
void qsort(void *v[], int left, int right, int (*comp)(void *, void *)) {
int i, last;
void swap(void *v[], int, int);
if (left >= right) /* 如果数组包含少于两个元素,则不进行任何操作 */
return;
swap(v, left, (left + right)/2);
last = left;
for (i = left + 1; i <= right; i++)
if ((*comp)(v[i], v[left]) < 0)
swap(v, ++last, i);
swap(v, left, last);
qsort(v, left, last - 1, comp);
qsort(v, last + 1, right, comp);
}
这些声明需要仔细研究。qsort
的第四个参数是
int (*comp)(void *, void *)
这表示 comp
是一个指向函数的指针,该函数有两个 void *
参数并返回一个 int
。在使用 comp
的代码行中
if ((*comp)(v[i], v[left]) < 0)
这一用法与声明一致:comp
是一个指向函数的指针,*comp
是函数,而 (*comp)(v[i], v[left])
则是对它的调用。需要使用括号以确保组件正确关联;如果没有括号,
int *comp(void *, void *) /* 错误的 */
则表示 comp
是一个返回 int *
的函数,这是完全不同的。
我们已经展示了 strcmp
,它用于比较两个字符串。这里是 numcmp
,它通过调用 atof
计算的前导数值来比较两个字符串:
#include <stdlib.h>
/* numcmp: 数字比较 s1 和 s2 */
int numcmp(char *s1, char *s2) {
double v1, v2;
v1 = atof(s1);
v2 = atof(s2);
if (v1 < v2)
return -1;
else if (v1 > v2)
return 1;
else
return 0;
}
交换两个指针的 swap
函数与本章前面介绍的相同,唯一的不同是声明更改为 void *
。
void swap(void *v[], int i, int j) {
void *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
完整程序
#include <stdio.h>
#include <string.h>
#define MAXLINES 5000 /* 最大的排序行数 */
char *lineptr[MAXLINES]; /* 指向文本行的指针数组 */
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qSort(void *lineptr[], int left, int right, int (*comp)(void *, void *));
int numcmp(char *, char *);
/* 排序输入行 */
int main(int argc, char *argv[]) {
int nlines; /* 读取的输入行数 */
int numeric = 0; /* 如果是数字排序则为1 */
if (argc > 1 && strcmp(argv[1], "-n") == 0)
numeric = 1;
if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
qSort((void**) lineptr, 0, nlines-1,
(int (*)(void*,void*))(numeric ? numcmp : strcmp));
writelines(lineptr, nlines);
return 0;
} else {
printf("输入太多,无法排序\n");
return 1;
}
}
/* qSort: 将 v[left] 到 v[right] 排序为递增顺序 */
void qSort(void *v[], int left, int right, int (*comp)(void *, void *)) {
int i, last;
void swap(void *v[], int, int);
if (left >= right) /* 如果数组包含少于两个元素,则不进行任何操作 */
return;
swap(v, left, (left + right)/2);
last = left;
for (i = left + 1; i <= right; i++)
if ((*comp)(v[i], v[left]) < 0)
swap(v, ++last, i);
swap(v, left, last);
qSort(v, left, last - 1, comp);
qSort(v, last + 1, right, comp);
}
#include <stdlib.h>
/* numcmp: 数字比较 s1 和 s2 */
int numcmp(char *s1, char *s2) {
double v1, v2;
v1 = atof(s1);
v2 = atof(s2);
if (v1 < v2)
return -1;
else if (v1 > v2)
return 1;
else
return 0;
}
void swap(void *v[], int i, int j) {
void *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
#define ALLOCSIZE 10000 /* 可用空间的大小 */
static char allocbuf[ALLOCSIZE]; /* 用于 alloc 的存储空间 */
static char *allocp = allocbuf; /* 下一个空闲位置 */
/* alloc: 返回指向 n 个字符的指针 */
char *alloc(int n) {
if (allocbuf + ALLOCSIZE - allocp >= n) { /* 足够的空间 */
allocp += n;
return allocp - n; /* 返回旧的指针位置 */
} else { /* 没有足够的空间 */
return 0;
}
}
int getLine(char *s, int lim) {
int i;
// 循环读取字符,直到达到限制或遇到 EOF 或换行符
for (i = 0; i < lim && (*s = getchar()) != EOF && *s != '\n'; s++, i++)
;
// 如果遇到换行符,处理换行符
if (*s == '\n') {
i++;
s++;
}
*s = '\0'; // 添加字符串结束符
return i; // 返回读取的字符数
}
#define MAXLEN 1000 /* 任意输入行的最大长度 */
int getLine(char *, int);
char *alloc(int);
/* readlines: 读取输入行 */
int readlines(char *lineptr[], int maxlines) {
int len, nlines;
char *p, line[MAXLEN];
nlines = 0;
while ((len = getLine(line, MAXLEN)) > 0)
if (nlines >= maxlines || (p = alloc(len)) == NULL)
return -1;
else {
line[len-1] = '\0'; /* 删除换行符 */
strcpy(p, line);
lineptr[nlines++] = p;
}
return nlines;
}
/* writelines: 写输出行 */
void writelines(char *lineptr[], int nlines) {
int i;
for (i = 0; i < nlines; i++)
printf("%s\n", lineptr[i]);
}
可以为排序程序添加各种其他选项;其中一些选项是具有挑战性的练习。
练习
- 修改排序程序以处理
-r
标志,该标志表示按逆序(递减)排序。确保-r
可以与-n
一起使用。 - 添加选项
-f
,使大小写字母不区分,在排序时将大写和小写字母视为相同;例如,a
和A
比较时相等。 - 添加
-d
(“目录顺序”)选项,使比较仅基于字母、数字和空格。确保它可以与-f
一起使用。 - 添加字段搜索功能,使得可以根据行内的字段进行排序,每个字段可以根据一组独立的选项进行排序。(本书的索引是使用
-df
对索引类别排序,使用-n
对页码排序。)
复杂声明
在C语言中,复杂声明(Complicated Declarations)可以包括多级指针、数组的指针、函数指针等。这些声明在初学者看来可能会比较难以理解,但通过分解每个部分并逐步分析,可以更好地掌握它们的用法。
声明阅读顺序
理解复杂声明的一个关键是按照C语言声明的阅读顺序来解析它们。一般来说,阅读顺序是从变量名开始,然后依次读取修饰符和类型。
基本例子
以下是一些常见的复杂声明及其解释:
-
指针声明
int *p;
p
是一个指向int
类型的指针。 -
指向指针的指针
int **pp;
pp
是一个指向int
类型指针的指针。 -
指向数组的指针
int (*pa)[10];
pa
是一个指向具有10个int
类型元素数组的指针。 -
数组的指针
int *ap[10];
ap
是一个数组,数组中有10个元素,每个元素是一个指向int
类型的指针。 -
函数指针
int (*fp)(int, int);
fp
是一个指针,指向一个返回类型为int
,参数类型为int, int
的函数。 -
返回指针的函数
int *func(int, int);
func
是一个函数,它有两个int
类型的参数,返回一个指向int
类型的指针。
复杂声明的例子
以下是一些更复杂的声明,包含多层指针、数组和函数指针的组合:
-
指向函数指针的数组
int (*arr[10])(int, int);
arr
是一个数组,数组中有10个元素,每个元素是一个指针,指向一个返回类型为int
,参数类型为int, int
的函数。 -
返回指向数组的指针的函数
int (*func(void))[10];
func
是一个函数,它不接受任何参数,返回一个指向具有10个int
类型元素数组的指针。 -
返回函数指针的指针
int (**func(int))(int, int);
func
是一个函数,它接受一个int
类型的参数,返回一个指向函数指针的指针,这个函数指针指向一个返回类型为int
,参数类型为int, int
的函数。
解析复杂声明
为了更好地理解复杂声明,可以使用如下步骤:
- 从变量名开始:找到变量名,然后向外阅读。
- 依次解析修饰符:解析变量名旁边的修饰符,如
*
(指针)、[]
(数组)、()
(函数)。 - 结合优先级:结合修饰符的优先级来确定整个声明的含义。
实践工具
为了帮助解析复杂声明,可以使用一些在线工具或命令行工具,如:
-
cdecl:一个命令行工具,可以将C语言声明转换为可读的英文解释。
$ cdecl Type `help' or `?' for help cdecl> explain int (*arr[10])(int, int) declare arr as array 10 of pointer to function (int, int) returning int
通过不断实践和使用这些工具,可以逐步掌握复杂声明的解析方法。