[Phy 405/905 Home Page] [ Lecturer ]
In a continuation of our discussion of types, we'll see how functions are declared and defined, and then take up the question of type conversion - automatic and explicit.
Some points:
int main()
{
// some code ...
// declaration of my_sqrt(), which takes a double, returns a double
extern double my_sqrt(double x);
double result = my_sqrt(4.0); // my_sqrt() has been declared,
//can use it now
// contents of subs.hh
#ifndef SUBS_HH_INCLUDED // ensure this file only gets included ONCE
#define SUBS_HH_INCLUDED
// declarations of functions
extern double my_sqrt(double x);
#endif
Then we would insert an extra line in prog.cc near the top of the file, right around the #include for <iostream.h> that appears in all these examples
// top of prog.ccBasically what happens is the preprocessor, (which is invoked automatically before each compilation of a file) goes through and inserts the contents of "subs.hh" into prog.cc, which thus declares my_sqrt() and introduces into the file scope of prog.cc. If you wanted, you could insert another declaration of my_sqrt() into the code (declarations can be repeated as desired, but not definitions), but its not necessary nor what I usually do.
#include <iostream.h> // make i/o functions accessible
#include "subs.hh" // start file scope of my_sqrt() and other functions
The lines in "subs.hh" beginning with "#" are preprocessor directives, in this case put in to ensure that subs.hh never gets included more than once in a file (to avoid circular inclusion). It's a good practice to include those three lines in all your header files, changing the "SUBS_HH" in "SUBS_HH_INCLUDED" to the name of the header file.
Header files (e.g., stdlib.h, subs.hh, math.h, ...) are convenient places to look when you don't remember how a particular function is called.
// subs.ccPretty simple function, but it illustrates the essential points:
#include "subs.hh" // get consistent declaration of my_sqrt()
#include <math.h> // get declaration of standard math functions
double my_sqrt(double x)
{
return sqrt(x);
}
// somewhere in your program
#include "subs.hh"
int main()
{
// (1) okay
double y_double = 57.3e0;
double x_result = my_sqrt(y_double);
// (2) not good? Very undefined in Fortran!!
// Much gnashing of teeth and pulling of hair till following
// bug found!!
float y_single = 57.3f0;
double x_double = my_sqrt(y_single);
// (3) even worse?
int y_int = 57;
double x_int = my_sqrt(y_int);
}
What's wrong with (2)? We're passing a single-precision float where a double's expected, i.e., a piece of data that is half as wide as expected. What's likely to happen in the equivalent case in Fortran is a disaster - the called routine foo() would at the least get a very different argument from the correct one (57.3)! And if it tried to change the argument (we'll need references to do that - more on that later), it'd try stuffing a (say) 8-byte number into a 4-byte space, probably overwriting the stack (give a reminder of what that is) and causing some kind of mysterious program crash. Case (3) seems even worse...
But luckily we're writing in C++ (and not Fortran, or even C, see below), where because of the declaration of my_sqrt() in "mydefs.hh", the compiler realizes that there's an argument type mismatch (float instead of double, or int instead of double) and does something about it (standard conversion, covered in the next section).
Now, what if we forgot to declare my_sqrt() in all its glory (i.e., never specified what it took, and what it returned)? In that case, the C++ compiler will issue a compile-time error (i.e., the file won't compile successfully, a hopefully non-cryptic compiler error message will be issued, and you'll fix things pronto). So here's an example of where the compiler checking all the types for appropriateness (type-safety) comes in handy.
Finally, because we put the function declarations in one file, namely "mydefs.hh", we can ensure consistent function declarations over a very large number of files that (over time) get compiled separately. That much less of a headache at link-time, if we are guaranteed that all the files are consistent in their function calls. If we didn't use the #include mechanism, and instead inserted (differing) declarations of my_sqrt in several files that would be compiled and ultimately linked together, the error would still be caught before run-time (avoiding mysterious crashes), but then we'd have to go back and edit and possibly recompile every single affected file.
By the way, ANSI C is laxer than C++ in type-safety. If a function gets used before declaration of its arguments and return values, the compiler assumes it to return a single int and take a single int as its arguments (ANSI C thinks of everything as an int by default), which could delay the problem at worst to the linking stage.
One ought to be able to do this:
unsigned int i = 2u;In fact, C++ does exactly what you expect (and want it to do). This is automatic conversion (in particular, integral promotion). There is bound to be some implementation dependencies here (e.g., converting a large unsigned int to an int).
int j = i;
float q = j;
double r = q;
long double s = r;
What about the opposite direction?
long double q = -40.3019231029301L;
double r = q;
int j = r;
unsigned int i = j;
// how about this?
int x = -4;
double z = x + 4.0; // conversion of int to double?
Same thing happens. Basically, the C++ compiler will apply standard conversions in assignments, in arithmetic expressions, and function calls (as well as in explicit conversion or casts, described below) that convert expressions from one type or another. Here's what happens for simple assignments or function calls:
Examples of this:
double x = 4.0L * 1; // integer converted to long-doubleint result = my_sqrt(34); // int arg->double, double result->int
char d = 'a' + 4 // char converted to int, then int result->char
double x = int(my_sqrt(34.0)); // truncate result, convert to doubleWe'll see conversion appear again (importantly) in the context of classes, where conversion can take place via a class constructor.
int int_pi = int( atan(1.0)*4 );
There's also the cast notation for explicit conversion, which effects the same thing:
double x = (int) 4.23;
Casting has its purposes (see the exercises for an example involving the randum number generator rand()), but usually in the context of low-level machine access or getting access to the "guts" of an object via pointers or references. We'll discuss this again soon, regarding pointer conversion.
[ Phy 405/905 Home Page] [Lecturer ]