Types in C

Subpage of C Deep Dive

A close look at the C language.

The type system was added primarily to help the compiler-writer distinguish floats, doubles, and characters from words on the early hardware system.

The data type dictates what assembly instructions are generated from the code because the instructions for the different types are different.

C rejects strong typing and permits the programmer to make assignments between objects of different types if desired. The type system was almost an afterthought, never rigorously evaluated or extensively tested for usability.

Types in C are the rules that determine how much space a value occupies, how it is represented, and what operations are valid.

Scalar Types

Built-in scalar types include:

  • integers (char, short, int, long, long long)
  • floating-point types (float, double, long double)
  • boolean type _Bool.

    The ANSI C standard has no value for true and false. C programs treat the value 0 as false, and any other value as true. The C99 standard does allow you to use the words true and false in your programs—but the compiler treats them as the values 1 and 0 anyway.

Be careful: signed overflow (e.g., INT_MAX + 1) is undefined, but unsigned arithmetic wraps modulo 2^N. Floating-point arithmetic follows IEEE-754 semantics on many platforms but with implementation variations for rounding and precision.

Integers

char

Each character is stored in the computer’s memory as a character code. And that’s just a number. So when the computer sees A, to the computer it’s the same as seeing the literal number 65.

sizeof(char) is always 1: The sizeof operator in C returns the size of its operand in units of char. Consequently, sizeof(char) always evaluates to 1.

Note that the type returned by the sizeof operator is size_t: an unsigned integer type used to represent the sizes of objects in bytes.

Using size_t helps prevent potential issues that might arise from using standard integer types (intunsigned int, etc.) when dealing with object sizes, particularly when moving code between systems with different architectural constraints. 

int

If you need to store a whole number, you can generally just use an int. The exact maximum size of an int can vary, but it’s guaranteed to be at least 16 bits. In general, an int can store numbers up to a few million.

Each integer type may be signed or unsigned, and C’s standard only guarantees minimum ranges (e.g., int is at least 16 bits), so exact sizes are implementation-defined; the <stdint.h> header provides fixed-width types like int32_t when exact widths are needed.

short

But sometimes you want to save a little memory. Why use an int if you just want to store numbers up to few hundreds or thousands? That’s what a short is for. A short number usually takes up about half the space of an int.

long

Yes, but what if you want to store a really large count? That’s what the long data type was invented for. On some machines, the long data type takes up twice the memory of an int, and it can hold numbers up in the billions. But because most computers can deal with really large ints, on a lot of machines, the long data type is exactly the same size as an int. The maximum size of a long is guaranteed to be at least 32 bits.

Floating-point types

float

float is the basic data type for storing floating-point numbers. For most everyday floating-point numbers—like the amount of fluid in your orange mocha frappuccino—you can use a float.

double

Yes, but what if you want to get really precise? If you want to perform calculations that are accurate to a large number of decimal places, then you might want to use a double. A double takes up twice the memory of a float, and it uses that extra space to store numbers that are larger and more precise.

However, double uses more memory and its operations are slower.

Type Casting

Type casting in C is the process of converting a value from one data type to another.

There are two types of type casting that can take place.

Implicit Type Casting

Implicit type casting, also known as type promotion or type conversion, is performed automatically by the compiler without the programmer's intervention. It typically occurs in the following scenarios:

  • Assignment to a larger type: When assigning a value of a smaller data type to a larger one (e.g., int to float, or char to int), the conversion is done automatically to prevent data loss.
  • Arithmetic operations with mixed types: In expressions involving different data types (e.g., adding an integer and a float), the compiler converts the "lower" data type to the "higher" data type based on a predefined hierarchy to maintain precision. The general hierarchy is: char -> int -> long -> float -> double -> long double.

Here’s an example:

#include <stdio.h>

int main() {
    int a = 10;
    float b = 3.14;
    float result = a + b; // 'a' is implicitly converted to float

    printf("Result = %.2f", result);
    return 0;
}

