C语言——实用调试技巧

news2024/11/20 15:27:44

一、为什么漏洞叫bug

为什么漏洞的英文是bug,众所周知bug有虫子的意思,这其实有一个很有名的故事。

1947年,计算机科学的先驱格蕾丝·霍普(Grace Hopper)和她的团队在使用哈佛大学的马克二电脑时,发现了一个故障。他们检查机器时,在其中一个继电器里找到了一只飞蛾,这只飞蛾卡在了机器的部件中,导致电脑出错。他们将这只飞蛾移除,并在日志中记录了这个事件,写下了"bug"这个词。

Grace Hopper
Grace Hopper第一次发现这个bug的手稿

因此,这个词就被用来形象地描述导致技术设备或软件出现问题的缺陷。

然而,值得注意的是,"bug"这个词作为故障的隐喻使用,其实早在电脑和电子设备之前就已经存在了。例如,19世纪的发明家托马斯·爱迪生就在他的通信中使用了"bug"这个词来描述一些技术上的难题或者故障。因此,虽然霍普的故事让这个词变得更加知名,但它作为技术故障的同义词在电脑产生之前就已经被使用了。

对应的debug就变成了调试。

有趣的是在许多编程编辑器和编译器中,调试(debugging)功能的图标常常采用一只虫子的形象。由于“debugging”字面意思就是“除虫”,因此使用虫子作为图标既幽默又形象地反映了这一过程——即检测并修复编程代码中的bug。

二、调试是什么

调试(debug/debugging)是软件开发中的一个关键过程,它涉及识别、检查、定位和修正软件程序中的错误或称为“bug”的缺陷。调试的目的是确保程序按照期望正确运行,并且解决任何可能导致程序崩溃、产生不正确结果或者表现出不预期行为的问题。

调试是一个可能既复杂又耗时的过程,它需要程序员有良好的逻辑思维能力、对代码的深入理解以及对可能出现问题的系统部分有敏锐的洞察力。通过调试,开发者能够确保软件的质量,提升用户体验,减少未来可能出现的问题。

程序员在进行代码调试的时候可以被比喻为侦探。每一段代码都像是一个潜在的线索,每一个异常都可能是犯罪现场的蛛丝马迹。当程序不按预期运行时,程序员就要开始他们的侦探工作,仔细检查代码库,寻找导致问题的源头。
他们会审查变量、跟踪函数调用、监视内存使用情况,并且利用断点来逐步执行代码,就像侦探在犯罪现场搜集证据一样。他们需要逻辑推理和细致观察,推敲每一个逻辑分支,解读每一行代码背后的意图,以期揭开导致软件故障的秘密。
调试的过程充满了挑战,它要求程序员不仅要有深厚的技术知识,还要有像侦探一样的耐心和洞察力,以便能够解决那些最棘手的编程之谜。

是否曾有过长时间埋首于代码,却苦于无法定位那个狡猾的错误?是否曾经用尽目力审视每一行代码,试图捕捉那个潜藏的bug?是否因为无法追踪到问题的根源而感到挫败?让我们一起尝试这种更好用的调试方法吧!

三、调试

这里用的编译器是Visual Studio 2022

1、调试基本步骤

(1)发现程序错误
(2)以隔离、消除等方式对错误进行定位
(3)确定错误产生的原因
(4)提出纠正错误的解决方法
(5)对程序错误予以改正、重新测试

2、debug和release的区别

debug称为调试版本,它包含调试信息,并且不做任何优化,以便程序员调试程序。

release称为发布版本,它通常是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

在选择debug版本后运行程序,对应的路径下就会产生debug文件夹:

debug文件夹中有相关可执行程序:

在选择release版本后运行程序,对应的路径下就会产生release文件夹:

release文件夹中有相关可执行程序:

可以看到release版本的可执行程序内存大小更小,因为它没有包含各种包含调试信息。所以在release版本下是没法正常调试的。

#include <stdio.h>

int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

如果选择release版本,再进行调试,监视局部变量时会发现:

如果选择debug版本,再进行调试,监视局部变量时是正常的:

而且在release版本下是无法进行逐步调试的,程序会直接运行跳过很大一个代码块。不只有这些区别,在反汇编等方面也有一些区别。

