重学C语言

重学C语言
1、简述 
2、一些真相 
3、C缺陷 
   3.1多做之过 
      3.1.1Switch语句 
      3.1.2相邻字符串常量的自动连接 
      3.1.3缺省情况下函数名字的全局可见性 
   3.2 误做之过 
      3.2.1符号重载 
      3.2.2 复杂的运算符优先级规则 
   3.3少做之过 
      3.3.1 空格的作用 
      3.3.2一些代码能轻松通过编译,但运行起来就是垃圾 
4、C语言声明 
   4.1  C语言声明概述 
   4.2  C语言中的组合类型声明 
   4.3 C语言声明分析 
5、指针与数组 
   5.1 数组与指针的异同 
   5.2 二维数组 
6、数组传参 

 

1、简述
C语言作为我平时用的最多的一门编程语言,时常感觉用的不那么得心应手,除去标准C库,就其语言本身而言,还有若干盲区。如果你只会用一些逻辑语句,不会用指针操作内存、不会用头文件整理工程、看不明白C语言声明,混淆传参过程,甚至连运算符的优先级都搞不清楚,再熟练也只能说知道C语言的一点皮毛,真正的精髓是指针,当然也是难点。不容易理解的地方包括指针与数组、声明、函数传参。距初学C语言四年后,重新学习,查缺补漏,幸得好书一本《C专家编程》,站在更高的地方审视C语言的好与坏,精髓与学习之道,收获颇丰,码出一些笔记跟大家交流。
可以把语言看成一门协议,最初的设计是有其背景的,协议有好有坏,性能不可能是最佳的,语言也是,标准化以后的ANSI C语言有它的优点,也有它“难以忍受”的缺点,当然,即使是缺点,也不会再优化,不然新的编译器将会使以前无数正常的代码出现令coder莫名其妙的bug。站在历史的、设计者的角度重新学习,而不是去抠具体的知识点,那种豁然开朗的感觉真好。就像搞嵌入式没听说过startup.s,做ARM不明白bootloader内容,玩51不知道idata、xdata,写工程不懂模块化没听说过链接描述文件,有些真相你如果接触不到,你就永远都是业余选手。
2、一些真相
1、根据C语言的设计哲学,它排斥强类型,允许程序员在需要时可在不同的类型对象之间赋值,许多C程序员仍然认为“强类型”不过是敲击键盘的无用功。ANSI C规定:执行算术运算时,操作数类型如果不同,就会发生转换,数据类型朝着浮点精度更高、长度更长的方向转换,如果整数转换为signed不会丢失信息则会转成signed,否则转换为unsigned。也就是采用值保留原则(value preserving)把几个整数操作数混在一起使用,结果类型可能是有符号数也可能是无符号数,取决于操作数的相对大小
2、C语言的很多特性在设计之初是为了方便编译器的设计者,比如其下标从0开始、比如表达式中的数组名可以看做是指针,比如用register关键字来定义热门变量,放在寄存器里存储。
3、typedef与#define的区别。后者是宏定义,最好只用于命名常量,并为一些适当的结构提供简记,宏名称应该大写,这样便容易与函数调用区分开来。Typedef是类型定义,它是一种有趣的声明形式,它为类型引入新的名字而不是为变量分配空间,类似于宏定义但又有关键性的区别。Typedef一般用于简法表示指向其它东西的指针,如:typedef char * string;。它与宏定义有两点关键区别:
可以用其它类型声明符对宏类型进行扩展,但typedef定义的类型则不能
#define peach int
unsigned peach i ; //no problem
typedef int banana;
unsigned banana i ; //it is wrong!
在连续的几个变量声明中,用type可以保证所有变量均为同一类型,而用宏定义则没有办法保证
#define int-ptr int *
int-ptr a,b;
经过宏扩展后,变为int *a,b ,b就变成整型了。
Typedef有几个使用原则,它应该用在数组指针及函数的组合类型定义中,而且不要为了方便起见对结构使用typedef,那样唯一的好处就是省去了struct,但是也省去了它的提示作用,不利于程序的可读性。
Typedef之所以被称为存储类型符,只是为了语法上更加方便而已。
4、应该去阅读ANSI C标准,从中寻找乐趣与裨益。ANSI C标准6.3.2.2节,每个实参都应该有自己的类型,这样它的值经过实参与形参的一致性检验后就可以赋给形参。也就是说传参类似于赋值,所以如果写一个交换值的小函数,不应该直接传值,而是传递指针。
5、关于指针赋值。指针赋值有两个原则:一个是两个操作数都是指向有限定符或者无限定符的相容类型。另一个,左侧指针指向的类型必须具有右侧指针指向类型的全部限定符。
Char *cp;
Const char *cpp;
Cpp=cp;
而char **和const char ** 是不相容的,const只修饰char ,前者指向char * ,后者指向const char *,一个符号前加上const限定符只表示这个符号不能被赋值,也就是只读,而不是变成了常量,所以 const int a=2;case a 的用法是错误的。它的有用之处在于限定函数形参,这个函数将不会修改实参指针所指向数据的值。但是指针本身的值是可以改变的,即指针指向可以变化。
Const int *p =&a;
Int i=27;
P=&I;
6、C++令人失望的原因在于它对C语言存在的最基本的问题并没有什么改进,而它对C语言最重要的扩展—类,却建立在脆弱的C类型模型上。
7、NUL 用于结束一个ASCII 字符串,而NULL表示什么都不指向(空指针)
Malloc(strlen(str));几乎可以断定是错误的.
Malloc(strlen(str)+1);才正确,因为字符串容纳结尾的’\0’。
3、C缺陷
3.1多做之过
3.1.1Switch语句
Switch语句在使用过程中有无正确匹配项是没有提示的,运行时检查与C语言的设计理念相悖,按照C语言的理念,程序员应该知道自己在干什么,并且保证所作所为均正确。而且很容易漏掉每一项中必须要有的break,不然很容易造成“fall though”的问题。最大的一个缺点是case后边的语句始终被允许,无论它是不是有错。Switch语句使用时需要小心翼翼,导致它的使用率比较低。1990年12月20日美国长话网络大面积瘫痪,经过仔细检查之后,是由于switch中的一个break本意是跳出最内层的循环,却不小心跳出了switch语句。这跟一个价值2000万美元的bug最后发现时由于while(a=1)有的一拼。很多非常有名的C语言bug事件,都与C语言的脆弱性有关。
3.1.2相邻字符串常量的自动连接
ANSI C引入了另一个新特性,相邻字符串常量自动合并成一个字符串的约定。这就省去了过去书写多行信息是必须在行末加”\”的做法。
Printf(“xxxxxxxxxx\
Xxxxx\
xxxxxx”);
 现在可以这样书写
