Lua 中编写 C 函数的一些便捷技巧

零、前言

使用 Lua 时,在编写 C/C++ 函数经常需要对栈进行交互,而这中间更多的操作和数组、字符串相关。

一、数组操作的便捷方式

从之前分享的 “Lua 数据类型——表” 文章中知道 Lua 中的 “数组” 是以表的形式存在,只是他的 key 值是有序的数值。

对于 Lua 的数组存取,当然可以使用之前介绍的 lua_gettablelua_settable 的方法,只是较为繁琐。所以 Lua 针对数组提供了以下的方法进行快捷的存取:

// Lua 5.3 之后才有
// 会调用 table 元方法
LUA_API void  (lua_seti) (lua_State *L, int idx, lua_Integer n);// 不调用 table 元方法
LUA_API void  (lua_rawseti) (lua_State *L, int idx, lua_Integer n);// Lua 5.3 之后才有
// 会调用 table 的元方法
LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n);// 不调用 table 元方法
LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n);

接下来一个个讲解

1、lua_seti 和 lua_rawseti

LUA_API void  (lua_seti) (lua_State *L, int idx, lua_Integer n);
LUA_API void  (lua_rawseti) (lua_State *L, int idx, lua_Integer n);

描述:

用于将栈顶的元素(即索引为 -1 )作为 value ,用 n 作为 key ,设置到索引为 idx 的 table 中。

参数:

  • L: Lua 状态机(Lua state)的指针。
  • idx: 要操作的 Lua 表在栈中的索引位置。
  • n: 设置在 table 中的 key 值(必须是整型)。

返回值:

没有返回,栈顶的元素会被使用后出栈。

两个函数的区别:

如果操作的 table 没有元表,则效果是一样的,使用 lua_rawseti 会稍微快一些。

如果操作的 table 有元表,则 lua_seti 会使用到 table 的元方法。

lua_settable 相同的用法:

lua_seti 只是针对 key 为整型的 table 设置更加便捷,当然也可以用 lua_settable 实现一样的效果

lua_pushstring(L, value);
lua_seti(L, t, key);// 当 t 为正整数时,等同于以下操作。
// 因为 t 为负数的话,lua_settable 的 t 要要深一个元素,lua_settable 的操作需要将 key 压入栈。lua_pushnumber(L, key);
lua_pushstring(L, value);
lua_settable(L, t);

2、lua_geti 和 lua_rawgeti

LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n);
LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n);

描述:

用于从索引为 idx 位置的 table 中获取 key 为 n 的值,将其压入栈中,即栈顶。

参数:

  • L: Lua 状态机(Lua state)的指针。
  • idx: 要操作的 Lua 表在栈中的索引位置。
  • n: 要获取 table 的 key 值(必须是整型)。

返回值:

返回 value 数据类型,并且会将获取到的值压入栈顶,如果没有找到对应的值,则会将 nil 压入(因为在 table 中查询一个不存在的键时,则会返回 nil )。

返回的数据类型有以下类型,可以根据类型判断是否符合期望。

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

两个函数的区别:

如果操作的 table 没有元表,则效果是一样的,使用 lua_rawgeti 会稍微快一些。

如果操作的 table 有元表,则 lua_geti 会使用到 table 的元方法。

lua_settable 相同的用法:

lua_geti 只是针对 key 为整型这一类型 table 的设置更加便捷,当然也可以用 lua_gettable 实现一样的效果

lua_geti(L, t, key)// 当 t 为正整数时,等同于以下操作。
// 因为 t 为负数的话,lua_gettable 的 t 要要深一个元素,lua_gettable 的操作需要将 key 压入栈。lua_pushnumber(L, key);
lua_gettable(L, t);

3、举个例子

以下例子实现了这几个步骤:

  1. 创建一个 table ,并压入栈中。
  2. 使用两种方式向 table 中设置元素。
  3. 使用两种方式获取 table 中的元素。
  4. 打印 table 的长度。
