C程序设计_翁凯_笔记 (监修中)

1. 程序设计与 C 语言

1.1 计算机和编程语言

  • 核心区别:人负责思考 What to do(做什么),计算机负责执行 How to do(怎么做)
  • 计算机的所有操作本质是“计算”,计算的步骤即为 算法
  • 程序的执行方式:
    • 解释:逐行翻译并执行
    • 编译:先整体翻译为机器码,再执行

1.2 C 语言

历史演变

FORTRAN → BCPL → B → C

  • 1973年3月:第三版 Unix 系统出现 C 语言编译器
  • 1973年11月:第四版 Unix 完全用 C 语言重写

版本迭代

  • 经典 C(早期版本)
  • 1989年:ANSI C(C89)
  • 1990年:ISO 接受 ANSI 标准,仍称 C89
  • 1995年/1999年:两次更新,分别为 C95、C99

应用场景

  • 操作系统开发
  • 嵌入式系统
  • 驱动程序
  • 底层驱动:图形引擎、图像处理、声音效果等

1.3 第一个程序

2. 计算

2.1 变量

定义与初始化

  • 变量本质:保存数据的内存空间

  • 定义语法: <变量类型> <变量名称1>, <变量名称2>;

    1
    int price, amount;
  • 注意:未初始化的变量会使用内存原有值,可能导致程序结果错误

  • 初始化语法: <类型名称> <变量名称1> = <初始值1>, <变量名称2> = <初始值2>;

    1
    int price = 0, amount = 100;
  • 规则:ANSI C 标准要求变量只能在代码块开头定义

输入与常量

  • scanf 语法:括号内的内容是“需要输入的数据标识”,而非“输出提示”
  • printf 语法:括号内的内容是“需要输出的内容”
  • const常量定义:用 const 修饰,值不可修改
1
2
3
printf("%f", ...);
scanf("%lf", ...);
const int AMOUNT = 100;

2.2 数据类型

运算转换规则

浮点数与整数混合运算时,整数会自动转换为浮点数

常见类型与格式符

数据类型 输出格式符 输入格式符 说明
int %d(十进制)、%o(八进制)、%u(无符号十进制)、%x(十六进制) %d 整数类型
double %f %lf 双精度浮点数

2.3 表达式

基本概念

  • 运算符:表示“运算动作”(如 +、-、*、/)
  • 算子:参与运算的值(如变量、常量)

运算符优先级与结合关系

优先级 运算符 运算 结合关系 举例
1 单目+ 单目不变 自右向左 a*+a
1 单目- 单目取负 自右向左 a*-b
2 * 自左向右 a*b
2 / 自左向右 a/b
2 % 取余 自左向右 a%b
3 + 自左向右 a+b
3 - 自左向右 a-b
= 赋值 自右向左 a=b

关键技巧

  1. 交换两个变量:必须借助临时中间变量(程序执行的是“步骤”,而非“关系”)

    1
    2
    3
    4
    int a = 1, b = 2, temp;
    temp = a;
    a = b;
    b = temp;
  2. 复合赋值

    • 语法:变量 += 表达式变量 *= 表达式
      1
      2
      3
      total *= sum + 12;
      // 等价于
      total = total * (sum + 12);
  3. 递增/递减运算符

    运算符 作用 表达式值 变量最终值
    a++ 先取值,后给a加1 a的原始值 a+1
    ++a 先给a加1,后取值 a+1的值 a+1
    a– 先取值,后给a减1 a的原始值 a-1
    –a 先给a减1,后取值 a-1的值 a-1

3. 判断与循环

3.1 判断

