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

指针和地址

地址

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

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

例如,在某个特定内存位置存储一个整数值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
指针的操作
  1. 访问值:通过解引用(dereference)指针,可以访问它所指向的变量的值。例如,*p获取指针p指向的变量的值。
  2. 修改值:通过解引用指针,可以修改它所指向的变量的值。例如,*p = 20将修改变量x的值为20。
  3. 指针运算:可以对指针进行加减操作,从而访问数组等连续内存块。例如,p+1指向下一个内存位置(通常是下一个元素)。
指针的应用
  1. 数组和字符串:指针用于遍历和操作数组和字符串。
  2. 动态内存分配:通过malloc等函数动态分配内存,并使用指针访问和管理这些内存。
  3. 函数参数:通过传递指针,可以在函数中修改外部变量的值,实现更高效的数据传递。
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;     // 返回下一个非数字字符
}

练习

  1. 编写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)
}

指针和数组

相同之处

  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 个元素。

举例:内存分配和回收。
#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 给出了从 sp(不包括 \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 传递给函数 ff 的声明应为:

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" };

练习

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

练习

  1. 编写程序 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
  1. 编写程序 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,参数类型为 intchar 的函数指针:

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 时,strcmpnumcmp 是函数的地址。因为它们被认为是函数,所以不需要使用 &,就像在数组名称前不需要 & 一样。我们编写的 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]);
}

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

练习

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

复杂声明

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

声明阅读顺序

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

基本例子

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

  1. 指针声明

    int *p;
    

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

  2. 指向指针的指针

    int **pp;
    

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

  3. 指向数组的指针

    int (*pa)[10];
    

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

  4. 数组的指针

    int *ap[10];
    

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

  5. 函数指针

    int (*fp)(int, int);
    

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

  6. 返回指针的函数

    int *func(int, int);
    

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

复杂声明的例子

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

  1. 指向函数指针的数组

    int (*arr[10])(int, int);
    

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

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

    int (*func(void))[10];
    

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

  3. 返回函数指针的指针

    int (**func(int))(int, int);
    

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

解析复杂声明

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

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

实践工具

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

  • 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
    

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/761866.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

“蓝潮卫士“水位雨量监测一体机,重塑城市防洪新防线!

​ 6月24日&#xff0c;湖南长沙遭遇了一场突如其来的特大暴雨侵袭。天空像破了个口子&#xff0c;雨水倾盆而下&#xff0c;仅仅1小时&#xff0c;就下了54个西湖&#xff0c;降水量突破了历史同期极值。这场暴雨直接导致了严重的城市内涝问题&#xff0c;部分地区瞬间变成一…

强化学习的数学原理:贝尔曼公式

大纲 这一节课程的大纲&#xff1a; 重点 对于这次课&#xff0c;重点是两个东西&#xff1a; Motivating examples(为什么需要贝尔曼公式) 首先要明白&#xff0c;为什么 return 是重要的&#xff1f; 之前其实就说过&#xff0c;return 能够帮助我们评价一个策略是好还是坏…

阿里云:云通信号码认证服务,node.js+uniapp(vue),完整代码

api文档&#xff1a;云通信号码认证服务_云产品主页-阿里云OpenAPI开发者门户 (aliyun.com) reg.vue <template> <div> <input class"sl-input" v-model"phone" type"number" maxlength"11" placeholder"手机号…

几度互联网站群管理系统全媒体解决方案

随着高考的结束&#xff0c;各高校开启了紧张的招生宣传工作&#xff0c;几度互联网站群系统助力各高校招生宣传。 学校官方网站是互联网时代学校对外交流的重要途径和信息公开的主要载体&#xff0c;是展示学校形象、密切联系师生的重要窗口&#xff0c;是加强校园宣传思想工…

2024 年的 13 个 AI 趋势

2024 年的 13 个 AI 趋势 人工智能对环境的影响和平人工智能人工智能支持的问题解决和决策针对人工智能公司的诉讼2024 年美国总统大选与人工智能威胁人工智能、网络犯罪和社会工程威胁人工智能治疗孤独与对人工智能的情感依赖人工智能影响者中国争夺人工智能霸主地位人工智能…

LVGL实现字库的下载和使用

1 字库 字库的概念&#xff1a;相应文字或字符的合集。 点阵字库&#xff1a;按字库顺序排列的字符/汉字字模的合集。 LVGL中字库使用Unicode编码&#xff0c;Unicode 是全球文字统一编码。它把世界上的各种文字的每一个字符指定唯一编码&#xff0c;实现跨语种、跨平台的应…

分体式天线的应用介绍

分体式天线的主要应用广泛且多样化&#xff0c;以下是对其主要应用的归纳&#xff1a; 1、仓储管理 在RFID仓储项目中&#xff0c;使用性能好的RFID分体式天线可以确保系统的稳定性&#xff0c;更能够降低整个项目的成本。 分体式天线通过其多个天线接口与分体式读写器连接&…

K-Planes代码记录

