1. Homepage of Dr. Zoltán Porkoláb
    1. Home
    2. Archive
  2. Teaching
    1. Timetable
    2. Multiparadigm programming (MSc)
    3. C programming (BSc for physicists)
    4. Project tools (BSc)
    5. Bolyai College
    6. C++ (for foreign studenst)
    7. Software technology lab
    8. BSc and MSc thesis
  3. Research
    1. Templight
    2. CodeChecker
    3. CodeCompass
    4. Projects
    5. Publications (up to 2011)
    6. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

The C programming language – Lecture 9.

Pointers, pointer arithmetic, arrays

Pointers

A pointer is a value that refers to a memory location, e.g. an address of a variable. The difference between C and many other languages that C pointers can refer not only head memory but also to variables with static and automatic life (global and local variables).

alt text

The type of the pointer is important: it has an effect of how we can manipulate pointers. As pointers can be stored in memory, there are pointers to pointer, etc. Generic pointers are pointers to void.

int  *pi;   // pointer to int
int **ppi;  // pointer to pointer to int
void *pv;   // generic pointer: "pointer to void"

declare a function with no parameters returning int, a function with no parameters returning pointer to int, and a pointer to function with no parameters returning int. Functions with no intended return value should be declared with void return type. Array return type is not allowed.

For declarations, we can use formal parameter names, but they are only for descriptive purposes:

1
2
int f(int *x, int *y);    // x and y has no role in declaration
int f(int, int);          // equivalent to the above

For historical reasons, parameter specification can be omitted:

1
2
int f(void);      // function with no parameters
int g();          // function with no parameter specification

Here f() and g() are different. We know, that f() has no parameter, but we do not know about the parameters of g() anything. The missing parameter specification is reverse compatible with early C language versions, but it is not supposed to use in new code.

When we want to express that we do not know the exact parameters of a function, like in the case of printf we use the ellipsis notation:

1
2
int printf(const char *format, ...); 
int fprintf(FILE *stream, const char *format, ...); 

The meaning of the ellipsis is that zero or more additional parameters of unknown type. To write such variadic parameter function is tricky (and usually done by the va_ macros from <stdarg.h> header.

Function call

When a function f is called, then the declaration prototype should match to the call:

  • The number of the parameters should be the same.
  • Each argument of the call shell have a type such that its value may be assigned to an object of the type of the corresponding parameter of the prototype.

If the number of parameters does not match with the number of arguments of the call, the behavior is undefined. If the function definition prototype have ellipsis or the argument in the call are incompatible with the correspondent parameter, the behavior is undefined.

If the called function has no prototype then default argument promotions will performed. These are the integer promotions and float to double promotions.

If the called function has a prototype then the arguments are implicitly converted, as if by assignment, to the corresponding parameters. Ellipsis stops the conversions, only the default argument promotions are executed.

1
2
3
double fahr2cels(double);
//...
printf("%f\n", fahr2cels(36));  // ok, 36 is converted to double

Two other conversion is allowed:

  • signedunsigned conversions, when the value is representable in both types.
  • void *char * conversions.

The function call is a sequence point, i.e. first the argument expressions are evaluated in undefined order, only then the function body is starting to execute. (The comma between parameters are different from the comma operator.)

( *t[f1()] ) ( f2(), f3(), f4() );

The functions f1, f2, f3, f4 can be called in any order.

Recursive functions are allowed both directly or indirectly:

1
2
3
4
5
6
7
int factorial(int n)
{
  if ( 1 == n ) 
    return 1;
  else 
    return n * factorial(n-1);
}

When this function is called with an argument smaller then one, infinite recursion happens and the program behavior is undefined.

Parameter passing

Arguments are passed by value, i.e. they are copied from the argument expression to the parameter as it would be a local automatic variable declared in the beginning of the function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void increment(int i)
{
  ++i;
  printf("i in increment() = %d\n",i);
} 
int main()
{
  int i = 0;
  increment(i);
  increment(i);
  printf("i in main() = %d\n",i);
  return 0;    
}
$ gcc -ansi -pedantic -Wall -W f.c 
$ ./a.out 
i in increment() = 1
i in increment() = 1
i in main() = 0

We can simulate the parameter passing by address with pointers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void increment(int *ip)
{
  ++*ip;
  printf("i in increment() = %d\n",*ip);
} 
int main()
{
  int i = 0;
  increment(&i);
  increment(&i);
  printf("i in main() = %d\n",i);
  return 0;    
}
$ gcc -ansi -pedantic -Wall -W f.c 
$ ./a.out 
i in increment() = 1
i in increment() = 2
i in main() = 2

Array parameters are passed as pointers to the first array element.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int f( int *t, int i)
{
  return t[i];
}
int main()
{
  int arr[] = {1, 2, 3, 4};
  printf("%d\n", f(arr,1));
  printf("%d\n", f(&arr[0],1));
  return 0;
}
$ gcc -ansi -std=c99 -Wall -W a.c
$ ./a.out 
2
2

Therefore these parameter declarations are equivalent:

1
2
3
4
5
6
int f( int *t, int i)   { return t[i]; }
int f( int t[], int i)  { return t[i]; }
int f( int t[4], int i) { return t[i]; }

// array limits are not checked: this is not reported as error
int f( int t[4], int i)  { return t[6]; }

Pointers to function

Type of a pointer to function includes the return type and the parameter list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
void increment(int *ip)
{
  ++*ip;
  printf("i in increment() = %d\n",*ip);
} 
int main()
{
  void (*fp)(int *); // pointer to void (int*) 
  void (*gp)();      // pointer to function with unknown parameters
  int i = 0;
  fp = increment;
  gp = fp;
  (*fp)(&i);    // call increment()
    fp (&i);    // simplified notation
    gp (&i);    // call increment(), no check for parameters
  printf("i in main() = %d\n",i);
  return 0;    
}
$ gcc -ansi -pedantic -Wall -W f.c 
$ ./a.out 
i in increment() = 1
i in increment() = 2
i in increment() = 3
i in main() = 3

A pointer to function can be:

  • point to a function by a compatible type.
  • assigned to a similar pointer variable.
  • checked against NULL pointer.
  • indirected in a function call.

Return type

Return values are converted to the return type of the function:

1
2
3
4
5
6
double f(void)
{
  int i;
  // ...
  return i;   // converted to double
}

Parameters of the main function

The main function is declared in one of the following ways:

1
2
3
4
5
int main(void) { /* ... */ }
int main( int argc, char *argv[]) { /* ... */ }

// only when the host environment supports it (mainly UNIX):
int main( int argc, char *argv[], char *envp[]) { /* ... */ }

If argc is defined then argv[argc] is NULL pointer. If argc is greater than zero, then argv[0] is the name of the program, and argv[1] … argv[argc-1] are the program parameters. The argv[i] parameters are pointers to NULL terminated character arrays.

In usual environments, argc is always greater than zero.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(int argc, char *argv[])
{
  printf("name of the program = %s\n", argv[0]);
  for (int i = 1; i < argc; ++i)
    printf("argv[%d] = %s\n", i, argv[i]);
  return 0;    
}
$ gcc -ansi -std=c99 -Wall -W -o mainpars mainpars.c 
$ ./mainpars
name of the program = ./mainpars
$ ./mainpars first second third
name of the program = ./mainpars
argv[1] = first
argv[2] = second
argv[3] = third