3

I've written a function that returns an array whilst I know that I should return a dynamically allocated pointer instead, but still I wanted to know what happens when I am returning an array declared locally inside a function (without declaring it as static), and I got surprised when I noticed that the memory of the internal array in my function wasn't deallocated, and I got my array back to main. The main:

int main()
{
    int* arr_p;
    arr_p = demo(10);

    return 0;
}

And the function:


int* demo(int i)
{
    int arr[10] = { 0 };
    for (int i = 0; i < 10; i++)
    {
        arr[i] = i;
    }
    return arr;
}

When I dereference arr_p I can see the 0-9 integers set in the demo function. Two questions:

  1. How come when I examined arr_p I saw that its address is the same as arr which is in the demo function?
  2. How come demo_p is pointing to data which is not deallocated (the 0-9 numbers) already in demo? I expected that arr inside demo will be deallocated as we got out of demo scope.
IntToThe
  • 59
  • 6
  • 2
    Functions execute in something called a stack frame which is it's own block of memory that fits any variables local to that function. When you exit out of demo(), the memory does not get destroyed in most cases which is why you're seeing that result. However, if you call another function after demo(), that array you have will almost certainly be invalid. Refer to https://en.m.wikipedia.org/wiki/Call_stack#STACK-FRAME – Expert Thinker El Rey Jul 25 '21 at 21:29
  • 2
    "*memory of the internal array in my function wasn't deallocated*". It did get deallocated. It just didn't get overwritten yet. But it can be overwritten at any time. Analogy: You check out of a hotel room and leave a mess behind. A few days later you break in and find that the mess is still there. But there was no guarantee that it would still be there and certainly not that it will continue to be going forward. The real owner can come in and clean it all out any any time. – kaylum Jul 25 '21 at 21:34

5 Answers5

4

One of the things you have to be careful of when programming is to pay good attention to what the rules say, and not just to what seems to work. The rules say you're not supposed to return a pointer to a locally-allocated array, and that's a real, true rule.

If you don't get an error when you write a program that returns a pointer to a locally-allocated array, that doesn't mean it was okay. (Although, it means you really ought to get a newer compiler, because any decent, modern compiler will warn about this.)

If you write a program that returns a pointer to a locally-allocated array and it seems to work, that doesn't mean it was okay, either. Be really careful about this: In general, in programming, but especially in C, seeming to work is not proof that your program is okay. What you really want is for your program to work for the right reasons.

Suppose you rent an apartment. Suppose, when your lease is up, and you move out, your landlord does not collect your key from you, but does not change the lock, either. Suppose, a few days later, you realize you forgot something in the back of one closet. Suppose, without asking, you sneak back to try to collect it. What happens next?

  • As it happens, your key still works in the lock. Is this a total surprise, or mildly unexpected, or guaranteed to work?
  • As it happens, your forgotten item still is in the closet. It has not yet been cleared out. Is this a total surprise, or mildly unexpected, or guaranteed to happen?
  • In the end, neither your old landlord, nor the police, accost you for this act of trespass. Once more, is this a total surprise, or mildly unexpected, or just about completely expected?

What you need to know is that, in C, reusing memory you're no longer allowed to use is just about exactly analogous to sneaking back in to an apartment you're no longer renting. It might work, or it might not. Your stuff might still be there, or it might not. You might get in trouble, or you might not. There's no way to predict what will happen, and there's no (valid) conclusion you can draw from whatever does or doesn't happen.

Returning to your program: local variables like arr are usually stored on the call stack, meaning they're still there even after the function returns, and probably won't be overwritten until the next function gets called and uses that zone on the stack for its own purposes (and maybe not even then). So if you return a pointer to locally-allocated memory, and dereference that pointer right away (before calling any other function), it's at least somewhat likely to "work". This is, again, analogous to the apartment situation: if no one else has moved in yet, it's likely that your forgotten item will still be there. But it's obviously not something you can ever depend on.