随记 原文 K-Planes: Explicit Radiance Fields in Space, Time, and Appearance&#xff0c;又要换baseline&#xff0c;可是效果不好能怎么办呢&#xff0c;我可不可以发疯。k-planes的代码又是非常工程琐碎的&#xff0c;大佬的代码果然不顾小白死活。随便记录下整个过程。…

哪个牌子的超声波清洗器好?精选四大超强超声波清洗机力荐

生活中戴眼镜的人群不在少数&#xff0c;然而要维持眼镜的干净却不得不每次都需要清洗&#xff0c;只是通过手洗的方式实在太慢并且容易操作不当让镜片磨损更加严重&#xff01;所以超声波清洗机就诞生了&#xff01;超声波清洗机能够轻松清洗机眼镜上面的油脂污渍&#xff0c;…

Spire.PDF for .NET【文档操作】演示:在 PDF 中创建目录 (TOC)

目录在增强文档的可读性和可导航性方面起着至关重要的作用。它为读者提供了文档结构的清晰概述&#xff0c;使他们能够快速找到并访问他们感兴趣的特定部分或信息。这对于较长的文档&#xff08;例如报告、书籍或学术论文&#xff09;尤其有价值&#xff0c;因为读者可能需要多…

国产压缩包工具——JlmPackCore SDK说明(二)——JlmPack_Create函数说明

一、JlmPack_Create函数说明 JlmPack_Create函数是创建jlm压缩文件的核心函数&#xff0c;最大允许CATALOG_MAX_LIMIT&#xff08;请参考Config.h&#xff09;个目录&#xff0c;意思是包括文件夹和文件在内&#xff0c;遍历整个列表最大允许CATALOG_MAX_LIMIT个目录对象&#…

【Unity小技巧】Unity字典序列化

字典序列化 在 Unity 中&#xff0c;标准的 C# 字典&#xff08;Dictionary<TKey, TValue>&#xff09;是不能直接序列化的&#xff0c;因为 Unity 的序列化系统不支持非 Unity 序列化的集合类型。可以通过手写字典实现 效果&#xff1a; 实现步骤&#xff1a; 继承ISe…

2024年制冷与空调设备运行操作证模拟考试题库及制冷与空调设备运行操作理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年制冷与空调设备运行操作证模拟考试题库及制冷与空调设备运行操作理论考试试题是由安全生产模拟考试一点通提供&#xff0c;制冷与空调设备运行操作证模拟考试题库是根据制冷与空调设备运行操作最新版教材&#…

振弦采集仪的工程安全监测实践与案例分析

振弦采集仪的工程安全监测实践与案例分析 振弦采集仪是一种常用的工程安全监测仪器&#xff0c;通过测量被监测结构的振动频率与振型&#xff0c;可以实时监测结构的安全状况。本文将结合实践经验和案例分析&#xff0c;探讨振弦采集仪在工程安全监测中的应用。 一&#xff0c…

奔驰G350升级原厂自适应悬挂系统有哪些作用

奔驰 G350 升级自适应悬挂系统后&#xff0c;可根据行车路况自动调整悬架高度和弹性&#xff0c;从而提升驾乘的舒适性和稳定性。 这套系统的具体功能包括&#xff1a; • 多种模式选择&#xff1a;一般有舒适、弯道、运动及越野等模式。例如&#xff0c;弯道模式在过弯时能为…

elk对于集群实例的日志的整合-基于logstash采集日志

说明&#xff1a;基于logstash采集日志 环境&#xff1a; 物理机192.168.31.151 一.启动2个测试实例&#xff0c;每5-10s随机生成一条订单日志 实例一 包位置&#xff1a;/home/logtest/one/log-test-0.0.1-SNAPSHOT.jar 日志位置:/docker/elastic/logstash_ingest_data/l…

塑造卓越企业家IP:多维度视角下的策略解析

在构建和塑造企业家IP的过程中&#xff0c;我们需要从多个维度进行考量&#xff0c;以确保个人品牌能够全面、立体地展现企业家的独特魅力和价值。以下是从不同角度探讨如何做好一个企业家IP的策略。 一、从个人特质出发 深入了解自我&#xff1a;企业家需要清晰地认识到自己的…

树立行业标杆,林清轩获“以油养肤开创者”市场地位认证

从0到1的创造&#xff0c;才能快速实现从1到100的裂变&#xff0c;这是亘古不变的商业逻辑。 6月25日&#xff0c;知名美妆国货品牌林清轩&#xff0c;获得了CIC灼识的市场地位确认书&#xff0c;确定“以油养肤开创者” 的地位。 近两年&#xff0c;以油养肤的概念逐渐兴起&am…

vivado VIO IP核

参考&#xff1a;pg159 VIO&#xff1a;可以模拟输入/输出功能&#xff0c;实时监视和修改FPGA中的信号&#xff0c;用于调试和验证&#xff0c;与ILA相比&#xff0c;VIO无需占用RAM资源。 VIO IP的输出对于FPGA内部逻辑是输入信号&#xff0c;可以在调试界面设置输入值&…