Go to the first, previous, next, last section, table of contents.

Data Objects

Variables and constants are the basic data objects in a C program. Declarations list the variables to be used, state what type they are, and may set their initial value.

Variables

Variable names are case-sensitive. The underscore character is allowed and is often used to enhance the readability of long variable names. C keywords like if, while, etc. may not be used as variable names.

Global variables and functions may not have the same name. In addition, local variables named the same as globals or functions prevent the use of that function within the scope of the local variable.

Declaration

In C, variables can be declared at the top level (outside of any curly braces) or at the start of each block (a functional unit of code surrounded by curly braces). In general, a variable declaration is of the form:

<type> <variable name>;
or 
<type> <variable name>=<initialization data>;

<type> can be int, long, float, char, or struct <struct name>, and determines the primary type of the variable declared. This form changes somewhat when dealing with pointer and array declarations, which are explained in a later section, but in general this is the way you declare variables.

Local and Global Scopes

If a variable is declared within a function, or as an argument to a function, its binding is local, meaning that the variable has existence only within that function definition.

If a variable is declared outside of a function, it is a global variable. It is defined for all functions, including functions which are defined in files other than the one in which the global variable was declared.

Variable Initialization

Local and global variables can be initialized to a value when they are declared. If no initialization value is given, the variable is initialized to zero.

All global variable declarations must be initialized to constant values. Local variables may be initialized to the value of arbitrary expressions including any globals, function calls, function arguments, or locals which have already been initialized.

Here is a small example of how initialized declarations are used.

int i=50;      /* declare i as global integer -- initial value 50 */
long j=100L;   /* declare j as global long -- initial value 100 */ 
int foo()
{
   int x;       /* declare x as local integer with initial value 0 */
   long y=j;    /* declare y as local integer with initial value j */
}

Local variables are initialized whenever the function containing them runs.

Global variables are initialized whenever a reset condition occurs. Reset conditions occur when:

  1. Code is downloaded;
  2. The main() procedure is run;
  3. System hardware reset occurs.

Persistent Global Variables

A special persistent form of global variable, has been implemented for IC . A persistent global may be initialized just like any other global, but its value is only initialized when the code is downloaded and not on any other reset conditions. If no initialization information is included for a persistent its value will be initialized to zero on download, but left unchanged on all other reset conditions.

To make a persistent global variable, prefix the type specifier with the key word persistent. For example, the statement

persistent int i=500;

creates a global integer called i with the initial value 500.

Persistent variables keep their state when the board is turned off and on, when main is run, and when system reset occurs. Persistent variables will lose their state when code is downloaded as a result of loading or unloading a file. However, it is possible to read the values of your persistents in IC if you are still running the same IC session from which the code was downloaded. In this manner you could read the final values of calibration persistents, for example, and modify the initial values given to those persistents appropriately.

Persistent variables were created with two applications in mind:

Constants

Integer Constants

Integers constants may be defined in decimal integer format (e.g., 4053 or -1), hexadecimal format using the "0x" prefix (e.g., 0x1fff), and a non-standard but useful binary format using the "0b" prefix (e.g., 0b1001001). Octal constants using the zero prefix are not supported.

Long Integer Constants

Long integer constants are created by appending the suffix "l" or "L" (upper- or lower-case alphabetic L) to a decimal integer. For example, 0L is the long zero. Either the upper or lower-case "L" may be used, but upper-case is the convention for readability.

Floating Point Constants

Floating point numbers may use exponential notation (e.g., " 10e3" or "10E3") or may contain a decimal period. For example, the floating point zero can be given as "0.", " 0.0", or "0E1", but not as just "0". Since the 6811 has no floating point hardware, floating point operations are much slower than integer operations, and should be used sparingly.

Characters and String Constants

Quoted characters return their ASCII value (e.g., 'x').

Character string constants are defined with quotation marks, e.g., "This is a character string.".

NULL

The special constant NULL has the value of zero and can be assigned to and compared to pointer or array variables (which will be described in later sections). In general, you cannot convert other constants to be of a pointer type, so there are many times when NULL can be useful.

For example, in order to check if a pointer has been initialized you could compare its value to NULL and not try to access its contents if it was NULL. Also, if you had a defined a linked list type consisting of a value and a pointer to the next element, you could look for the end of the list by comparing the next pointer to NULL.

Data Types

IC supports the following data types:

16-bit Integers

16-bit integers are signified by the type indicator int. They are signed integers, and may be valued from -32,768 to +32,767 decimal.

32-bit Integers

