Reputation: 11
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
Upvotes: 1
Views: 182
Reputation: 51920
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.
Upvotes: 4
Reputation: 155608
If you're new to C++, it's important that you get started on the right foot and to follow modern best practices, namely:
new
, delete
, new[]
and delete[]
.
unique_ptr
, shared_ptr
, but don't use auto_ptr
!).make_
functions instead of new
. That way you don't need to worry about delete
.
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++!)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;
}
Upvotes: 0