Categories
Computer Science / Information Technology Language: C

Pointers

A pointer is a variable whose value is the address of another variable, i.e., the direct address of the memory location.

Pointer Notation

Consider the declaration,

int i = 3 ;

This declaration tells the C compiler to:

  1. Reserve space in memory to hold the integer value.
  2. Associate the name ‘i’ with this memory location.
  3. Store the value 3 at this location. We may represent i’s location in memory by the following memory map.

We see that the computer has selected memory location 65524 as the place to store the value 3. The location number 65524 is not a number to be relied upon, because some other time the computer may choose a different location for storing the value 3. The important point is, i’s address in memory is a number. We can print this address number through the following program:

int main( )
{
   int i = 3 ;
   printf ( "\nAddress of i = %u", &i ) ;
   printf ( "\nValue of i = %d", i ) ;
}

The output of the above program would be:

Address of i = 65524

Value of i = 3
  • The ‘&’ used in printf( ) statement is C’s ‘address of’ operator. It returns the address of the variable ‘i’, which in this case happens to be 65524.
  • As the address cannot be negative, it is printed out using %u, which is a format specifier for printing an unsigned integer.
  • The other pointer operator available in C is ‘*’, called the ‘value at address’ operator. It gives the value stored at a particular address. The ‘value at address’ operator is also called the ‘indirection’ operator. Observe the output of the following program:
#include <stdio.h>
int main()
{
    int i = 3 ;
    printf ( "\nAddress of i = %u", &i ) ;
    printf ( "\nValue of i = %d", i ) ;
    printf ( "\nValue of i = %d", *( &i ) ) ;
    return 0;
}

The output of the above program would be:

Address of i = 65524
Value of i = 3
Value of i = 3

Note that printing the value of *( &i ) is the same as printing the value of i. The expression &i gives the address of the variable i. This address can be collected in a variable, by saying,

j = &i ;

But remember that j is not an ordinary variable like any other integer variable. It is a variable that contains the address of another variable (i in this case). Since j is a variable the compiler must provide it space in the memory. Once again, the following memory map would illustrate the contents of i and j.

As you can see, i’s value is 3 and j’s value is i’s address. But wait, we can’t use j in a program without declaring it. And since j is a variable that contains the address of ‘i’, it is declared as,

int *j ;

Like any variable or constant, you must declare a pointer before using it to store any variable address. The general form of a pointer variable declaration is −

type *var-name;

Here, type is the pointer’s base type; it must be a valid C data type and
var-name is the name of the pointer variable. The asterisk * used to declare a pointer is the same asterisk used for multiplication. However, in this statement, the asterisk is being used to designate a variable as a pointer. Take a look at some of the valid pointer declarations −

int *ip;     /* pointer to an integer */
double *dp;  /* pointer to a double */
float *fp;   /* pointer to a float */
char *ch     /* pointer to a character */

The actual data type of the value of all pointers, whether integer, float, character or otherwise, is the same, a long hexadecimal number that represents a memory address. The only difference between pointers of different data types is the data type of the variable or constant that the pointer points to.

How to Use Pointers?

There are a few important operations, which we will do with the help of pointers very frequently.

  1. We define a pointer variable.
  2. Assign the address of a variable to the pointer.
  3. Access the value at the address available in the pointer variable, also called dereferencing the pointer.

This is done by using the unary operator * that returns the value of the variable located at the address specified by its operand. The following example makes use of these operations −

#include <stdio.h>
int main ()
{
    int var = 20; /* actual variable declaration */
    int *ip; /* pointer variable declaration */

    ip = &var; /* store address of var in pointer variable*/

    printf("Address of var variable: %x\n", &var );

    /* address stored in pointer variable */
    printf("Address stored in ip variable: %x\n", ip );

    /* access the value using the pointer */
    printf("Value of *ip variable: %d\n", *ip );

    return 0;
}

When the above code is compiled and executed, it produces the following result −

Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20

The assignment operation (=) between two pointers makes them point to the same pointee. It’s a simple rule for a potentially complex situation, so it is worth repeating: assigning one pointer to another makes them point to the same thing. Consider the following example:

#include <stdio.h>
int main() {
    int num = 42;
    int *numPtr, *second;
    numPtr = &num;
    second = numPtr; // Same as: second = &num;
    printf("Value of num: %d\n", num);
    printf("Address of num: %x\n", &num);
    printf("Value of numPtr: %x\n", numPtr);
    printf("Value numPtr pointing to: %d\n", *numPtr);
    printf("Value of second: %x\n", second);
    printf("Value second pointing to: %d\n", *second);
    return 0;
}

Output

Value of num:             42
Address of num:           6a6e55f4
Value of numPtr:          6a6e55f4
Value numPtr pointing to: 42
Value of second:          6a6e55f4
Value second pointing to: 42

The above program can be summed up with the following block diagram:

Tip: Make drawings. Memory drawings are the key to thinking about pointer code.
When you are looking at code, thinking about how it will use memory at run time, make a quick drawing to work out your ideas. That’s the way to do it.

Sharing

Two pointers referring to the same memory address are said to be “sharing”. That two or more entities can cooperatively share a single memory structure is a key advantage of pointers in all computer languages. Sharing can be used to provide efficient communication between parts of a program.

Pointer Arithmetic

A pointer in c is an address, which is a numeric value. Therefore, you can
perform arithmetic operations on a pointer just as you can on a numeric value. There are four arithmetic operators that can be used on pointers: ++, –, +, and -. Before jumping into the examples, please make sure you have a basic idea of hexadecimal values and arithmetic operations on them as all virtual addresses are hexadecimal values. Consider the following program running on a 32-bit compiler (hence the size of an integer is 4 bytes):

#include <stdio.h>
int main() 
{
    int i = 5;
    int* ptr = &i;
    printf ("%p\n", ptr);
    return 0;
}

The output is:

0x7ffd58311d7c

In the above simple program, the address of an integer value is assigned to a pointer. And the address stored in the pointer is being printed. Now, consider adding 1 to the pointer directly and also by using post increment, which is done in the following program:

#include <stdio.h>
int main()
{
    int i = 5;
    int* ptr = &i;
    printf ("ptr : %p\n", ptr);
    ptr = ptr + 1;
    printf ("ptr + 1 : %p\n", ptr);
    ptr++;
    printf ("ptr++ : %p\n", ptr);
    return 0;
}

The output where the last two characters of the address is highlighted for convenience:

ptr : 0x7ffd58311d7c
ptr + 1 : 0x7ffd58311d80
ptr++ : 0x7ffd58311d84

One may notice that ptr + 1 is 80 and not 7d. Consider another example where we attempt to add 1 to a pointer pointing to a value of data type double:

#include <stdio.h>
int main()
{
    double i = 5;
    double* ptr = &i;
    printf("Size of double: %ld\n", sizeof(double));
    printf ("ptr : %p\n", ptr);
    ptr = ptr + 1;
    printf ("ptr + 1 : %p\n", ptr);
    ptr++;
    printf ("ptr++ : %p\n", ptr);
    return 0;
}

The output where the last two characters of the address are highlighted for convenience. It is important to remember that the addition is happening to hex values and not decimal values.

Size of double: 8
ptr : 0x7ffe92406338

ptr + 1 : 0x7ffe92406340
ptr++ : 0x7ffe92406348

Points to note from the above two examples:

  • One can add integer values to a pointer either by using the ‘+’ operator or by post (and pre) incrementation.
ExpressionData type ptr pointing toSize on 16-bit compilerOperation (hex addition)Result
ptr + 1int47c + 480
ptr++int490 + 484
ptr + 1double838 + 840
ptr++double840 + 848
  • Hence, it can be concluded that the compiler is smart enough to add the integral multiple of the size of the data type it is pointing to with the factor by which it is getting added. That is, if the factor is 1, the addition will be size * 1, if the factor is 2, the addition will be size * 2.

Pointer arithmetic is extensively used in arrays. It is further explored in the ‘Arrays’ section.

Comparison of pointers

Consider the following example:

#include <stdio.h>
int main()
{
    double i = 5;
    double* ptr_1 = &i;
    double* ptr_2 = &i; // Same as double* ptr_2 = ptr_1
    if (ptr_1 == ptr_2)
    printf("ptr_1 and ptr_2 are pointing to same location");
    return 0;
}

Output:

ptr_1 and ptr_2 are pointing to same location

From the above example, it is clear that a comparison operator of equality between two pointers checks if the pointers are pointing to the same memory location. It is often that we use a comparison operator to check if a pointer is a NULL pointer (discussed in the ‘NULL pointers’ subsection).

Caution

It is very important to realize that the pointers are pointing to an unallocated area or possibly an area allocated to some other process or variable local to the program. Any attempt to dereference a pointer in the above examples and similar scenarios will result in segmentation fault errors.
Do not attempt the following operations on pointers… they would never work out.

  1. Addition of two pointers
  2. Multiplication of a pointer with a constant
  3. Division of a pointer with a constant

Exercise: Write a program which decrements ptr values pointing to various other data types, using arithmetic ‘-’ operator and pre and post decrement unary operators. Analyze the behaviour of the pointers in the program.

Shallow and Deep Copying

In particular, sharing can enable communication between two functions. One function passes a pointer to the value of interest to another function. Both functions can access the value of interest, but the value of interest itself is not copied. This communication is called “shallow” since instead of making and sending a (large) copy of the value of interest, a simple pointer is sent.

The alternative where a complete copy is made and sent is known as a “deep” copy. Deep copies are simpler in a way since each function can change its copy without interfering with the other copy, but deep copies run slower because of all the copying. This topic is explained in detail with examples under “Heaps” in the “Advanced topics in Pointers” section.

NULL Pointers

A pointer that is assigned NULL is called a null pointer. It is always a good practice to assign a NULL value to a pointer variable in case you do not have an exact address to be assigned. This is done at the time of variable declaration.

The NULL pointer is a constant with a value of zero defined in several standard libraries. Consider the following program −

#include <stdio.h>
int main ()
{
    int *ptr = NULL;
    printf("The value of ptr is : %x\n", ptr );
    return 0;
}

Output:

The value of ptr is 0

In most operating systems, programs are not permitted to access memory at address 0 because that memory is reserved by the operating system. However, the memory address 0 has special significance; it signals that the pointer is not intended to point to an accessible memory location. But by convention, if a pointer contains the null (zero by address convention) value, it is assumed to point to nothing.

To check for a null pointer, you can use an ‘if’ statement as follows −

if(ptr) /* succeeds if p is not null */
if(!ptr) /* succeeds if p is null */

Void pointers

It is a pointer that has no associated data type with it. A void pointer can hold addresses of any type and can be typecast to any type. It is also called a generic pointer and does not have any standard data type. It is created by using the keyword void.

#include <stdio.h>
int main()
{
    void *p = NULL; //void pointer assigned to NULL
    printf("The size of pointer is:%ld\n", sizeof(p)); //size of p is
    platform dependant
    return 0;
}

Important Points:

  • void pointers cannot be dereferenced directly. It can however be done using type casting the void pointer. The following example illustrates the rule:
#include<stdlib.h>
#include<stdio.h>
int main()
{
    int x = 4;
    void *ptr = &x;
    
    // (int*)ptr - does type casting from void pointer to
    Integer pointer
    // *((int*)ptr) dereferences the typecasted void pointer
    variable.
    
    printf("Integer variable is = %d", *( (int*) ptr) );
    // Similarly, the same void pointer can hold address of a
    float variable
    float y = 3.6;
    ptr = &y;
    printf("\nFloat variable is= %f", *( (float*) ptr) );
    
    return 0;
}
  • In the above example, it is clear that one needs to typecast the pointer to the desired data type and then use it accordingly. The syntax to do this is as follows:
○ Typecast:
     (target_data_type *) pointer_variable
○ Dereference:
    * ((target_data_type *) pointer_variable)
  • Pointer arithmetic is not possible on pointers of void due to lack of concrete value and thus size.

Wild/Bad/Uninitialized pointers

Wild pointers are also called uninitialized pointers or bad pointers. Because they point to some arbitrary memory location and may cause a program to crash or behave badly.

Bad pointers are very common. In fact, every pointer starts out with a bad value. Correct code overwrites the bad value with a correct reference to a pointee, and thereafter the pointer works fine. There is nothing automatic that gives a pointer a valid initialisation.

The following program illustrates an example of wild pointers. This might produce an unpredictable output.

#include <stdio.h>
int main() {
    int *p; //wild pointer
    printf("\n%d",*p);
    return 0;
}

A very common bad pointer example is as follows:

void BadPointer() {
int* p;
// allocate the pointer, but not the pointee
*p = 42;
// this dereference is a serious runtime error
}

As one can see in the above program, a pointer is declared and is uninitialized but an attempt is made to dereference it which will cause the program to crash.

The bad code will compile fine, but at run-time, each dereferences with a bad pointer will corrupt memory in some way. The program will crash sooner or later. It is up to the programmer to ensure that each pointer is assigned a pointee before it is used.

Dangling pointer

To understand what dangling pointers are, first one needs to understand what dynamic memory allocation is along with their corresponding functions such as malloc( ), calloc( ), realloc( ) and most importantly free( ). These functions are discussed in detail in the “Memory allocation functions” of the “Memory allocation” subsection of the “Arrays” section.

When a pointer pointing to a freed memory location is a dangling pointer. Predominantly there are 3 scenarios that can give rise to dangling pointers.

  1. When allocated memory is freed.
#include <stdlib.h>
#include <stdio.h>
int main()
{
      int *ptr = (int *)malloc(sizeof(int));

      // After below free call, ptr becomes a
      // dangling pointer
      free(ptr);

      // No more a dangling pointer
      ptr = NULL; 
}
  1. Returning address of a memory location of a non-static member from function.
#include<stdio.h>
char *foo()
{
    // x is local non-static variable that goes out
    // of scope once foo returns control to caller
    char ch = 'a';
    return &ch;
}
int main()
{
    char *ptr = foo();
    // p points to something which is not
    // valid anymore
    printf("%c", *ptr);
    return 0;
}
  1. This type is an extension of the 2nd type. When a pointer is pointing to the address of a local variable and we are trying to access the pointer values out of the scope of the local variable.
int main()
{
   float *ptr;
   .....
   .....
{
    float marks = 90.7;
    ptr = &marks;
}
.....
// Here ptr is dangling pointer
}

Dangling pointers are one of the leading causes of segmentation faults which cause programs to crash erratically. These are one of the most non-descriptive errors and sometimes can get very difficult to trace in a large program without any specialized tools such as GNU GDB (GNU Debugger).

Hence, it is always recommended to make the pointer a NULL pointer soon after the deallocation of the memory to avoid dangling pointers. It is also recommended to resolve all the compiler warnings as compilers are smart enough to detect the possibilities of dangling pointers.

Advanced topics in Pointers

Memory layouts in C

A typical memory representation of a C program consists of the following sections. A pictorial representation:

  1. Text segment (i.e. instructions)
    • A text segment, also known as a code segment or simply as text, is one of the sections of a program in an object file or in memory, which contains executable instructions.
    • This begs a question, what is an object file? An object file is an output of an engine called an assembler which is a part of the compiler. This is an intermediary code that is not directly executable but can be relocated to other machine architectures and executed. To know more about this, please read about the phases of the compilation of a C program.
    • As a memory region, a text segment may be placed below the heap or stack in order to prevent heaps and stack overflows from overwriting it.
    • This segment is write-protected so as to prevent accidental modification of code. It is shared to increase memory utilization efficiency as many applications such as shell or debugger may use the same code. It is also helpful that this segment is shared when a process spawns multiple processes or threads.
  1. Initialized data segment
    • This segment is a portion of the virtual address space of a program, which contains the global, static and extern variables that are initialized by the programmer.
#include <stdio.h>
/* global variables stored in Initialized Data Segment in
read-write area*/
    char c[] = "Quant Masters";
    const char s[] = "C Programming";
int main()
{
    static int i=11; /* static variable stored in Initialized Data
    Segment*/
    return 0;
}
  1. Uninitialized data segment (bss)
    • Data in this segment is initialized to arithmetic 0 before the program starts executing. Uninitialized data starts at the end of the data segment and contains all global variables and static variables that are initialized to 0 or do not have explicit initialization in the source code.
#include <stdio.h>
char c; /* Uninitialized variable stored in bss*/
int main()
{
   static int i; /* Uninitialized static variable stored
   in bss */
   return 0;
}
  1. Heap
    • To understand heap and its uses, one must have the basic knowledge of dynamic memory allocation which is discussed in the Arrays chapter.
    • This is a place in the memory segment where dynamic allocation
      happens. All the memory is given to pointers when malloc, calloc and realloc are from the heap. Heaps are especially efficient when arrays are to be passed among multiple functions as just the pointers are getting copied into the function stack (shallow copy). Consider the following example:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 5
void foo(int *array)
{
    for (int i = 0 ; i < SIZE ; i++)
    array[i] = i * 10;
}
int main()
{
    int *array = (int *) malloc (sizeof(int) * SIZE);
    for (int i = 0 ; i < SIZE ; i++)
    array[i] = i;
    foo (array);
    for (int i = 0 ; i < SIZE ; i++)
    printf("%d ", array[i]);
    return 0;
}
  • The output of the above function:
0 10 20 30 40
  • In the above example, the array is pointing to a dynamically allocated
    location at heap as malloc is being used. Then the array elements are
    getting initialized to their corresponding index values. Post the
    initialization, the array is sent to foo as an argument. Here it is important to note that calling foo doesn’t create a deep copy array by allocating a new memory set in heap or stack. Instead, a new pointer in the stack frame (go to the advanced section of the functions chapter to learn more about stack frames) is created and is made to point to the same memory location in the heap. Hence the modification made in the function foo( ) is sustained even after the control comes back to the main ( ) function. The following example prints the address of the memory location. This should make the above point clearer.
#include <stdio.h>
#include <stdlib.h>
#define SIZE 5
void foo(int *array)
{
    printf("In foo function:\n");
    for (int i = 0 ; i < SIZE ; i++)
    printf("Address of array[%d]: %p\n", i, &array[i]);
}
int main()
{
    int *array = (int *) malloc (sizeof(int) * SIZE);
    for (int i = 0 ; i < SIZE ; i++)
    array[i] = i;
    foo (array);
    printf("In main function:\n");
    for (int i = 0 ; i < SIZE ; i++)
    printf("Address of array[%d]: %p\n", i, &array[i]);
    return 0;
}
  • The output of the above program:
In foo function:
Address of array[0]: 0x559b11c572a0
Address of array[1]: 0x559b11c572a4
Address of array[2]: 0x559b11c572a8
Address of array[3]: 0x559b11c572ac
Address of array[4]: 0x559b11c572b0
In main function:
Address of array[0]: 0x559b11c572a0
Address of array[1]: 0x559b11c572a4
Address of array[2]: 0x559b11c572a8
Address of array[3]: 0x559b11c572ac
Address of array[4]: 0x559b11c572b0
  • As the addresses resemble each other in both the functions, it can be
    concluded that the memory location the pointers are pointing to in both foo( ) and main( ) is the same.
  • It is very important to note that the heap grows upwards. The direction of the arrow in the block diagram should make this point obvious.
  1. Stack
    • The stack segment focuses on storing the contents of a function call. It consists of stack frames where each frame corresponds to a function call.
    • When a function is called, a dedicated stack frame is created and pushed on top of the stack segment. The stack frame just below it belongs to the function that called the currently executing function, and so forth. The bottom of the stack essentially should be the stack frame containing the main( ) function call. This frame is popped once the execution of the function is completed.
    • A stack frame stores the following data:
      • Local variables used in the function. Includes the received parameters from the calling function.
      • The return address of the instruction in the caller function that is to be executed after the function call is over.
      • Saved copies of registers modified by subprograms that could need restoration. This is done mainly for optimisation and keeping track of register contents.
    • Consider main( ) function calls a function foo( ) and passes an integer asargument.
1. void foo(int i)
2. {
3.      printf("%d", i);
4. }
5. int main()
6. {
7.      foo(3);
8.      printf("Hello World");
9. }
  • The following is the sequence of execution:
    • The program starts execution from the main function. A stack frame for the main( ) function call is created and pushed onto the stack segment.
    • The first line in the main function calls a function called foo( ). This results in the transfer of the control to foo function at line #1. Along with this, a stack for foo( ) function call is also created. This contains the parameter value ‘i’ and return address where the control has to start execution once foo has completed execution. In this example, it is line #8. This newly created stack frame is pushed onto the stack and the control continues executing the statements in foo( ).
    • Once the value of ‘i’ is printed, the control refers to the stack frame of foo( ), which is at the top of the stack and determines where the control should start executing in the main( ) function. That is from line #8. Hence the control starts executing from line #8 and not from the beginning of the main( ) function.
  • Note that the stack frames are created for each function call. This means that there could be multiple stack frames of the same function in the stack segment in case of recursion. This is the reason why recursion is inefficient when compared to iterative alternatives of the same logic.
  • The stack and heap are traditionally located at opposite ends of the process’s virtual address space.
  • Memory is a finite resource and if too many functions are called before any returns, a program can run out of space on the stack. This usually happens only in the case of recursive functions that are misbehaving but for programs with very tight memory constraints, it may happen in some kinds of programs. This scenario where the process runs out of dedicated memory for the stack is called stack overflow.

Near pointers

A near pointer is a 16-bit pointer to an object contained in the current segment, be it code segment, data segment, stack segment, or extra segment. The compiler can generate code with a near pointer and does not have to concern itself with segment addressing, so using near pointers is the fastest, and generates the smallest code. The limitation is that you can only access 64kb of data at a time because that is the size of a segment – 64kb. A near pointer contains only the 16-bit offset of the object within the currently selected segment.

Far Pointers

A far pointer is a 32-bit pointer to an object anywhere in memory. In order to use it, the compiler must allocate a segment register (segment register is the one that points to the base of the current segment being addressed), load it with the segment portion of the pointer, and then reference memory using the offset portion of the pointer relative to the newly loaded segment register. This takes extra instructions and extra time, so it is the slowest and largest method of accessing memory, but it can access memory that is larger than 64kb, sometimes, such as when dealing with video memory, a needful thing. A far pointer contains a 16-bit segment part and a 16 bit offset part. Still, at any one instant of time, without “touching” segment registers, the program only has access to four 64kb chunks or segments of memory. If there is a 100kb object involved, code will need to be written to consider its segmentation, even with far pointers.

