sec7-可派生和非抽象类型

news2024/11/16 19:49:52

  创建非抽象可派生类型比创建抽象类型更常见。本节介绍如何创建非抽象可派生类型对象。派生类型的例子是string的对象。它是TStr。它的子对象是一个数字字符串对象。数字字符串是表示数字的字符串。例如“0”、“-100”、“123.45”。子对象(数字字符串)将在下一节中解释。

我想先写字符串,再写可派生对象。因为TStr是字符串的对象,我们需要小心使用字符串编写程序。

字符串和内存管理

  TStr有一个字符串类型的值。它类似于TInt或TDouble,但string比int和double更复杂。在编写TStr程序时,需要注意内存管理,这对于TInt和TDouble来说是不必要的。

字符串和内存

  String是一个字符数组,以’\0’结束。String不是C类型,如char, int, float或double。但是指向字符数组的指针的行为与其他语言的字符串类型类似。我们通常称指针为字符串。

  如果指针为NULL,则它不指向任何东西。指针不是字符串。如果不小心使用空指针,带有字符串的程序将包含错误。

  另一个恼人的问题是内存分配。因为字符串是一个字符数组,所以需要分配内存来创建一个新的字符串。我们没有忘记分配内存,但经常忘记释放内存。它会导致内存泄漏。

char *s;
s = g_strdup ("Hello.");
... ... ... do something with s
g_free (s);

g_strdup复制一个字符串。它:

  • 计算字符串的大小。"Hello."的大小是7,因为字符串是以零结尾的。字符串是一个数组‘H’,‘e’,‘l’,‘l’,‘o’、‘。‘和零(’\0’)。
  • 请求系统分配7个字节的内存。
  • 将字符串复制到内存中。
  • 返回指向新分配内存的指针。
  • 如果参数为NULL,则g_strdup返回NULL。

如果字符串s不再使用,则必须释放s,这意味着必须将分配的7个字节返回给系统。g_free释放内存。

由双引号(如“Hello.”)限定的字符串是字符串常量。它是一个字符数组,但是数组的内容不允许更改。它不能被释放。如果将字符写入字符串字面量或释放字符串字面量,则结果为未定义。可能会发生不好的事情,例如,段错误。

当你用字符串文字初始化数组和指针时,它们之间是有区别的。如果数组是用字符串字面值初始化的,则数组可以被更改。

char a[]="Hello!";
a[1]='a';
g_print ("%s\n", a); /* Hallo will appear in your display. */

第一行是数组a初始化,初始化并不简单。首先,编译器计算“Hello!”的长度。它是7,因为字符串文字的末尾有’\0’。然后在静态内存或堆栈内存中分配7个字节的内存。它取决于数组的类,是静态的还是自动的。用“Hello!”初始化内存。数组中的字符串可以被改变。该程序成功显示“Hallo!”

上面程序的第一行如下所示。

char a[] = {'H', 'e', 'l', 'l', 'o', '!', '\0'};

如果你用字符串常量定义指针,你不能改变指针指向的字符串内容。

char *a = "Hello";
*(a+1) = 'a'; /* This is illegal. */
g_print ("%s\n", a);

第一行只是将字符串字面值的地址赋给变量a。字符串常量是一个字符数组,但它是只读的。它可能在程序代码区域或其他一些不可写的区域。这取决于编译器的实现。因此,第二行尝试将字符’a’写入只读内存,结果是未定义的,例如,发生了分段错误。不管怎样,不要写这样的程序。

总之,字符串是一个字符数组,它被放置在以下类型之一。

  • 只读内存。字符串常量是只读的。
  • 栈。如果一个数组的类是auto,那么该数组将被放置在栈中。stack是可写的。如果数组是在函数中定义的,它的默认类是auto。当函数返回给调用者时,栈区域将消失。
  • 静态区域。如果数组的类是静态的,则该数组被放置在静态区域中。它保持其值,并在程序的生命周期内保持不变。这个区域是可写的。
  • 堆。您可以为字符串分配和释放内存。对于分配,使用g_new或g_new0。对于释放,使用g_free。

拷贝字符串

复制字符串有两种方法。第一种方法是复制指针。

