为结构 C 中的指针提供适当的内存

Proper memory free for pointers inside struct C

提问人:fdv 提问时间:10/30/2023 最后编辑:Vlad from Moscowfdv 更新时间:11/2/2023 访问量:137

问:

我有一个包含一个类型的动态数组,这个数组的每个元素都指向两个类型的变量。在主要 我的程序,我通过 malloc 调用为数组和 对于每个元素指针。然后我为元素赋值,检查该值并释放内存。这是代码struct liststruct pairstruct elementstruct pair pairs

#include "stdlib.h"
#include "stdio.h"

struct element{
  int i;
};

struct pair{
  struct element *element1, *element2;
};

struct list{
  struct pair *pairs;
};

int main(void) {

  // declare variable my_list
  struct list my_list;

  // set size of pairs array (one for the example)
  my_list.pairs = (struct pair*)malloc(sizeof(struct pair));

  // set element1 and element2 of pairs[0]
  struct element element1 = {.i = 1};
  struct element element2 = {.i = 2};
  my_list.pairs[0].element1 = (struct element*)malloc(sizeof(struct element));
  my_list.pairs[0].element2 = (struct element*)malloc(sizeof(struct element));
  my_list.pairs[0].element1 = &element1;
  my_list.pairs[0].element2 = &element2;

  // check element values
  printf("elements are: %d %d\n", my_list.pairs[0].element1->i, my_list.pairs[0].element2->i); 

  // free memory
  free(my_list.pairs);

  return 0;
}

我可以毫无问题地编译和运行代码,但是如果我在 valgrind 中运行代码,我会得到以下内存泄漏:

*** valgrind --leak-check=full -s ./a.out 
==6676== Memcheck, a memory error detector
==6676== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==6676== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==6676== Command: ./a.out
==6676== 
elements are: 1 2
==6676== 
==6676== HEAP SUMMARY:
==6676==     in use at exit: 8 bytes in 2 blocks
==6676==   total heap usage: 4 allocs, 2 frees, 1,048 bytes allocated
==6676== 
==6676== 4 bytes in 1 blocks are definitely lost in loss record 1 of 2
==6676==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==6676==    by 0x1091EE: main (foo.c:27)
==6676== 
==6676== 4 bytes in 1 blocks are definitely lost in loss record 2 of 2
==6676==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==6676==    by 0x1091FF: main (foo.c:28)
==6676== 
==6676== LEAK SUMMARY:
==6676==    definitely lost: 8 bytes in 2 blocks
==6676==    indirectly lost: 0 bytes in 0 blocks
==6676==      possibly lost: 0 bytes in 0 blocks
==6676==    still reachable: 0 bytes in 0 blocks
==6676==         suppressed: 0 bytes in 0 blocks
==6676== 
==6676== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

第 27 行和第 28 行是泄漏的罪魁祸首。如果我尝试添加(在第 36 行之前),我会在 valgrind 中出现以下错误free(my_list.pairs[0].element1);

==6684== 1 errors in context 1 of 3:
==6684== Invalid free() / delete / delete[] / realloc()
==6684==    at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==6684==    by 0x10924F: main (foo.c:36)
==6684==  Address 0x1ffefff918 is on thread 1's stack
==6684==  in frame #1, created by main (foo.c:16)

所以我的问题是:

  • 如何避免内存泄漏并正确释放第 27 行和第 28 行分配的内存?
  • 为什么试图释放指针上升错误?my_list.pairs[0].element1Invalid free() / delete / delete[] / realloc()

代码已使用 gcc 编译如下: 我的 gcc 版本是gcc -Wall -g foo.c

gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
c struct malloc 自由

评论

