简单的 C 语言程序和算法

什么是计算机程序

所谓程序,就是一组计算机能识别和执行的「指令」。
程序: 一堆代码。
eg: 学生成绩统计程序。

程序≠软件。
软件是程序、数据及相关文档的集合,是程序“打包”后的产物。可以理解为:程序是“原材料”,软件是“成品”

![[软件与程序的关系]]

什么是计算机语言

计算机语言是人与计算机交流的“桥梁”。

  • 机器语言(低级语言)
    由0与1组成的二进制指令,计算机能直接执行。
  • 汇编语言(低级语言)
    用助记符(如MOV, ADD)表示机器指令,是机器语言的符号化。
  • 高级语言
    更接近人类自然语言和数学语言。
    • 非结构化语言
    • 结构化语言(如:C语言)
    • 面向对象语言(如: Java, C++)

C 语言的特点

  1. 语言简洁、紧凑、灵活:仅有32个关键字。
  2. 运算符和数据类型丰富:能高效处理各种复杂运算。
  3. 程序设计结构化、模块化
    结构化分为 「顺序结构」「选择结构」「循环结构」。这三大结构是构建所有复杂程序的基石。
  4. 生成目标代码质量高:程序执行效率高。
  5. 可移植性好:稍加修改即可在不同类型的计算机上运行。

C 的 32个关键字

这些是系统定义的保留字,不能用作其他定义(如变量名)。
eg: 比如肯德基商标被注册了,你就不能再开一个叫肯德基的店了。

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 unsigned union void
volatile while

关键字的共同特点:全都是小写字母

标识符

^97be1e

标识符是为变量、函数等实体起的名字。

  • 用户标识符规则: ^764b2c
    1. 由字母、数字、下划线(_)组成。
    2. 不能以数字开头
    3. 区分大小写(例如 sumSum 是两个不同的标识符)。
    4. 不能与关键字相同
  • 命名建议:尽量做到“见名知意”,如用 student_count 而非 a

最简单的 C语言程序

#include <stdio.h>  // 编译预处理指令:引入标准输入输出头文件
int main()          // 主函数,每个C程序有且仅有一个main函数,程序从这里开始执行
{
    printf("Hello World\n"); // 语句:调用printf函数在屏幕上输出“Hello World”并换行
    return 0;                // 语句:主函数结束,并向操作系统返回0,表示程序正常退出
}

C 语言程序的结构

结构特点

  • 函数与主函数
    • 程序由一个或多个函数组成。
    • 必须有且只能有一个主函数 main()
    • 程序执行从 main 开始,在 main 中结束,可以调用其他函数。
  • 程序语句
    • C程序由语句组成,完成特定操作。
    • 用分号 ; 作为语句终止符。
  • 注释
    • // 单行注释/* 多行注释 */,用于解释代码,编译器会忽略。

格式特点

  • 习惯用小写字母,大小写敏感
  • 不使用行号,无程序行概念。
  • 可使用空行和空格增强可读性。
  • 常用锯齿形(缩进)书写格式,使结构清晰。
    C程序格式示例

运行 C程序的步骤和方法

编写C程序到运行结果,一般需要四个步骤,可以用下图表示:
![[运行 C语言的步骤和方法]]

  1. 编辑:用文本编辑器编写源代码,保存为 .c 文件。
  2. 编译:用编译器将源代码翻译成机器语言,生成 .obj 目标文件。此阶段检查语法错误。
  3. 链接:将目标文件与系统库文件等连接起来,生成 .exe 可执行文件。
  4. 运行:执行 .exe 文件,得到运行结果。

算法

一个程序应该包括两个方面的内容:

  • 对数据组织的描述数据结构。例如顺序存储(数组)和链式存储(链表)。
  • 对操作步骤的描述算法。例如穷举、迭代、递归,以及查找、删除、排序等具体操作。

著名公式:数据结构 + 算法 = 程序

算法,就是为了解决一个问题而采取的确切、有限的方法和步骤

算法举例:求 1 + 2 + 3 + ... + 100 的和

问题分析:这是一个累加问题,从1加到100。
算法设计(描述)

  1. 设一个变量 sum 存放和,初始值为0;设一个变量 i 代表当前要加的数,初始值为1。
  2. 如果 i 的值小于或等于100,则执行第3步;否则,转到第5步。
  3. i 的值加到 sum 中,即 sum = sum + i
  4. i 的值增加1,然后返回第2步。
  5. 输出 sum 的值,它就是最终结果。

思考:这个算法用到了“循环”的思想。你可以尝试设计一个直接套用公式 (1+100)*100/2 的算法,比较两者的不同。

#include <stdio.h> // 包含标准输入输出头文件,以便使用printf函数

int main() {
    // 步骤1: 声明并初始化变量
    int sum = 0; // 变量sum用来存放和,初始值为0
    int i = 1;   // 变量i代表当前要加的数,初始值为1

    // 步骤2-4: 使用while循环实现累加过程
    while (i <= 100) { // 步骤2:判断 i 是否小于或等于100
        sum = sum + i; // 步骤3:将 i 的值加到 sum 中
        i = i + 1;     // 步骤4:让 i 的值增加1,准备下一次循环
    } // 循环会一直执行,直到 i > 100,然后跳转到第5步

    // 步骤5: 输出结果
    printf("1到100的和是:%d\n", sum); // 输出sum的值

    return 0; // 程序正常结束
}

代码解析

  1. 初始化int sum = 0; 和 int i = 1; 对应算法步骤1。
  2. 循环条件while (i <= 100) 对应步骤2的“如果 i 的值小于或等于100,则执行...”。
  3. 循环体
    • sum = sum + i; 对应步骤3的“将 i 的值加到 sum 中”。
    • i = i + 1; 对应步骤4的“让 i 的值增加1”。
  4. 循环流程:执行完一次循环体后,程序会回到while行再次检查条件(i <= 100),这对应步骤4的“然后返回第2步”。
  5. 输出结果:当i变为101时,不满足i <= 100,循环结束。程序执行到printf语句,输出最终结果。

算法的特性

  1. 有穷性:算法必须包含有限的操作步骤,不会无限循环。
  2. 确定性:算法中的每一步骤都应当是明确的,没有歧义。
  3. 有零个或多个输入:执行算法时可能需要从外界获取数据,但有些算法(如生成随机数)无需输入。
  4. 有一个或多个输出:算法必须产生结果,没有输出的算法没有意义。
  5. 有效性:算法中的每一步都必须是计算机可以执行的基本操作,并能得到确定的结果。

基本的数据类型

如何学好 C 语言:

  1. 理解与记忆:理解核心概念,记忆基本规则(如语法、关键字)。
  2. 实践与练习:多读、多写、多调试代码。

C语言数据类型总览

C语言的数据类型系统非常丰富,下图展示了其完整的分类:
C语言数据类型总览

基本数据类型

我们首先学习最常用、最基本的几种数据类型,它们是构建更复杂数据结构的基石。
基本数据类型

常量

定义:在程序运行过程中,其值不能被改变的量。

一、符号常量

用标识符(一个名字)来代表一个常量。

  • 定义格式#define 符号常量 常量值
    • 这是编译预处理指令,不是C语句,因此末尾没有分号
    • 在编译前,预处理器会将程序中所有出现该符号常量的地方,直接替换成对应的常量值。
  • 习惯:符号常量名通常用大写字母表示,以便与变量区分。
  • 优点
    1. 含义清晰:用一个有意义的名称(如PI)代替晦涩的数字(如3.14159),提高程序可读性。
    2. 一改全改:当常量的值需要修改时(例如税率调整),只需修改#define这一行即可,避免在代码中四处查找和修改,减少错误。

例题:符号常量的使用

