当前位置: 首页 > news >正文

【Lua之·Lua与C/C++交互·Lua CAPI访问栈操作】

系列文章目录


文章目录

  • 前言
  • 一、概述
    • 1.1 Lua堆栈
  • 二、栈操作
    • 2.1 基本的栈操作
    • 2.2 入栈操作函数
    • 2.3 出栈操作函数
    • 2.4 既入栈又出栈的操作函数
    • 2.5 栈检查与类型转换函数
    • 2.5 获取表数据
  • 三、实例演示
  • 总结


前言

  Lua是一种轻量级的、高性能的脚本语言,经常被用于游戏开发、嵌入式系统和其他需要灵活、可扩展的脚本功能的应用程序中。


一、概述

  • Lua是一种解释型语言,它具有简单的语法和动态类型系统,使得它易于学习和使用。它的设计目标是成为一种嵌入式脚本语言,在C/C++程序中作为库使用。这使得Lua非常适用于游戏开发,因为它可以与其他编程语言结合使用,提供灵活的脚本功能。

  • Lua的另一个重要特点是它的高性能。与其他动态语言相比,Lua的执行速度非常快,这得益于它的精简设计和高度优化的虚拟机。这使得Lua在游戏中处理大量的数据和逻辑时表现突出。

  • 除了高性能外,Lua还具有一些其他的优势。首先,它具有简洁而灵活的语法,使得编写Lua代码变得非常简单和直观。其次,Lua提供了强大的元表和闭包功能,使得它可以实现面向对象编程和函数式编程的高级特性。最后,Lua还具有可扩展性,可以通过编写C/C++扩展模块来增强其功能。

1.1 Lua堆栈

在这里插入图片描述

要理解Lua和C++交互,首先要理解Lua堆栈。

  • Lua和C/C++语言通信的主要方法是一个无处不在的虚拟栈。栈的特点是先进后出

  • 在Lua中,Lua堆栈就是一个struct,堆栈索引的方式可是是正数也可以是负数,区别是:正数索引1永远表示栈底,负数索引-1永远表示栈顶。

规则:

①若Lua虚拟机堆栈里有N个元素,则可以用 1 ~ N 从栈底向上索引,也可以用 -1 ~ -N 从栈顶向下索引,一般后者更加常用
②堆栈的每个元素可以为任意复杂的Lua数据类型,堆栈中没有元素的空位,隐含为包含一个“空”类型数据

特性:
若有4个元素分别入栈,则:

①. 正数索引,栈底是1,然后一直到栈顶是逐渐+1,最后变成4(4大于1)
②. 负数索引,栈底是-4,然后一直到栈顶是逐渐+1,最后变成-1(-1大于-4)

索引相关:

①. 正数索引,不需要知道栈的大小,我们就能知道栈底在哪,栈底的索引永远是1
②. 负数索引,不需要知道栈的大小,我们就能知道栈顶在哪,栈顶的索引永远是-1

二、栈操作

2.1 基本的栈操作

函数描述
lua_push*将一个值压入栈顶。例如,lua_pushnil、lua_pushboolean、lua_pushnumber等。
lua_gettop获取栈顶的索引,也就是栈中元素的数量。
lua_settop设置新的栈顶索引,可以-push、pop或者修改栈中的元素数量。
lua_remove移除指定的元素。
lua_insert将栈顶元素插入到指定位置。
lua_pop弹出指定数量的元素。
lua_settop(L, index)将栈顶设置为特定索引,这样栈上索引为index的位置就是当前的“虚拟”栈顶。
lua_to***函数获取值。
lua_set***函数设置值。

2.2 入栈操作函数

这些函数会将值压入 Lua 栈中,通常是执行某些操作后产生的结果或初始化时需要的值。