3赞 Some programmer dude 10/30/2023
my_list.pairs[0].element1 = (struct element*)malloc(sizeof(struct element));后跟 将覆盖指针。这与 You 替换变量的值并丢失原始值的情况类似。my_list.pairs[0].element1 = &element1;int a; a = 10; a = 5;
0赞 n. m. could be an AI 10/30/2023
旁白:不要投出结果,见 stackoverflow.com/questions/605845/...malloc
0赞 n. m. could be an AI 10/30/2023
另请参阅 stackoverflow.com/questions/72330113/...
0赞 Lundin 10/30/2023
如果您放弃 malloc 并使用普通结构变量而不是指针,这将是一个更简单、更快速的数据类型执行集合。
0赞 fdv 10/30/2023
@Lundin感谢您的评论。这段代码只是最小的指令集,用于重现我在较长版本的代码中遇到的问题。我需要一个指针结构,因为代码正在读取一个包含点之间连接的文件(STL 格式),并且我事先不知道点数和连接数

答:

2赞 Support Ukraine 10/30/2023 #1

您的内存泄漏在这里:

my_list.pairs[0].element1 = (struct element*)malloc(sizeof(struct element));
my_list.pairs[0].element2 = (struct element*)malloc(sizeof(struct element));
my_list.pairs[0].element1 = &element1; // overwrite of my_list.pairs[0].element1 --> memory leak
my_list.pairs[0].element2 = &element2; // overwrite of my_list.pairs[0].element2 --> memory leak

溶液:

选项 1

删除两行 doing 。malloc

struct element element1 = {.i = 1};
struct element element2 = {.i = 2};
my_list.pairs[0].element1 = &element1;
my_list.pairs[0].element2 = &element2;

选项 2

删除创建和使用局部变量的行。

my_list.pairs[0].element1 = malloc(sizeof(struct element));
my_list.pairs[0].element2 = malloc(sizeof(struct element));
my_list.pairs[0].element1->i = 1;
my_list.pairs[0].element2->i = 2;

....
....

free(my_list.pairs[0].element1);
free(my_list.pairs[0].element2);

注意:在 C 语言中,你不需要强制转换malloc

3赞 H.S. 10/30/2023 #2

在这里,您的程序正在泄漏内存:

  my_list.pairs[0].element1 = &element1;
  my_list.pairs[0].element2 = &element2;

原因是 - 并且持有动态分配的内存引用,在上述分配之后,它们会丢失这些内存引用。因此,导致内存泄漏。my_list.pairs[0].element1my_list.pairs[0].element2

请注意,在您的代码中,和不是动态分配的对象。它们是在堆栈上创建的对象:element1element2

struct element element1 = {.i = 1};   
struct element element2 = {.i = 2};

这对他们来说无效:free

free(my_list.pairs[0].element1); 

free只能用于由 、 或 分配的指针,否则将导致未定义的行为。malloc()calloc()aligned_alloc()realloc()

要解决此问题,要么不要将内存分配给 and 动态分配,要么动态分配它们,并在完成它们后分配它们。后文建议的实施情况:my_list.pairs[0].element1my_list.pairs[0].element2free

  struct element * element1 = (struct element*)malloc(sizeof(struct element));
  struct element * element2 = (struct element*)malloc(sizeof(struct element));
  element1->i = 1;
  element2->i = 2;
  my_list.pairs[0].element1 = element1;
  my_list.pairs[0].element2 = element2;

  // check element values
  printf("elements are: %d %d\n", my_list.pairs[0].element1->i, my_list.pairs[0].element2->i); 

  // free memory
  free(my_list.pairs[0].element1);
  free(my_list.pairs[0].element2);

附加:

遵循良好的编程实践,确保检查 的返回值。malloc()

评论

0赞 fdv 10/30/2023
struct element element1并且是静态对象,我同意。但是 和 是动态分配的指针。因此,我希望它们应该是一个有效的操作。我在这里遗漏了什么吗?struct element element2my_list[0].pairs.element1my_list[0].pairs.element2free
0赞 H.S. 10/30/2023
@fdv和松开这些分配后的动态分配的内存引用 - 和 .您正在动态地为它们分配内存,然后,您将为它们在堆栈上创建的对象的地址分配。my_list[0].pairs.element1my_list[0].pairs.element2my_list.pairs[0].element1 = &element1;my_list.pairs[0].element2 = &element2;
1赞 Vlad from Moscow 10/30/2023 #3

