皮皮虾 发布的文章

在我们平常工作中,isMemberOfClassisKindOfClass 出现的频次算比较高的,光看文档,我们知道 isMemberOfClass 用来判断一个对象是否为某个类的实例,而 isKindOfClass 用来判断一个对象是否为某个类的实例或者继承自该类的子类的实例。那么在 Objc 的底层是如何实现这一功能的呢,让我们一起来围观一下。

准备

需要一份 Objc 的源码,官方源码)(需要自己踩坑编译)
快速通道(已编译通过,附带demo)。

本文中使用的源码版本为:objc4-818.2 。

运行示例代码

打开编译好的 Ojbc 工程,工程目录如下图:
工程目录:
obj_project.png

如图中所示,我们已经添加了一个新的taget :TestObjc,接下来的实例代码也是写在它的main.m文件中。
在 main.m 文件中添加调试代码:

void testKindOf(){
    BOOL ret1 = [NSObject.class isMemberOfClass:NSObject.class];
    BOOL ret2 = [TestObject.class isMemberOfClass:TestObject.class];
    BOOL ret3 = [[NSObject alloc] isMemberOfClass:NSObject.class];
    BOOL ret4 = [[TestObject alloc] isMemberOfClass:TestObject.class];
    
    NSLog(@"isMemberOfClass== %d - %d - %d - %d",ret1,ret2,ret3,ret4);
    
    BOOL ret5 = [NSObject.class isKindOfClass:NSObject.class];
    BOOL ret6 = [TestObject.class isKindOfClass:TestObject.class];
    BOOL ret7 = [[NSObject alloc] isKindOfClass:NSObject.class];
    BOOL ret8 = [[TestObject alloc] isKindOfClass:TestObject.class];

    NSLog(@"isKindOfClass == %d - %d - %d - %d",ret5,ret6,ret7,ret8);
}

我们再 ret1 处添加一个断点,并在 源码 NSObject.mm 中给 isKindOfClassisMemberOfClass 方法实现中添加断点,然后运行代码。
运行代码之后,我们发现除了类方法 + (BOOL)isMemberOfClass:(Class)cls 和实例方法 - (BOOL)isMemberOfClass:(Class)cls 被调用外,isKindOfClass 的两个方法并没有被调用。why ?

汇编+断点调试

通过勾选 xcode 的菜单栏 Debug -> Debug Workflow -> Always Show Disassembly 开启汇编调试,我们看到了熟悉的函数调用逻辑。
汇编调试:
msg_send_isMemeber.png

如图中所示,程序并没有走 isKindOfClass 方法的实现,而是走了 objc_opt_isKindOfClass 函数,于是我们在源码中搜索找到该函数,并添加断点。而 isMemberOfClass 则通过 objc_msgSend 进行消息转发而被调用的,相对简单一些。

下面我们先来看看 isMemberOfClass 方法的源码实现:


类方法:+isMemberOfClass

当执行 [NSObject.class isMemberOfClass:NSObject.class];
这行代码时,调用的就是这个类方法。

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

方法内部调用了底层 objc_object.hISA() 函数,该函数经过层层调用(看流程图),最终来到 isa_t::getClass() 函数中返回一个 Class ,我们知道 Class 的本质是 objc_class 结构体的指针,因此当"=="运算符的两边都是同一个地址时,运算结果为真。所以我们稍微修改一下代码,然后用 LLDB 的 x 指令进行调试:
调试画面:
class_isMemberOfClass.png

如上图所示,我们看到,指针 acls 的值为:0x10036a0f0 ,而指针cls 的值为: 0x10036a140 ,并不是同一个地址,因此 ret 的值为 NO 。
虽然两个地址值不相同,但他们都指向了同一个地址 0x10036a0f0 ,和 acls 的值相同。而我们通过调用 object_getClass(cls) 方法获取 cls 的元类,发现依然是 0x10036a0f0 。类、元类、根元类 的补充在文章末尾。

