指针与数组
这一章将说明有关指针的内容
导言
首先我们先理解一下什么是指针,指针是一种保存变量地址的变量。在C语言中,指针的使用十分广泛,并且如果合理使用指针,那么会大幅度的简化操作,完成更加高效和紧凑的代码
需要注意的一点是,从ANSI C后,指针使用void*代替char*作为通用指针的类型
指针与地址
接下来开始正式介绍指针
内存是怎么组织的
通常来讲,机器都有一些列连续编号或者编址的存储单元,这些存储单元你可以单个进行操纵,也可以连续的成组操纵
一个字节可以存储一个char类型,而两个相邻的字节类型可以存储一个short类型的数据,而如果是相邻的四个字节存储单元可以存储一个long类型的数据
而指针是一个可以存放一个地址的一组存储单元
正文
在介绍内存是怎么组织的之后,便要开始讲正题了,首先先介绍一下一元运算符&
具体用法如下:
1 | p = &c |
这里把c的地址赋值给p,所以我们称p为指向C的指针,需要注意的一点是地址运算符&只能应用于内存中的对象,也就是变量和数组元素,并且不能作用于表达式、常量和寄存器变量
一元运算符*是间接寻址或者是间接引用运算符,当他作用于指针的时候,将访问指针所指向的对象
下面通过一个例子来说明这一点:
1 | int x = 1,y = 2, z[10]; |
在上面的例子中,我们初始化了一个指针:ip
这个指针的类型为整数类型
而在初始化之后,存在一个赋值的操作:ip = &x;,由上文可知,&的作用是找到该变量的内存地址
这里将&x的内存地址赋值给了指针ip,换句话讲,也就是指针ip指向了x
而下面一个表达式则为:y = *ip;
由于上文将指针ip指向了x,而x的值为1,所以这里y会被赋值为1
下面的*ip = 0;指的是把指针ip指向的值赋值为0,那么什么是指针指着的值呢?上面提及过为:x
所以,这里也就是把x的值赋值为0
另外,指针还可以指向数组中的某一位,比如最后一个表达式:ip = &z[0];
简单总结一下关于指针的用法
初始化指针为类型 *指针名;,想要把指针指向某个元素,不需要加星号,但对应的值必须为对应的地址:指针 = &变量名
而如果想要把指针指向的元素赋值给变量,则需要使用*:变量 = *指针名;
如果想要修改指针指向的值,也需要加*:*指针名 = 新的值;
另外,指针与变量一致,对应什么类型就只能指向什么类型的值,int的指针只能指向整型数,double的指针只能指向浮点数
但是有一点不同的是,void类型的指针可以存放任何类型的指针,但不能间接引用它本身
指针在使用的时候跟变量有些许一致的地方:
1 | *ip = *ip + 1; |
这里可能会出现疑惑了,诶,为什么下面的后缀要加括号呢?
那是因为如果不加括号,则会先对ip加1运算,而不是对指针指向的东西进行运算,如果不加括号,那就是对地址进行运算了
这也说明了*和++运算优先级是一致的,所以遵循从右到左的顺序
指针之间是可以互相指向的,例如:
1 | int x = 1; |
最后的iq = ip;会将指针ip的东西赋值给iq,则两者指向的东西就是一致的了
指针与函数参数
接下来讲讲有关指针在函数参数里面的应用
依旧从之前的一个函数讲起,在之前我们使用过这个函数swap()用于两个数之间的互相调换位置
1 | void swap(int x, int y){ |
但是,这个函数有一个比较大的缺陷,例如下面这种情况:
1 | void swap(int x,int y); |
很显然,在main()里面,我们将两个变量传入了函数里面,企图交换位置
但是实际运行的结果,发现输出的还是原来的参数
这是因为参数传递是采用传值的方式,被传入的值会创建一个副本,从而导致了只是交换副本的值,原先的值并没有发生任何变化
这也告诉我们一个点:调用的函数是没办法直接修改主调函数中变量的值
那要怎么办呢?诶,这里指针就可以派上用场了
我们只需要把指针的地址传给函数,即可让两个地址交换
由于打印的时候打印的是指针指向的变量的内存地址,而交换地址后指向的位置也会随之交换,这就可以完成我们的目的
1 | void swap(int *x,int *y); |
接下来通过一个实际的例子来说明
我们的目的是实现一个这样的函数:getint,使得所有输入进去的字符全部转换为整数,并且在返回的时候返回这个整数
那么我们要怎么去设计呢?
首先,按照惯例,我们需要在输入为EOF的时候停止程序,那么便可以把是否返回EOF作为标准
1 |
|
由于getint每次都会返回输入的值,所以这里便可以作为判断的条件
接下来是函数内部的表达式
首先,依旧选择跳过所有的空白符:
1 | while (isspace(c = getch())) { |
接下来检测输入的内容,如果输入的内容不等于数字,或者是不符合我们需要保留的字符(+和-),则把这个符号放入暂存区,并且返回0
1 | if (!isdigit(c) && c != EOF && c != '+' && c != '-') { |
这里使用到的ungetch(c)是之前在逆波兰计算器中使用到的存入缓存的函数,简单来讲就是把一个字符存取缓存区,在下一次调用的时候不选择获取getchar()里面的数字,而是选择提取缓存区里面的数字
这里就成功筛选出来剩下的我们需要的内容了,接下来就正式进入到我们的检测了
首先,我们需要检测出是否为负数,这里的方法也简单,只需要检测当前字符是否为负号即可,如果为负号,那么判断符号的变量改为负数即可
1 | sign = (c == '-') ? -1 : 1; |
这里的变量sign是检测是否为负数的变量
接下来是检测是否为加号或减号,如果为加号或者为减号则跳过该符号继续获取输入:
1 | if (c == '+'|| c == '-') { |
那么至此,我们剩下的字符就全都是数字了,主要的逻辑依旧与之前一致,每读取一位数字则乘10后加数字
由于我们传入的参数是一个地址,函数的参数是一个指针,这里等价于将内存地址赋值给指针
1 | int *ip = &array[n]; |
接下来的步骤与之前的是一致的:
1 | for (*pn = 0 ; isdigit(c) ; c = getch()) { |
这里也就是将数字逐一增加数字的过程
在完成计算数字之后,我们便要判断是否为正负数了
也就是使用到我们之前判断是否为正负数的这个变量sign
1 | *pn *= sign; |
最后返回对应的结果
完整代码如下:
1 |
|
指针与数组
接下来讲讲指针与数组,首先我们需要明白,指针与数组关系是十分密切的,我们可以用指针来等价替换数组下标
那么为什么要这样做呢,因为一般来讲,用指针编写的程序要比用数组下标编写的程序要更快,但代价是使用指针的程序要更难理解
首先我们先理解一下一个数组在内存中是什么储存的
数组在内存中是连续存储的,比如说:
1 | int a[] = {1,2,3,4,5,6,7}; |
假设第一项a[0]的地址为:0x7ffffcbd0
那么第二项a[1]的地址就为:0x7ffffcbd4
诶,为什么是加4而不是加1呢?其实之前也提及到了,在这里由于我们定义的是一个整数,所以占用的空间是4个字节
这也就是为什么是加4而不是加一的原因
类似的,我们可以写出这个数列的所有地址:
1 | a[0]: 0x7ffffcbd0 |
我们不妨假设一下,我们的指针pa的赋值语句为:pa = &a[0];
此时指针将指向这个a[0]所在的位置,换句话讲就是pa的值为这个地址,而加上*变成指针后就变成对应地址的值了
1 | int main() { |
接下来,你还可以将这个值赋值给一个新的变量:
1 | x = *pa; |
指针运算
这里先简单接触一下指针运算
这里的例子依旧为之前的数列
假设我们的指针pa指向的是数列的第一项,那么我们可以通过指针运算来让其指向后面的项:
1 | pa += 1; |
此时指针pa指向的地址便是下一项,也就是a[1]。那么同理pa + i就是后面第i个元素
下标与指针运算
在上文的例子中,我们有这么一个赋值:
1 | pa = &a[0]; |
但事实上,我们可以这样写:
1 | pa = a; |
如果将一个数组名称赋值给指针,那么此时对应的就是这个数组的第一个值,因为数组名所代表的就是该数组最开始的一个元素的地址
此外,如果你想引用数组的第n项,那么可以这样写:*(a + n)
首先我们需要理解为什么可以这么做
在C语言中,如果需要计算对应的数组的第n项的时候(例如:a[n]),会先把这个数组转换成这种格式:*(a + n)
也就是说:a[n] == *(a + n)
同理,如果加上&,那么就变成了这样:&a[n] = a + n
这与我们上文提及到的点类似:数组名称对应该数组的第一个值的地址
这里需要记住一个点,那就是指针是一个变量,而数组名称不是一个变量,所以,a++是不合法的
当我们把一个数组名称传给一个函数时,实际上是将这个数组第一个元素的地址传进函数
接下来通过一个例子来说明这个点:
1 | int strlen(char lst[]) { |
首先这里先把一个数组传进去,接下来是for循环,每次循环结束都将数组lst向后移动一项,直到最后一项为结束符
在每次循环中都将用于计数的o加一,也就是对应的数组的长度
需要注意的一个点是,在这里的函数参数中,int strlen(char lst[])的lst[]可以换成*lst
也可以将指向子数列起始位置的指针传递给函数,这样,就可以实现将函数的一部分传递给函数的效果:
1 | func(&a[2]) |
当然,同理于下面的表达:
1 | func(a + 2) |
如果起始位置不为第一项,那么便可以在参数中使用负数
假设我们传入的数组的起始位置为第三项,那么a[-1]则为第二项,a[-2]则为第一项,a[-3]由于超过数组的边界,会导致报错
地址算术运算
接下来讲讲这部分的内容
在这里我们将实现两个新的函数功能,分别是alloc()和afree(),第一个函数的功能是返回一个指向n个连续字符存储单元的指针,而下一个函数的功能是释放已经分配好的内存空间
这里的两个函数十分重要,在标准库中有相对应的函数,分别是malloc()和free()
让我们来简单介绍一下这两个函数
首先先让我们创建一个空白的储存区,接下来我们的东西将会存放在这里,接下来我们需要知道一个点,由于我们的这两个函数所处理的对象都是指针,所以其他函数可以不用知道数组的名字
而因此,我们在声明这两个函数的时候可以使用static关键字使得这两个函数变为静态变量,这样的原因是防止外部函数对这两个函数进行一些操作
首先,我们命名空白储存区为:allocbuf,并且命名另一个变量allocp作为一个标志,用于判断已经使用的空间
1 |
|
接下来看看函数alloc的本体
1 | char *alloc(int n){ |
这里的步骤为先获取所需要的空间大小int n,接下来是一个喜闻乐见的判断
先看到判断条件allocbuf + ALLOCSIZE - allocp >= n
这里是什么意思呢?我们可以先逐步逐步分析一下
第一个运算是allocbuf + ALLOCSIZE,通过上文我们可以得知allocbuf是这个存储区的首项,而后面是一共的大小
而为什么要- allocp呢?不妨让我们思考一下,这里的allocp是什么。由上文得知,这里是已经使用的空间
那么整条表达式所要表达的便是求出剩余的空间
这里就有人要问了:诶,allocbuf不是第一项的内存地址16进制码吗,那要怎么运算呢?
这里可以注意到一个有意思的点,allocp定义为什么?
嗯对的,这里一开始定义的就是一个内存的地址,那么两个十六进制相减,得到的就是这两者的距离了,也就是还剩余的空间
可能还有疑惑说,诶,那第一次要怎么运行呢
事实上,由于第一次还没有存进去任何的东西,此时存储区是空白的,换句话讲,此时剩余的空间为ALLOCSIZE
这里allocbuf和allocp由于相等直接抵消掉了,等到后面allocp才会增加
那么在判断完成后,便可以执行下面的语句了
1 | allocp += n; |
这个语句代表占用的空间大小
1 | return allocp - n; |
而这个语句为什么返回的是原来的allocp呢?
这里我们思考一下要返回什么东西,如果说还有空余位置,那么就返回空闲位置的起始点allocp,如果没有,则返回0
所以这里得返回一开始的起始点
一个有意思的点
前文说过,allocp是一个内存地址,那么当超出范围后返回的是0呢?
这里需要补充一个小知识,在C语言中,0永远不是有效的数据地址,因此,我们可以将返回值0表示发生了异常情况(也就是满了)
在程序中,通常用常量NULL来代替常量0
接下来继续来讲下一个函数afree()
这个函数的主要作用是释放已经使用的空间
首先我们先看到其传入的参数:char *p,很明显,这是个指针
接下来看看函数内部结构:
1 | void afree(char *p){ |
显而易见的,可以看到这里有个判断语句
那么判断的条件是什么呢?p >= allocbuf && p < allocbuf + ALLOCSIZE
这里有两个判断的部分,让我们看第一个部分
1 | p >= allocbuf |
这里看着可能有点头晕,但是没关系,先让我们理清楚p是什么,由上文可知,p是传入的参数,是一个指针
所以,这里对应的就是其指向的值的内存地址,而另一个参数就是这个存储区的第一项的内存地址
而第二个判断是在原有的基础上加上了存储区的大小,稍微思考一下可以得知,allocbuf + ALLOCSIZE便是存储区最右边的内存位置
总结来说,这里就是判断这个指针的内存地址是否位于存储区内,如果位于这个位置则进入下面的语句
而下面的语句也很简单,便是将指针赋值给我们的标识变量allocp
那么这样做会发生什么呢?
假设这个指针的地址为0x005
而allocp的位置是0x0A1
此时如果赋值,那么便会使allocp的位置变为0x005
也就是空出了一部分位置,传进函数的指针p的位置被空出来了!
这也就对应了这个函数的功能——释放已分配好的存储空间
字符指针与函数
接下来讲讲字符指针与函数
首先讲一下关于字符串常量的内容
首先我们需要知道的一点是字符串常量是一个字符数组
这一点在之前的很多方面都有使用到
当我们想要输出什么语句的时候常常会使用到printf()
例如我们一开始的例子:printf("Hello World!\n")
当出现这样的一个语句在程序中,实际上是使用字符指针来访问该字符串的
而在上面的例子中,语句接受的是一个指向字符数组第一个字符的指针,换句话来讲,字符串常量可以通过一个指向其第一个字符的指针访问
接下来还有其他的用法
假定我们声明了一个指针pmessage,并且将一个指向该字符数组的指针赋值给pmessage
1 | char *pmessage; |
那么此时这个指针指向的位置就是该字符串的位置
这里需要注意的一点是,此处与直接赋值一个数组是不同的:
1 | char amessage[] = "hello world!"; |
为什么是不同的?
在直接声明一个数组的情况下,数组amessage始终指向同一个位置,而指针pmessage是一个指针,指向的位置是一个字符串常量,这个指针可以被修改来指向其他地址,但如果试图修改这个指针指向的字符串内容 ,那么是没有被定义的
接下来通过一个例子来说明一下这个功能
在标准库中有一个函数strcpy(char s,char t)
这个函数的功能是将字符串t复制给s,起到一个文本复制的功能
首先是最基本的实现:
1 | void strcpy(char *s,char *t){ |
这里的本质为指针的复制,也就是把t的地址复制给指针s,因此,并不是复制字符(虽然效果一样)
接下来是第二种实现的方式:
1 | void strcpy(char *s,char *t){ |
这里使用到了之前提到的一个小技巧,t + i可以移动指针的内存地址
虽然这样看已经很简洁了,但是事实上经验丰富的程序员并不会这么写,而是选择这种格式:
1 | void strcpy(char *s,char *t){ |
利用自增运算符,可以使得整个表达更加简洁
从这里我们也可以发现,使用指针的的确确会比直接使用数组下标更加简便
接下来我们研究的函数是字符串比较函数:strcmp(s,t)
这个函数的作用是比较两个字符串的字符,其比较的准则为字典顺序
如果小于则返回负整数,如果等于则返回0,如果大于则返回正整数
返回值这里指的是s和t由前向后逐字符比较时遇到的第一个不相等字符处的差值
接下来开始复现这个函数
首先依旧是最简单的数组下标的写法:
1 | int strcmp(char *s, char *t){ |
接下来开始分析这一串代码:
首先我们先从最基础的开始讲起,也就是for语句
这个for语句指的是如果比较的这两个字符是相等的则继续比较,不返回值,如果相等且刚好为终止符,则返回0(也就是相同的意思)r
那么如果不相等会怎么样呢?根据代码内容来看,如果不相等则会直接跳出循环,进入到下一个语句,也就是返回两个字符的差值
这与我们上文说过的是相同的
接下来是指针的形式:
1 | int strcmp(char *s,char *t){ |
这里的核心思想与上文数组的思想基本上一致
首先还是照例:指针为数组名则代表为第一项的内存地址,所以这里初始化为:*s == *t
每次for语句结束自动便为下一项,继续比较:s++,t++
如果不相等则返回差值,这里的思路是一致的
补充点:压入栈与弹出栈
接下来来稍微补充这一点:
在第四章实现逆波兰计算器的时候稍微提到一嘴
在学完指针后可以使用更方便的方法来实现这一操作
首先是压入栈:
1 | *p++ = val; |
前文也说过,这里指的是先对内存地址p进行自增
弹出栈是这个:
1 | val = *--p; |
指针数组以及指向指针的指针
接下来讲讲这方面的内容,首先我们需要明确一个点,那就是指针也是一种变量
而正因如此,我们便可以将指针放入数组当中,
在之前的章节中,我们有提到一个排序的例子,在那个例子里面我们实现了排序的效果
但是如果我们将排序的东西稍微调整一下,比如说把排序的东西换成长度不一的文本行,那么就没办法很好的处理了
所以我们需要用更加高效的处理方式
这里我们将引入指针来处理类似的问题
现在假设我们待排序的文本行都相邻地存储在一个常字符数组里面,那么每个文本行便可以通过指向它的第一个字符的指针来访问
并且,这些指针可以存储在一个数组里面
如此一来,我们便可以实现排序的操作:当需要交换顺序的时候,直接交换数组里面的指针位置即可
一般而言,我们会将一个程序按照功能分割成几部分(利用函数),在需要使用的时候利用main函数来分别执行即可
接下来让我们实际写一个这样的程序
思路部分
首先,让我们捋一捋思路
这个程序大概长这样:
1 | 输入 -> 排序 -> 输出 |
排序部分我们可以直接使用之前讲过的快速排序,那么其他的呢?
我们需要知道我们最后的结果是什么,嗯,便是把排好序的数组一个一个打印出来
那么输出部分可以使用循环和自增来解决这个问题
但是最开始的输入呢?由于我们这里使用了指针,也就说我们需要在输入的时候建立指针,为了防止出现边界溢出的问题,我们还需要统计输入的行数,如果出现溢出的现象,则直接退出
接下来让我们构思一下输入的部分
首先,我们这里使用了指针,意味着需要为输入的东西分配内存,而分配内存,我们可以使用上一节中的alloc()
既然有了alloc(),那么指针地址的事情也就顺利解决了,只要在每次输入进去的时候,将返回的内存使用大小的地址当做使用的地址即可,并且,这里既然使用了alloc()便要记得检查内存地址是否正确,防止出现超过边界的情况
于是我们便可以写出这样的函数
1 | int readlines(char *lineptr[],int maxlines) |
接下来让我们细细的分析一下这串代码
首先先把里面使用到的函数讲一下:strcpy(p, line)指的是将line的内容复制给p,getline(line, MAXLEN)获取输入,line为接受字符的数组,而后面的第二个参数MAXLEN指的是数组最大的长度
之后,让我们看到第一个赋值那里:nlines = 0
这里的意思是把获取到的字符数量设置为0,变量nlines指的是目前所拥有的字符数量,这里的字符数量分割标准为每个换行符分割一次
接下来是循环语句,这里的作用是判断输入的长度是否大于零,说白了就是判断是否有输入
在循环语句里面便是一个判断语句
nlines之前讲过了,maxlines指的是最大的分割数量,p代表的每次分割的内存地址,此处(p = alloc(len)) = NULL的目的是防止内存地址错误
如果都不满足(也就是正常运行),则会进入到下面的部分
此处首先执行了这个操作line[len - 1] = '\0';,也就是将长度的倒数一项设置为终止符,那么倒数第一项是什么呢?
诶,我们可以思考一下我们是以什么作为分割标准的,对,就是换行符\n,所以,这里就是将换行符换成了终止符
换完后有什么作用呢?之前反复强调了这一点,一个字符数组必须以终止符\n作为结尾
所以,这里的目的其实是让每个分割的字符数组合法化
接下来是最后的lineptr[nlines++] = p;
前文得知,这里的p是一个内存地址,所以不难推断,此处的数组lineptr[]便是存储着每个被分割的字符数组的内存地址
在完成输入之后,接下来是输出部分:
输出部分也很简单,只需要用循环输出排列好的指针数组既可
1 | void writelines(char *lineptr[],int nlines){ |
这里需要注意的一点就是lineptr这个数组存储的是我们所有的内存地址
当然,与之前的例子是一致的,这里的函数也可以写为指针的形式
1 | void writelines(char *lineptr[], int nlines){ |
这里的检测标准为代表文本数量的nlines,每次执行的时候进行自减来进行输出,这样可以使得所有内容被正确输出
在这里需要知道一个点,由于这里输入的内容大部分为指针,也就是说在这之中使用过的一些之前出现过的函数,需要进行一些调整:
1 | void qsort(char *v[],int left,int right){ |
在这里,快排的核心逻辑并没有发生改变,但是有些细节的部分发生了变化
比如说传入的第一个参数变成了一个指针,还有下面的有关交换的判定部分
在原先的代码,这里是如果前一项大于后一项,那么两者交换
而这里的逻辑是,如果两个指针是不同的,那么就进行交换
同样,负责交换的swap()函数也需要进行改动
1 | void swap(char *v[],int i,int j){ |
这里的改动主要在传入参数这里,同样是使用了指针的格式
多维数组
接下来讲讲多维数组的相关内容
在C语言中是存在类似矩阵的多维数组的,虽然使用频率不如指针数组,但是在一些特定场合里面可以发挥出大作用
我们将通过一个实际的例子来说明这个内容
我们将实现一个功能:通过输入指定的日期来输出该日期是那一年的第几天
那么要怎么实现这个功能呢?
思路
想要知道是第几天,那么就得把这个日期之前的月份的天数加上,而后加上当前月份的天数
那么首先实现第一个功能,也就是每个月份的天数
我们可以创建一个数组:
1 | static char daytab[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31} |
这里可能就有疑问了,诶,为什么第一项是0呢?
由于我们是读取月份来进行计算的,如果直接使用daytab[i]会因为数v组从零开始计数的特性导致月份错位
所以这里将第一个月份给占用,可以十分有效的解决这个问题
但是,这里可能有人要问了,诶,那这跟多维数组有什么关系呢
这里就需要引入一个问题了,既然要处理月份,那就必须的涉及闰年的问题
为了处理这个问题,我们便可以使用一个多维数组来解决问题
首先先来介绍一下多维数组:
这里以二维数组来作为例子
1 | 数组名[第一个数组的元素数量][第二个数组的元素数量] = {{a,b,c},{d,e,f}} |
在上面这个多维数组里面,数组{a,b,c}和{d,e,f}是第一个数组的元素
而里面的a,b,c和d,e,f是第二个数组的各个元素
利用多维数组,我们便可以轻松解决这个问题
1 | month_day_lst[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}}; |
在这个例子中,我们可以看到第一个数组中第一个元素代表一般年份的每月日期表,而第二个元素代表的数组则是代表闰年的日期表
主程序
在完成这个数组之后,我们便要思考如何计算日期了
首先,最核心的部分我们可以这样操作
假设我输入的日期为3.12
那么只需要用for循环将3月之前的所有日期全部加起来即可
1 | for (i = 1;i < mouth;++i){ |
在这里可以发现第一个用于判断是否为闰年的参数leap,所以在此之前我们需要判断输入的年份是否为闰年
1 | leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0; |
这里的原理是,如果两者里面有一个是可以实现的(也就是闰年),为真,也就是1
代码如下:
1 | int day_of_year(int year,int month ,int day){ |
这里一个比较巧妙的点是day += month_day[leap][i]
通过在原输入日期上进行递增,可以减少一步用于加本月日期的步骤
接下来来介绍下一个方面的内容,也就是如何将输入的指定日期转化为对应的月和日
基本思路
接下来介绍一下基本思路:
这里的思路如下,由于我们需要返回对应的日期,所以我们可以先判断当前天数是否小于当前的数组项的天数
例如,如果我们的日期为29天,此时数组对应的天数为1月,这个月有31天,29 < 31,所以为当前这个月,返回1月29号
如果大于这个月,则减去这个月的日期后将项数增加,重新进行比较
例如,我们输入的日期为40,而40 > 31,也就是说这个日期不是一月,此时减去一月的31,并将项数增加一来到二月
11 < 28,故返回2月11号
在思路完成后,我们便可以开始写对应的程序了
1 | void month_day(int year, int yearday,int *pmonth,int *pday){ |
这里可以发现一个有意思的点,这个函数并没有使用return来返回值,而是选择指针来接收对应的值
这样做的好处是,这个函数可以运行在程序内的任何一个地方,输出的时候只需要将指针输出即可
为什么要这样做?如果使用return的形式的话,那么这个函数就必须在输出的地方运行才能正常输出
而如果使用指针的话则可以在程序的任何地方运行,使得程序更加灵活
我们可以试想一下,如果我们的程序有多个功能,而每个功能都得匹配一个输出来对应return的话,那么这个程序会变得十分臃肿
如果每个函数都将最后的结果赋值给指针的话,那么最后我们只需要使用一个输出语句便可以匹配所有函数
这也是这里的一个巧妙的点
接下来讲讲有关多维数组作为参数传入函数的有关内容
在这里,如果我们将这个日期的数组传入里面,那么我们需要保留后面的第二个数组的元素个数
第一个数组的元素个数可以省略,但是第二个数组的元素个数不能省略
1 | func(int daytab[][13]) |
这里需要复习一下之前的一个点,函数在调用的时候传递的是一个指针,指向的是一个一维数组,所以这里可以不用写第一个数组的元素个数
除此之外,还可以写成这样子:
1 | func(int (*daytab)[13]) |
这种声明是指针的声明,指向一个具有13个元素的一维数组,由于方括号的优先级大于*,所以这里需要先加上圆括号
指针数组的初始化
接下来来讲这部分的内容
首先我们不妨来编写一个函数,这个函数会返回一个指向第n个月名字的字符串的指针
1 | char *month_name(int n){ |
这个数组name是一个私有的字符串数组,当他被调用的时候会返回一个指向正确元素的指针
需要注意的一点是这个声明并没有指明数组name的长度,所以,编译器在编译的时候对初值个数进行统计,然后填入数组的长度
指针与多维数组
这一章来讲讲指针数组与多维数组的相关内容
首先我们不妨分别定义一个指针数组和一个多维数组
1 | int a[10][20]; |
那么,假设我们要引用这两个数组,分别使用a[3][4]和b[3][4]都是正确的引用
但是,数组a是一个真正的多维数组(这里a为二维数组),其分配了200个int类型的存储空间,可以通过常规的矩阵下标计算公式来得到具体元素的位置:
1 | // 200 * row + col |
而对于数组b而言,却不太一样
数组b只分配了10个指针,并且没有对其初始化,换句话说就是每个指针指向的对象均没有说明,也正因为没有初始化的原因,这些指针的初始化必须通过静态或者代码初始化
如果每个指针都指向20个元素的数组,那么编译器就得给数组b分配200个int类型长度的存储空间以及原本10个指针的存储空间
这里是这样的,由于每个指针指向的数组都有20个长度,所以总需求长度为20 * 10 + 10(原本的指针)
而在上文也提到过了,指针数组的每个元素是没有初始化的,换句话说这里并不想二维数组一样,每个数组必须按照规定的长度才行
这样的一大好处就是可以大大节省分配的存储空间
我们以上面5.8的例子为例
1 | char *name[] = { |
这里Illegal month\0占了15个字符,January\0占了9个字符,February\0占了10个字符
换句话说这个指针数组只占了15 + 9 + 10 = 34个字符
而如果使用多维数组呢?
1 | char name[3][15] = { |
由于在一开始初始化的时候就已经指明了数组的长度,也就是说无论怎么样数组长度都是不变的3 * 15 = 45
可以明显的看到,使用指针数组所分配的存储空间数量大大少于多维数组分配的存储空间数量
命令行参数
接下来来介绍这一方面的内容,首先我们需要先补充一件事情,在支持C语言的环境中,我们可以在程序开始的时候将命令行参数传递给程序
在调用主函数main的时候,有两个参数:第一个参数被称为argc,这个参数的作用是用于参数的计数,它的值用于表示运行程序时命令行中参数的数目
而第二个参数被称为argv,用于参数向量,它是一个指向字符串数组的指针,而每一个字符串就对应一个参数,如果我们需要处理这些参数,一般会使用到多级数组
我们将通过一个简单的例子来说明这一点
在Windows中有一个有趣的命令:echo,它的作用是将参数输出到屏幕上
举一个简单的例子:
1 | echo hello, world |
输入这串命令后输出效果为:
1 | hello, world |
接下来我们要实现这个命令
基本思路
在上面我们也提到过了,我们可以在程序开始的时候将命令行参数传递给程序
这时候我们的main可以写成这样子:
1 | int main(int argc, char *argv[]){ |
这里的两个参数都在之前提及过,所以这里不细讲
那么按照参数argv的定义,我们要怎么处理呢?
这里一般是以空格来分割,所以我们不妨回看我们输入了什么:
1 | echo hello, world |
也就是说,我们可以分成这几个部分:
argv[0]对应的是"echo",而argv[1]对应的是"hello,",以此类推,argv[2]是world
而之前也有提及到第一个参数的作用:计数,通过观察我们可以得到,argv的第一项为这个命令的名字,此时计数argc的值为1
也就是说,如果我们要判断我们输入的命令后面是否有参数(这里也就是防止输入错误了),只需要判断计数argc的值是否大于1即可
这里需要补充的一点是,在数组argv中,可选的最后一个参数为:argv[argc - 1]按照ANSIbiu熬准规定,argv[argc]必须为空指针
这里列出上面的argv的各个项的值:
argv[0] = “echo”
argv[1] = “hello,”
argv[2] = “world”
argv[3] = 0这里argv[3]为空指针,因为argc = 3,按照ANSI标准,这一项必须为空指针,也就是0
接下来我们继续说明思路
由上面的例子可以得到,这里我们真正要输出的内容为argv[1]到argv[argc - 1]的内容,所以这部分的内容我们只需要通过for语句循环输出即可
而这里我们不能忘记处理用于分割的空格,也就是说,我们需要在正确的时机将原本用于分割的空格也输出出来:
那么什么是正确的时机呢?如果当前项不为argc - 1则说明并不是最后一项,也就是说这一项跟后一项被空格分割了,在这种情况下我们需要把原先的空格补回去
接下来开始正式写程序,这里提供两个思路
第一个思路
第一个思路将数组argv视为一个字符数组
首先我们先写输出部分:
既然涉及到循环和自增,那就只能用到for语句
既然使用了for语句,就有必要知道循环条件
这里的循环条件为当前项为argc - 1,当然一般来讲会写为:i < argc的形式
1 | for (i = 0;i < argc;++i){ |
在上文也提到过,我们需要补充空格,这里的思路与上面的循环条件是一样的:i < argc - 1就说明需要补充空格
完整程序如下:
1 | int main(int argc,char *argv[]){ |
第二个思路
接下来讲讲第二个思路
第二个思路使用了指针的形式
1 | int main(int argc ,char *argv[]) { |
这里使用的是指针的形式,核心的逻辑相同,但是表现形式上存在些许不同:
在这里,循环使用的形式是通过自减次数来进行判断,这样同样也可以实现输出的效果
而这里的*++argv对应的是之前的字符数组的形式,利用的知识点是数组名代表第一项,以及a + i来实现项数增加的效果
当然,这里的printf语句还可以写成这样
1 | printf((argc > 1) ? "%s ": "%s",*++argv); |
具体的思路依旧一致,只不过表现形式不同罢了
find
接下来我们来讲一个例子:
这个例子我们将实现一个功能:打印与指定的模式匹配的行
那么要怎么实现这个例子呢?
1 | int main(int argc, char *argv[]) { |
接下来我们来分别解析一下这个例子
首先这里使用到了一个标准库里面的函数:strstr(t,s),这个函数的作用是指向字符串t在字符串s中第一次出现的位置
如果找不到这个字符串的话,则返回NULL
有意思的是这个函数我们之前写过类似的,就在函数一开始的时候已经介绍过了
这个函数位于string.h中
首先,这个程序表示找到的次数的变量为:found,一开始被初始化为0,接下来是判断的部分
有人可能有疑问了,诶,为什么是!=2呢?
首先我们不妨思考一下我们会输入什么
1 | find [参数1] [参数2] |
那么这里其实是这样的
1 | argv[0] = "find" |
假设这里不为2,那么必定多参数或者少参数,所以这里也就是为什么为2的原因
假设这里输入正确,那么则会到达下面的逐一检测的地方,这里如果出现一个匹配的,那么函数strstr会直接返回这个匹配的位置的指针,也就满足条件!= NULL
这里每找到一次,那么就次数就加1,最后到达末尾的时候就返回最终找到的次数
如果没找到就返回默认值0
接下来讲讲find的命令行
我们这里将在给这个函数新增两个参数,第一个是-x,代表打印所有与模式不匹配的文本行,而第二个是-n,代表在打印的时候顺带打印行号
那么这个程序要怎么写呢?
1 |
|
我们可以看到这里面一个核心的地方为中间的循环部分,这里的意思是,先自减argc检测是否大于0,并且(*++argv)[0]是否等于代表参数的-
那么这个(*++argv)[0]是什么意思呢?
我们不妨一点一点来分析
首先,由结果可以确定,这里指代的是参数项的第一个字符(由[0])可以确定
因此,这里*++argv代表便是一个字符数组
由上文对find的解析可以知道,这里一开始的时候指代的是argv[0],也就是find
这里自增使得变成了第二项,也就是参数项
在将检测的对象(变量)设置为数字参数后,就进入到了下面的switch语句检测
如果检测到c为字符x的话,那么状态机except为1,此时触发模式x,同理,当检测到参数n的时候,状态机number变为1,将在之后的对应环节进行检测
那么接下来是核心部分
首先,假设检测完发现依旧有参数(即无效参数),则会返回正确的格式(此时计数器argc由于没有跑满次数,导致不为1)
假设都满足,则进入到打印的环节
通过getline返回次数,之后lineno自增来计数输入的行数
接下来是判断部分,这里的这个语句:(strstr(line, *argv) != NULL) != except
首先我们需要知道的是,最后一个except为模式x的一个状态机,假设模式x未开启,此时except为初始值0,意味着想要执行就必须要满足前面的函数strstr返回的指针不为空
如果不为空,则意味着匹配,顺理成章的打印出匹配的行
如果x模式启动,那么此时这个程序会运行的条件为前面返回的指针为空,也就是不匹配的意思,根据参数x的意思,开启该模式的时候会打印出不匹配的行
这样,就成功实现了x参数
接下来-n同理
当每次运行一次的时候,会自动让计数器found自增
在最后会返回所打印的次数
指向函数的指针
接下来来讲讲这一方面的内容
首先我们需要对一些基本的内容进行科普
在C语言中,函数本身不是变量,但可以定义为指向函数的指针
这种指针可以放到数组之中,可以被赋值,或者作为函数的返回值
接下来我们将修改之前的快速排序程序,通过给参数,可以使其按照数值大小而非字典顺序对输入行进行排序
在之前我们提到过一个函数:strcmp,这个函数的作用是按照字典顺序比较输入行
那么为了实现其他功能,我们需要一个新的函数numcmp来比较数值大小
这个函数可以这样实现:
1 | int numcmp(char *s1, char *s2){ |
可以看到,这里如果条件满足的话,则会返回状态机,这样,就可以把处理结果的步骤放到外面,减少不必要的功能,使得程序更加灵活
接下来看看改进后的代码:
1 |
|
接下来依次介绍这里的代码:
首先,我们在要求中有提到一点,假设我们需要对不同的参数使用不同的排列模式(如用数组大小排序)
那么便可以使用参数-n
这里要实现这样的效果可以在加一个判断语句,如果判断结果为真,那么则令一个参数转变为1,而后在后面的语句中使用这个参数即可
这里实现判断的语句为:
1 | if (argc > 1 && strcmp(argv[1], "-n") == 0){ |
依旧,在检测到带有参数(argc > 1),并且参数的第一个不为-n(argv[1] != "-n’)时,参数numeric转变为1
这里将会影响到后面传入qsort的参数:numeric ? numcmp : strcmp
如果为真(也就是没有参数-n),那么则使用numcmp,反之,如果无参数,此时numeric为0,则使用strcmp
接下来的writelines则是将排序好的数组输出
qsort
接下来让我们看看修改后的快速排序代码
首先先看到括号内的参数:
1 | void qsort(void *lineptr[], |
可以看到,相较于之前用数组下标表示的快排,这里的快速排序使用了四个参数
并且,可以注意到最后一个参数是一个指向函数的指针
由于函数在C语言中不是一等公民,所以没办法直接用于参数
但是可以通过指针来实现这样的效果,这里也就是使用到了这一点
接下来看看四个参数分别是什么
首先,第一个参数是一个数组,这里指的是要进行排序的数组,特别注意到的一点是这里类型为void
void *为通用指针类型,任何类型的指针都可以转换为void *类,并且在转换回原来类型的时候不会丢失信息,所以,这里也就是为什么要选择这个类型的原因
快排的核心代码没有多大改变,唯一有改动的是比较的部分:
1 | if ((*comp)(v[i], v[left]) < 0){ |
原先的代码为前一项与后一项比较,而这里由于使用了比较的函数strcmp和numcmp,所以相应的也要进行改变
补充内容
这里补充一些有关这段代码的一些东西:
主要的补充地方依旧在快排这里:
1 | qsort((void **) lineptr, |
首先是第一个部分,第一个参数的写法
我们可以看到,这里使用了(void **)
这里是什么意思呢?这里指的是指向指针的指针
lineptr是一个char数组,里面的每一个元素都对应一个字符串
而在前文初始化的时候是这样的:
1 | char *lineptr[MAXLINE]; |
这意味了什么,意味着lineptr的实际类型为char **
也就是说,这里的每一个元素其实都是一个指针
而此处快排的操作其实也只是交换指针而已,并没有对字符串进行操作
这样做的原因是假设字符串长度较长,那么此时交换指针的效率要比直接交换字符串的效率要快得多
这也就是为什么这里要选择交换指针的原因
复杂声明
由于C语言中时常会出现一些常令人诟病的声明问题:
1 | char (*(*x[3])())[5] |
所以明白这些声明是什么意思是很重要的,例如下面提供了两个相似的声明:
1 | int *f(); |
1 | int (*pf)(); |
第一个声明代表返回一个int类型的指针,f()是一个函数
而第二个声明代表一个指向函数的指针(*pf),这个函数返回的类型为整数类型
很明显,这两者的区别在于有没有加圆括号
这也说明了*的优先级是低于圆括号的
那么要怎么读懂这些复杂的声明呢?
接下来介绍的两个程序便会解决这个问题,一个程序用于将C语言里面的声明转换为文字描述,另一个程序则将这个过程反过来