这些陈述

  my_list.pairs[0].element1 = (struct element*)malloc(sizeof(struct element));
  my_list.pairs[0].element2 = (struct element*)malloc(sizeof(struct element));
  my_list.pairs[0].element1 = &element1;
  my_list.pairs[0].element2 = &element2;

产生内存泄漏。

首先分配内存并将其地址分配给指针,然后使用局部变量和my_list.pairs[0].element1my_list.pairs[0].element2element1element2

  // set element1 and element2 of pairs[0]
  struct element element1 = {.i = 1};
  struct element element2 = {.i = 2};

因此,已分配内存的加分项丢失了。

而不是使用此分配

  my_list.pairs[0].element1 = &element1;
  my_list.pairs[0].element2 = &element2;

你应该写

  *my_list.pairs[0].element1 = element1;
  *my_list.pairs[0].element2 = element2;

复制分配的内存的值。element1element2

要正确释放分配的内存,您需要以相反的顺序释放它。那是

free( my_list.pairs[0].element1 );
free( my_list.pairs[0].element2 );
free( my_list.pairs );

至于你的代码,那么这个声明

  // free memory
  free(my_list.pairs);

在分配了三个内存盘区时,仅释放第一个分配的内存盘区

  my_list.pairs = (struct pair*)malloc(sizeof(struct pair));
  //...
  my_list.pairs[0].element1 = (struct element*)malloc(sizeof(struct element));
  my_list.pairs[0].element2 = (struct element*)malloc(sizeof(struct element));
-1赞 arfneto 10/31/2023 #4

在这一点上,关于为什么你有内存泄漏,你有一些很好的答案,但我将在这里留下一个创建这种有用的东西的方法的例子。或忽略。:)

从原始代码:

为了清楚起见,我换了。也。element1el1el2

    my_list.pairs[0].el1 =  (struct element*)malloc(sizeof(struct element));
    my_list.pairs[0].el2 =  (struct element*)malloc(sizeof(struct element));
    my_list.pairs[0].el1 = &el1;
    my_list.pairs[0].el2 = &el2;

重新排序行后:

    my_list.pairs[0].el1 =  (struct element*)malloc(sizeof(struct element));
    my_list.pairs[0].el1 = &el1;

    my_list.pairs[0].el2 =  (struct element*)malloc(sizeof(struct element));
    my_list.pairs[0].el2 = &el2;

它清楚地表明,一旦第 2 行和第 4 行运行,从返回的每个区域都将永远消失,因为在每种情况下,该区域的单个指针都被覆盖了。malloc

    // declare variable my_list
    struct list my_list;

也许你可以避免这种类型的评论。声明的作用很清楚。

示例

我们这里有一个货币对列表。可以实现为数组、链表、字典......javaPair as of Key 和 Value 的东西C++ std::p air 中有 pair of firstsecond 作为例子。并且可以是字符串列表的列表。事物的任何 3 级层次结构。

我将向你展示一种编写方法,使用封装,并查看你的对和列表,更像对象。这样做可以更有效率,因为它使用大量的复制和粘贴,并且简单地重用这些其他语言中的内容,并且(也很重要)形成一种语言。struct

层次结构:成对的元素列表

我们将像一个简单的指针数组一样使用固定容量的指针。它可以通过任何其他方式实现,例如使用链表或简单数组。但列表就是这样:一个(可能是空的)事物集合。在这种情况下,成对的东西。因为有什么东西没有区别。这实质上是封装。在这里,请注意,列表不引用,仅作为不透明对象引用。ListListElementPairs

名单List

#ifndef LIST_H
#define LIST_H

#include <stdio.h>
#include "pair.h"

typedef struct
{
    size_t size;
    size_t limit;
    Pair** pair;
} List;

List* make_list(size_t);
List* delete_list(List*);
int   insert_list(Pair*, List*);
int   show_list(List*, const char*);

#endif // LIST_H

因此,列表中包含其实际大小和容量的值。这些函数在其他语言中称为方法,不引用元素。甚至可以使用通用名称行或 .PairInfoNode

  • make_list返回指向具有请求容量的新空列表的指针
  • delete_list销毁列表并返回 。为什么?这样我们就可以删除一个并同时使其指针失效,以确保安全。NULLList
  • insert_list是否符合预期:将记录插入到列表中。在本例中,一个Pair
  • show_list在屏幕上显示列表的元素。要保存一些呼叫,可以添加前缀消息。printf

