C 语言学习笔记

本文记录阅读《The C Programming Language Second Edition》时的一些笔记,对之前所学查漏补缺。

数据类型,运算符和表达式

基础类型和类型修饰符

  • shortlong 实际是类型修饰符,它们都可修饰 intlong 还可修饰 double
  • 在声明变量时单独用 shortlong 实际是 short intlong int 的简写。
  • signedunsigned 仅用于修饰 int

字面量

  • 长整型字面量以 lL 结尾,无符号字面量以 uU 结尾,无符号长整型字面量则是以 ulUL 结尾
  • 浮点数字面量不带后缀默认为 double,带 fF 后缀为 float,带 lL 后缀为 long double
  • 八进制字面量以 0 开头,十六进制则以 0x0X。它们也可与上面的组合,如 0XFUL
  • 字符字面量也可用八进制或十六进制表示,如'\013','\xb'
  • 相邻的字符串字面量会自动合并,"hel" "lo" 等同于 "hello",因此可将过长的字符串写在多行

枚举

1
enum Direction { North = 1, East, South, West };
  • 如果未显式指定枚举的值,则会自动赋值。第一个值默认为 0,其他的依次递增。如果只有部分赋值,其他未指定的从指定的值开始递增
  • 不同枚举类型的枚举值名不能相同,下面的代码中 Tomato 重复定义了

    1
    2
    enum Fruit {Tomato, Apple};
    enum Vegetable {Tomato, Cabbage};
  • 一个枚举类型中的枚举值可以相同,如 enum Color {Red = 0, Pink = 0, Green = 1}; 是正确的

声明、定义和初始化

1
2
3
4
extern int a, arr[];  // 声明,数组大小在声明时可不指定
int b, d[10]; // 定义,数组大小在定义时必须指定
char c;
int x, y = z = 0; // x未初始化,y和z都初始化为0,这是因为表达式z=0的值为0
  • 声明只指定变量类型,没有分配内存
  • 定义会分配内存,定义同时也充当声明
  • 可在定义的同时初始化,初始化会分配内存并赋值
  • 如果一个变量不是自动变量(在函数内部定义的局部变量),则只会初始化一次,且只能用常量表达式初始化
  • 外部变量和静态变量会默认初始化为 0
  • 自动变量未初始化时有未定义的值
  • 外部变量只能有一个定义
  • const 修饰的变量的值不可直接改变,因此需要在声明的同时初始化,否则后面无法赋值
  • const 修饰数组时说明不能修改数组元素的值,const 修饰函数的参数时指定函数不会修改此变量

    1
    2
    3
    4
    const 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 寄存器中。寄存器变量不可取地址

运算符

  • % 不可用于 floatdouble
  • 如果二元运算符的操作数类型不同,会将低级类型转换为高级类型
  • << 会在右侧填充 0>> 会在左侧填充符号位或 0(取决于机器)
  • 赋值运算符将右侧看作表达式,因此 x *= y + 1 等价于 x = x * (y + 1)
  • 赋值表达式的值是赋值后的值,如 x = 3 的值为 3
  • C 没有规定运算符的操作数的求值顺序
1
2
3
4
5
6
/* f和g的求值顺序是不定的,如果f或g改变了另一个函数所依赖的变量,
* 那么x的值会依赖于f和g的求值顺序
*/
x = f() + g();
// 函数参数的求值顺序也是不定的
printf("%d %d\n", n, power(2, n));

优先级列表

优先级从高到低,相同优先级的运算符按照结合性从左到右计算。

运算符 结合性
() [] -> . 从左到右
! ~ ++ -- + - * & (type) sizeof 从右到左
* / % 从左到右
+ - 从左到右
<< >> 从左到右
< <= > >= 从左到右
== != 从左到右
& 从左到右
^ 从左到右
\| 从左到右
&& 从左到右
\|\| 从左到右
?: 从右到左
= += -= *= /= %= &= ^= \|= <<= >>= 从右到左
, 从左到右

一元的 +(正号)、-(负号) 和 *(解引用) 的优先级高于对应的二元运算符。

预处理器

#define 和 #undef

1
2
3
4
5
#undef PI
#define PI 3.14159
#define max(A, B) ((A) > (B) ? (A) : (B))
#define dprint(expr) printf(#expr " = %g\n", expr)
#define paste(front, back) front ## back
  • #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
2
3
4
5
6
7
8
9
#ifndef DEBUG
#define DEBUG 1
#endif

#if DEBUG
printf("Debug mode is enabled.\n");
#else
printf("Debug mode is disabled.\n");
#endif

#ifndef 判断宏是否定义,#if 判断表达式值是否非 0

指针和数组

  • 指针定义设计为 int *p 这样,意在表明表达式 *pint 类型
  • p+1 会指向下一个元素,实际加的是指针所指向类型的大小
  • 数组名实际是指向第一个元素的指针,但它不可变
  • a[i] 等价于 *(a+i),这对指针也适用。因此 a[3] 等价于 3[a]
  • 在函数参数列表中数组和指针等价,是可变的