函数描述
lua_pushnil(L)将 nil 值压入栈中。
lua_pushnumber(L, n)将一个数字 n 压入栈中。
lua_pushinteger(L, n)将一个整数 n 压入栈中。
lua_pushlstring(L, s, len)将一个长度为 len 的字符串 s 压入栈中。
lua_pushstring(L, s)将一个字符串 s 压入栈中(等同于 lua_pushlstring,len 会自动计算)。
lua_pushcclosure(L, f, n)将 C 函数 f 与 n 个 Lua 环境变量封装成闭包,压入栈中。
lua_pushboolean(L, b)将布尔值 b(0 或 1)压入栈中。
lua_pushlightuserdata(L, p)将一个指针 p 压入栈中(通常用于存储非 Lua 数据)。
lua_pushthread(L)将当前线程(协程)压入栈中。
lua_newtable(L)创建一个新的表并将其压入栈中。
lua_createtable(L, narr, nrec)创建一个新的表并将其压入栈中,narr 和 nrec 为初始数组和哈希部分的容量。
lua_newuserdata(L, size)创建一个新的用户数据块,并将其指针压入栈中。
luaL_newmetatable(L, tname)创建一个新的元表并将其压入栈中。
lua_getglobal(L, name)获取全局变量 name 的值并将其压入栈中。
lua_gettable(L, idx)将表 idx 中与栈顶值相对应的值压入栈中。
lua_getfield(L, idx, k)获取表 idx 中字段 k 的值并将其压入栈中。
lua_rawget(L, idx)从表 idx 中获取原始值并压入栈中(不调用元方法)。
lua_rawgeti(L, idx, n)从表 idx 中按整数索引 n 获取值并压入栈中。
lua_rawgetp(L, idx, p)从表 idx 中按指针索引 p 获取值并压入栈中。
luaL_loadfile(L, filename)加载 Lua 文件并将其作为函数压入栈中。
luaL_loadstring(L, s)加载 Lua 字符串并将其作为函数压入栈中。
luaL_loadbuffer(L, buff, size, name)加载一个缓冲区并将其作为 Lua 函数压入栈中。
luaL_newstate()创建一个新的 Lua 状态,并为其分配一个新的栈。

2.3 出栈操作函数

这些函数会将栈中的值弹出,以进行操作或清理栈。

函数描述
lua_pop(L, n)从栈中弹出 n 个元素。
lua_remove(L, idx)删除索引为 idx 处的值,并将栈中其上方的元素向下移动。
lua_replace(L, idx)将栈顶的值替换为索引 idx 处的值,栈顶元素被弹出。
lua_setglobal(L, name)将栈顶的值设置为全局变量 name 的值,并从栈中弹出。
lua_settable(L, idx)将栈顶的值设置为 idx 处表中的一个字段,并弹出键和值。
lua_setfield(L, idx, k)将栈顶的值设置为 idx 处表中的字段 k 的值,并弹出键和值。
lua_rawset(L, idx)将栈顶的值设置为 idx 处表中的一个字段(原始设置,不调用元方法)。
lua_rawseti(L, idx, n)将栈顶的值设置为 idx 处表中的键为整数 n 的字段。
lua_rawsetp(L, idx, p)将栈顶的值设置为 idx 处表中的键为指针 p 的字段。
lua_setmetatable(L, idx)将栈顶的值设置为 idx 处值的元表,并从栈中弹出元表。
lua_setuservalue(L, idx)将栈顶的值设置为 idx 处用户数据的用户值,并从栈中弹出值。
luaL_unref(L, idx, ref)释放引用 ref 指向的值,并将其从栈中弹出。

2.4 既入栈又出栈的操作函数

这些函数同时进行入栈和出栈操作。

函数描述
lua_getmetatable(L, idx)获取索引 idx 处值的元表(如果存在),并将其压入栈中;如果没有元表,栈不变。
lua_next(L, idx)获取索引 idx 处表的下一个键值对,并将键和值压入栈中,弹出上一个键。
lua_concat(L, n)将栈顶的 n 个字符串拼接起来,结果压入栈中,弹出原始字符串。
lua_call(L, nargs, nresults)调用栈顶的函数并弹出 nargs 个参数,返回 nresults 个结果压入栈中。
lua_pcall(L, nargs, nresults, errfunc)与 lua_call 类似,但支持错误处理,调用失败时错误信息压入栈中。
luaL_callmeta(L, idx, e)调用元表中字段为 e 的元方法,将栈顶的值作为参数传递,并将返回值压入栈中。

2.5 栈检查与类型转换函数

这些函数用于检查栈中的元素类型或将栈中的元素转换为特定类型。