32-bit integers are signified by the type indicator long. They are signed integers, and may be valued from -2,147,483,648 to +2,147,483,647 decimal.

32-bit Floating Point Numbers

Floating point numbers are signified by the type indicator float. They have approximately seven decimal digits of precision and are valued from about 10^-38 to 10^38.

8-bit Characters

Characters are an 8-bit number signified by the type indicator char. A character's value typically represents a printable symbol using the standard ASCII character code.

Arrays of characters (character strings) are supported, but individual characters are not.

Pointers

IC pointers are 16-bit numbers which represent locations in memory. Values in memory can be manipulated by calculating, passing and dereferencing pointers representing the location where the information is stored.

Arrays

Arrays are used to store homogenous lists of data (meaning that all the elements of an array have the same type). Every array has a length which is determined at the time the array is declared. The data stored in the elements of an array can be set and retrieved in the same manner that other variables can be.

Structures

Structures are used to store non-homogenous but related sets of data. Elements of a structure are referenced by name instead of number and may be of any supported type. Structures are useful for organizing related data into a coherent format, reducing the number of arguments passed to functions, increasing the effective number of values which can be returned by functions, and creating complex data representations such as directed graphs and linked lists.

Pointers

The address where a value is stored in memory is known as the pointer to that value. It is often useful to deal with pointers to objects, but great care must be taken to insure that the pointers used at any point in your code really do point to valid objects in memory. Attempts to refer to invalid memory locations could corrupt your memory. Most computing environments that you are probably used to return helpful messages like `Segmentation Violation' or `Bus Error' on attempts to access illegal memory. However, no such safety net exists in the 6.270 system and invalid pointer dereferencing is very likely to go undetected and cause serious damage to your data, your program, or even the pcode interpreter.

Pointer Safety

In past versions of IC, you could not return pointers from functions or have arrays of pointers. In order to facilitate the use of structures, these features have been added to the current version. With this change, the number of opportunities to misuse pointers have increased. However, if you follow a few simple precautions you should do fine.

First, you should always check that the value of a pointer is not equal to NULL (a special zero pointer) before you try to access it. Variables which are declared to be pointers are initialized to NULL, so many uninitialized values could be caught this way.

Second, you should never use the pointer to a local variable in a manner which could cause it to be accessed after the function in which it was declared terminates. When a function terminates the space where its values were being stored is recycled. Therefore not only may dereferencing such pointers return incorrect values, but assigning to those addresses could lead to serious data corruption. A good way to prevent this is to never return the address of a local variable from the function which declares it and never store those pointers in an object which will live longer than the function itself (a global pointer, array, or struct). Global variables and variables local to main will not move once declared and their pointers can be considered to be secure.

The type checking done by ic will help prevent many mishaps, but it will not catch all errors, so be careful.

Pointer Declaration and Use

A variable which is a pointer to an object of a given type is declared in the same manner as a regular object of that type, but with an extra $*$ in front of the variable name.

The value stored at the location the pointer refers to is accessed by using the $*$ operator before the expression which calculates the pointer. This process is known as dereferencing.

The address of a variable is calculated by using the & operator before that variable, array element, or structure element reference.

There are two main differences between how you would use a variable to be a given type and a variable declared as a pointer to that type.

For the following explanation, consider X and Xptr as defined as follows:

long X;
long *Xptr;

So, for example, consider what would happen if the first two statements after X and Xptr were declared were the following:

X=50L;
*Xptr=50L;

The first statement is valid: it sets the value of X to 50L. The second statement would be valid if Xptr had been properly initialized, but in this case it is not. Therefore, this statement would corrupt memory.

Here is a sequence of commands you could try which illustrate how pointers and the * and & operators are used. It also shows that once a pointer has been set to point at a place in memory, references to it actually share the same memory as the object it points to:

X=50L;		/* set the memory allocated for X to the value 50 */
Xptr=&X;	/* set Xptr to point to X */
*Xptr;		/* see that the value pointed at by Xptr is 50 */
X=100L;		/* set X to the value 100 */
*Xptr;		/* see that the value pointed at by Xptr changed to 100 */
*Xptr=200L;	/* set the value pointed at by Xptr to 200 */
X;		/* see that the value in X changed to 200 */

Passing Pointers as Arguments

Pointers can be passed to functions and functions can change the values of the variables that are pointed at. This is termed call-by-reference; a reference, or pointer, to a variable is given to the function that is being called. This is in contrast to call-by-value, the standard way that functions are called, in which the value of a variable is given the to function being called.

The following example defines an average_sensor function which takes a port number and a pointer to an integer variable. The function will average the sensor and store the result in the variable pointed at by result.

