C现代方法(第24章)笔记——错误处理

文章目录

  • 第24章 错误处理
    • 24.1 <assert.h>: 诊断
    • 24.2 <errno.h>: 错误
      • 24.2.1 perror函数和strerror函数
    • 24.3 <signal.h>: 信号处理
      • 24.3.1 信号宏
      • 24.3.2 signal函数
      • 24.3.3 预定义的信号处理函数
      • 24.3.4 raise函数
    • 24.4 <setjmp.h>: 非局部跳转
    • 问与答
    • 写在最后

第24章 错误处理

——编写无错程序的方法有两种,但只有第三种写程序的方法才行得通。

学习C语言的学生所编写的程序在遇到异常输入时经常无法正常运行,但真正商业用途的程序却必须“非常稳健”,即能够从错误中恢复正常而不至于崩溃。为了使程序非常稳健,我们需要能够预见程序执行时可能遇到的错误,包括对每个错误进行检测,并提供错误发生时的合适行为

本章讲述两种在程序中检测错误的方法:调用assert宏以及测试errno变量。24.1节介绍了<assert.h>头,assert宏就是在这里定义的。24.2节讨论了<errno.h>头,其中定义了errno变量。这一节还包含perror函数与strerror函数,这两个函数分别来自<stdio.h><string.h>,它们与errno变量紧密相关。


24.3节讲解如何检测并处理称为信号的条件,一些信号用于表示错误。处理信号的函数在<signal.h>头中声明。


最后,24.4节探讨setjmp/longjmp机制,它们经常用于响应错误。setjmplongjmp都属于<setjmp.h>头。

错误的检测和处理并不是C语言的强项。C语言对运行时错误以多种形式表示,而没有提供一种统一的方式。而且,在C程序中,需要由程序员编写检测错误的代码。因此,很容易忽略一些可能发生的错误。一旦发生某个被忽略的错误,程序经常可以继续运行,虽然这样也不是很好。C++JavaC#等较新的语言具有“异常处理”特性,可以更容易地检测和响应错误


24.1 <assert.h>: 诊断

void assert(scalar expression);

assert定义在<assert.h>中。它使程序可以监控自己的行为,并尽早发现可能会发生的错误。

虽然assert实际上是一个宏,但它是按照函数的使用方式设计的。assert有一个参数,这个参数必须是一种“断言”——一个我们认为在正常情况下一定为真的表达式。每次执行assert时,它都会检查其参数的值。如果参数的值不为0assert什么也不做;如果参数的值为0assert会向stderr(标准误差流,22.1节)写一条消息,并调用abort函数(26.2节)终止程序执行。

例如,假定文件demo.c声明了一个长度为10的数组a,我们关心的是demo.c程序中的语句

a[i] = 0; 

可能会由于i不在0~9之间而导致程序失败。可以在给a[i]赋值前使用assert宏检查这种情况:

assert(0 <= i && i < 10) ; /* checks subscript first */ 
a[i] = 0; /* now does the assignment */ 

如果i的值小于0或者大于等于10,程序在显出类似下面的消息后会终止:

Assertion failed: 0 <= i && i < 10, file demo.c, line 109 

C99assert做了两处小修改C89标准指出,assert的参数必须是int类型的。C99放宽了要求,允许参数为任意标量类型(因此在assert的原型中出现了单词scalar)。例如,现在参数可以为浮点数或指针。此外,C99要求失败的assert显示其所在的函数名。(C89只要求assert以文本格式显示参数、源文件及源文件中的行号。)C99建议的消息格式为

Assertion failed: expression, function abc, file xyz, line nnn. 

根据编译器的不同,assert生成的消息格式也不尽相同,但它们都应包含标准要求的信息。例如,GCC在上述情况下给出如下的消息:

a.out: demo.c:109: main: Assertion '0 <= i && i < 10' failed. 

