算法笔记 C/C++快速入门 | 全章节整理

目录

零.【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 的标准输入输出库,它提供了 printfscanfputsgets 等函数。如果你使用这些函数进行输入输出,必须包含这个头文件。

2. C++ 标准库中的输入输出

  • #include <iostream>:这个头文件是 C++ 中用于输入输出的标准库,提供了 cincoutcerrclog 等对象,通常用于代替 printfscanf
  • cpp使用注意(标红)

C++ 向下兼容 C

  1. C++ 是 C 的超集:C++ 是基于 C 的语言,增加了面向对象的特性、模板、异常处理等。大部分有效的 C 代码在 C++ 中也是有效的。
  2. 基本语法:C++ 继承了 C 的基本语法和数据结构,C 中的结构体、指针、数组、函数等在 C++ 中可以正常使用。
  3. C 函数库:C++ 可以直接使用 C 的标准库,包括 <cstdio><cstdlib><cstring>。需要注意的是,使用 C 标准库的 C 风格函数时,需要包含相应的头文件。

C 不完全兼容 C++

  1. 类型检查:C 是一种相对宽松的语言,对于类型的检查不如 C++ 严格。例如,C 中的隐式类型转换在 C++ 中可能会引发编译错误。
  2. C++ 特性:C++ 引入了许多新特性,如类、重载、模板、命名空间等,而这些特性在 C 中是没有的。因此,使用 C++ 特性编写的代码无法在 C 编译器中编译。
  3. 名称修饰: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;
}

四.【函数传参方式】

值传递和引用传递

引用和直接传参(值传递)在某些方面是相似的,但它们在行为和使用场景上有重要区别。下面我将详细说明这两者的区别,并给出一些例子。

直接传参(值传递)

  • 定义:在函数调用时,实参的值会被复制到形参中。函数内部对形参的修改不会影响实参。
  • 特点
    • 安全:由于是值的复制,实参不会被修改。
    • 性能开销大:对于基本数据类型(如 intchar)的传递影响不大,但对于大型对象(如大数组或结构体),复制会导致性能开销。
    • 简洁:对于简单数据类型(如基本类型 intchar),使用值传递更简洁,因为不需要担心引用的生命周期和管理
#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
  1. 数组名 a&a[0] 表示相同的地址,即数组的首地址(第一个元素的地址)。
  2. 指针 p 也可以指向数组的首地址,但 p 是可变的,可以通过 p++p + i 指向数组中的其他元素。

总结:

  • 数组名 a 在 C 语言中 等价于指向首元素的指针,即它表示的是数组的首地址。
  • 数组名是一个 常量指针,你不能修改它指向的位置,但你可以通过指针变量(如 p)来操作数组的元素。

因此,a 表示数组的首地址是因为在 C 语言中,数组名在大多数情况下会隐式地转换为指向数组首元素的指针

数组越界

当访问数组元素时,索引超出了数组的边界范围,会导致未定义行为。

const int maxn = 100010;

使用 100010 而不是 100000 通常是出于防止边界问题的考虑。

边界情况的处理:

  • 在算法中,尤其是在处理数组或者其他与下标相关的情况时,开发者往往会给常量值稍微多留一些空间。例如,如果程序中的逻辑可能会访问到 100000 的下标,为了防止数组越界的错误,开发者可能会定义 maxn = 100010,这样就有了一些额外的缓冲空间,以防在极限情况下出现问题。

假设你定义了一个长度为 100000 的数组,并且有一些算法可能会访问数组的边界,比如从 0100000 的位置,甚至进行一些复杂的操作(如排序或查找)。在这种情况下,如果没有额外的缓冲区,就可能出现数组越界的错误,导致程序崩溃或产生未定义的行为。为了避免这个问题,开发者往往会将数组大小稍微增大一些,比如用 100010 来提供额外的保护空间。


七.【数学相关】

阶乘

0 的阶乘定义为 1,即:

0!=10! = 1 0!=1

理由

这个定义源于组合数学和数学分析中的几种逻辑理由:

  1. 定义一致性:阶乘的定义是一个数的阶乘等于该数与它之前所有整数的乘积。对于 0!,没有任何整数与它相乘,所以定义为 1 是为了保持一致性。

  2. 组合数的计算在组合数学中,选择 0 个对象的方法只有一种,即不选择任何对象。这也可以表示为 (n0)=1\binom{n}{0} = 1(0n​)=1,而组合数的公式为:

    (nk)=n!k!(n−k)!\binom{n}{k} = \frac{n!}{k!(n-k)!}(kn​)=k!(n−k)!n!​

    当 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)或最小可表示差值

上一篇:深度学习--CNN实现猫狗识别二分类(附带下载链接, 长期有效)


下一篇:基于SSM电子资源管理系统的设计