Function arguments are declared to be pointers by prepending a star to the argument name, just as is done for other variable declarations.

void average_sensor(int port, int *result)
{
  int sum= 0;
  int i;

  for (i= 0; i< 10; i++) sum += analog(port);

  *result=  sum/10;
}

Notice that the function itself is declared as a void. It does not need to return anything, because it instead stores its answer in the pointer variable that is passed to it.

The pointer variable is used in the last line of the function. In this statement, the answer sum/10 is stored at the location pointed at by result. Notice that the asterisk is used to get assign a value to the location pointed by result.

Returning Pointers from Functions

Pointers can also be returned from functions. Functions are defined to return pointers by preceeding the name of the function with a star, just like any other type of pointer declaration.

int right,left;

int *dirptr(int dir)
{
   if(dir==0) {
      return(&right);
   }
   if(dir==1) {
      return(&left);
   }
   return(NULL);
}

The function dirptr returns a pointer to the global right when its argument dir is 0, a pointer to left when its argument is 1, and NULL if its argument is other than 0 or 1.

Arrays

IC supports arrays of characters, integers, long integers, floating-point numbers, structures, pointers, and array pointers (multi-dimensional arrays). While unlike regular C arrays in a number of respects, they can be used in a similar manner. The main reasons that arrays are useful are that they allow you to allocate space for many instances of a given type, send an arbitrary number of values to functions, and iterate over a set of values.

Arrays in IC are different and incompatible with arrays in other versions of C. This incompatibility is caused by the fact that references to ic arrays are checked to insure that the reference is truly within the bounds of that array. In order to accomplish this checking in the general case, it is necessary that the size of the array be stored with the contents of the array. It is important to remember that an array of a given type and a pointer to the same type are incompatible types in IC, whereas they are largely interchangeable in regular C.

Declaring and Initializing Arrays

Arrays are declared using square brackets. The following statement declares an array of ten integers:

int foo[10];

In this array, elements are numbered from 0 to 9. Elements are accessed by enclosing the index number within square brackets: foo[4] denotes the fifth element of the array foo (since counting begins at zero).

Arrays are initialized by default to contain all zero values. Arrays may also be initialized at declaration by specifying the array elements, separated by commas, within curly braces. If no size value is specified within the square brackets when the array is declared but initialization information is given, the size of the array is determined by the number of elements given in the declaration. For example,

int foo[]= {0, 4, 5, -8,  17, 301};

creates an array of six integers, with foo[0] equalling 0, foo[1] equalling 4, etc.

If a size is specified and initialization data is given, the length of the initialization data may not exceed the specified length of the array or an error results. If, on the other hand, you specify the size and provide fewer initialization elements than the total length of the array, the remaining elements are padded with zeros.

Character arrays are typically text strings. There is a special syntax for initializing arrays of characters. The character values of the array are enclosed in quotation marks:

char string[]= "Hello there";

This form creates a character array called string with the ASCII values of the specified characters. In addition, the character array is terminated by a zero. Because of this zero-termination, the character array can be treated as a string for purposes of printing (for example). Character arrays can be initialized using the curly braces syntax, but they will not be automatically null-terminated in that case. In general, printing of character arrays that are not null-terminated will cause problems.

Passing Arrays as Arguments

When an array is passed to a function as an argument, the array's pointer is actually passed, rather than the elements of the array. If the function modifies the array values, the array will be modified, since there is only one copy of the array in memory.

In normal C, there are two ways of declaring an array argument: as an array or as a pointer to the type of the array's elements. In IC array pointers are incompatible with pointers to the elements of an array so such arguments can only be declared as arrays.

As an example, the following function takes an index and an array, and returns the array element specified by the index:

int retrieve_element(int index, int array[])
{
    return array[index];
}

Notice the use of the square brackets to declare the argument array as a pointer to an array of integers.

When passing an array variable to a function, you are actually passing the value of the array pointer itself and not one of its elements, so no square brackets are used.

void foo()
{
    int array[10];

    retrieve_element(3, array);
}

Multi-dimensional Arrays

A two-dimensional array is just like a single dimensional array whose elements are one-dimensional arrays. Declaration of a two-dimensional array is as follows:

int k[2][3];

The number in the first set of brackets is the number of 1-D arrays of int. The number in the second set of brackets is the length of each of the 1-D arrays of int. In this example, k is an array containing two 1-D arrays; k[0] is a 1-D array of int of length 3; k[0][1] is an int. Arrays of with any number of dimensions can be generalized from this example by adding more brackets in the declaration.

