C语言入门-指针和数组5

指针和地址

地址

地址是内存中一个特定位置的标识符。每个内存位置都有一个唯一的地址,用于存储数据。这些地址通常表示为十六进制数。

  • 物理地址:硬件层次上的实际内存地址。
  • 逻辑地址:程序运行时使用的地址,由操作系统管理。

例如,在某个特定内存位置存储一个整数值42,那么这个内存位置就有一个特定的地址,比如0x7ffc1234。

指针

指针是一种变量,用于存储另一个变量的地址。指针允许间接访问和操作存储在内存中不同位置的数据。

在C语言中,指针的声明方式是使用星号(*)。例如:

c 复制代码
int x = 10;    // 定义一个整数变量x
int *p;        // 定义一个指向整数的指针p
p = &x;        // 将变量x的地址赋给指针p

在这个例子中:

  • int *p 声明了一个指向整数的指针。
  • &x 获取变量x的地址。
  • p = &x 将x的地址赋给指针p。

现在,p指向变量x,可以通过*p访问x的值:

c 复制代码
printf("%d\n", *p); // 输出10
指针的操作
  1. 访问值 :通过解引用(dereference)指针,可以访问它所指向的变量的值。例如,*p获取指针p指向的变量的值。
  2. 修改值 :通过解引用指针,可以修改它所指向的变量的值。例如,*p = 20将修改变量x的值为20。
  3. 指针运算 :可以对指针进行加减操作,从而访问数组等连续内存块。例如,p+1指向下一个内存位置(通常是下一个元素)。
指针的应用
  1. 数组和字符串:指针用于遍历和操作数组和字符串。
  2. 动态内存分配 :通过malloc等函数动态分配内存,并使用指针访问和管理这些内存。
  3. 函数参数:通过传递指针,可以在函数中修改外部变量的值,实现更高效的数据传递。
c 复制代码
void increment(int *p) {
    (*p)++;
}

int main() {
    int a = 10;
    increment(&a);  // 传递变量a的地址
    printf("%d\n", a);  // 输出11
    return 0;
}

在这个例子中,increment函数接受一个指针参数,并通过解引用来修改实际变量的值。

指针和函数参数

c 复制代码
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;    
}
更多例子
c 复制代码
*ip = *ip + 10;
y = *ip + 1;
*ip += 1;
++*ip;
(*ip)++;

指针与函数参数

指针可以作为函数参数传递,这样可以在函数内部修改外部变量的值,避免值传递带来的开销。

c 复制代码
#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;
}
举例:从字符串中读取整数
c 复制代码
#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;     // 返回下一个非数字字符
}

练习

  1. 编写getfloat(double *pn)。
c 复制代码
#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)
}

指针和数组

相同之处

  1. 访问数组元素的方式

    • 指针可以像数组一样通过下标来访问数组元素。例如,array[i] 等价于 *(array + i),其中 array 是一个指向数组首元素的指针。
  2. 数组名作为指针

    • 数组名在表达式中会被转换成一个指向其第一个元素的指针。因此,对于数组 int arr[10]arr 相当于指向 arr[0] 的指针。
  3. 遍历数组

    • 使用指针可以遍历数组。通过递增指针,可以访问数组的每一个元素。

不同之处

  1. 存储方式

    • 数组:数组是一块连续的内存区域。数组名是该块内存的首地址,且数组的大小在编译时确定,不能更改。
    • 指针:指针是一个变量,存储的是内存地址。指针本身可以指向任意位置,并且可以在运行时改变所指向的位置。
  2. 内存分配

    • 数组 :数组的内存是在声明时一次性分配的,例如 int arr[10]; 会在栈上分配10个 int 类型的空间。
    • 指针 :指针在声明时并不分配所指向的内存,需要手动分配,例如通过 malloccalloc,如 int *ptr = malloc(10 * sizeof(int));
  3. 大小(Sizeof操作符)

    • 数组sizeof(array) 返回整个数组的字节大小。例如,对于 int arr[10];sizeof(arr) 返回40(假设 int 占4字节)。
    • 指针sizeof(pointer) 返回指针变量本身的大小,而不是它指向的内存大小。例如,对于 int *ptr;sizeof(ptr) 通常返回8(在64位系统上)。
  4. 指针运算

    • 指针 :指针可以进行算术运算,如递增、递减等。例如,ptr++ 会使指针指向下一个元素。
    • 数组名 :数组名是一个常量指针,不能进行运算。例如,arr++ 是非法的。
  5. 函数参数传递

    • 数组:数组作为函数参数传递时,实际上传递的是指向数组首元素的指针。这意味着在函数中无法获取数组的大小。
    • 指针:指针作为函数参数传递时,直接传递的是指针变量的值(即地址),可以指向任何数据类型或内存区域。