assert有一个缺点:因为它引入了额外的检查,所以会增加程序的运行时间。偶尔使用一次assert可能对程序的运行速度没有很大影响,但在实时程序中,这么小的运行时间增加可能也是无法接受的。因此,许多程序员在测试过程中会使用assert,但当程序最终完成时就会禁止assert。要禁止assert很容易,只需要在包含<assert.h>之前定义宏NDEBUG即可:

#define NDEBUG 
#include <assert.h>

NDEBUG宏的值不重要,只要定义了NDEBUG宏即可。一旦之后程序又有错误发生,就可以去掉NDEBUG宏的定义来重新启用assert

请注意!!不要在assert中使用有副作用的表达式(包括函数调用)。万一某天禁止了assert,这些表达式将不会再被求值。考虑下面的例子:

assert((p = malloc(n)) != NULL);

一旦定义了NDEBUGassert就会被忽略并且malloc不会被调用。

函数assert是在程序运行期间做诊断工作,从C11开始引入的静态断言_Static_assert可以把检查和诊断工作放在程序编译期间进行(18.7节)。


24.2 <errno.h>: 错误

标准库中的一些函数通过向<errno.h>中声明的int类型errno变量存储一个错误码(正整数)来表示有错误发生。[errno可能实际上是个宏。如果确实是宏,C标准要求它表示左值(4.2节),以便像变量一样使用。]大部分使用errno变量的函数集中在<math.h>,但也有一些在标准库的其他部分中。

假设我们需要使用一个库函数,该库函数通过给errno赋值来产生程序运行出错的信号。在调用这个函数之后,我们可以检查errno的值是否为零。如果不为零,则表示在函数调用过程中有错误发生。举例来说,假如需要检查sqrt函数(23.3节)的调用是否出错,可以使用类似下面的代码:

errno = 0; 
y = sqrt(x); 
if (errno != 0 ) { fprintf(stderr, "sqrt error; program terminated.\n"); exit(EXIT_FAILURE); 
} 

当使用errno来检测库函数调用中的错误时,在函数调用前将errno置零非常重要。虽然在程序刚开始运行时errno的值为零,但有可能在随后的函数调用中已经被改动了。库函数不会将errno清零,这是程序需要做的事情。

当错误发生时,向errno中存储的值通常是EDOMERANGE。(这两个宏都定义在<errno.h>中。)这两个值代表调用数学函数时可能发生的两种错误:

  • 定义域错误(EDOM):传递给函数的一个参数超出了函数的定义域。例如,用负数作为sqrt的参数就会导致定义域错误。
  • 取值范围错误(ERANGE):函数的返回值太大,无法用返回类型表示。例如,用1000作为exp函数(23.3节)的参数就经常会导致取值范围错误,因为 e 1000 {e^{1000}} e1000太大导致无法在大多数计算机上用double类型表示。

一些函数可能会同时导致这两种错误。可以用errno分别与EDOMERANGE比较,然后确定究竟发生了哪种错误。

C99<errno.h>中增加了EILSEQ。特定头(尤其是<wchar.h>头,25.5节)中的库函数在发生编码错误(22.3节)时把EILSEQ的值存储到errno中。


24.2.1 perror函数和strerror函数

void perror(const char *s);     //来自<stdio.h> 
char *strerror(int errnum);     //来自<string.h> 

下面看两个与变量errno有关的函数,不过这两个函数都不属于<errno.h>

当库函数向errno存储了一个非零值时,可能会希望显示一条描述这种错误的消息。一种实现方式是调用perror函数(在<stdio.h>中声明),它会按顺序显示以下信息:(1)调用perror的参数;(2)一个冒号;(3)一个空格;(4)一条出错消息,消息的内容根据errno的值决定;(5)一个换行符。perror函数会输出到stderr流(22.1节)而不是标准输出。下面是一个使用perror的例子:

errno = 0; 
y = sqrt(x); 
if (errno != 0) { perror("sqrt error"); exit(EXIT_FAILURE); 
} 

如果sqrt调用因定义域错误而失败,perror会产生如下输出:

sqrt error:Numerical argument out of domain

perror函数在sqrt error后所显示的出错消息是由实现定义的。在这个例子中,Numerical argument out of domain是与EDOM错误相对应的消息。ERANGE错误通常会对应于不同的消息,例如Numerical result out of range

