在 C 中的堆栈 and/or 堆上实现用户定义的数组

Implementation of user defined array on stack and/or heap in C

我在学习 C 语言编程时试图了解 堆栈 之间的区别。
为此,我尝试实现二分查找。我想从用户那里获取二分查找的输入数据集。为此,我希望用户能够定义数据集的大小(在本例中为数组)。获得数组的大小后,我会初始化数组,然后要求用户用值填充它。

我对堆栈的(可能是错误的)理解是,如果在编译时知道数组的大小,则可以在堆栈上初始化数组。所以,我尝试了以下方法来实现堆栈上的用户输入(它 'works'):

void getUserInput(int sizeArray, int inputArray[], int userIn[]){
    /* get input data set and the element to be searched for from user */

    printf("Enter data elements seperated by a comma, e.g. 1,2,3. Press Enter at the end. Do not enter more than %d numbers: \n", sizeArray);
    for (int i = 0; i < sizeArray; i++){
        scanf("%d, ", &inputArray[i]);
    }
    printf("\nEnter the number to be searched for: \n");
    scanf("%d", &userIn[1]);

    printf("\nFor iterative implementation, enter 0. For recursive implementation, enter 1 :\n");
    scanf("%d", &userIn[0]);
}

int main(int arg, char **argv){
   int sizeArray;
   int userIn[2];                                                  // userIn[1]: element to be searched for; userIn[0]: iterative or recursive implementations
   printf("Enter size of input array: \n");
   scanf("%d", &sizeArray);
        
   int inputArray[sizeArray];
        
   getUserInput(sizeArray, inputArray, userIn);

// more code ...

}

对于堆上的实现,我尝试使用动态内存分配(它也 'works'):

int main(int arg, char **argv) {
    int sizeArray;
    int userIn[2];                                                  // userIn[1]: element to be searched for; userIn[0]: iterative or recursive implementations
    printf("Enter size of input array: \n");
    scanf("%d", &sizeArray);

    int *inputArray;

    inputArray = (int*) malloc(sizeArray * sizeof(int));
    if(inputArray == NULL) {
        printf("\n\nError! Memory not allocated, enter size of array again:\n");
        scanf("%d", &sizeArray);
        inputArray = (int*) malloc(sizeArray * sizeof(int));
    }
    getUserInput(sizeArray, inputArray, userIn);

// more code...
    free(inputArray)                                               // free memory allocated by malloc on the heap
}

现在,我想将这两种方法合并到一个文件中,所以我创建了一个开关来在堆栈和堆实现之间切换,如下所示:

int main(int arg, char **argv) {
    /* Obtain input */
    int stackHeap;                                                      // 0 = implementation on stack; 1 = implementation on heap
    printf("Implementation on stack or heap? Enter 0 for stack, 1 for heap: \n");
    scanf("%d", &stackHeap);

    int sizeArray;
    int userIn[2];                                                  // userIn[1]: element to be searched for; userIn[0]: iterative or recursive implementations
    printf("Enter size of input array: \n");
    scanf("%d", &sizeArray);
    int *inputArray;

    if (stackHeap == 0) {
        inputArray =  &inputArray[sizeArray];    
        printf("input array = %p\n", inputArray);
    } else {
        inputArray = (int*) malloc(sizeArray * sizeof(int));
        printf("input array = %p\n", inputArray);
        if(inputArray == NULL) {
            printf("\n\nError! Memory not allocated, enter size of array again:\n");
            scanf("%d", &sizeArray);
            inputArray = (int*) malloc(sizeArray * sizeof(int));
        }
    }

    getUserInput(sizeArray, inputArray, userIn);
// more code...
}

目前堆栈方法不起作用。我尝试在 if 语句中初始化 inputArray,而不是 inputArray = &inputArray[sizeArray]。然而,这是不允许的,因为它只在 if 语句的范围内有效。我想我对如何使用指针 *inputArray 来初始化堆栈上的数组感到困惑。

我一直在阅读 C 中的指针和数组,这就是为什么我认为实现它会很有趣。我非常感谢您的任何反馈(很高兴也很高兴我犯了任何基本错误 - 我对这个话题很陌生)。

非常感谢!

您的程序仅使用堆栈分配或仅使用堆分配都很好。堆栈分配的唯一潜在问题是,如果数组太大(即几 MB),它可能会溢出堆栈,而堆有一个 much 更高的限制。

组合程序的问题在于这没有意义:

inputArray =  &inputArray[sizeArray]; 

使用数组索引运算符实际上涉及取消引用指针,此时 inputArray 没有指向任何地方。

您需要选择其中之一,数组或指向动态分配数组开头的指针。

typedef enum
{
    STACK,
    HEAP,
}type;

size_t getSize(void)  // implement user input
{
    return 100;
}

type getType(void)   // implement user input
{
    return HEAP;
}

int main(void)
{
    size_t stackSize = 0;
    size_t size;
    type tp;
    int *inputArray;

    size = getSize();
    tp = getType();

    if(tp == STACK) stackSize = size;

    int array[stackSize];

    if(tp == STACK)
        inputArray = array;
    else
        inputArray = malloc(size * sizeof(*inputArray));

    /* ... */
}

TL;DR:除非您需要它们的特殊语义,否则不要使用 VLA。它们很棘手并且容易出错;使用堆通常更容易和更安全。