所以我们在进行调试时要选择debug版本。

3、调试相关的快捷键

F5

启动调试,经常是用来直接跳到下一个断点处。

F9

创建断点和取消断点。

在程序中设置断点是调试过程的一个基本而强大的功能。断点的作用主要包括:

  1. 暂停执行:断点允许程序员指定一个特定的代码位置,当程序执行到这个位置时,它会暂停运行。这意味着可以在代码执行到某一行时立即停下来,而不是等到程序完全运行结束或者出现错误后才停止。

  2. 检查程序状态:一旦程序暂停,程序员可以检查和评估程序的当前状态,包括变量的值、内存状态、调用栈和其他重要的调试信息。这有助于了解程序的行为和查找可能出错的原因。

  3. 控制执行流程:通过设置断点,程序员可以控制程序执行的流程,逐步执行(stepping)代码来观察程序的每一步行为,这有助于识别出问题发生的确切位置。

  4. 条件调试:许多调试工具允许设置条件断点,即只有当满足特定条件时,程序才会在断点处暂停。这使得调试工作更加高效,尤其是在处理需要在特定情况下才出现的错误时。

  5. 减少调试时间:通过合理地放置断点,程序员可以快速跳过他们确定没有问题的代码部分,直接关注可能出现问题的区域,这样可以节省寻找bug的时间。

设置断点是一种无需在代码中添加临时调试代码(例如打印语句)的调试方法。它提供了一种干净、可控且不非常侵入性的方式来观察程序的内部工作,使调试过程更加直观和高效。

条件断点

#include <stdio.h>

int main()
{
	int i = 0;

	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

假设要测试第六次循环,跳过前五次循环,可以在循环体内加一个条件断点,设置i == 5,即当第六次循环时停止。

这时按下F5(如若不行,则按Fn + F5)之后,就直接到达第六次循环:

F10

逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。不会进入函数内部。

F11

逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。

Ctrl + F5

开始执行(不调试),如果你想让程序运行起来而不进行调试就可以用这个快捷键。

4、查看程序当前信息

(1)查看变量的值

#include <stdio.h>

int main()
{
	int a = 10;
	printf("%d\n", a);
	return 0;
}

调试开始之后,才能看到窗口设置。

在蓝色框的三个窗口都可以看到变量的值。

自动窗口可以自动检测变量,直接显示上下文相关变量,不用手动输入变量。

局部变量窗口也是自动显示上下文相关变量的。

监视窗口中的变量虽然是手动输入的,但是在脱离变量作用范围时也能观察,这个更加实用。

在查看传递到函数中的数组的值时:

#include <stdio.h>

void test(int a[])
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", a[i]);
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test(arr);
	return 0;
}

F11进行到函数内部时,在监视窗口输入a,只能看到数组首元素一个值:

在a后面加逗号和个数可以解决:

(2)查看内存

查看内存的窗口同样在调试窗口中

可以看地址对应的数据。

#include <stdio.h>

int main()
{
	int i = 0x11223344;
	return 0;
}

可以点击这个两个三角形,调整现实的列数,一般是用4列,4列正好4字节,一个整型的大小。

这里的数据都是以十六进制显示的。

(3)查看调用堆栈

调用堆栈可以清晰地看到函数调用关系。

#include <stdio.h>

void test2()
{
	printf("6\n");
}

void test1()
{
	test2();
}

int main()
{
	test1();
	return 0;
}

可以选择显示外部代码,可以看到主调函数调用main函数,以及一系列函数的调用。

在调用堆栈窗口右击鼠标可以看到更多选项:

(4)查看汇编信息

汇编信息可以更清楚地了解程序,从程序底层了解程序。

#include <stdio.h>

int main()
{
	int a = 1;
	int b = 2;
	int res = a + b;
	printf("%d\n", res);
	return 0;
}

程序调试起来后可以在调试窗口找到反汇编,这里可以转到反汇编。

或者程序调试起来后在代码段右击鼠标,可以转到反汇编。

汇编代码

#include <stdio.h>

