Hello World & Build Tools
Hello, World
fn main() {
println!("Hello, World!");
} #include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
} C uses
printf from <stdio.h> where Rust uses the println! macro. The \n escape must be written explicitly — printf does not add a newline automatically. The int main(void) signature is the C17 convention; return 0 signals success to the operating system. Every C source file that uses printf must include <stdio.h>.Build tools
// Cargo (built into Rust):
// cargo new myproject — create new project
// cargo build — debug build → target/debug/
// cargo build --release — optimized build → target/release/
// cargo run — build + run
// cargo test — run tests
// cargo add serde — add a dependency (Cargo.toml + Cargo.lock)
// cargo fmt — format code (rustfmt)
// cargo clippy — lint // GCC directly (no standard build tool):
// gcc -std=c17 -Wall -Wextra -o hello hello.c
// ./hello
//
// With math library:
// gcc -std=c17 -lm -o calc calc.c
//
// Optimized release:
// gcc -std=c17 -O2 -o hello hello.c
//
// Make (common build system):
// make — runs the default target in Makefile
// make clean — remove built files
//
// CMake (larger projects):
// cmake -S . -B build && cmake --build build C has no single official build tool — projects use
make, CMake, Meson, or raw gcc invocations. Cargo integrates dependency management, building, testing, and formatting in one tool; C's ecosystem splits these across separate programs. The -Wall -Wextra flags enable warnings that catch many common mistakes and should always be used during development.Formatted output
fn main() {
let name = "Alice";
let score = 42;
let ratio = 0.75_f64;
println!("{name} scored {score} ({:.1}%)", ratio * 100.0);
eprintln!("debug: score={score}"); // stderr
} #include <stdio.h>
int main(void) {
const char *name = "Alice";
int score = 42;
double ratio = 0.75;
printf("%s scored %d (%.1f%%)\n", name, score, ratio * 100.0);
fprintf(stderr, "debug: score=%d\n", score);
return 0;
} printf uses C-style format specifiers: %s for strings, %d for integers, %.1f for floats with one decimal place. A literal % is written as %%. Rust's format strings use {} with optional specifiers like {:.1}. fprintf(stderr, ...) is the C equivalent of Rust's eprintln! — it writes to the standard error stream.Variables & Types
Variable declarations
fn main() {
let message = "hello"; // immutable, type inferred
let mut counter = 0; // mutable
let count: i32 = 42; // explicit type
const MAX_SIZE: usize = 100; // compile-time constant
counter += 1;
println!("{message} {counter} {count} {MAX_SIZE}");
} #include <stdio.h>
#define MAX_SIZE 100
int main(void) {
const char *message = "hello"; // immutable pointer to string literal
int counter = 0; // mutable by default
int count = 42; // type always required
counter += 1;
printf("%s %d %d %d\n", message, counter, count, MAX_SIZE);
return 0;
} In C, all variables are mutable by default — there is no
let/let mut distinction. Use const to declare a runtime constant; use #define for compile-time substitution (the preprocessor replaces the name with its value before compilation). C has no type inference — every variable declaration requires an explicit type.Integer types
fn main() {
let byte: i8 = 127;
let medium: i32 = 2_147_483_647;
let large: i64 = 9_223_372_036_854_775_807;
let unsigned: u32 = 4_294_967_295;
let size: usize = 42; // pointer-sized
println!("{byte} {medium} {large} {unsigned} {size}");
} #include <stdio.h>
#include <stdint.h>
int main(void) {
int8_t byte_val = 127;
int32_t medium = 2147483647;
int64_t large = 9223372036854775807LL;
uint32_t unsigned_val = 4294967295U;
size_t size_val = 42; // pointer-sized
printf("%d %d %lld %u %zu\n",
byte_val, medium, large, unsigned_val, size_val);
return 0;
} C's built-in
int has a platform-dependent width (at least 16 bits). For the fixed-width types that map directly to Rust's i8, i32, u64, etc., include <stdint.h> (C99+). Use size_t for sizes and array indices — it is unsigned and pointer-sized, matching Rust's usize. Format specifiers differ by type: %d for int, %lld for int64_t, %zu for size_t.Floating point
fn main() {
let single: f32 = 3.14_f32;
let precision: f64 = 3.141_592_653_589_793;
let result = precision.sqrt();
println!("{single:.4}");
println!("{precision:.10}");
println!("{result:.6}");
} #include <stdio.h>
#include <math.h>
int main(void) {
float single = 3.14f;
double precision = 3.141592653589793;
double result = sqrt(precision);
printf("%.4f\n", single);
printf("%.10f\n", precision);
printf("%.6f\n", result);
return 0;
} C uses
float (32-bit) and double (64-bit) matching Rust's f32/f64. Float literals require the f suffix (3.14f); without it, the literal is a double. Math functions like sqrt come from <math.h> and must be linked with -lm. In Rust, math is a method: x.sqrt(); in C it is a free function: sqrt(x).Boolean
fn main() {
let is_ready = true;
let is_done = false;
let combined = is_ready && !is_done;
println!("{is_ready} {is_done} {combined}");
// Booleans print as "true"/"false"
} #include <stdio.h>
#include <stdbool.h>
int main(void) {
bool is_ready = true;
bool is_done = false;
bool combined = is_ready && !is_done;
// No %b — use ternary to convert to string
printf("%s %s %s\n",
is_ready ? "true" : "false",
is_done ? "true" : "false",
combined ? "true" : "false");
return 0;
} C had no boolean type until C99, when
<stdbool.h> added bool, true, and false. Before that, programmers used int (0 = false, non-zero = true), and many C APIs still use that convention. printf has no %b specifier for booleans — the ternary condition ? "true" : "false" is the idiomatic way to print them.Type casting
fn main() {
let integer: i32 = 65;
let character = integer as u8 as char; // explicit cast chain
let float_val: f64 = integer as f64;
let truncated = 3.9_f64 as i32; // truncates, never rounds
println!("{character} {float_val} {truncated}");
} #include <stdio.h>
int main(void) {
int integer = 65;
char character = (char)integer; // explicit cast
double float_val = (double)integer; // widening
int truncated = (int)3.9; // truncates toward zero
printf("%c %f %d\n", character, float_val, truncated);
return 0;
} C casts use the
(type)expression syntax; Rust uses value as Type. Both truncate when converting a float to an integer. The key difference is safety: in Rust, as never causes undefined behavior — out-of-range values wrap or saturate predictably. In C, casting a value outside the target type's range gives implementation-defined behavior for signed types and wraps for unsigned types.Strings
String literals
fn main() {
let greeting: &str = "Hello"; // borrowed slice — immutable
let owned: String = String::from("World"); // heap-allocated
let combined = format!("{greeting}, {owned}!");
println!("{combined}");
println!("length: {} bytes", greeting.len());
} #include <stdio.h>
#include <string.h>
int main(void) {
const char *greeting = "Hello"; // pointer to read-only literal
char owned[32] = "World"; // stack copy — mutable
char combined[64];
snprintf(combined, sizeof(combined), "%s, %s!", greeting, owned);
printf("%s\n", combined);
printf("length: %zu bytes\n", strlen(greeting));
return 0;
} In C, a string literal like
"Hello" is a const char* pointer to read-only memory — modifying it is undefined behavior. To get a mutable string, declare a char array (char owned[32] = "World"), which copies the literal onto the stack. All C strings are null-terminated ('\0' at the end); there is no &str/String distinction, only char*.String length
fn main() {
let message = "Hello!";
println!("bytes: {}", message.len());
println!("chars: {}", message.chars().count());
println!("empty: {}", message.is_empty());
} #include <stdio.h>
#include <string.h>
int main(void) {
const char *message = "Hello!";
printf("bytes: %zu\n", strlen(message));
printf("empty: %s\n", strlen(message) == 0 ? "true" : "false");
return 0;
} strlen counts bytes up to (but not including) the null terminator — it has no concept of Unicode code points. For ASCII strings this equals the character count; for UTF-8 strings with multi-byte characters it counts bytes, not characters. There is no standard C function for counting Unicode code points. Unlike Rust's len(), which is O(1) because &str stores its length, strlen is O(n) — it scans until it finds '\0'.String comparison
fn main() {
let first = "apple";
let second = "banana";
println!("{}", first == second); // false
println!("{}", first < second); // true (lexicographic)
println!("{}", first.starts_with("app")); // true
println!("{}", first.contains("ple")); // true
} #include <stdio.h>
#include <string.h>
int main(void) {
const char *first = "apple";
const char *second = "banana";
printf("%s\n", strcmp(first, second) == 0 ? "equal" : "not equal");
printf("%s\n", strcmp(first, second) < 0 ? "true" : "false");
printf("%s\n", strncmp(first, "app", 3) == 0 ? "true" : "false");
printf("%s\n", strstr(first, "ple") != NULL ? "true" : "false");
return 0;
} Never compare C strings with
== — it compares pointer addresses, not content, and will be false even for identical strings that are different pointers. Always use strcmp for content equality. strcmp returns 0 for equality, a negative value if first < second, positive if greater. strstr returns a pointer to the first occurrence of the substring, or NULL if not found.String formatting
fn main() {
let name = "Alice";
let score = 95;
let message = format!("{name} scored {score}%");
println!("{message}");
let greeting = String::from("Hello") + ", " + name + "!";
println!("{greeting}");
} #include <stdio.h>
#include <string.h>
int main(void) {
const char *name = "Alice";
int score = 95;
char message[64];
snprintf(message, sizeof(message), "%s scored %d%%", name, score);
printf("%s\n", message);
char greeting[64] = "Hello";
strncat(greeting, ", Alice!", sizeof(greeting) - strlen(greeting) - 1);
printf("%s\n", greeting);
return 0;
} snprintf is the safe alternative to sprintf — it always writes a null terminator and never writes beyond the buffer size. Use it for all string formatting in C. The second argument is the total buffer size including the null terminator. Rust's format! returns a String that grows as needed; C requires pre-allocating a buffer large enough for the result.String ↔ integer
fn main() {
let number: i32 = "42".parse().unwrap();
let text = 42_i32.to_string();
println!("{number} {text}");
// Error handling:
match "not-a-number".parse::<i32>() {
Ok(value) => println!("{value}"),
Err(error) => println!("parse error: {error}"),
}
} #include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
// String to integer
long number = strtol("42", NULL, 10);
printf("%ld\n", number);
// Integer to string
char text[16];
snprintf(text, sizeof(text), "%d", 42);
printf("%s\n", text);
// Error detection
char *endptr;
errno = 0;
long invalid = strtol("not-a-number", &endptr, 10);
if (endptr == "not-a-number" || *endptr != '\0') {
printf("parse error\n");
}
return 0;
} Prefer
strtol over the simpler atoi — atoi returns 0 for invalid input with no way to detect the error, while strtol sets endptr to the first non-numeric character. If endptr equals the input string start or does not point to '\0', parsing failed. Rust's parse() returns Result<T, _> — the error case is part of the type and the compiler forces you to handle it.Arrays
Array declaration
fn main() {
let numbers = [1, 2, 3, 4, 5]; // fixed array, immutable
let mut mutable_numbers = [0i32; 5]; // 5 zeros, mutable
mutable_numbers[0] = 10;
println!("{numbers:?}");
println!("{mutable_numbers:?}");
} #include <stdio.h>
int main(void) {
int numbers[5] = {1, 2, 3, 4, 5};
int mutable_numbers[5] = {0}; // all zeros
mutable_numbers[0] = 10;
for (int index = 0; index < 5; index++) {
printf("%d%s", numbers[index], index < 4 ? " " : "\n");
}
for (int index = 0; index < 5; index++) {
printf("%d%s", mutable_numbers[index], index < 4 ? " " : "\n");
}
return 0;
} C arrays are raw memory blocks with no embedded length.
int numbers[5] allocates five consecutive integers on the stack. There is no bounds checking — numbers[10] on a five-element array silently reads or writes memory past the array (undefined behavior). Rust panics on out-of-bounds access in debug mode and performs the check in release builds too. {0} initializes the first element to 0 and zero-fills the rest.Array length
fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("length: {}", numbers.len());
let total: i32 = numbers.iter().sum();
println!("sum: {total}");
} #include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
size_t length = sizeof(numbers) / sizeof(numbers[0]);
printf("length: %zu\n", length);
int total = 0;
for (size_t index = 0; index < length; index++) {
total += numbers[index];
}
printf("sum: %d\n", total);
return 0;
} sizeof(array) / sizeof(array[0]) computes the element count, but only when the array name is in scope — once an array is passed to a function and decays to a pointer, its length is lost. This is why C functions that operate on arrays always take a separate count or length parameter. Rust's len() is always available because slices carry their length.Multidimensional arrays
fn main() {
let matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]];
for row in &matrix {
for value in row {
print!("{value:3}");
}
println!();
}
} #include <stdio.h>
int main(void) {
int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 3; column++) {
printf("%3d", matrix[row][column]);
}
printf("\n");
}
return 0;
} C multidimensional arrays are laid out in row-major order in contiguous memory —
matrix[row][column] is at offset row * 3 + column. When passing a 2D array to a function, all dimensions except the first must be specified in the parameter type: void print_matrix(int matrix[][3], int rows). Rust's nested arrays work the same way, but for row in &matrix iterates over references to the inner arrays cleanly.Slices vs pointer + length
fn sum_values(values: &[i32]) -> i32 {
values.iter().sum()
}
fn main() {
let numbers = [1, 2, 3, 4, 5];
let partial = &numbers[1..4]; // slice: [2, 3, 4]
println!("{}", sum_values(&numbers));
println!("{}", sum_values(partial));
} #include <stdio.h>
int sum_values(const int *values, size_t count) {
int total = 0;
for (size_t index = 0; index < count; index++) {
total += values[index];
}
return total;
}
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
size_t length = sizeof(numbers) / sizeof(numbers[0]);
printf("%d\n", sum_values(numbers, length));
printf("%d\n", sum_values(numbers + 1, 3)); // sub-array: [2, 3, 4]
return 0;
} C has no slice type — a "slice" is expressed as a pointer to the first element plus a separate count.
numbers + 1 is pointer arithmetic: it points to the second element, one sizeof(int) byte past the start. Passing the wrong count is a common bug in C because the compiler cannot detect it. Rust's slice (&[i32]) carries both the pointer and the length in one value, and bounds are always checked.Control Flow
if / else
fn main() {
let temperature = 22;
// if is an expression in Rust
let description = if temperature > 30 {
"hot"
} else if temperature > 20 {
"warm"
} else {
"cool"
};
println!("{description}");
} #include <stdio.h>
int main(void) {
int temperature = 22;
// if is a statement in C — cannot return a value
const char *description;
if (temperature > 30) {
description = "hot";
} else if (temperature > 20) {
description = "warm";
} else {
description = "cool";
}
printf("%s\n", description);
return 0;
} In Rust,
if is an expression that produces a value — every arm returns the same type. In C, if is a statement and cannot be used on the right side of an assignment. A simple conditional assignment can use the ternary operator condition ? a : b, but multi-statement bodies must use an if/else block with a pre-declared variable.for loop
fn main() {
// Range-based
for index in 0..5 {
print!("{index} ");
}
println!();
// Iterate over a collection
let fruits = ["apple", "banana", "cherry"];
for fruit in &fruits {
println!("{fruit}");
}
} #include <stdio.h>
int main(void) {
// Classic three-part for loop
for (int index = 0; index < 5; index++) {
printf("%d ", index);
}
printf("\n");
// Array traversal (no range-based for in C89; C++ has it)
const char *fruits[] = {"apple", "banana", "cherry"};
size_t count = sizeof(fruits) / sizeof(fruits[0]);
for (size_t index = 0; index < count; index++) {
printf("%s\n", fruits[index]);
}
return 0;
} C's
for (init; condition; increment) is the classic three-part loop. There is no for element in collection syntax — array traversal always requires manual indexing. C has no iterator protocol; Rust's for x in iter works with any type that implements IntoIterator. C++11 added range-based for, but standard C does not have it.while / do–while
fn main() {
let mut count = 0;
while count < 5 {
print!("{count} ");
count += 1;
}
println!();
// Rust uses loop + break for do-while semantics
let mut value = 10;
loop {
println!("{value}");
value -= 3;
if value <= 0 { break; }
}
} #include <stdio.h>
int main(void) {
int count = 0;
while (count < 5) {
printf("%d ", count);
count++;
}
printf("\n");
// C has a native do-while
int value = 10;
do {
printf("%d\n", value);
value -= 3;
} while (value > 0);
return 0;
} C has a native
do { ... } while (condition) loop that always runs the body at least once. Rust achieves the same pattern with loop { ...; if cond { break; } }. C's while loop is otherwise identical to Rust's. Note that C uses count++ (postfix increment) as a shorthand for count += 1; Rust dropped ++ because it was a source of subtle bugs.switch vs match
fn main() {
let day = 3;
let name = match day {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
_ => "Weekend",
};
println!("{name}");
} #include <stdio.h>
int main(void) {
int day = 3;
const char *name;
switch (day) {
case 1: name = "Monday"; break;
case 2: name = "Tuesday"; break;
case 3: name = "Wednesday"; break;
case 4: name = "Thursday"; break;
case 5: name = "Friday"; break;
default: name = "Weekend"; break;
}
printf("%s\n", name);
return 0;
} C's
switch falls through to the next case unless you add break — forgetting a break is one of the most common C bugs. Rust's match never falls through — each arm is independent. switch only works on integer and character types; Rust's match works on any type and supports patterns, guards, ranges, and destructuring. The compiler warns about unhandled enum variants in match; in C, switch silently ignores missing cases (use -Wswitch to opt into a warning).break / continue (nested loops)
fn main() {
'outer: for row in 0..4 {
for column in 0..4 {
if column == 2 { continue; }
if row == 2 && column == 1 { break 'outer; }
print!("({row},{column}) ");
}
}
println!();
} #include <stdio.h>
int main(void) {
int done = 0;
for (int row = 0; row < 4 && !done; row++) {
for (int column = 0; column < 4; column++) {
if (column == 2) { continue; }
if (row == 2 && column == 1) { done = 1; break; }
printf("(%d,%d) ", row, column);
}
}
printf("\n");
return 0;
} C's
break and continue apply only to the immediately enclosing loop — there are no labeled loops. Breaking out of nested loops requires a flag variable, a goto, or restructuring into a function that returns early. Rust's labeled loops ('outer:) allow break 'outer to exit any enclosing loop by name, without a flag or goto.Functions & Pointers
Function definition
fn add(first: i32, second: i32) -> i32 {
first + second // implicit return (last expression)
}
fn main() {
println!("{}", add(3, 4));
// Can also use explicit return:
// return add(3, 4);
} #include <stdio.h>
int add(int first, int second) {
return first + second; // explicit return required
}
int main(void) {
printf("%d\n", add(3, 4));
return 0;
} C requires an explicit
return statement — there is no implicit last-expression return. Functions declared with no return type default to int in old C; in C17, always write the return type explicitly. A void function returns nothing and omits the return or uses return; to exit early. Function prototypes (declarations without bodies) are needed when a function is called before its definition in the file.Multiple return values
fn min_max(values: &[i32]) -> (i32, i32) {
let minimum = *values.iter().min().unwrap();
let maximum = *values.iter().max().unwrap();
(minimum, maximum)
}
fn main() {
let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
let (minimum, maximum) = min_max(&numbers);
println!("min={minimum}, max={maximum}");
} #include <stdio.h>
void min_max(const int *values, size_t count,
int *minimum, int *maximum) {
*minimum = values[0];
*maximum = values[0];
for (size_t index = 1; index < count; index++) {
if (values[index] < *minimum) *minimum = values[index];
if (values[index] > *maximum) *maximum = values[index];
}
}
int main(void) {
int numbers[] = {3, 1, 4, 1, 5, 9, 2, 6};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
int minimum, maximum;
min_max(numbers, count, &minimum, &maximum);
printf("min=%d, max=%d\n", minimum, maximum);
return 0;
} C functions return only one value. To return multiple values, pass pointers as output parameters — the caller allocates the variables and passes their addresses; the function writes through the pointers. This is explicit about what the function modifies, but more verbose than Rust's tuple return. Alternatively, return a struct containing all the values, which is idiomatic in modern C.
Function pointers vs closures
fn double_value(value: i32) -> i32 { value * 2 }
fn triple_value(value: i32) -> i32 { value * 3 }
fn apply(value: i32, transform: fn(i32) -> i32) -> i32 {
transform(value)
}
fn main() {
println!("{}", apply(5, double_value)); // 10
// Closures capture from the environment
let multiplier = 4;
let quadruple = |value: i32| value * multiplier;
println!("{}", quadruple(5)); // 20
} #include <stdio.h>
int double_value(int value) { return value * 2; }
int triple_value(int value) { return value * 3; }
int apply(int value, int (*transform)(int)) {
return transform(value);
}
int main(void) {
printf("%d\n", apply(5, double_value)); // 10
printf("%d\n", apply(5, triple_value)); // 15
// No closures in C — function pointers cannot capture variables
return 0;
} C has function pointers (
int (*transform)(int)), but they cannot capture surrounding variables — a function pointer is just an address in code memory, not a bundle of code + captured state. To simulate closures in C, callbacks conventionally take a void *context parameter alongside the function pointer: void callback(void *context, int value). Rust's closures automatically capture by reference or by value — no manual context parameter needed.Variadic functions
fn main() {
// Rust has no traditional variadic functions in safe code.
// Use slices, iterators, or macros instead.
let numbers = [1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
println!("sum = {total}");
// println! itself is a variadic macro, not a variadic function.
} #include <stdio.h>
#include <stdarg.h>
int sum_ints(int count, ...) {
va_list arguments;
va_start(arguments, count);
int total = 0;
for (int index = 0; index < count; index++) {
total += va_arg(arguments, int);
}
va_end(arguments);
return total;
}
int main(void) {
printf("sum = %d\n", sum_ints(5, 1, 2, 3, 4, 5));
return 0;
} C's variadic functions use
<stdarg.h> — the caller must tell the function how many arguments were passed (or encode the count in a sentinel or format string). There is no runtime introspection. Passing the wrong count or type is undefined behavior; printf trusts the format string to infer argument types, which is why gcc -Wall checks format strings at compile time. Rust avoids variadic functions in safe code; variadic FFI (extern "C") is available for C interoperability.Structs
Struct definition
struct Point {
x: f64,
y: f64,
}
impl Point {
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance_to_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let point = Point::new(3.0, 4.0);
println!("distance: {:.1}", point.distance_to_origin());
} #include <stdio.h>
#include <math.h>
typedef struct {
double x;
double y;
} Point;
Point point_new(double x, double y) {
Point point = {x, y};
return point;
}
double point_distance_to_origin(const Point *point) {
return sqrt(point->x * point->x + point->y * point->y);
}
int main(void) {
Point point = point_new(3.0, 4.0);
printf("distance: %.1f\n", point_distance_to_origin(&point));
return 0;
} C structs are pure data containers — they have no methods. All "methods" are free functions that take a pointer to the struct as their first argument (the
const Point * parameter mirrors Rust's &self). typedef struct { ... } Name defines the struct and an alias in one step so you can write Point instead of struct Point. The -> operator dereferences a pointer and accesses a field: point->x is shorthand for (*point).x.Struct initialization
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
let bob = Person { name: String::from("Bob"), age: alice.age + 5 };
println!("{alice:?}");
println!("{} is {}", bob.name, bob.age);
} #include <stdio.h>
typedef struct {
const char *name;
unsigned int age;
} Person;
int main(void) {
// Positional initializer
Person alice = {"Alice", 30};
// C17 designated initializer (recommended for clarity)
Person bob = {.name = "Bob", .age = alice.age + 5};
printf("Person { name: \"%s\", age: %u }\n", alice.name, alice.age);
printf("%s is %u\n", bob.name, bob.age);
return 0;
} C17 supports designated initializers (
.name = "Bob") that initialize fields by name in any order — unspecified fields default to zero. Without designators, fields are initialized positionally. C has no #[derive(Debug)]; printing a struct requires manually writing a format string. On the stack, uninitialised struct members contain garbage; zero-initialization with {0} or memset is good practice.Methods and associated functions
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
fn area(&self) -> f64 { self.width * self.height }
fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) }
}
fn main() {
let rectangle = Rectangle::new(10.0, 5.0);
println!("area: {} perimeter: {}", rectangle.area(), rectangle.perimeter());
} #include <stdio.h>
typedef struct {
double width;
double height;
} Rectangle;
Rectangle rectangle_new(double width, double height) {
Rectangle rect = {width, height};
return rect;
}
double rectangle_area(const Rectangle *rect) {
return rect->width * rect->height;
}
double rectangle_perimeter(const Rectangle *rect) {
return 2.0 * (rect->width + rect->height);
}
int main(void) {
Rectangle rect = rectangle_new(10.0, 5.0);
printf("area: %.1f perimeter: %.1f\n",
rectangle_area(&rect), rectangle_perimeter(&rect));
return 0;
} The C convention for "methods" is to name the function with a
structname_methodname prefix and take a pointer to the struct as the first parameter. Callers pass &rect explicitly. Rust's impl block binds functions to a type so they are called with dot syntax (rect.area()); C has no such binding. This naming convention is enforced only by discipline — the compiler does not enforce it.Enums & Unions
Basic enums
#[derive(Debug, PartialEq)]
enum Direction { North, South, East, West }
fn describe(direction: &Direction) -> &str {
match direction {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
}
}
fn main() {
let heading = Direction::North;
println!("{}", describe(&heading));
println!("{heading:?}");
} #include <stdio.h>
typedef enum {
DIRECTION_NORTH,
DIRECTION_SOUTH,
DIRECTION_EAST,
DIRECTION_WEST,
} Direction;
const char *direction_describe(Direction direction) {
switch (direction) {
case DIRECTION_NORTH: return "heading north";
case DIRECTION_SOUTH: return "heading south";
case DIRECTION_EAST: return "heading east";
case DIRECTION_WEST: return "heading west";
default: return "unknown";
}
}
int main(void) {
Direction heading = DIRECTION_NORTH;
printf("%s\n", direction_describe(heading));
printf("%d\n", heading); // prints 0 — enums are just ints
return 0;
} C enums are simply named integer constants —
DIRECTION_NORTH is 0, DIRECTION_SOUTH is 1, and so on. They provide no type safety: a variable of type Direction can hold any int value including 99, with no warning. Rust enums are algebraic types with strict exhaustiveness checking in match — the compiler rejects a match that misses any variant. Use uppercase prefixes (DIRECTION_NORTH) to avoid name collisions with variables since C has no namespacing for enum members.Tagged unions (sum types)
use std::f64::consts::PI;
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => PI * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(5.0);
let rectangle = Shape::Rectangle(3.0, 4.0);
println!("circle: {:.2}", area(&circle));
println!("rectangle: {:.2}", area(&rectangle));
} #include <stdio.h>
#include <math.h>
#define MY_PI 3.14159265358979323846
typedef enum { SHAPE_CIRCLE, SHAPE_RECTANGLE } ShapeKind;
typedef struct {
ShapeKind kind;
union {
double radius;
struct { double width; double height; } rectangle;
} data;
} Shape;
double shape_area(const Shape *shape) {
switch (shape->kind) {
case SHAPE_CIRCLE:
return MY_PI * shape->data.radius * shape->data.radius;
case SHAPE_RECTANGLE:
return shape->data.rectangle.width * shape->data.rectangle.height;
default: return 0.0;
}
}
int main(void) {
Shape circle = {SHAPE_CIRCLE, {.radius = 5.0}};
Shape rectangle = {SHAPE_RECTANGLE, {.rectangle = {3.0, 4.0}}};
printf("circle: %.2f\n", shape_area(&circle));
printf("rectangle: %.2f\n", shape_area(&rectangle));
return 0;
} C has no native sum types. The standard idiom combines a discriminant enum (the "tag") with a
union for the variant data in a single struct. Reading the wrong union member is undefined behavior — C trusts you to check the tag first. Rust's enum is a safe tagged union: the compiler guarantees that match is exhaustive and prevents accessing non-active variants entirely.Pointers
Pointer basics
fn main() {
let value = 42;
let reference = &value; // immutable reference
println!("{value} {reference}"); // auto-deref in format
println!("{}", *reference); // explicit deref
let mut mutable = 10;
let mutable_ref = &mut mutable;
*mutable_ref += 5;
println!("{mutable}"); // 15
} #include <stdio.h>
int main(void) {
int value = 42;
const int *pointer = &value; // pointer to int (read-only)
printf("%d %d\n", value, *pointer);
int mutable_value = 10;
int *mutable_pointer = &mutable_value;
*mutable_pointer += 5;
printf("%d\n", mutable_value); // 15
return 0;
} In Rust,
& creates a reference; in C, & takes the address of a variable, producing a pointer. Both produce the memory address of the variable. The key difference is safety: Rust's borrow checker enforces at compile time that references don't outlive their data (no dangling pointers) and that mutable and immutable references cannot coexist (no data races). In C, none of these rules are enforced — every pointer access is a leap of faith.Pointer arithmetic
fn main() {
let numbers = [10, 20, 30, 40, 50];
// Rust uses indexing and iterators, not raw pointer arithmetic
for (index, value) in numbers.iter().enumerate() {
print!("{value} ");
}
println!();
// Raw pointer arithmetic requires unsafe
unsafe {
let pointer = numbers.as_ptr();
println!("{}", *pointer.add(2)); // 30
}
} #include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
size_t length = sizeof(numbers) / sizeof(numbers[0]);
// Pointer arithmetic is normal C code
int *pointer = numbers;
for (size_t index = 0; index < length; index++) {
printf("%d ", *(pointer + index));
}
printf("\n");
printf("%d\n", *(pointer + 2)); // 30
return 0;
} In C, pointer arithmetic is ordinary code — adding
n to a pointer advances it by n * sizeof(type) bytes. It is the idiomatic way to traverse arrays; numbers + 1 points to the second element. In Rust, raw pointer arithmetic requires an unsafe block; the normal approach is iterators or indexing. Going past the end of an array is undefined behavior in C; Rust's slice indexing panics on out-of-bounds access.NULL vs Option
fn find_first_even(numbers: &[i32]) -> Option<i32> {
numbers.iter().find(|&&value| value % 2 == 0).copied()
}
fn main() {
let numbers = [1, 3, 4, 7, 8];
match find_first_even(&numbers) {
Some(value) => println!("found: {value}"),
None => println!("not found"),
}
} #include <stdio.h>
const int *find_first_even(const int *numbers, size_t count) {
for (size_t index = 0; index < count; index++) {
if (numbers[index] % 2 == 0) {
return &numbers[index];
}
}
return NULL; // not found
}
int main(void) {
int numbers[] = {1, 3, 4, 7, 8};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
const int *result = find_first_even(numbers, count);
if (result != NULL) {
printf("found: %d\n", *result);
} else {
printf("not found\n");
}
return 0;
} C uses
NULL (a null pointer) to represent "no value" — dereferencing NULL causes a segmentation fault. Every pointer can be NULL, and checking for it is the programmer's responsibility. Rust's Option<T> makes absence explicit in the type: None is a real value, not a special pointer. The compiler forces you to handle None before accessing the inner value — there is no null pointer in safe Rust.Memory Management
Stack vs heap
fn main() {
// Stack: lives until scope ends, no allocation needed
let stack_value = 42;
// Heap: Box<T> owns the memory; freed when Box drops
let heap_value = Box::new(42);
println!("stack: {stack_value}");
println!("heap: {heap_value}");
// heap_value is automatically freed here
} #include <stdio.h>
#include <stdlib.h>
int main(void) {
// Stack: automatic storage, no allocation needed
int stack_value = 42;
// Heap: manual allocation with malloc
int *heap_value = malloc(sizeof(int));
if (heap_value == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
*heap_value = 42;
printf("stack: %d\n", stack_value);
printf("heap: %d\n", *heap_value);
free(heap_value); // must call free — or the memory leaks
return 0;
} Rust's
Box<T> is a heap pointer with automatic cleanup — the Drop trait calls free when the Box goes out of scope. In C, malloc and free must always be paired manually. Forgetting free causes a memory leak; calling it twice (double-free) corrupts the heap; using a pointer after free (use-after-free) is undefined behavior. All three are silent: the compiler does not warn about any of them.Dynamic array (Vec equivalent)
fn main() {
let mut items: Vec<i32> = Vec::new();
items.push(10);
items.push(20);
items.push(30);
println!("length: {}, capacity: {}", items.len(), items.capacity());
for item in &items {
print!("{item} ");
}
println!();
} // items freed automatically #include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t length = 0;
size_t capacity = 4;
int *items = malloc(capacity * sizeof(int));
if (items == NULL) { return 1; }
int new_values[] = {10, 20, 30};
for (size_t index = 0; index < 3; index++) {
if (length == capacity) {
capacity *= 2;
items = realloc(items, capacity * sizeof(int));
if (items == NULL) { return 1; }
}
items[length++] = new_values[index];
}
printf("length: %zu, capacity: %zu\n", length, capacity);
for (size_t index = 0; index < length; index++) {
printf("%d ", items[index]);
}
printf("\n");
free(items);
return 0;
} Rust's
Vec<T> is a dynamic array with built-in length, capacity, and automatic resizing — it calls malloc/realloc/free internally. In C, you implement this yourself: track length and capacity manually, double capacity when full, call realloc to resize, and free when done. Most C projects use a utility library (glib's GArray, or a custom header) to provide a growable-array abstraction.Zero-initialized allocation
fn main() {
// vec! macro initializes all elements to the given value
let zeroed: Vec<i32> = vec![0; 10];
let buffer: Vec<u8> = vec![0; 1024];
println!("{zeroed:?}");
println!("buffer size: {}", buffer.len());
} #include <stdio.h>
#include <stdlib.h>
int main(void) {
// calloc allocates AND zeroes memory in one call
size_t count = 10;
int *zeroed = calloc(count, sizeof(int));
if (zeroed == NULL) { return 1; }
for (size_t index = 0; index < count; index++) {
printf("%d ", zeroed[index]);
}
printf("\n");
free(zeroed);
// malloc leaves memory uninitialised — reading it is UB
int *uninitialised = malloc(count * sizeof(int));
if (uninitialised == NULL) { return 1; }
// Must write before reading uninitialised!
for (size_t index = 0; index < count; index++) {
uninitialised[index] = 0; // explicit zero-fill
}
free(uninitialised);
return 0;
} calloc(n, size) allocates n * size bytes and zeroes the entire block. malloc leaves memory uninitialised — reading it before writing is undefined behavior. Rust's vec![0; n] always initializes to zero. On modern operating systems, both calloc and freshly mapped pages from mmap return zeroed memory at nearly zero cost (the OS provides pre-zeroed pages), so calloc is often faster than malloc + memset.Error Handling
Return codes
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
Err(String::from("division by zero"))
} else {
Ok(numerator / denominator)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("10 / 2 = {result}"),
Err(error) => println!("error: {error}"),
}
match divide(5.0, 0.0) {
Ok(result) => println!("5 / 0 = {result}"),
Err(error) => println!("error: {error}"),
}
} #include <stdio.h>
// Returns 0 on success, -1 on failure.
// Result is written through the output pointer.
int divide(double numerator, double denominator,
double *result) {
if (denominator == 0.0) {
return -1;
}
*result = numerator / denominator;
return 0;
}
int main(void) {
double result;
if (divide(10.0, 2.0, &result) == 0) {
printf("10 / 2 = %f\n", result);
} else {
printf("error: division by zero\n");
}
if (divide(5.0, 0.0, &result) == 0) {
printf("5 / 0 = %f\n", result);
} else {
printf("error: division by zero\n");
}
return 0;
} C uses integer return codes to signal success (usually 0) or failure (non-zero). There is no
Result type — the "result" value is returned through an output pointer, and the error code is the function's return value. Errors are easy to ignore: divide(5.0, 0.0, &result); with no check on the return value is valid C. Rust forces you to handle Result — ignoring it without let _ = ... produces a compiler warning.errno — global error code
use std::fs;
fn main() {
// Errors are values — the OS error is carried in Err(io::Error)
match fs::read_to_string("nonexistent.txt") {
Ok(content) => print!("{content}"),
Err(error) => println!("error: {error}"),
}
} #include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
// fopen returns NULL and sets the global errno on failure
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
printf("error: %s (errno %d)\n", strerror(errno), errno);
// Note: returning 0 here to keep this example runnable.
// Real code would return 1 to signal failure to the caller.
} else {
fclose(file);
}
return 0;
} Many C standard-library functions set the global thread-local variable
errno on failure. strerror(errno) converts the code to a human-readable message. The global errno is fragile — any subsequent library call may overwrite it. Always copy it immediately after a failure: int saved = errno;. Rust's io::Error carries the OS error code inside the Err variant and never clobbers a global.Handling allocation failure
fn main() {
// Rust's allocator panics on OOM by default (calls the global alloc error handler)
let large_vec: Vec<u8> = Vec::with_capacity(1_000_000);
println!("capacity: {}", large_vec.capacity());
// Can install a custom alloc-error handler with #[alloc_error_handler]
} #include <stdio.h>
#include <stdlib.h>
int main(void) {
// malloc returns NULL on out-of-memory — always check!
size_t count = 1000000;
unsigned char *buffer = malloc(count * sizeof(unsigned char));
if (buffer == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
printf("allocated %zu bytes\n", count);
free(buffer);
return 0;
} Always check
malloc's return value. An unchecked malloc is one of the most common C bugs: on memory-exhausted systems, malloc returns NULL, and the subsequent *pointer = value dereferences NULL — a segmentation fault that may be hard to trace back to the allocation site. Rust's default allocator panics (calls alloc_error_handler) on OOM, which at least produces a clear error message and stack trace.Preprocessor & Macros
#define constants
const MAX_BUFFER_SIZE: usize = 4096;
const PI: f64 = std::f64::consts::PI;
fn main() {
println!("buffer: {MAX_BUFFER_SIZE}");
println!("pi: {PI:.5}");
// Constants are typed, scoped, and appear in debugger symbols
} #include <stdio.h>
#include <math.h>
#define MAX_BUFFER_SIZE 4096
#define MY_PI 3.14159265358979323846
int main(void) {
printf("buffer: %d\n", MAX_BUFFER_SIZE);
printf("pi: %.5f\n", MY_PI);
// Prefer typed const in C17:
const int buffer_size_typed = 4096;
printf("typed: %d\n", buffer_size_typed);
return 0;
} #define is handled by the C preprocessor — a text-substitution step before compilation. #define MAX_BUFFER_SIZE 4096 replaces every occurrence of the identifier with 4096 before the compiler sees it. Unlike Rust's const, a #define has no type, no scope, and no debugger symbol. For type safety, prefer const int MAX_BUFFER_SIZE = 4096; in C17 — the compiler then checks types and the constant appears in backtraces.#define macros
macro_rules! square {
($value:expr) => { ($value) * ($value) };
}
macro_rules! max_value {
($first:expr, $second:expr) => {
if $first > $second { $first } else { $second }
};
}
fn main() {
println!("{}", square!(5)); // 25
println!("{}", square!(3 + 1)); // 16 — correct: parens in macro
println!("{}", max_value!(3, 7)); // 7
} #include <stdio.h>
#define SQUARE(value) ((value) * (value))
#define MAX_VALUE(first, second) ((first) > (second) ? (first) : (second))
int main(void) {
printf("%d\n", SQUARE(5)); // 25
printf("%d\n", SQUARE(3 + 1)); // 16 — extra parens prevent bugs
printf("%d\n", MAX_VALUE(3, 7)); // 7
return 0;
} C preprocessor macros are text substitution.
SQUARE(3+1) without the extra parentheses would expand to 3+1 * 3+1 = 3+3+1 = 7 (wrong due to operator precedence). Always wrap macro parameters and the entire macro expansion in parentheses. Rust's macro_rules! is hygienic — it operates on syntax trees, not raw text, so square!(3 + 1) expands correctly without extra parentheses and cannot introduce identifier hygiene bugs.#include and header files
// Rust's module system: declare inline or in separate files.
// No header/implementation split — declarations and definitions
// live in the same .rs file.
fn square(value: i32) -> i32 {
value * value
}
fn main() {
println!("{}", square(5));
} // Normally split: square.h declares, square.c defines.
// Here, inlined for a self-contained runnable example:
#include <stdio.h>
static int square(int value) { return value * value; }
int main(void) {
printf("%d\n", square(5));
return 0;
} C splits code across header files (
.h — declarations: prototypes, struct definitions, macros) and implementation files (.c — definitions: actual function bodies). #include "square.h" copies the header text into the source file. Rust's module system uses mod and use — no header/implementation split, no risk of duplicate includes (C uses #pragma once or include guards to prevent that). static on a function limits its visibility to the current translation unit.Conditional compilation
fn main() {
#[cfg(debug_assertions)]
println!("debug build");
let platform = if cfg!(target_os = "windows") {
"Windows"
} else if cfg!(target_os = "macos") {
"macOS"
} else {
"Other"
};
println!("platform: {platform}");
} #include <stdio.h>
int main(void) {
#ifndef NDEBUG
printf("debug build\n");
#endif
#if defined(_WIN32)
const char *platform = "Windows";
#elif defined(__APPLE__)
const char *platform = "macOS";
#else
const char *platform = "Other";
#endif
printf("platform: %s\n", platform);
return 0;
} C uses preprocessor directives (
#ifdef, #ifndef, #if defined()) for conditional compilation. Platform macros like __APPLE__, _WIN32, and __linux__ are predefined by the compiler. Debug builds typically do not define NDEBUG; release builds add -DNDEBUG to strip assertions and debug code. Rust's #[cfg(...)] attributes are type-safe, work on any syntax node, and have a much richer predicate language than C's preprocessor.I/O & Files
Writing to stderr
fn main() {
println!("This goes to stdout");
eprintln!("This goes to stderr");
eprintln!("Error: something went wrong (code {})", 42);
} #include <stdio.h>
int main(void) {
printf("This goes to stdout\n");
fprintf(stderr, "This goes to stderr\n");
fprintf(stderr, "Error: something went wrong (code %d)\n", 42);
return 0;
} fprintf(stderr, ...) is the C equivalent of Rust's eprintln!. Both write to the standard error stream (file descriptor 2). In C, stderr is a FILE* handle predefined by the standard library — you can pass it to any fprintf call. The separation of stdout and stderr allows redirecting them independently: ./program 2>/dev/null discards errors while keeping normal output.File write and read
use std::fs;
fn main() {
let content = "Line 1\nLine 2\nLine 3\n";
fs::write("/tmp/example.txt", content)
.expect("write failed");
let read_back = fs::read_to_string("/tmp/example.txt")
.expect("read failed");
print!("{read_back}");
} #include <stdio.h>
#include <string.h>
int main(void) {
// Write a file
FILE *file = fopen("/tmp/example.txt", "w");
if (file == NULL) {
fprintf(stderr, "could not open for writing\n");
return 1;
}
const char *content = "Line 1\nLine 2\nLine 3\n";
fwrite(content, 1, strlen(content), file);
fclose(file);
// Read it back line by line
file = fopen("/tmp/example.txt", "r");
if (file == NULL) {
fprintf(stderr, "could not open for reading\n");
return 1;
}
char line[128];
while (fgets(line, sizeof(line), file) != NULL) {
printf("%s", line);
}
fclose(file);
return 0;
} Always
fclose every file you fopen — open file handles are a limited OS resource and unflushed buffered data may be lost if the process exits without closing. fwrite(ptr, 1, n, file) writes n bytes starting at ptr. fgets reads one line at a time (including the newline) up to size - 1 characters. Rust's fs::write and fs::read_to_string handle open, write/read, and close atomically, returning a Result.Reading from stdin
use std::io::{self, BufRead};
fn main() {
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line.expect("read failed");
println!("got: {line}");
}
} #include <stdio.h>
#include <string.h>
int main(void) {
char input[128];
printf("Enter text: ");
fflush(stdout); // flush prompt before blocking on fgets
if (fgets(input, sizeof(input), stdin) != NULL) {
// Strip the trailing newline
size_t length = strlen(input);
if (length > 0 && input[length - 1] == '\n') {
input[length - 1] = '\0';
}
printf("Got: %s\n", input);
}
return 0;
} This example requires interactive input and cannot run in a browser sandbox. Use
fgets over scanf("%s", ...) — scanf stops at whitespace and has no buffer-size limit, making it vulnerable to buffer overflows. fflush(stdout) forces the buffered prompt to appear before the program blocks waiting for input. Rust's BufRead::lines() returns an iterator and handles the newline stripping automatically.Standard Library
Math functions
fn main() {
let value: f64 = 2.0;
println!("{:.4}", value.sqrt());
println!("{:.4}", value.powi(10)); // integer exponent
println!("{:.4}", value.ln()); // natural log
println!("{:.4}", f64::sin(std::f64::consts::PI / 6.0));
println!("{:.4}", f64::abs(-3.14));
} #include <stdio.h>
#include <math.h>
int main(void) {
double value = 2.0;
printf("%.4f\n", sqrt(value));
printf("%.4f\n", pow(value, 10));
printf("%.4f\n", log(value)); // natural log
printf("%.4f\n", sin(M_PI / 6.0));
printf("%.4f\n", fabs(-3.14));
return 0;
} Math functions are in
<math.h> and require linking with -lm. Rust's math functions are methods on f64/f32 so 2.0_f64.sqrt() and f64::sqrt(2.0) are equivalent. C's pow(x, n) accepts any real exponent; Rust distinguishes powi (integer exponent, faster) and powf (float exponent). Use fabs for absolute value of double; abs from <stdlib.h> is for integers only.Sorting
fn main() {
let mut numbers = [5, 2, 8, 1, 9, 3];
numbers.sort();
println!("{numbers:?}");
let mut words = ["banana", "apple", "cherry"];
words.sort();
println!("{words:?}");
// Sort descending with a comparator
numbers.sort_by(|first, second| second.cmp(first));
println!("{numbers:?}");
} #include <stdio.h>
#include <stdlib.h>
#include <string.h>
int compare_ints(const void *first, const void *second) {
return (*(const int *)first) - (*(const int *)second);
}
int compare_strings(const void *first, const void *second) {
return strcmp(*(const char **)first, *(const char **)second);
}
int compare_ints_descending(const void *first, const void *second) {
return (*(const int *)second) - (*(const int *)first);
}
int main(void) {
int numbers[] = {5, 2, 8, 1, 9, 3};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
qsort(numbers, count, sizeof(int), compare_ints);
for (size_t index = 0; index < count; index++) {
printf("%d%s", numbers[index], index < count - 1 ? " " : "\n");
}
const char *words[] = {"banana", "apple", "cherry"};
size_t word_count = sizeof(words) / sizeof(words[0]);
qsort(words, word_count, sizeof(char *), compare_strings);
for (size_t index = 0; index < word_count; index++) {
printf("%s%s", words[index], index < word_count - 1 ? " " : "\n");
}
qsort(numbers, count, sizeof(int), compare_ints_descending);
for (size_t index = 0; index < count; index++) {
printf("%d%s", numbers[index], index < count - 1 ? " " : "\n");
}
return 0;
} qsort requires a comparator function taking two const void* arguments that must be cast to the actual type. The function returns negative, zero, or positive for less-than, equal, or greater-than. The void* casts bypass the type system — passing the wrong comparator compiles but silently produces wrong results. Rust's .sort() uses the Ord trait — type-safe, no cast required. For integer comparison, the subtraction trick (*a - *b) overflows for large values; use an explicit comparison instead.Memory operations
fn main() {
let source = [1i32, 2, 3, 4, 5];
let mut destination = [0i32; 5];
destination.copy_from_slice(&source);
println!("{destination:?}");
let mut zeroed = [99i32; 5];
zeroed.fill(0);
println!("{zeroed:?}");
let mut swapped = [1i32, 2, 3, 4, 5];
swapped.swap(1, 3);
println!("{swapped:?}");
} #include <stdio.h>
#include <string.h>
int main(void) {
int source[] = {1, 2, 3, 4, 5};
int destination[5];
memcpy(destination, source, sizeof(source));
for (int index = 0; index < 5; index++) {
printf("%d%s", destination[index], index < 4 ? " " : "\n");
}
int zeroed[] = {99, 99, 99, 99, 99};
memset(zeroed, 0, sizeof(zeroed));
for (int index = 0; index < 5; index++) {
printf("%d%s", zeroed[index], index < 4 ? " " : "\n");
}
// Swap two elements manually (no stdlib helper)
int array[] = {1, 2, 3, 4, 5};
int temp = array[1];
array[1] = array[3];
array[3] = temp;
for (int index = 0; index < 5; index++) {
printf("%d%s", array[index], index < 4 ? " " : "\n");
}
return 0;
} memcpy copies n bytes from source to destination; use memmove when the regions may overlap — memcpy has undefined behavior for overlapping memory. memset fills memory byte-by-byte; it works correctly for zero (0x00 == 0 for all numeric types) but is wrong for non-zero values of multi-byte types (memset(arr, 1, ...) does not set integers to 1). Rust's copy_from_slice panics if the slice lengths differ; C's memcpy does not check sizes.