strerror函数属于<string.h>。当以错误码为参数调用strerror时,函数会返回一个指针,它指向一个描述这个错误的字符串。例如,调用

puts(strerror(EDOM));

可能会显示

Numerical argument out of domain

strerror函数的参数通常是errno的值,但以任意整数作为参数时strerror都能返回一个字符串。

strerrorperror函数密切相关。如果strerror的参数为errno,那么perror所显示的出错消息与strerror所返回的消息是相同的。


24.3 <signal.h>: 信号处理

<signal.h>提供了处理异常情况(称为信号)的工具。信号有两种类型:运行时错误(例如除以0)发生在程序以外的事件。例如,许多操作系统都允许用户中断或终止正在运行的程序,C语言把这些事件视为信号。当有错误或外部事件发生时,我们称产生了一个信号。大多数信号是异步的:它们可以在程序执行过程中的任意时刻发生,而不仅是在程序员所知道的特定时刻发生。由于信号可能会在任何意想不到的时刻发生,因此必须用一种独特的方式来处理它们。

本节按C标准中的描述来介绍信号。这里对信号谈得很有限,但实际上信号在UNIX中的作用很大。这里不作详细讨论。


24.3.1 信号宏

<signal.h>定义了一系列的宏,用于表示不同的信号表24-1中列出了这些宏以及它们的含义。每个宏的值都是一个正整型常量C语言的实现可以提供更多的信号宏,只要宏的名字以SIG和一个大写字母开头就行。(特别地,UNIX实现提供许多额外的信号宏。)

表24-1 信号

宏名含义
SIGABRT异常终止(可能由于调用abort导致)
SIGFPE在算术运算中发生错误(可能是除以0或溢出)
SIGILL无效指令
SIGINT中断
SIGSEGV无效存储访问
SIGTERM终止请求

C标准并不要求表24-1中列出的信号都自动产生,因为对于某个特定的计算机或操作系统,不是所有的信号都有意义。大多数C语言的实现都至少支持其中的一部分。


24.3.2 signal函数

void (*signal(int sig, void (*func)(int)))(int);

<signal.h>提供了两个函数:raisesignal。这里先讨论signal函数,它会安装一个信号处理函数,以便将来给定的信号发生时使用。signal函数的使用比它的原型看起来要简单得多。它的第一个参数是特定信号的编码,第二个参数是一个指向会在信号发信生时处理这一号的函数的指针。例如,下面的signal函数调用为SIGINT信号安装了一个处理函数:

signal(SIGINT, handler); 

handler就是信号处理函数的函数名。一旦随后在程序执行过程中出现了SIGINT信号,handler函数就会自动被调用。

每个信号处理函数都必须有一个int类型的参数,且返回类型为void。当一个特定的信号产生并调用相应的处理函数时,信号的编码会作为参数传递给处理函数。知道是哪种信号导致了处理函数被调用是十分有用的,尤其是它允许我们对多个信号使用同一处理函数。

信号处理函数可以做许多事。这可能包含忽略该信号、执行一些错误恢复或终止程序。然而,除非信号是由调用abort函数(26.2节)raise函数引发的,否则信号处理函数不应该调用库函数或试图使用具有静态存储期(18.2节)的变量。(但这些规则也有例外。)

一旦信号处理函数返回,程序就会从信号发生点恢复并继续执行,但有2种例外情况:

  • 如果信号是SIGABRT,当处理函数返回时程序会(异常地)终止;
  • 如果处理的信号是SIGFPE,那么处理函数返回的结果是未定义的。(也就是说,不要处理它。)

虽然signal函数有返回值,但经常被丢弃。返回值是指向指定信号的前一个处理函数的指针。如果需要,可以将它保存在变量中。特别是,如果打算恢复原来的处理函数,那么就需要保留signal函数的返回值:

void (*orig_handler)(int); /* function pointer variable */ 
... 
orig_handler = signal(SIGINT, handler);