int main()
{
00007FF639742AE0  push        rbp  
00007FF639742AE2  push        rdi  
00007FF639742AE3  sub         rsp,148h  
00007FF639742AEA  lea         rbp,[rsp+20h]  
00007FF639742AEF  lea         rcx,[__39EA4C76_test@cpp (07FF639751008h)]  
00007FF639742AF6  call        __CheckForDebuggerJustMyCode (07FF639741352h)  
	int a = 1;
00007FF639742AFB  mov         dword ptr [a],1  
	int b = 2;
00007FF639742B02  mov         dword ptr [b],2  
	int res = a + b;
00007FF639742B09  mov         eax,dword ptr [b]  
00007FF639742B0C  mov         ecx,dword ptr [a]  
00007FF639742B0F  add         ecx,eax  
00007FF639742B11  mov         eax,ecx  
00007FF639742B13  mov         dword ptr [res],eax  
	printf("%d\n", res);
00007FF639742B16  mov         edx,dword ptr [res]  
00007FF639742B19  lea         rcx,[string "%d\n" (07FF639749BD8h)]  
00007FF639742B20  call        printf (07FF6397413B6h)  
	return 0;
00007FF639742B25  xor         eax,eax  
}
00007FF639742B27  lea         rsp,[rbp+128h]  
00007FF639742B2E  pop         rdi  
00007FF639742B2F  pop         rbp  
00007FF639742B30  ret

这里还有一些编译器加的一些调试或者其他的汇编代码,不太方便观察的话可以进行下面的设置操作:

找到解决方案管理器:

鼠标右击自己的解决方案,点击属性:

出现属性页:

这时再转到反汇编:

#include <stdio.h>

int main()
{
00007FF7FDF61840  push        rbp  
00007FF7FDF61842  push        rdi  
00007FF7FDF61843  sub         rsp,148h  
00007FF7FDF6184A  lea         rbp,[rsp+20h]  
	int a = 1;
00007FF7FDF6184F  mov         dword ptr [a],1  
	int b = 2;
00007FF7FDF61856  mov         dword ptr [b],2  
	int res = a + b;
00007FF7FDF6185D  mov         eax,dword ptr [b]  
00007FF7FDF61860  mov         ecx,dword ptr [a]  
00007FF7FDF61863  add         ecx,eax  
00007FF7FDF61865  mov         eax,ecx  
00007FF7FDF61867  mov         dword ptr [res],eax  
	printf("%d\n", res);
00007FF7FDF6186A  mov         edx,dword ptr [res]  
00007FF7FDF6186D  lea         rcx,[string "%d\n" (07FF7FDF69C24h)]  
00007FF7FDF61874  call        printf (07FF7FDF61195h)  
	return 0;
00007FF7FDF61879  xor         eax,eax  
}
00007FF7FDF6187B  lea         rsp,[rbp+128h]  
00007FF7FDF61882  pop         rdi  
00007FF7FDF61883  pop         rbp  
00007FF7FDF61884  ret

(5)查看寄存器信息

可以查看当前运行环境的寄存器使用情况。

#include <stdio.h>

int main()
{
	int a = 1;
	int b = 2;
	int res = a + b;
	printf("%d\n", res);
	return 0;
}

可以在调试窗口中找到寄存器窗口:

配合汇编更好观察(这里是32位平台,从寄存器名字和数据长度可以看出):

配合汇编更(这里是64位平台,从寄存器名字和数据长度可以看出):

在知道寄存器的名字的情况下,也可以在监视窗口中观察寄存器的信息:

这里显示的值是十进制,可以在监视窗口中右击鼠标,选择以十六进制显示:

四、实例

1、实例一

求1! + 2! + ... + n!的值

#include <stdio.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int i = 1;
	int j = 1;
	int res = 1;
	int sum = 0;
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= i; j++)
		{
			res *= j;
		}
		sum += res;
	}
	printf("%d\n", sum);
	return 0;
}

这里有一个很明显的错误,在运行时,我们使用3作为测试数据输入,运行结果为:

明显是错误的,因为1! + 2! + 3! = 1 + 2 + 6 = 9。

错误就是res在每次循环没有重置为一,导致答案加了之前的数据,以至于答案偏大。

这里假设我们没发现这个错误,在发现运行结果错误后,我们及时进行逐语句(按F10,如若不行,请按Fn + F10)调试:

在进行到外层循环的第三轮时,我们发现,res的值不应该是2,而应该是1,原来是res的值没有重置为一,所以我们发现了错误。

