Journal Articles
Browse in : |
All
> Journals
> CVu
> 113
(22)
All > Journal Columns > Code Critique (70) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: A Code Critique
Author: Administrator
Date: 03 April 1999 13:15:30 +01:00 or Sat, 03 April 1999 13:15:30 +01:00
Summary:
Body:
Not infrequently appeals from students for help appear in the better quality newsgroups. We have a general policy that we will not do a student's homework. However, where a student is making a genuine attempt and either post's their code or asks a specific question the resident gurus are more lenient.
In mid-January I noted such an appeal and as it was actually posted to an inappropriate NG I contacted the student directly. What I then learned disgusted me. This was not a first time student struggling with a C++ 101 course but a student who already had rather more relevant experience than the course required. The course was a pre-requisite for starting an MSIS course.
In the sidebars I provide two lightly edited emails from the student. Do you feel as angry as I do that any academic establishment should tolerate the work of such a clearly inadequate employee?
By the way does anyone know how to provide some cursor control in a console application? Using combinations of various control characters (\b, \t, \v and \n) allows forward navigation and moving backwards along a line but is there any way to move up a line? If you know, please tell me. Even better if you know of a library that provides cursor control in a console app window please pass it on. I asked the student concerned to let me see her source code so that I could pass it on to the Harpist for a code review. She kindly did so though I have removed anything that would identify the author or university.
Over to 'The Harpist.'
I am going to do a complete critique on the code provided, but do so in stages. As always, I would welcome comments from readers (just send them to Francis). I am going to tackle this in an unexpected order. First I am going to look at the application code.
#include <iostream> //for cin and cout #include "student.h" //for class student using namespace std; int main(){ int counter; //counts number of entries counter = 0; //initialize counter to zero char go; //to control looping char crap[30]; //to clear the buffer??? do { //Start loop so can repeat entry of 5 students Student s1; //instantiate object s1.GetSID(); //call member functions s1.GetName(); s1.GetPhone(); s1.GetAddress(); Student s2; //instantiate object s2.GetSID(); //call member functions s2.GetName(); s2.GetPhone(); s2.GetAddress();
< 3 more identical blocks for s3, s4 and s5 snipped>
s1.Show(); //show the student info for each object s2.Show(); <snipped> cout << "Do you wish to continue? (Y) or (N)o "; //prompt for end of data entry cin.get (go); //read input, compare to see if stop cin.getline (crap, 35); //I dunno, but it makes it work!!! } while ((go == 'y') ||(go == 'Y')); //test for quitting value return 0; }
Now here is my version to do substantially the same thing. I will assume that among all the other things the instructor has not introduced are arrays and references. I know that some of the identifiers are poorly chosen but I want to focus on simply improving what the student can already do.
#include <iostream> #include "student.h" using namespace std; void initialiseStudent(Student * s_ptr){ s_ptr->GetSID(); s_ptr->GetName(); s_ptr->GetPhone(); s_ptr->GetAddress(); } int main(){ char go; //to control looping do { //Start loop so can repeat entry of 5 students Student s1; //instantiate student initialiseStudent(&s1) // and initialise Student s2; // likewise for 4 more initialiseStudent(&s2) Student s3; initialiseStudent(&s3) Student s4; initialiseStudent(&s4) Student s5; initialiseStudent(&s5) s1.Show(); //show the student info for each one s2.Show(); s3.Show(); s4.Show(); s5.Show(); cout << "Do you wish to continue? (Y) or (N)o "; //prompt for end of data entry cin >> go; //read input, compare to see if stop while(cin.get()!= '\n'); // flush input buffer } while ((go == 'y') ||(go == 'Y')); //test for quitting value return 0; }
Note that I have removed the unused counter. Much more important, I have factored out the process of initialising student data. When I come to deal with the class Student I will think about changing that function to a member function of Student. I will also give consideration to naming conventions.
Look at the way I flush the input buffer. I think that is well worth remembering, but please do not turn it into a macro. However using an inline function has advantages. Perhaps:
inline void flushInput(istream & input) { while(input.get() != ''\n';}
meets the need. It documents the activity and makes it available for all istream objects.
Next let me redo main() assuming the student has now learnt about arrays.
int main(){ do { //Start loop to get data in blocks of 5 students Student s[5]; //instantiate array of five students for(int i = 0; i<5; ++i) initialiseStudent(s+i); for(int i = 0; i<5; ++i) s[i].show(); cout << "Do you have another five to do? (Y)es or (N)o "; char getMore; // moved to point of first use cin >> getMore; flushInput(cin); } while ((getMore== 'y') ||(getMore== 'Y')); return 0; }
The only thing that makes me uncomfortable with this code is the pointer arithmetic for the argument to initialiseStudent. Contrast that with the call of the show() function and we begin to see how member functions can clean up code.
Now let me have a look at the header file. In working on this I will want to limit the changes to the application code. Here is the original:
class Student // a class for organizing student information { public: Student(); //constructor void GetSID(); //allows entry of student ID void GetName(); //allows user to enter student name void GetPhone(); //allows user to enter phone number void GetAddress(); //allows user to enter address void Show(); //displays student info private: char number[7]; //for student ID char phone[13]; //for student phone number char name[30]; //for student name char address1[30]; //for student's street address char address2[35]; //for city, state and zip };
One problem with this code is that it is non-idiomatic. C programmers use get-functions in this way, C++ programmers do not. I am going to skip over whether this class should be value based (have publicly available copy constructor and copy assignment). In addition my design is going to be simplistic. In other words I am not trying to write production code nor the kind of code that and experienced programmer might.
#ifndef STUDENT_H #define STUDENT_H // standard sentinel to prevent multiple inclusion // class for acquiring student information class Student { public: Student(); Student & initialiseStudent(); Student const & displayStudentData(ostream & output) const; void Show() const { displayStudentData(cin); } protected: Student & id() Student & phone() Student & name() Student & address() private: enum MaxLengths { id_len = 7, phone_len = 13, name_len =30, addr1_len =30, addr2_len =35}; char id_m[id_len]; char phone_m[phone_len]; char name_m[name_len]; char address1_m[addr1_len]; char address2_m[addr2_len]; }; #endif // end of conditional inclusion
The first thing you will notice is that I have added an industry standard sentinel to ensure that this file is not multiply included into some other file. Multiple inclusion would cause redefinition errors.
I guess the instructor has never said anything about const member functions. However he should have done because it is important to develop the correct habits right from the start. Even if the explanation is very brief. Any member function that does not conceptually modify the state of an object should be qualified as const (by placing const after the closing parenthesis for the parameter list.
This is not the place to explain the return type for member functions, but there are good reasons for replacing void returns with references or const references to the class type.
I do not want applications to rely on being able to change individual pieces of data so I have made those functions protected (that will make more sense when the course moves on to inheritance)
I have provided a new function to write the data for a student from an istream, and then defined show() to call that function with cin. The advantage of the new function is that you now have something that can be used when you want to write the data to a file, send it to a printer etc.
Now why do you think I have provided that enum? It follows the rule that information should only be provided once. If, at some later stage, you decide you need to provide more space for the telephone number you only need to change the value provided in that enum for phone_len. Again, this kind of idiom can be introduced early with a full explanation to follow later. You do not need to know about enumerated types to use this idiom correctly. It is private because I do not want other code to rely on these values.
Now I can go back to the application and remove initialiseStudent() and change the line that uses it in main() to: for(int i = 0; i<5; ++i) s[i].initialiseStudent();
When the course has covered default parameters initialiseStudent() can be enhanced to work from other streams while still retaining the same default behaviour we are supplying here.
My definition of class Student is a bit longer than the original but it has more potential for development and is closer to the way good programmers write.
Here is the original:
#include <iostream> #include <string> #include "student.h" using namespace std; //definitions of member functions for class Student Student::Student() //Constructor { } void Student::GetSID() //Get student number { char num[7]; cout << "Enter six digit student number "; //prompt for student number cin.getline (num, 7); //read the input strcpy (number, num); //copy input to data member }
<3 Similar functions snipped>
void Student::Show() //display the student info { cout <<"\n\n"; cout << "Student number " << number <<"\n"; //show the student number cout << "Name: " << name <<"\n"; //show the name cout << "Phone number " << phone <<"\n"; //show the phone number cout << "Street address " << address1 <<"\n"; //show street address cout << "City, state, and zip " << address2 << "\n"; //show city, state. etc cout << endl; }
Again we have a chance to factor out common code. Look at the following function. I have declared it as static so that it is limited to the current file. I am well aware that using such things as unnamed namespaces are preferred but if students are being taught by instructors that are half as bad as the one in question here, we need to keep to as little new material as we can while doing a reasonable job.
static void init_array_of_char (char const * prompt, char * destination, size_t length) { cout << prompt; cin.getline(destination, length); while (cin.get() != '\n'); // flush input buffer }
Now my four protected functions can be written as:
Student & Student::id(){ init_array_of_char("What is the student number?", id_m, id_len); return *this; } Student & Student::phone(){ init_array_of_char("What is the student's telephone number?", phone_m, phone_len); return *this; } Student & Student::name(){ init_array_of_char("What is the student's name", name_m, name_len); return *this; } Student & Student::address(){ init_array_of_char("What is the student's street address", address1_m, addr1_len); init_array_of_char("What is the student's City, State and zip-code", address2_m, addr2_len); return *this; }
Now let me show how easy it is to use these to provide initialiseStudent()
Student & Student::initialiseStudent(){ return id(), phone(), name(), address(); }
That is a comma separated list so each function is evaluated in sequence and the return value of the last one is returned as the value of the function.
The next thing to consider is that constructor. The problem is that it leaves the object in an uninitialised state. If the show() function is ever called on an uninitialised Student object you may get bizarre output as the arrays will hold garbage (and may not even be zero terminated. It would be much better to use the following definition:
Student::Student():id_m(""), phone_m(""), name_m(""), address1_m(""), address2_m("") {}
to ensure that all the arrays have been set as empty strings.
And finally let me deal with my displayStudentData():
Student const & Student::displayStudentData(istream & input) { input <<"\n\n" << "Student number : " << number <<"\n" << "Name: " << name <<"\n" << "Phone number " << phone <<"\n" << "Street address " << address1 <<"\n" << "City, state, and zip " << address2 << "\n" << endl; }
Note that the way I have developed my Student class means that all the data, its capture and display have been encapsulated. The application neither knows, nor has any need to know, exactly what data is used and how it is structured. If I were teaching this course I would want to strongly emphasise this (perhaps by asking the student to add a datum, or separate city from state and zip code.
I think that just about wraps it up.
I am going to ask Francis to send this column to the original code writer and invite her questions and comments. If she sends those back to Francis in time I will try to add a response to this column.
Francis forwarded the student's first response to the above. Her comments are in italic Times Roman while mine are in Arial. I first added the following injunction:
OK. I hope this helps a little. Though as Steve Heller says in 'Whose Afraid of C++' programming has to be studied, you cannot expect to learn by simply reading once.
Now on with my comments on your response.
- Student
-
I do appreciate the critique of my admittedly inferior code. I suspect that advice from people as kind as you will be my best educational resource in this class.
- The Harpist
-
Thanks. But it isn't all one way. The problems faced by people such as you can help to enlighten others because it helps me and people like me answer the real problems you have.
- Student
-
You are correct; we have not yet had strings, arrays, pointers, functions other than the function signature, const as anything other than a simple data type, references, or any explanation of naming conventions. I used my old Pascal naming conventions as my guide. Please, where might I reference the naming conventions? Also, all data members were required to be private in this assignment.
- The Harpist
-
I find this information deeply shocking. Perhaps the instructor expects experienced students to find out for themselves. But I do not find this reasonable in a course that does not require any prior programming experience.
You know more about functions than you think you do because you have been writing member functions. Member functions have a special characteristic in that they are called by using the dot operator on an object of the correct type (and that object becomes an argument to the function that does not appear in the argument list - because we sometimes want to refer to that argument, C++ has a keyword this. Using it generates the address of the object in question. Prefixing a pointer (address) value with an asterisk results in the object itself. So this is a pointer to the object calling the member function on itself and *this will be the object itself (which can be bound to a reference.)
When an identifier is const qualified it means that the object identified cannot be changed through use of this identifier (it might be changed through some other route, if there is one). If we want to const qualify *this (in other words the member function is guaranteed not to change the state of the object calling it) the const is placed after the closing parenthesis of the function declaration/definition.
Naming conventions are hard to come by. To the best of my knowledge there is no definitive source for these. Many writers use the conventions they learnt as C programmers. These are not really appropriate to C++. For example 'get' functions in C were always functions that got their values from some form of input. In C++ they are generally member functions that make the value of some attribute of an object available. Even this usage is beginning to die out in C++ as other idioms are being developed.
If you look at my code you will see (I hope) that my data is private. This is one absolute guideline (in other words there is no reason for ever breaking it) when writing Object-Oriented code, data is always private. What I did was tighten the access to that data further by making my write access functions protected. Until you study inheritance there is no difference between private and protected, but when you get into inheritance you will appreciate the difference. Unfortunately you will also come across an evil habit of some writers to advocate making data protected rather than private. However good the rest of their advice this piece should be rigidly ignored.
- Student
-
The counter was inadvertently not deleted. I originally wanted to use the counter to help instantiate the student objects and call the member functions so I could write it once and loop it. C++ wouldn't let me use a variable in an object name or function call, i.e. student s(counter); s(counter) .GetSID; . I didn't have the tools to use a better method, like yours with pointers and references. Pointers are the last thing we will learn this term.
- The Harpist
-
Your intent was right on the mark. What you were missing was how to declare an array in C++. What you needed was to know that:
Student s[5];
declares an array of five Student objects that are accessible as s[0], s[1], ..., s[4]. C/C++ always indexes arrays from 0. Armed with that you would have enough to generate and initialise data objects. However you do have to decide how many you want before you start (when you learn about (probably have to teach yourself about) C++ vectors, deques and lists you will find that you can have collections that grow as needed.
On the subject of pointers, if your course was making use of the modern C++ components such as string and vector I think leaving discussion of pointers till late makes good sense. However if you have to use arrays in general and specifically arrays of char (to provide string behaviour) you must be given some early information. Not doing so is asking you to write code with one hand tied behind your back.
- Student
-
[ void initialiseStudent(Student * s_ptr){ ]
In the above line you have declared a function initialiseStudent, with a type of data I don't understand, but I'm certain it's a pointer. What is the "*" for? If a "&" is a reference to a memory location, as I heard in my last lecture, then how does a pointer differ from a reference?
- The Harpist
-
initialiseStudent returns a void (which effectively means it is like a Pascal procedure) and takes a single argument of type pointer to Student. That is what that asterisk means. Denis Ritchie when he invented C experimented with a declaration syntax which Bjarne Stroustrup has described as a failure (Bjarne can say things like this because Denis is a colleague). C++ for various pragmatic reasons adopted C as its base and changed as little as possible. One of Denis Ritchie's principles was that declarations should look like uses so the parameter declaration can be read in two ways:
s_ptr is a pointer to a Student (and so stores the address of a Student object)
*s_ptr is a Student object whose address is found in s_ptr.
C had no reference types but it was clearly inefficient to pass arrays by value so pointers were used together with syntax rules to provide a reference like mechanism for arrays. A pointer differs from a reference largely through some syntactic sugar (when you gain more experience you will find that to be something of a simplification).
- Student
-
Other assorted questions: What is "static", and what does it do? "Enum" sounds and looks like what I learned in Pascal as an enumerated data type. It is, then, a data type? Our instructor has not mentioned it yet.
- The Harpist
-
The keyword static in C++ has far too many meanings. However in the context of a file scope declaration it means that the name being declared is only available for use in the same file and will not cause problems at link time because other files use an identical name with a different definition. "enum" is a user-defined type (so are class, struct and union) that will be based on an underlying integer type. Within the braces that provide the definition you place a list of identifiers and the values assigned to them (the full rule is more complicated, but this will do for now). You can create variables of an enum type and you can use the enumerated identifiers to provide values. These values will implicitly convert to the underlying type if necessary but the conversion the other way must be explicit. So:
enum Colour { red =1, green = 2, blue = 4}; int main (){ Colour c = red; // OK int i = blue; // OK Colour c = 1; // error Colour c = Colour(3); // OK i = blue + red; //OK c = blue + red; // error, no operators available for enum Colour c = Colour(blue+red); // OK, convert to ints, add and convert back }
- Student
-
[Student const & displayStudentData(ostream & output) const;]
Could you repeat the above line in English? I have no clue (well one clue, it's a function).
- The Harpist
-
displayStudentData is a const Student (it only has read access to the Student object) member function that takes an argument (by reference) of type ostream and returns a read only reference to an object of type Student
- Student
-
[static void init_array_of_char (char const * prompt, char * destination, size_t length)]
Okay, in this line you are declaring an array. But, again, I don't recognise the parameter data types again. All stuff I haven't had yet.
- The Harpist
-
Not quite. init_array_of_char is a function returning nothing (i.e. it is a procedure) that is local to the file in which it is declared/defined (hence the static qualifier) that takes three parameters. The first (prompt) is a pointer to a non-modifiable char (at call time this will be bound to the start of an array of char, that is a C-style string). The second parameter (destination) is a pointer to a modifiable char (at call time this is bound to the start of an array of char that can be modified -written to). The third parameter (length) is of an opaque type (in this case some form of integer) that the implementation uses to measure the sizes of objects. In this case we will use it to tell the function how big the array is that is pointed to by the second parameter.
- Student
-
Thank you for a better way of flushing the buffer. Four days after I emailed the instructor asking about that problem he replied and said it was a bug and shouldn't be in Visual C++6, and was I sure I bought version 6? He also said to use strcpy to assign the keyboard input to the data member. I'm pretty sure it's not a bug since CodeWarrior does the same thing, but I'd like to know why it does what it does.
- The Harpist
-
As we see, there is no need to use strcpy() as we can use getline() to read the data straight to its destination. getline() should read the delimiter (by default, the end of line character) and discard it. If VC++ is not doing that, it is wrong. I must admit that I am surprised by this bug. CodeWarrior may be doing the same as VC++ for compatibility reasons. Francis tells me that MetroWerks plans to provide a VC++ compatibility switch in the next release (Pro 5) so that programmers can choose between coding the right way and coding the MSVC++ way☺.
Notes:
More fields may be available via dynamicdata ..