lua_State *L = luaL_newstate();// 创建并压入一个新表
lua_newtable(L);// --------- 添加至数组的操作方式 ---------
// 第一种:
// table[1] = "江澎涌!(快捷方式插入)"
lua_pushstring(L, "江澎涌!(快捷方式插入)");
lua_seti(L, -2, 1);// 第二种:
// 栈底 ----> 栈顶
// table - key - value
// 使用完后,key - value 会被弹出
// table[2] = "江澎涌!(常规方式插入)"
lua_pushnumber(L, 2);
lua_pushstring(L, "江澎涌!(常规方式插入)");
lua_settable(L, -3);// --------- 从数组中获取值方式 ---------
// 第一种
// 获取 table 中索引为 1 的值压入栈
printf("lua_geti(L, -1, 1) 类型:%d\n", lua_geti(L, -1, 1));
printf("table[1] = %s\n", lua_tostring(L, -1));
// 将获取的值弹出
lua_pop(L, 1);// 第二种
// 压入 key 值
lua_pushnumber(L, 2);
// 获取 table 中索引为 2 的值压入栈
printf("lua_gettable(L, -2) 类型:%d\n", lua_gettable(L, -2));
printf("table[2] = %s\n", lua_tostring(L, -1));
// 将获取的值弹出
lua_pop(L, 1);long long n = luaL_len(L, 1);
printf("lua table length: %lld\n", n);lua_close(L);

输出以下内容:

lua_geti(L, -1, 1) 类型:4
table[1] = 江澎涌!(快捷方式插入)
lua_gettable(L, -2) 类型:4
table[2] = 江澎涌!(常规方式插入)
lua table length: 2

4、luaL_len

LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx);

描述:

用于获取给定索引 idx 位置的 Lua 值的长度(length),可以用于获取字符串、表和用户数据等类型的长度。

参数:

  • L: Lua 状态机(Lua state)的指针。
  • idx: 要获取长度的值在栈中的索引位置。

返回值:

会对该索引位置的元素执行长度运算符,然后将其长度值(整数)返回。

但由于元方法的存在,该运算符有可能会返回任意类型的值,所以如果不是整型,则会发生错误。

二、字符串操作

1、字符串普通操作的 C-API

在 C/C++ 和 Lua 的交互中,字符串的使用也是比较频繁的,但由于内存回收的问题,使用过程中需要以下几点:

  1. 当 C/C++ 接收到一个 Lua 字符串为参数时,需要遵循在使用该字符串期间不能从栈中将其弹出,而且不修改字符串。
  2. 当需要将 C/C++ 的字符串传递给 Lua 时,Lua 提供了很多关于字符串的 C-API 给我们使用,Lua 内部会进行相应的内存分配和管理。

Lua 提供操作字符串的 C-API

C API 函数描述
const char *(lua_pushstring) (lua_State *L, const char *s);将 字符串(以 “\0” 结尾) 压栈。
const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len);将 字符串(会结合 “\0” 和长度的参数决定字符串的长度) 压栈。
const char *(lua_pushvfstring) (lua_State *L, const char *fmt,va_list argp);将 字符串(格式化字符串,接收可变参数) 压栈。
const char *(lua_pushfstring) (lua_State *L, const char *fmt, …);将 字符串(格式化字符串) 压栈。
void (lua_concat) (lua_State *L, int n);会将栈顶的 n 个值,弹出,然后进行连接,最后将结果压入栈中。

在之前分享的 《C++ 与 Lua 数据交互载体——栈》 中,已经展示了如何使用这些 API ,这里就不再一一罗列如何使用。

lua_pushfstring 可以接受以下的指示符

指示符描述
%s插入一个以 \0 结尾的字符串
%d插入一个 int
%f插入一个 Lua 语言的浮点数
%p插入一个浮点数
%I插入一个 Lua 语言的整型数
%c插入一个以 int 表示的单字节字符
%U插入一个以 int 表示的 UTF-8 字节序列
%%插入一个百分号

