# C Variables and types

> Learn how C variables and fundamental types work, how to inspect their limits and sizes, and why signed and unsigned overflow differ.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2020-01-31 | Updated: 2026-07-18 | Topics: [C](https://flaviocopes.com/tags/clang/) | Canonical: https://flaviocopes.com/c-variables-types/

C is a statically typed language.

This means that any variable has an associated type, and this type is known at compilation time.

This is different from how you work with variables in dynamically typed languages such as [Python](https://flaviocopes.com/python-introduction/) and [JavaScript](https://flaviocopes.com/javascript/).

When you create a variable in C, you have to specify the type of a variable at the declaration.

In this example we declare a variable named `age` with type `int`:

```c
int age;
```

A variable name can contain any uppercase or lowercase letter, can contain digits and the underscore character, but it can't start with a digit. `AGE` and `Age10` are valid variable names, `1age` is not.

You can also initialize a variable at declaration, specifying the initial value:

```c
int age = 37;
```

Once you declare a variable, you can assign another value that can be converted to its type:

```c
age = 100;
```

Conversions can lose information. In this example, `37.2` is converted to `37` before it is stored:

```c
#include <stdio.h>

int main(void) {
  int age = 37.2;
  printf("%d\n", age);
  return 0;
}
```

A compiler can warn about this conversion when suitable warning options are enabled, but you must not rely on every compiler diagnosing every lossy conversion.

The [C](https://flaviocopes.com/c-introduction/) fundamental arithmetic types include integer types such as `char`, `short`, `int`, `long`, and `long long`, plus the floating-point types `float`, `double`, and `long double`.

## Integer numbers

C provides us the following types to define integer values:

- `signed char` and `unsigned char`
- `short` and `unsigned short`
- `int` and `unsigned int`
- `long` and `unsigned long`
- `long long` and `unsigned long long`

Most of the time, you'll use `int` for ordinary integer values.

The plain `char` type is used to store characters and small integer values. It is always exactly 1 byte by definition, but a C byte contains at least 8 bits and can contain more. Whether plain `char` behaves like `signed char` or `unsigned char` is implementation-defined.

The standard guarantees minimum ranges and this ordering:

```text
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
```

Two adjacent types can have the same size. Their exact widths and ranges depend on the implementation.

Use the macros from `<limits.h>` instead of guessing:

```c
#include <limits.h>
#include <stdio.h>

int main(void) {
  printf("int range: %d to %d\n", INT_MIN, INT_MAX);
  printf("unsigned int maximum: %u\n", UINT_MAX);
  printf("bits in one byte: %d\n", CHAR_BIT);
  return 0;
}
```

This matters on embedded devices. For example, an `int` can be 16 bits on one microcontroller and 32 bits on another.

When you need an exact-width type, `<stdint.h>` defines names such as `int32_t` and `uint32_t` on implementations that provide those widths. Use the matching format macros from `<inttypes.h>` when printing them.

## Unsigned integers

Unsigned integer types start at 0. The standard guarantees at least these ranges:

- `unsigned char` will range from `0` to at least `255`
- `unsigned int` will range from `0` to at least `65,535`
- `unsigned short` will range from `0` to at least `65,535`
- `unsigned long` will range from `0` to at least `4,294,967,295`
- `unsigned long long` will range from `0` to at least `18,446,744,073,709,551,615`

## What happens on overflow?

Unsigned arithmetic is performed modulo one more than the type's maximum value. If `UCHAR_MAX` is 255, this conversion produces 9:

```c
#include <limits.h>
#include <stdio.h>

int main(void) {
  unsigned char j = UCHAR_MAX;
  j = j + 10;
  printf("%u\n", (unsigned int)j);
  return 0;
}
```

The addition happens after integer promotion, then the result is converted back to `unsigned char`. The conversion applies the unsigned modulo rule.

Signed integer overflow is different. It causes undefined behavior:

```c
#include <limits.h>

int main(void) {
  int value = INT_MAX;
  value = value + 1; /* undefined behavior */
  return 0;
}
```

It is not guaranteed to wrap. A compiler can optimize code on the assumption that signed overflow never occurs. Check a value before performing an operation that could exceed its range.

Converting an out-of-range value to a signed integer type is not the same operation as signed arithmetic overflow. The result is implementation-defined, or the implementation can raise an implementation-defined signal. Avoid depending on it.

Compile with useful diagnostics, for example `-Wall -Wextra -Wpedantic -Wconversion` with GCC or Clang, and treat the warnings as problems to investigate.

## Floating point numbers

Floating-point types can represent fractions and values over a very large range, but only with limited precision.

You can write floating-point constants using decimal exponent notation:

```c
double small = 1.29e-3;
double large = -2.3e+5;
```

The following types:

- `float`
- `double`
- `long double`

are used to represent numbers with decimal points (floating point types). All can represent both positive and negative numbers.

Most modern systems use binary IEEE 754 formats, so many decimal fractions cannot be represented exactly. The exact formats and precision are implementation-defined. The macros in `<float.h>`, including `FLT_DIG`, `DBL_DIG`, and `LDBL_DIG`, describe the implementation.

`double` provides at least as much precision and range as `float`, and `long double` provides at least as much as `double`. They can still have the same representation on a particular implementation.

## Inspecting type sizes

You can use `sizeof` to inspect how much storage a type occupies:

```c
#include <stdio.h>

int main(void) {
  printf("char: %zu byte\n", sizeof(char));
  printf("short: %zu bytes\n", sizeof(short));
  printf("int: %zu bytes\n", sizeof(int));
  printf("long: %zu bytes\n", sizeof(long));
  printf("long long: %zu bytes\n", sizeof(long long));
  printf("float: %zu bytes\n", sizeof(float));
  printf("double: %zu bytes\n", sizeof(double));
  printf("long double: %zu bytes\n", sizeof(long double));
  return 0;
}
```

The result of `sizeof` has type `size_t`, and `%zu` is the matching `printf()` conversion specifier.

Remember that `sizeof` reports C bytes, not necessarily 8-bit octets. Check `CHAR_BIT` when the number of bits matters.
