C Essentials – Part 1 Module 5
C Essentials – Part 1 Module 5
array indexing,
functions,
let’s summarize what we have learned so far. consider the following declaration: //char word[10]
= “dump”;
we’ve created a 10 element array of char and put a string [dump] there. uses 5 chars of array.
word[1] = ‘a’;
puts(word);
Don’t forget:
aposrophes: [char]
quotes: [char *]
word[-1] = 'x';
word[1000000] = 'y';
we can’t predict with certainty what will happen when the two above statements execute
the burden of responsibility for indexing accuracy falls solely on the programmer.
t[i] ≡ *(t + i)
the “C” language standard says: if any pointer is followed by an indexing operator, like this: t[i]
it’s always taken as: *(t + i) In particular, this means that the following expression
*(word + 1) is treated by the compiler just like this one: word[1]
STEP 2: the pointer is increased by one (word + 1). this means the pointer is “shifted toward the
right by one element of the array. the increased pointer is an argument for the dereference
operator, which means that it’s of type char, at least form a syntactic and semantic point
of view. it’s also an I-value. this means that this assignment is fully permissile:
*(word + 1) = ‘a’;
STEP 3: take a look at the expression: *(word + 1). can you expleain why we used the
parentheses? are they necessary? Yes, they are, due to the very high priority of the
dereference operator (*).
we’ve removes the parentheses: *word + 1; what’ll happen now? Firstly, the value (a
character) pointed to by [word] is dereferenced. secondly, a value of [1] is added to the
dereference character (not to the pointer itself). that’s not what we wanted.
let’s consider t[i] = i[t]
if [t] is a pointer and [i] is an expression of type int, t[i] is equivalent to *(t + i)
the addition is commutative, so we can write the prvious epression in the
following way: *(i + t)
this alo means that we’re allowed to write the same indexing operation as i[t]
this means the [] operator is commutative. the compiler doesn’t care whether you write:
word[1] = ‘a’;
or
1[word] = ‘a’;
We advise caution when constructing expressions using the * operator, since any lack
of parentheses will not be detected by the compiler as syntax flaws.
char *p;
char c;
Now we set p to point to the second element of the array string. The recommended
form of this assignment is as follows:
p = string + 1;
Acceptable, though less elegant (however, some would argue, clearer), is the following
form:
p = &string[1];
The p pointer will point to the second element of the array string – look at the figure:
Can you answer the question of what distinguishes the following two instructions:
c = *p++;
and
c = (*p)++;
We can explain: the first assignment is as if the following two disjoint instructions have
been performed:
c = *p;
p++;
c = *p;
string[1]++;
The p pointer is not changed and still points to the second element of the array, and
only this element is increased by 1 .
this is L-value not I-value “l-value” refers to memory location which identifies an
object.
p = string + 2;
p points to the third element of the string array. What happens now?
p[-1] = 'e';
It looks suspicious, because we’ve used the [] operator for a pointer that isn’t the name
of an array. Is this legal? Can we do it?
Yes, it is, and we can. The compiler treats this as normal and thinks that we’re trying
to do something like this:
*(p - 1) = 'e';
We want to change the element located before the one pointed to by the p pointer (in
effect, it leads us to the second element of the array). The entire expression is
treated as an l-value and is assigned the character 'e'.
1 [] , ++ , -- postfix
2 ! , ~ (type), ++ , -- , + , - , * , & , sizeof prefix
3 *, /, % binary
4 +, -
5 << , >>
6 < , <= , > , >=
7 == , !=
8 &
9 |
10 &&
11 ||
Mistake No.1: use of an uninitialized pointer. the compiler is unable to detect the
error, because its nature is revealed at run time only. consider the example
provided in the editor.
#include <stdio.h>
#include <string.h>
int main(void) {
char *ptr; //pointer declared but not initialized.
strcpy(ptr, "you may get into trouble soon");
puts(ptr);
return 0;
}
strcpy will use the current value of the ptr pointer to determine the location where
the string specified in the second parameter should be copied. However,
the ptr variable hasn’t been assigned. strcpy brings with it trouble.
MISTAKE NO.2: exceeding the size of the array. your program may finish its work
with a message about a memory violation error, although if you’re unlucky, the
program will go further, but the results may have little in common with your
intentions.
let’s consider the case where the array’s elements are just arrays, example a
chessboard.
the appearance of two pairs of brackets tells the compiler that the declared array is not
a vector – it’s an array whose elements are vectors.
now let’s go deeper into the multi-dimensional nature of arrays. to find any element of a
a two-dimensional array, we have to use two coordinates: (wording weird) a
vertical (row number) one and a horizontal (column number) one.
The “C” language doesn’t limit the size of the array's dimensions. Here we show
an example of a three-dimensional array.
Now imagine a hotel. It's a huge hotel consisting of three buildings, 15 floors each.
There are 20 rooms on each floor. We need an array that can collect and process
information on the number of guests registered in each room.
Step one – the type of the array's element. We think an int would fit, although it can be
unassigned as there’s no such thing as a negative number of guests.
Step two – calm analysis of the situation. Summarize the available information: 3
towers, 15 floors, 20 rooms.
Now we can write the declaration:
int guests[3][15][20];
The first index (0 through 2) selects one of the buildings; the second (0 through 14)
selects the floor, the third (0 through 19) selects the room number.
Now we can book a room for two newlyweds: in the second building, on the tenth floor,
room fourteen:
guests[1][9][13] = 2;
and release the second room on the fifth floor located in the first building:
guests[0][4][1] = 0;
Before we say goodbye and finish this part of our course, let's check if there are any
vacancies on the fifteenth floor of the third building:
int room;
int vacancy = 0;
for (room = 0; room <20; room++)
if (guests[2][14][room] == 0)
vacancy++;
The vacancy variable contains 0 if all the rooms are occupied; otherwise it displays the
number of available rooms.
we used it to indicate that a function doesn’t return a result, or doesn’t expect any parameters.
According to the following function prototype: void nothingatall (void);
the function should be invoked without parameters and return no result. this is how we should
invoke it: nothingatall();
despite the fact that the [void] type doesn’t represent any useful value, you can still declare
pointers to this type, as in the following example: void *ptr;
a pointer that points to nothing is a kind of pointer of the type void [*] and is called an
amorphous pointer to emphasize that fact that it can point to any value of any type. this
means that a pointer of type void [*] cannot be subject to the dereference operator, so
you must not write anything like this *ptr = 1; it can be justified by the argument that if
[ptr] was of type void *, [*ptr] would be of type [void] and the assignment of a value of
type int is prohibited by the compiler.
pointers of type [void *] are useful when you need to have a pointer, but don’t know what you’re
going to use it for in the future.
Memory on Demand
normally compiler taking care of how memory is allocated but sometimes the developer needs
full control. to manage allocating and freeing of memory the “C” language proivdeds a
set of specialized functions. will be shown two of those functions which require the
inclusion of the header file [stdlib.h]. the first function is used to request access to the
memory block of the specified size. we are asking so request for memory can be accessed
or denied. when allocated memory is no longer need we free\return the memory with the
second function.
the function invoked when the memory is no longer necessary has he following prototype:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *ptr;
ptr = (int *) malloc(sizeof(int));
if(ptr != NULL) {
*ptr = 1000;
printf("ptr points to value of %d", *ptr);
free(ptr);
} else
printf("allocation failed");
return 0;
}
We declare a variable called ptr which will point to the data of type int (the
pointer's type is int * ); initially, we assign no value to this variable;
We invoke the malloc function, requiring the allocation of a memory block
sufficient to store one value of type int ( sizeof (int) ) – this enables our
program to correctly execute, regardless of how many bytes are utilized by
the int type in the specific implementation of the language and/or hardware
platform;
The malloc 's return value is assigned to the ptr variable using type casting,
converting the pointer of the ( void * ) type into an ( int * ) type;
We need to check the resulting pointer value – if it’s not NULL, it can be safely
used; our program assigns a value of 200 to the allocated memory, prints the
value using the printf function and frees the memory;
If the memory allocation goes wrong, malloc returns NULL and we do nothing but
print an error message.
The option of allocating the amount of memory that we really need lets us write
programs that can adapt themselves to the size of the data currently being processed.
Let's go back to the bubble sort algorithm that we looked at some time ago. That
program assumed that there were exactly five numbers to sort. This is obviously a
serious inconvenience. It may happen one day that we want to sort 10,000 numbers or
maybe even hundreds of thousands of numbers.
What then? You can, of course, declare an array of the maximum predictable size, but it
would still be inconvenient. A much better way is to ask the user how many numbers
will be sorted and then allocate an array of the appropriate size.
Let's try to start with a simpler example. In the following program, we allocate an array
containing five elements of type int , set their values, sum them up and, finally, release
the previously allocated memory. As you see, we’ve done so quite haphazardly and
haven’t checked if the memory allocation has succeeded. It can be omitted in a
program like this, but when we perform more complex tasks, we need to verify it.
We want you to pay attention to the fact that the pointer returned by malloc is treated
as if it’s an array. Surprising?
The handling of dynamic arrays (created during the run of the program) is no different
than using regular arrays declared in the usual way.
We owe it all to the [] operator. Regardless of the nature of the array, we can access its
elements in the same way.
elements of arrays can be pointers. what if a 2D array needed to be a dynamic array? we would
NOT attempt to allocate the array like this:
this would make the array rely on us to do the work to calculate the pointer to the element like
this: ptrtab + (cols * r) + c
this is due to the fact that the “C” language arranges two-dimensional arrays row by row. this is
not a very satisfactory solution. we would prefer a clearer notation like this:
yes, it is.
we’ll store the pointer to the beginning of every row so we can each row without
t any acrobatics. how do we store theses pointers? in the array, of course. we’ll
call it the array of rows. every row will have as many elements as columns of the
desired array
every element in the array of rows will be a pointer to a separate row.
we need one more pointer to point to the array of rows – we call it [ptrtab]
what is the type of the variable [ptrtab]?
the type of [ptrtab] is a pointer to a pointer to [int], which is denoted as [int **]. we can write a
complete declaration now – you can see it in the editor window.
int **ptrtab;
once we’ve declared the pointer, we can allocate the array of rows.
Firstly, the pointer returned by [malloc] surrender is converted to type [int **] and assigned to
[ptrtab].
Secondly, the elements of the array of rows are pointers to the rows, to their type is [int *] and
hence, the size of the array is expressed as sizeof(int *) multiplied by the number of
rows.
Finally, we need to allocate memory for every row and store the resulting pointer inside the right
element of the array of rows. easiest way is using a loop
for example, if we want to assign 0 to the element lying in row [r], column [c], we’ll do it this
way: ptrtab[r][c] = 0;
the advantage of such arrays is that, unlike ordinary arrays, every row may be of a different
length.
this is useful for the algorithms that don’t need the entire array to run but only a slic of it. it refers
specifically to triangular metrices. such an array can be allocated in this way:
int rows = 5, r;
int **ptrab;
ptrtab = (int **) malloc (rows * sizeof (int *));
for (r = 0; r <rows; r++)
ptrtab[r] = (int *) malloc (sizeof (int) * (r + 1));//r being the
//row of the actual array
Pay attention to how the triangularity is obtained (the size of the allocated memory
block depends on the row number).
int *array[10];
in this way, we’ve declared a variable [array] which is a 10-element array of pointers to the data
of type [int].
and now let’s look at a seeminly very similar, but completely different, declaraion:
int (*array)[10];
look how the praentheses have changed the meaning of the declaration.
int *(*array)[10];
the statement creates a vraible [array], which is a pointer to a 10-element array whose
elements are pointers to [ints].
Functions – rationale
Going to learn how to declare and write your own funcitons, and how to use them. Also, how to
use functions written by someone else; even when we don’t have their source code.
it often happens that a particular piece of code is repeated many times in your program.
we can define the first condition which an help you decide when to start writing your own
function: if a particular fragment of the coe begins to appear in more than one place,
consider the possiblilibty of isolating it in the form of a function invoked from the points
where the original code was placed before
Reason #2
it may happen that the alogorithm you’re going to implement is so complex that the [main]
funciton begins to grow in an uncontrolled manner, and suddenly you notice that you’re
having problems simply navigating through it.
decomposition – a good, attentive developer divides the code (or more accurately: the problem)
into well-isolated pices and encodes each of them in a the form of a function. this
considerably simplifies the work on thte program ecause each piece of code could be
encoded separately and tested separately.
we can now state the second condition: if a piece of code becomes so large that reading and
understanding it may cause a problem, consider dividing it into separate, smaller
problems and implement each of them in the form of a separate function. this
decomposition contues until you get a set of short funcions, easy to understand and test.
Reason #3
it often happens that the problem is so large and complex that it cannot be assigned ot a single
developer, and a team of developers have to work on it.
the third condition: if you’re going to divide the work among muliple programmers, decompose
the problem to allow the product to be implemented as a set of separatel written
functions.
in other words, the compiler must have the following information for each function you’re going
to use: What is the name of the funcion? How many parameers does the funcion expect
and of which types? what is the function’s return type?
if you don’t provide this information, the compiler will try to deduce it from the first invocation
that appears in our code – this is called an implicit delcaration of the function, and is
both convenient and dangerous at the same time. Imagine what havoc can be wreaked if
the first funcion invocation contains an error.
the compiler can derive infroamtion avout the fucntions from two sources:
the declaration of a funciton is the part of the code containging all three key pieces of
information (name, parameters, type), but doesn’t contain the body of the funcition. it
say how to invoke the function but nothing about what the function does.
A definition of a function is a part of the code containing its full implementation (including the
body)
int CountSheep(void); /* declaration */
void hello(void) {
printf ("You've invoked me – what fun!\n");
return;
}
the first void means that the function does not return any useful value; a type
name may appear at this point, announcing that the function calculates the
result of a given type and returns it after completion;
an opening parenthesis – in fact, the presence of the parenthesis assures the
compiler that it’s dealing with the definition (or declaration) of the function;
a parameter list (we’ll soon say more about this) or the word void if the function
doesn’t expect any parameters;
a closing parenthesis;
a complete block enclosed in curly brackets and containing a set of “C” language
instructions and declarations.
void hello(void);
hello();
We try to treat it as if it evaluates and returns the value of type int which is
at odds with the declaration. The compiler will emit an error message.
hello(2);
We're trying to pass an argument whose type is int through the declaration's void
parameter.
Don’t forget – whenever the compiler knows that the entity has been declared, but knows
nothing about the entity’s type, it thinks it’s an int type.
Note the place in the code where the invocation of the hello function has appeared.
The compiler is ready to invoke the function there – it knows the function's name,
it knows that no parameter is expected and it knows that no return value has
been provided.
#include <stdio.h>
void hello(void) {
printf ("You've invoked me – what fun!\n");
return;
}
int main(void) {
printf("We are about to invoke hello()!\n");
hello();
printf("We returned from hello()!\n");
return 0;
}
Imagine that, due to some important reason, we had to change our code – it now looks
like this:
#include
int main(void) {
printf("We are about to invoke hello()!\n");
hello();
printf("We returned from hello()!\n");
return 0;
}
void hello(void) {
printf ("You've invoked me – what fun!\n");
return;
}
A layout of our code like this will puzzle the compiler, because it’s forced to guess all
the traits of the hello function before the compiler even reads its declaration or
definition. You should expect the compiler to generate a warning message and the
implicit declaration will perform its deduction.
The deduction is very simple – it assumes that all entities of unknown types
are ints. This means that the compiler is convinced that the actual hello declaration
looks as follows:
int hello(void);
The mismatch between the implicit and explicit declarations will cause the compiler to
signal a warning or an error.
Don't forget – whenever the compiler knows that the entity has been declared, but
knows nothing about the entity's type, it thinks it’s an int type.
what should we do to convince the compiler that the implicit delaration isn’t a good idea? is
there even anything we can do?
Yes, there is. we should warn the compiler that the funciton will be used and provide complete
information about it. in other words, we ought to provide the declaration before the first
invocation occurs.
#include <stdio.h>
void hello(void);
int main(void) {
printf("We are about to invoke hello()!\n");
hello();
printf("We returned from hello()!\n");
return 0;
}
void hello(void) {
printf ("You've invoked me – what fun!\n");
return;
}
the [return] statement executed inside any function causes immediate function termination and
a return to the invoker. the form of the [return] statement, and whether there is a need to
sue it, depends on the following circumstances: if the funciton is defined as void, then the
acceptable [return] statement looks like this: return;
if the body of the function doesn’t contain a [return] statement, it will be implicity added after
the last instruction of the funcion’s block.
this means that you can wirte the hello fucntion in the following way too:
void hello(void){
Note that more than one return statement mayexist in the funtion body.
if the function type isn’t specified as void, the only acceptable form of terun statement is as
follows: return expression;
where the expression must provide the value of the type matching the type of the function; in
this case using the [return] statement is mandatory and you cannot omit it in the funciton
body.
if the body doesn’t contain a [return] statement, it will be implicitly added after the last
instruction of the fnction’s block
This means that if we delcare a variable inside a block (e.g. a function’s block) the variable will
be known and recognized only inside that block and, consequently, will not be known in
any other part of the program.
this also means that the name will not interfere with other variables with identical names defined
inside other blocks.
Let’s have a look at an example in the editor, which illustrates this rule.
#include <stdio.h>
void hello(void) {
int i;
We want the hello function to be more exciting and print its happy message twice.
We’ll use the for loop for this task. We’ll also need a control variable, so we add
the int i ; declaration at the beginning of the function block.
The i variable is recognized only inside the hello function's block and nowhere else.
If you need to have another variable of the same name, but inside the main function,
you can declare it there without any problems.
Each of the functions have their own i variable. The variables exist independently of
each other and have nothing in common.
Global variables
If the variable is declared outside of all the blocks, it becomes a global variable.
#include <stdio.h>
int global;
void fun(void) {
int local;
local = 2;
global++;
printf("fun: local=%d global=%d\n", local, global);
global++;
}
int main(void) {
int local;
local = 1;
global = 1;
printf("main: local=%d global=%d\n", local, global);
fun();
printf("main: local=%d global=%d\n", local, global);
return 0;
}
the program begins with the declaration of a global variable – it’s truly
global because it’s outside of any function; this implies that the variable is
accessible to all the functions declared in the source file;
the local variable, which is declared within the fun function, is known only in
this function and has nothing to do with the variable of the same name declared
inside the main function;
Even though we’re using the global variable global , which is outside of all blocks, if you
code like below you may experience some problems, because it may be used in
the fun function before it’s declared (if we declare it too late). Remember that a
function or variable must be declared before it’s used.
Function Parameters
the function parameter is a special kind of local variable. it behaves like a local variabe but
differs from a local variable in two important features:
first, the paremeter is not declared within the function (i.e. it’s not a part of the
function defintion), but must be declared inside a pair of parentheses after the function
name (which means that the parameter declaration is a part of the function declaration)
for example, //void hello2(int times);
the [time] variable may e used inside the function in exactly the same way as if it were a local
variabl; this is called a formal parameter
int notmany = 5;
hello2(100); /* the actual parameter is a literal */
hello2(notmany); /* the actual parameter is a variable */
hello2(2 * notmany); /* the actual parameter is an expression */
it clearly shows that these three invocations are valid (they all deliver a value of type [int] to the
formal parameter)
the values of actual parameters are assigned ot formal parameters at the beginning of function
execution.
let’s assume that the [hello3] function has the following declaration:
hello3(100, 3.14);
the following assignments will be performed implicitly and beyond our control: [f = 3.14].
the second formal parameter is assigned with the current value of the second actual parameter.
the parameterized function may modify its own behavior according to the parameter’s value.
if you invoke this function as follows: [hello2(100);] the following assignment will take place
automatically: [times = 100];
this time causes the function to manifest its joy a hundred times.
Function Results
If the function has been declared with a type before its name, it must perform the return
statement equipped with an expression.
We’ll write a simple program helping us tot solve a trigonometry problem for right-angled
triangles. the program will calculate the length of the hypotenuse using the lengths of the
legs
ask the user for the length of the first leg and square it
ask the user for the length of the second leg an square it
square root the sum of both values using the sqrt() function (pay attention: the
[math.h] header file needs to be included at the top of our source)
you can see the first version of the solution in the editor
#include <math.h>
#include <stdio.h>
int main(void) {
float a, b, a_sqr, b_sqr, c;
printf(“A?\n”);
scanf(“%f”, &a);
a_sqr = a * a;
printf(“B?\n”);
scanf(“ %f”, &b);
b_sqr = b * b;
c = sqrt(a_sqr + b_sqr);
printf(“The length of the hypotenuse is: %f\n”, c);
return 0;
}
introducing a function
in the previous code, there’s a repeated clause used to square a leg. It’s the perfect opportunity to
introduce a function into our program.
/******************************
#include <math.h>
#inlcue <stdio.h>
int main(void){
float a, b, a_sqr, b_sqr, c;
printf(“A\n”);
scanf(“%f”, &a);
a_sqr = square(a);
printf(“B?\n”);
scanf(“%f”, &b);
b_sqr = square(b);
c = sqrt(a_sqr + b_sqr);
printf(“The length of the hypotenuse is: %f\n”, c);
return 0;
}
*****************************/
we can make one simple modification – note [a_sqr] and [b_sqr] are used as temporary
containers only. the [c] variable is utilized in the same way. we can remove them – take
a look at the code in the editor.
/*******************************
#include <math.h>
#inlcude <stdio.h>
int main(void) {
float a, b;
printf(“A?\n”);
scanf(“%f”, &a);
printf(“B?\n”);
scanf(“%f”, &b);
printf(“The length of the hypotenuse is: %f\n”, sqrt(square(a) + square(b)));
return 0;
}
**********************************/