关系运算符优先级

  • 赋值运算符优先级 < 关系运算符优先级 < 算术运算符优先级
  • “相等(==)”“不等(!=)”优先级 < 其他关系运算符(如 >、<、>=、<=)
  • 连续关系运算:从左到右执行(例:a < b < c 等价于 (a < b) < c

3.2 循环

三种循环语法

  1. while 循环:先判断条件,再执行循环体

    1
    2
    3
    while (循环条件) {
    循环体语句;
    }
  2. do-while 循环:先执行一次循环体,再判断条件(至少执行一次)

    1
    2
    3
    4

    do {
    循环体语句;
    } while (循环条件); // 注意末尾分号
  3. for 循环:初始化、条件判断、迭代动作集中定义
    语法:for (初始条件; 循环继续条件; 循环每轮动作) { 循环体语句; }
    常见示例:

    1
    2
    3
    4
    5
    6
    // 递增
    for (i=1; i<n; i++) { ... }
    // 递减
    for (i=n; i>1; i--) { ... }
    // 省略初始条件,需提前定义n
    for (; n>1; n--) { ... }

循环选择建议

  • 固定次数循环:用 for
  • 必须执行一次的循环:用 do-while
  • 其他情况:用 while

示例:for 循环输出

1
2
3
4
//  输出:10532(i++ 先输出原始值,再自增)
for (int i = 10; i > 1; i /= 2) {
printf("%d", i++);
}

4. 进一步的判断与循环

4.1 逻辑类型和运算

注意点

  • C语言无真正的 bool 类型,用整数表示(0为假,非0为真)
  • 逻辑运算符:!(非)、&&(与)、||(或)

优先级与短路特性

  1. 优先级! > && > ||;逻辑运算符优先级 < 比较运算符
    例:!done && (count > MAX) 无需额外括号(! 优先级最高)
优先级 运算符 结合性
1 () 从左到右
2 !+(单目)、-(单目)、++-- 从右到左(单目的+-
3 */% 从左到右
4 +-(双目) 从左到右
5 <<=>>= 从左到右
6 ==!= 从左到右
7 && 从左到右
8 寒冷的冬 从左到右
9 =+=-=*=/=%= 从右到左
  1. 短路特性
    • &&:左边为假时,不执行右边(结果已确定为假)
    • ||:左边为真时,不执行右边(结果已确定为真)

其他运算符

  1. 条件运算符(三目运算符):
    语法:条件 ? 满足时的值 : 不满足时的值

    1
    count = (count > 20) ? count - 10 : count + 10;

    规则:优先级 > 赋值运算符,结合关系“自右向左”

  2. 逗号运算符

    • 作用:连接两个表达式,结果为“右边表达式的值”
    • 优先级:所有运算符中最低
    • 例:(3+4, 5+6) 结果为 11;
    • 例:for (i=0,j=10; i<j; i++,j--)(for中使用)

4.2 级联和嵌套的判断

else 匹配规则

  • else 总是与“最近的未匹配 if”配对(不加大括号时易出错)
  • 建议:无论循环体/判断体是否只有一条语句,都用 {} 包裹

级联判断(多分支)

语法:

1
2
3
4
5
6
7
8
9
if (条件1) {
语句1;
} else if (条件2) {
语句2;
} else if (条件3) {
语句3;
} else {
语句4;
}

4.3 多路分支(switch-case)

语法

1
2
3
4
5
6
7
8
9
10
11
switch (控制表达式) { // 控制表达式结果为整数/字符
case 常量1:
语句1;
break; // 跳出switch,无break则“穿透”到下一个case
case 常量2:
语句2;
break;
default: // 所有case不匹配时执行
语句n;
break;
}
  • 注意:case 只是“入口”,需用 break 控制分支退出

4.4 循环的例子

小套路

  1. 需后续使用的“变化量”,提前用变量保存
  2. 大次数循环可先模拟小次数,再推断规律

整数分解与逆序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main() {
int x = 12345;
int digit;
int ret = 0;

while (x > 0) {
digit = x % 10; // 取个位数
printf("%d", digit); // 输出逆序(如12345输出54321)
ret = ret * 10 + digit; // 保存逆序后的整数
printf("x=%d, digit=%d, ret=%d\n", x, digit, ret);
x /= 10; // 丢掉个位数
}
printf("%d", ret); // 输出逆序整数(54321)
return 0;
}

4.5 判断和循环常见的错误

  1. 忘记用 {} 包裹多语句的循环体/判断体
  2. if 后面多写分号(导致循环体/判断体失效)
  3. 混淆 ==(判断相等)和 =(赋值)
  4. 代码风格混乱(缩进不统一,可读性差)

5. 循环控制

5.1 循环控制语句

语句 作用
break 跳出当前循环(多层循环中只跳出最内层)
continue 跳过循环体剩余语句,直接进入下一轮循环的“条件判断”环节

5.2 多重循环(嵌套循环)

示例:凑硬币(找1角、2角、5角组合得到指定金额)

方法1:用 exit 变量控制跳出多层循环(break)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int main() {
int x;
int one, two, five;
int exit = 0; // 控制跳出标志
scanf("%d", &x); // 输入目标金额(单位:元)

for (one = 1; one < x * 10; one++) { // 1角硬币数量(x*10为总角数)
for (two = 1; two < x * 10 / 2; two++) { // 2角硬币数量
for (five = 1; five < x * 10 / 5; five++) { // 5角硬币数量
if (one + two * 2 + five * 5 == x * 10) {
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",
one, two, five, x);
exit = 1;
break; // 跳出最内层循环
}
}
if (exit == 1) break; // 跳出中层循环
}
if (exit == 1) break; // 跳出外层循环
}
return 0;
}

方法2:用 goto 直接跳出多层循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int main() {
int x;
int one, two, five;
scanf("%d", &x);

for (one = 1; one < x * 10; one++) {
for (two = 1; two < x * 10 / 2; two++) {
for (five = 1; five < x * 10 / 5; five++) {
if (one + two * 2 + five * 5 == x * 10) {
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",
one, two, five, x);
goto out; // 直接跳转到out标签处
}
}
}
}
out: // 标签
return 0;
}