lua_pushfstringlua_concat 都可以简便的将多个字符串进行连接,但是如果如果字符串数量较大时,效率会比较低,可以考虑使用缓冲机制(下面的小节会进行分享)。

2、举个例子——字符串截取

这个例子的功能:用 C++ 函数实现一个分割字符串的功能,给到 Lua 进行调用

第一步,定义分割字符串的 C++ 函数。

  1. 接收和检查 “需要被分割的字符串” 和 “分割符” 。
  2. 创建一个 table ,并且压入到栈中,后续的分割结果会压入到该 table 中。
  3. 使用 C-API strchr 对字符串进行检索被分割的位置,然后使用 lua_pushlstring 进行对字符串裁剪后压栈。
  4. 使用 lua_rawseti 进行设置到 “第二步创建的 table ” 中。
  5. 循环结束后,将剩余的字符串也设置到 table 中。
  6. 返回 1 ,表示只有一个返回值。
int lua_split(lua_State *L) {// 被分割的内容const char *s = luaL_checkstring(L, 1);// 分割符const char *sep = luaL_checkstring(L, 2);// 创建一个 table ,并且压入到栈中,后续的分割结果会压入到该 table 中lua_newtable(L);const char *e;int i = 1;// char *strchr(const char *str, int c) 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置// 该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULLwhile ((e = strchr(s, *sep)) != nullptr) {// 将 s 中 ( e - s ) 个字符压入栈顶lua_pushlstring(L, s, e - s);// 设置到 -2 ( table )中lua_rawseti(L, -2, i++);s = e + 1;}// 把最后的字符也压入lua_pushstring(L, s);lua_rawseti(L, -2, i);return 1;
}

第二步,编写 Lua 脚本。

Lua 脚本很简单,只是调用一下第三步注入的 C 函数(函数内容为第一步编写的函数),然后打印 table 长度和内容

local result = split("江_澎_涌", "_");print("length", #result);for i, v in ipairs(result) doprint(i, " --- ", v)
end

第三步,将函数注入到 Lua 中,并运行 Lua 的脚本。

lua_State *L = luaL_newstate();
luaL_openlibs(L);lua_pushcfunction(L, lua_split);
lua_setglobal(L, "split");std::string fname = PROJECT_PATH + "/8、编写C函数技巧/字符串便捷操作/字符串截取.lua";
if (luaL_loadfile(L, fname.c_str()) || lua_pcall(L, 0, 0, 0)) {printf("can't run config. file: %s\n", lua_tostring(L, -1));
}lua_close(L);

最后输出的内容如下

length	3
1	 --- 	江
2	 --- 	澎
3	 --- 	涌

3、lua_concat 的使用

lua_concat 会将栈顶的 n 个值,弹出,然后进行连接,最后将结果压入栈中。

举个例子,我们压入 3 个字符串,然后使用 lua_concat 进行弹出拼接并压入栈中,最后将这个结果打印。

lua_State *L = luaL_newstate();// 压入三个字符串
lua_pushstring(L, "江!");
lua_pushstring(L, "澎!");
lua_pushstring(L, "涌!");
// 将三个字符串进行弹出并拼接
lua_concat(L, 3);printf("stack top: %d\n", lua_gettop(L));
printf("concat: %s\n", lua_tostring(L, -1));lua_close(L);

最后输出为

stack top: 1
concat: 江!澎!涌!

4、缓冲机制

如果操作的字符串数量较大,可以考虑使用缓冲机制。

4-1、已知长度的缓冲区

对于长度已知的情况,一般分为以下几个步骤:

  1. 声明一个 luaL_Buffer 类型的变量。
  2. 使用 luaL_buffinitsize 进行初始化,获取一个指向指定大小缓冲区的指针,后续就可以自由地使用该缓冲区来创建字符串。
  3. 最后调用 luaL_pushresultsize 将缓冲区中的内容转换为一个新的 Lua 字符串,并将该字符串压栈。
int str_upper(lua_State *L) {size_t l;// 检测参数是否为字符串,并且获取长度const char *s = luaL_checklstring(L, 1, &l);size_t i;// 声明缓冲区luaL_Buffer b;// 初始化缓冲区char *p = luaL_buffinitsize(L, &b, l);for (i = 0; i < l; i++) {p[i] = toupper(s[i]);}// 将缓冲区中的内容转换为一个新的 Lua 字符串,并将字符串压栈luaL_pushresultsize(&b, l);
}

调用的代码如下,这里为了方便,就省去了从 Lua 进行调用,直接通过 C 压入字符串,给到上述的函数进行使用。

lua_State *L = luaL_newstate();lua_pushstring(L, "jiang peng yong");
str_upper(L);
printf("to upper: %s\n", lua_tostring(L, -1));lua_close(L);

输出的内容如下

to upper: JIANG PENG YONG

str_upper 其实是 Lua 中 lstrlib.c 的源代码,感兴趣可以自行查阅。

luaL_pushresultsize 的调用并未传入 lua_State 类型的参数,是因为初始化之后,缓冲区保存了对 lua_State 状态的引用。

4-2、未知长度的缓冲区

如果不知道最终需要多大的缓冲区,可以通过逐步增加内容的方式来使用缓冲区。Lua 为此提供了以下 C-API

函数描述
void (luaL_buffinit) (lua_State *L, luaL_Buffer *B);初始化缓冲区,但不设置大小
char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz);为缓冲区分配内存空间,以容纳指定大小的数据。sz 即预分配的大小(以字节为单位),返回指向缓冲区的指针
void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);增加一个长度明确的字符串
void (luaL_addstring) (luaL_Buffer *B, const char *s);用于增加一个以 \0 结尾的字符串
void (luaL_addvalue) (luaL_Buffer *B);用于将栈顶的字符串放入缓冲区
void (luaL_pushresult) (luaL_Buffer *B);刷新缓冲区并在栈顶留下最终的结果字符串
luaL_addchar(B,c)是一个宏定义,用于增加单个字符

