1

What I am trying to achieve is creating a superclass array of subclass objects.

In this particular test I'm working on, I want to have an animal array that has some dog objs and some cat objs while they maintain their attributes.

#include <iostream>
using namespace std;

//ANIMAL
class animal
{
protected:
    int ID;
    string name;
public:
    animal(string = "Unknown");
    int get_ID() { return ID; }
    virtual string get_name() { return name; }
};

animal::animal(string n) { name = n; }

//DOG
class dog : public animal
{
    static int newID;
    string sound;
public:
    dog(string = "Corgi", string = "Woof!");
    string get_name() { return sound + " " + name; }
};

int dog::newID = 0;

dog::dog(string n, string s) : animal(n)
{
    newID++;
    ID = newID;
    cout << ID << "\t";
    sound = s;
}

//CAT
class cat : public animal
{
    static int meowID;
    string color;
public:
    cat(string = "Munchkin", string = "Calico");
    string get_name() { return color + " " + name; }
};

int cat::meowID = 89;

cat::cat(string n, string c) : animal(n)
{
    meowID++;
    ID = meowID;
    cout << ID << "\t";
    color = c;
}


//MAIN
int main(int argc, char* argv[])
{
    animal** test;
    animal* p;
    for (int i = 0; i < 6; i++)
    {
        p = new dog;
        p++;
    }
    cout << "\n";
    for (int i = 0; i < 6; i++)
    {
        p = new cat;
        p++;
    }
    cout << "\n";
    test = &p;
    cout << (*test-7)->get_ID();
    return 0;
}

What I've learned so far is that p isn't an array, and it keeps pointing to different memory addresses through the loops.

I cannot do animal** test = new dog[6]; as it is an invalid initialization. Even if that worked I would have trouble cascading another array segment of cat.

This is the output I obtained:

1       2       3       4       5       6
90      91      92      93      94      95
0

The first line is displaying dog IDs being invoked 6 times, and the second line is displaying cat IDs being invoked 6 times. (*test-7)->get_ID();is the last number.

It seems the constructors are being invoked right. However, I have no idea where my pointer is pointing, since I am expecting 91 not 0.

How do I get an animal array that I can access information from each element? For example,

animal** myArray;
{do something}
cout << myArray[2].get_name() << endl << myArray[7].get_ID();

and it outputs

Woof! Corgi
91
  • 1
    As you're writing C++ you should avoid the `new` operator and raw pointers and instead use the STL. You should use `vector animals` - but that won't work with subclasses so you'll need to use `vector>`. – Dai Oct 27 '20 at 03:52

2 Answers2

4

One important detail about the animal class: polymorphic types can run into issues when their destructors are called but those destructors are not virtual. It is recommended that you make the destructor of the base class virtual, even if that class itself does not actually need a destructor. In this case, you can tell the compiler that you want the destructor to be virtual but generate a default implementation of it with:

virtual ~animal() = default;

Add the above line in the public: section of your animal class. This ensures that any derived classes that you define later on will get a virtual destructor automatically.

Now to the rest of your code:

p = new dog;

So far, so good. But then this:

p++;

does nothing useful other than making the pointer point to an invalid address. Then in the next iteration, another p = new dog; will be performed. The previous dog object you allocated is now lost forever. You got a so-called "leak".

It seems you expect new to allocate objects an a way that lays them out in memory one after another. That is not the case. new will allocate memory in an unpredictable location. As a result, this:

*test-7

cannot possibly work, as the objects are not laid out in memory the way you expected. What you get instead is an address to some memory location 7 "positions" before the latest allocated object that pretty much certainly does not point to the animal object you were hoping. And when you later dereference that you get undefined behavior. Once that happens, you cannot reason about the results anymore. They can be anything, from seeing wrong text being printed to your program crashing.

If you want an array of animal pointers, you should specifically create one:

animal* animals[12];

This creates an array named animals that contains 12 animal pointers. You can then initialize those pointers:

for (int i = 0; i < 6; i++) {
    animals[i] = new dog;
}

cout << "\n";

for (int i = 6; i < 12; i++) {
    animals[i] = new cat;
}

You then just specify the array index of the one you want to access:

cout << animals[0]->get_ID() << '\n'; // first animal
cout << animals[6]->get_ID() << '\n'; // seventh animal

Don't forget to delete the objects after you're done with the array. Since animals is an array, you can use a ranged for loop to delete all objects in it:

for (auto* animal_obj : animals) {
    delete animal_obj;
}

However, all this low-level code is quite tedious and error-prone. It's recommended to instead use library facilities that do the allocations and cleanup for you, like std::unique_ptr in this case. As a first step, you can replace your raw animal* pointer with an std::unique_ptr<animal>:

unique_ptr<animal> animals[12];