5.3 循环应用

1. 交替求和(1 - 1/2 + 1/3 - 1/4 + … + 1/n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main() {
int n;
scanf("%d", &n);
double sum = 0.0;
double sign = 1.0; // 控制正负交替

for (int i = 1; i <= n; i++) {
sum += sign / i;
sign = - sign; // 每次迭代切换符号
}
printf("%lf", sum);
return 0;
}

2. 求最大公约数

方法1:枚举法

1
2
3
4
5
6
7
8
9
10
int gcd_enum(int a, int b) {
int min = (a < b) ? a : b; // 取a和b中的较小值
int ret = 0;
for (int i = 1; i <= min; i++) {
if (a % i == 0 && b % i == 0) {
ret = i; // 更新最大公约数
}
}
return ret;
}

方法2:辗转相除法(更高效)

1
2
3
4
5
6
7
8
int gcd_gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a; // b=0时,a即为最大公约数
}

3. 正序分解整数(如13425分解为1 3 4 2 5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
int main() {
int x;
scanf("%d", &x);
int mask = 1; // 掩码,用于定位最高位
int t = x;

// 计算掩码(如x=13425,mask最终为10000)
while (t > 9) {
t /= 10;
mask *= 10;
}

do {
int d = x / mask; // 取当前最高位
printf("%d", d);
if (mask > 9) {
printf(" "); // 非最后一位时加空格
}
x %= mask; // 去掉当前最高位
mask /= 10; // 掩码降级
} while (mask > 0);

return 0;
}

6. 数组与函数

6.1 数组

定义与特点

  • 定义语法: <类型> 变量名称[元素数量];

    1
    2
    //定义100个int类型元素的数组
    int number[100];
  • 特点:

    1. 所有元素数据类型相同
    2. 长度固定(创建后不可修改)
    3. 元素在内存中连续排列
    4. 编译器/运行环境不检查数组下标越界(需手动控制)

示例:统计数字出现次数(输入0-9的整数,统计每个数出现次数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
int main() {
const int number = 10; // 数组大小(0-9共10个数字)
int x;
int count[number]; // 统计数组

// 初始化统计数组为0
for (int i = 0; i < number; i++) {
count[i] = 0;
}

// 输入数字(-1结束)
scanf("%d", &x);
while (x != -1) {
if (x >= 0 && x <= 9) {
count[x]++; // 对应数字的计数+1
}
scanf("%d", &x);
}

// 输出结果
for (int i = 0; i < number; i++) {
printf("%d:%d\n", i, count[i]);
}
return 0;
}

6.2 函数的定义与使用

核心意义

避免“代码复制”(重复代码会降低程序质量、增加维护成本)

定义语法

1
2
3
4
返回类型 函数名(参数表) {
函数体语句;
return 返回值; // 无返回值时可省略(返回类型为void)
}
  • 无返回值:返回类型用 void,函数体中不可用 return 数值;

    1
    void sum(int begin, int end) { ... }

6.3 函数的参数和变量

1. 函数原型声明

  • 作用:告诉编译器函数的“返回类型、参数个数、参数类型”,可放在调用之前
  • 语法:返回类型 函数名(参数类型1, 参数类型2, ...);(参数名可省略)
    例:void swap(int, int);int add(int a, int b);
  • 特殊情况:无参数时需写 void,例:void print_hello(void);

2. 本地变量(局部变量)

  • 定义:函数内部定义的变量
  • 生存期:从函数调用开始到函数结束(函数返回后内存释放)
  • 作用域:仅在定义它的代码块({})内有效
  • 规则:
    • 块外定义的变量在块内仍有效,但块内同名变量会“覆盖”块外变量
    • 本地变量不会被默认初始化(值为随机值)
    • 函数参数本质是“被实参初始化的本地变量”

3. 其他规则

  • C语言不允许函数“嵌套定义”(可嵌套声明)
  • main 函数的 return 值:
    • return 0:程序正常结束
    • return 非0:程序异常结束(不同系统获取方式不同:Windows用if errorlevel 1,Unix Bash用echo $?

6.4 二维数组

初始化规则

  1. 列数必须明确,行数可由编译器自动计算
    例:int a[][3] = {1,2,3,4,5,6};(编译器会识别为2行3列)
  2. 每行可用 {} 包裹,逗号分隔,最后一行的逗号可省略
    例:int b[2][3] = {{1,2}, {3,4,5}};
  3. 未显式初始化的元素默认补0
    例:int c[2][3] = {{1}, {2}};(结果为 [[1,0,0], [2,0,0]]

7. 数组运算

7.1 数组运算

1. 数组集成初始化

初始化方式 说明 示例
全显式初始化 大小由元素数量确定 int a[] = {2,4,6,8};(大小为4)
部分显式初始化 未显式初始化的元素补0 int a[13] = {2};(仅a[0]=2,其余为0)
定位初始化(C99) [n] 指定下标,未定位元素补0 int a[] = {[0]=2, [2]=3, 6};(a[0]=2, a[2]=3, a[3]=6,其余为0)

2. 数组大小计算

语法:sizeof(数组名) / sizeof(数组元素类型)

1
2
3
int a[10]; int len = sizeof(a) / sizeof(int);

// len=10

3. 数组赋值

  • 不允许直接用 int a[] = {1,2}; int b[] = a;(数组名是“常量指针”,不可赋值)
  • 必须通过循环遍历赋值:
    1
    2
    3
    4
    5
    int a[3] = {1,2,3};
    int b[3];
    for (int i=0; i<3; i++) {
    b[i] = a[i];
    }

4. 数组作为函数参数

  • 本质:函数参数中的数组是“指针”(int a[] 等价于 int *a
  • 注意:
    1. 函数参数中不可写数组大小(如 int f(int a[10]) 中的10无意义)
    2. 函数内部无法用 sizeof 计算数组大小(会计算指针大小,而非数组)
    3. 需额外传一个参数表示数组大小,例:void print_arr(int a[], int len) { ... }

5. 求素数的优化方法 TODO

  1. 基础方法:遍历2到x-1,判断是否能整除x
  2. 优化1:仅遍历奇数(偶数除2外均非素数)
  3. 优化2:仅遍历到 sqrt(x)(大于sqrt(x)的因数必与小于sqrt(x)的因数成对出现)
  4. 优化3:用“素数表”筛选(埃氏筛法):
    • 假设所有数为素数,再剔除每个素数的倍数
    • 例:int is_prime[100] = {1};(1表示素数),再遍历剔除倍数

7.2 搜索

1. 线性搜索(遍历)

示例:查找数组中指定元素的下标(未找到返回-1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int search(int key, int a[], int len) {
int ret = -1; // 初始化为“未找到”
for (int i = 0; i < len; i++) {
if (key == a[i]) {
ret = i;
break; // 找到后跳出循环(单一出口,代码更清晰)
}
}
return ret;
}

int main() {
int a[] = {1,4,5,7,9};
int len = sizeof(a) / sizeof(a[0]);
int key = 4;
int idx = search(key, a, len);
printf("下标:%d", idx); // 输出1
return 0;
}

2. 关联数据搜索(如金额与名称对应)

方法1:两个数组对应(对缓存不友好)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int search(int key, int a[], int len); // 线性搜索函数

int main() {
int amount[] = {1,5,10,25,50}; // 金额(1分、5分、10分等)
char *name[] = {"penny", "nickel", "dime", "quarter", "half-dollar"}; // 对应名称
int key = 10;
int len = sizeof(amount) / sizeof(amount[0]);
int idx = search(key, amount, len);

if (idx != -1) {
printf("%s", name[idx]); // 输出dime
}
return 0;
}

方法2:用结构体整合数据(更直观)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
int main() {
// 定义结构体数组,整合金额与名称
struct {
int amount;
char *name;
} coins[] = {
{1, "penny"},
{5, "nickel"},
{10, "dime"},
{25, "quarter"},
{50, "half-dollar"}
};

int key = 10;
int len = sizeof(coins) / sizeof(coins[0]);
for (int i = 0; i < len; i++) {
if (coins[i].amount == key) {
printf("%s", coins[i].name); // 输出dime
break;
}
}
return 0;
}

3. 二分搜索(有序数组,效率更高)

  • 原理:每次取中间元素,排除一半数据,时间复杂度O(log₂N)
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #include <stdio.h>
    int binary_search(int key, int a[], int len) {
    int ret = -1;
    int left = 0;
    int right = len - 1;

    while (left <= right) { // 注意条件是left<=right
    int mid = (left + right) / 2;
    if (a[mid] == key) {
    ret = mid;
    break;
    } else if (a[mid] > key) {
    right = mid - 1; // 目标在左半部分
    } else {
    left = mid + 1; // 目标在右半部分
    }
    }
    return ret;
    }

    int main() {
    int a[] = {1,3,5,7,9,11}; // 必须是有序数组
    int len = sizeof(a) / sizeof(a[0]);
    int key = 7;
    int idx = binary_search(key, a, len);
    printf("下标:%d", idx); // 输出3
    return 0;
    }

7.3 排序初步(选择排序)

原理

每次从待排序部分找到最大值(或最小值),放到已排序部分的末尾

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
// 找数组中最大值的下标
int find_max_idx(int a[], int len) {
int max_idx = 0;
for (int i = 1; i < len; i++) {
if (a[i] > a[max_idx]) {
max_idx = i;
}
}
return max_idx;
}

int main() {
int a[] = {2,45,6,12,87,34,90,24,23,11,65};
int len = sizeof(a) / sizeof(a[0]);

// 选择排序:从后往前排(每次将最大值放到末尾)
for (int i = len - 1; i > 0; i--) {
int max_idx = find_max_idx(a, i + 1); // 找0~i的最大值下标
// 交换最大值与当前末尾元素
int temp = a[max_idx];
a[max_idx] = a[i];
a[i] = temp;
}

// 输出排序结果
for (int i = 0; i < len; i++) {
printf("%d ", a[i]); // 输出2 6 11 12 23 24 34 45 65 87 90
}
return 0;
}

8. 指针与字符串

8.1 指针

核心概念与基础操作

  • sizeof 运算符:返回变量或类型在内存中占用的字节数(静态计算,编译时确定结果)。
  • **取地址运算符 &**:
    • 功能:获取变量的内存地址,格式符用 %p(以十六进制形式输出地址)。
    • 规则:
      1. 操作数必须是变量(不能对常量、表达式取地址,如 &(a+1) 非法)。
      2. 地址大小与 int 是否相同,取决于编译器(32位系统通常为4字节,64位系统为8字节)。
  • 变量地址的规律
    • 连续定义的变量,地址“紧密相邻”,差值等于变量类型的字节数。
    • 栈内存生长方向为“自顶向下”:先定义的变量地址更低,后定义的变量地址更高。

指针的定义与使用

  • 指针本质:专门用于保存“内存地址”的变量。
  • 定义语法类型 *指针名;
    例:int *p; 表示 *pint 类型(指针指向的变量),p 是指向 int 的指针。
  • **核心操作:解引用 ***:
    • 功能:访问指针指向地址上的变量,可作为左值(修改变量)或右值(读取变量)。
    • 示例:
      1
      2
      3
      4
      int i = 10;
      int *p = &i; // p 保存 i 的地址
      printf("%d", *p); // 右值:读取 p 指向的变量,输出 10
      *p = 20; // 左值:修改 p 指向的变量,i 变为 20

指针作为函数参数

  • 作用:通过指针传递变量地址,实现函数对“外部变量”的修改(突破函数参数“值传递”的限制)。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 函数通过指针修改外部变量
    void modify(int *p) {
    *p = 26; // 访问指针指向的外部变量,修改其值
    }

    int main() {
    int i = 6;
    modify(&i); // 传入 i 的地址
    printf("%d", i); // 输出 26(i 被函数修改)
    return 0;
    }

指针与数组的关系

指针与数组在内存访问逻辑上高度关联,但存在关键差异,核心关系如下表:

特性 数组(如 int a[10] 指针(如 int *p
地址表示 数组名 a 本身就是“指向首元素的地址”(a == &a[0] 需显式赋值地址(如 p = ap = &a[0]
地址可修改性 数组名是常量指针int *const a),不可赋值(a = p 非法) 指针是变量,可修改指向(p = a+1 合法)
内存分配 数组在栈/全局区分配连续内存,存储元素本身 指针仅存储地址,不存储元素(需指向有效内存)

关键等价性(函数参数表中)

在函数参数表中,数组声明本质是指针声明,以下四种写法完全等价:

  1. int sum(int *ar, int n)
  2. int sum(int *, int)
  3. int sum(int ar[], int n)
  4. int sum(int [], int)

注意事项

  • 函数内部无法用 sizeof(ar) 获取数组大小:此时 ar 是指针,sizeof(ar) 计算的是“指针的字节数”(如4或8),而非数组长度。
  • 数组与指针的访问互通:
    • 指针可通过 [] 访问(如 p[0] 等价于 a[0])。
    • 数组可通过 * 访问(如 *a 等价于 a[0]*(a+1) 等价于 a[1])。

8.2 字符类型(char)

本质与特性

  • char1字节的整数类型,同时可表示“字符”(通过ASCII编码映射整数与字符)。
  • 字符常量:用单引号包裹(如 'a''1'),本质是对应ASCII码的整数(如 '1' 的ASCII码为49,'A' 为65,'a' 为97)。

字符的输入与输出

  • 格式符:%c(专门用于字符的输入输出)。
  • 示例:
    1
    2
    3
    char c;
    scanf("%c", &c); // 输入 '1',c 存储 ASCII 码 49
    printf("字符:%c,ASCII码:%d", c, c); // 输出“字符:1,ASCII码:49”

混合输入的关键注意事项

scanf%d(整数)与 %c(字符)混合使用时,空格的存在会改变读取逻辑

  • 无空格:scanf("%d%c", &i, &c)
    %d 读取整数后,%c 会直接读取整数后的“第一个字符”(包括空格、回车、Tab等空白字符)。
  • 有空格:scanf("%d %c", &i, &c)
    空格会“跳过所有空白字符”,%c 仅读取非空白字符。

字符计算(大小写转换)

利用ASCII码的规律实现大小写转换:

  • 大写转小写:c + 'a' - 'A'(如 'A' + 32 = 'a')。
  • 小写转大写:c + 'A' - 'a'(如 'a' - 32 = 'A')。

逃逸字符(转义字符)

用于表示“无法直接打印的控制字符”或“特殊字符”,以 \ 开头,常见类型如下:

逃逸字符 含义 应用场景
\b 退格(删除前一个字符) 修正输入错误
\t 制表符(跳至下一个制表位,通常8字符间隔) 格式化输出表格
\n 换行(编译器自动转为“回车+换行”) 换行输出
\r 回车(光标回到行首,不换行) 覆盖行内内容
\" 双引号(在字符串中表示双引号) 输出 "Hello"
\' 单引号(在字符常量中表示单引号) 定义 '\'' 字符
\\ 反斜杠(表示单个反斜杠) 输出路径(如 C:\\test

8.3 字符串

C语言字符串的定义

C语言无专门的字符串类型,字符串本质是“以 '\0'(整数0,而非字符 '0')结尾的字符数组”。

  • 合法字符串:char word[] = {'H', 'e', 'l', 'l', 'o', '\0'}(末尾必须有 '\0')。
  • 非法字符串:char word[] = {'H', 'e', 'l', 'l', 'o'}(无 '\0',无法用字符串函数处理)。

关键规则

  1. '\0' 是字符串的“结束标志”,不计入字符串长度(如 "Hello" 长度为5,实际占6字节)。
  2. 字符串必须以数组形式存在(内存中是连续的字符序列),访问时可通过数组或指针。

字符串的两种表示方式

表示方式 语法示例 内存分配 可修改性 适用场景
字符数组 char str[] = "Hello"; 栈/全局区分配6字节(含 '\0' 可修改 需要修改字符串内容(如拼接、替换)
字符指针 char *str = "Hello"; 指针存储字符串常量的地址(常量存于只读区) 不可修改 仅读取字符串(如打印、比较)

注意事项

  • 字符串常量(如 "Hello"):编译器会将其存储为“只读的字符数组”,多个相同常量可能指向同一地址(节省内存)。
  • 指针指向的字符串不可修改:若尝试 *str = 'h'(修改指针指向的字符串常量),会导致程序崩溃(访问只读内存)。

指针与数组的选择原则

需求 选择 原因
构造/修改字符串 字符数组 数组内存可写,支持修改字符内容
仅读取/传递字符串 字符指针 指针更简洁,无需预先确定字符串长度
处理函数参数 字符指针 函数参数中数组本质是指针,传递更高效
动态分配字符串内存 字符指针 配合 malloc 动态分配内存(后续内容)

8.4 字符串计算(依赖 <string.h> 库)

1. 字符串长度(strlen)

  • 函数原型size_t strlen(const char *s);
  • 功能:计算字符串的有效长度(不包含结尾的 '\0')。
  • 特性
    • const 修饰参数:保证函数不修改输入字符串。
    • 返回值 size_t 是无符号整数(需注意与int的运算,避免负数)。
  • 示例strlen("Hello") 返回 5,strlen("") 返回 0。

2. 字符串比较(strcmp)

  • 函数原型int strcmp(const char *s1, const char *s2);
  • 功能:按ASCII码逐字符比较 s1s2,返回差值。
  • 返回值规则
    • s1 == s2:返回 0。
    • s1 > s2:返回正整数(第一个不同字符的ASCII差值)。
    • s1 < s2:返回负整数(第一个不同字符的ASCII差值)。
  • 关键注意不可用 s1 == s2 比较字符串(会比较指针地址,而非字符串内容)。

自定义实现(mycmp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 版本1:用数组下标访问
int mycmp1(const char* s1, const char* s2) {
int idx = 0;
// 逐字符比较,直到不同或遇到 '\0'
while (s1[idx] == s2[idx] && s1[idx] != '\0') {
idx++;
}
return s1[idx] - s2[idx]; // 返回差值
}

// 版本2:用指针访问(更高效)
int mycmp2(const char* s1, const char* s2) {
// 逐字符比较,直到不同或遇到 '\0'
while (*s1 == *s2 && *s1 != '\0') {
s1++; // 指针移动到下一个字符
s2++;
}
return *s1 - *s2; // 返回差值
}

3. 字符串复制(strcpy)

  • 函数原型char *strcpy(char *restrict dst, const char *restrict src);
  • 功能:将 src 指向的字符串(含 '\0')复制到 dst 指向的内存。
  • 特性
    • restrict(C99):保证 dstsrc 内存不重叠(避免复制错误)。
    • 返回 dst:支持链式调用(如 strcpy(dst2, strcpy(dst1, src)))。
  • 风险dst 需足够大(至少 strlen(src)+1 字节),否则会导致内存溢出。

自定义实现(mycpy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 版本1:用数组下标访问
char* mycpy1(char* dst, const char* src) {
int idx = 0;
while (src[idx] != '\0') { // 等价于 while (src[idx])
dst[idx] = src[idx];
idx++;
}
dst[idx] = '\0'; // 手动添加结束标志
return dst;
}

// 版本2:用指针访问(简洁)
char* mycpy2(char* dst, const char* src) {
char* ret = dst; // 保存 dst 初始地址(用于返回)
while (*src != '\0') {
*dst++ = *src++; // 复制字符 + 指针移动
}
*dst = '\0'; // 手动添加结束标志
return ret;
}

// 版本3:极简写法(利用赋值表达式返回值)
char* mycpy3(char* dst, const char* src) {
char* ret = dst;
// *src 赋值给 *dst 后,判断是否为 '\0'(为0则循环结束)
while (*dst++ = *src++) {
; // 空循环体
}
return ret;
}

4. 字符串拼接(strcat)

  • 函数原型char *strcat(char *restrict s1, const char *restrict s2);
  • 功能:将 s2 拼接至 s1 末尾(覆盖 s1'\0',并添加新 '\0')。
  • 注意s1 需足够大(至少 strlen(s1)+strlen(s2)+1 字节),否则会溢出。

5. 安全版本函数(避免溢出)

strcpystrcat 无长度限制,存在溢出风险,推荐使用带长度限制的安全版本:

函数 原型 功能 关键规则
strncpy char *strncpy(dst, src, size_t n); 复制最多 n 个字符到 dst src 长度 < n,剩余位置补 '\0'
strncat char *strncat(s1, s2, size_t n); 拼接最多 n 个字符到 s1 末尾 自动添加 '\0',总长度不超过 strlen(s1)+n+1
strncmp int strncmp(s1, s2, size_t n); 比较前 n 个字符 用于仅需比较部分字符的场景(如版本号)

6. 字符查找(strchr / strrchr)

  • strchr:从左到右查找第一个匹配字符,返回指针;未找到返回 NULL
    原型:char *strchr(const char *s, int c);
  • strrchr:从右到左查找第一个匹配字符,返回指针;未找到返回 NULL
    原型:char *strrchr(const char *s, int c);

示例:查找字符串中第二个 ‘l’ 并分割字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main() {
char s[] = "Hello"; // 字符串:H e l l o \0
char *p = strchr(s, 'l'); // 找到第一个 'l'(下标2)

// 1. 提取第一个 'l' 之前的内容("He")
char c = *p; // 保存 'l'(避免修改后丢失)
*p = '\0'; // 将第一个 'l' 位置设为结束标志
char *t1 = (char*)malloc(strlen(s) + 1); // 分配内存
strcpy(t1, s); // 复制 "He"
*p = c; // 恢复 'l'(还原字符串)
printf("%s\n", t1); // 输出 He

// 2. 提取第一个 'l' 之后的内容("llo")
char *t2 = (char*)malloc(strlen(p) + 1);
strcpy(t2, p); // 复制 "llo"
printf("%s\n", t2); // 输出 llo

// 3. 查找第二个 'l' 并输出其后内容("o")
p = strchr(p + 1, 'l'); // 从第一个 'l' 后开始查找
printf("%s\n", p); // 输出 lo(第二个 'l' 及后续)

// 释放动态内存
free(t1);
free(t2);
return 0;
}

7. 字符串查找(strstr / strcasestr)

  • strstr:在 s1 中查找 s2 子串,找到返回子串起始指针;未找到返回 NULL
    原型:char *strstr(const char *s1, const char *s2);
  • strcasestr:功能与 strstr 相同,但忽略大小写(部分编译器支持)。

期中测验

  1. 编译预处理指令#include编译预处理指令,不是C语言关键字(C语言关键字如 intif 等)。
  2. 无符号短整型溢出unsigned short sht = 0; sht--;
    无符号类型溢出后“循环计数”,unsigned short 为16位,范围是 0~655350-1 结果为 65535(即 2^16 - 1)。
  3. 逻辑判断等价while (!e) 等价于 while (e == 0)!e 表示“e为假”,C语言中0为假,非0为真)。
  4. sizeof 静态特性sizeof静态运算符(编译时计算结果),sizeof(i++) 不会执行 i++i 的值不变)。

期末考试

1. scanf 输入匹配

题目:scanf("%d%c%f", &op1, &op, &op2); 需使 op1=1op='*'op2=2.0,正确输入为 D. 1 2*。
解析:无空格时,%d 读取 1 后,%c 读取 *(无空白字符干扰),%f 读取 2(自动转为 2.0);其他选项的空格会导致 %c 读取空格,不符合要求。

2. 逻辑等价

while (!x && !y) 等价于 while (!(x || y))(德摩根定律:!x && !y = !(x || y))。

3. for 循环条件省略

for(表达式1;;表达式3) 等价于 for(表达式1; 1; 表达式3)(省略循环条件时,默认条件为“真”,即 1)。

4. char 类型输出

char ch = -1; printf("%hhd\n", ch); 输出 -1
解析:%hhdchar 类型的格式符,char 为有符号类型时,-1 存储为补码 0xFF,输出时还原为 -1

5. 函数调用合法性

给定原型 void f(double dd); 和变量 double a;,不能编译的调用为 **D. f(&a);**。
解析:&adouble* 类型(指针),与参数 double 类型不匹配;其他选项会自动类型转换(如 1u1 转为 double),可编译。

6. 无效变量名

struct 是C语言关键字,不能作为变量名(关键字用于定义语法,不可用作标识符)。

7. 字符串长度与修改

代码:

1
2
3
char s[]="Zhejiang";  // 长度 8(Z h e j i a n g)
s[strlen(s)/2 - 1] = 0; // strlen(s)/2-1 = 3,s[3] = '\0'
printf("%lu#%s#", strlen(s), s);

输出 **3#Zhe#**。
解析:s[3] 设为 '\0' 后,字符串截断为 "Zhe",长度为 3

8. 字符串插入排序

代码功能:将 s="fbla" 的字符插入到 a="cehiknqtw" 中,保持升序,最终输出 abcefhiklnqtw
解析:逐字符遍历 s,找到每个字符在 a 中的插入位置,后移元素并插入,最终得到升序字符串。

9. swap 函数错误

代码:

1
2
3
4
5
6
7
void swap(int *pa, int *pb) {
int pt;
pt = *pa, *pa = *pb, *pb = *pa; // 逗号表达式,优先级低
}
int main() {
int x=1, y=2; swap(&x, &y); printf("%d%d", x, y);
}

输出 22
解析:逗号表达式从左到右执行,*pa = *pbx 变为 2)后,*pb = *pay 也变为 2),未实现交换;正确写法需用中间变量暂存 *pa,再赋值。