异常处理/CC++ 中 assert 断言 应用实践和注意事项

文章目录

  • 概述
  • assert 本质浅析
  • Release版本下的assert是否生效
    • 默认设置下 QtCreator环境 assert 过程
    • 默认配置下 VS环境 assert 过程
    • 配置VS发布模式下的断言生效
    • VS环境Release版本的UI程序
    • Release下请当我不生效
  • 请勿滥用assert
    • 导致逻辑错误
    • 再强调'不要在assert内执行逻辑功能'
    • 怎敢默认release下绝不会发生此错误?
    • 要不要在Release版本下使用断言
    • 使用assert的其他建议
  • 静态断言
  • 自定义断言
  • 小结

概述

本文主要讲解了 assert 断言机制,在编程中的作用和注意事项,如 assert 的工作原理、Release程序版本下的断言生效问题、为什么要杜绝在assert内执行逻辑、如何自定义断言等。断言机制是在开发和调试阶段快速发现程序中的错误和逻辑问题的重要手段,它可以帮助开发人员在程序中插入检查点,以验证程序的正确性和健壮性,一旦发现断言失败,开发人员可以通过查看错误消息和堆栈跟踪来定位和解决问题。

转载请标明原文链接,
https://blog.csdn.net/quguanxin/category_6223029.html

assert 本质浅析

标准C/C++库中的 assert 并不是一个函数,事实上,它是个宏,其用法像是一种"契约式编程",其表达的意思是,程序在我的假设条件下,能够正常良好的运作,否则就告警并调用 abort 终止程序。其使用场景大概是这样的,大多数情况下,我们要进行验证的假设,只是属于偶然性事件,又或者我们仅仅想测试一下,一些最坏情况是否发生。

int main() {
    printf("当前源文件名:%s, 当前代码行号:%s\n", FILE_NAME, __LINE__);
    assert(0 == 1);
    system("pause"); return 0;
}

在这里插入图片描述
通过上述测试,可以猜测,assert 内部极有可能是封装了 FILELINE 预处理宏的。看一下源码,

#ifdef NDEBUG
    #define assert(expression) ((void)0)
#else
    _ACRTIMP void __cdecl _wassert(
        _In_z_ wchar_t const* _Message,
        _In_z_ wchar_t const* _File,
        _In_   unsigned       _Line
        );

    #define assert(expression) (void)(                                                       \
            (!!(expression)) ||                                                              \
            (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
        )
#endif

与我们《》自定义函数的使用方法一致,先以 char* 和 int 为参数类型,定义函数,然后,使用宏封装此函数,这样,LINEFILE 就可以用来代表宏函数(assert) 的调用位置。

Release版本下的assert是否生效

验证下,assert断言在发布模式下是否起作用-/-
能有多灵验呢,能否在崩溃的时候,显示出来—
实际运行中,我在发布版里头没怎么体现出来呢?
最好的办法还是为自己的软件编写错误处理框架–

默认设置下 QtCreator环境 assert 过程

void Widget::on_pushButton_clicked()
{
    ui->label->setText("before assert");
    //assert(false);  //Line:22
    Q_ASSERT(false);  //Line:23
    ui->label->setText("after assert");
}

使用C库的assert测试
编译Debug版本和Release版本,脱离开发环境进行执行测试,结果是都可以准确的定位到断言所在的代码行。因此我们初步得出的结论是,在QtCreator开发环境下,默认的情况下,Release版本的程序中,C库中的assert是可以生效的。

使用Qt库的Q_ASSERT测试
使用Qt自带的Q_ASSERT断言宏时,经过测试,在Release版本的程序中并不生效。

#if !defined(Q_ASSERT)
#  if defined(QT_NO_DEBUG) && !defined(QT_FORCE_ASSERTS)
#    define Q_ASSERT(cond) static_cast<void>(false && (cond))
#  else
#    define Q_ASSERT(cond) ((cond) ? static_cast<void>(0) : qt_assert(#cond, __FILE__, __LINE__))
#  endif
#endif