当运行到下一行代码 [TestObject.class isMemberOfClass:TestObject.class]; 时,我们再跳入 +isMemberOfClass 方法去调试:
调试画面:
class_isMemberOfClass2.png

我们发现 acls 的值还是 0x100008160 ,而 cls 的值为 0x100008188 ,两者依然不同,但都指向同一个地址 0x10036a0f0 (NSObject)。
综上所述,我们可以判断 self->ISA(); 获取到的是元类。而上述的两行代码都是拿元类与类比较,因此结果都是 NO。

+isMemberOfClass 的流程图:
流程图:
flow1.png


实例方法:-isMemberOfClass
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

先上流程图:
流程图:
IsMemeberFlow.png

通过对比流程图,我们发现-isMemberOfClass 也是调用了底层的 ISA()函数,不同的是返回了对象的类,而不是元类。(可以使用 class_isMetaClass 函数来判断 Class 是否为元类)

[[NSObject alloc] isMemberOfClass:NSObject.class];
[[TestObject alloc] isMemberOfClass:TestObject.class];

因此,在运行这两行代码时,其结果都为 YES 。


接着,我们再转移到 isKindOfClass 方法,它同样有类方法和实例方法,来看看底层是如何实现的。

类方法:+isKindOfClass

在运行 [NSObject.class isKindOfClass:NSObject.class]; 这行代码时,我们使用单步调试发现,代码执行并不会走入 +isKindOfClass方法的实现,而是走了objc_opt_isKindOfClass 函数,这是因为底层对+isKindOfClass方法作了优化,将其IMP指向了objc_opt_isKindOfClass 函数,函数的具体实现如下:

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();  
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

NSObject.mm文件中+isKindOfClass方法的源码如下:

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

经过对比发现,该方法 Objective-c2.0版本除了代码优化,核心的内容并没有变化。
由于是类调用的 +isKindOfClass,根据上述的内容,可知 getIsa() 函数或者ISA()函数返回的都是元类。由此可见,[A isKindOfClass:B],实际上是获取 A 的元类以及遍历元类的父类,与 B 相比较。而根据类、元类的关系图(见“补充知识点部分”),我们知道元类的最终父类依然是 NSObject ,所以[NSObject.class isKindOfClass:NSObject.class];这行代码的结果就是 YES。

再看下一行代码:[TestObject.class isKindOfClass:TestObject.class];,TestObject 的元类及其父类,都不会与 TestObject 相等,因此结果为 NO。


实例方法:-isKindOfClass

通过单步调试,我们看到 -isKindofClass还是会调用 objc_opt_isKindOfClass 函数,只不过getIsa() 函数或者ISA()函数返回的都是实例对象的类。
那么[a isKindOfClass:B],实际上是获取实例对象a的类及其父类,与B类相比较。所以

[[NSObject alloc] isKindOfClass:NSObject.class];
[[TestObject alloc] isKindOfClass:TestObject.class];

这两行代码结果都是 YES。

补充知识点

类、元类、根元类

Class (类)对于我们来说很熟悉,想要创建实例对象就需要先定义一个类。而在 OC Runtime 中类是一个 objc_class 类型的对象 :typedef struct objc_class *Class;,我们通常称之为类对象 ,这个类对象的类被称为元类(meta class),而元类的类就是根元类,这个根元类就是 NSObject ,他们之间通过 isa 指针来联系起来,如下图:
类、元类、根元类:
isa_flow.png

关于 \_OBJC2_ 宏定义

\_OBJC2_ 是 objective-c 2.0 版本的宏定义,在宏定义的作用域里面的是2.0版本的代码,它是用来兼容iOS低版本系统的,Stack Overflow 有一个很好的解释,如下图:
OBJC2:
objc2_desc.png

Stack Overflow 链接

总结

1、类和实例对象对底层 getIsa() 函数或者ISA()函数 的调用结果是不同的:类调用会返回元类,而对象调用会返回类。
2、对 isKindOfClass类似的底层方法调用,并不一定会直接执行方法的 IMP ,因为底层优化会修改IMP的指向。比如调用 isKindOfClass 会执行 objc_opt_isKindOfClass 函数,而不是走isKindOfClass 的IMP实现。

