当前位置: 首页 > news >正文

CppCon 2016 学习:The Exception Situation

你这段内容主要在讨论 函数接口设计时,如何表达“失败”或“不满足前提条件”的情况,特别是处理除法时除数为零的问题。以下是对关键点的理解总结:

1. 函数自然接口(Natural Interface)

函数接口定义得很简单:

int integral_div(int num, int denom);

表示计算并返回 num / denom 的整数商(忽略余数)。
理想状态下,调用者传入两个参数,就得到结果。

2. 问题:denom == 0怎么办?

  • 除数为零是非法操作,会导致程序错误。
  • 这是一个边界或异常情况,需要处理。

3. 用前置条件表达接口约束

// Precondition: denom != 0
int integral_div(int num, int denom);
  • 这里声明:调用时必须保证 denom 不为零。
  • 责任转移到调用者,调用者必须保证满足这个条件。
  • 如果调用者不遵守,结果未定义(可能程序崩溃)。

4. 初学者的误区:打印错误信息然后继续执行

int integral_div(int num, int denom) {if (!denom)std::cerr << "Divide by zero" << std::endl;return num / denom;
}
  • 这种写法看似提示错误,但其实根本没阻止错误发生。
  • 除零后程序可能崩溃,打印没意义。
  • 这表明函数与调用者的交互不清晰。

5. 激进的处理:直接终止程序