Now, segments overlap. Each segment is 64kb in length, but each one overlaps the next and the prior by 65520 bytes. That means that every address in memory can be addressed by 64kb-1 different combinations of the segment: offset pairs. The result is that the total addressable memory was only 1MB, and the total usable memory address space was 500kb to 600kb. That sounds odd, but Intel built it, Microsoft wrote
it, and DOS/Windows 3.1 grew up around it. I still have that computer, and it still works just fine.

Huge pointers

The far pointer suffers because you can not just add one to it and have it point to the next item in memory – you have to consider segment: offset rules, because of the 16-bit offset issue. The huge pointer is a monolithic pointer to some item with a large chunk of memory, and there is no segment: offset boundaries.

Double Pointers

The concept of pointers can be further extended. Pointer, we know, is a variable that contains the address of another variable. Now this variable itself might be another pointer. Thus, we now have a pointer that contains another pointer’s address. The following example should make this point clear.

#include <stdio.h>
int main( )
{
    int i = 3, *j, **k ;
    j = &i ;
    k = &j ;
    printf ( "\nAddress of i = %u", &i ) ;
    printf ( "\nAddress of i = %u", j ) ;
    printf ( "\nAddress of i = %u", *k ) ;
    printf ( "\nAddress of j = %u", &j ) ;
    printf ( "\nAddress of j = %u", k ) ;
    printf ( "\nAddress of k = %u", &k ) ;
    printf ( "\nValue of j = %u", j ) ;
    printf ( "\nValue of k = %u", k ) ;
    printf ( "\nValue of i = %d", i ) ;
    printf ( "\nValue of i = %d", * ( &i ) ) ;
    printf ( "\nValue of i = %d", *j ) ;
    printf ( "\nValue of i = %d", **k ) ;
    return 0;
}

The output of the above program would be:

Address of i = 65524
Address of i = 65524
Address of i = 65524
Address of j = 65522
Address of j = 65522
Address of k = 65520
Value of j = 65524
Value of k = 65522

Remember that when you run this program the addresses that get printed might turn out to be something different than the ones shown in the figure. However, with these addresses the relationship between i, j and k can be easily established.

Observe how the variables j and k have been declared,

int i, *j, **k ;

Here, i is an ordinary int, j is a pointer to an int (often called an integer pointer), whereas k is a pointer to an integer pointer. We can extend the above program still further by creating a pointer to a pointer to an integer pointer. In principle, you would agree that likewise there could exist a pointer to a pointer to a pointer to a pointer to a pointer.
There is no limit on how far we can go in extending this definition.

Function Pointers

A function pointer is a pointer that holds the address of a function. The ability of pointers to point to functions turns out to be an important and useful feature of C. This provides us with another way of executing functions in an order that may not be known at compile time and without using conditional statements.

Branch prediction is a technique whereby the processor will guess which multiple execution sequences will be executed. Pipelining is a hardware technology commonly used to improve processor performance and is achieved by overlapping instruction execution.

One concern regarding the use of function pointers is their inefficiency. The processor may not be able to use branch prediction in conjunction with pipelining.

Declaring Function Pointers

  • Syntax:
return_type (*fptr_id)([dt1, dt2, ...]);
  • Where,
    • return_type is the return data type of the function whose address the pointer is intending to hold.
    • fptr_id is a valid identifier name for the function pointer.
      ○ dt1, dt2, … are the optional. They are the datatypes of the
      parameter list the function has whose address the pointer is
      intending to hold.
  • When function pointers are used, the programmer must be careful to ensure it is used properly because C does not check to see whether the correct parameters are passed.
  • A simple example of a function pointer which can point to a function which has a void as a parameter and returns a void is as follows:
void (*foo) ();
  • Other valid examples of function pointers are:
int (*f1)(double); // Accepts a double value as parameter and
returns an int
void (*f2)(char*); // Accepts a char pointer as parameter and
returns void
double* (*f3)(int, int); // Accepts two integer values as
parameters and returns a pointer to a double
  • One suggested naming convention for function pointers is to always begin their name with the prefix: fptr. This increases the readability and code maintainability.

Using a Function Pointer

  • Consider the following example:
#include <stdio.h>
int (*fptr1)(int);

int square(int num) {
    return num*num;
}
int main() {
    int n = 5;
    fptr1 = square;
    printf("%d squared is %d\n",n, fptr1(n));
    return 0;
}

Output:

5 squared is 25
  • Points to note from the above example:
    • The return type and the data type and the number of elements in the parameter list match between the function square( ) and the pointer which points to square( ): fptr1. That is, the return type is int and it accepts only one parameter which is of type int.
    • The function pointer fptr1 is declared before it is initialized.
    • Once initialized fptr to point square( ), fptr1 behaves as a replacement to the identifier ‘square’. That is, fptr1(n) and square(n) would yield the same results.
  • Another way of fptr1 initialization in the above example is using the address-of operator (&). It can be done as follows:
fptr1 = &square;
  • The use of the address of operator is of no significance and the compiler effectively ignores it.
  • Another way of using a function pointer is to use typedef. It is illustrated in the following example:
#include <stdio.h>
typedef int (*funPtr)(int);
int square(int num) 
{
    return num*num;
}
int main()
{
    int n = 5;
    funPtr fptr1;
    fptr1 = square;
    printf("%d squared is %d\n",n, fptr1(n));
    return 0;
}
  • The above yields the same results as its immediate previous example.
  • It is quite non-intuitive to use typedef for function pointers as typedef behaves differently for function pointers. Usually, typedef’s name is the declaration’s last element.

Passing Function Pointers

  • Passing function pointers to functions is quite intuitive. The following examples illustrate the same:
#include <stdio.h>
int multiply(int num1, int num2) 
{
   return num1 * num2;
}
int divide(int num1, int num2)
{
    return num1 / num2;
}
typedef int (*fptrOperate)(int, int);
int calculate(fptrOperate operation, int num1, int num2)
{
    return operation(num1, num2);
}
int main()
{
    int op1 = 10, op2 = 5;
    fptrOperate fptr1;
    fptr1 = multiply;
printf("%d * %d = %d\n", op1, op2, calculate(fptr1, op1,
op2));
    fptr1 = divide;
    printf("%d / %d = %d\n", op1, op2, calculate(fptr1, op1,
op2));
    return 0;
}
  • The output of the above program is:
10 * 5 = 50
10 / 5 = 2
  • Points to note from the above example:
    • A function pointer can simply be declared and passed to functions just like any other variable.
    • A function pointer can point to different functions at different points of execution times. In this example, initially fptr1 points to multiple( ) functions and later it points to divide( ).
    • A function pointer type definition should be declared before the function prototype or definition which accepts the function type definition. In this example, the type definition fptrOperate is declared before calculate( ) which uses fptrOperate in its parameter list.

Returning Function Pointers

  • Returning a function pointer requires declaring the function’s return type as a function pointer. Consider the following example:
#include <stdio.h>
int multiply(int num1, int num2)
{
    return num1 * num2;
}

int divide(int num1, int num2) 
{
    return num1 / num2;
}
typedef int (*fptrOperate)(int, int);
fptrOperate select(char opcode) 
{
    switch(opcode) {
        case '*': return multiply;
        case '/': return divide;
    }
}
int evaluate(char opcode, int num1, int num2)
{
    fptrOperate operation = select(opcode);
    return operation(num1, num2);
}
int main()
{
    int op1 = 10, op2 = 5;
    printf("%d * %d = %d\n", op1, op2, evaluate('*', op1, op2));
    printf("%d / %d = %d\n", op1, op2, evaluate('/', op1, op2));
    return 0;
}

Output:

10 * 5 = 50
10 / 5 = 2
  • Structure of the above program:
  1. Type definition of function which returns an integer value and accepts 2 integer values are made to fptrOperate.
  2. A function select( ) reads the operator and returns the function pointer of the corresponding function.
  3. Function evaluate( ) which:
    • Reads operator and operands and sends the operator to select( ).
    • Stores the function pointer returned by select( ) in ‘operation’.
    • Calls function pointed by ‘operation’ and passes the operands which it received as parameters (num1, num2).
    • Returns the value which is returned by the call from ‘operation’.
  4. Functions multiply( ) and divide( ) accept two integers and return an
    integer after performing multiplication and division operations on the
    parameter list. Hence, the function pointer return operation is similar to any other value return from a function.

Using an Array of Function Pointers

Arrays of function pointers can be used to select the function to evaluate on the basis of some criteria. Declaring such an array is straightforward.

typedef int (*funPointer)(int, int);
funPointer fptr_array[64] = {NULL};

Alternatively, one can use the syntax used in the following snippet:

int (*fptr_array[64])(int, int) = {NULL};

Both of the above code snippets declare a function pointer array named fptr_array with the capacity to hold 64 function pointers, each one of which returns an integer and accepts 2 parameters, both of which are of type integer. The entire array is initialized to NULL.

The example given in ‘Returning Function Pointers’ can be rewritten using an array of function pointers as follows: (Note that this example includes addition, subtraction and modulus operation as well)

#include <stdio.h>
int add (int num1, int num2) {
    return num1 + num2;
}

int subtract (int num1, int num2) {
    return num1 - num2;
}

int multiply (int num1, int num2) {
    return num1 * num2;
}

int divide (int num1, int num2) {
    return num1 / num2;
}

int mod (int num1, int num2) {
    return num1 % num2;
}

typedef int (*fptrOperate) (int , int);

fptrOperate fptr_array[5] = {NULL};

void initFptrArray () {
    fptr_array[0] = add;
    fptr_array[1] = subtract;
    fptr_array[2] = multiply;
    fptr_array[3] = divide;
    fptr_array[4] = mod;
}

fptrOperate select (char opcode) {
    switch (opcode) {            
        case '+': return fptr_array[0];
        case '-': return fptr_array[1];
        case '*': return fptr_array[2];
        case '/': return fptr_array[3];
        case '%': return fptr_array[4];
        default : return NULL;
    }
}

int evaluate (char opcode, int num1, int num2) {
    fptrOperate operation = select (opcode);  
    return operation (num1, num2);
}

int main () {
    initFptrArray ();
    int op1 = 10, op2 = 5;
    printf ("%d + %d = %d\n", op1, op2, evaluate ('+', op1, op2));
    printf ("%d - %d = %d\n", op1, op2, evaluate ('-', op1, op2));
    printf ("%d * %d = %d\n", op1, op2, evaluate ('*', op1, op2));
    printf ("%d / %d = %d\n", op1, op2, evaluate ('/', op1, op2));
    printf ("%d %% %d = %d\n", op1, op2, evaluate ('%', op1, op2));
    return 0;
}
  • The above program produces the following output:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
10 % 5 = 0
  • One may notice that the indexing scheme for the fptr_array is not so obvious and we need a select( ) function to identify and map the right index for the right operator. This can be eliminated using the operator characters as indices. For example, fptr_array[‘*’] should hold the function pointer to multiply( ).
  • This can be achieved in the following example which produces the same output as that of the previous example
#include <stdio.h>
int add(int num1, int num2) {
    return num1 + num2;
}
int subtract(int num1, int num2) {
    return num1 - num2;
}
int multiply(int num1, int num2) {
    return num1 * num2;
}
int divide(int num1, int num2) {
    return num1 / num2;
}
int mod(int num1, int num2) {
    return num1 % num2;
}
typedef int (*fptrOperate)(int, int);
fptrOperate fptr_array[47] = {NULL};
void initFptrArray() {
    fptr_array['+'] = add;
    fptr_array['-'] = subtract;
    fptr_array['*'] = multiply;
    fptr_array['/'] = divide;
    fptr_array['%'] = mod;
}
int evaluate(char opcode, int num1, int num2) {
    fptrOperate operation = fptr_array[(int)opcode];
    return operation(num1, num2);
}

int main() {
    initFptrArray();
    int op1 = 10, op2 = 5;
    printf("%d + %d = %d\n", op1, op2, evaluate('+', op1, op2));
    printf("%d - %d = %d\n", op1, op2, evaluate('-', op1, op2));
    printf("%d * %d = %d\n", op1, op2, evaluate('*', op1, op2));
    printf("%d / %d = %d\n", op1, op2, evaluate('/', op1, op2));
    printf("%d %% %d = %d\n", op1, op2, evaluate('%', op1, op2));
    return 0;
}
  • This begs the question, why is the size of fptr_array 47! As we are trying to subscript fptr_array using characters: ‘+’, ‘-’, ‘*’, ‘/’ and ‘%’, the highest ASCII value attained by this set of characters is 47 which is of ‘/’. Hence the size of the array is 47.
  • This begs one more question, is it not inefficient to declare an array of size 47 and just use 5 locations out of them? The C compiler doesn’t allocate memory right away just because it is declared. It allocates once the process attempts to use it. Hence the memory efficiency isn’t affected.

Comparing Function Pointers

Function pointers can be compared to one another using the equality and inequality operators. The following example illustrates the same:

#include <stdio.h>
int multiply(int num1, int num2) {
     return num1 * num2;
}
int divide(int num1, int num2) {
     return num1 / num2;
}
typedef int (*fptrOperate)(int, int);

int main() {
    fptrOperate fptr1 = multiply;
    if (fptr1 == multiply)
        printf("Pointing to multiply( )");
    if (fptr1 != multiply)
        printf("Not pointing to multiply( )");
    return 0;
}

The output of the above program is as follows:

Pointing to multiply( )

Casting Function Pointers

A pointer to one function can be cast to another type. This should be done with care since the runtime system does not verify that the parameters used by a function pointer are correct. It is also possible to cast a function pointer to a different function pointer and then back. The resulting pointer will be equal to the original pointer. The size of function pointers used is not necessarily the same. The following sequence illustrates this operation:

#include <stdio.h>
int multiply(int num1, int num2) {
        return num1 * num2;
}
int main() {
        typedef int (*fptr_1)(int);
        typedef int (*fptr_2)(int,int);
        fptr_2 fptrFirst = multiply;
        fptr_1 fptrSecond = (fptr_1)fptrFirst;
        fptrFirst = (fptr_2)fptrSecond;
        printf("%d", fptrFirst(5,6));
}

This sequence, when executed, will display 30 as its output.

The use of void* is not guaranteed to work with function pointers. That is, we should not assign a function pointer to void* as shown below:

void* pv = add;

However, when interchanging function pointers, it is common to see a “base” function pointer type as declared below. This declares fptrBase as a function pointer to a function, which is passed void and returns void:

typedef void (*fptrBase)();

The following sequence demonstrates the use of this base pointer, which duplicates the previous example:

fptrBase basePointer;
fptrFirst = multiply;
basePointer = (fptr_1)fptrFirst;
fptrFirst = (fptr_2)basePointer;
printf("%d",fptrFirst(5,6));

A base pointer is used as a placeholder to exchange function pointer values.

A word of warning: Function pointer casting is one of the most error-prone and vulnerable areas. It is always advised to use function pointers whose declaration matches the function signature it is pointing to

Categories
Computer Science / Information Technology Language: C

Strings

Introduction

The way a group of integers can be stored in an integer array, similarly, a group of characters can be stored in a character array. A string constant is a one-dimensional array of characters terminated by a
null ( ‘\0’ ).

Each character in the array occupies one byte of memory. NULL may look like two characters, but it is actually only one character, with the \ indicating that what follows it is something special. Note that ‘\0’ and ‘0’ are not the same. The ASCII value of ‘\0’ is 0, whereas
the ASCII value of ‘0’ is 48.

Generally, string characters are selected only from the printable character set. Nothing in C prevents any character other than the NULL from being used in a string. In fact, it is quite common to use formatting characters, such as tabs, in strings. Before jumping into this section, please go through the ‘Arrays’ section if you haven’t yet.

String literals

A string literal, also known as a string constant, is a sequence of characters enclosed in double-quotes. For example, each of the following is string literal:

"Hello world"
"Quants"
"Welcome to C language!"

When string literals are used in a program, C automatically creates an array of characters, initializes it to a null-delimited string, and stores it, remembering its address. It does all this because we use the double quotes that immediately identify the data as a string value.

Declaration and initialisation of a string

Declaring and initializing a string is similar to any other array. Two of the ways of doing it are as follows:

Syntax:

char string_id[size] = {'Q', 'u', 'a', 'n', 't', 's', '\0'};
char string_id[size] = {'Q', 'u', 'a', 'n', 't', 's'};
char string_id[size] = "Quants";
char string_id[size] = "Quants\0";

Where,

  1. string_id is any valid identifier for the string.
  2. size is a non-zero positive integer denoting the size of the string.

Note:

  1. The strings can also be initialized at the time of declaration using a string literal.
  2. size value is optional in this case as the initialisation is happening at the time of declaration. This should always be 1 plus the length of string planned to be stored in the array. This is to store the NULL delimiter at the end.
  3. The NULL character (‘\0’) at the end of the 1st and 4th example is optional as the modern compilers append NULL implicitly. But it is advised to put it explicitly.
  4. If the size value is smaller than the length of the string being initialized, the compiler will throw a warning saying “excess elements in array initializer” and initializes only the ‘size‘ number of characters into the array.
  5. It is important to remember that strings cannot be initialized with string literals in the following way:
char string[10];
string = "Quants"; // Error
  1. Note that you cannot copy the contents of one string to another using the equals operator. To do that there are string functions supported by seeing which we shall see at a later point.
char string[10] = "Quants";
char str[10];
str = string; // Error

Significance of NULL delimiter

The string in C is not a data type but a data structure. This means that its implementation is logical and not physical. The physical structure is the array in which the string is stored. Since the string is a variable-length structure, we need to identify the logical end of the data within the physical structure.

The terminating null (‘\0’) is important also because it is the only way the functions that work with a string can know where the string ends. In fact, a string not terminated by a ‘\0’ is not really a string, but merely a collection of characters.

Strings and characters

A character can be stored in 2 ways: as a character literal or as a string literal. To store in character literal, use single inverted commas. The character occupies a single memory location and a string containing a single character requires two bytes of memory location: 1 for the character and the other for the null delimiter. Hence, ‘a’ is a character, “a” is a string and “” is an empty string.

A character can be copied from one location (or variable) to another using an assignment operator but for strings, we would need library functions which we shall see later in this section.
Furthermore, a character is nothing but an unsigned short integer which is mapped to the corresponding value in the ASCII table. The following ASCII table describes the mapping:

Another important difference between string and character is how we represent the absence of data. A character cannot be empty. It can hold a null character or a space but simply cannot be empty. Hence ‘’ doesn’t make sense in C. On the other hand, strings can be empty. A string that contains no data consists of only a delimiter.

Referencing string literals

As it is clear by now that the strings are nothing but a character array with null as the last character, it should be obvious to note that the individual characters in the string can also be accessed just the way we do in arrays. The following are the valid ways to refer to characters in
the string:

string[0];     // First character
*(string + 1); // Second character
*(&string[2]); // Third character

Notice that the 3rd way is just referencing and dereferencing the address. The above three methods can be illustrated in the following example:

#include <stdio.h>
int main( ) 
{
     char string[10] = "ABC";
     printf("%c", string[0]);     // First character
     printf("%c", *(string + 1)); // Second character
     printf("%c", *(&string[2])); // Third character
     return 0;
}

Iterating through characters of a string

The following are the ways to iterate through the string using for loop:

char string[10] = "ABC";
for (int i = 0 ; string[i] != '\0' ; i++)
printf("%c", string[i]);

Using while loop:

char string[10] = "ABC";
int i = 0 ;
while ( string[i] != '\0') {
    printf("%c", string[i]);
    i++;
}

String Input/Output Functions

scanf( )

  • scanf( ) is a function that reads data from stdin (i.e, the standard input stream, which is usually the keyboard, unless redirected) and then writes the results into the arguments given.
  • The entered data is formatted as integers or floating-point numbers etc
  • Syntax:
int scanf(const char *format, ...);
  • The ‘format’ is a string in itself and contains conversion code. In this case for string, the conversion code is s. The simplest of examples of this is as follows:
char str[10];
scanf("%s", str);
  • scanf( ) ignores all the leading whitespaces in the input and reads only till it finds whitespace. All the characters from the first non-whitespace character till the last non-whitespace character are put into the memory location pointed by the pointer passed as an argument.
  • For example, if ” Hello World ” is entered, the leading whitespace is ignored and only “Hello” is stored in str and a null character is also stored at the end of the str. The rest of the input string is left in the input stream.
  • To remove the rest of the input from the input stream, one can use fflush( ) or define a macro. Simply calling fflush with stdin as a parameter should flush out all the residual input characters from the input stream. This is illustrated as follows:
char str[10];
scanf("%s", str);
fflush(stdin);
  • A macro to flush the input stream is illustrated as follows:
#define FLUSH while (getchar() != '\n') {
char str[10];
scanf("%s", str);
FLUSH;
}
  • We can protect against the user entering too much data by using width in the field specification. Width specifies the maximum number of characters to be read. The modified scanf statement is shown below:
scanf("%9s", str);
  • Any number of characters entered beyond the first 9 characters in the above case will be stored in buffer but not stored to str. Hence it is a recommended practice to flush after using scanf().

The scan set conversion code ([…])

  • The scan set conversion specification consists of the open bracket ([), followed by the edit characters, and terminated by the closing
    bracket (]).
  • The characters in the scan set identify the valid characters, known as the scan set, that are to be allowed in the string. All characters except the close bracket can be included in the set.
  • Edited conversion reads the input stream as a string. Each character read by scanf() is compared against the scan set. If the character just read is in the scan set, it is placed in
    the string and the scan continues.
  • The read will stop when the following conditions are met:
    • The first character does not match the scan set.
    • If the first character read is not in the scan set, the scanf() terminates and a null string is returned.
    • If an end-of-file is detected.
    • If a field width specification is included and the maximum number of characters has been read.
  • Scan set doesn’t skip the leading whitespace. Leading whitespace is either put into the string being read when the scan set contains the corresponding whitespace character or stops the conversion when it is not.
  • The non-matching character remains in the input stream for the next read operation. Hence it is advised to flush after using the scan set conversion code.
  • Example: A scanf() statement to read a string containing only digits, commas, periods, the minus sign, and a dollar sign and the maximum number of characters in the resulting string is 10. The format string for this operation would be:
scanf("%10[0123456789.,-$]", str);
  • Sometimes it is easier to specify what is not to be included in the scan set rather than what is valid. For instance, suppose that we want to read a whole line. We can do this by stating that all characters except the newline (\n) are valid. To specify invalid characters, we start the scan set with the caret ( symbol. The caret is the negation symbol and in
    effect says that the following characters are not allowed in the string. To read a line, we would code the scanf as shown below.
scanf("%[^\n]", line);

printf()

  • C has four options of interest when we write strings using these print functions: the left-justify flag, width, precision, and size. The left-justify flag (-) and the width are almost always used together.
  • Justification flag
    • The justification flag (-) is used to left justify the output. It has meaning only when the width is also specified, and then only if the length of the string is less than the format width. Using the justification flag results in the output being left-justified, as shown below. If no flag is used, the justification is right.
printf("|%-30s|\n", "This is the string");

○ Output:

|This is the string |
  • Minimum width
    • The width sets the minimum size of the string in the output. If it is used without a flag, the string is printed right-justified as shown below.
printf("|%30s|\n", "This is the string");

○ Output

|            This is the string|
  • Precision
    • C also uses the precision option to set the maximum number of characters that will be written. In the following example, we set the maximum characters to one less than the width to ensure space between the string and the next column.
printf("|%-15.14s|", "12345678901234567890");

○ Output:

|12345678901234 |

Arrays of strings

Ragged/Jagged array

Before moving into Arrays of strings, one needs to understand what is a Jagged/Ragged array. A Jagged array is a 2-dimensional array where each row might have a non-identical number of columns than the other arrays.

Arrays of strings as Ragged array

It is obvious that an array of strings will mostly be a ragged array as it’s extremely unlikely that the strings in the array will have the same length. This can be illustrated in the following figure:

There are many ways to declare and initialize an array of strings. Some of them are as follows:

Consider the following program:

#https://www.onlinegdb.com/#editor_1include <stdio.h>
int main() 
{
    char* days_of_week[7];
    days_of_week[0] = "Monday";
    days_of_week[1] = "Tuesday";
    days_of_week[2] = "Wednesday";
    days_of_week[3] = "Thursday";
    days_of_week[4] = "Friday";
    days_of_week[5] = "Saturday";
    days_of_week[6] = "Sunday";
    for(int i = 0 ; i < 7 ; i++)
    printf("%s\n", days_of_week[i]);
    return 0;
}

The output of the above program:

Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday

Points to note from the above program:

  • The string array days_of_week[] is an array of character pointers.
  • The array indices are getting initialized with the static strings which get stored in the text segment. These are not editable strings. When tried to edit, the process will throw a segmentation fault.


Now the previous example program saw how to create a string array where the strings are read-only as they are stored in the text segment. Please note that the array is not read-only.

At any given point, an index of the array can point to a different string but the string it is pointing to will be read-only.

At times, we need to store a string in the array which we can edit. The following program shows how to do the same:

#include <stdio.h>
#include <string.h>
int main()
{
    char days_of_week[7][10];
    strcpy(days_of_week[0], "Monday");
    strcpy(days_of_week[1], "Tuesday");
    strcpy(days_of_week[2], "Wednesday");
    strcpy(days_of_week[3], "Thursday");
    strcpy(days_of_week[4], "Friday");
    strcpy(days_of_week[5], "Saturday");
    strcpy(days_of_week[6], "Sunday");
    for(int i = 0 ; i < 7 ; i++)
    printf("%s\n", days_of_week[i]);
    return 0;
}

The output of the above program is:

Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday

Points to note from the above program:

  • Memory allocation for all the characters to be initialised is done during declaration.
  • The strings are copied (using strcpy( ) library function which we shall see in the next section) to the memory locations of the array, unlike the previous example where only the pointers to the strings were stored in a single dimensional array

String Manipulation Functions

  • Strings in C is not a primitive data type hence one cannot use operators such as ‘+’(presumably to concatenate two strings) or ‘=’ (presumable to copy one string to a memory location). To do all of these tasks, there is a standard string library defined in C.
  • The following are some of the string manipulation functions defined in string.h:

String length

  • The string length function returns the length of a string passed as an argument to it. The length of a string is defined as the number of characters in the string till the first occurrence of the null character in the string, excluding the null character.
  • The prototype of the string length function is as follows:
    • size_t strlen(const char *s);
  • strlen( ) is the function name and the return type is size_t which mostly is unsigned int.
  • The following program demonstrates strlen() without using the library function:
#include <stdio.h>
size_t strlen(char *str)
    {
        size_t len = 0;
        while (*str != '\0')
        {
          str++;
          len++;
        }
     return len;
    }
int main()
{
    char str[100];
    scanf("%s", str);
    printf("%ld is the length of the string.", strlen(str));
    return 0;
}

The output of the above program is as follows:

Quantmasters
12 is the length of the string.

The following program illustrates library function strlen()

#include <stdio.h>
#include <string.h>
int main()
{
    char str[100];
    scanf("%s", str);
    printf("%ld is the length of the string.", strlen(str));
    return 0;
}

Output:

Quantmasters
12 is the length of the string.

String Copy

  • Often we would need to copy the contents of one string variable to another. As C would not allow using of assignment operator on strings to achieve deep copy, string copy functions strcpy() and strncpy() come in handy.
  • The function signature of strcpy() is as follows:
char *strcpy(char *dest, const char *src);
  • The strcpy() function copies the string pointed to by src, including the terminating null byte (‘\0’), to the string pointed to by dest.
  • The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.
  • If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.
  • The strcpy() and strncpy() functions return a pointer to the destination string dest.
  • The following program illustrates the use of library function strcpy():
#include <stdio.h>
#include <string.h>
int main()
{
    char src[100], dest[100];
    scanf("%s", src);
    printf("%s is the dest string.", strcpy(dest, src));
    return 0;
}

Output:

HelloHi
HelloHi is the dest string.

The following program illustrates what a simple implementation of strcpy() might be:

#include <stdio.h>
char *
strcpy(char *dest, const char *src)
{
    size_t i;
    for (i = 0; src[i] != '\0'; i++)
    dest[i] = src[i];
    return dest;
}
int main()
{
    char src[100], dest[10] = {'\0'};
    scanf("%s", src);
    printf("%s is the dest string.", strcpy(dest, src));
    return 0;
}

Output

HelloHi
HelloHi is the dest string.

The following program illustrates what a simple implementation of strncpy() might be:

#include <stdio.h>
char *
strncpy(char *dest, const char *src, size_t n)
{
    size_t i;
    for (i = 0; i < n && src[i] != '\0'; i++)
    dest[i] = src[i];
    for ( ; i < n; i++)
    dest[i] = '\0';
    return dest;
}
int main()
{
    char src[100], dest[10] = {'\0'};
    scanf("%s", src);
    printf("%s is the dest string.", strncpy(dest, src, 9));
    return 0;
}

The output of the above programs is as follows

HelloHi
HelloHi is the dest string.

Things to note while using string copy functions in C:

Overlapping strings

  • It is important to note that the src and dest strings should not overlap. That is, src should not point to any character of dest and dest should not point to any character of src. A classic example of string overlap is as follows:
  • One can notice that dest and src are pointing to the same string but at two different indices. This is an overlap and the string copy functions should not be applied to such types of functions.

Size of dest string

  • The dest string should be large enough to accommodate the src string

Buffer overflow

  • One should use strcpy() only when it is 100% guaranteed that the src string is shorter or has an equal length to that of the capacity of dest string. If not, buffer-overflow attacks can be launched and important data on the stack can be written to point to a malicious code.
  • To avoid this, one can use strncpy() and mention the capacity of dest as the argument ‘n’.

What to use when?

  • Some programmers consider strncpy() to be inefficient and error-prone. If the programmer knows (i.e., includes code to test!) that the size of dest is greater than the length of src, then strcpy() can be used.
  • One valid (and intended) use of strncpy() is to copy a C string to a fixed-length buffer while ensuring both that the buffer is not overflowed and those unused bytes in the target buffer are zeroed out (perhaps to prevent information leaks if the buffer is to be written to media or transmitted to another process via an interprocess communication technique).
  • If there is no terminating null byte in the first n bytes of src, strncpy() produces an unterminated string in dest.
  • If buf has length buflen, you can force termination using something like the following:
strncpy(buf, str, buflen - 1);
if (buflen > 0)
buf[buflen - 1]= '\0';
  • Of course, the above technique ignores the fact that, if src contains more than buflen – 1 bytes, information is lost in the copying to dest.

strlcpy()

  • Some systems (the BSDs, Solaris, and others) provide the following function:
size_t strlcpy(char *dest, const char *src, size_t size);
  • This function is similar to strncpy(), but it copies at most size-1 bytes to dest, always adds a terminating null byte, and does not pad the target with (further) null bytes.
  • This function fixes some of the problems of strcpy() and strncpy(), but the caller must still handle the possibility of data loss if the size is too small.
  • The return value of the function is the length of src, which allows truncation to be easily detected: if the return value is greater than or equal to size, truncation occurred.
  • If loss of data matters, the caller must either check the arguments before the call or test the function return value.
  • strlcpy() is not present in Glibc and is not standardized by POSIX, but is available on Linux via the libbsd library.

String compare

C provides two functions to compare two strings: strcmp() and strncmp(). These are included with the header file string.h

strcmp()

  • Function signature:
int strcmp(const char *s1, const char *s2);
  • The strcmp() function performs case sensitive comparison of the two strings s1 and s2. It returns an integer less than, equal to, or greater than zero if s1 is found, respectively, to be less than, to match, or be greater than s2.
  • The return value of this function depends on the comparison operation and the following table illustrates the 3 possibilities:
Return valueCase
0if the s1 and s2 are equal
Negative integers1 is less than s2
Positive non zero integers1 is greater than s2

The following program illustrates strcmp():

#include <stdio.h>
#include <string.h>
int main()
{
    char str1[100], str2[100];
    printf("Enter first string: ");
    scanf("%s", str1);
    printf("Enter second string: ");
    scanf("%s", str2);
    int i = strcmp(str1, str2);
    if (i == 0)
         printf("%s == %s\n", str1, str2);
    else if (i < 1)
         printf("%s < %s\n", str1, str2);
    else
         printf("%s > %s\n", str1, str2);
    return 0;
}

Output: Run #1

Enter first string: hello
Enter second string: Hello
hello > Hello

Output: Run #2

Enter first string: Hello
Enter second string: Hello
Hello == Hello

Output: Run #3

Enter first string: Hello
Enter second string: hello
Hello < hello

strncmp()

  • The strncmp() function is similar, except it compares only the first (at most) n bytes of s1 and s2. The signature is as follows:
int strncmp(const char *s1, const char *s2, size_t n);
  • The program below is from the official man page of Linux. This demonstrates the operation of strcmp() (when given two arguments) and strncmp() (when given three arguments). First, some examples using strcmp():
/* string_comp.c
Licensed under GNU General Public License v2 or later.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int
main(int argc, char *argv[])
{
    int res;
    if (argc < 3) 
{
        fprintf(stderr, "Usage: %s <str1> <str2> [<len>]\n", argv[0]);
        exit(EXIT_FAILURE);
}

if (argc == 3)
res = strcmp(argv[1], argv[2]);
else
    res = strncmp(argv[1], argv[2], atoi(argv[3]));
    if (res == 0) {
    printf("<str1> and <str2> are equal");
    if (argc > 3)
    printf(" in the first %d bytes\n", atoi(argv[3]));
    printf("\n");
    } else if (res < 0) {
    printf("<str1> is less than <str2> (%d)\n", res);
    } else {
    printf("<str1> is greater than <str2> (%d)\n", res);
}
exit(EXIT_SUCCESS);

Output

$ ./string_comp ABC ABC
<str1> and <str2> are equal
$ ./string_comp ABC AB # 'C' is ASCII 67; 'C' - '\0' = 67
<str1> is greater than <str2> (67)
$ ./string_comp ABA ABZ # 'A' is ASCII 65; 'Z' is ASCII 90
<str1> is less than <str2> (-25)
$ ./string_comp ABJ ABC
<str1> is greater than <str2> (7)

And then some examples using strncmp():

$ ./string_comp ABC AB 3
<str1> is greater than <str2> (67)
$ ./string_comp ABC AB 2
<str1> and <str2> are equal in the first 2 bytes

String concatenate: strcat()

The strcat() function appends the src string to the dest string, overwriting the terminating null byte (‘\0’) at the end of dest, and then adds a terminating null byte hence the resulting string in dest is always null-terminated.

The strings may not overlap, and the dest string must have enough space for the result. The strcat() function returns a pointer to the resulting string dest. If dest is not large enough, program behaviour is unpredictable. The function signature is as follows:

int strcat(const char *s1, const char *s2);

Demonstration of strcat():

#include <stdio.h>
#include <string.h>
int main() 
{
    char *src = "World";
    char dest[20];
    strcpy(dest, "Hello ");
    printf("%s", strcat(dest, src)); // strcat() returns dest
    return 0;
}

Output:

Hello World

String concatenate: strncat()

The strncat() function is similar, except that
● it will use at most n bytes from src
● src does not need to be null-terminated if it contains n or more bytes.
The function signature is as follows:

char *strncat(char *restrict dest, const char *restrict src, size_t n);

If src contains n or more bytes, strncat() writes n+1 bytes to dest (n from src plus the terminating null byte). Therefore, the size of dest must be at least strlen(dest)+n+1. A simple implementation of strncat() might be:

char *
strncat(char *dest, const char *src, size_t n) 
{
    size_t dest_len = strlen(dest);
    size_t i;
    for (i = 0 ; i < n && src[i] != '\0' ; i++)
    dest[dest_len + i] = src[i];
    dest[dest_len + i] = '\0';
    return dest;
}

Other important string manipulation functions

Exercises

  1. Write a function that accepts a string (a pointer to a character) and deletes the last character by moving the null character 1 position to the left.
  2. Write a function that accepts a string (a pointer to a character) and deletes all the trailing spaces at the end of the string. Make sure that the resultant string is terminated with the null character.
  3. Write a function that accepts a string (a pointer to a character) and deletes all the leading spaces.
  4. Write a function that returns the number of times the character is found in a string. The function has two parameters: the first parameter is a pointer to a string and the second parameter is the character to be continued.
  5. Write a function that inserts a string into another string at a specified position. It returns a positive number if it is successful or zero if it has any problems, such as an insertion location greater than the length of the receiving string. The first parameter is the receiving string. The second parameter is the string to be inserted. And the third parameter is the index of the insertion position in the first string.
  6. Write a program that extracts part of the given string from the specified position. For example, if the string is “Working with strings is fun”, then if from position 4, four characters are to be extracted, then the program should print the string as “king”. If the number of characters to be extracted is 0, then the program should print the entire string
    from the specified position.
  7. Write a program that converts a string like “123” to an integer 123.
  8. Write a program that generates and prints the Fibonacci words of order 0 through 5.
    • f(0) = “a”
    • f(1) = “b”
    • f(2) = “ba”
    • f(3) = “bab”
    • f(4) = “babba”
  9. To uniquely identify a book a 10 digit ISBN (international standard book number) is used. The rightmost digit is a checksum digit. This digit is determined from the other 9 digits using the condition that d1 + 2d2 + 3d3 + … + 10d10 must be a multiple of 11 (where di
    22 denotes the ith digit from the right). The checksum digit d1 can be any value from 0 to 10 the ISBN convention is to use the value x to denote 10. Write a program that receives a 10 digit integer, computes the checksum, and reports whether the ISBN number is correct or not.
  10. A credit card number is usually a 16 digit number. A valid credit card number could satisfy a rule explained below with the help of a dummy credit card number- 4567 1234 5678 9129. Start with the rightmost – 1 digit and multiply every other digit by 2.

Then subtract 9 from any number larger than 10. Thus we get:
8 3 2 6 1 5 9 4
Add them all up to get 38.
Add all the other digits to get 42.
The sum of 38 and 42 is 80. Since 80 is divisible by 10, the credit card number is valid.
Write a program that receives a credit card number and checks using the above rule whether the credit card number is valid

Categories
Computer Science / Information Technology Language: C

Arrays

Introduction

  • An array in C is defined as the collection of similar types of data items stored at contiguous memory locations.
  • Arrays are the derived data type in C programming language which can store the primitive type of data such as int, char, double, float, etc. It also can store the collection of derived data types, such as pointers, structure, etc.
  • The array is the simplest data structure where each data element can be randomly accessed by using its index number.
  • Arrays make it a lot easier to deal with a large number of similar types of values such as marks of a student or age of students etc.
  • An array is also known as a subscripted variable.

Basic operations on an Array

Declaration

Syntax:

data_type array_identifier[size];

Where,

  • data_type is any primitive, non-primitive or user-defined data type.
  • array_identifier is any valid C identifier.
  • size is a positive integer value denoting the size of the array. example:
int seats[10]; // Declares an array named seats which can hold 10
integers.
float age[50]; // Declares an array named age which can hold 50
floating-point numbers.
char division[5]; // Declares an array named division which can hold
5 characters.

Initialisation

  • Indexing of an array in C starts from 0. That is, to access or modify the first element of an array, one has to use the following notation:
array_identifier[0] = value;
  • Note that the value should be of the same data type as that of the array.
  • Generalizing the above syntax:
array_identifier[index] = value;
  • Where, the index can attain any value from 0 to size – 1. And this notation of accessing values using an index enclosed in square brackets is called subscript notation.
  • Eg:
#include <stdio.h>
int main()
{
    int integers[5];
    integers[0] = 0;
    integers[1] = 1;
    integers[2] = 2;
    integers[3] = 3;
    integers[4] = 4;
    for(int i = 0 ; i < 5 ; i ++)
    printf("%d ", integers[i]);
    return 0;
}

output:

0 1 2 3 4
  • It can be noted from the example that array elements can be accessed through a loop as well. Similarly, one can initialize the array using a loop. It can be illustrated in the following example:
#include <stdio.h>
int main()
{
    int integers[5];
    for(int i =0 ; i < 5 ; i ++)
    integers[i] = i;
    for(int i =0 ; i < 5 ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • In this particular case, we initialize the array elements to their corresponding indices. That is, integers[0] is 0, integers[1] is 1 and so forth. This produces the same output as the previous example.
  • It is always a best practice to use a preprocessor directive to define the size of the array. This helps a lot when we want to change the size of the array. It is illustrated in the following example:
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[SIZE];
    for(int i =0 ; i < SIZE ; i ++)
    integers[i] = i;
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • This produces the same output as previous examples but is much more
    maintainable. If one has to change the size of the array, a modification to the preprocessor directive takes care of the rest. This reduces the errors and makes debugging easier.
  • C also provides a way to declare and initialize an array in a single statement. The following is the syntax to achieve the same:
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[SIZE]={0, 1, 2, 3, 4};
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • This also produces the same output as the previous examples in the section. An advantage of using this method is that the programmer need not mention SIZE while declaring. The compiler automatically calculates the size, allocates memory and initializes the given values. This can be illustrated using the following
    example:
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[]={0, 1, 2, 3, 4};
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • This produces the same output as previous examples. That is:
0, 1, 2, 3, 4
  • Till the array elements are not given any specific values, they are supposed to contain garbage values as the array is assumed to be an auto variable by default (Storage classes).
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[SIZE];
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • The above program produces a random garbage output such as this:
0 0 -498802560 21903 161117280
  • Also, note that trying to access an index other than the range 0 to size – 1 of the array would lead to unpredictable results. There is a good chance that the program will crash with a segmentation fault. The same is illustrated in the following example:
#include <stdio.h>
int main()
{
    int array[] = {1, 2, 3, 4, 5};
    printf("%d", array[6]);
    return 0;
}
  • In the above example, anything other than the values from 0 to 4 would result in an unpredictable output. It is the sole responsibility of the programmer to take care that the written program doesn’t go out of the bounds of the array. C language doesn’t assist in bounds checking.

Properties of an Array

  • Each element of an array is of the same data type and carries the same size, i.e., int = 4 bytes.
  • Elements of the array are stored at contiguous memory locations where the first element is stored at the smallest memory location. Consider the following example:
array[] = {1, 2, 3, 4, 5};
  • The memory representation of the above array would be as follows assuming that the array is starting at 65508 and running on a 16-bit compiler:
  • Elements of the array can be randomly accessed since we can calculate the address of each element of the array with the given base address and the size of the data element.

Advantages of an Array

  1. Code optimisation: Less code to access the data.
  2. Ease of traversing: By using the for loop, we can retrieve the elements of an array easily.
  3. Ease of sorting: To sort the elements of the array, we need a few lines of code only.
  4. Random Access: We can access any element randomly using the array.

Passing Array Elements to a Function

  • An array is a collection of contiguous and independent elements each with a dedicated address, it should be possible to pass elements of an array individually to a function either by value or reference.
  • This is done by passing values or references to functions using subscript notation. The following example illustrates this:
#include <stdio.h>
void print_by_value(int value) 
{
    printf("%d ", value);
}

void print_by_reference(int* value) 
{
    printf("%d ", *value);
}
int main() 
{
    int array[] = {1, 2, 3, 4, 5};

    printf ("Print by value:\t\t");
    for (int i = 0 ; i < 5 ; i++)
    print_by_value (array[i]);
    
    printf ("\nPrint by reference:\t");
    for (int i = 0 ; i < 5 ; i++)
    print_by_reference (&array[i]);
    
    return 0;
}
  • The output of the above example is:
Print by value: 1 2 3 4 5
Print by reference: 1 2 3 4 5
  • Points to note from the above example:
    • The print_by_value( ) accepts an integer value and prints the same value. A local copy of the value being passed to the function is made in the function. Any changes made to the variable inside this function will not result in a change in the array in the main( ) function.
    • The print_by_reference( ) accepts the address of an integer value and stores it in a local pointer. Any changes made to the value being pointed by this pointer will result in a value change in the array of the main( ) function.

Pointers and Arrays

Before jumping into this section, please make sure you understand pointer arithmetic. Pointer arithmetic is discussed in detail in the ‘Pointers’ section. Till now we were accessing array elements using subscript notation. The elements can be accessed using pointers too. Consider the following example:

#include <stdio.h>
int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int* ptr = &array[0];
    if (ptr == array)
    printf("%p\n%p", ptr, array);
    return 0;
}

Output:

0x7ffc0bd9e810
0x7ffc0bd9e810

A very important conclusion can be drawn from the above example. The array variable is also a pointer to the first element of the array. To reinforce this fact, consider the following example:

#include <stdio.h>
int main()
{
     int array[] = {1, 2, 3, 4, 5};
     int* ptr = &array[0];
     printf("%d", ptr[2]);
     return 0;
}

The output of the above program is:

3

So now, it is a fact that any pointer to the first element of the array can be a replacement for the array variable used earlier. With this fact as the premise, consider the following example:

#include <stdio.h>
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    for (int i = 0 ; i < 5 ; i++)
    printf("%d ", *(array+i));

    return 0;
}

The output of the above program is:

1 2 3 4 5

The subscript notation is converted to pointer notation internally. i.e.,
array[i] = *(array + i)
So it is clear from the above example that we can access array elements through pointer notation. And it is also clear that a pointer when incremented always points to an immediately next location of its type.

Another way of traversing all the elements of an array using pointer notation is illustrated in the following example:

#include <stdio.h>
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    const int* traverse_ptr = &array[0];
    for (int i = 0 ; i < 5 ; i++, traverse_ptr++)
    printf("%d ", *traverse_ptr);
    return 0;
}

Discretion between a pointer and an array

After going through the above examples, one may jump to a conclusion that there is no difference between an array and a pointer but there is.

  • An array has memory allocated to it and a pointer may or may not.
  • &array returns the address of the first element and not the address of the array variable which stores the address of the first element. Whereas &ptr gives the address of the ptr variable which stores the address of the first element of the array memory location to which it is pointing. If &array is returning any other address than the first element of the array, we call it “Array decay”
  • Another symptom of array decay is that the sizeof(array) returns a value other than the size of the memory allocated to the array.
  • These differences can be noticed in the following example program:
#include <stdio.h>
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    int* ptr = &array[0];
    printf("Address of ptr: %p\n", &ptr);
    printf("Address of array: %p\n", &array);
    printf("sizeof of ptr: %u\n", sizeof(ptr));
    printf("sizeof of ptr: %u\n", sizeof(array));
    return 0;
}

The output of the above program is as follows:

Address of ptr: 0061FF08
Address of array: 0061FF0C
sizeof of ptr: 4
sizeof of ptr: 20

Best practices

  1. Making array elements write protected while assigning a pointer for traversing using ‘const int*’ is essential to prevent accidental data modification while traversing.
  2. Accessing array elements by pointers is always faster than accessing them by subscripts.
  3. Array elements should be accessed using pointers if the elements are to be accessed in a fixed order, say from beginning to end, or from end to beginning, or every alternate element or any such definite logic.
  4. Instead, it would be easier to access the elements using a subscript if there is no fixed logic in accessing the elements.

Passing an Entire Array to a Function

At times there arise situations such as array sorting, where we need to pass entire arrays to functions instead of individual elements.
The following table contains the statements showing 2 ways to pass an array as arguments and 2 ways to receive an array as a parameter in the called function.

Consider that the function being called is named ‘foo’. Note that the C language does not have explicit library functions to determine the size of the array being passed. So one needs to make size available to all functions by declaring it at global space or pass the size as another parameter as shown in the table:

Calling statementCalled function prototype
foo(array, size);
or
foo(&array[0], size);
foo(int *array, int size)
or
foo(int array[], int size)

Notice that an array variable is nothing but a pointer to the first element of the array. In both of the calling ways described in the above table, implicitly, just the address of the first element of the array is being passed. Once an array is passed to the function, the array can be traversed in both subscript notation and pointer notation.

One can choose pairs of any combination from the above table to pass the array. The following example illustrates all 4 possibilities from the above table.

#include <stdio.h>
void print_array_1 (int array[], int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", array[i]);
}
void print_array_2 (int* array, int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", *(array+i));
}
void print_array_3 (int array[], int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", array[i]);
}
void print_array_4 (int* array, int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", array[i]);
}
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    print_array_1 (array, 5);
    printf("\n");
    print_array_2 (array, 5);
    printf("\n");
    print_array_3 (&array[0], 5);
    printf("\n");
    print_array_4 (&array[0], 5);
    return 0;
}

Output:

1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5

Notice that in the print_array_2( ) function, the array is being traversed using pointer notation and there is no change in the output.

It is very important to make a note that any changes we make to the array in the called functions will result in changes in the caller function too as we are sending the memory location to the function and not making a copy of it. The arrays we saw in this section are technically called ‘static arrays’ as we would know the size of the array at the compile time. These arrays are stored in the program stack.

Declaration

The syntax to declare a 2 dimensional array is:
data_type array_id[row_size][column_size];
Where,

  1. data_type is any valid primary or secondary data type supported by C.
  2. array_id is any valid identifier for the array.
  3. row_size is the length of the row needed.
  4. col_size is the length of the column
    Example: int array[2][3];
    The above array can be visualized as follows:

Points to note:

  • Indexing of row and column starts from 0 and ends at length-1. In the above example, the row index is in the range [0, 1] and the column index is in the range [0, 2].
  • Indexing of each cell in the two-dimensional array starts with the row index followed by the column index. So to address the 0th row 1st column, the index is [0][1].

Initialization

In order to initialize a value to a memory location in the array, the following is the
Syntax:

array_id [i][j] = value;

Where

  1. array_id is array declared.
  2. i is the row index which is in the range between 0 and row_size – 1
    inclusive.
  3. j is the column index which is in the range between 0 and column_size- 1 inclusive.
  4. value is the value to be initialized at the ith row and jth column of the
    array and should strictly belong to the same data type under which the
    array is declared.

Here is a sample program that stores roll numbers and marks obtained by a student side by side in a matrix.

#include <stdio.h>
int main( ) 
{
    int students[2][2] ;
    students[0][0] = 1;
    students[0][1] = 96;
    students[1][0] = 2;
    students[1][1] = 93;

    printf("Roll number: %d\tMarks: %d\n", students[0][0],
    students[0][1]);
    printf("Roll number: %d\tMarks: %d\n", students[1][0],
    students[1][1]);
    return 0;
}

The output of the above program is:

Roll number: 1 Marks: 96
Roll number: 2 Marks: 93

To understand the memory locations of the array in the above program, let us print the memory locations of the array:

#include <stdio.h>
int main( )
{
     int students[2][2] ;
     students[0][0] = 1;
     students[0][1] = 96;
     students[1][0] = 2;
     students[1][1] = 93;

     printf("%p\t%p\n", &students[0][0], &students[0][1]);
     printf("%p\t%p\n", &students[1][0], &students[1][1]);
     return 0;
}

The output (highlighted last 2 characters for readability):

0x7ffc2873dac0 0x7ffc2873dac4
0x7ffc2873dac8 0x7ffc2873dacc

Note that the size of int is 4 bytes as the program is executed on a 32-bit compiler. By analyzing the above output, it can be concluded that two-dimensional arrays are nothing but single dimensional arrays of length = row size * column size but indexed as rows and columns.

In the previous example, we saw that the arrays are initialized individually which can be a very tedious job and we might need to read the user input and store it in an array. This can be achieved by coupling scanf and loops. The following example illustrates the same:

#include <stdio.h>
int main( )
{
    int students[2][2] ;
    for (int i = 0 ; i < 2 ; i++)
    {
        printf("Enter the roll number: ");
        scanf("%d", &students[i][0]);
        printf("Enter the marks: ");
        scanf("%d", &students[i][1]);
    }
    for (int i = 0 ; i < 2 ; i++)
        printf("Roll number: %d\tMarks: %d\n", students[i][0], students[i][1]);
    return 0;
}

The output of the above program is:

Enter the roll number: 1
Enter the marks: 93
Enter the roll number: 2
Enter the marks: 96
Roll number: 1 Marks: 93
Roll number: 2 Marks: 96

The above example employs a for loop to iterate over rows as there are only 2 columns. When there are numerous columns, one can use nested loops as done in the following example:

#include <stdio.h>
#define ROWS 4
#define COLS 6
int main( )
{
        int array[ROWS][COLS] ;
        printf("Enter %d values: ", ROWS * COLS);
        for (int i = 0 ; i < ROWS ; i++)
        {
            for (int j = 0 ; j < COLS ; j++)
            {
            scanf("%d", &array[i][j]);
            }
            
        }
        printf("\nEntered values:\n");
        for (int i = 0 ; i < ROWS ; i++) {
        for (int j = 0 ; j < COLS ; j++) {
        printf("%d ", array[i][j]);
        }
        printf("\n");
        }
        return 0;
}

The output of the above program is:

Enter 24 values: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

Entered values:
1 2 3 4 5 6
7 8 9 10 11 12
13 14 15 16 17 18
19 20 21 22 23 24

Declaration and initialisation

Declaration and initialization of an array can be done in a single statement. The syntax is as follows:

data_type array_id[][] = {
{value_00, value_01, ..., value_0n},
{value_10, value_11, ..., value_1n},
18
…
{value_m0, value_m1, ..., value_mn},
};

Where,

  1. data_type is the data type of the value that has to be stored in the array.
  2. array_id is the identifier of the array being declared and initialized.
  3. value_ij are values of the array, where 0 <= i < desired row size of the array and 0 <= j < desired column size of the array

Another way to declare and initialize is as follows:

data_type array_id[row_size][col_size] = {
    value_00, value_01, ..., value_0n,
    value_10, value_11, ..., value_1n,
    …
    value_m0, value_m1, ..., value_mn,
};

One may notice that just the nested brackets are removed. This method mandates the programmer to mention at least column size if not both row size and column size. This method often leads the compiler to give a warning saying that the brackets are missing. Hence the first method is always prefered.

The following example illustrates declaration and initialisation:

#include <stdio.h>
#define ROWS 3
#define COLS 2
int main( ) {
    int array[ROWS][COLS] = {{1, 2}, {3, 4}, {5, 6}};
    for (int i = 0 ; i < ROWS ; i++) {
        for (int j = 0 ; j < COLS ; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
    return 0;
}

Output:

1 2
3 4
5 6

Please note that the rows and column size mentioned in the declaration are not mandatory as the compiler will determine the size of the rows and columns automatically by the initialisation part.

Jagged arrays

Jagged arrays are two-dimensional arrays where the column length is not constant throughout the rows. The following is a pictorial representation of a jagged array:

         0       1         2        3
    +--------+--------+
0   | [0][0] | [0][1] |
    +--------+--------+--------+
1   | [1][0] | [1][1] | [1][2] |
    +--------+--------+--------+
2   | [2][0] |
    +--------+--------+--------+--------+
3   | [3][0] | [3][1] | [3][2] | [3][3] |
    +--------+--------+--------+--------+

One might intuitively try to create a jagged array of the above sort by the method just discussed in the following way:

int array[][] = {{1, 2}, {3}, {5, 6}};

Please note that this will produce unpredictable results as this is NOT the proper way of creating jagged arrays in C. Jagged arrays can be created using dynamic arrays which we shall see in the latter part of this section.

Pointers and two-dimensional arrays

As mentioned earlier, two-dimensional arrays are nothing but a contiguous collection of single-dimensional arrays. The C language embodies this unusual but powerful capability that it can treat parts of arrays like arrays. More specifically, each row of a two-dimensional array can be thought of as a one-dimensional array. This is a very important fact if we wish to access array elements of a two-dimensional array using pointers.
Thus, the declaration,

int array_id[5][2] ;

can be thought of as setting up an array of 5 elements, each of which is a 1D (one-dimensional) array containing 2 integers. We refer to an element of a 1D array using a single subscript.

Similarly, if we can imagine array_id to be a one-dimensional array then we can refer to its zeroth element as array_id[0], the next element as array_id[1] and so on. More specifically, array_id[0] gives the address of the zeroth one-dimensional array, array_id[1] gives the address of the first one-dimensional array and so on. This fact can be demonstrated by the following program.

#include <stdio.h>
#define ROWS 4
#define COLS 2
int main( )
{
    int array_id[ROWS][COLS] = {
                                { 1234, 56 },
                                { 5678, 33 },
                                { 9012, 80 },
                                { 3456, 78 }
                            } ;
    for (int i = 0 ; i < ROWS ; i++ )
        printf ( "\nAddress of %d th 1-D array = %p", i, array_id[i]) ;

    return 0;
}

The output of the above program when run on a 32-bit compiler:

Address of 0 th 1-D array = 0x7fffe85bda40
Address of 1 th 1-D array = 0x7fffe85bda48
Address of 2 th 1-D array = 0x7fffe85bda50
Address of 3 th 1-D array = 0x7fffe85bda58

Points to note from the above example:

  • Compiler figures out the number of rows (4) and columns (2) from the declaration and initialisation line of code.
  • The compiler allocates the necessary amount of memory (8 bytes for 2 integers in the first row) linearly for the first row (from 40 to 44 for the first element, 44 to 48 for the second element in the above example).
  • Once allocated memory for the first row, the compiler allocates memory for the second row contiguously next to the first row’s memory location (from 48 to 4c for the first element, 4c to 50 for the second element in the above example)
  • The above pattern is followed for the 3rd and 4th row as well.

Pointer access to elements in a 2D array

Similar to the one-dimensional array, there are 2 ways to access the elements of a 2D array: subscript notation and pointer notation. We have been using the subscript access method so far and in this section, we shall peek into the pointer access method.
Syntax:

*(*(array_id + row_index) + column_index)

The evolution of the above syntax from subscript notation is as follows:

array_id[row_index][column_index]
* ( array_id[row_index] + column_index )
* ( * ( array_id + row_index ) + column_index)

Notice that the pointer access method is just a multi-level dereferencing of array element addresses.

Consider the following example illustrating array element access using the pointer method:

#include <stdio.h>
#define ROWS 4
#define COLS 2
int main( )
{
int array[ROWS][COLS] = {
                            { 1234, 56 },
                            { 1212, 33 },
                            { 1434, 80 },
                            { 1312, 78 }
} ;

for (int i = 0 ; i < ROWS ; i++ )
{
    for (int j = 0 ; j < COLS ; j++)
    printf ("\nAddress: %p Value: %d\t", (*(array + i) + j),
    *(*(array + i) + j) );
    printf("\n");
}
return 0;
}

Output when run on 32-bit machine:

Address: 0x7ffc2a646d30 Value: 1234
Address: 0x7ffc2a646d34 Value: 56

Address: 0x7ffc2a646d38 Value: 1212
Address: 0x7ffc2a646d3c Value: 33

Address: 0x7ffc2a646d40 Value: 1434
Address: 0x7ffc2a646d44 Value: 80

Address: 0x7ffc2a646d48 Value: 1312
Address: 0x7ffc2a646d4c Value: 78

Pointer to a 2D array

In this section, we shall explore how we can have a pointer to a 2-dimensional array. Consider the following example:

#include <stdio.h>
#define ROWS 4
#define COLS 2
int main( )
{
int array[ROWS][COLS] = {
                            { 1234, 56 },
                            { 1212, 33 },
                            { 1434, 80 },
                            { 1312, 78 }
                      } ;

int ( *p )[2] ;
int i, j, *pint ;
for ( i = 0 ; i <= 3 ; i++ )
{
    p = &array[i] ;
    pint = p ;
    for ( j = 0 ; j <= 1 ; j++ )
    printf ( "%d ", *( pint + j ) ) ;
    printf ( "\n" ) ;
}
return 0;
}

The output:

1234 56
1212 33
1434 80
1312 78

Here p is a pointer to a single-dimensional array of two integers. Note that the parentheses in the declaration of ‘p’ are necessary. The absence of them would make ‘p’ an array of 2 integer pointers. In the outer for loop each time we store the address of a new one-dimensional array. Thus the first time through this loop p would contain the address of the zeroth 1-D array. This address is then assigned to an integer pointer ‘pint’. Lastly, in the inner ‘for’ loop using the pointer ‘pint’, we have printed the individual elements of the 1-D array to which p is pointing.

The entity pointer to an array is immensely useful when we need to pass a 2-D array to a function. This is discussed in the next section.

Passing 2-D Array to a Function

There are three ways in which we can pass a 2-D array to a function. These are illustrated in the following program.

#include <stdio.h>
#define ROWS 4
#define COLS 2
void display ( int *array)
{
    int i, j ;
    for ( i = 0 ; i < ROWS ; i++ )
    {
        for ( j = 0 ; j < COLS ; j++ )
            printf ( "%d ", * ( array + i * COLS + j ) ) ;
        printf ( "\n" ) ;
    }
        printf ( "\n" ) ;
}
void show ( int ( *array )[COLS] )
{
    int i, j ;
    int *p ;
    for ( i = 0 ; i < ROWS ; i++ )
        {
        p = array + i ;
            for ( j = 0 ; j < COLS ; j++ )
            printf ( "%d ", * ( p + j ) ) ;
            printf ( "\n" ) ;
        }
        printf ( "\n" ) ;
}
void print ( int array[ ][COLS] ) 
{
    for ( int i = 0 ; i < ROWS ; i++ )
    {
        for ( int j = 0 ; j < COLS ; j++ )
        printf ( "%d ", array[i][j] ) ;
        printf ( "\n" ) ;
    }
        printf ( "\n" ) ;
}
int main( )
{
    int array[ROWS][COLS] = {
                            { 1234, 56 }
                            { 1212, 33 },
                            { 1434, 80 },
                            { 1312, 78 }
    } ;
    display(array);
    show(array);
    print(array);
    return 0;
}

The output of the above program is as follows:

1234 56
1212 33
1434 80
1312 78

1234 56
1212 33
1434 80
1312 78

1234 56
1212 33
1434 80
1312 78

display( )

In the display( ) function we have collected the base address of the 2-D array being passed to it in an ordinary int pointer. Then through the two for loops using the expression

* ( array + i * col + j )

we have reached the appropriate element in the array. Suppose i is equal to 2 and j is equal to 3, then we wish to reach the element array[2][3]. A more general formula for accessing each array element would be:

* ( base address + row no. * no. of columns + column no. )

show( )

In the show( ) function we have defined an array to be a pointer to an array of 4 integers through the declaration:

int ( *array )[COLS] ; // Where COLS is 4

To begin with, the array holds the base address of the zeroth 1-D array. This address is then assigned to ‘p’ which is an int pointer, and then using this pointer all elements of the zeroth 1-D array are accessed. Next time through the loop when ‘i’ takes a value of 1, the expression array + i fetches the address of the first 1-D array. This is because the array is a pointer to the zeroth 1-D array and adding 1 to it would give us
the address of the next 1-D array. This address is once again assigned to ‘p’ and using it all elements of the next 1-D array are accessed.

print( )

In the third function print( ), the declaration of the array looks like this:

int array[ ][COLS] ; // Where COLS is 4

This is the same as

int ( *array )[COLS],

where the array is a pointer to an array of 4 integers. The only advantage is that we can now use the more familiar expression array[i][j] to access array elements. We could have used the same expression in the show( ) function as well.

Consider the following example:

#include <stdio.h>
int main( ) 
{
        int *array[4] ; // array of integer pointers
        int p = 101, q = 102, r = 103, s = 104;
        array[0] = &p ;
        array[1] = &q ;
        array[2] = &r ;
        array[3] = &s ;
        
        printf ( "Address of p : %p\n", &p ) ;
        printf ( "Address of q : %p\n", &q ) ;
        printf ( "Address of r : %p\n", &r ) ;
        printf ( "Address of s : %p\n\n", &s ) ;

    for ( int i = 0 ; i <= 3 ; i++ ) 
    {
        printf ( "array[%d] : %p\n", i, array[i] ) ;
        printf ( "Value : %d\n\n", *array[i] );
    }
return 0;
}

Output:

Address of p : 0x7fff0c6dd45c
Address of q : 0x7fff0c6dd460
Address of r : 0x7fff0c6dd464
Address of s : 0x7fff0c6dd468

array[0] : 0x7fff0c6dd45c
Value : 101

array[1] : 0x7fff0c6dd460
Value : 102

array[2] : 0x7fff0c6dd464
Value : 103

array[3] : 0x7fff0c6dd468
Value : 104

As one can observe, arr contains addresses of isolated int variables i, j, k and l. The for loop in the program picks up the addresses present in the array and prints the values present at these addresses.

An array of pointers can even contain the addresses of other arrays. Consider the following example:

#include <stdio.h>
int main( )
{
    int a[ ] = { 0, 1, 2, 3, 4 } ;
    int *p[ ] = { a, a + 1, a + 2, a + 3, a + 4 } ;
    printf ( "%p %p %d", p, *p, * ( *p ) ) ;
    return 0;
}

Output:

0x7ffc5cf1dd10 0x7ffc5cf1dcf0 0

Remember that a subarray of another array is also an array.

Memory allocation

C gives two choices when a programmer wants to reserve memory for arrays. But before diving in, it is advised to go through “Memory layouts in C” under the “Advanced topics in Pointers” of the “Pointers” section.

  1. Static memory allocation
    1. Static memory allocation requires that the declaration and definition of memory be fully specified in the source program. The number of bytes reserved cannot be changed during runtime. This is the technique we have been using to allocate memory for variables, arrays, and pointers.
  2. Dynamic Memory allocation
    1. Dynamic memory allocation uses predefined functions to allocate and release memory for data while the program is running. It effectively postpones the data definition, but not the data declaration, to run time. It is important to note that we can refer to memory allocated in the heap only through a pointer.
      To use dynamic memory allocation, we use either standard data types or derived types that we have previously declared. Unlike static memory allocation, dynamic memory allocation has no identifier associated with it; it has only an address that must be used to access it.

Memory allocation functions.

There are 4 memory management functions:

  1. malloc ( )
  2. calloc ( )
  3. realloc ( )
  4. free ( )

The first three are used for memory allocation and the fourth is used to return memory when it is no longer needed. All 4 functions are found in the standard library file (stdlib.h).

Block memory allocation (malloc)

The malloc function allocates a block of memory that contains the number of bytes specified in its parameter. It returns a void pointer to the first byte of the allocated memory. The allocated memory is not initialized. The malloc ( ) function declaration is
shown below:

void* malloc (size_t size);

The type size_t is defined in several header files including stdio.h. This type is usually an unsigned integer, and by the standard, it is guaranteed to be large enough to hold the maximum address of the computer.
To provide portability, the size specification in malloc’s actual parameter is generally computed using sizeof operator. For example, if we want to allocate an integer in the heap we code the call as shown below:

pInt = malloc (sizeof(int));

If the memory allocation fails, instead of returning a pointer to the first byte of allocated memory, malloc ( ) returns a NULL pointer. An attempt to allocate memory from the heap when memory is insufficient is known as overflow. It is always the best practice for the program to check for memory overflow.

There is one subtlety to note here. As mentioned earlier, the malloc returns a void pointer but often we need a pointer of a specific type. C99 and above versions handle this situation with an implicit type casting but it is always recommended to explicitly typecast the pointer. It is to be done as follows:

pointer = (type *) malloc (size);

For example:

pInt = (int *) malloc (sizeof(int));

The malloc ( ) function has one more potential error. If we call malloc ( ) with a zero size, the results are unpredictable. It may return a NULL pointer, or it may return some other implementation-dependent value. Never call malloc with a zero size.

The following example shows the best way to use malloc ( ) to allocate a block of memory for an integer:

if (!(pInt = (int *) malloc (sizeof(int)))) {
    // No memory is available.
    exit(100);
}
// Memory available

Contiguous memory allocation (calloc)

The second memory allocation function, calloc, is primarily used to allocate memory for arrays. It differs from malloc only in that it sets memory to NULL characters. The calloc function declaration is shown below:

void *calloc (size_t element-count, size_t element_size);

The result is the same for both malloc and calloc when overflow occurs and when a zero size is given. A sample calloc call is illustrated in the following example. Here we attempt to allocate memory for an array of 200 integers:

if (!(pInt = (int *) calloc (200, sizeof(int)))) {
   // No memory is available.
   exit(100);
}
// Memory available

Reallocation of memory (realloc)

The realloc ( ) function can be highly inefficient and therefore should be used advisedly. When given a pointer to a previously allocated block of memory, realloc ( ) changes the size of the block by deleting or extending the memory at the end of the block. If the memory cannot be extended because of other allocations, realloc allocates a completely new block, copies the existing memory allocation to the new allocation, and deletes the old allocation.
The programmer must ensure that any other pointers to the data are correctly changed.

The realloc function declaration is shown below:

void *realloc (void* ptr, size_t newSize);

Releasing memory (free)

Garbage collection, that is, releasing the memory allocated by the above 3 functions once the need is satisfied should be done explicitly by the programmer. This can be done using the free( ) function.
It is an error to free memory:

  1. With a null pointer.
  2. A pointer to a location other than the first element of an allocated block of memory.
  3. A pointer that is different from the pointer that allocated the memory.
  4. Free the same memory location more than once.
    It is also a potential error to refer to memory after it is freed (dangling pointer). The function declaration statement for free is shown below:
void free(void* ptr);

Exercises

  1. Eleven integer values are entered from the keyboard. The first 10 are to be stored in an array. Search for the 11th integer in the array. Write a program to display the number of times it appears in the array.
  2. Read the size of the array and allocate an array dynamically using calloc. Read the array elements and also read a key value. Display the number of times the key has appeared in the array. Do not forget to free the allocated memory before exiting the program.
  3. Implement the Selection Sort, Bubble Sort and Insertion sort algorithms on a set of 10 numbers.
  4. Read the size of the array and allocate an array dynamically using calloc. Read the array elements and sort the array using selection sort, bubble sort and insertion sort. Do not forget to free the allocated memory before exiting the program.
  5. Implement the Sieve of Eratosthenes and print the first 100 prime numbers. The algorithm is as follows:
    1. Fill array values[100] with numbers from 1 to 100.
    2. Starting with the second entry in the array, set all its multiples to zero.
    3. Proceed to the next non-zero element and set all its multiples to zero.
    4. Repeat step 3 till you have set up the multiples of all the non-zero elements to zero.
    5. After step 4, all the non-zero entries left in the array would be prime numbers, so print out these numbers.F. Write a program to copy the contents of one array into another in reverse order.
  6. Write a program to copy the contents of one array into another in reverse order.
  7. Write a program to check if an array is symmetric. That is, in an array array_id check if array_id[0] = array_id[n-1], array_id[1] = array_id[n-2] and so on, where n is the size of the array. Statically initialize the array or take user input.
  8. Find the smallest number, the largest number, and an average of values in a floating numbers array using pointers.
  9. Write a program that performs the following tasks:
    1. initialize an integer array of 10 elements in main( )
    2. pass the entire array to a function modify( )
    3. in modify( ) multiply each element of the array by 4
    4. return the control to main( ) and print the new array of elements in main( )
  10. Write a function to find the norm of a matrix. The norm is defined as the square root of the sum of squares of all elements in the matrix.
  11. Write a program to read 10 coordinates. Each coordinate contains 2
  12. floating-point values X and Y. Print the two farthest points and 2 nearest points in the array.
Categories
Computer Science / Information Technology Language: C

Loops

Oftentimes we find ourselves writing programs to do repetitive tasks. After all, the use of a computer is that it can do the same task a million times a second with 0 margins of error. Loops play a very important role in achieving this objective. There are three types of loops C provides:

  1. for
  2. while
  3. do-while

These three types can be differentiated as entry controlled (for and while) and exit control (do-while) but more on this later.

For Loop

The for loop is the most popular way of using a loop instruction simply because of its ease. The syntax of the for loop is as follows:

for (initialisation ; condition ; updation)
{
      Statements to execute…
}

The following example illustrates a way to use for loop:

#include <stdio.h>
int main() 
{
    int i;
    for (i = 1 ; i <= 10 ; i++)
    printf("%d ", i);
    return 0;
}

The above program prints the first 10 natural numbers. The following steps elucidate how the for statement gets executed:

  • When the for statement is executed for the first time, the value of counter ‘i’ is set to an initial value 1. – Now the condition is checked. The condition set is i <= 10 which happens to be true for the iteration.
  • Since the condition is satisfied, the body of the loop, i.e., printf() in this case, is executed.
  • Once the body of the loop is executed, the update part of the for statement is executed where i gets incremented by 1 and then the condition is checked.
  • These iterations go on until the condition fails (or the control encounters a ‘break’ statement which we shall see in a short while).

Points to note

  • The scope of the variable ‘i’ is the main() function.
  • For loop is entry controlled loop: the condition is checked at the beginning of the loop and not at the end
  • The minimum number of times the body of the loop gets executed is zero. It is the case where the condition fails in the very first iteration.
  • The for loop allows us to specify three things about a loop in a single line, hence it is convenient to use:
    • Loop counter declaration/initialisation.
    • Loop termination condition.
    • Incrementation of loop counter at the end of each iteration of the loop.
  • The following flow chart should summarize the discussion:
for loop flow diagram

There is one more clean way to do it which is as follows:

#include <stdio.h>
int main()
{
    for (int i = 1 ; i <= 10 ; i++)
    printf("%d ", i);
    return 0;
}

Here, though the program produces the same output as earlier, the scope of the variable ‘i’ is just the for loop.
Similar to conditional statements such as if, else-if and if-else-if ladder, the default scope of any loop is the line immediately next to it. In the above example, it’s the printf() statement. If there are multiple sets of statements to be executed inside of the loops, one can use brackets
enclosure which is demonstrated in the following example:

#include <stdio.h>
int main() {
    for (int i = 1 ; i <= 10 ; i++)
    {
        printf("%d", i);
        printf("\n");
    }
    return 0;
}

It is important to note that the initialization, testing and incrementation part of a for loop can be replaced by any valid expression. For example:

int i = 0;
for (printf("Hello") ; i == 0 ; i++);

The above snippet will print “Hello”.

Multiple initialisations in for loop

The initialisation expression of the for loop can contain more than one statement separated by a comma. For example,

for ( i = 1, j = 2 ; j <= 10 ; j++ )

Multiple statements can also be used in the incrementation expression of for loop; i.e., you can increment (or decrement) two or more variables at the same time. However, only one expression is allowed in the test expression. This expression may contain several conditions linked together using logical operators.


The use of multiple statements in the initialisation expression also demonstrates why semicolons are used to separate the three expressions in the for loop. If commas had been used, they could not also have been used to separate multiple statements in the initialisation expression, without confusing the compiler.

While loops

Syntax:

Initialisation;
while (condition)
{
     statement1;
     statement2;
…
     statementn;
     updation;
}

}
Where,

  1. The initialisation is the declaration-assignment of a loop counter variable.
  2. Condition is the counter itself or a boolean expression involving the counter.
  3. Updation is an operation to alter the value of the loop counter variable.
  4. Statements inside the block of while constituting the ‘body’ of the while loop.

Points to note:

  • The statements in the body of the loop keep on getting executed till the condition remains true. When the condition is false, the control comes out of the loop and executes the first statement that follows the body of the while loop.
  • The condition can be replaced with any other valid expression which evaluates to a boolean true or false.
  • The default scope of the while loop is the single line following the loop statement. To override it, one must enclose the statements in a pair of parentheses as shown in the syntax.
  • Note that there is no semicolon at the end of the while loop. If there is a semi-colon, there is a high chance of the loop going into an infinite execution state as the updation of the loop counter is not happening.
  • If the condition of the while loop is a comparison, make sure that the elements being compared are having the same range bounds and the size of the elements is also the same. For example,
int i = 0;
while (i <= 32768) 
{
    printf("%d ", i);
    i++;
}
  • One might think that the above snippet would print integers from 0 to 32768 but the range of an int in a 32-bit compiler is till 32767 after which it becomes -32767 after increment, which certainly satisfies the condition. Hence this goes into an infinite loop.
  • It is not recommended to modify a variable inside of the while condition but one can do it without any syntactical error as shown in the following example:
int i = 0;
while (++i <= 10)
printf("%d ", i);
  • In this example, the value i pre increments in the condition of while. This prints integers from 1 to 10 inclusive. This is not recommended as it would be hard to read. One might easily assume that it prints from 0 to 10 inclusive by noticing that i is initialized to 0.
  • One more thing to notice in the previous example is that just by changing pre-increment to post-increment, the output would change from printing 1 to 10, to printing 1 to 11 inclusive.

The following flow chart should help in understanding the operation of while loop:

The following example prints the first 10 natural numbers using a while loop:

int n = 1;
while(n <= 10)
{
     printf("%d ", n);
     n++;
}

While loop is extensively used where there are a set of operations to be repeated a fixed number of times. For example, calculate the gross salaries of 10 different employees or convert cm to mm 10 times.

Do-while loop

The loops discussed till now are ‘entry controlled loops’ which means the condition is checked at the entry before executing the body of the loop for the first time. Ideally, a do-while loop is used when one does not know the number of iterations needed and the minimum number of times the body of the loop is to be executed is 1. The syntax is as follows:

do {
   statement(s);
} 
while( condition );

Notice that for a do-while loop, the pair of parenthesis is necessary. This is an ‘exit controlled loop’ as the condition is checked at the end of the loop. As discussed earlier, the do-while loop executes the statements in the body at least once while other loops the minimum number of
iterations are 0. This is illustrated clearly in the following 2 snippets with the same condition and body of the loop:

a. Using a do-while loop:

do {
    printf ( "Hello world") ;
} while ( 4 < 1 ) ;

Output: Hello world

b. Using while loop

while ( 4 < 1 )
    printf ( "Hello world") ;

The above loop produces no output.


The following example reads a number and prints the entered number’s square until the user wants:

do {
printf ( "Enter a number " ) ;
scanf  ( "%d", &num ) ;
printf ( "square of %d is %d", num, num * num ) ;
printf ( "\nWant to enter another number y/n " ) ;
scanf  ( " %c", &another ) ;
} while ( another == 'y' ) ;

The same output can be achieved using while and for. But the code wouldn’t be as ‘elegant’ as do-while.

The following is the flow chart of the do-while loop:

Nested loops

Nested loops are common in programming. Having one loop inside the block of another loop comes in handy for so many algorithmic applications such as sorting, reading matrix elements etc. It is perfectly common to place one loop inside the other to any level of nesting
and with various combinations of all 3 types of loops.

Consider one needs to print the following matrix:

00 01 02 03
10 11 12 13
20 21 22 23
30 31 32 33

It would be a terrible mess and a bad idea to do it without nested loops. The following snippet achieves the above output:

for (int i = 0 ; i < 4 ; i++)
{
    for (int j = 0 ; j < 4 ; j++)
        printf("%d%d ", i, j);
    printf("\n");
}

Notice that the scope of the loops is inherited, that is, by default, it’s the immediate line after the loop and it can be overridden with a pair of parenthesis.

The break statement

The break statement can be used with the loop to terminate the execution and continue with the execution of statements next to the loop. In other words, the break statement is used to ‘jump out of the loop’.

When a break is encountered inside any loop, control automatically passes to the first statement after the loop. A break is usually associated with an if conditional. Consider the following example which tells if the entered number is a prime number or not.

#include <stdio.h>
#include <math.h>
#include <stdbool.h>
int main()
{
    int n;
    bool isPrime = true;
    scanf("%d", &n);
    for (int i = 2 ; i <= sqrt(n) ; i++)
    {
        if (n % i == 0)
        {
            isPrime = false;
            break;
        }
    }
    printf("%s", isPrime ? "Prime" : "Not Prime");
    return 0;
}

In this program, the moment n % i evaluates to zero, (i.e. n is exactly divisible by i) the message “Not Prime” is printed and the control breaks out of the while loop. There are two ways the control could have reached outside the while loop:

  1. It jumped out because the number proved to be not a prime.
  2. The loop came to an end because the value of i became equal to the square root of n.

When the loop terminates in the second case, it means that there was no number between 2 and the square root of n that could exactly divide n. That is, n is indeed a prime. If this is true, the program should print out the message “Prime”. Notice that the statements above the break statement would get executed. And the statements after the break statement would get skipped.

The keyword break breaks the control only from the loop in which it is placed. Consider the following nested loop example:

#include <stdio.h>
int main()
{
    int i = 1 , j = 1;
    while ( i <= 10 ) {
        i++;
        while ( j <= 20 ) {
            j++;
            if ( j == 15 )
                break ;
            else
                printf ( "%d %d\n", i, j ) ;
        }
    }
    return 0;
}
2 2
2 3
2 4
2 5
2 6
2 7
2 8
2 9
2 10
2 11
2 12
2 13
2 14
3 16
3 17
3 18
3 19
3 20
3 21

In this program when j equals 15, the break takes the control outside the inner while only, since it is placed inside the inner while.

The continue statement

The continue statement takes the control to the beginning of the loop, bypassing the statements inside the loop, which have not yet been executed in that particular iteration. When a continue statement is encountered inside any loop, control automatically passes to the beginning of the loop. A continue is usually associated with an if.

#include <stdio.h>
int main() 
{
    int i, j ;
    for ( i = 1 ; i <= 2 ; i++ ) 
    {
       for ( j = 1 ; j <= 2 ; j++ )
       {
           if ( i == j )
               continue ;
           printf ( "%d %d\n", i, j ) ;
       }
    }
    return 0;
}

Output

1 2
2 1

Note that when the value of i equals that of j, the continue statement takes the control to the for loop (inner) bypassing the rest of the statements pending execution in the for loop (inner).

A continue statement in the do-while loop sends the control straight to the test at the end of the loop and not to the beginning of the loop.

Exercises

  1. Write a program to print all prime numbers from 1 to 300. (Hint: Use nested loops, break
    and continue)
  2. Write a program to generate all combinations of 1, 2 and 3.
  3. According to a study, the approximate level of intelligence of a person can be calculated using the following formula:
    i = 2 + ( y + 0.5 x )
    Write a program, which will produce a table of values of i, y and x, where y varies from 1 to 6, and, for each value of y, x varies from 5.5 to 12.5 in steps of 0.5.
  4. Write a program to print the following pattern:
  1. Write a program to read an integer number and print its table in the following format:
    12 * 1 = 12
    12 * 2 = 24

    12 * 10 = 120
  2. Write a program to print the following pattern:

    1
    2 3
    4 5 6
    7 8 9 10
  1. A machine is purchased which will produce an earning of Rs. 1000 per year while it lasts. The machine costs Rs. 6000 and will have a salvage of Rs. 2000 when it is condemned. If 12% per annum can be earned on alternative investments what would be the minimum life of the machine to make it a more attractive investment compared to alternative investments?
  1. Write a program in C to read x and the number of terms and display the sum of the
    series [ 1+x+x^2/2!+x^3/3!+….].
    Test data:
    x:3
    Terms:5
    Sum:16.375000

Categories
Computer Science / Information Technology Language: C

Case Control

Case-control statement – Switch

The control statement that allows us to make a decision from the number of choices is called a switch, or more correctly a switch-case-default since these three keywords go together to make up the control statement. A Switch is a composite statement used to make a decision between many distinct alternative possibilities of a constant presence in a variable or yielded from an expression. The syntax is as follows:

switch ( integer expression )
{
    case constant 1 :
    do this ;
    case constant 2 :
    do this ;
    case constant 3 :
    do this ;
    default :
    do this ;
}

The integer expression following the keyword switch is any C expression that will yield an integer value. It could be an integer constant like 1, 2 or 3, or an expression that evaluates to an integer. The keyword case is followed by an integer or a character constant. Each constant in each case must be different from all the others. The “do this” lines in the above form of switch represent any valid C statement.

A default label is a special form of the case label. It is executed whenever none of the other case values matches the value in the switch expression. Note, however, that default is not mandatory.

The need for a break

switch (i)
 {
    case 1: printf("It is 1\n");
    case 2: printf("It is 2\n");
    case 3: printf("It is 3\n");
    default: printf("None of the above");
}

The output of the above program is:
It is 1
It is 2
It is 3
None of the above

Clearly, this is not the desired output. The desired output is just “It is 1”. The program prints case 2 and 3 and the default case. It is important to note that it is the responsibility of the programmer to break out of the switch block once the intended case is executed. The following snippet illustrates the same:

switch (i) 
{
     case 1: printf("It is 1\n"); break;
     case 2: printf("It is 2\n"); break;
     case 3: printf("It is 3\n"); break;
     default: printf("None of the above");
}

Note that there is no need for a break statement after the last statement (in this case, the default statement) since the control comes out of the switch anyway. The operation of the switch is shown below in the form of a flowchart for a better understanding.

Case-control flow diagram

Points to note

  1. Cases can be sorted in any order. Not necessarily in incremental order as shown in previous examples. But note that it is advised to keep a logical order and the default statement at the beginning or at the end for better readability of the program written. The following switch snippet is equally valid and does the same job:
switch (i) 
{
     case 3: printf("It is 3\n"); break;
     case 1: printf("It is 1\n"); break;
     default: printf("None of the above"); break;
     case 2: printf("It is 2\n");
}
  1. One can use char values in a case as internally they are integer values mapping to ASCII table. The following snippet shows how this is done:
char ch = 'A';
switch (ch) 
{
    case 'A': printf("Apple\n"); break;
    case 'B': printf("Ball\n"); break;
    default: printf("None of the above"); break;
    case 'C': printf("Cat\n");
}

Output:
Apple

  1. char values in switch cases can be used interchangeably with their
    corresponding integer values. This is illustrated in the following snippet:
char ch = 'A';
switch (ch) 
{
    case 65: printf("Apple\n"); break;
    case 'B': printf("Ball\n"); break;
    default: printf("None of the above"); break;
    case 'C': printf("Cat\n");
}
  1. At times we may want to execute a common set of statements for multiple cases. How this can be done is shown in the following example.
char ch = 'A';
switch (ch) 
{
    case 'a':
    case 'A': printf("Apple\n"); break;
    case 'b':
    case 'B': printf("Ball\n"); break;
    default : printf("None of the above"); break;
    case 'c':
    case 'C': printf("Cat\n");
}

In the above snippet, the output remains the same regardless of the case of the character. Here, we are making use of the fact that once a case is satisfied the control simply falls through the case till it doesn’t encounter a break statement.

  1. Even if there are multiple statements to be executed in each case there is no need to enclose them within a pair of braces (unlike if, and else).
  2. Every statement in a switch must belong to some case or the other. If a statement doesn’t belong to any case the compiler won’t report an error. However, the statement would never get executed. For example, in the following program the printf( ) never goes to work.
char ch = 'A';
switch (ch) 
{
    printf("Inside switch");
    case 'A': printf("Apple\n"); break;
    case 'B': printf("Ball\n"); break;
    default: printf("None of the above"); break;
    case 'C': printf("Cat\n");
}
  1. If we have no default case, then the program simply falls through the entire switch and continues with the next instruction (if any,) that follows the closing brace of the switch, practically producing no effect.
  2. The disadvantage of the switch is that one cannot have a case in a switch which looks like case i <= 20 :

This is why a switch is not entirely replaceable with the if family of control statements. Even float is not allowed in the switch. The advantage of switch over if is that it leads to a more structured program and the level of indentation is manageable, more so if there are multiple statements within each case of a switch.

  1. switch works faster than an equivalent if-else ladder. Because the compiler generates a jump table for a switch during compilation. As a result, during execution, it simply refers to the jump table to decide which case should be executed, rather than actually checking which case is satisfied. As against this, if-elses are slower because they are evaluated at execution time. If on the other hand the conditions in the if-else were simple and less in number then if-else would work out faster than the lookup mechanism of a switch. Hence a switch with two cases would work slower than an equivalent if-else. Thus, you as a programmer should take a decision on which of the two should be used when.
  2. We can check the value of any expression in a switch. Thus the following switch statements are legal
switch ( i + j * k )
switch ( 23 + 45 % 4 * k )
switch ( a < 4 && b > 7 )

Expressions can also be used in cases provided they are constant expressions. Thus, case 3 + 7 is correct, however, case a + b is incorrect.

  1. The break statement when used in a switch takes the control outside the switch. However, the use of continue will not take control at the beginning of the switch as one is likely to believe.
  2. In principle, a switch may occur within another, but in practice, it is rarely done. Such statements would be called nested switch statements.
  3. The switch statement is very useful while writing menu-driven programs.

Exercises

  1. Write a program that tests the value of an integer num1. If the value is 10, square num1. If it is 9, read a new value into num1. If it is 2 or 3, multiply num1 by 99 and print out the result.
  2. Write a function called day_of_week that given an integer between 0 and 6, prints the corresponding day of the week. Assume that the first day of the week
    (0) is Sunday.
  3. Write a function called month_of_year that, given an integer between 1 and 12, prints the corresponding month of the year.
  4. Write a function called parkingCharge that, given the type of the vehicle (c for car, b for bus, t for truck) and the hours a vehicle spent in the parking lot, returns the parking charge based on the rates shown below:
    a. car : $2 per hour.
    b. bus : $3 per hour.
    c. truck : $4 per hou
Categories
Computer Science / Information Technology Language: C

Bitwise Operators

The smallest element in memory on which we are able to operate as yet is a bit. A bit is the building block of any data. In this section, we shall see how to operate on bits. Being able to operate on a bit level can be very important in programming, especially when a program must interact directly with the hardware. This is because the programming languages are byte-oriented, whereas hardware tends to be bit-oriented.

Before delving into bitwise operators, it is important to understand the bit numbering scheme in integers and characters.

Bit numbering scheme

Bits are numbered from zero onwards, increasing from left to right. This is shown below:

showbits()

This is a function which will be used throughout the section. We shall see the internal implementation of this function at a later part of the notes. For now, assume that the function prints the bits (binary format) of the number sent as an argument. For example:

int main() 
{
    for ( int j = 0 ; j <= 5 ; j++ ) {
        printf ( "\nDecimal %d is same as binary ", j ) ;
        showbits ( j ) ;
    }
    return 0;
}

Should produce the following output:

Decimal 0 is same as binary 0000000000000000
Decimal 1 is same as binary 0000000000000001
Decimal 2 is same as binary 0000000000000010
Decimal 3 is same as binary 0000000000000011
Decimal 4 is same as binary 0000000000000100
Decimal 5 is same as binary 0000000000000101

Bitwise operators

OperatorDescription
~
>>
<<
&
|
^
One’s Complement
Right shift
Left shift
Bitwise AND
Bitwise OR
Bitwise XOR
Bitwise operators in C

The above table contains one of the C’s powerful features: a set of bit manipulation operators. These permit the programmer to access and manipulate individual bits within a piece of data. These operators can operate on all primitive data types available in C.

One’s Complement

On taking one’s complement of a number, all 1’s present in the number is changed to 0’s and all 0’s are changed to 1’s. Examples:

  1. One’s complement of 1010 is 0101.
  2. One’s complement of 1111 is 0000.
  3. One’s complement of 65 is -66
    • The binary equivalent of 65 is 0000 0000 0100 0001.
    • One’s complement of the above binary is 1111 1111 0100 0001.
    • The compiler always displays the two’s complement of a negative number. The decimal equivalent of the two’s complement of the above binary is -66.

A program example:

#include <stdio.h>
int main()
{
    int i = 5;
    while (i--) {
    printf ( "\nDecimal: %d\tBinary:\t", i ) ;
    showbits ( i ) ;
    printf ( "\nOne’s complement: ") ;
    showbits ( ~i ) ;
    }
    return 0;
}

Output:

Decimal: 4 Binary: 0000000000000100
One’s complement: 1111111111111011
Decimal: 3 Binary: 0000000000000011
One’s complement: 1111111111111100
Decimal: 2 Binary: 0000000000000010
One’s complement: 1111111111111101
Decimal: 1 Binary: 0000000000000001
One’s complement: 1111111111111110
Decimal: 0 Binary: 0000000000000000
One’s complement: 1111111111111111

Right shift operator

The right shift operator is represented by >>. It needs two operands. It shifts each bit in its left operand to the right. The number of places the bits are shifted depends on the integer following the operator (i.e. its right operand)
Example:

  1. val >> 2 would shift all bits in val two places to the right.
    • If val is 5, then:
      • Val initially contains 0000 0101
      • The right shift results in the bits moving towards the right: 0000 0001.
  2. val >> 5 would shift all bits 5 places to the right.

Points to note

  • Note that as the bits are shifted to the right, blanks are created on the left. These blanks must be filled somehow. They are filled with zeros.
  • Note that right shifting once is the same as dividing it by 2 and ignoring the remainder. Thus,
    • 64 >> 1 gives 32
    • 64 >> 2 gives 16
    • 128 >> 2 gives 32
    • 27 >> 1 gives 13
    • 49 >> 1 gives 24
  • In the explanation a >> b if b is negative the result is unpredictable.
  • If a is negative then its leftmost bit (sign bit) would be 1. On some computers, right shifting would result in extending the sign bit.
    • For example, if a contains -1, its binary representation would be
      1111111111111111Without sign extension, the operation a >> 4 would be 0000111111111111.
#include <stdio.h>
int main() 
{
    int i = 1234;
    printf ( "\nDecimal %d is same as binary ", i ) ;
    showbits ( i ) ;
    for ( int j = 0 ; j <= 5 ; j++ ) {
    printf ( "\n%d right shift %d gives ", i, j ) ;
    showbits ( i >> j ) ;
    }
    return 0;
}

Output

Decimal 1234 is same as binary 0000 0100 1101 0010
1234 right shift 0 gives 0000 0100 1101 0010
1234 right shift 1 gives 0000 0010 0110 1001
1234 right shift 2 gives 0000 0001 0011 0100
1234 right shift 3 gives 0000 0000 1001 1010
1234 right shift 4 gives 0000 0000 0100 1101
1234 right shift 5 gives 0000 0000 0010 0110

Left shift operator

The left shift operator is represented by <<. It needs two operands. It shifts each bit in its left operand to the left. The number of places the bits are shifted depends on the integer following the operator (i.e. its right operand) Example:

  1. val << 2 would shift all bits in val two places to the left.
    • If val is 5, then:
      • Val initially contains 0000 0101
      • The left shift results the bits moved towards left: 0001 0100
  2. val << 5 would shift all bits 5 places to the left.

Points to note

  • Note that as the bits are shifted to the left, blanks are created on the right. These blanks must be filled somehow. They are filled with zeros.
  • Note that left shifting once is the same as multiplying it by 2. Thus,
    • – 64 << 1 gives 128
    • – 64 << 2 gives 256
  • In the explanation a << b if b is negative the result is unpredictable.

A program example:

#include <stdio.h>
int main() 
{
    int i = 1234;
    printf ( "\nDecimal %d is same as binary ", i ) ;
    showbits ( i ) ;
    for ( int j = 0 ; j <= 5 ; j++ )
    {
    printf ( "\n%d left shift %d gives ", i, j ) ;
    showbits ( i << j ) ;
    }
    return 0;
}

Output

Decimal 1234 is same as binary 0000 0100 1101 0010
1234 left shift 0 gives 0000 0100 1101 0010
1234 left shift 1 gives 0000 1001 1010 0100
1234 left shift 2 gives 0001 0011 0100 1000
1234 left shift 3 gives 0010 0110 1001 0000
1234 left shift 4 gives 0100 1101 0010 0000
1234 left shift 5 gives 1001 1010 0100 0000

Bitwise AND Operator

This operator is represented as & (not to be confused with &&, the logical AND operator). The & (an ampersand) operator operates on two operands and it is associative, that is, the order of the operands doesn’t change the result. While operating upon these two operands they are compared on a bit-by-bit basis. Hence both the operands must be of the same type. The second operand is often called an AND mask. The & operator operates on a pair of bits to yield a resultant bit. The rules (also called as truth table) that decide the value of the resultant bit are shown below:

+-----------+-----------+--------+
| Operand 1 | Operand 2 | Result |
+-----------+-----------+--------+
|     0     |     0     |    0   |
|     0     |     1     |    0   |
|     1     |     0     |    0   |
|     1     |     1     |    1   |
+-----------+-----------+--------+

The easier way to remember the truth table is that if both the operands are 1 then and then only the result will be 1, else 0.
Example:

+-----------+---+---+---+---+---+---+---+---+
| Operand 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
+-----------+---+---+---+---+---+---+---+---+
| Operand 2 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
+-----------+---+---+---+---+---+---+---+---+
| Result    | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
+-----------+---+---+---+---+---+---+---+---+

Thus, it must be clear that the operation is being performed on individual bits, and the operation performed on one pair of bits is completely independent of the operation performed on the other pairs.

The best use-case example of the AND operator is to check whether a particular bit or a set of bits of an operand is ON or OFF. Consider an operand whose 0th bit needs to be inspected to be 1 or not. All we need to do is AND the operand with another operand (the bitmask) whose 0th bit is set to 1, that is 00000001. If the result is equal to the bitmask, then the operand has the 0th bit set to 1. If the result is 0, then the 0th bit of the operator is set to 0. It is illustrated as follows:

+----------+---+---+---+---+---+---+---+---+
| Operand  | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
+----------+---+---+---+---+---+---+---+---+
| Bit mask | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
+----------+---+---+---+---+---+---+---+---+
| Result   | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+----------+---+---+---+---+---+---+---+---+

As the result is 0, the 0th bit of the operand is set to 0. A program example:

#include <stdio.h>
int main() {
    int i = 65, j ;
    printf ( "i: %d", i ) ;
    j = i & 32 ;
/*
		    +---------------------------+
		    |   Binary Representation   |
+---------------+---+---+---+---+---+---+---+---+
|     i = 65    | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
+---------------+---+---+---+---+---+---+---+---+
| Bit mask = 32 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
+---------------+---+---+---+---+---+---+---+---+
|     Result    | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+---------------+---+---+---+---+---+---+---+---+
*/
    if ( j == 0 )
        printf ( "\nFifth bit is off" ) ;
    else
        printf ( "\nFifth bit is on" ) ;
    j = i & 64 ;
/*
		    +---------------------------+
		    |   Binary Representation   |
+---------------+---+---+---+---+---+---+---+---+
|     i = 65    | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
+---------------+---+---+---+---+---+---+---+---+
| Bit mask = 64 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
+---------------+---+---+---+---+---+---+---+---+
|     Result    | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
+---------------+---+---+---+---+---+---+---+---+
*/
    if ( j == 0 )
        printf ( "\nSixth bit is off" ) ;
    else
        printf ( "\nSixth bit is on" ) ;
    return 0;
}

