C 语言学习笔记
本文记录阅读《The C Programming Language Second Edition》时的一些笔记,对之前所学查漏补缺。
数据类型,运算符和表达式
基础类型和类型修饰符
short和long实际是类型修饰符,它们都可修饰int,long还可修饰double。- 在声明变量时单独用
short和long实际是short int和long int的简写。 signed和unsigned仅用于修饰int
字面量
- 长整型字面量以
l或L结尾,无符号字面量以u或U结尾,无符号长整型字面量则是以ul或UL结尾 - 浮点数字面量不带后缀默认为
double,带f或F后缀为float,带l或L后缀为long double - 八进制字面量以
0开头,十六进制则以0x或0X。它们也可与上面的组合,如0XFUL - 字符字面量也可用八进制或十六进制表示,如
'\013','\xb' - 相邻的字符串字面量会自动合并,
"hel" "lo"等同于"hello",因此可将过长的字符串写在多行
枚举
1 | |
- 如果未显式指定枚举的值,则会自动赋值。第一个值默认为 0,其他的依次递增。如果只有部分赋值,其他未指定的从指定的值开始递增
不同枚举类型的枚举值名不能相同,下面的代码中
Tomato重复定义了1
2enum Fruit {Tomato, Apple};
enum Vegetable {Tomato, Cabbage};一个枚举类型中的枚举值可以相同,如
enum Color {Red = 0, Pink = 0, Green = 1};是正确的
声明、定义和初始化
1 | |
- 声明只指定变量类型,没有分配内存
- 定义会分配内存,定义同时也充当声明
- 可在定义的同时初始化,初始化会分配内存并赋值
- 如果一个变量不是自动变量(在函数内部定义的局部变量),则只会初始化一次,且只能用常量表达式初始化
- 外部变量和静态变量会默认初始化为
0 - 自动变量未初始化时有未定义的值
- 外部变量只能有一个定义
const修饰的变量的值不可直接改变,因此需要在声明的同时初始化,否则后面无法赋值const修饰数组时说明不能修改数组元素的值,const修饰函数的参数时指定函数不会修改此变量1
2
3
4const int arr[3] = {1, 2, 3};
int b[3];
arr[0] = 0; // 错误,数组元素不可变
arr = b; // 错误,数组名本身是一个指向数组首元素的指针常量staic修饰的全局变量或函数只能在当前文件内访问staic修饰的自动变量的值在多次调用时保持上次的值1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <stdio.h>
void counter() {
static int count = 0; // 局部静态变量
count++;
printf("Count = %d\n", count);
}
int main() {
counter(); // 输出 Count = 1
counter(); // 输出 Count = 2
counter(); // 输出 Count = 3
return 0;
}register只能修饰自动变量,编译器会尝试将变量存在 CPU 寄存器中。寄存器变量不可取地址
运算符
%不可用于float和double- 如果二元运算符的操作数类型不同,会将低级类型转换为高级类型
<<会在右侧填充0,>>会在左侧填充符号位或0(取决于机器)- 赋值运算符将右侧看作表达式,因此
x *= y + 1等价于x = x * (y + 1) - 赋值表达式的值是赋值后的值,如
x = 3的值为3 - C 没有规定运算符的操作数的求值顺序
1 | |
优先级列表
优先级从高到低,相同优先级的运算符按照结合性从左到右计算。
| 运算符 | 结合性 |
|---|---|
() [] -> . |
从左到右 |
! ~ ++ -- + - * & (type) sizeof |
从右到左 |
* / % |
从左到右 |
+ - |
从左到右 |
<< >> |
从左到右 |
< <= > >= |
从左到右 |
== != |
从左到右 |
& |
从左到右 |
^ |
从左到右 |
\| |
从左到右 |
&& |
从左到右 |
\|\| |
从左到右 |
?: |
从右到左 |
= += -= *= /= %= &= ^= \|= <<= >>= |
从右到左 |
, |
从左到右 |
一元的 +(正号)、-(负号) 和 *(解引用) 的优先级高于对应的二元运算符。
预处理器
#define 和 #undef
1 | |
#undef用于取消宏定义- 宏可以带参数,实现类似函数的操作,但和
++或--一起使用时要注意。如max(x++, y++)实际被替换为((x++) > (y++) ? (x++) : (y++))和期望的结果不同 #会将宏参数转换为字符串,如dprint(x + y);会替换为printf("x + y" " = %g\n", x + y);##会将参数拼接起来,paste(var, 1) = 10;会替换为var1 = 10;
条件宏
1 | |
#ifndef 判断宏是否定义,#if 判断表达式值是否非 0
指针和数组
- 指针定义设计为
int *p这样,意在表明表达式*p是int类型 p+1会指向下一个元素,实际加的是指针所指向类型的大小- 数组名实际是指向第一个元素的指针,但它不可变
a[i]等价于*(a+i),这对指针也适用。因此a[3]等价于3[a]- 在函数参数列表中数组和指针等价,是可变的
内存区域
- 程序运行时的内存区域分为栈、堆、全局 / 静态存储区和常量区
- 栈用于存放函数的局部变量和函数调用的参数,栈的大小是有限的
- 堆用于存放动态分配的内存(如
malloc),堆的大小是不定的,需要手动释放分配的内存 - 全局 / 静态存储区用于存放全局变量和静态变量,全局变量在程序运行期间一直存在,静态变量在函数调用结束后不会被释放
- 常量区用于存放常量字符串和全局常量,常量区的内容在程序运行期间不可变
1 | |
合法的指针运算
- 指针和
int的加减法,如p+1 - 指向同一数组内元素的两个指针的减法和比较
字符串
- 字符串实际是一个字符数组,以
\0结尾,因此占用的空间总比引号中的字符数多 1 - 字符串赋值或传参的是指向第一个字符的指针
下面两种定义方法是不同的,
amessage是一个数组,其大小刚好容纳字符串,而pmessage是指向字符串常量的指针。amessage不可变,但可通过它修改字符数组的值,而pmessage可变,但不能通过它修改字符串的值。amessage位于栈中,pmessage指向的字符串在常量区1
2char amessage[] = "now is the time";
char *pmessage = "now is the time";
其他
- 二维数组作为参数传递时必须指定列元素的数量,一般地,多维数组只有第一个索引可以省略
void *表示一个指针,该指针可以指向任意类型的数据,void *不可直接解引用,必须转换成正确的类型后使用- 函数指针
int (*p)(int, int)的使用方法为(*p)(1, 2)
复杂定义
运算符优先级:() = [] > *
识别方法:从变量名称出发,按照优先级和运算符结合判断类型,如 p() 是函数,p[] 是数组,*p 是指针。
1 | |
关于上面声明的详细判断,参考 C 语言中的复杂指针声明。
结构体
结构体声明语法如下,在声明后可直接定义该结构的变量。如果不给出结构体名,则只能在声明时定义变量。
1
2
3
4struct [struct_name] {
member_type member_name;
...
} [variable_name];如下面的代码定义了
struct point类型,并给出初始化方法。1
2
3
4
5
6struct point {
int x;
int y;
};
struct point pt = {1, 2};- 访问结构体成员使用
.,如pt.x .和->的优先级高于*,所以在使用结构体指针时需注意。1
2
3
4struct point p, *pp = &p;
(*pp).x = 1; // 正确
*pp.x = 1; // 错误
pp->x = 1; // 正确声明结构体类型后其使用方法就和基本类型一样了,如定义结构体数组
1
2
3// 两种定义方法等价,但第二种更清晰
struct point pts[3] = {1, 2, 3, 4, 5, 6};
struct point pts[3] = {{1, 2}, {3, 4}, {5, 6}};- 利用
sizeof运算符可以获取结构体数组的大小,如sizeof(pts) / sizeof(pts[0])或sizeof(pts) / sizeof(struct point) - 结构体的大小并不一定是其成员大小之和,编译器可能会在成员之间填充字节以对齐,以提高访问速度
可以指定结构体成员的比特位数,如
unsigned int x: 1;,这样x只能存储一个比特位。这常用来定义标志位,在使用时比宏定义和位运算的方式更加清晰。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 宏定义方式
#define KEYWORD 01
#define EXTERN 02
#define STATIC 04
unsigned flags = KEYWORD | STATIC; // 设置关键字和静态标志
flags &= ~KEYWORD; // 清除关键字标志
if((flags & (EXTERN | STATIC))==0) // 检查是否同时没有外部和静态标志
// 结构体方式
struct {
unsigned int is_keyword: 1;
unsigned int is_extern: 1;
unsigned int is_static: 1;
} flags;
// 设置关键字和静态标志
flags.is_keyword = 1;
flags.is_static = 1;
// 清除关键字标志
flags.is_keyword = 0;
// 检查是否同时没有外部和静态标志
if(flags.is_extern == 0 && flags.is_static == 0)
typedef 关键字
typedef 用于定义类型别名,可以简化复杂的类型声明,提高代码可读性。
1 | |
联合体
联合体是一种特殊的结构体,所有成员共享一个内存空间,即成员的偏移量都从 0 开始。因此联合体的大小是其成员中最大的那个成员的大小。
1
2
3
4
5union u_tag {
int ival;
float fval;
char *sval;
} u;- 联合体成员的访问方法和结构体一样,使用
.或->,但只有一个成员的值是有效的。 给其他成员赋值会影响到当前成员
1
2
3
4
5
6u.ival = 10;
printf("%d\n", u.ival); // 输出 10
printf("%f\n", u.fval); // 输出 0.000000
u.fval = 3.14;
printf("%d\n", u.ival); // 输出 1078523331
printf("%f\n", u.fval); // 输出 3.140000联合体只能用第一个成员的类型来初始化,如
union u_tag u = {10};。union u_tag u = {3.14};会被截断为3,而union u_tag u = {"Hello"};则是错误的。