C语言进阶---程序的编译(预处理操作)+链接

news2024/11/15 13:29:34

1、程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境,它用于实际执行代码。

1、每个源文件单独经过编译器处理,或生成一个对应的目标文件。

​ 在windows环境下目标文件是以.obj后缀的文件。

​ 在Linux环境下目标文件是以.o后缀的文件。

​ 在Linux环境下目标文件是以.out后缀的文件是可执行程序。

2、所有的目标文件+链接库,使用链接器,链接生成一个可执行程序。

​ 我们在引用头文件的时候,需要用到库函数依赖的东西,在链接库中提供。

3、所需要的两个工具:在windows、VS环境下

  • 编译器:cl.exe
  • 链接器:link.exe

在这里插入图片描述

1、组成一个程序的每个源文件通过编译过程分别转换为目标文件。

2、所有目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

3、链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库(程序员自己写的函数),将其需要的函数也链接到程序中。

在这里插入图片描述

2、编译、链接细节剖析

其实编译阶段由可以细分为三部分:预编译/预处理、编译、汇编

如下图:

在这里插入图片描述

这里使用Linux环境下的Gcc编译器,来观察具体每一步都发生了什么?

1、预处理 选项:

#-o     output,此选项是把输出在屏幕上的信息,改为输出在文件中。
gcc -E test.c -o test.i

预处理完成后就停下来,预处理之后产生的结果都放在test.i文件中。

通过观察可以发现,预处理做如下事情:

  • 头文件的包含:将指令#include所包含的内容,放在test.c文件中。

  • define定义符号的替换并且删除define指令比如:

    //预处理之前
    #define MAX 100;
    int m = MAX;
    
    //预处理之后,把#define MAX 100删除,且将MAX替换为100。
    int m = 100;
    
  • 删除注释。

总结:预处理阶段做的都是文本操作。比如:一些符号的替换,一些符号的删除,头文件的包含。

2、编译 选项:

gcc -S test.i

编译完成后就停下来,结果保存在test.s中。

  • 生成了test.s文件。
  • 把C语言代码转换成汇编代码。
  • 语法分析、词法分析、符号分析、语义分析。
  • 这里重点说一下—符号分析。符号分析就是会把:全局变量,自定义函数,main函数先全部统计一下。

3、汇编 选项:

gcc -c test.s

汇编完成之后就停下来,结果保存在test.o中。

  • 将上面编译产生的test.s文件会变为test.o文件,这个test.o文件就是目标文件。
  • 此操作会把汇编代码转为二进制指令。
  • 形成符号表。这个对应编译中的符号分析

什么是符号表呢?

比如现在工程中有一个test.c和add.c文件,add.c文件里面由个Add函数,test.c文件有个main函数和对Add和拿书的声明。它们经过每个.c文件都会经过预处理编译汇编,最终生成test.o和add.o目标文件。

在生成add.o文件时,Add是个符号,会给Add函数一个地址,用来关联Add函数,用符号表存放地址来关联Add函数。

然后在生成test.o文件时,声明的Add是个符号,会声明的Add一个无效的地址,并且给main函数一个地址。全部都用符号表关联起来。

所以一共生成两个符号表。

在这里插入图片描述

4、如果想要一步直接走完以上三步:

gcc test.c

5、链接

  • 合并段表。
  • 符号表的合并和重定位。

上面汇编过程生成个两个符号表:

在这里插入图片描述

在这里插入图片描述

现在在链接阶段需要将符号表进行合并和重定位。

就是把上面的两个符号表,给合并为一个符号表,并且Add是重复的,需要筛选出一个真实的Add符号,肯定会把声明的Add给抛弃掉,因为声明Add是个无效的地址,而真实的Add函数是有效的地址。所以会保留Add函数的符号表。这就是符号表的重定位。合并后的符号表,如下:

在这里插入图片描述

__链接过程将项目中的多个目标文件以及所需的库文件链接成最终的可执行文件 (Executable File) 。 __

3、运行环境

程序执行的过程:

​ 1、程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必 须由手工安排,也有可能是通过可执行代码置入只读内存来完成。

​ 2、程序的执行便开始,接着便调用main函数。

​ 3、开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序 同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。

​ 4、终止程序。正常终止main函数,也有可能时意外终止。

4、预处理详解

4.1、预定义符号

__FILE__      //进行编译的源文件
__LINE__      //文件当前的行号
__DATE__      //文件被编译的日期
__TIME__      //文件被编译的时间
__STDC__      //如果编译器遵循ANSI C,其值为1,否则未定义,VS编译器不遵循ANSI C。Gcc遵循

