前言

这节课是倒数第二节课了,这节课上老师讲了有关地址的运算、字符串I/O和进阶字符串函数。


地址运算

函数与指针

当我们要向一个函数里传入一个数组的时候我们该怎么操作呢。C 语言允许传递指针给函数,只需要简单地声明函数参数为指针类型即可。

例如下面这个求和的例子:

// sum_arr1.c -- sums the elements of an array
// use %u or %lu if %zd doesn't work
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
    int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
    long answer;

    answer = sum(marbles, SIZE);
    printf("The total number of marbles is %ld.\n", answer);
    printf("The size of marbles is %zd bytes.\n",
          sizeof marbles);
    
    return 0;
}

int sum(int ar[], int n)     // how big an array?
{
    int i;
    int total = 0;

    for( i = 0; i < n; i++)
        total += ar[i];
    printf("The size of ar is %zd bytes.\n", sizeof ar);
    
    return total;
}

因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。只有在这种情况下,C才会把int ar[]和int *ar 解释成一样。也就是说,ar只是指向int的指针。由于函数原型可以省略参数名, 所以原型我们一般可以写成这两种,他们都是等价的:
int sum (int *a r, int n) ;
int sum(int ar[], int n);

他们的区别是:int •ar 形式和int ar[ ]形式都表示ar 是一个指向int 的指针。但是, int ar[ ]只能用于声明形式参数。

因此sum函数更常见的写为:

/* use pointer arithmetic   */
int sump(int * start, int * end)
{
    int total = 0;

    while (start < end)
    {
        total += *start; /* 把数组元素的值加起来              */
        start++;         /* 让指针指向下一个元素 */
    }
  
    return total;
}

 我们的系统中用8 字节储存地址, 所以指针变量的大小是8字节(其他系统中地址的大小可能不是8 字节)。

简而言之,marbles 是一个数组,ar 是一个指向marbles 数组首元素的指针, 利用C中数组和指针的特殊关系, 可以用数组表示法来表示指针ar


函数中指针型参数

函数要处理数组必须知道何时开始、何时结束。sum( )函数使用一个指针形参标识数组的开始, 用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。(如sum1中的n为元素的个数)但是这并不是给函数传递必备信息的唯一方法。

还有一种方法(更常用,即上方指针的代码)是传递两个指针, 第1 个指针指明数组的开始处(与前面用法相同),第2 个指针指明数组的结束处。

/* use pointer arithmetic   */
int sump(int * start, int * end)
{
    int total = 0;

    while (start < end)
    {
        total += *start; /* 把数组元素的值加起来              */
        start++;         /* 让指针指向下一个元素 */
    }
  
    return total;
}

 指针start 开始指向marbles 数组的首元素, 所以赋值表达式total += *start 把首元素的值加给total。然后, 表达式start++递增指针变量start, 使其指向数组的下一个元素。因为start 是指向int 的指针, start 递增1 相当于其值递增int类型的大小,即指向了下一个元素的地址。

生动形象一点的解释就是,有一排人(数组),他们有他们的名字(值)和一个编号(地址)。指针就是指向编号(地址),而这个编号(地址)又对应了那个人的名字(数组值),当指针++时,即编号++,对应了下一个人的名字。

不过在以上代码中,值得注意的是:*end 指向的位置实际上在数组最后一个元素的后面第一个

所以我们传参数过去时,应该写成这三种情况:

sump(marbles, marbles + SIZE);//从0开始的情况
sump(marbles+1, marbles + SIZE + 1);//从1开始的情况
sump(marbles, marbles + SIZE - 1);//指向数组的最后一个元素的情况

在指针进位的时候要注意不要和数组名搞混:

例如:

char str[6] = "Hello";
char * cpt = str;
char ch;
ch = *(str+1); // ok ch = str[1]
ch = *(cpt++); // ok ch = str[0]
ch = cpt[1]; // ok ch = str[2]
ch = *(str++);// wrong, str is not a variable, cannot use ++.

 即:只有变量才可以进行运算,str是数组名,也表示首元素地址,*cpt表示指针变量,指向str的首元素