这个示例展示了如何通过数组和指针分别访问和操作内存,凸显了它们之间的异同。

地址运算

如果 p 是指向某个数组元素的指针,那么 p++ 会将 p 增加,使其指向下一个元素,而 p+=i 会将其增加 i,使其指向当前指向位置之后的第 i 个元素。

举例:内存分配和回收。
c 复制代码
#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;
    }
}

像下面的测试:

c 复制代码
if (allocbuf + ALLOCSIZE - allocp >= n) {  /* it fits */

c 复制代码
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

c 复制代码
/* strlen: return length of string s */
int strlen(char *s)
{
    char *p = s;
    while (*p != '\0')
        p++;
    return p - s;
}

这个函数通过移动指针 p 来计算字符串 s 的长度。p - s 给出了从 sp(不包括 \0)的字符数。

字符指针和函数

下面的定义之间有一个重要的区别:

c 复制代码
char amessage[] = "now is the time"; /* 一个数组 */
char *pmessage = "now is the time";  /* 一个指针 */

amessage 是一个数组,它的大小正好能够容纳初始化它的字符序列和终止符 '\0'。数组中的单个字符可以被修改,但 amessage 将始终指向同一个存储空间。另一方面,pmessage 是一个指针,初始化为指向一个字符串常量;这个指针随后可以被修改为指向其他地方,但如果尝试修改字符串内容,其结果是未定义的。

举例:编写函数strcpy(s, t),将字符串拷贝到s

之前的版本

c 复制代码
/* 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:

c 复制代码
/* 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 程序员会更喜欢这样写:

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' 的比较是多余的,因为问题只是表达式是否为零。所以函数很可能会写成这样:

c 复制代码
/* strcpy:  将 t 复制到 s; 指针版本 3 */
void strcpy(char *s, char *t)
{
    while (*s++ = *t++)
        ;
}

第二个我们将要讨论的例程是 strcmp(s, t),它用于比较字符串 s 和 t。如果 s 在字典序上小于、等于或大于 t,函数分别返回负值、零或正值。返回值是通过减去 s 和 t 在第一个不同位置的字符值得到的。

c 复制代码
/* 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 的指针版本:

c 复制代码
/* 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;
}

由于 ++-- 可以是前缀或后缀操作符,其他与 *++-- 的组合也会出现,尽管不那么常见。例如:

c 复制代码
*--p

会在获取 p 指向的字符之前先递减 p。实际上,下面这对表达式:

c 复制代码
*p++ = val;  /* 将 val 推入栈 */
val = *--p;  /* 将栈顶元素弹出到 val */

练习

练习 5-3. 编写一个用指针实现的函数 strcat(s, t), 将字符串 t 复制到 s 的末尾。

c 复制代码
#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。

c 复制代码
#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;
}

指针数组;指向指针的指针

由于指针本身也是变量,所以它们可以像其他变量一样存储在数组中。

举例:将输入的字符串读入到指针数组中
c 复制代码
#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日)。

c 复制代码
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 传递给函数 ff 的声明应为:

c 复制代码
f(int daytab[2][13]) { ... }

它也可以是:

c 复制代码
f(int daytab[][13]) { ... }

因为行数无关紧要,或者可以是:

c 复制代码
f(int (*daytab)[13]) { ... }

指针数组的初始化

考虑编写一个函数 month_name(n),它返回一个指向包含第 n 个月名称的字符字符串的指针。这是一个使用内部静态数组的理想应用。month_name 包含一个私有的字符字符串数组,并在调用时返回指向正确字符串的指针。本节展示了如何初始化该数组。其语法与之前的初始化类似:

c 复制代码
/* 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。给定如下定义:

c 复制代码
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 函数中那样。对比指针数组的声明和示意图:

c 复制代码
char *name[] = { "Illegal month", "Jan", "Feb", "Mar" };

与二维数组的声明和示意图:

c 复制代码
char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" };

练习

  1. 用指针重写 day_of_yearmonth_day ,而不是使用索引。
c 复制代码
#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 视为字符指针数组:

c 复制代码
#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 递减:

c 复制代码
#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 语句写为:

c 复制代码
printf((argc > 1) ? "%s " : "%s", *++argv);

这表明 printf 的格式参数也可以是一个表达式。作为第二个例子,让我们对第 4.1 节中的模式查找程序进行一些增强。如果你还记得,我们将搜索模式深深地嵌入到程序中,这是显然不令人满意的安排。借鉴 UNIX 程序 grep,让我们增强程序,以便匹配的模式由命令行上的第一个参数指定。

c 复制代码
#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

这是程序:

c 复制代码
#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]!很少使用比这些更复杂的指针表达式;在这种情况下,将它们分成两到三个步骤会更直观。

练习

  1. 编写程序 expr,从命令行求值逆波兰表达式,其中每个操作符或操作数是一个单独的参数。例如:

    expr 2 3 4 + *

计算 2 * (3+4)

c 复制代码
#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
  1. 编写程序 tail,打印输入的最后 n 行。默认情况下,n 设为 10,但可以通过可选参数更改为:

    tail -n

打印最后 n 行。无论输入或 n 的值多么不合理,程序都应表现合理。编写程序,使其最佳地利用可用存储;行应如第 5.6 节的排序程序中那样存储,而不是固定大小的二维数组。

c 复制代码
#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语言中,函数指针是一种指向函数的指针。它允许程序在运行时动态调用函数,这使得编写更灵活和可重用的代码成为可能。函数指针的声明、赋值和调用与普通指针类似,但需要注意一些特定的语法。

函数指针的声明

函数指针的声明语法如下:

c 复制代码
return_type (*pointer_name)(parameter_types);

例如,声明一个指向返回类型为 int,参数类型为 intchar 的函数指针:

c 复制代码
int (*func_ptr)(int, char);

函数指针的赋值

可以将函数的地址赋值给函数指针。例如,假设有一个函数 add,其原型如下:

c 复制代码
int add(int a, char b);

我们可以将函数 add 的地址赋给函数指针 func_ptr

c 复制代码
func_ptr = add;

使用函数指针调用函数

使用函数指针调用函数与直接调用函数类似,但需要使用指针名和参数列表。例如:

c 复制代码
int result = func_ptr(10, 'a');

示例代码

下面是一个完整的示例,展示了如何声明、赋值和调用函数指针:

c 复制代码
#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;
}

函数指针数组

函数指针数组是一组指向不同函数的指针。这在需要根据某种条件选择和调用不同的函数时特别有用。例如:

c 复制代码
#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语言中,函数指针还可以用作函数参数。这使得我们可以编写更通用的代码。例如:

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;
}
举例:字符串排序

排序既可以依照数字,也可以依照字符,因此可以穿入不同函数进行不同排序。

c 复制代码
#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 时,strcmpnumcmp 是函数的地址。因为它们被认为是函数,所以不需要使用 &,就像在数组名称前不需要 & 一样。我们编写的 qsort 可以处理任何数据类型,而不仅仅是字符字符串。根据函数原型,qsort 需要一个指针数组、两个整数和一个带有两个指针参数的函数。泛型指针类型 void * 用于指针参数。任何指针都可以转换为 void * 并且再转换回来而不丢失信息,因此我们可以通过将参数转换为 void * 来调用 qsort。对函数参数的详细转换确保了比较函数的参数类型匹配。虽然这些转换通常对实际表示没有影响,但它们确保了编译器的一致性。

c 复制代码
/* 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 的第四个参数是

c 复制代码
int (*comp)(void *, void *)

这表示 comp 是一个指向函数的指针,该函数有两个 void * 参数并返回一个 int。在使用 comp 的代码行中

c 复制代码
if ((*comp)(v[i], v[left]) < 0)

这一用法与声明一致:comp 是一个指向函数的指针,*comp 是函数,而 (*comp)(v[i], v[left]) 则是对它的调用。需要使用括号以确保组件正确关联;如果没有括号,

c 复制代码
int *comp(void *, void *)    /* 错误的 */

则表示 comp 是一个返回 int * 的函数,这是完全不同的。

我们已经展示了 strcmp,它用于比较两个字符串。这里是 numcmp,它通过调用 atof 计算的前导数值来比较两个字符串:

c 复制代码
#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 *

c 复制代码
void swap(void *v[], int i, int j) {
    void *temp;
    temp = v[i];
    v[i] = v[j];
    v[j] = temp;
}

完整程序

c 复制代码
#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]);
}

可以为排序程序添加各种其他选项;其中一些选项是具有挑战性的练习。