/*
  The Q_ASSERT macro calls this function when the test fails.
*/
void qt_assert(const char *assertion, const char *file, int line) Q_DECL_NOTHROW
{
    QMessageLogger(file, line, nullptr).fatal("ASSERT: \"%s\" in file %s, line %d", assertion, file, line);
}

通过上述Qt框架下的宏定义,我们可以看出,如果没有定义QT_FORCE_ASSERTS强制使用断言,而且定义了QT_NO_DEBUG(估计它在Release编译模式下会有定义),Q_ASSERT相当于没有定义。

默认配置下 VS环境 assert 过程

纯 C++ 代码

#include <iostream>
#include <assert.h>

int main() {
    int i = 0;
    std::cout << "Hello World!\n";
    assert(i != 0);
    std::cout << "Hello weifang!\n";
    system("pause");
}

在Debug模式下运行,断言如下。在Release模式下,断言不生效。
在这里插入图片描述

配置VS发布模式下的断言生效

还是上一小节中的项目,我们查看其项目属性,C/C++,预处理器,预处理器定义-
Debug配置:
在这里插入图片描述
Release配置:
在这里插入图片描述
我们针对Release模式下的预处理器定义,删除其中的NDEBUG宏,重新运行Release版本,
在这里插入图片描述
如上,只要删除NDEBUG宏定义,则assert在Release模式下也是生效的

注意一个现象,使用NDEBUG宏和删除该宏的情况下,编译生成的可执行文件的大小和占用空间大小保持一致不变。即使是在Release模式下预编译器配置上增加_DEBUG宏,其效果也是与不设置NDEBUG宏一致,与Debug模式下配置_DEBUG宏是不一样的。

另外,在 C++ 中,assert 宏通常在调试模式下才会生效,而在发布模式下会被编译器忽略掉。如果你希望在发布模式下也启用 assert 断言,可以在 “预处理器定义” 字段中,删除NDEBUG宏,或进一步添加 _DEBUG宏定义。如此便确保 _DEBUG 宏在发布模式和调试模式下都被定义。要注意的是, assert 断言主要用于在开发和调试阶段发现问题。在发布版本中,最好使用其他方式进行错误检查和处理,如异常处理、返回错误码或输出错误日志等。

VS环境Release版本的UI程序

在脱离开发环境的情况下,运行上一节的控制台测试程序。Debug版本程序(配有_DEBUG)的运行会弹出提示。Release版本程序(删除NDEBUG宏)的运行,在控制台有断言提示,没有弹窗提示,且控制台在数秒后自动退出。本小节我们进一步看看,在没有NDEBUG的情况下,R版的UI程序会是什么执行现象。

在VS下新建Qt项目,D版和R版的默认预处理器定义一致为$(Qt_DEFINES_);%(PreprocessorDefinitions),这里我暂时没有在Qt_DEFINES_变量中看到_DEBUG或NDEBUG宏的身影。它一定藏在其他地方了,我们暂不深究。

TestAssertUI::TestAssertUI(QWidget *parent) : QWidget(parent) , ui(new Ui::TestAssertUIClass())
{
    ui->setupUi(this);
    //
    connect(ui->pushButton, &QPushButton::clicked, []{
        int i = 0;
        assert(i != 0);
        qDebug() << "The assertion is not in effect";
    });
}

编译并执行my_D版本的程序,
在这里插入图片描述
编译并执行my_R版本的程序,断言并未生效。那,我们在my_R预处理器定义中增加 _DEBUG宏定义,重新编译执行。结果,断言依然是被优化不执行的。

还好我眼尖,在预处理器定义项目的下面发现了"取消预处理器定义"这个配置项,我将NDEBUG配置进去。
在这里插入图片描述
重新编译的过程提示,cl : 命令行 warning D9025: 正在重写“/DNDEBUG”(用“/UNDEBUG”),这一看就是起作用的样子啊。编译后执行,果然,与上述DEBUG版程序的弹窗告警一致