char *s = "Hello";
char *t = s;

两个指针st指向同一个地址。因此,您不能修改t,因为t指向一个只读的字符串字面值。

第二种方法是创建内存并将字符串复制到内存中。

char *s = "Hello";
char *t = g_strdup (s);

g_strdup分配内存并使用"Hello"初始化它,然后返回指向内存的指针。函数g_strdup几乎与下面的string_dup函数相同。

#include <glib-object.h>
#include <string.h>

char *
string_dup (char *s) {
  int length;
  char *t;

  if (s == NULL)
    return NULL;
  length = strlen (s) + 1;
  t = g_new (char, length);
  strcpy (t, s);
  return t;
}

如果使用g_strdup,则两个指针s和t指向不同的内存。您可以修改t,因为它位于从堆区域分配的内存中。

知道赋值指针和复制字符串之间的区别很重要。

常量限定符

限定符const创建的变量不会改变其值。它也可以应用于数组。这样,数组的元素就不会被改变。

const double PI = 3.1415926536;
const char a[] = "只读字符串";

函数中的数组形参可以用const限定,以指示该函数不改变数组。同样,函数的返回值(指向数组或字符串的指针)可以用const限定。调用者不能修改或释放返回的数组或字符串内容。

char *
string_dup (const char *s) {
……
}

const char *
g_value_get_string (const GValue *value);

限定符const表示在对象函数中使用该字符串时,谁是该字符串的所有者。“Owner”是一个对象或函数的调用者,它有权修改或释放字符串。

例如,g_value_get_string被赋予const GValue *value作为参数。由value指向的GValue归调用者所有,函数不会改变或释放它。该函数返回一个带有const限定的字符串。返回的字符串归对象所有,调用者不能改变或释放字符串。字符串可能稍后会被对象更改或释放。

头文件

本节的其余部分是关于TStr的。TStr是GObject的子对象,它包含一个字符串。字符串是一个指针和一个字符数组。指针指向数组。指针可以是NULL。如果为NULL,则TStr没有数组。TStr有一个字符串类型属性。

头文件tstr.h如下所示。

 1 #ifndef __T_STR_H__
 2 #define __T_STR_H__
 3 
 4 #include <glib-object.h>
 5 
 6 #define T_TYPE_STR  (t_str_get_type ())
 7 G_DECLARE_DERIVABLE_TYPE (TStr, t_str, T, STR, GObject)
 8 
 9 struct _TStrClass {
10   GObjectClass parent_class;
11   /* expect that descendants will override the setter */
12   void (*set_string)  (TStr *self, const char *s);
13 };
14 
15 TStr *
16 t_str_concat (TStr *self, TStr *other);
17 
18 /* setter and getter */
19 void
20 t_str_set_string (TStr *self, const char *s);
21 
22 char *
23 t_str_get_string (TStr *self);
24 
25 /* create a new TStr instance */
26 TStr *
27 t_str_new_with_string (const char *s);
28 
29 TStr *
30 t_str_new (void);
31 #endif /* __T_STR_H__ */
32 
  • 7:使用G_DECLARE_DERIVABLE_TYPE,类似于前一节中的例子。
  • 9-13: TStrClass有一个类方法。它是set_string。这将被子类TNumStr覆盖。类方法由公共函数t_str_set_string调用。它有一个字符串形参,并使用string(参数)设置实例字符串。TNumStr类保存类似TStr的字符串,但它也保存字符串的类型。类方法set_string将被重写,它不仅将设置字符串,还将设置子类中的类型。详细的解释将在本节的后面部分和下一节中。
  • 15-16: t_str_concat连接两个TStr实例字符串并返回一个新的TStr实例。
  • 19-23: Setter和getter。
  • 26-30:创建TStr对象的函数。

