IE盒子

搜索
查看: 161|回复: 0

C语言笔记(上)

[复制链接]

3

主题

8

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2023-1-17 15:40:07 | 显示全部楼层 |阅读模式
期末了,整理整理笔记
标识符

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 ...
}这样当然就不会出问题了。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表