内存区域

  • 程序运行时的内存区域分为栈、堆、全局 / 静态存储区和常量区
  • 栈用于存放函数的局部变量和函数调用的参数,栈的大小是有限的
  • 堆用于存放动态分配的内存(如 malloc),堆的大小是不定的,需要手动释放分配的内存
  • 全局 / 静态存储区用于存放全局变量和静态变量,全局变量在程序运行期间一直存在,静态变量在函数调用结束后不会被释放
  • 常量区用于存放常量字符串和全局常量,常量区的内容在程序运行期间不可变
1
2
3
4
5
6
7
8
9
10
11
12
13
// 数组a是局部变量,位于栈中,函数返回后会被销毁
// 因此返回的指针指向无效的内存区域
int *make_array(int n) {
int a[3] = {1, 2, 3};
return a;
}

// 正确的写法是使用malloc在堆上分配内存
int *make_array(int n) {
int *a = malloc(n * sizeof(int));
a[0] = 1; a[1] = 2; a[2] = 3;
return a;
}

合法的指针运算

  • 指针和 int 的加减法,如 p+1
  • 指向同一数组内元素的两个指针的减法和比较

字符串

  • 字符串实际是一个字符数组,以 \0 结尾,因此占用的空间总比引号中的字符数多 1
  • 字符串赋值或传参的是指向第一个字符的指针
  • 下面两种定义方法是不同的,amessage 是一个数组,其大小刚好容纳字符串,而 pmessage 是指向字符串常量的指针。amessage 不可变,但可通过它修改字符数组的值,而 pmessage 可变,但不能通过它修改字符串的值。amessage 位于栈中,pmessage 指向的字符串在常量区

    1
    2
    char amessage[] = "now is the time";
    char *pmessage = "now is the time";

其他

  • 二维数组作为参数传递时必须指定列元素的数量,一般地,多维数组只有第一个索引可以省略
  • void * 表示一个指针,该指针可以指向任意类型的数据,void * 不可直接解引用,必须转换成正确的类型后使用
  • 函数指针 int (*p)(int, int) 的使用方法为 (*p)(1, 2)

复杂定义

运算符优先级:() = [] > *

识别方法:从变量名称出发,按照优先级和运算符结合判断类型,如 p() 是函数,p[] 是数组,*p 是指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
int *p; // 指针,指向一个int变量
int **pp; // 指针,指向一个指针,该指针指向一个int变量
const int *p; // 指针,指向一个const int变量
int *const p; // 常量指针,指向一个int变量
const int *const p; // 常量指针,指向一个const int变量
int *p[5]; // 数组,包含5个元素,元素类型为指向int的指针
int (*p)[5]; // 指针,指向包含5个int元素的数组
int (*p)(int, int); // 函数指针,指向接收两个int参数,返回值为int的函数
int (*p)(int); // 函数指针,指向接收一个int参数,返回值为int的函数
int (*p[5])(int, int); // 数组,包含5个元素,元素类型为函数指针,该函数指针指向接收两个int参数,返回值为int的函数
int *(*p[5])(int); // 数组,包含5个元素,元素类型为函数指针,该函数指针接收一个int参数,返回值为int指针的函数
int (*(*p())[])() //函数,无参数,返回一个指针,该指针指向一个数组,数组中的元素类型为无参数返回值为int的函数指针
int (*(*p[3])())[5]; // 数组,包含3个元素,元素类型为函数指针,指向的函数无参数返回值为指向有5个int元素的数组的指针

关于上面声明的详细判断,参考 C 语言中的复杂指针声明

结构体

  • 结构体声明语法如下,在声明后可直接定义该结构的变量。如果不给出结构体名,则只能在声明时定义变量。

    1
    2
    3
    4
    struct [struct_name] {
    member_type member_name;
    ...
    } [variable_name];
  • 如下面的代码定义了 struct point 类型,并给出初始化方法。

    1
    2
    3
    4
    5
    6
    struct point {
    int x;
    int y;
    };

    struct point pt = {1, 2};
  • 访问结构体成员使用.,如 pt.x
  • .-> 的优先级高于 *,所以在使用结构体指针时需注意。

    1
    2
    3
    4
    struct 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
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef int Length;
Length len = 10; // 等价于 int len = 10;

typedef char *String;
String str = "Hello, World!"; // 等价于 char *str = "Hello, World!";

typedef struct point {
int x;
int y;
} Point;
Point pt = {1, 2}; // 等价于 struct point pt = {1, 2};

typedef int (*Func)(int, int); // 定义一个函数指针类型
Func add = add_func; // 等价于 int (*add)(int, int) = add_func;

联合体

  • 联合体是一种特殊的结构体,所有成员共享一个内存空间,即成员的偏移量都从 0 开始。因此联合体的大小是其成员中最大的那个成员的大小。

    1
    2
    3
    4
    5
    union u_tag {
    int ival;
    float fval;
    char *sval;
    } u;
  • 联合体成员的访问方法和结构体一样,使用.->,但只有一个成员的值是有效的。
  • 给其他成员赋值会影响到当前成员

    1
    2
    3
    4
    5
    6
    u.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"}; 则是错误的。


C 语言学习笔记
http://blog.qzink.me/posts/C语言学习笔记/
作者
Qzink
发布于
2025年1月5日
许可协议