改正后的代码:

#include <stdio.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int i = 1;
	int j = 1;
	int res = 1;
	int sum = 0;
	for (i = 1; i <= n; i++)
	{
		res = 1;
		for (j = 1; j <= i; j++)
		{
			res *= j;
		}
		sum += res;
	}
	printf("%d\n", sum);
	return 0;
}

2、实例二

#include <stdio.h>

int main()
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("6\n");
	}
	return 0;
}

只是简单的看了这段代码,发现是越界访问数组,可是当我们将程序运行(32位平台)起来后会发现这个程序是死循环。

在调试时我们发现在i等于12时即第13轮循环时,arr[i] = 0;这个操作使i变成了0,后面还会循环出现这种状况导致无限循环。

实际上在i变化的同时,arr[12]也在变化,实际上i的地址和arr[12]的地址是同一个所以i和arr[12]的空间实际上是同一个。

所以会导致这样的死循环,这种错误如果不调试的话是很难发现的。

这里的一个小格是一个整型的大小。

五、如何写出易于调试的代码

1、添加必要的注释
2、避免一些陷阱
3、良好的风格
4、使用assert函数
5、使用const修饰变量

1、assert函数

在C语言中,assert函数是一种用来辅助调试程序的宏,定义在assert.h头文件中。它用于检查程序中的假设是否为真,如果假设为假(即表达式的值为0),assert会打印一条错误信息到标准错误流,并终止程序的执行。如果假设为真,程序会继续进行。

assert函数的用法非常简单。只需包含assert.h头文件,并在代码中加入assert宏,后跟需要检查的表达式。例如:

#include <assert.h>

int main() {
    int a = 10;
    assert(a == 10); // 如果a等于10,则程序继续执行;如果a不等于10,程序终止,打印错误信息
    assert(a > 10);  // 如果a不大于10,则打印错误信息并终止程序,如果a大于10,程序继续进行

    // ... 其他代码 ...

    return 0;
}

如果assert宏的参数表达式结果为非零(表达式为真),则assert不会执行任何操作。如果表达式结果为零(表达式为假),assert会打印包含以下信息的错误消息:

  • 失败的表达式本身
  • 发生错误的源文件名
  • 发生错误的源代码行号
  • 发生错误的函数名(C99标准之后)
#include <assert.h>

int main()
{
	int a = 1;
	assert(a == 0);
	return 0;
}

打印错误:

使用assert的好处在于,它可以在开发阶段帮助程序员检测逻辑错误,但是它也有一个缺点:在发布的产品代码中,assert会增加不必要的性能开销。因此,在编译发布版本的代码时,通常会通过定义NDEBUG宏来禁用assert。这可以在编译命令中通过定义宏来实现,或者在包含assert.h之前的代码中定义。

#define NDEBUG
#include <assert.h>

定义了NDEBUG宏后,assert宏实际上不会执行任何操作,因此不会产生任何性能开销。

#define NDEBUG
#include <assert.h>

int main()
{
	int a = 1;
	assert(a == 0);
	return 0;
}

运行之后不会输出错误信息。

使用assert是一种很好的调试习惯,可以帮助迅速定位程序中的错误。不过,它不应该被用作常规的错误处理机制,而只应该用于检测程序中的逻辑错误和不应该发生的情况。

2、例子

仿照库函数strcpy写一个类似的函数实现这种功能。

#include <stdio.h>
#include <assert.h>

void my_strcpy(char* dest, char* src)
{
	assert(dest != NULL);//防止传参指针是空指针
	assert(src != NULL);
	while (*src != '\0')//复制内容
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src;//将\0复制
}

int main()
{
	char ch1[10] = "nihao";
	char ch2[10];

	my_strcpy(ch2, ch1);
	printf("%s\n", ch2);
	return 0;
}

3、const修饰

(1)引子

对于上面的例子,我们发现在库函数中strcpy的参数是:

这里的源字符串指针是使用const修饰的。

为社么呢?

实际上是防止两个变量赋值搞反。

如果上个例子中将变量赋值写反,就像下面这样,如果不用const修饰的话,编译器可能不报错。如果使用const修饰,则会无法编译,编译器会直接报错。

	*src = *dest;

