针对在校大学生的C语言入门学习——高级语法
- 对于一门语言,什么才算是高级语法呢?个人认为是一些比较难以理解的语法。之所以难以理解,是因为这类语法往往都是为了处理一些高级的问题,而这些高级问题对于没有开发经验的新手来说是从未遇到过的。比如我跟一个南方的学生聊东北的铁锅炖有多么多么的好吃,任我说的唾沫横飞他也不会真的明白有多好吃,因为他从来没吃过。
- 今天想跟大家聊的一些语法并非嵌入式笔试题中经常问到的一些难题,因为很多笔试题没有实际意义。这里顺便提一下,大家如果找工作的话,需要像应付升学考试一样刷题才行,因为笔试题真的很偏!我想聊的是在开发中实实在在经常使用的一些语法。并且在这篇文章之后我还想带大家做两个综合练习来实践我们聊过的所有语法,然后这一系列文章就算完成了。
多维数组
-
在多维数组这里我重点想聊二维数组。大家先不要着急跳过这部分内容,因为很多人真的不了解什么才是二维数组,更何况多维数组。
-
我问过很多同学什么是二维数组,得到的答案往往是表示行列的数组。那么三维数组呢?四维五维数组呢?我要郑重的强调,数组本身只是一段连续的内存,不表示任何含义。大家经常说的二维数组表示行列,那只是开发者赋予它的逻辑意义。就像一杯水,你可以用来浇花、用来解渴,但你却不能说浇花和解渴就是水。
-
下面我定义一个二维数组来分析一下它的性质。
int arr[2][3] = {
{
1,2,3},{
4,5,6}};
- 接下来我使用一个更加本质的描述来解释这个定义——arr是一个包含两个元素的一维数组,数组的每个元素是一个有三个int类型元素的一维数组。
- 上图是对这个数组的内存分析。arr[0]是一个数组,数组元素分别是1、2、3,访问方式分别是arr[0][0]、arr[0][1]、arr[0][2]。数组arr[1]同理。既然arr[0]和arr[1]分别是两个数组,那arr[0]和arr[1]难道分别是两个地址吗?没错,他们是地址!arr[0][0]可以写成*(arr[0]+1)或者*(*(arr+0)+1),这种写法教材里都会有,数组操作的本质就是地址偏移和间接运算。
- 如果打印arr[0]、arr[1]、&arr[0][0]、&arr[1][0],你会发现arr[0]和&arr[0][0]的地址值是一样的,arr[1]和&arr[1][0]的地址值是一样的。既然值一样,可以用&arr[0][0]代替arr[0]吗?答案是否定的,因为他们不是一个类型的地址,你可以输出arr[0]+1和&arr[0][0]+1试试看,他们偏移一个单位的值是不一样的。前者偏移12字节,后者偏移4字节。你还会发现arr[0]+1的值和arr[1]是一样的,很正常,因为数组是连续的。
- 也许有的同学看到这里脑子很乱,这是正常的,毕竟我们现在聊的是C语言的高级语法,需要我们付出更多的耐心去研究。我给二维数组的定义是——一个以一维数组为元素的一维数组。说到这里你再去翻看以前学习过的一维数组和二维数组的初始化方式,你会明白为什么二维数组初始化的时候第二维的长度是不可省的,因为第二维的长度表示的是数组元素类型!
- 至于多维数组,我们可以将我对二维数组的定义无限递归就可以了,其本质是一样的。为什么三维数组以上的数组很少见呢?道理很简单,数组的每一维都和上一维有着逻辑的从属关系,也就是我们常说的耦合性。二维数组的逻辑就已经很复杂了,如果真的有一个逻辑需要使用到三维以上的数组,那我们可以重新审视一下我们的编程思路是不是出现了问题。因为给逻辑加上一个维度,其复杂度可不是加一那么简单。
复杂声明
- 关于复杂声明前面我简单的介绍过了,今天就详细的来聊聊。说到复杂声明那我不得不请出我的C语言恩师丹尼斯理查(因为在老爷爷的书里学到很多东西,所以就不要脸的隔空自拜老爷爷为恩师了)老爷爷的《C程序设计语言》,看看C语言之父是怎么说的。
- 怎么样,有没有被老爷爷的光环照耀到!关于复杂声明,英文描述要比中文描述简单易懂得多。如果英文不是很好的同学,那我就来给大家别扭的翻译一下吧。
char **argv;
//argv:指针指向指针指向char。
/*
我的翻译:argv是一个指向指针的指针,所指向的指针指向char类型。就是我们常说的二级指针。
*/
int (*daytab)[13];
//daytab:指针指向13个int类型元素的数组。
/*
我的翻译:daytab是一个指针,指向一个13个元素的数组,该数组的元素类型是int。就是我们常说的数组指针。
*/
int *daytab[13];
//daytab:13个指向int类型指针元素的数组。
/*
我的翻译:daytab是一个13个元素的数组,数组的元素类型是int类型的指针。就是我们常说的指针数组。
*/
void *comp();
//comp:返回指向void类型指针的函数。
/*
我的翻译:comp是一个函数,函数的返回值类型是指向void类型的指针。就是我们常说的指针函数。
*/
void(*comp)();
//comp:指向返回值是void类型的函数的指针。
/*
我的翻译:comp是一个指针,指向的是一个返回值类型是void,参数列表是空的函数。就是我们常说的函数指针。
*/
char(*(*x())[])();
/*
太难了,我直接翻译吧!首先x是一个函数,函数的返回值是一个指向数组的指针,这个数组的元素类型是指向返回值是char类型函数的指针。这个由于太复杂了,所以并没有人给它起名字。
*/
char(*(*x[3])())[5];
/*
直接来了!首先x是一个3个元素的数组,数组的元素是指向函数的指针,函数的返回值是数组指针,所指向的数组是5个char类型的元素。
*/
- 用中文翻译这个实在是太累人了,而且不直观。建议大家好好理解好好体会英文描述。另外作为新人不要去特意记忆“数组指针”、“指针数组”、“函数指针”、“指针函数”这些概念。因为C语言本来没有这些名称,都是国内的大神为了简化语言自己起的名字。咱们把复杂声明搞清楚就可以了。
- 复杂声明的最后给大家做个总结:表示身份的符号有* () [],其中*表示指针,出现在标识符左边,()表示函数出现在标识符右边,[]表示数组出现在标识符右边。分析复杂声明的时候按照从标识符开始“先右后左,由内而外”的顺序分析。用来包括标识符的()是提升优先级的。
数组指针
- 复杂声明之后大家应该明白什么是数组指针了。但是有什么用呢?大家来看下面代码:
void printArr(int(*p)[3], int len);
int main()
{
int arr[2][3] = {
1,2,3,4,5,6};
printArr(arr, 2);//分行打印arr数组
return 0;
}
void printArr(int(*p)[3], int len)
{
int i,j;
for(i = 0;i < len;i++)//遍历指针p指向的数组
{
for(j = 0;j < 3;j++)//遍历每个元素数组
{
printf("%d ", p[i][j]);
}
printf("\n");
}
}
- 当我想在函数中传递一个多维数组的时候,我们就需要使用数组指针。难道定义int类型的指针做参数不行吗?当然不行,我们上面聊多维数组的时候讲过,arr+1偏移的可不是4个字节,所以不能用int类型指针。
- 给大家一个经验,定义一个指针指向一个数组时,指针要定义成数组的元素类型。如果数组的元素是int类型,那就定义int类型指针;如果数组的元素是数组类型,那就定义数组指针;如果数组的元素是函数指针,那就定义二级函数指针!说远了,越说越想笔试题了。
- 再有,在C语言中传递一个数组时,我们必须传递两个参数。第一是数组的首地址,第二是数组的长度。如果是字符串的话传递一个首地址就可以了,因为字符串有字符\0做结尾。
函数指针
- 函数指针的用处就更大了!函数指针是C语言实现多态的必要语法!你们没有看错,就是多态。很多人都认为多态是面向对象语言才有的,其实在有面向对象语言之前就有面向对象的思想了。C语言虽然是面向过程的语言,但是想做稍微大型一些的项目,面向对象的思想是少不了的。
- 什么是多态?我对多态的描述——通过参数传递逻辑,实现在不改变代码框架的情况下执行不同的逻辑。这个描述同样适用于C++、java等面向对象的语言。对于一个数组,如果我既想做升序排序,又想做降序排序,应该怎么做呢?初级方法是把排序写两遍,下面代码我以冒泡排序为例:
void printArr(int *p, int len);
void sortUp(int *p, int len);
void sortDown(int *p, int len);
int main()
{
int arr[10] = {
21,3,43,5,67,89,9,4,13,2};
sortUp(arr, 10);
printArr(arr, 10);
sortDown(arr, 10);
printArr(arr, 10);
return 0;
}
void sortUp(int *p, int len)
{
int i,j;
for(i = 0;i < len-1;i++)
{
for(j = 0;j < len-1-i;j++)
{
if(p[j] > p[j+1])
{
int t = p[j];
p[j] = p[j+1];
p[j+1] = t;
}
}
}
}
void sortDown(int *p, int len)
{
int i,j;
for(i = 0;i < len-1;i++)
{
for(j = 0;j < len-1-i;j++)
{
if(p[j] < p[j+1])
{
int t = p[j];
p[j] = p[j+1];
p[j+1] = t;
}
}
}
}
void printArr(int *p, int len)
{
int i;
for(i = 0;i < len;i++)
{
printf("%d ", p[i]);
}
printf("\n");
}
- 你会发现sortUp和sortDown两个函数里只有if判断条件不同。这样大量重复的代码非常不利于项目的维护和扩展。所以如果可以把if里面的逻辑通过参数的形式传递进来,那不就可以避免大量重复代码了吗!请看下面使用函数指针的代码:
void printArr(int *p, int len);
void sort(int *p, int len, int(*cmp)(int,int));
int cmpUp(int a, int b);
int cmpDown(int a, int b);
int main()
{
int arr[10] = {
21,3,43,5,67,89,9,4,13,2};
sort(arr, 10, cmpUp);
printArr(arr, 10);
sort(arr, 10, cmpDown);
printArr(arr, 10);
return 0;
}
void sort(int *p, int len, int(*cmp)(int,int))
{
int i,j;
for(i = 0;i < len-1;i++)
{
for(j = 0;j < len-1-i;j++)
{
if(cmp(p[j], p[j+1]))
{
int t = p[j];
p[j] = p[j+1];
p[j+1] = t;
}
}
}
}
int cmpUp(int a, int b)
{
return a > b;
}
int cmpDown(int a, int b)
{
return a < b;
}
void printArr(int *p, int len)
{
int i;
for(i = 0;i < len;i++)
{
printf("%d ", p[i]);
}
printf("\n");
}
- 通过sort函数的参数cmp,分别调用了cmpUp和cmpDown函数,实现了逻辑传递。这样使代码得到了很大的优化。
结构体
- 结构体的语法是很容易理解的,但是什么时候使用结构体呢?比如我们现在来开发一款格斗类游戏,人物的属性包括攻击力、防御、HP等简单元素,那么我们应该用什么类型来表示一个人物呢?你会发现C语言的基本数据类型没有能够表达这么复杂信息的。但是我们可以用三个int类型来分别表示三个属性,不过这样的话对于三个变量的管理有事大问题,因为从语法来说三个变量之间是没有任何联系的。这个时候就可以使用结构体来把这三个属性都封装到一个类型中。
struct Hero
{
int act;
int def;
int hp;
};
int main()
{
struct Hero superMan;
superMan.act = 100;
superMan.def = 50;
superMan.hp = 500;
return 0;
}
- 结构体的好处就是将一些逻辑上相关的变量封装成一个类型,这样编程的时候无论是从逻辑上还是可读性上都有更加准确表达。
枚举
- 枚举类型是非常常用但是又经常在学习中被忽略的类型。当我们需要一些逻辑上相关,但是值又不能重复的常量时,我们往往使用枚举。
enum Debuff
{
dizziness,//眩晕
toxic, //中毒
freezing //冰冻
};
void showDebuff(enum Debuff db);
int main()
{
showDebuff(dizziness);
return 0;
}
void showDebuff(enum Debuff db)
{
switch (db) {
case dizziness:
printf("眩晕了\n");
break;
case toxic:
printf("中毒了\n");
break;
case freezing:
printf("冰冻了\n");
break;
}
}
- 我可以用0、1、2这些常量来分别表示三种状态,但是常量本身是没有意义的。写成宏定义又体现不出三种状态的逻辑关联性。所以定义成枚举既易读又好写。
- 关于语法就聊到这里。前面的文章中我也说过,学习编程最好的方式就是写代码。说来容易,对于新手而言写代码是需要辅导的。我不能辅导大家写代码,但是我可以把我写代码的思路一步一步的写出来给大家做参考。后面我会给大家献上《扫雷》和《学生管理系统》两个练习。注意,我给的不仅仅是代码,将是我编写代码的整个思路。