Cpp 小技巧/冷知识记录
记录一下C++的小坑/冷知识。
int8_t 输入
虽然C++提供了很多数据类型,但是最基础的其实还是有无符号的字符和整数浮点数等,其他的数据类型是对它们的简单包装,因此还是表现原本的行为,例如下面两种类型在msvc可能的定义为
12typedef signed char int8_ttypedef unsigned char uint8_t
这表明int8_t和uint8_t实际上还是char类型的重命名,这会影响很多地方的处理,例如cin在接收字符流输入时,会根据接收变量的数据类型进行转换:如果输入1,可能会被解释为ASCII字符1(值为49),也可能会被解释为整数1,这完全取决于接收变量的类型
1234567char a; cin >> a; // '1' = 49int b; cin >> b; // 1int8_t c; cin >> c; // '1' = 49uint8_t d; cin >> d; // '1' = 49
这种情况下,由于int8_t和uint8_ ...
Cpp 成员函数中的 this
整理一下关于C++中特殊的this指针的知识,并且学习C++23中的新内容:显式推导this。
隐式this
基础
this指针是C++面向对象编程中的重要机制,在自定义类型的非静态成员函数中,都存在这一个自动传递的this指针指向当前对象自身,例如
12345678910111213#include <iostream>struct Test { int data = 0; void call() { std::cout << "call: " << data << "\n"; }};int main() { Test test{1}; test.call(); return 0;}
对于编译器来说,这里的定义和调用过程等效于下面的形式(因为this是关键词,在代码中使用this_来代表)
1234567891011121314#include <iostream>struct ...
Cpp 未定义行为
概述
C/C++存在很多的未定义行为,如果程序中使用了未定义行为,那么得到的结果是不可知的,编译器给出任何反馈都是符合语法标准的,因为未定义行为导致的BUG是难以察觉的。
未定义行为可能会导致编译报错,也可能导致运行出错等,还可能无事发生,具体结果可能与平台/编译器有关,不过在大部分情况下不同编译器会得到类似的结果。
未定义行为的存在是有客观原因的,一方面语法标准无法穷尽所有的可能情况;另一方面有些可能非法的行为(例如下标越界)在编译期难以直接检测,如果在运行期进行检测(例如检查下标越界),又会牺牲很多的运行效率。
如果我们在不经意间使用了未定义行为,那么即使是相同的代码,在C++的编译器不同等级的优化措施下,也可能得到完全不同的结果,例如:
Debug模式正常,Release模式异常
例如产生随机的结果,通常的原因是使用没有正确初始化的变量,在Debug模式下被编译器初始化为0,但是Release模式下直接使用了内存中的随机值;
其它未定义行为,在Release模式下经过编译器优化中,产生不合理的结果。
Debug模式异常,Release模式正常
代码中的未定义行为在Debu ...
编程语言中的整除和取余
整除和取余是两个看起来非常简单明确的基本数学运算,但是在不同编程语言的实现中,其实存在着很多的差异,需要注意一下。
数论中的整除和取余
我们从这两个概念的数学定义出发,在数论中的整除和取余定义为:对于整数
\(a,b \in \mathbb{Z}\),其中 \(b \neq 0\),存在唯一的商 \(q\in \mathbb{Z}\) 和余数 \(r \in \mathbb{Z}\),使得 \[
a = b \,q + r
\] 对于余数要求 \(0 \le r <
b\),即余数是一个非负的且不超过除数的整数。
虽然初等数论主要关注的都是整数,通常不会涉及对实数的整除和取余运算,但是我们仍然可以直接将上述定义推广到实数中:对于实数
\(a,b \in \mathbb{R}\),其中 \(b \neq 0\),存在唯一的商 \(q\in \mathbb{Z}\) 和余数 \(r \in \mathbb{R}\),使得 \[
a = b \,q + r
\] 对于余数要求 \(0 \le r <
b\),即余数是一个非负的且不超过除数的实数。
数论中要求在任何情况下,商是一 ...
编程语言中的整数和浮点数
整理一下编程语言中的整数和浮点数的相关内容,针对的情景是科学计算。
整数
整数模型
编程语言中的整数类型不同于数学意义上的整数,而只是它的一个有限子集,因为计算机为了计算效率,会使用固定的字节数来存储一个整数数据,例如
\(n\) 个字节,这意味这只有 \(2^{8n}\) 个不同状态,只能表示 $2^{8n} $
个整数。
将 \(n\) 个字节所对应的 \(8n\) 比特的值依次记作 \(a_i \in \{0,1\}\),这里 \(i=0,\dots,8n-1\),那么通常有两类方案:
第一种方案是无符号整数,表示的值 \(V\) 为 \[
V = 2^0 a_0 + 2^1 a_1 + \dots + 2^{8n-2} a_{8n-2} + 2^{8n-1} a_{8n-1}
\] 表示的范围为 \[
[0,2^{8n}-1]
\]
第二种方案是有符号整数,表示的值 \(V\) 为 \[
V = 2^0 a_0 + 2^1 a_1 + \dots + 2^{8n-2} a_{8n-2} - 2^{8n-1} a_{8n-1}
\] 表示的范围为 \[
[-2^{8n-1},2^ ...
时间离散的潜在问题
概述
在数值求解ODE和PDE时,我们通常需要进行时空离散:
空间离散看起来就很麻烦:处理方式与具体的求解格式相关,但是都会涉及到网格剖分,网格加密,单元之间的数据交换,边界处理等;
时间离散看起来就很简单:只需要加上一层for或者while循环,最多加上时间步长的自适应调整即可。
但是就算是看起来非常简单的时间离散,在具体编程中也可能存在一些潜在的问题,下面先回顾一下几种最简单的时间离散情况,然后给出一个由格式和浮点数模型共同造成的最后时间步问题。
固定时间步数
固定时间步数是最简单的情况,在固定时间步数为 \(N\) 时,我们可以直接生成 \(N\) 次循环即可,例如 123456789const int N = 20;const double Tend = 1.0;double tnow = 0;double dt = Tend / N;for (int i = 0; i < N; ++i) { update(&data, tnow, dt); // from tnow to tnow+dt tnow = tnow + dt;} ...
Zotero 7 配置笔记
磨刀不误砍柴工,文献管理软件这把刀确实是值得打磨的,选择Zotero而非其它文献管理工具,主要是看重了它的免费和可配置性,适合重度使用。
最新版 Zotero 7 在 Zotero 6
的基础上,进行了升级,包括非常多的优化,例如最基础的UI看着更漂亮了,等我把它的各种插件和配置都鼓捣好了,就更有动力(但愿吧)打开来看文献了。
网上关于Zotero有很多分享教程,但是绝大部分都是入门级的配置笔记,参考价值并不大,本文最主要的参考是Zotero非官方中文社区。
基础
本地安装
直接从官网下载 Zotero
7,然后傻瓜式安装即可,需要注意的是两个位置:
首先是软件安装位置,可以使用自定义安装方式,挑一个合适的位置即可,例如D:\ProgramMain\Zotero7;
然后是本地数据存储位置,默认是~/Zotero。
数据存储位置比较占地方,我想将其迁移到别的位置,例如E:\<user>\Documents\Zotero7,首先需要在设置中更改
1编辑 -> 设置 -> 高级 -> 数据存储位置
将其更改为自定义的目标路径,然后根据提示进行操作:将软件关 ...
Cpp lambda表达式笔记
在另一篇关于可调用对象的笔记中已经对lambda表达式的语法本质和应用情景进行了整理,
这篇笔记主要是整理lambda表达式的语法细节,假定读者对lambda表达式已经有了基本的概念。
虽然早在C++11中就提出了lambda表达式,但是相关的语法细节始终在不断地发展和完善(C++实在是太复杂了!),
本文以C++20已经支持的语法为主,对于最新的C++23增加的语法不作讨论,例如Deducing
This等内容。
基础
基本捕获
首先介绍两种隐式捕获符:
[=]:全部按值捕获
[&]:全部按引用捕获
它们会自动地捕获在lambda表达式中所有实际被使用的局部变量,无需我们逐个列出被使用变量对应的名称。
这里存在一个问题:如果当前处于一个普通成员函数中,如何处理特殊的this/*this所代表的当前对象?见下文中的讨论。
在默认捕获符的基础上,我们可以进行一些微调,例如:
[=, &a, &b]:表示除了后面明确提到的这些变量按引用捕获,其它情况下默认按值捕获
[&, a, b]:表示除了后面明确提到的这些变量按值捕获,其它情况下默认按引用捕获 ...
Cpp 可调用对象笔记
概述
在C++中,可调对象(Callable
Objects)是指可以像函数一样被调用的对象,通常包括:
函数指针
仿函数
std::function
lambda 表达式
它们大部分都是基于面向对象实现的,但是函数指针是个例外,因此给对象两个字加上引号其实更合适。
可调用对象是函数的扩展概念,引入它们的主要目的就是补上函数天生的短板:
函数在语法上不可以像变量一样作为参数被传递给其它函数;可调用对象可以。
函数在语法上通常不具有内部状态,或者即使有,也只是通过局部静态变量实现的唯一内部状态;每一个可调用对象都可以拥有独立的内部状态。
这使得可调用对象在泛型编程、回调函数和函数式编程中发挥重要的作用。
函数的这两个短板是针对C/C++这种系统级编程语言来说的,但是对于某些高级语言来说,这些完全不是问题,
例如对于JavaScript、Python和Lua来说,函数在语法上就是一个可调用的变量,可以像普通变量一样直接作为参数传递给其他函数,并且允许拥有内部状态,称之为闭包可能更合适。
因为这些高级语言的执行由其解释器或虚拟机负责,而C/C++需要直接执行。
函数指针(C)
函 ...
高精度时间戳的获取
获取毫秒级的高精度时间戳是一个很常见的需求,尤其在向日志文件中输出信息时通常需要附带格式化的时间戳,
下面在不同的语言中尝试生成形如[2024-07-30 00:52:47.379]的高精度时间戳。
C++
对于C++,标准库chrono可以获取高精度的时间,
然后通过localtime函数进行格式化,由于它不支持毫秒部分的格式化,我们还需要对毫秒进行额外处理。
下面是一个生成时间戳的示例函数 12345678910111213141516static std::string time_stamp() { // unsafe auto now = std::chrono::system_clock::now(); auto now_time_t = std::chrono::system_clock::to_time_t(now); auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epo ...