这条语句将handler函数安装为SIGINT的处理函数,并将指向原来的处理函数的指针保存在变量orig_handler中。如果要恢复原来的处理函数,我们需要使用下面的代码:

signal(SIGINT, orig_handler); /* restores original handler */

24.3.3 预定义的信号处理函数

除了编写自己的信号处理函数,还可以选择使用<signal.h>提供的预定义的处理函数。有两个这样的函数,每个都是用宏表示的。

  • SIG_DFLSIG_DFL按“默认”方式处理信号。可以使用下面的调用安装SIG_DFL

    signal(SIGINT, SIG_DFL); /* use default handler */ 
    

    调用SIG_DFL的结果是由实现定义的,但大多数情况下会导致程序终止。

  • SIG_IGN。调用

    signal(SIGINT, SIG_IGN); /* ignore SIGINT signal */ 
    

    指明随后当信号SIGINT产生时,忽略该信号。

除了SIG_DFLSIG_IGN<signal.h>可能还会提供其他的信号处理函数,其函数名必须是以SIG_和一个大写字母开头。当程序刚开始执行时,根据不同的实现,每个信号的处理函数都会被初始化为SIG_DFLSIG_IGN

<signal.h>还定义了另一个宏SIG_ERR,它看起来像是个信号处理函数。实际上,SIG_ERR是用来在安装处理函数时检测是否发生错误的。如果一个signal调用失败(即不能对所指定的信号安装处理函数),就会返回SIG_ERR并在errno中存入一个正值。因此,为了测试signal调用是否失败,可以使用如下代码:

if (signal(SIGINT, handler) == SIG_ERR) { perror("signal(SIGINT, handler) failed"); ... 
}

在整个信号处理机制中,有一个棘手的问题:如果信号是由处理这个信号的函数引发的,那会怎样呢?为了避免无限递归,C89标准为程序员安装的信号处理函数引发信号的情况规定了一个两步的过程。首先,要么把该信号对应的处理函数重置为SIG_DFL(默认处理函数),要么在处理函数执行的时候阻塞该信号。(SIGILL是一个特殊情况,当SIGILL发生时这两种行为都不需要。)然后,再调用程序员提供的处理函数。

请注意!!信号处理完之后,处理函数是否需要重新安装是由实现定义的。UNIX实现通常会在使用处理函数之后保持其安装状态,但其他实现可能会把处理函数重置为SIG_DFL。在后一种情况下,处理函数可以通过在其返回前调用signal函数来实现自身的重新安装。

C99对信号处理过程做了一些小的改动。当信号发生时,实现不仅可以禁用该信号,还可以禁用别的信号。对于处理SIGILLSIGSEGV信号(以及SIGFPE信号)的信号处理函数,函数返回的结果是未定义的。C99还增加了一条限制:如果信号是因为调用abort函数或raise函数而产生的,信号处理函数本身一定不能调用raise函数。(我的理解是,raise不能连续发生)


24.3.4 raise函数

int raise(int sig); 

通常信号是由于运行时错误或外部事件而产生的,但有时候如果程序可以触发信号会非常方便。raise函数就可以实现这一目的。raise函数的参数指定所需信号的编码:

raise(SIGABRT); /* raises the SIGABRT signal */ 

raise函数的返回值可以用来测试调用是否成功:0代表成功,非0则代表失败。

下面的程序说明了如何使用信号。首先,给SIGINT信号安装了一个惯用的处理函数(并小心地保存了原先的处理函数),然后调用raise_sig产生该信号;接下来,程序将SIG_IGN设置为SIGINT的处理函数并再次调用raise_sig;最后,它重新安装信号SIGINT原先的处理函数,并最后调用一次raise_sig

/*
tsignal.c
--Tests signals
*/
#include <signal.h> 
#include <stdio.h> 
void handler(int sig); 
void raise_sig(void);
int main(void) 
{ void (*orig_handler)(int); printf("Installing handler for signal %d\n", SIGINT); orig_handler = signal(SIGINT, handler); raise_sig(); printf("Changing handler to SIG_IGN\n"); signal(SIGINT, SIG_IGN); raise_sig(); printf("Restoring original handler\n"); signal(SIGINT, orig_handler); raise_sig(); printf("Program terminates normally\n"); return 0; 
} 
void handler(int sig) 
{ printf("Handler called for signal %d\n", sig); 
} 
void raise_sig(void) 
{ raise(SIGINT); 
}

