Functions


At the core of all Managed C++ programs is the function. It is the source of all activity within a program. Functions also enable programmers to break their programs into manageable chunks. You have already been using a function called main(). Now let's see how you can go about creating a few of your own.

The general format of a function looks like this:

 return-type function-name ( parameter-list ) {     statements-of-the-function; } 

The return-type of the function is the value type, a pointer or a reference that is returned by the function when it finishes. The return type can be any value type, reference, or pointer, even ones that are user defined. If no return type is specified for the function, then Managed C++ defaults the return value to int. If the function does not return a value, then the return value should be set to the keyword void.

The function-name is obviously the name of the function. The rules of naming a function are the same as those for naming a variable.

The parameter-list is a comma-separated list of variable declarations that define the variable, which will be passed to the function when it starts executing. Parameter variables can be any value types, references, or pointers, even ones that are user defined.

Passing Arguments to a Function

You have two different ways of passing arguments to a function: by value and by reference. Syntactically, there is little difference between the two. In fact, the only difference is that passing by reference has an additional ampersand (&) placed before the value name:

 Int32 example ( Int32 ByValue, Int32 &ByReference ) { } 

The big difference is in how the actual values are passed. When passing by value, a copy of the variable is passed to the function. Because the argument is a copy, the function can't change the original passed argument value. For example, this function takes the value of parameter a and adds 5 to it:

 Int32 example ( Int32 a ) {     a = a + 5;     return a; } 

When the function is called

 Int32 a = 5; Int32 b = example(a); 

the value of a will still be 5, whereas the value of b will be 10.

What if you want to actually update the value of the parameter passed so that it reflects any changes made to it within the function? You have two ways to handle this. The first is to pass a pointer by value. Because you are passing an address to the value, and not the actual value, any changes that you make to the value within the function will be reflected outside the function. The problem of passing by pointer is that now the syntax of the function is more complicated because you have to worry about pointers.

 Int32 example ( Int32 *a ) {     *a = *a + 5;     return *a; } 

When the function is called

 Int32 a = 5; Int32 b = example(&a); 

the value of a and b will both be 10.

The second approach is to pass the arguments by reference. When passing arguments by reference, the argument value is not copied; instead, the function is accessing an alias of the argument or, in other words, the function is accessing the argument directly.

 Int32 example ( Int32 &a ) {     a = a + 5;     return a; } 

When the function is called

 Int32 a = 5; Int32 b = example(a); 

the value of a and b will both be 10.

There is a pro and a con to using references. The pro is that it is faster to pass arguments by reference, as there is no copy step involved. The con is that, unlike using pointers, other than &, there is no difference between passing by value or reference. This can make for the very real possibility of changes happening to argument variables within a function without the programmer knowing about them.

