|
期末了,整理整理笔记
标识符
C语言源程序的最小单位是token(有不同意见)。Token包括关键字、标识符、常量、字面量、符号等。
C语言的标识符比较常规(大部分语言也是同样的规则),可用字母和数字,大小写敏感,数字不能开头,唯一允许的符号是下划线_。
几个常用的命名规范。
驼峰
驼峰命名有大驼峰(也叫帕斯卡命名)和小驼峰之分。
一般来说大驼峰就是每个单词首字母大写,比如 LayoutInflater。大驼峰命名法常用于类名,有些函数和宏为了看起来像是一个类(或者有其它作用)也会用大驼峰命名法,Jetpack Compose 就经常把 @Composable 注解的函数用大驼峰起名,比如 CenterAlignedTopAppBar。
小驼峰命名法可以当作首字母小写的大驼峰,比如 layoutInflater,一般用作变量名、函数名等。一个常用的用法是 LayoutInflater layoutInflater,也就是大驼峰用作类名,小驼峰用作对应类型的变量。
驼峰命名时常量一般用全大写加上下划线分隔的形式,比如 JQ_DEBUG_TRACE_DETAIL,常用于常量,宏,枚举等。 驼峰命名有一个缺点或者叫不足,就是对于全大写名如 IOS 比较难处理,容易引起误会和分歧,比如 isIOSUser 和 isIosUser 都是合法标识符,但是不是同一个标识符,一般团队规范中会规定解决方法。
匈牙利(不要用)
匈牙利命名的特点时可以之间看出变量类型,它遵循匈牙利法则,即 属性+类型+描述。常见规则有:
g: 全局变量
m_: 类成员变量
s_: 静态变量
c_: 常量
b: 布尔类型
sz: 以零结束的字符串
p: 指针
n: 整型
dw: 双字
l: 长整型
u: 无符号
fn: 函数例如 m_nCustomers。微软比较喜欢用这个。
蛇形
也叫下划线命名,全部用小写,所有单词之间用下划线 _ 分隔,python 官方推荐。比如 seen_default。
注意
- 普通变量不要用下划线开头,大多数语言中下划线开头的标识符有特定用处,比如C语言中以下划线开头的一般是私有标识,正常情况下外部不应调用,比如_assert;python中有些前后用双下划线的标识符有特殊含义,比如 __main__。
- 如果准备把代码分享给别人用,而使用的语言又没有命名空间之类的概念,可以在标识符前面加上一些特殊字符,防止冲突,可以是这个代码库名,如 jq_set_input_cb ,其中jq就是这个库的名字。
- 在不超过限制的情况下,尽可能完整而简洁地描述这个标识符代表的东西。一般函数/方法等用动词,变量/类等用名词。绝对不要用 num1, num2 这种变量名。
- 少用缩写,如果一定要用,查一下有没有公认的缩写,千万不要出现 analysis 缩写成 anal 这种情况。
- qian_wang_bu_yao_yong_ping_ying_ming_min
- ping_cuo_le_heng_diu_lian
- mei_cuo_shuo_de_jiu_shi_wo
- er_qie_heng_nan_du_dong
- ni_du_zhe_bu_lei_ma
保留字/关键字
以下是C语言规定的保留字,这些是C语言使用的标识符,一般程序中不能使用。
类型
- int, long, short, float, double, char, void
- 基础类型
- void 是无类型,NULL是空指针,不要搞混了
- char 是一种特殊整数
- unsigned, signed, const, volatile
- 类型修饰
- unsigned指无符号,而signed为有符号,用在基础类型前,仅用于整数
- const 用于声明常量,即不可变的变量(只读变量)。这依然是变量,因此C中const int n = 5; int num[n]; 仍然是不允许的。
- const作用规则是默认作用于左边,否则作用于右边,后面解释。
- enum, struct, union
- 构造类型
- enum其实是基本类型,不过这里放在构造讲
- 数组也是构造类型
语句
- if, else, switch, case, default
- 流程控制
- if else多层嵌套不要超过3层
- 尽量用大括号,原因在后面
- switch case default结构要完整,case可以提取函数出来
- 如果不好提取函数,尽量加大括号,原因后面讲
- do, while, for, continue, break
- 循环
- for(;;)等于while(1),都是死循环
- continue break只能跳出单层循环,最好在这两个后面加注释
- return
- 返回值
- 函数里面可以直接return来跳出多层循环
- typedef
- 定义新类型
- 经常和struct union enum 一起用
- 注意struct里面不能用本身类型名,因为这里面没有定义这个类型,要用struct的标签,两个名字可以一样
- goto
存储类型
- auto, register, extern, static
- 自己看C和指针第3章,比我讲的好一万倍
- register不一定会生效
- static用在文件中影响作用域
- 函数也能用static extern
sizeof
const
const声明的常量是只读变量,不是真的常量。如果只是单独定义一个字面常量值,考虑能不能用宏,如果不能,再用const。
const比较重要的用法是函数参数,用来防止参数值被改变,一般这时候都是传的指针,所以const位置比较麻烦,规则是默认作用于左边,否则作用于右边。
对于普通的变量,const位置前后没有多大影响,比如:
const int LIST_LEN = 10;
int const ARRAY_SIZE = 10;这里LIST_LEN ARRAY_SIZE都是int类型的只读变量。第一行const左边没有可修饰的类型,所以作用于右边,使这个int只读;第二行const左边是int同样使这个int只读。
int a = 10, b = 5;
const int * p1 = &a;
int const * p2 = &a;
int * const p3 = &a;
int const * const p4 = &a; // 考虑用宏这里第三行和第二行是一样的,p1 p2等价,都是指向整型常量的指针,而指针本身可以改,p1 = &b p2 = &b都可以,但是*p1 = 5就不行;第四行是常量指针,指向一个int,指针本身只能指向这个地址,但是地址内容可以改,p1 = &b p2 = &b都不行,但是*p1 = 5就可以;第五行是上面两个的结合,都不能改。
volatile
volatile表示这个变量随时可能改变(外界因素)。有些C语言的编译器会对变量读取做优化,如果这时候有其他情况改动这个变量,C程序不一定能知道,volatile告诉编译器不要对该变量优化,这样每次用到变量就会去对应内存读取而不用缓存的数据。volatile和const是可以一起用的,因为volatile只是告诉编译器这个变量可能会变,而const是告诉本程序不要修改这个变量,不影响其他程序修改它。
多线程编程中有时会出现多个线程操作同一变量的情况,这时候就要将其声明为voaltile,避免数据更新问题等。
enum
enum主要解决的是常量可读性的问题。有时候会用常量代表某个特定意义,比如某个函数要一个int position参数,0代表左边,1代表中间,2代表右边。数目少的时候直接用宏没什么问题,但是如果数目太大,用宏不易管理,还容易出错,于是就可以用枚举。
定义枚举的语法是
enum EnumTypeName {
ENUM_TYPE_A, ENUM_TYPE_B, ENUM_TYPE_C
}一般枚举类型名用大驼峰,每个元素命名和宏的规则一样。
枚举值默认从0开始,往后递增1,但是也可以自己指定;如果指定第一个值,后面会自动向后递增,指定的这个元素不一定是第一个,可以从中间开始指定。
enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY = 5,
FRIDAY,
SATARDAY,
SUNDAY
};
printf("%d %d %d %d %d %d %d\n", MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
FRIDAY, SATARDAY, SUNDAY);
// 结果是: 0 1 2 5 6 7 8如果把上述的5改成0,得到的结果就会是0 1 2 0 1 2 3。
struct
struct一般与typedef一起用,注意这个结构体内部不能用typedef定义的名字:
typedef struct {
int num;
LinkedListNode* next;
} LinkedListNode;这么定义是错误的,在第三行定义next时,LinkedListNode还没有定义,直到第四行后它才被定义,在此之前都不能使用。正确做法是:
typedef struct LinkedListNode {
int num;
struct LinkedListNode* next;
} LinkedListNode;注意着两个LinkedListNode不一样,实际上结构体的标签也可以起成别的,不过一般都是一样的。
另外在函数中,如果要使用struct作为参数,最好传指针进去。由于C语言所有函数参数都是按值传递(后面会提到数组其实也是按值传递而不是按址传递),直接把结构体传进去会把它复制一份,开销比较大。如果要防止原本的结构体被修改,可以定义成const,比如int getValue(const LinkedListNode* node)。
struct的大小也不只是所有成员大小之和,详见C和指针第10章。
union
union平时见的不是很多,不过用好了有很大作用。union用法是:
union UnionName {
type1 var1;
type2 var2;
type3 var3;
} union1, union2;同样可以和typedef一起用。union的作用是使多个变量同时使用同一片内存。对于union内部各种不同类型成员,编译器会为union分配其中最大的一片内存。假如上述三个类型的大小分别是8 4 1,那么这个联合体大小就是8。在使用时,不同成员会按照自己所需的内存来使用。比如当union1.var1 = t1时,所有8个字节都用上了,而当union2.var2 = t2时,只用了type2所需的大小,也就是前4个字节。
使用union的优势是减少内存使用。当struct中存在多个不同类型,但是同时只会有其中一个有意义(被使用)时,union就能减少内存占用,比如上面的union如果分开写在结构体中,至少需要13字节,而用union实现,只要8字节。
比如在写编译器时,用一个结构体表示变量,定义如下:
typedef enum VariableType { INT, FLOAT, STRING, BOOLEAN } VariableType;
typedef struct Variable {
VariableType type;
int integer;
float number;
char* str;
int boolean;
} Variable;如果int float char*都占4字节,那么这个结构体大小就是20字节。但是我们知道,只有type为特定值的时候,对于的值才会用上,而其他空间都是浪费的。使用union:
typedef enum VariableType { INT, FLOAT, STRING, BOOLEAN } VariableType;
typedef struct Variable {
VariableType type;
union {
int integer;
float number;
char* str;
int boolean;
} value;
} Variable;可以把空间占用降低到8字节,并且代码更加易读。
if/else
if/else的问题在于注意悬挂else。if/else后面是可以直接跟但语句的,这时候可以不写花括号,都是一旦出现嵌套,最好要保证除了最内层if/else以外,都写上了花括号,否则容易出现悬挂else。
if (xxx)
if (xxx)
xxx
else
xxx一个简单的判断,看起来没问题,但是代码实际意义与缩进给人的感觉并不一样。实际上它应该这么理解:
if (xxx) {
if (xxx){
xxx
} else xxx
}但是很多人会把它当作:
if (xxx) {
if (xxx) xxx
} else xxx原因是else与缩进无关,它只会匹配同作用域中与它最近的if。
另一个可能的原因是if后面直接接的应该是一句,而很多人把if else理解成了两句。实际上if else是一个完整的结构,应该被视为一句。类似的感觉还有花括号,很多新手不理解为什么if后面加上花括号就能写多句代码,在这里也可以理解成花括号把内部所有的代码包装成一句代码,那么就符合if else的使用规范了。
同样的这也可以解释else if为什么可以这么写。
switch/case/default
switch也是一个完整的结构,在使用中我们要尽量保持完整,最好覆盖所有可能并且写好default。
switch让很多新手疑惑的是switch内部定义变量:
Week week = MONDAY;
switch (week) {
case MONDAY:
int isHoliday;
monday();
break;
case TUESDAY:
tuesday();
break;
default:
other();
}如果这么写,必然报错,但是让人疑惑的是,如果给第一个case加上花括号,报错就会消失:
case MONDAY: {
int isHoliday;
monday();
break;
}如果用goto来写一个类似switch的代码,就好理解了:
{
if (week == MONDAY)
goto __week_CASE_MONDAY;
else if (week == TUESDAY)
goto __week_CASE_MONDAY;
else
goto __week_SWITCH_DEFAULT;
__week_CASE_MONDAY:
int isHoliday;
monday();
goto __END_OF_SWITCH;
__week_CASE_TUESDAY:
tuesday();
goto __END_OF_SWITCH;
__week_SWITCH_DEFAULT:
other();
__END_OF_SWITCH:
other();
}这段代码和上面第一个switch是一样的(花括号对应switch的花括号),如果尝试编译运行,你会发现编译器甚至是报同样的错,从这里看,你甚至可以把switch看作这段代码的语法糖。那么在这里,实际上goto并不影响代码,它影响的是执行的顺序。当week = MONDAY,代码执行并没有问题;然而如果week = TUESDAY,这时代码直接跳转到__week_CASE_TUESDAY:的位置执行,然而我们说过,goto不影响代码,这里所有代码都在同一个作用域中,__week_CASE_TUESDAY:之后的代码完全可以用这个变量,但是在goto的影响下,int isHoliday;并没有执行,变量没有定义就使用,当然会造成错误。因此为了避免出现错误,C语言不允许在case内部直接定义变量。而要想在case中定义变量,就需要缩小其作用——用花括号。
另外这还能解释为什么在高版本gcc中要求default:标签之后不能为空,至少要有一个空语句;,因为goto并不能直接跳转到作用域尾端。
如果你一定要在switch内定义变量,又不想给case加花括号,那么可以提取一个函数出来。当然要是函数也不想提取的话,有种神奇的办法:
switch(week) {
int isHoliday;
case ...
}理解一下的话,它类似于:
{
int isHoliday;
if ...
}这样当然就不会出问题了。 |
|