当然,调用raise并不需要在单独的函数中。这里定义raise_sig函数只是为了说明一点:无论信号是从哪里产生的(无论是在main函数中还是在其他函数中),它都会被最近安装的该信号的处理函数捕获

这段程序的输出可能会有多种。下面是一种可能的输出形式:

Installing handler for signal 2 
Handler called for signal 2 
Changing handler to SIG_IGN 
Restoring original handler

这个输出结果表明,我们的实现把SIGINT的值定义为2,而且SIGINT原先的处理函数一定是SIG_DFL。(如果是SIG_IGN,应该会看到信息Program terminates normally。)最后,我们注意到SIG_DFL会导致程序终止,但不会显示出错消息。


24.4 <setjmp.h>: 非局部跳转

int setjmp(jmp_buf env); 
_Noreturn void longjmp(jmp_buf env, int val); 

通常情况下,函数会返回到它被调用的位置。我们无法使用goto语句(6.4节)使它转到其他地方,因为goto只能跳转到同一函数内的某个标号处。但是<setjmp.h>可以使一个函数直接跳转到另一个函数,不需要返回。

<setjmp.h>中最重要的内容就是setjmp宏和longjmp函数setjmp宏“标记”程序中的一个位置,随后可以使用longjmp跳转到该位置。虽然这一强大的机制可以有多种潜在的用途,但它主要被用于错误处理。

如果要为将来的跳转标记一个位置,可以调用setjmp宏,调用的参数是一个jmp_buf类型(在<setjmp.h>中声明)的变量。setjmp宏会将当前“环境”(包括一个指向setjmp宏自身位置的指针)保存到该变量中,以便将来可以在调用longjmp函数时使用,然后返回0

要返回setjmp宏所标记的位置可以调用longjmp函数,调用的参数是调用setjmp宏时使用的同一个jmp_buf类型的变量。longjmp函数会首先根据jmp_buf变量的内容恢复当前环境,然后从setjmp宏调用中返回——这是最难以理解的。这次setjmp宏的返回值是val,就是调用longjmp函数时的第二个参数。(如果val的值为0,那么setjmp宏会返回1。)

请注意!!一定要确保作为longjmp函数的参数之前已经被setjmp调用初始化了。还有一点很重要:包含setjmp最初调用的函数一定不能在调用longjmp之前返回。如果两个条件都不满足,调用longjmp会导致未定义的行为。(程序很可能会崩溃。)

总而言之,setjmp会在第一次调用时返回0;随后,longjmp将控制权重新转给最初的setjmp宏调用,而setjmp在这次调用时会返回一个非零值。明白了吗?我们可能需要一个例子。

下面的程序使用setjmp宏在main函数中标记一个位置,然后函数f2通过调用longjmp函数返回到这个位置。

/*
tsetjmp.c 
--Tests setjmp/longjmp
*/
#include <setjmp.h> 
#include <stdio.h> 
jmp_buf env; 
void f1(void); 
void f2(void); 
int main(void) 
{ if (setjmp(env) == 0) printf("setjmp returned 0\n"); else { printf("Program terminates: longjmp called\n"); return 0; } f1(); printf("Program terminates normally\n"); return 0; 
} void f1(void) 
{ printf("f1 begins\n"); f2(); printf("f1 returns\n"); 
}void f2(void) 
{ printf("f2 begins\n"); longjmp(env, 1); printf("f2 returns\n"); 
} 

这段程序的输出如下:

setjmp returned 0 
f1 begins 
f2 begins 
Program terminates: longjmp called 

