📝 我的笔记

还没有笔记

选中页面文字后点击「高亮」按钮添加

1 C 语言基础:

📜 原文
📖 逐步解释
∑ 公式拆解
💡 数值示例
⚠️ 易错点
📝 总结
🎯 存在目的
🧠 直觉心智模型
💭 直观想象

1C 语言基础:

2自定义类型、指针、函数

COMS W3157
Borowski 博士

3编码风格

https://www.kernel.org/doc/Documentation/process/coding-styl e.rst

https://en.wikipedia.org/wiki/Indentation style

4C 关键字

| auto | break | case | char | const | continue | default | do |

| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |

| double | else | enum | extern | float | for | goto | if |

| int | long | register | return | short | signed | sizeof | static |

| struct | switch | typedef | union | unsigned | void | volatile | while |

有关更多信息,请参阅 https://www.geeksforgeeks.org/keywords-in-c/。

5C 运算符优先级

| 优先级 | | 运算符 | 描述 | 结合性 |

| :--- | :--- | :--- | :--- | :--- |

| | 1 | ++ --
()
[]
.
->
(type)\{list\} | 后缀增量和减量
函数调用
数组下标
结构体联合体成员访问
通过指针访问结构体联合体成员
复合字面量 (C99) | 从左到右 |

| | 2 | ++ --
+ -
! ~
(type)
*
&
sizeof
Alignof | 前缀增量和减量 ${ }^{[\text {注 1] }}$
一元加和减
逻辑非和位非
类型转换
间接寻址 (解引用)
取地址
大小 (sizeof) ${ }^{[\text {注 2] }}$
对齐要求 (C11) | 从右到左 |

6C 运算符优先级

| 3 | * / % | 乘法、除法和取余 | 从左到右 |

| :--- | :--- | :--- | :--- |

| 4 | + - | 加法和减法 | |

| 5 | <<>> | 位左移和位右移 | |

| 6 | < <=
> >= | 关系运算符 < 和 $\leq$
关系运算符 > 和 $\geq$ | |

| 7 | == != | 关系运算符 = 和 $\neq$ | |

| 8 | & | 按位与 | |

| 9 | ^ | 按位异或(独占或) | |

| 10 | | | 按位或(包含或) | |

| 11 | && | 逻辑与 | |

| 12 | || | 逻辑或 | |

7C 运算符优先级

| 13 | ?: | 三元条件运算符 ${ }^{[\text {注 3] }}$ | 从右到左 |

| :--- | :--- | :--- | :--- |

| $14^{[\text {注 4] }}$ | =
+= -=
*= /= %=
<<= >>=
&= ^= |= | 简单赋值
加法和减法赋值
乘法、除法和取余赋值
位左移和位右移赋值
按位与、异或和或赋值 | |

| 15 | , | 逗号 | 从左到右 |

有关更多详细信息,请参阅 https://en.cppreference.com/w/c/language/operator precedence.html。

8类型转换

在 C 语言中,类型转换是一种非常有用的工具,用于将值从一种数据类型更改为另一种数据类型

我们为什么需要类型转换

也许我们想要为某个值分配更多内存(例如:int -> double),或者在字母和数字之间切换(例如:char -> int)等。

有 2 种不同的类型转换方法:

  1. 隐式类型转换 - 当编译器在我们没有编写任何代码指令的情况下进行转换时。
  2. 显式类型转换 - 当我们明确告诉编译器进行类型转换时。

两种方法的示例将在下文介绍!

9隐式类型转换

10隐式类型转换

11隐式类型转换示例

```

int main(int argc, char **argv) {

char var1 = 0x41; // type cast: 0x41 -> 'A'; integer-> char

int var2 = 1.5; // type cast: 1.5 -> 1; float -> integer

// type cast: -1 -> Oxffffffff; signed -> unsigned

unsigned int var3 = -1;

}

```

char $\mathrm{c}=0 \times 41424344$; c 的值是多少?假设未启用 -Werror=overflow。

12隐式类型转换示例

```

int main(int argc, char **argv) {

char var1 = 0x41; // type cast: Ox41 -> 'A'; integer-> char

int var2 = 1.5; // type cast: 1.5 -> 1; float -> integer

// type cast: -1 -> Oxffffffff; signed -> unsigned

unsigned int var3 = -1;

}

```

char $\mathrm{c}=0 \times 41424344$;

c 的值是多少?$16^{1} \times 4+16^{0} \times 4=68$,或 ' D ' 假设未启用 -Werror=overflow。

13显式类型转换示例

为了保证在 C 中将值转换为新类型,请使用类型转换运算符,其语法为 (new_type) value。

```

int main(int argc, char **argv) {

float f;

int a = 20, b = 3;

f = a/b; // f 是什么? 6

f = (float)a/b; // f 现在是什么? ~6.666667

```

}

14通过宏定义常量

通常用于定义永远不会改变且程序其余部分经常访问的值。

使用 \#define 定义

预处理器在将源代码传递给编译器之前,将 $P /$ 的每个实例替换为 3.14。