参考文章

1、《内存管理之Tagged pointer》
2、《iOS标记指针(Tagged Pointer)技术》

autoreleasepool 深度解析

什么是 autoreleasepool

autoreleasepool(自动释放池)是 Objective-C 和 Swift 中用于延迟对象释放的一种内存管理机制。它允许你将对象的释放操作"延迟"到池被销毁时,而不是立即执行。

底层数据结构

自动释放池是一个结构体对象,在底层维护一个以栈为节点的双向链表,它是有所属的线程的。

AutoreleasePoolPage 类,它维护一个栈结构,自动释放池维护多个page为节点的双向链表。

class AutoreleasePoolPage {
        magic_t const magic;
        id *next; // 指向栈的下一个空闲地址
        pthread_t const thread; //所属线程
        AutoreleasePoolPage * const parent; //指向链表前一个节点
        AutoreleasePoolPage *child; //指向链表后一个节点
        uint32_t const depth;
        uint32_t hiwat;
};

AutoreleasePoolPage 类(每个池由多个 Page 组成)

  • 每个 Page 大小为 4096 字节(一页内存)
  • 以双向链表形式连接
  • 包含以下关键部分:

    • magic:校验字段
    • next:指向下一个空闲位置
    • thread:所属线程
    • parentchild:构成双向链表

工作原理

入栈过程(添加对象)

  1. 当对象调用 autorelease 时:

    [obj autorelease]; // 相当于压栈
  2. 系统会获取当前线程的"hot page"(活跃页面)
  3. 将对象地址存入 next 指向的位置,然后 next++

出栈过程(释放对象)

  1. 当池销毁时(objc_autoreleasePoolPop):
  2. 从当前页面的 next 指针开始向前遍历
  3. 对每个对象发送 release 消息
  4. 重置 next 指针位置

多页面管理

当单个页面空间不足时,系统会:

  1. 创建新的 AutoreleasePoolPage 作为 child
  2. 新页面成为新的"hot page"
  3. 形成双向链表结构:
[Page1] <--> [Page2] <--> [Page3]
  ↑             ↑           ↑ 
parent        parent      (最新页面)

为什么说是栈结构?

尽管物理实现是链表,但从行为上看是栈:

  • 压栈autorelease 操作相当于 push
  • 弹栈pool pop 操作相当于批量 pop
  • 嵌套池:外层池在内层池销毁后依然有效

线程局部存储

每个线程有自己独立的自动释放池栈:

  • 通过 thread 字段关联特定线程
  • TLS(Thread Local Storage)存储当前页指针

实际内存布局示例

假设有3个对象被 autorelease:

AutoreleasePoolPage:
+---------------+ 
| magic         |
| next ───────┐ |
| thread      | |
| parent      | |
| child       | |
| depth       | |
| hiwat       | |
+---------------+ 
| obj1        | ← next 最初指向这里
| obj2        |
| obj3        | ← next 现在指向这里
| ...         |

这种设计既保持了栈的逻辑特性,又通过链表实现了动态扩容,是工程实践中经典的"逻辑栈-物理链表"实现方式。

处理大型数据时的作用

处理大型数据(如循环中创建大量临时对象)时,autoreleasepool 可以:

  1. 及时释放内存

    for i in 0..<100000 {
        autoreleasepool {
            let tempImage = processLargeImage(at: i) // 临时大对象
            // 使用tempImage...
        } // 这里tempImage会被立即释放
    }
  2. 避免内存峰值

    • 没有 autoreleasepool:所有临时对象会累积直到循环结束
    • 使用 autoreleasepool:每次迭代后立即释放

实际应用场景

  1. 主线程

    • RunLoop 每次循环会自动创建和销毁自动释放池
    • 所以主线程通常不需要手动管理
  2. 子线程

    DispatchQueue.global().async {
        autoreleasepool {
            // 处理大量数据
            let data = loadHugeData()
            process(data)
        } // 数据在这里被释放
    }
  3. Swift 与 Objective-C 交互

    • 纯 Swift 代码通常不需要 autoreleasepool
    • 但当调用 Objective-C API 返回自动释放对象时仍然有用