这就像其他地方/语言中的构造函数、析构函数和 toString序列化方法。仅使用指针。这更容易。

可能的实现List.c

#include "list.h"

List* make_list(size_t size)
{
    List* one = malloc(sizeof(List));
    if (one == NULL) return NULL;
    one->limit = size;
    one->size  = 0;
    one->pair  = (Pair**)malloc(size * sizeof(Pair*));
    if (one->pair == NULL)
    {
        free(one);
        return NULL;
    }
    return one;
}

List* delete_list(List* L)
{
    if (L == NULL) return NULL;
    fprintf(
        stderr, "\n    deleting %llu elements list\n",
        L->size);
    for (size_t i = 0; i < L->size; i += 1)
        delete_pair(L->pair[i]);
    free(L);
    return NULL;
}

int insert_list(Pair* p, List* l)
{
    if (p == NULL) return -1;
    if (l == NULL) return -2;
    if (l->size == l->limit) return -3;
    Pair* pair       = make_pair(p->first, p->second);
    l->pair[l->size] = pair;
    l->size += 1;
    return 0;
}

int show_list(List* L, const char* msg)
{
    if (L == NULL) return -1;
    if (msg != NULL) printf("%s", msg);
    printf(
        "[%llu/%llu] element(s) in list\n", L->size,
        L->limit);
    for (size_t i = 0; i < L->size; i += 1)
        show_pair(L->pair[i], NULL);
    printf("\n");
    return 0;
}

这对Pair.h

#ifndef PAIR_H
#define PAIR_H
#include <stdio.h>
#include <stdlib.h>
#include "el1.h"

typedef struct
{
    Element* first;
    Element* second;
} Pair;

Pair* delete_pair(Pair*);
Pair* make_pair(Element*, Element*);
int   show_pair(Pair*, const char*);
#endif // PAIR_H

这里我们有成对的命名:元素是 和 。Java 使用 AND 来表示键和值。C++firstsecondKV

  • make_pair返回带有提供的元素的 A。Pair
  • show_pair按预期执行。
  • delete_pair释放一对并返回 。NULL

这些只是复制和改编的。仅使用指针。List

可能的实现Pair.c

#include "pair.h"
Pair* delete_pair(Pair* gone)
{
    if (gone == NULL) return NULL;
    delete_el(gone->first);
    delete_el(gone->second);
    free(gone);
    return NULL;
}

Pair* make_pair(Element* a, Element* b)
{
    if (a == NULL) return NULL;
    if (b == NULL) return NULL;
    Pair* one = malloc(sizeof(Pair));
    if (one == NULL) return NULL;
    one->first  = copy_el(a);
    one->second = copy_el(b);
    return one;
}

int show_pair(Pair* p, const char* msg)
{
    if (p == NULL) return -1;
    if (msg != NULL) printf("%s", msg);
    printf("  [");
    show_el(p->first, NULL);
    printf(",");
    show_el(p->second, NULL);
    printf("]\n");
    return 0;
}

元素,终于

这是定制内容的地方。我将使用 2 个:Element

typedef struct
{
    int i;
} Element;

typedef struct
{
    char* name;
    int   code;
} Element;

第一个与问题相同。第二个只是为了说明一个可以分配内存,里面可以有指针,所以我们不能只把它的值复制到一个里面。ElementPair

因此,为了处理此类内容,用户需要提供方法。这样做---并且---像其他语言一样从一个程序到另一个程序保持不变。 具有标头。PairPairListpair.h#includeElement

这就是我们在提供 compare 函数时所做的,因此代码可以对任何内容进行排序。qsort

引用可以用其他更聪明的方式完成,但这里只是一个玩具,展示如何快速构建这些东西,所以使用了 an。Element#include

元素所需的函数

Element* copy_el(Element*);
Element* delete_el(Element*);
int      show_el(Element*, const char*);

