Variable Types in C¶
Learning Objectives: After going through this material, you will know the types of variables commonly used in the C language.
Variable Types¶
The C language standards have predefined a set of variable types for the convenience of the programmer (and okay, to save memory). The variable type tells us its intended use: character, integer, or floating-point number.
The C language standard defines these variable types and specifies minimum requirements for them. However, some properties, especially their exact size, can depend on the compiler and target architecture.
| Variable Type | Keyword | Typical Size | Exact size standardized? |
| Character | char | 1 byte | Yes, 1 byte by definition |
| Integer (historically associated with a machine word) | int | Usually 4 bytes | No |
| Short integer | short int | Usually 2 bytes | No |
| Long integer | long int | Usually 4 or 8 bytes | No |
| Single-precision floating-point | float | Usually 4 bytes | No |
| Double-precision floating-point | double | Usually 8 bytes | No |
You can check the size of different types using the
sizeof operator.Integers¶
Modern systems commonly represent signed integers using two's complement, and this is the representation we use throughout this course.
According to the standard, integer variable types are guaranteed to have minimum and maximum ranges, meaning the variable will work within this specified range regardless of the compiler and computer. (Note! The full range is not always available as specified in the standard.)
Integer types can be refined using type specifiers such as
signed, unsigned, short, and long. The keyword const is instead a type qualifier that prevents modification of the object through that identifier.signed(signed), reserves the MSB for the sign, as we learned earlier. In two's complement numbers, this is the default, so it doesn't need to be specified separately (except in special cases).unsigned(unsigned) "frees" the MSB for use in the number range, making the numbers always non-negative. Naturally, this changes the number range accordingly.shortandlongare actually type specifiers, but when used, the wordintcan be omitted. In general, and to make the code easier to read, you will often findshort intandlong intexplicitly used.constindicates that a value should not be modified after it has been defined. Besides helping the compiler detect accidental modifications, in embedded systems constant data can often be stored in read-only or program memory instead of RAM. The exact placement depends on the target architecture and compiler.
For example, a 4-bit integer's range: unsigned and signed.
Character Variable¶
The
char type is actually an integer type that occupies one byte. It can therefore be used both to store small integer values and to represent characters. On the systems considered in this course, characters are normally encoded using ASCII, where each character corresponds to a numeric value. For example, 'a' corresponds to 97. This solution is inherited from the 1970s when handling characters separately would have been too expensive and memory-intensive.This leads to interesting results. If the programmer wants, they can forget the character nature of the
char type and use it (as a signed integer) for integer variables. This allows for arithmetic operations on letters to work, as they are interpreted by the compiler as numbers. Only when character is shown in any system output the corresponding character is presented. For example, operations like 'a' + 1 = 'b' (97 + 1 = 98) or 'c' - 'a' = 2 (99 - 97 = 2).In embedded programming, this is actually hugely beneficial in resource-constrained devices, as strings composed of letters can be handled numerically, such as comparing the "equality" of two words. Strange but handy.
Please note that modern systems commonly use Unicode because it can represent a much larger set of characters from different languages (e.g. Cyrillic characters). One of the most widely used encodings is UTF-8. UTF-8 is variable-length: a character may require from 1 to 4 bytes. The first 128 UTF-8 values are identical to ASCII, so ordinary ASCII text is also valid UTF-8. Recent C standards, such as C11, provide additional support for Unicode-related character types and literals, but we will not cover them in detail in this course.
Other variable modifiers¶
C also has register, pointer, global, and static variables, but more on those later.
Fixed-width Integer Types¶
The
<stdint.h> header provides fixed-width integer types whose names explicitly indicate their number of bits. From now on in this course, we will mainly use these types when the exact size of an integer is important.| Fixed-width Integer Type | Size in Bytes |
| int8_t / uint8_t | 1 |
| int16_t / uint16_t | 2 |
| int32_t / uint32_t | 4 |
| int64_t / uint64_t | 8 |
The
intN_t variable type means a signed integer, and the uintN_t type means an unsigned integer. The use of fixed-width types is increasingly important in modern programming specially in cross-platform development. These types are also widely use in C++ and are critical to ensure portability across different systems. Only available from the C99 standard. You need to include the
<stdint.h> header. This is the preferred way of defining variables (specially ints) in this course, specially when we move to the embedded programming part. Initializing Variables¶
A variable can be defined and initialized at the same time. Initialization provides the variable with its initial value as part of its definition, and this value is then stored in memory. If the value is changed later using the assignment operator
=, this is called an assignment rather than initialization.Examples of initializing variables:
int16_t integer = -123;
uint16_t unsigned_integer = 3333;
uint32_t long_integer = 0x12345678;
double floating_point = 1.234;
float smaller_floating_point = 1.2e-10;
For character variables
char, the initialization uses the ' (single quote) to enclose a character, or a number as shown above. The corresponding ASCII value is stored in memory.// These initializations are equivalent:
char character = 'a'; // Value is the ASCII code for 'a'
char character = 97; // In the ASCII table, 'a' corresponds to the number 97
Challenges¶
Since C is an old hardware-oriented programming language, many things are left to the programmer's knowledge.
For example, if a programmer tries to initialize a variable with a number outside its range, the C compiler generally (depending on the compiler and the flags used) issues a warning.
int8_t a = 1234;
...
warning: overflow in implicit constant conversion.
However! The compiler only warns about the problem, and the program may continue to compile. It is important to be careful not to let such mistakes end up in the compiled program.
What happens in the compiler? Why is this just a warning? We know that
int8_t can only represent a limited range of values. Here, the value 1234 is outside that range, so a conversion to int8_t is required. On the compiler and two's complement architecture used in this course, the result behaves as if the higher bits were cut off, like a guillotine.In the example above, 1234 in binary is
10011010010 (11 bits). Keeping the lowest 8 bits gives 11010010, which is interpreted as -46 in two's complement. Therefore, on our system the variable is initialized as int8_t a = -46 instead of the intended 1234. Oops!!!!OFFTOPIC: If you want to be sure that compiler warns about this possible error (and many others) you can compile it with the
-Wall flag. OFFTOPIC:
What is the difference between warning vs error? WARNING: A warning is a message generated by the compiler that indicates a potential issue in the code. The compiler allows the program to compile and run, but it alerts the developer to something that could lead to unintended behavior or a bug. Warnings do not stop the compilation process, but they highlight areas of code that may need attention. ERROR: An error is a message from the compiler that indicates a critical issue in the code, which prevents the program from being compiled. Errors usually indicate syntax or semantic mistakes that must be corrected before the code can be successfully compiled and executed.
In the example above, here's what happens: 1234 in binary is
10011010010 (11 bits). The compiler cuts off the top three bits 100, so that the remaining 8 bits fit in memory 11010010. The problem is that this binary number is interpreted as a two's complement number, so the variable a is initialized as int8_t a = -46 instead of the intended 1234. Oops!!!!Array Variables¶
In C, array variables are declared/defined using square brackets (as in many other programming languages), with the size of the array inside the brackets. You can declare arrays for all basic variable types (and other types, which we'll cover later). Of course, multidimensional arrays are also possible in C.
The syntax is as follows:
uint8_t array[5];
uint8_t array[5] = { 1, 3, 5, 7, 9 }; // Initialize at the same time
uint8_t array[] = { 1, 3, 5, 7, 9 }; // Compiler determines the array size automatically!
//This is wrong: uint8_t[]; We cannot define an array without including its size unless we are initializing at the same time.
uint8_t array[3][3];
uint8_t array[2][3] = { { 1, 2, 3 }, // Initialize
{ 4, 5, 6 },
};
uint8_t array[][3] = { { 1, 2, 3 }, // The compiler can sometimes determine the array size!
{ 4, 5, 6 },
{ 7, 8, 9 } };
Strings¶
Since C does not have a separate string variable type, strings are arrays of type char. That is, they are regular numeric arrays, but the compiler interprets them as representing characters according to the ASCII table.
However, there is a little elegant peculiarity in initializing strings. Strings must always end with the literal character '\0' (NULL Terminator). This is crucial to know because many C standard library functions (and other ready-made functions) assume this! String handling in the program typically breaks if the string does not end with a zero. See for instance how
strlen and strcpy uses the null terminator to detect the end of a string. Forgetting the null terminator can lead to undefined behaviour and security vulnerabilities, such as out-of-bounds memory access, and can provoke, for instance, a buffer overflow. Note! The null terminator is entirely different (in the compiler's view) from the ASCII character '0', which corresponds to the number 48. Null terminator is represented in memory by 0x00.
String initialization can be done in several ways, just like with arrays, as shown above.
char message[] = "Hello"; // The compiler automatically adds a trailing 0, array length is 5+1 characters
char message[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char message[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // The compiler calculates the array size
char message[] = {72, 101, 108, 108, 111, 0}; // ASCII codes for the characters
If an array initializer contains fewer values than the declared array size, the remaining elements are automatically initialized to zero. The following initializations are therefore equivalent:
char message[5] = "H";
char message[5] = {'H'};
char message[5] = {'H', 0, 0, 0, 0 };
Indexes¶
Otherwise, handling arrays is familiar: you move through them using indexes, as in other programming languages. However, the first index value is
0 and the last allowed index is array size - 1.An example of using indexes:
uint8_t array[3][3] = { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };
for (int i = 0; i < 3; i++) { // elements 0,1,2
for (int j = 0; j < 3; j++) { // elements 0,1,2
printf("%d\n", array[i][j]);
}
}
You can also traverse arrays in other ways, which we will cover in upcoming material on pointers. Being close to the hardware offers all kinds of fun ways to handle memory. In some cases, a skilled or even less skilled programmer might manage to go outside the bounds of an array using an index, without the compiler noticing, so be careful!! This can lead to situations named as undefined behaviour an important concept in C. Sometimes applications might work, other time might crash.
Variable Type Conversions¶
In C, the type of variables can be changed according to certain rules. The standard explains this in detail, but the following general rules are sufficient for us.
- Conversions that may result in loss of data usually trigger a warning in the compiler. For example, converting a value larger than the range of the target type, where the compiler's guillotine drops the highest bits.
- In addition, during addition and multiplication, overflow can occur, causing the guillotine to cut off the highest bits.
- As a simplified rule for this course, the general conversion hierarchy in arithmetic operations can be seen from bottom to top: double <- float <- long int <- int <- char.
- In arithmetic operations, the basic rule is that the "lower" type of the operands is generally converted to the "higher" type according to this simplified hierarchy.
- In conversions between signed types (e.g., short -> long), sign extension occurs, where the sign bit is copied to the higher bits.
- Example.
int8_t -> int16_t: 11001010 -> 1111111111001010, which is derived from two's complement representation. - When signed and unsigned integer values are used together, C may convert the signed value to an unsigned type before performing the operation. This can lead to unexpected results, especially when the signed value is negative.
- When converting floating-point numbers to integers, the fractional part is dropped without rounding!!!
- For example, the conversion
(int)0.5 = 0. - The character type
charis somewhat special. ASCII values0..127require only 7 bits, but acharoccupies one byte. Whether a plaincharis treated as signed or unsigned is implementation-dependent. Therefore, whencharis used numerically and negative values matter, it is safer to writesigned charorunsigned charexplicitly.
Examples.
uint8_t a = 1234; // Compilation usually produces a warning main.c: In function 'main': main.c:6: warning: large integer implicitly truncated to unsigned type // The addition is performed as int: 33 + 101 = 134. // The result is then converted back to int8_t. // On our system, this results in -122. int8_t a = 33; int8_t b = 101; int8_t c = a + b; // result is -122 on our system
Forcing Type Conversion (Casting)¶
C provides a type conversion operator (known as a cast), which can be used to explicitly force a conversion at any point in the code. The cast operator is in the form
(typename) expression.Sometimes a conversion happens automatically without requiring an explicit cast. For example, the
sqrt function expects a double, so C automatically converts x to double:uint16_t x = 7;
double y = sqrt(x); // x is automatically converted to double
However, an explicit cast can also change how an expression is evaluated. For example, when two integers are divided, C performs integer division:
int x = 7;
int y = 2;
double result1 = x / y; // integer division: 7 / 2 = 3 -> 3.0
double result2 = (double) x / y; // x is converted first: 7.0 / 2 = 3.5
Thus, casting can be used when we explicitly want to control the type used in an operation.
The cast operator can break the previously mentioned hierarchy when needed.
Conclusion¶
We will work more with arrays in conjunction with pointer variables. When dealing with variable conversions and initializations, it is good to remember the compiler's guillotine, which can save you from many strange bugs.
Give feedback on this content
Comments about this material