性能考虑

  1. 创建代价

    • 创建和销毁池本身有开销
    • 只应在处理大量临时对象时使用
  2. 最佳实践

    // 好:处理大量迭代时使用
    for item in hugeCollection {
        autoreleasepool {
            process(item)
        }
    }
    
    // 不好:单个小对象没必要
    autoreleasepool {
        let smallObject = createObject()
        use(smallObject)
    }

通过理解 autoreleasepool 的底层机制,我们可以在处理内存密集型操作时更精确地控制内存使用,避免不必要的内存增长。

题目

描述
对于小明生成的 n 个 1到 500 之间的随机整数,你需要帮助他完成以下任务:
1、删去重复的数字,即相同的数字只保留一个,把其余相同的数去掉;
2、然后再把这些数从小到大排序,按照排好的顺序输出。

你只需要输出最终的排序结果。
输入描述:
第一行输入一个整数 n(1 ≦ n ≦ 1000),代表小明生成的数字个数。
此后 n 行,第 i 行输入一个整数 a (1 ≦ a ≦ 500),代表小明生成的随机整数。
输出描述:
输出若干行,每行输出一个整数,代表输入数据排序后的结果。第一行输出最小的数字。
示例1
输入:

3
2
2
1

输出:

1
2

解题思路

1、输入的数值 1 - 500 之间,可以定义一个501个元素的数组,利用数组下标来快速去重。

代码

#include <stdio.h>
#include <stdlib.h>

int main() {
    int num = 0;
    scanf("%d",&num);
    int list[501] = {0}; // 初始化后,每个元素都是0
    while (num > 0) {
        int n = 0;
        scanf("%d",&n);
        if (n >= 1 && n <= 500) {
            list[n] = 1; //数组已存在n,则改变标识为1
        }
        num--;
    }

    // 遍历数组,只输出标识不为0的下标,即去重后的结果
    for(int i = 1; i <= 500; i++){
        int n = list[i];
        if(n > 0){
            printf("%d\n",i);
        }
    }

    return 0;
}

在 C 语言中,构建哈希表(Hash Table)是一种常见的实现高效查找、插入和删除操作的方法。哈希表的核心思想是通过哈希函数将键(key)映射到一个索引,然后将值(value)存储在该索引对应的位置。

以下是构建哈希表的详细步骤和示例代码:


1. 哈希表的基本结构

哈希表通常由以下部分组成:

  1. 哈希函数:将键映射到索引。
  2. 数组:存储键值对。
  3. 冲突解决机制:处理哈希冲突(如链地址法或开放地址法)。

2. 哈希函数

哈希函数的设计直接影响哈希表的性能。一个好的哈希函数应该:

  • 均匀分布键。
  • 计算速度快。

以下是一个简单的哈希函数示例(假设键是字符串):

unsigned int hash(const char *key, int tableSize) {
    unsigned int hashValue = 0;
    while (*key) {
        hashValue = (hashValue << 5) + *key++; // 使用移位和加法
    }
    return hashValue % tableSize; // 取模确保索引在数组范围内
}

3. 链地址法解决冲突

链地址法是一种常见的冲突解决机制。它的基本思想是将哈希表中每个索引位置存储为一个链表,所有哈希到同一索引的键值对都存储在该链表中。

数据结构定义:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TABLE_SIZE 100 // 哈希表大小

// 键值对结构
typedef struct Entry {
    char *key;
    int value;
    struct Entry *next; // 指向下一个键值对
} Entry;

// 哈希表结构
typedef struct HashTable {
    Entry *table[TABLE_SIZE]; // 存储链表的数组
} HashTable;

4. 哈希表操作

(1) 初始化哈希表

HashTable *createHashTable() {
    HashTable *ht = (HashTable *)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; i++) {
        ht->table[i] = NULL; // 初始化每个链表为空
    }
    return ht;
}

