I understand that a project for school may have certain restrictions. I must ask, therefore, that since you are writing a C++ application if you are restricted from using C++ standard library classes like std::string, std::vector and others.
I ask because your storage strategies are distinctly C oriented. I'm a developer of many decades experience. I wrote code for years before C++ existed, in C, and I'm familiar (though it's happily a distant memory ;) ) this type of storage strategy. Typical examples are found here in your example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
class Shop
{
private:
char* m_storeName;
int m_factor;
int m_numOfProducts;
Product **m_prod; //pointers array from Product type.
public:
Shop(char *storeName,int factor,int m_numOfProducts,Product **prod);
};
Shop::Shop(char *storeName,int factor,int numOfProducts,Product **prod):m_factor(factor),m_numOfProducts(0)
{
m_storeName=new char[strlen(storeName)+1];
strcpy(m_storeName,storeName);
m_prod=new Product*[PROD];
}
|
The use of strcpy, for example, is a C library function. A C++ programmer actively refuses to use this function in particular, but the overall concept of allocating storage for a string or other containers generally.
The same is more deeply true of m_prod. These types of storage strategies, once the mainstay of C (and of entire operating systems, including UNIX) is now widely recognized as a major source of bugs, and a waste of mental energy in professional work.
That said, even the most prejudiced C++ developers (I'm not QUITE of that description) recognize the occasion when this type of storage management is unavoidable, and for library authors may even be required. I find it uncomfortable to find a modern academic course on the subject mixing the paradigms, while at the same time I recognize why it may be so.
For example, The above code, focused just on the name of the shop, is much safer and easier written this way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
class Shop
{
private:
std::string m_storeName;
public:
Shop( const char *storeName );
};
Shop::Shop( const char *storeName )
{
m_storeName = storeName;
}
|
While I'm not suggesting the removal of the other members, there are two points I'm making here. First, the std::string class is many times more convenient and safer. Storage is automatically managed. The usage of the const specifier allows the constructor to work in a few more situations (though that has limited necessity in a singleton class, like Shop might be), and - just as important - the std::string m_storeName will automatically delete the string storage at the proper time. The classic mistake made in C style storage is to forget to delete the string storage, leaving a memory leak, or deleting storage prematurely, causing a crash. Neither can happen with std::string (except under tortured, contrived examples of sophistry).
Further, if an STL container is used for product storage, the same power of automatic management is provided for the product database as is provided for the string. For that reason, the parameters you specified for shop indicating product count wouldn't be required. Most containers automatically expand storage as it's used, so you wouldn't have to specify storage before the shop is created.
I'd personally expect a std::map as product storage, but that's a slightly more advanced container which provides keyed lookup (you could find products by an id or some other identifier). You might do as well with std::vector as a rather direct replacement of the m_prod array.
If your reason for choosing the array is not because of an academic restriction, but because of limited familiarity with STL, such that you're opting for character arrays and product arrays out of a sense of familiarity, let me point out that the time soaked up by debugging C style storage will likely overtake any time needed to familiarize yourself with stl storage (they're actually quite simple).
With that said, let's assume, for whatever reason, you still intend to use m_prod as it is declared in your example.
To that end, I'll focus on this function sample:
1 2 3 4 5 6 7
|
void Shop::addProduct(Product& p)
{
m_prod[m_numOfProducts]=&p;
m_numOfProducts++;
}
|
This would most certainly initiate the conditions which will cause a crash later. It's also an excellent example of the reason I find mixing C++ and this C style storage strategy to be really uncomfortable.
This function takes the Product parameter as a reference. That tells us nothing about how p was fashioned, and most certainly indicates the function can't assume ownership of p. However, the m_prod array was declared as Product m_prod **, indicating a pointer to an array of pointers. This function shows you clearly understand what that means, but what you've missed is the fact that p is not a pointer this array can own.
I see no code which shows how you may have called addProduct, but what you'll never see in production code is this:
1 2 3 4
|
Product *np = new Product(....some constructor parameters );
shop.addProduct( *np );
|
This might happen to work, and you obviously know you can take the address of a reference. However, this call is also valid to write:
1 2 3 4
|
Product np(....some constructor parameters );
shop.addProduct( np );
|
Yet, because this is likely inside a function, np is "on the stack". It will evaporate when the function exits. The object stored in the m_prod array would be invalid, certain to cause a crash when used.
If addProduct takes a pointer, instead, it communicates more clearly what is intended (though, it's still known to be an evil paradigm, it's C and it worked for decades, it's just not protected).
Personally, for this storage strategy, I'd prefer a function which accepts parameters describing a product (whatever those are), which is fashioned with "new Product..." inside the addProduct function.
That isolates the USER code (that which creates new products) from the detail of creating and storing the product, placing the act of allocating a new product closer to the mechanics of that storage.
Put another way, this:
1 2 3 4
|
Product *np = new Product(....some constructor parameters );
shop.addProduct( *np );
|
Puts the "new Product", a strategy of allocation and therefore of the storage, inside the application code (likely in many places), which could "dangle". What if addProduct failed? What if storage were already exhausted and it said "nope". How would you know to delete np to avoid a leak? Why would that be the caller's job?
It shouldn't be the callers job. addProduct should do that. If addProduct is going to refuse storage, it should realize there's no room and simply skip the "new Product" safely.
That being said, your storage strategy is otherwise ok. You have an array of pointers to products, and each one is to be allocated - I just don't see where you are allocating them in the sample you provided.
Now, that brings the question of deletion. Here I'm talking about how you go about closing down "Shop". When m_prod is to be deleted, this storage strategy requires that you loop through each pointer in m_prod and delete and Product, one at a time. If not, each Product would be a memory leak.
Here's where I think std::vector would be of great help. Consider:
1 2 3 4 5 6 7
|
// in the Shop class
std::vector< std::shared_ptr< Product > > products;
// in some addProduct function
products.push_back( std::shared_ptr< Product >( new Product(....constructor parameters for a product ) ) );
|
push_back is the function used for vectors to "push a new item" into the vector. It's std::vector's version of "add...whatever". Notice, THAT IS IT!
Storage is automatically managed. There will be no memory leaks.
|
cout << products[ 2 ]->ProductName();
|
That little thing would print the product name of product number 3 (starting from 0).
Questions?