目录
零.【C语言中的输入和输出函数 】
sscanf
应用场景 1:解析用户输入
应用场景 2:解析文件内容
应用场景 3:处理网络协议数据
应用场景 4:字符串解析和数据转换
应用场景 5:解析复杂的日志数据
其他应用场景:
scanf
一【编程语言相关】
c和cpp
二.【数据结构相关】
结构体循环定义
三.【数据访问与内存管理 】
引用
四.【函数传参方式】
直接传参(值传递)
引用传参
五.【函数相关】
带参函数和无参函数
Main函数
void * 指针
六.【数组和指针】
数组与指针的关系
编辑
数组越界
七.【数学相关】
阶乘
极小数 eps
零.【C语言中的输入和输出函数 】
输入函数 | 主要功能 | 输入来源 | 支持数据类型 | 安全性 | 适用场景 |
---|---|---|---|---|---|
scanf | 格式化输入,读取各种数据类型 | 标准输入 | 多种格式 | 可能导致缓冲区溢出 | 适合需要读取多种类型输入时使用,但需小心处理字符串输入 |
scanf_s |
scanf 的安全扩展 |
避免缓冲区溢出 | |||
gets | 读取整行字符串(已弃用) | 标准输入 | 字符串 | 不安全,容易溢出 | 已被弃用,建议使用 fgets 替代 |
fgets | 读取指定长度的字符串 | 标准输入 | 字符串 | 安全,支持指定长度 | 读取带空格和换行的字符串,确保不会溢出 |
getchar | 读取单个字符 | 标准输入 | 字符 | 安全 | 适合逐字符读取的场景 |
sscanf | 从字符串中解析数据 | 字符串 | 多种格式 | 取决于字符串内容 | 从已经获得的字符串中解析格式化数 |
输出函数 | 功能 | 适用场景 | 自动换行 | 是否格式化输出 | 输出目标 |
---|---|---|---|---|---|
printf | 输出格式化的字符串到标准输出(如控制台) | 通常用于向用户显示格式化的文本 | 否 | 标准输出(屏幕/控制台) | |
puts | 输出字符串到标准输出,并自动添加换行符 | 适用于简单的字符串输出(无需格式化数据) | 否 | 同上 | |
sprintf | 将格式化的字符串写入一个字符数组(缓冲区) | 将格式化数据写入字符串,适合构建复杂字符串,或保存后续使用 | 否 | 字符数组(缓冲区) |
sscanf
sscanf
是 C 语言中非常强大的函数,主要用于从一个字符串中解析格式化的数据。相比于scanf
从标准输入读取数据,sscanf
更适合在处理已经获得的字符串数据时进行进一步解析。以下是一些常见的应用场景及示例,帮助理解sscanf
的使用场景。
应用场景 1:解析用户输入
在用户输入中,有时需要对一行字符串(如名字、年龄、性别等)进行解析,sscanf
可以帮助提取出每个具体的值。
char input[] = "John 25";
char name[20];
int age;
sscanf(input, "%s %d", name, &age); // 从字符串中解析出名字和年龄
printf("Name: %s, Age: %d\n", name, age); // 输出: Name: John, Age: 25
应用场景 2:解析文件内容
读取文件内容后,我们可能会将文件的内容存入字符串,并使用 sscanf
来解析文件中的格式化数据。
假设文件包含如下数据:
Alice 22
Bob 30
char line[100];
char name[20];
int age;
// 假设 line 是从文件中读取的一行数据
strcpy(line, "Alice 22");
sscanf(line, "%s %d", name, &age); // 解析每一行的名字和年龄
printf("Name: %s, Age: %d\n", name, age); // 输出: Name: Alice, Age: 22
应用场景 3:处理网络协议数据
在网络编程中,经常需要处理从网络接收到的字符串数据,例如解析 HTTP 请求、处理自定义协议等。使用 sscanf
可以帮助从网络消息中提取出关键信息。
char request[] = "GET /index.html HTTP/1.1";
char method[10];
char path[50];
char version[10];
sscanf(request, "%s %s %s", method, path, version);
printf("Method: %s, Path: %s, Version: %s\n", method, path, version);
// 输出: Method: GET, Path: /index.html, Version: HTTP/1.1
应用场景 4:字符串解析和数据转换
sscanf
可以用来解析复杂的字符串表达式,将文本数据转换成数值类型或其他格式化的数据。
示例:
解析日期字符串:
char date[] = "2024-10-16";
int year, month, day;
sscanf(date, "%d-%d-%d", &year, &month, &day); // 解析日期字符串
printf("Year: %d, Month: %d, Day: %d\n", year, month, day); // 输出: Year: 2024, Month: 10, Day: 16
当需要将字符串数据转换成具体的数值(如日期、时间、坐标等)时,sscanf
是非常有用的工具。
应用场景 5:解析复杂的日志数据
系统日志、应用日志等通常以结构化字符串的形式输出。通过 sscanf
可以将日志中的特定字段提取出来。
char log[] = "[ERROR] File not found: file.txt (code: 404)";
char level[10], message[50], filename[20];
int code;
sscanf(log, "[%s] %[^:]: %s (code: %d)", level, message, filename, &code);
printf("Level: %s, Message: %s, File: %s, Code: %d\n", level, message, filename, code);
// 输出: Level: ERROR, Message: File not found, File: file.txt, Code: 404
用于解析日志中的错误级别、文件名、错误码等详细信息,方便程序自动化处理或分析日志内容。
其他应用场景:
处理输入的数学表达式
处理命令行参数等
#include <stdio.h>
int main() {
int n; // 用来存储整数
double db; // 用来存储浮点数
char str[100] = "2048:3.14,你好", str2[100]; // 原始字符串和存储解析后的字符串
// 使用 sscanf 从 str 字符串中解析出整数 n、浮点数 db 和字符串 str2
sscanf(str, "%d:%lf,%s", &n, &db, str2);
// 输出解析的结果
printf("n = %d, db = %.2f, str2 = %s\n", n, db, str2);
return 0;
}
运行结果:
n = 2048, db = 3.14, str2 = 你好
char str[100] = "2048:3.14,你好", str2[100]; 可以分开来写,str是自定义变量名也可以改成其他自定义变量名
char stry2[100] = "2048:3.14,hello";
char output[100]; // 你可以使用任意自定义变量名
sscanf(styr2, "%d:%lf,%s", &n, &db, output);
scanf
scanf
函数用于从标准输入读取数据,并根据格式化字符串将输入的数据存储到指定的变量中。它需要知道输入的数据应该存储到哪个内存地址,因此要求传入变量的地址,而不是变量本身的值。
scanf("%d", &T)
:这里的%d
表示要读取一个整数,而&T
传递的是变量T
的内存地址,这样scanf
才能将读取到的整数存储到T
的位置中。如果你写成
scanf("%d", T)
(没有&
),实际上传递的是T
的值,而不是它的地址。这意味着scanf
并不知道将读取的值存储在哪里,程序会因为访问了无效的内存位置而崩溃或产生未定义行为。
一【编程语言相关】
c和cpp
-
C++ 是 C 的超集:C++ 兼容 C 语言,大多数 C 语言代码可以直接在 C++ 编译器中编译。把 C 语言的文件(
.c
文件)直接放到 C++ 项目中,它通常能通过编译。
(superset)是集合论中的一个概念,指的是一个集合包含了另一个集合的所有元素。用简单的话来说,如果集合 A 包含了集合 B 的所有元素,那么 A 就是 B 的超集。
-
头文件:在 C++ 中,使用 C 语言标准库时,你可以直接包含 C 语言的头文件,例如:
#include <stdio.h> #include <string.h> //C++ 编译器会自动处理这些头文件,并且 C 代码通常能正常运行。
1.C 标准库中的输入输出
#include <cstdio>
:这个头文件是 C++ 中用于包含 C 的标准输入输出库,它提供了printf
、scanf
、puts
、gets
等函数。如果你使用这些函数进行输入输出,必须包含这个头文件。2. C++ 标准库中的输入输出
#include <iostream>
:这个头文件是 C++ 中用于输入输出的标准库,提供了cin
、cout
、cerr
、clog
等对象,通常用于代替printf
和scanf
。
-
cpp使用注意(标红)
C++ 向下兼容 C
- C++ 是 C 的超集:C++ 是基于 C 的语言,增加了面向对象的特性、模板、异常处理等。大部分有效的 C 代码在 C++ 中也是有效的。
- 基本语法:C++ 继承了 C 的基本语法和数据结构,C 中的结构体、指针、数组、函数等在 C++ 中可以正常使用。
- C 函数库:C++ 可以直接使用 C 的标准库,包括
<cstdio>
、<cstdlib>
、<cstring>
等。需要注意的是,使用 C 标准库的 C 风格函数时,需要包含相应的头文件。C 不完全兼容 C++
- 类型检查:C 是一种相对宽松的语言,对于类型的检查不如 C++ 严格。例如,C 中的隐式类型转换在 C++ 中可能会引发编译错误。
- C++ 特性:C++ 引入了许多新特性,如类、重载、模板、命名空间等,而这些特性在 C 中是没有的。因此,使用 C++ 特性编写的代码无法在 C 编译器中编译。
- 名称修饰:C++ 对函数的名称进行了修饰(name mangling),以支持函数重载。在 C 中没有这个概念,所以 C++ 的一些功能无法直接在 C 中使用。
二.【数据结构相关】
结构体循环定义
结构体的循环定义在 C 语言中通常是禁止的,因为这会导致编译器无法确定结构体的大小,形成一种“无限循环”。
以下模拟“循环定义”的情境,定义一个“动物”结构体,让它包含另一个“动物”的引用,来展示动物之间的人际关系。虽然在实际代码中不会出现循环定义,但可以举个例子。
#include <stdio.h>
typedef struct Animal {
char name[20];
struct Animal* friend; // 让一个动物有一个朋友,友谊关系
} Animal;
void introduce(Animal* animal) {
printf("这是一只叫 %s 的动物,它的朋友是 %s。\n", animal->name, animal->friend->name);
}
int main() {
// 创建两个动物
Animal dog = {"狗"};
Animal cat = {"猫"};
// 设定朋友关系
dog.friend = &cat; // 狗的朋友是猫
cat.friend = &dog; // 猫的朋友是狗
// 进行介绍
introduce(&dog);
introduce(&cat);
return 0;
}
三.【数据访问与内存管理 】
引用
引用通常用于指向其他变量的内存地址,可以帮助我们实现更复杂的数据结构,如链表、树和图等。
引用使得对象或数据结构的传递更为高效,因为它们允许通过引用而不是复制整个数据结构(如值传递)来访问和修改数据。
示例:使用引用交换两个整数的值
#include <iostream>
using namespace std;
// 函数使用引用来交换两个整数的值
void swap(int& a, int& b) {
int temp = a; // 将 a 的值保存在临时变量中
a = b; // 将 b 的值赋给 a
b = temp; // 将临时变量的值赋给 b
}
int main() {
int x = 10;
int y = 20;
cout << "Before swap: x = " << x << ", y = " << y << endl;
// 调用 swap 函数,传入 x 和 y 的引用
swap(x, y);
cout << "After swap: x = " << x << ", y = " << y << endl;
return 0;
}
四.【函数传参方式】
引用和直接传参(值传递)在某些方面是相似的,但它们在行为和使用场景上有重要区别。下面我将详细说明这两者的区别,并给出一些例子。
直接传参(值传递)
- 定义:在函数调用时,实参的值会被复制到形参中。函数内部对形参的修改不会影响实参。
-
特点:
- 安全:由于是值的复制,实参不会被修改。
-
性能开销大:对于基本数据类型(如
int
、char
)的传递影响不大,但对于大型对象(如大数组或结构体),复制会导致性能开销。 -
简洁:对于简单数据类型(如基本类型
int
、char
),使用值传递更简洁,因为不需要担心引用的生命周期和管理。
#include <iostream>
using namespace std;
void modify(int num) {
num++; // 仅修改形参,不影响实参
}
int main() {
int a = 5;
modify(a); // a 的值被复制到 num
cout << "Value of a after modify: " << a << endl; // 输出 5
return 0;
}
输出
Value of a after modify: 5
引用传参
- 定义:在函数调用时,实参的地址(引用)会被传递到形参中。函数内部对形参的修改会直接影响实参。
-
特点:
- 直接修改:可以直接通过形参修改实参的值。
- 性能优化:对于大型对象,避免了不必要的复制,提高了性能。
#include <iostream>
using namespace std;
void modify(int& num) { // 使用引用传参
num++; // 直接修改原始变量
}
int main() {
int a = 5;
modify(a); // a 的地址传递给 num
cout << "Value of a after modify: " << a << endl; // 输出 6
return 0;
}
输出
Value of a after modify: 6
五.【函数相关】
带参函数和无参函数
int judge(int x)
和 `int judge() { int x; } 有实质性区别,主要体现在函数的参数传递和变量作用范围上。
1. int judge(int x)
- 这是一个带参数的函数声明。
-
x
是函数的参数,它是通过调用时传入的值。 - 函数签名中明确说明了该函数需要传递一个整数值作为参数。
-
调用时,你需要提供一个参数,例如:
judge(5)
,传递5
作为参数。 - 这个
x
只在函数judge
的作用域内有效,但它的值是在函数调用时由外部传递进来的。
int judge(int x) {
if (x > 0) return 1;
else if (x == 0) return 0;
else return -1;
}
调用示例
int result = judge(5); // result 将为 1,因为 5 > 0
2. int judge() { int x; }
- 这是一个不带参数的函数声明。
- 在函数体内部声明了一个局部变量
x
。 - 这个
x
是一个未初始化的局部变量,它的值是不确定的,未经初始化的局部变量通常会导致未定义行为。 -
调用时,无需传递任何参数,例如:
judge()
。 - 在这个版本中,
x
完全由函数内部控制,不受外部传入数据的影响。
int judge() {
int x; // 局部变量未初始化
if (x > 0) return 1;
else if (x == 0) return 0;
else return -1;
}
调用示例
int result = judge(); // 结果可能不确定,因为 x 没有被初始化
总结
-
int judge(int x)
是一个带参数的函数,行为依赖于调用时传入的参数值。 -
int judge() { int x; }
是一个无参数的函数,x
是在函数内部定义的局部变量,但如果没有初始化,这样的变量将导致不可预知的行为。
Main函数
在 C 和 C++ 语言中,main
函数的返回类型是 int
。这表示 main
函数应返回一个整数值,以指示程序的退出状态。
#include <stdio.h>
int main() {
// 程序代码
return 0; // 返回值
}
返回值的意义
-
return 0;
:通常,返回值0
表示程序成功执行并正常退出。 -
返回非零值:一般情况下,返回值非零表示程序出现错误或异常。例如,
return 1;
通常表示发生了某种错误。
void * 指针
void *memset(void *s, int c, size_t n);
void *
:泛型指针,可以指向任何类型的数据,常用于内存操作。不需要提前知道具体的数据类型。这使得 memset
函数能够处理不同类型的内存区域,比如字符数组、整数数组、结构体等。
六.【数组和指针】
-
数组与指针的关系
数组在函数传递时会退化为指针。
#include <stdio.h>
int main() {
int a[5] = {10, 20, 30, 40, 50}; // 定义并初始化数组 a
int* p = a; // p 指向数组的首地址,相当于 p = &a[0]
printf("数组 a 的首地址:%p\n", (void*)a); // 输出数组 a 的首地址
printf("数组第一个元素的地址:%p\n", (void*)&a[0]); // 输出数组第一个元素的地址
printf("指针 p 的地址:%p\n", (void*)p); // 输出通过指针 p 获取的地址
printf("第一个元素的值:%d\n", *p); // 输出指针 p 指向的第一个元素的值
// 使用指针遍历数组
for (int i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, *(p + i)); // 输出数组中的元素
}
return 0;
}
【输出结果】
数组 a 的首地址:0x7ffee48056d0
数组第一个元素的地址:0x7ffee48056d0
指针 p 的地址:0x7ffee48056d0
第一个元素的值:10
a[0] = 10
a[1] = 20
a[2] = 30
a[3] = 40
a[4] = 50
-
数组名
a
和&a[0]
表示相同的地址,即数组的首地址(第一个元素的地址)。 -
指针
p
也可以指向数组的首地址,但p
是可变的,可以通过p++
或p + i
指向数组中的其他元素。
总结:
- 数组名
a
在 C 语言中 等价于指向首元素的指针,即它表示的是数组的首地址。 - 数组名是一个 常量指针,你不能修改它指向的位置,但你可以通过指针变量(如
p
)来操作数组的元素。
因此,a
表示数组的首地址是因为在 C 语言中,数组名在大多数情况下会隐式地转换为指向数组首元素的指针
数组越界
当访问数组元素时,索引超出了数组的边界范围,会导致未定义行为。
const int maxn = 100010;
使用 100010
而不是 100000
通常是出于防止边界问题的考虑。
边界情况的处理:
-
在算法中,尤其是在处理数组或者其他与下标相关的情况时,开发者往往会给常量值稍微多留一些空间。例如,如果程序中的逻辑可能会访问到
100000
的下标,为了防止数组越界的错误,开发者可能会定义maxn = 100010
,这样就有了一些额外的缓冲空间,以防在极限情况下出现问题。
假设你定义了一个长度为 100000
的数组,并且有一些算法可能会访问数组的边界,比如从 0
到 100000
的位置,甚至进行一些复杂的操作(如排序或查找)。在这种情况下,如果没有额外的缓冲区,就可能出现数组越界的错误,导致程序崩溃或产生未定义的行为。为了避免这个问题,开发者往往会将数组大小稍微增大一些,比如用 100010
来提供额外的保护空间。
七.【数学相关】
阶乘
0
的阶乘定义为1
,即:0!=10! = 1 0!=1
理由
这个定义源于组合数学和数学分析中的几种逻辑理由:
-
定义一致性:阶乘的定义是一个数的阶乘等于该数与它之前所有整数的乘积。对于
0!
,没有任何整数与它相乘,所以定义为1
是为了保持一致性。 -
组合数的计算:在组合数学中,选择
(nk)=n!k!(n−k)!\binom{n}{k} = \frac{n!}{k!(n-k)!}(kn)=k!(n−k)!n!0
个对象的方法只有一种,即不选择任何对象。这也可以表示为 (n0)=1\binom{n}{0} = 1(0n)=1,而组合数的公式为:当 k=0k = 0k=0 时,公式会变成 n!0!⋅n!=1\frac{n!}{0! \cdot n!} = 10!⋅n!n!=1,因此必须有 0!=10! = 10!=1。
极小数 eps
极小数
eps
的全称是 epsilon,来源于希腊字母ε
(epsilon),在计算机科学和数值分析中通常用来表示机器精度(machine epsilon)或最小可表示差值。