(2) 插入键值对

void insert(HashTable *ht, const char *key, int value) {
    unsigned int index = hash(key, TABLE_SIZE);

    // 创建新节点
    Entry *newEntry = (Entry *)malloc(sizeof(Entry));
    newEntry->key = strdup(key); // 复制键
    newEntry->value = value;
    newEntry->next = NULL;

    // 插入到链表头部
    if (ht->table[index] == NULL) {
        ht->table[index] = newEntry;
    } else {
        newEntry->next = ht->table[index];
        ht->table[index] = newEntry;
    }
}

(3) 查找键值对

int search(HashTable *ht, const char *key) {
    unsigned int index = hash(key, TABLE_SIZE);
    Entry *current = ht->table[index];

    // 遍历链表
    while (current != NULL) {
        if (strcmp(current->key, key) == 0) {
            return current->value; // 找到键,返回值
        }
        current = current->next;
    }

    return -1; // 未找到键
}

(4) 删除键值对

void delete(HashTable *ht, const char *key) {
    unsigned int index = hash(key, TABLE_SIZE);
    Entry *current = ht->table[index];
    Entry *prev = NULL;

    // 遍历链表
    while (current != NULL) {
        if (strcmp(current->key, key) == 0) {
            if (prev == NULL) {
                ht->table[index] = current->next; // 删除链表头部
            } else {
                prev->next = current->next; // 删除链表中间或尾部
            }
            free(current->key);
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }
}

(5) 释放哈希表

void freeHashTable(HashTable *ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        Entry *current = ht->table[i];
        while (current != NULL) {
            Entry *temp = current;
            current = current->next;
            free(temp->key);
            free(temp);
        }
    }
    free(ht);
}

5. 完整示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TABLE_SIZE 100

typedef struct Entry {
    char *key;
    int value;
    struct Entry *next;
} Entry;

typedef struct HashTable {
    Entry *table[TABLE_SIZE];
} HashTable;

// 哈希函数
unsigned int hash(const char *key, int tableSize) {
    unsigned int hashValue = 0;
    while (*key) {
        hashValue = (hashValue << 5) + *key++;
    }
    return hashValue % tableSize;
}

// 创建哈希表
HashTable *createHashTable() {
    HashTable *ht = (HashTable *)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; i++) {
        ht->table[i] = NULL;
    }
    return ht;
}

// 插入键值对
void insert(HashTable *ht, const char *key, int value) {
    unsigned int index = hash(key, TABLE_SIZE);

    Entry *newEntry = (Entry *)malloc(sizeof(Entry));
    newEntry->key = strdup(key);
    newEntry->value = value;
    newEntry->next = NULL;

    if (ht->table[index] == NULL) {
        ht->table[index] = newEntry;
    } else {
        newEntry->next = ht->table[index];
        ht->table[index] = newEntry;
    }
}

// 查找键值对
int search(HashTable *ht, const char *key) {
    unsigned int index = hash(key, TABLE_SIZE);
    Entry *current = ht->table[index];

    while (current != NULL) {
        if (strcmp(current->key, key) == 0) {
            return current->value;
        }
        current = current->next;
    }

    return -1;
}

// 删除键值对
void delete(HashTable *ht, const char *key) {
    unsigned int index = hash(key, TABLE_SIZE);
    Entry *current = ht->table[index];
    Entry *prev = NULL;

    while (current != NULL) {
        if (strcmp(current->key, key) == 0) {
            if (prev == NULL) {
                ht->table[index] = current->next;
            } else {
                prev->next = current->next;
            }
            free(current->key);
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }
}

// 释放哈希表
void freeHashTable(HashTable *ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        Entry *current = ht->table[i];
        while (current != NULL) {
            Entry *temp = current;
            current = current->next;
            free(temp->key);
            free(temp);
        }
    }
    free(ht);
}

