K&R学习笔记 第五章

指针可是C语言的精髓。本章一开始就用一个例子swap(交换两个数的值)来说明。想想也奇怪,我本科阶段学习C语言的时候其实很差的,这里当时也是一团乱麻,形参、实参、指针这些东西怎么也搞不懂,后来是自学了数据结构以后,就突然开窍了。指针声明通常有两种方法:

int a = 0;
int b = 0;
int *pa = &a;
int* pb = &b;

我很喜欢使用第二种,因为他明确地指出了某个变量的类型,比如pb就是int*类型,在参数传递的时候很好理解,而作者倾向于使用第一种类型,因为它表明了对pa取内容时,会得到一个int类型的值,至于大家,按照自己的习惯就好,不必非要改正。
对于指针的运算,也有一些需要注意的地方:
按照C语言的运算符次序,自增自减操作符是优先于取地址和*的。所以++*p是对p指向的变量自增1;而*p++是对p指向的下一个变量取值。
指针与数组有着密切的关系:数组在作为参数传递时,实际上传递的是他的首地址。而数组元素a[i]会被编译器修改为*(pa+i),使用指针的效率会略高于使用数组下标。
再说一下可能公司笔试、面试题都考烂了的strcpy与strcmp:

void strcpy1(char *s,char *t)
{
	int i = 0;
	while((s[i] = t[i]) != '\0')
		i++;
}

void strcpy2(char *s,char *t)
{
	while((*s = *t) != '\0')
	{
		s++;
		t++;
	}
}

void strcpy3(char* s,char* t)
{
	while((*s++ = *t++) != '\0')
		;
}

void strcpy4(char *s, char* t)
{
	while(*s++ = *t++)
		;
}

似乎这4种写法性能差异不大,但是可读性却差了很多。个人觉得有点华而不实。

书中在介绍完多维数组一个,出了一个课后题,大体的意思是通过指针而不是数组下标来访问一个二维数组。具体的解答也很简单,我只想用一个更简单的例子来澄清一些概念(这个类型的题目来自于程序员面试宝典)

int main(void)
{
	int Arr[3][4] = {10,20,30,40,50,60,70,80,90,100,110,120};
	int (*pArr)[4] =Arr;//注意pArr的类型
	printf("%d\n",**pArr);
	printf("%d\n",**(pArr+1));
	printf("%d\n",*(*pArr+1));
}

这个问题关键在于理解pArr到底是什么类型(其实如果看过后面的复杂类型声明机制,会更好理解一点)。pArr是一个指针,它指向一个数组,数组中有4个元素,每个元素都是整形。这个指针跟Arr是什么关系呢?其实,C语言中并没有什么高维数组,只有一种基本的数组,但是允许数组的每个元素还是数组,这样嵌套成了所谓的高维数组。理清了这个概念,我们再看Arr,Arr是一个3维数组,每个数组的元素都是一个4维数组。所以,这里可以把Arr(的首地址)赋值给一个指向4维数组的指向pArr——即把一个4维数组的地址赋给一个指向4维数组的指针。
罗嗦了这么多,现在再看这几条打印语句。pArr指向的是3维数组Arr的首地址,对他第一次接引*pArr得到的是Arr的第0个元素,(这个元素也是一个数组{10,20,30,40}的首地址),所以**pArr得到的是数组{10,20,30,40}的第一个元素。而paArr+1得到的是Arr的第1个元素{50,60,70,80},**(pArr+1)便是他的第一个元素50;*(*pArr+1)输出为20则是因为*pArr如前所述,是数组{10,20,30,40}的首地址,*pArr+1则是其第二个元素20的地址。
关于命令行参数,说起来也挺简单的:
int main(int argc, char* argv[])
中,argc记录了参数的个数(至少为1个,这个参数为程序名);而argv是一个argv[argc-1]的数组,数组的每个元素是一个指针,指针指向各个命令行参数。但是实际使用起来,还是有点小复杂的,代表性的问题就是Unix和linux很多带参数的的程序,比如:
find -x -n 模式
将打印所有与模式不匹配的行,并打印行号。(-x 与-n可以组合使用),我们来看看书中给出的程序:

#include <stdio.h>
#include <string.h>

#define MAXLINE	1000