Output:

i: 65
Fifth bit is off
Sixth bit is on

Bitwise AND can also be used to turn off a particular bit in a given number. To do this, all the bits in the bitmask need to be set to 1 except for the bit position which we need to set 0 in the operand. For example, if the operand is 01000001 and we need to set the bit in the 0th
position to 0, the bitmask should be 11111110. The result of the AND operation of these two will give the desired result: 01000000.

Bitwise OR operator

Bitwise OR operator is represented as | (a pipe). The truth table of OR is as follows:

+-----------+-----------+--------+
| Operand 1 | Operand 2 | Result |
+-----------+-----------+--------+
|     0     |     0     |    0   |
|     0     |     1     |    1   |
|     1     |     0     |    1   |
|     1     |     1     |    1   |
+-----------+-----------+--------+

Notice that if any one of the operands of the OR operator is 1, the result is 1. Example:

+-----------+---+---+---+---+---+---+---+---+
| Operand 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
+-----------+---+---+---+---+---+---+---+---+
| Operand 2 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
+-----------+---+---+---+---+---+---+---+---+
| OR Result | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
+-----------+---+---+---+---+---+---+---+---+

Bitwise OR operator is usually used to put ON a particular bit in a number. To do this, all the bits in the second operand need to be set to 0 except for the bit position which we need to set 1 in the first operand. For example, if the operand is 01000001 and we need to set the bit in
the 2nd position to 1, the bitmask should be 00000010. The result of the OR operation of these two will give the desired result: 01000011.