```

#define PI 3.14

int main(int argc, char **argv) {

float pi = PI; // pi = 3.14

return 0;

}

```

15枚举

16枚举的使用

```

enum week { Sunday, Monday, Tuesday,

Wednesday, Thursday, Friday, Saturday };

int main(int argc, char **argv) {

enum week today;

today = Wednesday; // today = ?

return 0;

}

```

17枚举的使用

```

enum week { Sunday, Monday, Tuesday,

Wednesday, Thursday, Friday, Saturday };

int main(int argc, char **argv) {

enum week today;

today = Wednesday; // today = 3

return 0;

}

```

18结构体

结构体是不同类型变量的集合,在单一类型下。

```

struct my_struct {

int x;

int y;

int z;

};

```

19结构体的使用

```

struct book {

int year;

int month;

int book_id;

}; // 不要忘记这里的 ";"。

int main() {

struct book book1; // 声明 book1 为 Books 类型

book1.book_id = 100; // 访问结构体中的成员

return 0;

}

```

20联合体

联合体用于在同一内存位置存储不同类型的数据。

```

union my_union {

int i;

short s;

char c;

};

```

$$ \underset{\text { int }}{\text { 整数 }} \text { (4 字节) }\left\{\begin{array}{c} \text { short } \\ \text { (2 字节) } \end{array}\right\} $$

联合体需要足够的空间来存储其最大的成员。但是,它只能容纳一个信息;对一个成员的赋值会影响其他成员。

my_union 的大小是多少?

21联合体的使用

```

union my_union { int i; short s; char c; };

int main(int argc, char **argv) {

union my_union test;

int var0;

test.i = 0;

test.c = 'A';

test.s = 16383;

varO = test.c;

}

```

22数组

```

在栈上分配的数组示例:

int a[10]; // type name[size1]

char b[10][100];

struct my_struct c[5][10][20];

```

23数组的使用

```

int main(int argc, char **argv) {

int array[10];

array[1] = 100;

array[2] = 200;

array[3] = array[1] + array[2];

array[0] = 1000; // 这是对的吗?

array[10] = 1000; // 这是对的吗?

}

```

24数组的使用

```

int main(int argc, char **argv) {

int array[10];

array[1] = 100;

array[2] = 200;

array[3] = array[1] + array[2];

array[0] = 1000; // 是的,数组是基于 0 的。

array[10] = 1000; // 不,有效索引是 0-9。

}

```

25数组初始化

数组可以按如下方式进行显式初始化:

```

int a[3];

a[O] = O; a[1] = 1; a[2] = 2;

// 0, 1, 2 分别赋值给 a[0], a[1], a[2]。

int a[3] = { 0, 1, 2 };

// 与 int a[3] = { 0, 1, 2 }; 相同,大小推断。

int a[] = { 0, 1, 2 };

// 与 int a[3] = { 1, 0, 0 }; 相同

int a[3] = { 1 };

// 将所有 100 个元素初始化为 0。

// 第一个元素显式为 0,其余隐式为 0。

int a[100] = { 0 };

```

26sizeof()

是一个看起来像函数运算符。返回类型变量的字节大小。

```

int x = 56;

// sizeof(x) 和 sizeof(int) 都返回 4

int a[3];

// sizeof(a) 返回 12 (3 乘以 sizeof(int))

```

27指针

(C 语言中最重要的概念之一)

28高级概述

内存就像一个长的字节数组

| 72 | 101 | 108 | 108 | 111 | 32 | 65 | 80 | 33 |

| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

指针存储一个字节(或字节块的开始)的索引,称为内存地址

例如,值为 5 的指针将指向上面示例中的值 32。(细节要复杂一些,但这是基本思想。) UNIVERSITY

29高级概述

| 72 | 101 | 108 | 108 | 111 | 32 | 65 | 80 | 33 |

| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

如果我们有一个名为 p 的指针,它指向地址 5,我们可以:

30高级概述

理解指针的关键在于理解指针本身存储在内存中的某个位置,所以我们可以有一个指向指针指针

地址 3 指向地址 6,地址 6 指向地址 1。

31指针(实践中)

int intptr; // 指向一个整数 char charptr; // 指向一个字符 struct my_struct *structptr; // 指向一个结构体 int **intptrptr; // 指向一个整数指针

32指针的使用

| | 8 字节
" | | 4 字节 | 4 字节 | | 4 字节 | |

| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |

| | | | | | | | |

| int *p; int a,b,c; a = 1; | □
未初始化 | | 1 | 未初始化 | | 未初始化 | |

| | p | 999 | 1000 | 1004 | b | 1008 | C |

33指针的使用

既然我们有一个指向 a 的指针,我们可以使用解引用运算符 (*) 访问和修改 a 的值。

这两行具有相同的效果。

$$ \text { *p = 2; } \quad a=2 ; $$

*p 相当于说:“沿着粉色箭头,访问那里的值。”

34指针算术

COLUMBIA

35指针实现

C 中指针的一些更精细的细节:

36指针实现

C 中指针的一些更精细的细节:

```

int *p;

double *x = p; // 编译器错误:不兼容的指针类型

Instead we should write double x = (double ) p.

```

```

void *p;

int x = p; // 编译器错误:无法解引用 void

```

37指针实现

C 中指针的一些更精细的细节:

```

int *p = NULL;

int x = *p; // 运行时段错误

```

38定义您自己的类型

```

typedef ;

typedef unsigned int uint; typedef struct my_struct {

...

} my_struct;

typedef union my_union {

int i;

char c;

double d;

char data[sizeof(double)];

} my_union;

```

Typedefs 缩短了整个项目中的声明

39定义您自己的类型

```

typedef struct book {

int year;

int month;

int book_id;

} book; // do not forget about the ";" here

int main() {

book book1; // struct Book Book1;

book1.book_id = 100;

return 0;

}

```

40变量作用域 - 局部

有三种变量作用域:局部、全局、静态。这些不同作用域级别告诉我们变量可以从何处访问。

```

void func1() {

int i;

i = 1;

}

void func2() {

i = 1; // 无效作用域(我们不能从 func1 外部访问 'i'

// 除非重新声明它)

}

```

41变量作用域 - 全局

```

int g_debug_level;

void func1() {

g_debug_level = 1;

}

void func2() {

g_debug_level = 2;

}

void func3() {

g_debug_level = 3;

}

```

42变量作用域 - 文件静态

```

static int g_debug_level; // 在 file1.c 中

void func1() { // 在 file1.c 中

g_debug_level = 1; // OK

}

void func2() { // 在 file2.c 中

g_debug_level = 2; // NOT OK

}

```

43变量作用域 - 块静态

```

int static_integer() {

static int i = 100;

i++;

return i;

}

int main() {

for (int i = 0; i < 5; i++) {

printf("%d\n", static_integer());

}

}

```

44函数

声明函数很容易!

```

int my_func(char arg1, int arg2, float arg3);

// return_type, function_name(arguments list)

```

函数可见性

```

static int my_func(char arg1, int arg2, float arg3);

```

45参数传递 (1)

C 仅使用传值调用

这意味着子函数所做的更改不会影响父函数

```

int main() {

int i = 0;

callee(i);

printf("%d\n", i);

return 0;

}

```

```

void callee(int c) {

c = 10;

}

```

这个程序的输出是什么?

46参数传递 (1)

C 仅使用传值调用

这意味着子函数所做的更改不会影响父函数

```

int main() {

int i = 0;

callee(i);

printf("%d\n", i);

return 0;

}

```

```

void callee(int c) {

c = 10;

}

```

这个程序的输出是什么? 0

47参数传递 (2)

我们还可以使用函数的返回值更新变量的值。

```

int main() {

int i = 0;

i = callee(i);

printf("%d\n", i);

return 0;

}

```

```

int callee(int c) {

c = 10;

return c;

}

```

这个程序的输出是什么?

48参数传递 (2)

我们还可以使用函数的返回值更新变量的值。

```

int main() {

int i = 0;

i = callee(i);

printf("%d\n", i);

return 0;

}

```

```

int callee(int c) {

c = 10;

return c;

}

```

这个程序的输出是什么? 10

49参数传递 (3)

我们还可以使用指针更新变量的值。

```

int main() {

int i = 0;

callee(&i);

printf("%d\n", i);

return 0;

}

```

```

void callee(int *c) {

*c = 10; // 解引用

}

```

这个程序的输出是什么?

50参数传递 (3)

我们还可以使用指针更新变量的值。

```

int main() {

int i = 0;

callee(&i);

printf("%d\n", i);

return 0;

}

```

```

void callee(int *c) {

*c = 10; // 解引用

}

```

这个程序的输出是什么? 10

51函数指针

正如我们可以有一个指向内存中其他位置数据指针变量一样,我们也可以有一个函数指针,它存储函数地址,允许函数作为参数传递并动态调用。

在本课程中,我们最常使用函数指针来实现多态性

52函数指针示例

```

#include

int add(int a, int b) { return a + b; }

void say_hi() { puts("Hi"); }

int main() {

// 声明一个与 add() 函数签名匹配的函数指针。

int (*add_ptr) (int, int);

// 声明一个与 say_hi() 函数签名匹配的函数指针。

void (*say_hi_ptr) ();

// 赋值函数指针。

add_ptr = &add; say_hi_ptr = &say_hi;

// 通过它们的函数指针调用函数。

printf("%d", add_ptr(2, 3));

say_hi_ptr();

return 0;

}

```

53传递函数指针

```

/**

*/

int int_cmp(int a, int b) {

if (a > b) {

return 1;

}

if (a < b) {

return -1;

}

return 0;

}

int binary_search(const int key, const int *values,

const size_t num_elems, int (*cmp) (int, int)) {

...

int result = cmp(key, values[mid]);

}

int array[] = { 1, 4, 7, 18, 90 };

int retval = binary_search(4, array, 5, int_cmp);

```