DAY
- 3
In yesterday’s class we were dealing
with procedural programming in C++. In today’s class we will just continue with
the same thing.
As you know, the procedural approach
mainly deals with functions. So lets again start with the basic syntax of a
function. The syntax of a function is
Return_Type
Function_Name(Function_Arguments) {..definition..}
Lets not worry
about the definition. Let’s start from the RHS ie) function arguments and move
forward.
Consider the
following function:
#include
<iostream>
using namespace
std;
void volume(int
length, int width, int height) {
cout << "Vol of a box with
length = " << length << '\t' ;
cout << "Width = "
<< width << '\t';
cout << "Height = " << height << '\t'
<< "is = ";
cout << length * width * height
<< endl;
}
int main() {
int l = 10, w = 2, h = 5;
volume(l, w, h);
}
Sticking on to
our C rules, we know that whenever we invoke the function volume, I need to
strictly pass three arguments. Else it is going to throw me an explicit compile
time error. But in C++, we have the flexibility of invoking a particular
function by passing lesser number of arguments. ie_ I can invoke it as
volume(l, w);
volume(l);
volume();
This is achieved
using something known as Default Arguments.
Default Arguments :
Whenever we have to invoke a
particular function with a lesser number of arguments, we need to provide
default values which can be considered by your compiler in case of missing
arguments. The default arguments should be provided in the prototype
declaration of that particular function. If the function is defined above main,
then we can provide it in the definition itself. Lets try to modify the above
example and see how it works.
#include
<iostream>
using namespace
std;
void volume(int
length = 1, int width = 1, int height = 1)
{
cout << "Vol of a box with
length = " << length << '\t' ;
cout << "Width = "
<< width << '\t';
cout << "Height = " << height << '\t'
<< "is = ";
cout << length * width * height
<< endl;
}
int main() {
int l = 10, w = 2, h = 5;
volume(l, w, h);
volume(l, w);
volume(l);
volume();
}
Usually whenever
a function is invoked, the compiler maps the passed parameters with the
function from LHS to RHS. Ie) leading edge to trailing edge.
In the first
case, when function is invoked with all the three arguments, compiler simply
ignores the default values and considers the user passed parameters. In the
second case, when one argument is missing, the compiler maps the first and
second argument passed by the user to the first and second argument in the
function and assumes that the trailing argument is missing. He checks if you
have provided default values for the trailing argument. Since you have done it,
he is perfectly happy.
In the second case, compiler simply
maps the single argument passed by the user to the left most parameter and
assumes that the 2 trailing arguments are missing. He checks for default
values. Since you have provided it, he is perfectly happy.
In the third case, since the user
have not passed any arguments, compiler considers all the default values for
the further manipulation. This is how the concept of default arguments works in
the above program and ultimately the output printed will be
Vol of a box
with length = 10 Width = 2 Height =
5 is = 100
Vol of a box
with length = 10 Width = 2 Height =
1 is = 20
Vol of a box
with length = 10 Width = 1 Height =
1 is = 10
Vol of a box
with length = 1 Width = 1 Height =
1 is = 1
In the above
function I have provided default value for all the arguments. in reality, it is
not necessary that I pass default values for all the arguments. if you are not
providing default values for all the arguments, you need to specify a specific
order while providing it. In the sense, you cannot pass it randomly. As I told
you before, when ever user passes some argument, the compiler maps it from LHS
to RHS. So if ever some arguments are missing, compiler always considers it as
the trailing argument. So when we provide default values, it is a must that we
provide it from RHS to LHS. ie) from trailing edge to leading edge. Compilers
always do this check of default arguments even before any of the function is
called. If default arguments are not provided correctly, at the point of
declaration itself, compiler will throw you an error.
Summarizing,
following can be the working conditions with function calls for the above
volume program.
1.
void
volume(int length, int width = 1, int height = 1);
a.
volume(l,
w, h);
b.
volume(l,
w);
c.
volume(l);
d.
volume();
//only for this call, the program will through you an error.
2.
void
volume(int length, int width , int height = 1);
a.
volume(l,
w, h);
b.
volume(l,
w);
c.
volume(l);
// for this call compiler will throw error
d.
volume();
// for this call also compiler will throw error
But the
following function declarations are going to throw you error at the point of
prototype declarations itself.
1.
void
volume(int length = 1, int width = 2, int height );
2.
void
volume(int length = 1, int width , int height );
3.
void
volume(int length , int width = 1, int height );
4.
void
volume(int length = 1, int width , int height = 1);
That s all about
default arguments. Lets dig into few more details of our function arguments.
All this time we
were worrying about the values of arguments. Now lets talk a little about how
the parameters are passed to the function. If you notice, in the previous
function, I am passing all the three arguments by value to the function. Other
than pass by value you might have learnt about one more passing technique in C
right. what is that ? pass by address or collection by pointers right.
Both the above
techniques can be compared in terms of two things. One is memory and the other
one is reflecting the changes back in the calling function. Lets take the case
of memory. As long as programs are simple like our volume program, memory
cannot be compared at all since even pointers occupy 4 bytes of memory. But
when we are passing large sized user defined variables, pass by address of course
aids in saving some memory since in pass by value the entire memory will be
duplicated.
Secondly, if you
consider returning, as long as it is a single variable whose value have to be
reflected in called function, a return statement will serve our purpose. But in
functions like swap where we have to reflect the changes in two variables to
the calling function, a single return statement is not going to serve our
purpose. In this case again our pass by address is going to save us in that the
changes made in the called function will be automatically reflected back in the
calling function.
Due to backward
compatibility both the above methods are perfectly legal in C++. In addition to
this, C++ have a better way of argument passing which is termed as Call by reference.
To understand this, first we should know, what exactly a reference is.
Reference Variable:
Reference variable can be defined in
two different ways. One is from user’s point of view and the other is from
implementation point of view.
From user’s point of view, reference
can be considered as an alias or an alternate name for an already existing
variable. The syntax for reference is
data_type &new_variable_name =
old_variable_name;
Let us see and
example and lets try to analyze how reference variable works.
//illustrating
reference in memory
#include
<iostream>
using namespace
std;
int main() {
int a = 10;
int *b = &a;
int &c = a;
cout << &a << endl;
cout << a << endl;
cout << &b << endl;
cout << b << endl;
cout << *b << endl;
cout << &c << endl;
cout << c << endl;
return 0;
}
Let’s try to put
the memory picture of the above program as below
& = 910
|
a / c = 10
|
b
|
& = 914
|
If you notice
you can see that, &a and &c remains the same or atleast printed the
same giving an impression to the outsider that reference doesn’t occupy any
memory. But it is not true. To justify this, we need to get the definition of
reference from implementation point of view.
From
implementation point of view
“Reference is an
internal constant pointer which gets de-referenced automatically whenever you
use it “ which means reference do occupy memory of 4 bytes. Since it is
internally treated as a constant pointer, reference need to be initialized at
the time of declaration itself and once aliased to a particular variable, it
cannot be changed further down in the program.
Now lets try to
justify the behavior of the above program.
When I write
int &c = a,
internally the
pointer c is holding the address of a. As I told you, reference gets
automatically dereferenced when ever I use it.ie) whenever we write c in the above program, it
will be automatically treated as *c. that was the reason it was showing us 10
when I try to print c. in the same manner when I gave &c, it was taken as
&(*c)
& and * are
complement operator. They cancels each other. So cout statement prints the
contents of c which is the address of a.
This same
reference can be used in call by
reference technique in C++. Let s try to modify the above volume program
with call by reference.
#include
<iostream>
using namespace
std;
void volume(int
&length, int &width, int &height) {
cout << "Volume of a box
with length = " << length << '\t';
cout << "width = "
<< width << '\t';
cout << "Height = " << height
<< '\t' << "is = ";
cout << length * width * height
<< endl;
}
int main() {
int l = 10, w = 20, h = 4;
volume(l, w, h);
}
Comparing all
the three techniques :
Here the disadvantage
of memory overhead associated with pass by value is avoided. We could have used
pointers instead of this reference to save memory but it is ur responsibility
to dereference it. Another thing is pointer may point to anything or it may not
point to anything ie) NULL. So it may result in unexpected behaviour if used.
We started with
our syntax of function right. Let’s move a little more towards left. The next
thing which is going to come into picture is function name.
In your C, whenever
you wrote functions, u ensured that each and every function carries distinct
name right. Even while writing related functions : in the sense function which
are doing similar job, for eg : let s say swap on integer, float and character,
you used to provide three distinct names like swap_int(int, int),
swap_float(float, float) and swap_char(char, char). Isn’t it
a headache to the programmer to remember all these names even though
they are doing the same job. Keeping these things in mind, C++ designers
introduced a new concept where in we can group functions performing similar job
under a single name. But for your compiler to identify between different
functions we need to ensure that each and every function differs in terms of
arguments. In the sense, arguments can differ either in terms of type, order,
number, consteness of pointer, consteness of reference and ultimately scope
(namespace scope and global scope or class scope). This feature of C++ is known
as
Function overloading:
Lets try to
include this concept in our volume program.
#include
<iostream>
using namespace
std;
void volume(int
side) {
cout << "Volume of cube with
side = " << side << "is = " << side * side *
side;
}
void volume(int
radius, float height) {
cout << "vol of a cyl =
" << 3.14 * radius * radius * height << endl;
}
void volume(int
l, int w, int h) {
cout << "vol of prism =
" << l * w * h << endl;
}
void
volume(float r) {
cout << "vol of a sphere =
" << (4 / 3) * 3.14 * r * r * r << endl;
}
int main() {
int cube_side = 10;
int cyl_rad = 20;
float cyl_hgt = 12.2;
int prism_len = 1, prism_wid = 2,
prism_hgt = 3;
float sph_rad = 2.2;
volume(cube_side); //volume of a cube = a * a * a
volume(cyl_rad, cyl_hgt); //volume of a cyllinder
= pi * r * r * h
volume(prism_len, prism_wid,
prism_hgt); //volume of a prism =
l * w * h
volume(sph_rad); //volume of a sphere = 4/3
* pi * r * r * r
}
How it works internally?
Function overloading works internally using a new principle termed as name
mangling or name decoration. Let’s see what it is.
In C++, every function name we use
(whether you use function overloading or not) will internally converted into a
distinct name by your compiler based on its arguments and scope. This property
is known as name mangling. Remember it does it purely based on argument and
name and not based on return type. Because of this same reason, we cannot
overload functions based on return type.
C language doesn’t support name
mangling. That s why we cannot implement function overloading in C.
Try out for the
following ambiguities in function overloading.
1.
//illustrating
the ambiguities which can arise in function overloading
#include
<iostream>
using namespace
std;
void fun(float
a) {
cout << "in flt fun"
<< endl;
}
void fun(int a)
{
cout << "in int fun"
<< endl;
}
int main() {
fun(1.1);
}
//Ambiguity
arises due to confusion in typecasting. Avoid it by specifying 1.1f
2.
//illustrating
the ambiguities which can arise in function overloading
#include
<iostream>
using namespace
std;
void fun(int
&b) {
cout << "in flt fun"
<< endl;
}
void fun(int a)
{
cout << "in int fun"
<< endl;
}
int main() {
int a = 10;
fun(a);
}
//cannot
overload it like this since function call syntax for both the functions are the
same.
If you notice
here, you can see that a single function name is used for multiple
functionalities. Or I can say, a single person existing in many forms. I have
already mentioned the technical term for it right. Polymorphism.
In function overloading binding
(resolution of which function call to be related to which function definition)
occurs during compile time, we call function overloading as one type of compile
time polymorphism.
Since I have
introduced polymorphism to you, let me tell you, abstraction, encapsulation,
inheritance and polymorphism is considered to be the major features of object
oriented programming languages.
Finally let me
talk about your function as a whole. You know why we use functions and how your
function works right. Let us brush it up first
Why
do we go for functions? It has so many advantages and thing of our interest is
saving memory space. This is bcos, instead of writing it as functions, if the
code is getting repeated in our program, it ties up the stack memory as well as
the code segment memory. Generally, when the compiler sees a fn call, it
normally generates a jump to the fn. At the end of the fn it jumps back to the
instruction following the call.
The
above sequence may save memory but it takes some extra execution time. There
need to have an instruction for the jump to the fn, the instructions for saving
registers, instructions for pushing arguments onto the stack in the calling
program and removing them from the stack in the fn, instructions for storing
registers and an instruction to return to the calling program. Even if it s a
very small fn we do all the above steps.
Usually
when the function is large, since we can save much amount of memory, we
compromises on the time overheads generated by the above situation. But if the
function size is small, the amount of memory we are going to save is very less.
At the same time we have to compromise on time overheads also. This situation
is not that desirable.
This
execution time in short fn could have been saved if I had a provision of
putting the code in the fn body directly in line with the code in the calling
program. Ie) each time there s a fn call in the source file, the actual code
from the fn is inserted instead of a jump to the fn.
Normal function inline function
Fn definiton
Fn1 fun
Fn
1
Fn definition
C++
designers implemented a technique which used the above logic and it is known as
inline functions. Inline function is a request to the compiler. It requests the
compiler to first check if the function size is small and if small, instead of
jumping to the function definition, the function call will get replaced by
function definition. If big, the inline request will be neglected by the
compiler.
The syntax for inline
functions is
inline return type
function name(function arguments) {}
If ever the functions are considered as inline functions,
since it is text replacement, it will increase the text segment size.
Have you studied any text replacement like this in your C
language? Yes right. Macros. But both these things are completely different.
First of all macro is a pre processor directive and so
cannot be ignored. Ie) if declared as macro, the name for sure will be replaced
by the preprocessor. But inline functions are just request to the compiler and
there are changes, the request gets neglected. Some of the situations where the
inline request gets neglected is
- If the
function size is pretty large.
- If the
function declaration and function definition is in different translation
unit.
- If there
are large loops in the function
- If the
declaration is above the main and definition is below the main
- If the
function is recursive.
- If there is
a function pointer pointing to the inline function.. This is because
function pointer need to have the address of the function where as since
inline function are simple text replacement it wont be having any address.
Another difference
between the macro’s and inline functions is, since the macro is taken care of
by the pre processor, there wont be any type checking occurring here where as
since inline functions are taken care of by the compiler, proper type checking
will be taking place there. For example,
lets say I have written a macro for finding the square of an integer. For this
macro, even if I pass character instead of integer, there wont be any error at
all where as if it is inline function since compiler is doing the replacement
it will check if you have passed integer itself for the function. Else it will
throw you and explicit error.
That s all about
the functions in the procedural approach of C++ . From tomorrow we will start off with the
object oriented approach in C++.
Reference :
C++ Complete Reference : Herbert Schildt
C++ Primer Plus : Stephen Prata
0 comments:
Post a Comment