文章目录
前言
这节课是倒数第二节课了,这节课上老师讲了有关地址的运算、字符串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是一个数组名,以下三个形式是一样的:
- *(&dates)
- dates
- &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>。
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 标签。
不过使用较为复杂,不建议使用。
后记
这一篇是目前最长的吧,可能读起来很累,不过都读到这里了,还是非常感谢的。
Comments NOTHING