曾经给我醍醐灌顶般感觉的C语言语法概念的理解、领悟和总结。
并不全面,但是希望能让大家对C语言的理解有所启发。
C语言的设计宗旨之一,“ Trust the programmer 信任程序员”,所以既然你要使用C语言就要好好学会它,别辜负了它对你的信任。
一般编程语言都有的语法基本框架,三种流程控制结构:
- 顺序结构
- 分支结构(if
,switch
)
- 循环结构(for
,while
)
goto
到底可不可以用?
可以用,那里用?跳出多层循环时用 goto
再好不过了。
为什么使用 switch
时必须要格式完整,其中的 case
,break
和 default
都不能省略?
switch(整数类型表达式)
{
整数类型表达式1:语句;break;
整数类型表达式2:语句;break;
……
default:语句;}
因为其实 switch
是用和 goto
类似的跳转实现的,case
其实就相当于一个个 goto
的跳转标签。一次跳转后就会连续执行后面的代码命令了,那如果后面还有其他 case
对应的代码也会被执行,这是不行的,所以为了防止这种情况则需要用 break 来跳出去。而 default 则是为了防止所有条件都不符合时,作为 goto
没有 case
可以跳转时的跳转位置。
究竟什么是变量?
变量:一段连续的,被命名了的用于存放数据的,且可以求得位置的具有类型含义的存储空间。
auto
为变量默认类别,同名变量作用域重叠时,局部优先。
如果需要要让某个变量只有在特定程序块中才可以使用和改变,可以在该模块内定义 static
变量。
char c = 'A';
字符变量 c
实际上是存放了一个通过隐式类型转换被转换成 char
类型的 int
型数值,即 'A'
对应的 ASCII码。
字符串是以第一个出现的空字符作为结束且包含该空字符的连续的字符序列,也就是说字符串末尾有个\0
强制类型转换是暂时的,只转换了当时表达式中的数据的类型。
必须要了解浮点数的生成原理,要知道有些小数是永远不可能精确表示的。所以在循环分支结构中不要把对浮点数进行 ==
和 !=
运算的结果作为判断条件。尤其不要直接用 ==
来判断浮点型数是否为 0
贴一个知乎上看到的不错的关于浮点数的博文链接:《浮点数解惑》http://t.cn/hBcDTU
逗号表达式的值是最后一个分式的值。
C语言代码中=
念“赋值为 ”,而==
才是“等于”
关于运算操作符优先级的意义:
高优先级运算符先选择对象并进行运算操作,得到运算结果给低优先级运算符作为运算对象,而()的作用就是把其内的表达式的运算结果作为一个对象。
<
,>
,<=
等等是关系运算符,是运算就一定每一步都有运算结果,且其结果只为 0或 1
%
运算只对整型数据
当表达式中有 i++
时,它的具体操作是用 i
的原值完成参与的运算完后,再把 i+1
的值赋值给 i
,而 ++i
则相反。
关于 i++++i
(中间无空格)是如何算的:
只想说C编译器一般会有这样一个贪心法则:构成操作符的字符个数越多越好,假如编译器是从左到右分析的话就有 i++ ++ +i
(当然,这是个不合法的表达式)
如何理解多维数组?
数组元素可以是任何类型的对象,也可以是另外一个数组,那就可以一层一层的了。BTW,如果对象是变量的地址(指针)或者初始化的时候直接写了几个字符串(如下)这就是指针数组了。
char \*a[]={"doubi","jindoubi","hehe"};//存进去的是字符串首地址
更多关于数组的:
int a[10];
a[0] = a[9];
[ ]
在数组定义和数组元素表示时的意义是不同的,前者为类型说明符后者为运算符,
作为算符 a[10]
解释为*(a+10)
(所以说数组元素写成 0[a]
其实是可以的。。。),因为数组地址是从 a+0
开始的,而 a+10
这个地址已经超出定义的范围了,所以定义数组时有 int a[10]
,但是实际数组元素只有 a[0]~a[9]
再说一下数组为什么从 0 开始的理解:
a[0]
,[ ]
中的数实际表示的是地址偏移量,偏移量是从0开始的,这是实际的设计出发点(从C语言的前身之一BCPL语言中继承而来)。
从算法方面考虑,方便了数组使用时的边界定位问题。不对称入界点和出界点,x>=0
,x<10
,包含入界点,不包含出界点(边界不对称)。
详细请见:知乎链接http://t.cn/RvRwOTu
指针变量是一类数据类型,有 int,double,char 等等类型,不同类型指向的内存大小不同,而其实质就是地址编号。
*
和 &
,互为逆运算,*
间接访问操作符,操作对象需为指针变量;&
是取址操作符,操作对象为变量(当然也包括指针变量)
*&a=25
就是a=25
&a
的值是个指针常量
int *ip;
// ip
为 int
型指针变量,这里主要是要注意格式(*
和 ip
靠在一起)和其意义(*ip
代表一个 int
型变量,而 ip
是一个 int
型指针变量,它存放着某个 int
元素的地址),没有所谓的 int*
类型,int *a,*b;
//这样是可以成功定义出指针变量 b
的
ip++;
//实际上是地址加了 sizeof(int)
4位 。
关于指针和数组名:
int a[10];
int *p;
p=a;
sizeof(p)
,只会求得指针变量 p
所占字节数,而 sizeof(a)
则会求得数组总共所占字节数,这是数组名与指针变量的区别之一,另一个区别是数组名应该算是个指针常量即代表该数组首地址的值,它是不能改变的,比如就不能有a++
了。
结构体为程序员提供了一种封装一组相关数据元素的简便办法。
同类结构体可以用=
来赋值。
结构体里可以设置指向自己类型的指针变量,链表就是靠这个实现的。
结构体大小要用 sizeof()
来获得,因为有对齐优化,所以结构体成员降序排列来定义结构体可以节省空间但是会影响访问速度,所谓不可兼得啊。。。
你要知道一个关于函数的概念:
函数的副效应,C语言中的函数除了 void
类型以外都会有一个函数执行完毕的返回值,但是函数的作用并不一定在于求得这个返回值。比如 printf()
函数,其实它也有返回值,返回成功输出的数据个数,但是他的作用其实是为了输出而不是计算成功输出的数据个数,这里输出数据到屏幕就是 printf()
函数的一个副效应。
函数参数问题:
传值调用和传址调用,关键在于是否需要改变实际参数的值。
函数指针和指针函数:
int (*f) (int x); //定义函数指针
int *func(int x);//定义指针函数
int *p;
int n;
f = func; //将 func 函数的首地址值赋给指针变量 f,也可以写成 f = &fun
p = fun(a);//将 func 函数的返回值(也是个指针变量)赋给指针变量 p
而此时 f
也代表了 func
函数,所以上一句也就可以写成:
p = f(n);//用 f 来表示 func
函数指针声明时用 ( *f
) 作为函数名,表示进行 *
运算后获得实际函数,即 f
表示的是一个函数的地址(要明白函数名的意义其实是函数的地址,从这里可以一窥函数调用实现的本质),使用函数指针代替函数的要求是被指的具体函数的参数类型要和函数指针定义中对应的参数类型相同。
而指针函数,即返回值是一个指针变量,定义时函数名左右没有(),表示定义了一个进行函数调后获得的函数返回值将这个值进行 *
运算能够获得的对应的类型数据的指针变量(所以说,C 中定义函数时其实是在定义函数的返回值)
calloc()
会用 0 初始化申请的内存,malloc()
不会。
printf()
和 scanf()
中的 f
其实是表示 formatted 格式化,即格式化输入输出,
它们的不定参数输入输出是由一个宏实现的,具体可看标准函数库中他们的定义。
记忆输入输出相关知识的关键是要理解流结构 FILE
的指针变量和与之相关的功能函数。
gets()
没有设置缓存区大小,所以正式程序里是不用的(关于这个的原因可以去了解一下缓冲区溢出攻击)
main函数要尽量地短小清晰。
随机函数 rand()
输出为 int
类型,
使用时需要设置种子,srand((unsigned int)time(NULL))
执行系统控制台命令:
void system(char \*command)
关于标准库的使用:
会使用一个程序语言的库函数(或者是模板库,优秀的框架等等)是学会一门语言的必要条件。
对了,最近学了一点汇编语言以后感觉到C语言就像是一种通过编译器实现的精妙的宏汇编。
啊,还有C语言的宏预处理,建议尽量简单的用用就好,这东西一复杂就容易出问题。
最后了,想说:好喜欢C语言啊~
参考书目:
《C语言入门经典》,这本其实讲的都是些比较基础的,只是想说一下,第一次撸完了一本书里的代码。好吧,其实后几章的大多都是是抄的。。。
《狂人C》,一本不错的国人写的书
《C和指针》,经典,经典,经典!
《C陷阱与缺陷》,貌似后面的几章当时还没看懂的,经典
《C语言解惑》,一些好玩的代码,深入透彻的解析,经典
《C专家编程》,也算经典吧,写得很好玩
《C标准库》,根本看不懂其实。。。
另,郝斌老师的C语言&数据结构教学视频不错的