C file

  1 #include "tstr.h"
  2 
  3 enum {
  4   PROP_0,
  5   PROP_STRING,
  6   N_PROPERTIES
  7 };
  8 
  9 static GParamSpec *str_properties[N_PROPERTIES] = {NULL, };
 10 
 11 typedef struct {
 12   char *string;
 13 } TStrPrivate;
 14 
 15 G_DEFINE_TYPE_WITH_PRIVATE(TStr, t_str, G_TYPE_OBJECT)
 16 
 17 static void
 18 t_str_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) {
 19   TStr *self = T_STR (object);
 20 
 21   if (property_id == PROP_STRING) {
 22     t_str_set_string(self, g_value_get_string (value));
 23   } else
 24     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
 25 }
 26 
 27 static void
 28 t_str_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) {
 29   TStr *self = T_STR (object);
 30 
 31   if (property_id == PROP_STRING)
 32     g_value_set_string (value, t_str_get_string(self));
 33   else
 34     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
 35 }
 36 
 37 static void
 38 t_str_real_set_string (TStr *self, const char *s) {
 39   TStrPrivate *priv = t_str_get_instance_private (self);
 40 
 41   if (priv->string)
 42     g_free (priv->string);
 43   priv->string = g_strdup (s);
 44 }
 45 
 46 static void
 47 t_str_finalize (GObject *object) {
 48   TStr *self = T_STR (object);
 49   TStrPrivate *priv = t_str_get_instance_private (self);
 50 
 51   if (priv->string)
 52     g_free (priv->string);
 53   G_OBJECT_CLASS (t_str_parent_class)->finalize (object);
 54 }
 55 
 56 static void
 57 t_str_init (TStr *self) {
 58   TStrPrivate *priv = t_str_get_instance_private (self);
 59 
 60   priv->string = NULL;
 61 }
 62 
 63 static void
 64 t_str_class_init (TStrClass *class) {
 65   GObjectClass *gobject_class = G_OBJECT_CLASS (class);
 66 
 67   gobject_class->finalize = t_str_finalize;
 68   gobject_class->set_property = t_str_set_property;
 69   gobject_class->get_property = t_str_get_property;
 70   str_properties[PROP_STRING] = g_param_spec_string ("string", "str", "string", "", G_PARAM_READWRITE);
 71   g_object_class_install_properties (gobject_class, N_PROPERTIES, str_properties);
 72 
 73   class->set_string = t_str_real_set_string;
 74 }
 75 
 76 /* setter and getter */
 77 void
 78 t_str_set_string (TStr *self, const char *s) {
 79   g_return_if_fail (T_IS_STR (self));
 80   TStrClass *class = T_STR_GET_CLASS (self);
 81 
 82   class->set_string (self, s);
 83 }
 84 
 85 char *
 86 t_str_get_string (TStr *self) {
 87   g_return_val_if_fail (T_IS_STR (self), NULL);
 88   TStrPrivate *priv = t_str_get_instance_private (self);
 89 
 90   return g_strdup (priv->string);
 91 }
 92 
 93 TStr *
 94 t_str_concat (TStr *self, TStr *other) {
 95   g_return_val_if_fail (T_IS_STR (self), NULL);
 96   g_return_val_if_fail (T_IS_STR (other), NULL);
 97 
 98   char *s1, *s2, *s3;
 99   TStr *str;
100 
101   s1 = t_str_get_string (self);
102   s2 = t_str_get_string (other);
103   if (s1 && s2)
104     s3 = g_strconcat (s1, s2, NULL);
105   else if (s1)
106     s3 = g_strdup (s1);
107   else if (s2)
108     s3 = g_strdup (s2);
109   else
110     s3 = NULL;
111   str = t_str_new_with_string (s3);
112   if (s1) g_free (s1);
113   if (s2) g_free (s2);
114   if (s3) g_free (s3);
115   return str;
116 }
117 
118 /* create a new TStr instance */
119 TStr *
120 t_str_new_with_string (const char *s) {
121   return T_STR (g_object_new (T_TYPE_STR, "string", s, NULL));
122 }
123 
124 TStr *
125 t_str_new (void) {
126   return T_STR (g_object_new (T_TYPE_STR, NULL));
127 }
128 
  • 3-9: enum定义属性id。PROP_STRING是“string”属性的id。这里只定义了一个属性,所以可以在没有enum的情况下定义它。但是,enum可以用于定义两个或多个属性。所以,更推荐这种方式。以同样的方式,使用数组str_properties。它为GParamSpec存储一个静态指针。
  • 11-13: TStrPrivate是一个C结构体。它是TStr的私有数据区域。如果TStr是最终类型,则不存在后代,TStr实例可以看作是私有数据区域。但是TStr是可派生的,所以你不能在对后代开放的TStr实例中存储这样的私有数据。这个结构的名称是“<object name>Private”,例如TStrPrivate。该结构必须在G_DEFINE_TYPE_WITH_PRIVATE之前定义。
  • 15: G_DEFINE_TYPE_WITH_PRIVATE宏。这个宏展开为:
      1.声明t_str_class_init是一个类初始化函数。
      2.声明实例初始化函数t_str_init。
      3.定义t_str_parent_class静态变量。它指向TStr的父类。
      4.将私有实例数据添加到类型中。它是一个C结构,它的名字是TStrPrivate。(见上图)。
      5.定义t_str_get_type()函数。该函数在第一次调用时注册该类型。
      6.定义一个私有实例getter t_str_get_instance_private()。这个函数有一个参数,它是指向实例的指针。
  • 17-25: t_str_set_property。这类似于t_int_set_property,但是属性值存储在私有区域中。
  • 22:使用t_str_set_string函数用GValue中的字符串设置私有数据区域。这很重要,因为t_str_set_string调用类方法set_string,该方法将被子类覆盖。因此,t_str_set_property函数将在子类中设置字符串及其类型。函数g_value_get_string返回GValue拥有的字符串的指针。你不能拥有字符串。这意味着你不能改变或释放字符串。因此,有必要在存储字符串之前复制字符串。复制在函数t_str_set_string中完成。
  • 27-35: t_str_get_property。它使用t_str_get_string函数。字符串复制在函数中完成。
  • 37-44 t_str_real_set_string函数。这是类中的set_string所指向的。所以,它是类方法的主体。首先,它使用t_str_get_instance_private函数获取私有区域的指针。如果实例拥有一个字符串,在使用新字符串设置它之前释放它。它复制字符串并将其放入priv->字符串。
  • 46-54:当TStr实例被销毁时调用t_str_finalize。这个函数释放字符串priv->。之后,它调用父类的finalize方法。这被称为“chain up to its parent”,这将在本节后面解释。
  • 56-61: t_str_init初始化priv->字符串。
  • 63-74: t_str_class_init函数初始化TStr对象的类。
  • 67:覆盖finalize方法。在销毁过程中调用此方法。
  • 68-69:覆盖set_property和get_property方法。
  • 70-71:创建GParamSpec。然后安装该属性。
  • 73:类方法set_string指向t_str_real_set_string。这个方法预计将在子类中被替换。
  • 77-91: Setter和getter。它们被属性设置/获取函数t_str_set_property和t_str_get_property使用。setter t_str_set_string只调用类方法。所以它的行为会在子类中改变。
  • 93-116: t_str_concat函数。它连接了self和other的字符串,并创建了一个新的TStr。
  • 119-127: t_str_new_with_string和t_str_new创建一个新的TStr实例。