Printf(“xxxxxxxxxx”
“xxxxxxxxxx”
“xxxxxxxxxx”);
这也意味着,在字符串数组初始化的时候,如果漏掉了一个逗号,编译器不会警告,而是将字符串合并在一起。
缺省情况下函数名字的全局可见性
在之前的一篇文章中,讨论extern和static的用法,以及工程文件规范化时候,讨论过extern这个存储类型声明符就是个鸡肋,但是为了工程文件规范,还是要在头文件中将外部可利用的函数声明一下。如果限制对这个函数的访问,则必须加上static。事实上,几乎所有人都没有在函数名前加存储类型的习惯,但这种缺省容易导致错误,与inter position相互影响(用户编写和库函数同名的函数并取而代之)。
3.2 误做之过
这是指语言中有误导性质或者不适当的特性,有些与过于简洁有关,有些与操作符的优先级相关。
3.2.1符号重载
符号重载导致了C语言的符号作用域规则不那么清晰。
Static
修饰函数的内部变量,表示该变量在多次调用时能值能够保持。
修饰函数,表示本函数仅本文件可用。
修饰全局变量,表示该变量在本文件各函数调用中保持其值。
Extern
对于函数定义,表示全局可见(冗余)
对于变量,表示在其它地方定义。
Void
作为函数的返回类型,表示不返回任何值。
在指针声明中表示通用指针类型
位于参数列表中表示没有参数
*
乘法运算符
用于指针的间接引用
在声明中表示指针
&
位与操作
取地址操作符
=赋值 ==比较等
<=小于等于 <<=左移+赋值
( )
在函数定义中包围参数表
调用一个函数
改变表达式的运算顺序
强制类型转换
定义带参数的宏
包围sizeof操作符的操作数(如果它是类型名,若是变量则不加括号)让人误以为它是一个函数。
3.2.2 复杂的运算符优先级规则
. 优先级高于*
即*p.f 正确的含义是对p取偏移,再作为指针引用。
[ ]优先级高于*
Int *ap[ ]正确的含义是指向int类型的指针数组,即int *(ap[ ])。而int (*ap)[]指的是ap是指向int数组的指针。
( )优先级高于*
Int *f( )指的是f是一个函数,它返回的是一个指向int类型的指针int*
Int (*f)()意思是f是一个函数指针,该函数返回值类型是int
= =和!=优先级高于位操作符
(val&mask!=0)  意思是 val&(mask!=0)
= =和!=优先级高于赋值符
C=getchar( )!=EOF 意思是 c=(getchar()!=EOF)
算术运算符高于移位
msb<<4+lsb 意思是 msb<<(4+lsb)
逗号运算符在所有的运算符中最低
i=1,2;其实是i=1,2被丢弃。
C语言一个设计不当、复杂之处就是coder需要记住这些优先级规则,想告诉你的是,这是C不好,在表达式中如果有布尔操作、算术操作、位操作的混合计算,你应该之中保持加上括号的习惯,清楚明了,有专家建议,只记乘除高于加减,其它情况一律加括号。
优先级和结合性规则告诉我们,多个符号组成一个意群时,意群内部的计算顺序没有定义,如x=f()+g()+h()。大部分编程语言没有定义操作数的计算顺序,这样编译器可以充分利用自身架构的特点或充分利用寄存器来计算。
C语言的结合性没有一个非常明确的定义,它负责仲裁,几个优先级相同的操作符,先执行哪一个。比如:int a,b=1; c=2; a=b=c;那么a=1还是=2?答案是所有的赋值符都具有右结合性,从最右侧开始执行。位操作符&和|具有做结合性。
3.3少做之过
3.3.1 空格的作用
z=y+++x;
这个表达有两种解释,到底采用哪种?ANSI C规定了“maximal munch strategy”即:选取组成最长字符串方案,但如果是:z=y+++++x将会报错,因为编译器解释为:z=y++ ++ +x;这是编译错误。即使唯一有效的排版是z=y++ + ++x,它还是识别不了。
另一个体现空格作用的是ratio=*x/ *y,/和*之间要有空格,紧贴在一起被认为是注释部分,C++引入了//,但关于注释的失误仍然存在。
3.3.2一些代码能轻松通过编译,但运行起来就是垃圾
C语言确实很容易出现这样的错误。比如
Char *f(void)
{
Char buffer[20];

Return buffer
}
编译过程完全没有错误,但是你可能得到意料之外的结果,因为buffer是函数的局部变量,一旦函数结束,变量的生存周期就结束了。在C语言中自动变量(局部变量)在堆栈中分配内存,当包含该变量的函数或者代码块退出时,内存便收回了。解决问题的方法是,使用全局数组,但数组的值容易被其它函数改动,而且耗费空间,还可以在函数内部使用静态数组,浪费空间。最和谐的方法是使用malloc+free配合使用。
4、C语言声明
4.1  C语言声明概述
多年来程序员、老师、学生都在努力寻找一种更好的记忆方法来记清楚恐怖的C语言声明语法。声明与定义的界限是不明确的。声明相当于普通的声明,它所说明的并非出自自身,而是描述其它地方创建的对象。这样我可以为我所用而不分配空间,这样的声明可以有很多个。而定义可以看做是特殊的声明,它为对象分配内存(创建它),只能有一次。
Typedef char * string
String punchline =”I am a knot”;
C语言的声明模型是如此的晦涩,在60年代晚期,人们在设计C语言的这部分时,type model这个概念还很陌生,而且,C语言的祖先BCPL语言几乎没有类型,这属于它的先天缺陷。后来出现了一种设计哲学,要求对象的声明形式与它的使用形式尽可能相似。
一个int类型的指针数组,被声明为int *P[3],使用的时候用*P[i]。一个比较好的声明指针的方式是int &p,它至少能提示p是一个整型数据地址。
Char (*j)[20];
j=(char (*)[20])malloc(20);
const/volatile 左侧紧挨着*则是修饰指针,其它情况修饰指向的类型。如:
const int * grape
int const *grape  //前两者代表数据只读
int * const grape  //指针只读