重点总结,
在使用VS做集成开发环境时,如果想使得R版本程序中C库的assert生效,需要在项目属性配置,C/C++,预处理器,取消预处理器,这个配置项中配置取消 “NDEBUG宏”。(仅)在预处理定义中增加_DEBUG宏定义是无效的,这可能是因为NDEBUG宏等项目属性中隐含的定义,会在编译过程靠后的阶段进行加载和覆盖。
在VS开发环境下,针对Release配置。其中非Qt项目,其预处理器定义中会直接标明NDEBUG,若想打开assert调试功能,只需要删除即可。针对Qt项目,由于NDEBUG没有直接定义在预处理器配置项中,你需要在取消预处理定义这个配置项中添加NDEBUG宏。

Release下请当我不生效

为了保险起见,一种可行的做法是,认为任何IDE下的任何ASSERT语句,是不生效的,不执行的。

请勿滥用assert

虽然 assert 方便好用,但勿滥用!

导致逻辑错误

知道assert在release模式下"可能"不被执行,可还是手欠。为了图省事,伪代码如下:

//!!危险!!
assert(SomethingBegin());
//函数定义
bool SomethingBegin()
{
	if (s_bBuseFlage)
		return false;
	//重置上下文	
	s_count = 0; s_bBuseFlage = false;
	return true;
}

如上,我assert了一个函数的执行返回值。在VS环境下,当编译release版本时,上述assert语句将被优化掉,也就是说其中的含有逻辑功能的函数将不会被执行,这必然导致运行异常。Debug下没有问题,release时抓瞎,通常会火烧眉毛。

再强调’不要在assert内执行逻辑功能’

//这条代码在R模式下可能不执行,从而导致逻辑异常
 assert(m_pObserver->Subscribe_Open(_ID_STREAM_INFO));

上述是曾经在实际项目中犯下的错误,而且那还是在总结过assert使用注意事项,已经很清楚assert内不可以执行逻辑代码。总归有那么几个脑子变浆糊的时刻,手欠。因此,遇到D模式和R模式执行不一致的情况,assert使用,算是一个检查点。

 //不要偷懒,只在assert中判断结果值
bOk &= m_pObserver->Subscribe_Open(_ID_STREAM_INFO);
assert(bOk);

怎敢默认release下绝不会发生此错误?

一种情形是这样的,Debug下并没有触发assert,但release下却发生了要捕获的异常,由于编码和编译原因,若release版本中assert被优化掉,那么,你相当于是放弃了对此异常情形的处理!那么就危险了,这种危险远不止是你的程序异常退出了一次,而是你没有什么可用信息去定位异常位置。

要不要在Release版本下使用断言

首先表明立场,不建议在Release版本中使用assert断言生效。你最好使用其他形式的错误处理和日志记录来补充 assert 断言,以提供更好的用户体验和容错能力。

好处:
错误检测:assert 断言是一种在运行时检测程序中的错误和异常的方法。在 Release 版本中启用 assert 断言可以帮助及早发现潜在的问题和错误,提高代码质量和可靠性。
调试信息:assert 断言通常会输出有关断言失败的相关信息,例如断言所在的文件和行号等。在 Release 版本中启用 assert 断言可以提供有用的调试信息,以便更好地理解和排查问题。
安全验证:通过启用 assert 断言,您可以在 Release 版本中对关键的安全验证进行检查。这可以帮助捕获潜在的安全漏洞和错误用法,提高程序的安全性。

坏处:
性能影响:assert 断言通常在断言失败时会导致程序终止。这会对程序的性能产生一定的负面影响。因此,在 Release 版本中启用 assert 断言可能会导致性能下降,尤其是在大规模或性能敏感的应用程序中。
用户体验:断言失败可能导致程序异常终止,这可能会对用户体验产生负面影响。在某些情况下,这可能会导致数据丢失或不可预测的行为。因此,在启用 assert 断言时需要谨慎权衡用户体验和错误检测的需求。
可移植性问题:某些平台或环境可能不支持 assert 断言,或者对其行为进行了修改。因此,在跨平台或跨环境的应用程序中,依赖于 assert 断言的特定行为可能会导致可移植性问题。

使用assert的其他建议

assert 在那些一次性执行的代码语句上可以使用,这些语句往往是非常的硬,如果一旦有错误错误,程序将没有继续运行的必要。
在那些动态频繁执行的函数中,如果使用assert来进行某些校验,往往会给自己带来不少麻烦。