The speed benefit is something some programmers don't want to give up, but they still want to feel secure that calling a function will not change argument values. To solve this problem, it is possible to pass const reference values. When these are implemented, the compiler makes sure that nothing within the function will cause the value of the argument to change:

 Int32 example ( const Int32 &a ) { //    a = a + 5;      // This line will cause a compiler error because                        // we are trying to change the const a     return a + 5; } 

When the function is called

 Int32 a = 5; Int32 b = example(a); 

the value of a will still be 5, and the value of b will be 10.

Returning Values From a Function

Returning a value from a function is a two-step process. First, specify the type of value the function will return, and second, using the return statement, pass a return value of that type:

 Double example() {     Double a = 8.05;     // do some stuff     return a; } 

Returning Pointers

You need to take care when you return a pointer from a function.

Caution

Never return a pointer to a variable of local scope to a function, because it will not be a valid pointer upon exiting the function.

Never do this:

 Int32* ERRORexample() {     Int32 a = 8;     // do some stuff;     return &a;    // This variable will disappear when the function ends so }                  // pointer will be invalid 

Instead, you should return the pointer a that was passed to the function or the pointer b that was created by the new operator in the function:

 Int32* Okexample( Int32* a) {     Int32 *b= new Int32(8); // Initialize to 8     // do some stuff;     if (*a > *b)         return a;     else         return b; } 

In traditional C++, the variable b in the preceding example would be a classic location for a memory leak, as the developer would have to remember to call the delete statement on the returned value b. This is not the case in Managed C++, because pointers to Int32 value types are garbage collected automatically when no longer used, thus delete need not be called.

Returning References

You also need to take care when you return a reference from a function.

Caution

Never return a reference to a variable of local scope to a function, because it will not be a valid reference upon exiting the function.

Never do this:

 Int32& ERRORexample() {     Int32 a = 8;     // do some stuff;     return a;      // This variable will disappear when the function ends so }                   // reference will be invalid 

Instead, you should return a reference that was passed to the function, or a pointer or reference to a variable that was created by the new operator within the function:

 Int32& OKexample( Int32& a) // Passing a reference {     Int32 &b= *new Int32(8);  // Creating a reference     // do some stuff;     if (a > b)         return a;     else         return b; } 

Something worth noting in this function is the creation of a reference using the new operator. Again, with traditional C++ you would have to delete the reference. Fortunately, because Int32 references such as pointers get garbage collected in Managed C++, there is no need for the delete statement and no memory leak occurs.

Returning Managed Arrays

Another thing that you will probably return from a function is an array. In traditional C++, this would be handled by returning a pointer to the array. Unfortunately, this does not work for managed arrays, as managed arrays are not pointers. It is true that they are reference types, but there is no syntax for accessing arrays using the address of (&) operator. As this is the case, a new syntax for return arrays has been added for Managed C++:

 Int32 MethodName() [] { } 

For example:

 Int32 retArray() [] {     Int32 array[] = new Int32[5];     for (Int32 i = 0; i < array.Length; i++)     {         array[i] = i;     }     return array; } 

To assign the return value to an array is simple enough:

 Int32 i[]; i = retArray(); 

Or you can do it in one statement, like this:

 Int32 i[] = retArray(); 

Prototypes

You can't use a function until after it has been defined. Okay, there is nothing stopping you from placing function declarations in every *.cpp file where it is used, but then you would have a lot of redundant code.

The correct approach is to create prototypes of your functions and place them within an include (.h) file, (I cover include files in Chapter 4.) This way, the compiler will have the definition it needs and the function implementation will be in only one place. A prototype is simply a function without its body followed by a semicolon:

 Int32 example ( const Int32 &a ); 

Function Overloading

In the dark ages of C, it was a common practice to have many functions with very similar names doing the same functionality for different data types. For example, you would see functions such as PrintInt (int x) to print an integer, PrintChar(char c) to print a character, PrintString(char *s) to print an array of characters, and so on. Having many names doing the same thing became quite a pain. Then along came C++, and now Managed C++, with an elegant solution to this annoyance: function overloading.

Function overloading is simply Managed C++'s capability to have two or more methods with exactly the same name but with a different number or type of parameter. Usually, the overloaded functions provide the same functionality but use different data types. Sometimes the overloaded functions provide a more customized functionality due to having more parameters to more accurately solve the problem. But, in truth, the two overloaded functions could do completely different things. This, however, would probably be an unwise design decision, as most developers would expect similar functionality from functions using the same name.

When a function overloaded call takes place, the version of the method to run is determined at compile time by matching the calling function's signature with those of the overloaded function. A function signature is simply a combination of the function name, number of parameters, and types of parameters. For function overloading, the return type is not significant when it comes to determining the correct method. In fact, it is not possible to overload functions by changing only the return type. When you do this, the compiler will give a bunch of errors, but only the one indicating that a function is duplicated is relevant.

There is nothing special about coding overloaded functions. For example, here is one function overloaded three times for the super-secret Test function:

 Int32 Test () { /* do stuff */ } Int32 Test (Int32 x) { /* do stuff */ } Int32 Test (Int32 x, Int32 y, Double z) { /* do stuff */ } 

Calling an overloaded function is nothing special either. Simply call the function you want with the correct parameters. For example, here is some code to call the third super-secret Test function:

 Test (0, 1, 2.0); 

The only thing that a Managed C++ programmer needs to concern herself with that a traditional C++ programmer doesn't is that built-in value types and their corresponding runtime __value types produce the same signature. Thus, these two functions are the same and will produce an error:

 Int32 Test (Int32 x) { /* do stuff */ } int Test (int x) { /* do stuff */ }  // Error Duplicate definition of Test 

Passing Arguments to the main() Function

So far in every example in this book, the main() function has had no arguments. If you have worked with C++ before, you know that it is possible to retrieve the parameters passed to a program from the command line via the main() function's arguments. (If you haven't, well, now you know too.)

Basically, the main() function counts all the parameters passed to it, including the program that is being run, and places the count in the first argument, traditionally called argc. Next, it takes all the parameters and places them in a char or SByte array, each parameter being a separate element of the array. Finally, it passes a pointer to this array as the second argument, usually called argv.

In Managed C++, the main() function actually compiles to native code and not MSIL code, so the argv argument is not garbage collected. Fortunately, the cleanup of argv is handled automatically (so I guess you could say that it is sort of garbage collected). Unfortunately, because it is not garbage collected, it adds a minor wrinkle in its declaration. SByte by default uses garbage collection when defined as a pointer; therefore you need to add the __nogc keyword after SByte to let the compiler know that you do not want a garbage collected pointer definition. I cover __nogc in much more detail in Chapter 3. (By the way, if you were to use char, you would not have this problem, because all the class library aliases do not default to garbage collection.)

Listing 2-15 is a little program that reads in all the parameters passed to it and then writes them out.

Listing 2-15: Parsing a Command Line

start example
 #using <mscorlib.dll> using namespace System; Int32 main ( Int32 argc, SByte __nogc *argv[] ) {      Console::WriteLine ( argc.ToString() );      for (int i = 0; i < argc; i++)      {          Console::WriteLine ( new String(argv[i]) );      }      return 0; } 
end example

Figure 2-16 shows the results of this little program when passed the parameter "This is a test this is only a test".

click to expand
Figure 2-16: Results of MainArgs.exe




Managed C++ and. NET Development
Managed C++ and .NET Development: Visual Studio .NET 2003 Edition
ISBN: 1590590333
EAN: 2147483647
Year: 2005
Pages: 169

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net