#include <stdio.h>
#define PRICE 30   // 定义符号常量PRICE,代表单价30元

int main() {
    int total;
    total = PRICE * 5; // 编译前,此行会被替换为 total = 30 * 5;
    printf("总价格为:%d元\n", total);

    // PRICE = 40; // 错误!符号常量不能被重新赋值。
    return 0;
}

解析#define PRICE 30定义后,PRICE就固定代表30。程序计算total = PRICE * 5时,实际执行的是total = 30 * 5。试图给PRICE赋值是语法错误。

二、直接常量(字面常量)

直接写在代码中的常量值,根据其形式分为以下几类:

1. 整型常量

即整常数,可以用三种进制表示:

  • 十进制:最常用,由数字0~9和正负号组成。如 123, -456, 0
  • 八进制:以数字 0 开头,后跟0~7的数字。如 0123(十进制为 1*8^2 + 2*8^1 + 3*8^0 = 83)。
  • 十六进制:以 0x0X 开头,后跟09,AF(或a~f)的数字。如 0x1A(十进制为 1*16^1 + 10*16^0 = 26)。

例题:不同进制的整型常量

#include <stdio.h>
int main() {
    printf("十进制 100: %d\n", 100);
    printf("八进制 0100 的十进制值: %d\n", 0100); // 输出64
    printf("十六进制 0x100 的十进制值: %d\n", 0x100); // 输出256
    return 0;
}

2. 实型常量(浮点型常量)

即带小数点的实数,有两种表示形式:

  • 十进制小数形式:必须包含小数点。例如 3.14, 0.5 (可写为 .5), 100. (可写为 100.0)。
  • 指数形式:用于表示很大或很小的数,格式为 a E na e n,表示 a × 10^n。
    • 规则E (或 e) 前后必须有数字,且指数 n 必须为整数
    • 例如:3.14E-2 表示 3.14×10^{-2} (即0.0314),-5.6e8 表示 -5.6×10^8。

例题:实型常量的表示

#include <stdio.h>
int main() {
    printf("小数形式: %f\n", 123.456);
    printf("指数形式(大数): %f\n", 1.23456e2); // 等价于 1.23456 * 10^2 = 123.456
    printf("指数形式(小数): %f\n", 1.23456E-2); // 等价于 1.23456 * 10^{-2} = 0.0123456
    // printf("%f\n”, 1.2E);   // 错误!E后面没有数字。
    // printf("%f\n”, E5);     // 错误!E前面没有数字。
    return 0;
}

3. 字符常量

单引号括起来的一个字符。

  • 普通字符:如 'A', '2', '+'
  • 转义字符:以反斜杠 \ 开头,后跟特定字符,表示具有特殊功能的字符或难以直接输入的字符。
    转义字符表
    常用转义字符:
    • \n: 换行
    • \t: 水平制表符(Tab)
    • \\: 表示一个反斜杠本身
    • \': 表示一个单引号
    • \": 表示一个双引号
    • \0: 空字符(NULL),ASCII码为0,常用于字符串结束标志。

例题:字符常量与转义字符

#include <stdio.h>
int main() {
    printf("字符常量: %c\n", 'A'); // 输出字符 A
    printf("转义字符-换行: Hello\nWorld\n"); // \n使输出换行
    printf("转义字符-制表: Name\tAge\nJohn\t20\n"); // \t产生对齐效果
    printf("输出单引号本身: \' \n"); // 输出一个 '
    printf("输出反斜杠本身: \\ \n"); // 输出一个 \
    return 0;
}

4. 字符串常量

双引号括起来的一串字符序列

  • 例如:"Hello, World!", "A", "123"
  • 存储特点:系统会在字符串的末尾自动添加一个空字符 '\0' 作为字符串的结束标志。因此,字符串 "A" 在内存中实际占用2个字节:'A''\0';而字符常量 'A' 只占1个字节。
  • 与字符常量的区别"A" 是字符串常量,'A' 是字符常量,两者类型不同,占用内存大小也不同。

例题:字符串常量与结束标志

#include <stdio.h>
#include <string.h> // 引入字符串处理函数头文件

int main() {
    printf("字符串: %s\n", "Hello");

    // 演示字符串的结束标志 '\0'
    char str1[] = "ABC"; // 数组 str1 实际存储为:'A', 'B', 'C', '\0'
    char str2[] = {'A', 'B', 'C', '\0'}; // 手动添加结束标志,与str1等价
    char str3[] = {'A', 'B', 'C'}; // 没有结束标志,不是规范的C字符串

    printf("str1长度(strlen): %zu\n", strlen(str1)); // 输出3,strlen函数遇到'\0'停止计数
    printf("str2长度(strlen): %zu\n", strlen(str2)); // 输出3
    // printf("str3长度(strlen): %zu\n”, strlen(str3)); // 危险!str3没有'\0',strlen会一直向后读取直到遇到内存中的'\0',结果不可预知。

    return 0;
}

解析:这个例题清晰地展示了字符串的存储方式及其与字符数组的区别。理解 '\0' 作为结束标志是掌握C语言字符串操作的关键。

变量

在程序运行期间,值可以发生变化的量。
基本操作:

  1. 赋值:将一个值存储到变量中。
  2. 取值:从变量中读取存储的值。