虽然启用断言可能会带来性能影响和一些不太友好的用户体验,但在开发和调试阶段,启用断言可以帮助发现和修复潜在的错误和异常,从而提高代码质量和可靠性。在发布和部署阶段,需要谨慎权衡性能与错误检测需求之间的平衡。

像是动态内存申请之类的操作,强烈建议不要用assert检验返回结果。但有时候你实在懒得一坨,加个assert并打开release的DEBUG开关,也是种手段,这至少比你置之不理要好很多。因为空指针通常会导致程序毫无征兆的死掉,让你束手无策,那不仅不优雅,还会让运维和开发工程师很头疼。故,当你的软件没有完备的异常处理或日志记录机制,那就用assert做最后的救命稻草吧!

静态断言

C/C++ 中的静态断言机制(Static Assertion)是一种在编译时进行静态检查的机制,用于在编译器发现错误之前捕获问题。static_assert 接受一个编译时求值为布尔值的表达式作为参数,并在表达式为假时触发编译错误。如果表达式为真,则静态断言不会产生任何代码或运行时开销。如下,是ROS2.0 异常处理模块中的源码片段,

/// Struct wrapping a fixed-size c string used for returning the formatted error string.
typedef struct rcutils_error_string_s {
  /// The fixed-size C string used for returning the formatted error string.
  char str[RCUTILS_ERROR_MESSAGE_MAX_LENGTH];
} rcutils_error_string_t;  //该结构的长度与rcutils_error_state_t长度相同

/// Struct which encapsulates the error state set by RCUTILS_SET_ERROR_MSG().
typedef struct rcutils_error_state_s {
  /// User message storage, limited to RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH characters.
  char message[RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH];
  /// __FILE__宏代表的代码文件名 File name, limited to what's left from RCUTILS_ERROR_STATE_MAX_SIZE characters after subtracting storage for others.
  char file[RCUTILS_ERROR_STATE_FILE_MAX_LENGTH];
  /// __LINE__宏代表的代码行号 Line number of error.
  uint64_t line_number;
} rcutils_error_state_t;  //该结构的长度与rcutils_error_string_t长度相同

// make sure our math is right... //编译时进行静态断言from C++ 11
#if __STDC_VERSION__ >= 201112L
static_assert(
  sizeof(rcutils_error_string_t) == (  /* 1024 == 768 + 229 + 20 + 6 + 1(null terminating character) */
    RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH + RCUTILS_ERROR_STATE_FILE_MAX_LENGTH + RCUTILS_ERROR_STATE_LINE_NUMBER_STR_MAX_LENGTH + RCUTILS_ERROR_FORMATTING_CHARACTERS + 1),
    "Maximum length calculations incorrect");
#endif

自定义断言

如在heap4中有类似如下定义,

//定义错误信息输出函数
#define vAssertCalled(charFile, intLine) AflDebugError("Error:%s,%d\r\n",charFile, intLine)
//利用 __FILE__,__LINE__ 预定义宏
#define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)

早期在打印调试单元中定义的断言,不去中断程序执行,

//assert 自定义断言
#define AFLPRINTF_ASSERT(bAssert)  \
    if (!(bAssert))                \
        { \
            printf("AFLPRINTF_ASSERT %s,%d:", FUNCNAME, __LINE__); \
            printf("\r\n");   \
            fflush(stdout);   \
        }  \

其他的定义可参考 《异常处理/LINEFILE 宏在调试和异常处理中的高级使用》 等文章。

小结

在百科有提到 assert.h 常用于防御式编程。,防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。这种思想是将可能出现的错误造成的影响控制在有限的范围内。这里也临时总结几个使用断言的几个原则,
(1)使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
(2)使用断言对函数的参数进行确认。
(3)在编写函数时,要进行反复的考查,并且自问:"我打算做哪些假定?"一旦确定了的假定,就要使用断言对假定进行检查。
(4)一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果"不可能发生"的事情的确发生了,则要使用断言进行报警。
ASSERT 是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏通常原来判断程序中是否出现了明显非法的数据,如果出现了终止程序以免导致严重后果,同时也便于查找错误。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