您 运行 遇到的基本问题是定义 VLA 是一个 声明 ,因此无法有条件地创建一个在条件块。这几乎是设计使然 - 该块用于限制生命周期,因此可以自动释放它。

因此您的选择是无条件地创建一个(如果 stackHeap 为真则不使用它)或将您所有的 ...更多代码... 移动到条件块中。后者最容易通过将其全部放在一个函数中来实现(例如 processArray(int sizeArray, int inputArray[],... 很像您的 getUserInput 函数)。然后你只需要在 if 和 else 情况下调用函数两次。

I am trying to understand the difference between the stack and the heap and whilst learning programming in C.

那是你的第一个错误。堆栈和堆不是 C 语言的概念,C 语言不依赖于内存区域之间的任何此类区别。话虽如此,许多 C 实现实际上确实依赖于它们,因此了解它们可能是值得的。然而,就学习 C 语言而言,最好专注于相关的原生 C 概念。

在这个特定领域,这些将是对象的 生命周期 存储持续时间 的相关概念。对象的生命周期是程序执行的一部分,在此期间该对象存在并保留其最后存储的值,该值是其存储持续时间的函数,对于某些对象而言,还取决于它们的创建时间。存储持续时间由对象定义的位置(如果有)和应用于它的类型限定符定义。

在代码块内没有 static 限定符声明的对象具有“自动”存储持续时间,并且从进入块到块执行终止都有效。在基于堆栈的机器上,此类对象的存储通常分配在堆栈上。其他存储持续时间是“静态”、“已分配”和“线程”,并且在基于堆栈的机器上,具有这些持续时间的对象的存储通常在堆栈外部实现,有些人将其描述为在堆上。

My (potentially wrong) understanding of the stack is, that the array can be initialised on the stack if its size is known at compile time.

这有点不准确。

一方面,如果在编译时已知所需的大小,那么您当然可以为具有该大小的自动持续时间数组编写有效的声明。但是,这并不能保证程序真的可以在运行的时候得到想要的space。当声明具有非常大的存储大小的对象时,这是一个特殊的问题:当程序执行到必须分配此类对象的点时,面向堆栈的 C 实现可能没有足够的堆栈 space 可用。这就是该站点命名的“堆栈溢出”。

另一方面,所有符合标准的 C99 实现和大多数 C11 和 C17 实现都支持可变长度数组,其大小在 运行 时确定,并且可以是自动对象的类型。事实上,这就是您在第一个示例中为变量 inputArray 使用的内容。该代码有效。一些实现也可能提供其他机制来分配可变长度数据作为自动存储。例如,参见 alloca()

For an implementation on the heap, I attempted to use dynamic memory allocation

这会为您提供一个具有“已分配”存储持续时间的对象。在您可以描述为维护堆栈/堆区别的实现中,此类对象的存储确实通常在堆上。

Now, I wanted to combine both approaches into one file, so I created a switch to switch between the stack and heap implementations

这是可行的,但不是您尝试的方式。宣布...

    int *inputArray;

...您需要先将该指针指向某物,然后才能访问其元素。在一种情况下,您通过使用 malloc 分配内存来做到这一点,但在另一种情况下,这 ...

        inputArray =  &inputArray[sizeArray];    

... 根本没有按照您的意愿行事。与在表达式而不是声明中一样,inputArray[sizeArray] 不会导致分配任何内存。相反,它会尝试访问 inputArray 指向的对象中索引 sizeArray 处的 int。由于 inputArray 在这一点上还没有被分配一个值,这会产生未定义的行为。因此,确实,

Currently the stack approach doesn't work.

你也说得对

initialising the inputArray within the if statement [is] not allowed, since it is then only valid within the scope of the if statement.

所以当你说,

I think I am getting confused as how to use the pointer *inputArray to initialise the array on the stack.

,我倾向于同意。事实上,我认为你错过了一个关键点,那就是在任何一种情况下你都不能使用指针来初始化数组,也不能使用类似的东西。相反,您必须分配数组,并或多或少地分别分配指向它的指针。

您清楚地知道如何用 malloc 做到这一点,但我想您并没有从这些方面考虑。你这样做的方式是有效的,因为通过 malloc 分配的对象具有“分配的”存储持续时间,这意味着它的生命周期持续到它被释放为止。要对自动对象执行类似操作,您需要一个具有足够生命周期的对象来满足您的目的,在这种情况下,这意味着它必须直接在 main() 的主体内声明,而不是在任何嵌套块中声明。您可以这样实现:

    int sizeArray;
    int sizeAutomatic = 1;

    printf("Enter size of input array: \n");
    scanf("%d", &sizeArray);

    if (stackHeap == 0) {
        sizeAutomatic = sizeArray;
    }

    int automaticArray[sizeAutomatic];
    int *inputArray;

    if (stackHeap == 0) {
        inputArray = automaticArray;
        // or, equivalently: inputArray = &automaticArray[0];
    } else {
        inputArray = malloc(sizeArray * sizeof(int));
        if(inputArray == NULL) {
            /// handle allocation error ...
        }
    }

    printf("input array = %p\n", (void *)inputArray);

如果用户选择自动分配选项,inputArray 将指向 automaticArray 的第一个元素,有效地使两个别名成为彼此。在另一种情况下,inputArray 将指向一个分配的对象。 automaticArray 对象在后一种情况下仍然存在,但它的长度为 1 并且未被使用。这一 int 开销是您为允许用户决定使用哪种分配方法而付出的代价的一部分。