C Language – Variable Data Types

1. C Data Types

signedunsignedshortlongchar
intfloatdoubleautoregister
staticexternconstvolatile

1.1. Integer Sign Modifiers

These are generally used in combination with integer types such as char or int

  • They can also be used alone, in which case they are interpreted as having an implied int.

signed
unsigned

signed and unsigned are keywords that determine whether an integer type can represent negative values.

  • signed uses a sign bit to represent both negative and positive values.
  • unsigned does not represent negative values and only represents values greater than or equal to 0.

Even with the same 1 byte (8 bits), the range of values differs depending on whether it is signed or unsigned.

1.2. Integer Size Modifiers

These are generally used in combination with the integer type int

  • They can also be used alone, in which case they are interpreted as having an implied int.

short
long

short

  • short is a modifier used to reduce the size of an integer type.

long

  • long is a modifier used to increase the size of an integer type.
  • It can represent a wider range of integers than int, but on 32-bit systems, int and long int may have the same size.

1.3. Fundamental Types

These are the basic built-in data types in C used to represent integer and floating-point values.

1.3.1. Integer Types

Integer types represent whole numbers without a fractional part. Although char is used to store characters, it is classified as an integer type.

char
int
Type Bits Hex Range Value Range (Decimal)
(signed) char 8bit 0x80 ~ 0x7f -128 ~ 127
unsigned char 8bit 0x00 ~ 0xff 0 ~ 255
short (int) 16bit 0x8000 ~ 0x7fff -32,768 ~ 32,767
unsigned short (int) 16bit 0x0000 ~ 0xffff 0 ~ 65,535
(signed) int 32bit 0x80000000 ~ 0x7fffffff -2,147,483,648 ~ 2,147,483,647
unsigned int 32bit 0x00000000 ~ 0xffffffff 0 ~ 4,294,967,295
long (int) 32bit* 0x80000000 ~ 0x7fffffff -2,147,483,648 ~ 2,147,483,647
unsigned long (int) 32bit* 0x00000000 ~ 0xffffffff 0 ~ 4,294,967,295

  • In general, the most significant bit (MSB) is interpreted as the sign bit, and two’s complement representation is used so that both positive and negative arithmetic can be handled with a single adder, simplifying hardware and keeping operations consistent.

※ Under C90, integer types are signed by default. Therefore, even if signed is not explicitly written, the compiler interprets it as signed by default, so it is generally omitted. Since short and long are also modifiers of int, int is treated as the default and may be omitted as well.

※ Since C99, long long int has been introduced, making 64-bit integer representation possible.

Type Bits Hex Range Value Range (Decimal)
(signed) long long int 64bit 0x8000000000000000
    ~ 0x7fffffffffffffff
-9,223,372,036,854,775,808
    ~ 9,223,372,036,854,775,807
unsigned long long int 64bit 0x0000000000000000
    ~ 0xffffffffffffffff
0 ~ 18,446,744,073,709,551,615

1.3.2. Floating-Point Types

Floating-point types represent real numbers with a fractional part and store approximate values using floating-point representation.

float
double
Type Storage Size Value Range (Decimal) Precision (Decimal)
float 4 bytes 1.2E-38 ~ 3.4E+38 6 decimal places
double 8 bytes 2.2E-308 ~ 1.8E+308 15 decimal places
long double 10 bytes* 3.4E-4932 ~ 1.1E+4932 19 decimal places

  • float and double represent real numbers using a sign, exponent, and mantissa. 
  • The exponent determines the representable range, while the mantissa determines precision. float is lighter and faster but has limited range and precision, whereas double provides a wider range and higher precision.

[Source] C Pocket Reference - 2003 O’Reilly

※ The storage size and range of long double may vary depending on the platform and compiler.

Floating-Point Representation

Floating-point representation stores real numbers by allowing the decimal point to move freely, and instead of storing the exact decimal value, it stores an approximation using the nearest binary value.

#include <math.h>
double a = 0.1;
double b = 0.2;
if ((a + b) != 0.3) {
    /* condition is true due to floating-point precision error */
}
if (fabs((a + b) - 0.3) < 1e-9) {
    /* treated as equal */
}

  • The computer stores 0.1 and 0.2 not as exact values, but as the nearest binary approximations.
    • As a result, the actual internal value of a + b becomes something slightly off from the exact value, such as 0.30000000000000004.
    • Therefore, even though it looks equal to 0.3 to a human, the program does not treat it as exactly the same.

