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"};
则是错误的。