C++函数在库中的地址

news2024/11/15 15:37:43

本文讲述C++如何直接调用动态库dll或者so中的函数。

首先我们准备一个被调用库,这个库里面有两个函数,分别是C++98 与 C++11 下的,名称是run2和run1。

被调用库

相关介绍请看之前的文章《函数指针与库之间的通信讲解》。

//dll_ex_im.h
#ifndef __DLL_EX_IM_H__
#define __DLL_EX_IM_H__
#include <functional>
#include <string>
#include <iostream>
#include <thread>
#include <chrono>
#ifdef _WINDOWS
#ifdef DLLProvider
#define DLL_EXPORT_IMPORT		 __declspec(dllexport)  
#else  
#define DLL_EXPORT_IMPORT		 __declspec(dllimport)  
#endif  
#else
#define DLL_EXPORT_IMPORT __attribute__((visibility("default")))
#endif

//typedef class DLL_EXPORT_IMPORT std::function< void(std::string)> output_to_caller;
DLL_EXPORT_IMPORT void run1(int a, std::function< void(std::string)> output);
DLL_EXPORT_IMPORT void run2(int a, void(*output)(std::string));

#endif //__DLL_EX_IM_H__
//dll_ex_im.cpp
#include "dll_ex_im.h"

void run1(int a, std::function< void(std::string)> output)
{
	std::cout << "run1" << std::endl;
	std::cout << "got parametrer: " << a << std::endl;
	std::cout << "return signal: " << std::endl;
	while (true) {
		output("run1: " + std::to_string(a++));
		std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	}
}

