C language provides various data types for holding different kinds of values. There are several integral data types, a character data type, floating point data types for holding real numbers and more. In addition you can define your own data types using aggregations of the native types.
The C standard gives detailed specifications on the language; implementations of C must conform to the standard. An “implementation” is basically the combination of the compiler, and the hardware platform. As you will see, the C standard usually does not specify exact sizes for the native types, preferring to set a basic minimum that all conforming implementations of C must meet. Implementations are free to go above these minimum requirements.
This means that different implementations can have different sizes for the same type. For example a C compiler from the fictitious company ABC running on a 64-bit Intel processor might define an int to be 64 bits, while another C compiler from the fictitious XYZ organization for a 16-bit TI embedded processor might define an int to be 16 bits. They both meet the minimum requirements so both are valid.
The next sections look at C types in detail, starting with the smallest.
Boolean type
The _Bool keyword denotes a value that can hold either zero or one. If another numeric or pointer type is assigned to a boolean the value stored is 0 if the numeric or pointer value evaluates to 0, otherwise it is one. Let’s see an example:
#include <stdio.h>
void main()
{
int a = 3944;
long b = -199020930;
double c = 7.534e-10;
double * d = &c;
_Bool ba = a;
_Bool bb = b;
_Bool bc = c;
_Bool bd = d;
_Bool be = ( 1 == 2 );
printf( "ba = %d\n", ba );
printf( "bb = %d\n", bb );
printf( "bc = %d\n", bc );
printf( "bd = %d\n", bd );
printf( "be = %d\n", be );
}
The output of this program is:
ba = 1 bb = 1 bc = 1 bd = 1 be = 0
As you can see, any value that is not 0 is considered 1. On line 14 be is assigned to the value of the equality expression. Recall from the operators tutorial that relational operators return an int with the value 0 if false or 1 if true.
Integer types
C provides several standard integer types, from small magnitude to large magnitude numbers: char, short int, int, long int, long long int. Each type can be signed or unsigned. Signed types can represent positive and negative numbers while unsigned can represent zero and positive numbers. As mentioned above, the standard does not specify exact sizes for these types, and the only says that a larger type’s range shall be at least as big as the smaller type. For example the range of an int variable is at least as big as the range of a short int.
C defines a minimum set of characters that need to be supported on a system where C programs are written and run (more on this in the next tutorial). Each character is given a numeric value such as A = 65, B = 66, etc. A char is an integer number big enough to at least hold the maximum value in this set, though in typical implementations it is bigger. C does not specify whether a plain ‘char’ refers to a signed or unsigned number – that is implementation defined. If you need to be sure your char variable is signed or unsigned, specify it in the declaration like this: unsigned char c.
The short int is a signed small integer. C allows abbreviated or longer names for the same type: short, signed short, signed short int. For the unsigned version use one of these: unsigned short, unsigned short int.
A signed integer may be declared as: int, signed, signed int. An unsigned integer may be declared as: unsigned, unsigned int.
A signed long int may be declared using one of these names: long, signed long, ,long int, signed long int. An unsigned version may be declared as: unsigned long, unsigned long int.
A signed long long can be declared like this: long long, singed long long, long long int, signed long long int. An unsigned can be declared as: unsigned long long, unsigned long long int.
This table defines the minimum ranges allowed for these integer types. An implementation is free to define these types to hold greater ranges than the ones given here.
Type |
Minimum Value |
Maximum Value |
signed char |
-127 |
127 |
unsigned char |
0 |
255 |
short |
-32767 |
32767 |
unsigned short |
0 |
65535 |
int |
-32767 |
32767 |
unsigned int |
0 |
65535 |
long |
-2147483647 |
2147483647 |
unsigned long |
0 |
4294967295 |
signed long long |
-9223372036854775807 |
9223372036854775807 |
unsigned long long |
0 |
18446744073709551615 |
If you want to know the actual minimum and maximum values an implementation uses they are defined in the limits.h header file. It defines various constants like SCHAR_MIN, SCHAR_MAX, etc. that give the values for this implementation of C. The following example shows the limits of this implementation:
#include <stdio.h>
#include <limits.h>
void main()
{
printf("Signed char minimum value: %d\n", SCHAR_MIN );
printf("Signed char maximum value: %d\n", SCHAR_MAX );
printf("Unsigned char minimum value: %d\n", 0 );
printf("Unsigned char maximum value: %d\n", UCHAR_MAX );
printf("Char minimum value: %d\n", CHAR_MIN );
printf("Char maximum value: %d\n", CHAR_MAX );
printf("Signed short minimum value: %d\n", SHRT_MIN );
printf("Signed short maximum value: %d\n", SHRT_MAX );
printf("Unsigned short minimum value: %d\n", 0 );
printf("Unsigned short maximum value: %d\n", USHRT_MAX );
printf("Signed int minimum value: %d\n", INT_MIN );
printf("Signed int maximum value: %d\n", INT_MAX );
printf("Unsigned int minimum value: %u\n", 0 );
printf("Unsigned int maximum value: %u\n", UINT_MAX );
printf("Signed long minimum value: %ld\n", LONG_MIN );
printf("Signed long maximum value: %ld\n", LONG_MAX );
printf("Unsigned long minimum value: %lu\n", 0 );
printf("Unsigned long maximum value: %lu\n", ULONG_MAX );
printf("Signed long long minimum value: %lld\n", LLONG_MIN );
printf("Signed long long maximum value: %lld\n", LLONG_MAX );
printf("Unsigned long long minimum value: %lu\n", 0 );
printf("Unsigned long long maximum value: %llu\n", ULLONG_MAX );
}
The output of this program is :
Signed char minimum value: -128 Signed char maximum value: 127 Unsigned char minimum value: 0 Unsigned char maximum value: 255 Char minimum value: -128 Char maximum value: 127 Signed short minimum value: -32768 Signed short maximum value: 32767 Unsigned short minimum value: 0 Unsigned short maximum value: 65535 Signed int minimum value: -2147483648 Signed int maximum value: 2147483647 Unsigned int minimum value: 0 Unsigned int maximum value: 4294967295 Signed long minimum value: -2147483648 Signed long maximum value: 2147483647 Unsigned long minimum value: 0 Unsigned long maximum value: 4294967295 Signed long long minimum value: -9223372036854775808 Signed long long maximum value: 9223372036854775807 Unsigned long long minimum value: 0 Unsigned long long maximum value: 18446744073709551615
As you can see this implementation keeps close to the minimums allowed except for ints. The output lines 5-6 for char types also shows that in this implementation chars are signed (otherwise the minimum and maximum values would be the same as for unsigned char).
An enumerated type is an integer type that allows you to give names to certain values. Here is an example of an enumeration:
enum language { english, french, spanish = 4, german, chinese };
This declares language as an integral type that can have one of 5 values. Each of the members of the enumeration are assigned an integer value. If a value is given, as for the spanish member above, that value is used; otherwise the value is 1 more than the previous member’s value. For the first member if a value is not given it is assigned 0. Following these rules we can see that the values for the members would be english = 0, french = 1, spanish = 4, german = 5, chinese = 6.
Here is an example of how to use a variable of this enumerated type:
enum language lang = spanish;
if ( lang == english )
printf( "I speak english\n" );
else if ( lang == spanish )
printf( "Yo hablo espanol\n" );
else
printf( "I don't speak that language\n" );
This declares lang as having the enumeration type language and assigns it the spanish member of the enumeration. Then it compares lang with the english and spanish members of the enumeration to print the appropriate message.
Since the members of an enumerated type have integer values you can compare them to other numbers. But generally you want to avoid comparing them to hardcoded values like if ( lang == 5 ) to check for the german value because any change in the members of the type can possibly change their values. Suppose the language enumeration was changed to add a new russian value after the spanish value and before the german value. Now russian has the value 5, not german, so the statement if ( lang == 5 ) will be true for the wrong language.
The C standard does not define specific sizes for the integer types discussed so far. However, it does require the the implementation to define integer types that have at least a certain number of bits. It also allows an implementation to define integer types that have a specific size in bits. These types are defined in the stdint.h header file. The types int_leastN_t and uint_leastN_t define integer types that are at least N bits wide. For example int_least8_t defines a signed integer that is at least 8 bits, and a uint_least64_t defines an unsigned integer that is at least 64 bits. In addition, an implementation might provide intN_t and uintN_t types that a exactly N bits wide.
l_eight is 1 bytes thirtytwo is 4 bytes
This shows that the int_least8_t type is 8 bits (1 byte) and thirtytwo is 32 bits (4 bytes).
In addition to all the types discussed above, an implementation may define its own integer types. Consult the implementation’s documentation for details about these types.
Floating point types
Floating point numbers are real numbers that, unlike integers, may contain fractional parts of numbers, like 1.446, -112.972, 3.267e+27. There are three standard floating point types: float, double and long double. Just like integers, their sizes are not specified by the standard, it just sets the minimum allowed ranges of the magnitudes and precision for these types. This table shows the minimum and maximum magnitudes and digits of precision these types must have (in base 10):
Type |
Minimum Magnitude |
Maximum Magnitude |
Precision |
float |
1.0e-37 |
1.0e+37 |
6 |
double |
1.0e-37 |
1.0e+37 |
10 |
long double |
1.0e-37 |
1.0e+37 |
10 |
The magnitude values in this table are absolute values. A floating point number may be positive or negative. This table says that an implementation’s float, double and long double types must be able to represent a value at least 1.0e-37 small and at least 1.0e+37 big.
There is a floating point counterpart to limits.h called float.h that defines constants that contain the implementation’s minimum and maximum magnitudes and precision for floating point types:
#include <stdio.h>
#include <float.h>
void main()
{
printf("Minimum value of a float: %.5g\n", FLT_MIN );
printf("Maximum value of a float: %.5g\n", FLT_MAX );
printf("Precision of a float: %d digits\n", FLT_DIG );
printf("Minimum value of a double: %.5g\n", DBL_MIN );
printf("Maximum value of a double: %.5g\n", DBL_MAX );
printf("Precision of a double: %d digits\n", DBL_DIG );
printf("Minimum value of a long double: %.5Lg\n", LDBL_MIN );
printf("Maximum value of a long double: %.5Lg\n", LDBL_MAX );
printf("Precision of a long double: %d digits\n", LDBL_DIG );
}
You can see that this implementation’s double and long double types greatly exceed the minimum required.
Most implementations will also provide a type for handling complex numbers, which can be combined with the above floating types depending on how much magnitude and precision you want. A complex type may be declared as: float _Complex, double _Complex or long double _Complex. Here is a simple example adding two complex numbers:
#include <stdio.h>
void main()
{
float _Complex a = 5.3 + 3I;
double _Complex b = 1.6 - 8I;
double _Complex c;
c = a + b;
printf("Result is %g + %gI\n", creal(c), cimag(c));
}
Result is 6.9 + -5I
Line 5 declares a complex that is comprised of float real and float imaginary parts. Lines 6 and 7 declare complex numbers with doubles for the real and imaginary parts, and line 9 adds them together. As you learned in the operators and expressions tutorials, the operands of the addition operator may be converted to a common type – in this case the float _Complex is converted to a double _Complex and the result is a double _Complex. The functions creal() and cimag() used on line 11 return the real and imaginary parts of a complex number.
The header file complex.h has many math functions that work with complex numbers. Some implementations may provide an imaginary type as well, but this is not required by the C standard. Consult your implementation’s documentation for more info about functions on complex numbers and support for imaginary types.
Void type
The void type basically means “nothing”. A void type cannot hold any values. You cannot declare a variable to be a void, but you can declare a variable to be a pointer to a void. A pointer to a void is used when the pointer may point to various different types. To use the value that a void pointer is pointing to, it must be cast into another type. In the previous tutorial on expressions you saw an example of this in the section on explicit type conversion.
You can also declare a function’s return type as void to indicate that the function does not return any value. You have seen this already in several examples where the main() function is declared to return void. Here is another example:
#include <stdio.h>
int func1( int a, int b )
{
return a + b;
}
void func2( int c, int d )
{
printf("c + d = %d", c + d );
}
void main()
{
int sum = func1( 8, 10 );
printf("a + b = %d\n", sum );
func2( 8, 10 );
}
The output of this program is:
a + b = 18 c + d = 18
This program has two functions func1() and func2() that add two integers. The function func1() returns the sum of its arguments. The function func2() adds the arguments and prints out the result; it does not return a result.
The single void inside parentheses can also be used in function declarations to indicate that a function takes no parameters. For example, if you have two function declarations:
extern int func1();
extern int func2(void);
The first declares func1() as a function that takes an unspecified number and types of parameters and returns an integer. The second declares func2() as a function that takes no parameters and returns an integer.
Another way the void keyword is sometimes used is to indicate that you purposefully ignore the returned value of a function. For example, the fclose() function returns an integer that is 0 for success or EOF (a special constant defined in stdio.h) on errors. However, in some cases you might not care if there are errors or not. In those cases you can precede the call with (void) like this:
(void) fclose( fp );
This indicates to anyone reading the source code that the return value of fclose() is being purposefully ignored. The (void) is not necessary, a statement without it would do the same thing, with the return value being silently ignored. It is really just a matter of style.
Pointers
Pointers are variables that “point to” other variables. A pointer must be declared to point to one certain type of variable such as a pointer to integer or pointer to float. A void pointer is a special case as discussed above.
Just as an integer variable holds a number, a pointer variable holds a memory address. This is usually the address of another variable, or an area of allocated memory, but it can be any address. Once a pointer is assigned an address, you can access the value that it is pointing to with the dereference operator *. You’ve already seen some examples of pointers in previous tutorials, but let’s look at another simple example:
#include <stdio.h>
void main()
{
int a = 123;
int *b = &a;
printf( "Address of a = %p\n", &a );
printf( "Value of a = %d\n", a );
printf( "Value of b = %p\n", b );
printf( "Value of variable that b is pointing to = %d\n", *b );
*b = 321;
printf( "Value of a = %d\n", a );
}
The output of this program is:
Address of a = 0xcfbf0404 Value of a = 123 Value of b = 0xcfbf0404 Value of variable that b is pointing to = 123 Value of a = 321
Line 6 in this program declares b as a pointer to an integer and initializes it with the address of integer a. You can see in the output that the value of b is the address of a. The dereference operator on lines 12 and 13 give access to the value that b is pointing to, which is the value of a. In the last line of the program output you can see that the value of a has changed because it was set to 321 on line 13.
The address 0 is a special case. A pointer that points to 0 is called a “null pointer”, and there is a constant NULL that denotes a null pointer. Null pointers are used to indicate error conditions. Many functions that return pointers return NULL if there was an error. Code like the lines below are very common:
char filename[1024];
FILE *fp;
/* code here to set filename to the name of file to be read. */
if (( fp = fopen( filename, "r" )) == NULL )
{
fprintf( stderr, "Could not read %s\n", filename );
return 1;
}
Here the function fopen() is used to open a file. That function returns a pointer to a FILE type, but if fopen() fails for any reason it returns NULL. In that case the code prints an error message and returns.
The above example also shows that you can compare any pointer type (in this case a FILE *) to a null pointer without explicit conversion. Normally when comparing different types you would need an implicit or explicit conversion, such as:
if (( fp = fopen( filename, "r" )) == (FILE *)NULL )
But when comparing pointers to null pointer that is not necessary.
Arithmetic done on pointers take the size of the type pointed to into account. For example, if an implementation’s size of an int is 4 bytes, and the address of integer a is 0x100, the following code:
int *p = &a; /* stores 0x100 in p */
p += 8;
will increment the address stored in p by 8 * 4 bytes or 32 bytes total, leaving it with a value of 0x100 + 32 = 0x120.
Pointers will be covered in more detail in another tutorial.
Derived types
Derived types are types constructed from the ones we’ve seen so far in this tutorial. There are three main derived types: arrays, structures and unions.
Arrays
You have probably seen arrays in other programming languages. Arrays are one or more instances of one type. You declare arrays with a type name followed by variable name and then the size of the array in brackets:
char name[50];
long long num_array[256];
float m[2][4];
The first two lines declare name as an array of 50 characters and num_array as an array of 256 long longs. The third line declares m as a 2-dimensional array of floats, like a matrix with 2 rows and 4 columns.
In C you cannot assign to arrays except when they are first declared or if they are members of a struct. So copying an array like this:
int arr1[4] = { 1, 2, 3, 4 };
int arr2[4];
arr2 = arr1;
will not work. There are ways around this, like using a for loop to copy each element of the array, or using memcpy().
Array elements are accessed using the [] operator, and elements are numbered starting at 0. It is also possible to declare variable sized arrays (if they meet certain conditions). Let’s see some examples of using arrays:
#include <stdio.h>
#include <stdlib.h>
void main()
{
int numarr[ 4 ] = { 12, 92, 54, 890 };
int sz;
char buff[64];
float m[2][4] = { { 1.1, 1.2, 1.3, 1.4 }, { 2.1, 2.2, 2.3, 2.4 }};
printf( "numarr's 3rd element is %d\n", numarr[2] );
printf( "m's 2nd row's 4th column value is %f\n", m[1][3] );
printf("Enter size of variable sized array: ");
fgets( buff, 64, stdin );
sz = atoi(buff);
int arr2[ sz ];
int i;
for( i = 0; i < sz; ++i )
arr2[ i ] = i+1;
for( i = 0; i < sz; ++i )
printf("arr2[%d] = %d\n", i, arr2[ i ]);
}
The program’s output follows:
numarr's 3rd element is 54 m's 2nd row's 4th column value is 2.400000 Enter size of variable sized array: 5 arr2[0] = 1 arr2[1] = 2 arr2[2] = 3 arr2[3] = 4 arr2[4] = 5
On lines 6 and 9 are examples of initializing an array when you declare it. The initial values are inside the { } symbols separated by commas. On line 9, each of the two rows of m is initialized in two separate { } initializers. Lines 11-12 show that to access element N of an array you use index N-1 since the first element has index 0. The third element of numarr is accessed with index [2] and the second row and fourth column of m is accessed with index [1][3].
{mosgoole}
Line 18 declares a variable sized array. All the other arrays in this program are constant size – their sizes never change and can be calculated at compile time. The arr2 array’s size depends on the number the user enters when the program is run. On this run the user enters 5 so it is 5 elements long. Lines 20-21 initialize the array so that the element at index 0 gets the number 1, element at 1 gets the number 2, and so on. Lines 23-24 print out the elements.
Arrays will be discussed further in another tutorial.