函数描述
lua_isnil(L, idx)检查索引 idx 处的值是否为 nil。
lua_isboolean(L, idx)检查索引 idx 处的值是否为布尔值。
lua_isnumber(L, idx)检查索引 idx 处的值是否为数字。
lua_isstring(L, idx)检查索引 idx 处的值是否为字符串。
lua_istable(L, idx)检查索引 idx 处的值是否为表。
lua_isfunction(L, idx)检查索引 idx 处的值是否为函数。
lua_tonumber(L, idx)将栈中 idx 处的值转换为数字,如果不行返回 0。
lua_tointeger(L, idx)将栈中 idx 处的值转换为整数,如果不行返回 0。
lua_toboolean(L, idx)将栈中 idx 处的值转换为布尔值。
lua_tostring(L, idx)将栈中 idx 处的值转换为字符串。
lua_topointer(L, idx)将栈中 idx 处的值转换为指针。
lua_type(L, idx)获取栈中 idx 处的值的类型。

2.5 获取表数据

函数描述
lua_getfield(L, t_idx, key)从 Lua 栈上的一个表中,根据字段名(key)获取对应的值,并将这个值压入 Lua 栈顶

注意: Lua 可以高效地进行数据传递、类型转换和函数调用等操作。在实际编程中,开发者需要注意栈的管理,确保栈操作符合预期,以避免栈溢出或数据丢失等问题。

三、实例演示

示例1:

lua_State *L = luaL_newstate();
luaL_openlibs(L);
// 将值推入栈中
lua_pushnumber(L, 123);
lua_pushstring(L, "Hello, World");
// 设置栈顶索引为1,即栈中第二个元素
lua_settop(L, 1);
qDebug() << "栈顶索引" << lua_gettop(L);
qDebug() << "1" << lua_tostring(L,1);
qDebug() << "2" << lua_tostring(L,2);
qDebug() << "-2" << lua_tostring(L,-2);
qDebug() << "-1" << lua_tostring(L,-1);qDebug() << "消费堆栈----------start";
lua_pop(L,1);
qDebug() << (int)lua_tointeger(L, 1);
qDebug() << lua_tostring(L, 0);
qDebug() << "消费堆栈----------end";// 获取并打印第二个元素
if (lua_isnumber(L, 1)) {printf("The second element is a number: %d\n", (int)lua_tointeger(L, 1));qDebug() << (int)lua_tointeger(L, 1);qDebug() << lua_tostring(L, 0);
}
// 替换第二个元素为新的字符串
lua_pushstring(L, "New string");
lua_replace(L, 1);
// 再次设置栈顶
lua_settop(L, 1);
// 获取并打印修改后的第二个元素
if (lua_isstring(L, 1)) {printf("The second element is now a string: %s\n", lua_tostring(L, 1));
}
// 清理Lua状态机
lua_close(L);

运行结果:
在这里插入图片描述

示例2:

lua_State *L = luaL_newstate();
luaL_openlibs(L);
// Push values onto the stack
lua_pushstring(L, "Hello");
lua_pushnumber(L, 123);
// Get the stack top index
int stack_top = lua_gettop(L);
printf("Stack top index: %d\n", stack_top);
// Set the stack top to a specific index
lua_settop(L, 5); // Pushes nil values to make the stack size 5
qDebug() << "栈大小:" << lua_gettop(L);
qDebug() << lua_tostring(L,-1);
qDebug() << lua_tostring(L,-2);
qDebug() << lua_tostring(L,-3);
qDebug() << lua_tostring(L,-4);
qDebug() << lua_tostring(L,-5);
qDebug() << "---------------";
// Remove an element
lua_remove(L, -1); // Removes the second element from the top (-2 because of the nil values)
qDebug() << "栈大小:" << lua_gettop(L);
qDebug() << lua_tostring(L,-1);
qDebug() << lua_tostring(L,-2);
qDebug() << lua_tostring(L,-3);
qDebug() << lua_tostring(L,-4);
qDebug() << lua_tostring(L,-5);
qDebug() << "---------------";
// Insert an element
lua_pushstring(L, "World");
lua_insert(L, -2); // Inserts the string "World" before the top element
qDebug() << "栈大小:" << lua_gettop(L);
qDebug() << lua_tostring(L,-1);
qDebug() << lua_tostring(L,-2);
qDebug() << lua_tostring(L,-3);
qDebug() << lua_tostring(L,-4);
qDebug() << lua_tostring(L,-5);
qDebug() << "---------------";
//Replace an element
lua_pushstring(L, "New string");
lua_replace(L, -3); // Replace the string "World" before the top element
qDebug() << lua_tostring(L,-1);
qDebug() << lua_tostring(L,-2);
qDebug() << lua_tostring(L,-3);
qDebug() << lua_tostring(L,-4);
qDebug() << lua_tostring(L,-5);
qDebug() << "---------------";
// Pop elements
lua_pop(L, 2); // Pops the top two elements
qDebug() << "栈大小:" << lua_gettop(L);
qDebug() << lua_tostring(L,-1);
qDebug() << lua_tostring(L,-2);
qDebug() << lua_tostring(L,-3);
qDebug() << lua_tostring(L,-4);
qDebug() << lua_tostring(L,-5);
qDebug() << "---------------";
lua_close(L);