Determining the size of Arrays at Runtime

An advantage of the way IC deals with arrays is that you can determine the size of arrays at runtime. This allows you to do size checking on an array if you are uncertian of its dimensions and possibly prevent your program from crashing.

Since _array_size is not a standard C feature, code written using this primitive will only be able to be compiled with IC .

The _array_size primitive returns the size of the array given to it regardless of the dimension or type of the array. Here is an example of declarations and interaction with the _array_size primitive (don't worry about the multi- dimensional arrays, they will be explained next section):

int i[4]={10,20,30};
int j[3][2]={{1,2},{2,4},{15}};
int k[2][2][2];

_array_size(i);		/* returns 4 */
_array_size(j); 	/* returns 3 */
_array_size(j[0]); 	/* returns 2 */
_array_size(k);		/* returns 2 */
_array_size(k[0]);	/* returns 2 */

Structures

Structures are used to store non-homogenous but related sets of data. Elements of a structure are referenced by name instead of number and may be of any supported type. Structures are useful for organizing related data into a coherent format, reducing the number of arguments passed to functions, increasing the effective number of values which can be returned by functions, and creating complex data representations such as directed graphs and linked lists.

The following example shows how to define a structure, declare a variable of structure type, and access its elements.

struct foo {
   int i;
   int j;
};

struct foo f1;

void set_f1(int i,int j)
{
   f1.i=i;
   f1.j=j;
}
void get_f1(int *i,int *j)
{
   *i=f1.i;
   *j=f1.j;
}

The first part is the structure definition. It consists of the keyword struct, followed by the name of the structure (which can be any valid identifier), followed by a list of named elements in curly braces. This definition specifies the structure of the type struct foo.

Once there is a definition of this form, you can use the type struct foo just like any other type. The line struct foo f1; is a global variable declaration which declares the variable f1 to be of type struct foo.

The dot operator is used to access the elements of a variable of structure type. In this case, f1.i and f1.j refer to the two elements of f1. You can treat the quantities f1.i and f1.j just as you would treat any variables of type int (the type of the elements was defined in the structure declaration at the top to be int).

Pointers to structure types can also be used, just like pointers to any other type. However, with structures, there is a special short-cut for referring to the elements of the structure pointed to.

struct foo *fptr;

void main()
{
   fptr=&f1;
   fptr->i=10;
   fptr->j=20;
}

In this example, fptr is declared to be a pointer type type struct foo. In main, it is set to point to the global f1 defined above. Then the elements of the structure pointed to by fptr (in this case these are the same as the elements of f1), are set. The arrow operator is used instead of the dot operator because fptr is a pointer to a variable of type struct foo. Note that (*fptr).i would have worked just as well as fptr->i, but it would have been clumsier.

Note that only pointers to structures, not the structures themselves, can be passed to or returned from functions.

Complex Initialization examples

Complex types -- arrays and structures -- may be initialized upon declaration with a sequence of constant values contained within curly braces and separated by commas. Arrays of character may also be initialized with a quoted string of characters.

For initialized declarations of single dimensional arrays, the length can be left blank and a suitable length based on the initialization data will be assigned to it. Multi-dimensional arrays must have the size of all dimensions specified when the array is declared. If a length is specified, the initialization data may not overflow that length in any dimension or an error will result. However, the initialization data may be shorter than the specified size and the remaining entries will be initialized to 0.

Following is an example of legal global and local variable initializations:

/* declare many globals of various types */
int i=50;

int *ptr=NULL;

float farr[3]={ 1.2, 3.6, 7.4 };
int tarr[2][4]={ { 1, 2, 3, 4 }, { 2, 4, 6, 8} };

char c[]="Hi there how are you?";
char carr[5][10]={"Hi","there","how","are","you"};

struct bar {
    int i;
    int *p;
    long j;} b={5, NULL, 10L};
struct bar barr[2] = { { 1, NULL, 2L }, { 3 } };

/* declare locals of various types */
int foo()
{
  int x;                 /* create local variable x
                            with initial value 0    */
  int y= tarr[0][2];     /* create local variable y
                            with initial value 3    */
  int *iptr=&i;          /* create a local pointer to integer 
                            which points to the global i */
  int larr[2]={10,20};   /* create a local array larr 
                            with elements 10 and 20 */
  struct bar lb={5,NULL,10L}; /* create a local variable of type 
                            struct bar with i=5 and j=10 */
  char lc[]=carr[2];     /* create a local string lc with 
                            initial value "how" */
  ...
}

Go to the first, previous, next, last section, table of contents.