Types and Variables
Every value in a running program occupies bytes in memory. C and Rust both force you to think about types, but Rust does it with stricter rules and stronger guarantees. This chapter maps the C type system onto Rust's so you can translate between them without hesitation.
Integer Types in C
C gives you a menu of integer types whose exact sizes are platform-dependent.
/* int_types.c */
#include <stdio.h>
#include <stdint.h>
int main(void)
{
char c = 'A';
short s = 32000;
int i = 2000000000;
long l = 2000000000L;
long long ll = 9000000000000000000LL;
unsigned int u = 4000000000U;
printf("char: %d (size: %zu)\n", c, sizeof(c));
printf("short: %d (size: %zu)\n", s, sizeof(s));
printf("int: %d (size: %zu)\n", i, sizeof(i));
printf("long: %ld (size: %zu)\n", l, sizeof(l));
printf("long long: %lld (size: %zu)\n", ll, sizeof(ll));
printf("unsigned: %u (size: %zu)\n", u, sizeof(u));
return 0;
}
$ gcc -Wall -o int_types int_types.c && ./int_types
char: 65 (size: 1)
short: 32000 (size: 2)
int: 2000000000 (size: 4)
long: 2000000000 (size: 8)
long long: 9000000000000000000 (size: 8)
unsigned: 4000000000 (size: 4)
The C standard only guarantees minimum sizes. int is at least 16 bits, long at
least 32, long long at least 64. For exact-width integers, use <stdint.h>:
int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t,
uint64_t. For object sizes, use size_t (unsigned, pointer-width).
Driver Prep: The Linux kernel uses fixed-width types extensively:
u8,u16,u32,u64,s8,s16,s32,s64. These map directly to the<stdint.h>types. Always prefer fixed-width types in systems code.
Integer Types in Rust
Rust makes the bit width part of the type name. No ambiguity.
// int_types.rs fn main() { let a: i8 = -128; let b: u8 = 255; let c: i16 = -32_768; let d: u16 = 65_535; let e: i32 = -2_147_483_647; let f: u32 = 4_294_967_295; let g: i64 = -9_223_372_036_854_775_807; let h: u64 = 18_446_744_073_709_551_615; let s: usize = 1024; println!("i8: {}", a); println!("u8: {}", b); println!("i32: {}", e); println!("u64: {}", h); println!("usize: {} (size: {} bytes)", s, std::mem::size_of::<usize>()); }
Note the underscores in numeric literals (65_535). Rust allows them for readability.
Type size comparison
+------------+----------+------------------+
| C type | Rust | Size (bytes) |
+------------+----------+------------------+
| char | i8 / u8 | 1 |
| short | i16 | 2 |
| int | i32 | 4 |
| long | i64* | 8 (on LP64) |
| long long | i64 | 8 |
| size_t | usize | pointer-width |
| ptrdiff_t | isize | pointer-width |
+------------+----------+------------------+
* C long is 4 bytes on Windows (LLP64), 8 on Linux (LP64).
Rust i64 is always 8 bytes.
Rust Note: Rust has no type whose size varies by platform except
usizeandisize, which are always pointer-width. Everything else is fixed. This eliminates an entire class of portability bugs.
Try It: In both C and Rust, print
sizeof(long)/size_of::<i64>()and confirm the sizes on your machine.
Floating-Point Types
C provides float (32-bit) and double (64-bit). Rust provides f32 and f64.
/* floats.c */
#include <stdio.h>
int main(void)
{
float f = 3.14f;
double d = 3.141592653589793;
printf("float: %.7f (size: %zu)\n", f, sizeof(f));
printf("double: %.15f (size: %zu)\n", d, sizeof(d));
return 0;
}
// floats.rs fn main() { let f: f32 = 3.14; let d: f64 = 3.141592653589793; println!("f32: {:.7} (size: {})", f, std::mem::size_of::<f32>()); println!("f64: {:.15} (size: {})", d, std::mem::size_of::<f64>()); }
Both follow IEEE 754. The behavior is identical at the bit level.
Characters
This is where C and Rust diverge sharply.
C: char is one byte
/* chars_c.c */
#include <stdio.h>
int main(void)
{
char c = 'A';
printf("char: %c, value: %d, size: %zu\n", c, c, sizeof(c));
return 0;
}
C's char is a single byte. It holds ASCII values (0-127). Whether char is signed
or unsigned is implementation-defined.
Rust: char is four bytes (a Unicode scalar value)
// chars_rust.rs fn main() { let c: char = 'A'; let heart: char = '\u{2764}'; let kanji: char = '\u{6F22}'; println!("char: {}, value: {}, size: {}", c, c as u32, std::mem::size_of::<char>()); println!("heart: {}, value: U+{:04X}", heart, heart as u32); println!("kanji: {}, value: U+{:04X}", kanji, kanji as u32); }
Rust Note: Rust's
charis a Unicode scalar value and always occupies 4 bytes. This is fundamentally different from C's 1-bytechar. In Rust,u8is the equivalent of C'scharwhen you want a raw byte.
C char 'A':
+----+
| 41 | 1 byte
+----+
Rust char 'A':
+----+----+----+----+
| 41 | 00 | 00 | 00 | 4 bytes (little-endian, Unicode scalar)
+----+----+----+----+
Booleans
C: Booleans are integers
/* bools_c.c */
#include <stdio.h>
#include <stdbool.h>
int main(void)
{
bool a = true;
bool b = false;
int n = 42;
printf("true: %d (size: %zu)\n", a, sizeof(a));
printf("false: %d (size: %zu)\n", b, sizeof(b));
if (n) {
printf("%d is truthy in C\n", n);
}
return 0;
}
In C, true is 1, false is 0, and any integer can be used where a boolean is
expected. Zero is false; everything else is true.
Rust: bool is a distinct type
// bools_rust.rs fn main() { let a: bool = true; let b: bool = false; let n: i32 = 42; println!("true: {} (size: {})", a, std::mem::size_of::<bool>()); println!("false: {} (size: {})", b, std::mem::size_of::<bool>()); // if n { } // ERROR: expected `bool`, found `i32` if n != 0 { println!("{} is non-zero", n); } }
Caution: C's implicit integer-to-boolean conversion is a source of bugs.
if (x = 0)(assignment, not comparison) evaluates to false and silently succeeds. Rust rejects this at compile time.
Constants
C: const and #define
/* constants_c.c */
#include <stdio.h>
#define MAX_BUFFER 1024
static const int MAX_RETRIES = 5;
int main(void)
{
printf("buffer size: %d\n", MAX_BUFFER);
printf("max retries: %d\n", MAX_RETRIES);
return 0;
}
#define performs textual substitution -- no type, no scope, no address.
const creates a typed, scoped value.
Rust: const and static
// constants_rust.rs const MAX_BUFFER: usize = 1024; // compile-time constant, inlined static MAX_RETRIES: i32 = 5; // fixed address in memory fn main() { println!("buffer size: {}", MAX_BUFFER); println!("max retries: {}", MAX_RETRIES); }
| Keyword | Compile-time? | Has address? | Mutable? |
|---|---|---|---|
C #define | Preprocessor | No | N/A |
C const | No | Yes | No |
Rust const | Yes | No (inlined) | No |
Rust static | No | Yes | No* |
(*) static mut exists but is unsafe to access.
Rust Note: Rust's
constis evaluated at compile time and inlined at every use site. Rust'sstaticlives at a fixed address for the entire program lifetime.
Variable Declaration and Mutability
C: mutable by default
/* mutability_c.c */
#include <stdio.h>
int main(void)
{
int x = 10;
x = 20; /* fine -- variables are mutable by default */
const int y = 30;
/* y = 40; */ /* error: assignment of read-only variable */
printf("x = %d, y = %d\n", x, y);
return 0;
}
Rust: immutable by default
// mutability_rust.rs fn main() { let x = 10; // x = 20; // error: cannot assign twice to immutable variable let mut y = 30; y = 40; // fine -- declared with `mut` println!("x = {}, y = {}", x, y); }
This is the opposite default. In C, you opt into immutability with const. In Rust,
you opt into mutability with mut.
Try It: In Rust, try reassigning an immutable variable. Read the compiler error message. Rust error messages are famously helpful -- get used to reading them.
Type Casting and Coercion
C: implicit and explicit casts
/* casting_c.c */
#include <stdio.h>
int main(void)
{
int i = 42;
double d = i; /* implicit: int -> double */
int j = (int)3.99; /* explicit: double -> int (truncation!) */
char c = 300; /* implicit: int -> char (overflow!) */
printf("d = %f\n", d);
printf("j = %d\n", j);
printf("c = %d\n", c);
return 0;
}
Caution: C silently narrows values.
char c = 300wraps around without error. The compiler may warn with-Wall, but it compiles.
Rust: explicit only
// casting_rust.rs fn main() { let i: i32 = 42; let d: f64 = i as f64; // explicit: i32 -> f64 let j: i32 = 3.99_f64 as i32; // explicit: f64 -> i32 (truncates to 3) let c: u8 = 300_u16 as u8; // explicit: wraps to 44 println!("d = {}", d); println!("j = {}", j); println!("c = {}", c); }
Rust requires as for every numeric conversion. No implicit narrowing or widening.
sizeof in C, size_of in Rust
/* sizeof_demo.c */
#include <stdio.h>
int main(void)
{
printf("char: %zu bytes\n", sizeof(char));
printf("int: %zu bytes\n", sizeof(int));
printf("long: %zu bytes\n", sizeof(long));
printf("double: %zu bytes\n", sizeof(double));
printf("void*: %zu bytes\n", sizeof(void *));
return 0;
}
// sizeof_demo.rs use std::mem::size_of; fn main() { println!("i8: {} bytes", size_of::<i8>()); println!("i32: {} bytes", size_of::<i32>()); println!("i64: {} bytes", size_of::<i64>()); println!("f64: {} bytes", size_of::<f64>()); println!("bool: {} bytes", size_of::<bool>()); println!("char: {} bytes", size_of::<char>()); println!("usize: {} bytes", size_of::<usize>()); }
Complete type size reference (64-bit Linux)
+---------------+-------+---------------+-------+
| C type | Bytes | Rust type | Bytes |
+---------------+-------+---------------+-------+
| char | 1 | i8 / u8 | 1 |
| short | 2 | i16 / u16 | 2 |
| int | 4 | i32 / u32 | 4 |
| long | 8 | i64 / u64 | 8 |
| long long | 8 | i64 / u64 | 8 |
| (none) | 16 | i128 / u128 | 16 |
| float | 4 | f32 | 4 |
| double | 8 | f64 | 8 |
| _Bool | 1 | bool | 1 |
| char | 1 | char | 4 |
| void* | 8 | *const T | 8 |
| size_t | 8 | usize | 8 |
+---------------+-------+---------------+-------+
Integer Overflow
C: undefined behavior for signed, wrapping for unsigned
/* overflow_c.c */
#include <stdio.h>
#include <limits.h>
int main(void)
{
unsigned int u = UINT_MAX;
printf("UINT_MAX: %u\n", u);
printf("UINT_MAX + 1: %u\n", u + 1); /* wraps to 0 -- defined behavior */
int s = INT_MAX;
printf("INT_MAX: %d\n", s);
/* s + 1 is UNDEFINED BEHAVIOR for signed integers */
printf("INT_MAX + 1: %d\n", s + 1);
return 0;
}
Caution: Signed integer overflow in C is undefined behavior. The compiler is allowed to assume it never happens, and it may optimize your code in surprising ways based on that assumption.
Rust: panics in debug, wraps in release
// overflow_rust.rs fn main() { let u: u32 = u32::MAX; // Use wrapping_add for explicit wrapping: let v = u.wrapping_add(1); println!("u32::MAX wrapping_add(1) = {}", v); // Use checked_add to detect overflow: match u.checked_add(1) { Some(val) => println!("result: {}", val), None => println!("overflow detected!"), } // Use saturating_add to clamp at max: let w = u.saturating_add(1); println!("u32::MAX saturating_add(1) = {}", w); }
$ rustc overflow_rust.rs && ./overflow_rust
u32::MAX wrapping_add(1) = 0
overflow detected!
u32::MAX saturating_add(1) = 4294967295
Rust Note: Rust gives you four explicit choices for overflow:
wrapping_*,checked_*,saturating_*, andoverflowing_*. In debug builds, the standard+operator panics on overflow. In release builds, it wraps. There is no undefined behavior.
Quick Knowledge Check
- What is the size of
charin C versuscharin Rust? - What happens when you add 1 to
INT_MAXin C? In Rust (debug mode)? - How do you declare a mutable variable in Rust?
Common Pitfalls
- Assuming
intis always 32 bits. The C standard only guarantees at least 16. Useint32_twhen you need exactly 32 bits. - Forgetting that C's
charsignedness is implementation-defined. On ARM,charis unsigned. On x86, it is signed. Usesigned charorunsigned charto be explicit. - Using
%dto printsize_t. Use%zu. The wrong format specifier is undefined behavior. - Implicit narrowing in C. Assigning a
longto anintsilently truncates. Rust forces you to writeas i32. - Forgetting
mutin Rust. Variables are immutable by default. The compiler error is clear, but it catches newcomers off guard.