Steve Summit
  • 45,437
  • 7
  • 70
  • 103
  • The analogy presented in this answer reminds me very much of [this very similar analogy](https://stackoverflow.com/questions/6441218/can-a-local-variables-memory-be-accessed-outside-its-scope/6445794#6445794). – Andreas Wenzel Jul 26 '21 at 00:46
  • @AndreasWenzel "No, what are you talking about, they're completely different! The analogy you mentioned was about C++, but this one is about C!" :-) – Steve Summit Jul 26 '21 at 01:13
  • @AndreasWenzel Seriously, yes, that's a good one. I'd seen it before, but forgotten it when I wrote this answer. (Also I hadn't noticed kaylum's very similar comment above.) – Steve Summit Jul 26 '21 at 01:14
3

arr is a local variable in demo that will get destroyed when you return from the function. Since you return a pointer to that variable, the pointer is said to be dangling. Dereferencing the pointer makes your program have undefined behavior.

One way to fix it is to malloc (memory allocate) the memory you need.

Example:

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

int* demo(int n) {
    int* arr = malloc(sizeof(*arr) * n);  // allocate

    for (int i = 0; i < n; i++) {
        arr[i] = i;
    }

    return arr;
}

int main() {
    int* arr_p;
    arr_p = demo(10);
    printf("%d\n", arr_p[9]);
    free(arr_p)                 // free the allocated memory
}

Output:

9

How come demo_p is pointing to data which is not deallocated (the 0-9 numbers) already in demo? I expected that arr inside demo will be deallocated as we got out of demo scope.

The life of the arr object has ended and reading the memory addresses previously occupied by arr makes your program have undefined behavior. You may be able to see the old data or the program may crash - or do something completely different. Anything can happen.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • Note - storing the return value of the function in `arr_p` already causes undefined behaviour. Dangling pointers have *indeterminate value* which may be a trap representation. – M.M Jul 25 '21 at 22:11
2

… I noticed that the memory of the internal array in my function wasn't deallocated…

Deallocation of memory is not something you can notice or observe, except by looking at the data that records memory reservations (in this case, the stack pointer). When memory is reserved or released, that is just a bookkeeping process about what memory is available or not available. Releasing memory does not necessarily erase memory or immediately reuse it for another purpose. Looking at the memory does not necessarily tell you whether it is in use or not.

When int arr[10] = { 0 }; appears inside a function, it defines an array that is allocated automatically when the function starts executing (or at certain times within the function execution if the definition is in some nested scope). This is commonly done by adjusting the stack pointer. In common systems, programs have a region of memory called the stack, and a stack pointer contains an address that marks the end of the portion of the stack that is currently reserved for use. When a function starts executing, the stack pointer is changed to reserve more memory for that function’s data. When execution of the function ends, the stack pointer is changed to release that memory.

If you keep a pointer to that memory (how you can do that is another matter, discussed below), you will not “notice” or “observe” any change to that memory immediately after the function returns. That is why you see the value of arr_p is the address that arr had, and it is why you see the old data in that memory.

If you call some other function, the stack pointer will be adjusted for the new function, that function will generally use the memory for its own purposes, and then the contents of that memory will have changed. The data you had in arr will be gone. A common example of this that beginners happen across is:

int main(void)
{
    int *p = demo(10);
    // p points to where arr started, and arr’s data is still there.

    printf("arr[3] = %d.\n", p[3]);
    // To execute this call, the program loads data from p[3]. Since it has
    // not changed, 3 is loaded. This is passed to printf.

    // Then printf prints “arr[3] = 3.\n”. In doing this, it uses memory
    // on the stack. This changes the data in the memory that p points to.

    printf("arr[3] = %d.\n", p[3]);
    // When we try the same call again, the program loads data from p[3],
    // but it has been changed, so something different is printed. Two
    // different things are printed by the same printf statement even
    // though there is no visible code changing p[3].
}

Going back to how you can have a copy of a pointer to memory, compilers follow rules that are specified abstractly in the C standard. The C standard defines an abstract lifetime of the array arr in demo and says that lifetime ends when the function returns. It further says the value of a pointer becomes indeterminate when the lifetime of the object it points to ends.

