Search code examples
c++memorymemory-managementdynamic

Using one dynamic array for two struct objects in c++


I tried running the code on a few compilers and got different results. I either got some weird exit code like -1073741819 (0xC0000005) or free(): double free detected in tcache 2 or when debugging I got SIGTRAP (Trace/breakpoint trap) SIGSEGV (Segmentation fault).

I'm 99% sure the problem is that I'm freeing the ships array twice. But why do I not get errors when I don't run func() twice on both players or if I change anything else in the code? What is the problem exactly and how can I fix the error?

Yes I am using a dynamic array instead of a vector, because I'm trying to practise and learn. Yes I know this memory allocation method creates memory fragmentation, but I never change the size of the matrix, so it shouldn't be a problem.

Code:

struct point_t {
  int x;
  int y;

  point_t();
};

point_t::point_t() {
  this->x = 0;
  this->y = 0;
}

struct ship_t {
  int size;
  point_t end_coords[2];
};

struct player_t {
  int **map;
  int map_size;
  ship_t *ships;
  int ships_count;

  player_t(int, ship_t *, int);
  ~player_t();
};

player_t::player_t(int map_size, ship_t *ships, int ships_count) {
  this->map = nullptr;
  this->map_size = map_size;
  this->ships = ships;
  this->ships_count = ships_count;
}

player_t::~player_t() {
  // Free map memory

  // Free each column (sub-array)
  for(int i = 0; i < map_size; i++) {
    delete[] map[i];
  }

  // Free each row (array of pointers)
  delete[] map;
  map = nullptr;
  map_size = 0;

  delete[] ships;
  ships = nullptr;
  ships_count = 0;
}

void func(player_t &player) {
  player.map = new int *[player.map_size];
  for(int i = 0; i < player.map_size; i++) {
    player.map[i] = new int[player.map_size];
    for(int j = 0; j < player.map_size; j++) {
      player.map[i][j] = 0;
    }
  }
}

int main()
{
    ship_t *ships = new ship_t[(5 * 5) / 2];
    
    for(int i = 0; i < 5; i++) {
        ships[i].size = i + 1;
    }
    
    player_t player1 = player_t(5, ships, 5);
    player_t player2 = player_t(5, ships, 5);
    
    func(player1);
    func(player2);
   
    return 0;
}

Solution

  • Running your code with Visual Studio gives me:

    HEAP CORRUPTION DETECTED: after Normal block (#72) at 0x00B065E8.
    CRT detected that the application wrote to memory after end of heap buffer.
    

    when doing the delete[] ships;.

    The first problem is that you are trying to initialize points in the ships constructor, but:

        this->end_coords[2] = {};
    

    this statement is assigning a default initialized point to the third point in the end_coords member of ships. Unfortunately the array is two points large.

    Moreover, since player_t destructor destroys ships, you are giving ownership of ships to player_t. But you are giving ownership of the same dynamic allocated array to two players:

    int main()
    {
        ship_t *ships = new ship_t[(5 * 5) / 2];
        
        for(int i = 0; i < 5; i++) {
            ships[i].size = i + 1;
        }
        
        player_t player1 = player_t(5, ships, 5); // <-- ships
        player_t player2 = player_t(5, ships, 5); // <-- ships
    

    Not a good idea. The same ships array is used by two players, which makes no sense, since each player should have his own ships, I guess.

    Finally you shall not call the destructor of player, because it will be called again on return (scope exit).

    Minimal modifications fixed code:

    Comment out this line in the ship_t constructor:

    //this->end_coords[2] = {};
    

    Allocate two ships arrays, to give each player ownership:

        ship_t *ships = new ship_t[(5 * 5) / 2];
        for (int i = 0; i < 5; i++) {
            ships[i].size = i + 1;
        }
        player_t player1 = player_t(5, ships, 5);
    
        ship_t *ships2 = new ship_t[(5 * 5) / 2];
        for (int i = 0; i < 5; i++) {
            ships2[i].size = i + 1;
        }
        player_t player2 = player_t(5, ships2, 5);
    

    A more C++ style solution

    Let's call structs with their names. Then use default initialization within the struct to avoid useless constructors. Remove all allocations unless really really needed. Let's use std::vector<>. I'm not a fan of vectors of vectors for maps, but in this case it is really a simple solution. No need to store the sizes when using vectors. Finally, copy the ship vector into each player, using move, in case there is some temporary being passed around.

    #include <vector>
    
    struct point {
        int x = 0;
        int y = 0;
    };
    
    struct ship {
        int size = 0;
        point end_coords[2];
    };
    
    struct player {
        std::vector<std::vector<int>> map;
        std::vector<ship> ships;
    
        player(int map_size, std::vector<ship> ships_) :
            map(map_size, std::vector<int>(map_size)),
            ships(std::move(ships_)) {}
    };
    
    int main()
    {
        std::vector<ship> ships;
        for (int i = 0; i < 5; ++i) {
            ships.push_back({ i + 1 });
        }
    
        player player1(5, ships);
        player player2(5, ships);
    
        return 0;
    }
    

    A more C++ style version, with memory allocation:

    Here I'm not using std::vector (but it's not a good idea). Notice how we can initialize a vector int to 0, and that you don't need a pointer to pointer. With a couple of operators it's much easier to manage the map. As an example I'm printing your map. Suggestion: don't use i and j. Use r and c for rows and columns, to avoid exchanging their meaning by accident.

    #include <iostream>
    
    struct point {
        int x = 0;
        int y = 0;
    };
    
    struct ship {
        int size = 0;
        point end_coords[2];
    };
    
    // use _ after names, to make clear that these are members
    struct player {
        int *map_;           // owned
        int map_size_;
        ship *ships_;        // owned
        int ships_count_;
    
        // player constructor copies the ships vector
        player(int map_size, const ship *ships, int ships_count) :
            map_size_{ map_size }, ships_count_{ ships_count }
        { 
            map_ = new int[map_size * map_size]{}; // notice the initializer here to default zero
            ships_ = new ship[ships_count];
        }
    
        // Rule of 4: if you own memory, you need all these 4 methods. Or delete them.
        player(const player&) = delete;
        player& operator=(const player&) = delete;
    
        ~player() {
            delete[] map_;  // no setting to nullptr: these will never be accessed anymore
            delete[] ships_;
        }
    
        int size() const { return map_size_; }
        int& operator()(int r, int c) {
            return map_[r * map_size_ + c];
        }
        const int& operator()(int r, int c) const {
            return map_[r * map_size_ + c];
        }
    };
    std::ostream& operator<<(std::ostream& os, const player& p) {
        auto size = p.size();
        for (int r = 0; r < size; ++r) {
            for (int c = 0; c < size; ++c) {
                if (p(r, c) == 0) {
                    std::cout << "~";
                }
                else {
                    std::cout << p(r, c);
                }
            }
            std::cout << "\n";
        }
        return os;
    }
    
    int main()
    {
        ship ships[5];
        for (int i = 0; i < 5; ++i) {
            ships[i].size = i + 1;
        }
    
        player player1(5, ships, 5);
        player player2(5, ships, 5);
    
        std::cout << "player1:\n" << player1 << "\n";
        std::cout << "player2:\n" << player2 << "\n";
    
        return 0;
    }