修改后的代码:

#include <stdio.h>
#include <assert.h>

void my_strcpy(char* dest, const char* src)
{
	assert(dest != NULL);//防止传参指针是空指针
	assert(src != NULL);
	while (*src != '\0')//复制内容
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src;//将\0复制
}

int main()
{
	char ch1[10] = "nihao";
	char ch2[10];

	my_strcpy(ch2, ch1);
	printf("%s\n", ch2);
	return 0;
}

(2)介绍

在C语言中,const关键字用于修饰变量,表示变量的值不应被改变。使用const修饰符有多个好处:

  1. 类型安全: 当你将const用于函数参数时,如const char* src,这告诉编译器和阅读代码的人,这个参数指向的数据不应该被该函数修改。这提供了额外的类型安全,因为如果函数内部的代码尝试修改src指向的内容,编译器将报错。

  2. 自文档化: const关键字作为函数接口的一部分有助于自我文档化。人们立即可以看出src是不应该改变的,而dest因为没有const修饰,是可以被修改的。

  3. 优化: 编译器可以利用const关键字进行优化。因为编译器知道某些数据是不变的,它可以做出一些优化决策,比如将const数据放在只读段中。

  4. 保护数据: 当你使用指针作为函数参数传递大型结构或数组时,const可以确保传入的数据在函数内部不会被意外修改,从而防止潜在的bug和副作用。

简单来说,使用const修饰符是一种良好的编程实践,它有助于提高代码的可读性和健壮性。

(3)细节

在C语言中,const关键字用来修饰变量、数组或者指针,以指示它们的值不应该被修改。当用于指针时,const可以用于指针自身、指针所指向的数据,或者同时用于两者。具体来说:

1、指针指向的内容为常量 (const修饰的是指针指向的类型):

	const char i = '\0';
	const char* ptr = &i;

同时也可以这样表示,只不过第一种表示方式更常见:

	const char i = '\0';
	char const* ptr = &i;

这里ptr是一个指向const char的指针。这意味着,你不能通过ptr来修改它所指向的内容,实际上这里的const char*类型的指针也不仅可以指向const char类型的数据,也可以指向char类型的变量,当指向char类型的变量时,虽然不能通过ptr改变变量的值,但是可以直接用char类型的变量名改变变量的值,即对变量名直接赋值。然而,指针ptr本身不是常量,所以你可以改变指针ptr来指向另一个地址。

2、指针自身为常量(const修饰的是指针自身):

	char i = '\0';
	char* const ptr = &i;

这里ptr是一个指向char的常量指针。这意味着,ptr一旦被初始化后,就不能再指向另一个地址,但是你可以修改ptr所指向的内容。

3、指针和所指的内容都为常量(const同时修饰指针指向的类型和指针自身):

	const char i = '\0';
	const char* const ptr = &i;

这里ptr是一个指向const char的常量指针。这意味着你既不能修改ptr指向的地址,也不能通过ptr修改它所指向的内容。

(4)小调整

对于上面的例子,我们采用的返回值是void,实际上strcpy的返回值是char *类型:

然而,返回char *类型又有什么好处呢,返回char *类型实际上是返回目标内存空间的起始地址,这样可以实现链式访问,即一个函数的返回值作为另一个函数的参数。

#include <stdio.h>
#include <assert.h>

char* my_strcpy(char* dest, const char* src)
{
	assert(dest != NULL);//防止传参指针是空指针
	assert(src != NULL);
	char* p = dest;//将目标空间初始地址保存,下面dest的值会发生改变
	while (*src != '\0')//复制内容
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src;//将\0复制
	return p;
}

int main()
{
	char ch1[10] = "nihao";
	char ch2[10];
	printf("%s\n", my_strcpy(ch2, ch1));//实现链式访问
	return 0;
}

六、容易出现的错误

1、编译型错误

一般是语法或词法错误,编译器会提示,很容易找到了。

2、链接错误

一般是标识符拼写错误,编译器只会报错,不会提示到哪一行。

#include <stdio.h>

int main()
{
	int a = 0;
	int b = 1;
	int res = Add(a, b);
	printf("%d\n", res);
	return 0;
}

这里调用了Add函数,但是未定义Add函数。