If your compiler is simplistically generating code, as it does when you compile using GCC with -O0 to turn off optimization, it typically keeps the address in p and you will see the behaviors described above. But, if you turn optimization on and compile more complicated programs, the compiler seeks to optimize the code it generates. Instead of mechanically generating assembly code, it tries to find the “best” code that performs the defined behavior of your program. If you use a pointer with indeterminate value or try to access an object whose lifetime has ended, there is no defined behavior of your program, so optimization by the compiler can produce results that are unexpected by new programmers.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Thanks for the answer with an example! Can you please elaborate what happens when we call twice the prinft function? I noticed the stack pointer is not changed along the prinft calls, so why the frame changed? Also - Do you know where I can set in visual studio my complier so it will do optimizations and warn me/throw error on using deallocated memory as in my demo example? Currently my VS 2019 doesn't mention any error for my program. – IntToThe Jul 26 '21 at 07:01
  • @IntToThe I see you've also asked [this question](https://stackoverflow.com/questions/68525940) about the stack pointer, so some of these sub-questions may get answered there. – Steve Summit Jul 26 '21 at 11:21
  • @IntToThe I doubt there's a warning you can enable for "using deallocated memory", because that's very difficult to detect in general. I don't know why VS2019 doesn't warn about returning the address of a local variable, though, because that's an easy and useful warning. gcc says "warning: function returns address of local variable", and clang says "warning: address of stack memory associated with local variable 'arr' returned". – Steve Summit Jul 26 '21 at 11:25
  • @IntToThe: The stack pointer is changed when `printf` is called. It returns to its previous value when `printf` returns. Visual Studio does provide a warning (C4172) when it observes the address of a local object being returned. That warning is included in warning level 1 (switch `/W1`) and appears to be included in the default. Use `/WX` to elevate warnings to errors. – Eric Postpischil Jul 26 '21 at 11:35
2

As you know dear, the existence of a variable declared in the local function is within that local scope only. Once the required task is done the function terminates and the local variable is destroyed afterwards. As you are trying to return a pointer from demo() function ,but the thing is the array to which the pointer points to will get destroyed once we come out of demo(). So indeed you are trying to return a dangling pointer which is pointing to de-allocated memory. But our rule suggests us to avoid dangling pointer at any cost.

So you can avoid it by re-initializing it after freeing memory using free(). Either you can also allocate some contiguous block of memory using malloc() or you can declare your array in demo() as static array. This will store the allocated memory constant also when the local function exits successfully.

Thank You Dear..

#include<stdio.h>
#define N 10

int demo();
int main()
{
   int* arr_p;
   arr_p = demo();
   printf("%d\n", *(arr_p+3));
}

int* demo()
{
   static int arr[N];

for(i=0;i<N;i++)
{
   arr[i] = i;
}

   return arr;
}

OUTPUT : 3

Or you can also write as......

#include <stdio.h>
#include <stdlib.h>
#define N 10

int* demo() {
   int* arr = (int*)malloc(sizeof(arr) * N);

   for(int i = 0; i < N; i++)
{
   arr[i]=i;
}

   return arr;
}

int main()
{
  int* arr_p;
  arr_p = demo();
  printf("%d\n", *(arr_p+3));
  free(arr_p);  
  
  return 0;
}

OUTPUT : 3
0

Had the similar situation when i have been trying to return char array from the function. But i always needed an array of a fixed size.

Solved this by declaring a struct with a fixed size char array in it and returning that struct from the function:

#include <time.h>

typedef struct TimeStamp
{
    char Char[9];
} TimeStamp;

TimeStamp GetTimeStamp()
{
    time_t CurrentCalendarTime;

    time(&CurrentCalendarTime);

    struct tm* LocalTime = localtime(&CurrentCalendarTime);

    TimeStamp Time = { 0 };

    strftime(Time.Char, 9, "%H:%M:%S", LocalTime);

    return Time;
}
Lex
  • 29
  • 1
  • 4