它与其他“类”几乎相同:

  • copy_el复制构造函数,这就是我们在 a 中插入一个元素所需要的: 它返回元素副本的地址,因此它归列表所有,可以插入到ListPair
  • delete_el是析构函数,因为 可能有复杂的分配问题。Element
  • show_el是一种在屏幕上显示Element

使用int

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

int main(void)
{
    Element* one     = make_el(42);
    Element* other   = make_el(-42);
    Pair*    my_pair = make_pair(one, other);
    delete_el(one);
    delete_el(other);

    show_pair(my_pair, "test_pair is ");
    List* my_list = make_list(5);  // capacity = 5
    show_list(my_list, "\n(list still empty) ");
    while (0 == insert_list(my_pair, my_list)) {};
    show_list(
        my_list, "\n(list now filled with same pair) ");
    my_pair = delete_pair(my_pair);  // free test_pair
    my_list = delete_list(my_list);  // free list
    return 0;
}
  • make_el只是在这些示例中创建 a 的助手。容器只需要知道如何复制一个,而不需要知道如何创建一个Element

这个程序

  • 创建一个Pair
  • 创建一个 5 个元素的 ' List
  • 用这对填充列表,因为内容在这里无关紧要。
  • 在屏幕上显示列表
  • free的everithing

输出

test_pair is   [42,-42]

(list still empty) [0/5] element(s) in list


(list now filled with same pair) [5/5] element(s) in list
  [42,-42]
  [42,-42]
  [42,-42]
  [42,-42]
  [42,-42]


    deleting 5 elements list

简单易碎的实现Element

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

typedef struct
{
    int i;
} Element;

Element* copy_el(Element*);
Element* delete_el(Element*);
Element* make_el(const int);
int      show_el(Element*, const char*);

Element* copy_el(Element* orig)
{
    if (orig == NULL) return NULL;
    Element* one = malloc(sizeof(Element));
    if (one == NULL) return NULL;
    one->i = orig->i;
    return one;
}

Element* delete_el(Element* orig)
{
    if (orig == NULL) return NULL;
    free(orig);
    return NULL;
}

Element* make_el(const int value)
{
    Element* one = malloc(sizeof(Element));
    if (one == NULL) return NULL;
    one->i = value;
    return one;
}

int show_el(Element* el, const char* msg)
{
    if (el == NULL) return -1;
    if (msg != NULL) printf("%s", msg);
    printf("%d", el->i);
    return 0;
}

同样,这里只是为了示例。容器---任何一个---本身不会创建元素。它只是在需要时复制一个,并在需要时销毁。make_el

使用 分配内存的Elementstruct

这是一个内部具有指针并分配内存的示例。它可以以相同的方式使用,因为用户提供了复制构造函数

main对于这种情况

这里当然不同,但这只是为了示例。 看不到这一点,也不会创建元素,只是复制它们。当然,用户有一些项目需要管理,并且知道如何去做。只要有效,就可以“列出”。只要作品能做好自己的工作。make_elListPairListMagicStuffcopy_elshow_elList

事实上,指向这些函数的指针可以添加,我们将拥有更接近 C++ 的东西。List

只有前五行是不同的,因为我们需要构建一对元素。代码的其余部分是相同的。更重要的是,代码 和 是相同的。PairList

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

int main(void)
{
    Element* one     = make_el("Stack", 42);
    Element* other   = make_el("Overflow", -42);
    Pair*    my_pair = make_pair(one, other);
    delete_el(one);
    delete_el(other);
    show_pair(my_pair, "test_pair is ");

    List* my_list = make_list(5);  // capacity = 5
    show_list(my_list, "\n(list still empty) ");

    while (0 == insert_list(my_pair, my_list)) {};
    show_list(
        my_list, "\n(list now filled with same pair ");

    my_pair = delete_pair(my_pair);  // free test_pair
    my_list = delete_list(my_list);  // free list
    return 0;
}

outout 对于此示例

test_pair is   [ ("Stack",42), ("Overflow",-42)]

(list still empty) [0/5] element(s) in list


