Home > Enterprise >  std::vector - why does the element still exist after invoking delete?
std::vector - why does the element still exist after invoking delete?

Time:02-03

I have this code I don't understand why I need to delete and then pop_back().
Can I do it in 1 operation only ?

struct T
 {
    T(int n):x(n){};
    int print() {
       return x; 
    };
    private:
        int x;
 };

int main(int argv,char** argc)
{
    std::vector t = { new T(1),new T(2)};
    delete t.back();
    std::cout << t.size() << std::endl;
    std::cout << t.back()->print() << std::endl;

    t.pop_back();
    std::cout << t.size() << std::endl;
    std::cout << t.back()->print() << std::endl;

    return 0;
}

The output - as you can see after delete it looks like the vector is still holding the element witout the object:

2
179185600
1
1 

My question is why do I need delete and then remove , can't same operation be done in single command ?

CodePudding user response:

1. The issues in your code:

This line:

delete t.back();

Is not removing any element from the std::vector. Your std::vector is holding pointers, and it will continue to hold the same pointers.
What the line above does is deallocate the object pointed by the element in the std::vector.

This line (the 1st occurance):

std::cout << t.back()->print() << std::endl;

Invokes UB (Undefined Behavior) because you derefernce a pointer after it has beed deleteed.

Then after:

t.pop_back();

The last pointer (the one that you deleteed, i.e. freed) is removed from the std::vector, and t.back() is the other pointer you inserted, which is still valid.


2. A different approach - avoid pointers:

Having wrote all that, in this case you do not actually need to deal with pointers at all.
You can use std::vector<T>, (instead of T* like you did) and store the elements by value. You will not need to new or delete anything.

Code example:

#include <vector>
#include <iostream>

struct T
{
    T(int n) :x(n) {};
    int print() { return x; };
private:
    int x;
};

int main(int argv, char** argc)
{
    std::vector t = { T{1}, T{2} };
    std::cout << t.size() << std::endl;
    std::cout << t.back().print() << std::endl;
    t.pop_back();
    std::cout << t.size() << std::endl;
    std::cout << t.back().print() << std::endl;
    return 0;
}

Output:

2
2
1
1

3. Using smart pointers:

If you need to use pointers, use smart pointers (like std::unique_ptr, std::shared_ptr) instead of raw ones. The benefit is that the deallocation is done automatically.
std::unique_ptr is the more light-weight and should be default one. Use std::shared_ptr if you need shared ownership.

Code example (with the same output as above):

#include <vector>
#include <iostream>
#include <memory>

struct T
{
    T(int n) :x(n) {};
    int print() { return x; };
private:
    int x;
};

int main(int argv, char** argc)
{
    std::vector<std::unique_ptr<T>> t;
    t.push_back(std::make_unique<T>(1));
    t.push_back(std::make_unique<T>(2));
    std::cout << t.size() << std::endl;
    std::cout << t.back()->print() << std::endl;
    t.pop_back();
    std::cout << t.size() << std::endl;
    std::cout << t.back()->print() << std::endl;
    return 0;
}

CodePudding user response:

In your code there are two distinct types of objects to be considered.

The elements in the vector are pointers to T, T*. The lifetime of those pointers is managed by the vector. You can add or remove them via member methods of the vector.

Then there are the objects you decided to manage manually via the raw pointers in the vector. They are instances of T. Because you used new to create them you must use delete to delete them.

The vector cannot know about this relation between the pointers (the elements in the vector) and the T objects. It was your decision to manage them manually. You do not have to do that.

Chances are high that you either want a std::vector<T>. Or if for some reason (there is none in your code) you need to have the Ts allocated dynamically, then you would use a std::vector<std::unique_ptr<T>>.


Now this line:

delete t.back();

deletes the T, but the pointer is still in the vector. It is an invalid pointer, the object is was pointing to is no longer alive. When you call t.back()->print() your code invokes undefined behavior. You cannot use an already deleted object. If you do, anything can happen. This has nothing to do with the vector, it is similar to

T* t = new T();   // create object
delete t;         // delete it 
t->print();       // use it -> !!! undefined behavior !!!

Instead of this you should write

T t;
t.print();

In case you are coming from Java, you must unlearn to use new to create objects. The way to create objects in C is not via new.

CodePudding user response:

My question is why i need delete and then remove , can't same operation done in single command ?

There would be 6 answers from me to your question and Let this be your introduction to Pointers , Stack and the Heap.