void run2(int a, void(*output)(std::string)) {
	std::cout << "run2" << std::endl;
	std::cout << "got parametrer: " << a << std:bian

编译一下,出来的动态库在Windows下是Reflection-DLL_TEST.dll,在Linux下是libReflection-DLL_TEST.so。

Windows下直接调用

我们用BinaryViewer这款二进制查看器看看函数run1和run2在Reflection-DLL_TEST.dll长什么样子。

查找函数run1的位置:

找到三个位置,前两个应该都是函数的名称指引(知道的同学可以介绍下前两个是做啥的)。
第三个是run1函数的地址:
在这里插入图片描述
我把这个run1地址写下来:

?run1@@YAXHV?$function@$$A6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@std@@@Z

原来的函数run1长这样:

void run1(int a, std::function< void(std::string)> output);

查找函数run2的位置:

找到三个位置,前两个应该都是函数的名称指引(知道的同学可以介绍下前两个是做啥的)。
第三个是run2函数的地址:
在这里插入图片描述
我把这个run2地址写下来:

?run2@@YAXHP6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@Z

原来的函数run2长这样:

void run2(int a, void(*output)(std::string));

地址解释

以上run1和run2函数在dll二进制文件中的地址是一个经过VS C++编译器名称修饰后的函数名,这种修饰是编译器用来区分具有相同名称但不同参数或返回类型的函数的方法。
由于我不是做编译器的,下面我用聊天机器人查了一下,仅给出以上run1函数地址的解释:
?run1@@:这是函数名run1的开头部分,其中?和@@是Microsoft编译器特有的名称修饰符号。
YAXH:这表示函数的返回类型和参数类型。在这个例子中,Y 表示返回类型为 void,AX 通常表示没有通过值传递的参数(但在这个特定情况下,由于后面有V,它实际上表示有一个通过引用或指针传递的参数),H 是参数列表的开始。不过,这里的AX和H的具体含义可能因编译器的具体实现而略有不同,重要的是理解整体结构。
V:这表示接下来的部分是一个通过引用或指针传递的参数。
?$function@…@std@@:这是对 std::function 模板的修饰表示,其中省略号(…)代表模板参数的具体类型,即 void(std::string)。
KaTeX parse error: Can't use function '$' in math mode at position 7: A6AXV?$̲basic_string@..…A6 是与调用约定相关的(可能是 __cdecl 的某种变体,但具体取决于编译器和平台),AXV 表示函数接受一个参数(V 表示通过引用或指针),?$basic_string@…@std@@@Z 是对 std::string 类型的修饰表示。

Windows调用程序

#include <iostream>
//#include <list>
#include <functional>
#ifdef _WINDOWS 
#include <shlwapi.h>
#include <Psapi.h> 
#include <codecvt> 
#else
#include <dlfcn.h>
#include <codecvt>
#endif

void callback(std::string info) {
	std::cout << info << std::endl;
}


void Run1(const std::string& dllpath, const std::string& initFuncName)
{
	std::string funName = initFuncName;
#ifdef _WINDOWS
	typedef void(_stdcall* pfnInitPlugin) (int, std::function< void(std::string)>);
	funName = "?" + funName + "@@YAXHV?$function@$$A6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@std@@@Z";
	auto module = LoadLibrary(dllpath.c_str());
	//寻找初始化函数,并执行
	pfnInitPlugin initfunc = (pfnInitPlugin)GetProcAddress(module, funName.c_str());
	if (initfunc)
	{
		//常规方式
		//std::function< void(std::string)> cb = callback;
		//initfunc(4, cb);
		//lambda方式
		initfunc(4, [](std::string info) {
			std::cout << info << std::endl; 
			}
		);
	}
	else {
		std::cout << "未找到函数地址" << std::endl;
	}
#else
	typedef  void(__attribute__((__stdcall__))* pfnInitPlugin) (int, std::function< void(std::string));
	funName = "_Z22" + funName + "v";
	auto dp = dlopen(p.c_str(), RTLD_LAZY | RTLD_GLOBAL);
	if (dp)
	{
		pfnInitPlugin initfunc = (pfnInitPlugin)dlsym(dp, funName.c_str());
		if (initfunc)
		{
			initfunc(4, [](std::string info) {
				std::cout << info << std::endl;
				}
			);
		}
		//dlclose(dp);
	}
	else {
		std::cout << "未找到函数地址" << std::endl;
	}
#endif
}

void Run2(const std::string& dllpath, const std::string& initFuncName)
{
	std::string funName = initFuncName;
#ifdef _WINDOWS
	typedef void(_stdcall* pfnInitPlugin) (int, void(*output)(std::string));
	//run1
	funName = "?" + funName + "@@YAXHP6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@Z";
	auto module = LoadLibrary(dllpath.c_str());
	//寻找初始化函数,并执行
	pfnInitPlugin initfunc = (pfnInitPlugin)GetProcAddress(module, funName.c_str());
	if (initfunc)
	{
		//void(*cb)(std::string);
		//cb = callback;
		//initfunc(4, cb);
		initfunc(4, [](std::string info) {
			std::cout << info << std::endl;
			}
		);
	}
	else {
		std::cout << "未找到函数地址" << std::endl;
	}
#else
	typedef  void(__attribute__((__stdcall__))* pfnInitPlugin) (int, void(*output)(std::string));
	funName = "_Z22" + funName + "v";
	auto dp = dlopen(p.c_str(), RTLD_LAZY | RTLD_GLOBAL);
	if (dp)
	{
		pfnInitPlugin initfunc = (pfnInitPlugin)dlsym(dp, funName.c_str());
		if (initfunc)
		{
			initfunc(4, [](std::string info) {
				std::cout << info << std::endl;
				}
			);
		}
		//dlclose(dp);
	}
	else {
		std::cout << "未找到函数地址" << std::endl;
	}
#endif
}

int main(int argc, char* argv[]) {
	std::string argv1 = argv[1];
	if (argc == 2 && argv1 == "-h")
	{
		std::cout << "用法:\n【exe-name】【dll-path】【func-name】" << std::endl;
		return 0;
	}
	if (argc != 3)
	{
		std::cerr << "传入的参数数量不对,应该是俩,检查检查!!" << std::endl;
		return -1;
	}
	std::string dllPath = argv[1];
	if (dllPath.find(".dll") == dllPath.npos)
	{
		std::cerr << "传入的文件没有dll,检查检查!!" << std::endl;
		return -1;
	}
	std::string argv2 = argv[2];
	if (argv2 == "run1")
	{
		Run1(argv[1], "run1");
	}
	else if (argv2 == "run2") {
		Run2(argv[1], "run2");
	}
	else {
		std::cerr << "传入的函数名既不是 run1 也不是 run2 ,检查检查!!" << std::endl;
		return -1;
	}

	return 0;
}

在Windows下,核心代码是下面这几句:

	typedef void(_stdcall* pfnInitPlugin) (int, std::function< void(std::string)>);
	//run1
	funName = "?" + funName + "@@YAXHV?$function@$$A6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@std@@@Z";

	auto module = LoadLibrary(dllpath.c_str());
	//寻找初始化函数,并执行
	pfnInitPlugin initfunc = (pfnInitPlugin)GetProcAddress(module, funName.c_str());
	if (initfunc)
	{
		//常规方式
		//std::function< void(std::string)> cb = callback;
		//initfunc(4, cb);
		//lambda方式
		initfunc(4, [](std::string info) {
			std::cout << info << std::endl; 
			}
		);
	}
	else {
		std::cout << "未找到函数地址" << std::endl;
	}

程序读入动态库,通过函数在动态库中的地址进行直接调用。
下面是调用结果:
在这里插入图片描述

Linux下直接调用

我们用BinaryViewer这款二进制查看器看看函数run1和run2在libReflection-DLL_TEST.so长什么样子。

查找函数run1的位置:

找到4个位置,前两个应该都是函数的名称指引(知道的同学可以介绍下前3个是做啥的)。
第4个是run1函数的地址:
在这里插入图片描述
我把这个run1地址写下来:

_Z4run1iSt8functionIFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEE

原来的函数run1长这样:

void run1(int a, std::function< void(std::string)> output);

查找函数run2的位置:

找到4个位置,前两个应该都是函数的名称指引(知道的同学可以介绍下前3个是做啥的)。
第4个是run2函数的地址:
在这里插入图片描述
我把这个run2地址写下来:

_Z4run2iPFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

原来的函数run2长这样:

void run2(int a, void(*output)(std::string));

地址解释

以上run1和run2函数在so二进制文件中的地址是一个由 GCC编译器生成的 mangled(修饰)名称。这种名称用于在编译后的代码中唯一标识函数、变量等符号,同时包含类型信息。Mangled 名称对于人类来说通常是不直观的,但它们对于编译器和链接器来说是必要的,以确保在复杂的程序中正确地解析和链接符号。
由于我不是做编译器的,下面我用聊天机器人查了一下,仅给出以上run1函数地址的解释:
_Z 前缀是 GCC 编译器用于 mangled 名称的标识。
4run1i 部分是函数名称的编码,其中 run 是函数名,1 表示该函数接受一个参数,i 表示该参数的类型(在这个上下文中,它实际上是指接下来的类型信息,而不是直接的类型)。
St8functionIFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEE 是参数类型的 mangled 表示。这个类型是一个 std::function,它包装了一个可调用对象,该对象接受一个 std::string 类型的参数(没有返回值,因为 Fv 表示一个函数类型,没有返回类型)。
St8function 表示 std::function。
IFv 表示一个函数(F)没有返回值(v,即 void),并且接下来是参数类型。
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE 是 std::__cxx11::basic_string<char, std::char_traits, std::allocator> 的 mangled 表示,即 std::string。

Linux调用程序

#include <iostream>
//#include <list>
#include <functional>
#ifdef _WINDOWS 
#include <shlwapi.h>
#include <Psapi.h> 
#include <codecvt> 
#else
#include <dlfcn.h>
#include <codecvt>
#endif

void callback(std::string info) {
	std::cout << info << std::endl;
}


void Run1(const std::string& dllpath, const std::string& initFuncName)
{
	std::string funName = initFuncName;
#ifdef _WINDOWS
	typedef void(_stdcall* pfnInitPlugin) (int, std::function< void(std::string)>);
	funName = "?" + funName + "@@YAXHV?$function@$$A6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@std@@@Z";
	auto module = LoadLibrary(dllpath.c_str());
	//寻找初始化函数,并执行
	pfnInitPlugin initfunc = (pfnInitPlugin)GetProcAddress(module, funName.c_str());
	if (initfunc)
	{
		//常规方式
		//std::function< void(std::string)> cb = callback;
		//initfunc(4, cb);
		//lambda方式
		initfunc(4, [](std::string info) {
			std::cout << info << std::endl; 
			}
		);
	}
	else
	{
		std::cout<<"not find function name"<<std::endl;
	}
#else
	typedef  void(* pfnInitPlugin) (int, std::function< void(std::string)>);
	funName = "_Z4" + funName + "iSt8functionIFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEE";
	auto dp = dlopen(dllpath.c_str(), RTLD_LAZY | RTLD_GLOBAL);
	if (dp)
	{
		pfnInitPlugin initfunc = (pfnInitPlugin)dlsym(dp, funName.c_str());
		if (initfunc)
		{
			initfunc(4, [](std::string info) {
				std::cout << info << std::endl;
				}
			);
		}
		dlclose(dp);
	}
	else
	{
		std::cout<<"not find function name"<<std::endl;
	}
#endif
}

void Run2(const std::string& dllpath, const std::string& initFuncName)
{
	std::string funName = initFuncName;
#ifdef _WINDOWS
	typedef void(_stdcall* pfnInitPlugin) (int, void(*output)(std::string));
	funName = "?" + funName + "@@YAXHP6AXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z@Z";
	auto module = LoadLibrary(dllpath.c_str());
	//寻找初始化函数,并执行
	pfnInitPlugin initfunc = (pfnInitPlugin)GetProcAddress(module, funName.c_str());
	if (initfunc)
	{
		//常规方式
		//void(*cb)(std::string);
		//cb = callback;
		//initfunc(4, cb);
		//lambda方式
		initfunc(4, [](std::string info) {
			std::cout << info << std::endl;
			}
		);
	}
	else
	{
		std::cout<<"not find function name"<<std::endl;
	}
#else
	typedef  void(* pfnInitPlugin) (int, void(*output)(std::string));
	funName = "_Z4" + funName + "iPFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE";
	auto dp = dlopen(dllpath.c_str(), RTLD_LAZY | RTLD_GLOBAL);
	if (dp)
	{
		pfnInitPlugin initfunc = (pfnInitPlugin)dlsym(dp, funName.c_str());
		if (initfunc)
		{
			initfunc(4, [](std::string info) {
				std::cout << info << std::endl;
				}
			);
		}
		dlclose(dp);
	}
	else
	{
		std::cout<<"not find function name"<<std::endl;
	}
#endif
}

int main(int argc, char* argv[]) {

	//C++ 98
	//Lambda回调函数
	//Run1("./Reflection-DLL_TEST.dll","run1");
	std::string argv1 = argv[1];
	if (argc == 2 && argv1 == "-h")
	{
		std::cout << "用法:\n【exe-name】【dll-path】【func-name】" << std::endl;
		return 0;
	}
	if (argc != 3)
	{
		std::cerr << "传入的参数数量不对,应该是俩,检查检查!!" << std::endl;
		return -1;
	}
	std::string dllPath = argv[1];
	if (dllPath.find(".so") == dllPath.npos)
	{
		std::cerr << "传入的文件没有so,检查检查!!" << std::endl;
		return -1;
	}
	std::string argv2 = argv[2];
	if (argv2 == "run1")
	{
		Run1(argv[1], "run1");
	}
	else if (argv2 == "run2") {
		Run2(argv[1], "run2");
	}
	else {
		std::cerr << "传入的函数名既不是 run1 也不是 run2 ,检查检查!!" << std::endl;
		return -1;
	}
	//system("pause");
	return 0;
}

在Linux下,核心代码是下面这几句:

	typedef  void(* pfnInitPlugin) (int, std::function< void(std::string)>);
	funName = "_Z4" + funName + "iSt8functionIFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEE";
	auto dp = dlopen(dllpath.c_str(), RTLD_LAZY | RTLD_GLOBAL);
	if (dp)
	{
		pfnInitPlugin initfunc = (pfnInitPlugin)dlsym(dp, funName.c_str());
		if (initfunc)
		{
			initfunc(4, [](std::string info) {
				std::cout << info << std::endl;
				}
			);
		}
		dlclose(dp);
	}
	else
	{
		std::cout<<"not find function name"<<std::endl;
	}

程序读入动态库,通过函数在动态库中的地址进行直接调用。
下面是调用结果:
在这里插入图片描述

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

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

相关文章

【通俗理解】二项分布的均值与方差——从成功与失败的概率看分布

【通俗理解】二项分布的均值与方差——从成功与失败的概率看分布 关键词提炼 #二项分布#均值#方差#成功概率#失败概率#伯努利试验 公式解释与案例 二项分布的基本公式 二项分布描述的是在n次独立重复的伯努利试验中&#xff0c;成功次数的概率分布。每次试验的成功概率为p&…

【Android安全】Ubuntu 16.04安装GDB和GEF

1. 安装GDB sudo apt install gdb-multiarch 2. 安装GEF(GDB Enhanced Features) 官网地址&#xff1a;https://github.com/hugsy/gef 2.1 安装2021.10版本 但是在Ubuntu 16.04上&#xff0c;bash -c "$(curl -fsSL https://gef.blah.cat/sh)"等命令不好使&…

如何用 OBProxy 实现 OceanBase 的最佳路由策略

引言 OBProxy&#xff0c;即OceanBase Database Proxy&#xff0c;也简称为ODP&#xff0c;是 OceanBase数据库的专属服务代理。通过应用OBProxy&#xff0c;由后端OceanBase集群的分布式特性所带来的复杂性得以屏蔽&#xff0c;从而使得访问分布式数据库的体验如同访问单机数…

linux上用yolov8训练自己的数据集(pycharm远程连接服务器)

pycharm如何远程连接服务器&#xff0c;看之前的文章 首先去GitHub上下载项目地址&#xff0c;然后下载预训练模型放到项目主目录下 然后下载数据集&#xff0c;我这有个推荐的数据集下载网站&#xff0c;可以直接下载yolov8格式的数据集&#xff08;还支持其他格式的数据集&a…

进程间通信-命名管道

目录 原理 代码 简单通信 回归概念 原理 mkfifo 是 Linux 系统中的一个命令&#xff0c;用于创建命名管道&#xff08;named pipe&#xff09;&#xff0c;也称为 FIFO&#xff08;First In, First Out&#xff09;。命名管道是一种特殊类型的文件&#xff0c;用于进程间通…

从0到1!本地部署一个大语言模型!完整方法!

要想从零开始部署一个**大语言模型&#xff08;LLM&#xff09;**到本地&#xff0c;不仅仅是硬件上安装软件包&#xff0c;还需要对模型选择、优化和应用搭建有一定的理解。下面是一份完整教程&#xff0c;手把手带你走过如何在本地环境中部署LLM。 1. 了解部署需求与硬件准备…

交换机链路聚合

一、概述 通过链路聚合&#xff0c;可以提高链路的可靠性&#xff0c;提升链路带宽。链路具有一般有手工模式和LACP模式。 二、拓扑图 三、实操演练 1、手工模式 手工模式一般用于老旧、低端设备。 缺点&#xff1a; &#xff08;1&#xff09;为了使链路聚合接口正常工作…

brew install node提示:Error: No such keg: /usr/local/Cellar/node

打开本地文件发现Cellar目录下无法生成 node文件&#xff0c;应该是下载时出现问题&#xff0c;重复下载无法解决问题&#xff0c;只能重新安装brew。 步骤1&#xff08;安装 brew&#xff09;&#xff1a; /bin/zsh -c “$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/ra…

打造高效实时数仓,从Hive到OceanBase的经验分享

本文作者&#xff1a;Coolmoon1202&#xff0c;大数据高级工程师&#xff0c;专注于高性能软件架构设计 我们的业务主要围绕出行领域&#xff0c;鉴于初期采用的数据仓库方案面临高延迟、低效率等挑战&#xff0c;我们踏上了探索新数仓解决方案的征途。本文分享了我们在方案筛选…

uniapp离线(本地)打包

安卓离线打包 注意&#xff1a;jdk建议选择1.8 下载Android Studio配置gradle仓库地址 第一步&#xff1a;先下载对应的版本&#xff0c;进行压缩包解压 第二步&#xff1a;在电脑磁盘&#xff08;D盘&#xff09;&#xff0c;创建文件夹存放压缩包并进行解压&#xff0c;并创…

8.8canny算子检测

目录 实验原理 示例代码 运行结果 实验原理 在OpenCV中&#xff0c;Canny边缘检测是一种广泛使用的边缘检测算法。它是由John F. Canny在1986年提出的&#xff0c;并且因其性能优良而被广泛应用。在OpenCV中&#xff0c;Canny边缘检测是通过Canny函数实现的。 函数原型 v…

【爬虫软件】小红书按关键词批量采集笔记,含笔记正文、转评赞藏等!

一、背景介绍 1.1 爬取目标 熟悉我的小伙伴都了解&#xff0c;我之前开发过2款软件&#xff1a; 【GUI软件】小红书搜索结果批量采集&#xff0c;支持多个关键词同时抓取&#xff01; 【GUI软件】小红书详情数据批量采集&#xff0c;含笔记内容、转评赞藏等&#xff01; 现在…

HuggingFists算子能力扩展-PythonScript

HuggingFists作为一个低代码平台&#xff0c;很多朋友会关心如何扩展平台算子能力。扩展平台尚不支持的算子功能。本文就介绍一种通过脚本算子扩展算子能力的解决方案。 HuggingFists支持Python和Javascript两种脚语言的算子。两种语言的使用方式相同&#xff0c;使用者可以任选…

C++速通LeetCode简单第3题-相交链表

简单解&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/ class Solution { public:ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {Li…

ACL-latex模板中参考文献出现下划线---由于宏包的冲突

% \usepackage{ulem} %加入后造成参考文献有下划线&#xff0c;正常情况是没有的。 别的包也可能造成此情况&#xff0c;可以仔细检查。 如下图所示&#xff1a; \usepackage{ulem}在LaTeX中的作用主要是提供了一系列用于文本装饰和强调的命令。ulem宏包由Donald Arseneau…

移动订货小程序哪个好 批发订货系统源码哪个好

订货小程序就是依托微信小程序的订货系统&#xff0c;微信小程序订货系统相较于其他终端的订货方式&#xff0c;能够更快进入商城&#xff0c;对经销商而言更为方便。今天&#xff0c;我们一起盘点三个主流的移动订货小程序&#xff0c;看看哪个移动订货小程序好。 第一、核货宝…

Redis搭建集群

功能概述 Redis Cluster是Redis的自带的官方分布式解决方案&#xff0c;提供数据分片、高可用功能&#xff0c;在3.0版本正式推出。 使用Redis Cluster能解决负载均衡的问题&#xff0c;内部采用哈希分片规则&#xff1a; 基础架构图如下所示&#xff1a; 图中最大的虚线部分…

AI基础 L19 Quantifying Uncertainty and Reasoning with Probabilities I 量化不确定性和概率推理

Acting Under Uncertainty 1 Reasoning Under Uncertainty • Real world problems contain uncertainties due to: — partial observability, — nondeterminism, or — adversaries. • Example of dental diagnosis using propositional logic T oothache ⇒ C av ity • H…

Tomact的基本使用

一.Web服务器 Web服务器是一个软件程序,对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让web开发更加便捷.主要功能是"提供网上信息浏览服务" 二.将Demo程序部署到webapps 直接复制进webapps目录,然后打开浏览器来进行访问 基于tomact服务器部署的项…

PCL 读取STL文件转换为点云

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.2完整代码 三、实现效果 3.1原始点云 3.2数据显示 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&#xff08;长期更新&#xff09; 一、概述…