链接到它的父节点

“链到其父类”流程如下图所示。
在这里插入图片描述有两个类,GObjectCLass和TStrClass。每个类都有它们的finalize方法(函数),由类结构中的指针指向。TStrClass的finalize方法会终结TStr实例中自己的部分。在函数的末尾,它会调用父函数的finalize方法。它是GObjectClass的finalize方法。它调用自己的finalize函数,完成GObject私有数据的处理。

如果GObjectClass有两个或多个子类,则finalize函数的数量可能与后代类的数量相同。它们通过“链式连接”的方式连接在一起。
在这里插入图片描述

如何编写可派生类型

  • 在头文件中使用G_DECLARE_DERIVABLE_TYPE宏。你需要在G_DECLARE_DERIVABLE_TYPE之前写一个#define T_TYPE_STR (t_str_get_type())这样的宏。
  • 在头文件中声明类结构。类的内容对后代开放。大多数成员是类方法。
  • 在C文件中使用G_DEFINE_TYPE_WITH_PRIVATE。需要在G_DEFINE_TYPE_WITH_PRIVATE之前定义私有区域。它是一个C结构,名称为“<object name>Private”,类似于“TStrPrivate”。
  • 定义类和实例初始化函数。

翻译自:Gobject-tutorial

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

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

相关文章