练习

  1. 修改排序程序以处理 -r 标志,该标志表示按逆序(递减)排序。确保 -r 可以与 -n 一起使用。
  2. 添加选项 -f,使大小写字母不区分,在排序时将大写和小写字母视为相同;例如,aA 比较时相等。
  3. 添加 -d("目录顺序")选项,使比较仅基于字母、数字和空格。确保它可以与 -f 一起使用。
  4. 添加字段搜索功能,使得可以根据行内的字段进行排序,每个字段可以根据一组独立的选项进行排序。(本书的索引是使用 -df 对索引类别排序,使用 -n 对页码排序。)

复杂声明

在C语言中,复杂声明(Complicated Declarations)可以包括多级指针、数组的指针、函数指针等。这些声明在初学者看来可能会比较难以理解,但通过分解每个部分并逐步分析,可以更好地掌握它们的用法。

声明阅读顺序

理解复杂声明的一个关键是按照C语言声明的阅读顺序来解析它们。一般来说,阅读顺序是从变量名开始,然后依次读取修饰符和类型。

基本例子

以下是一些常见的复杂声明及其解释:

  1. 指针声明

    c 复制代码
    int *p;

    p 是一个指向 int 类型的指针。

  2. 指向指针的指针

    c 复制代码
    int **pp;

    pp 是一个指向 int 类型指针的指针。

  3. 指向数组的指针

    c 复制代码
    int (*pa)[10];

    pa 是一个指向具有10个 int 类型元素数组的指针。

  4. 数组的指针

    c 复制代码
    int *ap[10];

    ap 是一个数组,数组中有10个元素,每个元素是一个指向 int 类型的指针。

  5. 函数指针

    c 复制代码
    int (*fp)(int, int);

    fp 是一个指针,指向一个返回类型为 int,参数类型为 int, int 的函数。

  6. 返回指针的函数

    c 复制代码
    int *func(int, int);

    func 是一个函数,它有两个 int 类型的参数,返回一个指向 int 类型的指针。

复杂声明的例子

以下是一些更复杂的声明,包含多层指针、数组和函数指针的组合:

  1. 指向函数指针的数组

    c 复制代码
    int (*arr[10])(int, int);

    arr 是一个数组,数组中有10个元素,每个元素是一个指针,指向一个返回类型为 int,参数类型为 int, int 的函数。

  2. 返回指向数组的指针的函数

    c 复制代码
    int (*func(void))[10];

    func 是一个函数,它不接受任何参数,返回一个指向具有10个 int 类型元素数组的指针。

  3. 返回函数指针的指针

    c 复制代码
    int (**func(int))(int, int);

    func 是一个函数,它接受一个 int 类型的参数,返回一个指向函数指针的指针,这个函数指针指向一个返回类型为 int,参数类型为 int, int 的函数。

解析复杂声明

为了更好地理解复杂声明,可以使用如下步骤:

  1. 从变量名开始:找到变量名,然后向外阅读。
  2. 依次解析修饰符 :解析变量名旁边的修饰符,如 *(指针)、[](数组)、()(函数)。
  3. 结合优先级:结合修饰符的优先级来确定整个声明的含义。

实践工具

为了帮助解析复杂声明,可以使用一些在线工具或命令行工具,如:

  • cdecl:一个命令行工具,可以将C语言声明转换为可读的英文解释。

    sh 复制代码
    $ 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

通过不断实践和使用这些工具,可以逐步掌握复杂声明的解析方法。

相关推荐
Best_Me0727 分钟前
快速排序算法-C语言
c语言·算法·排序算法
学习前端的小z28 分钟前
【C语言】深入剖析 C 语言中数组与指针的紧密联系及高效应用
c语言·开发语言
Peter_chq1 小时前
【计算机网络】多路转接之poll
linux·c语言·开发语言·网络·c++·后端·poll
ahadee1 小时前
蓝桥杯每日真题 - 第23天
c语言·vscode·算法·蓝桥杯
冉佳驹2 小时前
数据结构 ——— 直接选择排序算法的实现
c语言·数据结构·算法·排序算法·直接选择排序算法
ahadee2 小时前
蓝桥杯每日真题 - 第20天
c语言·算法·蓝桥杯·bfs
可乐鸡翅好好吃4 小时前
STM32的中断(什么是外部中断和其他中断以及中断号是什么)
c语言·stm32·单片机·嵌入式硬件·mcu·物联网·51单片机
ahadee4 小时前
蓝桥杯每日真题 - 第21天
c语言·算法·蓝桥杯
程序员与背包客_CoderZ6 小时前
C++设计模式——Singleton单例模式
c语言·开发语言·c++·单例模式·设计模式
小猿_006 小时前
C语言蓝桥杯组题目
c语言·开发语言·蓝桥杯