1.) new operator "Allocates" memory on the heap not the stack,Create that Object (Using it's Constructors) then return a Pointer to the Object that is now on the Heap, hence the need for using the delete operator to "De-allocate" the memory you have requested and "Destroy" the created Object.

2.) delete "De-Allocates" the memory from heap and "Destroy" the created Object, and it does not remove it from the vector

3.)t.pop_back() will remove the Pointer element from the vector but will not de-allocate and Destroy the object the Pointer is Pointing too.

4.) It can't be done with one line since you have requested to allocate a Memory on the heap that can fit struct T inside via the new keyword. By using the keyword new the vector std::vector t is not storing an Object struct T,[std::vector<T>] instead storing Pointers to an Object of struct T [std::vector<T*>] since you did not explicitly Say what the Vector would Hold.

5.) Heap Allocated memory is perpetual , meaning it will always be there and labeled "In Use". Neither the Program, Runtime nor the O.S. can use it to allocate a new Memory Region on the heap unless you explicitly de-allocate and destroy it when using delete, Essentially saying "I'm done with this memory location thank you You may now destroy this object and re-use the memory)". or the Program Closes / Ends. So every new or new[] must be accompanied by delete or delete[] somewhere in the code when that variable is no longer in use anywhere in the program.

6.) After you understand the behavior of Heap / Dynamic Variables have a look at Smart Pointers. Which Handles the destruction of an object for you automatically. If you are coming from Managed Languages, you might want to read up on "Object Lifetimes" on C .

The following Code will help you visualize my answer.

#include <vector>
#include <iostream>

struct T
 {
    T(int n):x(n){};
    int print() {
       return x; 
    };
    // Replaces the Value of X
    void SetX(int n){x=n;};
    private:
        int x;
 };

 int main(int argv,char** argc)
{

    // We explicitly Say that the Vector is holding a Pointers to an object struct T
    std::vector<T*> t;
    // Insert New Elements to the Vector
    t.push_back(new T(1));
    t.push_back(new T(2));

    // Will show that there are 2 elements in the vector.
    std::cout << "Size of Vector Is " << t.size() << std::endl << std::endl;

    // Assign the Last Element to a Pointer of T.
    T* pointerToT = NULL;
    pointerToT = t.back();

    // Print the Memory Address and value of pointerToT and the Last Element
    // And we can see they are the same location and holds the same value.
    std::cout << "The Memory Location of pointerToT is " << pointerToT << std::endl;
    std::cout << "The Memory Location of t.back() is " << t.back() << std::endl;
    std::cout << "The Value of X in pointerToT is " << pointerToT->print() << std::endl;
    std::cout << "The Value of X in t.back() is " << t.back()->print() <<std::endl << std::endl;
    
    //Removes the Last Element of the Vector
    t.pop_back();

    // Will show that there is only 1 element in the vector.
    std::cout << "Size of Vector Is " << t.size() << std::endl << std::endl;

    // Print the Memory Address of pointerToT and the Value of X
    // And as we can see pointerToT is still holding the same location
    // and still holding the value 2, since the program knows that
    // The memory Location is "Still" in use and not yet De-Allocated 
    // ready to be re-used and the object not yet destroyed.
    std::cout << "The Value of X after pop_back() is " << pointerToT->print() << std::endl;
    std::cout << "The Memory Location of pointerToT after pop_back is " << pointerToT << std::endl << std::endl;
    
    
    //De-Allocate the Memory and Destroy the Object that was created by t.push_back(new T(2));
    delete pointerToT;
    
    // Prints the value of X, that is stored in the memory on the heap that it's pointing to. 
    // This Value may or may not be 2 if it is now used other parts of the Code 
    // which we call an Undefined Behaviour.
    // On a Larger Program this could and would Cause a Segmentation fault or 
    // an Access Violation since this is now a Memory Region that is not explicitly
    // Allocated for pointerToT , to the Member Variable X, nor
    // The Last Element of the Vector and may now be in use by other variables and that the object is Destroyed already destroyed.
    std::cout << "The Memory Location of pointerToT after delete is " << pointerToT << std::endl;
    std::cout << "The Value of X after Delete is " << pointerToT->print() << std::endl;
    
    // This is also an undefined Behaviour since you are now accessing and modifying 
    // a Memory Location that is not Allocated for pointerToT . But would show 
    // that the value of X is now 10. So only delete(Destroy)when you are sure you are not going to be
    // accessing that variable again.
    pointerToT->SetX(10);
    std::cout << "The Value of X after SetX() is " << pointerToT->print() << std::endl;
    
    
    
    return 0;
}
  • Related