指针优先级

上面的加和我们可以写成一步

total += *start++;

一元运算符*和++的优先级相同, 但结合律是从右往左, 所以start+ +先求值, 然后才是*start。也就是说, 指针start 先递增后指向。使用后缀形式(start++而不是++start) 意味着先把指针指向位置上的值加到total 上, 然后再递增指针。

如果使用*++start, 顺序则反过来, 先递增指针, 再使用指针指向位置上的值。如果使用(*start) ++, 则先使用start 指向的值, 再递增该值, 而不是递增指针。这样, 指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然*start+ +的写法比较常用, 但是*(start++ )这样写更清楚,虽然没什么区别 。

关于指针运算的优先级,我们可以参考下图:


指针运算

指针能不能像普通变量一样进行运算呢,答案是可以的,但和一般的变量相比,它的运算有些特别,例如:

Given two pointers (variables or constants), p1 and p2 of the same type t,  and an integer j:
p1 + j  true.结果还是一个地址
p1 – p2  is  (value of p1  -  value of p2) / sizeof(t)
p1 < p2,  when value of p1 is smaller than p2. 
 <=, >=, != are similar. 
p1 – p2,   When they have different types,error. If they have same types, true.
p1 * p2,  error
p1 / p2,   error 

*与&

首先,它们是两种指针运算符,一种是取地址运算符 &,一种是间接寻址运算符 *。

& 是一元运算符,返回操作数的内存地址。如果一个变量 var 是一个的类型为T的变量,则 &var 是它的地址。因此&读作"取地址运算符",这意味着,&var 读作"var 的地址"。

相反的,当已具有一个地址,并且希望获取它所引用的对象时,使用间接运算符 *。例如有一个指针变量ptr,我同样定义为类型T。即:T *ptr; ,那么我们让它和var产生联系:

ptr = &var;//令指针指向var

则此时的*ptr就表示了var的值了。

总结一下就是:指针变量不加*表示地址,加了*表示指针指向位置的值。

// 定义var
T var;
// 获取 var 的地址
ptr = &var;
// 获取 ptr 的地址所对应的值,即var的值
T val = *ptr;

*和&的互补性

由上方的代码可以看出,&和*其实有互补性,那么以下代码是合法的:

For any expression E, such that &E is valid (Example &5 is invalid)
*(&E) == E // * and & are inverse operators. *和&抵消了

如果date是一个数组名,以下三个形式是一样的:

  1. *(&dates)
  2. dates
  3. &dates[0]

都表示首元素的地址。


字符串IO

上面的指针操作可能有点难,还不如进入下一章看看字符串的操作。我们在之前也介绍过字符串操作了,不过是比较基础的,下面我们来看看字符串的输入输出操作的进阶版。

字符的输入输出我们在之前的介绍中知道了使用getchar和putchar,但这个是单个字符的,那我们有什么办法一次性输入一串字符呢?

输入

scanf()

我们最熟悉的输入方式就是scanf了,当然我们可以用scanf来输入一串字符。

例如:

char str[81];
scanf("%s",str);

scanf在输入字符串是可以指定输入长度,例如我想输入5个字符到str[]里,我可以写成:

scanf("%5s",str);

但是scanf遇到空格符、换行符就会停止读入,如果我们想一次性读入一整行,而这一行又有几个空格,该怎么做呢?

这时候我们就要用到gets()函数了。

gets()

我们可以通过gets()读入一行,例如:

char str[81]; 
gets(str);

 看似非常的简单,但是在编译的时候,也许编译器会弹出一个警告:

warning: this program uses gets(), which is unsafe.

每次运行这个程序, 都会显示这行消息。但是, 并非所有的编译器都会这样做。其他编译器可能在编译过程中给出警告, 但不会引起你的注意。这是怎么回事?问题出在gets()唯一的参数是words, 它无法检查数组是否装得下输入行。如果输入的字符串过长会导致溢出,会导致程序崩溃或擦写掉其他正常的数据。