这些预定义符号都是语言内置的。

举个例子:

printf("file:%s line:%d\n",__FILE__,__LINE__);

#include <stdio.h>

int main()
{
	printf("file:%s line=%d date:%s time:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);
	return 0;
}

输出:

在这里插入图片描述

用处:可以在记录日志时使用。

4.2、#define

4.2.1、#define定义标识符

语法:
    #define name stuff          //后面不要加分号;

举个例子:

#define MAX 100
#define reg register      //为register这个关键字,创建一个简短的名字。
#define do_forever for(;;)   //用更形象的符号来替换一种实现。
#define CASE break;case      //在写case语句的时候自动把break写上。

//如果定义的stuff过长,可以分为几行写,除了最后一行外,每行的后面加一个反斜杠(续行杠)
#define DEBUG_PRINT printf("file:%s\tline=%d\t \
							date:%s\ttime:%s\n", \
                            __FILE__, __LINE__, \
                            __DATE__, __TIME__);

4.2.2、#define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义(define macro)。

下面是宏的声明方式:

//parament-list    参数列表
#define name(parament-list) stuff           //后面不要加分号;

其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

eg1:写一个x+y的宏

#include <stdio.h>

//定义宏
#define Add(x,y) ((x)+(y));

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

在这里插入图片描述

如:写一个x*x的宏

#include <stdio.h>
//定义一个x*x的宏
#define SQUARE(x) x*x

int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

重点:宏是完成替换的,而不是先计算的。

那是如何个替换呢?

如下:

#include <stdio.h>
//定义一个x*x的宏
#define SQUARE(x) x*x

int main()
{
	int ret = SQUARE(5+1);    //把此参数变为5+1。
	printf("%d\n", ret);
	return 0;
}

输出:

在这里插入图片描述

分析:

宏只是替换操作,5+1在SQUARE里面不会计算为6,然后在传给#define定义的宏。而是直接把5+1传给#define定义的宏,然后x*x就变为了:5+1*5+1=5+5+1=11。

那如何改进呢?

如下:

#include <stdio.h>
//定义一个x*x的宏
#define SQUARE(x) ((x)*(x))  //加上小括号,每个参数加个括号,然后整体的宏也加小括号。

int main()
{
	int ret = SQUARE(5+1); 
	printf("%d\n", ret);
	return 0;
}

输出:

在这里插入图片描述

4.2.3、#define宏的替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

​ 1、在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

​ 2、替换文本随后被插入到程度中原来文本的位置。对于宏,参数名被它们的值所替换。

​ 3、最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过 程。

注意:

​ 1、宏参数和#define定义中可以出现其它#define定义的符号。但是对于宏,不能出现递归。

​ 2、当预处理器搜索#define定义的符号的时候,字符串常量的内容并不内搜索。

4.2.4、#和##

1、#

#----------把参数插入到字符串中

如下代码:

#include <stdio.h>
//定义一个x*x的宏
#define PRINT(N) printf("the value of "#N" is %d\n",N);

int main()
{
	int a = 10;
	PRINT(a);
	return 0;
}

输出:

在这里插入图片描述

这个输出结果其实就是:

printf("the value of ""a"" is %d\n");

#的作用就是把参数当作字符串给插入到字符串中去。并不是替换为相对应的值。

2、##

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

如下代码:

#include <stdio.h>
#define CAT(Class,num) Class##num

int main()
{
	int Class111 = 100;
	printf("%d\n", CAT(Class, 111));
	return 0;
}

输出:

在这里插入图片描述

4.2.5、带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现永久性效果。

例如:

x+1;            //不带副作用
x++;           //带副作用

4.2.6、宏和函数的对比

宏通常被应用于执行简单的运算。

比如:在两个数中找出较大值。宏和函数的对比

#define MAX(x,y) ((x)>(y)?(x):(y))

int Max(int x, int y)
{
	return (x>y?x:y);
}

int main()
{
    return 0;
}

那为什么不用函数来完成这个任务呢?

原因有二:

​ 1、用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。

​ _所以宏比函数在程序的规模和速度方面更胜一筹。

​ 2、更为重要的是函数的参数必须声明为特定的类型。

​ 所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型,长整型,浮点型等可以用来比较> 来比较的类型。

宏是类型无关的。

适用宏时,就相当于运行个表达式。

而使用函数时,需要函数调用(参数传参,栈帧创建)、计算、函数返回。

宏的缺点:

  • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  • 宏时没法调式的。
  • 宏由于类型无关,也就不够严谨。
  • 宏可能会带来运算符优先级的问题,导致容易出现错误。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

比如:要实现malloc(40)的功能,我们在计算我们需要多少大小的字节时,如果数字的情况下就不方便,我们能不能这样写:malloc(10,int)。如果能这样写就很方便了。但是很遗憾,函数不支持,那现在只能使用宏来实现了。

如下:

#include <stdio.h>
#define MALLOC(num,type) (type*)malloc((num)*sizeof(type))

int main()
{
	int* p = MALLOC(10, int);
	return 0;
}

//以上代码就转换为如下:
int* p = (int*)malloc(10*sizeof(type));

宏和函数的区别总结如下:

属性#define宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括函数参数只在函数调用的时候求值- -次,它的结果值传递给函数。表达式的求值结果更容易预测。
带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次结果更容易控制。
参数类型宏的参数与类型无关,只要对参数的操作是合法的.它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
调试宏不方便调试函数是可以逐语句调试的
递归宏是不能递归的函数可以递归。

4.2.7、命名约定

一般来讲函数的宏的使用语法相似,所以语言本身没法帮我们区分二者。

那可以通过平时习惯:

  • 把宏名全部大写。
  • 函数名不要全部大写。

4.3、#undef

这条指令用于移除一个宏定义。

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要移除。



#include <stdio.h>
#define M 100

int main()
{
	printf("%d\n", M);    //这个正常运行
#undef M
	printf("%d\n", M);    //这个会报错。
	return 0;
}

4.5、条件编译

常用编译指令:可以选择是否进行编译某个语句,或者说同时有多个语句,我们选择判断来决定编译那个语句。

也可以指是否判断被定义。

#define M 3

1、单分支条件编译
#if M<5
    //...
#endif
    //常量表达式由预处理器求值

eg:
#define __DEBUG__ 1
    #if __DEBUG__
		//...
    #endif
    
    
    
2、多分支条件编译
#define ...
#if M<3
    //...
#elif M>6
    //...
#else 
    //...
#endif 
    

3、判断是否被定义
#define ...
#if define(symbol)
#ifdef symbol
    
#if !define(symbol)
#ifndef symbol
    

4、嵌套指令
#if define(AAA)
    #ifdef OPTION1
    	//...
    #endif
    #ifdef OPTION2
    	//...
    #endif
#elif defined(BBB)
    #ifdef OPTION3
    	//...
    #endif
#endif

在编译一个程序的时候我们如果将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h>
#define __DEBUG__

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__     //判断是否有这个__DEBUG__指令,如果有则执行下面的打印。
	printf("%d\n", arr[i]);
#endif // __DEBUG__
	}
	return 0;
}

