Even though C really only stores numeric data, different formats and size ranges require varying data types to store and interpret.
Data types
When referring to "data types", we really only mean builtin scalar types meant for direct data storage. By this distinction, numeric types like int or size_t are included, while complex types like structs or opaque file handles are not.
Some data types can be signed or unsigned, with the only notable difference being the supported value range. A signed type can hold both positive and negative values with reduced maximum values, whereas an unsigned type can only hold positive values but supports larger values for the same storage size.
Default integer types
Most commonly known are the original builtin integer types. Most of these end in the keyword int, although all except one can omit it:
char: At least 8 bit integer. Intended for storing arbitrary bytes or character values from the ASCII table. Usually signed by default, but the standard does not guarantee that for this type.short/short int: At least 16 bit integer. Typically 16bit, but standard only guarantees minimum size.int: At least 16 bit integer, typically 32bit.long/long int: At least 32bit integer, now its 64bit on most modern systems except Windows.long long/long long int: At least 64bit integer.
All of the mentioned types support the signed and unsigned keyword prefix to either support smaller positive and negative values (signed), or larger non-negative values only (unsigned).
As you can see, the implementation and sizes for most of the default integer types differ per machine. The only real guarantees are the minimum bit sizes and the relative sizes: char <= short <= int <= long <= long long.
Because of this loose standard, an implementation could make all integer types 256bit and still be compliant, so relying on exact sizes falls apart quickly when writing cross-platform applications.
Default floating point types
Storing floating point numbers requires specialized data types, the default ones being:
float: At least 6 decimal digit precision, typically 32bit size (6 digits).double: At least 10 decimal digit precision, typically 64bit (15 digits).long double: At least 10 decimal digit precision, actual size is usually one of 64bit (15 digits), 80bit (18 digits) or 128bit (34 digits) depending on compiler and platform.
Floating point types are always signed, there are no unsigned variants or alternatives.
These type definitions suffer even more from loose definitions than their integer equivalents, especially the difference between double and long double is very ambiguous and borderline unusable across different platforms.
As with integer types, relative sizes are also guaranteed by the C standard: float <= double <= long double, but that might also allow a platform to provide 91bits storage size for all of them and still be compliant.
Fixed-width integers
In order to combat the unreliable storage size, also called "width", the stdint.h header defines fixed-width integer types with exactly storage size guarantees:
int8_t/uint8_t: Exactly 8 bit integerint16_t/uint16_t: Exactly 16 bit integer.int32_t/uint32_t: Exactly 32 bit integerint64_t/uint64_t: Exactly 64 bit integer
Since these types do not support the signed and unsigned keywords, they offer direct signed type definitions like int8_t for signed values and uint8_t as an unsigned variant.
There are more definitions to allow some variation to take advantage of integer sizes with better native platform speed, but modern C code will almost always use the default integer types when they do not need exact storage size and reach for fixed-width types when they do.
While useful, the fixed-width integer definitions are an extension of the C standard and may not be available on minimal platforms or embedded devices.
Booleans
The boolean data type bool defined in stdbool.h is more of a macro trick than an actual type.
Booleans are implemented by defining true as 1 and false as 0. The bool type can only store values 0 or 1. When assigning a value to bool, it will implicitly convert any value other than 0 to 1:
bool x = 5; // converts to x = 1 (true)
bool y = 0; // stays y = 0 (false)Because booleans are mostly macro definitions, they can be used in simple arithmetic:
int n = true + 4; // n = 5Since true is not a real type but just a macro, it is replaced with 1 during compilation, so no type error is raised.
Size types
Size values of objects in memory have their own type size_t defined by stddef.h. It is an unsigned integer that can represent any number between 0 and whatever the maximum size for a single object in memory supported by the platform is.
It is used in function calls like malloc/calloc and in array indexing syntax like arr[i], where i is of type size_t.
The sister type ssize_t is a POSIX extension that allows all values from size_t and -1 to indicate errors. it is used primarily on linux/unix-like systems for kernel calls like read that either return the number of bytes read or -1 to indicate an error:
ssize_t len;
len = read(file, &buf, 1024);
if(len == -1){
// read error
}Note that returning -1 only indicates that any error occurred, finding out what went wrong still relies on checking errno from errno.h.