函数的返回值不能是函数,所以foo( )( )是非法的。
但是函数的返回值可以是一个函数指针,如函数int(*fun())()
函数的返回值不能使数组,所以foo( )[ ]是非法的
但是返回值可以是数组指针,如int(*fun( ))[ ]
数组里不能有函数,所以a[]()是非法的
但是数组里可以有函数指针,如int(*a[ ])()
数组里允许有其它数组,如intfoo[ ][ ]
4.2  C语言中的组合类型声明
C语言中的组合类型主要有三个:结构体struct,联合union,枚举enum。
结构体声明:
Struct 结构标签(可选)
{
类型1:标识符1;

类型N:标识符N;
}变量定义(可选)
使用结构标签的好处是,声明结构体之后,可以使用struct+结构标签来声明结构体变量,这是很常用的。同类型的结构体变量可以直接赋值(可以作为数组整体赋值的实现方法)下面的例子可以说明
Struct s_tag {int a[100];};
Struct s_tag a,b,c;
Struct s_tag twofold(struct s_tag s)
{

Return s
}
Main()
{
a=twofold(b);
b=c;

}
若结构体中包含一个指向结构类型本身的指针,这种方法常用于链表(list)、树(tree)等其它动态数据结构。
Struct node_tag {
Int datnum;
Struct node_tag *next;
}
Struct node_tag a,b;
a. next=&b;
a. next->next=NULL;
联合
联合的外表与结构相似,但是内存布局上有关键性的区别,结构中,每个成
员依次存储,在联合中所有的成员都从偏移0开始存储,所以某一时刻只有一个成员有效。它的优点就是与结构有相同的外表,缺点是:优点不够出色。联合一般用来节省空间,因为有些项是不能同时出现的。结构比联合要常用的多。
枚举
通过一种简单的途径把一串名字与一串整型值联系在一起。很少有什么事情只能靠枚举来完成而不能用#define来解决的,早期的编译器省了它。
enum 可选标签 {内容}可选变量定义
enum sizes {small=7,medium,large=10,};缺省是从0开始。
4.3 C语言声明分析
用优先级规则分析(C语言声明的分析不分左右)
A 声明从名字开始读,然后按照优先级次序
B 优先级从高到底依次是
  B1 声明中被括号括起来的部分
  B2 后缀操作符 ()表示函数 [ ]表示数组
  B3 前缀操作符 * 表示指向…的指针。