这时会报错:

这时可以用 Ctrl + f 来搜索Add函数的调用位置以及定义:

3、运行时的错误

能正常运行,但是运行结果不正确。

这个就要通过上面的一些调试方法来进行调试排错。

七、结语

这些只是一些较为简单的调试方法,后面会接着更新一些进阶的调试方法。

对于初学者可能大部分时间在写代码,少部分时间在调试;而对于程序员则是大部分时间在调试,少部分时间在写代码。

调试要多上手练习。

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

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

相关文章

为CT_P自动注册与CT_Run相关的方法

概述 在docx.oxml.text.paragraph模块中定义了CT_P段落对象元素类&#xff0c;但是CT_P中并未定义add_r等与CT_Run相关的方法。在不断探索源码逻辑的过程中&#xff0c;对这种自动为类注册合适的方法的功能进行了梳理——xmlchemy这个模块设计的真好&#xff01;&#xff01;&…

rust跟我学(一):模块编写与使用

在rust中,单元文件可以被称为模块。 以下内容,将以get_local_info为例,讲解mod在工程中的使用。 先看下图,network.rs是src文件夹里的一个文件,我们可以单独把这个文件理解成一个模块。Rust比较智能,它可以将文件名称直接理解成一个模块,这在go里面是做不到的。 Rust其实…

​​社交媒体与新闻:Facebook在信息传播中的作用

社交媒体已经成为我们获取和传播新闻的主要渠道之一&#xff0c;而Facebook作为社交媒体的巨头&#xff0c;在信息传播中扮演着举足轻重的角色。本文将深入探讨社交媒体对新闻传播的影响&#xff0c;聚焦于Facebook在这一领域的独特作用&#xff0c;以及这种作用对我们的新闻体…

2024北京智博会:共赴科技盛会,助力跨界融合与实体经济深度发展

2024年6月&#xff0c;一场备受瞩目的科技盛会将在北京亦创国际会展中心拉开帷幕。作为国内外科技交流的重要平台&#xff0c;2024北京智博会将汇集众多科技领域的精英和企业&#xff0c;共同探讨跨界应用、实体经济深度融合等议题&#xff0c;为推动科技强国发展注入新动力。 …

SSL之mkcert构建本地自签名

文章目录 1. 什么是SSL2. mkcert&#xff1a;快速生成自签名证书2.1 mkcert的工作流程如下&#xff1a;2.2 window 本地实现自签证书2.2.1 下载安装2.2.2 下载,生成本地 SSL2.2.3 生成 pem 自签证书,可供局域网内使用其他主机访问。2.2.4 使用-psck12 生成*.p12 文件 2.3 Sprin…

7.11、Kali Linux中文版虚拟机安装运行教程

目录 一、资源下载准备工作 二、安装教程 三、kali linux换源 四、apt-get update 报错 一、资源下载准备工作 linux 中文版镜像历史版本下载:http://old.kali.org/kali-images/ 大家可以自行选择版本下载&#xff0c;本人下载的是2021版本 二、安装教程 打开vmvare wokst…

修炼九阳神功——“函数”

目录 前言 1. 函数的概念 2. 库函数 2.1 标准库和头⽂件 2.2 库函数的使用方法 2.2.1 功能 2.2.2 头⽂件包含 2.2.3 实践 2.2.4 库函数⽂档的⼀般格式 3. 自定义函数 3.1 函数的语法形式 3.2 函数的举例 ​编辑 4. 形参和实参 4.1 实参 4.2 形参 4.3 实参和形…

【dc-dc】世微AP5127平均电流型LED降压恒流驱动器 双色切换的LED灯驱动方案

这是一款双色切换的LED灯方案&#xff0c;12-50V 降压恒流,输出&#xff1a;6V 2.5A ​ 这是一款PWM工作模式 , 高效率、 外围简单、内置功率管&#xff0c;适用于 输入的 高 精度降压 LED 恒流驱动芯片。输出大功率可 达 25W&#xff0c;电流 2.5A。 可实现全亮/半亮功能切换…

Ansible Filter滤波器的使用(二)