前端框架搭建(九)搭建页面布局框架【vite】

## 1.创建目录BasicLayout——全局布局 components——布局组件 GlobalContent&#xff1a;全局内容GlobalHeader&#xff1a;全局头部页面 2.处理GlobalHeader 创建HeaderMenu——头部菜单 声明相关类型 在typings目录下创建system.d.ts declare namespace App {/** 全局…

Canadian Coding Competition(CCC) 2021 Junior 题解

目录 Problem J1: Boiling Water Problem J2: Silent Auction Problem J3: Secret Instructions Problem J4: Arranging Books Problem J5/S2: Modern Art Problem J1: Boiling Water Problem Description At sea level, atmospheric pressure is 100 kPa and water begi…

自动挂载USB和TF卡

转自链接https://zhuanlan.zhihu.com/p/443745437 ①创建用于挂载U盘的目录 mkdir /mnt/usb –p②在/etc/udev/rules.d目录下添加用于检测U盘插入规则&#xff08;add&#xff09;&#xff0c;终端下执行以下命令创建第一个U盘插入规则。 vim /etc/udev/rules.d/11-add-usb.r…

【ROS】—— ROS通信机制——服务通信(三)

文章目录前言1. 服务通信理论模型2. 服务通信自定义srv2.1 定义srv文件2.2 编辑配置文件2.3 编译3. 服务通信自定义srv调用(C)3.1 vscode配置3.2 服务端3.3 客户端3.4 配置 CMakeLists.txt4. 服务通信自定义srv调用(python)4.1 vscode配置4.2 服务端4.3 客户端4.4 配置 CMakeLi…

将Android进行到底之内容提供者(ContentProvider)

文章目录前言一、ContentProvider是什么&#xff1f;二、使用示例1.为应用创建内容提供者2.使用内容提供者2.1 内容URI2.2 Uri参数解析2.2 使用内容URI操作数据3.ContentProvider妙用4 内容URI对应的MIME类型5.ContentProvider重点注意6 演示demo源码总结前言 随着现在的应用越…

java通过JDBC连接mysql8.0数据库,并对数据库进行操作

目录 一、JDBC简介 二、添加依赖 三、JDBC操作数据库的步骤 四、JDBC操作数据库——增删改查 (一)新增数据 (二)删除数据 (三)修改数据 (四)查询数据 (五)多表连接查询 一、JDBC简介 Java数据库连接&#xff0c;&#xff08;Java Database Connectivity&#xff0c;简…

C进阶:字符串相关函数及其模拟实现

目录 &#x1f431;&#x1f638;一.strlen &#x1f54a;️1.功能 &#x1f43f;️2.模拟实现 &#x1f42c;&#x1f40b;二.strcpy &#x1f432;1.功能 &#x1f916;2.注意事项 &#x1f47b;3.模拟实现 &#x1f431;&#x1f42f;三.strcat &#x1f984;1.功能…

i.MX8MP平台开发分享(IOMUX篇)- Linux注册PAD

专栏目录&#xff1a;专栏目录传送门 平台内核i.MX8MP5.15.71文章目录1. pinfunc.h2.pinctrl-imx8mp.c3.PAD信息注册这一篇开始我们深入Linux中的pinctl框架。1. pinfunc.h pinfunc.h中定义了所有的引脚&#xff0c;命名方式是MX8MP_IOMUXC___&#xff0c;例如下面的MX8MP_IO…

【JDBC】----------ServletContext和过滤器

分享第二十四篇励志语句 幸福是什么&#xff1f;幸福不一定是腰缠万贯、高官显禄、呼风唤雨。平凡人自有平凡人的幸福&#xff0c;只要你懂得怎样生活&#xff0c;只要你不放弃对美好生活的追求&#xff0c;你就不会被幸福抛弃。 一&#xff1a;ServletContext&#xff08;重要…

js对象篇

面向对象 对象 如果对象的属性键名不符合JS标识符命名规范&#xff0c;则这个键名必须用引号包裹 访问属性有2种方法&#xff0c;有点语法和方括号填写法&#xff0c;特别地&#xff0c;如果属性名不符合JS命名规范或者使用变量存储属性名&#xff0c;则必须使用方括号访问 属…