setjmp宏的最初调用返回0,因此main函数会调用f1。接着,f1调用f2f2使用longjmp函数将控制权重新转给main函数,而不是返回到f1。当longjmp函数被执行时,控制权重新回到setjmp宏调用。这一次setjmp宏返回1(就是在longjmp函数调用时所指定的值)。


问与答

问1:书上说,在调用可能修改errno的库函数之前把errno设置为0是很重要的。但是,我见过一些UNIX程序在没有把errno设置为0的情况下就对其进行测试。这是什么缘故呢?

答:UNIX程序通常包含对操作系统函数的调用。这些系统调用需要用到errno,但使用方法与本节提到的方法略有不同。当这样的调用失败时,除了在errno中存储一个值之外,还会返回一个特殊的值(例如-1空指针)。程序不需要在这些调用之前往errno中存储0,因为函数的返回值本身就可以表明发生了错误。C标准库中的一些函数也是这样的:errno更多地用于指明错误类型而不是用于发出出错信号

问2:我使用的<errno.h>版本中除了EDOMERANGE以外,还定义了其他的宏。这是合法的吗?

答:是合法的C标准允许使用宏表示其他错误条件,只要宏的名字以字母E开头并且其后有一个数字或大写字母。UNIX实现中通常会定义许多这样的宏。

问3:一些表示信号的宏的名字含义比较模糊,比如SIGFPESIGSEGV。这些名字是如何得来的呢?

答:信号的名字可以追溯到早期的C编译器,这些编译器运行在DECPDP-11计算机上。PDP-11的硬件可以检测一些错误,诸如“Floating Point Exception”“Segmentation Violation”。

问4:我很好奇。书上说除非信号是由abort函数或raise函数引发的,否则信号处理函数不应该调用库函数。但你又说有例外情况,是什么例外呢?

答:信号处理函数可以调用singal函数,只要第一个参数是当前正在处理的信号就可以。这一限定条件很重要,因为它允许信号处理函数自身进行重新安装。在C99中,信号处理函数还可以调用abort函数或_Exit函数(26.2节)

问5:接着上一个问题,信号处理函数通常不能访问具有静态存储期的变量。这个规则的例外是什么?

答:这个问题要难回答一些。答案涉及<signal.h>头中声明的一个名为sig_atomic_t的类型。根据C标准,sig_atomic_t是一个可以作为一个“原子实体”访问的整型。换句话说,CPU可以用一条指令从内存中取出sig_atomic_t的值或将其存放到内存中,而不需要用两条或更多条指令。通常把sig_atomic_t定义为int,因为大多数CPU可以用一条指令存取int类型的值。

下面谈谈信号处理函数不可以访问静态变量这一规则的例外情况C标准允许信号处理函数在sig_atomic_t类型的变量中存储值(即使该变量具有静态存储期也可以),前提是该变量声明为volatile。为了了解这一不可思议的规则产生的原因,考虑信号处理函数要修改一个类型比sig_atomic_t宽一些的静态变量的情况。如果程序在信号发生之前从内存中取出了该变量的一部分,并在信号处理完毕后取完该变量,那么这个值就没有价值了。sig_atomic_t类型的变量可以一步取出,所以不会出现这种问题。把变量声明为volatile会警告编译器,变量的值随时可能改变。(信号可能突然产生,并调用信号处理函数来修改该变量。)

问6:程序tsignal.c在信号处理函数内调用了printf函数。这不是非法的吗?

答:如果信号处理函数是由raiseabort调用的,那么就可以调用库函数。tsignal.c使用raise来调用信号处理函数。

问7setjmp会如何修改传递给它的参数呢?C语言不是始终以值的形式传递参数吗?

答:C标准要求jmp_buf必须是一个数组类型,因此传递给setjmp的实际上是一个指针。

问8:我在使用setjmp时遇到一些问题。使用setjmp有什么限制吗?

答:按照C标准,只有2种使用setjmp的方式是合法的。

  • 作为表达式语句中的表达式(可能会强制转换成void)。

  • 作为ifswtichwhiledofor语句中控制表达式的一部分。整个控制表达式必须符合下面的形式之一,其中constexp是一个整型常量表达式,而op是关系或判等运算符。

    • setjmp(...)
    • !setjmp(...)
    • constexpr op setjmp(...)
    • setjmp(...) op constexpr