(Don't forget to #include <memory> in your source file, since std::unique_ptr is provided by that library header.)

Now you've got an array of smart pointers instead of raw pointers. You can initialize that array with:

for (int i = 0; i < 6; i++) {
    animals[i] = make_unique<dog>();
}

cout << "\n";

for (int i = 6; i < 12; i++) {
    animals[i] = make_unique<cat>();
}

Now you don't need to delete anything. The smart pointer will do that automatically for you once it goes out of scope (which in this case means once the animals array goes out of scope, which happens when your main() function exits.)

As a second step, you can replace the animals array with an std::vector or an std::array. Which one you choose depends on whether or not you want your array to be able to grow or shrink later on. If you only ever need exactly 12 objects in the array, then std::array will do:

array<unique_ptr<animal>, 12> animals;

(You need to #include <array>.)

Nothing else changes. The for loops stay the same.

std::array is a better choice than a plain array (also known as "built-in array") because it provides a .size() member function that tells you the amount of elements the array can hold. So you don't have to keep track of the number 12 manually. Also, an std::array will not decay to a pointer, like a plain array will do, when you pass it to functions that take an animal* as a parameter. This prevents some common coding bugs. If you wanted to actually get an animal* pointer from an std::array, you can use its .data() member function, which returns a pointer to the first element of the array.

If you want the array to be able to grow or shrink at runtime, rather than have a fixed size that is set at compile time, then you can use an std::vector instead:

vector<unique_ptr<animal>> animals;

(You need to #include <vector>.)

This creates an empty vector that can store elements of type unique_ptr<animal>. To actually add elements to it, you use the .push_back() function of std::vector:

// Add 6 dogs.
for (int i = 0; i < 6; i++) {
    animals.push_back(make_unique<dog>());
}
// Add 6 cats.
for (int i = 0; i < 6; i++) {
    animals.push_back(make_unique<cat>());
}

Instead of push_back() you can use emplace_back() as an optimization, but in this case it doesn't matter much. They key point to keep in mind here is that a vector will automatically grow once you push elements into it. It will do this automatically without you having to manually allocate new elements. This makes writing code easier and less error-prone.

Once the vector goes out of scope (here, when main() returns,) the vector will automatically delete the memory it has allocated to store the elements, and since those elements are smart pointers, they in turn will automatically delete the animal objects they point to.

Nikos C.
  • 47,168
  • 8
  • 58
  • 90
  • Thank you very much! Your implement makes so much sense! It's actually almost obvious, but I couldn't think of it until you pointed out. Also, would you explain how I free the dynamic memory allocation? I have tried 'delete[] animals;' but the compiler didn't like that. – Leona Huang Oct 27 '20 at 04:35
  • @LeonaHuang I expanded the answer to show how to clean up the allocated objects in the array when using raw pointers, and added a better way of doing this using `std::unique_ptr` that does the deletion automatically. – Nikos C. Oct 27 '20 at 05:29
  • @LeonaHuang I just added a mention that it's a good idea to have a virtual destructor in the base class, even if it's just the default one. – Nikos C. Oct 27 '20 at 05:59
  • Thank you. I'll study smart pointers for now. Also, I noted on making a deconstructor. – Leona Huang Oct 27 '20 at 06:28
0

If you're new to C++, it's important that you get started on the right foot and to follow modern best practices, namely:

  • Avoid using pointers, new, delete, new[] and delete[].
  • Use std::vector<T> (and std::array<T,N> if you have fixed-size collections) instead of new[] or p** (and never use malloc or calloc directly in C++!)
  • I note that you should also generally prefer Composition over Inheritance, but with trivial examples like yours it's difficult to demonstrate the concept because a Dog and a Cat "are" Animals.
  • I also note that when the possible set of subclasses is known at compile-time you should consider using a union-type instead of subclassing because it allows consumers to exhaustively work with returned values without needing to use RTTI or guesswork.
    • This can be done with using AnAnimal = std::variant<cat,dog>.

Anyway, this is what I came-up with. The class animal, class dog, and class cat code is identical to your posted code (and is located within the // #region comments), but the #include and using statements at the top are different, as is the main method.

Note that my code assumes you have a compiler that complies to the C++14 language spec and STL. Your compiler may default to C++11 or older. The std::make_unique and std::move functions require C++14.

Like so:

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

// Containers:
using std::vector;
using std::string;

// Smart pointers:
using std::unique_ptr;
using std::move;
using std::make_unique;

// IO:
using std::cout;
using std::endl;

// #region Original classes

//ANIMAL
class animal
{
protected:
    int ID;
    string name;
public:
    animal(string = "Unknown");
    int get_ID() { return ID; }
    virtual string get_name() { return name; }
};

animal::animal(string n) { name = n; }

//DOG
class dog : public animal
{
    static int newID;
    string sound;
public:
    dog(string = "Corgi", string = "Woof!");
    string get_name() { return sound + " " + name; }
};

int dog::newID = 0;

dog::dog(string n, string s) : animal(n)
{
    newID++;
    ID = newID;
    cout << ID << "\t";
    sound = s;
}

//CAT
class cat : public animal
{
    static int meowID;
    string color;
public:
    cat(string = "Munchkin", string = "Calico");
    string get_name() { return color + " " + name; }
};

int cat::meowID = 89;

cat::cat(string n, string c) : animal(n)
{
    meowID++;
    ID = meowID;
    cout << ID << "\t";
    color = c;
}

// #endregion

int main()
{
    // See https://stackoverflow.com/questions/44434706/unique-pointer-to-vector-and-polymorphism
    
    vector<unique_ptr<animal>> menagerie;
    
    // Add 6 dogs:
    for( int i = 0; i < 6; i++ ) {
        menagerie.emplace_back( make_unique<dog>() );
    }
    
    // Add 6 cats:
    for( int i = 0; i < 6; i++ ) {
        menagerie.emplace_back( make_unique<cat>() );
    }
    
    // Dump:
    for ( auto &animal : menagerie ) {
        cout << "Id: " << animal->get_ID() << ", Name: \"" << animal->get_name() << "\"" << endl;
    }

    return 0;
}
Dai
  • 110,988
  • 21
  • 188
  • 277
  • I greatly appreciate your advice! Those concepts are very foreign to me as a beginner. However, I would love to get an in-depth understanding of those topics. Would you be so kind to provide me with some kind of reference? Warm regards! – Leona Huang Oct 27 '20 at 04:45