举个例子

实现一个简易的将 table 内容连接为字符串功能。

第一步,实现拼接的 C++ 函数。

  1. 检测第一个参数是否为 table ,不是则会抛异常到 Lua 中
  2. 获取 table 的长度
  3. 初始化缓冲区,不进行大小的设置
  4. 循环 table ,将内容添加到缓冲区中
  5. 将缓冲区内容组装为字符串后压入栈,返回 Lua
int bufferConcat(lua_State *L) {luaL_Buffer b;long long i, n;// 检测是否为 tableluaL_checktype(L, 1, LUA_TTABLE);// 获取 table 长度n = luaL_len(L, 1);// 初始化缓冲区luaL_buffinit(L, &b);// 循环取出 table 的值,添加到缓冲区中for (i = 1; i <= n; i++) {lua_geti(L, 1, i);luaL_addvalue(&b);}// 将缓冲区内容组装为字符串,压入栈luaL_pushresult(&b);return 1;
}

第二步,实现 Lua 脚本内容。

Lua 脚本中的内容如下,只是简单的调用 C++ 暴露的函数,然后将其打印。

print('concat({"江", "澎", "涌", 29}', concat({"江", "澎", "涌", 29}))

第三步,注入第一步函数到 Lua 中,然后运行第二步的脚本。

lua_State *L = luaL_newstate();
luaL_openlibs(L);lua_pushcfunction(L, bufferConcat);
lua_setglobal(L, "concat");std::string fname = PROJECT_PATH + "/8、编写C函数技巧/字符串便捷操作/字符串连接(缓冲区).lua";
if (luaL_loadfile(L, fname.c_str()) || lua_pcall(L, 0, 0, 0)) {printf("can't run config. file: %s\n", lua_tostring(L, -1));
}lua_close(L);

输出的内容如下

