The C Programming Language - Functions and Program Structure

Edusagar - notes - Functions and Program Structure
  • C has been designed to make functions efficient and easy to use. C programs generally consists of many small functions rather than a few big ones.

  • Specifying the proper types in function declarations allows the compiler to check whether the arguments passed through function call are correct.

    Consider a case where a function is supposed to return float and is defined in some other source file than where it is called. In this case, meaningless results might occur if the declaration of that function is not made available in the file where caller resides. In absence of a function prototype, the return type would be assumed as int and we will start getting unexpected results without any compiler error.

    Moreover, if arguments are missing from the function prototype, then nothing is assumed about the parameters and no compiler checks will happen.

    So, rule is to explicitly declare arguments and return type if function has any. If there are no arguments, use 'void' to denote the same.

  • int strindex(char s[], char t[])
    {
    	int i,j,k;
    	
    	for (i=0; s[i] != '\0'; i++) {
    		for (j=i, k=0; t[k] != '\0' && s[j] == t[k]; k++, j++)
    			;
    		if (k > 0 && t[k] == '\0') {
    			return i;
    		}
    	}
    	
    	return -1;
    }
  • dummy() {} is a dummy function which doesn't do anything and doesn't return anything. If no return type is specified, int is assumed. Such dummy functions can act as a placeholder during software developement.

  • It is not illegal, but probably a sign of trouble, if function is returning value from one place and no value from other. If a function fails to return a value, it is treated as garbage.

  • main() can also return values, which can be used by the environment that called it.

  • #include <ctype.h>
    double atof(char s[])
    {
    	double val, power;
    	int i, sign;
    	
    	for (i=0; isspace(s[i]); i++) 
    		;
    	
    	sign = (s[i] == '-') ? -1 : 1;
    	if (s[i] == '+' || s[i] == '-')
    		i++;
    	
    	for (val = 0.0; isdigit(s[i]); i++) {
    		val = val*10 + (s[i] - '0');
    	}
    	
    	if (s[i] == '.') {
    		i++;
    	}
    	
    	for (power=1.0; isdigit(s[i]); i++) {
    		val = val*10 + (s[i] - '0');
    		power *= 10;
    	}
    	
    	return sign * val / power;
    }
  • Functions themselves are external variables, since one function can not be defined inside another function.

  • #define BUFSIZE 100
    char buf[BUFSIZE];
    int bufp = 0;
    
    int getch(void) /* get a character(possibly pushed back) */
    {
    	return (bufp > 0) ? buf[--bufp] : getchar();
    }
    
    void ungetch(int c) /*push character back on stack */
    {
    	if (bufp >= BUFSIZE)
    		printf("Error: cant push back the character");
    	else
    		buf[bufp++] = c;
    }
    
  • Scope of a name is the part of program within which the same name can be used. For automatic variables it is the function in which it is defined, the same is true for the function parameters.

    External variable's scope starts from the point in file where it is declared till the end of the file. If external variable is to be used before it is declared or if it is declared in a different source file, then an "extern" declaration is neccessary.


    extern int sp;
    extern double val[];

    Note that extern declaration doesn't allocate separate storage for the variable i.e. it is not a definition(which can be only one for a variable) and just a declaration. Array size is neccessary with definition but not during an extern declaration. Moreover, initialization can only be done while defining an external variable.

  • Static Declaration:

    static declaration when applied to extern variable, it limits the scope of that variable to the current file. In other words, by making a variable or a function static, you are free to reuse the same name of the variable or function in other source files without any conflict. In some way, it helps in encapsulating data within one file where all variables/functions can only be referred within that file itself. If you want to access some functions from other files, then you need to call a specific non-static (public) function to do the same(thus hiding away the implementation from the caller routine).

    Even a local variable can be declared as static, in which case its value will persist throughout the program execution - even between different function calls of the same function. But still the scope will be limited to the function in which it is defined.

  • Register Declaration:

    A register declaration tells the compiler that the variable will be heavily used and it would make program faster if it is copied to internal hardware register instead of stack. Such declaration can only be applied to local variables or function arguments. However, compilers are free to ignore the request since there are very limited number of hardware registers.

    There are many restrictions over the usage of register variables varying from machine to machine. One such restriction is that you cannot get the address of the register variable - even if compiler has not allocated it in the hardware register.

  • Declarations of variables (including initializations) may follow the left brace that introduces any compound statement, not just the one that begins a function. Variables declared in this way hide any identically named variables in outer blocks and even extern variables, and remain in existence until the matching right brace.


    int x;
    int y;
    
    f(double x) 
    {
    	double y;
    }

    Here, both x and y inside function refer to the local double variables instead of the global int ones. To avoid such confusion, it is best to avoid using variables names that conceals names in an outer scope.

  • int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }


    When the size of the array is omitted, the compiler will compute the length by counting the initializers, of which there are 12 in this case. If there are fewer initializers for an array than the specified size, the others will be zero for external, static and automatic variables. It is an error to have too many initializers. There is no way to specify repetition of an initializer, nor to initialize an element in the middle of an array without supplying all the preceding values as well.

  • Character arrays are a special case of initialization; a string may be used instead of the braces and commas notation:

    char pattern = "ould";

    is a shorthand for the longer but equivalent:

    char pattern[] = { 'o', 'u', 'l', 'd', '\0' };

    In this case, the array size is five (four characters plus the terminating '\0').

  • /*print n in decimal format*/
    void printd(int n) {
    	if (n<0) {
    		putchar('-');
    		n = -n;
    	}
    	
    	if (n/10) {
    		printd(n/10);
    	}
    	
    	putchar(n%10 + '0');
    }
  • When a function calls itself recursively, each invocation gets a fresh set of all automatic variables, independent of the previous set.

  • Quicksort

    void qsort(int v[], int left, int right)
    {
    	int i, last;
    	void swap(int v[], int i, int j); /* function prototype fop swap function */
    	
    	if (left >= right) 
    		return;
    		
    	swap(v, left, (left+right)/2);
    	last = left;
    	
    	for(i = left+1; i <= right; i++) {
    		if (v[i] < v[left]) 
    			swap(v, ++last, i);
    	}
    	
    	swap(v, left, last);
    	qsort(v, left, last-1);
    	qsort(v, last+1, right);
    }
    
    void swap(int v[], int i, int j) 
    {
    	int temp;
    	
    	temp = v[i];
    	v[i] = v[j];
    	v[j] = temp;
    }
  • #include

    The C preprocessor takes care of #includes and #defines before doing anything else.

    File inclusion makes it easy to handle collections of #defines and declarations (among other things). Any source line of the form

    #include "filename"

    or

    #include <filename>

    is replaced by the contents of the file filename. If the filename is quoted, searching for the file typically begins where the source program was found; if it is not found there, or if the name is enclosed in < and >, searching follows an implementation-defined rule to find the file. An included file may itself contain #include lines.

  • #define

    Scope of a name defined with #define is from its point of definition to the end of the source file. A definition may use previously defined macros. Macro substitution only works for tokens and doesnt work in quoted strings. e.g. if YES is a #define macro, there would be no substitution in printf("YES") or YESMAN.

    Macros can take arguments too, e.g.

    #define MAX(A,B) ((A) > (B) ? (A) : (B))

  • Following is an example of how macros would be expanded inline.

    x = MAX(p+q, r+s);

    would become:

    x = ((p+q) > (r+s) ? (p+q) : (r+s));

  • Issues with Macros

    Macros can induce very subtle bugs in program. For example,

    x = MAX(i++, j++)

    Here, the larger element will be incremented twice, which is definitely undesirable side-effect of using macros.

    Similarly, following macro would produce wrong results for square(z+1)

    #define square(x) x*x

    Still, macros are needed where we want to avoid the run-time overhead of calling a function.

  • Macros can be undefined with #undef. This is useful if we want to use a function in the same scope with the same name as of macro.

  • If a parameter name is preceded by # in the replacement text, the combination will be expanded into a quoted string with the parameter replaced by the actual argument.

    #define dprint(expr) printf(#expr " = %g\n", expr)

    If we invoke dprint as dprint(x/y), this would produce the following statement:

    printf("x/y" " = %g\n", x/y);

    which eventually is equivalent to following statement after the two strings are concatenated:

    printf("x/y = %g\n", x/y);

    The above macro can be used to print debug information for an expression with its value and expression itself converted to string.

  • The preprocessor operator ## provides a way to concatenate actual arguments during macro expansion. If a parameter in the replacement text is adjacent to a ##, the parameter is replaced by the actual argument, the ## and surrounding white space are removed, and the result is re-scanned. For example, the macro paste concatenates its two arguments:

    #define paste(front, back) front ## back

    so paste(name, 1) creates the token name1.

  • Using conditional inclusions we can include code selectively based on the conditions during compilation.

    #if line evaluates a constant integer expression(which may not include sizeof, casts or enum constants). If condition is non-zero, subsequent lines until an #endif or #elif or #else are included. For example, to make sure that header file hdr.h is included only once, we add the following lines in hdr.h

    #if !defined(HDR)
    #define HDR
    
    /* contents of hdr.h */
    
    #endif

    Now, if 1.c includes hdr.h, it will define the name HDR; subsequent inclusion will always find HDR to be defined and thus skip to the last line containing #endif.

  • This sequence tests the name SYSTEM to decide which version of a header to include:

    #if SYSTEM == SYSV
    	#define HDR "sysv.h"
    #elif SYSTEM == BSD
    	#define HDR "bsd.h"
    #elif SYSTEM == MSDOS
    	#define HDR "msdos.h"
    #else
    	#define HDR "default.h"
    #endif
    #include HDR
comments powered by Disqus