Explicit Type Casting

Explicit type casting, often simply called "casting," is performed manually by the programmer using the cast operator () to force a conversion.

For example, to convert whole numbers to floats, we can use casting. The (float) will cast an integer value into a float value. This will then work just as if you were using floating-point values the entire time.

Here is an example of casting to an (int):

#include <stdio.h>

int main() {
    float price = 99.99;
    int roundedPrice = (int) price; // Decimal part is truncated (lost)

    printf("Rounded Price = %d", roundedPrice);
    return 0;
}
// Output: Rounded Price = 99

Note that this behaviour does not come about due to rounding but rather truncation. This applies to all naive conversions from floats to whole numbers in C.

Data types size

Data types are different sizes on different platforms. The C Standard Library has a couple of headers with the details.

This program is an example that will tell you about the sizes of ints and floats:

#include <stdio.h>
#include <limits.h>
#include <float.h>

int main()
{
	printf("The value of INT_MAX is %i\n", INT_MAX);
	printf("The value of INT_MIN is %i\n", INT_MIN);
	printf("An int takes %z bytes\n", sizeof(int));
	printf("The value of FLT_MAX is %f\n", FLT_MAX);
	printf("The value of FLT_MIN is %.50f\n", FLT_MIN);
	printf("A float takes %z bytes\n", sizeof(float));
	return 0;
}

What if you want to know the details for chars or doubles? Or longs?

No problem. Just replace INT and FLT with CHAR (chars), DBL (doubles), SHRT (shorts), or LNG (longs).

Working with scalar types

When you’re passing around values, you need to be careful that the type of the value matches the type of the variable you are going to store it in.

short x = 15;
int y = x;
printf("The value of y = %i\n", y);
// This works OK
int x = 100000;
short y = x; // Oops...
print("The value of y = %hi\n", y);
// The value of y = -31072

Notice: %hi is the proper code to format a short value.

Sometimes, the compiler would spot the error (on the right) and give you a warning but a lot of the time the compiler won’t be smart enough for that, and it will compile the code without complaining.

In that case, when you try to run the code, the computer won’t be able to store a number 100,000 into a short variable. The computer will fit in as many 1s and 0s as it can, but the number that ends up stored inside the y variable will be very different from the one you sent it.

Keywords

You can put some other keywords before data types to change the way that the numbers are interpreted:

unsigned

The number will always be positive. Because it doesn’t need to worry about recording negative numbers, unsigned numbers can store larger numbers since there’s now one more bit to work with.

So an unsigned int stores numbers from 0 to a maximum value that is about twice as large as the maximum number that can be stored inside an int. There’s also a signed keyword, but you almost never see it, because all data types are signed by default.

long

That’s right, you can prefix a data type with the word long and make it longer. So a long int is a longer version of an int, which means it can store a larger range of numbers. And a long long is longer than a long. You can also use long with floating-point numbers.

Composite Types

Composite types include arrays, pointers, functions, structures (struct), unions (union), and enumerations (enum).

Note: The compiler may insert padding between members for alignment, which affects sizeof.

Type qualifiers const, volatile, and (since C99) restrict modify the behavior of types:

  • const prevents modifying the object through that reference
  • volatile tells the compiler the value may change unexpectedly (hardware registers, signal handlers)
  • restrict asserts that a pointer is the sole initial means of accessing the object it points to, enabling optimizations

Integer promotions mean small integer types (char, short) are promoted to int or unsigned int before many operations.

C distinguishes type categories and their usage:

  • arrays decay to pointers to their first element in most expressions
  • function types decay to function pointers when used in expressions
  • pointers to incomplete types (e.g., struct S forward declarations) can be used before the full definition so long as you don’t dereference them.

Finally, storage duration ties types and declarations to lifetimes: automatic variables live until block exit, static objects persist for the program’s lifetime, and dynamically allocated objects live until freed.

The static keyword is essential to allow us to create large data structures because these cannot be held in the memory available to a function.

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.