那我们还需要限制输入的长度才行,故有了gets()的代替品,fgets()函数。

fgets()

fgets()函数稍微复杂些, 在处理输入方面与gets()略有不同。

这个函数的原型是:char *fgets(char *str, int n, FILE *stream) 

  • str -- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
  • n -- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。屏幕输入则为stdin。

如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针(NULL)。

我们可以用返回值来判断是否输入正确。


输出

输入完可能要输出嘛,我们输出和输入相同有相应的输出函数。

printf()

和我们知道一样,输出字符串即:

printf("%s",str);

 不过printf不会自动输出换行符,换行时需要我们加上'\n'。

puts()

与gets()相对的,就是puts()函数,表示输出一整行。例如:

puts(str);

 缺点也和gets()一样,可能存在溢出的现象,同样有一个函数fputs()来代替它。

fputs()

C 库函数 int fputs(const char *str, FILE *stream) 把字符串写入到指定的流 stream 中,但不包括空字符。

  • str -- 这是一个数组,包含了要写入的以空字符终止的字符序列。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流,屏幕输出为stdout。

字符串函数

在之前的介绍中,介绍过了基本的字符串函数,但我们还有进阶版。在使用字符串函数时,需要加上头文件<string.h>。

【C语言学习之路】第五节课——常量定义、数组以及基本字符串

strlen()函数

strlen()函数用于统计字符串的长度。我们也许是一串输入的字符串,但我们不知道输入了多少个字符,strlen()就可以帮你计算有多少个字符。

strncat()函数

在之前的介绍中介绍了strcat()函数,而这个strncat()函数比它多了一个n,那它多了什么功能呢?

strcat()函数无法检查第1个数组是否能容纳第2个字符串。如果分配给第1个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。即strcat()和 gets()类似, 也会导致缓冲区溢出。 

char *strncat(char *dest, const char *src, size_t n) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。

  • dest -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串,包括额外的空字符。
  • src -- 要追加的字符串。
  • n -- 要追加的最大字符数。

strncmp()函数

 int strncmp(const char *str1, const char *str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字节。

  • str1 -- 要进行比较的第一个字符串。
  • str2 -- 要进行比较的第二个字符串。
  • n -- 要比较的最大字符数。

该函数返回值如下:

  • 如果返回值 < 0,则表示 str1 小于 str2。
  • 如果返回值 > 0,则表示 str2 小于 str1。
  • 如果返回值 = 0,则表示 str1 等于 str2。

strncpy()函数

char *strncpy(char *dest, const char *src, size_t n) 把 src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。

  • dest -- 指向用于存储复制内容的目标数组。
  • src -- 要复制的字符串。
  • n -- 要从源中复制的字符数。

该函数返回最终复制的字符串。


memset()函数

void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。

  • str -- 指向要填充的内存块。
  • c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
  • n -- 要被设置为该值的字符数。

简单来说就是初始化赋值函数,支持绝大多数的类型。我们可以用它来初始化数组为一个非0的数。

例如:

int a[80];
memset(a,1,sizeof(a));//把a全部的值初始化赋值为1

sprintf()函数

sprintf()函数声明在stdio.h 中, 而不是在string.h 中。该函数和printf()类似, 但是它是把数据写入字符串,而不是打印在显示器上。因此,该函数可以把多个元素组合成一个字符串。sprintf()的第1个参数是目标字符串的地址。其余参数和printf( )相同, 即格式字符串和待写入项的列表。

函数的声明是这样的:int sprintf(char *str, const char *format, ...)

  • str -- 这是指向一个字符数组的指针,该数组存储了 C 字符串。
  • format -- 这是字符串,包含了要被写入到字符串 str 的文本,它可以包含嵌入的 format 标签。

不过使用较为复杂,不建议使用。


后记

这一篇是目前最长的吧,可能读起来很累,不过都读到这里了,还是非常感谢的。

这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2024-01-14