int getline(char* line, int max);

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] == '-')//检查第二个命令行参数是否为"-"		这里要注意 (*++argv)[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;

}

int getline(char s[],int lim)
{
	int c, i ;
	for(i = 0; i < lim-1 && (c = getchar())!= EOF && c != '\n';++i)
		s[i] = c;
	if(c == '\n')
	{
		s[i] = c;
		++i;
	}
	s[i] = '\0';
	return i;
}

while(--argc > 0 && (*++argv)[0] == '-')检查第二个命令行参数是否为"-",这里要注意的是 (*++argv)[0]与 *++argv[0]的区别。 (*++argv)[0]是先自增argv让它指向第二个命令行参数,然后对其解引,获取第二个命令行参数字符串的首地址,然后判断首地址存放的是否是'-';而 *++argv[0]先取argv[0],此时它指向的是'-',然后自增,再解引。while(c == *++argv[0])就是利用这个作用。通过这两重循环,命令行参数中-x或者-n是否被选中已经通过except和number被记录了;而且此时argv也指向了命令行的第三个参数,也就是模式。此时通过getline函数不停地一行一行获取输入存入到line中,然后比较line中有没有模式对应的单词。如果有则返回该单词第一次出现的位置,否则返回NULL。这里的语句:
if((strstr(line,*argv) != NULL) !=  except)
稍微有一点绕弯,在这里解释一下。分两种情况1:except=0,此时程序要求打印的是出现该模式的行,而如果找到,那么strstr(line,*argv) != NULL就为1,1 != 0,if语句条件成立,打印这一行;如果没有找到那么strstr(line,*argv) != NULL就为0,0!= 0,if语句条件不成立,就不打印这一行。2:except=1.刚好与上面相反,就不啰嗦了。同理lineno的作用类似。


书中最后讲到了C语言复杂的类型声明机制,(这个问题产生的主要原因是因为C的声明不是从左向右阅读的)也就是面试中姜常考的指向函数的指针、返回值为指针的函数、指向数组的指针、每个元素都是指针的数组,书中通过了一个示例程序,这个程序能够分辨一些简单的情况,来说明这个问题。这个程序程序看似不长,还是很有难度的。

我在这里顺便谈一下我对这些问题的处理方法,其实就是3点:

(1)先看变量名,从中间向两边看,注意有没可以构成数组的[]或者函数参数的();

(2)记住()和[]的优先级高于*;

(3)空的()或者()里面有类型,说明这里面一定有函数指针,向前面找函数的返回值。

我举几个例子(这些例子来自于程序员面试宝典):
(1)float(**def)[10];
(2)double*(*gh)[10];
(3)duble(*f[10])();
(4)int*((*b)[10]);
(5)long(*fun)(int);
(6)int(*(*F)(int,int))(int)
书中没有对他们进行详细的说明,我仔细说说我的分析方法。第一个例子,变量名为def紧接着修饰def的**,说明def是一个二级指针。这个指针指向什么呢?看看右边,是[10],再看看左边是float,所以是指向一个数组,数组中有10个元素,每个元素都是float类型。
第二个例子,先看变量名gh,紧接着修饰gh的是*,说明gh是个指针。指向什么?10个元素的数组,数组的类型是double*。
第三个例子,注意:括号优先级高于*,所以f是一个10个元素的数组。数组中的每个元素都是一个指针,什么指针呢?看到了(),向前找返回值,是double类型,所以是指向没有形参,返回值为double类型函数的指针
第四个例子,*b说明b是一个指针,[10]说明是指向数组的指针,int*说明数组的每个元素都是int*类型(指向int的指针)。
第五个例子,*F说明F是一个指针。指向什么,右面看到了(int,int)说明这是一个指向函数的指针,那么函数的返回值是什么呢?(*F)左边是一个*,说明函数返回值也是一个指针。这个指针又是什么类型的呢?还是向右边看有一个(int),说明还是一个指向函数的指针。前面的int说明这个函数的返回值为int类型。
说到这里,其实也能感觉出来《程序员面试宝典》也是很有特色的一本书,它不是教你如何编程或者这样编程正确,或者那样编程容易出错的书,它抠出了很多C和C++的细节问题考你。如果你对自己的基本功很有信息,可以试试看。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值