(list now filled with same pair [5/5] element(s) in list
  [ ("Stack",42), ("Overflow",-42)]
  [ ("Stack",42), ("Overflow",-42)]
  [ ("Stack",42), ("Overflow",-42)]
  [ ("Stack",42), ("Overflow",-42)]
  [ ("Stack",42), ("Overflow",-42)]


    deleting 5 elements list

一个简单的实现,用于非平凡的Element

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

typedef struct
{
    char* name;
    int   code;
} Element;

Element* copy_el(Element*);
Element* delete_el(Element*);
int      show_el(Element*, const char*);

Element* copy_el(Element* orig)
{
    if (orig == NULL) return NULL;
    Element* one = malloc(sizeof(Element));
    if (one == NULL) return NULL;
    one->name = malloc(1 + strlen(orig->name));
    if (one->name == NULL)
    {
        free(one);
        return NULL;
    }
    strcpy(one->name, orig->name);
    one->code = orig->code;
    return one;
}

Element* delete_el(Element* orig)
{
    if (orig == NULL) return NULL;
    free(orig->name);
    free(orig);
    return NULL;
}

int show_el(Element* el, const char* msg)
{
    if (el == NULL) return -1;
    if (msg != NULL) printf("%s", msg);
    printf(" (\"%s\",%d)", el->name, el->code);
    return 0;
}

Element* make_el(const char* name, const int code)
{
    if (name == NULL) return NULL;
    Element* one = malloc(sizeof(Element));
    if (one == NULL) return NULL;
    one->name = malloc(1 + strlen(name));
    if (one->name == NULL)
    {
        free(one);
        return NULL;
    }
    strcpy(one->name, name);
    one->code = code;
    return one;
}```

### A note on *encapsulation* ###

Here in `insert_list` a pair is inserted
```C
    Pair* pair       = make_pair(p->first, p->second);

但做到这一点的代码来自实现。Pair

但是在make_pair

    one->first  = copy_el(a);
    one->second = copy_el(b);

我们看到使用了用户提供的副本创建器。

关于构建此内容的说明

  • 每个级别的容器通常都会添加一对文件、一个头文件和实现文件。.h.c
  • 每个级别只能看到下面的级别,并且代码只是复制和改编
  • 通常有一个构造函数、一个析构函数、一个复制构造函数、一个打印函数,声明为 here 或遵循某种模式。它通常工作正常,因为这是在其他领域和语言中所做的。

代码和 Visual Studio 项目可在此链接的 GitHub 上找到

评论

0赞 Fe2O3 10/31/2023
这么多片段,但没有为函数提供示例代码......而且,当将来存储的需求发生变化时,编码人员会后悔在这么多函数名称甚至文件名上留下当前的“配对”印象......很高兴不是那个家伙!*_el()Element* third;
0赞 arfneto 10/31/2023
@Fe2O3这很有趣!我忘了把这些贴在:)谢谢你指出这一点!至于代码和名称,不难理解我解释的内容。它像 javaC++ *容器一样实现,由外而内。这里只有 2 个级别。而且名字是一样的......
0赞 Fe2O3 10/31/2023
“好笑吗?”有趣的是,发布了这么长的答案,颁布了 OP 的短视,命名了一个结构体及其成员。您已将 OP 的 & 重命名为 和 。因此,数据类型、函数甚至文件和标头保护的名称都与“配对”绑定。我的问题是:这些项目何时加入和将由加入会发生什么?三个结构成员不是“一对”。对 Makefile 的更改也是如此!进行更改时会出现很多错误!有趣。。。pairpairselement1element2firstsecondfirstsecondthird
0赞 arfneto 10/31/2023
@Fe2O3更改为任何其他事情都需要再次执行我解释过的操作:复制和重写构造函数、析构函数、复制结构器和打印。这是预期的,因为它是一个新。然后更改上面容器上的函数以指向新函数。它在许多语言中运行良好。 正如我所说,是 中使用的名称。PairPairinsertfirstsecondC++
0赞 Fe2O3 10/31/2023
C 不是 C++。而且,为什么不简单地调用来减少代码重复呢?copy_el()make_el()