C const/volatile 左侧紧挨着*则是修饰指针,其它情况修饰指向的类型。
Char * const * (*next)( )
这是一个叫next的指针,它指向一个函数,这个函数返回一个指针,这个指针指向一个char类型的指针,而且这个char类型的指针是只读的。
Char *(*c[10])( )
这是一个10个元素的指针数组,指针指向函数,函数的返回值为指向char类型的指针。
5、指针与数组
5.1 数组与指针的异同
很多人都认为数组名就是地址,其和指针的用法是相同的。很多教科书上对此也避而不谈,其实指针和数组有众多的不同之处,但单就应用的层面,指针和数组可以互换的情形更多,在表达式中使用,全部可以互换,因为他们在编译器中的最终形式都是指针。
Int a[10],*p,i=2;
P=a;
a[i]与p[i]或者*(p+i)或者{p=a+I;*p}都是相同的
char *p =”abcde”;  p[3]
但是有两点需要注意:
1、变量y可以代表地址,也可以代表地址里的内容,编译器根据上下文确定。X=Y;X是可修改的左值,是地址。Y则代表内容值。数组名虽然存储着地址(指向第一个元素),但不能直接赋值,属于不可修改左值。
2、与普通符号的直接访问不同,数组元素的访问时数组的起始地址加上偏移量确定的,地址在编译时确定,这属于直接引用。指针的访问需要去指针变量的地址中提取指向内容的地址,再访问指向的内容,这属于间接访问,多了一个提取地址的过程。
有意思的是,虽然函数传参的过程是传值,却没有办法将数组本身传递给一个函数,总是被自动转换为指针。利用指针传参的原因有两种,数组传值开销比较大,效率低,而且单纯的传值不能改变其值。所以,对于一维数组传参,函数声明你写成下面的三种形式都是没有问题的。
Fun(int *a )
Fun(int a[ ])
Fun(int a[100])
我们倾向于始终把参数定义成指针,这样是编译器内部使用的形式。
5.2 二维数组
有人声称C语言中没有多维数组,这是不对的。ANSI C 6.5.4.2定义了如果[ ]修饰符被多次使用则定义的就是一个多维数组。
C语言中定义和引用多维数组的唯一方法是使用数组的数组。所有的N维数组存储方式都是线性存储,绝对不是什么矩阵存储。所以对于a[i][j][k],如果你清楚自己在做什么,用[i][j][k]计算偏移,然后用一个单一的下标[z]来引用数组元素,当然这不是推荐的做法。
访问单个字符用a[i][j],在编译的时候解析为*(*(a+i)+j),这个原因跟数组的数组的概念和它的内存布局相关,根据数组的数组,存在两级数组,行向量数组及行向量内部。所有的多维数组都可以看做一维数组,每一级数组名都可以看做一个指针,指针变量的内容是起始地址。而对于指针最重要的概念是指向的类型及其步长。
对于int ap[2][3][5]
Int(*p)[3][5]=ap //行向量数组起始(当一维)
Int(*r)[5]=ap[i] //其中的一个行向量做为次级数组的首地址
Int *t=ap[i][j]
对于一维数组的定义和声明可以不指明元素个数,因为步长始终是1,但是对于2维或者多维数组,除了最左维可以不指定,其余维的长度必须指定,这样编译器才能计算出步长。
二维数组一般用来存储多个字符串。其实可以通过声明一个一维指针数组,每个指针指向一个字符串来取得与二维数组相似的效果,而且每个二级数组不要求长度相等,避免了空间浪费。也可以使用一维模拟二维。
6、数组传参
以一个实例说明:

#include
/*
1、二维数组的传参,必须要搞清楚指针的指向、步长及含义,同类型的指针才可赋值或传参,才能取到正确的数据,这是最重要的.

2、搞清楚数组的直接地址(或加偏移)访问方式和指针的间接地址访问方式(从指针变量中取得地址再加偏移)
   但编程层面不考虑这个。[]就是对地址取偏移后访问。指针和数组的互换性因为数组名a指向数组起始,它的值就是地址,
   它就是指针。对指针p加[]后,编译器会处理,先取指针的内容,再对内容加偏移,再取值。
   而真正的数组寻址,是数组的起始地址都是在编译阶段都确定的,直接加地址和偏移就可以访问。
   把数组名a当成指针来用是编译器的约定,实际上并没有一个名为a的指针变量

3、最最值得注意的是xx[],xx表示的是起始值。无论是a还是p,编译器均翻译成左值,即指针存储的起始地址。
   那么,当起始地址所在的指针没有专有符号表示的话,就必须取值,再用[],如a是指向数组的指针,a++指向下一个数组,
   我想取这个数组中某一个值的话就得(*a)[x].下面是一个例子
#include
main()
{
 char a[2][3]={{'a','b','c'},{'d','e','f'}};
 char (*b)[3];
 char c;
 b=a;
 c=(*++b)[1];
printf("the value is : %c \n",c);
b=a;
 c=(*(b+1))[1];
printf("the value is : %c \n",c);
b=a;
 c=b[1][1];
printf("the value is : %c \n",c);
b=a;
 c=*(*(b+1)+1);
printf("the value is : %c \n",c);

 

}
4、二维数组或者多维数组就是数组的数组(可以看作多级数组),且线性行存储,搞清楚每一级数组的步长,如:
   a[2][3][4] 把它看作一维的话,就是有两个[3][4]数组构成,也就是说int (*p)[3][4]=a;p指针步长为12
   继续深入下一级,每个[3][5]即每个a[i](数组名)有3个[4],也就是说int (*r)[4]=a[i] 步长为4
   继续深入下一级,每一个[4]即每一个a[i][j]有4个int 数据,也就是说int *t=a[i][j],步长为1
   1、根据二维数组的含义,我们在进行传参的时候可以当作二维数组传参(注意一定不能忽略步长计算即维数传递,
      否则出错)fun2 fun3
   2、也根据其存储结构可以用指向数组的指针传参,fun p[2]和a[2]所存储的值都是次级数组的地址,相当于数组名(指针),
      p[i][j]和a[i][j]都是取值
   3、也可以放弃二维数组,用指针数组模拟二维数组(可以实现锯齿多维),也可以用指针数组传参,fun1
   4、也可以放弃二维数组,完全使用一维,手动计算偏移
5、对于一维数组传参,编译器默认的是指针传递,不会进行值拷贝,所以你形参用char a[5]还是char a[]或者char *a
   都无所谓,会转换。一维传参的时候注意,数组和指针几乎都是可以通用的,访问方式的不同,在编程层面可以不考虑

*/
char fun(char (*a)[5])
{
 printf("fun:the char is %c \n",a[0][0]);//注意指针和数组名的互换性和在二维数组中a[i]是次级数组名,其存储着数组的首地址
                                         //我可以直接使用a[i]来表示该地址,也可以使用*(a+i)提取,然后加上[]取偏移。
 return (*++a)[0];//相当于*(*(a+1)+0)。因为a[i]也是指针,相当于*(a+i)

}
//char fun1(char *a[2])//a[i]或*(a+i)是指针变量的值,即要指向地方的地址,再加[]可以取到相应数
char fun1(char **a)//形式参数也可以使用指向指针的指针,这与指针数组的含义是一样的,传参是b相当于指向指针,而指针指向数组


 printf("fun1 the char is %c \n",a[0][0]);
 return (*++a)[0];
}
char fun2(char a[2][5])


 printf("fun2 the char is %c \n",a[0][0]);
 return (*++a)[0];
}
char fun3(char a[][5])//第一维省略是不要紧的,跟步长无关,只是没有参数来表示数组边界了


 printf("fun3 the char is %c \n",a[0][0]);
 return (*++a)[0];
}
void  fun4(char a[5])
{
printf("the value is %c \n",*(a+3));

}
void  fun5(char a[])
{
printf("the value is %c \n",*(a+3));

}
void  fun6(char *a)
{
printf("the value is %c \n",*(a+3));

}
main()
{
 char a[2][5]={{'a','b','c','d','e'},{'f','g','h','i','j'}};
 char *b[2]={"abcde","fghij"};
 char c;
 char d[5]={'a','b','c','d','e'};
 char *e="abcde";
 c=fun(a);
 c=fun1(b);
 c=fun2(a);
 c=fun3(a);
 fun4(d);
 fun5(e);
 fun6(e);

}

你可能感兴趣的:(重学C语言)