1.4. Storage Class Specifiers

A storage class specifier is a keyword that determines the lifetime and scope of a variable.

auto
register
static
extern

1.4.1. auto

  • It is the default storage class for local variables declared inside a block.
  • Even if not specified explicitly, all local variables are auto by default, so it is generally not written explicitly.
    • Since C99, auto has effectively become a meaningless keyword.

auto int x; /* same as int x; */

1.4.2. register

  • It is a specifier that requests that a variable be stored in a CPU register.
    • It is only a hint intended to improve access speed, and whether it is actually stored in a register is decided by the compiler.
  • You cannot use the address-of operator (&).

register int i;

※ Since C11, most compilers perform optimization automatically, so the register keyword is effectively meaningless.

1.4.3. static

1.4.3.1. Local Variables

  • Its lifetime lasts for the entire program, while its scope remains inside the block.
  • Its value is preserved even after the function call ends.

void func() {
    static int count = 0;
    count++;
}

1.4.3.2. Global Variables

  • Makes it accessible only within the file.
  • It cannot be referenced from other source files.

static int global;

1.4.4. extern

  • Used to refer to a global variable defined in another file.
    • It does not allocate actual storage; it only provides a declaration.

extern int shared;

1.5. Type Qualifiers

Type qualifiers are keywords that restrict the meaning and behavior of a variable. const means the value cannot be modified, and volatile indicates that it may be changed externally.

const
volatile

1.5.1. const

  • const is a qualifier that restricts a variable so that its value cannot be changed.
    • Once declared, its value cannot be modified through assignment.

const int max = 10;
/* max = 20; // compilation error */

1.5.2. volatile

  • volatile indicates that a variable’s value may be changed externally, so the program should always use the latest value from memory.
    • It tells the compiler not to optimize the variable away and to always read the value again from memory.

volatile const int reg;

It is used for variables that may be changed by hardware registers or interrupts.

volatile does not guarantee multithreaded synchronization and does not replace atomic operations or mutexes.


2. Printing C Data Types

2.1. Example

The following example prints the sizes of various data types.

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
 
#define SIZEOF_BITS(type) (sizeof(type) * 8)
 
int main(void)
{
    /* Character types */
    printf("%-19s : %zu byte (%zu bit)\n", "char", sizeof(char), SIZEOF_BITS(char));
    printf("%-19s : %zu byte (%zu bit)\n", "signed char", sizeof(signed char), SIZEOF_BITS(signed char));
    printf("%-19s : %zu byte (%zu bit)\n", "unsigned char", sizeof(unsigned char), SIZEOF_BITS(unsigned char));
    printf("\n");
 
    /* Boolean type */
    printf("%-19s : %zu byte (%zu bit)\n", "bool", sizeof(bool), SIZEOF_BITS(bool));
    printf("\n");
    /* Integer types */
    printf("%-19s : %zu byte (%zu bit)\n", "short", sizeof(short), SIZEOF_BITS(short));
    printf("%-19s : %zu byte (%zu bit)\n", "unsigned short", sizeof(unsigned short), SIZEOF_BITS(unsigned short));
    printf("%-19s : %zu byte (%zu bit)\n", "int", sizeof(int), SIZEOF_BITS(int));
    printf("%-19s : %zu byte (%zu bit)\n", "unsigned int", sizeof(unsigned int), SIZEOF_BITS(unsigned int));
    printf("%-19s : %zu byte (%zu bit)\n", "long", sizeof(long), SIZEOF_BITS(long));
    printf("%-19s : %zu byte (%zu bit)\n", "unsigned long", sizeof(unsigned long), SIZEOF_BITS(unsigned long));
    printf("%-19s : %zu byte (%zu bit)\n", "long long", sizeof(long long), SIZEOF_BITS(long long));
    printf("%-19s : %zu byte (%zu bit)\n", "unsigned long long", sizeof(unsigned long long), SIZEOF_BITS(unsigned long long));
    printf("\n");
 
    /* Floating-point types */
    printf("%-19s : %zu byte (%zu bit)\n", "float", sizeof(float), SIZEOF_BITS(float));
    printf("%-19s : %zu byte (%zu bit)\n", "double", sizeof(double), SIZEOF_BITS(double));
    printf("%-19s : %zu byte (%zu bit)\n", "long double", sizeof(long double), SIZEOF_BITS(long double));
    printf("\n");
 
    /* Fixed-width integer types from stdint.h */
    printf("%-19s : %zu byte (%zu bit)\n", "int8_t", sizeof(int8_t), SIZEOF_BITS(int8_t));
    printf("%-19s : %zu byte (%zu bit)\n", "uint8_t", sizeof(uint8_t), SIZEOF_BITS(uint8_t));
    printf("%-19s : %zu byte (%zu bit)\n", "int16_t", sizeof(int16_t), SIZEOF_BITS(int16_t));
    printf("%-19s : %zu byte (%zu bit)\n", "uint16_t", sizeof(uint16_t), SIZEOF_BITS(uint16_t));
    printf("%-19s : %zu byte (%zu bit)\n", "int32_t", sizeof(int32_t), SIZEOF_BITS(int32_t));
    printf("%-19s : %zu byte (%zu bit)\n", "uint32_t", sizeof(uint32_t), SIZEOF_BITS(uint32_t));
    printf("%-19s : %zu byte (%zu bit)\n", "int64_t", sizeof(int64_t), SIZEOF_BITS(int64_t));
    printf("%-19s : %zu byte (%zu bit)\n", "uint64_t", sizeof(uint64_t), SIZEOF_BITS(uint64_t));
    printf("\n");
 
    return 0;
}