Bitwise XOR operator

The XOR operator is represented as ^ and is also called an Exclusive OR Operator. XOR returns 1 only if one of the two bits is 1. The truth table for the XOR operator is given below.

+-----------+-----------+------------+
| Operand 1 | Operand 2 | XOR Result |
+-----------+-----------+------------+
|     0     |     0     |      0     |
|     0     |     1     |      1     |
|     1     |     0     |      1     |
|     1     |     1     |      0     |
+-----------+-----------+------------+

XOR operator is used to toggle a bit ON or OFF. A number XORed with another number twice gives the original number. This is shown in the following program.

#include <stdio.h>
int main() {
    int b = 45 ;
    b = b ^ 6 ;
/*
```
+-----------+---+---+---+---+---+---+---+
| Operand 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 |
+-----------+---+---+---+---+---+---+---+
| Operand 2 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
+-----------+---+---+---+---+---+---+---+
|XOR Result | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
+-----------+---+---+---+---+---+---+---+
```
*/
    printf ( "\n%d", b ) ; /* this will print 43 */
    b = b ^ 12 ;
    printf ( "\n%d", b ) ; /* this will print 39 */
    return 0;
}

The showbits() function

We have been using the showbits() function throughout the text and now, as we have sufficient knowledge of bitwise operators, let’s have a look at the definition of this mighty function which is very useful and important:

void showbits(int number) 
{
    for (int i = 15 ; i >= 0 ; i--)
    {
        printf("%d", (number >> i) & 1);
        if (i % 4 == 0)
            printf(" ");
    }
}

The given number is shifted right bit by bit and a bitwise AND operation is performed with 1. This will print 1 if the bit is set to 1 at a location in number, else 0. Trace the program for better understanding.

Exercises

  1. Write a C program to count trailing zeros in a binary number.
  2. Write a C program to flip bits of a binary number using the bitwise operator.
  3. Write a C program to count total zeros and ones in a binary number.
  4. Write a C program to rotate bits of a given number.
  5. Modify showbits() function to return an integer containing the binary number of the input number.
  6. Write a C program to swap two numbers using the bitwise operator.
  7. Write a C program to check whether a number is even or odd using a bitwise operator.
  8. Write a C program to get the nth bit of a number
Categories
Computer Science / Information Technology Language: C

Functions and Recursion

Introduction to Functions

A function is a self-contained block of statements that perform a coherent task of some kind. Every C program can be thought of as a collection of these functions. In other words, a function is a block of code with a name and statements in it and is supposed to execute a particular task. A task could be anything which the programmer
desires. Consider the following example:

#include <stdio.h>
foo ( ) 
{
   printf("In foo function!\n") ;
}
int main ( ) 
{
    foo ( );
    printf ( "In the main function" ) ;
    return 0;
}

output:

In foo function!
In the main function

Note: The programs defined here might throw warnings when run. Just assume that the programs are correct for the time being. The warnings will be dealt with once we reach the function declaration and prototype topic.


The main( ) itself is a function and through it, we are calling the function foo( ). The word ‘call’ means that the control passes to the function foo( ). When the foo( ) function runs out of statements to execute, the control returns to main( ) and resumes executing its code at the exact point where it left off. Thus, main( ) becomes the ‘calling’ function, whereas foo( ) becomes the ‘called’ function.

Consider the following example:

#include <stdio.h>
hyderabad( )
{
printf( "\nAt Hyderabad" ) ;
printf( "\nGoing back home\n" ) ;
}
chennai( )
{
printf( "\nAt Chennai" ) ;
printf( "\nGoing back home\n" ) ;
}
bengaluru( )
{
printf( "\nAt Bengaluru" ) ;
printf( "\nGoing back home\n" ) ;
}
int main( )
{
printf( "At home\n" ) ;
printf( "\nGoing to Bengaluru" ) ;
bengaluru( ) ;
printf( "\nGoing to Chennai" ) ;
chennai( ) ;
printf( "\nGoing to Hyderabad" ) ;
hyderabad( ) ;
}

The output of the above program is as follows:

At home

Going to Bengaluru
At Bengaluru

Going back home

Going to Chennai
At Chennai
Going back home

Going to Hyderabad
At Hyderabad
Going back home

From this program several conclusions can be drawn:

  • Any C program contains at least one function.
  • If a program contains only one function, it must be the main( ).
  • If a C program contains more than one function, then one (and only one) of these functions must be main( ) because program execution always begins with main().
  • There is no limit on the number of functions that might be present in a C program.
  • Each function in a program is called in the sequence specified by the function calls in main( ).
  • After each function has done its thing, control returns to main( ). When main( )runs out of function calls, the program ends.

As we have noted earlier the program execution always begins with main( ). Except for this fact, all C functions enjoy a state of perfect equality. No precedence and no priorities. A function can call another function and the second function can, in turn, call the third function and so on. To better illustrate this, consider the following example.

#include <stdio.h>
hyderabad( ) 
{
    printf ( "\nI am in Hyderabad" ) ;
}
chennai( ) 
{
    printf ( "\nI am in Chennai" ) ;
    hyderabad( ) ;
    printf ( "\nI am back in Chennai" ) ;
}
bengaluru( )
{
    printf ( "\nI am in Bengaluru" ) ;
    chennai( ) ;
    printf ( "\nI am back in Bengaluru" ) ;
}
int main( ) {
printf ( "I am in main" ) ;
bengaluru( ) ;
printf ( "\nI am finally back in main" ) ;
}

The output:

I am in main
I am in Bengaluru
I am in Chennai
I am in Hyderabad
I am back in Chennai
I am back in Bengaluru
I am finally back in main

Here, main( ) calls other functions, which in turn call still other functions. Trace carefully the way control passes from one function to another. Since the compiler always begins the program execution with main( ), every function in a program must be called directly or indirectly by main( ). In other words, the main( ) function drives other functions

Points to remember so far

a. C program is a collection of one or more functions.
b. A function gets called when the function name is followed by a semicolon. For example,

bengaluru( ) 
{
 chennai( ) ;
}

c. A function is defined when the function name is followed by a pair of braces in which one or more statements may be present. For example,

void bengaluru( )
{
   Statement 1 ;
   Statement 2 ;
   Statement 3 ;
}

d. Any function can be called from any other function. Even main( ) can be called from other functions. For example,

#include <stdio.h>
message( )
{
   printf ( "\nMessage" ) ;
   main( ) ;
}
int main( )
{
   message( ) ;
}

Note: This executes without an error but goes into an infinite loop by both the functions calling each other.
e. A function can be called any number of times. For example,

#include <stdio.h>
foo( ) 
{
    printf ( "\nIn foo" ) ;
}
int main( ) {
foo( ) ;
foo( ) ;
}

f. The order in which the functions get called is the same as the order in which the function calls occur and not in the order how the function is defined.

#include <stdio.h>
foo2( )
{
    printf ( "\nIn foo2" ) ;
}
foo1( ) {
printf ( "\nIn foo1" ) ;
}
int main( ) {
foo1( ) ;
foo2( ) ;
}

Here, even though the function foo2 is defined before function foo1, foo1 is called first as the calling statement in the main( ) function occurs in that order. However, it is advisable to define the functions in the same order in which they are called. This makes the program easier to understand.
g. A function can call itself. Such a process is called ‘recursion’. More on that later.
h. A function cannot be defined in another function. Thus, the following program code throws a compilation error.

#include <stdio.h>
int main( ) 
{
     printf ( "\nI am in main" ) ;
     foo( ) 
   {
       printf ( "\nI am in foo" ) ;
   }
}

There are basically two types of functions:
i. Library functions Ex. printf( ), scanf( ) etc.
ii. User-defined functions Ex. bengaluru( ), chennai( ) etc.
As the name suggests, library functions are commonly required functions grouped and stored in what is called a Library. This library of functions is present on the disk and is written by compiler authors. Almost always a compiler comes with a library of standard functions. The procedure of calling both types of functions is the same.

Advantages of using functions:

  • The use of functions enhances the readability of a program. A big code is always difficult to read. Breaking the code into smaller Functions keeps the program organized, easy to understand and makes it reusable.
  • The C compiler follows top-to-down execution, so the control flow can be easily managed in the case of functions. The control will always come back to the main() function.
  • It reduces the complexity of a program and gives it a modular structure.
  • In case we need to test only a particular part of the program we will have to run the whole program and figure out the errors which can be quite a complex process. Another advantage here is that functions can be individually tested which is more convenient than the above-mentioned process.
  • A function can be used to create our header file or a function can be called n number of times in the same file which can be used in any number of programs i.e. the reusability. This also helps in error isolation during the debug process.
  • Functions help in dividing a complex problem into simpler ones.

Passing values between functions:

  • Communication between the ‘calling’ and the ‘called’ functions is necessary during various scenarios.
  • The mechanism used to convey information to the function is the ‘argument’.
  • You have unknowingly used the arguments in the printf( ) and scanf( ) functions; the format string and the list of variables used inside the parentheses in these functions are arguments.
  • In the following program, in main( ) we receive the values of a, b and c through the keyboard and then output the sum of a, b and c. The calculation of sum is done in function calculate_sum( ). So, we must pass on the values of a, b, c to calculate_sum( ), and once calculate_sum( ) calculates the sum we must return it from calculate_sum( ) back to main( ).
#include <stdio.h>
int calculate_sum ( x, y, z )
int x, y, z ;
{
    int d ;
    d = x + y + z ;
    return ( d ) ;
}
int main( )
{
    int a, b, c, sum ;
    printf ( "\nEnter any three numbers " ) ;
    scanf ( "%d %d %d", &a, &b, &c ) ;
    sum = calculate_sum ( a, b, c ) ;
    printf ( "\nSum = %d", sum ) ;
}

output

Enter any three numbers 1 2 3
Sum = 6

Points to note:

  • The values a, b, c are passed from main( ) to caculate_sum( ) by making a call to the function calculate_sum( ) and mentioning a, b and c in the parentheses:
sum = calculate_sum ( a, b, c ) ;
  • In the calculate_sum( ) function these values get collected in three variables x, y and z:
calculate_sum ( x, y, z )
int x, y, z ;
  • The variables a, b and c are called ‘actual arguments’, whereas the variables x, y and z are called ‘formal arguments’ or ‘parameters’.
  • Any number of arguments can be passed to a function being called. The order, type and the number of the actual and formal arguments must always be the same.
  • Instead of using different variable names x, y and z, we could have used the same variable names a, b and c. But the compiler would still treat them as different variables since they are in different functions.
  • There are two methods of declaring formal arguments. The one that we have used in our program is known as the Kernighan and Ritchie (or just K & R) method.
calculate_sum ( x, y, z )
int x, y, z ;
  • Another method is the ANSI method which is more commonly used:
calculate_sum ( int x, int y, int z )
  • In the above program, we want to return the sum of x, y and z. Therefore, it is necessary to use the return statement. The return statement serves two purposes:
    • On executing the return statement it immediately transfers the control back to the calling function.
    • It returns the value next to return, to the calling program. In the above program, the value of the sum of three numbers is returned.
  • There is no restriction on the number of return statements in a function. Also, the return statement need not always be present at the end of the called function. The following program illustrates these facts.
foo( ) 
{
char ch ;
printf ( "\nEnter any alphabet " ) ;
scanf ( "%c", &ch ) ;
if ( ch >= 65 && ch <= 90 )
    return ( ch ) ;
else
    return ( ch + 32 ) ;
}
  • In this function, different return statements will be executed depending on the evaluation of if-else.
  • Whenever the control returns from a function some value is returned. If a meaningful value is returned then it should be accepted in the calling program by equating the called function to some variable. For example,
sum = calculate_sum ( a, b, c ) ;
  • All the following are valid return statements given that the return type of the functions they are in, justify the data-type of values being returned by return statements.
return ( a ) ; // If the return type of the function is the
same as that of the data type of ‘a’
return ( 23 ) ; // If the return type of the function is int
return ( 12.34 ) ; // If the return type of the function is
float or double
return 89 ; // If the return type of the function is int
return ; // If the return type of the function is void
  • In the last statement, a garbage value is returned to the calling function since we are not returning any specific value. Note that in this case the parentheses after return are dropped.
  • If we want that a called function should not return any value, in that case, we must mention so by using the keyword void as shown below.
void display( ) 
{
printf ( "\nHeads I win..." ) ;
printf ( "\nTails you lose" ) ;
}
  • A function can return only one value at a time. Thus, the following statements are invalid.
return ( a, b ) ;
return ( x, 12 ) ;
  • But the following examples are valid:
return ( a ) ;
return ( x ) ;
  • If the value of a formal argument is changed in the called function, the corresponding change does not take place in the calling function. For example,
#include <stdio.h>
fun ( int b ) {
    b = 60 ;
    printf ( "\n%d", b ) ;
}
int main( )
 {
    int a = 30 ;
    fun ( a ) ;
    printf ( "\n%d", a ) ;
}

Output:

60
30
  • Even if the value of b is changed in fun( ), the value of ‘a’ in main( ) remains unchanged. This means that when values are passed to a called function the values present in actual arguments are not physically moved to the formal arguments; just a photocopy of values of the actual argument is made into formal arguments.

Scope Rule of Functions

Consider the following program

display ( int j ) 
{
   int k = 35 ;
   printf ( "\n%d", j ) ;
   printf ( "\n%d", k ) ;
}
int main( )
{
   int i = 20 ;
   display ( i ) ;
}
  • Note that the main function is sending the value 20 in variable ‘i’ to the function display( ).
  • This program begs a vital question. Is it necessary to send the value of ‘i’ to display( )? Is it not possible for the display( ) function to directly access it without main( ) having to pass the value?
  • The answer to the above questions is No. Because by default the scope of a variable is local to the function in which it is defined. The presence of i is known only to the function main( ) and not to any other function.
  • Similarly, the variable k is local to the function display( ) and hence it is not available to main( ). That is why to make the value of ‘i’ available to display( ) we have to explicitly pass it to display( ). This is done by calling functions. A function can be called in 2 ways.
    ○ Call by value (Which we are seeing in this section)
    ○ Call by reference (Which we will see in a short while)
  • Likewise, if we want k to be available to main( ) we will have to return it to main( ) using the return statement. In general, we can say that the scope of a variable is local to the function, more specifically, the block in which it is defined.

Calling convention

  • Calling convention indicates the order in which arguments are passed to a function when a function call is encountered. There are two possibilities here:
    • Arguments might be passed from left to right.
    • Arguments might be passed from right to left.

C language follows the second order. Consider the following function call:

fun (a, b, c, d ) ;

In this call, it doesn’t matter whether the arguments are passed from left to right or from right to left. However, in some function calls, the order of passing arguments becomes an important consideration. For example:

int a = 1 ;
printf ( "%d %d %d", a, ++a, a++ ) ;

It appears that this printf( ) would output 1 2 3. This however is not the
case. Surprisingly, it outputs 3 3 1. This is because C’s calling convention is from right to left.

Call by Value and Call by Reference

Whenever we have called a function with arguments, we have always passed the ‘values’ of variables to the called function. Such function calls are called ‘call by value’. All the examples we saw till now that pass values to functions were called by value. For example:

f = factr ( a ) ;

Here, one can see that the ‘value’ of ‘a’ is being passed to function factr().
Hence, call by value. One can pass the address of the variable to the function. This is called ‘call by reference’. Note that this feature of C functions needs at least an elementary knowledge of a concept called ‘pointers’.
In call by reference, the addresses of actual arguments in the calling function are copied into formal arguments of the called function. This means that by using these addresses we would have access to the actual arguments and hence we would be able to manipulate them. The following program illustrates this fact.

#include <stdio.h>
void swap( int *x, int *y )
{
     int t ;
     t = *x ;
    *x = *y ;
    *y = t ;
}
int main( )
{
int a = 10, b = 20 ;
swap ( &a, &b ) ;
printf ( "\na = %d b = %d", a, b ) ;
}

Output:

a = 20 b = 10

Note that this program swaps the values of a and b using their addresses stored in x and y. Using a call by reference intelligently we can make a function return more than one value at a time, which is not possible ordinarily. This is shown in the program given below.

#include <stdio.h>
void area_perimeter ( float r, float *a, float *p )
{
*a = 3.14 * r * r ;
*p = 2 * 3.14 * r ;
}
int main( ) 
{
     float area, perimeter, radius ;
     printf ( "\nEnter radius of a circle " ) ;
     scanf ( "%f", &radius ) ;
     area_perimeter ( radius, &area, &perimeter ) ;
     printf ( "Area = %f", area ) ;
     printf ( "\nPerimeter = %f", perimeter ) ;
}

Output

Enter radius of a circle 5
Area = 78.500000
Perimeter = 31.400000

Here, we are passing the value of radius but addresses of area and perimeter. And since we are passing the addresses, any change that we make in values stored at addresses contained in the variables a and p would make the change effective in main( ). That is why when the control returns from the function area_perimeter( ) we can output the values of area and perimeter.
Thus, we have been able to indirectly return two values from a called function, and hence, have overcome the limitation of the return statement, which can return only one value from a function at a time.

Library functions

  • Note that pow(), printf(), scanf(), sqrt() etc are all library functions that are already defined by compiler authors and are ready to use as they come shipped with the compiler.
  • But one cannot use these functions without “including the header file” associated with the functions.
  • Header files are a special type of file that only contain the function prototypes, i.e., the function name, return type and the order and data type of the parameters of the functions.
  • Once the header file is included, it helps the compiler to evaluate the function calls you do in your program to find if there is any mismatch in the way you are calling and the way it has to be called. If there is a mismatch, the compiler throws a compile-time error namely ‘undefined reference’ which means that the function call you are making is referring to no function definition.
  • So it is not just essential to include the header files corresponding to the functions you are going to use in the program but it is equally important to call the functions with the proper name, arguments, the data type, and order of the arguments.

Exercises

  1. Write a function to calculate the factorial value of any integer entered through the keyboard. The function is to take the integer as the parameter and return the factorial of the received number back. Read the user input and print the factorial in the main function.
  2. Write a function power ( a, b ), to calculate the value of a raised to b. Read a and b using scanf( ) in main( ) and print the power value returned from power( ) function in main( ) function.
  3. Write a function to take a year as an input parameter and return 1 if the year is a leap year and 0 otherwise.
  4. Write a function to take a positive integer as an input parameter and return 1 if the integer is palindrome and 0 otherwise.
  5. Write a function to take a positive integer as an input parameter and print the prime factors of the input integer. Let the return type of the function be void. a. Prime factors of the 24 are 2, 2, 2 and 3, whereas prime factors of 35 are
    5 and 7
  6. Write a function that receives a float and an int from main( ), finds the product of these two and returns the product which is printed through main( ).
  7. Write a function that receives 5 integers and returns the sum, average and standard deviation of these numbers. Call this function from main( ) and print the results in main( ).
  8. Write a function that receives marks received by a student in 3 subjects and returns the average and percentage of these marks. Call this function from main() and print the results in main( ).
  9. A 5-digit positive integer is entered through the keyboard, write a function to calculate the sum of digits of the 5-digit number. Call this function from main( ) and print the results in main( ).
  10. Write a function to find the binary equivalent of a given decimal integer as an argument and display it.

Function Declaration and Prototypes

Any C function by default returns an int value. By default (when you do not mention a return type), the return type of a function is assumed to be an integer. If we desire that a function should return a value other than an int, then it is necessary to explicitly mention it. Suppose we want to find out the square of a number using a function. This is what this simple program would look like:

#include <stdio.h>
#include <math.h>
square ( float x ) 
{
    float y ;
    y = x * x ;
    return ( y ) ;
}
int main( ) 
{
    float a, b ;
    printf ( "Enter any number " ) ;
    scanf ( "%f", &a ) ;
    b = square ( a ) ;
    printf ( "Square of %f is %f", a, b ) ;
}

And here are three sample runs of this program…

Enter any number 3
Square of 3 is 9.000000
Enter any number 1.5
Square of 1.5 is 2.000000
Enter any number 2.5
Square of 2.5 is 6.000000

The first of these answers is correct. But a square of 1.5 is not 2. Neither is 6 a square of 2.5. This happened because any C function, by default, always returns an integer value. Therefore, even though the function square( ) calculates the square of 1.5 as 2.25, the problem crops up when this 2.25 is to be returned to main( ). square( ) is not capable of returning a float value. So it typecasts 2.25 to integer i.e.2 and then it
returns 2. The value 2 is stored in b which is a floating-point variable. This will cause the compiler to typecast an integer value to a floating-point value i.e. 2.000000. The following program segment illustrates how to make square( ) capable of returning a float value.

#include <stdio.h>
#include <math.h>
int main( )
{
    float square ( float ) ;
    float a, b ;
    printf ( "Enter any number " ) ;
    scanf ( "%f", &a ) ;
    b = square ( a ) ;
    printf ( "Square of %f is %f", a, b ) ;
}
float square ( float x ) 
{
    float y ;
    y = x * x ;
    return ( y ) ;
}

output

Enter any number 1.5

Square of 1.5 is 2.250000
Enter any number 2.5
Square of 2.5 is 6.250000

Now the expected answers i.e. 2.25 and 6.25 are obtained. Note that the function square( ) must be declared in main( ) as

float square ( float ) 

This statement is often called the prototype declaration of the square( ) function. This means that square( ) is a function that receives a float and returns ‘a’ float. We have done the prototype declaration in main( ) because we have called it from main( ). There is a possibility that we may call square( ) from several other functions other than main( ). In such a case we would make only one declaration outside all the functions at the beginning of the program.
In some programming situations, we want that a called function that should not return any value. This is made possible by using the keyword void. This is illustrated in the following program.

#include <stdio.h>
#include <math.h>
int main( ) 
{
   void foo( ) ;
   foo( ) ;
}
void foo( )
{
   printf ( "\nIn foo" ) ;
}

Here, the foo( ) function has been defined to return void; which means it would return nothing. Therefore, it would just flash the message and return the control back to the main( ) function.
One can avoid writing function prototypes altogether. Just place the function definition anywhere before the function is called for the first time and the compiler should not throw any warning or errors. The same program as above can be rewritten as follows:

#include <stdio.h>
#include <math.h>

void foo( ) 
{
  printf ( "\nIn foo" ) ;
}
int main( ) 
{
  foo( ) ;
}

This produces the same output as the previous one. It is always advised to declare the function prototypes soon after the header files in the order they are called and then define the main( ) after which the functions can be defined in the order they have been declared.

Variadic functions

Variadic functions are functions (e.g. printf) that take a variable number of arguments. The declaration of a variadic function uses an ellipsis as the last parameter, e.g.

int printf(const char* format, ...);

Accessing the variadic arguments from the function body uses the following library facilities from stdarg.h:

  1. va_list: A data type that holds the information needed by va_start, va_arg, va_end, and va_copy.
  2. va_start: Enables access to variadic function arguments. Should be called first. This takes two parameters: identifier of type va_list and number of arguments.
  3. va_arg: Accesses the next variadic function arguments. Takes two parameters: identifier of type va_list and data type of the value currently being traversed. This returns the value being traversed.
  4. va_copy: Makes a copy of the variadic function arguments.
  5. va_end: Ends traversal of the variadic function arguments. Takes one parameter: identifier of type va_list which was used in va_start.

The following example illustrates a variadic function:

#include <stdio.h>
#include <stdarg.h>
// The first argument is the total number of arguments being passed.
// This is done for the ease to know when to stop traversing a list
of arguments.
double average(int num,...)
{
    va_list valist;
    double sum = 0.0;
    int i;
    /* initialize valist for num number of arguments */
    va_start(valist, num);
    /* access all the arguments assigned to valist */
    for (i = 0; i < num; i++) {
    sum += va_arg(valist, int);
}
    /* clean memory reserved for valist */
    va_end(valist);
    return sum/num;
}
int main() 
{
    printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
    printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
    return 0;
}

Recursion

  • In its simplest sense, recursion in a programming language is a function calling itself. That is, the caller function and the called function are the same.
  • A typical recursive function consists of an exit condition that, upon being true, breaks the process of the function calling itself. And an optional logic of operation and then a statement to call itself.
  • To understand recursion better, consider the following program to calculate the factorial of a given number. Note that this approach doesn’t make use of recursion.
#include <stdio.h>
int factorial ( int x )
{
    int f = 1, i ;
    for ( i = x ; i >= 1 ; i-- )
    f = f * i ;
    return ( f ) ;
}
int main( )
{
    int a, fact ;
    printf ( "\nEnter any number " ) ;
    scanf ( "%d", &a ) ;
    fact = factorial ( a ) ;
    printf ( "Factorial value = %d", fact ) ;
}

Output

Enter any number 3
Factorial value = 6
  • Now, the following is the recursive version of the function to calculate the factorial value.
#include <stdio.h>
int rec ( int x )
{
    int f ;
    if ( x == 1 )
       return ( 1 ) ; // Exit condition
    else
       f = x * rec ( x - 1 ) ;
    return ( f ) ;
}
int main( )
{
     int a, fact ;
     printf ( "\nEnter any number " ) ;
     scanf ( "%d", &a ) ;
     fact = rec ( a ) ;
     printf ( "Factorial value = %d", fact ) ;
}

And here is the output for four runs of the program

Enter any number 1
Factorial value = 1
Enter any number 2
Factorial value = 2
Enter any number 3
Factorial value = 6
Enter any number 5
Factorial value = 120
  • In the first run number is 1, the condition if ( x == 1 ) is satisfied and hence 1 (which indeed is the value of 1 factorial) is returned through the return statement.
  • When the number is 2, the ( x == 1 ) test fails, so we reach the statement
f = x * rec ( x - 1 ) ;
  • And here is where we meet recursion. Since the current value of x is 2, it is the same as saying that we must calculate the value (2 * rec ( 1 )). Now the control goes into the rec( ) function again with 1 as an argument. The function returns 1 and hence value (2 * rec( 1 )) reduces to (2 * 1) and the value 2 is returned to main ( ).
  • Now perhaps you can see what would happen if the value of a is 3, 4, 5 and so on.
    ● For the value of a = 5, we might say what happens is,
    ○ rec ( 5 ) returns ( 5 times rec ( 4 ),
    ■ which returns ( 4 times rec ( 3 ),
    ● which returns ( 3 times rec ( 2 ),
    ○ which returns ( 2 times rec ( 1 ),
    ■ which returns ( 1 )
    ○ )
    ● )
    ■ )
    ○ )

Exercises

  1. A 5-digit positive integer is entered through the keyboard, write a function to calculate the sum of digits of the 5-digit number using recursion
  2. A positive integer is entered through the keyboard and write a program that prints the prime factors of the number recursively.
  3. Write a recursive function to obtain the first 25 numbers of a Fibonacci sequence.
  4. A positive integer is entered through the keyboard, write a function to find the binary equivalent of this number using recursion.
  5. Write a recursive function to obtain the running sum of the first 25 natural numbers.
  6. Given three variables x, y, z write a function to circularly shift their values to right. In other words if x = 5, y = 8, z = 10 after circular shift y = 5, z = 8, x =10 after
    circular shift y = 5, z = 8 and x = 10. Call the function with variables a, b, c to circularly shift values. Hint: Use call by reference function calls.
Categories
Computer Science / Information Technology Language: C

Decision Control Statements

Many times, we want a set of instructions to be executed in one situation, and an entirely different set of instructions to be executed in another situation. This kind of situation is dealt with in C programs using decision control instruction. A decision control instruction can be implemented in C using:

  • The if statement
  • The if-else statement
  • The conditional operators

The if statement

C uses the keyword ‘if’ to implement the decision control instruction. The general form of if statement looks like this:

if ( this condition is true )
    execute this statement;

The keyword ‘if’ tells the compiler that what follows is a decision control instruction. The condition following the keyword if is always enclosed within a pair of parentheses. If the condition, whatever it is, is true, then the statement is executed. If the condition is not true then the statement is not executed; instead, the program skips past it. A condition can be expressed using C’s ‘relational’ operators which are listed in the following table:

ExpressionExplanation
x == yx is equal to y
x != yx is not equal to y
x < yx is less than y
x > yx is greater than y
x <= yx is less than or equal to y
x >= yx is greater than or equal to y
Relational operator expressions example

The following example program illustrates the if statement. It reads the age of the user and determines if the user can vote.

#include <stdio.h>
int main() 
{
    int age;
    scanf("%d", &age);
    if (age > 18)
        printf("Eligible");
    return 0;
}

Expression as condition

An expression can be any valid expression including a relational expression which can be used as a condition inside of if. We can even use arithmetic expressions in the if statement. For example, all the following if statements are valid

if ( 3 + 2 % 5 )
    printf ( "This works" ) ;
if ( a = 10 )
    printf ( "Even this works" ) ;
if ( -5 )
    printf ( "Surprisingly even this works" ) ;

Points to note:

  • Note that in C a non-zero value (integer or float) is considered to be true, whereas a 0 is considered to be false. Hence all three printf() statements get executed in the above example.
  • In the second if, 10 gets assigned to a so the if is now reduced to if ( a ) or if ( 10). Since 10 is non-zero, it is true hence again printf( ) gets executed.

Multiple Statements within if

It may so happen that in a program we want more than one statement to be executed if the expression following if is satisfied. If such multiple statements are to be executed then they must be placed within a pair of braces as illustrated in the following example.

#include <stdio.h>
int main()
{
    int age;
    scanf("%d", &age);
    if (age > 18)
    {
        printf("Eligible");
        printf("Please get the voter id ready");
    }
    return 0;
}

If the pair of brackets enclosing the printf() statements under if is removed, the compiler considers only the first statement as the if block. Hence the default scope of the if statement is the immediately next statement after it.

The if-else statement

if statement executes a set of statements when the condition placed in it evaluates to true. It does nothing when the expression evaluates to false. An else block coupled with an if block helps in executing a totally different set of instructions when the condition fails. Consider the same voter example which we saw earlier. The program should check if the age is greater than 18. And if it is not, the program should print “Not eligible”.

#include <stdio.h>
int main() 
{
    int age;
    scanf("%d", &age);
    if (age > 18)
    {
        printf("Eligible");
        printf("Please get the voter id ready");
    }
    else 
    {
        printf("Not eligible");
        printf("Wait for appropriate age");
    }
    return 0;
}

Points to note:

  • The group of statements after the if up to and not including the else is called an ‘if block’. Similarly, the statements after the else form the ‘else block’.
  • Notice that the else is written exactly below the if. There should not be any statement between the if block and else block. This will raise a compilation error.
  • Had there been only one statement to be executed in the if block and only one statement in the else block we could have dropped the pair of braces.
  • As with the if statement, the default scope of else is also the statement immediately after the else. To override this default scope a pair of braces as shown in the above example must be used.

Nested if-else

It is perfectly all right if we write an entire if-else construct within either the body of the if statement or the body of an else statement. This is called nesting of ifs. This is shown in the following program:

#include <stdio.h>
int main() 
{
    int n;
    scanf("%d", &n);
    if (n % 3 == 0)
        printf("Divisible by 3");
    else 
    {
        if (n % 5 == 0)
            printf("Divisible by 5");
        else
            printf("Neither divisible by 3 nor 5");
    }
    return 0;
}

Use of logical operators

Logical operators (‘AND’, ‘OR’, ‘NOT’) are extensively used in conditional statements. These operators can be used to reduce the nesting and complexity of the conditional statements. Consider the following example where the percentage of a student is read and the class is printed based on the following criteria:

  • Percentage above or equal to 60 – First division
  • Percentage between 50 and 59 – Second division
  • Percentage between 40 and 49 – Third division
  • Percentage less than 40 – Fail
#include <stdio.h>
int main() 
{
    float per;
    scanf("%f", &per);
    if ( per >= 60 )
            printf ( "First division ") ;
    else 
    {
            if ( per >= 50 )
                printf ( "Second division" ) ;
            else 
            {
                if ( per >= 40 )
                    printf ( "Third division" ) ;
                else
                    printf ( "Fail" ) ;
            }
    }
    return 0;
}

Points to note:

  • As the number of conditions goes on increasing the level of indentation also goes on increasing. As a result, the whole program creeps to the right.
  • Care needs to be exercised to match the corresponding ifs and elses.
  • Care needs to be exercised to match the corresponding pair of braces.
  • The readability of the program takes a serious hit.

The same program can be rewritten using logical operators as follows and all the above disadvantages can be eliminated:

#include <stdio.h>
int main()
{
    float per;
    scanf("%f", &per);
    if ( per >= 60 )
        printf ( "First division ") ;
    if ( ( per >= 50 ) && ( per < 60 ) )
        printf ( "Second division" ) ;
    if ( ( per >= 40 ) && ( per < 50 ) )
        printf ( "Third division" ) ;
    if ( per < 40 )
        printf ( "Fail" ) ;
    return 0;
}

Points to note:

  • The mismatching of the ifs with their corresponding elses gets avoided.
  • In spite of using several conditions, the program doesn’t creep to the right