输出:

在这里插入图片描述

4.6、文件包含

我们已经知道,#define指令可以使另外一个文件被编译,就像它实际出现于#define指令的地方一样。

我们来说一下文件包含的重要性。

现有有个test.c文件和test.h文件。

//test.c
#include "test.h"    //这个引入的使test.h

int main()
{

	return 0;
}


//test.h
int Add(int x, int y);

如上场景在Linux下观察,执行命令gcc -E test.c -o test.i进行预编译后,我们会发现,会将test.h文件的Add函数声明内容给放进test.c里面,这个很正常。

但是如果,我不小心多次引用头文件呢?

//test.c
#include "test.h"    //这个引入的使test.h
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"

int main()
{

	return 0;
}


//test.h
#pragma once
int Add(int x, int y);

这回我引用了5次头文件,那是否会将test.h中的Add函数声明内容也放test.c文件里面5次呢?

答案:是的,会!!!。

所以这问题就出来了,那如果我们在引用<stdio.h>的时候不小心多引用了几次呢?那者会多出几千甚至上万行重复的代码。因此我们需要利用上面条件编译来防止这种事情发生。

我们可以这样做:

#ifndef __TEST_H__
#define __TEST_H__
int Add(int x, int y);
#endif


//这里我们使用条件编译来判断一下,就行了。
//分析过程:第一次引用头文件时,由于__TEST_H__我们没有定义,所以#ifndef为真,然后向下运行,#define 定义__TEST_H__。然后如果我们重复引用头文件,那第二次执行#ifndef,由于第一次定义了__TEST_H__,所以if哦按段为假,不在执行。这样就避免了重复引用头文件。

当然也有个更简单的写法可以避免以上问题:

#pragma once            //添加这一行内容,这个作用:防止头文件被多次重复的包含
int Add(int x, int y);

4.7、在引用头文件时,<>和""的区别

我们在以往的学习中会发现,在引用库自带的头文件和引用我们自己创建的头文件的时候,方式不一样。

  • 引用库自带的头文件:#include <stdio.h>
  • 引用自创建的头文件:#include "stdio.h"