其他的用法会导致未定义的行为。

问9:调用longjmp函数后,程序中变量的值是什么?

答:大部分变量的值保留了longjmp函数被调用时的值。然而,包含setjmp宏的函数中,自动变量的值是不确定的,除非该变量被声明为volatile或者在执行setjmp时没有被修改过。

问10:在信号处理函数里调用longjmp函数合法吗?

答:是合法的,只要该信号处理函数的调用不是由某个信号处理函数执行过程中触发的信号引发的。(C99删除了这一限制。)


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/186603.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【腾讯云云上实验室】向量数据库+LangChain+LLM搭建智慧辅导系统实践

目录 一、搭建智慧辅导系统——向量数据库实践指南1.1、创建向量数据库并新建集合1.2、使用 TKE 快速部署 ChatGLM1.3、部署 LangChain PyPDFVectorDB等组件1.4、配置知识库语料1.5、基于 VectorDB LLM 的智能辅导助手 二、LLM时代的次世代引擎——向量数据库2.1、向量数据库L…

FastDFS+Nginx - 本地搭建文件服务器同时实现在外远程访问「内网穿透」

文章目录 前言1. 本地搭建FastDFS文件系统1.1 环境安装1.2 安装libfastcommon1.3 安装FastDFS1.4 配置Tracker1.5 配置Storage1.6 测试上传下载1.7 与Nginx整合1.8 安装Nginx1.9 配置Nginx 2. 局域网测试访问FastDFS3. 安装cpolar内网穿透4. 配置公网访问地址5. 固定公网地址5.…

ProgrammingError: nan can not be used with MySQL

该错误怎么发生的&#xff1f; 我们先在本地创建测试表&#xff1a; CREATE TABLE users_test (id int NOT NULL AUTO_INCREMENT COMMENT 主键,trade_account varchar(50) DEFAULT NULL COMMENT 交易账号,username varchar(50) DEFAULT NULL,email varchar(100) DEFAULT NULL…

acwing算法基础之数学知识--求组合数进阶版

目录 1 基础知识2 模板3 工程化 1 基础知识 请明确如下关于取余的基本定理&#xff1a; 数a和数b的乘积模上p&#xff0c;等于数a模上p和数b模上p的乘积。即&#xff0c; ( a ⋅ b ) m o d p ( a m o d p ) ⋅ ( b m o d p ) (a \cdot b ) \ mod \ p (a \ mod \ p) \cdot …

数字系列——数字经济

数字经济是全球经济未来发展方向&#xff0c;正在成为重组全球要素资源、重塑全球经济结构、改变全球竞争格局的关键力量。都知道数字经确实很重要&#xff0c;但有些人还傻傻搞不懂数字经济到底是什么&#xff1f;小编今天就给大家捋一捋。 什么是数字经济&#xff1f; 数字经…

Glove学习笔记

global vectors for word representation B站学习视频 1、LSA与word2vec 我们用我们的见解&#xff0c;构建一个新的模型&#xff0c;Glove&#xff0c;全局向量的词表示&#xff0c;因为这个模型捕捉到全局预料的统计信息。 LSA:全局矩阵分解word2vec&#xff1a;局部上下文…

全国最大液冷数据中心全链基地

10月27日&#xff0c;曙光数创于青岛举办“深蓝计划”新品发布会暨曙光数创(青岛)产业创新基地启动仪式。会上曙光数创发布新一代一体化风液混冷先进数据中心&#xff0c;并宣布全国规模最大的液冷数据中心全链条产业创新基地正式启动。 “曙光数创希望通过全场景可用、全行业适…

AI生成的图片有版权了

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 把发到小红书的AI图片搬运到百家号&#xff0c;然后被起诉了! 长知识了&#xff0c;原来AI生成的图片也有版权了&#xff0c;AI生成图片著作权第一案判了&#xff0c;这绝对是一件划时代事情&…

