第一节：Prompt定义及基本框架一、 什么是Prompt大模型 prompt 指在大语言模型应用中，用于 “提示” 模型唤起特定能力以解决实际问题的提问方式。早期它被称作 “输入形式” 或 “输入模板”，后来 “prompt”（提示）这一叫法因契合大语言模型语境，能准确体现其在调用模型能力方面的关键作用，成为公认术语。大模型多数核心能力在预训练阶段形成，prompt 就像钥匙，引导模型从预训练积累的海量信息中，精准唤起如理解复杂文本、总结信息、生成内容、逻辑推理等能力，满足用户实际需求。它并非把模型当作单纯知识库，而是高效调用其能力，实现类似人类运用智能解决复杂问题的效果，是 prompt 工程的核心，对用好大语言模型意义重大。二、 撰写Prompt的基本框架1、Prompt包含的要素● 指令：想要模型执行的特定任务或指令。● 上下文：包含外部信息或额外的上下文信息，引导语言模型更好地响应。● 输入数据：用户输入的内容或问题。● 输出指示：指定输出的类型或格式。2、Prompt五大框架2.1 RTF框架RTF(Role-Task-Format)框架是一个非常简单通用的Prompt提示框架，我们和任意大模型对话场景下都可以使用该规范进行改进输出。● R-Role(角色)：指定大模型担当固定角色(程序员、数据分析师、讲解员、记者等等)● T-Task(任务): 任务，告诉大模型需要为我们做的事情● F-Format(格式)：大模型最终结果的返回格式(比如：表格、Markdown、英文等等)主要优点：● 简单、方便。● 指定Role角色，可以让大模型在当前的角色范围内回答知识，这在一些特定的领域中非常有效。● 指定Role角色也能让工程上检索知识能够确定边界范围，配合元数据所发挥的威力会更强。● 如果结合RAG知识内容检索，那么上下文回答的内容会让用户感觉更加是顺畅。2.2 思考链模式通过这种模式来逐步改善大模型的推理能力，非常适合一些复杂的任务处理。例如：● 分析型或者逻辑推理型的任务● 决策● 解决问题(比如程序员根据错误日志找Bug)而要使用这种模式，只需要在末尾添加”让我们逐步思考”即可。例如：# 数据源（与指令区分） user_datasource = """XXX……""" prompt1 = """分析一下在人工客服服务场景下，'''{user_datasource}'''中客户有哪些诉求。用一句话概括。""" prompt2 = """分析一下在人工客服服务场景下，'''{user_datasource}'''中客户有哪些诉求。用一句话概括。让我们逐步思考。""" # 模型输出结果 output1：在人工客服服务场景下，客户主要诉求为：微信账号存在安全风险导致无法添加好友、单点拦截、下载安装微信出现问题等，寻求客服协助解决问题。 output2：在人工客服服务场景下，客户主要诉求为：微信账号存在安全风险导致无法添加好友，以及因违规行为被限制登录，客户希望客服能够尽快处理这些问题。 # 分析结果 output1中的”单点拦截”并不是用户的诉求，而output2显然更加正确2.3 RISEN框架● R-Role:大模型扮演的角色● I-Instructions: 指示命令，和Task-任务差不多● S-Steps: 步骤● E-End Goal: 最终目标● N-Narrowing(Constraints): 缩小范围(约束条件)，和RTF框架中的Format有异曲同工之妙，一个是格式的约束，而这里的约束可以是任意方面，比如回答的内容(特定领域)、字数限制等等方面该框架主要适合：● 撰写具有特定约束的任务(例如博客文章)● 有明确指导方针的任务（例如商业计划）2.4 RODES框架● R-Role: 角色● O - Objective: 目标● D - Details: 详细的细节● E - Examples: 示例● S - Sense Check: 感官检查2.5 密度链模式密度链模式Prompt是Salesforce、麻省理工学院和哥伦比亚大学的研究人员推出的一种新提示，它非常的高效，使用递归来创建越来越好的输出的提示，与普通提示生成的 GPT-4 摘要相比，它生成的摘要更加密集且更适合人们理解。适合：● 总结● 改进您最喜欢的提示● 通过递归生成可用的长格式内容(感兴趣的同学可以自己检索一下)第二节：打造高效Prompt的两大核心原则在Prompt工程领域，掌握精妙的撰写策略是释放语言模型强大潜力的关键。本节将深入探讨prompt撰写的两大核心策略，为您揭示如何通过优化指令，引导模型生成更贴合需求、更具质量的输出。一、原则一：编写明确和具体的指令为了让模型精准理解我们的意图，编写清晰、明确且具体的指令至关重要。这不仅能避免模型产生误解，还能显著提升输出结果的相关性和准确性。策略1：使用分隔符清晰界定输入部分在构建prompt时，使用分隔符将特定文本部分与提示的其他部分清晰隔开，能有效避免提示词冲突。任何能让模型识别出单独部分的符号都可作为分隔符，常见的包括：● 章节标题：通过不同层级的标题区分不同内容模块。● 三重双引号：""" ，在代码编写中常用于包裹较长的文本字符串。● 三重单引号：''' ，功能与三重双引号类似，适用于不同的语法环境。● 三重破折号：--- ，在文本中起到明显的分隔作用。● 角括号：<> ，常被用于标记特定元素。● XML标签：，利用结构化的标签形式区分不同内容。示例1：假设我们需要对一段产品描述进行总结，可通过如下方式使用分隔符： product_description = f"""这是一款全新的智能手表，具备心率监测、睡眠追踪等健康功能，还支持多种运动模式记录，拥有高清显示屏和长达7天的续航能力。""" prompt = f"""请总结以下产品描述：'''{product_description}'''""" 示例2：如果要让模型对一段用户评论进行情感分析，示例如下： user_comment = f"""这款手机的拍照效果真的太棒了，色彩还原度高，夜景模式也很出色，唯一不足的是电池续航有点短。""" prompt = f"""分析以下用户评论的情感倾向：''' {user_comment}''' """这种方式能够确保模型明确区分指令和待处理文本，避免因指令混淆而产生错误输出。策略2：要求结构化输出为了便于后续对模型输出进行解析和处理，我们可以要求模型以特定的结构化格式输出，如HTML或JSON。这种方式能极大提高数据处理的效率，尤其是在Python编程环境中，结构化输出可直接读入字典或列表中，方便进一步的分析与操作。示例 1：要求模型以 JSON 格式输出。prompt = "请以json格式列出每个nodeType所体现的用户诉求、客服方案。每一个json-value不超过20个字" # 数据源（与指令区分） user_datasource = """XXX……""" # 模型输出 { "IVR": { "用户诉求": "微信使用问题求助", "客服方案": "引导正常使用或自助处理" }, "ASYNC": { "用户诉求": "账号封禁等相关申诉", "客服方案": "按规则处理或引导自助" }, "AI": { "用户诉求": "支付相关问题咨询", "客服方案": "要求用户详细描述问题" } }示例2：以HTML格式输出一个简单的书籍。prompt = "请以HTML格式列出三本你推荐的科幻小说及其作者。" # 模型输出 《三体》 - 刘慈欣 《基地》 - 艾萨克·阿西莫夫 《银河帝国：机器人五部曲》 - 艾萨克·阿西莫夫 策略3：要求模型检查条件是否满足在面对复杂任务时，如果在一些假设条件，而这些条件并非总是成立，那么我们需要引导模型首先对这些假设进行检查。若条件不满足，模型应明确指示并停止继续执行任务，以避免无效计算和错误结果。同时，考虑潜在的边缘情况，并为模型设定相应的处理规则，能够有效减少意外错误，确保模型输出的稳定性和可靠性。示例：假设我们要求模型计算一个数学表达式，但前提是表达式所有都为正数。 expression = "5 + 3 - (-2)" prompt = f"""首先检查表达式 '{expression}' 中的所有数字是否都为正数。如果是，请计算该表达式的值；如果不是，请输出'表达式中存在非正数，无法计算'。"""模型输出表达式中存在非正数，无法计算策略4：Few-shot prompting（少样本提示）在要求模型执，提供成功完成任务的示例，能够帮助模型更好地理解任务要求和期望输出的格式。通过这些示例，模型可以学习到任务的模式和规律，从而在处理实际输入时生成更符合预期的结果。少样本提示尤其适用于处理一些复杂或具有特定格式要求的任务，能够显著提升模型的表现。示例：要求模型按照特定格式将中文词汇翻译成英文并造句。prompt = """ 示例1： 中文词汇：苹果 翻译：apple 造句：I like to eat apples.示例2：中文词汇：学校 翻译：school 造句：I go to school every day. 中文词汇：图书馆 翻译： 造句： """ # 模型输出 翻译：library 造句：I often read books in the library.二、原则二：给予模型充足的思考时间当模型因急于得出结论而出现推理错误时，我们需要通过优化式，为模型提供更清晰的推理路径，引导其进行更深入、全面的思考。策略1：明确完成任务所需的步骤为模型详细指定完成任务所需遵循的步骤，能够帮助模型有条不推理和计算。通过这种方式，模型可以更清晰地理解任务的逻辑结构，避免因思路混乱而产生错误。例如，要求模型按照特定格式输出，能够引导其在处理过程中注重信息的组织和呈现方式。示例：要求模型分析一首古诗，格式为“诗歌主题 - 主要意象 - 表达情感”。poem = "床前明月光，疑是地上霜。举头望明月，低头思故乡。" prompt = f"""请按照“诗歌主题 - 主要意象 - 表达情感”的格式分析这首诗：{poem}""" # 模型输出 思乡之情 - 明月 - 对故乡的深切思念 ### 策略2：引导模型在得出结论前充分思考方案 明确指示模型在得出最终结论前，先进行充分的推理和分析，往往能获得更优的结果。这种方式能够促使模型深入挖掘问题的本质，综合考虑各种因素，从而避免因片面或仓促的判断而导致的错误。示例：问模型“如何提高公司的员工满意度”。prompt = "在回答如何提高公司的员工满意度之前，请先思考至少三个方面的因素，然后综合这些因素给出全面的建议。" # 模型输出 首先，从薪酬福利方面，可适当提高薪资水平、完善福利体系，如增加带薪休假天数等；其次，从职业发展角度，为员工提供更多培训机会和晋升通道；再者，从工作环境来看，营造舒适、和谐的办公氛围，加强团队建设活动。综合这些方面，提高公司员工满意度可以从优化薪酬福利、提供职业发展机会以及改善工作环境等措施入手。三、模型的局限性与应对策略尽管语言模型在大量数据的训练下展现出强大的能力，但它们并非完美无缺。其中，“幻觉”现象是模型面个重要挑战。模型“幻觉”即使模型在训练过程中接触了海量知识，但它并不能完美记住所有信息，也难以精准把握自身知识的边界。这就导致在面对晦涩主题的问题时，模型可能会编造出看似合理但实际上错误的内容，这就是所谓的“幻觉”。解决幻觉的一种策略为了有效应对幻觉问题，我们可以要求模型在回答问题前，首先从给定文本中寻找相关引用，并依据这些引用来构建答这种方式，模型的回答将基于可靠的信息来源，而非凭空臆测，从而大大提高回答的准确性和可信度。同时，这种方法还能够实现答案的可追溯性，便于对模型输出进行验证和审查。示例：给定一段关于历史事件的文本，要求模型回答相关问题。text = """1861年，美国爆发了南北战争，这场战争是美国历史上的重要转折点，它主要围绕奴隶制的存废问题展开，北方主张废除奴隶制，南方则坚持保留。最终北方取得了胜利，为美国的工业化发展铺平了道路。""" prompt = f"""请从上述文本中找出相关信息，回答南北战争主要围绕什么问题展开？""" # 模型输出 南北战争主要围绕奴隶制的存废问题展开，北方主张废除奴隶制，南方则坚持保留。第三节：Prompt技术剖析与应用在深入探索 prompt 技术的征程中，我们将逐一解析多种关键技术，助力您精准把握其核心要点与应用场景，切实提升应用能力。一、零样本提示（Zero-Shot Prompting）零样本提示是一种让模型在没有特定任务示例展示的情况下直接处理任务的技术。其原理在于模型经过大量数据训练和指令调整后，自身具备了一定的通用知识和任务理解能力。例如：prompt = "判断此文本的情感倾向为积极、消极或中性：这部电影情节紧凑，特效惊人。情感：" # 模型会依据自身预训练知识对这个提示进行处理并输出结果模型能依据自身预训练知识输出“积极”。然而，由于缺乏具体示例引导，对于复杂任务，其效果可能受限。二、少样本提示（Few-Shot Prompting）少样本提示在零样本提示效果不佳时发挥作用。它通过为模型提供少量任务示例，帮助模型学习任务模式和规律。例如：prompt = """“whatpu”是坦桑尼亚的一种小型毛茸茸的动物。一个使用 whatpu 这个词的句子的例子是：我们在非洲旅行时看到了这些非常可爱的whatpus。“farduddle”是指快速跳上跳下。一个使用 farduddle 这个词的句子的例子是：""" # 模型会根据前面的示例学习并生成新的句子通过提供如上述新词汇造句的示例，模型能够模仿示例的结构和逻辑生成新的内容。不过在复杂推理任务中，仍需进一步优化。三、思维链提示（Chain-of-Thought Prompting）思维链提示旨在为模型提供清晰的推理步骤引导，从而显著提升其在复杂推理任务中的表现。它通过在提示中展示详细的推理过程，让模型学习如何逐步分析和解决问题。比如：prompt = """Roger 有 5 个网球，他又买了 2 罐网球，每罐有 3 个球，他现在共有多少球？ Roger 一开始有 5 个球。 2 罐网球，每罐 3 个，共 6 个球。 5 + 6 = 11。 答案是 11。""" # 模型按照提示中的推理步骤进行计算和回答此技术可与少样本提示结合，增强效果，尤其适用于算术、常识推理等任务，帮助模型更有条理地处理问题。四、自我一致性（Self-Consistency）自我一致性技术主要用于优化思维链提示中的推理路径选择。其核心思想是通过提供多个少样本推理示例，让模型从多样的推理结果中筛选出最一致的答案，增强模型在算术和常识推理任务中的可靠性。比如：prompt = """Q：林中有 15 棵树。林业工人今天将在林中种树。完成后，将有 21 棵树。林业工人今天种了多少棵树？ A：我们从 15 棵树开始。后来我们有 21 棵树。差异必须是他们种树的数量。因此，他们必须种了 21 - 15 = 6 棵树。答案是 6。 Q：停车场有 3 辆汽车，又来了 2 辆汽车，停车场有多少辆汽车？ A：停车场已经有 3 辆汽车。又来了 2 辆。现在有 3 + 2 = 5 辆汽车。答案是 5。 Q：当我 6 岁时，我的妹妹是我的一半年龄。现在我 70 岁了，我的妹妹多大？ A：""" # 模型会参考前面的推理示例对最后一个问题进行回答，并从多个可能的回答中选择最一致的答案五、生成知识提示（Generated Knowledge Prompting）生成知识提示主要用于解决模型在处理需要额外知识的任务时出现的局限性。它的操作方式是先让模型生成与任务相关的知识，再整合这些知识得出准确答案。比如：prompt = """输入：高尔夫球的一部分是试图获得比其他人更高的得分。 知识：高尔夫球的目标是以最少的杆数打完一组洞。一轮高尔夫球比赛通常包括 18 个洞。每个洞在标准高尔夫球场上一轮只打一次。每个杆计为一分，总杆数用于确定比赛的获胜者。 解释和答案：""" # 模型根据生成的知识对问题进行回答像这样先生成相关知识，再整合知识得出准确答案“不是，高尔夫球的目标是以最少杆数完成比赛，而非追求更高得分”，有效提升模型在常识推理任务的准确性。六、链式提示（Prompt Chaining）链式提示是将复杂任务拆解为多个子任务，通过逐个子任务生成提示并传递结果的方式来实现复杂任务的有序处理。以文档问答为例：# 第一个提示用于提取相关引文 prompt1 = """你是一个很有帮助的助手。你的任务是根据文档回答问题。第一步是从文档中提取与问题相关的引文，由####分隔。请使用<quotes></quotes>输出引文列表。如果没有找到相关引文，请回应“未找到相关引文！”。#### {{文档}} ####""" # 假设这里有一个文档变量 doc，将其代入提示中进行处理 # 处理后得到引文列表，假设为 quotes_list # 第二个提示根据引文和文档生成回答 prompt2 = """根据从文档中提取的相关引文（由<quotes></quotes>分隔）和原始文档（由####分隔），请构建对问题的回答。请确保答案准确、语气友好且有帮助。#### {{文档}} #### <quotes>{quotes_list}</quotes>""" # 模型根据第二个提示生成最终回答这种方式提高了任务处理的准确性与可靠性，使得模型能够逐步深入地处理复杂问题。七、思维树（ToT）思维树框架是为了帮助模型应对复杂的探索性任务而设计的。它通过维护一棵思维树，让模型在解决问题时能够生成和评估中间思维步骤，并结合搜索算法进行系统性探索。示例如下：# 假设给定数字为 4、9、10、13 prompt = """输入: 4 9 10 13 4 + 9 = 13 (left 10 13 13) 10 - 4 = 6 (left 6 9 13) 13 - 6 = 7 13 - 9 = 4 4 + 6 = 10 (left 7 9 4 * 6 = 24 (left 4 6) 评估是否能得到 24: sure """ # 模型按照思维树的步骤进行推理和计算，并根据评估结果继续搜索或得出答案八、检索增强生成（RAG）检索增强生成（RAG）技术将信息检索与文本生成相结合，专门用于处理知识密集型任务。它通过检索相关文档来为模型提供额外的知识支持，从而缓解模型的“幻觉”问题。例如：# 假设这里有一个检索函数 retrieve_documents 和一个生成答案的函数 generate_answer question = "谁是首次登上月球的人？" documents = retrieve_documents(question) prompt = "根据以下文档回答问题：" + str(documents) + " 问题是：" + question answer = generate_answer(prompt) # 最终得到的 answer 即为模型结合检索结果生成的答案这有效确保答案基于可靠知识源，在自然问题、事实验证等基准测试中表现卓越。九、自动推理并使用工具（ART）自动推理并使用工具（ART）技术使模型能够自动生成包含推理步骤的程序，并在需要时调用外部工具。例如：prompt = """计算 16 个苹果平均分给 4 个人，每人几个，再加上 3 是多少？ 请生成解决这个问题的程序步骤。""" # 模型会生成类似下面的程序步骤 # result = (16 / 4) + 3 # 然后可以在代码中执行这个程序步骤得到最终答案在 BigBench 和 MMLU 基准测试中，ART 在多种任务上表现突出，显著超越少样本提示和自动 CoT 等方法，提升了模型解决问题的能力和效率。十、自动提示工程师（APE）自动提示工程师（APE）技术能够自动生成和筛选任务指令。它通过利用大型语言模型生成指令候选项，再依据评估分数选择最佳指令，从而提升提示生成的效率与效果。比如：# 假设这里有一个生成指令候选项的函数 generate_candidates 和一个选择最佳指令的函数 select_best_instruction article = "这里是一篇新闻文章的内容" candidates = generate_candidates(article) prompt = select_best_instruction(candidates) + " " + article # 模型根据最终的 prompt 生成文章总结例如总结新闻文章时，它先利用大型语言模型生成如“提取文章关键人物、事件和时间”“概括文章主要内容并突出重点”等指令候选项，再依据评估分数选择最佳指令，指导模型生成高质量总结。十一、Active-PromptActive-Prompt 技术主要用于解决思维链示例有效性的问题。它通过先查询模型生成多个答案，计算不确定度后挑选最不确定的问题由人类注释示例，再用新示例推断其他问题，从而优化模型对不同任务的适应性。比如：# 假设这里有一个查询模型生成答案的函数 query_model 和一个计算不确定度的函数 calculate_uncertainty prompt = "对以下文本进行分类：这是一篇科技类文章。" answers = query_model(prompt, num_answers=5) # 生成 5 个答案 uncertainty = calculate_uncertainty(answers) if uncertainty > threshold: # 如果不确定度大于设定阈值 # 选择最不确定的问题由人类注释示例，然后更新提示并重新查询模型 new_prompt = update_prompt_with_annotated_example(prompt) final_answer = query_model(new_prompt) else: final_answer = select_most_consistent_answer(answers) # 最终得到的 final_answer 即为经过 Active-Prompt 处理后的答案通过这种方式，能够提高模型在文本分类等任务中的准确性。十二、方向性刺激提示（Directional Stimulus Prompting）方向性刺激提示通过训练策略 LM 生成引导提示，增强对模型生成结果的掌控力。例如文本摘要任务：article = "这里是一篇文章的内容" prompt = "请根据文章内容生成一个引导提示，重点关注文章的主要事件和结果。" # 策略 LM 会根据这个提示生成如“重点提取文章中事件的主体、发生的时间和最终结果”的引导提示 # 然后可以将这个引导提示用于后续的文本摘要任务中在文本摘要任务中，策略 LM 依据文章主题、关键信息等生成引导提示，指导模型生成符合需求的摘要，提升摘要质量。十三、PAL（程序辅助语言模型）PAL（程序辅助语言模型）技术让模型生成程序来解决问题，借助编程运行时提升解决复杂问题的能力。以日期计算问题为例：question = "今天是 2023 年 2 月 27 日，我出生于 25 年前，我的出生日期是多少？" prompt = """ import datetime from dateutil.relativedelta import relativedelta # Q: 2015 年在 36 小时后到来。从今天起一周后的日期是多少（格式为 MM/DD/YYYY）？ # 如果 2015 年在 36 小时后到来，那么今天是 36 小时前。 today = datetime(2015, 1, 1) - relativedelta(hours=36) # 从今天起一周后， one_week_from_today = today + relativedelta(weeks=1) # 格式化后的答案 one_week_from_today.strftime('%m/%d/%Y') # Q: 今天是 2023 年 2 月 27 日，我出生于 25 年前，我的出生日期是多少？ """ # 模型会生成类似下面的代码 # today = datetime(2023, 2 27) # born = today - relativedelta(years=25) # born.strftime('%m/%d/%Y') # 然后可以在代码中执行模型生成的代码得到最终答案十四、ReAct 框架ReAct 框架使模型交错生成推理轨迹和操作，提升答案的可靠性与可解释性。以回答问题为例：question = "除苹果遥控器外，哪些设备可控制其设计交互的程序？" prompt = """问题：除苹果遥控器外，哪些设备可控制其设计交互的程序？ 思考 1：我需要搜索苹果遥控器相关程序及可替代控制设备。 操作 1：搜索 [苹果遥控器 可替代控制设备] 观察 1：搜索结果为…… 思考 2：根据搜索结果，分析哪些设备符合要求。 操作 2：筛选 [符合要求的设备] 观察 2：筛选结果为…… 思考 3：整理最终答案。 操作 3：整理 [最终答案] """ # 模型会按照提示中的推理轨迹和操作步骤进行处理，并最终得出答案在知识密集型和决策任务中表现良好，通过与外部环境交互获取信息来辅助推理。十五、自我反思（Reflexion）自我反思框架包含参与者、评估者和自我反思三个模型，旨在帮助模型从错误中学习并提升性能。模拟编程任务的示例如下：# 假设这里有参与者模型 actor、评估者模型 evaluator 和自我反思模型 reflexion code = "这里是一段初始代码" # 参与者模型生成尝试解决问题的代码和动作 result = actor(code) # 评估者模型评价输出 score = evaluator(result) # 自我反思模型根据评价结果生成改进建议 reflection = reflexion(score, result) # 将改进建议应用到下一次的代码生成中，实现自我学习和提升如代码出现语法错误，自我反思模型会提示可能错误原因及修正方向，帮助模型快速学习。附录一、术语表Prompt：在大语言模型应用中，用于“提示”模型唤起特定能力以解决实际问题的提问方式，是prompt工程的核心。RTF框架：Role - Task - Format框架，通过指定角色、任务和格式规范大模型输出，简单通用。思考链模式：在提示末尾添加“让我们逐步思考”，引导大模型逐步分析复杂任务，改善推理能力。RISEN框架：包含角色、指示命令、步骤、最终目标和缩小范围（约束条件），适合特定约束或明确指导方针的任务。RODES框架：涵盖角色、目标、详细细节、示例和感官检查的提示框架。密度链模式：利用递归创建更好输出的提示，生成的摘要更密集且适合理解，适用于总结等任务。少样本提示（Few - shot prompting）：为模型提供少量任务示例，帮助其学习任务模式和规律，提升在复杂或特定格式任务中的表现。零样本提示（Zero - Shot Prompting）：模型在无特定任务示例展示下，依靠自身预训练知识和任务理解能力直接处理任务。思维链提示（Chain - of - Thought Prompting）：为模型展示详细推理过程，提升其在复杂推理任务中的表现，可与少样本提示结合。自我一致性（Self - Consistency）：通过提供多个少样本推理示例，让模型筛选最一致答案，增强在算术和常识推理任务中的可靠性。生成知识提示（Generated Knowledge Prompting）：先让模型生成与任务相关知识，再整合知识得出准确答案，解决模型处理需额外知识任务的局限性。链式提示（Prompt Chaining）：将复杂任务拆分为多个子任务，逐个子任务生成提示并传递结果，提高任务处理准确性与可靠性。思维树（ToT）：通过维护思维树，让模型生成和评估中间思维步骤，结合搜索算法应对复杂探索性任务。检索增强生成（RAG）：将信息检索与文本生成结合，检索相关文档为模型提供知识支持，缓解“幻觉”问题。自动推理并使用工具（ART）：使模型自动生成包含推理步骤的程序，并在需要时调用外部工具，提升解决问题能力和效率。自动提示工程师（APE）：利用大型语言模型生成指令候选项，依据评估分数选择最佳指令，提升提示生成效率与效果。Active - Prompt：先查询模型生成多个答案，计算不确定度后挑选最不确定问题由人类注释示例，优化模型对不同任务的适应性。方向性刺激提示（Directional Stimulus Prompting）：训练策略LM生成引导提示，增强对模型生成结果的掌控力，提升文本摘要等任务的质量。PAL（程序辅助语言模型）：让模型生成程序来解决问题，借助编程运行时提升解决复杂问题的能力。ReAct框架：使模型交错生成推理轨迹和操作，提升答案的可靠性与可解释性，在知识密集型和决策任务中表现良好。自我反思（Reflexion）：包含参与者、评估者和自我反思三个模型，帮助模型从错误中学习并提升性能。二、常见问题解答1.如何选择合适的Prompt框架？根据任务类型和需求选择。如简单对话场景可选用RTF框架；复杂分析任务可考虑思考链模式或RISEN框架；对输出格式有严格要求的，可在RTF、RISEN框架中关注格式或约束条件设定；进行总结类任务，密度链模式可能更合适。2.为什么模型会出现“幻觉”现象？模型虽经大量数据训练，但无法完美记住所有知识，也难以精准把握知识边界，面对复杂或生疏主题问题时，就可能编造看似合理但错误的内容。3.如何利用少样本提示提升模型性能？提供与任务相关、格式规范的成功示例，示例要涵盖任务的关键要素和模式，让模型学习示例中的规律和逻辑，从而在处理实际输入时生成更符合预期的结果。尤其适用于复杂任务或对输出格式有特定要求的情况。4.链式提示在实际应用中有哪些优势？将复杂任务拆解为子任务，使模型处理过程更清晰、有序，逐步深入解决问题，提高了任务处理的准确性与可靠性，避免模型在处理复杂问题时因任务过于复杂而出现混乱或错误。
20分钟聊聊clang-tidy那些事在1月初凯新有过在各算法/公共模块引入 Clang-tidy 的提议 ，在3月初我向大家分享了一个关于完善算法/业务项目CI流程的静态代码检查的文档，其中都提到了一个工具，那就是clang-tidy，今天我便带大家聊一聊clang-tidy。Clang-tidy大家应该多少对clang-tidy这个工具有一定的了解，它是一个基于clang的静态代码分析框架。同时它是一个功能非常强大的lint工具。绝大部分lint工具只能在出现问题的地方给出提示，之后需要人为修改，而clang-tidy拥有自动修复功能（当然怎么修复需要相应 checker 作者提供）。并且clang-tidy采用模块化设计，非常容易扩展。如果用户想往clang-tidy添加一个新的检测功能，只需要编写一个clang-tidy checker实现（如何编写一个clang-tidy check以后将会有一章详细介绍），每一个checker检测一种问题，例如检测某个违反Code style的模式，检测某些API不正确使用的方法等等。那么提到了Code Style，我们不得说一说Google Code Style，这是目前使用最为广泛的一种代码规范。为此Google Code style也提供了一个cpplint脚本，用于检测代码中违反code style的地方。同时clang-tidy也支持检测代码中违反google code style的地方（google-*check）。那么可能会有人问他们之间有什么区别呢？cpplint是一个python的脚本，它是采用正则表达式匹配出违反code style的代码, 所以它能检测的功能会受限于正则表达式，它不能够检测所有的违反code style的地方，并且还会有False positive和True negative；而clang-tidy则是基于抽象语法树(AST)对源文件进行分析，相比之下，是在分析的结果更加准确，能检测的问题也更多。OK 到这里我们应该对clang-tidy 进行静态代码分析的原理有了一个大致的框架它基于clang，基于抽象语法树AST实现的。那clang和AST是什么？Clang众所周知clang是一个编译器，是一个以LLVM为后端的编译前端那么clang和llvm还有什么其他关系呢？这里我们可能需要说一下广义的llvm和狭义上的llvm了。在LLVM整体架构中，前端用的是clang，因此广义的LLVM是指整个LLVM架构，而狭义的LLVM指的是LLVM后端（包含代码优化和目标代码生成）。在LLVM架构中，源代码（c/c++）经过clang生成中间代码，之后中间代码通过优化器进行一系列的优化，最后通过后端生成机器码。而在经过前端生成中间代码过程中，前端会对代码进行词法分析、语法分析以及语义分析。而在语法分析过程中变为生成一棵语法树，也就是我们说的AST。int add(int a, int b){ typedef long long ll; return a + b; }// 我们可以使用-fsyntax-only -Xclang -ast-dump 打印出源码文件的ast // clang -fmodules -fsyntax-only -Xclang -ast-dump main.m `-FunctionDecl 0x1398df030 <test.cpp:1:1, line:4:1> line:1:5 add 'int (int, int)' |-ParmVarDecl 0x1398deed8 <col:9, col:13> col:13 used a 'int' |-ParmVarDecl 0x1398def58 <col:16, col:20> col:20 used b 'int' `-CompoundStmt 0x1398df250 <col:22, line:4:1> |-DeclStmt 0x1398df198 <line:2:5, col:25> | `-TypedefDecl 0x1398df140 <col:5, col:23> col:23 ll 'long long' | `-BuiltinType 0x139840750 'long long' `-ReturnStmt 0x1398df240 <line:3:5, col:16> `-BinaryOperator 0x1398df220 <col:12, col:16> 'int' '+' |-ImplicitCastExpr 0x1398df1f0 <col:12> 'int' <LValueToRValue> | `-DeclRefExpr 0x1398df1b0 <col:12> 'int' lvalue ParmVar 0x1398deed8 'a' 'int' `-ImplicitCastExpr 0x1398df208 <col:16> 'int' <LValueToRValue> `-DeclRefExpr 0x1398df1d0 <col:16> 'int' lvalue ParmVar 0x1398def58 'b' 'int'AST从上可以看出，每一行包括AST node的类型，行号、列号以及类型的信息。在AST中有上千种node类型，我们这边简单看下常见的四种node：Type，表示一个typedefDecl，它表示程序中的一个声明，在其下面存在很多派生类。比如常见的FunctionDeclStmt，这个类表示程序中的一个语句，比如我们的while语句WhileStmtExpr，它表示程序中的一个表达式。那么既然有了这样一棵树，我们就可以通过对其节点结构做文章，精确定位到声明语句、赋值语句、运算语句等等，实现对代码的静态检查。Clang-tidy Checker而clang-tidy 的一系列checker便是去做了这件事。它在干这活的时候主要分为两部分，静态分析引擎（GRExprEngine.cpp以及其它）和 多个静态检查器（*Checker.cpp），然后所有的检查器都是建立在静态分析引擎的基础之上。那他们怎么工作的呢？首先分析器是一个源码的模拟器，追踪代码可能的执行路径。程序状态（变量和表达式的值）被封装为 ProgramState 。程序中的位置被叫做 ProgramPoint 。state 和 point 的组合是 ExplodedGraph 中的节点。分析器会沿着 ExplodedGraph 执行可达性分析（Reachability Analysis）。从具有 entry program point 和 initial state 的根节点开始，分析模拟每个单独表达式的转移。表达式分析会产生状态改变，使用更新后的 program point 和 state 创建新节点。当满足某些 bug 条件时（违反检测不变量，checking invariant），就认为发现了 bug 。最终，每个单独检查器（Checkers）也通过操作分析状态来工作。分析引擎通过访问者接口（visitor interface）与之沟通。比如，PreVisitCallExpr() 方法被 GRExprEngine 调用，来告诉 Checker 我们将要分析一个 CallExpr ，然后这个检查器被请求检查任意前置条件，这些条件可能不会被满足。检查器不会做除此之外的任何事情：生成一个新的 ProgramState 和包含更新后的检查器状态的 ExplodedNode 。如果它发现了一个 bug ，它会把错误告诉 BugReporter 对象，提供引发该问题的路径上的最后一个 ExplodedNode 节点。静态分析引擎按下图所示流程运行。所有 Checker 都由 CheckerManager 管理调度，而CheckerManager 由分析引擎内核管理。Checker 内部沿 ExplodedGraph 构造节点，遇到违反约束条件的状态时汇报错误。Checker DIYOK 到这里我们已经大概了解了checker是怎么工作的了，那我们应该怎么去编写一个自定义的checker呢？为了在ParseAST阶段快速找到自己想处理的AST 节点，clang提供了两种方式：RecursiveASTVisitor和ASTMatcher，我们今天只讲下使用clang提供的用来匹配和遍历AST 的DSL --- ASTMatcher来自定义一个checker我们在这里简单实现一个检查if条件中为非零的整型常量且body为空的checker我们先构建一个demo源文件，这里为clang-demo.cppint deeprouteDemo(int a, int b) { if (1) {} if (0.1) {} if ('a') {} if (!1) {} if (true) {} }使用前面说的clang -fmodules -fsyntax-only -Xclang -ast-dump命令查看demo的AST`-FunctionDecl 0x55984380ad38 <demo.cpp:1:1, line:7:1> line:1:5 deeprouteDemo 'int (int, int)' |-ParmVarDecl 0x55984380abb8 <col:19, col:23> col:23 a 'int' |-ParmVarDecl 0x55984380ac30 <col:26, col:30> col:30 b 'int' `-CompoundStmt 0x55984380b490 <col:33, line:7:1> |-IfStmt 0x55984380ae88 <line:2:2, col:10> | |-<<<NULL>>> | |-<<<NULL>>> | |-ImplicitCastExpr 0x55984380ae60 <col:6> 'bool' <IntegralToBoolean> | | `-IntegerLiteral 0x55984380ae40 <col:6> 'int' 1 | |-CompoundStmt 0x55984380ae78 <col:9, col:10> | `-<<<NULL>>> |-IfStmt 0x55984380b2e0 <line:3:2, col:12> | |-<<<NULL>>> | |-<<<NULL>>> | |-ImplicitCastExpr 0x55984380aee0 <col:6> 'bool' <FloatingToBoolean> | | `-FloatingLiteral 0x55984380aec0 <col:6> 'double' 1.000000e-01 | |-CompoundStmt 0x55984380b2d0 <col:11, col:12> | `-<<<NULL>>> |-IfStmt 0x55984380b358 <line:4:2, col:12> | |-<<<NULL>>> | |-<<<NULL>>> | |-ImplicitCastExpr 0x55984380b330 <col:6> 'bool' <IntegralToBoolean> | | `-CharacterLiteral 0x55984380b318 <col:6> 'char' 97 | |-CompoundStmt 0x55984380b348 <col:11, col:12> | `-<<<NULL>>> |-IfStmt 0x55984380b3f8 <line:5:2, col:11> | |-<<<NULL>>> | |-<<<NULL>>> | |-UnaryOperator 0x55984380b3c8 <col:6, col:7> 'bool' prefix '!' | | `-ImplicitCastExpr 0x55984380b3b0 <col:7> 'bool' <IntegralToBoolean> | | `-IntegerLiteral 0x55984380b390 <col:7> 'int' 1 | |-CompoundStmt 0x55984380b3e8 <col:10, col:11> | `-<<<NULL>>> `-IfStmt 0x55984380b458 <line:6:2, col:13> |-<<<NULL>>> |-<<<NULL>>> |-CXXBoolLiteralExpr 0x55984380b430 <col:6> 'bool' true |-CompoundStmt 0x55984380b448 <col:12, col:13> `-<<<NULL>>>clang-tidy 在LLVM/clang-tools-extra/clang-tidy下提供了一个名为add_new_check 的脚本文件（用 Python 编写），用以快速生成新的 checker，我们可以借助他来 init./add_new_check.py misc checker-nameinit之后它会替我们创建并更新一系列文件，短期我们只需要关心./misc/check-name.h和./misc/check-name.cpp便可以，他们是实现我们自定义checker检查的主要文件。而我们需要做的是在./misc/check-name.cpp中实现两个虚函数registerMatchers和check补充registerMatcher函数，编写匹配我们自定义规则的DSL，编写时可以使用clang-query进行验证DSL是否正确void DeeprouteChecker1Check::registerMatchers(MatchFinder *Finder) { // FIXME: Add matchers. Finder->addMatcher(ifStmt(isExpansionInMainFile(), hasCondition( implicitCastExpr( hasDescendant( integerLiteral( unless(equals(false)))))), hasThen( compoundStmt( statementCountIs(0))) ).bind("deeproute-checker-1"), this); }验证Matcher正确性$ clang-query clang-demo.cpp $ match ifStmt(isExpansionInMainFile(),hasCondition(implicitCastExpr(hasDescendant(integerLiteral(unless(equals(false)))))), hasThen(compoundStmt(statementCountIs(0)))).bind("deeproute-checker-1") Match #1: /tmp/demo.cpp:2:2: note: "deeproute-checker-1" binds here if (1) {} ^~~~~~~~~ /tmp/demo.cpp:2:2: note: "root" binds here if (1) {} ^~~~~~~~~ 1 match.补充check函数我们根据注册的matcher所绑定的节点名称进行访问，判断是否出现我们符合我们匹配要求的节点，并进行相应的警告打印void DeeprouteChecker1Check::check(const MatchFinder::MatchResult &Result) { // FIXME: Add callback implementation. if (const auto *MatchedDecl = Result.Nodes.getNodeAs<IfStmt>("deeproute-checker-1")) { diag(MatchedDecl->getIfLoc(), "The condition is an integer constant and the body is empty") << FixItHint::CreateInsertion(MatchedDecl->getLParenLoc(), "true "); } }编译clang-tidycd llvm-project mkdir build cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \ -DLLVM_ENABLE_ASSERTIONS=On \ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \ -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON \ -DLLVM_PARALLEL_LINK_JOBS=2 -DLLVM_BUILD_EXAMPLES=ON \ -DLLVM_INCLUDE_EXAMPLES=ON ../llvm ninja clang-tidy -j1参考文档clang-tidy-check.md · jahentao/学习LLVM - Gitee.comhttps://zhuanlan.zhihu.com/p/369254889https://code.woboq.org/llvm/clang/AST Matcher Reference
Clang 新手教程长话短说这个新手教程会让你弄清楚什么是clang，clang AST，clang plugins 和clang tools 等等。让你大概知道clang 可以解决什么问题，而且小白也是可以用clang libraries 来开发工具的 :)Clang 是什么？Clang 是一个以LLVM为后端的编译前端。编译前端主要负责parse 源码，检查错误，并生成Abstract Syntax Tree (AST)。Ref: Understanding the Clang AST什么时候会用到Clang？需要基于编译器的AST 对源码做精确的编辑需要引入自定义的编译错误和警告基于C/C++ 源码的代码生成(code generation)，比如生成数据结构的序列化，语言交互接（Foreign function interface）Clang AST 是什么？Clang AST 的节点是由几种没有共同基类的类来组成（建模）的Clang’s AST nodes are modeled on a class hierarchy that does not have a common ancestor.其中比较常用的四种class 是Type,Decl,DeclContext,Stmt每种节点都有专门的遍历函数来获取子节点ASTContext 里可以获取AST 的额外信息，比如源码的地址例子// test.cc int foo(int num) { int result = (num / 2); return result; } void test_f() { int i = 10; int b = foo(i); }clang -Xclang -ast-dump -fsyntax-only test.ccRef: Introduction to the Clang AST - Clang 15.0.0git documentationClang APIsClang 提供C 和C++ APIC API (libClang) 稳定但是不完整C++ API（LibTooling）完整但是不稳定LibTooling 主要用于开发standalone 工具，并且plugins 只能使用LibToolingRef: Choosing the Right Interface for Your Application - Clang 15.0.0git documentationClang plugins v.s. Clang toolsClang plugin 在编译时对AST 进行一些额外的操作。plugin是动态库（.so），在运行时由编译器加载，可以很容易集成到编译环境中。Clang tools 是由LibTooling 开发的可执行程序，比如clang-check，clang-format，clang-tidy 这些llvm project 提供的。有时候两种方式都能解决问题：# 1. As a loadable Clang plugin: clang -cc1 -load <BUILD_DIR>/lib/libLACommenter.so '\' -plugin LACPlugin test/LACInt.cpp # 2. As a standalone tool: <BUILD_DIR>/bin/ct-la-commenter test/LACInt.cpp考虑点：能不能控制用哪个编译器？clang / gcc?需不需要make 或者break buildClang 6.0 写的plugin Clang 7.0 不一定能用（API 不稳定！）CI 环境使用哪种工具用户接入成本更低？RecursiveASTVisitor v.s. ASTMatcherLibTooling 有两种framework: RecursiveASTVisitor 和ASTMatcherThe RecursiveASTVisitor provides hooks of the form bool VisitNodeType(NodeType *) for most AST nodesASTMatcher 是一套用来匹配和遍历AST 的DSL两种framework 可以一起使用来解决比较复杂的问题！https://github.com/banach-space/clang-tutor/blob/main/README.md#unusedforloopvar优先考虑用ASTMatcher，因为可以用clang-query 来验证DSL 是否正确clang-query test.cc# some useful commands to setup clang-query set output dump set bind-root false set traversal IgnoreUnlessSpelledInSource m functionDecl() m functionDecl(hasName("foo"))Ref:How to write RecursiveASTVisitor based ASTFrontendActions. - Clang 15.0.0git documentationTutorial for building tools using LibTooling and LibASTMatchers - Clang 15.0.0git documentationAST Matcher Reference如何写一个Clang 工具？想清楚目的写一些最小的测试用例源码并用clang -Xclang -ast-dump 来观察生成的AST找到想要匹配的AST 节点使用clang-query 来在测试用例上检验ASTMatcher将ASTMatcher 的DSL集成到clang LibTooling 的脚手架里怎么创建Clang 工具项目？In tree编译llvmClang - Getting Started注意：llvm 编译linking 占用内存非常高！所以要用单线程make，而且16G 内存的话，要将swap file 增加到16G 才能编译。ref: How to increase swap space?在llvm-project/clang-tools-extra下创建目录和写MakefileHow to write RecursiveASTVisitor based ASTFrontendActions. - Clang 15.0.0git documentationOut of treeclang-tutor/README.md at main · banach-space/clang-tutor有用的资源clang-tutor https://github.com/banach-space/clang-tutor/blob/main/README.md#clang-tutorhttps://static.linaro.org/connect/yvr18/presentations/yvr18-223.pdf
单元测试的必要性？一文聊聊单元测试
单元测试写的代码能一次正确执行是每个程序员的追求，但世事皆不能尽如人意，我们的代码经常会有 Bug，这就需要测试的存在。测试有黑盒和白盒之分。黑盒测试，测试时认为被测程序就像一个漆黑的盒子，虽然不明白其中的运行原理，但知道怎么输入有对应的输出。QA (Quality assurance)，也就是我们的测试部门一般负责对程序进行黑盒测试，调用接口时传确定的参数，再校验接口响应值符合某种预期。与黑盒测试对应的是白盒测试，白盒测试要求被测试人员了解被测程序的构造，从而构造测试用例校验程序各个分支逻辑。从这一方面来说，单元测试就是一种白盒测试。单元测试，又称为模块测试，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作，一般对面向对象语言来说，这个最小单位是类或重要的类方法，它不仅可以用作功能测试，将单元测试集成到依赖集成工具之后，它们还可以在模块编译时执行，进行模块的回归测试。工作以来，我对单元测试的认知不断刷新，从拒绝到勉强接受，再到理解其中妙处，最近竟然有些理解我以前觉得不可思议的 TDD（测试驱动开发）了，我觉得有必要把其中心路历程整理一下，无论是之后回忆还是翻出来打脸，应该都很有意思。初遇单测不写单测刚开始工作时的公司是一个小型公司，项目小且业务简单，部门也没有要求，我是不写单元测试的。不接触就无法理解它，这时候我对单元测试的认识就是收益很低的”测试工具”，认为代码是自己写的，自己再编造一些数据去迎合这些代码，根本测试不出来什么，而且编造数据还要花费大量的尽力，收益和付出完全不成正比，程序的正确性靠 QA 就完全够了。可能跟我有类似想法的同事也有很多，后来我们干脆把所有的类方法都改成了静态方法，程序运行时不用再去创建服务对象了，这样，代码就变成了披着面向对象外衣的函数式程序。当然，这也进一步导致了单元测试不可能实行了，因为方法是层层调用的，想要构造出一组能正确运行的数据都非常困难，就更不用说再测试各种分支逻辑了。随波逐流后来换到了目前所在的岗位，部门强制要求每段逻辑都要有对应的单测 case，这样我才真正接触到了单元测试。刚开始写单元测试时内心是非常煎熬的，不认同还要被强制要求做，当然很不爽，更重要的是不习惯。原来功能开发完成后，自己改改参数调两遍接口测下就行，现在还要再写同样多甚至更多的代码来校验功能代码是否正确，花费的不仅是时间，还有心力。而且有些代码就没法写单测，费尽心思构造出数据，可能还没测出功能代码的问题，改单测 case 的 bug 就能让人崩溃了。这时我采用的策略就是仿着原来的单测 case 写，现在看来之前的很多单测 case 也没什么质量，又由于自己水平低，导致最终写出来的单测 case 基本没什么意义，要么是重要逻辑覆盖不到，要么是只能覆盖一些通用逻辑，就算有问题，调接口测一下也能测出来。收获也不能说完全没有，毕竟有非常明显的 bug 还是能够测出来的，而且有时候误改了之前的代码，也能够在 QA 反馈前及时解决，但总体来说，这样写单元测试是不划算的。单测的意义缘由后来 case 越写越多，在越来越熟练地满足单测覆盖的要求时，我也在不停思考这样的工作有什么意义，直到有一天被 leader review 代码，我感觉有些开悟了。被 review 代码的功能是将一个 json 字符串解析为服务里的配置模型，考虑到它只是一个解析字符串的功能，我把它定义为一个”工具类”，里面用静态方法实现，这样调用解析方法时不用注入 bean，使用类名.方法即可。我以为这个类虽然称不上完美无缺，但命名、日志、异常处理都很完善，至少算得上中规中矩，没想到被 leader 一顿狠批，因为他直接跳到这个工具类的上层类，发现这个类没有单测覆盖。为什么上层类没有写单元测试呢，不是因为上层的逻辑太过复杂，而且因为如果我想测这个上层类，就需要构造出一个能够解析为配置模型的大型字符串，还要传上一堆配置参数到这个解析工具类里，这个字符串不好构造不说，即使构造出来了，写在代码里这么一堆，也异常难看，如果之后要变更这个配置模型的结构，更是一场噩梦。思考被教育一顿后，我终于明白了，上层代码的单元测试难写，是因为这么一个工具类，工具类的静态方法无法 Mock 返回值，这就需要我构造大量的真实数据，费力也讨不了好，简而言之，是因为我的设计烂导致单测不好写。接着我观察了更多据说写不出单测的代码，反思了之前遇到的单测难写的代码，拿这些代码去对比代码设计原则，发现了这些代码的一个共同点：它们都严重违背了一种或更多设计原则，也就是说，它们都是烂代码。”不好写单测的代码都是烂代码”，我觉得我理解了单元测试的（部分）意义，单元测试不仅用来测试代码功能，还可以用来测试代码设计。从此之后，我开始更重视单元测试了，单元测试的名字不再用 “testMethodName” 这么敷衍的名字，也开始考虑设计单测的边界值，每次写单测时也在不停问自己，这个 case 写起来费劲吗，我的设计合理吗？终于，单测对我来说不再只是负担了。TDD 的思考TDD，测试驱动开发，是一种先写单元测试再根据单元测试写功能代码的开发模式。一直以来都觉得这种开发模式很不可思议，在自己都不知道类和方法怎么拆分时，怎么能写出单元测试呢，就算强行写出了，可是结果又跟 QA 写的测试 case 有什么区别呢？都是假想有这么一个黑盒能够对某种输出响应某种输出。在理解了单测的意义后，再往深处去想，TDD 是十分可行的。首先对于良好的代码设计，恰当的功能拆分来说，要测试的模块是基本确定的，这就提供了先写单元测试的可能性，而且这种开发方式也能有效避免业务代码开发完成后，写单元测试时发现设计不可理的窘境。虽然我不熟悉 TDD，也没有想真正实践 TDD 的开发模式，但 TDD 也能给我一些启发。我不会在功能开发完成前写单元测试，但我可以在进行代码设计前先考虑单测 case 的结构，或者先预定单测 case 的方法，功能开发完后再补充单测的方法体，这也是 TDD 的另一种实践方式吧。小结现在能明白为什么 leader 一直要求大家代码必须补充单测了，但盲目地写收益真的很小，思想上不认同的话，懒惰小人也会不停地跳出来阻止你写单元测试。如果你也跟我之前一样挣扎在单测的苦海，那你可以重新思考一下单测的意义，如果你是充满”灵性”的程序员，对我的看法有意义的话，欢迎评论。
一、基础知识1、基本知识说一下C++和C的区别设计思想上：C++是面向对象的，C是面向过程的语法上：C++具有封装、继承和多态三种特性；C++相比C，增加了许多类型安全的功能，比如强制类型转换；C++支持范式编程，比如模板类、函数模板等C++11的新特性auto关键字、右值引用、初始化列表对类进行初始化、新增容器array和tuple、智能指针inline函数的作用加快函数的执行速度吧。因为内敛函数调用，是将调用表达式直接用内敛函数体来替换。虚函数和纯虚函数的区别声明不同：纯虚函数后面必须跟=0;虚函数必须实现，纯虚函数一定没有实现虚函数在子类里面可以不重载，但是纯虚函数必须重载虚函数在子类里面也可以不重载的；但纯虚必须在子类去实现说一下C++中static关键字的作用1、修饰局部变量：特点：有默认值0，只执行一次，运行一开始就开辟了内存，内存放在数据段2、修饰全局函数和全局变量：特点：只能在本源文件使用3、修饰类里面的成员变量：特点：和1差不多，定义多个类对象,但共用相同的静态成员变量,不进入类的大小计算，不依赖于类对象的存在而存在（可直接调用，要进行外置声明）（类名或对象直接访问）4、修饰类的成员函数：特点：f():括号里无this指针，只能调用他的本类静态函数和他的静态变量，即是用static修饰过的不依赖于类对象的存在而存在（可不进行外置声明，直接调用）（类名或对象直接访问）说一说C++中的四种cast转换C++中四种类型转换是：static_cast、dynamic_cast、const_cast、reinterpret_cast1、dynamic_cast 用于动态类型转换。只能用于含有虚函数的类，用于类层次间的向上和向下转化。只能转指针或引用。向下转化时，如果是非法的对于指针返回NULL，对于引用抛异常。要深入了解内部转换的原理。 向上转换：指的是子类向基类的转换 向下转换：指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行类型和要转换的类型是否相同来判断是否能够进行向下转换。2、static_cast可以转换相关联的类，可以从子类转换成父类。也能从父类转向子类，但是如果转换的父类指针（或者父类引用）所指向的对象是完整的，那么是没有问题；但是如果所指向的对象并不完整，那么会出现runtime错误。 用于各种隐式转换，比如非const转const，void*转指针等，**static_cast**能用于多态向上转化，如果向下转能成功但是不安全，结果未知。 3、**reinterpret_cast**几乎什么都可以转，比如将int转指针，可能会出现问题，尽量少用3、reinterpret_cast该操作不会去进行动态类型或者静态类型的检测，它仅仅将值强行赋值过去。从某种意义上对编译器进行了一种欺骗，同时也带来了一定的不安全性4、const_cast常量转换，用于将const变量转为非const指针和引用的区别？1、指针是一个变量，引用是一个别名2、指针可以为空，引用不可以为空，创建时候必须初始化3、指针的值在初始化后可以改变，但是引用不可以改变4、指针可以多级，但是引用只能一级左值和右值引用的区别左值与变量绑定一次，而右值只能和表达式绑定一起，且右值引用只能绑定到一个即将销毁的对象上左值有持久性，右值一般是个临时变量或者字面常量为什么要使用智能指针智能指针的作用是管理一个指针，因为存在以下这种情况：申请的空间在函数结束时忘记释放，造成内存泄漏。使用智能指针可以很大程度上的避免这个问题，因为智能指针就是一个类，当超出了类的作用域是，类会自动调用析构函数，析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间，不需要手动释放内存空间。请你说一下你理解的c++中的smart pointer四个智能指针auto_ptr（C++11已抛弃）采用所有权模式，使用=将会给所有权传给对方。auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”)); auto_ptr<string> p2; p2 = p1; //auto_ptr不会报错.此时不会报错，p2剥夺了p1的所有权，但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是：存在潜在的内存奔溃问题！unique_ptr（独占的智能指针）unique_ptr实现独占式拥有或严格拥有概念，保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露（例如“以new创建对象后因为发生异常而忘记调用delete”）特别有用。unique_ptr<string> p3 (new string ("auto")); //#4 unique_ptr<string> p4； //#5 p4 = p3;//此时会报错！！编译器认为p4=p3非法，避免了p3不再指向有效数据的问题。因此，unique_ptr比auto_ptr更安全。另外unique_ptr还有更聪明的地方：当程序试图将一个 unique_ptr 赋值给另一个时，如果源 unique_ptr 是个临时右值，编译器允许这么做；如果源 unique_ptr将存在一段时间，编译器将禁止这么做，比如：unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed注：如果确实想执行类似与#1的操作，要安全的重用这种指针，可给它赋新值。C++有一个标准库函数std::move()，让你能够将一个unique_ptr赋给另一个。例如：unique_ptr<string> ps1, ps2; ps1 = demo("hello"); ps2 = move(ps1); ps1 = demo("alexia"); cout << *ps2 << *ps1 << endl;shared_ptr（共享的智能指针）shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象，该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享，它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造，还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用reset时，当前指针会释放资源所有权，计数减一。当计数等于0时，资源会被释放。shared_ptr 是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针，因为一个是指针，一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。成员函数：use_count 返回引用计数的个数。unique 返回是否是独占所有权( use_count 为 1)。swap 交换两个 shared_ptr 对象(即交换所拥有的对象)。reset放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少。get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的。weak_ptr（弱引用的智能指针）weak_ptr是一种不控制对象生命周期的智能指针，它指向一个shared_ptr管理对象。进行该对象的内存管理的是那个强引用的share_ptr.weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为配合 shared_ptr而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr或另一个 weak_ptr对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用，不会增加对象的引用计数，和shared_ptr之间可以相互转化，shared_ptr可以直接赋值给它，它可以通过调用lock函数来获得shared_ptr。class B; class A { public: shared_ptr pb_; ~A() { cout<<"A delete\n"; } }; class B { public: shared_ptr pa_; ~B() { cout<<"B delete\n"; } }; void fun() { shared_ptr pb(new B()); shared_ptr pa(new A()); pb->pa_ = pa; pa->pb_ = pb; cout<<pb.use_count()<<endl; cout<<pa.use_count()<<endl; } int main() { fun(); return 0; }可以看到fun函数中pa ，pb之间互相引用，两个资源的引用计数为2，当要跳出函数时，智能指针pa，pb析构时两个资源引用计数会减一，但是两者引用计数还是为1，导致跳出函数时资源没有被释放（A B的析构函数没有被调用），如果把其中一个改为weak_ptr就可以了，我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下，这样的话，资源B的引用开始就只有1，当pb析构时，B的计数变为0，B得到释放，B释放的同时也会使A的计数减一，同时pa析构时使A的计数减一，那么A的计数为0，A得到释放。注意的是我们不能通过weak_ptr直接访问对象的方法，比如B对象中有一个方法print(),我们不能这样访问，pa->pb_->print();英文pb_是一个weak_ptr，应该先把它转化为shared_ptr,如：shared_ptr p = pa->pb_.lock(); p->print();弱引用能检测到所管理的对象是否已经被释放，从而避免访问非法内存。请你回答一下智能指针有没有内存泄露的情况有，当两个对象相互使用一个shared_ptr成员变量指向对方，会造成循环引用，使引用计数失效，从而导致内存泄漏。请你来说一下智能指针的内存泄漏如何解决对象中使用weak_ptr弱指针对象，weak_ptr的构造函数不会修改引用计数的值，从而不会对对象的内存进行管理，其类似一个普通指针，但不指向引用计数的共享内存，但是其可以检测到所管理的对象是否已经被释放，从而避免非法访问。如何判断内存泄露程序里可以记一下申请的内存和释放的内存，看看是否一致请你介绍下C++中的智能指针智能指针是C++11 引入的，用于管理在堆上分配的内存，它将普通的指针封装为一个类对象。当类对象的生存周期结束后，会在析构函数中释放掉申请的内存，从而防止内存泄漏。包含在头文件<memory>中，其中包括：请回答一下数组和指针的区别数组是用于储存多个相同类型数据的集合。指针相当于一个变量，但是它和一般变量不一样，它存放的是其它变量在内存中的地址。数组是以某种类型为单位的连续的一段内存空间作为存储区域，其中存储相应的数据。其变量名代表数组起始空间地址，也就是首元素的地址。指针是的本质是内存中某一字节的地址，其存储在变量名所对应的内存空间中。之所以说数组的本质是指针，是因为在在具体实现上，数组是基于指针实现的，编译器只提供了数组首元素的地址，因此在访问时需要使用首地址+偏移量的形式，所谓的偏移量由下标决定。请回答一下野指针是什么 野指针，也就是指向不可用内存区域的指针。通常对这种指针进行操作的话，将会使程序发生不可预知的错误。不是NULL指针，造成原因：1、指针变量没有被初始化；2、指针p被free或者delete之后没有置NULL请你来说一下函数指针函数指针本身首先是一个指针变量，该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样，这里是指向函数入口地址。char *run(char *P){...} char *(*pf)(char*p); pf=run; pf(p)请你回答一下为什么析构函数必须是虚函数？为什么C++默认的析构函数不是虚函数将可能会被继承的父类的析构函数设置为虚函数，可以保证当我们new一个子类，然后使用基类指针指向该子类对象，释放基类指针时可以释放掉子类的空间，防止内存泄漏。C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针，占用额外的内存。而对于不会被继承的类来说，其析构函数如果是虚函数，就会浪费内存。因此C++默认的析构函数不是虚函数，而是只有当需要当作父类时，设置为虚函数。请你来说一下C++中析构函数的作用用于释放资源，防止内存泄露请你来说一下fork函数创建与当前进程几乎一样的进程，两进程同时继续运行。如果父进程调用fork成功，返回子进程pid，子进程调用成功返回0，失败放回负值用于创建一个进程，调用一次，返回两次。返回值为0时，为子进程；返回值大于0时，为父进程。采用写时复制技术，因此fork的速度极快...请你来说一下fork函数与vfork函数的区别1、fork( )的子进程拷贝父进程的数据段和代码段；vfork( )的子进程与父进程共享数据段2、fork( )的父子进程的执行次序不确定；vfork( )保证子进程先运行，在调用exec或exit之前与父进程数据是共享的，在它调用exec或exit之后父进程才可能被调度运行请你来说一下静态函数和虚函数的区别静态函数在编译的时候就已经确定运行时机，虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制，调用的时候会增加一次内存开销虚函数有什么作用？实现子类与基类之间的多态，基类和子类调用同名的虚函数时，所调用的就不是同一个函数。请你说一说你理解的虚函数和多态多态的实现主要分为静态多态和动态多态，静态多态主要是重载，在编译的时候就已经确定；动态多态是用虚函数机制实现的，在运行期间动态绑定虚函数表虚函数表保存的是类中虚函数的地址重载和覆盖有什么区别1、重载是指不同的函数使用相同的函数名，但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。是水平关系2、覆盖（也叫重写）是指在派生类中重新对基类中的虚函数（注意是虚函数）重新实现。即函数名和参数都一样，只是函数的实现体不一样。是垂直关系说一说strcpy和strlenstrcpy是字符串拷贝函数，从src逐字节拷贝到dest，直到遇到'0'结束，因为没有指定长度，可能会导致拷贝越界，造成缓冲区溢出漏洞,安全版本是strncpy函数（加入了第三个参数拷贝长度）。strlen函数是计算字符串长度的函数，返回从开始到'0'之间的字符个数。写一个函数在mian函数执行前先运行__attribute((constructor))是gcc扩展，标记这个函数应当在main函数之前执行。同样有一个__attribute((destructor))，标记函数应当在程序结束之前（main结束之后，或者调用了exit后）__attribute((constructor))void f(){ cout<<111<<endl; } int main() { cout<<"aaa"<<endl; return 0; }请你来说一下C++里是怎么定义常量的？常量存放在内存的哪个位置？定义前面加const，局部常量存放在栈区，全局常量放在数据段中请你说一说malloc的原理，另外brk和mmap系统调用的作用分别是什么？malloc函数用于动态申请内存，malloc采用了内存池方法，先申请一个大块内存作为堆区，然后将堆区划分为若干个内存块，以内存块作为内存管理的基本单位。用户申请内存时，直接从堆区分配合适的空闲内存块。隐形链表将堆区分成连续的，大小不一的快，包含已分配和未分配的。采用显示链表管理所有空闲快。申请内存小于128k时会使用brk申请内存，将数据段最高地址指针往高地址推；申请内存大于128k时会使用mmap申请内存，在堆栈中间找一块空闲的虚拟内存；malloc和new的区别1、malloc需要给定申请内存的大小，返回的指针需要强转2、new会调用构造函数，malloc不会3、new是一个操作符可以重载，而malloc是一个库函数const修饰成员函数的目的表明函数不会对对象作出任何修改allocator用于封装STL的容器在内存管理上的底层细节，它将new，delete分为4个函数来实现，内存申请、对象构造、对象析构、释放内存迭代器及其作用，遍历容器时怎么删除元素提供了一种顺序访问一个聚合对象中各个元素，并不暴露该对象的内部表示的方法vector来说，删除一个元素后，后面每个元素的迭代器都会失效，但每个元素都会往前移一个位置，erase返回下一个有效地址。对于map和set，因为内部是红黑树，删除一个元素，不会影响其他元素位置，所以删erase之前，需要记录下一个迭代器list两种方法都可以deque的底层实现deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间 ，其中每个元素(此处称为一个节点, node)都是指针，指向另一段(较大的) 连续线性空间，称为缓冲区。缓冲区才是deque的存储空间主体。resize和reserveresize可以更改容器中元素个数reserve改变容器最大容纳元素个数struct 和class的区别struct成员与继承默认都是公有的，class则默认为私有。class可以定义函数类模板，struct不行template<class t,int>编译过程预处理阶段：对源代码文件中的头文件，预编译语句进行分析和替换，生成预编译文件编译阶段：将预编译文件转换成汇编代码生成汇编文件汇编阶段：对汇编文件转换成机器码，生成可重定位目标文件链接阶段：将多个目标文件及所需要的的库进行连接成最终的可执行文件内存管理C++中虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区、栈区代码段：只读存储区用于存放字符常量，文本区存放机器代码数据段：用于存放初始化了的全局变量和静态变量bss段：存放未初始化的全局变量和静态变量，初始化为0的全局变量和静态变量也存放在这堆区：动态分配内存从堆区进行分配文件映射区：mmap函数就是在这里分配内存，存储动态链接库栈区：存放局部变量，函数返回地址、参数、返回值红黑树和AVL平衡树AVL平衡树，保证了每个子树的左右子树高度差绝对值不超过1。但是红黑树的话则没有这一要求，红黑树确保了没有一条路径会比其他路径长出两倍。同时红黑树的根和叶子节点都为黑色，对于任意节点到其叶子节点的黑色节点数都相同。插入的节点是红色的。每一个红色节点儿子一定是黑色节点插入最多扭转2次，删除最多3次扭转当插入点的父红，叔为红色，父叔变为黑色，祖父变为红色，往祖父向上递归当插入点的父红，叔不红且，父子不在同一边，可以以子节点进行左扭转，变成父子同一边当插入点的父红，叔不红且，父子在同一边，可以以父节点进行右扭转删除的话不太清楚B+和B树B树，概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同，B-树为系统最优化大块数据的读和写操作B+ 树是对B树的一种变形树，它与B树的差异在于：有k个子结点的结点必然有k个关键码；非叶结点仅具有索引作用，跟记录有关的信息均存放在叶结点中。树的所有叶结点构成一个有序链表，可以按照关键码排序的次序遍历全部记录。函数调用过程1、根据调用的函数名找到函数入口；2、在栈中申请调用函数中的参数及函数体内定义的变量的内存空间（保护现场，入口地址，参数从右到左）4、函数执行完后，释放函数在栈中的申请的参数和变量的空间，最后返回值。（恢复现场）二叉树反序列化struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; class Solution{ public: TreeNode * Build(int pl,int pr,int vl,int vr,vector<int>&pre,vector<int>&vin){ if(pl>pr||vl>vr)return NULL; TreeNode *head=new TreeNode(pre[pl]); //前序第一个为根 int root=0; for(int i=vl;i<=vr;++i){ //找到根在中序位置分左右儿子 if(vin[i]==pre[pl]){ root=i; break; } } head->left=Build(pl+1,pl+root-vl,vl,root-1,pre,vin); head->right=Build(pl+root-vl+1,pr,root+1,vr,pre,vin); return head; } TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin){ return Build(0,pre.size()-1,0,vin.size()-1,pre,vin); } };快速排序void Quick(int l,int r,int *a){ if(l>=r)return; int key=a[l]; int first==l; int last==r; while(first<last){ while(first<last&&a[last]>=key)--last; a[first]=a[last]; while(first<last&&a[first]<=key)first++; a[last]=a[first]; } a[first]=key; Quick(l,first-1,a); Quick(first+1,r,a); }归并排序void Sort(int a[],int f[],int l,int mid,int r){ int i=l,j=mid+1,k=l; while(i<mid&&j<=r){ if(a[i]>a[j])f[k++]=a[j++]; else f[k++]=a[i++]; } while(i<=mid)f[k++]=a[i++]; while(j<=r)f[k++]=a[j++]; } void Merge(int a[],int f[],int k,int n){ int i=0; while(i<=n-2*k){//每个快大小 Sort(a,f,i,i+k-1,i+2*k-1); i+=2*k; } if(i<n-k+1)Sort(a,f,i,i+k-1,n-1); //超过k个直接归并 else for(int j=i;j<n;++j)f[j]=a[j]; //不超过 for(int i=0;i<n;++i)a[i]=f[i]; } void GSort(int a[],int n){ int *f=(int *)malloc(sizeof(int)*(n+1)); int i=1; while(i<n){ Merge(a,f,i,n); i*=2; } free(f); }递归版本void Merge(int a[], int l, int r, int mid){ //合并 int *f = (int*)malloc((r - l + 1)*sizeof(int)); int i = l, j = mid + 1, k = 0; while (i <= mid&&j <= r){ if (a[i] <= a[j])f[k++] = a[i++]; else f[k++] = a[j++]; } while (i <= mid)f[k++] = a[i++]; //如果左边有没加入的则全部合并进来 while (j <= r)f[k++] = a[j++]; //等同 for (i = 0; i<k; i++){ //扔回给a a[i + l] = f[i]; } free(f); } void MergeSort(int a[], int l, int r){ //拆分 if (l >= r)return; MergeSort(a, l, (l + r) / 2); MergeSort(a, (l + r) / 2 + 1, r); Merge(a, l, r, (l + r) / 2); //合并 }最长上升子序列int a[maxn]; int dp[maxn]; int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;++i){ scanf("%d",&a[i]); } int len=1; dp[len]=a[1]; for(int i=2;i<=n;++i){ if(a[i]>=dp[len])dp[++len]=a[i]; else{ int id=lower_bound(dp+1,dp+1+len,a[i])-dp; //最长不下降为upper_bound dp[id]=a[i]; } } printf("%d\n",len); return 0; }最长公共子序列char a[maxn],b[maxn]; int dp[maxn][maxn]; int main(){ scanf("%s%s",a+1,b+1); int lena=strlen(a+1),lenb=strlen(b+1); for(int i=1;i<=lena;++i)dp[1][0]=0; for(int i=1;i<=lenb;++i)dp[0][1]=0; for(int i=1;i<=lena;++i){ for(int j=1;j<=lenb;++j){ if(a[i]==b[j])dp[i][j]=dp[i-1][j-1]+1; else dp[i][j]=max({dp[i][j],dp[i-1][j],dp[i][j-1]}); } } printf("%d\n",dp[lena][lenb]); return 0; }01背包int w[maxn],v[maxn],dp[maxn]; int main(){ int n,ww; scanf("%d%d",&n,&ww); for(int i=1;i<=n;++i)scanf("%d%d",&w[i],&v[i]); for(int i=1;i<=n;++i){ for(int j=ww;j>=w[i];--j){ dp[i]=max(dp[i],dp[i-w[i]]+v[i]); } } printf("%d\n",dp[ww]); return 0; }完全背包int w[maxn],c[maxn]; int dp[2000]; int main(){ int n,v; scanf("%d%d",&n,&v); for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&c[i]); for(int i=1;i<=n;i++){ for(int j=w[i];j<=v;j++){ //设 f[v]表示重量不超过v公斤的最大价值 dp[j]=max(dp[j-w[i]]+c[i],dp[i]); } } printf("%d\n",dp[v]); }多重背包int w[maxn],v[maxn],num[maxn],dp[maxn]; int main(){ int n,ww; scanf("%d%d",&n,&ww); for(int i=1;i<=n;++i)scanf("%d%d",&w[i],&v[i],&num[i]); for(int i=1;i<=n;++i){ for(int k=1;k<=num[i];k<<=1){ num[i]-=k; for(int j=ww;j>=w[i]*k;--j){ dp[i]=max(dp[i],dp[i-w[i]*k]+v[i]*k); } } if(num[i]) for(int j=ww;j>=w[i]*num[i];--j){ dp[i]=max(dp[i],dp[i-w[i]*num[i]]+v[i]*num[i]); } } printf("%d\n",dp[ww]); return 0; }
数据库数据库事务及4个特性事务：由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。事务是DBMS中最基础的单位，事务不可分割。具有4个特性ACID原子性：事务是最小的执行单元，不允许分割一致性：执行事务后，数据保持一致，多个事务对同一个数据读取结果相同隔离性：并发访问数据库是，一个用户的事务不被其他事务所干扰。并发事务之间相互独立持久性：一个事务被提交后，他对数据库的改变是持久的。DB DBMS DBS DBADB(数据库)：按一定结构组织并长期存放在计算机内的、可共享的大量数据集合DBMS(数据库管理系统)：是管理和维护数据库的系统软件DBA(数据库管理员)：管理操作数据库人员DBS(数据库系统)：由数据库、数据库管理系统、数据库管理员、硬软件等组成的集合体数据库三大范式第一范式：所有属性不能再分解为更基本的数据单元第二范式：在第一范式基础上，所有非主属性完全依赖于每一个候选码第三范式：满足第一范式，非主属性不存在传递依赖数据库索引对数据库的一列或者多列进行排序的一种结构，使用索引可以快速访问数据库表中的特定信息
计算机网络基础TCP可靠传输实现校验和序列号确认应答超时重传连接管理窗口控制拥塞控制：慢启动，拥塞避免，快重传，快恢复tcp本身通过ack确认机制来保证确认信息收到了，并通过重传机制对丢失信息进行重传，也就是说，应用端将数据交给TCP，自己啥都不用管了，他一定能传到，所以称之为可靠性TCP和UDP区别TCP面向字节流，UDP面向报文TCP面向连接的传输协议，传输前必须建立连接，UDP无连接TCP是可靠的，UDP不保证可靠TCP保证了数据顺序，UDP不保证TCP首部开销大于UDP，TCP需要20字节，UDP则是8字节TCP三次握手四次挥手三次握手：主机向服务器发送一个请求连接报文服务器收到报文后，向主机发送一个确认连接请求报文主机收到后，送一个检查ACK报文，服务器收到连接建立成功开始传输四次挥手：主机向服务器发送连接释放报文服务器收到后，向主机发送一个ACK报文当服务器无数据传输时，发送一个连接释放报文主机收到后，发送一个ACK报文，进入时间等待状态，等待两个报文段最大生存时间，然后关闭连接（保证最后一次报文能到B，2MSL后，这次连接的所有报文都会消失）不是2次握手：无法获取到对方的状态，无法知道主机是否收到服务器的确认连接，且不会超时重传，TCP不会为没有数据的ACK进行重传；为了防止 已失效的链接请求报文突然又传送到了服务端，因而产生错误。不是4次挥手：主动关闭方需要等待被动关闭方传送完数据，所以在收到主机连接释放报文时，不能直接回复一个连接释放报文，需要等服务器数据发送完https和http的区别HTTP是以明文的方式在网络中传输数据，而HTTPS协议传输的数据则是经过TLS加密后的，HTTPS具有更高的安全性HTTPS在TCP三次握手阶段之后，还需要进行SSL的握手，协商加密使用的对称加密密钥HTTPS协议需要服务端申请证书，浏览器端安装对应的根证书HTTP协议端口是80，HTTPS协议端口是443HTTPS的安全性更高，握手阶段延时较高HTTP三个缺点：被篡改、被监听、被伪装https的建立过程客户端发送请求到服务端服务端返回公钥和证书到客户端客户端接收后会验证证书的安全性,如果通过则会随机生成一个随机数,用公钥对其加密,发送到服务端服务端接受到这个加密后的随机数后会用私钥对其解密得到真正的随机数,随后用这个随机数当做私钥对需要发送的数据进行对称加密客户端在接收到加密后的数据使用私钥(即生成的随机值)对数据进行解密并且解析数据呈现结果给客户SSL加密建立对称加密和非对称加密对称加密：即加密的密钥和解密的密钥相同非对称加密：非对称加密将密钥分为公钥和私钥，公钥可以公开，私钥需要保密，客户端公钥加密的数据，服务端可以通过私钥来解密http1.0 http1.1和http2.0http1.0 是短连接 http1.1是长链接短连接每次请求都需要建立连接，长连接则允许传送多个HTTP请求响应http2.0增加了多路复用，也就是连接共享，一个连接可以有多个请求并行执行ipv4和ipv6的区别地址长度不同，iPv4为4字节32位，ipv6为16字节128位，因此他们的寻址空间不同IPv4使用了地址解析通讯协议，IPv6使用的是多点传播IPV6的安全性更高。在使用IPv6网络中，用户可以对网络层的数据进行加密并对IP报文进行校验，这极大地增强了网络安全post和get请求区别对于get方式，浏览器会把http 消息头和data 一并发送过去，服务器响应200（返回数据）；对于post方式，浏览器先发送消息头，服务器响应100 continue，浏览器再发送data，服务器响应200 ok（返回浏览器）区别：l、get参数通过url传递，post放在request body中。2、get 请求在url中传递的参数是有长度限制的，而post没有。3、get比post更不安全，因为参数直接暴露在url中，所以不能用来传递敏感信息。4、get 请求只能进行url编码，而post支持多种编码方式。5、get请求参数会被完整保留在浏览历史记录里，而post中的参数不会被保留。6、GET和POST本质上就是TCP链接，并无差别。但是由于HTTP的规定和浏览器/服务器的限制，导致他们在应用过程中体现出一些不同。7、GET产生一个TCP数据包:POST产生两个TCР数据包。session和cookie区别Cookie通过在客户端记录信息确定用户身份，Session通过在服务器端记录信息确定用户身份。1、cookie数据存放在客户的浏览器上，session数据放在服务器上。2、cookie不是很安全，别人可以分析存放在本地的cookie并进行cookie欺骗，考虑到安全应当使用session。3、session会在一定时间内保存在服务器上。当访问增多，会比较占用你服务器的性能，考虑到减轻服务器性能方面，应当使用cookie。4、单个cookie保存的数据不能超过4K，很多浏览器都限制一个站点最多保存20个cookie。5、可以考虑将登陆信息等重要信息存放为session，其他信息如果需要保留，可以放在cookie中。Session 是存放在服务器端的，类似于Session结构来存放用户数据，当浏览器 第一次发送请求时，服务器自动生成了一个Session和一个Session ID用来唯一标识这个Session，并将其通过响应发送到浏览器。当浏览器第二次发送请求，会将前一次服务器响应中的Session ID放在请求中一并发送到服务器上，服务器从请求中提取出Session ID，并和保存的所有Session ID进行对比，找到这个用户对应的Session。一般情况下，服务器会在一定时间内（默认30分钟）保存这个 Session，过了时间限制，就会销毁这个Session。在销毁之前，程序员可以将用户的一些数据以Key和Value的形式暂时存放在这个 Session中。当然，也有使用数据库将这个Session序列化后保存起来的，这样的好处是没了时间的限制，坏处是随着时间的增加，这个数据 库会急速膨胀，特别是访问量增加的时候。一般还是采取前一种方式，以减轻服务器压力。Cookie实际上是一小段的文本信息。客户端请求服务器，如果服务器需要记录该用户状态，就使用response向客户端浏览器颁发一个Cookie。客户端会把Cookie保存起来。当浏览器再请求该网站时，浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie，以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。OSI七层模型和TCP四层模型应用层、表示层、会话层、传输层、网络层、数据链路层、物理层应用层、传输层、网络层、数据链路层、物理层TCP各层相关的协议应用层：FTP、SMTP、HTTP传输层：TCP、UDP网络层：IP、ICMP、IGMP（组管理协议）、ARP链路层：MAC，VLANDNS解析过程以及DNS劫持1)、在浏览器中输入一个域名，例如www.tmall.com，操作系统会先检查自己本地的hosts文件是否有这个网址映射关系，如果有，就先调用这个IP地址映射，完成域名解析， windows下hosts文件在C:/Windows/System32/drivers/etc/hosts下，linux一般都在/etc/hosts下。2)、如果hosts里没有这个ip到域名的映射，那么就要查找本地DNS解析器缓存，是否有这个域名到ip地址的映射关系，如果有，直接返回，完成域名解析。3)、如果本机的hosts与本地DNS解析器缓存都没有相应的网址映射关系，那么就要找到你的TCP/ip参数中设置的首选DNS服务器，我们叫它本地DNS服务器，此服务器收到查询时，如果要查询的域名，包含在本地配置区域资源中，则返回解析结果给客户机，完成域名解析，此解析具有权威性。4)、如果要查询的域名，不由本地DNS服务器区域解析，但该服务器已缓存了此网址映射关系，则调用这个域名到ip地址的映射，完成域名解析，但是此解析不具有权威性。5)、如果本地DNS服务器本地区域文件与缓存解析都失效，则根据本地DNS服务器的设置（是否设置转发器）进行查询，如果未用转发模式，本地DNS就把请求发至13台根DNS，根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理，并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后，将会联系负 责.com域的这台服务器。这台负责(.com)域名的服务器收到请求后，如果自己无法解析，它就会找一个管理(.com)域的下一级DNS服务器地址 (tmall.com)给本地DNS服务器。当本地DNS服务器收到这个地址后，就会找(tmall.com)域服务器，重复上面的动作，进行查询，直至找到 (www.tmall.com)主机。6)、如果用的是转发模式，此DNS服 务器就会把请求转发至上一级DNS服务器，由上一级服务器进行解析，上一级服务器如果不能解析，或找根DNS或把转请求转至上上级，以此循环。不管是本地 DNS服务器用是是转发，还是根提示，最后都是把结果返回给本地DNS服务器，由此DNS服务器再返回给客户机。DNS劫持又称域名劫持，是指在劫持的网络范围内拦截域名解析的请求，分析请求的域名，把审查范围以外的请求放行，否则返回假的IP地址或者什么都不做使请求失去响应，其效果就是对特定的网络不能反应或访问的是假网址。举一个简单的例子，比如你输入的域名地址是www.google.cn然后弹出来百度的页面，很明显发生了DNS域名劫持。MAC地址和IP地址MAC地址是一个硬件地址，用来定义网络设备的位置，主要由数据链路层负责。IP地址是IP协议提供的一种统一的地址格式，为互联网上每个网络和每一台主机分配一个逻辑地址，以此来屏蔽物理地址的差异。TCP/IP数据链路层的交互过程先去本地arp缓存表查找是否有目的ip的mac地址，如果查到了就将mac地址封装到数据报的报头，没有找到的话，就发起一个广播，所有收到广播的机器看这个ip是不是自己，如果是就以单拨形式发送自己的mac地址给请求的机器。如果没有找到，则不可达（报错）
操作系统线程池线程池就是首先创建一些线程，他们的集合称为线程池。使用线程池可以很好地提高性能，线程池在系统启动时即创建大量空闲的线程，程序将一个任务传给线程池，线程池就会启动一条线程来执行这个任务，执行结束以后，该线程并不会死亡，而是再次返回线程池中成为空闲状态，等待执行下一个任务。1、设置一个生产者消费者队列，作为临界资源2、初始化n个线程，并让其运行起来，加锁去队列取任务运行3、当任务队列为空的时候，线程阻塞4、当生产者队列来了一个任务后，先对队列加锁，把任务挂在队列上，然后使用条件变量去通知阻塞中一个线程进程和线程概念及区别进程：进程是对运行时程序的封装，是系统进行资源调度和分配的基本单位，实现了操作系统的并发；进程=程序+数据+PCB(进程控制块)线程：是进程的子任务，是CPU调度与分配的基本单位，用于保证程序的实时性，实现了线程内部的并发。区别：线程属于进程，一个线程只能属于一个进程，进程可以拥有多个线程，线程依赖于进程存在进程是资源分配的基本单位，线程是CPU调度的基本单位进程执行的过程中拥有独立内存单元，线程共享进程的内存进程切换资源开销大，线程小进程间不会相互影响，一个线程挂了整个进程就会挂掉进程的通信可以通过管道、系统IPC实现进程的通信管道是半双工的，具有固定的读端和写端，它可以用于具有亲缘关系的进程之间的通信。系统IPC：消息队列：消息的链接表，存放在内核中，一个消息队列由一个标识符来标记。面向记录信号量：就是一个计数器，可以用来控制多个进程对共享资源的访问。用于实现进程间的互斥与同步共享内存：他使得多个进程可以访问同一块内存空间，不同进程可以及时看到对方进程对共享内存数据的更新。进程调度策略先来先服务(FCFS)： 队列实现，非抢占的，先请求先分配CPU最短作业优先调度(SJF)： 平均等待时间最短，但难以知道下一个CPU区间长度优先级调度算法： 优先级高的先分配，相同优先级的先到先服务。容易出现饥饿时间片轮转调度： 每个进程执行只分配一个时间片的CPU时间，时间片结束，进程被抢占并放回就绪队列进程的五种基本状态创建状态：进程正在被创建就绪状态：进程被加入到就绪队列中等待CPU调度运行执行状态：进程正在被运行等待阻塞状态：进程因为某种原因，比如等待I/O，等待设备，而暂时不能运行终止状态：进程运行完毕就绪状态被调度变为执行状态，发生时间片到了或者高优先级则会从执行变为就绪执行状态因等待某事件而睡眠变为等待阻塞等待阻塞因等待事件发生而唤醒变为就绪上下文切换上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制 。在切换的过程中，操作系统需要先存储当前进程的状态(包括内存空间的指针，当前执行完的指令等等)，再读入下一个进程的状态，然后执行此进程。（ 程序计数器、CPU寄存器状态等数据 ）进程切换一般分两步：切换页目录以使用新的地址空间（只有进程需要做）切换内核栈和硬件上下文（线程与进程都要做）确实进程交换过程进程交换技术： 换出一部分进程到外存，腾出内存空间将内存暂时不能运行的进程，或者暂时不用的数据和程序，换出到外存，来腾出足够的内存空间，把已经具备运行条件的进程，或进程所需的数据和程序换入到内存。挂起状态： 进程被交换到外存，进程状态就成为了挂起状态活动阻塞：进程在内存中，但由于某种原因被阻塞了静止阻塞：进程在外存，同时被某种原因阻塞了活动就绪：进程在内存，处于就绪状态，只要给CPU和调度就可以运行静止就绪：进程在外存，处于就绪状态，只要调度到内存，给CPU和调度就可以运行活动就绪到静止就绪（内存不够，调到外存）活动阻塞到静止阻塞（内存不够，调到外存）执行到静止就绪（时间片用完）系统分配的资源处理器、存储器、I/O 设备以及信息线程的通信及同步线程通信主要通过：使用全局变量、使用消息实现通信、使用事件实现通信线程同步的方式：临界区：通过多线程的串行化来访问公共资源或一段代码、速度快、适合控制数据访问互斥量：采用互斥对象机制、只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个、所以保证公共资源不会被多个线程同时访问信号量：为具有有限数量的用户资源而设计的，它允许多个线程在同一时刻去访问同一个资源（但一般需要限制同一时刻访问此资源的最大线程数目）事件：通过通知操作的方式来保持多线程同步，还可以方便实现多线程优先级的比较操作守护、僵尸、孤儿进程的概念守护进程：运行在后台的一种特殊进程，独立于控制终端并周期性地执行某些任务。僵尸进程：一个进程 fork 子进程，子进程退出，而父进程没有wait/waitpid子进程，那么子进程的进程描述符仍保存在系统中，这样的进程称为僵尸进程。孤儿进程：一个父进程退出，而它的一个或多个子进程还在运行，这些子进程称为孤儿进程。（孤儿进程将由 init 进程收养并对它们完成状态收集工作）死锁死锁指两个或两个以上的线程，因资源的争夺而相互等待的现象死锁的四个条件：互斥条件、不可剥夺条件、请求与保持条件、环路等待条件死锁避免：系统对进程发出的每一个系统能够满足的资源申请进行动态检查，并根据检查结果决定是否分配资源，如果分配后系统可能发生死锁，则不予分配，否则予以分配，这是一种保证系统不进入死锁状态的动态策略。预防死锁解决办法：可剥夺资源、资源一次性分配、资源有序分配4种锁互斥锁：用于保证任意时刻只有一个线程访问对象，当获得锁操作失败时，线程会进入睡眠，直到锁释放时被唤醒读写锁：分为读锁和写锁，对于读锁，允许多个线程同时获得读锁，但对于写锁，同一时刻只允许一个线程获得写锁，并且写锁会堵塞其他写锁与读锁。写锁优先与读锁。自旋锁：和互斥锁差不多，区别在于获取锁失败线程不会睡眠，而是原地自旋等待锁释放条件锁：同步线程之间同步贡献数据的值，当某个共享数据到达某个值时，唤醒等待这个共享数据的一个/多个线程分页和分段的区别段式存储管理： 将程序的地址空间划分为若干段，如代码段，数据段，堆栈段；段大小可变，所以没有内碎片（可以改变大小消除内碎片，但是在换出的时候会产生外碎片）页式存储管理： 将程序的逻辑地址分为固定大小的页，而物理内存划分为同样大小的帧，程序加载时，将需要的一页放入内存中的任意一帧，这些帧不必连续，从而实现了离散分离。优点：没有外碎片，但会产生内碎片。结合二者优点产生了段页式存储管理，内存分段，段内分页不同：大小不同：页的大小固定且由操作系统决定，段的长度却不固定，由其所完成的功能决定地址空间不同：段向用户提供二维地址空间；页向用户提供的是一维地址空间内存碎片不同：页没有外碎片有内碎片，段 相反虚拟内存虚拟内存是计算机管理内存的一种技术；他可以让所有进程共享同一物理内存，每一个进程只把自己当前需要的虚拟内存空间存储到物理内存中。每个进程创建时，只建立好虚拟内存和磁盘文件之间的映射，等到运行到对应程序时，通过缺页中断，来拷贝数据到物理内存中可以扩大地址空间，当物理内存不足时可以当做一个临时的“物理内存”，保护内存。让电脑可以运行比物理内存大很多的程序页面置换需要磁盘I/O，非常耗时，虚拟地址转物理地址，增加了指令的执行时间；内存中可以保留多个进程，提高了系统并发度缺页中断在请求分页系统中，可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存。每当所要访问的页面不在内存中，就会产生缺页中断，此时操作系统会根据页表中的外存地址在外存中找到所缺的一页，将其调入内存中断的4个步骤：保护CPU现场、分析中断原因、转入缺页中断处理程序进行处理、恢复CPU现场，继续执行页面置换算法先进先出淘汰算法（FIFO）：最先进入的数据最先被淘汰，新加入的页面放队尾最近最不经常访问淘汰算法（LFU）：每个数据块引用一个计数，对数据块进行计数排序，计数相同的按时间排序，每次淘汰队尾数据块最近最少使用替换算法（LRU）：使用一个栈，新页面或者命中的页面将其移到栈低，每次替换掉栈顶的元素（其实可以用双向链表去实现，相对较快点）在LRU基础上进行拓展有LRU-K 和2Q算法用于解决LRU缓存污染问题缓存污染：突然大量偶发性的数据访问，会让内存中存放大量冷数据局部性原理时间上的局部性： 最近被访问的页在不久的将来还会被访问空间上的局部性： 内存中被访问的页周围的页也很可能被访问页表寻址通过页表，有逻辑地址的高位想找到逻辑地址对应的页基地址，再由页基地址偏移一定长度就得到最后的物理地址，偏移长度有逻辑地址低位决定页表：页表内容就是该进程的虚拟地址到物理地址的一个映射，每一项都记录了这个页的基地址fork和vforkfork：成功调用的话，会创建一个新的进程，它几乎与调用fork()的进程一模一样。父进程调用成功返回子进程的pid，子进程调用成功返回0，出现错误返回一个负值vfork：在fork基础上加了，子进程必须要执行一次对exec的调用，或者调用exit_()退出。内核态和用户态内核态和用户态是操作系统两种工作模式，两者区别主要是特权级不同，用户态特权级较低，内核态较高。运行在用户态的程序不能直接访问系统内核数据结构和程序。但是可以通过系统调用的方式切换到内核态进行访问。3种用户态切换内核态的方式：系统调用、异常、外围设备的中断系统调用用户态进程主动要求切换到内核态的一种方式栈内存和堆内存的区别栈内存：由程序自动向操作系统申请分配以及回收，速度快，使用方便，但程序员无法控制。若分配失败，则提示栈溢出错误。注意，const局部变量也储存在栈区内，栈区向地址减小的方向增长。堆内存：程序员向操作系统申请一块内存，当系统收到程序的申请时，会遍历一个记录空闲内存地址的链表，寻找第一个空间大于所申请空间的堆结点，然后将该结点从空闲结点链表中删除，并将该结点的空间分配给程序。分配的速度较慢，地址不连续，容易碎片化。此外，由程序员申请，同时也必须由程序员负责销毁，否则则导致内存泄露。
[scode type="red"]非常抱歉！！因为视频的分段录制，在第一段录制时屏蔽了录制机麦克风声源，但在第二段录制时忘记打开麦克风声源了，导致第二段视频成了无声视频 ::aru:crying:: 。[/scode][scode type="yellow"]如果哪位同学有录制第二段视频，还请联系我，谢谢 ::aru:bigeye:: 。[/scode] var player = new DogePlayer({ container: document.getElementById('player'), userId: 575, vcode: '33f460ad2f9c49e0', autoPlay: false, plcode: 'ed4f9a8c16e35f57', plsort: 'name' });