The else-if clause

The else-if clause (also called as if-else-if ladder) gives a powerful way of handling multiple cases and possibilities. The syntax is as follows:

if (condition)
    statement 1;
else if (condition)
    statement 2;
.
.
else
    Statement;

Here, the last else statement is optional. The if statements are executed from the top down. As soon as one of the conditions controlling the if is true, the statement associated with that if is executed, and the rest of the C else-if ladder is bypassed. If none of the conditions is true, then the final else statement will be executed.


Consider the same example of the printing class of the student based on the percentage. It can be written in a different way using else-if as follows:

#include <stdio.h>
int main() 
{
    float per;
    scanf("%f", &per);
    if ( per >= 60 )
        printf ( "First division ") ;
    else if ( ( per >= 50 ) && ( per < 60 ) )
        printf ( "Second division" ) ;
    else if ( ( per >= 40 ) && ( per < 50 ) )
        printf ( "Third division" ) ;
    else
        printf ( "Fail" ) ;
    return 0;
}

Points to be noted

  • This program reduces the indentation of the statements. In this case, every else is associated with its previous if.
  • The last else goes to work only if all the conditions fail.
  • No other statements should be placed between the ladder. If there are multiple statements in an else-if case, they have to be enclosed in brackets.

Advantages of the else-if clause:

  • In case if-else had to replicate the output logic of the else-if ladder, it would be more complex. The following example illustrates the contrast between the two:
if (i == 10)
printf("i is 10");
else if (i == 15)
printf("i is 15");
else
printf("i is not
present");

if (i == 10)
printf("i is 10");
else {
if (i == 15)
printf("i is 15");
else
printf("i is not present");
}

Common mistakes

Using assignment operator instead of ‘==’ comparison operator as shown:

#include <stdio.h>
int main() 
{
    float per = 8.9;
    if ( per = 7.9 );
    printf ( "Same");
    return 0;
}

The program will print “Same” irrespective of the value of ‘per’.

  • Putting semicolon at the end of if() such as follows:
#include <stdio.h>
int main() 
{
      float per = 8.9;
      if ( per == 7.9 );
      printf ( "Same");
      return 0;
}

The above program prints “Same”.

  • Comparing floating-point variables with double. Consider the following example.
#include <stdio.h>
int main() 
{
    float per = 8.9;
    if ( per == 8.9 )
    printf ( "Same");
    else
    printf("Different");
    return 0;
}

The output of the above program will surprisingly be “Different” as ‘8.9’ is double and ‘per’ is float and they are not the same.

The conditional operators / Ternary operators

The conditional operators ‘?’ and ‘:’ are sometimes called ternary operators since they take three arguments. They form a foreshortened if-then-else. The general form is:

expression 1 ? expression 2 : expression 3 

This decodes to:

if expression 1 is true, then the value returned will be expression 2, otherwise, the value returned will be expression 3.

Example: y = ( x > 5 ? 3 : 4 ); The above statement assigns y to 3 if the value of x is greater than 5 and 4 otherwise.

Points to note:

  • Conditional operators could be used for statements other than arithmetic statements. This is illustrated as follows:
    • ( i == 8 ? printf (“QM”) : printf (“C Programming”)) ;
    • printf ( “%c” , ( a <= ‘z’ ? a : ‘!’ ));
  • The conditional operators can be nested as shown below.
    • big = ( a > b ? ( a > c ? 3: 4 ) : ( b > c ? 6: 8 ));
  • Consider the following conditional expression:
    • a > b ? g = a : g = b ;
    • This will make the compiler throw an error ‘Lvalue Required’. The error can be overcome by enclosing the statement in the : part within a pair of parenthesis. This is shown below:
    • a > b ? g = a : ( g = b ) ;
  • In absence of parentheses, the compiler believes that b is being assigned to the result of the expression to the left of second =. Hence it reports an error.
  • The limitation of the conditional operators is that after the ? or after the : only one C statement can occur. In practice rarely is this the requirement.

Excercise

  1. If cost price and selling price of items is given, write a program to determine whether the seller has made profit or incurred loss and print how much profit he made or loss he incurred.
  2. Write a program to find out whether a given number is odd or even.
  3. Given an year, write a program to determine whether it is a leap year or not.
  4. According to the Gregorian calendar, it was Monday on the date 01/01/1900. If any year is input through the keyboard write a program to find out what is the day on 1st January of this year.
  5. Given a five-digit number, write a program to obtain the reversed number and to determine whether the original and reversed numbers are equal or not.
  6. Given 3 numbers, print the minimum, maximum and average of them.
  7. Given the three angles of triangle, check if the triangle is valid or not.
  8. Find the absolute value of a number entered through the keyboard.
  9. Given the length and breadth of a rectangle, write a program to find whether the area of the rectangle is greater than its perimeter.
  10. Given three points (x1, y1), (x2, y2) and (x3, y3), write a program to check if all the three points fall on one straight line.
  11. Given the coordinates (x, y) of a center of a circle and it’s radius, write a program which will determine whether a point lies inside the circle, on the circle or outside the circle. (Hint: Use sqrt( ) and pow( ) functions)
  12. Given a point (x, y), write a program to find out if it lies on the x-axis, y-axis or at the origin, viz. (0, 0).
  13. Given the three sides of a triangle, write a program to check whether the triangle is isosceles, equilateral, scalene or right angled triangle.
  14. If the three sides of a triangle are entered through the keyboard, write a program to check whether the triangle is valid or not. The triangle is valid if the sum of two sides is greater than the largest of the three sides.
Categories
Computer Science / Information Technology Language: C

Introduction to C

The inception of C

  • Designed and developed in AT&T’s Bell Labs in 1972 by Dennis Ritchie.
  • It is a reliable, fast, simple, and easy to use language.
  • It is a middle-level language which just close enough to the machine as well as to the programmer. This gives the programmer fine control over machine properties such as memory and processer.
  • UNIX, Linux, Android, PHP, Python, C++ and so many other products and languages are written in C making it the most influential and powerful language for a professional to learn.
  • C is everywhere. Smartphones, computers, enterprise machines, dish TV set-up boxes, cameras, microwaves, washing machines and 99% of smart devices run C or a morphed version of C. So, C is a basic skill to have if ones want to become a professional in computers.

Let’s get started

  • C is a language just like English and Kannada which is used to communicate.
  • English and other natural languages are used to communicate with other humans and we use C to communicate with computers.
  • Just the way we learn natural languages by starting with the alphabet followed by words that form sentences finally constituting a paragraph, we learn C from alphabets, digits, and special symbols followed by constants, variables, and keywords which form instructions finally constituting a program.

The C Character Set

  • A character denotes any alphabet, digit or special symbol used to represent information.
  • In C,
    • Alphabets are: from A to Z and from a to z. (Obviously)
    • Digits are from 0 to 9 (Duh!)
    • Special symbols are anything printable on your keyboard other than the above two groups.

C Tokens

Tokens are the result of a combination of C character set and form the fundamental building blocks of the C programming language. There are 5 types of C tokens. They are:

Keywords:

  • These are reserved words that have a specific meaning in the language.
  • Each keyword has its dedicated functionality.
  • English has over 4,70,000 words and we have learnt it. Guess how many keywords does C have? Just 32. Do you still think C is difficult?
  • Here are the keywords of C:
autodoubleintstruct
breakelselongswitch
caseenumregistertypedef
charexternreturnunion
constfloatshortunsigned
continueforsignedvoid
defaultgotosizeofvolatile
doifstaticwhile
Keywords in C

Identifiers

  • The computer consists of millions of cells in which data is stored. To make the retrieval, storage and usage of the values easy, these memory cells are given names. Since the value in a particular cell might change or vary, these locations are called variables/identifiers.
  • Here are rules for naming identifiers:
    • The first character of an identifier should either be an alphabet or an underscore and nothing else. And then it could be followed by characters or digits or underscores.
    • Identifiers in C are case sensitive. I.e., abc is a different identifier than Abc.
    • Keywords should not be used as identifiers.
    • The length of the identifiers should not be more than 31 characters.
    • Tip: Identifiers should be written in such a way that it is meaningful, short and easy to read.
  • Examples of valid and invalid identifiers:
ValidInvalid
num
sum_of_array
sumOfArray
_len
first
side1
side_2
Side2
X’mas
switch
main()
sum of array
2nd
$dollars
period.
percent%
Examples of valid and invalid identifiers

Constants:

  • Contrary to variables, constants are the entities that remain unchanged throughout the execution of the program.
  • Essentially the values which variables hold are constants.
  • Consider ‘num’ variable holding the value 3 as shown in the following figure:
  • Here, 3 is the constant. The value ‘3’ never changes. ‘num’ is the variable that holds the constant 3. The constant in ‘num’ may change over the course of the execution; hence, it is a variable.

Types of constants:

  • Primary constants:
    • Integer constant
    • Real constant
    • Character constant
    • Void
  • Secondary / Derived constants:
    • Array: Collection of similar primary constants under a single name.
    • Pointer: Constant which is the location of another constant
    • Structure: Collection of similar or dissimilar primary constants.
    • Union: Memory overlapping collection of similar or dissimilar primary / secondary constants.
  • User-defined:
    • Enum: It is mainly used to assign names to integral constants, the names make a program easy to read and maintain.
    • Typedef: can be used to give a type a new name.

We shall currently focus on primary constants for now and pick up the 2ndand 3rd types in upcoming modules.

Primary constants:

Integer constant;

  • An integer constant is a positive or negative non-decimal integer value.
  • Must not have decimal point.
  • If no sign is present, it is assumed positive.
  • No commas or blanks are to be present within an integer constant.
  • There are classifications of integer constants based on their base value. They are listed as follows:
    • Decimal Integer Constants
      • Integer constants consist of a set of digits, through 9, preceded by an optional – or + sign.
      • Example of valid decimal integer constants 341, -341, 0, 8972
    • Octal Integer Constants
      • Integer constants consisting of a sequence of digits from the set 0 through 7 starting with 0 are said to be octal integer constants.
      • Example of valid octal integer constants 010, 0424, 0, 0540
    • Hexadecimal Integer Constants
      • Hexadecimal integer constants are integer constants having a sequence of digits preceded by 0x or 0X. They may also include alphabets from A to F representing numbers 10 to 15.
      • Example of valid hexadecimal integer constants 0xD, 0X8d, 0X, 0xbD

Real constant

  • Real constants also called Floating Point constants, are numbers with the addition of decimal values to integers.
  • The real constants could be written in two forms—Fractional form and Exponential form.
  • Following rules must be observed while constructing real constants expressed in the fractional form:
    • A real constant must have at least a one-digit, decimal point. (Yes! ‘5.’ is a valid floating-point number!)
    • It could be either positive or negative.
    • If no sign is mentioned, by default, it is positive.
    • No commas or blanks are allowed within a real constant.
  • In the exponential form, the real constant is represented in two parts. The part appearing before ‘e’ is called mantissa, whereas the part following ‘e’ is called the exponent. Thus 0.000342 can be written in exponential form as 3.42e-4 (which in normal arithmetic means 3.42 x 10 -4).
  • Following rules must be observed while constructing real constants expressed in exponential form:
    • The mantissa part and the exponential part should be separated by the letter e or E.
    • The mantissa part may have a positive or negative sign.
    • The default sign of the mantissa part is positive.
    • The exponent must have at least one digit, which must be a positive or negative integer. The default sign is positive.

Character constant:

  • A character constant is a single alphabet, a single digit or a single special symbol enclosed within single inverted commas.
  • Both the inverted commas should point to the left. For example, ’A’ is a valid character constant whereas ‘A’ is not.
  • Ex.: ‘A’, ‘I’, ‘5’, ‘=’

Void

  • A void is an empty data type that has no value. We use void data type in functions when we don’t want to return any value to the calling function and we also use it in pointers to create a generic pointer. We shall see functions and pointers in upcoming modules.

Operators:

  • Operators in C are special symbols used to perform the functions. The data items on which the operators are applied are known as operands. Operators are applied between the operands. Depending on the number of operands, operators are classified as follows:

Unary Operator

  • A unary operator is an operator applied to a single operand. For example: increment operator (++), decrement operator (–), sizeof, (type)*, address-of operator (&).

Binary Operator

  • The binary operator is an operator applied between two operands. The following is the list of the binary operators:
  • Arithmetic Operators (+, -, *, /, %)
    • Surprisingly, there are no operator for exponentiation, but there is a function named pow() which we shall see later.
  • Relational Operators (<, >, <=, >=, ==, !=)
  • Shift Operators (<<, >>)
  • Logical Operators (!, &&, ||)  Logical ‘!’ operator is unary
  • Bitwise Operators (~, &, |, ^)  Bitwise ‘!’ is unary
  • Conditional Operators / Ternary operator
  • Assignment Operator (=)

Special symbols:

  • The following are the special symbols in C: ~ ‘ ! @ # % ^ & * ( ) _ – + = | \ { } [ ] : ; ” ‘ < > , . ? / $
  • They have special meaning and are used to achieve concepts through length and breadth of C. (They will reveal themselves along with their purpose as and when the topics are rolled out)

Qualifiers:

There are several qualifiers such as short, long, signed, unsigned that can be applied to the primary data types. The possible qualifiers for the basic type are shown in the table:

Data typeQualifier
Charsigned, unsigned
intshort, long, signed, unsigned
floatNo qualifier
doublelong
voidNo qualifier
Data types and their corresponding qualifiers

Each compiler is free to choose the appropriate size for its hardware with restrictions that short and int are at least 16 bits and long is at least 32 bits and the size of short < int < long.

Char is the same as an integer. If a char is stored in a variable, it stores its corresponding ASCII value which ranges from (0, 255). Hence, qualifier signed or unsigned may be applied to char or any integer.

Unsigned numbers are always positive or zero and obey the laws of arithmetic modulo 2n, where n is the number of bits in the type. E.g., char is 8 bits so unsigned char variables have values between 0 and 28 i.e., values between 0 and 255

Ranges of various data types in C
  • If we do not specify either signed or unsigned, most compilers will assume the type to be signed.
  • So, signed int x;
    can be written as: int x;
  • short and long can be used alone as type specifiers.
    short = short int
    long = long int
  • short int x;
    can be written as short x;

First C program!

It’s a tradition that the first program should be “Hello world!”. Following the same tradition, let’s step into our first program.

#include <stdio.h>
int main()
{
    printf("Hello World");
    return 0;
}

Let us now understand this program in detail.

Structure of a C Program

  • Each instruction in a C program is written as a separate statement.
  • The statements in a program must appear in the same order in which we wish them to be executed.
  • Blank spaces may be inserted between two words to improve the readability of the statement.
  • All statements should be in lower case letters.
  • C has no specific rules for the position at which a statement is to be written in a given line. That’s why it is often called a free-form language. But it is advisable to indent with a tab for every progressive nested block.
  • Every C statement must end with a semicolon (;). Thus; acts as a statement terminator.

Comments in a C Program

  • Comments are used in a C program to clarify either the purpose of the program or the purpose of some statements in the program.
  • It is not considered by the compiler while creating an executable file. It is solely for the reader of the program.
  • It is a good practice to begin a program with a comment indicating the purpose of the program, its author and the date on which the program was written.
  • Here are a few things that you must remember while writing comments in a C program:
    • Comment about the program should be enclosed within /* */
    • When the purpose of a statement(s) isn’t obvious, it is worthwhile mentioning the purpose using a comment. For example:
/* formula for simple interest */
si = p * n * r / 100 ;
  • Any number of comments can be written at any place in the program. For example, a comment can be written before the statement, after the statement or within the statement as shown below.
/* formula */
si = p * n * r / 100;
si = p * n * r / 100 ;
/* formula */
si = p * n * r / /* formula */ 100 ;
  • The normal language rules do not apply to text written within /* .. */. Thus, we can type this text in a small case, capital or a combination. This is because the comments are solely given for the understanding of the programmer or the fellow programmers and are completely ignored by the compiler.
  • Comments cannot be nested. This means one comment cannot be written inside another comment. The following example is invalid.
/*Cal of SI /* Author: QM date: 25/06/2016 */ */
  • A comment can be split over more than one line, as in,
/* This comment
has three lines
in it */

Such a comment is often called a multi-line comment.

  • ANSI C permits single-line comments to be written as follows:
// Calculation of simple interest
// Formula

But note that ‘//’ comments a single line starting from where it was written.

Si = p * t * r / 100 // simple interest
// Formula

So, in this example, the comments are considered from ‘//’ and everything before it till the margin is considered for compilation.

What is main()?

  • main() is a function. A function is nothing but a container for a set of statements.
  • In a C program, there can be multiple functions.
  • The execution of statements starts with the main() function
  • All statements that belong to main() are enclosed within a pair of braces {} as shown below.
int main()
{
    statement 1;
    statement 2;
    statement 3;
}
  • A function may or may not return a value. The main() function always returns an integer value, hence there is an int before main().
  • The integer value that we are returning is 0. 0 indicates success. If for any reason the statements in main() fail to do their intended work we can return a non-zero number from main(). This would indicate failure.
  • As there could be many reasons for failure, we could place a return code standard concerning the program. One of such codes could be returning -1 for entering a floating-point value instead of an integer value.
  • Some compilers like Turbo C/C++ even permit us to return nothing from main(). In such a case we should precede it with the keyword void. But this is a non-standard way of writing the main() function.

Variables and their Usage

Consider the following C program:

/* Calculation of simple interest */
/* Author: QM Date: 25/06/2016 */
# include <stdio.h>
int main()
{
    int p, n ;
    float r, si ;
    p = 1000 ;
    n = 3 ;
    r = 8.5 ;
    /* formula for simple interest */
    si = p * n * r / 100 ;
    printf ( "%f\n" , si ) ;
    return 0 ;
}

Any variable used in the program must be declared before using it. For example,

int p, n ; /* declaration */
float r, si ; /* declaration */
si = p * n * r / 100 ; /* usage */

In the statement,

si = p * n * r / 100 ;

* and / are the arithmetic operators.

printf() and its Purpose

printf()is used to display contents onto the user screen. Concerning the above program, we can note the following points:

  • Once the value of si is calculated it needs to be displayed on the screen. We have used printf() to do so.
  • For us to be able to use the printf() function, it is necessary to use #include at the beginning of the program. #include is a pre-processor directive. It has to be used every time we use printf(). The purpose and definition of pre-processor directives will be covered in later modules.
  • Syntax of printf( ) function is as follows:
printf ( "<format specifier>",<list of variable> ) ; 
  • The following table is a reference for data type and their corresponding format specifiers:
Format SpecifierType
%cCharacter
%dSigned integer
%e or %EScientific notation of floats
%fFloat values
%g or %GSimilar to %e or %E
%hiSigned integer (short)
%huUnsigned Integer (short)
%iUnsigned Integer
%l or %ld or %liLong
%lfDouble
%LfLong Double
%luUnsigned int or unsigned long
%lli or %lldLong long
%lluUnsigned long long
%oOctal representation
%pPointer
%sString
%uUnsigned int
%x or %XHexadecimal representation
%nPrints nothing
%%Prints % character
Format Specifiers

In addition to format specifiers like %f, %d and %c, the format string may also contain any other characters. These characters are printed as they are when printf() is executed.

  • Given below are some more examples of usage of printf() function:
printf ( "%f", si ) ;
printf ( "%d %d %f %f", p, n, r, si ) ;
printf ( "Simple interest = Rs. %f", si ) ;
printf ( "Principal = %d \nRate = %f", p, r ) ;

The output of the last statement would look like this…

Principal = 1000
Rate = 8.500000

‘\n’ is called the newline character and it takes the cursor to the next line. Therefore, you get the output split over two lines. ‘\n’ is one of the several Escape Sequences available in C.
Here are some more escape sequences and we shall see them in detail in upcoming modules.

  • An expression is nothing but a valid combination of constants, variables and operators. printf() can also print the result of an expression. Examples of valid expressions are as follows:
3
3 + 2
c
a + b * c – d

The results of these expressions can be printed as shown below.

printf( "%d %d %d %d", 3, 3 + 2, c, a + b * c – d );

Note that using expressions in printf() is optional. We can print a statement without any expression just as well.

Receiving Input

  • In the program we saw earlier, the values of p, n and r were hard-coded.
  • Often C programs are required to read input from the user to make the program more resourceful and portable.
  • To achieve this motive, scanf() is one of the programs we use. It reads constants from the user and stores them into the address of a mentioned variable.
  • It can be considered as a complementary to printf() function. • The syntax of scanf() is as follows:
scanf("<format -specifier>", &<variable_name>); 

This is illustrated in the program given below.

/* Calculation of simple interest */
/* Author gekay Date 25/06/2016 */
# include <stdio.h>
int main( )
{
    int p, n ;
    float r, si ;
    printf ( "Enter values of p, n, r" ) ;
    scanf ( "%d %d %f", &p, &n, &r ) ;
    si = p * n * r / 100 ;
    printf ( "%f\n" , si ) ;
    return 0 ;
}
  • Note the use of the ampersand (&) before the variables in the scanf() function is a must.
  • ‘&’ is an ‘Address of’ operator. It gives the location number (address) used by the variable in memory.
  • When we say &a, we are telling scanf() at which memory location should it store the value supplied by the user from the keyboard.
  • Note that a blank, a tab or a new line must separate the values supplied to scanf().
  • A blank is created using a spacebar, a tab using the Tab key and a new line using the Enter key. This is shown below. Ex.: The three values are separated by blank:
    1000 5 15.5

Ex.: The three values separated by tab:
1000 5 15.5
Ex.: The three values separated by a newline:
1000
5
15.5
The following is another example stitching all the theories discussed:

/* Just for fun. Author: QM */
# include <stdio.h>
int main( )
{
    int num ;
    printf ( "Enter a number" ) ;
    scanf ( "%d", &num ) ;
    printf ( "Now I’m letting you on a secret...\n" ) ;
    printf ( "You have just entered the number %d\n", 
    num ) ;
    return 0 ;
}

Programming challenges

  • Shradha has a basic salary of n rupees. 10% of the basic salary is her house rent allowance and 40% of the basic salary is her dearness allowance. Print the gross salary she gets.
    • INPUT:
      • First line: An integer n
    • Output:
      • Her gross salary

You cannot copy content of this page