运行结果:
在这里插入图片描述

示例3:


运行结果:


总结

  Lua是一种轻量级、高性能的脚本语言,适用于游戏开发、嵌入式系统和其他需要灵活、可扩展的脚本功能的应用程序。

相关文章:

【Lua之·Lua与C/C++交互·Lua CAPI访问栈操作】

系列文章目录 文章目录 前言一、概述1.1 Lua堆栈 二、栈操作2.1 基本的栈操作2.2 入栈操作函数2.3 出栈操作函数2.4 既入栈又出栈的操作函数2.5 栈检查与类型转换函数2.5 获取表数据 三、实例演示总结 前言 Lua是一种轻量级的、高性能的脚本语言&#xff0c;经常被用于游戏开发…...

LabVIEW实现LoRa通信

目录 1、LoRa通信原理 2、硬件环境部署 3、程序架构 4、前面板设计 5、程序框图设计 6、测试验证 本专栏以LabVIEW为开发平台,讲解物联网通信组网原理与开发方法,覆盖RS232、TCP、MQTT、蓝牙、Wi-Fi、NB-IoT等协议。 结合实际案例,展示如何利用LabVIEW和常用模块实现物联网系…...

【数字化】华为数字化转型架构蓝图-2

目录 1、客户联结的架构思路 1.1 ROADS体验设计 1.2 具体应用场景 1.3 统一的数据底座 1.4 案例与成效 2、一线作战平台的架构思路 2.1 核心要素 2.2 关键功能 2.3 实施路径 2.4 案例与成效 3、能力数字化的架构思路 3.1 能力数字化的核心目标 3.2 能力数字化的实…...

【Agent】AutoGen Studio2.0开源框架-UI层环境安装+详细操作教程(从0到1带跑通智能体AutoGen Studio)

&#x1f4a5; 欢迎来到我的博客&#xff01;很高兴能在这里与您相遇&#xff01; 首页&#xff1a;GPT-千鑫 – 热爱AI、热爱Python的天选打工人&#xff0c;活到老学到老&#xff01;&#xff01;&#xff01;导航 - 人工智能系列&#xff1a;包含 OpenAI API Key教程, 50个…...

Linux 网络配置基础

文章目录 1. 前言2. Linux 的网络配置2.1 传统的网络配置方法2.2 新的网络配置方法2.3 用 DHCP 客户端管理网络 3. 参考资料 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. Linux 的网络配置 …...

科技创新 数智未来|清科·沙丘投研院走进竹云

12月20日&#xff0c;清科沙丘投研院带领企投家团队走进竹云交流分享&#xff0c;聚焦技术创新、企业数字化管理、行业前沿应用案例等热点议题&#xff0c;深入探讨数字技术如何点燃企业高质量发展的澎湃动力&#xff0c;共话企业数字化、智能化发展之道。 达晨财智股权管理部…...

Java 常见面试算法题汇总与解析

Java 常见面试算法题汇总与解析 算法题是程序员面试中常见的一部分&#xff0c;也是提升编程能力的核心手段。本文将汇总一些 Java 中常见的算法题&#xff0c;并提供详细的解析和实现代码&#xff0c;帮助开发者更好地理解和掌握算法。 一、字符串相关算法 1.1 字符串反转 …...

【社区投稿】自动特征auto trait的扩散规则

自动特征auto trait的扩散规则 公式化地概括&#xff0c;auto trait marker trait derived trait。其中&#xff0c;等号右侧的marker与derived是在Rustonomicon书中的引入的概念&#xff0c;鲜见于Rust References。所以&#xff0c;若略感生僻&#xff0c;不奇怪。 marker …...

云原生相关的 Go 语言工程师技术路线(含博客网址导航)