int integral_div(int num, int denom) {if (!denom) {std::exit(-1);  // 或 abort(), terminate() 等}return num / denom;
}
  • 这是激进的防御性编程。
  • 保证函数只在有效输入时运行,否则程序立即退出。
  • 适合一些关键系统,但通常不推荐直接杀死程序。

总结理解

  • 函数接口最好明确约束(前置条件),让调用者负责提供合法参数。
  • 函数内部不应简单“打印错误”然后继续执行,那样无效且不安全。
  • 激进措施(退出程序)虽简单粗暴,但不能解决所有场景。
  • 理想情况是用更优雅的错误处理机制(比如异常、返回错误码、或其他错误传播机制)来表达失败。
    如果你需要,我可以帮你写几个示例,展示如何更合理地表达函数的失败(比如用异常或返回 std::optional)。需要吗?

这段内容继续讲述了如何在函数接口中表达“失败”或“不满足前置条件”的情况,同时给出了多种不同的设计方案及其利弊。下面是逐条理解总结:

1. 使用 assert 断言

int integral_div(int num, int denom) {assert(denom && "divide by zero");return num / denom;
}
  • 断言检查 denom != 0,不满足时程序终止。
  • 这是激进的“防御式”编程,保证只有在合法输入下才执行。
  • 缺点:assert 在 Release 模式通常会被编译器去掉,不会检查。
  • 需要开发阶段彻底测试,保证调用前就不会传入非法值。

2. 客户端代码示例

int main() {int n, d;if (cin >> n >> d)cout << integral_div(n,d) << endl;
}
  • 如果输入为 0,会因为断言失败而程序终止。
  • 这种设计保持了函数的自然接口(两个参数,返回结果),调用简单。

3. 改变函数签名,添加“成功标志”参数

int integral_div(int num, int denom, bool &ok) {if (!denom) {ok = false;return -1; // 随意返回一个值}ok = true;return num / denom;
}
  • 通过引用参数 ok 通知调用者是否成功。
  • 缺点:增加了函数参数,调用变得复杂。
  • 需要调用者检查 ok,否则可能忽略错误。

4. 客户端代码示例

int main() {int n, d;if (cin >> n >> d) {bool ok = true;int res = integral_div(n,d,ok);if (ok)cout << res << endl;}
}
  • 代码更复杂,调用者必须管理额外的成功标志。
  • 容易忘记检查 ok,导致潜在错误。

5. 改变函数签名,返回成功码,输出结果通过引用参数

bool integral_div(int num, int denom, int &res) {if (!denom)return false;res = num / denom;return true;
}
  • 这样调用时,返回值表示是否成功,结果通过引用参数返回。
  • 这种方式更符合“布尔值表示成功/失败”的习惯。
  • 但函数签名依然和自然接口不同。

6. 客户端代码示例

int main() {int n, d;if (cin >> n >> d) {int res;if (integral_div(n,d,res))cout << res << endl;}
}
  • 调用代码更清晰,逻辑明显:先判断是否成功,再使用结果。
  • 但如果调用者忘记检查返回值,依然会有问题。
  • C++17 引入的 [[nodiscard]] 属性可以帮助避免忽略返回值。

总结理解

  • 使用断言简单明了,保持了自然接口,但断言只适合调试阶段,不能依赖于生产环境。
  • 添加成功标志参数,或者返回 bool 并通过引用参数输出结果,是比较常用的表达失败的方式,但会改变函数的自然接口。
  • 这类设计使得调用更安全,但调用者负担变重,代码更冗长。
  • 最理想的是函数接口简洁,且失败信息不能被调用者忽略,这正是异常机制的优势所在(本节未提及,但可以理解为后续改进方向)。

这段内容主要讲的是如何优雅地表达函数“失败”的情况,特别是针对integral_div(num, denom)函数中,除数denom可能为0时,如何设计函数返回值,让调用者能够知道计算是否成功,而不是简单地返回一个整数或直接崩溃。

关键点总结:

  1. 改变返回类型以表达失败
    • 使用 std::pair<bool, int> (或类似结构)
      std::pair<bool,int> integral_div(int num, int denom) {if (denom == 0) return {false, -1};return {true, num / denom};
      }
      
      调用时检查bool标志判断是否成功。类似Go语言的返回值模式(返回值+错误码)。
    • 使用 C++17 的结构化绑定简化调用代码
      if (int n, d; std::cin >> n >> d) {if (auto [ok, res] = integral_div(n, d); ok) {std::cout << res << std::endl;}
      }
      
  2. 使用 std::optional<int> 来表示可能失败的结果
    • 返回optional<int>,若除数为0返回空的optional,表示无有效结果。
      std::optional<int> integral_div(int num, int denom) {if (denom == 0) return {};return num / denom;
      }
      
    • 调用时检查是否有值:
      auto res = integral_div(n, d);
      if (res) {std::cout << res.value() << std::endl;
      }
      
    • C++17结构化绑定简化调用:
      if (auto res = integral_div(n, d); res) {std::cout << res.value() << std::endl;
      }
      
  3. 使用类似expected<T, E>的模式(C++标准中暂时没有)
    • expected类型包含成功时的结果或错误类型
      class divide_by_zero{};
      expected<int, divide_by_zero> integral_div(int num, int denom) {if (denom == 0) return divide_by_zero{};return num / denom;
      }
      
    • 调用时像optional一样检查是否有有效结果,同时还能得到具体的错误类型。
    • 这比optional更通用,可以传递具体错误信息(比如std::error_code),而不仅仅是“有/无”。

这几种设计思想的核心目的:

  • 避免函数内部“直接打印错误”、“终止程序”等不灵活的错误处理手段,而是把错误信息通过返回值传递给调用者,让调用者决定如何处理。
  • 让接口更安全,避免非法输入导致程序崩溃。
  • 让调用者可以清晰地检测到函数是否成功执行,并安全地访问结果。

你理解的关键点:

  • 函数自然接口: 简单的int integral_div(int num, int denom),但不表达失败。
  • 表达失败的改进接口: 返回一个能表达成功/失败的类型,如pair<bool,int>optional<int>expected<int,Error>等。
  • 客户端代码通过检查返回值状态,决定下一步操作。

这部分内容继续探讨了如何优雅地表达函数执行失败的情况,但这次是用**抛异常(throw)**的方式来处理错误,比如除以零的情况。

核心内容理解:

  1. 用异常来表达错误
    class divide_by_zero {};
    int integral_division(int num, int denom) {if (!denom)throw divide_by_zero{};return num / denom;
    }
    
    • 如果除数为0,就抛出一个divide_by_zero类型的异常。
    • 这样函数的接口(签名)看起来完全没变,依然是int integral_division(int, int)
    • 但是调用时,如果遇到除零,程序会跳转到异常处理逻辑。
  2. 调用代码例子
    int main() {int n, d;if (std::cin >> n >> d)std::cout << integral_division(n, d) << std::endl;
    }
    
    • 这里调用函数看起来和原来完全一样,没有像之前那样显式处理错误的返回值。
    • 但是,程序运行时如果遇到denom == 0,会抛异常。
  3. 疑问:没有try/catch块怎么办?
    • 代码示例中没有显式的trycatch块,异常会向上传递到调用栈外层。
    • 如果最终没有捕获异常,程序会异常终止(通常会调用std::terminate)。
    • 这是这段内容隐含的思考:如果用异常机制,调用代码要么包裹try/catch捕获异常,要么允许异常继续传播。

这和之前用optionalpair的区别

  • 异常机制的优点:
    • 保持函数接口简洁,没有改变函数返回值类型。
    • 错误处理和正常逻辑分离,调用者可以选择在哪里捕获异常。
  • 缺点:
    • 需要调用者意识到可能有异常,需要写异常处理代码,否则程序会崩溃。
    • 异常机制的运行时开销较大。
    • 异常控制流程不如返回值直观,可能导致隐藏的异常路径。

总结

  • 异常是另一种表达“失败”的手段,属于“非局部错误处理”机制。
  • 它让函数接口保持原样,不必返回“可能失败”的类型。
  • 调用者需要考虑是否捕获异常。
  • 和用optionalexpected等“显式返回错误”方法相比,异常更隐式,但更灵活。

这部分内容讲了异常的“捕获(catch)”原则和设计示例(一个简单的动态数组类 Array),主要有两个核心点:

1. 异常处理的哲学:何时捕获异常?

  • “大多数情况下,没有必要捕获异常。”
    只有当你能够“做些什么”时,才去捕获异常,否则直接让它继续传播。
  • 捕获了异常不一定马上处理它,如果当前层不知道怎么处理,可以重新抛出异常(rethrow),让更上层的代码去处理。
  • 这样做的好处是异常处理代码更聚焦,错误不会被“无意义的捕获”而吞掉。

2. 示例:简单的动态数组 Array<T>

  • 基本结构
    • elems:指向存储元素的动态数组
    • nelems:当前元素数量
    • cap:数组容量
  • 常用方法
    • size(), capacity() 返回大小和容量
    • begin(), end() 返回迭代器
    • full() 判断是否已满
    • push_back(const T&) 插入元素
    • grow() 扩容方法(当满了时调用)
  • grow() 的实现思路
    • 新容量 = 当前容量的两倍(或者初始64)
    • 新开辟一个数组 p
    • 复制旧元素到新数组
    • 释放旧数组内存
    • 指针和容量更新到新数组和新容量

结合异常处理:

  • 这段代码很“天真”,没有专门的异常安全设计,比如:
    • 如果new T[new_cap]失败会抛异常(std::bad_alloc
    • std::copy中元素复制可能抛异常
    • 如果异常发生在中间,可能导致内存泄漏或状态不一致
  • 理想情况下,grow()要做到异常安全(比如使用std::vector那样的策略,或者先分配新内存,再复制,成功后才切换指针)。
  • 异常处理设计原则:
    你不必在所有地方捕获异常,除非你能恢复或做补救。否则让异常往上传递,由更高层统一处理或终止程序。

总结

  • 异常不是用来随便捕获的,只捕获能做实事的。
  • 示例的动态数组展示了典型的“可能抛异常的操作”,提醒我们设计时要考虑异常安全。
  • 这帮助理解如何设计既方便又安全的接口与异常策略。

这段内容讲了用RAII(资源获取即初始化)来优雅地管理资源和异常安全,并介绍了异常处理机制中的特殊情况。

1. 传统的 grow() 实现(显式异常处理)

void grow() {std::size_t new_cap = capacity() ? capacity() * 2 : 64;auto p = new T[new_cap];try {std::copy(begin(), end(), p);} catch (...) {delete[] p;throw;}delete[] elems;elems = p;cap = new_cap;
}
  • 这里先用 new 分配新内存,拷贝旧元素。
  • 拷贝过程中可能抛异常,所以用 try...catch 捕获,异常时释放刚分配的内存,避免泄漏,再重新抛出异常。
  • 代码比较冗长,异常处理显得繁琐。

2. 用 RAII 简化资源管理

void grow() {std::size_t new_cap = capacity() ? capacity() * 2 : 64;std::unique_ptr<T[]> p { new T[new_cap] }; // RAII 自动管理内存std::copy(begin(), end(), &p[0]);delete[] elems;elems = p.release(); // 释放智能指针控制权,内存交给 elems 管理cap = new_cap;
}
  • 使用 std::unique_ptr<T[]> 自动管理新分配的内存。
  • 如果 std::copy 抛异常,p 会自动析构,释放内存,不会泄漏。
  • 不用手动写异常处理代码,更简洁更安全。
  • 体现了RAII的强大——资源和异常安全自动管理。

3. 关于异常处理的特殊情况(std::terminate

标准说明中提到,当异常处理机制遇到以下情况时,必须放弃异常处理,调用 std::terminate()

  • (1.1) 异常机制在构造异常对象完成后、异常被捕获处理前,调用了又抛异常的函数(异常传播链中再次抛异常)。
  • (1.2) 找不到合适的异常处理器(catch块)处理抛出的异常。

在这两种情况下,程序会调用 std::terminate(),通常意味着程序非正常终止。

  • 这提醒我们,异常处理并不是万能的,某些情况下异常机制会停止运行,程序崩溃。
  • 设计异常时,要注意避免“异常中再抛异常”的情形。

总结

  • RAII 是C++中实现异常安全和资源管理的利器,减少手动异常处理代码。
  • std::unique_ptr 管理内存,拷贝失败时自动释放,防止内存泄漏。
  • 异常处理机制有边界和限制,异常处理失败时调用 std::terminate()
  • 编写异常安全代码时,要考虑这些特殊情况。

这段内容主要讲在C++异常处理及错误表达中不同方案的设计权衡和效率问题,我帮你总结理解:

1. 关于异常处理和 noexceptstd::terminate

  • 如果异常传播进入了一个 noexcept 限定的函数(该函数声明不允许抛异常),且该异常未被捕获处理,标准规定:
    • 编译器是否会展开(unwind)调用栈,以及展开程度,是实现定义的。
    • 但在其他情况下(没有 noexcept 限定),异常传播失败时必须调用 std::terminate(),且不能提前停止展开调用栈。
  • 意味着对带有 noexcept 的函数,异常处理行为可能因编译器实现不同而不同。

2. 错误处理的总结:

  • 仅打印错误信息通常不够好,程序会“悬着”,用户和开发者都不知道发生了什么。
  • **终止程序或断言(assert)**是一种合理方案,适合一些快速失败、重启的场景,也便于开发时捕捉错误。
  • 修改函数签名,让返回值携带状态(如C风格的返回码)是经典做法,需要调用者主动检查,且要保证调用者有良好习惯(“纪律”)。
  • 丰富返回类型,用 pair, optional, expected 等封装结果和状态,要求调用者显式检查错误,但接口依旧简洁。
  • 抛异常,不改接口,错误直接通过异常机制传播,调用者用 try/catch 处理。

3. 效率(constexpr)考量:

  • 打印和程序终止(terminate, exit, abort)无法作为 constexpr 函数使用,因为这些操作不是编译时可执行的。
  • 使用 assert 可以写成 constexpr,因为它在编译时有条件能触发断言:
    constexpr int integral_div(int num, int denom) {return assert(denom != 0), num / denom;
    }
    
  • 修改函数签名,比如多返回一个 bool &ok 状态参数,可以写成 constexpr
    constexpr int integral_div(int num, int denom, bool &ok) {if (!denom) {ok = false;return -1;  // arbitrary}ok = true;return num / denom;
    }
    
  • 返回类型是字面类型(如 pair<int,bool>)的版本也能写成 constexpr
    constexpr std::pair<int,bool> integral_div(int num, int denom) {return !denom ? std::make_pair(-1, false) : std::make_pair(num/denom, true);
    }
    
  • optionalexpected 通常因为有非平凡析构函数,不适合做 constexpr

总结:

  • 错误处理方式设计有多种,折中于易用性、语义表达、异常安全和效率。
  • constexpr 在错误处理方案中约束了选择,简单的返回码或字面类型封装能支持,复杂的错误封装类型则不行。
  • noexcept 的异常传播和 std::terminate() 行为是实现依赖,需注意。

“非平凡析构函数”(non-trivial destructor)是C++术语,简单说就是编译器不能自动生成的简单析构函数,而是用户自定义的或编译器生成但比较复杂的析构函数。它有以下特点和影响:

1. 平凡(trivial)析构函数 vs 非平凡析构函数

  • 平凡析构函数(trivial destructor):
    • 编译器自动生成,没有任何用户自定义代码。
    • 不做任何操作,直接释放对象内存即可。
    • 通常用于简单的、只含基本类型成员或不需要释放资源的类型。
    • 编译器可以进行一些优化,比如允许对象放在只读内存区,允许 constexpr 构造和析构。
  • 非平凡析构函数(non-trivial destructor):
    • 用户显式定义了析构函数,或类含有成员变量自身的析构函数非平凡。
    • 需要执行用户代码,比如释放动态资源(内存、文件句柄等)。
    • 编译器必须调用析构函数代码,不能简单地忽略。
    • 这种析构函数使得类对象的生命周期管理更复杂。

2. 对 constexpr 的影响

  • C++ 标准要求 constexpr 析构函数必须是平凡的析构函数(trivial destructor)或者符合某些条件的 constexpr 析构函数。
  • 大多数标准库容器或包装类型(如 std::optionalstd::expected)因为需要管理资源和复杂状态,它们的析构函数是非平凡的,因此不支持在 constexpr 函数中使用(特别是在C++14之前)。
  • 反过来,如果一个类型的析构函数是平凡的,且满足其他 constexpr 约束,则可以用于 constexpr 函数和常量表达式。

3. 如何判断?

一个类的析构函数是平凡的,通常满足:

  • 没有自定义析构函数;
  • 所有非静态数据成员的析构函数也是平凡的;
  • 没有虚析构函数。
    例如:
struct A {int x;// 平凡析构函数
};
struct B {~B() {}  // 用户自定义析构函数,非平凡析构函数
};
struct C {std::string s;  // std::string 的析构函数非平凡// 因此C的析构函数也是非平凡
};

总结:

  • 非平凡析构函数意味着析构时需要执行额外代码,无法被编译器简单忽略。
  • 这会影响类型能否用于 constexpr 函数中,因为 constexpr 要求对象生命周期简单明确。
  • 这也是为什么 optionalexpected 这种封装复杂状态的类型不能轻易用作 constexpr 的原因。

这部分内容重点讲了异常的设计目的、对代码路径的影响,以及错误处理效率和代码清晰度的权衡,我帮你总结理解:

1. constexpr 与抛异常

  • 带抛异常的 constexpr 函数是允许的(比如C++11起就支持),只不过:
    • 在没有异常时,函数可在编译期计算(即常量表达式)。
    • 一旦触发异常,计算就转为运行时处理(throw)。
  • 例如:
    constexpr int integral_div(int num, int denom) {return !denom ? throw divide_by_zero{} : num / denom;
    }
    
  • 这种写法兼顾了编译期常量求值与运行时异常处理。

2. 异常设计目的:错误检测与错误处理分离

  • 检测错误的地方和处理错误的地方不一定是同一个。
  • 例如 integral_div 能检测除零,但不负责决定“除零错误怎么办”。
  • 错误处理可能是:
    • 打印信息(比如日志)
    • 弹窗警告用户
    • 触发紧急停止(核反应堆停止等)
  • 异常机制让错误处理从正常代码路径中剥离出来,不用到处插入错误检查,减少代码污染。

3. 错误处理如何影响代码路径的“干净度”

  • 使用错误码(HRESULT等)和显式检查,会导致“错误处理代码”穿插在正常流程中,看起来很杂乱,且容易遗漏处理。
  • 代码示例中演示了COM接口常用的HRESULT错误检查写法,显得冗长且容易错。
  • 这是一种经典问题:错误处理代码污染正常业务代码,使代码难读难维护
  • Knuth建议用goto跳转简化错误处理,Armstrong(Erlang作者)认为错误应并行处理,不应污染主流程。

4. 错误处理永远不会消失

  • 不论用异常、错误码还是其他机制,错误都可能发生。
  • 不能假设“代码永远不会错”,错误处理应设计得易于维护和阅读。
  • 异常就是为了解决这一痛点,通过机制把错误处理从主流程里分离出来。

总结

  • constexpr 可以用异常,异常路径在运行时执行,非异常路径可编译时计算。
  • 异常的本质是将错误检测与错误处理分离,减少正常代码的污染。
  • 传统错误码机制导致代码里充满检查和分支,影响可读性和维护性。
  • 理想状态是错误处理“异步”于主流程,主流程保持简洁。
  • 但是在现实中,错误处理难以完全隐藏,设计要兼顾效率、可维护性和正确性。

异常(Exceptions)的优缺点以及性能成本,帮你总结和理解如下:

Exceptions — 优点

  • 不改变函数的自然接口
    异常不会通过改变函数签名来处理错误(除非加上 noexcept),调用接口更干净。
  • 为异常情况提供独立代码路径
    异常代码路径和正常代码路径分开,不会污染主业务逻辑。
  • 将错误检测与错误处理分离
    抛出异常只是通知错误发生,具体怎么处理需要上下文环境决定。
  • 构造函数可以用异常传递错误
    构造函数没有返回值,用异常是传递错误的合理方式。

Exceptions — 缺点

  • 并非人人喜欢异常机制
    有人觉得异常机制复杂且难以管理。
  • 异常创建了另一条代码路径
    这是优点也是缺点,代码逻辑更复杂。
  • 异常有非零开销
    主要是发生异常(throw、catch)时的成本,不是正常执行路径的成本。
  • 异常可能被滥用
    像语言中的其他特性一样,使用不当会导致问题。
  • 异常和非异常安全代码的边界问题
    例如与C语言、其他语言的库或工具交互时,异常处理比较困难。
  • Lippincott函数 是应对这种边界问题的一个工具。

Exceptions — 性能成本的实测与分析

  • 多个测试案例比较了异常和无异常代码在不同情况下的性能:
    • 生成大量数据,偶尔出错时抛异常与返回错误码的开销比较。
    • 多层递归函数调用时异常的堆栈展开成本。
    • 复杂数据结构(vector, vector<vector>)下异常处理成本。
    • 错误频率不同(频繁出现、偶尔出现、从不出现)的性能对比。
  • 结论是:
    异常的主要开销在于发生异常时的堆栈展开和异常处理代码执行
    在正常执行路径(无异常时)性能影响较小。

理解总结

  1. 异常机制设计为正常路径开销小,但错误路径开销可能大。
  2. 错误不频繁时,异常机制整体效率较高。
  3. 异常代码路径增加代码复杂度,但能让正常业务逻辑更清晰。
  4. 异常和错误码各有利弊,选择需权衡易用性、性能和代码可维护性。

这段内容主要介绍了标准库中异常的使用情况,以及一个非常有趣的技巧——用异常机制做类型擦除和错误处理。

1. 标准库中的异常使用

  • 标准库通常不主动抛异常,主要是因为要保持效率和简洁。
  • 例外情况是 vector.at(),它会在越界时抛 std::out_of_range
  • 还有少数情况,主要是内存分配失败时抛 std::bad_alloc

2. 异常 = 错误?

  • 在 Boost.Graph 里,有一个有趣的技巧来自 Caso Neri。
  • 这个技巧利用异常机制来实现类型擦除(type erasure)和错误管理,解决了传统方法难以实现的“安全地从基类或子类转换”的问题。

3. “any_ptr” 类的实现和工作原理(异常用作类型转换)

  • any_ptr 类通过存储一个 void* 指针和两个函数指针(用于抛出异常和销毁对象),实现了类型擦除。
  • 关键函数是 thrower,它会抛出存储的指针,借助 C++ 的异常捕获机制,捕捉特定类型的指针。
  • cast_to<T>() 函数尝试通过抛出异常并捕获来“转换”指针类型,返回正确类型的指针,或者失败时返回 nullptr
  • 析构时通过 destroyer 函数指针来释放资源。
    这种方法看起来奇特,但依赖异常机制来完成类型安全的转换,是一种聪明的设计。

4. “自诊断异常”示例

  • 代码中展示了一个有趣的用法:
    抛出一个函数指针(lambda),用于异常处理时执行特定的诊断操作。
  • 这种用法很少见,也不推荐在生产代码中使用,但很有创意。
using diag_t = void(*)(ostream&);
int f(int n) {if (n < 0)throw static_cast<diag_t>([]{ cout << "Ouch!" << endl; });return n;
}
int main() {try {cout << f(-3) << endl;} catch(diag_t diagnosis) {diagnosis();}
}

总结

  • 标准库中的异常用得很少,主要是边界检查和内存分配失败。
  • 异常可以被巧妙地用来实现类型擦除和安全转换,尽管不常见。
  • 异常本身也能承载“行为”,比如抛出一个函数指针来执行特定操作。
http://www.lryc.cn/news/571473.html

相关文章:

  • 【wsl】docker
  • Python FastAPI详解
  • 在Docker上安装Mongo及Redis-NOSQL数据库
  • JVM(4)——引用类型
  • CubeMax配置串口通讯
  • 微信小程序:将搜索框和表格封装成组件,页面调用组件
  • Kafka 向 TDengine 写入数据
  • 游戏技能编辑器界面优化设计
  • Java + Spring Boot + MyBatis 枚举变量传递给XML映射文件做判断
  • node.js使用websockify代理VNC代理使用NoVNC进行远程桌面实现方案
  • docker问题排查
  • 【Python系列PyCharm实战】ModuleNotFoundError: No module named ‘sklearn’ 系列Bug解决方案大全
  • 使用Kotlin开发后端服务的核心方法
  • 【大模型:知识库管理】--MinerU本地部署
  • 最新整理【剑侠情缘龙雀修复BGU版】linux服务端带授权后台+详细教程+包进游戏
  • LangSmith 深度解析:构建企业级LLM应用的全生命周期平台
  • 【day51】复习日
  • conda 下载指定 python 版本安装,即 base 环境为指定的python版本
  • Unity Editor代码引用子场景物体,需要激活子场景
  • 【 FastJSON 】解析多层嵌套
  • 希尔脚本简介及常用命令代码整理
  • 20倍光学镜头怎么实现20+20倍数实现
  • Spring @OnApplicationEvent 典型用法
  • MacOS15.5 MySQL8 开启 mysql_native_password
  • 【入门级-基础知识与编程环境:计算机的历史和常见用途】
  • 【RocketMQ 生产者和消费者】- 消费者重平衡(2)- 分配策略
  • 338比特位技术
  • element ui el-table嵌套el-table,实现checkbox联动效果
  • 轻松搭建Linux开发环境:使用`build-essential`安装GCC编译器**
  • Flask设计网页截屏远程电脑桌面及切换运行程序界面