concat({"江", "澎", "涌", 29}	江澎涌29

三、写在最后

Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。

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

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

相关文章

Z-IETD-FMK;caspase-8 抑制剂 210344-98-2星戈瑞

Z-IETD-FMK是一种caspase-8抑制剂。它通过与caspase-8的活性位点结合&#xff0c;阻断其切割关键蛋白质&#xff0c;进而抑制细胞凋亡过程。该抑制剂具有高选择性、高活性、低毒性等优点。 Z-IETD-FMK通过与caspase-8的半胱氨酸残基形成共价键&#xff0c;从而抑制caspase-8的活…

谷歌发布Gemini 1.0,开启生成式AI模型新时代!

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; IT杂谈 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. Gemini的发布前期1.1 Gemini的准备1.2 DeepMnid 二. Gemini的三大杀手锏2.1 多模态能力2…

AWS RDS慢日志文件另存到ES并且每天发送邮件统计慢日志

1.背景&#xff1a;需要对aws rds慢日志文件归档到es&#xff0c;让开发能够随时查看。 2.需求&#xff1a;并且每天把最新的慢日志&#xff0c;过滤最慢的5条sql 发送给各个产品线的开发负责人。 3.准备&#xff1a; aws ak/sk &#xff0c;如果rds 在不同区域需要认证不同的…

Apache RocketMQ 5.0 腾讯云落地实践

Apache RocketMQ 发展历程回顾 RocketMQ 最早诞生于淘宝的在线电商交易场景&#xff0c;经过了历年双十一大促流量洪峰的打磨&#xff0c;2016年捐献给 Apache 社区&#xff0c;成为 Apache 社区的顶级项目&#xff0c;并在国内外电商&#xff0c;金融&#xff0c;互联网等各行…

GBASE南大通用 ADO.NET 中的事务

GBASE南大通用 ADO.NET 中支持事务&#xff0c;可以使用GBASE南大通用Connection 对象的BeginTransaction 函数开始一个事务&#xff0c;并默认使用 ReadCommitted 模式初始化。 事务中可以对单个表执行多个操作&#xff0c;或者对多个表执行多个操作&#xff0c;在事务未提交…

用vue3封装自用的echarts组件

封装的组件 目录 封装的组件在项目中使用 BaseChart.vue <script setup>import {ref,onMounted,onBeforeUnmount,watch,markRaw} from vue;import {debounce} from "/utils"; //节流函数 import * as echarts from "echarts";const emit defineEmit…

TSINGSEE青犀边缘AI计算基于车辆结构化数据的车辆监控方案

随着人工智能技术的不断发展&#xff0c;边缘AI技术逐渐成为智能交通领域的研究热点。其中&#xff0c;基于边缘AI的车辆结构化数据技术与车辆监控系统是实现智能交通系统的重要手段之一。为了满足市场需求&#xff0c;TSINGSEE青犀边缘AI智能分析网关/视频智能分析平台推出了一…

windows安装库报错

报错信息 ERROR: Command errored out with exit status 1: ‘D:\test\venv\Scripts\python.exe’ -u -c ‘import io, os, sys, setuptools, tokenize; sys.argv[0] ‘"’"‘C:\Users\aaa\AppData\Local\Temp\pip-install-j oni55ju\xxx_350c8d1094f749eb97d8f049…

《代码随想录》--二叉树(一)

《代码随想录》--二叉树 第一部分 1、二叉树的递归遍历2、二叉树的迭代遍历3、统一风格的迭代遍历代码4、二叉树的层序遍历226.翻转二叉树 1、二叉树的递归遍历 前序遍历 中序遍历 后序遍历 代码 前序遍历 class Solution {public List<Integer> preorderTraversal(T…

阿里云国际版CDN查询实时带宽步骤

调用DescribeDomainRealTimeBpsData查询加速域名的带宽数据。 接口说明 单用户调用频率&#xff1a;100次/秒。如果您不指定StartTime和EndTime&#xff0c;该接口默认返回过去1小时的数据&#xff1b;指定StartTime和EndTime&#xff0c;该接口返回指定时间段的数据。 返回…

opencv 入门二(播放视频)

环境配置如下&#xff1a; opencv 入门一&#xff08;显示一张图片&#xff09;-CSDN博客 用OpenCV播放视频就像显示图像一样简单。唯一不同的是&#xff0c;我们需要某种循环来读取视频序列中的每一帧。 源码如下&#xff1a; #include <iostream> #include <str…

实时时钟(RTC)的选择与设计:内置晶体与外置晶体的优缺点对比

实时时钟(RTC)作为一种具备独立计时和事件记录功能的设备&#xff0c;现已广泛应用于许多电子产品中&#xff0c;并对时钟的精度要求越来越高。根据封装尺寸、接口方式、附加功能、时钟精度和待机功耗等因素进行分类&#xff0c;市场上有各种种类的RTC产品可供选择。 而在设计…

epi 外延炉 简介

因半导体制造工艺复杂&#xff0c;各个环节需要的设备也不同&#xff0c;从流程工序分类来看&#xff0c;半导体设备主要可分为晶圆制造设备&#xff08;前道工序&#xff09;、封装测试设备&#xff08;后道工序&#xff09;等。 本文介绍影响着晶体管性能和可靠性的外延炉。 …

C#调用阿里云接口实现动态域名解析,支持IPv6(Windows系统下载可用)

电信宽带一般能申请到公网IP&#xff0c;但是是动态的&#xff0c;基本上每天都要变&#xff0c;所以想到做一个定时任务&#xff0c;随系统启动&#xff0c;网上看了不少博文很多都支持IPv4&#xff0c;自己动手写了一个。 &#xff08;私信可全程指导&#xff09; 部署步骤…

衡量芯片运算能力的指标

MACCs MACCs&#xff08;Multiply-accumulate operations&#xff09;表示乘加运算&#xff1a;b乘c加a为一次MACC指令&#xff0c;两次OP。 乘加运算是模型运算里的基本单元&#xff0c;矩阵的运算基本都是乘加。 TOPS TOPS&#xff08;Tera Operation Per Second&#xf…

QT isEnable、isSelected、setEnabled 、 setClickable

isEnable&#xff1a;是否启用部件的键盘和鼠标事件 isSelected&#xff1a;判断某个元素是否被选中 setEnabled 和setClickable参考&#xff1a; qt -- setEnabled() 、 setClickable()_qt setenabled-CSDN博客 void SwitchButton::mousePressEvent(QMouseEvent *event) {…

Vue 使用 js-audio-recorder 实现录制、播放、下载音频

Vue 使用 js-audio-recorder 实现录制、播放、下载 PCM 数据 Vue 使用 js-audio-recorder 实现录制、播放、下载 PCM 数据js-audio-recorder 简介Vue 项目创建下载相关依赖主界面设计设置路由组件及页面设计项目启动源码下载 Vue 使用 js-audio-recorder 实现录制、播放、下载 …

FPGA时序分析与时序约束(二)——时钟约束

目录 一、时序约束的步骤 二、时序网表和路径 2.1 时序网表 2.2 时序路径 三、时序约束的方式 三、时钟约束 3.1 主时钟约束 3.2 虚拟时钟约束 3.3 衍生时钟约束 3.4 时钟组约束 3.5 时钟特性约束 3.6 时钟延时约束 一、时序约束的步骤 上一章了解了时序分析和约束…

IDEA shorten command line介绍和JAR manifest 导致mybatis找不到接口类处理

如果类路径太长&#xff0c;或者有许多VM参数&#xff0c;程序就无法启动。原因是大多数操作系统都有命令行长度限制。在这种情况下&#xff0c;IntelliJIDEA将试图缩短类路径。最好选中 classpath file模式。 shorten command line 选项提供三种选项缩短类路径。 none&#x…

破局:国内母婴市场“红利减退”,母婴店如何拓客引流裂变?

破局&#xff1a;国内母婴市场“红利减退”&#xff0c;母婴店如何拓客引流裂变&#xff1f; 背景&#xff1a;中国母婴市场近年来人口出生率一直在恒定范围值&#xff0c;国家也在鼓励优生、多生政策&#xff0c;并且随着互联网的高速发展&#xff0c;人均可支配收入也在增加&…