微信小程序真机调试技巧,解决各种疑难杂症

1.在真机上看log 也许你调试的时候&#xff0c;会使用到真机调试或者预览模式或者体验版模式&#xff0c;这些模式都有可能出现意想不到的bug问题&#xff0c;这时候调试模式就非常非常重要了&#xff0c;特别是给领导看的时候&#xff0c;在领导手机上出现bug了&#xff0c;这…

QT 项目中添加文件夹(分类文件)

为了更方便的整理项目的文件&#xff0c;添加文件夹把文件进行分类。 1.首先在项目文件中创建新的文件夹 2.把需要归类的文件放入新建的文件中 3.右键然后选择add..... 4.运行此程序&#xff0c;会报错因为文件路径改变了&#xff0c;需要在.pro中修改路径 注意事项 文件夹内部…

NSSCTF第14页(2)

[UUCTF 2022 新生赛]ezpop 提示说看看反序列化字符串逃逸 PHP反序列化字符串逃逸_php反序列化逃逸-CSDN博客 php反序列化字符逃逸_php反序列化逃逸_Leekos的博客-CSDN博客 buuctf刷题9 (反序列化逃逸&shtml-SSI远程命令执行&idna与utf-8编码漏洞)_extract($_post);…

[PTP][1588v2] Delay_Resp消息

一、报文格式 0------3--------7--------11--------15--------------------------------31 |TranSpec|MsgType|Reserved1| VerPTP | MsgLength | ----------------|------------------|---------------------------------| | DomainNumber | Res…

码云配置遇到秘钥不正确

你这个就是秘钥没有和git绑定&#xff0c; 需要 git config --global user.name "你的用户名随便写" git config --global user.email "你的邮箱"

DCAMnet网络复现与讲解

距论文阅读完毕已经过了整整一周多。。。终于抽出时间来写这篇辣&#xff01;~ 论文阅读笔记放这里&#xff1a; 基于可变形卷积和注意力机制的带钢表面缺陷快速检测网络DCAM-Net&#xff08;论文阅读笔记&#xff09;-CSDN博客 为了方便观看&#xff0c;我把结构图也拿过来了。…

VS的调试

1.Visual Studio 中&#xff0c;Release、Debug、x86和x64 四个不同的概念 1&#xff09;Release 和 Debug 是编译的不同配置&#xff0c;用于控制编译器生成的代码和符号信息的方式。 Release 配置用于生成最终发布版本的代码。在 Release 模式下&#xff0c;编译器会进行各…

软考:2024年软考高级:软件工程

软考&#xff1a;2024年软考高级: 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &#xff08;1…

2023 年 IntelliJ IDEA下载、安装教程,附详细图文

大家好&#xff0c;今天为大家带来的是 2023年 IntelliJ IDEA 下载、安装教程&#xff0c;超详细的图文教程&#xff0c;亲测可用。 文章目录 1 IDEA 下载2 IDEA 安装3 IDEA 使用4 快捷键新手必须掌握&#xff1a;Ctrl&#xff1a;Alt&#xff1a;Shift&#xff1a;Ctrl Alt&a…

机械臂仿真之vrep如添加视觉传感器

基于视觉的机械臂作业任务&#xff0c;如何在vrep中加入视觉传感器&#xff0c;并获取画面&#xff1f;

认证鉴权方案

现在一般使用比较多的认证方式有四种: SessionTokenSSO单点登录OAtuth登录1.Cookie + Session 最常见的就是 Cookie + Session 认证。 Session,是一种有状态的会话管理机制,其目的就是为了解决HTTP无状态请求带来的问题。 当用户登录认证请求通过时,服务端会将用户的信息存…

【排序,直接插入排序 折半插入排序 希尔插入排序】

文章目录 排序排序方法的分类插入排序直接插入排序折半插入排序希尔插入排序 排序 将一组杂乱无章的数据按照一定规律排列起来。将无序序列排成一个有序序列。 排序方法的分类 储存介质&#xff1a; 内部排序&#xff1a;数据量不大&#xff0c;数据在内存&#xff0c;无需…