变量命名: 遵守C 语言[[#^764b2c]]标识符命名规则。

使用原则:先定义,后使用。编译器需要知道变量的数据类型,才能为其分配正确的内存空间。

变量的定义与初始化

变量定义语句的一般形式为:

类型标识符 变量名1, 变量名2, ...;

例如:

int a, b;        // 定义两个整型变量 a 和 b
float f;         // 定义一个单精度浮点型变量 f
double x;        // 定义一个双精度浮点型变量 x
char ch;         // 定义一个字符型变量 ch

在定义变量的同时可以为其赋初值,这称为初始化

int sum = 0;     // 定义整型变量 sum 并初始化为 0
float e = 2.717; // 定义浮点变量 e 并初始化为 2.717
char ch = 'a';   // 定义字符变量 ch 并初始化为字母 'a'

数据类型转换

在 C 语言中,不同类型的数据进行运算时,编译器会自动或按程序员指示进行类型转换。

1. 自动类型转换(隐式转换)

当不同类型的变量混合运算时,编译器会遵循一套规则自动进行类型提升,以保证精度不丢失。基本规则是:向数据范围更广、精度更高的类型转换

常见的自动转换方向:
char / short -> int -> unsigned int -> long -> unsigned long -> float -> double

示例:

int i = 10;
float f = 3.14;
double d;

d = i + f; // i 自动转换为 float 与 f 相加,结果再自动转换为 double 赋给 d
// 过程:int(10) -> float(10.0) -> 10.0 + 3.14 = 13.14 -> double(13.14)

在赋值运算中,等号右边的值会自动转换为左边变量的类型。

int num;
float pi = 3.14159;
num = pi; // pi 的值被自动转换为 int 类型,小数部分被截断。num 最终为 3。

2. 强制类型转换(显式转换)

程序员可以使用强制类型转换符,明确指示编译器将一种类型转换为另一种类型。

形式:

(目标类型名) 表达式

示例:

(double) 2        // 将整数 2 强制转换为 double 类型,得到 2.0
(int) 2.2         // 将浮点数 2.2 强制转换为 int 类型,小数部分被丢弃,得到 2

float f = 3.6;
int a = (int)f;   // a 的值为 3

int x = 10, y = 4;
float result;
result = (float)x / y; // 将 x 强制转为 float,使除法变为浮点除法,result = 2.5
// 如果不强制转换:result = x / y; 则为整数除法,结果为 2.0

运算符和表达式

算术运算符和算术表达式

基本的算术运算符:+(加) -(减) *(乘) /(除) %(求余,模运算)。

  • 结合方向: 从左到右。
  • 优先级: *, /, % > +, -

重要细节:

  1. 除法 /:
    • 如果 / 两边的操作数都是整数,则执行整数除法,结果舍弃小数部分,只保留整数
    • 如果 / 两边的操作数至少有一个是浮点数,则执行浮点数除法,结果为浮点数。
    5 / 2     // 结果为 2 (整数除法)
    5.0 / 2   // 结果为 2.5 (浮点数除法)
    
  2. 求余 %:
    • 求余运算符要求两端的操作数必须都是整数
    • 结果的符号与被除数 (a % b 中的 a) 的符号相同。
    7 % 3     // 结果为 1
    -7 % 3    // 结果为 -1
    7 % -3    // 结果为 1 (在大多数编译器中)
    // 7.5 % 3 // 错误!不能对浮点数使用 % 运算符
    

c 表_upscayl_4x_upscayl-standard-4x.png

赋值运算符和表达式

  • 基本赋值运算符: =
  • 赋值表达式形式: 变量 = 表达式
    • 例如:x = 5; a = 3 + 2;
  • 优先级: 非常低,仅高于逗号运算符。
  • 结合性: 自右向左。这使得链式赋值成为可能。
    a = b = c = 5; // 等价于 c = 5; b = c; a = b;
    
  • 说明: 赋值表达式尾部加上分号,即构成赋值语句。
    y = x + 10; // 这是一个赋值语句
    ; // 单独一个分号是空语句
    

复合赋值运算符

包含:+=, -=, *=, /=, %=

  • 优先级与结合性: 与基本赋值运算符 = 相同。
  • 作用: 简化“运算后赋值”的操作。
a += 2;   // 完全等效于 a = a + 2;
a -= 2;   // 完全等效于 a = a - 2;
a *= 2;   // 完全等效于 a = a * 2;
a /= 2;   // 完全等效于 a = a / 2;
a %= 2;   // 完全等效于 a = a % 2;

// 注意右边是一个整体表达式
b %= b + 2; // 等效于 b = b % (b + 2); 而不是 b = b % b + 2;

自增自减运算符

运算符:自增 (++)、自减 (--)。
作用:使单个变量的值增 1 或减 1。

用法和运算规则:

  • 前置运算: ++i, --i 【先增减、后运算】
  • 后置运算: i++, i-- 【先运算、后增减】

1. 前置运算(++i, --i):先变后用

前置就像是“先付钱,再拿货”。程序会先执行加 1 操作,然后再把加完后的新值拿去参与运算。

  • 执行逻辑
    1. i 自身增加 1。
    2. 返回 i 更新后的值。

代码示例:

int i = 5;
int a = ++i;
// 过程:i 先变成 6,然后把 6 赋值给 a。
// 结果:a = 6, i = 6

2. 后置运算(i++, i--):先用后变

后置就像是“先拿货,后付钱”。程序会先记录下变量当前的值,让这个旧值参与运算,等运算结束的那一刻,再偷偷把变量加 1。

  • 执行逻辑
    1. 先记住 i 当前的值(产生一个临时副本)。
    2. i 自身增加 1。
    3. 返回刚才记下的那个“旧值”。

代码示例:

int i = 5;
int a = i++;
// 过程:先记录 i 的旧值 5 赋给 a,然后 i 再变成 6。
// 结果:a = 5, i = 6

重要提示:

  • 自增自减运算符的操作数必须是变量,不能是常量或表达式。
    int i = 5;
    i++;     // 正确
    5++;     // 错误!常量不能自增
    (i+1)++; // 错误!表达式不能自增
    
  • 在复杂的表达式中避免对同一变量多次使用 ++/--,因为其求值顺序可能因编译器而异(未定义行为)。
    int i = 0;
    int b = i++ + i++; // 避免这样写!结果不确定。
    

image.png

逗号运算符和表达式

  • 形式: 表达式1, 表达式2, ..., 表达式n
  • 结合性: 从左到右依次计算。
  • 优先级: 所有运算符中最低。
  • 逗号表达式的值: 等于最后一个表达式 表达式n 的值。

例:

// 例1:整个逗号表达式的值是 a*4 的值
a = 3*5, a*4;
// 计算:a先被赋值为15,然后计算 a*4 得60。整个逗号表达式的值是60,但a的值仍是15。

// 例2:赋值只对第一个表达式有效
a = 3*5, a*4, a+5;
// 计算:a=15, 计算 15*4=60(丢弃), 计算 15+5=20。整个表达式的值是20,a=15。

// 例3:括号改变优先级
x = (a = 3, 6 * 3); // 括号内是逗号表达式,值=18。所以 x = 18, a = 3。

// 例4:注意结合性
x = a = 3, 6 * a;   // 等价于 (x = a = 3), (6 * a)
                   // x 和 a 都等于3,整个逗号表达式的值是 6*3=18。

// 例5:逗号作为分隔符 vs 运算符
int a = 1, b = 2, c = 3; // 这里的逗号是分隔符
printf("%d,%d,%d", a, b, c); // 输出:1,2,3

printf("%d,%d,%d", (a, b, c), b, c); // 第一个参数是逗号表达式(a,b,c),值为c=3
                                      // 输出:3,2,3

关系运算符和表达式

  • 种类: <(小于) <=(小于等于) >(大于) >=(大于等于) ==(等于) !=(不等于)。
  • 结合方向: 自左向右。
  • 优先级别:
    • 较高:<, <=, >, >=
    • 较低:==, !=
  • 关系表达式的值: 关系成立则为逻辑“真”,用整数 1 表示;不成立则为逻辑“假”,用整数 0 表示。
    int result;
    result = (5 > 3);  // result 的值为 1
    result = (5 == 3); // result 的值为 0
    

逻辑运算符和表达式

  • 种类: !(逻辑非) &&(逻辑与) ||(逻辑或)。

image.png

运算规则(真值表简化):

  • &&(与): 有假即为假。全真才为真。
  • ||(或): 有真即为真。全假才为假。
  • !(非): 真假互换。

C语言中的真假:

  • 判断运算量时: 0 表示“假”,任何非 0 值(如1, -1, 100)都表示“真”。
  • 逻辑运算的结果: 只产生整数 1(真)或 0(假)。

优先级(从高到低): ! > && > ||
结合方向: ! 从右向左;&&|| 从左向右。

短路求值特性

逻辑表达式求解时,并非所有运算符都会被执行。只有在必须执行下一个运算符才能确定整个表达式的最终结果时,才会执行它。

  • a && b:如果 a 为假(0),则整个表达式结果已确定为假,b 将不会被计算
  • a || b:如果 a 为真(非0),则整个表达式结果已确定为真,b 将不会被计算

示例:

int i = 0, a = 5;
if (i != 0 && a / i > 2) { // 由于 i!=0 为假,`a / i` 这个危险的除法根本不会执行
    // 安全地避免了除零错误
}

int j = 10;
if (j > 5 || ++j < 20) { // 由于 j>5 为真,`++j` 不会执行,j 的值仍为 10
    printf(“%d”, j); // 输出 10
}

image.png

条件运算符和表达式

条件运算符 (?:) 是 C 语言中唯一的三目运算符(需要三个操作数),它提供了一种简洁的表达“二选一”逻辑的方式,可以看作是 if-else 语句的简化形式。

  • 一般形式:

    表达式1 ? 表达式2 : 表达式3
    
    • 表达式1:通常是一个关系或逻辑表达式,用于判断条件。
    • 表达式2:当 表达式1真(非0) 时,整个条件表达式的值。
    • 表达式3:当 表达式1假(0) 时,整个条件表达式的值。
  • 执行过程(“二选一”):

    1. 首先计算 表达式1 的值。
    2. 如果 表达式1 的值为真(非0),则计算 表达式2 的值,并将该值作为整个条件表达式的最终结果表达式3 不会被执行
    3. 如果 表达式1 的值为假(0),则计算 表达式3 的值,并将该值作为整个条件表达式的最终结果表达式2 不会被执行

    其逻辑可以概括为:为真取前者(表达式2),为假取后者(表达式3)

    ![[条件运算符执行过程]]

  • 优先级与结合性:

    • 优先级(13): 高于赋值运算符 (=),但低于大多数其他运算符(如算术、关系、逻辑运算符)。通常为了清晰,建议在复杂表达式中使用括号 ()
    • 结合性:自右向左。这一特性在嵌套使用时尤为重要,决定了多个条件运算符同时出现时的求值顺序。
  • 嵌套使用:
    由于结合性是自右向左,可以将多个条件运算符串联起来,实现多分支选择(类似于 if-else if-else 链)。

    示例:将成绩分数转换为等级

    int score = 85;
    char grade;
    
    // 使用嵌套的条件表达式
    grade = (score >= 90) ? 'A' : 
            (score >= 80) ? 'B' : 
            (score >= 70) ? 'C' : 
            (score >= 60) ? 'D' : 'F';
    
    printf("成绩 %d 的等级是:%c\n", score, grade); // 输出:成绩 85 的等级是:B
    

    执行顺序解析(自右向左结合):

    1. 先看最右边的 (score >= 60) ? 'D' : 'F',这是一个独立的条件表达式。
    2. 它作为其左边表达式 (score >= 70) ? 'C' : (上一步结果) 的“假值部分”。
    3. 以此类推,从左到右进行判断,直到找到第一个为真的条件。
  • 常见用途:

    1. 简化简单的 if-else 赋值:
      int a = 5, b = 10, max;
      // if-else 写法
      if (a > b) {
          max = a;
      } else {
          max = b;
      }
      // 条件运算符写法(更简洁)
      max = (a > b) ? a : b;
      
    2. 在函数调用或表达式中进行内联选择:
      printf("绝对值是:%d\n", (x >= 0) ? x : -x);
      int fee = (age < 18) ? 50 : 100; // 根据年龄选择费用
      

    注意: 虽然条件表达式很简洁,但过度嵌套或逻辑复杂时会降低代码可读性。在这种情况下,使用 if-else 语句通常是更好的选择。

数据的输入和输出

C语言没有 I/O 语句,I/O 操作由函数实现
#include <stdio.h>

数据输出

  1. 字符输出函数
    格式: putchar(c)
    参数:c 为字符常量、变量或者表达式
    功能: 把字符 c 输出到显示器上
    例题

    #include <stdio.h>
    main()
    {
        int c;
        char a;
        c = 65 ;
        a = 'B';
        putchar(c);putchar('\n');putchar(a);  // A 
                                              // B 
    }
    
  2. 格式输出函数
    格式: printf("格式输出串",输出表)
    功能: 按指定格式向显示器输出数据
    输出表:要输出的数据(可以没有,多个时以“,”分隔)
    格式输出串: 包含两种信息
    - 格式说明: %[修饰符] 格式字符,用于指定输出格式
    - 普通字符或转义序列:原样输出
    格式字符
    image.png

[!note]
说明

  • 格式字符要用小写
  • 格式字符与输出项个数应相同,按照先后顺序一一对应
  • 输出转换: 格式字符与输出项类型不一致,自动按指定格式输出

有符号的 -1 就等价于无符号的 65535

image.png

数据输入

在C语言中,要从键盘获取用户输入的数据,最常用的是字符输入函数 getchar() 和格式输入函数 scanf()

1. 字符输入函数 getchar()

  • 函数原型: int getchar(void);
  • 功能:标准输入流(通常为键盘) 读取下一个字符(包括空格、制表符、换行符)。
  • 返回值:
    • 成功时,返回读取到的字符(转换为 unsigned char 类型,再转为 int)。
    • 遇到文件结束符(EOF,在Windows命令行中通常由 Ctrl+Z 产生)或发生错误时,返回 EOF(通常定义为 -1)。
  • 特点: 它是一个阻塞式函数,程序会暂停并等待用户输入,直到用户按下回车键。回车键本身(\n)也会作为一个字符被读取或留在输入缓冲区中。

示例:从键盘读取一个字符并回显

#include <stdio.h>

int main() {
    char ch;
    printf("请输入一个字符,然后按回车键:");
    ch = getchar(); // 等待用户输入,读取第一个字符

    printf("您输入的字符是:%c\n", ch);
    printf("它的ASCII码是:%d\n", ch);

    // 一个常见用法:用getchar()来“吃掉”scanf输入后留在缓冲区的换行符
    int num;
    printf("请输入一个数字:");
    scanf("%d", &num);
    getchar(); // 读取并丢弃数字输入后残留的换行符,为后续输入清空缓冲区

    printf("现在请输入一个字母:");
    ch = getchar();
    printf("字母是:%c\n", ch);

    return 0;
}

2. 格式输入函数 scanf()

  • 函数原型: int scanf(const char *format, ...);
  • 格式: scanf("格式控制串", 地址表);
  • 功能: 按照“格式控制串”指定的格式,从标准输入读取数据,并依次存入“地址表”中对应的变量内存地址里。输入同样以回车键结束。
  • 地址表: 必须是变量的内存地址。对于基本类型变量(如 int, float, char 数组首字符),使用取地址运算符 & 来获取其地址。对于字符数组名(字符串)和指针变量,其本身代表地址,则不需要 &

示例:读取不同类型的数据

#include <stdio.h>

int main() {
    int age;
    float height;
    char name[50]; // 字符数组,用于存储字符串

    printf("请依次输入您的年龄、身高(米)和姓名(用空格隔开):\n");
    // 注意:name是数组名,本身代表地址,所以不需要 &name
    int count = scanf("%d %f %s", &age, &height, name);

    printf("成功读取了 %d 个数据。\n", count);
    printf("年龄:%d岁,身高:%.2f米,姓名:%s\n", age, height, name);

    return 0;
}

输入分隔符的指定:

  • 一般以空格回车键作为分隔符
  • 其他字符做分隔符:格式串中两个格式符间字符

3. gets() 是一个 C 语言函数,作用是:从键盘读入一整行文字,存到你给它的变量里。

gets(str[i]); 到底在做什么?
假设你有一个字符串数组,大概是这样定义的:
c
运行
char str[100][100];  // 可以存 100 个字符串,每个最长100字符
那么:
str = 整个字符串数组
str[i] = 第 i 个字符串的地址(就是一段可以放文字的空间)
gets(str[i]) =
从键盘读入一行文字,直接存到第 i 个字符串里

fgets

编写程序,将⽤户输⼊的字符串中所有的字符a去掉,然后输出剩余的字符。


#include <stdio.h>
int main(){
    char a[100],b[100];
    int i ,j=0;
    fgets(a,100,stdin);  // 100 限定长度,stdin 标准输入
    /*
    * //scanf("%s",a) 雷同,这是整个地址。
    */
    for(i = 0; i<100 ; i++){
        if(a[i] != 'a'){
            b[j] = a[i];
            j++;
        }
    }
    puts(b); // 输出,与 gets 对应
}

\0 写法,节约时间和运行空间

//编写程序,将⽤户输⼊的字符串中所有的字符a去掉,然后输出剩余的字符。
#include <stdio.h>
int main(){
    char a[100],b[100];
    int i ,j=0;
    fgets(a,100,stdin);  // 100 限定长度,stdin 标准输入
    /*
    * //scanf("%s",a) 雷同,这是整个地址。
    */
    for(i = 0; a[i] != '\0' ; i++){
        if(a[i] != 'a'){
            b[j] = a[i];
            j++;
        }
    }
    b[j] = '\0';
    puts(b);
}

重要的格式控制符与示例:

格式符 含义 变量类型 示例输入 (scanf语句)
%d 读取十进制整数 int * scanf(“%d”, &num);
%f 读取浮点数 float * scanf(“%f”, &price);
%lf 读取双精度浮点数 double * scanf(“%lf”, &pi);
%c 读取单个字符 char * scanf(“ %c”, &ch); (注意%c前的空格)
%s 读取一个字符串(遇空格/换行终止) char * (字符数组) scanf(“%s”, str);
%[...] 扫描字符集合 char * scanf(“%[A-Z]”, str); //只读入大写字母

当 %d%d 在一起,一定要有间隔
当 %c%c 在一起,不需要间隔
当 %d%c 在一起,不需要间隔
当 %c%d在一起,有没有间隔都可以
%c 前面一定不可以有间隔

⚠️ 关键注意事项(常见陷阱与解决方案):

  1. scanfgetchar 的缓冲区问题:
    scanf 在读取数字或字符串后,会将用户按下的回车符(\n 留在输入缓冲区。如果后面紧跟着一个 getchar()scanf(“%c”, &ch),它会立刻读取到这个残留的 \n,导致程序“跳过”等待。
    解决方案: 在读取字符前,使用 getchar() 清空缓冲区。

    int a;
    char op;
    scanf(“%d”, &a);   // 输入 10<回车>
    getchar();          // 读取并丢弃缓冲区中的回车符
    op = getchar();     // 现在可以正常等待用户输入运算符,如 ‘+’
    
  2. scanf 读取字符串的安全问题:
    scanf(“%s”, str); 不会检查数组边界,如果输入过长会导致缓冲区溢出,这是严重的安全漏洞。
    更安全的替代方案: 使用 fgets() 函数读取整行。

    char safe_str[100];
    fgets(safe_str, sizeof(safe_str), stdin); // 从标准输入读取,最多99个字符+‘\0’
    
  3. scanf 的返回值:
    scanf 返回成功匹配并赋值的数据项个数。可以利用它来判断输入是否符合预期格式。

    if (scanf(“%d %d”, &x, &y) != 2) {
        printf(“输入格式错误!需要两个整数。\n”);
        // 清空错误输入缓冲区
        while (getchar() != ‘\n’);
    }
    

复合语句

形式 : a=2;b=3;c=a+b;
说明: 复合语句在语法上视为一条语句

空语句

形式:
说明: 空语句的功能是什么也不作

选择结构

if 语句

1. 单分支 if 语句

用于条件成立时执行特定操作。
语法:

if (条件表达式) {
    语句块;
}

示例:

if (score >= 60) {
    printf(“及格\n”);
}

2. 双分支 if-else 语句

用于在两种可能的情况下选择其一执行。
语法:

if (条件表达式) {
    语句块1;
} else {
    语句块2;
}

示例:

if (score >= 60) {
    printf(“及格\n”);
} else {
    printf(“不及格\n”);
}

3. 多分支 if-else if-else 语句

用于在多个条件中进行选择。
语法:

if (条件表达式1) {
    语句块1;
} else if (条件表达式2) {
    语句块2;
} // ... 可以有多個 else if
else {
    语句块N; // 所有条件都不满足时执行
}

示例:

if (score >= 90) {
    printf(“优秀\n”);
} else if (score >= 80) {
    printf(“良好\n”);
} else if (score >= 60) {
    printf(“及格\n”);
} else {
    printf(“不及格\n”);
}

switch 语句

用于基于一个整型或字符型表达式的值,从多个选项(case)中选择一个执行。与 if 相比,switch 在判断离散、固定的值时结构更清晰。

语法:

switch (表达式) {
    case 常量表达式1:
        语句块1;
        break; // 通常需要 break 来跳出 switch
    case 常量表达式2:
        语句块2;
        break;
    // ... 可以有多个 case
    default: // 可选的,当所有 case 都不匹配时执行
        语句块N;
        break;
}

关键点:

  • 表达式 的结果必须是整型字符型
  • case 后的 常量表达式 必须是编译时常量(如 1‘A’MONDAY)。
  • break 语句的作用:跳出整个 switch 语句。如果省略 break,程序会继续执行下一个 case 中的语句,直到遇到 breakswitch 结束(这称为“case穿透”或“落空”)。
  • default 子句是可选的,用于处理所有 case 都不匹配的情况。

示例:

int day = 3;
switch (day) {
    case 1:
        printf(“星期一\n”);
        break;
    case 2:
        printf(“星期二\n”);
        break;
    case 3:
        printf(“星期三\n”);
        break;
    default:
        printf(“无效的日期\n”);
        break;
}
// 输出:星期三

break 语句在 switch 中的作用

switch 语句中,break 语句用于终止当前 case 的执行并跳出整个 switch 语句体,防止程序继续执行下一个 case 的代码(即避免“case穿透”)。

图示说明了 break 如何控制执行流的跳出:

对比示例 (无 break):

int option = 2;
switch (option) {
    case 1: printf(“执行选项1\n”);
    case 2: printf(“执行选项2\n”); // 从这里开始执行
    case 3: printf(“执行选项3\n”); // 因为没有break,会继续执行
    default: printf(“执行默认\n”); // 继续执行
}
// 输出:
// 执行选项2
// 执行选项3
// 执行默认

总结:

  • if 语句:适用于条件判断逻辑复杂、范围判断(如 x > 10)、或条件非整型的情况。
  • switch 语句:适用于根据一个离散的整型或字符型值进行多路分支选择,结构清晰。
  • break:在 switch 中是控制流程的关键,通常每个 case 后都应添加,除非刻意利用“穿透”特性。

循环结构

C语言可实现循环的语句

  • while语句
  • do~while语句
  • for 语句

while 语句

一般形式:

while(表达式)
    循环体语句;

执行流程 :
![[while 执行流程]]
特点: 先判断表达式,后执行循环体。适用于循环次数不确定,但循环条件明确的场景。

例题1:计算 1 到 100 的累加和

#include <stdio.h>
int main() {
    int i = 1;
    int sum = 0;
    while (i <= 100) {
        sum += i; // 等价于 sum = sum + i;
        i++;      // 循环变量自增,避免死循环
    }
    printf("1到100的和为: %d\n", sum);
    return 0;
}

例题2:输入一系列正整数,以0结束,计算它们的和

#include <stdio.h>
int main() {
    int num, sum = 0;
    printf("请输入正整数(输入0结束): ");
    scanf("%d", &num);
    
    while (num != 0) {
        sum += num;
        scanf("%d", &num); // 继续读取下一个数
    }
    printf("所有数的和为: %d\n", sum);
    return 0;
}

do~while 语句

一般形式:

do {
    循环体语句;
} while(表达式); // 注意分号

执行流程:
![[do~while 语句]]
特点: 先执行循环体,后判断表达式。循环体至少会执行一次。适用于需要先执行操作再检查条件的场景(如菜单显示)。

例题:用户密码验证(至少给一次输入机会)

#include <stdio.h>
int main() {
    int password;
    int correct_pwd = 123456;
    
    do {
        printf("请输入密码: ");
        scanf("%d", &password);
        if (password != correct_pwd) {
            printf("密码错误!\n");
        }
    } while (password != correct_pwd); // 密码错误则继续循环
    
    printf("登录成功!\n");
    return 0;
}

for 语句

一般形式:

for([expr1]; [expr2]; [expr3]) {
    循环体语句;
}
// expr1: 循环变量初始化(只执行一次)
// expr2: 循环条件(每次循环前判断)
// expr3: 循环变量更新(每次循环体执行后执行)

执行流程:
![[for 语句执行流程]]
先执行 expr1 ,再判断 expr2 ,如果为真则执行循环体,然后执行 expr3,接着再次判断 expr2... 如果 expr2 为假则跳出循环。

例题1:打印九九乘法表

#include <stdio.h>
int main() {
    int i, j;
    for (i = 1; i <= 9; i++) {         // 外层循环控制行
        for (j = 1; j <= i; j++) {     // 内层循环控制列
            printf("%d*%d=%-2d ", j, i, i * j); // %-2d 左对齐,占2位
        }
        printf("\n"); // 换行
    }
    return 0;
}

例题2:计算 n 的阶乘

#include <stdio.h>
int main() {
    int n;
    long long factorial = 1; // 阶乘结果可能很大,用 long long
    
    printf("请输入一个正整数: ");
    scanf("%d", &n);
    
    // for循环的三个表达式都可以灵活变化
    for (int i = 1; i <= n; i++) {
        factorial *= i;
    }
    
    printf("%d! = %lld\n", n, factorial);
    return 0;
}

三种循环对比与选择

循环类型 特点 适用场景
while “当型”循环,先判断后执行。可能一次都不执行。 循环次数不确定,但结束条件明确。例如:读取数据直到文件结尾。
do...while “直到型”循环,先执行后判断。至少执行一次 需要至少执行一次的场景。例如:菜单显示、输入验证。
for 结构紧凑,将循环变量的初始化、条件判断、更新写在一起。 循环次数确定或需要精确控制循环变量的场景。例如:遍历数组、固定次数的计算。

提示

  1. 三种循环可以互相转换,选择哪种主要看代码可读性逻辑清晰度
  2. 循环中可以使用 break 语句立即终止整个循环,或使用 continue 语句跳过本次循环的剩余代码,直接进入下一次循环条件判断。
  3. 小心死循环!确保循环条件最终能变为假,或在循环体内有能跳出循环的机制(如 break)。

breakcontinue

break 语句

  • 功能:立即终止所在的 switch 语句或循环(for, while, do...while),并跳出该结构,继续执行结构之后的代码。
  • 一般形式break;
  • 注意break 语句只能用于 switch 语句或循环语句内部。

示例:在循环中查找元素

#include <stdio.h>

int main() {
    int numbers[] = {1, 3, 5, 7, 9, 2, 4, 6};
    int target = 5;
    int found = 0; // 0 表示未找到

    for (int i = 0; i < 8; i++) {
        if (numbers[i] == target) {
            printf("在索引 %d 找到了数字 %d。\n", i, target);
            found = 1; // 标记为已找到
            break; // 找到后立即跳出循环,不再继续遍历
        }
    }

    if (!found) {
        printf("数组中未找到数字 %d。\n", target);
    }
    return 0;
}

continue 语句

  • 功能:结束本次循环的当前迭代,跳过循环体中 continue 之后尚未执行的语句,直接进入下一次循环的条件判断for 循环则会先执行迭代语句,如 i++)。
  • 一般形式continue;
  • 注意:它并不终止整个循环,只是“跳过”一轮。

示例:打印 1 到 10 之间的奇数

#include <stdio.h>

int main() {
    for (int i = 1; i <= 10; i++) {
        if (i % 2 == 0) { // 如果 i 是偶数
            continue; // 跳过本次循环的剩余部分,不执行下面的 printf
        }
        printf("%d ", i); // 只会打印奇数
    }
    // 输出: 1 3 5 7 9
    return 0;
}

循环嵌套

一个循环体内包含另一个完整的循环结构,称为循环嵌套

  • 执行流程
    1. 外层循环执行一次。
    2. 对于外层的这一次迭代,内层循环要完整地执行其所有迭代。
    3. 内层循环结束后,回到外层循环,进行下一次迭代(如 i++),然后重复步骤2。
    4. 外层循环的条件变为假时,整个嵌套循环结束。
  • 循环体的确定
    • 如果循环体包含多条语句,必须用花括号 {} 括起来形成一个复合语句
    • 如果循环体只有一条语句,可以省略 {},此时循环体到第一个分号 ; 结束。

示例:打印九九乘法表

#include <stdio.h>

int main() {
    for (int i = 1; i <= 9; i++) { // 外层循环,控制行数(被乘数)
        for (int j = 1; j <= i; j++) { // 内层循环,控制每行的列数(乘数)
            printf("%d*%d=%2d  ", j, i, i * j); // %2d 用于对齐
        }
        printf("\n"); // 每行结束后换行
    }
    return 0;
}

示例:使用 break 在嵌套循环中

#include <stdio.h>

int main() {
    for (int i = 0; i < 3; i++) {
        printf("外层循环 i = %d\n", i);
        for (int j = 0; j < 5; j++) {
            if (j == 2) {
                break; // 此 break 只跳出当前的**内层** for 循环
            }
            printf("  内层循环 j = %d\n", j);
        }
    }
    return 0;
}
/* 输出:
外层循环 i = 0
  内层循环 j = 0
  内层循环 j = 1
外层循环 i = 1
  内层循环 j = 0
  内层循环 j = 1
外层循环 i = 2
  内层循环 j = 0
  内层循环 j = 1
*/

注意:此处的 break 仅跳出内层循环,外层循环继续正常执行。

数组的应用

数组: 是一组具有相同数据类型的数据的有序集合

定义格式: 类型说明符 数组名 [常量表达式] :
例如: int a [10]
它表示定义了一个整形数组,数组为 a,此数组有 10 个元素
a[0] , a[1],a[2] ......a[9]]

补充说明:

  • 常量表达式:通常是一个整型常量,它指定了数组的大小(元素个数),在编译时必须确定,不能是变量。
  • 下标范围:C语言数组下标从 0 开始,到 [数组长度-1] 结束。访问 a[10]越界的,会导致未定义行为。

一维数组的引用

引用方式:
数组名[下标]

下标可以是整型常量或整型表达式
例如:
int a[10];
a[0]=2
a[1] = a[5]+a[7]-a[2*3]

补充说明:

  • 数组必须先定义,后使用。
  • 引用时,下标表达式的值必须在合法的下标范围内。

一维数组的初始化

  1. 在定义数组时对数组元素赋以初值。
    int a [10] = {0,1,2,3,4,5,6,7,8,9};
  2. 可以只给一部分元素赋值------没赋值的默认为 0
    int a [10]= {0,1,2,3,4,5};
  3. 在对全部数组元素赋初值时,由于数组的个数已经确定,因此可以不指定数组长度
    例如: int a [5] = {1,2,3,4,5};
    也可以写成 int a [] = {1,2,3,4,5} ;

补充说明与例题:

  • 初始化与赋值的区别:初始化是在定义数组时完成的。定义后,不能再用 a = {1,2,3}; 这样的语法整体赋值。
  • 例题1:数组遍历与求和
    #include <stdio.h>
    int main() {
        int nums[] = {12, 45, 7, 23, 56}; // 不指定长度,由初始化列表决定
        int sum = 0;
        int size = sizeof(nums) / sizeof(nums[0]); // 计算数组元素个数
    
        for(int i = 0; i < size; i++) {
            printf("nums[%d] = %d\n", i, nums[i]);
            sum += nums[i];
        }
        printf("数组元素总和为:%d\n", sum);
        return 0;
    }
    

冒泡排序

//编写一个c语言程序,输入十个学生的成绩,储存在一维数组中,用冒泡法进行排序,按分数从高到低输出成绩。
#include <stdio.h>
int main(){
    float a[10],t;
    int i ,j;
    for(i=0;i<10;i++){
        scanf("%f",&a[i]);
    }
    for(j=0;j <9 ; j++){   // 次数=要排序的数量-1
    for(i=0;i<9-j;i++){
        if(a[i]<a[i+1]){
            t = a[i];
            a[i] = a[i+1];
            a[i+1] = t;
        }
    }
}
    for(i=0;i<10;i++){
        printf("%.2f  ",a[i]);
    }
    return 0;
}

选择排序

image.png

#include <stdio.h>
int main(){
    float a[10] ,t ;
    int i ,j , z ;
    for(i=0;i<10;i++){
        scanf("%f",&a[i]);
    }
    for(i=0;i<9;i++){
        z = i;
        for(j=i+1;j<10;j++){
            if (a[j]<a[z]){    // 这里是后者比前者 j=i+1 , 所以是下标大的比下标小的
                z = j;
            }
        }
        t = a[i];        // 因为此时的 z 不是等于 i 的状态是等于 j 的状态,所以要把 i 和 z交换
        a[i] =a[z];
        a[z] = t ;
    }
    for(i=0;i<10;i++){
        printf("%f\n",a[i]);
    }
    return 0;
}

二维数组的定义与引用

定义:
类型说明符 数组名 [常量表达式] [常量表达式];
例如
定义 a 为 3*4 (3 行四列的数组),b为 5*10 (5 行 10 列)的数组
float a[3] [4], b [5] [10]

补充说明:

  • 可以将二维数组理解为“数组的数组”。例如 a[3][4] 是一个包含3个元素的数组,每个元素又是一个包含4个整数的数组。
  • 在内存中,二维数组的元素是按行连续存放的。a[0][0], a[0][1], a[0][2], a[0][3], a[1][0]... 依次排列。

二维数组的初始化

  1. 分行给二维数组赋初值
    int a[3] [4] = {{1,2,3,4},{5,6,7,8}}
1 2 3 4
5 6 7 8
0 0 0 0
  1. 可以将所有数据写在一个花括弧内,按数组排列的顺序对各个元素赋初值。
    int a [2] [4] = {1,2,3,4,5,6,7,8}
1 2 3 4
5 6 7 8
  1. 可以对部分元素赋初值 ----未赋值的为 0
    int a [3] [3] ={{1},{5},{9}};
1 0 0
5 0 0
9 0 0
  1. 如果说对全部元素都赋初值,则定义数组时对第一维的长度可以不指定,但第二维的长度不能省
    int a [] [4] = {1,2,3,4,5,6,7,8,9,10,11,12}

补充例题:

  • 例题2:二维数组遍历与找最大值
    #include <stdio.h>
    int main() {
        int matrix[][3] = {{9, 8, 1}, {4, 12, 7}, {2, 6, 11}}; // 3x3矩阵
        int max = matrix[0][0];
        int maxRow = 0, maxCol = 0;
    
        for(int i = 0; i < 3; i++) {
            for(int j = 0; j < 3; j++) {
                printf("%4d", matrix[i][j]);
                if(matrix[i][j] > max) {
                    max = matrix[i][j];
                    maxRow = i;
                    maxCol = j;
                }
            }
            printf("\n");
        }
        printf("最大值是:%d,位于[%d][%d]\n", max, maxRow, maxCol);
        return 0;
    }
    

字符数组

用来存放字符数据的数组是字符数组,字符数组中的一个元素存放一个字符

char c[5];
c[0] = 'C' ; c[1]='H' ; c[2]='I' ;c[3]='N';c[5]='A';

:上面例子中 c[5]='A';错误的,因为数组 c 的下标有效范围是 04c[5] 是越界访问。


字符数组的初始化

对字符数组初始化,最容易理解的方式是逐个字符赋给数组中各元素`

char c[10]={'I',' ','a','m',' ','h','a','p','p','y'}; 
//如果为 char c[5]; 如果只定义为赋值,那么里面的数为随机值

char c[15]={'I',' ','a','m',' ','h','a','p','p','y'}; // 未赋值的默认为'\0'

char c[]={'I',' ','a','m',' ','h','a','p','p','y'}; // 可以为空

char c[5]={'I',' ','a','m',' ','h','a','p','p','y'}; // 前面小,后面大,这是错的

字符串和字符串结束标志

C语言规定了字符串结束标志,以字符'\0' 作为标志
可以用字符串常量来使字符数组初始化

char c [6] = {"China"};  // 相当于 China后还有一个\0
char c [6] = "China";
char c [6] = {'C','h','i','n','a','\0'};
// 以上三者在计算机内部是相等的,都是一样的大小,有无花括号都可以

关键补充:

  • \0 (空字符):ASCII码为0,是字符串的终止符。strlenprintf(“%s”) 等函数都依赖它来判断字符串在哪里结束。
  • 数组大小必须足够:用字符串初始化字符数组时,数组长度必须至少字符串长度 + 1(为\0预留空间)。char s[5] = “Hello”; 是错误的,因为"Hello"需要6个字节(5个字符+1个\0)。

字符数组的输入和输出

%c 输出任一一个字符
%s 输出一串字符

如果输出包含多个\0 那么截止到第一个\0的就输出

gets puts 专门针对字符串

重要补充:

  • scanf(“%s”, str):用于输入字符串,遇到空格、制表符或换行符即停止。它不会检查数组边界,有缓冲区溢出的风险。
  • gets(str) (已废弃):读取整行,直到换行符,但不安全(不检查边界)。在C11标准中已被移除。
  • fgets(str, sizeof(str), stdin) (推荐):安全地读取一行,包括换行符,并自动在末尾添加\0。它能防止缓冲区溢出。
  • puts(str):输出字符串,并自动追加一个换行符。
  • 例题3:安全的字符串输入输出
    #include <stdio.h>
    #include <string.h>
    int main() {
        char name[20];
        printf("请输入您的姓名(可包含空格): ");
        fgets(name, sizeof(name), stdin);
    
        // 去掉 fgets 可能读取的换行符
        size_t len = strlen(name);
        if (len > 0 && name[len-1] == '\n') {
            name[len-1] = '\0';
        }
    
        printf("您好,");
        puts(name); // puts 会自动换行
        return 0;
    }
    

字符串处理函数 使用之前要加上 #include <string.h>

  1. stract函数---连接
    char s[20]="ABC";
    char t[20] = "DEF";
    strcat(s,t); //把 t 连接到 s 的后面
    printf("%s,%s\n",s,t);
    输出结果为 ABCDEF,DEF

  2. strcpy 函数----复制
    char s[20]="ABC";
    char t[20] = "DEF";
    strcpy(s,t); //把 t 复制给 s
    printf("%s,%s\n",s,t);
    输出结果
    DEF,DEF

  3. strcmp 函数----比较(ASCII码)
    strcmp("ABC","ABC"); // 0
    strcmp("ABC","ABB"); // 1
    strcmp("ABC","ACB"); // -1, B和 c 已经比较出来了,后面的就不需要比了

    如果字符串 1=字符串 2 函数值为 0
    如果字符串 1> 字符串 2,函数值为一正整数
    如果字符串 1<字符串 2,函数值为一负整数

  4. strlen 函数 ----- 长度
    char str [10] ={"China"};
    printf("%d",strlen(str));
    输出结果
    5
    strlen 是测试字符串长度的函数,函数的值为字符串中的实际长度(不包括'\n'在内)

  5. strlwr 函数-----小写
    一般形式为: strlwr(字符串)
    strlwr 函数的作用是将字符串中大写字母换成小写字母

  6. strupr 函数-----大写
    一般形式为: strupr(字符串)
    strlwr 函数的作用是将字符串中小写字母换成大写字母

补充重要知识点:

  • strcatstrcpy 的安全性:这两个函数不检查目标数组的剩余空间,容易导致缓冲区溢出。更安全的替代函数是 strncatstrncpy,它们允许指定最大拷贝/连接字符数。

    char dest[10] = "Hello";
    strncat(dest, " World!", sizeof(dest) - strlen(dest) - 1); // 安全连接
    
  • strcmp 的返回值:标准规定,相等返回0,大于返回正数,小于返回负数。不一定是1或-1,所以判断时应使用 if(strcmp(s1, s2) == 0),而非 if(strcmp(s1, s2))

  • strlwrstrupr:这两个函数不是C标准库函数,而是某些编译器(如MSVC)提供的扩展。在可移植的代码中,应避免使用,可以自己循环遍历字符并用 tolower()/toupper() 函数转换(需 #include <ctype.h>)。

  • 例题4:综合应用——用户登录验证(简化版)

    #include <stdio.h>
    #include <string.h>
    int main() {
        char username[20];
        char password[20];
        char correctUser[] = "admin";
        char correctPass[] = "123456";
    
        printf("请输入用户名: ");
        scanf("%19s", username); // 限制输入长度
        printf("请输入密码: ");
        scanf("%19s", password);
    
        // 比较用户名和密码
        if (strcmp(username, correctUser) == 0 && strcmp(password, correctPass) == 0) {
            printf("登录成功!\n");
        } else {
            printf("用户名或密码错误。\n");
        }
    
        // 演示字符串连接和长度
        char welcomeMsg[50] = "欢迎,";
        strcat(welcomeMsg, username);
        printf("%s\n", welcomeMsg);
        printf("欢迎信息的长度是:%zu\n", strlen(welcomeMsg));
    
        return 0;
    }
    

函数

函数的定义

定义无参函数的一般形式:
类型标识符 函数名 ()
{
声明部分
语句部分
}

定义有参函数的一般形式:
类型标识符 函数名 (形式参数表列)
{
声明部分
语句部分
}
定义有参函数 max

int max (int x , int y)
{
int z ;
  z = x > y ? x : y ;   //x>y 成立走 x,不成立走 y
  return (z); 
}

定义空函数的一般形式:
类型标识符 函数名 ()
{

}

函数的调用

函数的调用形式

  1. 语句调用
    例如: fun(a,b);

  2. 表达式调用
    例如: y=fun(x);

  3. 参数的调用
    将函数的返回值作为实参进行调用,成为函数的参数调用

int max (int x , int y)    // 形参,负责接受实参传过来的参数
{
int z ;
  z = x > y ? x : y ;   //x>y 成立走 x,不成立走 y
  return (z); 
}
// 从主函数开始读
main() 
{
int a=3,b=5,c;
c=max(a,b);   // 实参,负责往外传数据
printf("%d\n",c)
}

函数参数和函数的值

形式参数: 在定义函数时函数名后面括弧中的变量名,简称形参,作用是接受传来的数据

实际参数: 在主调函数中调用一个函数的时候,函数名后括弧中的参数(可以是一个表达式),简称实参

函数返回值: return 后面表达式的值

函数返回值类型: 定义函数时的类型

[!tip]+ 形参与实参的说明
1.形参需单独定义,属于临时变量,调用函数时为其分配存储单元,函数调用结束后释放其存储单元。
2.实参可以是常量、变量或表达式 (形参不可以是常量,因为形参需要接受数据,他是变化的)
3.实参的类型可以与形参类型不一致,以形参的类型为准
4.实参向形参的数据传递是“值传递”,单向传递,只由实参传给形参,而不能由形参传回来给实参。
5.实参与形参各占用独立的存储单元。

[!note] 关于返回值的说明

  1. 如果需要从被调用函数(max)带回一个函数值供主调函数(main) 使用,被调用函数中必须包含 return 语句。如果不需要从被调用函数带回函数值可以不要 return 语句。
  2. 一个函数中可以有一个以上的 return 语句,执行到哪一个 return 语句,哪一个语句起作用,return语句后面的括弧也可以不要。
  3. 对于不带回值的函数,应当用“void”定义函数为“无类型”(或称“空类型”)。这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值,此时在函数体中不得出现 return 语句。

函数声明

声明的作用: 是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用的时候,编译系统能正确识别函数并检查调用是否合法

如果被调用函数的定义出现在主调函数之前,可以不必加以声明

函数的嵌套调用

c 语言不能嵌套定义函数(嵌套定义就是在定义一个函数时,其函数体又包含另一个函数的完整定义),但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又调用另一个函数

函数的递归调用

在调用一个函数的过程中又出现了直接或间接地调用该函数本身,成为函数的递归调用

int f (int x) 
{
int y, z;
z = f(y);
return (2*z);
}

局部变量和全局变量

局部变量: 所有函数内部、复合语句内部定义的变量都是局部变量,仅仅在其所属范围有效

全局变量: 在所有函数外面定义的变量是全局变量,它的作用范围是从定义开始直至源文件结束

变量的存储类别

存储方式分为两大类: 静态存储类和动态存储类

具体包含四种: 自动的(auto), 静态的(static),寄存器的(register),外部的(extern)

静态局部变量
静态(static)局部变量的初值在编译时赋予,在整个程序运行期间存在,其作用域只限于定义它的函数,其他函数不能引用

指针

指针与变量

  • 指针的概念
    一个变量的地址称为该变量的指针
  • 指针的定义
    类型名 *指针变量名;
    int *p; // p 就是指针
    这里的* 号就是指针说明符
  • 指针的赋值
    1. 通过取地址运算符:
      int a, *p; p是个指针
      p= &a; // 把 a 的地址给了 p
      ![[取地址运算符图解]]

      int a ,*p=&a 这里也是说把 a 的地址给了 p 而不是*p,因为*只是说明符

    2. 通过另一个指针
      int a, *p ,*q;
      p=&a,q=p;

    3. 空指针的定义
      int *p =NULL; 这种写法是正确的,但是没有意义,不能用,一般多出现判断题

int a =3 , b, *P;
p = &a;
b =*p+2;   // 把 p 指向的内容拿出来,再+2 ,因为 a 把地址给了 p,所以这里是 a 的值
printf("%d,%d\n",a,b)


/*
如果 p =&a ,那么*p 就是 a 的值
*/
// 假设输入 3 5 
int a,b,*p,*q,t;
p = &a;q=&b;
scanf("%d%d",p,q); // 这里就相当于scanf("%d%d,&a,&b"); 所以 现在a=3,b=4 
t = *p ; *p = *q;*q =t;
printf("%d,%d\n",*p,*q)
// 交换内容,地址不变

// 5 3 



int a,b,*p,*q,t;
p = &a;q=&b;
scanf("%d%d",p,q);
t = p ; p = q;q = t;    // 这里没有* ,所以其实交换的是内存地址
printf("%d,%d\n",*p,*q)
// 5 3
// 交换地址,内容不变
 

指针与一维数组

指向数组元素的指针

int a[10];
int *p;
p = &a[0];  // p指向了 a[0]的位置

通过指针引用数组元素

  1. 下表法,如 a[i] 形式;
  2. 指针法,如*(a+i)*(p+1) .
    其中 a是数组名,p 是指向数字元素的指针变量,其初值 p=a
    首地址就是第 0 个地址,比如&a[0]
a <=> a[0]
a + 1 <=> a[1]
a + i <=> a[i]
*(a+i) <=> a[i] 

指针与二维数组

二维数组的地址
int a[3][4]
说明: a是二维数组名,即二维数组的首地址,第 0 行地址

*(a+1)+2 : 第一行第二列的地址
*(*(a+2)+3): 第二行第三列的地址
a[1]+2 : 第一行第二列的地址
*(a[2]+3): 第二行第三列的地址

二维数组的四种引用形式

a[i][j] <=> *(*(a+i)+j) <=>  *(a[i]+j) <=> (*(a+i))[j]
posted @ 2026-06-10 17:38  super派大星  阅读(30)  评论(0)    收藏  举报
© super派大星 · Powered by Cnblogs