react18【系列实用教程】useContext —— Context 机制实现越层组件传值 (2024最新版)

什么是 Context 机制&#xff1f; Context 机制是 react 实现外层组件向内层组件传值的一种方案&#xff0c;父组件可以向其内部的任一组件传值&#xff0c;无论是子组件还是孙组件或更深层次的组件。 实现步骤 1.使用createContext方法创建一个上下文对象 Ctx 2.在顶层组件中通…

轮转数组 与 消失的数字

轮转数组 思路一 创建一个新内存空间&#xff0c;将需轮转的数依次放入&#xff0c;之后在把其它数放入 代码&#xff1a; void rotate(int* nums, int numsSize, int k) {k k % numsSize;// 确定有效的旋转次数if(k 0)return;int* newnums (int*)malloc(sizeof(int) * nu…

An 2024下载

An2024下载&#xff1a; 百度网盘下载https://pan.baidu.com/s/1cQQCFL16OUY1G6uQWgDbSg?pwdSIMS Adobe Animate 2024&#xff0c;作为Flash技术的进化顶点&#xff0c;是Adobe匠心打造的动画与交互内容创作的旗舰软件。这款工具赋予设计师与开发者前所未有的创意自由&#x…

设计模式-结构型-桥接模式-Bridge

桥接模式可以减少类的创建 矩阵类 public class Matrix {private String fileName;public Matrix(String fileName) {this.fileName fileName;}public String getFileName() {return fileName;} } 图片抽象类 public abstract class Image {protected ImageImp imp;public …

vivado Kintex UltraScale 配置存储器器件

Kintex UltraScale 配置存储器器件 下表所示闪存器件支持通过 Vivado 软件对 Kintex UltraScale 器件执行擦除、空白检查、编程和验证等配置操作。 本附录中的表格所列赛灵思系列非易失性存储器将不断保持更新 &#xff0c; 并支持通过 Vivado 软件对其中所列非易失性存…

深入解析MySQL中的事务(下)

MySQL事务管理 3. 隔离性&#xff08;Isolation&#xff09;查看和设置隔离级别隔离级别作用域区别与解析 四种隔离级别解析小结 4. 一致性&#xff08;Consistency&#xff09;如何保持一致性 5.“保持原子性、隔离性、持久性就能保证一致性”的理解&#xff1a; 四、如何理解…

图论专题训练

leecode 547 并查集 class Solution { public:int findCircleNum(vector<vector<int>>& isConnected) {ini();int len isConnected.size();for(int i0;i<len;i){for(int j0;j<len;j)if(isConnected[i][j]){unio(i,j);}}int ans 0;for(int i0;i<len;…

自动驾驶技术与传感器数据处理

目录 自动驾驶总体架构 感知系统 决策系统 定位系统 ​计算平台​ 仿真平台​ 自动驾驶公开数据集 激光点云 点云表征方式 1) 原始点云 2) 三维点云体素化 3)深度图 4)鸟瞰图 点云检测障碍物的步骤 PCL点云库 车载毫米波雷达 车载相机 设备标定 自动驾驶…

python选修课期末考试复习

目录 记住输出小数的格式文件条件判断随想循环小星星计算金额猜数字折纸 函数找最大值 基础知识总结 记住输出小数的格式 输出a&#xff0c;保留两位小数 %.2f%a打开文件有点儿难&#xff0c;多记几遍格式吧 文件的格式后面有冒号&#xff0c;谨慎一点&#xff0c;都用双引号…

花了24小时做的采购、库存、进销存excel模板,真心好用,免费分享

花了24小时做的采购、库存、进销存excel模板&#xff0c;真心好用 在企业的日常运营中&#xff0c;进销存管理是一项至关重要的任务。它不仅涉及到商品的采购、销售和库存管理&#xff0c;还直接影响到企业的财务状况和市场竞争力。为了提高管理效率&#xff0c;许多企业选择使…

字典是如何实现的?Rehash 了解吗?