主要区别就是符号的不同。

<>""的区别是查找的策略不同

  • <>查找的策略:直接去编译器提供的库目录下查找。
  • ""查找的策略:
    • 先去代码所在的路径下查找。
    • 如果上面的找不到,再去编译器提供的库目录下查找。

所以说,我们在引用系统自带的头文件时,也可以用"",但是不推荐,因为需要查找两次,效率会变低。

#include "stdio.h"

Linux环境的标准头文件的路径:

/usr/include

4.8、笔试题

1、头文件中的ifndef/define/endif是干什么用的?
    
    
2、#icnlude<filename.h>和#include "filename.h"有什么区别。

5、模拟实现offsetof

那如何模拟实现一个offsetof宏呢?如下思路:

在这里插入图片描述

我们将每一个成员的存储首位置减去结构体的起始位置就可以得到偏移量。

那可不可以这样:我们把结构体的起始位置变为0,那么每个成员的首地址,其实就是偏移量了。

下面代码实现:

#include <stdio.h>
#include <stddef.h>
//type*就是struct S*
//(struct S*)0是吧结构体起始位置强制转为从0开始
//(struct S*)0)->m_name  指向每一个成员变量
//(size_t)将每一成员变量的地址强制类型转为int类型的,从而求出偏移量。
#define OFFSETOF(type,m_name) (size_t)&(((type*)0)->m_name)

struct S
{
	char c1;
	int i;
	char c2;
};

int main()
{
	struct S s = { 0 };
	printf("%d\n", OFFSETOF(struct S, c1));
	printf("%d\n", OFFSETOF(struct S, i));
	printf("%d\n", OFFSETOF(struct S, c2));
	return 0;
}

输出:

在这里插入图片描述

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

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

相关文章

ps htop 输出可读文件

需要安装sudo apt-get install aha echo q | ps auxf | aha --black --line-fix > psps.html echo q | htop | aha --black --line-fix > htop.html 使用浏览器打开

Python神经网络学习(七)--强化学习--使用神经网络

前言 前面说到了强化学习&#xff0c;但是仅仅是使用了一个表格&#xff0c;这也是属于强化学习的范畴了&#xff0c;毕竟强化学习属于在试错中学习的。 但是现在有一些问题&#xff0c;如果这个表格非常大呢&#xff1f;悬崖徒步仅仅是一个长12宽4&#xff0c;每个位置4个动…

制造业网络安全最佳实践

网络安全已成为生产部门的一个重要关注点&#xff0c;制造业网络安全现在是高管层的主要考虑因素。 在工业 4.0和物联网 ( IoT )出现的推动下&#xff0c;当今的互连工业系统提供了多种优势&#xff0c;但也使组织面临新的风险和漏洞。 让我们探讨一些制造业网络安全最佳实践…

SOLIDWORKS软件有哪些版本?

SOLIDWORKS软件是基于Windows开发的三维 CAD系统&#xff0c;技术创新符合CAD技术的发展潮流和趋势&#xff0c;SOLIDWORKS每年都有数十乃至数百项的技术创新&#xff0c;公司也获得了很多荣誉。该系统在1995-1999年获得全球微机平台CAD系统评比NO1&#xff1b;从1995年至今&am…

Quiz 15: Object-Oriented Programming | Python for Everybody 配套练习_解题记录

文章目录 Python for Everybody课程简介 Quiz 15: Object-Oriented Programming单选题&#xff08;1-11&#xff09;Multiple instances Python for Everybody 课程简介 Python for Everybody 零基础程序设计&#xff08;Python 入门&#xff09; This course aims to teach e…

qemu 源码编译 qemu-system-aarch64 的方法

前言 最近调试 RT-Thread bsp qemu-virt64-aarch64 时&#xff0c;遇到无法使用网络设备问题&#xff0c;最后确认是 当前 ubuntu 20.04 系统 使用 apt install 安装的 qemu qemu-system-aarch64 版本太低。 RT-Thread qemu-virt64-aarch64 里面的网络设备&#xff0c;需要较新…

回顾分类决策树相关知识并利用python实现

大家好&#xff0c;我是带我去滑雪&#xff01; 决策树&#xff08;Decision Tree&#xff09;是一种基本的分类与回归方法&#xff0c;呈树形结构&#xff0c;在分类问题中&#xff0c;表示预计特征对实例进行分类的过程。它可以认为是if-then规则的集合&#xff0c;也可以认为…

多表-DDL以及DQL