要成为一名云原生相关的 Go 语言工程师&#xff0c;需要在 Go 语言、云原生技术栈以及相关的开发和运维工具上建立扎实的基础。下面是一个前字节员工总结的技术路线规划&#xff1a; 1. 掌握 Go 语言基础 深入理解 Go 语言&#xff1a;你需要熟练掌握 Go 的语法、数据结构、并…...

mui框架开发的手机APP——众筹约课类【只有前端,无后端】

点击获取源码...

Python的内存管理

文章目录 1. **内存管理的基本原理**&#xff08;1&#xff09;动态内存分配&#xff08;2&#xff09;引用计数机制 2. **垃圾回收&#xff08;Garbage Collection, GC&#xff09;机制**&#xff08;1&#xff09;循环引用问题&#xff08;2&#xff09;垃圾回收器的作用 3. …...

VSCode调试

目录 C/C远程本地调试插件配置参考 C/C远程本地调试 测试源码&#xff1a;https://github.com/jrhee17/ssl-study 插件 Remote - SSH C/C 配置 .vscode/launch.json {"version": "0.2.0","configurations": [{"name": "afte…...

Direct Preference Optimization (DPO) 简介与流程解析:中英双语

Direct Preference Optimization (DPO) 简介与流程解析 Direct Preference Optimization (DPO) 是一种基于人类偏好的强化学习优化方法&#xff0c;用于训练语言模型&#xff0c;使其更好地满足用户需求或偏好。本文将详细介绍 DPO 的核心思想、优化流程&#xff0c;并结合代码…...

fisco-bcos手动搭建webase启动注意事项

手动搭建webase-front启动注意事项 Java环境变量&#xff1a;1.8.301时候的错误 一直提示节点连接不上&#xff0c;无法连接chanale端口 这是官方提供的解决办法Help wanted: solution for secp256k1 being disabled Issue #470 FISCO-BCOS/java-sdk Java SDK 2.x连接节点失败…...

ospf 的 状态机详解

OSPF&#xff08;开放最短路径优先&#xff0c;Open Shortest Path First&#xff09;协议的状态机是其核心部分之一&#xff0c;用于确保路由器之间的邻接关系&#xff08;neighbor relationship&#xff09;建立和路由信息的交换。OSPF的状态机模型由多个状态组成&#xff0c…...

TP5 动态渲染多个Layui表格并批量打印所有表格

记录&#xff1a; TP5 动态渲染多个Layui表格每个表格设置有2行表头&#xff0c;并且第一行表头在页面完成后动态渲染显示内容每个表格下面显示统计信息可点击字段排序一次打印页面上的所有表格打印页面上多个table时,让每个table单独一页 后端代码示例&#xff1a; /*** Nod…...

spring专题笔记(六):bean的自动装配(自动化注入)-根据名字进行自动装配、根据类型进行自动装配。代码演示,通俗易懂。

目录 一、根据名字进行自动装配--byName 二、根据类型进行自动装配 byType 本文章主要是介绍spring的自动装配机制&#xff0c; 用代码演示spring如何根据名字进行自动装配、如何根据类型进行自动装配。代码演示&#xff0c;通俗易懂。 一、根据名字进行自动装配--byName Us…...

监听器listener

文章目录 监听器( listener)对Application内置对象监听的语法和配置对session内置对象监听的语法和配置 监听器( listener) 对象与对象的关系&#xff1a; 继承关联 tomcat一启动创建的顺序&#xff1a;监听器&#xff0c;config&#xff0c;application(全局初始化参数)&am…...

重温设计模式--10、单例模式

文章目录 单例模式&#xff08;Singleton Pattern&#xff09;概述单例模式的实现方式及代码示例1. 饿汉式单例&#xff08;在程序启动时就创建实例&#xff09;2. 懒汉式单例&#xff08;在第一次使用时才创建实例&#xff09; 单例模式的注意事项应用场景 C代码懒汉模式-经典…...

Flutter动画学习二

如何在 Flutter 中使用自定义动画和剪裁&#xff08;clipping&#xff09;实现一个简单的动画效果。 前置知识点学习 AnimationController AnimationController 是 Flutter 动画框架中的一个核心类&#xff0c;用于控制动画的生命周期和状态。它提供了一种灵活的方式来定义动…...

【Python 教程15】-Python和Web