一、【说在前面】 Ansible Filter一般被称为滤波器或者叫过滤器。 这个东西初次听到以为是什么科学计算的东西&#xff0c;但是想来ansible不太可能有什么滤波操作&#xff0c;所以这个东西本质是一个数值筛选器&#xff0c;内置函数&#xff0c;本质是一个为了做区别化的工具…

“15个必备的自动化测试工具,助你构建2024年的自动化策略!“

以下为作者观点&#xff1a; 如何选择正确的自动化测试工具&#xff1f;自动化测试工具是旨在通过自动化测试脚本验证功能或非功能软件需求的应用程序&#xff0c;帮助加快发布速度、提高项目质量并强化成果。 自动化测试工具可以帮助开发测试人员轻松创建、运行和维护测试&a…

闲鱼宝库亮相!闲鱼商品详情关键词搜索电商API接口助你畅享无尽好货!

随着互联网的快速发展&#xff0c;电商平台的崛起已经改变了人们的购物习惯。而在众多电商平台中&#xff0c;闲鱼作为一款社区二手交易平台&#xff0c;一直备受用户喜爱。如今&#xff0c;闲鱼宝库正式亮相&#xff0c;为用户带来了更加全面、详细的商品详情关键词搜索电商AP…

两整数之和

题目链接 两整数之和 题目描述 注意点 不使用 运算符 和 - ​​​​​​​&#xff0c;计算并返回两整数之和-1000 < a, b < 1000 解答思路 需要用位运算来模拟加法&#xff0c;关键是要找到相加的和以及进位1的部分。如果不考虑进位的话&#xff0c;相加可以运用异…

Spring - 如何控制多个 AOP 切面执行顺序?

众所周知&#xff0c;Spring 声明式事务是基于 AOP 实现的&#xff0c;那么&#xff0c;如果我们在同一个方法自定义多个 AOP&#xff0c;我们如何指定他们的执行顺序呢&#xff1f; 三种解决方案 1、通过实现 org.springframework.core.Ordered 接口 Component Aspect S…

​LeetCode解法汇总83. 删除排序链表中的重复元素

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给定一个已排序的链表的头 head &#xf…

【论文笔记合集】卷积神经网络之深度可分离卷积(Depthwise Separable Convolution)

本文作者&#xff1a; slience_me 我看的论文地址&#xff1a;MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications 内容 1. 标准卷积 假设输入为DFDFM&#xff0c;输出为输入为DFDFN&#xff0c;卷积核为DKDKM&#xff0c;共有N个卷积核进…

【AIGC入门一】Transformers 模型结构详解及代码解析

Transformers 开启了NLP一个新时代&#xff0c;注意力模块目前各类大模型的重要结构。作为刚入门LLM的新手&#xff0c;怎么能不感受一下这个“变形金刚的魅力”呢&#xff1f; 目录 Transformers ——Attention is all You Need 背景介绍 模型结构 位置编码 代码实现&…

51单片机学习总结(自学)

1、模块化编程 c语言模块化编程实现思路设计代码 具体的程序实现代码如下所示 1&#xff1a;程序的头文件 2&#xff1a;程序的函数文件 3&#xff1a;程序的主文件控制函数的实现 持续更新中......

算法部署过程中如何确保数据的安全?

在数字化时代&#xff0c;数据安全成为了企业和个人面临的一项主要挑战。随着技术的迅速发展&#xff0c;尤其在算法部署过程中&#xff0c;确保敏感数据的安全性变得更加复杂和关键。在这个背景下&#xff0c;软件加密和授权机制的作用显得尤为重要。软件加密不仅仅是转换数据…

高纯气体市场调研:预计2029年将达到331亿美元

高纯气体应用领域极宽&#xff0c;在半导体工业&#xff0c;高纯氮、氢、氩、氦可作为运载气和保护气;高纯气体可作为配制混合气的底气。随着LED和半导体的发展&#xff0c;对于其原物料生产的所需要的高纯气体&#xff0c;特别是7N级别的高纯氨气的需求不断增加&#xff0c;近…

CMake TcpServer项目 生成静态库.a / 动态库.so

CMake 实战构建TcpServer项目 静态库/动态库-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135608829?spm1001.2014.3001.5501 在这篇博客的基础上&#xff0c;我们把头文件放在include里边&#xff0c;把源文件放在src里边&#xff0c;重新构建 hehedali…