Ubuntu 24.04(x86_64)

char                : 1 byte (8 bit)
signed char         : 1 byte (8 bit)
unsigned char       : 1 byte (8 bit)
bool                : 1 byte (8 bit)
short               : 2 byte (16 bit)
unsigned short      : 2 byte (16 bit)
int                 : 4 byte (32 bit)
unsigned int        : 4 byte (32 bit)
long                : 8 byte (64 bit)
unsigned long       : 8 byte (64 bit)
long long           : 8 byte (64 bit)
unsigned long long  : 8 byte (64 bit)
float               : 4 byte (32 bit)
double              : 8 byte (64 bit)
long double         : 16 byte (128 bit)
int8_t              : 1 byte (8 bit)
uint8_t             : 1 byte (8 bit)
int16_t             : 2 byte (16 bit)
uint16_t            : 2 byte (16 bit)
int32_t             : 4 byte (32 bit)
uint32_t            : 4 byte (32 bit)
int64_t             : 8 byte (64 bit)
uint64_t            : 8 byte (64 bit)

※ The size of a data type may differ depending on the compiler implementation, OS, and architecture. → Use Fixed-width Integer Types

2.2. Void Type

  • void is a special type that means "no value."

It is used when a function has no parameters or no return value.

stdbool.h
void init(void); /* no parameters and no return value */

※ In pointers, void means a generic pointer (void *) whose data type is unspecified.

2.3. Boolean type

Based on C99

  • <stdbool.h> defines bool
  • It also provides the true / false macros
stdbool.h
#define bool _Bool
#define true 1
#define false 0
  • Its size is 1 byte, and internally it is an integer
    • _Bool is actually an unsigned char integer type
  • Any nonzero value is true
    • If an integer value is not 0, it is interpreted as true; if it is 0, it is interpreted as false.
    • You can assign a nonzero integer to bool, but it is automatically normalized to 0 or 1.

2.4. Fixed-width Types

Used to guarantee the same integer size regardless of platform or compiler.

stdint.h
typedef __int8_t int8_t;
typedef __int16_t int16_t;
typedef __int32_t int32_t;
typedef __int64_t int64_t;
typedef __uint8_t uint8_t;
typedef __uint16_t uint16_t;
typedef __uint32_t uint32_t;
typedef __uint64_t uint64_t;

Ubuntu 24.04(x86_64)

  
typedef signed char __int8_t;
typedef short __int16_t;
typedef int __int32_t;
typedef long long __int64_t;
typedef unsigned char __uint8_t;
typedef unsigned short __uint16_t;
typedef unsigned int __uint32_t;
typedef unsigned long long __uint64_t;

  • Fixed-width Integer Types are used to keep the bit width of integer types exactly fixed.
  • They always guarantee the same size and range even across different platforms and compilers.
  • For this reason, they are used in code where portability and binary compatibility are important.