字典是 Redis 服务器中出现最为频繁的复合型数据结构。除了 hash 结构的数据会用到字典外&#xff0c;整个 Redis 数据库的所有 key 和 value 也组成了一个 全局字典&#xff0c;还有带过期时间的 key 也是一个字典。(存储在 RedisDb 数据结构中) 字典结构是什么样的呢&#xf…

loongarch64 electron打包deb改成符合统信测试通过的deb

需要做软件适配统信系统的自主认证。 我之前是在 麒麟 龙芯 loongarch64 电脑上使用 electron 打包的 deb包&#xff1a;麒麟龙芯loongarch64 electron 打包deb包_electron麒麟系统打包的-CSDN博客 安装在统信电脑 处理器&#xff1a;Loongson-3A60000-HV 2.5GHz 可以使用&…

陪玩系统APP小程序H5音视频社交系统陪玩系统源码,陪玩app源码,陪玩源码搭建陪玩社交系统开发(现成,可定制)线下陪玩系统项目开发搭建

线下陪玩系统项目的设计 在需求分析完成后&#xff0c;接下来进行系统设计。系统设计主要包括以下几个部分&#xff1a; 1. 数据库设计&#xff1a;根据需求分析的结果&#xff0c;设计数据库结构&#xff0c;包括用户信息表、服务信息表、订单信息表等。 2. 界面设计&#…

Git在windows和Linux安装并自动更新代码超详细讲解

一、Git官网安装 1、官网安装地址&#xff1a; Git - Downloading Packagehttps://git-scm.com/download/win 官网下载比较慢建议使用下面链接 2、国内镜像下载地址&#xff1a; CNPM Binaries Mirrorhttps://registry.npmmirror.com/binary.html?pathgit-for-windows/ 3、…

【mysql篇】执行delete删除大量数据后,磁盘未清空,为什么?

目录 迁移脚本删除数据以及备份数据 解决方法OPTIMIZE TABLE二进制日志按月生成数据 最近某个项目虽说用户量不大&#xff0c;但是&#xff0c;单表的数据量越来越大&#xff0c;mysql一般单表超过千万级别后&#xff0c;性能直线下降&#xff0c;所以利用shardingphere按月做了…

c语言柔性数组

柔性数组 在c99中&#xff0c;结构体的最后一个元素允许是未知大小的数组&#xff0c;这个就是柔性数组 柔性数组的特点 1.结构体中的柔性数组成员前面必须至少有一个其他成员 2.sizeof返回的这种结构大小不包括柔性数组的内存 3.包含柔性数组成员的结构用malloc()函数进行内存…

C++11:常用语法汇总

目录 &#x1f341;统一的列表初始化 { }initializer_list &#x1f341;decltype 推导表达式类型&#x1f341;可变参数模板解析可变参数包方法一方法二 &#x1f341;lambda 表达式捕捉列表的使用运用场景举例lambda表达式 与 函数对象 &#x1f341;统一的列表初始化 { } 在…

8个免费无版权视频素材网站,高清无水印视频任性下载

在数字化时代&#xff0c;优质的视频素材成为各种项目不可缺少的元素&#xff0c;从短片制作到商业广告&#xff0c;高品质的视频能显著提高作品的吸引力和传播效果。然而&#xff0c;寻找既免费又无版权问题的高清视频素材并非易事。以下介绍几个优秀的免费视频素材网站&#…

基于YOLOV8复杂场景下船舶目标检测系统

1. 背景 海洋作为地球上70%的表面积&#xff0c;承载着人类生活、经济发展和生态系统的重要功能。船舶作为海洋活动的主要载体之一&#xff0c;在海上运输、资源开发、环境监测等方面发挥着重要作用。复杂海洋环境下的船舶目标检测成为了海事管理、海洋资源开发和环境保护等领…

嵌入式全栈开发学习笔记---C语言笔试复习大全17

目录 指针和字符串 用数组和指针定义字符串 两种定义方法有什么区别&#xff1f; 第一个区别&#xff1a; 第二个区别&#xff1a; 第三个区别&#xff1a; 指针数组 上一篇复习了指针和数组,这一篇我们来复习指针和字符串。 说明&#xff1a;我们学过单片机的一般都是有…