int main() {
    HashTable *ht = createHashTable();

    insert(ht, "apple", 10);
    insert(ht, "banana", 20);
    insert(ht, "orange", 30);

    printf("apple: %d\n", search(ht, "apple"));   // 输出 10
    printf("banana: %d\n", search(ht, "banana")); // 输出 20
    printf("grape: %d\n", search(ht, "grape"));  // 输出 -1

    delete(ht, "banana");
    printf("banana after delete: %d\n", search(ht, "banana")); // 输出 -1

    freeHashTable(ht);
    return 0;
}

6. 总结

  • 哈希表是一种高效的数据结构,适用于快速查找、插入和删除操作。
  • 链地址法是解决哈希冲突的常见方法。
  • 在实际应用中,可以根据需求调整哈希函数、哈希表大小和冲突解决机制。

通过以上代码,你可以在 C 语言中实现一个简单的哈希表,并用于解决实际问题。

在 C 语言中,创建结构体并不一定每次都要用 malloc。是否使用动态内存分配(malloc)取决于结构体的使用场景和生命周期需求。以下是两种主要方式及其适用场景:


1. 静态分配(无需 malloc

直接在栈上创建结构体变量,无需手动分配或释放内存。
适用场景:结构体生命周期仅限于当前作用域(如函数内部临时使用)。

示例代码

#include <stdio.h>

typedef struct {
    int id;
    char name[20];
} Student;

void printStudent(Student s) {
    printf("ID: %d, Name: %s\n", s.id, s.name);
}

int main() {
    // 静态分配结构体
    Student s1 = {1, "Alice"}; // 直接初始化
    Student s2;
    s2.id = 2;                // 逐个赋值
    strcpy(s2.name, "Bob");

    printStudent(s1); // 输出: ID: 1, Name: Alice
    printStudent(s2); // 输出: ID: 2, Name: Bob

    return 0;
}

特点

  • 内存自动分配和释放(函数结束时栈内存自动回收)。
  • 无需手动管理内存,但作用域有限。

2. 动态分配(使用 malloc

通过 malloc 在堆上分配内存,需手动释放(free)。
适用场景

  • 结构体需要跨函数长期存在。
  • 结构体大小动态变化(如链表节点)。
  • 需要灵活控制生命周期。

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char *name; // 动态分配的字符串
} Student;

Student* createStudent(int id, const char *name) {
    Student *s = (Student*)malloc(sizeof(Student));
    if (s == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    s->id = id;
    s->name = (char*)malloc(strlen(name) + 1); // 为name分配内存
    strcpy(s->name, name);
    return s;
}

void freeStudent(Student *s) {
    free(s->name); // 先释放内部指针
    free(s);       // 再释放结构体
}

int main() {
    // 动态分配结构体
    Student *s = createStudent(3, "Charlie");
    printf("ID: %d, Name: %s\n", s->id, s->name); // 输出: ID: 3, Name: Charlie
    freeStudent(s); // 必须手动释放!

    return 0;
}

特点

  • 内存生命周期由程序员控制(需显式调用 free)。
  • 适合复杂或长期存在的数据结构。

3. 如何选择?

方式静态分配动态分配
内存位置栈(自动管理)堆(手动管理)
生命周期当前作用域结束自动释放需显式调用 free
灵活性固定大小可动态调整(如链表、动态数组)
典型场景临时变量、小型结构体跨函数传递、动态数据结构

4. 常见问题

Q1:什么时候必须用 malloc

  • 需要结构体在函数返回后仍然有效时(如返回结构体指针)。
  • 结构体包含动态分配的成员(如字符串、指针数组)。

Q2:忘记 free 会怎样?

导致内存泄漏,程序长时间运行可能耗尽内存。

Q3:静态分配的结构体可以返回吗?

不可以!函数返回后栈内存会被回收,返回指向局部结构体的指针是未定义行为:

// 错误示例!
Student* createStudent() {
    Student s = {1, "Alice"};
    return &s; // 返回后s的内存已失效!
}

5. 总结

  • 不需要 malloc:结构体仅在局部使用且无需动态扩展时。
  • 需要 malloc:结构体需长期存在、动态调整或包含动态成员时。
  • 始终记得配对使用 mallocfree 以避免内存泄漏!