正则表达式&#xff1a;快准狠的“文本手术刀” 在 Python 的世界里&#xff0c;正则表达式&#xff08;Regular Expression&#xff0c;简称 Regex&#xff09;就像一把锋利的“手术刀”&#xff0c;能让你在杂乱无章的文本中&#xff0c;精准地切割、匹配、提取出你想要的部分…...

KingbaseES V8R6备份还原踩坑实录:sys_dump、sys_restore和ksql到底怎么选?

KingbaseES V8R6备份还原实战指南&#xff1a;工具选型与典型问题解析 第一次接触KingbaseES V8R6的备份还原工作时&#xff0c;面对sys_dump、sys_restore和ksql这三个工具&#xff0c;我像大多数新手一样陷入了选择困难。记得那次紧急数据迁移任务&#xff0c;当我信心满满地…...

东方电机RS485嵌入式协议库:多型号统一控制与工业可靠性设计

1. 项目概述OrientalCommon_asukiaaa 是一个专为东方电机&#xff08;Oriental Motor&#xff09;RS485通信设备设计的嵌入式通用接口库。该库不直接实现物理层驱动&#xff0c;而是聚焦于协议层抽象与控制逻辑封装&#xff0c;为上层应用提供统一、可移植、符合工业现场总线规…...

OpenAI收购科技脱口秀TBPN,力图塑造AI叙事话语权

OpenAI正通过收购备受硅谷内部人士关注的科技脱口秀TBPN进军媒体行业&#xff0c;该节目主持人周三宣布了这一消息。联合主持人约翰库根和乔迪海斯每个工作日从洛杉矶直播TBPN节目三小时&#xff0c;邀请的嘉宾包括创业者、风险投资家和科技界重要人物。此次交易的财务条款未予…...

嵌入式系统中命令模式的应用与优化

1. 嵌入式系统中的误操作救赎之道在嵌入式开发中&#xff0c;参数配置误操作就像厨房里的盐罐打翻——一瞬间的失误可能导致整锅菜报废。上周我就遇到一个真实案例&#xff1a;某工业设备因为工程师误触"恢复出厂设置"&#xff0c;导致产线上30台设备参数全部重置&am…...

OpenClaw高Token消耗解决方案:Qwen3-4B-Thinking本地化部署指南

OpenClaw高Token消耗解决方案&#xff1a;Qwen3-4B-Thinking本地化部署指南 1. 当OpenClaw遇上Token消耗困境 上周我尝试用OpenClaw自动整理半年的技术笔记时&#xff0c;遇到了一个棘手问题——任务执行到一半突然中断了。查看日志才发现&#xff0c;仅仅是"读取文件→…...

深入解析Cache机制:从原理到性能优化实战

1. 从理论到实战&#xff1a;Cache概念的职场觉醒第一次真正理解Cache的重要性&#xff0c;是在我接手硬件性能监控项目的那一刻。当时领导让我用perf工具监控处理器性能&#xff0c;输入perf list后满屏的cache-misses、cache-loads指标让我彻底懵了——这些在大学《计算机组成…...

Android学习资源与成长指南

Android学习资源与成长指南 概述 本文将Android开发者的成长路径、学习资源、开源项目、技术社区、推荐书籍和面试准备整合为一份完整指南&#xff0c;覆盖从入门到架构师的全阶段。一、学习路线图&#xff1a;从入门到架构师 1.1 第一阶段&#xff1a;初级开发&#xff08;0-6…...

中航迈特光束整形金属3D打印技术取得重要进展,多种材料已成功验证

中航迈特在金属3D打印装备研发方面持续发力&#xff0c;尤其是光束整形技术近期取得重要进展。在本届TCT亚洲展&#xff0c;它推出的MT280搭载了无级点环光斑能量智调系统&#xff0c;是光束整形金属3D打印当前较新的看点。据3D打印技术参考了解&#xff0c;无级点环光斑能量智…...

清明节海报设计指南:4个要点打造高级感视觉呈现

每到清明临近&#xff0c;总有人为海报设计发愁。想做一张既体面又有格调的清明节海报&#xff0c;打开设计软件却不知从何下手&#xff0c;勉强拼凑出来的效果又总觉得差点意思。要么太过花哨显得不够庄重&#xff0c;要么过于简陋显得敷衍。其实高级感并不难&#xff0c;关键…...