C++/array
Expert: Ralph McArdell - 3/10/2005
QuestionA Hrec contains the below information:
Name : 30 characters
Height : float
My program should be able to store up to 10 Hrec records in an array called Harray.
How can I write?
Thankyou!
AnswerWell first you need to define the Hrec type. In C you would use a struct to make a user defined compound type:
struct Hrec
{// add one character for the zero terminator of a C-string
char name[31];
float height;
};
and you would define an instance of an Hrec like so:
struct Hrec my_hrec;
In C++ we can still define Hrec as a struct, but we can use the type name directly:
Hrec my_rec;
To define an array of 10 Hrec objects we use an array definition, similar to that used to define the array of chars for the name:
Hrec Harray[10];
Of course the values 31 and 10 are magic numbers – what do they mean? Why 31? Why 10? A better way is to create named constants for these values:
int const NameFieldMaxChars(30);
// add one character for the zero terminator of a C-string
int const NameFieldStringLength(NameFieldMaxChars + 1);
struct Hrec
{
char name[NameFieldStringLength];
float height;
};
// ...
int const MaxHrecRecords(10);
// ...
Hrec Harray[MaxHrecRecords];
Notice that I assume that you store the name as a C-style string of characters – which is just a character array with space for a zero character at the end of the string to indicate its end. This increases the length required to store the name from 30 to 31 characters.
Now in C++ we can make use of additional facilities – such as classes and collection types provided by the standard C++ library – including string and vectors (a vector is a one dimensional array). If you are satisfied with my answer you can stop reading now. However for a quick look at some other possible ways of doing what you wish read on. Note that am not going to fully explain everything shown. Therefore I do not expect you to follow all of what is presented from here on in but it should provide food for thought and topics for you to look up on your own.
Class types allow us to extend the facilities offered by C-structs. These include the following abilities (it is not an exhaustive list):
- to associate member functions with a type
- to extend or sub class new types from existing types
- to restrict access to internal details of a type
- to define special construction and destruction member
functions to ensure correct object initialisation and
de-initialisation.
In C++ there is little difference between a class and a struct other than default access of members and base types – for a struct they are all public (anyone can access them), for a class they are all private (only the class and its friends may access them).
So our Hrec struct (a poor choice of name by the way – HeightRecord might be better – what does H mean – Height, Handle (a common technical term), Holiday, Hotel, Hierarchy etc.. ?) could be defined as a class:
class Hrec
{ // private members by default, no one can access them but
// Hrec and its friends
char name[NameFieldStringLength];
float height;
public: // This part can be accessed by every one
// This is a special member function called a constructor
// As it takes no parameters it is a default constructor
// It is used to initialise an Hrec object
Hrec();
// Users call these to access an Hrec's values (read only)
char const * GetName() const;
float GetHeight() const;
};
Now having declared that we have some member functions in our Hrec class we need to define them – often, especially if the class is to be used by many source file modules, the class is defined in a header file – usually with an extension such as .h, .hpp or .hxx – hrec.h for example - and the required implementation definitions such as the member function definitions are placed in a source file having an extension such as .cpp, cxx or .cc – hrec.cpp for example:
// Our implementation needs to know the definition of Hrec
#include "hrec.h"
// Default constructor – initialise data to known values
Hrec::Hrec()
: height(0) // height can be initialled in a constructor's
// so called initialiser list
{
// the name array cannot be so initialised so we do it in
// the constructor function body
name[0] = '\0';
}
char const * Hrec::GetName() const
{
// an array implicitly converts to (or some say decays to)
// a pointer to the element type
return name;
}
float Hrec::GetHeight() const
{
return height;
}
Then in some source module we define the Hrec array:
// Need to know about the definition of the Hrec class
#include "hrec.h"
// ...
Hrec Harray[MaxHrecRecords];
Note that as it stands Hrec cannot have values other than the default values assigned to the name and height members – so additional functionality will be needed to complete this class. You might like to think on _exactly_ what ways you would need to set these values. The answers depend on how you intend to use Hrec objects – which of course comes from the specification of the program. As a hint here is one possible scenario: You read each Hrec's values from a file. After this the values are never changed. Here one solution is to add a constructor that takes an input stream (std::istream) reference as a parameter and the constructor uses values read from this stream to initialise its members. On the other hand, if the data comes from elsewhere as two separate values – the name and the height - then it makes sense to provide a constructor that took the name and height as parameters directly.
Alternatively or in addition you may like to provide member functions to allow these operations to be performed after construction. This would permit the values to be changed at any time. The most obvious way is to add SetName and SetHeight member functions to set the values on at a time. If so why not just make the data members name and height public? Well you might change the implementation so that these are not stored in the same way or you might like to enforce changing of the values – maybe ensure the height is positive and that the name is not empty and matches some specific format such as it consists of all alpha (letter) characters. In short it is generally bad form to allow unrestricted access to data members of a class.
Now in standard C++ we can make use of the facilities of the C++ standard library. Two such features may be of interest here – the std::string class and the std::vector class template.
The std::string class can be used in place of a char array:
#include <string>
class Hrec
{
std::string name;
float height;
public:
Hrec();
std::string GetName() const;
float GetHeight() const;
};
The good thing about std::string is that it will initialise and de-initialise itself, that it takes care of extending itself automatically and that it provides useful operations and services. Notice that we now return a std::string by _value_ in GetName – this is because std::string objects know how to copy themselves. So the returned string is a copy of the one held by an Hrec – so it no longer needs to be const. Previously we returned a pointer to the actual character data held in the Hrec and so we made those values constant so a user could not modify an Hrec willy nilly. Here is the revised implementation of GetName:
std::string Hrec::GetName() const
{
// Here name is copied in full to the return value
return name;
}
Oh, and of course we need to update the definition of the default constructor – in fact it gets simpler as name now default initialises itself, we need do nothing:
Hrec::Hrec()
: height(0)
{
}
Now we come to the use of the std::vector class template as a replacement for built in array types. Now std::vector is a class template - is it is not a class as such but a template that can be used to define a whole set of classes. In order to obtain a vector class we need to provide some details – called template parameters – which are most often types used to define a class from the template. In the case of std::vector the only template parameter we need to provide to define a vector class is the type of the elements in the vector – which in our case is an Hrec:
#include "hrec.h"
#inlcude <vector>
// ...
// For convenience we create an alias for the particular
// std::vector class we require
typedef std::vector<Hrec> HrecArrayType;
HrecArrayType Harray;
The advantages of an instance of a std::vector class is that like std::strings they take care of the housekeeping details such as initialisation, de-initialisation and growing the vector as needed, so we are no longer dependent on the fixed size limit of 10 in the built in array example. Note that in order to use a type as the element type of a std::vector class it must be a good citizen – which for the C++ standard library container types means it must be copyable – which in some cases requires the provision of a special constructor called a copy constructor – the compiler will generate one for a class if it does not provide one and this will do for many but not all cases. Likewise, it must be assignable ( element1 = element2 ), and again the compiler generated assignment operator ( operator= ) may not do for all cases. Finally it must be destroyable by a destructor – again the compiler will generate one for a class but again it may not do in all cases so an explicit destructor may be needed for some classes.
In our cases both versions of the Hrec class are good C++ library citizens as the default copy construction and assignment operations will make a straight byte-by-byte copies of non-class members such as name and height in the first version – which is OK as height is a scalar float value and name is an array which is wholly included in Hrec objects. In the second version name is a class object of type std::string and in this case the compiler will call the copy constructor or assignment operator for this member's class – which for std::string does the right thing. And the compiler generated destructors for each version are also OK for similar reasons.
Finally, consider how you might improve the structure of your program by wrapping the Harray in its own class, and provide operations on the Harray as member functions. Here is a starting point:
// heightrecords.h : defines HeightRecords class
#include "hrec.h"
#inlcude <vector>
// ...
typedef std::vector<Hrec> HrecArrayType;
class HeightRecords
{
HrecArrayType heightrecords;
// Other private data members and implementation specific
// details such as helper member functions for HeightRecords
// internal use only...
public: // public interface for use by any one
// Declare constructors, assignment operations and
// destructor as required
// Define useful operations for the program in question
};
Then of course you need to write the definitions of those member functions:
// heightrecords.cpp : define HeightRecords class implementation
Hope this not only answers your questions but has given you a glimpse of the facilities offered by C++ and the ways these can be used to improve the structure of your program.