1. 再谈头文件与编译
头文件类似于一个索引,当在一个文件中引入一个头文件时,这个文件就可以直接调用头文件中定义过的函数。
头文件编写需要注意:
- 组织性:通过头文件可以清晰组织其他文件,一个头文件应该专注处理特定部分的内容;
- 重用性:一个头文件中的函数功能,在其他项目或功能中也可以引入并使用;
- 编译效率:文件非常多时,应当注意把声明函数放在头文件中,把实现函数放在源文件中,提高增量编译效率;
- 避免重复包含:避免出现重定义函数。
“保持热爱,奔赴星海”
头文件类似于一个索引,当在一个文件中引入一个头文件时,这个文件就可以直接调用头文件中定义过的函数。
头文件编写需要注意:
目前大多数网站仍采用账号密码登录,注重安全性的平台会在登录时增加二次验证,从最基础的图形验证码,到交互式人机验证,再到发送至手机或邮箱的验证码、2FA等。但这些方式本质上仍未脱离账号密码的框架——只要登录依赖密码,就始终面临账号被盗或字典爆破的风险,安全性始终存在隐患。
那么,是否存在一种彻底告别密码、同时兼顾安全与便捷的登录方式?答案是肯定的。通行密钥(Passkeys)正是这样一种全新的认证方式,它与硬件设备绑定,完全无需密码即可完成登录。借助设备内置的安全功能(如Touch ID、Face ID等),通行密钥在安全性和易用性上均优于密码及现有双因素认证。
随着技术与设备的发展,如今绝大多数主流设备都已支持通行密钥,并达到了真正“好用”的程度。
Pocket ID + Tinyauth这套轻量化方案,可以将所有自托管的服务统一纳入无密码认证体系。借助Pocket ID提供的用户与用户组权限管理,还能灵活满足小型组织中按权限控制访问的需求。
之前学过的指针可以指向变量、数组、字符串、结构体、指针,函数指针就是指向函数的指针。
声明函数指针的方法:"返回类型" (*"指针变量名") ("参数类型");
例如有一个返回值为int,参数为两个int类型的函数为int function(int num1, int num2);,可以定义指向这个函数的函数指针为:int (*myFunctionPointer) (int, int);
将原函数的地址赋给函数指针后,直接使用函数指针myFunctionPointer即可调用原函数。
赋地址时,取地址符&可以省略。
1 | #include <stdio.h> |
函数指针的用途为实现回调函数,函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。简单讲:回调函数是由别人的函数执行时调用你实现的函数,它允许将一个函数作为参数传递给另一个函数,这样,当特定事件发生的时候,可以触发调用传递的参数。
以下是来自知乎作者常溪玲的解说:
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
这种机制在事件驱动编程中非常常用。
指针在指向变量地址的同时,它自身也存在一个内存地址中,多级指针就是指向指针的指针,保存的是指针所在的内存地址。
例如:
1 | int a = 10; // 定义一个整数变量 a |
指针和动态内存分配是C语言中最重要的两个部分。在C语言中,对内存的管理至关重要。
传统数组占用的内存在定义的那一刻已经被定死了。
如果没有动态内存分配,可能会出现为了满足最大需求而浪费内存的情况。例如,有极少部分用户需要输入很长的数据时,需要为了这部分用户扩大变量所占用的内存,而对于大多数用户来说,这样显然会浪费很多内存空间。
因此以往传统的数组定义形式灵活性低,应用受限。整个程序都依赖于预设编译前的大小,可能不够用,也可能会浪费,内存不够用时可能会有内存溢出,导致一些安全隐患。
这时候就需要动态内存分配,可以处理可变大小的数据,有高效的数据结构和资源优化。
本章主要对一些常用的库函数做介绍,包括数学库、时间库与错误处理库。
其中数学库可能会在科研计算中用到,包含各种常见曲线与函数。
时间库函数包含时间戳的使用,比较重要。
错误处理库在前面一些章节中已经有所涉及。
文件的读写,标准上称之为输入流(input stream)和输出流(output stream)。
输入流的数据会首先被暂存到一个叫缓冲区(buffer)的内存区域。
缓冲区的作用:
标准输入(stdin):指从键盘输入数据到操作系统中,存储这些数据在标准输入关联的缓冲区中,等待被程序读取。
标准输出(stdout):可以把数据发送到外部,例如输出数据到控制台、文件等。
正常情况下,在官网下载Visual Studio时,会下载一个Visual Studio Installer,在软件中可以选择想要下载的VS版本并管理组件和更新。
但Visual Studio Installer中一般只有当前的最新版本,例如目前只有VS 2026可以下载,此时如果有一些特殊需求,想要下载以前版本的Visual Studio,就比较麻烦了,官网很难找到对应的安装包。
本文提供了一种通过官方渠道一键下载旧版本VS的方法,希望可以帮到你。
C语言中字符串的处理是非常重要的。
可以使用char直接定义一个单个字符,使用单引号('')初始化,用%c可以将单个字符输出:
1 | char c = 't'; |
字符串本质上来说是一个由字符组成的数组,类型是char[],字符串可以储存由多个字符组成的字符序列,用大括号加双引号({""})或直接用双引号("")初始化,用%s可以将字符串输出,直接传入数组名即可:
1 | char str[] = "Hello"; // 实际的字符数组:{'H', 'e', 'l', 'l', 'o', '\0'} |
C语言中,字符串的结尾必定是\0,如果结尾丢失\0,会导致意料之外的问题。
1 | char str1[6] = "Hello"; // 正确,数组长度足以存储字符串和终止符 |
因此定义字符串时一般不会显式指定字符数组的长度,应该使用和数组类似的特性,直接让编译器自行计算长度,这样可以防止手动计算出错。
微软在处理字符串问题时,推荐使用带_s后缀的函数,例如对于字符串读取,微软推荐使用scanf_s而不是scanf,这种新的函数对字符串有特殊处理,可以避免出现字符串的未定义行为。
字符串也可以使用指针定义,与数组的指针类似,字符串的指针可以理解为指向字符数组中第一个字符的指针。
指针定义的字符串同样可以使用%s输出,但输出时与其他类型的指针不同,不需要有解引用符(*),直接将指针传入即可。
1 | char* ptr_str = "Hello!"; // 指向字符串常量的指针 |
需要注意的是,使用指针定义的字符串,其指针指向的是字符串常量,因此不可以修改单个字符,强制修改会导致未定义行为,且编译器不会显式报错,非常危险。
1 | ptr_str[0] = 'h'; // 未定义行为,字符串常量不可修改,可能会导致程序崩溃 |
想要修改指针定义的字符串时,只能直接修改指针,使其直接指向另一个字符串常量:
1 | ptr_str = "world!"; // 指针可以指向另一个字符串常量 |
一般情况下,字符串定义后不会再进行修改,因此字符串会定义为const常量字符数组类型,或者在定义指针字符串的时候显式写上const关键字。
1 | const char str[] = "Hello"; |
这样定义的话,修改字符串中的某个字符时,编译器会显式报错,更为安全。
如果不加
const常量类型,使用字符数组定义的字符串可以直接单独修改其中的字符。
1 str[0] = 'h';此外,如果定义字符指针指向定义好的指针数组,通过指针可以实现字符的修改,这个与其他类型的数组类似。
1
2
3 char str[] = "Hello";
char* ptr_str = str;
ptr_str[0] = 'h'; // 修改指针指向的内容同样也可以使用这个特性结合指针加法遍历字符串数组:
1
2
3
4
5
6
7 char str[] = "Hello";
char* ptr_str = str;
while (*ptr_str != '\0'){
printf("%c\n", *ptr_str);
ptr_str++;
}
因此,如果想要定义一个需要修改的字符串,应该使用字符数组来定义,要么隐式定义,要么定义的数组长度应满足最大长度要求。
当字符数组的长度大于有效值时,字符串的结尾以及其他无效字符均为\0。
如果想要定义一个不需要修改的字符串,可以使用const指针定义,也可以定义为const常量类型的字符数组,在修改时会有明显的报错,可读性更强。