面试常见问题 1

部分内容为GPT的回答。 和红黑树的功能类似的还有哪些数据结构 红黑树是一种自平衡的二叉搜索树,它通过特定的属性和操作保持树的平衡,从而实现在最坏情况下仍能保持对数级别的查找、插入和删除时间复杂度。和红黑树提供相似功能的数据结构主要包括其他类型的自平衡二叉搜索树和一些特殊的数据结构,它们都能提供高效的查找、插入和删除操作。下面列出一些常见的例子: AVL树 AVL树是最早被发明的自平衡二叉搜索树之一。它严格保持二叉树的平衡,即任何节点的两个子树的高度最大差为1。这种严格的平衡要求使得AVL树在查找操作中表现得非常好,但在频繁的插入和删除操作中可能会有较多的旋转调整,相比于红黑树可能略显低效。 B树及其变种 B树是一种平衡的多路搜索树,主要用于数据库和文件系统。B树通过将节点中存储多个元素并允许节点有多个子节点来减少树的高度,从而优化磁盘读写操作。B+树和B*树是B树的变种,它们在B树的基础上做了进一步的优化,广泛应用于数据库索引。 Splay树 Splay树是一种自调整的二叉搜索树,它通过一种称为“伸展”的操作来保证最近进行的操作可以在较快的时间内完成。伸展树不保证树的严格平衡,但它能保证分摊的时间复杂度为O(log n)。 Treap Treap是一种结合了二叉搜索树和堆的数据结构,节点既按键排序又按优先级堆组织。它利用旋转操作来维持二叉搜索树的性质和堆的性质,实现了平衡树的效果。 Skip List 跳跃表是一种概率性的数据结构,通过在多层链表上添加“快速通道”来实现快速查找。虽然不是树结构,跳跃表在功能上与红黑树相似,提供了高效的查找、插入和删除操作,其平均时间和空间复杂度都是O(log n)。跳跃表的实现相比于红黑树和AVL树更为简单,易于理解和实现。 AA树 AA树是一种变种的红黑树,它简化了红黑树的平衡规则和旋转操作。通过限制只有右链接可以是红色,AA树使得实现变得更加简单,同时保持了与红黑树相似的操作性能。 这些数据结构各有特点,适用于不同的场景。选择合适的数据结构需要根据实际的应用场景和性能要求来决定。 五千万个整数求和,尽量降低时间消耗。怎么做? 对于五千万个整数求和的问题,主要目标是优化时间复杂度和减少执行时间。以下是一些优化策略,可以帮助提高求和操作的效率: 1. 使用有效的数据结构 确保整数存储在连续的内存空间中,比如使用数组或列表,这样可以减少内存访问的开销。 2. 循环优化 尽可能使用高效的循环结构,减少循环内不必要的计算和条件判断。 3. 多线程或并行计算 利用现代计算机多核心的优势,可以将五千万个整数分成几个部分,每个核心处理一部分数据,然后将结果汇总。这种方式可以大大减少总的计算时间。 使用如OpenMP、Intel TBB(Thread Building Blocks)、Python的concurrent.futures模块或Java的java.util.concurrent包等并行框架和库来简化多线程编程的复杂性。 4. 向量化运算 在支持向量化的编程语言中(如使用NumPy库的Python),可以利用向量化运算来提高性能,因为这些运算是在底层进行优化的。 5. 减少函数调用 在求和过程中尽量减少函数调用,比如避免在循环中调用函数,因为函数调用会增加额外的时间开销。 实践示例(Python 并行计算) 这里是一个使用Python的concurrent.futures模块进行并行计算求和的简单例子: 1import concurrent.futures 2import numpy as np 3 4def parallel_sum(arr, n_workers=4): 5 # 分割数组为多个子数组 6 chunk_size = len(arr) // n_workers 7 futures = [] 8 with concurrent.futures.ThreadPoolExecutor(max_workers=n_workers) as executor: 9 for i in range(n_workers):10 chunk = arr[i*chunk_size : (i+1)*chunk_size]11 futures.append(executor.submit(np.sum, chunk))12 13 # 收集结果14 result = sum(f.result() for f in futures)15 return result1617# 生成五千万个整数的数组18arr = np.random.randint(1, 100, 50000000)19result = parallel_sum(arr)20print(result) 这个例子展示了如何使用Python的并发特性和NumPy库来提高求和操作的效率。通过将数据分割成多个块并并行计算每个块的和,然后再将这些和加起来,可以显著减少计算所需的时间。注意,实际效果会根据你的硬件配置、Python版本和其他因素而有所不同。 static关键字在C++中的作用 在C++中,static关键字有几个不同的用途,根据其应用的上下文,它可以用于控制变量的存储期(生命周期)、可见性(链接性)、以及在类定义中控制成员的属性。以下是static在不同情况下的用法概述: 1. 控制局部变量的存储期 在函数内部声明的局部变量通常在函数调用时创建,在函数返回时销毁。如果将局部变量声明为static,则该变量的生命周期会被延长到程序的整个运行期间,即使它的作用域仍然局限于声明它的那个函数内部。这意味着函数在多次调用之间可以保留变量的值。 1void function() {2 static int counter = 0; // counter的值在函数调用之间保持3 counter++;4 std::cout << counter << std::endl;5} 2. 控制全局变量的链接性 在文件范围内声明的static变量或函数,其链接性为内部的(internal linkage),这意味着它们只在定义它们的文件内可见,对其他文件是隐藏的。这有助于避免命名冲突,并保护数据不被其他文件意外访问或修改。 1static int globalVar = 0; // 仅在此文件内可见23static void helperFunction() { // 仅在此文件内可用4 // 函数实现5} 3. 类的静态成员 在类定义中,static关键字用于声明静态成员变量和静态成员函数。静态成员属于整个类而不是类的任何特定对象,因此即使没有创建类的实例,也可以访问静态成员。所有实例共享同一静态成员数据。 1class MyClass { 2public: 3 static int staticVar; // 静态成员变量 4 5 static void staticFunction() { // 静态成员函数 6 std::cout << "Accessing static function." << std::endl; 7 } 8}; 910int MyClass::staticVar = 0; // 静态成员变量的定义和初始化1112void example() {13 MyClass::staticVar = 5; // 访问静态成员变量14 MyClass::staticFunction(); // 调用静态成员函数15} 总结 当static用于函数内的局部变量时,它延长了变量的生命周期。 当static用于文件范围内的全局变量或函数时,它限制了变量或函数的链接性,使其仅在定义它们的文件内可见。 在类的上下文中,static用于声明属于整个类而不是某个实例的成员,允许在没有类实例的情况下访问这些成员。 这些特性使得static成为C++编程中一个非常有用和灵活的关键字。 Static 成员函数 在C++中,类的静态成员函数是属于整个类而不是类的任何特定对象的函数。静态成员函数与静态成员变量一样,它们不依赖于类的实例。这意味着即使没有创建类的对象,也可以调用静态成员函数。静态成员函数只能访问其类的静态成员变量和其他静态成员函数;它们不能直接访问类的非静态成员变量或函数,因为这些成员需要一个具体的类实例。 特性 类级别的函数:静态成员函数作用于类级别而非实例级别。 访问限制:只能直接访问静态成员变量和其他静态成员函数,不能直接访问非静态成员。 调用方式:通过类名和作用域解析操作符::调用,而不是通过类的实例。 使用场景 工具函数:当需要一个与类相关但不需要类实例就能执行的功能时,静态成员函数很有用。例如,计算两个参数的最大值或最小值等。 单例模式:在实现单例模式时,用于获取类的唯一实例的函数通常声明为静态的。 访问静态成员变量:用于访问和修改静态成员变量的值,尤其是当这些变量被声明为私有时,提供公共的静态成员函数来获取和设置它们的值是一种常见的做法。 示例代码 1#include <iostream> 2 3class Example { 4public: 5 static int staticVar; 6 7 static void staticFunction() { 8 std::cout << "Accessing staticVar: " << staticVar << std::endl; 9 }1011 void nonStaticFunction() {12 // 静态成员函数可以被非静态成员函数调用13 staticFunction();14 }15};1617// 静态成员变量的初始化18int Example::staticVar = 10;1920int main() {21 // 通过类名调用静态成员函数22 Example::staticFunction(); // 输出: Accessing staticVar: 102324 // 修改静态成员变量25 Example::staticVar = 20;2627 // 再次调用静态成员函数28 Example::staticFunction(); // 输出: Accessing staticVar: 202930 // 创建类实例并调用非静态成员函数31 Example example;32 example.nonStaticFunction(); // 输出: Accessing staticVar: 203334 return 0;35} 在这个例子中,staticFunction是一个静态成员函数,它访问了同一个类的静态成员变量staticVar。即使没有Example类的实例,也可以通过Example::staticFunction()来调用这个函数。静态成员函数是类的一部分,而不是任何特定实例的一部分,因此它们在设计类时提供了一种灵活的工具,尤其是在实现只与类相关而不依赖于类实例的行为时。 不同文件中的static变量的初始化顺序有规定吗 在C++中,不同文件(编译单元)中的静态变量(也称为静态存储期变量,包括全局变量、文件作用域的静态变量以及类的静态成员变量)的初始化顺序涉及到两个主要概念:在单个编译单元内的初始化顺序和跨多个编译单元的初始化顺序。 单个编译单元内的初始化顺序 在一个给定的编译单元(通常是一个源文件)内,静态变量按照它们出现的顺序被初始化。这意味着第一个声明的静态变量会先被初始化,以此类推。这部分是明确且可预测的。 跨多个编译单元的初始化顺序 跨多个编译单元(不同的源文件)的静态变量初始化顺序是未定义的。这意味着,如果你有两个源文件,每个文件中都定义了一个静态变量,那么你不能保证这两个变量哪一个会先被初始化。这种情况通常被称为“静态初始化顺序问题”或“静态初始化顺序地狱”。 静态初始化顺序问题的解决办法 为了解决或避免跨编译单元的静态初始化顺序问题,C++提供了一种被称为“构造函数初始化”(Construct On First Use)的技巧。它通过将静态变量封装在函数内部来延迟变量的初始化,直到这个变量被首次使用时才进行。这样做的好处是确保了初始化的顺序性和确定性。 例如: 1// 获取静态变量的函数2SomeClass& getStaticInstance() {3 static SomeClass instance; // 首次调用此函数时初始化4 return instance;5} 在这个例子中,instance变量在getStaticInstance()函数首次被调用时初始化。这保证了无论变量在程序中被首次访问的时间点或顺序如何,instance的初始化都是安全和确定的。 总结 虽然C++标准没有规定不同编译单元中静态变量的初始化顺序,但通过使用特定的设计模式和技巧,如“构造函数初始化”模式,可以有效地避免由此产生的问题。这种方法不仅可以解决初始化顺序问题,还可以延迟对象的初始化直到真正需要时,从而提高程序的启动性能和资源使用效率。 C++单例模式代码 单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。在C++中实现单例模式通常涉及以下几个关键步骤: 将构造函数、复制构造函数和赋值运算符标记为私有或删除,以防止外部创建类的多个实例。 在类内部提供一个静态私有指针变量,用于持有唯一的类实例。 提供一个公有的静态方法,用于获取这个唯一实例。 下面是单例模式的一个基本实现示例: 1#include <iostream> 2 3class Singleton { 4private: 5 // 私有静态成员变量,指向唯一实例 6 static Singleton* instance; 7 8 // 私有构造函数,防止外部直接创建实例 9 Singleton() {}1011 // 私有复制构造函数和赋值运算符,防止复制实例12 Singleton(const Singleton&) = delete;13 Singleton& operator=(const Singleton&) = delete;1415public:16 // 公有静态方法,用于获取唯一实例的引用17 static Singleton& getInstance() {18 if (instance == nullptr) {19 instance = new Singleton();20 }21 return *instance;22 }2324 // 示例方法25 void doSomething() {26 std::cout << "Doing something." << std::endl;27 }28};2930// 初始化静态成员变量31Singleton* Singleton::instance = nullptr;3233int main() {34 // 获取单例的引用并调用方法35 Singleton::getInstance().doSomething();3637 return 0;38} 这个例子展示了单例模式的核心要素:私有的构造函数和静态方法getInstance()确保全局只能通过该方法访问唯一的实例。通过将复制构造函数和赋值运算符设置为私有或删除,防止了实例的复制。 需要注意的是,这个基本实现在多线程环境下可能不是线程安全的。在多线程程序中,可能需要额外的同步机制(如互斥锁)来确保在创建单例实例时只有一个线程可以执行初始化代码。 对于多线程安全的单例实现,C++11及以后的版本提供了一个更简洁的方法,依靠局部静态变量的特性来保证线程安全: 1class Singleton { 2public: 3 // 删除复制构造函数和赋值运算符 4 Singleton(const Singleton&) = delete; 5 Singleton& operator=(const Singleton&) = delete; 6 7 // 提供一个静态方法获取单例引用 8 static Singleton& getInstance() { 9 static Singleton instance; // 局部静态变量10 return instance;11 }1213 void doSomething() {14 std::cout << "Doing something." << std::endl;15 }1617private:18 // 私有构造函数19 Singleton() {}20}; 在这个版本中,instance是一个局部静态变量,C++标准保证了当getInstance()第一次被调用时,instance将被创建,并且只会创建一次,即使在多线程环境下也能保证这一点。这种方法简化了代码,同时保证了线程安全性。 静态变量和线程安全的关系? 静态变量和线程安全之间的关系是一个重要的考虑点,特别是在多线程程序设计中。理解这一关系有助于开发出安全且高效的并发应用。 静态变量 静态变量在程序的生命周期内只被初始化一次,并在后续的程序运行过程中保持其状态。在C++中,静态变量可以是全局静态变量、局部静态变量、或是类的静态成员变量。全局静态变量和类的静态成员变量在所有对象和函数调用中共享,而局部静态变量在其所在函数的所有调用中共享。 线程安全 线程安全是指代码在多线程环境中被执行时,能够正确处理多个线程同时访问相同资源的情况,而不会导致数据损坏或不一致。如果一个函数或数据结构可以在多个线程中安全地被并发使用,不需要额外的同步机制,就称它为线程安全的。 静态变量与线程安全 由于静态变量在多个线程中共享,如果多个线程试图同时修改同一个静态变量,就可能导致数据冲突和不一致的问题。因此,静态变量本身不是线程安全的,除非采取了适当的同步措施。 全局静态变量和类的静态成员变量:当多个线程访问和修改这些变量时,如果没有适当的锁机制(例如互斥锁)来控制访问,就可能导致数据竞争和不一致性问题。 局部静态变量:在C++11及以后的版本中,局部静态变量的初始化是线程安全的,即在第一次访问变量时进行的初始化操作是由编译器自动加锁的,确保只有一个线程可以初始化变量。然而,初始化之后对变量的访问和修改仍然需要额外的同步措施来保证线程安全。 保证线程安全的策略 互斥锁:使用互斥锁(mutex)来同步对静态变量的访问。任何线程在访问变量之前必须首先获得锁,并在访问完成后释放锁。 原子操作:对于简单的数据类型,可以使用原子操作来更新静态变量,原子操作保证了操作的不可分割性,从而避免了数据竞争。 线程局部存储:如果静态变量不需要在多个线程间共享,可以考虑将其改为线程局部存储(Thread Local Storage, TLS),使得每个线程都有自己的变量副本。 总之,虽然静态变量在多线程程序中提供了便利和效率,但正确管理对它们的访问至关重要,以确保程序的线程安全性和稳定性。在设计多线程程序时,应该仔细考虑如何同步对静态资源的访问,以避免潜在的竞争条件和其他线程安全问题。 输入url到网页显示的过程 当你在浏览器中输入一个URL并按下回车键后,发生了一系列复杂的过程,这些过程一起工作,将网页内容呈现到你的屏幕上。以下是这个过程的高级概述: 1. 解析URL **URL(统一资源定位符)**分析成不同的部分:协议(如HTTP或HTTPS)、服务器地址(域名)和资源路径。 浏览器会检查URL格式是否正确,如果URL不完整或有误,浏览器可能会尝试修正它或使用搜索引擎来寻找正确的地址。 2. 域名解析(DNS查询) 浏览器会查找URL中域名对应的IP地址。这通常涉及到DNS(域名系统)查询。 首先,浏览器检查本地缓存中是否有此域名的记录;如果没有,浏览器会向配置的DNS服务器发送查询请求。 3. 建立连接 一旦获取到服务器的IP地址,浏览器会向服务器发起连接。对于HTTP协议,这意味着建立一个TCP连接;对于HTTPS协议,还需要进行TLS握手以建立一个加密连接。 4. 发送HTTP请求 浏览器构建一个HTTP请求,并通过TCP连接发送给服务器。这个请求包括请求行(如GET /index.html HTTP/1.1)、请求头(包括浏览器类型、接受的内容类型等)和请求体(对于某些类型的请求)。 5. 服务器处理请求并响应 服务器接收到请求后,会根据请求类型处理请求(如查找请求的资源)。 服务器将响应发送回浏览器,响应通常包括一个状态码(如200表示成功),响应头(包含内容类型、编码等信息)和响应体(即请求的资源内容)。 6. 浏览器渲染页面 浏览器接收到服务器的响应数据后,会根据响应的内容类型解析响应体。 如果是HTML文档,浏览器会解析HTML结构,构建DOM(文档对象模型)树。 浏览器解析CSS样式,并应用到DOM树上,构建渲染树。 执行JavaScript代码,可能会修改DOM树和渲染树。 浏览器布局渲染树中的元素,并将它们绘制到屏幕上,呈现最终的页面。 7. 后续处理 如果页面中包含了需要进一步加载的资源(如图片、CSS文件、JavaScript文件等),浏览器会重复上述过程来获取这些资源,并更新页面内容。 整个过程涉及网络、服务器、浏览器等多个层面的复杂交互。每一步都优化了速度和安全性,以确保用户能够快速、安全地获取和查看网页内容。 什么时候会返回404 HTTP 404错误状态码表示服务器无法找到请求的资源。这种情况通常发生在以下几种场景中: 1. URL错误或拼写错误 如果URL中的路径或文件名存在拼写错误,服务器会因为找不到匹配的资源而返回404错误。例如,如果用户尝试访问的网页路径拼写错误(如/hom代替/home),服务器将无法找到该资源。 2. 资源被移动或删除 如果之前存在的网页或资源被网站管理员移动到另一个位置,或者被完全删除,没有进行适当的重定向处理,那么对这个资源的请求将会返回404错误。这是导致404错误的常见原因。 3. 服务器配置问题 服务器配置错误也可能导致404错误。例如,如果Web服务器(如Apache或Nginx)的配置文件中指定的资源目录路径错误,或者配置了错误的重写规则,那么即使请求的资源存在,用户也可能收到404错误。 4. 域名解析错误 尽管这种情况较少见,但如果域名未正确解析到正确的服务器,或者服务器上没有为该域名配置虚拟主机,那么请求可能会被发送到错误的服务器,从而可能返回404错误。 5. 链接过时 对于一些内容管理系统(CMS)或动态生成的网站,页面的URL可能随着内容更新而改变。如果其他网站或用户的书签链接到了旧的URL,这将导致404错误。 6. 权限问题 在某些情况下,服务器可能配置为对未授权访问某些资源返回404错误,而不是403错误(禁止访问),这是为了隐藏资源的存在,增加安全性。 应对404错误的措施 检查URL:确认URL输入正确,没有拼写错误。 使用搜索功能:如果网站提供搜索功能,可以尝试搜索想要找到的内容。 检查网站的站点地图:站点地图列出了网站上所有可访问的页面链接。 联系网站管理员:如果认为页面应该存在,可以尝试联系网站的管理员或支持团队获取帮助。 设置自定义404页面:对于网站开发者来说,设置一个友好的404错误页面可以提供返回主页的链接或者搜索框,帮助用户找到他们感兴趣的内容。 404错误虽然令人沮丧,但正确处理这些错误可以提升用户体验,并帮助访问者找到他们感兴趣的内容或返回网站的其它部分。 TCP怎么实现可靠传输 TCP(传输控制协议)通过一系列的机制来实现在不可靠的网络层之上的可靠数据传输。这些机制确保了数据正确、有序地传输,即使在网络条件不佳的情况下也能尽可能保证数据的完整性和顺序。以下是TCP实现可靠传输的主要机制: 1. 三次握手建立连接 TCP使用一种称为“三次握手”的过程来建立连接,以确保双方都准备好进行数据传输。这个过程也同步双方的序列号,以便于后续的数据传输可以被正确排序,并跟踪哪些数据已被成功接收。 2. 数据包排序 每个TCP段都包含一个序列号,这允许接收方对接收到的数据包进行排序,以确保数据的正确顺序。 3. 确认应答与超时重传 接收方对收到的TCP段发送确认(ACK)回复,表明它已成功接收到该段。如果发送方在一个预定的超时期限内没有收到确认,它将重新发送该数据段。这个机制确保了数据的成功传输。 4. 流量控制 TCP使用窗口大小来进行流量控制,这个窗口大小告诉发送方接收方还能接收多少数据。这避免了发送方发送太多数据,导致接收方处理不过来而丢失数据。 5. 拥塞控制 TCP实现了几种拥塞控制算法(如慢启动、拥塞避免、快重传和快恢复)来减少网络中的拥塞。这些算法调整数据的传输速率,以响应网络中的拥塞情况,避免过多的数据包同时传输导致的网络拥塞和数据包丢失。 6. 保活机制 TCP保活机制可以检测出空闲的连接,防止连接由于长时间的无活动而被意外关闭。通过定期发送保活探针,TCP可以判断对方是否仍然可达。 7. 四次挥手断开连接 TCP使用四次挥手的过程来安全地关闭连接。这个过程确保了双方都已经发送和接收了所有的数据。 通过上述机制,TCP能够提供一种可靠的数据传输服务,即使在网络状况不稳定的情况下也能尽可能保证数据的完整性和顺序。这些特性使得TCP成为许多需要可靠数据传输的应用(如Web浏览、电子邮件和文件传输)的理想选择。 TCP连接过多怎么办 当TCP连接过多时,可能会导致服务器资源紧张,性能下降,甚至服务不可用。处理这种情况需要综合考虑,采取多种策略来优化和管理TCP连接。以下是一些常见的处理方法: 1. 优化服务器配置 增加最大连接数:调整操作系统和服务器软件的配置,增加可以同时打开的最大连接数。 调整TCP参数:比如调整TCP Keepalive时间,减少TIME_WAIT状态持续的时间,以便更快地回收和重用端口。 2. 使用负载均衡 部署负载均衡器:在服务器前端部署负载均衡器,将流量分发到后端的多个服务器上。这样可以分散压力,提高整体的处理能力和可用性。 3. 采用更高效的架构 使用异步IO:传统的同步IO模型在处理大量并发连接时可能会遇到瓶颈。采用异步IO模型(如IO多路复用)可以更高效地处理大量并发连接。 事件驱动架构:事件驱动的服务器架构(如Node.js)能够更好地支持高并发连接,提高资源利用率。 4. 优化应用层 连接复用:在应用层面,尽量复用已建立的TCP连接(如HTTP Keep-Alive),减少频繁建立和断开连接的开销。 限制连接时间:对于某些不需要长时间保持连接的应用,可以设定超时时间,超过时间限制后自动关闭连接。 5. 资源监控和自动扩展 监控资源使用:持续监控服务器的CPU、内存、网络等资源使用情况,及时发现瓶颈。 自动扩展:在云环境中,可以设置自动扩展策略,根据负载情况自动增减服务器实例。 6. 防御拒绝服务攻击 如果TCP连接过多是由拒绝服务攻击(DoS/DDoS攻击)引起的,需要采取相应的防御措施,如部署专业的DDoS防御系统,限制IP连接速率等。 处理TCP连接过多的问题需要从系统、网络和应用多个层面进行综合考虑和优化。在设计系统时,考虑到高并发和高可用性的需求,并采取适当的架构和技术来应对可能的高负载情况,是避免此类问题的关键。 为什么有IP地址还有MAC地址 IP地址和MAC地址都是网络中设备通信的重要组成部分,但它们在网络通信中扮演着不同的角色,服务于不同的网络层。理解它们之间的区别和为什么两者都需要,可以帮助我们更好地理解网络是如何工作的。 IP地址 IP地址(Internet Protocol Address)是网络层(特别是在TCP/IP模型中的第三层)的一个标识符,用于标识网络上的每个设备,以及帮助路由数据包从源头到目的地。IP地址主要用于不同网络或网络段之间的通信。因为网络设备可能频繁改变位置,进入不同的网络环境,IP地址可能会改变,特别是在使用DHCP服务时。 IP地址的主要功能包括: 标识:为每个连接到网络的设备提供唯一标识。 路由:帮助网络中的路由器决定如何将数据包从源地址转发到目的地址。 MAC地址 MAC地址(Media Access Control Address),也称为物理地址,是数据链路层(在TCP/IP模型中的第二层)的一个标识符,嵌入到网络接口卡(NIC)硬件中。MAC地址是在设备制造过程中分配的,通常是不变的(尽管可以通过软件进行修改)。MAC地址主要用于同一个物理网络内的设备之间的通信。 MAC地址的主要功能包括: 物理地址标识:为网络上每个物理设备提供唯一标识。 局域网通信:在局域网(LAN)内部,数据包的传输是基于MAC地址的。设备使用MAC地址来确定数据包是否是发往自己的。 为什么两者都需要 不同的网络层:IP地址和MAC地址分别服务于TCP/IP模型的不同层次。IP地址用于网络层,实现跨网络的数据传输。而MAC地址用于数据链路层,实现同一局域网内的设备通信。 不同的功能和作用范围:IP地址提供逻辑地址,使得设备可以在全球范围内通信,而MAC地址提供物理地址,用于局域网内部的通信。 路由和交换:在数据包传输过程中,路由器利用IP地址来路由数据包到正确的目的网络,而交换机则使用MAC地址来将数据包转发到正确的设备。 综上所述,IP地址和MAC地址共同确保了数据能够在复杂的网络环境中,从源设备准确、高效地传输到目标设备。 路由器有IP地址吗 是的,路由器具有IP地址。实际上,路由器通常会有至少两个IP地址:一个是用于局域网(LAN)侧的内部网络接口的IP地址,另一个是用于广域网(WAN)侧的外部网络接口的IP地址。这些IP地址使路由器能够在不同的网络环境中正确地路由数据包。 局域网(LAN)侧IP地址 私有IP地址:路由器的LAN侧IP地址通常是一个私有IP地址,这个地址是路由器内部网络的默认网关地址。设备在发送数据到局域网外部的互联网时,数据包会被发送到这个地址。 网关地址:在家庭或小型办公室网络中,路由器的LAN侧IP地址通常被配置为192.168.x.1或10.0.x.1这样的地址(x是0到255之间的任意数字),它作为内部网络中所有设备的默认网关。 广域网(WAN)侧IP地址 公有IP地址:路由器的WAN侧IP地址通常是由互联网服务提供商(ISP)分配的一个公有IP地址。这个地址是路由器在互联网上的标识,允许局域网内的设备通过路由器访问互联网。 唯一性:为了能在互联网上被其他系统识别和访问,每个连接到互联网的设备必须有一个唯一的公有IP地址。由于公有IP地址是有限的,家庭和小型办公网络通常只有路由器拥有一个公有IP地址,而内部设备则使用NAT(网络地址转换)技术通过这个公有IP地址共享互联网连接。 管理IP地址 用于管理的IP地址:除了用于路由的IP地址外,路由器还可能配置有一个特别的管理IP地址,用于访问路由器的管理界面(如Web界面)。这通常是路由器LAN侧的IP地址。 总之,路由器拥有IP地址是它完成数据包路由、网络通信和管理任务的基础。通过这些IP地址,路由器能够在局域网内提供连接到互联网的通道,同时允许网络管理员对其进行配置和管理。 内存泄露怎么解决 内存泄露是指程序中已分配的内存未能成功释放,导致无法再被程序利用的情况。长时间运行或多次执行这样的程序可能会导致可用内存逐渐减少,最终影响程序性能或导致程序崩溃。解决内存泄露的问题通常需要几个步骤,包括检测、定位和修复泄露。 检测内存泄露 使用内存分析工具:大多数编程语言都提供了内存分析工具或库,如Valgrind、LeakSanitizer、Visual Studio的诊断工具、Xcode的Instruments等,可用于检测内存泄露。 代码审查:定期进行代码审查,特别关注内存分配和释放的逻辑,可以帮助识别潜在的内存泄露问题。 监控系统性能:通过监控应用程序的内存使用情况,可以发现异常的内存增长模式,这可能是内存泄露的迹象。 定位内存泄露 分析内存分配:利用内存分析工具,可以获取内存分配和释放的详细报告,从而定位未被释放的内存块。 日志和调试信息:在关键的内存分配和释放点添加日志信息,可以帮助追踪内存使用情况,进而定位泄露的位置。 代码分析:静态代码分析工具可以帮助识别内存管理的常见问题,例如未匹配的内存分配和释放。 修复内存泄露 确保匹配的内存释放:每次内存分配(例如new、malloc)都应该有对应的释放操作(例如delete、free)。 使用智能指针(C++):在C++中,使用智能指针(如std::unique_ptr、std::shared_ptr)可以自动管理内存,减少显式释放内存的需要。 利用RAII原则(资源获取即初始化):确保资源(包括内存)的分配和释放都在同一个对象的构造函数和析构函数中进行,利用作用域自动管理资源。 避免内存泄露的编程模式:例如,在C++中避免循环引用(可通过std::weak_ptr解决),在Java或C#中注意事件处理器的注册和注销等。 内存池:对于频繁分配和释放的小对象,使用内存池可以减少内存碎片和泄露的可能性。 解决内存泄露的关键在于深入理解代码的内存使用情况,以及利用合适的工具和策略来监控、定位和修复问题。持续的性能监控、代码审查和测试是防止内存泄露的重要手段。 为什么要有虚拟内存空间 虚拟内存空间是现代计算机系统中的一个关键概念,它提供了一个抽象层,使得软件开发者和操作系统能够更有效地管理硬件资源。虚拟内存的引入主要基于以下几个原因和优势: 1. 内存扩展 扩大可用内存:虚拟内存允许系统使用硬盘空间作为额外的内存,从而扩大了可供程序使用的内存空间。这意味着即使物理RAM不足,程序也能运行,因为操作系统可以将部分数据暂时存储在硬盘的虚拟内存页中。 2. 进程隔离 提供进程间隔离:每个进程都在其自己的虚拟地址空间中运行,这使得进程之间的内存访问相互隔离,防止一个进程的错误操作(如越界访问)影响到其他进程。 3. 内存管理简化 简化内存管理:虚拟内存使得操作系统可以为每个进程提供一致的地址空间,简化了内存的分配和管理。程序员无需担心物理内存的具体位置和限制,可以认为有一个几乎无限大的内存空间。 4. 内存保护 增强安全性和稳定性:操作系统可以通过虚拟内存来控制每个进程对内存的访问权限(如只读、读写或执行权限),增加了系统的安全性和稳定性。 5. 数据共享 方便数据共享与通信:虚拟内存机制也使得不同进程间共享内存成为可能,便于进程间的数据共享和通信。 6. 物理内存优化 优化物理内存使用:虚拟内存允许操作系统更灵活地管理物理内存,如通过页面置换算法(如最近最少使用LRU算法)来决定哪些数据应当保留在RAM中,哪些可以被移动到硬盘,从而最优化物理内存的使用。 7. 支持多任务 支持多任务处理:虚拟内存为多任务操作系统提供了基础,使得多个应用程序能够同时运行,同时保证它们的运行环境互不干扰,提高了计算机系统的效率和响应速度。 虚拟内存通过上述优势,不仅提高了计算机的性能和资源利用率,也极大地简化了程序设计和系统管理,是现代操作系统不可或缺的一部分。 i++是原子性的吗 i++操作(自增操作)在多数编程语言中并不是原子性的,尽管在单个线程的上下文中这个操作看起来只是简单地增加变量的值。i++实际上包含了三个独立的步骤:读取变量i的当前值、增加这个值、将新值写回到变量i。在多线程环境中,如果没有适当的同步机制,这三个步骤之间可能会被其他线程的操作打断,导致竞态条件和数据不一致的问题。 为什么i++不是原子性的 读取:首先,程序需要读取变量i的当前值到CPU寄存器。 修改:在CPU寄存器中,给这个值加1。 写回:最后,将新值写回内存中的变量i。 这个过程中的任何步骤都可能被其他线程打断,特别是在没有锁或其他同步机制保护的情况下。例如,两个线程几乎同时读取i的值,然后分别增加1并尝试写回,结果是i实际上只增加了1而不是2。 如何确保操作的原子性 要确保类似i++这样的操作在多线程环境中的原子性,可以采用以下一种或多种方法: 使用互斥锁(Mutex):通过在执行i++操作之前获取互斥锁,然后执行操作,最后释放锁,可以确保这个操作在完成之前不会被其他线程打断。 原子操作函数:许多编程语言和库提供了原子操作API,比如C++11引入的std::atomic类型和相关操作,或Java的java.util.concurrent.atomic包中的类。这些API能够保证变量操作的原子性。 使用硬件同步原语:某些平台提供了CPU指令级的支持来执行原子操作,如x86架构的CMPXCHG指令。编程语言或库的原子操作API底层可能会利用这些硬件特性。 总之,i++操作本身在多线程环境下不是原子性的,需要通过同步机制或使用特定的原子操作API来保证其安全性。在设计多线程程序时,正确处理这类操作至关重要,以避免数据不一致和竞态条件。 子类中变量初始化顺序和销毁顺序 在面向对象的编程中,特别是在使用如Java、C++等语言时,子类和父类中变量的初始化和销毁顺序遵循特定的规则,这些规则保证了对象的构造和析构过程的正确性和逻辑性。下面是这些语言中常见的初始化和销毁顺序的概述: Java 初始化顺序 父类静态变量和静态初始化块:按照它们在父类中出现的顺序。 子类静态变量和静态初始化块:按照它们在子类中出现的顺序。 父类非静态变量和非静态初始化块:按照它们在父类中出现的顺序。 父类构造器。 子类非静态变量和非静态初始化块:按照它们在子类中出现的顺序。 子类构造器。 销毁顺序 Java中,对象的销毁是由垃圾回收器(GC)处理的,没有像C++中析构函数那样直接的销毁过程。但是,可以通过finalize()方法提供一定的清理逻辑。通常,finalize()方法的调用顺序与构造器的调用顺序相反,但依赖于GC的具体实现和行为,finalize()方法的调用时机和顺序可能是不确定的。 C++ 初始化顺序 父类构造器:首先调用基类的构造函数。 成员变量初始化:按照它们声明的顺序初始化父类的成员变量。 父类构造器体内的代码。 成员变量初始化:按照它们声明的顺序初始化子类的成员变量。 子类构造器体内的代码。 销毁顺序 销毁顺序与初始化顺序相反: 子类析构器体内的代码。 子类成员变量的析构:按照初始化顺序的逆序进行销毁。 父类析构器体内的代码。 父类成员变量的析构:按照初始化顺序的逆序进行销毁。 在C++中,析构函数是显式定义的,它们提供了在对象销毁时执行清理资源等操作的机会。与Java不同,C++的对象销毁是确定的,由程序员或对象作用域结束时自动触发。 这些初始化和销毁的规则确保了对象在其生命周期内的状态和行为的正确性,是面向对象编程中重要的概念。理解这些规则对于编写可靠和高效的代码至关重要。 cpu如何实现除法 CPU实现除法的方法可以根据其设计和指令集架构的不同而不同。在硬件层面,有几种常见的方法用于实现除法运算: 1. 长除法(硬件实现) 这是最直观的方法,类似于我们在纸上执行的长除法过程,但是在二进制下进行。CPU通过一系列的移位、比较和减法操作来实现除法。这个过程涉及将被除数左移(相当于在二进制下的乘以2),然后与除数比较,如果被除数大于等于除数,就从被除数中减去除数,并在商的相应位置上放置一个1,否则放置一个0。这个过程重复进行,直到完成所有的位操作。 2. 逼近法(硬件实现) 牛顿-拉弗森迭代法(Newton-Raphson):这种方法利用迭代逼近来找到除法的结果。它基于牛顿迭代法求解函数零点的数学原理,通过迭代计算逐步逼近真实的除法结果。这种方法特别适用于浮点除法。 高德(Goldschmidt)迭代法:这是另一种迭代逼近方法,通过同时对被除数和除数进行乘法操作来逼近结果,最终将除法问题转化为乘法问题。 3. 查表法(软件/硬件辅助实现) 对于一些特定的除数,可以通过查找预先计算好的除法结果表来快速得到结果。这种方法在执行小范围内的除法操作时效率很高,但对于大数的除法则不太实用,因为查表需要大量的存储空间。 4. 软件实现 对于一些没有硬件除法支持的简单CPU或者特定应用场景,除法运算可能通过软件实现,即编译器将除法操作转换为一系列的基本操作,如移位、加法和减法。这种方法比直接的硬件支持要慢,但可以在不支持硬件除法的CPU上执行除法运算。 性能考虑 在CPU中,除法是相对较慢的操作,特别是浮点除法。因此,现代CPU设计采用高级算法和技术(如流水线处理、并行执行等)来优化除法操作的性能。在软件开发中,避免不必要的除法运算或将其替换为更快的操作(如移位)是常见的性能优化手段。 总的来说,CPU中除法的实现是复杂的,涉及多种算法和技术,旨在平衡计算的准确性和执行的效率。 三个线程如何交替打印ABC循环100次,用C++实现 实现三个线程交替打印ABC循环100次的任务,可以通过同步机制来控制线程间的执行顺序。在C++中,可以使用std::mutex和std::condition_variable来实现这一目标。下面是一个具体的实现示例: 1#include <iostream> 2#include <thread> 3#include <mutex> 4#include <condition_variable> 5 6class ABCPrinter { 7public: 8 ABCPrinter(int count) : count_(count) {} 910 void printA() {11 for (int i = 0; i < count_; ++i) {12 std::unique_lock<std::mutex> lock(m_);13 cv_.wait(lock, [this]() { return currentChar_ == 'A'; });14 std::cout << 'A';15 currentChar_ = 'B';16 cv_.notify_all();17 }18 }1920 void printB() {21 for (int i = 0; i < count_; ++i) {22 std::unique_lock<std::mutex> lock(m_);23 cv_.wait(lock, [this]() { return currentChar_ == 'B'; });24 std::cout << 'B';25 currentChar_ = 'C';26 cv_.notify_all();27 }28 }2930 void printC() {31 for (int i = 0; i < count_; ++i) {32 std::unique_lock<std::mutex> lock(m_);33 cv_.wait(lock, [this]() { return currentChar_ == 'C'; });34 std::cout << 'C';35 currentChar_ = 'A';36 cv_.notify_all();37 }38 }3940private:41 std::mutex m_;42 std::condition_variable cv_;43 char currentChar_ = 'A';44 int count_;45};4647int main() {48 const int count = 100;49 ABCPrinter printer(count);5051 std::thread t1(&ABCPrinter::printA, &printer);52 std::thread t2(&ABCPrinter::printB, &printer);53 std::thread t3(&ABCPrinter::printC, &printer);5455 t1.join();56 t2.join();57 t3.join();5859 std::cout << std::endl;6061 return 0;62} 这个程序中,ABCPrinter类控制着打印行为。类中有一个std::mutex用于同步,一个std::condition_variable用于线程间的条件等待和通知,以及一个currentChar_变量来标记当前应该打印哪个字符。 每个打印函数(printA、printB、printC)都会尝试获得锁,并通过条件变量等待直到它们各自负责的字符(currentChar_)是当前应该打印的字符。 当条件满足时,打印相应的字符,并更新currentChar_为下一个字符,然后通过cv_.notify_all()唤醒所有等待的线程,这些线程再次检查条件是否满足自己打印的条件。 这个循环持续进行,直到打印了指定的次数。 通过这种方式,三个线程可以协同工作,依次交替打印ABC,共计100次。 unique_lock是什么 std::unique_lock是C++标准库中的一个实用工具,提供了一种灵活的方式来管理带有互斥锁(std::mutex)的锁定和解锁操作。它是头文件<mutex>的一部分,是对基本互斥锁管理的一个封装,提供了比std::lock_guard更多的功能和灵活性。尽管std::unique_lock比std::lock_guard占用的资源稍多,但它在需要更复杂锁管理的场景下非常有用。 主要特点和用途 灵活的锁管理:std::unique_lock允许在运行时进行锁的获取和释放,而std::lock_guard仅在构造时加锁,在析构时解锁,不提供中途手动控制锁的功能。 支持条件变量:std::unique_lock可以与std::condition_variable一起使用,用于等待条件或通知其他线程。std::condition_variable需要与std::unique_lock<std::mutex>一起使用来等待条件或发送通知。 支持移动语义:std::unique_lock支持移动操作,这意味着可以将锁的所有权从一个std::unique_lock对象转移到另一个。这对于从函数返回已锁定的互斥锁或将其作为参数传递给函数等操作非常有用。 示例用法 1#include <mutex> 2#include <thread> 3#include <iostream> 4 5std::mutex mtx; // 全局互斥锁 6 7void print_block(int n, char c) { 8 // 在std::unique_lock对象构造时加锁 9 std::unique_lock<std::mutex> lock(mtx);10 for (int i = 0; i < n; ++i) { std::cout << c; }11 std::cout << '\n';12 // lock在析构时自动解锁13}1415int main() {16 std::thread t1(print_block, 50, '*');17 std::thread t2(print_block, 50, '$');1819 t1.join();20 t2.join();2122 return 0;23} 在这个例子中,两个线程使用同一个互斥锁来同步对std::cout的访问。std::unique_lock<std::mutex>在print_block函数中创建并自动管理锁的生命周期:自动加锁和在离开作用域时解锁。 总之,std::unique_lock提供了一种灵活的方式来控制互斥锁的锁定和解锁,特别适合需要条件变量、可移动锁、或者需要在运行时进行更复杂的锁管理操作的场景。 CV的条件变为真的时候,谁来唤醒呢? 条件变为真时,线程被唤醒通常是由其他线程执行的操作导致的。这些操作包括调用std::condition_variable的notify_one或notify_all成员函数。这两个函数的作用是通知正在等待的一个或所有线程(分别对应notify_one和notify_all),条件可能已经满足,等待的线程应该再次检查条件。 这里是如何使用它们的简化示例: notify_one: 当条件变为真时,某个线程调用cv.notify_one()会唤醒一个正在等待(通过cv.wait()或cv.wait_for()等方法)的线程。如果有多个线程在等待,只有一个线程(由调度程序随机选择)会被唤醒。 notify_all: 相比之下,当条件变为真时,调用cv.notify_all()会唤醒所有正在等待的线程。这对于需要多个线程响应同一个事件的情况特别有用。 示例场景 假设有两个线程A和B: 线程A执行某个任务,完成后需要通知线程B任务已完成,线程B可以开始执行依赖于A的任务。 线程A在完成任务后调用cv.notify_one()或cv.notify_all()(假设只有线程B在等待,两者效果相同)。 线程B在开始自己的任务前调用cv.wait(lock, predicate)等待线程A的通知。一旦线程A调用了notify_one()或notify_all(),线程B被唤醒。 被唤醒后,线程B会自动重新获得之前wait操作中释放的锁,并重新检查条件(谓词函数)。如果条件满足,线程B继续执行后续操作;如果不满足(虽然被唤醒,但条件可能由于某些原因未真正满足),线程B会再次进入等待状态。 这种机制确保了线程间的协作和同步,允许线程在适当的时间点安全地进行操作,避免了资源竞争和不一致的风险。 如何实现断点续传 实现断点续传的关键在于能够记录下载或上传的当前进度,并在传输被中断后能够从该进度处恢复而不是重新开始。断点续传通常用于处理大文件传输,尤其是在网络条件不稳定的情况下。以下是实现断点续传功能的基本步骤和策略: 1. 记录传输进度 客户端和服务器都需要记录文件的传输进度。这可以通过保存已经传输的数据量(字节)来实现,或者记录已经成功传输的文件块的标识。 2. 支持范围请求 服务器需要支持范围请求(HTTP协议的Range头部)。这允许客户端请求文件的特定部分而不是整个文件。客户端可以通过发送一个包含Range头部的HTTP请求来实现,指定需要下载的文件部分的起始和结束字节位置。 3. 客户端请求特定数据块 客户端在恢复下载时请求未完成的数据块。基于已记录的进度,客户端计算出需要从哪个字节位置开始恢复下载,并发送带有Range头部的请求。 4. 服务器响应范围请求 服务器识别Range请求并发送相应的文件片段。服务器需要正确处理带有Range头部的请求,只发送请求的文件部分。 5. 校验和完整性验证 使用校验和或哈希来确保数据完整性。在传输完成后,客户端可以对接收到的文件片段或整个文件计算校验和或哈希值,与服务器端提供的值进行比较,以验证数据的完整性和正确性。 示例:HTTP断点续传 假设客户端已经下载了文件的前1024字节,现在希望从第1025字节开始继续下载: 1GET /path/to/file HTTP/1.12Host: example.com3Range: bytes=1025- 服务器响应: 1HTTP/1.1 206 Partial Content2Content-Range: bytes 1025-2048/20493Content-Length: 102445[数据块] 实现注意事项 持久化进度信息:为了在客户端或服务器重启后能够恢复传输,进度信息需要被持久化存储,例如保存在文件系统或数据库中。 并发控制:如果支持多个客户端同时对同一文件进行断点续传,需要实现适当的并发控制机制,以防止数据冲突。 安全性:在设计断点续传功能时,应考虑到数据传输的安全性,使用加密连接(如HTTPS)来保护传输过程中的数据。 通过上述步骤和策略,可以有效实现文件的断点续传功能,提高大文件传输的效率和可靠性,尤其是在网络环境不稳定的情况下。 explicit关键字在C++中的作用 在C++中,explicit关键字用于修饰类的构造函数,以防止隐式类型转换或拷贝初始化。使用explicit关键字可以避免因不经意的类型转换而导致的错误,使代码的意图更加清晰。 隐式类型转换 默认情况下,如果一个构造函数只接受一个参数,或者所有参数除了一个都有默认值,那么它可以被用于隐式类型转换。例如: 1class MyClass { 2public: 3 MyClass(int x) { 4 // 构造函数的实现 5 } 6}; 7 8void func(MyClass obj) { 9 // func的实现10}1112int main() {13 func(10); // 隐式调用 MyClass(int) 构造函数14 return 0;15} 在上面的例子中,当func(10)被调用时,整数10会隐式地被用来构造一个MyClass的对象,因为MyClass有一个接受int类型参数的构造函数。 使用explicit防止隐式转换 要防止这种隐式类型转换,可以在构造函数前加上explicit关键字: 1class MyClass { 2public: 3 explicit MyClass(int x) { 4 // 构造函数的实现 5 } 6}; 7 8void func(MyClass obj) { 9 // func的实现10}1112int main() {13 func(10); // 这会引发编译错误14 func(MyClass(10)); // 正确的调用方式15 return 0;16} 通过将构造函数声明为explicit,你告诉编译器不允许使用该构造函数进行隐式类型转换。这样,除非显式地调用构造函数(如MyClass(10)),否则编译器会报错。 explicit的作用域 C++11之前,explicit关键字只能用于修饰只接受一个参数的构造函数。 从C++11开始,explicit可以用于修饰接受多个参数的构造函数,以及转换运算符。 例如,防止类的对象被隐式转换为其他类型: 1class MyClass { 2public: 3 // 构造函数 4 explicit MyClass(int x) {} 5 6 // 转换运算符 7 explicit operator bool() const { 8 return true; 9 }10};1112int main() {13 MyClass obj(10);14 bool flag = obj; // 这会引发编译错误15 bool flag2 = static_cast<bool>(obj); // 正确的调用方式16 return 0;17} 使用explicit关键字是一种良好的编程实践,它可以增加代码的安全性和可读性,防止意外的类型转换导致的错误。 链表和数组的区别 队列和栈的区别 二叉树先序遍历、中序遍历、后序遍历 引用和指针的区别 using,typedef和define有什么区别 using、typedef和define在C++中都是与类型和符号定义相关的语法元素,但它们在用法、功能和适用范围上有着明显的不同。 typedef typedef是C和C++中用来为类型定义一个新名字的关键字。它常用于简化复杂类型的名称,提高代码的可读性。 1typedef unsigned long ulong;2typedef void (*FuncPtr)(int, double); 上述例子中,ulong成为了unsigned long的别名,FuncPtr成为了指向特定函数的指针类型的别名。 using using是C++11引入的类型别名声明的新语法,功能上与typedef相似,但语法更加直观,尤其是在定义模板别名时。 1using ulong = unsigned long;2using FuncPtr = void (*)(int, double); 对于模板别名,using的优势更为明显: 1template<typename T>2using Ptr = T*; 上述代码定义了一个模板别名Ptr,可以用于任意类型的指针。 #define #define是C和C++中的预处理指令,用于定义宏。它在编译前对代码进行文本替换,可以用来定义常量值、函数宏,或是进行条件编译等。 1#define PI 3.141592#define MAX(a, b) ((a) > (b) ? (a) : (b)) 主要区别 作用范围:typedef和using仅用于类型别名,增强代码可读性和简化复杂类型声明。#define是预处理指令,功能更广泛,但主要用于宏定义和条件编译。 类型安全:typedef和using是类型安全的,它们定义的别名完全等同于原类型。#define进行的是文本替换,不涉及类型检查,有可能导致难以追踪的错误。 适用范围:typedef和using仅适用于类型定义,而#define几乎可以用于定义任何文本替换。 编译阶段:typedef和using在编译阶段处理,#define在预处理阶段处理。 总结来说,尽管typedef、using和#define在某些场景下可能看起来有相似的作用(如定义别名),但它们的用途、功能和适用范围有很大差别。在现代C++编程中,推荐使用using来定义类型别名,因为它比typedef更灵活,且语义更清晰。而#define因其灵活性和潜在的风险,在定义常量和类型别名时应谨慎使用,更多的是用于条件编译等场景。 const修饰成员函数会怎么样?可以有两个f(),其中一个带const,另一个不带吗? static定义的变量默认值是多少?普通变量呢? extern有什么作用 简述指针常量与常量指针区别 构造函数能否为虚函数 为什么基类的析构函数需要是序函数? 重载、隐藏、重写(覆盖)三者的区别? 重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型(统一为void,否则报错)。 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。 重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。 重载的参数不同,函数体不同;隐藏的参数可以不同,函数体不同;重写或者覆盖仅仅函数体不同。 重载为什么改变参数就可以实现调用不同的函数? 重载(Overloading)是一种允许多个在同一作用域内的函数或操作符拥有相同名称,但是参数类型、个数或顺序不同的编程技术。编译器根据这些不同的参数列表来区分它们,这个过程称为重载解析(Overload Resolution)。当一个重载的函数被调用时,编译器通过匹配调用中使用的参数类型和数量,选择最合适的重载版本执行。下面解释为什么通过改变参数可以实现调用不同的函数: 参数类型、数量和顺序的不同 类型:如果两个函数的参数数量相同,但参数类型不同,编译器可以根据传递给函数的实参类型来决定调用哪个函数。 数量:如果两个函数的参数类型相同或者兼容,但参数数量不同,编译器会根据传递的参数数量来决定调用哪个函数。 顺序:如果两个函数的参数数量和类型都相同,但参数的顺序不同,编译器同样可以根据传递的参数的顺序来决定调用哪个函数。 编译器的重载解析过程 当调用一个重载的函数时,编译器执行一个重载解析过程,按照以下步骤: 候选函数集合:收集所有与调用匹配的重载函数,包括那些参数可以通过隐式转换匹配的函数。 可行函数集合:从候选函数中筛选出实参可以被隐式转换以匹配形参类型的函数。 最佳匹配:从可行函数中选择“最佳匹配”的函数。编译器会根据类型匹配的精确度(如是否需要类型转换,转换的复杂程度等)来决定最佳匹配。 调用:根据最佳匹配调用相应的函数。 如果编译器无法找到一个明确的最佳匹配,或者找到多个同样好的匹配,就会产生一个重载解析的歧义,编译时会报错。 重载的实用性 重载使得函数名可以根据上下文有不同的行为,提高了代码的可读性和易用性。例如,标准库中的std::cout就重载了多种类型的<<操作符,使得我们可以方便地输出不同类型的数据。 示例 1void print(int i) { 2 std::cout << "Integer: " << i << std::endl; 3} 4 5void print(double f) { 6 std::cout << "Double: " << f << std::endl; 7} 8 9void print(const std::string& s) {10 std::cout << "String: " << s << std::endl;11}1213int main() {14 print(10); // 调用 print(int)15 print(3.14); // 调用 print(double)16 print("Hello"); // 调用 print(const std::string&)17} 在这个例子中,print函数被重载了三次,分别接受int、double和std::string类型的参数。编译器根据传递给print函数的参数类型来决定调用哪一个重载版本。 通过这种方式,重载为编程提供了更高的灵活性和表达力。 重载对链接有什么影响?重载的底层实现? 函数重载在编译阶段对函数名进行了修饰(或称为名字改编、名字矫正、mangling),以保证每个重载函数在程序的链接阶段有一个唯一的标识。这个过程对链接有重要影响,因为它确保了链接器可以正确地区分和链接各个重载函数,即使它们有相同的基础名称。 名字修饰(Name Mangling) 定义:名字修饰是一种编译器技术,用于在内部符号表中生成唯一的函数和变量标识符。对于重载函数,编译器将函数的名称、参数类型列表(有时还包括命名空间或类名称)编码到生成的唯一标识符中。 目的:主要目的是解决名称冲突问题,特别是在函数重载和模板实例化的情况下,这些情况下可能会有多个实体共享相同的名称。 链接阶段的影响 在链接阶段,链接器需要解析程序中的所有外部符号引用,将它们与相应的符号定义匹配起来。由于名字修饰,每个重载函数都有了独特的内部名称,链接器可以正确地识别和链接到正确的函数实现,即使多个函数具有相同的基本名称。 不同编译器的差异 不同的编译器可能采用不同的名字修饰规则。这意味着用不同编译器编译的代码在链接时可能会因为名字修饰的不兼容而遇到问题,尤其是在尝试链接不同编译器生成的二进制库时。为了解决这个问题,可以采用以下策略: 使用相同的编译器:对于需要链接在一起的所有模块,尽量使用相同的编译器和编译选项。 C语言接口:对于C++库,如果需要与使用不同编译器的代码链接,可以提供一个“纯C”的接口,因为C语言没有函数重载,也不进行名字修饰,具有更好的二进制兼容性。 外部接口(Extern “C”):在C++中,可以使用extern "C"来告诉C++编译器对于特定的函数或变量不要进行名字修饰,从而使得这些符号能够被不同编译器编译的代码所链接。 通过这些方法,可以减少或避免由于名字修饰规则差异导致的链接问题,确保重载函数的正确链接和使用。 构造函数可以被重载么?析构函数呢? new和malloc有什么区别? new operator和operator new的区别? 在C++中,new操作符和operator new函数经常令人混淆,但它们有着明显的不同和各自的作用。 new操作符 new操作符用于动态分配内存并调用构造函数初始化对象。它是一个高级操作,执行了两个主要步骤: 内存分配:首先,new操作符调用operator new函数分配足够的内存以容纳特定类型的对象。这是一个底层操作,仅负责分配内存,并不负责构造对象。 构造对象:然后,在分配的内存上调用对象的构造函数来初始化对象。 这个过程可以通过下面的例子来说明: 1MyClass* obj = new MyClass(); 上面的代码首先使用operator new分配足够的内存来存储一个MyClass类型的对象,然后在这块内存上调用MyClass的默认构造函数初始化对象。 operator new函数 operator new是一个全局函数或者类成员函数,仅负责分配内存,不涉及对象的构造。它是new操作符背后的内存分配机制。当你使用new操作符时,实际上是隐式调用了operator new函数来分配内存。 如果需要,可以重载operator new来提供自定义的内存分配策略。例如: 1void* operator new(size_t size) {2 // 自定义内存分配逻辑3 void* p = malloc(size);4 // 处理内存分配失败的情况5 if (!p) throw std::bad_alloc();6 return p;7} 需要注意的是,重载operator new需要非常谨慎,因为它会改变程序的基本内存分配行为。 总结 new操作符是一个高级操作,用于分配内存并初始化对象。 operator new函数是一个底层操作,仅用于分配内存,不负责对象的构造。 在执行new操作符时,实际上会调用operator new函数来分配内存,然后在分配的内存上调用构造函数来构造对象。 可以重载operator new和operator delete来提供自定义的内存分配和释放策略,但需要谨慎操作,以避免意外的行为。 深入解析new、operator new、::new、placement new https://blog.csdn.net/songchuwang1868/article/details/81353577 在C++中,new操作的不同形式提供了内存分配和对象构造的灵活手段。深入理解它们之间的区别对于编写高效、可靠的C++代码非常重要。下面是对new、operator new、::new和placement new的深入解析: 1. new操作符 new操作符用于动态分配内存,并调用构造函数初始化对象。它是一个高级操作,封装了内存分配和对象构造两个步骤: 1MyClass* obj = new MyClass(args); 上述代码首先调用operator new函数分配足够的内存来存储MyClass类型的对象,然后在分配的内存上调用MyClass的构造函数,使用args作为参数。 2. operator new函数 operator new是一个全局函数或类成员函数,负责分配内存。当使用new操作符时,背后就是调用operator new来进行内存分配。与new操作符不同,operator new仅分配内存,不负责构造对象: 1void* ptr = operator new(sizeof(MyClass)); 可以重载operator new来自定义内存分配策略。 3. ::new ::new指的是全局作用域下的new操作符,用来明确调用全局的operator new函数,而不是某个类的重载版本。这在有重载operator new的情况下很有用,确保调用的是全局版本: 1MyClass* obj = ::new MyClass(args); 4. Placement new placement new允许在已分配的内存上构造对象。这种方式不分配内存,只调用对象的构造函数。placement new非常有用,特别是在需要在特定位置构造对象的场景中: 1void* ptr = malloc(sizeof(MyClass)); // 先分配内存2MyClass* obj = new(ptr) MyClass(args); // 在ptr指向的内存上构造对象 需要注意的是,使用placement new时,应当手动调用对象的析构函数,并负责释放内存: 1obj->~MyClass(); // 调用析构函数2free(ptr); // 释放内存 总结 new操作符:高级操作,分配内存并构造对象。 operator new函数:底层操作,仅分配内存,可被重载。 ::new:使用全局operator new,避免调用类的重载版本。 placement new:在指定内存位置构造对象,不分配内存,需要手动管理内存和析构。 理解这些不同的new形式及其用途,可以帮助开发者更有效地管理内存和对象的生命周期,编写出更加精细控制和高效的C++代码。 虚函数表的结构是怎样的? 虚函数表是一个函数指针数组,数组里存放的都是函数指针,指向虚函数所在的位置。 对象调用虚函数时,会根据虚指针找到虚表的位置,再根据虚函数声明的顺序找到虚函数在数组的哪个位置,找到虚函数的地址,从而调用虚函数。 A,B两个类,类中有虚函数。C继承AB,有几张虚函数表? 答:2张 再问:为什么2张? 多继承就会有多个虚函数表。因为每个父类的虚函数是不同的,指针也是不同的。 如果共用一张虚函数表,就分不清到底子类的实例化是针对哪一个基函数的。 为什么不应该在构造函数中调用虚函数 在C++中,父类(基类)的构造函数中调用虚函数是合法的,但这可能不会按照初学者期望的方式工作。在基类构造期间调用虚函数时,并不会调用派生类(子类)中的重写版本,即使是在构造派生类对象的过程中。相反,会调用基类中该虚函数的版本,或者是更上层基类中该虚函数最近的重写版本。这是因为在基类构造期间,对象类型被视为基类类型,而不是派生类类型,从而防止了对尚未完全构造的对象执行操作。 为什么不应该在构造函数中调用虚函数 调用尚未完全构造的对象的成员函数可能会导致未定义行为或错误。如果虚函数依赖于派生类中的某些成员变量,而这些成员变量在基类构造函数被调用时尚未初始化,那么虚函数可能无法正常工作或产生错误结果。 示例 考虑以下示例代码: 1#include <iostream> 2 3class Base { 4public: 5 Base() { 6 callVirtual(); 7 } 8 9 virtual void callVirtual() {10 std::cout << "Base version of callVirtual\n";11 }12};1314class Derived : public Base {15public:16 Derived() : Base() {}1718 void callVirtual() override {19 std::cout << "Derived version of callVirtual\n";20 }21};2223int main() {24 Derived d;25 return 0;26} 输出将是: 1Base version of callVirtual 尽管Derived对象被构造,但在Base构造函数中调用callVirtual()时,只会调用Base类中的callVirtual()版本,而不是Derived类中重写的版本。 最佳实践 为了避免潜在的错误和混淆,最佳实践是在构造函数和析构函数中避免调用虚函数。如果需要在对象构造期间执行某些操作,并且这些操作需要在派生类中进行特定的实现,考虑使用其他设计模式,如工厂模式,其中对象在完全构造后立即进行初始化,或者通过非虚成员函数调用虚函数,该非虚成员函数在对象构造完成后明确调用。 在构造函数中调用虚函数通常不是一个好的做法,原因主要涉及到对象的构造过程和多态行为的安全性。这里有几个关键点解释了为什么在构造函数中调用虚函数可能会导致问题: 1. 对象构造的阶段性 当创建一个派生类的对象时,对象的构造是按顺序进行的,从基类开始,然后是派生类。在基类的构造函数执行期间,派生类的部分还没有被构造。这意味着,如果在基类构造函数中调用了一个虚函数,该虚函数如果被派生类重写,那么调用的将是基类版本的实现,即使这个调用发生在派生类的构造函数的上下文中。这是因为此时对象的动态类型仍然是基类,而非派生类,C++的多态性在此时还未完全建立。 2. 安全性和一致性 如果虚函数在基类构造期间被调用,并且该虚函数被派生类重写,由于派生类的构造器尚未执行,任何由派生类添加的成员变量都还未被初始化。如果重写的虚函数依赖于这些成员变量,那么它可能会访问未初始化的变量,导致未定义行为或程序错误。 3. 设计上的限制 在构造函数中调用虚函数强加了设计上的限制,即要求派生类在其虚函数实现中只能使用那些在基类构造期间就已经初始化完毕的资源。这限制了派生类设计的灵活性,使得派生类的开发者需要对基类的内部实现细节有深入的了解。 替代方案 为了避免这些问题,通常建议不在构造函数(以及析构函数)中调用虚函数。作为替代,可以考虑以下设计策略: 延迟初始化:通过在构造函数之后显式调用初始化函数来进行操作,这可以确保对象完全构造后再进行多态行为的相关操作。 非虚成员函数调用:在构造函数中调用一个非虚成员函数,该非虚成员函数然后再调用一个虚函数。这样做同样需要小心,以确保不违反上述原则。 设计模式:考虑使用工厂模式或者建造者模式来创建对象,这样可以在对象完全构造好之后再执行需要多态行为的操作。 通过遵循这些指导原则,可以避免在对象构造期间因调用虚函数而可能引入的问题,使得代码更加安全和健壮。 上面都是GPT,下面是人话 对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。也就是说,虚机制在构造函数中不工作。 这种行为有两个理由: 第一个理由是概念上的。 在概念上,构造函数的工作是生成一个对象。在任何构造函数中,可能只是部分形成对象——我们只能知道基类已被初始化,但并不能知道哪个类是从这个基类继承来的。然而,虚函数在继承层次上是“向前”和“向外”进行调用。它可以调用在派生类中的函数。如果我们在构造函数中也这样做,那么我们所调用的函数可能操作还没有被初始化的成员,这将导致灾难发生。 第二个理由是机械上的。 当一个构造函数被调用时,它做的首要的事情之一就是初始化它的VPTR。然而,它只能知道它属于“当前”类——即构造函数所在的类。于是它完全不知道这个对象是否是基于其它类。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE。但如果接着还有一个更晚派生类的构造函数被调用,那么这个构造函数又将设置VPTR指向它的VTABLE,以此类推,直到最后的构造函数结束。VRTP的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是按照从基类到最晚派生类的顺序的另一个理由。 但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后派生的VTABLE(所有构造函数被调用后才会有最后派生的VTABLE)。另外,许多编译器认识到,如果在构造函数中进行虚函数调用,应该使用早绑定,因为它们知道晚绑定将只对本地函数产生调用。无论哪种情况,在构造函数中调用虚函数都不能得到预期的结果。 静态函数可以是虚函数么?为什么? static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。所以无法访问vptr. 进而不能访问虚函数表。 析构函数可以是纯虚函数么? 是的,析构函数可以被声明为纯虚函数(pure virtual destructor)在C++中。这通常用于定义抽象基类(Abstract Base Class,ABC),即这样的类不打算被实例化,而是作为派生类的基础。声明纯虚析构函数的目的是确保基类有一个虚析构函数,允许通过基类指针正确地删除派生类的对象。 纯虚析构函数 当你将析构函数声明为纯虚函数时,你表明了类是抽象的,不能直接实例化,并且你期望从它派生出新的类。但与其他纯虚函数不同,纯虚析构函数必须提供一个定义,因为派生类的析构过程中会调用它。 示例 1class AbstractBase {2public:3 virtual ~AbstractBase() = 0; // 纯虚析构函数4};56AbstractBase::~AbstractBase() {7 // 必须提供实现,即使是空实现8} 在这个示例中,AbstractBase有一个纯虚析构函数,使得AbstractBase成为一个抽象基类。尽管析构函数是纯虚的,我们仍然提供了它的定义,这是必须的。 注意事项 即使类中有纯虚析构函数,这个类也需要提供析构函数的定义。这是因为当派生类被销毁时,析构函数的调用会沿着继承链向上进行,最终会调用到基类的析构函数。 如果一个类有纯虚析构函数,它可以没有其他的纯虚函数。但是,这样的类仍然是抽象类,不能直接实例化。 纯虚析构函数的存在不影响派生类的析构函数的实现。派生类应该提供自己的析构函数来确保正确的资源清理。派生类的析构函数会自动调用基类的析构函数。 声明纯虚析构函数是一种表明类是为了被继承而设计,并且不应该直接实例化的方式。同时,它确保了派生类对象通过基类指针被正确销毁的能力,这对于避免内存泄漏等问题至关重要。 定义一个A* pa= new A[5]; delete pa; 类A的构造函数和析构函数分别执行了几次? 构造函数执行了5次,每new一个对象都会调用一个构造函数,析构函数只调用一次,如果调用delete[] pa 析构函数才会调用5次。 reserve和resize的区别是什么? 智能指针有几种?分别介绍一下他们的底层实现? 为什么需要智能指针 你刚才说到循环引用,那你口述一个循环引用的实例。在你说的这个实例中,那你怎么用weak_ptr来解决呢? 说一下lambda表达式的底层实现 C++中的lambda表达式在底层实现上可以被视为一个匿名类(或称为闭包类型)的实例。当编译器遇到lambda表达式时,它会生成一个与lambda行为相匹配的唯一的类类型,这个类会重载函数调用操作符operator(),使得该类的实例(对象)可以像函数那样被调用。这个自动生成的类通常被称为闭包类型,而生成的对象称为闭包对象。 Lambda表达式的组成 一个lambda表达式通常包含三个部分:捕获列表(capture list)、参数列表(parameter list)以及函数体(function body)。例如: 1auto lambda = [capture](parameters) -> return_type { body }; 底层实现细节 闭包类型的生成:对于每个lambda表达式,编译器生成一个唯一的未命名(匿名)类。这个类实现了operator(),使得其对象可以通过函数调用语法使用。 捕获列表的处理: 值捕获(By Value):被捕获的变量在闭包对象创建时通过拷贝初始化。每个被捕获的变量都会成为闭包类型的一个数据成员。 引用捕获(By Reference):通过引用捕获的变量不会被拷贝,相反,闭包对象存储的是变量的引用。 隐式捕获:编译器根据lambda表达式体中使用的外部变量自动生成捕获列表。 构造函数:闭包类型会有一个构造函数,用于初始化闭包对象中的数据成员。这个构造函数根据捕获列表来定义。 operator()的实现:闭包类型重载的operator()会包含lambda表达式的函数体作为其实现。这个重载允许闭包对象像函数那样被调用。 例子 考虑下面的lambda表达式: 1int x = 4;2auto lambda = [x](int y) { return x + y; }; 编译器会为这个lambda生成类似于以下伪代码的闭包类型: 1class UniqueClosure {2 int captured_x;3public:4 UniqueClosure(int x) : captured_x(x) {}5 int operator()(int y) const {6 return captured_x + y;7 }8}; 然后,lambda变量实际上是UniqueClosure类型的一个实例: 1UniqueClosure lambda(x); 总结 C++中的lambda表达式底层通过生成一个特殊的闭包类型来实现,这个类型重载了函数调用操作符并存储了所有必要的捕获变量,使得lambda表达式既可以捕获周围的上下文,也可以像普通函数那样被调用。这个过程完全由编译器自动完成,对程序员透明。 深拷贝和浅拷贝的区别 讲一下四种类型转换符? 进程间通信 线程间通信 协程是什么? 用户态和内核态区别 死锁产生的原因及四个必要条件 系统调用是什么,有哪些系统调用 孤儿进程、僵尸进程 单核机器上写多线程程序,是否需要考虑加锁,为什么? 静态链接和动态链接 硬链接和软链接 为什么是成倍扩容,而不是等差扩容 :: 范围解析运算符有哪些用法 inline关键字 union关键字 using关键字用法 friend关键字 volatile关键字 为避免对同一对象进行赋值操作,this指针,用法 在C++中,this指针是一个特殊的指针,它指向当前对象。使用this指针的一个常见场景是在成员函数中,特别是在赋值操作符operator=的重载函数中,来避免自赋值的情况。自赋值发生在一个对象被赋值给自身的时候,如果不进行检测,这可能导致意想不到的错误,尤其是在涉及动态内存分配的时候。 下面是如何使用this指针来避免自赋值的一个例子: 1class MyClass { 2public: 3 MyClass& operator=(const MyClass& other) { 4 // 检查自赋值 5 if (this == &other) { 6 // 是自赋值,直接返回*this 7 return *this; 8 } 910 // 不是自赋值,执行赋值操作11 // 例如,释放当前对象持有的资源12 //delete[] this->data;1314 // 然后复制other的数据到当前对象15 //this->data = new int[...];16 //std::copy(...);1718 return *this; // 返回当前对象的引用19 }20}; 自赋值检查的工作原理 比较this和&other:通过比较当前对象的this指针和赋值右侧对象的地址&other,可以检查赋值操作是否试图将对象赋值给自身。 提前返回:如果检测到自赋值,函数直接返回*this,避免执行无意义或可能危险的资源释放和重新分配操作。 正常赋值流程:如果不是自赋值,函数则继续执行正常的赋值逻辑,如释放旧资源,分配新资源,复制数据等。 为什么要避免自赋值 在某些情况下,如果不检查自赋值,赋值操作可能会破坏对象的状态。例如,如果赋值操作首先释放了对象持有的资源,然后试图从自身复制这些资源,那么在释放资源之后,对象将尝试从已经被释放的资源复制数据,这将导致未定义行为。 通过在赋值操作中添加自赋值检查,可以保证即使在自赋值的情况下,对象的状态也能保持一致和安全。这是一个良好的编程实践,特别是在处理涉及动态内存管理的复杂类时。 什么是copy on write 模板特化与偏特化 在C++模板编程中,模板特化和偏特化是两种重要的技术,它们允许程序员为模板提供特定类型的实现。虽然它们的目的相似,即为某些特定情况提供定制的模板代码,但它们在使用和概念上有着明显的区别。 模板特化(Template Specialization) 模板特化是指为模板的一个具体类型或类型组合提供一个特定的实现。当模板参数满足特化条件时,编译器会使用特化的实现而不是通用模板。模板特化可以应用于函数模板和类模板。 全特化(Full Specialization) 当为模板的所有参数提供特定的类型时,称之为全特化。 类模板全特化示例: 1template<typename T>2class MyTemplate { /* 通用实现 */ };34// 全特化为int类型5template<>6class MyTemplate<int> { /* 特化实现 */ }; 函数模板全特化示例: 1template<typename T>2void myFunction(T value) { /* 通用实现 */ }34// 全特化为int类型5template<>6void myFunction<int>(int value) { /* 特化实现 */ } 模板偏特化(Partial Specialization) 模板偏特化是类模板的一种特殊形式,它允许为模板的一部分参数提供特定的类型,而不是全部参数。注意,函数模板不支持偏特化,偏特化仅适用于类模板。 类模板偏特化示例: 假设我们有一个模板用于处理指针,我们可以为指针类型提供一个偏特化版本: 1template<typename T>2class MyTemplate { /* 通用实现 */ };34// 偏特化为指针类型5template<typename T>6class MyTemplate<T*> { /* 指针类型的特化实现 */ }; 在这个例子中,当MyTemplate的模板参数是任何类型的指针时,会使用偏特化版本。 区别总结 全特化:为模板提供一个针对特定类型或类型组合的完全定制的实现。适用于函数模板和类模板。 偏特化:只针对类模板,允许为模板的一部分参数提供特定的类型。它是对模板的进一步泛化,用于处理更具体的情况,但不像全特化那样针对全部参数。 模板特化和偏特化是C++模板编程中强大的特性,允许开发者根据不同的类型参数定制模板的行为,提高了代码的灵活性和可重用性。 为什么函数模板不支持偏特化 写一个宏版本的MIN auto作为返回值和模板一起怎么用 TCP和UDP区别 C++ 类对象的初始化顺序,有多重继承情况下的顺序 如果三次握手时候每次握手信息对方没收到会怎么样,简答 AVL 和红黑树的差别 数据库事务的特点 虚拟地址如何转为物理地址 说一下滑动窗口,如果接收方滑动窗口满了,发送方会怎么做 页面置换算法 既然有了malloc/free,C++中为什么还需要new/delete呢? delete[]怎么实现 计算类的sizeof 解决哈希冲突的方式? 结构体内存对齐方式和为什么要进行内存对齐? 调试程序的方法 遇到coredump要怎么调试 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)? 有三种情况是必须使用成员初始化列表进行初始化 常量成员的初始化,因为常量成员只能初始化不能赋值引用类型没有默认构造函数的对象必须使用成员初始化列表的方式进行初始化 见:C++ 初始化列表 C++的调用惯例(简单一点C++函数调用的压栈过程) 一个函数或者可执行文件的生成过程或者编译过程是怎样的 定义和声明的区别 被free回收的内存是立即返还给操作系统吗?为什么 引用作为函数参数以及返回值的好处 建立TCP服务器的各个系统调用 TCP和UDP相关的协议与端口号 http的请求方法有哪些?get和post的区别。 TCP三次握手时的第一次的seq序号是怎样产生的 一个机器能够使用的端口号上限是多少,为什么?可以改变吗?那如果想要用的端口超过这个限制怎么办? 对称密码和非对称密码体系 数字证书是什么? 服务器出现大量close_wait的连接的原因以及解决方法 消息摘要算法列举一下,介绍MD5算法,为什么MD5是不可逆的,有什么办法可以加强消息摘要算法的安全性让它不那么容易被破解呢? 介绍一下ping的过程,分别用到了哪些协议 TCP/IP的粘包与避免介绍一下 一个ip配置多个域名,靠什么识别? DNS的工作过程和原理 ARP协议 关系型和非关系型数据库的区别 说一下 MySQL 执行一条查询语句的内部执行过程? 数据库的索引类型有哪些 说一下事务是怎么实现的 MySQL怎么建立索引,怎么建立主键索引,怎么删除索引? 索引的优缺点,什么时候使用索引,什么时候不能使用索引 索引的底层实现 B树和B+树的区别 Mysql的优化 高频访问 并发优化 索引最左前缀/最左匹配 数据库中事务的ACID 什么是脏读,不可重复读和幻读? 数据库的隔离级别,mysql和Oracle的隔离级别分别是什么 Mysql的表空间方式,各自特点 数据库的范式 乐观锁与悲观锁解释一下 乐观锁与悲观锁是怎么实现的 Linux的I/O模型介绍以及同步异步阻塞非阻塞的区别 文件系统的理解(EXT4,XFS,BTRFS) 什么是IO多路复用 IO复用的三种方法(select,poll,epoll)深入理解,包括三者区别,内部原理实现? Epoll的ET模式和LT模式(ET的非阻塞) 文件权限怎么看(rwx) 文件的三种时间(mtime, atime,ctime),分别在什么时候会改变 Linux监控网络带宽的命令,查看特定进程的占用网络资源情况命令 怎么修改一个文件的权限 coredump是什么 怎么才能coredump Linux理论上最多可以创建多少个进程?一个进程可以创建多少线程,和什么有关 冯诺依曼结构有哪几个模块?分别对应现代计算机的哪几个部分? 进程之间的通信方法有哪几种 进程调度方法详细介绍 什么是饥饿 可重入函数是什么 内核空间和用户空间是怎样区分的 同一个进程内的线程会共享什么资源? brk和mmap是什么 什么是字节序?怎么判断是大端还是小端?有什么用? 写单例模式,线程安全版本 写三个线程交替打印ABC 二维码登录的实现过程 不使用临时变量实现swap函数 找到数组里第k大的数字 Top K问题 B树 B+树 怎么写sql取表的前1000行数据 布隆过滤器 实现一个队列,并且使它支持多线程 100层楼,只有2个鸡蛋,想要判断出那一层刚好让鸡蛋碎掉,给出策略 毒药问题,1000瓶水,其中有一瓶可以无限稀释的毒药,要快速找出哪一瓶有毒,需要几只小白鼠 先手必胜策略问题:100本书,每次能够拿1-5本,怎么拿能保证最后一次是你拿 放n只蚂蚁在一条树枝上,蚂蚁与蚂蚁之间碰到就各自往反方向走,问总距离或者时间。 瓶子换饮料问题:1000瓶饮料,3个空瓶子能够换1瓶饮料,问最多能喝几瓶 在24小时里面时针分针秒针可以重合几次 生成随机数问题:给定生成1到5的随机数Rand5(),如何得到生成1到7的随机数函数Rand7()? 蓄水池采样算法 赛马:有25匹马,每场比赛只能赛5匹,至少要赛多少场才能找到最快的3匹马? 烧 香/绳子/其他 确定时间问题:有两根不均匀的香,燃烧完都需要一个小时,问怎么确定15分钟的时长? 掰巧克力问题 NM块巧克力,每次掰一块的一行或一列,掰成11的巧克力需要多少次?(1000个人参加辩论赛,1V1,输了就退出,需要安排多少场比赛)(快手提前批) 介绍一下Hadoop 说一下MapReduce的运行机制 消息队列是什么 为什么kafka吞吐量高?/介绍一下零拷贝 spark是什么 kafka如何选举leader 斐波那契数列第n项,O(log n)求出 react的特点以及数据流向,diff算法主要做了啥? 内存为什么要分段?分段就只是为了方便程序员吗? https可以防止DNS劫持吗? 设计一款聊天软件,你打算采用什么方案? cpu3级缓存 快表(TLB)是什么 http无状态优劣—cookie 什么是局部性原理 rabbitMQ怎么用的 redis怎么用的 数据库中哪些地方用了链表 HTTP了解吗,怎么样不用框架用原生语言解析请求的Json数据 怎么样用数据结构和算法写一个定时器 tcp是怎么样超时重传的 MQTT webserver 项目中如果所有线程都在工作,收到请求怎么做? python routine 自己写一个协程控制器,包括添加协程,暂停等功能 写个生产者消费者模型 线程安全的数据结构 线程池的创建方法 C++反射是什么 https加密协商出来的密钥的类型是什么 海量数据问题 https://wangpengcheng.github.io/2019/12/17/hailiangshuju_problems/ C++问题大全 https://zhuanlan.zhihu.com/p/603532168

2024/4/8
articleCard.readMore

二分问题的统一解决方案

思路来自 https://www.youtube.com/watch?v=QvcM99na30k。本文为学习记录+结合自己的思考修改后得出的框架。 引入 在算法题中有时候会遇到这种场景,对于一个问题,它的答案是一个整数且一定取在一个范围内,例如答案只可能是$[1, 10^9]$中的一个数字。且存在一个条件,使得对于任何小于答案的数字,这个条件都不成立;而对于任何大于等于答案的数字,这个条件都成立。那么这个时候就可以使用二分的方法来找到答案。 框架与求解 下面用严格的语言来表达这类问题(我称为二分问题): 存在一个函数 check , 它接受一个整数 i 作为输入, 且 check(i) 只可能等于 True or False . 并且,存在一个数 $k$ ,使得对于任何小于 $k$ 的整数 i ,都有 check(i)==False ,而对于任何大于等于$k$的整数 i ,有 check(i)==True . 我们现在有函数 check ,需要找到这个整数$k$. 且我们知道 $k$ 在 $[a,b]$ 区间内。 我们接下来提出一个框架来解决这个问题。 首先,我们从获取信息的角度思考这个问题。在我们去实际跑一次 check 函数之前,我们是没有获取任何信息的。这个时候我们可以认为所有的 $[a,b]$ 之间的整数都是未知状态。 而我们在运行了 check(i) 之后,由于问题的性质,我们获取到的信息不只是 check(i) 本身为真还是假。而是可以知道一个范围内的所有整数都满足或者不满足条件。 例如我运行了 check(i) ,得到 check(i)==False. 那么我就知道对于所有的 $[a,i]$ 闭区间内的整数,它们check的结果一定也都是 False . 而如果我运行了 check(i) ,得到 check(i)==True. 那么我就知道对于所有的 $[i,b]$ 闭区间内的整数,它们check的结果一定也都是 True . 定义一个闭区间 $[l,r]$ 为求解区间,使得它满足以下3条性质: $[l,r]$ 中全是我们未知它check的结果是True还是False的整数 对于任何大于 $r$ 的整数,check的结果一定是True 对于任何小于 $l$ 的整数,check的结果一定是False 接下来我们把求解区间初始化为 $[a,b]$. 因为一开始,$[a,b]$ 中全是我们未知的。 相当于我们定义了两个变量l,r, 且初始化它们为l<-a, r<-b. 然后定义变量mid, mid <- l + (r-l)/2 . mid也就是求解区间的中点。 然后计算check(mid). 如果check(mid)==True,说明 $[mid, r]$ 都是 True. 因此 $[mid, r]$ 从未知变为已知。需要让新的r变为 mid - 1. 也就是执行 r <- (mid - 1) .这个过程中维护了3条性质始终成立。 如果check(mid)==False,说明 $[l, mid]$ 都是 False. 因此 $[l, mid]$ 从未知变为已知。需要让新的l变为 mid + 1. 也就是执行 l <- (mid + 1) .这个过程中维护了3条性质始终成立。 然后重复执行 mid <- l + (r-l)/2 和上述操作。 直到 $r < l$ 为止。此时求解区间为空。也就是未知的区间为空。说明 $[a,b]$ 内的每一个整数的check情况已经全部已知。我们的目标就是把未知区间变为空。且保持3条性质均始终成立。 在未知区间为空之后,$r$ 一定等于 $l-1$. 此时,r就是最后一个False所在位置,而l就是第一个True所在位置。然后根据题目要求哪一个就返回哪一个就行。 举例子说明(可以先看这个部分) 例如对于一个check函数,假设我们把它应用在1,2,3,4,5,6,7,8,9上面,得到的结果分别是 $$[F,F,F,F,T,T,T,T,T]$$ 那么取 l <- 0, r <- 8. 计算得到 mid <- 4. 此时 check(mid) == check(4) == T. 说明 [4,8] 区间内的每一个整数check之后都是T. 因此它们从未知变成已知。 我们执行 mid <- (r - 1). 得到新的未知区间为 [l,r] == [0,3]. 计算 mid <- (l+r)/2, 得到新的 mid == 1. 此时 check(mid) == F. 说明 [0,1] 区间均为F. 因此执行 mid <- (l + 1). 新的未知区间为 [l,r] == [2,3]. 计算 mid <- (l+r)/2, 得到新的 mid == 2. 此时 check(mid) == F. 说明 [0,2] 区间均为F. 因此执行 mid <- (l + 1). 新的未知区间为 [l,r] == [3,3]. 计算 mid <- (l+r)/2, 得到新的 mid == 3. 此时 check(mid) == F. 说明 [0,3] 区间均为F. 因此执行 mid <- (l + 1). 新的未知区间为 [l,r] == [4,3] == Empty. 计算结束。 此时 l == 4, r == 3. l就是第一个T所在的位置。 r就是最后一个F所在的位置。

2024/4/4
articleCard.readMore

Go 并发与并行的区别

2024/4/4
articleCard.readMore

Cpp 笔记 新系列1

2024/4/3
articleCard.readMore

Go 学习笔记 1

ChatGPT摘抄 Go和C++的区别 Go语言(通常称为Golang)和C++在多个方面有显著的区别,包括它们的设计哲学、类型系统、内存管理、并发模型等方面。下面是一些主要的区别: 设计哲学: Go 设计简洁,以提高开发效率和易用性为目标。它旨在使得编写高性能、可靠、易于维护的代码更加直接和简单。 C++ 设计为一种通用编程语言,强调性能和灵活性。它支持多种编程范式(如过程式、面向对象、泛型编程),但这也使得它变得相对复杂。 类型系统: Go 有一个静态类型系统,但它的类型推导使得代码更简洁。它支持接口,但不支持类和继承,而是通过组合和接口实现多态和代码复用。 C++ 也是静态类型的,但它支持更复杂的类型系统,包括类、继承、模板等。这提供了强大的灵活性,但也增加了复杂性。 内存管理: Go 提供自动垃圾收集,减少了内存泄漏的风险,简化了内存管理。 C++ 需要程序员手动管理内存,虽然提供了智能指针等现代特性来帮助管理,但管理不当仍可能导致内存泄漏和其他问题。 并发模型: Go 的并发模型是其核心特性之一,基于“goroutines”(轻量级线程)和通道。这使得编写并发程序变得更简单和安全。 C++ 支持多线程,但并发并不是语言核心的一部分。C++11及以后的版本引入了一些并发编程的标准库支持,但使用起来比Go更复杂。 标准库和生态系统: Go 有一个丰富的标准库,尤其是对于网络编程和并发编程。Go的生态系统随着语言的普及而快速增长,特别是在云服务和微服务开发中。 C++ 拥有庞大的第三方库和工具集,适用于各种编程任务,但标准库相对较小。C++在高性能计算、游戏开发、系统编程等领域有着悠久和广泛的应用。 编译和执行: Go 编译器倾向于生成单个静态链接的二进制文件,简化了部署和分发。 C++ 通常需要更复杂的构建系统,尤其是对于依赖于多个库和平台的项目。 总之,Go和C++在设计上有根本的不同。Go旨在简化并发编程,提高开发效率,适合快速开发大型分布式系统。而C++提供了更多的控制和灵活性,适合那些需要极致性能优化和复杂系统设计的场景。选择哪种语言取决于项目需求、团队熟悉度和特定任务的需求。 讲一下go语言中的协程 Go语言中的协程(goroutine)是Go语言并发设计的核心。goroutine 类似于线程,但在执行和资源消耗方面更为高效。它们由Go运行时(runtime)管理,而不是直接由操作系统管理。这样做的好处是goroutine的创建和销毁的成本比操作系统线程低得多,允许在同一程序中并发运行成千上万的goroutine。 创建goroutine 在Go语言中,创建一个goroutine非常简单,只需要在函数调用前加上go关键字。例如: 1go funcName(params) 这会启动一个新的goroutine并在这个新的goroutine中调用funcName函数,params是传递给funcName函数的参数。 特点 轻量级:每个goroutine的堆栈大小在初始化时很小,根据需要可以动态地增长和缩减。这种设计让成千上万的goroutine并发运行成为可能。 不需要显式创建和销毁:goroutine由Go的运行时环境管理,程序员只需要创建它们,无需关心销毁和回收。 使用通道(channels)进行通信:goroutine之间的同步通常通过channels进行,channels是Go语言中的一种类型安全的消息传递机制。使用channels,可以很容易地在goroutine之间传递数据和信号。 示例 下面的示例展示了如何使用goroutine和channels进行并发编程: 1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8// 函数打印传入的消息 9func printMessage(message string) {10 for i := 0; i < 5; i++ {11 fmt.Println(message)12 time.Sleep(time.Millisecond * 100)13 }14}1516func main() {17 // 创建一个新的goroutine运行printMessage函数18 go printMessage("hello")1920 // 主goroutine中也运行printMessage函数21 printMessage("world")2223 // 等待按键结束程序,避免主goroutine提前结束24 fmt.Scanln()25 fmt.Println("done")26} 在这个例子中,printMessage函数将在两个不同的goroutine中并发执行:一个是通过go printMessage("hello")创建的goroutine,另一个是主goroutine。这两个goroutine将并发地运行,打印出交替的"hello"和"world"消息。 总之,goroutine是实现并发编程的强大工具,它使得编写高性能并发程序在Go语言中变得既简单又高效。 协程和线程的区别是什么 协程(Coroutines)和线程(Threads)都是程序执行流的基本单位,它们之间有一些关键的区别和优缺点。理解这些区别有助于选择最适合特定应用需求的并发模型。 1. 管理方式 线程:通常由操作系统管理。操作系统负责线程的调度和管理,线程切换涉及到上下文切换,包括保存当前线程的状态并恢复另一个线程的状态,这是一个相对昂贵的操作。 协程:由程序和运行时(runtime)管理,不直接依赖操作系统的线程管理。协程上下文切换的成本通常比线程低得多,因为这种切换是在用户态完成的,避免了内核态与用户态之间的切换开销。 2. 资源消耗 线程:每个线程都有自己的堆栈和局部变量,占用的内存量相对较大。线程的数量受到系统资源的限制。 协程:通常非常轻量,拥有很小的堆栈和低资源消耗。因此,在同一程序中可以同时运行成千上万个协程。 3. 并发模型 线程:多线程编程模型可以实现真正的并行执行(在多核处理器上),但线程间的同步和通信(如使用锁、信号量等)可能导致复杂的竞态条件和死锁问题。 协程:通常在单线程中运行,实现并发而非并行。协程之间的通信和同步通过使用消息传递(如Go语言的channels)或事件循环(如Python的asyncio)等机制,这些机制旨在避免传统的锁和条件变量带来的问题。 4. 控制流 线程:操作系统控制线程的调度,程序员通常不能精确控制执行流的切换点。 协程:提供了更精细的控制能力,允许程序员确定何时以及如何切换任务,通过这种方式,可以更容易地编写非阻塞代码和管理并发。 5. 使用场景 线程:适用于需要实现真正并行任务处理的场景,尤其是在计算密集型应用中。 协程:适合高并发、IO密集型应用,如网络服务器、异步处理任务等,能有效提高资源的利用率和程序的响应性。 总结来说,线程是操作系统层面的并发执行单元,资源开销较大,适合于并行处理;而协程是更轻量级的用户态线程,它们提供了更高的并发性和更灵活的控制方式,适合于构建高并发的应用程序。正确地选择使用线程还是协程,取决于具体的应用场景和性能需求。 假设我在go语言中开启了10个协程,它们会运行在几个线程上,几个CPU上 在Go语言中,协程(goroutine)的运行不是直接映射到操作系统线程上的,而是由Go运行时(runtime)进行管理。Go运行时使用了M:N调度模型(多个goroutines被多路复用到较少的操作系统线程上),这意味着数千个goroutines可能只用到很少数量的操作系统线程。具体到你的问题,让我们看看这个模型的两个主要组件以及如何影响goroutines的运行: 1. GOMAXPROCS GOMAXPROCS是一个环境变量,用于控制Go程序运行时可以使用的CPU核心数。默认情况下,它的值设为了你的机器上的逻辑CPU核心数。你可以通过runtime.GOMAXPROCS()函数查询或设置这个值。GOMAXPROCS决定了有多少个操作系统线程可以同时执行Go代码。 2. 调度器 Go的调度器是基于M:N模型实现的,其中: M 代表操作系统线程(machine)。 N 代表goroutines。 调度器的任务是将N个goroutines分配给M个操作系统线程上执行。这意味着,即使你有数千个goroutines,Go运行时也可能只使用GOMAXPROCS指定数量的操作系统线程来执行这些goroutines。 运行在多少个线程和CPU上? 假设你开启了10个goroutines: 线程:这些goroutines的实际运行可能全部在一个单独的操作系统线程上,也可能分布在多个操作系统线程上,具体取决于GOMAXPROCS的设置和Go调度器的决定。如果GOMAXPROCS的值大于1(默认情况下通常如此),那么这些goroutines有可能被调度到不同的操作系统线程上执行。 CPU:这些goroutines实际上在GOMAXPROCS设定的CPU核心上并发运行。如果GOMAXPROCS大于1,goroutines可以在多个CPU核心上并行执行。 因此,如果GOMAXPROCS设置为大于1的值,你的10个goroutines可能会在多个线程和多个CPU上运行,具体分配由Go运行时的调度器动态管理。但要注意,goroutine到线程的具体映射以及调度行为是由Go运行时内部决定的,可能会随着Go版本和具体的运行情况而有所不同。 go的通道是无序的,这样怎么能做好并发呢 在Go语言中,说通道(channels)是“无序的”可能会有点误导。实际上,通道本身维持着发送和接收操作的顺序:如果你按顺序发送了值A然后是值B到同一个通道,接收方也将首先接收到A,然后是B,这意味着在单个通道上,发送和接收是有序的。然而,当涉及到从多个并发发送者接收时,哪个发送者首先将消息送达到通道是不确定的,这可能导致看似“无序”的行为。 当你在处理并发时,确保程序的正确性通常依赖于同步和通信机制,而Go通过通道和goroutines提供了强大的工具来帮助你做到这一点。这里有一些策略来确保你在并发程序中使用通道时能够正确地同步和顺序操作: 使用无缓冲通道进行同步:无缓冲通道保证了发送操作会阻塞,直到接收操作发生,这可以用作goroutines之间的同步点。 使用有缓冲通道控制并发:有缓冲通道允许在阻塞之前发送多个值。它们可以用来限制并发级别,比如通过控制同时运行的goroutines的数量。 关闭通道来广播消息:关闭一个通道会通知所有的接收者没有更多的值会被发送到这个通道。这是一种广播机制,可以用来通知多个goroutine某个条件已经成立,比如终止。 选择语句:select 语句允许你同时等待多个通道操作,并根据哪个通道准备就绪来执行相应的代码块。这可以用来处理来自多个来源的输入,同时保持逻辑上的清晰和顺序。 顺序控制:当需要精确控制多个goroutine的执行顺序时,可以设计一种协议或使用一系列通道来顺序地进行操作,确保并发操作的正确顺序。 通过这些机制,Go允许你构建复杂的并发程序,同时保持代码的简洁性和清晰度。正确地使用通道和goroutines,你可以有效地解决并发中的同步和顺序问题,从而实现高效且易于理解的并发逻辑。 管道例子探究 1func main() { 2s := []int{7, 2, 8, -9, 4, 0} 3 4c := make(chan int) 5fmt.Println(s[:len(s)/2]) 6fmt.Println(s[len(s)/2:]) 7sum(s[:len(s)/2], c) // 17 8sum(s[len(s)/2:], c) 9x, y := <-c, <-c // 从 c 中接收10fmt.Println(x, y, x+y)11} 这样为什么不对?管道必须和goroutine搭配使用吗? 你的新代码片段中,sum 函数不是在goroutines中执行的,而是直接顺序执行。问题在于,在你尝试向通道 c 发送数据时(使用 c <- sum 语法),因为 c 是一个无缓冲的通道,发送操作会阻塞直到另一方准备好接收数据。在这种情况下,因为你的发送操作 (c <- sum) 发生在主goroutine中,而且没有其他并行的goroutine去接收这些值,这将导致死锁:主goroutine在等待自己向通道发送数据的操作完成,但这永远不会发生,因为没有其他并发操作去从通道中接收数据。 通道确实设计为在goroutines之间进行通信的一种方式,它们非常适合用于并发编程。在单个goroutine内使用无缓冲通道进行发送和接收操作通常会导致死锁,因为无缓冲通道要求发送和接收操作几乎同时发生。 简而言之,你的最后一个示例不正确,原因如下: 死锁:由于 sum 函数调用并非在goroutines中执行,它们将顺序执行。第一个 sum 调用试图向通道 c 发送一个值,但会阻塞,因为没有其他goroutine在接收端等待这个值。程序将在第一个 sum 函数调用时阻塞,导致死锁。 通道与goroutine搭配使用:虽然技术上不是必须将通道与goroutines搭配使用,但在实践中,这是避免死锁和实现有效并发程序的常见模式。使用无缓冲通道时,发送操作会阻塞,直到接收操作开始,反之亦然。这种机制非常适合在不同goroutines之间同步数据访问和传递消息。 为了使你的程序按预期工作,你应该在goroutines中调用 sum 函数,并通过通道交换数据,正如之前正确的版本那样。这样,两个 sum 操作可以并发执行,主goroutine将等待并从通道中接收它们的结果,从而避免死锁并正确计算总和: 1go sum(s[:len(s)/2], c) // 在goroutine中执行2go sum(s[len(s)/2:], c) // 在另一个goroutine中执行3x, y := <-c, <-c // 从通道c中接收 通过这种方式,每个 sum 调用都在自己的goroutine中执行,并且能够独立地完成其向通道的发送操作。这避免了死锁,使主goroutine能够从通道接收两个结果并正确计算总和。 1package main 2 3import "fmt" 4 5func main() { 6ch := make(chan int, 2) 7ch <- 1 8ch <- 2 9fmt.Println(<-ch)10fmt.Println(<-ch)11} 为什么这个又对了?这不是只有一个goroutine吗? 你的最新示例之所以能够正确运行,是因为它使用了一个有缓冲的通道。与无缓冲通道不同,有缓冲通道允许在阻塞发送者或接收者之前存储一个固定数量的值。具体到你的例子中,通过 make(chan int, 2) 创建了一个可以存储两个整数的缓冲通道。这意味着你可以在没有任何接收者准备好接收之前,向通道发送(或“缓冲”)两个值。 这段代码中的关键点是: ch := make(chan int, 2) 创建了一个有缓冲的通道,缓冲大小为2。这允许通道存储最多两个整数,直到它们被接收。 ch <- 1 和 ch <- 2 这两行向通道发送两个整数。由于通道是有缓冲的,并且缓冲未满,这两个发送操作不会阻塞。这两个值被存储在通道的缓冲区中。 fmt.Println(<-ch) 和 fmt.Println(<-ch) 这两行从通道中接收并打印这两个值。由于这两个值已经在通道的缓冲区中等待,接收操作立即发生,没有阻塞。 在这个例子中,即使所有操作都在单个goroutine(主goroutine)中进行,程序也能正常运行,没有发生死锁。这是因为有缓冲通道提供了足够的空间来存储发送到通道中的值,直到它们被接收,从而避免了在发送操作时必须立即有一个接收者准备好接收值的需求。 有缓冲通道是并发Go程序中一个非常有用的工具,允许goroutines在某种程度上独立操作,而不必在每次通信时都完全同步。然而,设计有缓冲通道的程序时需要谨慎,以避免逻辑错误或死锁,特别是当缓冲区满时或在更复杂的通信模式中。 Go支持垃圾回收吗? 是的,Go语言支持垃圾回收(Garbage Collection,简称GC)。Go的垃圾回收器是一个实现了自动内存管理的程序部分,它负责检测并自动回收不再被程序使用的内存空间,从而防止内存泄漏。这意味着在Go中,开发者不需要(也不能)手动管理内存的分配和回收,大大简化了编程工作,减少了内存泄漏和其他与手动内存管理相关的错误。 Go的垃圾回收器设计为“非阻塞式”的,意味着它尽量减少对程序执行的干扰。从Go 1.5版本开始,引入了并发的、三色标记清除算法,该算法允许垃圾收集过程在大部分时间与程序的其他部分并发执行,减少了垃圾收集对程序性能的影响。尽管如此,垃圾收集过程仍然会引入一定的延迟,这在极少数延迟敏感的应用中可能成为一个考虑因素。 Go的运行时系统还包括其他几个与性能和资源管理相关的特性,如goroutine的调度和网络轮询器,它们一起确保了Go程序的高性能和高效的资源使用。开发者可以使用内置的分析工具(如pprof)来监视和优化程序的内存使用,以及诊断性能问题。 如果Go 代码会被预先编译成原生的机器码,那么它在运行的时候怎么和runtime结合呢 Go语言确实编译成原生机器码,这意味着Go程序在执行时不需要依赖于虚拟机,它们直接由操作系统调度。尽管如此,Go的执行模型和运行时(runtime)系统包括了许多复杂的功能,如垃圾回收(GC)、goroutine调度、通道通信、反射以及更多。这些功能的实现是预先集成在每个Go程序中的,部分原因是Go的编译器和链接器在构建最终的可执行文件时会自动包含运行时。 这里是Go运行时和编译后的机器码结合的一些关键方式: 运行时包含:当Go程序被编译时,运行时代码被静态链接到最终的二进制中。这包括了垃圾回收器、goroutine调度器等核心功能的实现。因此,即使Go代码被编译成了原生机器码,运行时的功能也被内嵌于程序中,随程序一起运行。 初始化运行时:在Go程序启动时,它的入口点不是 main.main 函数。实际上,程序开始执行于运行时的初始化代码,该代码负责设置运行时环境,包括初始化内存管理器、启动垃圾回收器、创建主goroutine等。只有在这些准备工作完成之后,main.main 函数才会被调用。 goroutine调度:Go语言的并发模型基于goroutines。运行时包含一个调度器,它在用户态模拟多线程环境,允许成千上万的goroutines伪并行地运行在较少的操作系统线程上。编译后的代码中包含与运行时交互的指令,这些指令使得goroutines能被合适地调度和管理。 内置函数调用运行时:一些Go的内置函数,如 make、new,以及用于map、切片和通道操作的函数,实际上是对运行时系统的直接调用。这些函数的实现是在运行时代码中,而非Go的标准库中。 接口和反射:Go的接口和反射机制也依赖于运行时的支持。运行时维护了类型信息,允许程序在运行时查询和操作类型,这是动态语言特性的一个重要基础。 总的来说,虽然Go代码被编译成原生机器码,但通过将运行时的功能静态链接到每个程序、在程序启动时初始化运行时环境、以及在需要时通过编译后的代码调用运行时支持的机制,Go程序能够实现高级的语言特性,同时保持高性能和跨平台的兼容性。 Go语言中变量的生命周期 Go语言中变量的生命周期是由变量能够被程序访问的时间段定义的。理解变量的生命周期对于编写高效和无内存泄漏的程序至关重要。在Go中,变量的生命周期取决于它是如何声明的,主要分为两种情况:在堆(heap)上和在栈(stack)上。 栈上的变量 栈上的变量通常具有短暂的生命周期。当声明一个局部变量时,它通常位于栈上,其生命周期与包含它的函数调用的生命周期相匹配。当函数被调用时,局部变量被创建;当函数返回时,这些局部变量被销毁。这意味着栈上的变量的生命周期是由它们所在的函数的执行时间来决定的。 堆上的变量 堆上的变量有一个更长久的生命周期。当你使用new函数或者复合字面量(如切片、映射、通道等)创建一个变量,而这个变量的引用逃逸到函数外部时,这个变量就会在堆上分配。堆上的变量的生命周期不由单个函数调用控制,而是由是否仍然可达(即程序的其他部分是否还有指向该变量的引用)来决定。只要变量可达,它就会继续存在。当变量变得不可达时,它最终会被垃圾回收器回收。 变量生命周期的决定因素 编译器优化:Go编译器有时会做出决策,将本应在堆上分配的变量改为在栈上分配,如果它能确定变量的生命周期在函数范围内结束。这种优化减少了垃圾回收的负担。 逃逸分析:Go编译器执行逃逸分析来决定变量应该分配在堆还是栈上。如果函数内部创建的变量在函数外部仍然被引用(即“逃逸”到函数外部),则这些变量需要在堆上分配,以保证它们在函数返回后仍然存在。 理解变量的生命周期对于优化Go程序的性能和内存使用非常重要。例如,过度使用堆分配可能会导致频繁的垃圾回收,从而影响程序性能。通过适当的变量作用域控制和利用栈分配,可以提高程序效率。 Go 常量 在 Go 语言中,常量是一个简单值的标识符,在程序运行时,其值不会被修改。常量可以是字符、字符串、布尔或数字类型的值。 定义常量的基本语法如下: 1const name [type] = value const 关键字用于声明常量。 name 是常量的名称。 type 是可选的,指定常量的类型。 value 是常量的值。 示例 1const Pi = 3.14 在这个例子中,Pi 是一个常量,其值为 3.14。 常量的类型 在 Go 中,常量可以是任何基本类型,如整型、浮点型、布尔型、字符串等。如果在声明常量时没有指定类型,Go 语言会根据常量的值推导出其类型。 iota Go 语言的 iota 关键字用于生成一组以递增整数形式表示的常量,通常用于声明一个枚举。iota 在 const 关键字出现时被重置为 0,然后在每声明一个常量时自动递增。 1const (2 a = iota // a == 03 b // b == 14 c // c == 25) 在这个例子中,a、b、c 分别被赋予了递增的整数值 0、1、2。 未类型化常量 在 Go 中,可以声明未类型化的常量,这意味着常量没有明确的类型。未类型化常量提供了更高的精度,并可以根据上下文转换为需要的类型。 使用常量 常量可以用在任何变量可以使用的地方。使用常量可以提高代码的可读性和维护性。 总之,Go 语言中的常量是一个非常有用的特性,它允许你在程序中使用固定的值,而不担心这些值会被改变。通过使用 const 关键字和 iota 生成器,你可以方便地声明和管理常量。 Go 创建新结构体 在 Go 语言中,结构体(struct)是一种将多个不同类型的变量组织到一个名字下的复合数据类型,常用于定义和创建数据实体。创建新结构体涉及两个步骤:定义结构体类型和实例化该类型的变量。 步骤 1:定义结构体类型 定义结构体类型使用 type 和 struct 关键字。结构体类型通常在函数外部定义,以便可以在不同函数间共享。 1type Person struct {2 Name string3 Age int4} 在这个例子中,我们定义了一个名为 Person 的结构体,它有两个字段:Name 和 Age。 步骤 2:实例化结构体 一旦定义了结构体类型,你就可以创建该类型的实例了。有几种不同的方式来实例化结构体。 方法 1:使用 new 关键字 new 关键字可以用来创建一个指向结构体类型的新实例的指针。此方法会分配内存并将结构体的字段初始化为零值。 1p1 := new(Person)2p1.Name = "Alice"3p1.Age = 30 方法 2:使用结构体字面量 结构体字面量允许在一个表达式中创建结构体实例,并同时初始化其字段。 命名字段方式: 1p2 := Person{Name: "Bob", Age: 25} 按字段顺序初始化(不推荐,因为添加或修改字段时易出错): 1p3 := Person{"Charlie", 28} 方法 3:使用指针和结构体字面量 你也可以直接创建一个指向结构体实例的指针,并初始化其字段。 1p4 := &Person{Name: "Diana", Age: 32} 示例 1package main 2 3import "fmt" 4 5type Person struct { 6 Name string 7 Age int 8} 910func main() {11 // 使用 new 关键字12 p1 := new(Person)13 p1.Name = "Alice"14 p1.Age = 301516 // 使用结构体字面量17 p2 := Person{Name: "Bob", Age: 25}1819 // 使用指针和结构体字面量20 p3 := &Person{Name: "Charlie", Age: 28}2122 fmt.Println(*p1)23 fmt.Println(p2)24 fmt.Println(*p3)25} 这个例子展示了不同的方式来创建和初始化结构体 Person 的实例。选择哪种方式取决于具体情况,比如是否需要在初始化时立即设置字段值,或者是否需要操作结构体指针。 Go切片append 在 Go 语言中,切片(slice)是一个动态数组,它提供了一种灵活、强大的方式来管理数据集合。尽管 Go 语言的切片类型没有直接提供 push back 的方法(类似于其他语言中的数组或列表的 append 功能),但可以使用内建的 append 函数来实现向切片末尾添加元素的效果。 使用 append 函数添加元素 append 函数可以将一个或多个新的元素添加到切片的末尾,并返回一个包含了这些新元素的新切片。如果原始切片有足够的容量来存储新增的元素,append 会直接在原切片的末尾添加这些元素。否则,它会自动分配一个新的底层数组,将原切片的元素和新元素一起复制到这个新数组中,然后返回这个新数组的切片表示。 示例 1package main 2 3import "fmt" 4 5func main() { 6 // 创建一个整数切片 7 slice := []int{1, 2, 3} 8 9 // 向切片末尾添加一个元素10 slice = append(slice, 4)1112 // 向切片末尾添加多个元素13 slice = append(slice, 5, 6, 7)1415 fmt.Println(slice)16} 输出将是: 1[1 2 3 4 5 6 7] 注意事项 容量自动扩展:append 操作可能会导致切片的容量增加。如果原切片的底层数组没有足够的空间来存放新添加的元素,Go 语言运行时会自动分配一个新的、更大的数组,并将原切片的元素以及新元素一起复制到这个新数组中。 返回值:不要忘记 append 函数返回的是更新后的切片。你需要用返回的切片来更新你的切片变量,否则添加的新元素不会被保存。 通过上面的示例和说明,你可以看到在 Go 语言中,虽然没有直接的 push back 函数或方法,但是通过使用 append 函数,我们可以轻松实现向切片末尾添加新元素的功能。 Go 切片怎样自动扩展 在 Go 语言中,切片的自动扩展是通过内建的 append 函数来实现的。当你向一个切片添加元素,而该切片的底层数组没有足够的空间容纳更多元素时,append 会自动处理切片的扩容。这个过程涉及到分配一个新的底层数组,并将原有元素以及新添加的元素复制到这个新数组中。切片的容量(capacity)是指底层数组能够容纳的元素总数。 扩容机制 当使用 append 向切片添加元素,导致切片的长度超过其容量时,Go 语言运行时会自动进行如下步骤: 分配一个新的底层数组:新数组的容量通常是原数组容量的两倍,但这个增长因子可能根据添加的元素数量和切片的当前容量而变化,尤其是在切片的容量非常大或者添加了多个元素的情况下。 复制原有元素到新数组:原切片的元素会被复制到新的底层数组中。 添加新元素:新的元素会被添加到复制的元素之后。 返回新切片:函数返回新的切片,这个切片引用了新的底层数组,并更新了其长度和容量。 示例代码 下面的示例演示了如何通过 append 自动扩展切片的容量。 1package main 2 3import "fmt" 4 5func main() { 6 slice := make([]int, 0, 2) // 创建一个初始容量为2的切片 7 8 // 打印初始切片的长度和容量 9 fmt.Printf("Initial: Length=%d, Capacity=%d\n", len(slice), cap(slice))1011 // 向切片添加元素,超过其初始容量12 for i := 1; i <= 5; i++ {13 slice = append(slice, i)14 // 打印每次添加元素后切片的长度和容量15 fmt.Printf("After appending %d: Length=%d, Capacity=%d\n", i, len(slice), cap(slice))16 }17} 在这个示例中,初始切片的容量是2,但是随着元素的添加,切片的容量会自动增长,以容纳更多的元素。你会注意到,每当切片的容量不足以添加更多元素时,其容量会增加,通常是翻倍,尽管具体的增长策略可能会有所不同。 注意事项 性能考虑:虽然 append 可以自动扩展切片的容量,但频繁地重新分配内存和复制数据可能会影响性能。如果你预先知道大概需要多少容量,使用 make 预分配一个足够大的切片可能会更高效。 扩容策略:Go 语言的切片扩容策略可能会因版本和具体实现而略有差异。通常,对于较小的切片,扩容会翻倍其容量;对于较大的切片,增长因子可能会小于2,以减少因为扩容而浪费的内存。 Go 实现stack 和 queue 在 Go 语言中,没有内置的栈(Stack)和队列(Queue)数据结构,但是可以使用切片(Slice)非常容易地实现它们。切片提供了灵活的操作,如 append 函数和切片操作,使得实现栈和队列成为可能。 栈(Stack) 栈是一种后进先出(LIFO, Last In First Out)的数据结构,主要操作有: Push:将一个元素放入栈顶。 Pop:从栈顶移除一个元素。 实现栈 1package main 2 3import "fmt" 4 5type Stack []int 6 7// Push 添加一个元素到栈顶 8func (s *Stack) Push(v int) { 9 *s = append(*s, v)10}1112// Pop 从栈顶移除一个元素13func (s *Stack) Pop() int {14 if len(*s) == 0 {15 fmt.Println("Stack is empty!")16 return -1 // 返回 -1 表示栈为空17 }18 index := len(*s) - 1 // 获取栈顶元素索引19 element := (*s)[index] // 获取栈顶元素20 *s = (*s)[:index] // 移除栈顶元素21 return element22}2324func main() {25 var stack Stack2627 stack.Push(1)28 stack.Push(2)29 stack.Push(3)3031 fmt.Println(stack.Pop()) // 输出: 332 fmt.Println(stack.Pop()) // 输出: 233 fmt.Println(stack.Pop()) // 输出: 134} 队列(Queue) 队列是一种先进先出(FIFO, First In First Out)的数据结构,主要操作有: Enqueue:在队列末尾添加一个元素。 Dequeue:从队列开头移除一个元素。 实现队列 1package main 2 3import "fmt" 4 5type Queue []int 6 7// Enqueue 在队列末尾添加一个元素 8func (q *Queue) Enqueue(v int) { 9 *q = append(*q, v)10}1112// Dequeue 从队列开头移除一个元素13func (q *Queue) Dequeue() int {14 if len(*q) == 0 {15 fmt.Println("Queue is empty!")16 return -1 // 返回 -1 表示队列为空17 }18 element := (*q)[0] // 获取队列开头元素19 *q = (*q)[1:] // 移除队列开头元素20 return element21}2223func main() {24 var queue Queue2526 queue.Enqueue(1)27 queue.Enqueue(2)28 queue.Enqueue(3)2930 fmt.Println(queue.Dequeue()) // 输出: 131 fmt.Println(queue.Dequeue()) // 输出: 232 fmt.Println(queue.Dequeue()) // 输出: 333} 在这些示例中,我们使用了切片和其操作来实现栈和队列的基本功能。这两种数据结构在实际应用中非常有用,特别是在需要特定数据处理顺序的场合。通过简单的方法,Go 语言使得实现这些数据结构变得既简单又直接。 make函数在Go中的作用 在 Go 语言中,make 函数是一个特殊的内置函数,它被用来初始化并分配内存给内置的数据类型,如切片(slice)、映射(map)和通道(channel)。不同于 new 函数,make 返回的是类型的初始化值,而不是指向该值的指针。make 的使用对于这些复合类型的正确初始化和使用是必要的,因为它们需要在内存中进行特定的布局和初始化。 作用 切片(Slice): make 用于创建一个具有指定长度和容量的切片。如果未指定容量,那么容量将与长度相同。 使用 make 创建切片可以直接使用,无需担心初始值,因为所有元素都会被初始化为其类型的零值。 示例:s := make([]int, 5, 10) 创建了一个长度为 5,容量为 10 的 int 类型切片 s。 映射(Map): make 用于创建一个映射,并分配足够的内存,以便它可以开始存储键值对。创建的映射将为空,没有任何键值对。 使用 make 可以确保返回的映射是可用的,避免了将 nil 映射用于赋值或操作的错误。 示例:m := make(map[string]int) 创建了一个键类型为 string,值类型为 int 的空映射 m。 通道(Channel): make 用于创建一个通道,并指定其可以存储的值的类型以及(可选的)缓冲大小。 创建通道后,可以在协程(goroutines)之间用于通信和同步。 示例:ch := make(chan int, 5) 创建了一个可以存储 int 类型值,且有 5 个缓冲空间的通道 ch。 语法 1make(T, size, ...optional capacity) T:要创建的类型(切片、映射或通道)。 size:对于切片,它是长度(length);对于通道,它是缓冲大小(buffer capacity)。对于映射,此参数通常省略,因为映射的大小是动态增长的。 optional capacity:可选,仅用于切片,指定切片的容量(capacity)。如果省略,容量将与长度相同。 注意 使用 make 初始化的数据结构(切片、映射、通道)可以直接使用,而无需进一步的分配或初始化。 对于其他类型,如自定义结构体或基本数据类型,应使用 new 函数或字面量构造进行初始化。 make 仅适用于切片、映射和通道,尝试用于其他类型将导致编译错误。 通过正确使用 make 函数,Go 程序员可以确保他们的数据结构被正确初始化,准备好安全地用于后续操作。 go中怎么样实现Equaler接口 在Go语言中,实现一个Equaler接口主要涉及到定义一个接口,该接口包含一个方法,用于比较两个对象是否相等。这样的接口可以被任何想要支持等值比较操作的类型实现。下面是如何定义和实现这样一个Equaler接口的示例: 步骤 1: 定义Equaler接口 首先,你需要定义Equaler接口。通常,这个接口会包含一个Equal方法,这个方法接受另一个相同类型的对象作为参数,并返回一个布尔值,表示这两个对象是否相等。 1package main23// Equaler 接口定义了一个方法 Equal,任何实现了这个接口的类型4// 都必须提供 Equal 方法的具体实现。5type Equaler interface {6 Equal(other Equaler) bool7} 步骤 2: 实现Equaler接口 然后,你可以为你自己的类型实现Equaler接口。假设你有一个Point类型,你想要比较两个Point实例是否相等。 1package main 2 3import "math" 4 5// Point 表示一个二维点。 6type Point struct { 7 X, Y float64 8} 910// Equal 实现了 Equaler 接口,用于比较两个 Point 是否相等。11// 这里的other参数需要被类型断言为*Point,以便进行比较。12func (p *Point) Equal(other Equaler) bool {13 otherPoint, ok := other.(*Point)14 if !ok {15 return false16 }17 return math.Abs(p.X-otherPoint.X) < 0.001 && math.Abs(p.Y-otherPoint.Y) < 0.00118} 注意事项 在Equal方法中,参数other被定义为Equaler类型。这允许方法接受任何实现了Equaler接口的类型。然而,在实际的比较中,你通常需要将other类型断言为你需要比较的具体类型(在这个例子中是*Point),以便能够访问该类型的字段。 使用类型断言(otherPoint, ok := other.(*Point))时,需要处理断言失败的情况。如果断言失败,表示other不是一个*Point实例,此时应当返回false。 比较浮点数时使用了一个小的容忍度值(0.001)来判断两个点是否相等。这是因为直接比较浮点数可能会因为精度问题而导致不准确的比较结果。 通过这种方式,你可以为任何类型实现Equaler接口,只要提供了一个适当的Equal方法即可。这种接口抽象允许你编写更通用、更灵活的代码,因为你可以编写依赖于Equaler接口的函数或方法,而不是依赖于特定的类型。 go中能否让自定义的类型也可以使用==来比较 在Go语言中,使用==运算符来比较两个值是否相等的能力是内建的,且适用于所有基本类型(如int、float64、string等)和一些复合类型(如数组)。对于自定义的类型,能否使用==取决于该类型的底层类型: 基本类型和结构体:如果你的自定义类型是基于基本类型的(例如通过type关键字定义的新类型),那么可以直接使用==进行比较,因为这种比较是对其底层类型进行的。对于结构体,如果所有字段都可以比较,则可以使用==来比较两个结构体实例是否相等。 切片、映射和函数类型:对于这些类型,即使是作为自定义类型的底层类型,也不能使用==进行比较(切片、映射、函数只能和nil进行比较)。尝试这样做会导致编译错误。 示例 基于基本类型的自定义类型: 1package main 2 3import "fmt" 4 5type MyInt int 6 7func main() { 8 var a MyInt = 5 9 var b MyInt = 510 fmt.Println(a == b) // 输出: true11} 这里,MyInt是基于int类型的自定义类型,所以可以使用==来比较。 结构体类型: 1package main 2 3import "fmt" 4 5type Point struct { 6 X, Y int 7} 8 9func main() {10 p1 := Point{X: 1, Y: 2}11 p2 := Point{X: 1, Y: 2}12 fmt.Println(p1 == p2) // 输出: true13} 在这个例子中,Point是一个结构体类型,由于它的所有字段都是可以比较的类型,所以两个Point类型的实例可以使用==进行比较。 总结来说,是否可以使用==来比较自定义类型的值取决于该类型的底层类型以及是否满足Go语言中==运算符适用的类型约束。对于复杂类型或需要更灵活的比较逻辑的情况,通常会实现一个比较函数或方法,如上一条回答中所述的Equaler接口实现。 append是不是永远不会改变传入的切片的值 append函数在Go语言中的行为有一些特点需要理解,尤其是关于它如何影响传入的切片的值。append操作本身不会直接修改传入的切片的元素。但是,它可能会改变切片的长度和容量,以及返回一个可能指向新数组的切片。这里有几个关键点需要注意: 不修改原切片元素:append不会改变原有切片中的元素值。如果append操作不导致扩容,添加的元素会出现在原有元素之后。 可能返回新的切片:如果append操作导致了切片扩容(即,需要更多的容量来容纳新元素),Go将分配一个新的数组,并将原切片的元素以及新添加的元素复制到这个新数组中。这时,append返回的切片将指向这个新的数组,而原来的切片仍指向原来的数组。 不改变原切片引用:重要的是,无论append操作是否导致扩容,它都不会更改传入切片本身的引用。换句话说,append函数对原切片的修改是通过返回一个新的切片实现的,而不是直接修改传入的切片变量。 例子 下面的例子展示了append如何工作,以及它是如何影响切片的: 1package main 2 3import "fmt" 4 5func main() { 6 originalSlice := []int{1, 2, 3} 7 fmt.Println("Original slice:", originalSlice) 8 9 // Append without capacity overflow10 modifiedSlice := append(originalSlice, 4)11 fmt.Println("Modified slice:", modifiedSlice)12 fmt.Println("Original slice after append:", originalSlice)1314 // Append that causes capacity overflow15 for i := 5; i <= 10; i++ {16 modifiedSlice = append(modifiedSlice, i)17 }18 fmt.Println("Modified slice after more appends:", modifiedSlice)19 fmt.Println("Original slice remains unchanged:", originalSlice)20} 在这个例子中,originalSlice在初次append后仍指向原来的数组,modifiedSlice可能也指向同一个数组(如果没有超出容量)或者指向一个新的数组(如果执行了扩容)。通过连续的append操作,最终modifiedSlice将指向一个不同的、更大的数组,而originalSlice保持不变,仍然指向最初的数组。 综上所述,append操作不会改变传入切片的值或其指向的数组中的元素,但它会根据需要扩容的情况返回一个新的切片,这个新切片可能指向一个新的底层数组。 go 什么时候用make 在Go语言中,nil是一个预定义的标识符,用于表示指针、通道、函数、接口、映射或切片类型的零值。简而言之,nil用于表示一个未初始化的引用或一个空的引用。以下是nil在不同上下文中的含义和作用: 指针:对于指针类型,nil表示没有指向任何对象的指针。 切片:对于切片类型,nil表示没有分配空间的切片。一个nil切片的长度和容量都是0,但它不等同于空切片(例如[]int{})。 映射:对于映射类型,nil表示没有分配空间的映射。尝试向一个nil映射添加键值对会引发运行时panic。 通道:对于通道类型,nil表示没有分配空间的通道。向nil通道发送或从中接收会永远阻塞。 函数:对于函数类型,nil可以表示一个没有被赋予任何实际函数的函数变量。 接口:对于接口类型,nil表示没有绑定实现的接口。一个nil接口既没有存储任何值,也没有具体类型。 为什么需要nil? nil的存在使得Go语言的错误处理和资源管理变得更加灵活和简洁。以下是一些使用nil的理由: 区分初始化状态:通过检查一个变量是否为nil,可以轻松判断出该变量是否已经初始化。这在处理可选的或者延迟初始化的资源时非常有用。 简化错误处理:在Go中,返回错误的惯用方式是返回一个值和一个error类型的值。如果没有错误发生,这个error类型的值通常会被设置为nil,这样调用者就可以通过检查error是否为nil来判断操作是否成功。 节省资源:nil切片和nil映射在没有分配内存之前不占用额外的内存,这允许开发者在不需要立即使用这些类型的情况下,延迟内存分配,从而更有效地使用资源。 总之,nil在Go语言中扮演着重要的角色,它不仅帮助标识变量的初始化状态,还参与错误处理和资源管理,使代码更加简洁和高效。 为什么map需要make 在Go语言中,映射(map)是一种内建的数据类型,用于存储键值对的集合。映射的键可以是任何可比较的类型,例如整数、字符串等。映射的特性使其成为存储和检索数据的强大工具。然而,与其他一些语言不同,Go的映射需要通过make函数进行初始化才能使用,原因包括: 内存分配:映射在Go中是引用类型,这意味着当你声明一个映射变量时,变量本身并不直接包含数据,而是指向数据所在的内存地址。使用make函数初始化映射可以为其分配内存空间,从而使映射准备好存储键值对。如果不通过make进行初始化,映射变量将是nil,试图向其添加数据将导致运行时panic。 避免nil映射的错误:直接使用未经初始化的nil映射进行赋值操作会导致运行时错误,因为nil映射没有分配内存空间,无法存储任何数据。通过make函数创建映射,你可以确保映射已经准备好接收数据,避免这种类型的错误。 性能优化:使用make函数初始化映射时,可以选择性地指定初始容量,这有助于提高映射操作的效率。指定一个接近或等于预期存储元素数量的初始容量可以减少映射在存储数据时需要进行的内存重新分配的次数,从而提高性能。 示例: 1m := make(map[string]int, 100) // 创建一个初始容量为100的映射 简而言之,通过make初始化映射是为了为其分配内存,确保映射在使用前是非nil的,从而可以安全地添加或访问键值对。此外,make还提供了一种方式来优化映射的内存使用和性能。 如何复制一个map 在Go语言中,没有直接复制一个映射(map)的内建方法,因为映射是引用类型。如果你想复制一个映射,需要手动创建一个新映射并将原映射中的所有键值对复制到新映射中。这可以通过遍历原映射并将每个键值对添加到新映射来实现。以下是一个如何复制映射的示例: 1func CopyMap(originalMap map[string]int) map[string]int { 2 // 创建一个新的映射,用于存储复制的键值对 3 newMap := make(map[string]int) 4 5 // 遍历原映射,复制每个键值对到新映射 6 for key, value := range originalMap { 7 newMap[key] = value 8 } 910 // 返回新的映射副本11 return newMap12} 在这个例子中,CopyMap函数接受一个map[string]int类型的映射作为参数,创建一个新的同类型映射,并遍历原映射,将所有键值对复制到新映射中。最后,函数返回这个新映射,实现了映射的复制。 需要注意的是,这种复制方法是浅复制(shallow copy),意味着如果映射的值是指针或包含指针的复杂类型(如切片、映射等),那么复制的是指针本身,而不是它指向的数据。如果需要深复制(deep copy)这样的值,你需要实现更复杂的逻辑来复制指针指向的数据,而不仅仅是指针本身。 go实现stack 在Go语言中,可以使用切片(slice)来实现一个栈(stack)。栈是一种后进先出(LIFO)的数据结构,支持两个基本操作:push(向栈中添加一个元素)和pop(从栈中移除最顶端的元素)。下面是如何使用切片实现栈的一个简单例子: 1package main 2 3import "fmt" 4 5// Stack 表示栈的结构体 6type Stack struct { 7 elements []int // 使用切片存储栈元素 8} 910// NewStack 创建并返回一个空栈11func NewStack() *Stack {12 return &Stack{[]int{}}13}1415// Push 向栈中添加一个元素16func (s *Stack) Push(element int) {17 s.elements = append(s.elements, element)18}1920// Pop 从栈中移除最顶端的元素并返回该元素21// 如果栈为空,返回0和一个错误22func (s *Stack) Pop() (int, error) {23 if len(s.elements) == 0 {24 return 0, fmt.Errorf("stack is empty")25 }26 // 获取栈顶元素27 topElement := s.elements[len(s.elements)-1]28 // 移除栈顶元素29 s.elements = s.elements[:len(s.elements)-1]30 return topElement, nil31}3233// IsEmpty 检查栈是否为空34func (s *Stack) IsEmpty() bool {35 return len(s.elements) == 036}3738// Size 返回栈的大小39func (s *Stack) Size() int {40 return len(s.elements)41}4243func main() {44 stack := NewStack()45 stack.Push(1)46 stack.Push(2)47 fmt.Println(stack.Pop()) // 输出:2 <nil>48 fmt.Println(stack.IsEmpty()) // 输出:false49 fmt.Println(stack.Size()) // 输出:150} 这个例子中,Stack类型使用一个int类型的切片来存储栈元素。Push方法通过append函数将一个新元素添加到切片的末尾,实现了向栈中添加元素。Pop方法检查栈是否为空,如果不为空,则移除并返回切片的最后一个元素,模拟了从栈中移除元素的操作。此外,IsEmpty和Size方法分别用于检查栈是否为空和获取栈的大小。 这个栈的实现是泛型的基础版本,只能存储int类型的元素。如果需要一个能够存储任何类型元素的栈,可以考虑使用interface{}类型代替int,或者使用Go 1.18及以上版本引入的泛型特性来实现。 go语言new的作用 在Go语言中,new是一个内置的函数,用于分配并初始化一个类型的空间,并返回指向这个新分配空间的指针。new(T)将分配类型T的零值内存空间,并返回一个指向该空间的指针,其类型为*T,其中T表示任意给定的类型。使用new分配的空间被初始化为类型的零值,例如,数值类型的零值是0,布尔类型的零值是false,指针类型的零值是nil等。 使用new的示例 假设你有一个简单的结构体: 1type MyStruct struct {2 Field1 int3 Field2 string4} 你可以使用new来创建这个结构体的一个实例: 1ms := new(MyStruct) 这行代码将分配MyStruct的内存,并将ms初始化为指向这块内存的指针。由于使用了new,MyStruct中的字段Field1和Field2会被初始化为它们各自类型的零值,即Field1为0,Field2为""(空字符串)。 new与复合字面量的区别 虽然new可以用来创建类型的实例并获取其指针,Go语言还提供了另一种创建并初始化类型实例的方法,即使用复合字面量(composite literals): 1ms := &MyStruct{} 这种方法不仅创建了MyStruct的实例,并将其初始化为零值,还返回了指向这个实例的指针,类似于new(MyStruct)的作用。不过,复合字面量还允许你在创建实例的同时初始化字段值: 1ms := &MyStruct{Field1: 10, Field2: "Hello"} 总结 new函数在Go中用于分配并初始化一个类型的零值实例,并返回指向这个实例的指针。尽管new在某些情况下很有用,但在Go中,使用复合字面量初始化结构体及其指针更加常见,因为这种方法更灵活,可以直接在创建实例时初始化字段。 go range 本质 在Go语言中,range是一个用于迭代各种集合元素的关键字,包括数组、切片、映射、字符串和通道(channel)。range的本质是提供了一种简洁的语法来遍历容器类型的元素,而具体的迭代逻辑是根据被迭代对象的类型而内置在Go语言中的。下面是range用法的一些关键点,帮助理解其本质: 1. 数组和切片 当使用range迭代数组或切片时,它会返回每个元素的索引和该索引处的值的副本。因为是副本,所以在迭代过程中修改这些值不会影响到原数组或切片中的元素。 1slice := []int{1, 2, 3}2for i, v := range slice {3 fmt.Println(i, v) // i 是索引,v 是该索引处的值的副本4} 2. 映射(Map) 迭代映射时,range返回每个键值对的键和值。由于映射的迭代顺序是不确定的,每次迭代同一个映射可能会得到不同的顺序。 1m := map[string]int{"a": 1, "b": 2}2for k, v := range m {3 fmt.Println(k, v) // k 是键,v 是对应的值4} 3. 字符串 对于字符串,range迭代的是Unicode字符(rune),而不是字节。它返回当前字符的字节索引(不一定连续,因为Unicode字符可能占多个字节)和字符本身的rune值。 1s := "Go语言"2for i, r := range s {3 fmt.Println(i, r) // i 是rune的起始字节索引,r 是rune值4} 4. 通道(Channel) 当从通道中迭代时,range会连续从通道接收值,直到通道被关闭。 1c := make(chan int)2go func() {3 c <- 14 c <- 25 close(c)6}()7for v := range c {8 fmt.Println(v) // v 是从通道接收到的值9} 本质理解 range的本质是语法糖,它为不同的集合类型提供了一种统一的迭代接口。在底层,Go语言根据被迭代对象的类型执行相应的迭代逻辑。这种设计简化了遍历数据结构的代码,使得开发者可以使用相同的语法模式来迭代不同类型的集合,而无需担心每种类型背后具体的迭代实现细节。 go string如何push_back 在Go语言中,字符串(string)是不可变的,这意味着一旦一个字符串被创建,它所包含的内容就不能被修改了。因此,Go中的字符串没有类似C++中std::string的push_back方法来直接在字符串的末尾添加一个字符。但是,你可以通过字符串连接的方式来“添加”字符或者另一个字符串到现有字符串的末尾,从而实现类似push_back的功能。 使用+操作符或fmt.Sprintf函数是实现这一点的常见方法: 使用+操作符 1s := "hello"2s += " world" // 将" world"添加到"hello"的末尾3fmt.Println(s) // 输出: "hello world" 使用fmt.Sprintf 1s := "hello"2s = fmt.Sprintf("%s%s", s, " world") // 将" world"添加到"hello"的末尾3fmt.Println(s) // 输出: "hello world" 如果你只想添加一个字符,可以将该字符先转换为字符串: 1s := "hello"2c := '!' // 字符类型在Go中是rune3s += string(c) // 将字符'!'添加到"hello"的末尾4fmt.Println(s) // 输出: "hello!" 请记住,每次字符串连接操作实际上都会创建一个新的字符串,因为Go中的字符串是不可变的。如果你需要执行大量的字符串连接操作,使用strings.Builder类型可能会更高效: 使用strings.Builder 1var builder strings.Builder2builder.WriteString("hello")3builder.WriteString(" world")4fmt.Println(builder.String()) // 输出: "hello world" strings.Builder提供了一个可变的字符串缓冲区,可以更高效地构建字符串,特别是在涉及到大量字符串拼接操作时。它在内部管理一个字节切片([]byte)来避免多次分配和复制字符串带来的性能开销。 介绍go中的错误处理 在Go语言中,错误处理是通过使用error接口类型来实现的,这与许多其他语言使用异常(exceptions)的方式有所不同。error类型是Go语言标准库中的一个接口,定义如下: 1type error interface {2 Error() string3} 任何拥有Error() string方法的类型都可以实现error接口,从而成为一个可以用于错误处理的错误类型。Go语言鼓励显式的错误处理,这意味着函数在遇到错误情况时,通常会返回一个错误值(实现了error接口的值),调用者应检查此返回值来决定如何应对错误。 错误处理的惯用法 在Go中处理错误的典型方式是通过检查函数的返回值中的错误值: 1result, err := someFunction()2if err != nil {3 // 处理错误4 fmt.Println("发生错误:", err)5 return // 或者其他错误处理6}7// 继续正常逻辑 自定义错误 你可以通过实现error接口创建自定义错误类型。这通常通过定义一个结构体,然后为它实现Error() string方法来完成: 1type MyError struct { 2 Message string 3 Code int 4} 5 6func (e *MyError) Error() string { 7 return fmt.Sprintf("%d - %s", e.Code, e.Message) 8} 910// 使用自定义错误的函数示例11func doSomething() error {12 // 假设发生了某种错误情况13 return &MyError{Message: "出了点问题", Code: 404}14} 错误检查与处理 Go语言的错误处理模式鼓励开发者显式地检查和处理错误,这可以让错误的来源和处理更加清晰,有助于编写可靠和容易维护的代码。虽然这种方式可能使代码量略增,但它提高了程序的稳定性和可读性。 errors包和fmt.Errorf Go标准库提供了errors包,用于简化常见错误的创建。使用errors.New函数可以快速创建一个基本的错误实例: 1import "errors"23err := errors.New("发生了一个错误") 另外,fmt包的Errorf函数可以用来格式化错误信息并返回一个错误。这是创建带有格式化信息的错误的便捷方式: 1err := fmt.Errorf("发生了错误:%v", "具体信息") 错误处理的进阶使用 在一些复杂的应用场景中,Go语言的错误处理还涉及错误的封装(wrapping)、检查(inspection)、以及自定义错误类型的设计,以满足更高级的错误处理需求。从Go 1.13开始,标准库errors包引入了对错误封装和展开的支持,使得错误处理更加灵活。 Go的错误处理,和其他语言相比,优点是什么?缺点是什么? Go语言的错误处理模型有其独特之处,相较于其他编程语言,这种模型既有优点也有缺点。 优点 简明易懂:Go的错误处理非常直接。函数要么成功并返回结果,要么失败并返回错误。这种明确性使得错误处理逻辑易于理解和维护。 显式错误处理:强制要求检查错误迫使开发者面对可能出现的错误情况,这有助于提高代码的健壮性。在Go中,忽略错误不是默许的做法,这与一些其他语言中可能通过异常处理隐式地忽略错误的情况不同。 灵活性:由于错误被视为普通值,开发者可以使用Go的所有语言特性来处理错误,比如可以很容易地将错误信息记录到日志、添加额外的上下文信息,或者根据错误类型进行不同的处理。 性能:相比异常处理机制,Go的错误处理方式通常有更好的性能。在Go中,错误作为返回值处理,避免了异常处理可能带来的性能开销。 缺点 代码冗余:在每个可能发生错误的函数调用后都需要检查错误,这可能会导致大量重复的错误检查代码。虽然这提高了代码的明确性和可靠性,但也使得代码更加冗长。 错误处理分散:错误需要在它们发生的地方立即被处理,这可能导致错误处理逻辑分散在代码库的各个部分,而不是集中处理。 缺乏统一的错误处理结构:虽然Go的interface{}类型允许以统一的方式处理所有错误,但标准库并没有提供一个统一处理错误的结构或者模式,这可能导致不同的开发者或团队采用不同的错误处理策略,降低了代码的一致性。 功能限制:与支持异常的语言相比,Go的错误处理模型在某些情况下可能不够灵活。例如,在需要清理资源的情况下,缺少类似于其他语言中finally块的机制,尽管可以通过defer语句在某种程度上实现相似的功能。 总体而言,Go的错误处理模型强调简洁性和明确性,鼓励开发者显式地处理每个可能出现的错误。这种模型有其明显的优势,特别是在提高代码可读性和可维护性方面,但也要求开发者付出更多努力来管理错误处理代码的冗余和分散。 Go的错误处理和java,C++相比? Go语言的错误处理与Java和C++的异常处理机制有显著不同。每种语言的错误处理机制都有其设计哲学、优势和局限。下面是一个比较概览: Go 错误作为值:在Go中,错误被视为值,通过error接口表示。函数通常返回一个错误值,以指示是否发生了错误。 显式检查:调用者必须显式检查错误值,以决定是否成功执行了函数。 无异常机制:Go语言没有异常机制。所有的错误都通过正常的控制流和值来处理。 优势: 易于理解和预测错误处理的流程。 鼓励开发者面对并显式处理错误情况。 局限: 可能导致大量重复的错误检查代码。 错误处理逻辑可能会分散于代码的多个地方。 Java 异常机制:Java使用异常来处理错误和异常情况。方法可以抛出异常,调用者需要捕获这些异常。 检查型和非检查型异常:Java区分检查型(必须显式处理)和非检查型异常(可选处理)。 try-catch-finally:通过try-catch语句块捕获和处理异常,finally块用于资源清理。 优势: 异常提供了一种强制错误处理的机制,有助于捕获运行时错误。 通过异常类型,可以传递丰富的错误信息。 局限: 异常可能导致控制流难以跟踪,尤其是在多层嵌套调用中。 过度使用异常可能会影响代码的可读性和性能。 C++ 异常机制:C++同样使用异常来处理错误情况。函数可以抛出异常,使用try-catch块捕获异常。 资源管理:C++提倡RAII(资源获取即初始化)原则,利用对象生命周期自动管理资源,配合异常使用。 noexcept:C++11引入了noexcept关键字,用于指示函数是否抛出异常。 优势: 异常允许将错误处理代码从正常逻辑中分离出来,提高代码清晰度。 RAII原则和智能指针等机制与异常配合,有效管理资源。 局限: 异常可能导致性能开销,特别是在深层嵌套调用中。 需要谨慎管理异常安全性和资源泄漏问题。 总结 Go的错误处理模式倾向于简单和显式,强迫开发者面对错误,但可能导致代码冗余。 Java和**C++**使用异常机制处理错误,能够将错误处理代码与业务逻辑分离,提供更丰富的错误信息,但可能使得控制流程复杂化,且需要注意异常安全性和性能问题。 选择哪种错误处理方式,很大程度上取决于项目的具体需求、团队的偏好和特定语言的生态。 go中我不想定义自己的err类型,应该用哪种默认的? 如果你不想在Go中定义自己的错误类型,可以使用标准库errors包中的errors.New函数来创建一个简单的错误。这种方式适用于那些不需要从错误中提取额外信息的场景。errors.New接收一个字符串参数,返回一个符合error接口的错误对象。 使用errors.New 1import ( 2 "errors" 3 "fmt" 4) 5 6func someFunction() error { 7 // 假设发生了某种错误情况 8 return errors.New("这里发生了一个错误") 9}1011func main() {12 err := someFunction()13 if err != nil {14 fmt.Println(err)15 }16} 格式化错误信息 如果你需要在错误信息中包含变量或者更复杂的数据结构,可以使用fmt.Errorf来代替errors.New。fmt.Errorf允许你像使用fmt.Sprintf一样格式化错误字符串,并返回一个错误对象。 1import ( 2 "fmt" 3) 4 5func someFunction(param int) error { 6 // 使用 fmt.Errorf 格式化错误信息 7 return fmt.Errorf("错误发生在 %d", param) 8} 910func main() {11 err := someFunction(42)12 if err != nil {13 fmt.Println(err)14 }15} 从Go 1.13开始,errors包提供了As和Is函数,用于检查错误的类型和值。同时,fmt.Errorf支持使用%w动词来包装错误,使得你可以保留原始错误的同时添加上下文信息。即使你不定义自己的错误类型,这些功能也能帮助你更好地管理和检查错误。 使用错误包装 1import ( 2 "errors" 3 "fmt" 4) 5 6var ErrSpecific = errors.New("特定错误") 7 8func someFunction() error { 9 // 返回一个包装了原始错误的新错误10 return fmt.Errorf("操作失败: %w", ErrSpecific)11}1213func main() {14 err := someFunction()15 if errors.Is(err, ErrSpecific) {16 fmt.Println("发生了特定的错误:", err)17 }18} 使用errors.New和fmt.Errorf可以满足大多数不需要自定义错误类型的场景,并且通过错误包装和检查,你依然可以构建出结构化和可管理的错误处理逻辑。 如何判断 map 中是否包含某个 key ? 1var sample map[int]int2if _, ok := sample[10]; ok {34} else {56} strings.Join ≈ strings.Builder > bytes.Buffer > “+” > fmt.Sprintf 在Go语言中,拼接字符串可以通过多种方式实现,每种方式在不同场景下的性能和适用性各有差异。下面是对这五种方法——strings.Join、strings.Builder、bytes.Buffer、"+"操作符、fmt.Sprintf——的分析,按照性能从高到低排列: 1. strings.Join 适用场景:当你有一个字符串切片,需要将其元素连接成一个单独的字符串时,strings.Join非常高效。 性能:通常比+操作符和fmt.Sprintf更高效,因为它在内部计算结果字符串的确切长度,然后一次性分配足够的内存。 示例: 1parts := []string{"Hello", "world"}2result := strings.Join(parts, " ") 2. strings.Builder 适用场景:对于在循环或多个步骤中构建字符串尤其有用。自Go 1.10引入以来,它是推荐的字符串构建方法。 性能:提供了高效的字符串构建能力,因为它避免了不必要的内存分配和复制。 示例: 1var sb strings.Builder2sb.WriteString("Hello")3sb.WriteString(" ")4sb.WriteString("world")5result := sb.String() 3. bytes.Buffer 适用场景:在需要频繁修改字符串或者处理二进制数据和字符串转换时适用。bytes.Buffer与strings.Builder类似,但更通用,也支持字节操作。 性能:虽然性能略低于strings.Builder,但在需要处理字节和字符串的场景下是一个很好的选择。 示例: 1var buf bytes.Buffer2buf.Write([]byte("Hello"))3buf.WriteString(" world")4result := buf.String() 4. "+"操作符 适用场景:简单的字符串拼接操作,尤其是少量字符串时。 性能:在拼接少数几个字符串时性能可接受,但在循环或大量字符串拼接场景中性能较差,因为每次拼接都会产生新的字符串,导致多次内存分配和复制。 示例: 1result := "Hello" + " " + "world" 5. fmt.Sprintf 适用场景:需要格式化字符串时,如插入变量、调整格式等。 性能:相比于其他方法,性能较低,因为它需要解析格式字符串并执行类型断言等操作,但它提供了非常强大的格式化功能。 示例: 1name := "world"2result := fmt.Sprintf("Hello %s", name) 总结 选择哪种方法取决于具体的应用场景。对于简单或少量的字符串拼接,直接使用"+"可能是最直观的。当需要构建大量字符串时,strings.Builder是首选。strings.Join适合已经存储在切片中的字符串拼接。fmt.Sprintf提供了格式化的灵活性,适用于需要格式化输出的场景。bytes.Buffer则在处理既包含文本又包含二进制数据的情况下非常有用。 strings.Builder原理 strings.Builder 是 Go 语言在 1.10 版本中引入的一个结构体,它提供了一种高效构建字符串的方法。与其他字符串拼接方法相比,strings.Builder 更为高效,主要因为它在内部优化了内存的分配和复制操作。下面是 strings.Builder 的一些关键原理和特点: 1. 最小化内存分配 strings.Builder 在内部维护了一个字节切片([]byte)。当向 Builder 添加字符串时,它实际上是将这些字符串的字节追加到内部的字节切片中。Builder 会根据需要自动增长这个字节切片的容量,但它尽量通过预分配足够的内存空间来避免频繁的内存重新分配。 2. 避免不必要的复制 在传统的字符串拼接操作中,每一次拼接都可能产生一个新的字符串对象,这意味着需要复制字符串的内容到新的内存位置。相比之下,strings.Builder 通过直接在已有的字节切片上追加内容,避免了这种复制,从而提升了性能。 3. 高效的字符串生成 当调用 strings.Builder 的 String() 方法来获取最终的字符串时,它会基于内部的字节切片创建一个新的字符串。由于 Go 语言字符串的不可变性,这个操作会涉及一次内存复制。然而,这仅仅发生在整个构建过程的最后,因此相比于多次字符串拼接,总体上仍然大大减少了内存复制的次数。 4. Grow 方法 strings.Builder 提供了 Grow 方法,允许手动指定预期需要的额外容量。这可以进一步优化性能,因为它可以在大量数据被写入之前预分配足够的内存,从而避免自动扩容过程中的多次内存分配。 5. 安全性 strings.Builder 的设计也考虑到了安全性。一旦通过 String() 方法获取了最终的字符串,再尝试向 Builder 写入新的数据将会导致 panic。这个设计确保了一旦字符串被生成,内部的字节切片不会被进一步修改,保持了字符串不可变的特性。 总的来说,strings.Builder 的设计充分利用了 Go 语言的特性,如切片的灵活性和字符串的不可变性,通过减少内存分配和复制的次数,提供了一种高效且安全的方式来构建字符串。 Go语言 …是什么 在Go语言中,...符号有两个主要用途: 1. 可变参数函数(Variadic Function) 当...用于函数的参数类型前时,表示该函数可以接收任意数量的该类型的参数。这使得函数可以灵活处理不同数量的参数,类似于其他语言中的可变参数列表。 例如,定义一个可接收多个int类型参数的函数,并计算它们的和: 1func sum(nums ...int) int { 2 total := 0 3 for _, num := range nums { 4 total += num 5 } 6 return total 7} 8 9func main() {10 fmt.Println(sum(1, 2, 3)) // 输出 611 fmt.Println(sum(1, 2, 3, 4, 5)) // 输出 1512} 在这个例子中,sum函数可以接收任意数量的int参数。 2. 切片参数展开(Slice Unfolding) 当...用于调用可变参数函数时,紧跟在切片变量后面,表示将切片的元素展开为参数列表传递给函数。 继续使用上面的sum函数作为例子,如果你有一个int切片,并希望将其元素作为参数传递给sum,可以这样做: 1nums := []int{1, 2, 3, 4, 5}2fmt.Println(sum(nums...)) // 输出 15 在这个例子中,nums...将nums切片中的每个元素展开,作为独立的参数传递给sum函数。 总结 ...在Go语言中用作可变参数函数定义和切片参数展开的语法元素,提供了编写灵活函数接口和调用的能力。

2024/4/1
articleCard.readMore

软件使用小技巧

Chrome 如何在MacOS下使chrome全屏,并且是不显示tab栏和地址栏的真全屏? 首先Fn+F使它假全屏,之后按 ⌘ + ⇧ + F , (Command + Shift + F) 使得它真全屏. Qemu 按Option+Control+G 并不能release mouse。需要按下Option+Control+Command+G才可以。

2024/3/10
articleCard.readMore

如何准备程序员面试

本文既讲解国内和北美面试中间的共同点,也对于两地不同的情况进行单独讲解。例如国内的八股文和北美的BQ。 本文主要面向的读者是还没参加过正式工作的学生群体。作者也没有跳槽或者高级别的工作经验。如有不合理、不详尽之处,请联系本人改正。十分感谢。 首先讲解一些互联网上常见的术语。本文可能用到其中的一些。如果你在本文阅读过程中看到看不懂的词语,请使用ctrl+F搜索。 术语表 JD: (Job Description) 工作岗位描述 bar: 招人的标准。类似高考的分数线。一般用来形容难度高低。 DP: (data point) 数据点。一般含义就是指自己or身边朋友的例子。例如我GPA2.0被牛津录取了,我就可以在网上说,给大家贡献一个DP,GPA2.0被牛津录取。或者我在面试谷歌的过程中遇到了Leetcode1234这道题,我就可以说贡献一个DP,谷歌面试会考Leetcode1234. OA: (Online Assessment) 在线笔试 BQ: (behavioral question) 和代码无关的问题。例如你在工作中遇到难题会怎么办? VO: (Virtual Onsite) 电话或者视频面试。 HC: (Head Count) 人头数。也就是岗位计划招几个人。例如你面试了一个组,表现很好,但是他们这个组今年的hc只有1个人,而他们在面试你之前,已经签了1个人。那么这个时候由于hc不够,你就无法被录用。 CV/Resume: 简历 IC: (Individual Contributor) 个人贡献者,与之对应的是Manager。IC指的就是写代码的,Manager指的是管理岗位。 toxic: 有毒。往往用于形容令人不适的团队氛围或公司文化。 NG: (New Grad) 指刚毕业的学生。 ROI: (return on investment) 投资回报率 Skip: 你老板的老板。你也可以分别称呼他们为skip manager和 direct manager. RA: (research assistant) 研究助理,你本科在实验室打杂也可以称自己做了2年的RA。 corner case: 写代码的时候遇到的极端or边界情况。 写在前面的话 心态 我们作为面试者,在求职的过程中,最终目标当然是拿到工作。因此很多时候你没有必要准备到万无一失了再投递简历或者面试。或者由于自己的不自信而不敢去投递或面试。哪怕心里很慌,也要先拿下offer再说。一般对于学生求职者来说,在拿到offer到正式上班中间还有好几个月的时间,如果你真的觉得自己不能胜任这份工作,那么你还有好几个月可以继续学习钻研相关领域。你如果总是想让自己的技术水平先达到高级工程师水平再去求职,那永远也准备不完。 所以,第一点,永远不要胆怯。 重要概念 Match大于一切. 即你和这个职位是否匹配是大于一切的,大于你的学历,能力。你再有优势,如果和职位不够匹配,那也很难拿到offer。 简历 推荐使用网上的现成模版来写。例如https://github.com/skyzh/chicv. 简历上没必要写GPA,除非这是你的优势。 简历大概分为教育经历,工作经历,项目等部分。 工作经历可以包含实习或者RA的经历。描写一下自己主要做了什么工作。要写出自己的impact并突出自己的技术点,不要只写自己做了什么。如果你做出来的东西对于公司没有任何影响,那等于没做。 (impact: 你对这个世界的影响) 例如写我在字节跳动开发推荐系统。就不如写:我在字节跳动的XX部门,使用C++和python语言,使用XXX技术框架,独立/和团队一起,使用XXX算法,改进了推荐系统在XX方面的性能,最后经过统计,使得性能提高了30%。 一般建议写上一些数字,例如使得效率提高了30%等,可以增强可信度和说服力。 重复一下就是,主要两点。一是体现自己的能力,二是体现自己的impact。 项目部分,有github链接是最好的。自己平常写的个人项目建议上传到github上面。项目就主要是突出自己的技术栈了。个人自己写的开源项目一般很难产生impact。 简历以一页为宜。如果你想写的太多,你可以针对不同岗位写多份简历。例如你投后端的岗位,就没必要花大量篇幅写前端的东西。 投简历 能早投就早投! 能早投就早投! 能早投就早投! 能早投就早投! 大公司,大家都耳熟能详。直接进入官网投递即可。 小公司,国内可以使用boss直聘等软件。国外可以使用simplify等软件。 内推: 内推建议多找自己身边认识的人、学长学姐、网友等。 领英直接联系hr: 这也是一种方法。首先你需要先把自己领英的界面搞好,然后找hr进行私聊。 OA(笔试) 最好在收到的当天就做了。不然别人做的早,可能先把hc占了。 珍惜每一个OA。 技术面试 从面试官的角度看面试 在你面试的时候,往往你会注意到面试官在敲键盘。他是在记录你面试时候的表现。一份面试官的记录可能如下: 8:00pm 开始 8:10 自我介绍太啰嗦 8:12 给出问题。面试者询问一些clarification 8:15 面试者给出暴力解法,有效。时间复杂度太高,让面试者先实现暴力算法,再问面试者能否提高。 8:25 实现暴力算法,完成。 8:30 通过了几组样例,让面试者自己书写样例。自行书写的样例不错。 8:35 面试者分析了暴力解法的时间复杂度。 8:40 面试者无法想到如何优化。给出提示XXX。 8:50 面试者实现了提示。 这些记录是面试官日后向公司提交关于你的报告的时候所需要参考的。因为人的记忆力是有限的,所以需要记录。 此外,面试官一般还会对你在不同的维度进行打分。例如代码、解决问题、沟通能力等方面。 解决问题包括你对这个问题的理解、分析和表达。你是否真正理解了题目?你能否分析不同的解决方案之间的优劣和时空复杂度?你能否清晰的讲述自己的想法? 代码包括代码质量,是否有bug,变量命名和代码风格等。 沟通能力包括你能否把自己的思路讲清楚。能否逻辑清晰地回答面试官的问题? 总而言之可以分为下面几点: 通过例子和反问来理解题目。 例如涉及图的题,就可以反问面试官,这是一个联通图吗? 描述自己的思路 把思路变成数据结构和算法 描述自己的代码逻辑 思考是否覆盖所有的corner case 解法不是最优解的时候,发现哪一部分重复计算了or发现了新的思路,从而进行优化。 最重要的点是让面试官觉得他的思路和你是同步的。 自我介绍 不要太长。简要说明自己的身份和优点就可以了。 Resume Deep Dive 面试官会对着你的简历询问相关的问题。例如你用C++写了一个项目,他可能会问你在这个过程中用到了C++的哪些特性。或者针对你的实习经历,他会问你能再描述一下你做了什么吗?你当时遇到的困难是什么? 这些只要自己对着简历,把自己想象成面试官,然后自己问自己一些问题进行准备就可以了。 Coding 写代码部分。推荐的网站是Leetcode. https://www.techinterviewhandbook.org/grind75 这个网站上面对Leetcode的题目做了分类精选。非常推荐。如果时间紧,就先刷这个上面的题目。 做题重要的是学习思想,而不是死记硬背。算法题是背不完的。 System Design 这个属于北美的特殊要求。国内的同学可以跳过这一段。 看书,DDIA。解决所有问题。 八股文 这个国内比较多,北美偶尔也有考的。 推荐的方法是首先分类。计算机网络,操作系统,编译原理,数据库。然后自己针对薄弱的地方学习。时间够的话直接在B站看南京大学或者MIT的课程,把整门课都复习一遍。这是正道。时间不够的话看这个网站,https://www.xiaolincoding.com/. BQ 北美向 https://sugarac.gitbooks.io/high-frequency-interview-questions-and-answers/content/bq-by-xiao-tu-dao.html https://www.youtube.com/c/DanCroitor/videos 反问 反问环节一般出现在面试最后,面试官会说,我的问题问完了,你有什么要问我的吗?这是一个常规流程。 反问适用的常见问题 以下问题有的只能用来询问组长(team leader),不要用他们去询问做IC的面试官。例如您对我未来2年的期望是什么?这应该很好区分。 下面是总结的常见反问: 你的/我入职后的 每天的工作流程是什么? 可以了解对方和自己的工作流程。常规反问。 我该在未来几个月(入职前),继续学习哪些知识来准备这份工作? 表示自己对工作的重视。但是不要把语气说成自己目前还不能胜任这份工作的样子。 团队的氛围怎么样?有没有什么活动? 团队内的人数如何?有几个人?分别角色是什么? 摸清团队情况,有助于了解这份工作的前景。例如这是一个新创建的团队,就要考虑自己未来的职业规划是否打算加入一个新创建的团队。如果是一个人很多的大组,那可能竞争比较激烈。要记住你加入的是一个团队,而不是一个公司。 你的日常工作、心情、升职等机会都是和团队直接相关的。学生思维常常会只看公司,而忽略团队。 我们团队未来1-2年的规划是什么? 如果我加入团队,那么我的职责会是什么?你期待我立刻产出什么内容?你期待我在未来1-2年产出什么内容? 了解面试官对你的期望,和你自己的职业规划是否相符合。例如你没打算做前端,最后却给你分配到做前端去了。也可以了解老板对你未来的发展是不是有一些想法,有的话说明对你还是比较重视的。 我的哪一段经历最让你感兴趣? 你怎样评价一个项目是否成功? 可以看一下老板属于哪种类型。更看重速度还是质量?喜欢创新一些还是稳定?可以看一下自己的工作风格和老板是不是一致。 你如何评价一个工程师的表现是否优秀?你最在乎哪一点? 你觉得我哪些地方可以提高? 可以从回答中了解自己的面试结果。但是问这个要慎重,有的面试官不喜欢别人问这个问题。(他也知道回答这个约等于提前告诉你面试结果) 你对于像我这样的新手程序员有什么人生经验可以传授的? 你怎么样处理和其他人的冲突? 你每天工作中最大的挑战是什么? HR面 HR面常见的出现时间是投完简历之后的第一轮面试,或者技术面全部通过之后的最后一轮面试。这点不同公司差别很大。阿里的HR权力较大,一般HR面在最后需要认真准备。有的公司技术面全通过之后的HR面基本就是走个过场了。有的公司第一轮就是HR面,这属于一种初步筛选。 常见问题 你为什么对本公司的这个职位感兴趣? 你有没有做过什么项目? 你在过去的项目中经历过最难的事情是什么?你是怎么克服的?有什么收获? 你最满意的项目是什么? 你做过的失败的项目里面印象深刻的是哪一个? 你对加班的看法? 你最大的缺点是什么? 你最大的优点是什么? 你对薪资的要求? 你在未来2年/5年的职业规划是什么? 在完成某项工作时,你认为领导要求的方式不是最好的,自己还有更好的方法,你应该怎么做? 你对跳槽的看法? 回答策略 STAR方法: STAR原则是结构化面试当中非常重要的一个理论: S—Situation:事情是在什么情况下发生; T—Task:你是如何明确你的任务的; A—Action:针对这样的情况分析,你采用了什么行动方式; R—Result:结果怎样,在这样的情况下你学习到了什么。 面试官会按照STAR方法来问你,你也按照STAR方法来回答就好了。 写在后面 天生我材必有用,千金散尽还复来。 相信自己。

2024/3/8
articleCard.readMore

UCSD CSE 230 Midterm Review Note

01-lambda Programs are expressions e (also called λ-terms) of one of three kinds: Variable x, y, z Abstraction (aka nameless function definition) (\x -> e) x is the formal parameter, e is the body “for any x compute e” Application (aka function call) (e1 e2) e1 is the function, e2 is the argument in your favorite language: e1(e2) Execute = rewrite step-by-step Following simple rules until no more rules apply An variable x is free in e if there exists a free occurrence of x in e. If e has no free variables it is said to be closed. Closed expressions are also called combinators What is the shortest closed expression? \x->x Rewrite Rules of Lambda Calculus β-step (aka function call)α-step (aka renaming formals) Semantics: RedexA redex is a term of the form (\x -> e1) e2 A function (\x -> e1) x is the parameter e1 is the returned expressionApplied to an argument e2 e2 is the argument Semantics: β-Reduction A redex b-steps to another term … (\x -> e1) e2 =b> e1[x := e2] where e1[x := e2] means “e1 with all free occurrences of x replaced with e2” and as long as no free variables of e2 get captured Computation by search-and-replace: If you see an abstraction applied to an argument, take the body of the abstraction and replace all free occurrences of the formal by that argument We say that (\x -> e1) e2 β-steps to e1[x := e2] Semantics: α-Renaming \x -> e =a> \y -> e[x := y]where not (y in FV(e)) We rename a formal parameter x to y By replace all occurrences of x in the body with y We say that \x -> e α-steps to \y -> e[x := y] Recall redex is a λ-term of the form (\x -> e1) e2 A λ-term is in normal form if it contains no redexes. Semantics: EvaluationA λ-term e evaluates to e’ if There is a sequence of stepse =?> e_1 =?> … =?> e_N =?> e'where each =?> is either =a> or =b> and N >= 0 e’ is in normal form So the result of a evaluation must be a normal form!!! ELSA Named λ-terms: let ID = \x -> x – abbreviation for \x -> x To substitute name with its definition, use a =d> step: ID apple=d> (\x -> x) apple – expand definition=b> apple – beta-reduce Evaluation: e1 =*> e2: e1 reduces to e2 in 0 or more steps where each step is =a>, =b>, or =d> e1 =~> e2: e1 evaluates to e2 and e2 is in normal form

2023/11/2
articleCard.readMore

Infra知识点

本文为摘录网络内容. 由于时间原因未记录原文地址,谷歌即可找到原文。 简述数据库三大范式 第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。 数据库第二范式:关系模式必须满足第一范式,并且所有非主属性都完全依赖于主码。注意,符合第二范式的关系模型可能还存在数据冗余、更新异常等问题。关系模型(学号,姓名,专业编号,专业名称)中,学号->姓名,而专业编号->专业名称,不满足数据库第二范式 数据库第三范式:关系模型满足第二范式,所有非主属性对任何候选关键字都不存在传递依赖。即每个属性都跟主键有直接关系而不是间接关系。接着以学生表举例,对于关系模型(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)院校地址,院校电话和学号不存在直接关系,因此不满足第三范式。 简述MySQL的架构 MySQL可以分为应用层,逻辑层,数据库引擎层,物理层。 应用层:负责和客户端,响应客户端请求,建立连接,返回数据。 逻辑层:包括SQK接口,解析器,优化器,Cache与buffer。 数据库引擎层:有常见的MyISAM,InnoDB等等。 物理层:负责文件存储,日志等等。 简述执行SQL语言的过程 客户端首先通过连接器进行身份认证和权限相关如果是执行查询语句的时候,会先查询缓存,但MySQL 8.0 版本后该步骤移除。没有命中缓存的话,SQL 语句就会经过解析器,分析语句,包括语法检查等等。通过优化器,将用户的SQL语句按照 MySQL 认为最优的方案去执行。执行语句,并从存储引擎返回数据。 简述MySQL的共享锁排它锁 共享锁也称为读锁,相互不阻塞,多个客户在同一时刻可以同时读取同一个资源而不相互干扰。排他锁也称为写锁,会阻塞其他的写锁和读锁,确保在给定时间内只有一个用户能执行写入并防止其他用户读取正在写入的同一资源。 简述MySQL中的按粒度的锁分类 表级锁: 对当前操作的整张表加锁,实现简单,加锁快,但并发能力低。 行锁: 锁住某一行,如果表存在索引,那么记录锁是锁在索引上的,如果表没有索引,那么 InnoDB 会创建一个隐藏的聚簇索引加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 Gap 锁:也称为间隙锁: 锁定一个范围但不包括记录本身。其目的是为了防止同一事物的两次当前读出现幻读的情况。 Next-key Lock: 行锁+gap锁。 如何解决数据库死锁 预先检测到死锁的循环依赖,并立即返回一个错误。当查询的时间达到锁等待超时的设定后放弃锁请求。 简述乐观锁和悲观锁 乐观锁:对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。 悲观锁:对于数据冲突保持一种悲观态度,在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的。 简述InnoDB存储引擎 InnoDB 是 MySQL 的默认事务型引擎,支持事务,表是基于聚簇索引建立的。支持表级锁和行级锁,支持外键,适合数据增删改查都频繁的情况。 InnoDB 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别。其默认级别是 REPEATABLE READ,并通过间隙锁策略防止幻读,间隙锁使 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定防止幻行的插入。 索引是什么? 索引是存储引擎中用于快速找到记录的一种数据结构。在关系型数据库中,索引具体是一种对数据库中一列或多列的值进行排序的存储结构。 为什么引入索引? 为了提高数据查询的效率。索引对数据库查询良好的性能非常关键,当表中数据量越来越大,索引对性能的影响越重要。 Mysql有哪些常见索引类型? 数据结构角度B-Tree索引 哈希索引 R-Tree索引 全文索引 物理存储角度 主键索引(聚簇索引):叶子节点存的是整行的数据 非主键索引(二级索引):叶子节点存的主键的值 简述聚集索引和稀疏索引 聚集索引按每张表的主键构建一棵B+树,数据库中的每个搜索键值都有一个索引记录,每个数据页通过双向链表连接。表数据访问更快,但表更新代价高。 稀疏索引不会为每个搜索关键字创建索引记录。搜索过程需要,我们首先按索引记录进行操作,并按顺序搜索,直到找到所需的数据为止。 简述辅助索引与回表查询 辅助索引是非聚集索引,叶子节点不包含记录的全部数据,包含了一个书签用来告诉InnoDB哪里可以找到与索引相对应的行数据。 通过辅助索引查询,先通过书签查到聚集索引,再根据聚集索引查对应的值,需要两次,也称为回表查询。 简述联合索引和最左匹配原则 联合索引是指对表上的多个列的关键词进行索引。 对于联合索引的查询,如果精确匹配联合索引的左边连续一列或者多列,则mysql会一直向右匹配直到遇到范围查询(>,<,between,like)就停止匹配。Mysql会对第一个索引字段数据进行排序,在第一个字段基础上,再对第二个字段排序。 简述覆盖索引 覆盖索引指一个索引包含或覆盖了所有需要查询的字段的值,不需要回表查询,即索引本身存了对应的值。 为什么数据库不用红黑树用B+树 红黑树的出度为 2,而 B Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B Tree 大非常多,IO次数很多,导致会比较慢,因此检索的次数也就更多。 B+Tree 相比于 B-Tree 更适合外存索引,拥有更大的出度,IO次数较少,检索效率会更高。 基于主键索引的查询和非主键索引的查询有什么区别? 对于select * from 主键=XX,基于主键的普通查询仅查找主键这棵树,对于select * from 非主键=XX,基于非主键的查询有可能存在回表过程(回到主键索引树搜索的过程称为回表),因为非主键索引叶子节点仅存主键值,无整行全部信息。 非主键索引的查询一定会回表吗? 不一定,当查询语句的要求字段全部命中索引,不用回表查询。如select 主键 from 非主键=XX,此时非主键索引叶子节点即可拿到主键信息,不用回表。 ——- 火焰图 类型横轴纵轴解决方法采样方式 ON-CPUcpu占用的时间调用栈找出占用高的函数,分析代码热路径固定频率采样cpu调用栈 OFF-CPU阻塞时间调用栈IO,网络等阻塞;锁、死锁导致的固定频率采样阻塞事件调用栈 内存内存申请/释放函数调用次数调用栈内存泄漏问题;内存占用高的对象/申请内存多的函数;有四种方式:跟踪malloc/free;跟踪brk;跟踪mmap;跟踪页错误 如果是 CPU 则使用 On-CPU 火焰图,如果是 IO 或锁则使用 Off-CPU 火焰图.如果无法确定, 那么可以通过压测工具来确认:通过压测工具看看能否让 CPU 使用率趋于饱和, 如果能那么使用 On-CPU 火焰图如果不管怎么压, CPU 使用率始终上不来, 那么多半说明程序被 IO 或锁卡住了, 此时适合使用 Off-CPU 火焰图.如果还是确认不了, 那么不妨 On-CPU 火焰图和 Off-CPU 火焰图都搞搞, 正常情况下它们的差异会比较大, 如果两张火焰图长得差不多, 那么通常认为 CPU 被其它进程抢占了 火焰图分析技巧 纵轴代表调用栈的深度(栈桢数),用于表示函数间调用关系:下面的函数是上面函数的父函数。横轴代表调用频次,一个格子的宽度越大,越说明其可能是瓶颈原因。不同类型火焰图适合优化的场景不同,比如 on-cpu 火焰图适合分析 cpu 占用高的问题函数,off-cpu 火焰图适合解决阻塞和锁抢占问题。无意义的事情:横向先后顺序是为了聚合,跟函数间依赖或调用关系无关;火焰图各种颜色是为方便区分,本身不具有特殊含义多练习:进行性能优化有意识的使用火焰图的方式进行性能调优(如果时间充裕) 生成和创建火焰图需要如下几个步骤 流程描述脚本 捕获堆栈使用 perf/systemtap/dtrace 等工具抓取程序的运行堆栈perf/systemtap/dtrace 折叠堆栈trace 工具抓取的系统和程序运行每一时刻的堆栈信息, 需要对他们进行分析组合, 将重复的堆栈累计在一起, 从而体现出负载和关键路径FlameGraph 中的 stackcollapse 程序 生成火焰图分析 stackcollapse 输出的堆栈信息生成火焰图flamegraph.pl perf 命令(performance 的缩写)讲起, 它是 Linux 系统原生提供的性能分析工具, 会返回 CPU 正在执行的函数名以及调用栈(stack) 不同的观测对象对应着不同的 profiler,仅就 CPU 而言,profiler 也数不胜数。 按照观测范围来分类,CPU 上的 profiler 大致可以分为两大类:进程级(per-process,某些地方也叫做应用级)和系统级(system wide),其中: 进程级只观测一个进程或线程上发生的事情系统级不局限在某一个进程上,观测对象为整个系统上运行的所有程序需要注意的是,某些工具既能观测整个系统也支持观测单个进程,比如 perf,因此这样的工具同时属于两个类别。 按照观测方法来分类,大致可以分为 event based 和 sampling based 两大类。其中: event based:在一个指定的 event 集合上进行,比如进入或离开某个/某些特定的函数、分配内存、异常的抛出等事件。event based profiler 在一些文章中也被称为 tracing profiler 或 tracersampling based:以某一个指定的频率对运行的程序的某些信息进行采样,通常情况下采样的对象是程序的调用栈 浅谈性能优化的十种手段 性能优化通常是“时间”与“空间”的互换与取舍。本文分两个部分,在上篇,讲解六种通用的“时间”与“空间”互换取舍的手段: 索引术压缩术缓存术预取术削峰填谷术批量处理术 在下篇,介绍四种进阶性的内容,大多与提升并行能力有关: 八门遁甲 —— 榨干计算资源影分身术 —— 水平扩容奥义 —— 分片术秘术 —— 无锁术 索引 索引的原理是拿额外的存储空间换取查询时间,增加了写入数据的开销,但使读取数据的时间复杂度一般从O(n)降低到O(logn)甚至O(1)。索引不仅在数据库中广泛使用,前后端的开发中也在不知不觉运用。 在数据集比较大时,不用索引就像从一本没有目录而且内容乱序的新华字典查一个字,得一页一页全翻一遍才能找到;用索引之后,就像用拼音先在目录中先找到要查到字在哪一页,直接翻过去就行了。书籍的目录是典型的树状结构,那么软件世界常见的索引有哪些数据结构,分别在什么场景使用呢? 哈希表(Hash Table):哈希表的原理可以类比银行办业务取号,给每个人一个号(计算出的Hash值),叫某个号直接对应了某个人,索引效率是最高的O(1),消耗的存储空间也相对更大。K-V存储组件以及各种编程语言提供的Map/Dict等数据结构,多数底层实现是用的哈希表。 二叉搜索树(Binary Search Tree):有序存储的二叉树结构,在编程语言中广泛使用的红黑树属于二叉搜索树,确切的说是“不完全平衡的”二叉搜索树。从C++、Java的TreeSet、TreeMap,到Linux的CPU调度,都能看到红黑树的影子。Java的HashMap在发现某个Hash槽的链表长度大于8时也会将链表升级为红黑树,而相比于红黑树“更加平衡”的AVL树反而实际用的更少。平衡多路搜索树(B-Tree):这里的B指的是Balance而不是Binary,二叉树在大量数据场景会导致查找深度很深,解决办法就是变成多叉树,MongoDB的索引用的就是B-Tree。 叶节点相连的平衡多路搜索树(B+ Tree):B+Tree是B-Tree的变体,只有叶子节点存数据,叶子与相邻叶子相连,MySQL的索引用的就是B+树,Linux的一些文件系统也使用的B+树索引inode。其实B+树还有一种在枝桠上再加链表的变体:B*树,暂时没想到实际应用。 日志结构合并树(LSM Tree):Log Structured Merge Tree,简单理解就是像日志一样顺序写下去,多层多块的结构,上层写满压缩合并到下层。LSM Tree其实本身是为了优化写性能牺牲读性能的数据结构,并不能算是索引,但在大数据存储和一些NoSQL数据库中用的很广泛,因此这里也列进去了。字典树(Trie Tree):又叫前缀树,从树根串到树叶就是数据本身,因此树根到枝桠就是前缀,枝桠下面的所有数据都是匹配该前缀的。这种结构能非常方便的做前缀查找或词频统计,典型的应用有:自动补全、URL路由。其变体基数树(Radix Tree)在Nginx的Geo模块处理子网掩码前缀用了;Redis的Stream、Cluster等功能的实现也用到了基数树(Redis中叫Rax)。 跳表(Skip List):是一种多层结构的有序链表,插入一个值时有一定概率“晋升”到上层形成间接的索引。跳表更适合大量并发写的场景,可以认为是随机平衡的二叉搜索树,不存在红黑树的再平衡问题,Redis强大的ZSet底层数据结构就是哈希加跳表。 倒排索引(Inverted index):这样翻译不太直观,可以叫“关键词索引”,比如书籍末尾列出的术语表就是一种倒排索引,标识出了每个术语出现在哪些页,这样我们要查某个术语在哪用的,从术语表一查,翻到所在的页数即可。倒排索引在全文搜索引擎中经常用到,比如ElasticSearch一个核心的机制就是倒排索引;Prometheus的时序数据库按标签查询时,也是在用倒排索引。 数据库主键之争:自增长 vs UUID。主键是很多数据库非常重要的索引,尤其是MySQL这样的RDBMS会经常面临这个难题:是用自增长的ID还是随机的UUID做主键? 自增长ID的性能最高,但不好做分库分表后的全局唯一ID,自增长的规律可能泄露业务信息;而UUID不具有可读性且太占存储空间。争执的结果就是找一个兼具二者的优点的折衷方案:用雪花算法生成分布式环境全局唯一的ID作为业务表主键,性能尚可、不那么占存储、又能保证全局单调递增,但雪花算法又引入了额外的复杂性,再次体现了取舍之道。 再回到数据库中的索引,建索引要注意哪些点呢? 定义好主键并尽量使用主键,多数数据库中,主键是效率最高的聚簇索引;在Where或Group By、Order By、Join On条件中用到的字段也要按需建索引或联合索引,MySQL中搭配explain命令可以查询DML是否利用了索引;类似枚举值这样重复度太高的字段不适合建索引(如果有位图索引可以建),频繁更新的列不太适合建索引;单列索引可以根据实际查询的字段升级为联合索引,通过部分冗余达到索引覆盖,以避免回表的开销;尽量减少索引冗余,比如建A、B、C三个字段的联合索引,Where条件查询A、A and B、A and B and C 都可以利用该联合索引,就无需再给A单独建索引了;根据具体的数据库特有的索引特性选择适合的方案,比如像MongoDB可以建自动删除数据的TTL索引、不索引空值的稀疏索引、地理位置信息的Geo索引等等 缓存 缓存优化性能的原理和索引一样,是拿额外的存储空间换取查询时间,理论依据是程序的局部性原理。缓存无处不在,设想一下我们在浏览器打开这篇文章,会有多少层缓存呢? 首先解析DNS时,浏览器一层DNS缓存、操作系统一层DNS缓存、DNS服务器链上层层缓存;发送一个GET请求这篇文章,服务端很可能早已将其缓存在KV存储组件中了;即使没有击中缓存,数据库服务器内存中也缓存了最近查询的数据;即使没有击中数据库服务器的缓存,数据库从索引文件中读取,操作系统已经把热点文件的内容放置在Page Cache中了;即使没有击中操作系统的文件缓存,直接读取文件,大部分固态硬盘或者磁盘本身也自带高速缓存;数据取到之后服务器用模板引擎渲染出HTML,模板引擎早已解析好缓存在服务端内存中了;历经数十毫秒之后,终于服务器返回了一个渲染后的HTML,浏览器端解析DOM树,发送请求来加载JS/CSS等静态资源;需要加载的静态资源可能因Cache-Control在浏览器本地磁盘和内存中已经缓存了;即使本地缓存到期,也可能因Etag没变服务器告诉浏览器304 Not Modified继续缓存;即使Etag变了,静态资源服务器也因其他用户访问过早已将文件缓存在内存中了;加载的JS文件会丢到JS引擎执行,其中可能涉及的种种缓存就不再展开了;整个过程中链条上涉及的所有的计算机和网络设备,执行的热点代码和数据很可能会载入CPU的多级高速缓存。 这里列举的仅仅是一部分常见的缓存,就有多种多样的形式:从廉价的磁盘到昂贵的CPU高速缓存,最终目的都是用额外的空间来换取宝贵的时间。 缓存是“银弹”吗? 不,Phil Karlton 曾说过: 计算机科学中只有两件困难的事情:缓存失效和命名规范。There are only two hard things in Computer Science: cache invalidation and naming things.缓存的使用除了带来额外的复杂度以外,还面临如何处理缓存失效的问题。 多线程并发编程需要用各种手段(比如Java中的synchronized volatile)防止并发更新数据,一部分原因就是防止线程本地缓存的不一致;缓存失效衍生的问题还有:缓存穿透、缓存击穿、缓存雪崩。解决黑客用不存在的Key来穿透攻击的问题,需要用空值缓存或布隆过滤器;解决单个缓存过期后,瞬间被大量恶意查询击穿的问题需要做查询互斥;解决某个时间点大量缓存同时过期的雪崩问题,需要添加随机TTL等等;热点数据如果是多级缓存,在发生修改时需要清除或修改各级缓存,这些操作往往不是原子操作,又会涉及各种不一致问题。 除了通常意义上的缓存外,对象重用的池化技术,也可以看作是一种缓存的变体。常见的诸如JVM,V8这类运行时的常量池、数据库连接池、HTTP连接池、线程池、Golang的sync.Pool对象池等等。在需要某个资源时从现有的池子里直接拿一个,稍作修改或直接用于另外的用途,池化重用也是性能优化常见手段。 名词:布隆过滤器 TODO 压缩 说完了两个“空间换时间”的,我们再看一个“时间换空间”的办法——压缩。压缩的原理是消耗计算的时间,换一种更紧凑的编码方式来表示数据。 为什么要拿时间换空间?时间不是最宝贵的资源吗? 举一个视频网站的例子,如果不对视频做任何压缩编码,因为带宽有限,巨大的数据量在网络传输的耗时会比编码压缩的耗时多得多。对数据的压缩虽然消耗了时间去换取更小的空间存储,但更小的存储空间会在另一个维度带来更大的时间收益。 这个例子本质上是:“操作系统内核与网络设备处理负担 vs 压缩解压的计算负担”的权衡和取舍。 我们在通常使用的是无损压缩,比如下面这些场景: HTTP协议中Accept-Encoding添加Gzip/deflate,服务端对接受压缩的文本(JS/CSS/HTML)请求做压缩,大部分图片格式本身已经是压缩的无需压缩;HTTP2协议的头部HPACK压缩;JS/CSS文件的混淆和压缩(Uglify/Minify);一些RPC协议和消息队列传输的消息中,采用二进制编码和压缩(Gzip、Snappy、LZ4等等);缓存服务存体积过大的数据,通常也会事先压缩一下再存;大文件的存储,或者不常用的历史数据存储,采用更高压缩比的算法存储;JVM的对象指针压缩,JVM在32G以下的堆内存情况下默认开启“UseCompressedOops”,用4个byte就可以表示一个对象的指针,这也是JVM尽量不要把堆内存设置到32G以上的原因;MongoDB的二进制存储的BSON相对于纯文本的JSON也是一种压缩,或者说更紧凑的编码。但更紧凑的编码也意味着更差的可读性,这一点也是需要取舍的。纯文本的JSON比二进制编码要更占存储空间,却是REST API的主流,因为数据交换的场景下可读性是非常重要的。 信息论告诉我们,无损压缩的极限是信息熵。进一步减小体积只能以损失部分信息为代价,也就是有损压缩。 那么,有损压缩有哪些应用呢? 预览和缩略图,低速网络下视频降帧、降清晰度,都是对信息的有损压缩;音视频等多媒体数据的采样和编码大多是有损的,比如MP3是利用傅里叶变换,有损地存储音频文件;jpeg等图片编码也是有损的。虽然有像WAV/PCM这类无损的音频编码方式,但多媒体数据的采样本身就是有损的,相当于只截取了真实世界的极小一部分数据;散列化,比如K-V存储时Key过长,先对Key执行一次“傻”系列(SHA-1、SHA-256)哈希算法变成固定长度的短Key。另外,散列化在文件和数据验证(MD5、CRC、HMAC)场景用的也非常多,无需耗费大量算力对比完整的数据。除了有损/无损压缩,但还有一个办法,就是压缩的极端——从根本上减少数据或彻底删除。 能减少的就减少: JS打包过程“摇树”,去掉没有使用的文件、函数、变量;使用更“简洁”的通信协议,比如开启HTTP/2和高版本的TLS,减少了Round Trip,节省了TCP连接,自带大量性能优化;减少不必要的信息传输,比如Cookie的数量,去掉不必要的HTTP请求头;更新采用增量更新,比如HTTP的PATCH,只传输变化的属性而不是整条数据;缩短单行日志的长度、缩短URL、在具有可读性情况下用短的属性名等等;使用位图和位操作,用风骚的位操作最小化存取的数据。典型的例子有:用Redis的位图来记录统计海量用户登录状态;布隆过滤器用位图排除不可能存在的数据;大量开关型的设置的存储等等。 能删除的就删除: 删掉不用的数据;删掉不用的索引;删掉不该打的日志;删掉不必要的通信,不去发不必要的HTTP、RPC请求或调用,轮询改发布订阅;终极方案:砍掉整个功能。 No code is the best way to write secure and reliable applications. Write nothing; deploy nowhere. —— Kelsey Hightower 预取 预取通常搭配缓存一起用,其原理是在缓存基础上更进一步,再加上一次“时间换时间”,也就是:用事先预取的耗时,换取第一次加载的时间。当可以猜测出以后的某个时间很有可能会用到某种数据时,把数据预先取到需要用的地方,能大幅度提升用户体验或服务端响应速度。 预取作为提升性能的手段会在哪些场景用呢? 视频或直播类网站,在播放前先缓冲一小段时间,就是预取数据。有的在播放时不仅预取这一条数据,甚至还会预测下一个要看的其他内容,提前把数据取到本地;HTTP/2 Server Push,在浏览器请求某个资源时,服务器顺带把其他相关的资源一起推回去,HTML/JS/CSS几乎同时到达浏览器端,相当于浏览器被动预取了资源;一些客户端软件会用常驻进程的形式,提前预取数据或执行一些代码,这样可以极大提高第一次使用的打开速度;服务端同样也会用一些预热机制,一方面热点数据预取到内存提前形成多级缓存;另一方面也是对运行环境的预热,载入CPU高速缓存、热点函数JIT编译成机器码等等;热点资源提前预分配到各个服务实例,比如:秒杀、售票的库存性质的数据;分布式唯一ID等等。天上不会掉馅饼,预取也是有副作用的。正如烤箱预热需要消耗时间和额外的电费,在软件代码中做预取/预热的副作用通常是启动慢一些、占用一些闲时的计算资源、可能取到的不一定是后面需要的。 削峰填谷术 削峰填谷的原理也是“时间换时间”,谷时换峰时。削峰填谷与预取是反过来的:预取是事先花时间做,削峰填谷是事后花时间做。就像三峡大坝可以抗住短期巨量洪水,事后雨停再慢慢开闸防水。软件世界的“削峰填谷”是类似的,只是不用三峡大坝实现,而是用消息队列、异步化等方式。 常见的有这几类问题,我们分别来看每种对应的解决方案: 针对前端、客户端的启动优化或首屏优化:代码和数据等资源的延时加载、分批加载、后台异步加载、或按需懒加载等等。背压控制——限流、节流、去抖等手段。“一夫当关,万夫莫开”,从入口处削峰,防止一些恶意的重复请求以及请求过于频繁的爬虫,甚至是一些DDoS攻击。简单做法有网关层根据单个IP或用户用漏桶控制请求速率和上限;前端做按钮的节流去抖防止重复点击;网络层开启TCP SYN Cookie防止恶意的SYN洪水攻击等等。彻底杜绝爬虫、黑客手段的恶意洪水攻击是很难的,DDoS这类属于网络安全范畴了。针对正常的业务请求”洪峰“,用消息队列暂存再异步化处理:常见的后端消息队列Kafka、RocketMQ甚至Redis等等都可以做缓冲层,第一层业务处理直接校验后丢到消息队列中,在洪峰过去后慢慢消费消息队列中的消息,执行具体的业务。另外执行过程中的耗时和耗计算资源的操作,也可以继续丢到消息队列或数据库中,等到谷时处理。捋平毛刺:有时候“洪峰”不一定来自外界,如果系统内部大量定时任务在同一时间执行,或与业务高峰期重合,很容易在监控中看到“毛刺”——短时间负载极高。一般解决方案就是错峰执行定时任务,或者分配到其他非核心业务系统中,把“毛刺”摊平。比如很多数据分析型任务都放在业务低谷期去执行,大量定时任务在创建时尽量加一些随机性来分散执行时间。避免错误风暴带来的次生“洪峰”:有时候网络抖动或短暂宕机,业务会出现各种异常或错误。这时处理不好很容易带来次生灾害,比如:很多代码都会做错误重试,不加控制的大量重试,甚至会导致网络抖动恢复后的瞬间,积压的大量请求再次冲垮整个系统;还有一些代码没有做超时、降级等处理,可能导致大量的等待耗尽TCP连接,进而导致整个系统被冲垮。解决之道就是做限定次数、间隔指数级增长的Back-Off重试,设定超时、降级策略。 批量处理术 批量处理同样可以看成“时间换时间”,其原理是减少了重复的事情,是一种对执行流程的压缩。以个别批量操作更长的耗时为代价,在整体上换取了更多的时间。 批量处理的应用也非常广泛,我们还是从前端开始讲: 打包合并的JS文件、雪碧图等等,都是将一批资源集中到一起,一次性传输;前端使用requestAnimationFrame在UI渲染时批量处理积压的变化,而不是有变化立刻更新,在游戏开发中也有类似的应用;前后端中使用队列暂存临时产生的数据,积压到一定数量再批量处理;在不影响可扩展性情况下,一个接口传输多种需要的数据,减少大量ajax调用(GraphQL在这一点就做到了极致);系统间通信尽量发送整批数据,比如消息队列的发布订阅、存取缓存服务的数据、RPC调用、插入或更新数据库等等,能批量做尽可能批量做,因为这些系统间通信的I/O时间开销已经很昂贵了;数据积压到一定程度再落盘,操作系统本身的写文件就是这么做的,Linux的fwrite只是写入缓冲区暂存,积压到一定程度再fsync刷盘。在应用层,很多高性能的数据库和K-V存储的实现都体现了这一点:一些NoSQL的LSM Tree的第一层就是在内存中先积压到一定大小再往下层合并;Redis的RDB结合AOF的落盘机制;Linux系统调用也提供了批量读写多个缓冲区文件的系统调用 readv/writev 等等;延迟地批量回收资源,比如JVM的Survivor Space的S0和S1区互换、Redis的Key过期的清除策略。 批量处理如此好用,那么问题来了,每一批放多大最合适呢? 这个问题其实没有定论,有一些个人经验可以分享。 前端把所有文件打包成单个JS,大部分时候并不是最优解。Webpack提供了很多分块的机制,CSS和JS分开、JS按业务分更小的Chunk结合懒加载、一些体积大又不用在首屏用的第三方库设置external或单独分块,可能整体性能更高。不一定要一批搞定所有事情,分几个小批次反而用户体验的性能更好。Redis的MGET、MSET来批量存取数据时,每批大小不宜过大,因为Redis主线程只有一个,如果一批太大执行期间会让其他命令无法响应。经验上一批50-100个Key性能是不错的,但最好在真实环境下用真实大小的数据量化度量一下,做Benchmark测试才能确定一批大小的最优值。MySQL、Oracle这类RDBMS,最优的批量Insert的批大小也视数据行的特性而定。我之前在2U8G的Oracle上用一些普遍的业务数据做过测试,批量插入时每批5000-10000条数据性能是最高的,每批过大会导致DML的解析耗时过长,甚至单个SQL语句体积超限,单批太多反而得不偿失。消息队列的发布订阅,每批的消息长度尽量控制在1MB以内,有些云服务商提供的消息队列限制了最大长度,那这个长度可能就是性能拐点,比如AWS的SQS服务对单条消息的限制是256KB。总之,多大一批可以确保单批响应时间不太长的同时让整体性能最高,是需要在实际情况下做基准测试的,不能一概而论。而批量处理的副作用在于:处理逻辑会更加复杂,尤其是一些涉及事务、并发的问题;需要用数组或队列用来存放缓冲一批数据,消耗了额外的存储空间。 空间 空间都去哪儿了?在计算机历史上,非易失存储技术的发展速度超过了摩尔定律。除了嵌入式设备、数据库系统等等,现在大部分场景已经不太需要优化持久化存储的空间占用了,这里主要讲的是另一个相对稀缺的存储形式 —— RAM,或者说主存/内存。 以JVM为例。 堆里面有很多我们创建的对象(Object)。 每个Object都有一个包含Mark和类型指针的Header,占12个字节每个成员变量,根据数据类型的不同占不同的字节数,如果是另一个对象,其对象指针占4个字节数组会根据声明的大小,占用N倍于其类型Size的字节数成员变量之间需要对齐到4字节,每个对象之间需要对齐到8字节如果在32G以上内存的机器上,禁用了对象指针压缩,对象指针会变成8字节,包括Header中的Klass指针,这也就不难理解为什么堆内存超过32G,JVM的性能直线下降了。 举个例子,一个有8个int类型成员的对象,需要占用48个字节(12+32+4),如果有十万个这样的Object,就需要占用4.58MB的内存了。这个数字似乎看起来不大,而实际上一个Java服务的堆内存里面,各种各样的对象占用的内存通常比这个数字多得多,大部分内存耗在char[]这类数组或集合型数据类型上。 堆内存外面,又是另一个世界了。 从操作系统进程的角度去看,也有不少耗内存的大户,不管什么Runtime都逃不开这些空间开销:每个线程需要分配MB级别的线程栈,运行的程序和数据会缓存下来,用到的输入输出设备需要缓冲区…… 代码“写出来”的内存占用,仅仅是冰山之上的部分,真正的内存占用比“写出来”的要更多,到处都存在空间利用率的问题。 比如,即使我们在Java代码中只是写了 response.getWriter().print(“OK”),给浏览器返回2字节,网络协议栈的层层封装,协议头部不断增加的额外数据,让最终返回给浏览器的字节数远超原始的2字节,像IP协议的报头部就至少有20个字节,而数据链路层的一个以太网帧头部至少有18字节。 如果传输的数据过大,各层协议还有最大传输单元MTU的限制,IPv4一个报文最大只能有64K比特,超过此值需要分拆发送并在接收端组合,更多额外的报头导致空间利用率降低(IPv6则提供了Jumbogram机制,最大单包4G比特,“浪费”就减少了)。 这部分的“浪费”有多大呢?下面的链接有个表格,传输1460个字节的载荷,经过有线到无线网络的转换,至少再添120个字节,空间利用率<92.4%。 这种现象非常普遍,使用抽象层级越高的技术平台,平台提供高级能力的同时,其底层实现的“信息密度”通常越低。像Java的Object Header就是使用JVM的代价,而更进一步使用动态类型语言,要为灵活性付出空间的代价则更大。哈希表的自动扩容,强大的反射能力等等,背后也付出了空间的代价。 再比如,二进制数据交换协议通常比纯文本协议更加节约空间。但多数厂家我们仍然用JSON、XML等纯文本协议,用信息的冗余来换取可读性。即便是二进制的数据交互格式,也会存在信息冗余,只能通过更好的协议和压缩算法,尽量去逼近压缩的极限 —— 信息熵。 Profile Guiided Optimization 错误分支预测优化 下面用简单的一个 if 判断语句来说明为什么减少错误分支预测可以实现正优化。看下面示例代码: if condition {// 执行逻辑1} else {// 执行逻辑2}在编译时,由于编译器并不能假设 condition 为 true 或者 false 的概率,所以按照定义的顺序:如果 condition 为 true 执行逻辑 1,如果条件不满足跳跃至 else 执行逻辑 2。在 CPU 的实际执行中,由于指令顺序执行以及 pipeline 预执行等机制,因此,会优先执行当前指令紧接着的下一条指令。上面的指令如果 condition 为 true 那么整个流水线便一气呵成,没有跳转的开销。相反的,如果 condition 为 false,那么 pipeline 中先前预执行的逻辑 1 计算则会被作废,转而需要从 else 处的重新加载指令,并重新执行逻辑 2,这些消耗会显著降低指令的执行性能。 如果在实际运行中,condition 为 true 的概率比较大,那么该代码片段会比较高效,反之则低效。借助对程序运行期的 pprof profile 数据进行采集,则可以得到上面的分支判断中,实际走 if 分支和走 else 分支的次数。借助该统计数据,在 PGO 编译中,若走 else 分支的概率较大(相差越大效果越明显),编译器便可以对输出的机器指令进行调整,使其生成的指令从而对 执行逻辑 2 更加有利。其实很简单比如汇编指令 je (等于就跳转) 等价替换成 jne(不等于就跳转)。 虚函数优化 C++的虚函数使用起来非常方便,代码的抽象层次也非常好,但是他还是有一定的开销相比普通函数,如果大量使用虚函数在性能要求非常高的场景对性能还是有一定的影响,主要体现在如下的方面: 空间开销:由于需要为每一个包含虚函数的类生成一个虚函数表,所以程序的二进制文件大小会相应的增大。其次,对于包含虚函数的类的实例来说,每个实例都包含一个虚函数表指针用于指向对应的虚函数表,所以每个实例的空间占用都增加一个指针大小(32 位系统 4 字节,64 位系统 8 字节)。这些空间开销可能会造成缓存的不友好,在一定程度上影响程序性能。 虚函数表查找:虚函数增加了一次内存寻址,通过虚函数指针找到虚函数表,有一点点开销但是还好。 间接调用(indirect call)开销:由于运行期的实际函数(或接口)代码地址是动态赋值的,机器指令无法做更多优化,只能直接执行 call 指令(间接调用)。对于直接调用而言,是不存在分支跳转的,因为跳转地址是编译器确定的,CPU 直接去跳转地址取后面的指令即可,不存在分支预测,这样可以保证 CPU 流水线不被打断。而对于间接寻址,由于跳转地址不确定,所以此处会有多个分支可能,这个时候需要分支预测器进行预测,如果分支预测失败,则会导致流水线冲刷,重新进行取指、译码等操作,对程序性能有很大的影响。 无法内联优化:由于 virtual 函数的实现本身是多态的,编译中无法得出实际运行期会执行的实现,因此也无法进行内联优化。同时在很多场景下,调用一个函数只是为了得到部分返回值或作用,但函数实现通常还执行了某些额外计算,这些计算本可以通过内联优化消除,由于无法内联,indirect call 会执行更多无效的计算。 阻碍进一步的编译优化:indirect call 相当于是指令中的一个屏障,由于其本身是一个运行期才能确定的调用,它在编译期会使各种控制流判断以及代码展开失效,从而限制进一步编译及链接的优化空间。 -> 去虚拟化 (不是虚拟机的虚拟,而是虚函数的虚拟) 使用final关键字去虚拟化 Basic devirtualization我们通过下面一个例子来简单说明编译器是如何去虚拟化的: 1class A { 2public: 3 virtual int foo() { return ; } 4}; 5 6class B : public A { 7public: 8 int foo() { return 2; } 9};1011int test(B* b) {12 return b->foo() + ; } 当调用 test(B *b)里面的 b->foo()函数时,编译器并不知道 b 是一个真正的 B 类型,还是 B 的子类型,所以编译生成的代码会包含间接调用。 而如果我们给class B的代码中增加final关键字。 1class B : public A {2public:3 int value() final { return 2; }4}; 这样编译器知道 class B 不可能有子类,可以进行去虚拟化优化(-fdevirtualize ). 猜测性去虚拟化 Speculative devirtualization根据实际运行情况,去推导去虚拟化。还是举一个简单的例子来说明下:A* ptr->foo(),ptr 是一个指针,他可以是 A 也可以是 B,甚至是他们的子类,编译器在编译无法确定其类型。假设在实际的生产环境中的,ptr 大概率是 A 对象,而不是 B 对象或者其子类对象,speculative devirtualization,gcc 的编译参数(-fdevirtualize-speculatively) 优化就会尝试进行如下的转换: 1if (ptr->foo == A::foo)2 A::foo ();3else4 ptr->foo (); 经过此转换后,将间接调用转换成直接调用,就可以进行直接调用优化,比如说 inline 等。 性能优化相关的书籍 http://book.easyperf.net/perf_book Optimizing software in C++: An optimization guide for Windows, Linux and Мас platforms Optimizing subroutines in assembly language: An optimization guide for x86 platforms The microarchitecture of Intel, AMD and VIA CPUs: An optimization guide for assemblyprogrammers and compiler makers Instruction tables: Lists of instruction latencies, throughputs and micro-operationbreakdowns for Intel, AMD and VIA CPUs Calling conventions for different C++ compilers and operating systemsSoftware optimization resources. C++ and assembly. Windows, Linux, BSD, Mac OS X。 简单的代码层面优化 整数运算效率高于浮点 除法和取余 在标准处理器中,一个32位的除法需要使用20至140次循环操作。对于ARM处理器,有的版本需要20+4.3N次循环。因此,可以通过乘法表达式来替代除法:比如循环里面需要一直除以一个数,可以在循环外先求出这个数的倒数,然后带入到循环内通过乘法计算如果知道被除数的和除数的符号,判断$\frac{a}{b} > c$,如果a,b为正,可以转换为 $a>b∗c$. 通过2的幂次进行除法和取余数如果除法中的除数是2的幂次,编译器会使用移位操作来执行除法。有符号signed的除法需要移位到0和负数,因此需要更多的时间执行。 其实很多技巧编译器都可以自动完成。。。 volatile volatile用于声明一个变量可被其他线程改变,阻止编译器依赖变量始终具有代码中先前分配的值的假设来进行优化 。 thread-local 大多数编译器可以使用关键字 __thread 或 __declspec(thread) 来实现静态变量和全局变量的线程本地存储。这样的变量对于每个线程都有一个实例 。线程本地存储是低效的,因为它是通过存储在线程访问块中的指针进行访问的。因此建议尽量避免线程本地存储,代之以stack存储。 进程上下文切换 从用户态切换到内核态需要通过系统调用来完成,这里就会发生进程上下文切换(特权模式切换),当切换回用户态同样发生上下文切换。 一般每次上下文切换都需要几十纳秒到数微秒的 CPU 时间,如果切换较多还是很容易导致 CPU 时间的浪费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,这里同样会导致系统平均负载升高。 Linux 为每个 CPU 维护一个就绪队列,将 R 状态进程按照优先级和等待 CPU 时间排序,选择最需要的 CPU 进程执行。这里运行进程就涉及了进程上下文切换的时机: 进程时间片耗尽、。进程在系统资源不足(内存不足)。进程主动sleep。有优先级更高的进程执行。硬中断发生 线程上下文切换 线程和进程: 当进程只有一个线程时,可以认为进程就等于线程。当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。所以线程上下文切换包括了 2 种情况: 不同进程的线程,这种情况等同于进程切换。通进程的线程切换,只需要切换线程私有数据、寄存器等不共享数据 /Proc user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越大,优先级反而越低。system(通常缩写为 sys),代表内核态 CPU 时间。idle(通常缩写为 id),代表空闲时间。注意,它不包括等待 I/O 的时间(iowait)。iowait(通常缩写为 wa),代表等待 I/O 的 CPU 时间。irq(通常缩写为 hi),代表处理硬中断的 CPU 时间。softirq(通常缩写为 si),代表处理软中断的 CPU 时间。steal(通常缩写为 st),代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。 小问题: 使得CPU为正弦曲线 编写程序查看自己的L1缓存大小 如何优化程序性能 单核 消除冗余: 代码重构,临时变量,数据结构与算法,池化,inline,RVO,vDSO,COW,延迟计算,零拷贝 名词:池化 名词:vDSO virtual dynamic shared object 快速系统调用之争在x86-32系统大行其道的时代,调用系统调用的方法就是int $0x80。这种方法的执行速度非常慢,原因是它需要经历一个完整的中断处理过程,这包括Linux内核以及与中断流程相关的处理器微码的执行开销。 后来为了提升系统调用的性能,Intel最先实现了专门的快速系统调用指令sysenter和系统调用返回指令sysexit;后来AMD针锋相对地实现了另一组专门的快速系统调用指令syscall和系统调用返回指令sysret。 快速系统调用的“快”字,体现在以下几个方面: 处理器在切换到内核态后不再自动往内核栈中自动保存任何上下文信息了,这样避免了访内开销。处理器也不再自动加载内核栈的值到rsp寄存器了,节省了指令开销。syscall和sysret指令只能用在平坦内存模型中,因此在执行快速系统调用时bypass了MMU的分段单元的检查,节省了微码的执行开销。处理器微码不再需要走中断处理和中断恢复流程,大幅度提高了执行性能。与Intel的快速系统调用指令相比,AMD的syscall/sysret要更快更灵活: 执行syscall和sysret指令时,不再需要处理器自动保存和恢复用户栈指针了,因此也不需要再事先设置MSR来指定要恢复和保存的用户栈指针了。通过系统调用进入内核态后,rflags寄存器中哪些位应该清0原本是固定的,但是如果是用syscall来执行系统调用的话,那这些位是可以通过编程来事先设置的。最后,Intel也提供了对sysenter/sysexit指令的支持。之后,为了获得最好的兼容性(Intel和AMD通用),x86-64 Linux内核将快速系统调用的支持方式统一到了syscall/sysret。 题外话:Intel要求在内核启动阶段必须事先设置好IA32_EFER.SCE位才能在64位处理器模式中使用syscall和sysret指令,否则会触发#GP异常。 吐槽:死傲娇!64位处理器模式都是人家AMD先出的,你还在乎个指令? linux性能优化思路 icache miss 减少调用栈深度 短小且多次用到的函数inline,省去栈帧变化开销 精简热路径代码 分支预测 likely/unlikely 减少参数,通过寄存器传参,尽量避免栈传参 dcache miss 思路: 尽量让数据在进入cache到出cache这段时间里被充分使用 保障代码局部性 时间局部性:刚访问的数据很快被再次访问 空间局部性:相邻数据很快被再访问 方法: 调整结构体字段:将短时间内可能同时访问的字段放在临近位置 cacheline对齐:关键结构需要cacheline对齐。(注意避免false sharing) 结构体最常访问字典放在最前面,最好不超过一个cacheline 少用全局变量。 TLB miss 大页 热点函数和数据集中在一个页 (代码段大页) Top-Down 性能分析模型 是什么:将 CPU pipeline 和指令执行效率联系在一起,是一个通用的程序性能评估模型。 概念: pipeline slot: 处理一个uops所需的硬件资源。每个核,在每个时钟周期有几个可用的pipeline slot,称为pipeline width IPC,英文全称“Instruction Per Clock”,中文翻译过来就是每个时钟的指令,即CPU每一时钟周期内所执行的指令多少. pipeline指令执行情况: Retiring:指令执行正常,经历了取指,译码,执行,回写.例如 对于4-width的机器,100% retiring 意味着IPC为4. Bad Speculation:预测错误导致pipeline slot被浪费掉。 branch mispredict:在分支预测错误时解码的指令。 Machine clear: 清空整个pipeline(锁竞争) Front End Bound:前端提供的指令不足,导致后端处于等待状态 fetch latency:指令延迟,可能因为itlb(cache) miss,或 pipeline 刷新 fetch bandwidth:指令带宽不足,程序的指令分支比重过大。 Back End Bound:后端缺乏必要的资源导致pipeline停顿: core bound:计算单元缺乏并行度。 memory bound:momory 无法及时提供数据。 C++优化:模块边界 众所周知,C++ 的一个源文件就是一个模块,而编译器以一个模块为一个编译单元,生成一个二进制目标文件。最后再将所有的目标文件链接到一起形成一个可执行程序或动态库。 但这种机制也导致了编译器很难跨模块优化,优化的操作被局限在一个源文件内。编译器无法知道这个模块内的操作与其他模块内操作的联系,是否可以打乱重排、合并或内联。最后模块之间通过查找符号名进行链接,但哪怕是链接融合到一起,其实也只是单纯的代码拷贝,链接器无力在二进制层面做进一步整体优化。这是历史遗留问题,早年的硬件内存不支持同时加载整个项目的源码进行编译,也是现在 C/C++ 一直为人所诟病的一点。 C/C++ 的编译系统和其他高级语言存在很大的差异,在其他高级语言中,无论是 Java,亦或是新兴的 Golang/Rust,都是以包(package)的形式来管理代码,包内的所有源码都会被放在一起编译,编译一个文件时能获得另一个文件里的源码内容。理论上不管是编译速度,还是整体优化效果,都会优于 C/C++。不过,也有一些应对措施。 header-only 一般来说,我们会将模块需要导出的数据结构和函数声明放在头文件中,这样其它模块就能知道这个模块提供了怎样的功能,但既然函数声明能放在头文件中,函数定义为什么不能也放在头文件中呢?使用 header-only 大概有这样几个动机: 方便分发,所有的代码都在头文件里,一个 include 就能使用,不需要考虑库的链接,非常省心C++ 模板实例化必须要有函数定义性能考虑:一些常用操作可能到处都在高频使用,属于是性能热点,将实现放在头文件中开放出去,有利于编译器进一步优化 Unity Build 将多个 c/cpp 文件组合在一起来编译,打破模块边界。 链接时优化(LTO) Unity Build 虽好,但仍有诸多限制。比如不同的两个代码文件中,全局命名空间下,都存在相同名字的全局变量和函数。那么 unity-build 就会带来编译冲突问题,编译器通常会报全局变量重定义错误。这时就需要手动修改代码。还是想要更加无感的方法,链接时是可以优化的! C++ 别名优化 -fstrict-aliasing allow the compiler to assume the strictest aliasing rules applicable to the language being compiled. For C (and C++), this activates optimizations based on the type of expressions. In particular, an object of one type is assumed never to reside at the same address as an object of a different type, unless the types are almost the same. The -fstrict-aliasing option is enabled at levels -O2, -O3, -Os. 简而言之就是当两个指针或引用的类型不一样时,编译器会假定它们不会指向同一块内存,方便编译器进一步优化代码。来看一个例子: 1void foo(int &a, int &b) {2 a = b + 1;3 a = b + 1;4} 因为类型相同,编译器不敢做进一步优化,进行了两次相同的操作,每次都从内存中重新取 b 的值,因为 a 和 b 的背后完全可能是同一个变量。 那肯定有人会问,我很明确知道两个变量背后不是同一地址,但是它们的类型就是一样,编译器有没有办法优化呢? 这时就轮到 restrict 出场了,restrict 属于 C 标准,但是并没有进入 C++ 标准,C++ 可以使用 __restrict,各大主流编译器都有实现。 但是需要注意文档中这样一句话: For example, an unsigned int can alias an int, but not a void* or a double. A character type may alias any other type. 也就是说,char 类型指针总是会被当做其它类型指针的别名,需要手动声明 restrict 程序喵 C++性能优化白皮书 作为一个程序员,想要性能优化,最好要了解些硬件,特别是CPU架构的一些知识点: 流水线分支预测寄存器重命名数据预取指令重排和乱序执行同时多线程(超线程)数据并行 SIMD 单指令多数据 还要了解CPU的特点: 一个处理器上,多条指令可能同时执行一个处理器上,代码的执行结果会和程序员可观察到的顺序一致,但其他处理器观察到的执行结果可能不是一个顺序顺序、无跳转的代码性能最高相邻且对齐的数据访问性能最高 链接期优化:link-time optimization LTO LTO可以: 跨编译单元的函数内联跨编译单元的程序整体优化死代码消除 关于C++语言层面的优化,可以在下面这些方向做优化: 优先栈内存,次之堆内存巧妙使用RAII管理资源移动语义虽然不好理解,但也可以巧妙使用移动语义减少对象的非必要拷贝模板和泛型技巧华而不实,给开发标准库的人使用还好,而且调试难度也较高,我们普通业务开发者只需要做到能看懂即可(个人见解)异常是可以考虑使用的,可以看看ISO C++网站和C++ 核心指南62,异常会导致程序的二进制体积有膨胀(5%-15%),异常不能代替所有的错误码,因为异常catch会使得程序性能下降。作者认为:使用异常对于大部分C++项目仍然适用,不使用异常的麻烦大于好处,除非真因为二进制文件和实时性方面的原因需要禁用异常。字符串默认类型时 const char[],传参时会退化成const char*,创建全局字符串最好使用const char[]标准库容器的方法至少提供了基本异常安全保证:要了解强异常安全保证和无异常保证。vector的移动构造函数标记为noexcept才会使用移动构造,移动构造函数需要标记为noexcept,如果没有标记,代码性能可能会有较大的负面影响。shared_ptr构造优先使用make_shared 了解function,function用作回调很方便,支持类型擦除,它还有个好处,可以用来存储带状态的函数对象,不像C语言那样需要个void*存储状态。但需要了解它的开销,貌似48个字节是个坎。 堆内存管理:可以了解下jemalloc mimalloc tcmalloc12.输入输出流可以考虑使用ios_base::sync_with_stdio(false)关闭同步,性能会提升,也最好使用\n取代endl,免得频繁刷新缓冲区。可考虑使用fmt 并发 需要了解内存序的概念一些优质的多生产者多消费者并发队列moodycamed::ConcurrentQueueatomic_queueFolly中MPMCQueue标准库也有些并行策略:execution::seq 序列执行,不可并行par:可并行化par_unseq:可并行化 向量化unseq:可向量化 名词:NUMA CPU的各级缓存 首先说说L1i和L1d的区别,i指的是instruction指令缓存,d是数据data缓存。作为冯·诺依曼体系的计算机,x86价格的指令和数据在内存中是统一管理的。但由于两者内容访问特性的不同(指令刷新率更低且不会被复写),L1的缓存是做了区分的。当前的Intel平台中L1缓存的时延为3个时钟周期,以2.0GHz的CPU计算约1.5纳秒。这种级别的时延可以极大的加速超线程以及CPU分支预测带来的性能优势。L2缓存的时延是L1的5倍左右,即8ns。每个CPU的物理核心都有自己独立的L2缓存空间。而L3的时延在50~70个时钟周期,30ns。不同于L2,L3缓存是多个核心共享的,L3在使用场景中最大的用途是减少数据回写内存的频率,加速多核心之间的数据同步。说到L3的“多核心之间”共享,传统的设计是每个CPU插槽或者一块硅片共享一个L3空间,由于内存空间地址是唯一的,这就可能牵扯到同一份内存地址存在两份相同的缓存内容。MESI状态控制就是为了同步数据在多个L3空间之间的流转而设置的。 内存和cpu cache的访问统一都是64byte对齐的。也就是说即便你只需要读取1bit的数据,CPU还是会把64byte的数据从内存逐步扔到L1。 CPU中事实上还存在着TLB(Translation Look aside Buffer,页表缓存)的组件类似于cache的功能。它主要负责缓存页表逻辑到物理地址的对应关系。跟L1类似,TLB也分为iTLB和dTLB分别对应了指令页表和数据页表的地址转换结果。 inclusive cache和Non-inclusive cache的区别 名称已经很直白了,inclusive/non-inclusive就是数学上的“包含”和“不包含”关系。 目前的趋势是逐步从inclusive cache向non-inclusive转变。 如果有了解过LRU,你会发现如果把所有的cache理解为一个整体的话,其实每次的数据读取都会伴随着其他缓存数据的更新。传统上的多层cache考虑到实现难度,严格要求数据保持L3-L2-L1各有一份拷贝。数据update后,一旦被LRU踢出当前缓存则合并更新到下级缓存。L3的大小即为所有缓存数据的最大容量。另一方面,CPU在不断的演进之后,core的数目越来越多。从前面的内容中你可以知道每个CPU核心都会有独立的L1/L2。那理论上如果继续沿用inclusive L3,L3的容量就必须大大于所有核心上L1/L2的总和才有意义。显然这将会是设计瓶颈。于是就有了non-inclusive的L3——其实non-inclusive的L2很早就有了。 方便你理解的话:non-inclusive cache意味着下级cache事实上是上级cache的回收站。当上级缓存的数据被踢出的时候,踢出的数据回写下级cache。以skylake为例L1没有命中的情况: L2 miss,跟L3逻辑上同级的SF(snoop filter)记录了各个L2中数据的状态。检查L3和snoop filter记录,获得L3或者其他L2是否有所需数据。 数据不存在于L3和sf时:数据从内存直接载入L2。L1由L2获取数据后将该cache line与当前L1中的旧数据交换空间。淘汰的L1数据踢出了L2中的旧数据,数据将从L2写入L3。L3获得L2淘汰数据并保存,踢出一份旧数据并检查是否需要回写内存。最后更新snoop filter中L2的数据变化。 数据存在于L3时,像是一个两两交换位置的过程(然而并不是真的两两交换)。数据从L3载入L2。L1由L2获取数据后将该cache line与当前L1中的旧数据交换空间。淘汰的L1数据踢出了L2中的旧数据,L2旧数据将从L2回写L3,更新SF。 数据只存在于sf,意味着当前数据正在被其他核心的L1/L2缓存,有概率内存中是脏数据(已经更新但没有回写内存)CPU将触发MESI流程确保全局一致性(参见:缓存一致性保障一文) Front end bound 优化 Front-End的职责即取指令(可能会根据预测提前取指令)、解码、分发给后端pipeline, 它的性能受限于两个方面,latency和bandwidth。对于latency,一般就是取指令(比如L1 ICache、iTLB未命中或解释型编程语言python\java等)、decoding (一些特殊指令或者排队问题)导致延迟。当Front-End 受限了,pipeline利用率就会降低。对于BandWidth 将它划分成了MITE,DSB和LSD三个子类。 代码尽可能减少代码的footprint:C/C++可以利用编译器的优化选项来帮助优化,比如GCC -O* 都会对footprint进行优化或者通过指定-fomit-frame-pointer也可以达到效果 充分利用CPU硬件特性:宏融合(macro-fusion)宏融合特性可以将2条指令合并成一条微指令,它能提升Front-End的吞吐。所以建议循环条件中的类型采用无符号的数据类型可以使用到宏融合特性提升Front-End 吞吐量。 调整代码布局(co-locating-hot-code):①充分利用编译器的PGO 特性:-fprofile-generate -fprofile-use ②可以通过__attribute__ ((hot)) attribute ((code)) 来调整代码在内存中的布局,hot的代码 在解码阶段有利于CPU进行预取。 分支预测① 消除分支可以减少预测的可能性能:比如小的循环可以展开比如循环次数小于64次(可以使用GCC选项 -funroll-loops) ② 尽量用if 代替:? ,不建议使用a=b>0? x:y 因为这个是没法做分支预测的 ③ 尽可能减少组合条件,使用单一条件比如:if(a||b) {}else{} 这种代码CPU没法做分支预测的 ④对于多case的switch,尽可能将最可能执行的case 放在最前面 ⑤ 我们可以根据其静态预测算法投其所好,调整代码布局,满足以下条件: 1bool is_expect = true; 2 if(is_expect) { 3 // 被执行的概率高代码尽可能放在这里 4 } else { 5 // 被执行的概率低代码尽可能放在这里 6 } 7// 后置条件,使条件分支的具有向后目标的分支不太可能的目标 8 9 do {10 // 这里的代码尽可能减少运行11 } while(conditions); back end bound优化 这一类别的优化涉及到CPU Cache的使用优化,CPU cache[14]它的存在就是为了弥补超高速的 CPU与DRAM之间的速度差距。CPU 中存在多级cache(register\L1\L2\L3) ,另外为了加速virtual memory address 与 physical address 之间转换引入了TLB。 如果没有cache,每次都到DRAM中加载指令,那这个延迟是没法接受的。 优化建议:调整算法减少数据存储,减少前后指令数据的依赖提高指令运行的并发度根据cache line调整数据结构的大小避免L2、L3 cache伪共享 合理使用缓存行对齐CPU的缓存是弥足珍贵的,应该尽量的提高其使用率,平常使用过程中可能存在一些误区导致CPU cache有效利用率比较低。 缓存行对齐使用原则: 多个线程存在同时写一个对象、结构体的场景(即存在伪共享的场景)对象、结构体过大的时候将高频访问的对象属性尽可能的放在对象、结构体首部 伪共享 前面主要是缓存行误用的场景,这里介绍下如何利用缓存行解决SMP 体系下的伪共享(false shared)。多个CPU同时对同一个缓存行的数据进行修改,导致CPU cache的数据不一致也就是缓存失效问题。为什么伪共享只发生在多线程的场景,而多进程的场景不会有问题?这是因为linux 虚拟内存的特性,各个进程的虚拟地址空间是相互隔离的,也就是说在数据不进行缓存行对齐的情况下,CPU执行进程1时加载的一个缓存行的数据,只会属于进程1,而不会存在一部分是进程1、另外一部分是进程2。 伪共享之所以对性能影响很大,是因为他会导致原本可以并行执行的操作,变成了并发执行。这是高性能服务不能接受的,所以我们需要对齐进行优化,方法就是CPU缓存行对齐(cache line align)解决伪共享,本来就是一个以空间换取时间的方案。 Bad Speculation分支预测当Back-End 删除了微指令,就出现Bad Speculation,这意味着Front-End 对这些指令所作的取指令、解码都是无用功,所以为什么说开发过程中应该尽可能的避免出现分支或者应该提升分支预测准确度能够提升服务的性能。虽然CPU 有BTB记录历史预测情况,但是这部分cache 是非常稀缺,它能缓存的数据非常有限。 分支预测在Font-End中用于加速CPU获取指定的过程,而不是等到需要读取指令的时候才从主存中读取指令。Front-End可以利用分支预测提前将需要预测指令加载到L2 Cache中,这样CPU 取指令的时候延迟就极大减小了,所以这种提前加载指令时存在误判的情况的,所以我们应该避免这种情况的发生,c++常用的方法就是: 在使用if的地方尽可能使用gcc的内置分支预测特性。 避免间接跳转或者调用 在c++中比如switch、函数指针或者虚函数在生成汇编语言的时候都可能存在多个跳转目标,这个也是会影响分支预测的结果,虽然BTB可改善这些但是毕竟BTB的资源是很有限的。 [1] CPI(cycle per instruction) 平均每条指令的平均时钟周期个数 [2] IPC (instruction per cycle) 每个CPU周期的指令吞吐数 [3] uOps 现代处理器每个时钟周期至少可以译码 4 条指令。译码过程产生很多小片的操作,被称作微指令(micro-ops, uOps) [4] pipeline slot pipeline slot 表示用于处理uOps 所需要的硬件资源,TMAM中假定每个 CPU core在每个时钟周期中都有多个可用的流水线插槽。流水线的数量称为流水线宽度。 [5] MIPS(MillionInstructions Per Second) 即每秒执行百万条指令数 MIPS= 1/(CPI×时钟周期)= 主频/CPI [6]cycle 时钟周期:cycle=1/主频 [7] memory footprint 程序运行过程中所需要的内存大小.包括代码段、数据段、堆、调用栈还包括用于存储一些隐藏的数据比如符号表、调试的数据结构、打开的文件、映射到进程空间的共享库等。 [8] MITE Micro-instruction Translation Engine [9]DSB Decode stream Buffer 即decoded uop cache [10]LSD Loop Stream Detector

2023/10/12
articleCard.readMore

New Macbook Setup

Install HomeBrew see https://brew.sh/. 增加软件仓库 1brew tap homebrew/cask-fonts 2brew tap homebrew/cask 在增加了homebrew/cask之后,安装cask中的软件不需要brew install --cask, 只需要直接brew install即可。 但是官方不建议这么做,官方做法是使用brew install --cask XXX Use homebrew to install Apps 1## Install Rust CLI Apps2brew install bat fd wget bottom lsd ripgrep3## Install cask Apps4brew install --cask google-chrome visual-studio-code wechat qq iterm2 iina Install snap With Snap App, you can use Command ⌘ + 1 to call the first app in your dock. 从App Store下载。这是免费的。 Install Flow Flow可以定时提醒自己休息。从App Store下载或者brew安装都可以。 Tex 安装MacTex即可。 1brew install --cask mactex 下载字体 1brew install font-jetbrains-mono-nerd-font2brew install font-jetbrains-mono 配置iterm2和zsh 在~/.zshrc中配置alias ins="brew install" 1ins zsh-fast-syntax-highlighting2ins zsh-autosuggestions3ins zsh-completions4ins zsh-history 安装了之后需要在zshrc中配置才能生效如下 1 2source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh 3 4if type brew &>/dev/null; then 5 FPATH=$(brew --prefix)/share/zsh-completions:$FPATH 6 zstyle ':completion:*' matcher-list '' 'm:{a-zA-Z}={A-Za-z}' 'r:|=*' 'l:|=* r:|=*' 7 autoload -Uz compinit 8 compinit 9fi1011source /opt/homebrew/opt/zsh-fast-syntax-highlighting/share/zsh-fast-syntax-highlighting/fast-syntax-highlighting.plugin.zsh 安装zoxide zoxide会记忆你最近访问的目录。例如你最近访问过~/aaa/bb/banana,那么不管你当前处在哪一个目录,你都可以用z ba来跳转到banana目录下吗。z b, z ba,z ban都是可以的。安装方法:ins zoxide之后在zshrc中配置eval "$(zoxide init zsh)" 安装p10k 参考https://github.com/romkatv/powerlevel10k 使终端中的option ⌥ + ← 可以实现跨单词跳转 在zshrc中配置 1bindkey "\e\e[D" backward-word2bindkey "\e\e[C" forward-word 使用espanso espanso是一个键盘快捷键映射软件。例如在任何地方输入:date会自动变为当前的日期。输入:name会自动显示自己的姓名等。

2023/10/5
articleCard.readMore

Mogan_note4

如何画曲线 1public: 2 inline curve_rep () {} 3 inline virtual ~curve_rep () {} 4 5 inline virtual int nr_components () { return 1; } 6 // the number of components of the curve is useful for getting 7 // nice parameterizations when concatenating curves 8 9 virtual point evaluate (double t)= 0;10 // gives a point on the curve for its intrinsic parameterization11 // curves are parameterized from 0.0 to 1.01213 array<point> rectify (double eps);14 // returns a rectification of the curve, which, modulo reparameterization15 // has a uniform distance of at most 'eps' to the original curve1617 virtual void rectify_cumul (array<point>& a, double eps)= 0;18 // add rectification of the curve (except for the starting point)19 // to an existing polysegment2021 /*22 NOTE: more routines should be added later so that one23 can reliably compute the intersections between curves24 One might for instance take the following:25 */26 virtual double bound (double t, double eps)= 0;27 // return delta such that |t' - t| < delta => |c(t') - c(t)| < eps.2829 virtual point grad (double t, bool& error)= 0;30 // compute the first derivative at t.31 // set error= true if this derivative does not exist.3233 virtual double curvature (double t1, double t2)= 0;34 // compute a bound for the second derivative between t1 and t2.35 /* FIXME: What is computed is *really* a bound for the curvature,36 not for the norm of the second derivative. Make it precise37 what it is that is computed exactly. */38 // return a very large number if such a bound does not exist.3940 // returns the number of control points which belong to the curve.41 // these control points are ordered and come first in pts & cips.42 virtual int get_control_points (array<double>& abs, array<point>& pts,43 array<path>& cip);4445 virtual array<double> find_closest_points (double t1, double t2, point p,46 double eps);4748 virtual double find_closest_point (double t1, double t2, point p, double eps,49 bool& found); 理解这段代码并用中文解释 这段代码定义了一个名为curve_rep的类,它是abstract_struct类的子类。以下是对代码的中文解释: curve_rep类具有默认构造函数和虚析构函数。 nr_components()函数返回曲线的组件数,对于连接曲线时获取良好的参数化很有用。 evaluate(double t)函数根据曲线的内在参数化,在曲线上给出一个点。曲线的参数化范围是从0.0到1.0。 rectify(double eps)函数返回曲线的修正,它与原曲线的距离在重新参数化的情况下最多为’eps’。 rectify_cumul(array& a, double eps)函数将曲线的修正(除起始点外)添加到现有的多段线中。 bound(double t, double eps)函数返回一个delta值,使得当|t’ - t| < delta时,|c(t’) - c(t)| < eps。它用于计算曲线之间的交点。 grad(double t, bool& error)函数计算t处的一阶导数,并设置error为true,如果该导数不存在。 curvature(double t1, double t2)函数计算t1和t2之间的二阶导数的界限。如果不存在这样的界限,它将返回一个非常大的数。 get_control_points(array& abs, array& pts, array& cip)函数返回属于曲线的控制点的数量。这些控制点在pts和cip中按顺序排列。 find_closest_points(double t1, double t2, point p, double eps)函数查找曲线上与给定点p最接近的点,并返回其参数值的数组。 find_closest_point(double t1, double t2, point p, double eps, bool& found)函数查找曲线上与给定点p最接近的点,并返回其参数值。如果找到了最接近的点,将设置found为true。 这段代码定义了一个抽象的曲线表示类,提供了计算曲线上点、修正、交点计算、曲率计算等功能的接口。具体的实现将在该类的派生类中完成。

2023/9/3
articleCard.readMore

TeXmacs/Mogan Note 3

修改为两点画圆 研究三点画圆的代码实现 菜单中选择画圆的时候使用 1("Circle" (graphics-set-mode '(edit carc))) 但是edit carc没有继续出现。因此推测是得到mode之后判断,如果第一个值是edit则统一处理,不需要分别处理。 以 1(tm-define (edit_right-button mode x y)2 (:require (== mode 'edit))3 (:state graphics-state)4 (set-texmacs-pointer 'graphics-cross)5 (when current-obj6 (graphics-delete))) 为例子。 它在这里被调用 1(tm-define (graphics-release-right x y)2 ;;(display* "Graphics] Release-right " x ", " y "\n") 3 (when (not (inside-graphical-text?))4 (edit_right-button (car (graphics-mode)) x y))) 可以看到确实是按照(car (graphics-mode))来进行调用。 研究如何通过菜单进行markup中的两点画圆 1(menu-bind graphics-focus-menu 2 (-> (eval (upcase-first (gr-mode->string (graphics-mode)))) 3 (link graphics-mode-menu)) 4 (if (inside-graphical-over-under?) 5 ("Exit graphics" (graphics-exit-right))) 6 (assuming (nnot (tree-innermost overlays-context?)) 7 (link graphics-focus-overlays-menu)) 8 (assuming (nnull? (graphics-mode-attributes (graphics-mode))) 9 ---10 (assuming (graphics-mode-attribute? (graphics-mode) "color")11 (-> "Color" (link graphics-color-menu)))12 (assuming (graphics-mode-attribute? (graphics-mode) "fill-color")13 (-> "Fill color" (link graphics-fill-color-menu)))14 (assuming (graphics-mode-attribute? (graphics-mode) "opacity")15 (assuming (== (get-preference "experimental alpha") "on")16 (-> "Opacity" (link graphics-opacity-menu))))17 (assuming (graphics-mode-attribute? (graphics-mode) "pen-enhance")18 (-> "Enhance" (link graphics-pen-enhance-menu))) 这是焦点工具栏,在菜单中的版本。 1(tm-menu (graphics-property-icons) 这个是图形界面中的第三栏。除了属性卡之外的选项们。 1(tm-menu (graphics-icons) 2 (link graphics-global-icons) 3 / 4 (link graphics-insert-icons) 5 / 6 (link graphics-group-icons)) 7 8(tm-menu (graphics-focus-icons) 9 (mini #t10 (=> (balloon (eval (upcase-first (gr-mode->string (graphics-mode))))11 "Current graphical mode")12 (link graphics-mode-menu)))13 (assuming (nnot (tree-innermost overlays-context?))14 (link graphics-focus-overlays-icons))15 (assuming (nnull? (graphics-mode-attributes (graphics-mode)))16 (link graphics-property-icons))17 (assuming (graphics-get-anim-type)18 /19 (mini #t20 (group "Status:")21 (=> (eval (graphics-get-anim-type))22 (link graphics-anim-type-menu))))23 /24 (link graphics-snap-icons)) 这是整体。 则事实上要找的含有markup选项的菜单为 1(menu-bind graphics-mode-menu2 ("Point" (graphics-set-mode '(edit point)))3 ("Line" (graphics-set-mode '(edit line)))4 ("Polygon" (graphics-set-mode '(edit cline)))5 (-> "Curve" 其中含有插件的为 1(assuming (style-has? "std-markup-dtd") 2 (with u '(arrow-with-text arrow-with-text*) 3 (with l (list-filter u (lambda (s) (style-has? (symbol->string s)))) 4 (for (tag (sort l symbol<=?)) 5 ((eval (upcase-first (symbol->string tag))) 6 (import-from (graphics graphics-markup)) 7 (graphics-set-mode `(edit ,tag)))))) 8 (with u (list-difference gr-tags-user '(arrow-with-text arrow-with-text*)) 9 (with l (list-filter u (lambda (s) (style-has? (symbol->string s))))10 (assuming (nnull? l)11 ---12 (for (tag (sort l symbol<=?))13 ((eval (upcase-first (symbol->string tag)))14 (import-from (graphics graphics-markup))15 (graphics-set-mode `(edit ,tag))))))))

2023/8/2
articleCard.readMore

Change0729

Now I changed the theme of this blog to https://github.com/luizdepra/hugo-coder. Also I modified the theme a little to make it looks better.

2023/7/29
articleCard.readMore

Vscode 返回上一个位置快捷键

非常有用的一个功能。 Windows: $Alt + \leftarrow$ Linux: $Control + Alt + -$

2023/7/22
articleCard.readMore

Texmacs/Mogan Note 2

23_15 1(get-init-tree "text-at-halign") 这个与缓存有关。缓存位置在~/.Texmacs/。如果不更改缓存,更改源代码不会生效。 23_7 1(tm-define (graphics-release-left x y) 2 ;;(display* "Graphics] Release-left " x ", " y "\n") 3 (if (inside-graphical-text?) 4 (with-innermost t graphical-text-context? 5 (let* ((ps (select-first (s2f x) (s2f y))) 6 (p (and ps (car ps)))) 7 (if (and p (list-starts? p (tree->path t))) 8 (go-to p) 9 (tree-go-to t :start))))10 (edit_left-button (car (graphics-mode)) x y))) 1(tm-define (get-keyboard-modifiers)2 the-keyboard-modifiers)34(tm-define (set-keyboard-modifiers mods)5 (set! the-keyboard-modifiers mods)) 结论:https://github.com/XmacsLabs/mogan/pull/796 问题 .ts文件怎么使用 C++代码中有很多字符串类型的东西可以用枚举类型来替代,可以重构一下? 绘图模式有没有给座标系标注1、2、3的模式 编译时候出现问题 1generating scheme glueA glue_font ... ok2generating scheme glueA glue_analyze ... ok3generating scheme glueA glue_drd ... ok4installing libmogan ..5error: install failed, method 'trim' is not callable (a nil value) 看起来是lua语言中的trim方法无法调用。结论:有一个环境变量字符串被设置为nil,之后在他的上面调用trim方法则报错。 23_14 edit_interface_rep::handle_mouse 处理鼠标事件。 1edit_interface_rep::handle_mouse (string kind, SI x, SI y, int m, time_t t,2 array<double> data) kind的类型可以有move, press-left, release-left, press-right 等。 1else { 2 string rew= kind; 3 SI dist= (SI) (5 * PIXEL / magf); 4 rew= detect_left_drag ((void*) this, rew, x, y, t, m, dist); 5 if (rew == "start-drag-left") { 6 call_mouse_event (rew, left_x, left_y, m, t, data); 7 delayed_call_mouse_event ("dragging-left", x, y, m, t, data); 8 } 9 else {10 rew= detect_right_drag ((void*) this, rew, x, y, t, m, dist);11 if (rew == "start-drag-right") {12 call_mouse_event (rew, right_x, right_y, m, t, data);13 delayed_call_mouse_event ("dragging-right", x, y, m, t, data);14 }15 else call_mouse_event (rew, x, y, m, t, data);16 }17 } 这段检测了是否有拖动发生,如果没有,最终会执行call_mouse_event (rew, x, y, m, t, data); 1static void2call_mouse_event (string kind, SI x, SI y, SI m, time_t t, array<double> d) {3 array<object> args;4 args << object (kind) << object (x) << object (y)5 << object (m) << object ((double) t) << object (d);6 call ("mouse-event", args);7} 然后转发给scheme来处理。 1(tm-define (mouse-event key x y mods time data)2 ;;(display* "mouse-event " key ", " x ", " y ", " mods ", " time ", " data "\n")3 (mouse-any key x y mods (+ time 0.0) data)) 转到mouse-any mouse-any再转回C++ 1{ 2 scm_name = "mouse-any", 3 cpp_name = "mouse_any", 4 ret_type = "void", 5 arg_list = { 6 "string", 7 "int", 8 "int", 9 "int",10 "double",11 "array_double"12 }13 }, 在mouse_any中进行dispatch. 1 if (type == "press-left" || type == "start-drag-left") {2 if (mods > 1) {3 mouse_adjusting = mods;4 mouse_adjust_selection(x, y, mods);5 } else6 mouse_click (x, y);7 } 在这里进入mouse_click函数 1void2edit_interface_rep::mouse_click (SI x, SI y) {3 if (mouse_message ("click", x, y)) return;4 start_x= x;5 start_y= y;6 send_mouse_grab (this, true);7} 思路改为倒着找,从绘图的移动功能出发。因为只需要在绘图上进行改变鼠标样式的操作,而改变鼠标样式(setCursor)是依赖于具体的widget来执行的。 1(tm-define (edit_move mode x y) 1(tm-define (graphics-move x y)2 ;;(display* "Graphics] Move " x ", " y "\n")3 (when (not (inside-graphical-text?))4 (edit_move (car (graphics-mode)) x y))) 根据("Move objects" (graphics-set-mode '(group-edit move))), 应该重载到group-edit版本。 测试发现正确: 1(tm-define (edit_move mode x y) 2 (:require (eq? mode 'group-edit)) 3 (:state graphics-state) 4 (display* "HERE\n") 5 (cond (sticky-point 6 (set! x (s2f x)) 7 (set! y (s2f y)) 8 (with mode (graphics-mode) 9 (cond ((== (cadr mode) 'move)10 (sketch-transform11 (group-translate (- x group-old-x)12 (- y group-old-y))))13 ((== (cadr mode) 'zoom)14 (sketch-set! group-first-go)15 (sketch-transform (group-zoom x y)))16 ((== (cadr mode) 'rotate)17 (sketch-set! group-first-go)18 (sketch-transform (group-rotate x y)))))19 (set! group-old-x x)20 (set! group-old-y y))21 (multiselecting22 (graphical-object!23 (append24 (create-graphical-props 'default #f)25 `((with color red26 (cline (point ,selecting-x0 ,selecting-y0)27 (point ,x ,selecting-y0)28 (point ,x ,y)29 (point ,selecting-x0 ,y)))))))30 (else31 (cond (current-path32 (set-message (string-append "Left click: operate; "33 "Shift+Left click or Right click: select/unselect")34 "Group of objects"))35 ((nnull? (sketch-get))36 (set-message "Left click: operate"37 "Group of objects"))38 (else39 (set-message "Move over object on which to operate"40 "Edit groups of objects")))41 (graphics-decorations-update)))) 之后倒到edit_graphics.cpp中的: 1bool2edit_graphics_rep::mouse_graphics (string type, SI x, SI y, int mods, time_t t,3 array<double> data) { 倒退到 1bool2edit_graphics_rep::mouse_graphics (string type, SI x, SI y, int mods, time_t t,3 array<double> data) { 然后倒退到scheme中。没找到可以利用的。 换思路,搜索: public QWidget,看都有谁继承了。因为setCursor是它的成员函数。QtMainWindow也是Qt库中的,它也继承了QWidget. 看不出。直接看主界面怎么生成的吧。 qt_tm_widget.hpp中写了: 1 QMainWindow* mainwindow () {2 return qobject_cast<QMainWindow*> (qwid); 3 } 这个函数可以获取主页面的QWidget. 但它仍然是一个成员函数所以可能不太对。 在qt_dialogues.cpp中,发现了 1 QWidget* mainwindow = QApplication::activeWindow (); 这个可能是Qt自带的找到当前活跃窗口的函数。 23_14 Bug 1(tm-define (graphics-get-property var) 2 (with val (graphics-get-raw-property var) 3 (tm->stree val))) 4 5(define (graphics-get-raw-property var) 6 (with val (get-upwards-tree-property (graphics-graphics-path) var) 7 (if (eq? val nothing) 8 (get-default-tree-val var) 9 (if (eq? (tm-car val) 'quote)10 (tree-ref val 0)11 val)))) 研究 get-env-tree 1tree2edit_typeset_rep::get_env_value (string var, path p) {3 typeset_exec_until (p);4 tree t= cur[p][var];5 return is_func (t, BACKUP, 2)? t[0]: t;6} 鼠标点击研究 1 if (type == "press-left" || type == "start-drag-left") {2 if (mods > 1) {3 mouse_adjusting = mods;4 mouse_adjust_selection(x, y, mods);5 } else6 mouse_click (x, y);7 } 之后进入 1void2edit_interface_rep::mouse_click (SI x, SI y) {3 cout << "DEBUG: ";4 if (mouse_message ("click", x, y)) return;5 start_x= x;6 start_y= y;7 send_mouse_grab (this, true);8} 之后 1inline void2send_mouse_grab (widget w, bool get_grab) {3 // request a mouse grab for the widget4 send<bool> (w, SLOT_MOUSE_GRAB, get_grab);5} 在这里似乎捕获了 1void 2qt_tm_widget_rep::send (slot s, blackbox val) { 3 switch (s) { 4 case SLOT_INVALIDATE: 5 case SLOT_INVALIDATE_ALL: 6 case SLOT_EXTENTS: 7 case SLOT_SCROLL_POSITION: 8 case SLOT_ZOOM_FACTOR: 9 case SLOT_MOUSE_GRAB:10 main_widget->send(s, val);11 return; 似乎会跑到这里 1 case SLOT_MOUSE_GRAB:2 {3 check_type<bool> (val, s);4 bool grab = open_box<bool>(val);5 if (grab && canvas() && !canvas()->hasFocus())6 canvas()->setFocus (Qt::MouseFocusReason);7 }8 break;

2023/7/13
articleCard.readMore

Archlinux/Manjaro Install Guide

manjaro I3新电脑配置 假设已经安装好了manjaro i3,接下来要做的是: 基本装机 按顺序执行以下操作 换源 编辑/etc/pacman.d/mirrorlist内容改为 1Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch 加入archlinuxcn源并安装yay 在 /etc/pacman.conf 文件末尾添加以下两行 1[archlinuxcn]2Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch 之后通过以下命令安装 archlinuxcn-keyring 包导入 GPG key。 1sudo pacman -Sy archlinuxcn-keyring 之后就可以用yay 安装各种软件了 记得先sudo pacman -Syyu一下。 科学上网 由于安装某些软件的时候,yay会从国外下载,所以要先科学上网才能尽情安装想要的软件。 1yay -S clash-for-windows-bin 之后导入已有的订阅。之后在终端中设置(例如加入到~/.zshrc中): 1export https_proxy=http://127.0.0.1:7890;2export http_proxy=http://127.0.0.1:7890;3export all_proxy=socks5://127.0.0.1:7890; 对于i3wm等无法设置系统级代理的桌面环境,请把chrome的启动改为 1bindsym $mod+F2 exec google-chrome-stable --proxy-server="socks5://127.0.0.1:7890" 这样chrome也可以自动使用代理上网了。 安装输入法 1yay -S fcitx5-im2yay -S base-devel3yay -S fcitx5-rime4yay -S rime-cloverpinyin 配置fcitx5的环境变量: 1sudo vim /etc/environment 内容为: 1GTK_IM_MODULE=fcitx2QT_IM_MODULE=fcitx3XMODIFIERS=@im=fcitx4SDL_IM_MODULE=fcitx 创建并写入rime-cloverpinyin的输入方案: 1nano ~/.local/share/fcitx5/rime/default.custom.yaml 内容为: 1patch:2 "menu/page_size": 53 schema_list:4 - schema: clover 安装中文维基百科词库: 1yay -S fcitx5-pinyin-zhwiki-rime 设置fctix5自动启动: 1nano ~/.i3/config 加入 1exec --no-startup-id fcitx5 安装geek字体 1yay -S nerd-fonts-jetbrains-mono2yay -S ttf-jetbrains-mono 之后把终端的字体改为该字体。例如更改Konsole的配置,如果默认终端不是Konsole建议改为Konsole。 安装常用软件 1sudo pacman -S ntfs-3g # 使系统可以识别 NTFS 格式的硬盘2sudo pacman -S adobe-source-han-serif-cn-fonts wqy-zenhei # 安装几个开源中文字体。一般装上文泉驿就能解决大多 wine 应用中文方块的问题3sudo pacman -S noto-fonts noto-fonts-cjk noto-fonts-emoji noto-fonts-extra # 安装谷歌开源字体及表情 1yay -S typora-free2yay -S visual-studio-code-bin3yay -S google-chrome 一些常用命令的Rust版本: 1yay -S fd bat exa ripgrep procs dust 安装latex 1sudo pacman -S texlive-most texlive-lang 配置vscode-latexworkshop使用xelatex为默认 https://blog.csdn.net/Haulyn5/article/details/124128533 修改settings.json 1"latex-workshop.latex.tools": [ 2 3 { 4 "name": "latexmk", 5 "command": "latexmk", 6 "args": [ 7 "-xelatex", 8 "-synctex=1", 9 "-interaction=nonstopmode",10 "-file-line-error",11 "%DOC%"12 ],13 "env": {}14 }, 安装zsh 1sudo pacman -S zsh zsh-autosuggestions zsh-syntax-highlighting zsh-completions 更改默认shell 1chsh -s /usr/bin/zsh 对于Konsole需要修改它的配置文件来改变它启动的时候默认使用的shell 由于manjaro i3有自带的zsh配置已经够用,因此这里我们暂时不使用oh my zsh 配置zsh为manjaro风格 安装了archlinux之后如果想把zsh配置为manjaro风格,可以安装yay -S manjaro-zsh-config这个包。之后把~/.zshrc更改为: 1# Use powerline 2USE_POWERLINE="true" 3# Source manjaro-zsh-configuration 4if [[ -e /usr/share/zsh/manjaro-zsh-config ]]; then 5 source /usr/share/zsh/manjaro-zsh-config 6fi 7# Use manjaro zsh prompt 8if [[ -e /usr/share/zsh/manjaro-zsh-prompt ]]; then 9 source /usr/share/zsh/manjaro-zsh-prompt10fi 输出炫酷neofetch 1yay -S lolcat neofetch 1neofetch | lolcat 你会得到炫酷的manjaro/Archlinux图案和你的电脑的基本信息显示。 GIT配置 1git config --global user.name "111" 2git config --global user.email "111@111.111" 配置好的gitconfig文件在~/.gitconfig github ssh 配置 生成新 SSH 密钥 可在本地计算机上生成新的 SSH 密钥。 生成密钥后,可以将密钥添加到您在 GitHub.com 上的帐户,以启用通过 SSH 进行 Git 操作的身份验证。 1ssh-keygen -t ed25519 -C "your_email@example.com" 之后 1cat ~/.ssh/id_ed25519.pub 然后在github网站上加入SSH KEY即可。 配置SSH 使用代理 安装netcat 1sudo pacman -S netcat 2:: 有 2 个软件包可提供 netcat : 3:: 软件仓库 extra 4 1) gnu-netcat 5:: 软件仓库 community 6 2) openbsd-netcat 7 8输入某个数字 ( 默认=1 ): 2 9正在解析依赖关系...10正在查找软件包冲突...1112软件包 (1) openbsd-netcat-1.219_1-11314下载大小: 0.02 MiB15全部安装大小: 0.05 MiB1617:: 进行安装吗? [Y/n] 需要下载openbsd版本的 之后 修改 ~/.ssh/config 文件 1Host github.com2 User git3 ProxyCommand nc -v -x 127.0.0.1:7890 %h %p 这样git使用ssh方式的时候就可以走代理加速了。 注释掉archlinuxcn源 装机的时候用一下可以了。由于manjaro毕竟和archlinux有区别,日常使用不需要打开这个源。可能会导致软件版本出现不兼容。 杂项 默认应用 默认应用的设置文件:vim ~/.config/mimeapps.list, 可以在里面更改默认浏览器 I3配置 配置文件在 ~/.i3/config I3快捷键 1MOD+9 锁屏2MOD+数字 切换工作区3MOD+Return 打开终端4MOD+F2 打开浏览器 可以在~/.i3/config中查看快捷键。

2023/7/5
articleCard.readMore

Texmacs Graphics Tutorial

原文:https://www.texmacs.org/tmweb/documents/tutorials/TeXmacs-graphics-tutorial.pdf

2023/7/5
articleCard.readMore

Texmacs/Mogan Note

Note graphics-utils.scm 1;;These abbreviations are very convenient 2;;to use. A nice naming scheme is : 3;; 4;; -> b=bool ; 5;; -> i=integer ; 6;; -> f=float ; 7;; -> sy=symbol ; 8;; -> s=string ; 9;; -> o=Scheme object ;10;; -> p=path.11;; -> t=tree.12;;13;; One can add the missing ones on demand.14(tm-define f2s float->string)15(tm-define s2f string->float)16(tm-define sy2s symbol->string)17(tm-define s2sy string->symbol)18(tm-define o2s object->string)19(tm-define s2o string->object)20(tm-define t2o tree->object)21(tm-define o2t object->tree) graphics-single.scm editing routines for single graphical objects. graphics-env.scm routines for managing the graphical context. current-x在此定义。 graphics-object.scm routines for managing the graphical object. 研究图形移动功能 在edit_move函数中有下面一段 1(cond ((== (cadr mode) 'move)2 (sketch-transform3 (group-translate (- x group-old-x)4 (- y group-old-y)))) 其中sketch-transform的定义为 1(tm-define (sketch-transform opn)2 (set! the-sketch (map opn the-sketch))3 (set! current-obj4(if (graphics-group-mode? (graphics-mode)) '(nothing) #f))5 (set! current-path #f)6 (graphics-decorations-update)) 而group-translate定义为: 1(define (group-translate x y)2 (lambda (o)3 (traverse-transform o (translate-point x y)))) 其中用到了 1(define (traverse-transform o opn) 2 (define (traverse o) 3 (opn (if (pair? o) (map traverse o) o))) 4 (traverse o)) 5 6(define (translate-point x y) 7 (lambda (o) 8 (if (match? o '(point :%2)) 9`(point ,(f2s (+ x (s2f (cadr o)))) ,(f2s (+ y (s2f (caddr o)))))10 o))) 研究测试相关代码 tm-ref 1(define-public (tm-ref t . l)2 (and (tm? t)3 (with r (select t l)4 (and (nnull? r) (car r))))) tm? 1 tmscm_install_procedure ("tm?", contentP, 1, 0, 0); 1tmscm2contentP (tmscm t) {3 bool b= tmscm_is_content (t);4 return bool_to_tmscm (b);5} 1bool 2tmscm_is_content (tmscm p) { 3 if (tmscm_is_string (p) || tmscm_is_tree (p)) return true; 4 else if (!tmscm_is_pair (p) || !tmscm_is_symbol (tmscm_car (p))) return false; 5 else { 6 for (p= tmscm_cdr (p); !tmscm_is_null (p); p= tmscm_cdr (p)) 7 if (!tmscm_is_pair (p) || !tmscm_is_content (tmscm_car (p))) return false; 8 return true; 9 }10} 因此tm-ref等代码实际上应用于tree数据结构。tm?就是用来检测这个的。例如: 1Scheme] (tm? '(point 1 2))23#f4Scheme] (tm? (stree->tree '(point 1 2)))56#t 之后tm-ref可以取出列表中除了car的第i个元素。 1Scheme] (tm-ref (stree->tree '(1 2 3 4)) 0)232 研究宏包 为什么有的包已经引入,有的需要作为宏包引入呢?init中会初始化一些包。 SRFI 列表长度length 1 (assuming (style-has? "std-markup-dtd") 2 (with u '(arrow-with-text arrow-with-text*) 3 (with l (list-filter u (lambda (s) (style-has? (symbol->string s)))) 4 (for (tag (sort l symbol<=?)) 5 ((eval (upcase-first (symbol->string tag))) 6 (import-from (graphics graphics-markup)) 7 (graphics-set-mode `(edit ,tag)))))) 8 (with u (list-difference gr-tags-user '(arrow-with-text arrow-with-text*)) 9 (with l (list-filter u (lambda (s) (style-has? (symbol->string s))))10 (assuming (nnull? l)11 ---12 (for (tag (sort l symbol<=?))13 ((eval (upcase-first (symbol->string tag)))14 (import-from (graphics graphics-markup))15 (graphics-set-mode `(edit ,tag)))))))) 23_15 1if (N(p) == 0) 2 typeset_dynamic (tree (ERROR, "bad text-at"), ip); 3 else { 4 SI ox= (SI) p[0], oy= (SI) p[1], axis= (b->h() >> 1), x= ox, y= oy; 5 if (halign == "left") x -= b->x1; 6 else if (halign == "center") x -= ((b->x1 + b->x2) >> 1); 7 else if (halign == "right") x -= b->x2; 8 if (valign == "bottom") y -= b->y1; 9 else if (valign == "axis") {10 axis= env->fn->yfrac - b->y1;11 y -= env->fn->yfrac;12 }13 else if (valign == "center") y -= ((b->y1 + b->y2) >> 1);14 else if (valign == "top") y -= b->y2;15 SI snap= env->get_length (TEXT_AT_SNAPPING);16 print (text_at_box (ip, b, x, y, ox - x, oy - y, axis, snap));17 SI pad = env->text_at_repulse;18 if (pad >= 0)19 env->white_zones << rectangle (x + b->x1 - pad, y + b->y1 - pad,20 x + b->x2 + pad, y + b->y2 + pad);21 } 在这里设置了偏移量。 1(tm-define (edit_left-button mode x y) 2 (:require (== mode 'edit)) 3 (:state graphics-state) 4 (display "graphics-single 377\n") 5 (display* sticky-point "\n") 6 (display* (current-in? (graphical-text-tag-list)) "\n") 7 (set-texmacs-pointer 'graphics-cross) 8 (cond (sticky-point 9 (if (current-in? (graphical-text-tag-list))10 (object_commit)11 (next-point)))12 ((and (current-in? (graphical-text-tag-list))13 (== (car (graphics-mode)) 'edit)14 (graphical-contains-text-tag? (cadr (graphics-mode)))15 (not (graphical-contains-curve-tag? (cadr (graphics-mode))))16 (pointer-inside-graphical-text?))17 (set-texmacs-pointer 'text-arrow)18 (go-to (car (select-first (s2f current-x) (s2f current-y)))))19 (else20 (edit-insert x y)21 (display* "HERE\n")))22 (set! previous-leftclick `(point ,current-x ,current-y))) 进入edit-insert 1(define (edit-insert x y)2 (edit-clean-up)3 (object_create (cadr (graphics-mode)) x y)) 进入object_create 1(tm-define (object_create tag x y) 2 (texmacs-error "object-create" "invalid tag")) 3 4(tm-define (object_create tag x y) 5 (:require (== tag 'point)) 6 (object-set! `(point ,x ,y) 'new)) 7 8(tm-define (object_create tag x y) 9 (:require (or (in? tag gr-tags-curves) (in? tag gr-tags-user)))10 (with o (graphics-enrich `(,tag (point ,x ,y) (point ,x ,y)))11 (graphics-store-state 'start-create)12 (set! current-point-no 1)13 (object-set! o 'checkout)14 (graphics-store-state #f)))1516(tm-define (object_create tag x y)17 (:require (graphical-text-tag? tag))18 (with long? (graphical-long-text-tag? tag)19 (object-set! `(,tag ,(if long? `(document "") "") (point ,x ,y)) 'new)20 (and-with d (path->tree (cDr (cursor-path)))21 (when (tree-func? d 'document)22 (tree-go-to d 0 :start))))) 得到对应的重载为 1(tm-define (object_create tag x y)2 (:require (graphical-text-tag? tag))3 (display* "HERE\n")4 (with long? (graphical-long-text-tag? tag)5 (object-set! `(,tag ,(if long? `(document "") "") (point ,x ,y)) 'new)6 (and-with d (path->tree (cDr (cursor-path)))7 (when (tree-func? d 'document)8 (tree-go-to d 0 :start))))) 进入了这一行(graphics-group-enrich-insert o) 1(tm-define (graphics-group-enrich-insert t)2 (graphics-group-insert (graphics-enrich t))) 1(tm-define (graphics-enrich t)2 (let* ((l1 (graphics-all-attributes))3 (l2 (map gr-prefix l1))4 (l3 (map graphics-get-property l2))5 (tab (list->ahash-table (map cons l1 l3))))6 (graphics-enrich-bis t "default" tab))) 1(tm-define (graphics-enrich-bis t id tab)2 (set! tab (list->ahash-table (ahash-table->list tab)))3 (ahash-remove! tab "gid")4 (let* ((attrs (graphical-relevant-attributes t))5 (sel (ahash-table-select tab attrs))6 (l1 (cons (cons "gid" id) (ahash-table->list sel)))7 (l2 (map (lambda (x) (list (car x) (cdr x))) l1)))8 ;;(display* "l= " l2 "\n")9 (graphics-enrich-sub t l2))) 1class concater_rep {2 edit_env env; // the environment3 array<line_item> a; // the line items4 bool rigid; // when surely not wrappable 设置属性 1(tm-define (object-set-text-at-halign val)2 (:argument val "Horizontal alignment")3 (:check-mark "*" (object-test-property? "text-at-halign"))4 (object-set-property "text-at-halign" val)) 1(tm-define (object-set-property var val)2 (and-with t (tree-innermost graphical-context?)3 (object-set-property-bis t var val))) 1(define (object-set-property-bis t var val)2 (cond ((tree-is? t :up 'with)3 (with-set (tree-up t) var val 0))4 ((!= val "default")5 (tree-set! t `(with ,var ,val ,t))))) 菜单 1(tm-menu (text-at-halign-menu)2 ("Center" (object-set-text-at-halign "default"))3 ("Right" (object-set-text-at-halign "right"))4 ("Left1111" (object-set-text-at-halign "left"))) 1(tm-define-macro (tm-menu head . l)2 (receive (opts body) (list-break l not-define-option?)3 `(tm-define ,head ,@opts (menu-dynamic ,@body)))) 1(tm-define-macro (menu-dynamic . l)2 `($list ,@(map gui-make l))) 1(tm-define (gui-make x) 2 ;;(display* "x= " x "\n") 3 (cond ((symbol? x) 4 (cond ((== x '---) '$---) 5 ((== x '===) (gui-make '(glue #f #f 0 5))) 6 ((== x '======) (gui-make '(glue #f #f 0 15))) 7 ((== x '/) '$/) 8 ((== x '//) (gui-make '(glue #f #f 5 0))) 9 ((== x '///) (gui-make '(glue #f #f 15 0)))10 ((== x '>>) (gui-make '(glue #t #f 5 0)))11 ((== x '>>>) (gui-make '(glue #t #f 15 0)))12 ((== x (string->symbol "|")) '$/)13 (else14 (texmacs-error "gui-make" "invalid menu item ~S" x))))15 ((string? x) x)16 ((and (pair? x) (ahash-ref gui-make-table (car x)))17 (apply (car (ahash-ref gui-make-table (car x))) (list x)))18 ((and (pair? x) (or (string? (car x)) (pair? (car x))))19 `($> ,(gui-make (car x)) ,@(cdr x)))20 (else21 (texmacs-error "gui-make" "invalid menu item ~S" x)))) 菜单中的勾选可能是check-mark和object-test-property? 1(tm-define (object-set-text-at-halign val)2 (:argument val "Horizontal alignment")3 (:check-mark "*" (object-test-property? "text-at-halign"))4 (object-set-property "text-at-halign" val)) 1(define (object-test-property? var)2 (lambda (val)3 (if (== val "default") (set! val (tree->stree (get-init-tree var))))4 (== (object-get-property var) val))) 下一步观察get-init-tree 使用了GLUE 1{2 scm_name = "get-init-tree",3 cpp_name = "get_init_value",4 ret_type = "tree",5 arg_list = {6 "string"7 }8 }, 1tree 2edit_typeset_rep::get_init_value (string var) { 3 if (init->contains (var)) { 4 tree t= init [var]; 5 if (var == BG_COLOR && is_func (t, PATTERN)) t= env->exec (t); 6 return is_func (t, BACKUP, 2)? t[0]: t; 7 } 8 if (N(pre)==0) typeset_preamble (); 9 tree t= pre [var];10 if (var == BG_COLOR && is_func (t, PATTERN)) t= env->exec (t);11 return is_func (t, BACKUP, 2)? t[0]: t;12} 查到一个叫init的hashmap 1class edit_typeset_rep: virtual public editor_rep {2protected:3 tree the_style; // document style4 hashmap<path,hashmap<string,tree> > cur; // environment at different paths5 hashmap<string,tree> stydef; // environment after styles6 hashmap<string,tree> pre; // environment after styles and init7 hashmap<string,tree> init; // environment changes w.r.t. style 1void 2edit_typeset_rep::set_init (hashmap<string,tree> H) { 3 init= hashmap<string,tree> (UNINIT); 4 add_init (H); 5} 6 7void 8edit_typeset_rep::add_init (hashmap<string,tree> H) { 9 init->join (H);10 ::notify_assign (ttt, path(), subtree (et, rp));11 notify_change (THE_ENVIRONMENT);12} 在这里调用了set_init 1void 2edit_typeset_rep::set_data (new_data data) { 3 set_style (data->style); 4 set_init (data->init); 5 set_fin (data->fin); 6 set_ref (data->ref); 7 set_aux (data->aux); 8 set_att (data->att); 9 notify_page_change ();10 add_init (data->init);11 notify_change (THE_DECORATIONS);12 typeset_invalidate_env ();13 iterator<string> it = iterate (data->att);14 while (it->busy()) {15 string key= it->next ();16 (void) call (string ("notify-set-attachment"),17 buf->buf->name, key, data->att [key]);18 }19} set_data的调用位置是 1void2set_buffer_data (url name, new_data data) {3 array<url> vs= buffer_to_views (name);4 for (int i=0; i<N(vs); i++) {5 view_to_editor (vs[i]) -> set_data (data);6 view_to_editor (vs[i]) -> init_update ();7 }8} set_buffer_data的调用位置是 1void 2set_buffer_tree (url name, tree doc) { 3 tm_buffer buf= concrete_buffer (name); 4 if (is_nil (buf)) { 5 insert_buffer (name); 6 buf= concrete_buffer (name); 7 tree body= detach_data (doc, buf->data); 8 set_document (buf->rp, body); 9 buf->buf->title= propose_title (buf->buf->title, name, body);10 if (buf->data->project != "") {11 url prj_name= head (name) * as_string (buf->data->project);12 buf->prj= concrete_buffer_insist (prj_name);13 }14 }15 else {16 string old_title= buf->buf->title;17 string old_project= buf->data->project->label;18 tree body= detach_data (doc, buf->data);19 assign (buf->rp, body);20 set_buffer_data (name, buf->data);21 buf->buf->title= propose_title (old_title, name, body);22 if (buf->data->project != "" && buf->data->project != old_project) {23 url prj_name= head (name) * as_string (buf->data->project);24 buf->prj= concrete_buffer_insist (prj_name);25 }26 }27 pretend_buffer_saved (name);28} 其中set_buffer_data (name, buf->data); 而 函数第一行有tm_buffer buf= concrete_buffer (name); 1tm_buffer2concrete_buffer (url name) {3 int i, n= N(bufs);4 for (i=0; i<n; i++)5 if (bufs[i]->buf->name == name)6 return bufs[i];7 return nil_buffer ();8} 下一步问题:buf中的data怎么创建 1class tm_buffer_rep { 2public: 3 new_buffer buf; // file related information 4 new_data data; // data associated to document 5 array<tm_view> vws; // views attached to buffer 6 tm_buffer prj; // buffer which corresponds to the project 7 path rp; // path to the document's root in the_et 8 link_repository lns; // global links 9 bool notify; // notify modifications to scheme1011 inline tm_buffer_rep (url name):12 buf (name), data (),13 vws (0), prj (NULL), rp (new_document ()), notify (false) {}1415 inline ~tm_buffer_rep () {16 delete_document (rp); }1718 void attach_notifier ();19 bool needs_to_be_saved ();20 bool needs_to_be_autosaved ();21}; 疑似相关代码: 1url temp= get_current_view_safe ();2 set_current_view (abstract_view (vw));3 if (is_none (tm_init_buffer_file))4 tm_init_buffer_file= "$TEXMACS_PATH/progs/init-buffer.scm";5 if (is_none (my_init_buffer_file))6 my_init_buffer_file= "$TEXMACS_HOME_PATH/progs/my-init-buffer.scm";7 if (exists (tm_init_buffer_file)) exec_file (tm_init_buffer_file);8 if (exists (my_init_buffer_file)) exec_file (my_init_buffer_file);9 set_current_view (temp); object-get-property 1(tm-define (object-get-property var)2 (tree->stree (get-env-tree var))) 1{2 scm_name = "get-env-tree",3 cpp_name = "get_env_value",4 ret_type = "tree",5 arg_list = {6 "string"7 }8 }, 1tree 2edit_typeset_rep::get_env_value (string var, path p) { 3 typeset_exec_until (p); 4 tree t= cur[p][var]; 5 return is_func (t, BACKUP, 2)? t[0]: t; 6} 7 8tree 9edit_typeset_rep::get_env_value (string var) {10 /* FIXME: tp is wrong (and consequently, crashes TeXmacs)11 * when we call this routine from inside the code which12 * is triggered by a button, for example.13 *14 * Test: fire TeXmacs, then open a new Graphics, then click15 * on the icon for going in spline mode. Then it crashes,16 * because we call (get-env-tree) from inside the Scheme.17 * If we call (get-env-tree-at ... (cDr (cursor-path))),18 * then it works.19 */20 return get_env_value (var, tp);21}

2023/7/5
articleCard.readMore

Scheme Review 1

https://courses.cs.washington.edu/courses/cse341/04wi/lectures/14-scheme-quote.html Quote Scheme has a convenient syntax for representing data literals: prefix any expression with ’ (single quote) and the expression, rather than being evaluated, will be returned as data: 1'3 ; => 3 (a number) 2'"hi" ; => "hi" (a string) 3'a ; => a (a symbol) 4'(+ 3 4) ; => (list '+ '3 '4) (a list) 5'(a b c) ; => (list 'a 'b 'c) (a list) 6 7'(define x 25) (a list) 8 ; => (list 'define 'x '25) 9 ; => (list 'define 'x 25)1011'(lambda (x) (+ x 3)) (a list)12 ; => (list 'lambda (list 'x) (list '+ 'x '3))13 ; => (list 'lambda (list 'x) (list '+ 'x 3)) As these examples illustrate, “quoted” data remains unevaluated, and provides a convenient way of representing Scheme programs. This is one of the big payoffs of Lisp’s simple syntax: since programs themselves are lists, it is extremely simple to represent Lisp programs as data. Compare the simplicity of quoted lists with the ML datatype that we used to represent ML expressions. This makes it simple to write programs that manipulate other programs — it is easy to construct and transform programs on the fly. Note that names in Lisp programs are translated into symbols in quoted Lisp expressions. This is so that quoted names can be distinguished from quoted strings; consider the difference between the following two expressions: 1'(define x 10) ; => (list 'define 'x 10)2'("define" x 10) ; => (list "define" 'x 10) This distinction is probably the only good reason that strings and symbols are distinct data types in Lisp. Quasiquote Sometimes one doesn’t want to escape evaluation of an entire list. In this case, one can use the ` (backquote) operator, also called quasiquote with the , (comma) operator, also called unquote. Quasiquote behaves like quote, except that unquoted expressions are evaluated: 1`(1 2 ,(+ 3 4)) ; => '(1 2 7)2(quasiquote (1 2 (unquote (+ 3 4)))) ; => '(1 2 7)

2023/7/4
articleCard.readMore

In archlinux, chrome cannot be displayed normally under AMD.

Solution: 1rm -rf .config/google-chrome/Default/GPUCache

2023/6/7
articleCard.readMore

Value_categories_in_Cpp

Intro 1#define IS_XVALUE(expr) \2(is_rvalue_reference<decltype((expr))>{})3#define IS_PRVALUE(expr) \4(!is_reference<decltype((expr))>{})5#define IS_LVALUE(expr) \6(is_lvalue_reference<decltype((expr))>{}) This is a method I recently discovered to determine which value category an expression is. I’ll explain why it works later. First, let’s take a look at value categories in C++. One of the most important things you should notice before you start is that value categories are not a property about values, but rather a property about expressions. Actually it depends on how you understand value, I looked through the C++ standard but it seems they don’t define what the term value means. However, we all know what an expression is. (in case you don’t know, you can refer to cppreference) Anyway, there are 3 kinds of value categories. They are called LVALUE, PRVALUE, XVALUE. We know you can write x=3 but not 3=x. Since 3 and x are both expressions, there must be some qualitative difference between them. Since x can be placed to the left of the equals sign(assignment sign), so we call it an lvalue. 3 is called an rvalue accordingly. OK. Now I know the difference between LVALUE and RVALUE, but what is PRVALUE and XVALUE? Well, there are two dimensions to the form of expressions. 1. Does the expression have an identity? 2. Can the expression be moved? The one that has identity and can be moved is called XVALUE (eXpiring value).The one that has identity but cannot be moved is called LVALUE.The one that has no identity but can be moved is called PRVALUE. For example, these are examples for XVALUE: 1std::move(x);2a.m, the member of object expression, where a is an rvalue and m is a non-static data member of non-reference type; Temporary materialization The most useful example to help you understand XVALUE is: Temporary materialization. 1struct S { int m; }; 2int i = S().m; // member access expects glvalue as of C++17;3 // S() prvalue is converted to xvalue As the cppreference goes: A prvalue of any complete type T can be converted to an xvalue of the same type T. This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object. If T is a class or array of class type, it must have an accessible and non-deleted destructor. So the key difference between XVALUE and PRVALUE is that can we reuse the resources of it. We cannot reuse the number 3 or 3.7 but we can reuse the memory allocated for a temporary string object. Decltype and value categories Decltype is a good tool to help us determine the category of an expression. Because its behavior is to generate an rvalue reference for XVALUE, an unreferenced type for PRVALUE, and an lvalue reference for LVALUE. Well, someone may argue that if int x=3; then x is an LVALUE but decltype(x) is int itself rather than int&. That’s because the special case for id-expr in the behavior of decltype. If the argument is an unparenthesized id-expression or an unparenthesized class member access expression, then decltype yields the type of the entity named by this expression. If there is no such entity, or if the argument names a set of overloaded functions, the program is ill-formed. So the right way is int x=3; decltype((x)) xref=x;. Here the type of xref is int&. std::move The famous function move actually does nothing to do with “move something”. Actually, it just change the value category of the parameter from LVALUE to XVALUE. In the most cases, you should call move using an LVALUE. std::move is used to indicate that an object t may be “moved from”, i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.

2022/8/23
articleCard.readMore

Revisit_OS

For some reason, I’ve been reviewing OS related stuff lately. hardlink and softlink ln creates hark links.ln -s creates soft (a.k.a symbolic) links. ln -l show the number of hardlinks points to the same inode. hardlink points to the inode.softlink points to the path. pros and cons:hardlink must be on the same partition, can only link to files.softlink can link to a file or directory, no partition limitation. Hard links are only valid within the same File System. Symbolic links can span file systems as they are simply the name of another file. Underneath the file system, files are represented by inodes. A file in the file system is basically a link to an inode.A hard link, then, just creates another file with a link to the same underlying inode. When you delete a file, it removes one link to the underlying inode. The inode is only deleted (or deletable/over-writable) when all links to the inode have been deleted. A symbolic link is a link to another name in the file system. Once a hard link has been made the link is to the inode. Deleting, renaming, or moving the original file will not affect the hard link as it links to the underlying inode. Any changes to the data on the inode is reflected in all files that refer to that inode. 1➜ test git:(main) ✗ touch 111 222 2➜ test git:(main) ✗ echo 111 > 111 3➜ test git:(main) ✗ echo 222 > 222 4➜ test git:(main) ✗ ln 111 l111 5➜ test git:(main) ✗ ln -s 222 s222 6➜ test git:(main) ✗ ls 7111 222 l111 s222 8➜ test git:(main) ✗ ls -l 9总计 1210-rw-r--r-- 2 zhangjia zhangjia 4 8月20日 16:46 11111-rw-r--r-- 1 zhangjia zhangjia 4 8月20日 16:46 22212-rw-r--r-- 2 zhangjia zhangjia 4 8月20日 16:46 l11113lrwxrwxrwx 1 zhangjia zhangjia 3 8月20日 16:46 s222 -> 222 Now, why the soft link’s file mode is lrwxrwxrwx. l means soft link. The rest is same to a normal file mode.

2022/8/20
articleCard.readMore

Using Cpp17 or higher version of Cpp on Vscode extension Clangd

Recently I’ve switched my code completion tool in VScode from Microsoft’s official plugin to clangd. On the one hand, clangd can provide a better user experience, on the other hand, I also use this plugin in my company (Bytedance). I joked to myself: Two of the most interesting things I learned when I interned at the company were that indentation changed from four spaces to two, and braces changed from wrap to no wrap. Now, there is a problem. I installed clangd, switched over and everything worked fine. But when I use string_view, clangd told me it didn’t recognize this thing. I quickly realized that this is due to the fact that the default Cpp version of clangd is still 14. So changing its default version became something I had to do. I googled but didn’t find any useful information. I try to use compile_commands.json file. But on the one hand, it needs to be configured separately for each project, while the effect I expect is to use Cpp17 for any file that is opened at random. On the other hand, it turns out that it does not work. (maybe I didn’t configure it properly) So, here is the final solution. Open (create if not already there) ~/.config/clangd/config.yaml.and write the following information 1CompileFlags: 2 Add: [-std=c++20] Cool. Now let’s have fun with Cpp20. Edit in 2023-10-21 What is written above applies to linux. Under macOS, the configuration file is in ~/Library/Preferences/clangd/config.yaml. For details, please refer to https://clangd.llvm.org/config#files Also, you can use If in config.yaml to avoid add -std-c++20 for C files (that’s funny). Like this: 1If:2 PathExclude: [.*\.c, .*\.h]3CompileFlags:4 Add:5 - "-std=c++2b"

2022/8/7
articleCard.readMore

First Post

新博客 决定把博客切换到hugo。也算是人生路上一个新的开始。

2022/7/24
articleCard.readMore

C++ Note 4

C++ 笔记4 引用与重载 1#include <iostream> 2using namespace std; 3void f(double x) 4{ 5 cout << "DOUBLE" << endl; 6} 7 8void f(int &x) 9{10 cout << "INT&" << endl;11}1213int main()14{15 f(2);16} 这段代码会输出DOUBLE 原因在于 2是右值 不能用于初始化一个int& 则只能选择第一个版本的函数重载 重载的其他规则 T和const T同样T*和const T*不一样 但是和T* const 一样T&和const T&不一样 模版实现数组求平均值

2022/7/7
articleCard.readMore

BASH脚本

BASH 条件判断 test 和 [ 内置命令 test 根据表达式expr 求值的结果返回 0(真)或 1(假)。也可以使用方括号:test expr 和 [ expr ] 是等价的。 可以用 $? 检查返回值;可以使用 && 和 || 操作返回值;也可以用本技巧后面介绍的各种条件结构测试返回值。 1[ian@pinguino ~]$ test 3 -gt 4 && echo True || echo false 2 3false 4 5[ian@pinguino ~]$ [ "abc" != "def" ];echo $? 6 70 8 9[ian@pinguino ~]$ test -d "$HOME" ;echo $?10110 在清单 1 的第一个示例中,-gt 操作符对两个字符值之间执行算术比较。在第二个示例中,用 [ ] 的形式比较两个字符串不相等。在最后一个示例中,测试 HOME 变量的值,用单目操作符 -d 检查它是不是目录。 可以用 -eq、 -ne、-lt、 -le、 -gt 或 -ge 比较算术值,它们分别表示等于、不等于、小于、小于等于、大于、大于等于。 可以分别用操作符 =、 !=、< 和 > 比较字符串是否相等、不相等或者第一个字符串的排序在第二个字符串的前面或后面。单目操作符 -z 测试 null 字符串,如果字符串非空 -n 返回 True(或者根本没有操作符)。 说明:shell 也用 < 和 > 操作符进行重定向,所以必须用 < 或 > 加以转义。清单 2 显示了字符串测试的更多示例。检查它们是否如您预期的一样。 一些常见的文件测试 操作符 特征 -d 目录 -e 存在(也可以用 -a) -f 普通文件 -h 符号连接(也可以用 -L) -p 命名管道 -r 可读 -s 非空 -S 套接字 -w 可写 -N 从上次读取之后已经做过修改 除了上面的单目测试,还可以使用表 2 所示的双目操作符比较两个文件: 表 2. 测试一对文件 操作符 为 True 的情况 -nt 测试 file1 是否比 file2 更新。修改日期将用于这次和下次比较。 -ot 测试 file1 是否比 file2 旧。 -ef 测试 file1 是不是 file2 的硬链接。 清单 5. 分配和测试算术表达式 1[ian@pinguino ~]$ let x=2 y=2**3 z=y*3;echo $? $x $y $z 2 30 2 8 24 4 5[ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w 6 70 3 8 16 8 9[ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w10110 4 8 13 清单 6. 使用 [[ 复合命令 1[ian@pinguino ~]$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]] &&23> echo "home is a writable directory"45home is a writable directory 在使用 = 或 != 操作符时,复合命令 [[ 还能在字符串上进行模式匹配。匹配的方式就像清单 7 所示的通配符匹配。 清单 7. 用 [[ 进行通配符测试 1[ian@pinguino ~]$ [[ "abc def .d,x--" == a[abc]*\ ?d* ]]; echo $? 2 30 4 5[ian@pinguino ~]$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $? 6 71 8 9[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $?10111 甚至还可以在 [[ 复合命令内执行算术测试,但是千万要小心。除非在 (( 复合命令内,否则 < 和 > 操作符会把操作数当成字符串比较并在当前排序序列中测试它们的顺序。清单 8 用一些示例演示了这一点。 清单 8. 用 [[ 包含算术测试 1[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || (( 3 > 2 )) ]]; echo $? 2 30 4 5[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 -gt 2 ]]; echo $? 6 70 8 9[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 > 2 ]]; echo $?101101213[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a > 2 ]]; echo $?141501617[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a -gt 2 ]]; echo $?1819-bash: a: unbound variable 条件测试 虽然使用以上的测试和 &&、 || 控制操作符能实现许多编程,但 bash 还包含了更熟悉的 “if, then, else” 和 case 结构。学习完这些之后,将学习循环结构,这样您的工具箱将真正得到扩展。 If、then、else 语句 bash 的 if 命令是个复合命令,它测试一个测试或命令($?)的返回值,并根据返回值为 True(0)或 False(不为 0)进行分支。虽然上面的测试只返回 0 或 1 值,但命令可能返回其他值。请参阅 LPI exam 102 prep: Shells, scripting, programming, and compiling 教程学习这方面的更多内容。 Bash 中的 if 命令有一个 then 子句,子句中包含测试或命令返回 0 时要执行的命令列表,可以有一个或多个可选的 elif 子句,每个子句可执行附加的测试和一个 then 子句,子句中又带有相关的命令列表,最后是可选的 else 子句及命令列表,在前面的测试或 elif 子句中的所有测试都不为真的时候执行,最后使用 fi 标记表示该结构结束。 使用迄今为止学到的东西,现在能够构建简单的计算器来计算算术表达式,如清单 9 所示: 清单 9. 用 if、then、else 计算表达式 1[ian@pinguino ~]$ function mycalc () 2 3> { 4 5> local x 6 7> if [ $# -lt 1 ]; then 8 9> echo "This function evaluates arithmetic for you if you give it some"1011> elif (( $* )); then1213> let x="$*"1415> echo "$* = $x"1617> else1819> echo "$* = 0 or is not an arithmetic expression"2021> fi2223> }2425[ian@pinguino ~]$ mycalc 3 + 426273 + 4 = 72829[ian@pinguino ~]$ mycalc 3 + 4**330313 + 4**3 = 673233[ian@pinguino ~]$ mycalc 3 + (4**3 /2)3435-bash: syntax error near unexpected token `('3637[ian@pinguino ~]$ mycalc 3 + "(4**3 /2)"38393 + (4**3 /2) = 354041[ian@pinguino ~]$ mycalc xyz4243xyz = 0 or is not an arithmetic expression4445[ian@pinguino ~]$ mycalc xyz + 3 + "(4**3 /2)" + abc4647xyz + 3 + (4**3 /2) + abc = 35 这个计算器利用 local 语句将 x 声明为局部变量,只能在 mycalc 函数的范围内使用。let 函数具有几个可用的选项,可以执行与它密切关联的 declare 函数。请参考 bash 手册或使用 help let 获得更多信息。 如清单 9 所示,需要确保在表达式使用 shell 元字符 —— 例如(、)、*、> 和 < 时 —— 正确地对表达式转义。无论如何,现在有了一个非常方便的小计算器,可以像 shell 那样进行算术计算。 在清单 9 中可能注意到 else 子句和最后的两个示例。可以看到,把 xyz 传递给 mycalc 并没有错误,但计算结果为 0。这个函数还不够灵巧,不能区分最后使用的示例中的字符值,所以不能警告用户。可以使用字符串模式匹配测试(例如 1[[ ! ("$*" == *[a-zA-Z]* ]] , 或使用适合自己范围的形式)消除包含字母表字符的表达式,但是这会妨碍在输入中使用 16 进制标记,因为使用 16 进制标记时可能要用 0x0f 表示 15。实际上,shell 允许的基数最高为 64(使用 base#value 标记),所以可以在输入中加入 _ 和 @ 合法地使用任何字母表字符。8 进制和 16 进制使用常用的标记方式,开头为 0 表示八进制,开头为 0x 或 0X 表示 16 进制。清单 10 显示了一些示例。 清单 10. 用不同的基数进行计算 1[ian@pinguino ~]$ mycalc 015 2 3015 = 13 4 5[ian@pinguino ~]$ mycalc 0xff 6 70xff = 255 8 9[ian@pinguino ~]$ mycalc 29#37101129#37 = 941213[ian@pinguino ~]$ mycalc 64#1az141564#1az = 47711617[ian@pinguino ~]$ mycalc 64#1azA181964#1azA = 3053802021[ian@pinguino ~]$ mycalc 64#1azA_@222364#1azA_@ = 12508405742425[ian@pinguino ~]$ mycalc 64#1az*64**3 + 64#A_@262764#1az*64**3 + 64#A_@ = 1250840574 对输入进行的额外处理超出了本技巧的范围,所以请小心使用这个计算器。 elif 语句非常方便。它允许简化缩进,从而有助于脚本编写。在清单 11 中可能会对 type 命令在 mycalc 函数中的输出感到惊讶。 清单 11. Type mycalc 1[ian@pinguino ~]$ type mycalc 2 3mycalc is a function 4 5mycalc () 6 7{ 8 9local x;1011if [ $# -lt 1 ]; then1213echo "This function evaluates arithmetic for you if you give it some";1415else1617if (( $* )); then1819let x="$*";2021echo "$* = $x";2223else2425echo "$* = 0 or is not an arithmetic expression";2627fi;2829fi3031} 当然,也可以只用 $(( 表达式 )) 和 echo 命令进行 shell 算术运算,如清单 12 所示。这样就不必学习关于函数或测试的任何内容,但是请注意 shell 不会解释元字符,例如 *,因此元字符不能在 (( 表达式 )) 或 [[ 表达式 ]] 中那样正常发挥作用。 清单 12. 在 shell 中用 echo 和 $(( )) 直接进行计算 1[ian@pinguino ~]$ echo $((3 + (4**3 /2)))

2022/5/27
articleCard.readMore

链接 Note 1

链接的笔记 本文部分信息来自网络 .bss段不占用存储空间 为什么要有.bss段?因为已经初始化的全局变量,需要在目标文件里面占用空间来存储它们被初始化的值。例如int a=3,b=2;我们需要存下3和2. 但是如果是没有初始化的全局变量,只需要记录一下长度就会好了。例如两个int,记录一些.bss段的长度为8字节即可。 那么,这个长度总要占地方的吧? .bss段占据的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各个段的各种信息,比如段的名字、段的类型、段在elf文件中的偏移、段的大小等信息。同时符号存放在符号表.symtab中。 .bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。 程序为什么要分成数据段和代码段 数据和指令被映射到两个虚拟内存区域,数据段对进程来说可读写,代码段是只读,这样可以防止程序的指令被有意无意的改写。 有利于提高程序局部性,现代CPU缓存一般被设计成数据缓存和指令缓存分离,分开对CPU缓存命中率有好处。 代码段是可以共享的,数据段是私有的,当运行多个程序的副本时,只需要保存一份代码段部分。 链接器通过什么进行的链接 链接的接口是符号,在链接中,将函数和变量统称为符号,函数名和变量名统称为符号名。链接过程的本质就是把多个不同的目标文件之间相互“粘”到一起,像玩具积木一样各有凹凸部分,有固定的规则可以拼成一个整体。 可以将符号看作是链接中的粘合剂,整个链接过程基于符号才可以正确完成,符号有很多类型,主要有局部符号和外部符号,局部符号只在编译单元内部可见,对于链接过程没有作用,在目标文件中引用的全局符号,却没有在本目标文件中被定义的叫做外部符号,以及定义在本目标文件中的可以被其它目标文件引用的全局符号,在链接过程中发挥重要作用。 为什么需要extern “C” C语言函数和变量的符号名基本就是函数名字变量名字,不同模块如果有相同的函数或变量名字就会产生符号冲突无法链接成功的问题,所以C++引入了命名空间来解决这种符号冲突问题。同时为了支持函数重载C++也会根据函数名字以及命名空间以及参数类型生成特殊的符号名称。 由于C语言和C++的符号修饰方式不同,C语言和C++的目标文件在链接时可能会报错说找不到符号,所以为了C++和C兼容,引入了extern “C”,当引用某个C语言的函数时加extern “C"告诉编译器对此函数使用C语言的方式来链接,如果C++的函数用extern “C"声明,则此函数的符号就是按C语言方式生成的。 以memset函数举例,C语言中以C语言方式来链接,但是在C++中以C++方式来链接就会找不到这个memset的符号,所以需要使用extern “C"方式来声明这个函数,为了兼容C和C++,可以使用宏来判断,用条件宏判断当前是不是C++代码,如果是C++代码则extern “C”。 1#ifdef __cplusplus2extern "C" {3#endif45void *memset(void *, int, size_t);67#ifdef __cplusplus8}9#endif 强符号和弱符号 我们经常编程中遇到的multiple definition of ‘xxx’,指的是多个目标中有相同名字的全局符号的定义,产生了冲突,这种符号的定义指的是强符号。有强符号自然就有弱符号,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。attribute((weak))可以定义弱符号。 1extern int ext;23int weak; // 弱符号4int strong = 1; // 强符号5__attribute__((weak)) int weak2 = 2; // 弱符号67int main() {8 return 0;9} 链接器规则: 不允许强符号被多次定义,多次定义就会multiple definition of ‘xxx’一个符号在一个目标文件中是强符号,在其它目标文件中是弱符号,选择强符号一个符号在所有目标文件中都是弱符号,选择占用空间最大的符号,int类型和double类型选double类型 强引用和弱引用 一般引用了某个函数符号,而这个函数在任何地方都没有被定义,则会报错error: undefined reference to ‘xxx’,这种符号引用称为强引用。与此对应的则有弱引用,链接器对强引用弱引用的处理过程几乎一样,只是对于未定义的弱引用,链接器不会报错,而是默认其是一个特殊的值。 1#include <cstdio> 2 3__attribute__((weak)) void foo(); 4 5int main() 6{ 7 printf("%d\n", &foo); // 0 8 foo(); 9 return 0;10} 这里foo的地址是0 则可以改为 1__attribute__ ((weak)) void foo();23int main() {4 if (foo) {5 foo();6 }7 return 0;8} 这种强引用弱引用对于库来说十分有用,库中的弱引用可以被用户定义的强引用所覆盖,这样程序就可以使用自定义版本的库函数,可以将引用定义为弱引用,如果去掉了某个功能,也可以正常连接接,想增加相应功能还可以直接增加强引用,方便程序的裁剪和组合。 例如再加一个文件 1void foo()2{3 printf("foo2\n");4} 这个时候就可以g++ 1.cpp 2.cpp从而得到一个可以输出foo2的可执行文件。 弱符号的出现主要是为了解决宏条件编译问题的,宏条件编译对于长期维护的代码是个灾难。 程序首先执行的代码并不是main开始的代码,也就是程序的真正入口不是main函数,而是运行库的入口函数;运行库会 先把main函数需要的参数、环境变量等准备好;然后将标准输入输出文件描述符打开,这样才能保证main函数开始就可以使用printf;然后把堆初始化,这样才能保证程序可以自由地执行malloc、new等;还有就是把全局变量初始化、全局构造函数执行完;做完这么多工作之后运行库就执行回调函数main,这时候程序才开始进入人们常说的函数入口。程序的运行环境组成是:程序本身逻辑代码、运行库、系统内核、内存;内存分为用户空间和内核空间,只要内核才使用内核空间,其它的包括运行库都是使用用户空间;程序运行空间分为栈空间和堆空间,函数运行的环境就是栈空间,栈空间都是有固定大小的,一般是2M,地址增加方向是向低地址扩张;堆空间比较大,使用也很灵活,这个堆空间一般都是运行库在帮你管理,堆分配算中中最简单的就是空闲链表算法;程序运行完之后,运行库还要帮你把所有的后事处理掉,释放堆空间、关闭所有打开的文件描述符、释放所有的进程资源等,这就是进程关闭内存泄漏的那些空间能够得到回收的原因;从程序的整个过程可以看书,main函数只不过是运行库的一个回调函数,不是真正的函数入口,当然我们也可以自己写一个运行库,这样就可以直接运行在系统内核之上了,运行库主要部分就是标准c接口的实现,听上去并不复杂。 动态和静态的比较 动态链接库有两个设计目标,也是它的优势: 动态链接。例如MySQL支持MyISAM和InodeDB等不同的存储引擎,你也可以为它添加新的存储引擎,那么这个存储引擎就只能编译成动态链接库的形式。因为MySQL释出(release)的时候,还不知道有会有谁为它写新的存储引擎,所以也不知道应该链接那个静态库。当然,MySQL有源码,你可以重新编译一下,把你的新存储引擎链接进去,但是像Oracle这种没有源码的程序,就只能用动态链接库解决这个问题了。 共享,节约内存。例如libc.so提供了基础的C语言函数和系统调用接口,每个应用程序都会用到,如果每个进程都用静态库,那么printf这个函数的代码在内存中会有许多份,每个进程都有一个副本。使用动态链接库就可以避免这个问题,同一个动态链接库在多个不同进程之间的代码是共享的,不会占用多余的内存空间。数据不是共享的,每个进程都有自己的独有数据。 缺点1、当系统中多个应用程序都用了一个动态链接库,但是要求的版本不同,这时动态链接库之间就会相互干扰。2、性能开销。动态链接库为了做到“共享代码,但是不共享数据”,引入了不小的开销,调用动态链接库中的函数,需要好几次间接内存访问才能走到函数入口,全局数据也是。 静态链接的优缺点: 优点:代码装载速度快,执行速度略比动态链接库快;只需保证在开发者的计算机中有正确的.lib文件,在以二进制形式发布程序时不需考虑在用户的计算机上.lib文件是否存在及版本问题。 缺点: 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费。 静态库对程序的更新、部署和发布会带来麻烦。如果静态库liba.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新) 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。 动态链接的优缺点: 优点:生成的可执行文件较静态链接生成的可执行文件小;适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试;不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性; 缺点:使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息;速度比静态链接慢; 为什么要进行动态链接?为了解决静态链接浪费空间和更新困难的缺点。 动态链接的方式?装载时重定位和地址无关代码技术。 地址无关代码技术原理?通过GOT段实现间接跳转。 延迟加载技术原理?对外部函数符号通过PLT段实现延迟绑定及间接跳转。 如何进行显式运行时链接?通过<dlfcn.h>头文件中的四个函数。

2022/3/24
articleCard.readMore

C++ Note 3

本文是我的C++笔记的第三篇My Thrid C++ Note; explict 防止类的构造函数的隐式转换 1class A 2{ 3private: 4 int x; 5public: 6 A(int x_): x{x_} {} 7}; 8 9// ...1011A a = 3; // YES12A a2{3}; // YES 这样是对的 1class A 2{ 3private: 4 int x; 5public: 6 explict A(int x_): x{x_} {} 7}; 8 9// ...1011A a = 3; // NO12A a{3}; //YES 析构函数 移动构造 和 emplace_back 下列代码 1class A 2{ 3private: 4 int x; 5 6public: 7 A(int xx) : x{xx} {} 8 A() : A(0) {} 9 ~A() { cout << "goodbye " << x << endl; }10};1112int main()13{14 vector<A> v;15 v.emplace_back(A{0});16 v.emplace_back(A{1});17 v.emplace_back(A{2});18 cout << "--------------------------------------\n";1920 // OUTPUT21 // goodbye 022 // goodbye 023 // goodbye 124 // goodbye 025 // goodbye 126 // goodbye 227 // --------------------------------------28 // goodbye 029 // goodbye 130 // goodbye 231} 其原因在于:首先:emplace_back只有在传入构造参数列表的时候和push_back有区别。其他的时候没有区别。这里相当于先生成临时对象A{0},再拷贝构造到v中。从而执行了一次对于0的析构之后,因为要再加入A{1},而目前的v的可用空间大小是1因此需要再向系统申请一块大小为2的空间。之后再把原来的A{0}复制过去。再放弃原来的大小为1的地址上的A{0},因此又执行一次对于0的析构。之后构造临时对象A1,再复制到v中。则临时对象析构,造成一次对于1的析构。之后要加入2. 方法一样。先开辟新内存空间,把原来的对象复制过去。再加入2 即可最终,3个对象的生命周期均到达终点。则依次析构。 而 1 vector<A> v;2 v.reserve(10);3 v.emplace_back(A{0});4 v.emplace_back(A{1});5 v.emplace_back(A{2}); 在提前预留了空间之后,输出为 1goodbye 02goodbye 13goodbye 24--------------------------------------5goodbye 06goodbye 17goodbye 2 我们输出capacity即可看到我们的猜测是正确的。 1 vector<A> v;2 cout << "CAPACITY " << v.capacity() << endl;3 v.emplace_back(A{0});4 cout << "CAPACITY " << v.capacity() << endl;5 v.emplace_back(A{1});6 cout << "CAPACITY " << v.capacity() << endl;7 v.emplace_back(A{2});8 cout << "CAPACITY " << v.capacity() << endl;9 cout << "--------------------------------------\n"; 得到 1CAPACITY 0 2goodbye 0 3CAPACITY 1 4goodbye 0 5goodbye 1 6CAPACITY 2 7goodbye 0 8goodbye 1 9goodbye 210CAPACITY 411--------------------------------------12goodbye 013goodbye 114goodbye 2 结论:emplace_back以参数列表的形式传入时,不论是否有移动构造函数,都是原地构造,只会调用一次构造函数(只有这一项和push_back有区别,其它都是一样的) emplace_back以左值对象的形式传入时,不论是否有移动构造函数,都是调用一次拷贝构造函数 emplace_back以右值对象(例如move(左值对象),或者就是右值)的形式传入时a. 有移动构造函数,调用一次移动构造b. 没有移动构造函数,调用拷贝构造函数 emplace_back以 Person(“aaa”, “shandong”, 1991) 形式传入时a. 有移动构造函数,构造临时文件 —> 移动构造 —> 临时文件析构b. 没有移动构造函数,构造临时文件 —> 拷贝构造 —> 临时文件析构

2022/2/22
articleCard.readMore

位运算技巧

位运算技巧 lowbit 1inline int lowbit(int x)2{3 return x & -x;4} lowbit返回最右侧的1例如:lowbit(0b0011) == 0b1lowbit(0b001100) == 0b100lowbit(0b10101010) == 0b10lowbit(0b100) == 0b100 gcc内置位运算函数 •int __builtin_ffs (unsigned int x)返回x的最后一位1的是从后向前第几位,比如7368(1110011001000)返回4。•int __builtin_clz (unsigned int x)返回前导的0的个数。•int __builtin_ctz (unsigned int x)返回后面的0的个数,和__builtin_clz相对。•int __builtin_popcount (unsigned int x)返回二进制表示中1的个数。•int __builtin_parity (unsigned int x)返回x的奇偶校验位,也就是x的1的个数模2的结果。 gosper’s hack 用于生成$n$元集合所有的$k$元子集 1void GospersHack(int k, int n) 2{ 3 int cur = (1 << k) - 1; 4 int limit = (1 << n); 5 while (cur < limit) 6 { 7 do_stuff(cur); 8 // do something 9 int lb = cur & -cur;10 int r = cur + lb;11 cur = ((r ^ cur) >> __builtin_ctz(lb) + 2) | r;12 // 或:cur = (((r ^ cur) >> 2) / lb) | r;13 }14} 枚举子集 1subset = mask2while subset != 0 do3 // subset 是 mask 的一个子集,可以用其进行状态转移4 ...5 // 使用按位与运算在 O(1) 的时间快速得到下一个(即更小的)mask 的子集6 subset = (subset - 1) & mask7end while

2022/2/21
articleCard.readMore

现代C++白皮书 笔记1

现代C++白皮书 笔记1 C++14 0b11001010 0b1100'1010'0010 函数返回类型推导 1auto add(auto&x,auto&y)2{3 return x+y;4} constexpr中的局部变量 移动捕获 [p = move(ptr)](){}; 按类型访问元组 x = get<int>(t); 用户定义字面量 3ms, 55us, 10i, "hello"s 变量模版 1template <typename T>2T x = T(3.1);34cout << x<int> << " " << x<double> << endl; // 3 3.1 泛型lambda表达式 1auto get_size = [](auto& m){return m.size();}; 在其他一些语言里面,有专门的特殊语法: 1C# x => x*x2JAVA x -> x*x3D (x){return x*x;} Bjarne认为不使用特殊记法是对的,但认为应该引入概念。 Concept 概念 概念——用于指定对于模版参数要求的编译期谓词类型和值概念:Buffer<unsigned char, 128> 即值也可以作为概念的参数(感觉BS就是让Alexander Stepanov和STL给带坏了才老想着整Concept) Concept的问题: 1template <typename T> 2concept Tickable = requires(T t) 3{ 4 t.tick(); 5}; 6 7struct Foo 8{ 9 void tick() { cout << "tick\n"; }10 void tock() {}11};1213void Bar(Tickable auto &t)14{15 t.tock();16 t.tock1();17 t.tick();18}1920int main()21{22 Foo f{};23 Bar(f);24} tickable即可表达 可以tick的概念 例如原子钟,手表,金表,都可以属于这个概念但C++的CONCEPT的一个问题就是: 我们在Bar里面可以使用t.tock()这样的语句。 这个时候你传Foo进去是可以编译通过的。但是传别的Tickable的东西就不一定能通过。而还有一种做法是你只能用明确写出的接口。例如Tickable里面只写了tick,那就只能用tick而不能用tock.这种必须都写明白才能用的方式在 C++ 社区里称作 full template definition checkingC++ 的 Concept 不可能做到这样,因为: 兼容性你这么搞了,之前的代码怎么办?之前没有 concept 的代码都是无约束的,这么改直接全死。 编译性能 我的天,你想想,C++ ,每个函数里面每一个表达式都检查一通,啧啧啧……但是你不这么搞,那这个 Concept 的作用就没啥意思了——万一不小心用了什么没约束的操作错误信息不还是很难看。要我说,这方面 Rust 做得太好了。我认为 Rust 的这个 Trait 真是把静态接口和所谓的“动态接口”完美地、无缝地结合到一块去了。真好。

2022/2/17
articleCard.readMore

VsCode Latex 插件配置

更改vscode的 setting.json文件加入 1"latex-workshop.latex.tools": [ 2 { 3 "name": "latexmk", 4 "command": "latexmk", 5 "args": [ 6 "-synctex=1", 7 "-interaction=nonstopmode", 8 "-file-line-error", 9 "-pdf",10 "-outdir=%OUTDIR%",11 "%DOC%",12 "-xelatex"13 ],14 "env": {}15 }, 即可 即在命令参数的最后增加 -xelatex 参数 备注:之前在文件头部添加% !TEX program = xelatex是管用的,现在不知道为啥又不行了。

2022/2/10
articleCard.readMore

链表 笔记

链表笔记 判断是否有环做法:龟兔赛跑,快慢指针 判断两链表是否相交,若相交则找出交点。 1struct ListNode 2{ 3 int val; 4 ListNode *next; 5 ListNode(int x) : val(x), next(NULL) {} 6}; 7 8class Solution 9{10public:11 ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)12 {13 ListNode *pA{headA}, *pB{headB};14 while (pA != pB)15 {16 pA = pA == nullptr ? headB : pA->next;17 pB = pB == nullptr ? headA : pB->next;18 }19 return pA;20 }21};

2021/12/28
articleCard.readMore

ICS 笔记3

ICS笔记3 Chapter8-1 Subroutines ADT Abstract Data Types Up to now, we have processed a single value an integer a floating point number, or an ASCII character The real world is abstract data types, or more colloquially data structures,far more complex than simple, single numbers. E.g. a company’s organization chart a list of items arranged in alphabetical order In this chapter, we will study three abstract data types: stacks queues and character strings Before we get to stacks, queues, and character strings, however, we introduce a new concept that will prove very useful in manipulating data structures: subroutines, or what is also called functions. Subroutine A subroutine is a program fragment that. . . Resides in user space (i.e, not in OS) Performs a well-defined task Is invoked (called) multiple times by a user program Returns control to the calling program when finished Virtues: Reuse code without re-typing it (and debugging it!) Divide task into parts (or among multiple programmers) Use vendor-supplied library of useful routines that one software engineer writes a program that requires such fragments and another software engineer writes the fragments. math library square root, sine, and arctangent,etc. Important: The Call/Return Mechanism 相关的指令 JSR,JSRR,RET JSR 0100 1 PCoffset11作用:跳转,并且把之前的PC保存在R7 JSRR 0100 0 00 BaseR 000000作用:根据BaseR的值跳转,并且把PC的值保存在R7 RET 1100 000 111 000000作用:把R7的值赋给PC 例子:取负数 1Negate NOT R0,R02ADD R0,R0,#13RET 1; call the subroutine to compute R4=-R32AND R0,R0,#0 ; let R0=03ADD R0,R0,R3 ; let R0=R34JSR Negate5AND R4,R4,#0 ; let R4=06ADD R4,R4,R0 如何使用 Programmer must know Address: or at least a label that will be bound to its address Function: what it does NOTE: The programmer does not need to know how the subroutine works, but what changes are visible in the machine’s state after the routine has run Arguments: what they are and where they are placed Return values: what they are and where they are placed 调用时的信息传递 Argument(s) Value passed in to a subroutine is called an argument This is a value needed by the subroutine to do its job Examples TwosComp: R0 is number to be negated OUT: R0 is character to be printed PUTS: R0 is address of string to be printed 传递方法:How?In registers (simple, fast, but limited number)In memory (many, but awkward, expensive)Both 寄存器和内存进行传递参数 Return Values A value passed out of a subroutine is called a return value This is the value that you called the subroutine to compute How?Registers, memory, or bothSingle return value in register most common通常,采用寄存器保存返回值 caller-save and caller-save 在调用和返回的过程中势必会对寄存器产生修改,那么如何分配保存与恢复现场的责任呢?一般有两种办法:caller-save和callee-save即责任在谁?caller-save要求调用者来保存相关的寄存器的信息,并且恢复现场。callee-save要求被调用者来存在相关的信息并且恢复现场。 一般都采用callee-save,只有对于函数返回值R7采用的是caller-save Generally use “callee-save” strategy, except for return valuesSame as trap service routinesSave anything that subroutine alters internally that shouldn’t be visible when the subroutine returnsRestore incoming arguments to original values (unless overwritten by return value) RememberYou MUST save R7 if you call any other subroutine or trapOtherwise, you won’t be able to return! Chapter8-2 Memory Model for Program Execution & the Stack

2021/12/19
articleCard.readMore

压行技巧

压行技巧 1 1for(init;check;update)2{34}5XXX 可以改成 1for(init; check || (XXX,0) ; update)2{3 4} 2 逗号的用法 1if()2{3 AAA;4 BBB;5 CCC;6 return DDD;7} 可以改成 1if()2{3 return AAA,BBB,CCC,DDD;4} 3 赋值语句和普通表达式的值 1A=func(B);2printf("%d",A); 可以改为 1printf("%d", A=func(B)); 4 短路语句 1if(AAA)2BBB 可以写成 1(!AAA)||BBB; 当AAA为假,则短路,不计算BBB 当AAA为真,计算BBB 5 利用for 1for(int i=1;i<=n;i++)2{3 for(int j=1;j<=n;j++)4 printf("%d",a[i][j]);5 puts("");6} 可以改为 1for(int i=1;i<=n;i++,puts(""))2 for(int j=1;j<=n;j++)3 printf("%d",a[i][j]); 6 综合运用例如 1int find(int x)2{3 return fa[x]==x?x:fa[x]=find(fa[x]);4} 并查集 7

2021/12/16
articleCard.readMore

三道面试题

三道面试题 不使用额外空间交换两个数解1:加法减法解2:异或 1void f(int &a, int &b) 2{ 3 a = a + b; 4 b = a - b; 5 a = a - b; 6} 7 8void ff(int &a, int &b) 9{10 a = a ^ b;11 b = a ^ b;12 a = a ^ b;13} 1-1000的一个排列 丢失了三个数字 怎样$O(n)$时间,$O(1)$额外空间找到 25个马 🐎!每次可以让5个马赛跑问最少几次能找出最快的三匹马 答案:7次可以。暂时不知道咋证明7次是最少的。https://math.stackexchange.com/questions/1361065/why-6-races-are-not-sufficient-in-the-25-horses-5-tracks-problem 一个证明

2021/12/9
articleCard.readMore

数论笔记

本文是我的数论笔记的第一篇关于:4k+1型和4k+3型素数 4k+3型素数 命题一 $$a^2\equiv-1\mod p$$ 无解 证明 假设有解,即存在$a$使得 $$a^2\equiv-1\mod p$$ 则不妨设$p=4k+3$ 由费马小定理得: $$a^{p-1}\equiv1\mod p$$ 但又有: $$a^{p-1}=a^{4k+2}=(a^2)^{2k+1}\equiv(-1)^{2k+1}\equiv-1\mod p$$ 则矛盾 因此不存在 命题二 $4k+3$型的素数有无穷多个 证明 假设只有有限个,易知不可能只有3是$4k+1$型素数。则记所有比3大的$4k+1$型素数为$p_1,p_2,\dots,p_k$ 构造一个数: $4p_1p_2\dots p_k+3$ 则这个数的任何一个素因子均不属于${p_i}$ 但由于这个数模4余3,则它的素因子不可能都模4余1,否则它也会模4余1 因此它存在至少一个模4余3的素因子,且这个素因子不可能是3 则与所有比3大的$4k+1$型素数为$p_1,p_2,\dots,p_k$矛盾,因为存在一个新的比3大的$4k+1$型素数 4k+1型素数 命题一 存在一个数$a$使得$a^2\equiv-1\mod p$成立 证明 这个数字就是$(\frac{p-1}{2})!$ 其理由如下: 设$p=4k+1$ 则由wilson定理得 $$(p-1)!\equiv-1\mod p$$ 即 $$(p-1)!=(4k)!=(2k)!\times(2k+1)(2k+2)\dots(2k+2k)$$ $$=(2k)!\times(4k+1-2k)(4k+1-(2k-1))\dots(4k+1-1)$$ $$=(2k)!\times(-2k)(-(2k-1))\dots(-1)\=(2k)!\times(2k)!\equiv-1\mod p$$ 从而原命题成立 命题二 存在无限多个$4k+1$型素数 证明 假设只有有限个,记为$p_1,\dots,p_k$ 构造一个新的数:$(p_1p_2\dots p_k)^2+1$ 考虑任取这个数的一个素因子$p$,易知$p\notin{p_i}$ 则有$(p_1p_2\dots p_k)^2\equiv-1\mod p$ 从而$p$一定是$4k+1$型的素数 从而矛盾

2021/12/6
articleCard.readMore

二分总结

## Leetcode 704 二分查找target 这种是在一个元素各不相同的有序数组里面找一个等于target的数解法: 1class Solution 2{ 3public: 4 int search(vector<int> &nums, int target) 5 { 6 int l{0}; 7 int r = nums.size() - 1; 8 int mid; 9 while (l <= r)10 {11 mid = (l + (r - l) / 2);12 if (nums[mid] == target)13 {14 return mid;15 }16 if (target < nums[mid])17 {18 r = mid - 1;19 }20 else21 {22 l = mid + 1;23 }24 }25 return -1;26 }27}; 此时,分三种情况。注意到,在不等于的时候,r和l都对mid有排斥 排斥性 在二分问题中,mid能不能等于l或者r称为排斥性 二分模版 常见的二分情景是:对于01数组,找到里面的第一个1出现的位置(重点:这里我们认为1一定会出现,后面的部分会讨论1有可能不出现的情况)00011111这符合自然界的规律,某个实验,在参数小于某个阈值的时候,都是失败的。大于等于这个阈值的时候,都是成功的。 这种时候需要我们认真考虑排斥性对于先0后1,一定会出现1的实验,代码为: 1vector<int> v{0, 0, 0, 1, 1, 1}; 2 auto yes = [&](int mid) 3 { return v[mid] == 1; }; 4 size_t l{0}, r{v.size() - 1}; 5 int mid; 6 while (l < r) 7 { 8 mid = l + (r - l) / 2; 9 if (yes(mid))10 r = mid;11 else12 l = mid + 1;13 }14 cout << l; 原因是:我们之所以设置 while(l<r)因为我们实际上是弄了一个闭区间 [l,r]规定真正的答案只能出现在这个闭区间里面也就是说区间定义为:答案可能出现的位置这样,当我们把l和r缩减到重合的时候,答案就被我们抓住了。 缩减的方法:注意如果mid取了1,那么它可能是答案,也可能在答案的右边,因此设置 r=mid.但如果mid取0,那么答案一定在mid右边,那么设置 l=mid+1 有可能找不到答案的情况 注意到:按照我们上面闭区间的思考模式,一开始就默认了答案在 [l,r]里面如果采用 [,)左闭右开的方式,也许有别的处理方法。但这里我的处理是加入 1if(yes(l))return l;2else return -1; 这虽然丑陋,但是有效。 例如 1int main() 2{ 3 vector<int> v{0, 0, 0}; 4 auto yes = [&](int mid) 5 { return v[mid] == 1; }; 6 size_t l{0}, r{v.size() - 1}; 7 int mid; 8 while (l < r) 9 {10 mid = l + (r - l) / 2;11 if (yes(mid))12 r = mid;13 else14 l = mid + 1;15 }16 if (yes(l))17 cout << l;18 else19 cout << -1;20} 确实有效 LOWER_BOUND 和 UPPER_BOUND 对于v={0,1,2,3}lower_bound(v,2)得到2upper_bound(v,2)得到3 对于 1,1,2,2,2,3,3,3l(v,2)=2u(v,2)=5 l(v,1)=0r(v,1)=2 l(v,0)=0r(v,0)=0 l(v,3)=5r(v,3)=6 l(v,4)=6r(v,4)=6 对于1,1,3,3,5,5,7,7 lower_bound(4)=4upper (4) =4 lower_bound:Returns an iterator pointing to the first element in the range [first, last) that is not less than (i.e. greater or equal to) value, or last if no such element is found. upper_bound:Returns an iterator pointing to the first element in the range [first, last) that is greater than value, or last if no such element is found. 区别在于lower_bound是大于等于的第一个upper_bound是大于的第一个 equal_range 返回 pair<Itr,Itr> 第一个指向区域里面第一个等于val的元素,第二个指向区域里面第一个大于val的元素也就是左闭右开的 [,)区间

2021/11/30
articleCard.readMore

C++ Note 2

本文是我的C++笔记的第二篇My Second C++ Note; vector.clear() vector.clear()并不释放数组,其会调用data[0]...data[size-1]的析构函数。vector在析构的时候会先调用clear()然后释放数组例如 1 vector<int> x{1, 2, 4};2 cout << x.capacity() << x.size() << endl; // 3 33 x.push_back(10);4 cout << x.capacity() << x.size() << endl; // 6 45 x.clear();6 cout << x.capacity() << x.size() << endl; // 6 0 1 2class World 3{ 4public: 5 World() 6 { 7 cout << "World()!\n"; 8 } 9 ~World()10 {11 cout << "~World()!\n";12 }13};1415int main()16{17 vector<World> v(3);18 cout << "AAA\n";19 v.clear();20 cout << "BBB\n";21} 这个会输出 1World()!2World()!3World()!4AAA5~World()!6~World()!7~World()!8BBB clear and minimize Before C++11 1vector<int> v{1,2,3,4,5};2vector<int> temp;3v.swap(temp);4// to make the capacity and size of v to be both 0 After C++11 1vector<int> v{1,2,3,4,5};2v.clear();3v.shrink_to_fit();// to make the capacity == size partition() 按照规则进行进行分组返回分出的第二组的首位的迭代器 例如 1vector<int> v{1, 2, 3, 4, 5, 6, 7};2 partition(v.begin(), v.end(), [](int x)3 { return x % 2 == 0; });4 pr(v); 得到6 2 4 3 5 1 7前面一组都是符合的后面一组都是不符合的可能会打乱顺序 对于不想打乱顺序的,使用stable_partition() partial_sort() 1void partial_sort (RandomAccessIterator first,2 RandomAccessIterator middle,3 RandomAccessIterator last); partial_sort() 会将 [first, last) 范围内最小(或最大)的 middle-first 个元素移动到 [first, middle) 区域中,并对这部分元素做升序(或降序)排序。 partial_sort() 函数:容器支持的迭代器类型必须为随机访问迭代器。这意味着,partial_sort() 函数只适用于 array、vector、deque 这 3 个容器。当选用默认的升序排序规则时,容器中存储的元素类型必须支持 <小于运算符;同样,如果选用标准库提供的其它排序规则,元素类型也必须支持该规则底层实现所用的比较运算符;partial_sort() 函数在实现过程中,需要交换某些元素的存储位置。因此,如果容器中存储的是自定义的类对象,则该类的内部必须提供移动构造函数和移动赋值运算符。 partial_sort_copy() partial_sort_copy() 函数的功能和 partial_sort() 类似,唯一的区别在于,前者不会对原有数据做任何变动,而是先将选定的部分元素拷贝到另外指定的数组或容器中,然后再对这部分元素进行排序。 1RandomAccessIterator partial_sort_copy (2 InputIterator first,3 InputIterator last,4 RandomAccessIterator result_first,5 RandomAccessIterator result_last); partial_sort_copy() 函数会将 [first, last) 范围内最小(或最大)的 result_last-result_first 个元素复制到 [result_first, result_last) 区域中,并对该区域的元素做升序(或降序)排序。 值得一提的是,[first, last] 中的这 2 个迭代器类型仅限定为输入迭代器,这意味着相比 partial_sort() 函数,partial_sort_copy() 函数放宽了对存储原有数据的容器类型的限制。换句话说,partial_sort_copy() 函数还支持对 list 容器或者 forward_list 容器中存储的元素进行“部分排序”,而 partial_sort() 函数不行。 但是,介于 result_first 和 result_last 仍为随机访问迭代器,因此 [result_first, result_last) 指定的区域仍仅限于普通数组和部分类型的容器,这和 partial_sort() 函数对容器的要求是一样的。 nth_element() nth_element is a partial sorting algorithm that rearranges elements in [first, last) such that: The element pointed at by nth is changed to whatever element would occur in that position if [first, last) were sorted. All of the elements before this new nth element are less than or equal to the elements after the new nth element. 简单的理解 nth_element() 函数的功能,当采用默认的升序排序规则(std::less)时,该函数可以从某个序列中找到第 n 小的元素 K,并将 K 移动到序列中第 n 的位置处。不仅如此,整个序列经过 nth_element() 函数处理后,所有位于 K 之前的元素都比 K 小,所有位于 K 之后的元素都比 K 大。 is_sorted() 1//判断 [first, last) 区域内的数据是否符合 std::less<T> 排序规则,即是否为升序序列2bool is_sorted (ForwardIterator first, ForwardIterator last); is_sorted_until() 和 is_sorted() 函数相比,is_sorted_until() 函数不仅能检测出某个序列是否有序,还会返回一个正向迭代器,该迭代器指向的是当前序列中第一个破坏有序状态的元素。 1ForwardIterator is_sorted_until (ForwardIterator first, ForwardIterator last); merge() merge() 函数用于将 2 个有序序列合并为 1 个有序序列,前提是这 2 个有序序列的排序规则相同(要么都是升序,要么都是降序)。并且最终借助该函数获得的新有序序列,其排序规则也和这 2 个有序序列相同。 1OutputIterator merge (InputIterator1 first1, InputIterator1 last1,2 InputIterator2 first2, InputIterator2 last2,3 OutputIterator result); inplace_merge() 事实上,当 2 个有序序列存储在同一个数组或容器中时,如果想将它们合并为 1 个有序序列,除了使用 merge() 函数,更推荐使用 inplace_merge() 函数。 1//默认采用升序的排序规则2void inplace_merge (BidirectionalIterator first, BidirectionalIterator middle,3 BidirectionalIterator last); 其中,first、middle 和 last 都为双向迭代器,[first, middle) 和 [middle, last) 各表示一个有序序列。 find() find_if() 1InputIterator find (InputIterator first, InputIterator last, const T& val); find() 函数本质上是一个模板函数,用于在指定范围内查找和目标元素值相等的第一个元素。另外,该函数会返回一个输入迭代器,当 find() 函数查找成功时,其指向的是在 [first, last) 区域内查找到的第一个目标元素;如果查找失败,则该迭代器的指向和 last 相同。值得一提的是,find() 函数的底层实现,其实就是用==运算符将 val 和 [first, last) 区域内的元素逐个进行比对。这也就意味着,[first, last) 区域内的元素必须支持==运算符。 和 find() 函数相同,find_if() 函数也用于在指定区域内执行查找操作。不同的是,前者需要明确指定要查找的元素的值,而后者则允许自定义查找规则。 1InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred); find_end()与search() 序列A: 1,2,3,7,8,9,1,2,3,4序列B: 1,2,3 find_end(A,B)返回B在A里面最后一次出现的位置search(A,B)返回B在A里面第一次出现的位置 all_of(), any_of(), none_of() algorithm 头文件中定义了 3 种算法,用来检查在算法应用到序列中的元素上时,什么时候使谓词返回 true。这些算法的前两个参数是定义谓词应用范围的输入迭代器;第三个参数指定了谓词。检查元素是否能让谓词返回 true 似乎很简单,但它却是十分有用的。 count(),count_if() 返回序列中count(): 等于val的个数count_if(): 满足pred的个数 C++中的map,filter,reduce 分别等价于transform,copy_if,accumulate 用法: copy_if 1vector<int> v{1, 2, 3, 4, 5, 6, 7};2vector<int> vv;3copy_if(begin(v), end(v), inserter(vv,begin(vv)), [](auto &x)4 { return x % 2 == 0; });5pr(vv); 注意:这里使用inserter来创建了插入迭代器,从而实现向v2里面插入元素的功能 类似的 1 vector<int> v{1, 2, 3};2 vector<int> cc{11, 22, 33, 44};3 copy_if(v.begin(), v.end(), back_inserter(cc), [](auto &x)4 { return x % 2 == 0; });5 pr(cc);// 11 22 33 44 2 这里使用back_inserter向后面追加元素 transform 对容器里面的每个元素都做一次操作 accumulate 迭代所有元素并做类似求和等操作 1 vector<int> v{1, 2, 3};2 cout << accumulate(v.begin(), v.end(), v[0], [](auto &x, auto &y)3 { return x * y; });//will print 6 注意:当我们把accumulate作用在monoid上面的时候,要把第三个参数,即初始值设置为幺元.这有时候比较烦人。那么,当我们已经确定了所处理的容器不会是空的的时候,可以这么写 1vector<int> v{1, 2, 3, 4};2 cout << accumulate(next(begin(v)), end(v), v[0], [](auto &x, auto &y)3 { return x + y; }); 也就是把初始值设置为v[0]之后再累加剩余的元素即可。 unique unique() 算法可以在序列中原地移除重复的元素,这就要求被处理的序列必须是正向迭代器所指定的。在移除重复元素后,它会返回一个正向迭代器作为新序列的结束迭代器。可以提供一个函数对象作为可选的第三个参数,这个参数会定义一个用来代替 == 比较元素的方法。 for_each() 1 vector<int> v{1, 2, 3};2 for_each(v.begin(), v.end(), [](auto &x)3 { x = x * 2; });4 pr(v);//2 4 656 vector<int> vv{1, 2, 3};7 for_each(vv.begin(), vv.end(), [](auto x)8 { x = x * 2; });9 pr(v);//1 2 3

2021/11/28
articleCard.readMore

ICS PPT Note 1

计算系统概率PPT笔记1 复习期中 PPT 1-1 架构:ApplicationAlgorithm and Data StructureProgramming Language, CompilerOS/VMISA (Instruction Set Architecture)MicroarchitectureRTLDigital Circuits/Analog CircuitsElectonic DevicesPhysics PPT1-2 计算机是一个二进制的系统。通过操纵电子进行计算。高电压(高于某个特定值)视为1,低电压视为0. 信息的基本单位是Bit MOS管:有N和P型N型MOS管:三个极, #1 , #2 和 Gate当Gate为1时,连通当Gate为0时,断开 P型MOS管恰恰相反当Gate为0时,连通当Gate为1时,断开 PPT 2-1 进制:八进制,二进制,十六进制无符号整数有符号整数: n个bit,有$2^n$个不同的值 原码(signed Magnitude):一个符号位,n-1个数值位。有+0,-0,导致空间的浪费反码(1’s Complement):正数的反码等于原码。负数的反码等于原码的符号位不变,而数值位取反。补码(2’s Complement):反码+1等于补码。例如3bit 0000 0011 0102 0113 100-4 101-3 110-2 111-1 Summary: 每个计算机里面的内容都是一个数字,也就是二进制的0和1.负数用补码(2’s Complement)来表示Overflows can be detected utilizing the carry bit浮点数有特殊的表示法 PPT2-2 a data type includes representation and operations.对整数的操作:1. 相加2. 相减3. 带符号扩展(Sign Extension)例如:0001的8位扩展为0000 0001而 1001的8位扩展为1111 1001将sign bit进行扩展 判断overflow: 当输入的两个操作数的符号一致,而和的符号却不一样的时候。即产生了overflow. 小数:定点数,浮点数 浮点数:IEEE754 S:1bit Exp:8bit Fraction:23bit 1,8,23S,Exp,Frac $$(-1)^S*(1.Frac)*2^{(Exp-127)}, 1\le Exp\le 254$$ 特殊:当指数部分全1,小数部分全0时,表示无穷。符号位为1表示负无穷,符号位为0表示正无穷。浮点正常表示的指数范围为00000001-11111110,即$2^{-126}~2^{127}$若指数全为0,则为subnormal number,它表示的指数位仍然是$2^{-126}$,但小数从1.frac变为0.frac,从而可以表示更小的数 Some data types are supported directly by the instruction set architecture. For LC-3, there is only one supported data type: 16-bit 2’s complement signed integer Operations: ADD, AND, NOT PPT 3-1 晶体管和逻辑门MOS管:已学习CMOS: Complementary MOS使用N和P型的MOS管来构造逻辑门 题目1:构造一个3个输入的NOR门,使用CMOS 记号: 在线上面打斜线写4,表示这个线是4位宽的 小圈表示否定 PPT 3-2 从组合逻辑到时序逻辑DecoderMUXFull adder(input: A,B,C_in,output: S,C_out)使用1位的full adder可以构造多位的加法 减法器:使用全加器和补码的原理构造,把输入B取反之后和A相加,加的时候设置全加器的C_in为1(补码要求) 即可 RS锁存器(RS Latch):R:Reset (to be 0)S:Set (to be 1) RS功能 00未定义 01设为0 10设为1 11保持不变 为了解决RS锁存器,当R,S同时取1的时候未定义的问题设计D锁存器 DE功能 ~0保持不变 ~1跟随D改变 D触发器:俩D锁存器相连,功能:在clk的上升沿读取数据,其他时候保持不变. 表示多位数据:A=10101A[4:2]=101A[1:0]=01 Memory:k*mk=2^n个locations每个位置有m bit 的信息 k: Address Spacenumber of locations(usually a power of 2) m: Addressabilitynumber of bits per location(e.g., byte-addressable) RAM:(Random Access Memory)Static RAM(SRAM) fast, not very dense (bitcell is a latch)Dynamic RAM(DRAM) slower but denser, bit storage must be periodically refreshed each bitcell is a capacitor (like a leaky bucket) that decays ROM:(Read Only Memory)

2021/11/26
articleCard.readMore

A strange way to compute GCD

A strange way to compute GCD in one line 1int gcd(int x, int y)2{3 while (x ^= y ^= x ^= y %= x)4 ;5 return y;6} 有趣的是:这种写法在CPP17之前是ub(在同一个语句中多次改变一个变量的值,且反复使用该变量的值,为ub 因为标准没有规定求值的顺序),在CPP17中,对于求值的顺序做了进一步的规定,从而这种写法具备了可移植性。

2021/11/18
articleCard.readMore

ICS Note 2

This is my notes for ICS. Source: book计算系统概论笔记2 来自课本 Control Instructions 5种可以改变顺序的指令。 conditional branch unconditional jump subroutine call TRAP (service call) RTI (return from trap or interrupt)

2021/11/18
articleCard.readMore

ICS Note 1

This is my notes for ICS. Source: ppt计算系统概论笔记1 来自PPT NAMES MAR: Memory Address RegisterMDR: Memory Data Register(联想Lisp的 CAR,CDR) LC-3 ALU: ADD,AND,NOT Registers: 8 registers. R0,R1,…,R7 Word Size: number of bits normally processed by ALU in one instruction also width of registers LC-3 is 16 bits I/O: Devices for getting data into and out of computer memory Each device has its own interface, usually a set of registers like the memory’s MAR and MDR LC-3: I/O: keyboard, console keyboard: data register(KBDR) and status reg (KBSR) console: data reg(CRTDR) and status reg(CRTSR) frame buffer: memory-mapped pixels CRT: cathode ray tube (CRT) 阴极射线管Program that controls access to a device is usually called a driver. CONTROL UNIT Instruction Register (IR) contains the current instruction.Program Counter (PC) contains the address of the next instruction to be executed. Control unit: reads an instruction from memory the instruction’s address is in the PC interprets the instruction, generating signals that tell the other components what to do an instruction may take many machine cycles to complete Instruction The instruction is the fundamental unit of work. Specifies two things: opcode: operation to be performed operands: data/locations to be used for operation An instruction is encoded as a sequence of bits. (Just like data!) Often, but not always, instructions have a fixed length, such as 16 or 32 bits. Control unit interprets instruction: generates sequence of control signals to carry out operation. Operation is either executed completely, or not at all. (原子性) 原子性: 比如:从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。原子性定义: 一个操作或者多个操作,要么全部执行并且不被打断,要么就都不执行。 JAVA中volatile关键字的含义之一: 使得JVM保证每次都从内存里面读取,而不会从CPU缓存中读取。这被称为可见性:当一个线程修改了共享变量的值,其他线程会马上知道这个修改。当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从缓存中读取。 有序性: 虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。而volatile本身就包含了禁止指令重排序的语义。而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。 小结: synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。 在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。 我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性) A computer’s instructions and their formats is known as its Instruction Set Architecture (ISA). LC3 ISA ADD(0001): opcode[15:12] Dst[11:9] Src1[8:6] ???[5:3] Src2[2:0] LDR(0110) opcode[15:12] Dst[11:9] Base[8:6] Offset[5:0] JMP(1100) opcode[15:12] ???[11:9] Base[8:6] ???[5:0] means: load the content of ‘Base’ reg into PC BR(0000) opcode[15:12] nzp[11:9] PCoffset[8:0] nzp: negative, zero, positive Process FETCH Load next instruction (at address stored in PC) from memory into Instruction Register (IR). Load contents of PC into MAR. Send “read” signal to memory. Read contents of MDR, store in IR.Then increment PC, so that it points to the next instruction in sequence. PC becomes PC+1. DECODE First identify the opcode. In LC-3, this is always the first four bits of instruction. A 4-to-16 decoder asserts a control line corresponding to the desired opcode. Depending on opcode, identify other operands from the remaining bits.Example: for ADD, last three bits is source operand #2 for LDR, last six bits is offset EVALUATE ADDRESS For instructions that require memory access, compute address used for access.Examples: add offset to base register (as in LDR) add offset to PC (or to part of PC) add offset to zero FETCH OPERANDS Obtain source operands needed to perform operation. Examples: read data from register file (ADD) load data from memory (LDR) EXECUTE Perform the operation, using the source operands. Examples: send operands to ALU and assert ADD signal do nothing (e.g., for loads and stores) STORE Write results to destination. (register or memory) Examples: result of ADD is placed in destination register result of memory load is placed in destination register for store instruction, data is stored to memory write address to MAR, data to MDR assert WRITE signal to memory Changing the Sequence of Instructions In the FETCH phase, we incremented the Program Counter by 1. What if we don’t want to always execute the instruction that follows this one?examples: loop, if-then-else, function callNeed special instructions that change the contents of the PC.These are called jumps and branches. jumps are unconditional – they always change the PC branches are conditional – they change the PC only if some condition is true (e.g., the contents of a register is zero) MISC The clock is a signal that keeps the control unit moving.Clock cycle (or machine cycle) – rising edge to rising edge.Clock generator circuit: based on crystal oscillator. The control unit is a state machine. Control unit will repeat instruction processing sequence as long as clock is running.If not processing instructions from your application, then it is processing instructions from the Operating System (OS).The OS is a special program that manages processor and other resources. To stop the computer: AND the clock generator signal with ZERO when control unit stops seeing the CLOCK signal, it stops processing Summary Instructions look just like data – it’s all interpretation.Three basic kinds of instructions: computational instructions (ADD, AND, …) data movement instructions (LD, ST, …) control instructions (JMP, BRnz, …)Six basic phases of instruction processing: not all phases are needed by every instruction phases may take variable number of machine cycles

2021/11/17
articleCard.readMore

The language UNIMODAL is not context free

In this article, we will prove that: the language ‘UNIMODAL’ is not cfg. Also, we will prove that cfg can not be used to compare binary number. proposition one:“BINARY EQUAL”: Alphabet:{S,0,1} language BINARY_EQUAL:string such as ${(0|1)^* S (0|1)^*}$,AND the binary number before ‘#’ is equal to the the latter one. statement: BINARY_EQUAL is not context free grammar Prove: Assume that BINARY_EQUAL is cfl(context free language), then use the pump lemma. For a string s=“xxxx…xxxx#yyyy…yyyy” $$\exists uvwxy, \text{such that} \ s=uvwxy\ ,\text{and}\ \ \forall i\ge 0, uv^iwx^iy\in \mathbb{BINARY_EQUAL}$$ We choose a special string: $s=1^k0^kS0^k1^k$, so when k is large enough, the v and x must be different. Hence, the pumped string is not BINARY_EQUAL. So the proposition is right. proposition two:“BINARY BIGGER”: Alphabet:{S,0,1}language BINARY_EQUAL:string such as ${(0|1)^* S (0|1)^*}$,AND the binary number before ‘#’ is BIGGER to the the latter one.statement: BINARY_BIGGER is not context free grammar Prove: Assume that BINARY_BIGGER is cfl, use the pump lemma for the string $1^k0^{k-1}1S1^k0^k$. If v and x are both in the left number, the pump down it.(pump down means that we choose the $i$ in the pump lemma as 0. a.k.a $i=0$) then the digits of left num is 2k-2, while the right is 2k, so the right number is bigger than the left. If v and x are both in the right number, just choose i as much as you need. Then the right is obvious larger than the left. If v and x are in different sides:(1) If v is ‘1’ and x is ‘1’,pump down it. Then two num should be equal(2) If v is ‘00…1’, and x is ‘11…1’ then pump up it and the right num should be larger. So, the proposition is true. proposition three:“UNIMODAL”: Alphabet:{S,0,1}language UNIMODAL:string such as ${(0|1)^* S (0|1)^(S(0|1)^)^*}$. This string will be considered as an array of binary number. And when and only when the array is UNIMODAL. The string belongs to the language. For example: 1 3 5 3 1 is a unimodal array. The form in binary is 1#11#101#11#1. So the string ‘1#11#101#11#1’ is unimodal.statement: UNIMODAL is not context free grammar Prove: use pump lemma. s=uvwxy. Let s be ‘$1^K0^KS1^K0^KS1^K0^KS1^K0^KS1^K0^K$’, Actually, we need $1^K0^KS1^K0^K(S1^K0^K)^k$. Because every num in this array is equal. It’s a UNIMODAL string. And if v and x belongs to one num, then pump down it. We get a decrease then increase, hence a controdiction. And if v and x belongs to different num. Then v is ‘000…’, x is ‘111…’,(because we let the k be large enough). Hence,we pump down, we we will get a decrease and then increase and then decrease. Controdiction!

2021/10/6
articleCard.readMore

VSCODE 使用笔记

工欲善其事,必先利其器。 快捷键 CK C-K C-F为格式化当前段落 (Format as F)C-K C-T is to choose theme CS C+S-I is to format all the docC+S-K is to kill the current lineC+S+[ is to fold S S+A is multi-cursor C C-SPACE is intellisenseC+[ is to tab the current line. F F2 is to rename symbolF8 is to detect errors

2021/9/28
articleCard.readMore

CCPC华为的一道趣题

本文记录了CCPC华为比赛中的一道题目 [TOC] 题面 骤风起,仓颉飘飘乎不自觉于孤岛焉。岛无人迹,唯有有理数二族尔。一族曰甲分之乙,一族曰乙分之甲,甲、乙皆正整数。数之,则族族不竭其数。鹦鹉谓仓颉:“日择二数,合其为平均或调和平均。造得一,吾送汝归!”仓颉能归于九千九百九十九亿九千九百九十九万九千九百九十九日否? 仓颉被一阵风刮到了一个荒无人烟的小岛上,那里有两族有理数,$\frac a b$和$\frac b a$,($a$,$b$ 为正整数),每族数有无穷多个。鹦鹉告诉仓颉:“每天,你可以选两个已有的数 x,y,将它们合成为$\frac{x+y}{2}$或$\frac{2xy}{x+y}$。如果你能合成 1,我就送你回家!”仓颉能在 999999999999 天内回家吗? T 组数据。 ##输入样例 1321 131 245 3 输出样例 1Yes2No3Yes 做法 先打表,因为是网络赛,可以用mma先求出一些较小值。得到(1,3),(1,7),(1,15),(7,9),(3,13)等可以做到。猜想:分子分母(注意一定要约分)的和为2的幂次的时候,可以做到。 先写一波代码试一下: 1#include <iostream> 2#include <vector> 3 4using namespace std; 5 6 7int gcd(int a, int b) 8{ 9 if (!a)10 {11 return b;12 }13 if (a < b)14 {15 return gcd(b, a);16 }1718 return gcd(a % b, b);19}2021struct frac22{23 int up;24 int down;25 frac(int a, int b)26 {27 int d = gcd(a, b);28 up = a / d;29 down = b / d;30 }31};323334frac simplify(frac x)35{36 int d = gcd(x.down, x.up);37 return frac(x.up / d, x.down / d);38}3940void pr(frac x)41{42 cout << x.up << "/" << x.down << " ";43}4445bool is_power(int x)46{47 if (x == 1 || x == 2)48 {49 return true;50 }51 while (x >= 3)52 {53 if (x % 2 == 1)54 {55 return false;56 }57 x = x / 2;58 }59 return x == 2;60}6162bool judge(frac x)63{64 return is_power(x.up + x.down);65}6667int main()68{69 int t;70 scanf("%d", &t);71 int n, m;72 while (t--)73 {74 scanf("%d", &n);75 scanf("%d", &m);76 frac x(n, m);77 if (judge(x))78 {79 cout << "Yes\n";80 }81 else82 {83 cout << "No\n";84 }85 }86} 发现是可以的。 证明 接下来考虑怎么证明。首先可以得到,对于$2^k$个数字,通过不断对两两取平均的方式,可以得到它们的整体的平均值。即$$\frac{\sum_{k=1}^{k=2^n}x_k}{2^n}$$从而,若现有分子分母和为$2^k$的初始分数$\frac b a$,即满足$a+b=2^k$,则可看作手头有$2^k$个数,分别是$b$个$\frac{a}{b}$和$a$个$\frac{b}{a}$其均值为$$\frac{b*\frac{a}{b}+a*\frac{b}{a}}{2^k}=1$$即可满足条件。注意到,若在约分之前满足分子分母和为2的幂次,则约分后也满足。但约分之前不满足的,约分后也有可能满足。例如$\frac{3}{9}$即有如下结论: 对任意给的初始分数(可以未约分),只要分子分母满足和为2的幂次,则应输出Yes. 但注意$$未约分分数分子分母和为2的幂次\implies 对应的既约分数的分子分母和为2的幂次$$ 因此可以先约分,后判断,若满足分子分母和为2的幂次,则一定可以。 那么,对于既约分数,如果和不是2的幂次会怎么样? 设分数为$\frac{a}{b}$且既约,即$gcd(a,b)=1$则等价于存在奇质数$p$使得$p|a+b$假设手里有的数均为满足$p|分子+分母$现考虑我们得到的新数$(\frac x y+\frac a b)/2=\frac{xb+ay}{2yb}$,则考虑其分子分母的和。此次有可能产生约分,但不会约去p,这是因为$p\not| yb$,而这是因为:$p|yb\implies p|y \ \ \text{or}\ \ p|b$而$p|y且p|x+y\implies p|x$与$\frac{x}{y}$为既约分数矛盾,ab同理。 这可以知道我们不会约去p,因此考虑p是否整除既约后的分数的分子分母和,等价于考虑当前分数的分子分母和。而$xb+ay+2yb\equiv -xa-xa+2xa \equiv 0 \mod p$则p整除。 同理对调和均值也有,p依然整除分子分母和。 则可知,若初始分数的分子分母和不为2的幂次,那么永远不能得到1. 因为$1=\frac11$其分子分母和为2,不含奇质因子p 证明完毕。

2021/8/22
articleCard.readMore

Fix a grub bug

In this article, I will record how to fix the problem that grub can’t find Windows on nvme. So that someday, I can reuse this to help myself. First of all, install os-prober. Then, run update-grub or grub-mkconfig.Then you will find that os-prober is not working. It will print a warning saying like: OS-PROBER will not be executed in order to protect xxx. If you want to know more, please look GRUB-DISABLE-OR-PROBER document to find more information. Well, only stupid people will do as what the warning says. For me, I just vim /etc/default/grub and add one line GRUB-DISABLE-OR-PROBER = false in this file. Then run grub-mkconfig again. This silly problem will get fixed.

2021/3/20
articleCard.readMore

name and shakespeare

匿名函数与莎士比亚大定理 This article is translated from Here 提问:为什么在lambda calculus里面,函数必须是匿名的。 我正在观看一个Youtube视频,在这个视频里面,讲者介绍了Y组合子的概念。Y组合子概念产生的动机之一,正如讲者所述,是为了用lambda calculus来表示递归函数,以使得Church理论(任何能被实际上计算的东西都可以用lambda calculus来计算)保持成立。我的问题在于:为什么我们不能通过名字来简单的调用一个函数。例如日常生活中我们经常会写下这种式子:$$n(x,y)=x+y$$但是在lambda calculus里面我们不允许把这个函数与名字n相关联,我们只能匿名的定义它为$$(x,y)\rightarrow x+y$$为什么在lambda calculus里面我们不能拥有被命名的函数? 如果存在具名函数,我们会破坏什么准则? 或者仅仅是我搞错了视频的意思? 回答 关于这个问题的主要结论来自一个十六世纪晚期的英国数学家,他叫莎士比亚。他最著名的关于这个问题的论文名为《罗密欧与茱丽叶》,在1597年发表。 他的主要结论在第二幕的第二场景中阐明。即如下的著名定理: 名称有什么关系呢?玫瑰不叫玫瑰,依然芳香如故! 这个定理可以被直观的理解为"名字对意义毫无帮助"莎士比亚的论文的大部分内容是一个用来补充定理的例子,用以表明名字尽管名字没有任何意义,但它们却是无穷无尽的问题的根源。正如莎士比亚所指出的那样,名称可以在不改变含义的情况下进行更改,这一操作后来被丘奇及其追随者称为α转换。结果,如何确定名字表示的意义变成了造成了许许多多的问题。例如我们要发展“环境”的概念,在环境中名字-意义联系是确定的,并且发展出一系列的规则来辨别当前的环境。这使得计算机科学家们困惑了很长一段时间,引起了诸如臭名昭著的Funarg问题之类的技术难题。“环境”在许多流行的编程语言中仍然是个大问题,几乎和莎士比亚在其论文中提出的例子一样致命。这个问题也与形式语言理论中提出的问题接近,即必须将字母和形式系统定义为同构,以便强调字母符号是抽象实体,而与它们如何作为某些集合中的元素而“实现”无关。莎士比亚的主要结果也表明了科学随后即将与魔术和宗教告别。因为在魔术与宗教的世界里面,人们认为一个东西有它的“真名”。所有这一切的结论是:尽管名字可以方便人们的日常工作和生活,但是在理论研究中不被名字而困扰更为重要。记住:不是所有被叫做娘的都是你的母亲。 评论 最近,玫瑰正在被foobar所取代。

2021/1/25
articleCard.readMore

C++ Note 1

本文是我的C++笔记的第一篇My First C++ Note; 一些例子 1 1#include <iostream> 2 3using namespace std; 4 5int main() 6{ 7 string x{"222333"}; 8 char *y = "222333"; 9 string xx{x, 3};10 string yy{y, 3};11 cout << xx << endl;12 cout << yy << endl;13} 2 1void pp(int x) 2{ 3 cout<<"int"; 4} 5 6void pp(int* x) 7{ 8 cout<<"int*"; 9}101112int main()13{14 pp(nullptr);// 输出int*15 pp(NULL); // 有歧义,编译失败16} 一些疑问 1int s[2][2]{2 {1, 2}, {3, 4}};3for (int *i : s)4{5 for (int j{}; j < 2; ++j)6 {7 cout << i[j];8 }9} 这段代码为何生效?s的类型不应该是int()[2]吗?解释:s的类型为int[2][2],但是非常容易decay,因此大多数时候会提示为int()[2],例如你用char x{s};试图从报错中得到s的类型的时候就会提示为int()[2].此处从s中取出的应该是int[2],然后被decay为int追问:那有没有办法让i的类型为int[2]解答:写成auto& i 或者 int(&x)[2]即可完整代码为 1int s[2][2]{ 2 {1, 2}, {3, 4}}; 3for (int(&i)[2] : s) 4{ 5 for (int j : i) 6 { 7 cout << j; 8 } 9}10for (auto &i : s)11{12 for (auto j : i)13 {14 cout << j;15 }16}17for (int j : (*s))18{19 cout << j;20}

2021/1/5
articleCard.readMore

C Hello World Again

本文是我给地空学院的学生的C语言讲座的讲稿 Hello World Again [TOC] 指针与数组 a[i]=*(a+i)=i[a] int (*x)[10]与int *x[10]的区别前者是指向一个长度为10的整型数组的指针后者是一个长度为10的(指向整型变量的指针)的数组 指针与数组的互换 1for(int i=0;i<10;++i) 2{ 3 a[i]=0; 4} 5 6for (int *p = a; p != (a + 10); ++p) 7{ 8 printf("%d\n", *p); 9}10 C风格的字符串处理 字符串的表示 1char s[100]="Hello"; 1char *s="Hello";2// 这是一个特殊的行为3// 注意到int *x=2是完全错误的4// 但这个语句是对的5// 编译器会开辟一块内存来存放"Hello"并用s指向它6// 但注意:这个"Hello"是不可更改的7// 例如下面的语句会出现错误8s[1]='a'; 1printf("%c\n", "abc"[2]);//输出c2printf("%s\n", "abc"+1); //输出bc3//此处的"abc"会被解释为指向char数组{'a','b','c','\0'}的指针 1char s[100]={'a','b','c'}; 1char *s = (char *)malloc(sizeof(char) * 10);2*s++ = 'a';3*s++ = 'b';4*s++ = 'c';5printf("%s", s - 2); //输出bc6printf("%s", s - 1); //输出c7free(s - 3); //释放掉用malloc申请的内存8 字符串拷贝 1void strcpy1(char *a, char *b) //把b赋给a 2{ 3 while (*b) 4 { 5 *a++ = *b++; 6 } 7 while (*a) 8 { 9 *a++ = '\0';10 }11}12 13int main()14{15 char a[100];16 char b[100];17 while (1)18 {19 scanf("%s", b);20 strcpy1(a, b);21 printf("%s\n", a);22 }23} 求值顺序 1printf("%c%c%c\n",getchar(),getchar(),getchar()); 该语句在gcc编译器下的作用是:读入三个字符并倒序输出。因为gcc编译器的实现方法是从右向左求值 变量作用域 1{2 int x=2;3}4printf("%d",x); //不会输出 1int x=8;2{3 int x=3;4 printf("%d",x);// 输出35} 用宏实现max函数 #define MAX(x,y) x > y ? x : y但考虑MAX(1!=2,3)为1 != 2 > 3 ? 1 != 2 : 3由于!=的优先级小于>因此上式为1 != (2>3) ? 1!=2 : 3为(1 != 0) ? 1!=2 : 3为1 ? 1 : 3为1而该式应该是MAX(1,3)=3因此加括号为#define MAX(x,y) (x) > (y) ? (x) : (y)但考虑3+ MAX(1,2)为3 + 1 > 2 ? 1 : 2为4 > 2 ? 1 : 2为1 ? 1 : 2为1而实际上应该是5因此我们继续修改这个宏#define MAX(x,y) ((x) > (y) ? (x) : (y))但考虑MAX(i++,j++)展开后为i++ > j++ ? i++ : j++这会使得i与j都自增两次为此考虑这么定义宏 1#define MAX(x,y)({ \2 int _x = x; \3 int _y = y; \4 _x > _y ? _x : _y; \5}) 该宏会重新定义两个变量_x与_y来进行比较,从而使得MAX(i++,j++)符合要求。 函数与数组的相似点与共同点 相似点:数组声明:int x[10]函数声明:int sum(int,int)数组类型:int [10]函数类型:int (int,int)数组指针:int (x)[10]函数指针:int (sum)(int,int)数组指针类型:int ()[10]函数指针类型:int ()(int,int) 数组、函数共同点:1.数组、函数都不可拷贝。 2.因为第1点,数组、函数不可以做函数的返回值,但函数可以返回数组的指针或函数的指针。 3.数组、函数可用于函数形参,但因为第1点,编译器会对其做处理。如果形参类型为数组,实际形参类型会转换成元素类型的指针,例如voidfun(int arr[5])等价于void fun(int arr*)。如果形参类型为函数,实际形参类型会转换成对应的函数指针类型,例如void fun (int test())等价于voidfun( int (*test)()) 如何返回一个数组 用结构体包装一下 1#include <stdio.h> 2 3#define MAXNUM 1000 4 5struct Array 6{ 7 int a[MAXNUM]; 8}; 9 10struct Array DoubleIt(struct Array x)11{12 struct Array y;13 for (int i = 0; i < MAXNUM; ++i)14 {15 y.a[i] = (x.a[i] << 1); //移位运算符的优先级非常低,应该在可能的情况下加上括号16 }17 return y;18}19 20int main()21{22 struct Array a = {1, 2, 3, 4, 5};23 struct Array b = DoubleIt(a);24 for (int i = 0; i < 5; ++i)25 {26 printf("%d\n", b.a[i]);27 }28}29 推荐阅读书目: 《C专家编程》《C陷阱与缺陷》《征服C指针》以上三本在图书馆应该都能借到,特别推荐《征服C指针》,是日本最受欢迎的C语言书籍之一,写的很好 C学习经验 多写一些简单的程序做实验,验证自己的想法。从实践中学习。 多写代码,多写代码,多写代码。 找一些好书看,不要看谭浩强啥的。。 比如《C与指针》,《C专家编程》,《C陷阱与缺陷》,《明解C指针》。 学会使用搜索引擎。 不要害怕写代码,其实真的不难。。这个属于技术活,写的越多越熟练。 C其实是比较偏向底层的语言,如果有对于计算机硬件底层相关的知识可能会更好理解。

2021/1/2
articleCard.readMore

N queens Problem

This article will provide a fast way to find a solution for the famous N queens problem. Note: we only try to find one possibility of all positions of chesses, NOT (ALL posibilities). For example, when N=8, there are 92 ways to put the chesses. However, our algorithm just focus on finding one of these 92 ways. If you still feels not familar with the problem we will solve. Please Google 8 queens problem and look down. N=4 When N=4, there are only two ways to put these chesses. 10100200013100040010 10010210003000140100 Our program, as I just said, will try to find one of the two ways. So, Here we go~ Code 1#include <stdio.h> 2#include <stdlib.h> 3#include <time.h> 4#include <string.h> 5 6#define N 123456789 7 8 9// 最大能运行多少个皇后 10 11int output_ok; 12 13void swap_int(int *p, int *q) 14{ 15 int temp; 16 temp = *(p); 17 *(p) = *(q); 18 *(q) = temp; 19} 20 21int swap_ok, change_of_collisions, temp, n; 22// swap_ok判断是否交换两个皇后会使得collisions减小 23// change_of_collisions计算交换前后的collisions的变化量 24// temp用作交换两个变量的临时变量 25// n为皇后数目 26 27void print_solve1(int *x, int n, FILE *fp) // 传入queen,n和fp 28// 输出形象化的棋盘 29// *代表皇后 -代表棋盘 30{ 31 fprintf(fp, "\nBEGIN\n"); 32 for (int i = 1; i <= n; ++i) 33 { 34 for (int j = 1; j <= n; ++j) 35 { 36 if (j == x[i]) 37 { 38 fprintf(fp, "*"); 39 } 40 else 41 { 42 fprintf(fp, "-"); 43 } 44 } 45 fprintf(fp, "\n"); 46 } 47 fprintf(fp, "\nEND\n\n\n"); 48} 49 50void print_solve(int *x, int n, FILE *fp) // 传入queen,n,fp 51// 输出皇后数列到文件n-queens-out.txt 52{ 53 fprintf(fp, "%d\n", n); 54 for (int i = 1; i <= n; ++i) 55 { 56 fprintf(fp, "%d ", x[i]); 57 } 58 if (n <= 20) 59 { 60 print_solve1(x, n, fp); 61 } 62} 63 64int main() 65{ 66 FILE *fp = fopen("n-queens-out.txt", "w"); 67 68 int n; 69 // n皇后 70 printf("@Author: ZhangJia\n"); 71 printf("@university: SCGY of USTC\n"); 72 printf("@Github: Oyyko\n"); 73 printf("\n"); 74 printf("\n"); 75 printf("Please Input the number N where the board is N*N.\n"); 76 printf("which means we will have N queens\n"); 77 scanf("%d", &n); 78 79 printf("Do you want to output to\"n-queens-out.txt\"\n"); 80 printf("input 1 as yes ,other character as no\n"); 81 scanf("%d", &output_ok); 82 if (output_ok != 1) 83 { 84 output_ok = 0; 85 } 86 printf("\nProgram START\n"); 87 88 clock_t start, end; 89 //用于记录时间 90 91 int *pre_queen = (int *)malloc(sizeof(int) * N); 92 int *pre_dn = (int *)malloc(sizeof(int) * (2 * N - 1)); 93 int *pre_dp = (int *)malloc(sizeof(int) * (2 * N - 1)); 94 int *pre_attack = (int *)malloc(sizeof(int) * N); 95 96 int *queen = pre_queen - 1; 97 int *dn = pre_dn - 2; 98 int *dp = pre_dp - 1 + n; 99 int *attack = pre_attack - 1;100 // 四个pre数组开辟空间101 // 之后由对应的指针指向它们中间的位置102 // 从而满足算法运行对数组下标的要求(即可能会出现负的下标)103 // 例如 queen[1] = pre_queen[0]104 // 例如 queen[m] = pre_queen[m-1]105 // 例如 dn[m] = pre_dn[m-2]106 // 例如 dp[m] = pre_dp[m+n-1]107 // 例如 dp[-2] = pre_dp[-2+n-1]108109 // 数列queen[i]=j 表示第i行的皇后在第j列110 // 数列dn[i]=k 表示斜率为负的对角线中编号为i的对角线上有k个皇后111 // 数列dp[i]=k 表示斜率为正的对角线中编号为i的对角线上有k个皇后112 // dn,dp的编号方法分别为(列号+行号),(列号-行号)113 // 数列attack[i]=j 表示第i个被攻击的皇后在queen中的序号为j114115 memset(pre_dn, 0, sizeof(int) * (2 * n - 1));116 memset(pre_dp, 0, sizeof(int) * (2 * n - 1));117118 int limit, collisions, number_of_attacks, loopcount;119120 collisions = 1;121 start = clock();122123 while (collisions)124 // 当评估函数为0时,不断执行,直到评估函数为0125 // 评估函数为0也就是找到一组解126 {127 memset(pre_dn, 0, sizeof(int) * (2 * n - 1));128 memset(pre_dp, 0, sizeof(int) * (2 * n - 1));129 collisions = 0;130131 for (int i = 0; i <= n - 1; ++i)132 {133 pre_queen[i] = i + 1;134 }135 int Search_Max = (n << 2);136 size_t x = -1;137 x = (x >> 1);138 int i, j;139 int m;140 int rand_max = n;141 size_t rand_seed;142 rand_seed = (size_t)time(NULL);143 for (i = 1, j = 1; i <= Search_Max && j <= n; ++i)144 {145 // 我们的目标是在初始化阶段生成一个很好的初态146 // 这样我们就能很快的爬山找到最优解147 // 因为初态的collisions已经很小148 // 因此爬山的过程会大幅度缩短149 // 优化的方法就是在初始化的时候就尽可能的避免碰撞150 // j的作用是衡量已经有多少个皇后被优化处理了,如果优化了n个,那么说明该停止优化了151 // i的作用是限制优化阶段所采用的时间 每循环一次i就自加1152 // 当i大于Search_Max的时候,强制退出优化过程153 rand_seed *= 1103515245;154 rand_seed += 12345;155 rand_seed = rand_seed % (x + 1);156 // LCG随机数生成器157 // X(n+1) = (a * X(n) + c) % m158 // 此处我采用a等于1103515245 c等于12345 的参数取法159 // 该取法为gcc编译器的参数取值160 // 为了合理的取值范围我采用了size_t类型161 // 实际上就是生成一个 0~x的随机数162163 m = rand_max * ((double)rand_seed / (double)x) + j;164 // m为 j~n之间的一个随机数165 if (!(dn[queen[m] + j]) && !(dp[queen[m] - j]))166 // 如果 皇后m 与 皇后j 交换可以减小评估函数的值167 // 那么就交换m与j168 {169 swap_int(&queen[m], &queen[j]);170 ++dn[queen[j] + j];171 ++dp[queen[j] - j];172 ++j;173 --rand_max;174 }175 }176 // 在优化完了j个皇后之后177 // 打乱剩下的皇后,以使得初态更加平均178 // 同时计算出所有dn与dp的值179 for (i = j; i <= n; ++i)180 {181 m = rand() % rand_max + i;182 swap_int(&queen[m], &queen[i]);183 ++dn[queen[i] + i];184 ++dp[queen[i] - i];185 --rand_max;186 }187 // 以上完成了对整个棋盘的初始化188189 for (int i = 2; i <= 2 * n; ++i)190 {191 if (dn[i] > 1)192 collisions += (dn[i] - 1);193 }194 for (int i = 1 - n; i <= n - 1; ++i)195 {196 if (dp[i] > 1)197 collisions += (dp[i] - 1);198 }199 if (!collisions)200 // 若恰好生成了满足要求的皇后排列,则输出答案并且结束程序201 {202 end = clock();203 printf("It takes %f seconds to find a solotion\n", (double)(end - start) / CLOCKS_PER_SEC);204 print_solve(queen, n, fp);205 exit(0);206 }207208 limit = collisions >> 1; // means limit=0.5*collisions209 // limit的作用是评估何时适合重新计算attack数组,从而达到更快的运行速度。210 // 如果每次交换都重新计算attack数组,那么开销过大.211 // 为此我们采用设置阈值的方法212 // 仅当collision<limit时,才重新计算attack。从而减小了不必要的损耗213214 // compute attack START215216 number_of_attacks = 0;217 int k = 1;218 for (int i = 1; i <= n; ++i)219 {220 if (dn[queen[i] + i] > 1 || dp[queen[i] - i] > 1)221 {222 attack[k++] = i;223 ++number_of_attacks;224 }225 }226 //compute attack END227228 loopcount = 0;229 // loopcount用来判断何时随机重启230 // 每次爬山都会增加loopcount的值231 // 当loopcount比较大时,说明爬山法陷入了局部困境,需要进行随机重启232233 // Initialization END234 // 初始化过程结束,下面开始爬山算法235236 while (loopcount < (n << 5))237 {238 for (int k = 1; k <= number_of_attacks; ++k)239 {240 int i = attack[k];241 int j = ((rand() << 6) + rand()) % (n) + 1;242 // 取一个被攻击的皇后和一个随机取得皇后,观察是否可以交换243244 swap_ok = 0;245 change_of_collisions = 0;246247 change_of_collisions -= (dp[queen[i] - i] > 1);248 change_of_collisions -= (dp[queen[j] - j] > 1);249 change_of_collisions -= (dn[queen[i] + i] > 1);250 change_of_collisions -= (dn[queen[j] + j] > 1);251252 change_of_collisions += (dn[queen[j] + i] >= 1);253 change_of_collisions += (dn[queen[i] + j] >= 1);254 change_of_collisions += (dp[queen[j] - i] >= 1);255 change_of_collisions += (dp[queen[i] - j] >= 1);256 // 计算评估函数的改变量257 if (change_of_collisions < 0)258 {259 if (!(queen[i] + i - queen[j] - j) && !(dp[queen[i] - j]))260 {261 change_of_collisions += 2;262 }263 if (!(queen[i] - i - queen[j] + j) && !(dn[queen[i] + j]))264 {265 change_of_collisions += 2;266 }267 if (change_of_collisions < 0)268 {269 // perform swap270 // 若改变量小于0,则执行交换271 --dn[queen[i] + i];272 --dp[queen[i] - i];273 --dn[queen[j] + j];274 --dp[queen[j] - j];275 ++dn[queen[j] + i];276 ++dn[queen[i] + j];277 ++dp[queen[j] - i];278 ++dp[queen[i] - j];279280 temp = queen[j];281 queen[j] = queen[i];282 queen[i] = temp;283284 collisions += change_of_collisions;285286 if (!collisions)287 // 若找到了解,则输出答案并且结束程序288 {289 end = clock();290 printf("It takes %f seconds to find a solotion\n", (double)(end - start) / CLOCKS_PER_SEC);291 printf("Press any key to EXIT\n");292 if (output_ok)293 print_solve(queen, n, fp);294 getchar();295 getchar();296 exit(0);297 }298 if (collisions < limit)299 // 当棋盘变动较大时,重新计算attack数列300 {301 limit = collisions >> 1;302 // compute attack303304 number_of_attacks = 0;305 int k = 1;306 for (int i = 1; i <= n; ++i)307 {308 if (dn[queen[i] + i] > 1 || dp[queen[i] - i] > 1)309 {310 attack[k++] = i;311 ++number_of_attacks;312 }313 }314 }315 }316 }317 }318319 loopcount = loopcount + number_of_attacks;320 }321 }322323 return 0;324}

2020/12/27
articleCard.readMore