【王道操作系统】2.3.6 进程同步与互斥经典问题(生产者消费者问题、吸烟者问题、读者写者问题、哲学家进餐问题)

进程同步与互斥经典问题(生产者消费者问题、吸烟者问题、读者写者问题、哲学家进餐问题) 文章目录进程同步与互斥经典问题(生产者消费者问题、吸烟者问题、读者写者问题、哲学家进餐问题)1.生产者-消费者问题1.1 问题描述1.2 问题分析1.3 如何实现1.4 实现互斥的P操作一定在实现…

深化全面To C战略魏牌发布与用户共创大六座SUV蓝山

对魏牌而言&#xff0c;与用户共创不是吸引眼球的营销噱头&#xff0c;而是“直面用户需求&#xff0c;真实倾听用户意见”的有效途径。 2022年12月30日&#xff0c;第二十届广州国际汽车展览会&#xff08;以下简称“广州车展”&#xff09;正式启幕。魏牌以“品位蓝山 有咖有…

神经网络必备基础知识:卷积、池化、全连接(通道数问题、kernel与filter的概念)

文章目录卷积操作实际操作filter与kernel1x1的卷积层可视化的例子池化全连接卷积操作 这个不难理解。我们知道图像在计算机中是由一个个的像素组成的&#xff0c;可以用矩阵表示。 假设一个5x5的输入图像&#xff0c;我们定义一个3x3的矩阵&#xff08;其中的数值是随机生成的…

excel图表美化:设置标记样式让拆线图精巧有趣

折线图作为我们平时数据视图化非常常规的表现方式&#xff0c;想必大家已经司空见惯了。折线图很简单&#xff0c;每个人都会做&#xff0c;但是不同的人做出来的折线图却千差万别。大多数人的折线图都是直接插入默认折线图样式生成的&#xff0c;这样的折线图先不说有没有用心…

五、IDEA中创建Web项目

文章目录5.1 创建Web项目5.1.1 创建项目5.1.2 编写Servlet类5.2 手动部署项目5.3 自动部署项目5.3.1 IDEA集成Tomcat5.3.2 IDEA部署JavaWeb项目5.4 war包部署5.4.1 导出war包5.1 创建Web项目 5.1.1 创建项目 1、打开IDEA&#xff0c;单击“New Project”或者通过File–>ne…

Perl语言入门

一、简介 Perl语言是拉里.沃尔&#xff08;Larry Wall&#xff09;在1987年开发的一种编程语言&#xff0c;借鉴了C、sed、awk、shell脚本语言以及其他语言的特性&#xff0c;专门用于文本处理。 它可以在各种平台上运行&#xff0c;例如Windows&#xff0c;Mac OS和各种UNIX…

bean生命周期

1.Aware和InitializingBean接口 Aware 接口用于注入一些与容器相关信息&#xff0c;例如 BeanNameAware: 注入bean的名字BeanFactorAware&#xff1a; 注入beanFactor容器ApplicationContextAware&#xff1a; 注入applicationContext容器EmbeddedValueResolverAware: ${} 代码…

爬虫进阶一(基础一)

文章目录简介cookie爬取雪球热帖代理模拟登陆防盗链异步爬虫协程asyncioM3U8HLS爬取seleniumbilibili无头浏览器规避检测MySQLMongoDBRedis简介 这个系列分四部分 基础进阶Scrapy 框架逆向分析实战运用 先补充一些爬虫需要的基础知识和技能预热&#xff0c;爬取个简历模板网站…

浅谈Git

Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理,也是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源码的版本控制软件。 版本控制 什么是版本控制&#xff1f; 版本控制是一种在开发的过程中用于管理我们对文…

如何评价唐卫国公李靖的战功、军事才能、政治才能?

link 一鞭直渡清河洛Research and no development已关注470 人赞同了该回答个人以为&#xff0c;在军事上&#xff0c;李靖是当之无愧的唐朝第一名将&#xff0c;他用兵如神&#xff0c;精于谋略&#xff0c;无论是在实际的军事指挥上&#xff0c;还是军事理论上&#xff0c;他…