多表DDL 个表之间也可能存在关系 存在在一对多和多对多和一对多的关系 一对多&#xff08;外键&#xff09; 在子表建一哥字段&#xff08;列&#xff09;和对应父表关联 父表是一&#xff0c;对应子表的多&#xff08;一个部门对应多个员工&#xff0c;但一个员工只能归属一…

结构体和数据结构--从基本数据类型到抽象数据类型、结构体的定义

在冯-诺依曼体系结构中&#xff0c;程序代码和数据都是以二进制存储的&#xff0c;因此对计算机系统和硬件本身而言&#xff0c;数据类型的概念其实是不存在的。 在高级语言中&#xff0c;为了有效的组织数据&#xff0c;规范数据的使用&#xff0c;提高程序的可读性&#xff0…

使用Streamlit和Matplotlib创建交互式折线图

大家好&#xff0c;本文将介绍使用Streamlit和Matplotlib创建一个用户友好的数据可视化Web应用程序。该应用程序允许上传CSV文件&#xff0c;并为任何选定列生成折线图。 构建Streamlit应用程序 在本文中&#xff0c;我们将指导完成创建此应用程序的步骤。无论你是专家还是刚刚…

three.js利用点材质打造星空

最终效果如图&#xff1a; 一、THREE.BufferGeometry介绍 这里只是做个简单的介绍&#xff0c;详细的介绍大家可以看看THREE.BufferGeometry及其属性介绍 THREE.BufferGeometry是Three.js中的一个重要的类&#xff0c;用于管理和操作几何图形数据。它是对THREE.Geometry的一…

leetcode 226. 翻转二叉树

2023.7.1 这题依旧可以用层序遍历的思路来做。 在层序遍历的代码上将所有节点的左右节点进行互换即可实现二叉树的反转。 下面上代码&#xff1a; class Solution { public:TreeNode* invertTree(TreeNode* root) {queue<TreeNode*> que;if(root nullptr) return{};que…

gradio库中的Dropdown模块:创建交互式下拉菜单

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

2020年全国硕士研究生入学统一考试管理类专业学位联考逻辑试题——纯享题目版

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;考取过HCIE Cloud Computing、CCIE Security、CISP等证书。&#x1f433; &#x1f495;兴趣爱好&#xff1a;b站天天刷&#xff0c;题目常常看&a…

编译原理期末复习简记(更新中~)

注意&#xff1a;该复习简记只是针对我校期末该课程复习纲要进行的&#xff0c;仅供参考 第一章 引论 编译程序是什么&#xff1f; 编译程序是一个涉及分析和综合的复杂系统 编译程序组成 编译程序通常由以下内容组成 词法分析器 输入 组成源程序的字符串输出 记号/单词序列语法…

Jenkins+Gitlab+Springboot项目部署Jar和image两种方式

Springboot环境准备 利用spring官网快速创建springboot项目。 添加一个controller package com.example.demo;import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;RestController public class…

新华三眼中的AI天路

ChatGPT的火爆&#xff0c;在全球范围内掀起了新一轮的AI风暴。如今&#xff0c;各行各业都在讨论AI&#xff0c;各个国家都在密集进行新一轮的AI基础设施建设与技术投入。 但眼前的盛景并非突然到来&#xff0c;就拿这一轮大模型热潮来说&#xff0c;谷歌早在2018年底就发布了…

协议速攻 IIC协议详解

介绍 IIC是一种 同步 半双工 串行 总线 同步 指的是同一根时钟线(SCL) 半双工 可以进行双向通信&#xff0c;但是收发不能同时进行&#xff0c;发的时候禁止接收&#xff0c;接的时候禁止发送 串行 数据是一位一位发送的 总线 两根线(SCL SDA)可以接多个IIC类型器件&#…

《统计学习方法》——逻辑斯蒂回归和最大熵模型

参考资料&#xff1a; 《统计学习方法》李航通俗理解信息熵 - 知乎 (zhihu.com)拉格朗日函数为什么要先最大化&#xff1f; - 知乎 (zhihu.com) 1 逻辑斯蒂回归 1.1 逻辑斯蒂回归 输入 x ( x ( 1 ) , x ( 2 ) , ⋯ , x ( n ) , 1 ) T x(x^{(1)},x^{(2)},\cdots,x^{(n)},1…

【动态规划算法练习】day11

文章目录 一、1312. 让字符串成为回文串的最少插入次数1.题目简介2.解题思路3.代码4.运行结果 二、1143. 最长公共子序列1.题目简介2.解题思路3.代码4.运行结果 三、1035. 不相交的线1.题目简介2.解题思路3.代码4.运行结果 总结 一、1312. 让字符串成为回文串的最少插入次数 1…