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

news2024/11/18 10:51:46

指针和地址

地址

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

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

例如,在某个特定内存位置存储一个整数值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.coloradmin.cn/o/1884173.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Edge浏览器添加新标签页网址为 百度 搜索

默认不能直接设置&#xff0c;需要安装New Tab Change插件 安装拓展插件 url 这里点击获取即可&#xff08;我已经安装过了&#xff09; 某些扩展会更改浏览器设置&#xff0c;例如默认搜索引擎、新选项卡页和其他类型的网站数据。 为了防止扩展更改在安装时设置的首选项Micr…

MQ:RabbitMQ

同步和异步通讯 同步通讯: 需要实时响应,时效性强 耦合度高 每次增加功能都要修改两边的代码 性能下降 需要等待服务提供者的响应,如果调用链过长则每次响应时间需要等待所有调用完成 资源浪费 调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下…

来聊聊Redis定期删除策略的设计与实现

写在文章开头 我们都知道redis通过主线程完成内存数据库的指令操作&#xff0c;由于只有一个线程负责核心业务流程&#xff0c;所以对于每一个操作都要求尽可能达到尽可能的高效迅速&#xff0c;而本文就基于源码来聊聊redis的定期删除策略的设计与实现。 Hi&#xff0c;我是 …

上传头像到Domino中

大家好&#xff0c;才是真的好。 首先&#xff0c;说一个消息&#xff0c;2024年6月25号HCL发布了一则公告&#xff0c;就是从2024年12月10号开始结束Notes/Domino 11.0.x版本的市场订单申请&#xff0c;从从2025年6月26号开始停止对Notes/Domino 11.0.x版本的产品技术支持&am…

宝塔linux网站迁移步骤

网站迁移到新服务器步骤 1.宝塔网站迁移&#xff0c;有个一键迁移工具&#xff0c;参考官网 宝塔一键迁移API版本 3.0版本教程 - Linux面板 - 宝塔面板论坛 (bt.cn)2 2.修改域名解析为新ip 3.如果网站没有域名&#xff0c;而是用ip访问的&#xff0c;则新宝塔数据库的wp_o…

Ubuntu机器安装rdkit指定版本,通过conda安装不需要make,有手就行。

阿里云购买Ubuntu 22.0机器 IP没错&#xff0c;访问外网没问题 图片中的命令放在下面了。 useradd test-user -s /bin/bash mkdir /home/test-user chown -R test-user: /home/test-user passwd test-uservi /etc/sudoers wget -c https://repo.anaconda.com/archive/Anacon…

springcloud-gateway 网关组件中文文档

Spring Cloud网关 Greenwich SR5 该项目提供了一个基于Spring生态系统的API网关&#xff0c;其中包括&#xff1a;Spring 5&#xff0c;Spring Boot 2和项目Reactor。Spring Cloud网关的目的是提供一种简单而有效的方法来路由到API&#xff0c;并向它们提供跨领域的关注&#x…

[快易签]免越狱苹果签名工具快易签自用证书签名教程学会了可签一切应用

相关地址 快易签官网&#xff1a;快易签 定制版&#xff1a;快易签.定制款(含证书) 自签版&#xff1a;https://s1.kyq1.cn/ 免费源&#xff1a;https://app.eqishare.com/appstore 网盘&#xff1a;路灯网盘-iOS砸壳分享网-IPA分享网-巨魔商店IPA软件资源-后厂村路灯的网…

白话负载均衡、正反向代理(入门科普版)

什么是负载均衡&#xff1f;为什么需要负载均衡 从字面上理解&#xff0c;什么是负载&#xff0c;服务器承受访问量的大小是负载&#xff0c;但是单台服务器的访问性能是有限的&#xff0c;最典型的例子就是双十一、春运抢票这种&#xff0c;这时候就需要一种方案来解决这类问…

互联网框架五层模型详解

注&#xff1a;机翻&#xff0c;未校对。 What is the Five Layers Model? The Framework of the Internet Explained 五层模型互联网框架解释 Computer Networks are a beautiful, amazing topic. Networks involve so much knowledge from different fields, from physics…

idea启用多个环境

背景 在平常的后端开发中&#xff0c;需要与前端联调&#xff0c;比较方便的是让前端直接连自己的本地环境&#xff08;毕竟每次都要打包部署到测试环境实在是太麻烦了&#xff09;。但是这样子也有点不好&#xff0c;就是自己功能还没写好呢&#xff0c;结果前端连着自己的环…

LLaVA1.5训练数据和时间分析

LLaVA的PT+SFT训练_llava sft-CSDN博客文章浏览阅读379次。这个阶段,使用8个A100(80G)训练LLaVA-v1.5-13B大约需要20h。全量微调,非lora跑不起来啊,以前一直用swift,llama-factory这种框架式的代码库,但用原作者开源的代码也是有很多好处的。在这个阶段,使用 8 个 A100(…

Web端登录页和注册页源码

前言&#xff1a;登录页面是前端开发中最常见的页面&#xff0c;下面是登录页面效果图和源代码&#xff0c;CV大法直接拿走。 1、登录页面 源代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>登录</ti…

云计算【第一阶段(24)】Linux文件系统与日志分析

一、文件与存储系统的inode与block 1.1、硬盘存储 最小存储单位&#xff1a;扇区(sector) 每个扇区大小&#xff1a;512字节 1.2、文件存取 最小存取单位&#xff1a;块(block)连续八个扇区组成&#xff1a;块(block) 每个块大小&#xff1a;4K文件数据&#xff1a;实际数据…

为什么我学个 JAVA 就已经耗尽所有而有些人还能同时学习多门语言

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「JAVA的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01;我的入门语言是C&#xff0c…

网安小贴士(3)网安协议

一、前言 网络安全协议是构建安全网络环境的基础&#xff0c;它们帮助保护网络通信免受各种威胁和攻击。 二、定义 网络安全协议是指在计算机网络中用于确保网络通信和数据传输安全的协议。它们定义了在网络通信过程中的安全机制、加密算法、认证和授权流程等&#xff0c;以保…

SOC模块LoRa-STM32WLE5有哪些值得关注

SoC 是片上系统的缩写&#xff0c;是一种集成芯片&#xff0c;集成了计算机或其他电子系统的所有或大部分组件。这些组件通常包括中央处理器 (CPU)、内存、输入/输出接口和辅助存储接口。包含数字、模拟、混合信号和通常的 RF 信号处理功能&#xff0c;具体取决于应用。片上系统…

Kotlin扩展函数(also apply run let)和with函数

also apply run let with的使用例子 private fun testOperator() {/*** also*/val person Person("ZhangSan", 18)person.also {// 通常仅仅打印使用, 也可以通过it修改it.name "ZhangSan1"println("also inner name: " it.name)}println(&qu…

DevOps认证是什么?DevOps工具介绍

DevOps 这个词是由Development&#xff08;开发&#xff09; 和 Operations&#xff08;运维&#xff09;组合起来的&#xff0c;你可以把它理解成为一种让开发团队和运维团队紧密合作的方法。 DevOps从2009年诞生到现在已经14年多了&#xff0c;一开始大家还在摸索&#xff0…

【gitee使用教程】(创建项目仓库并上传代码简易版)

gitee使用教程&#xff0c;创建项目仓库并上传代码简易版 1.在码云上创建一个仓库2.将代码克隆到本地1.复制仓库地址2.找到你想要放置的文件位置&#xff0c;右键点击更多选项&#xff0c;选择Git Clone3.将复制的仓库地址填入URL 3. IDEA结合GIT和Gitee的简单使用idea需要识别…