Ruby.CodeCompared.To/C

An interactive executable cheatsheet for Rubyists learning C

Ruby 4.0 C17 (GCC)
Syntax Basics
Hello World
puts "Hello, World!"
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }

#include <stdio.h> copies declarations from the Standard I/O header into this file β€” it tells the compiler that printf exists and how to call it. Without it, the compiler does not know what printf is. Every C program must define a main function: the return type int is the exit code (0 = success, non-zero = failure), and void in the parameter list explicitly declares that main accepts no arguments. printf does not add a newline automatically β€” \n must be explicit.

Comments
# Single-line comment x = 42 # inline =begin Multi-line comment block (rarely used in practice) =end puts x
#include <stdio.h> int main(void) { // Single-line comment (C99+) int x = 42; /* inline block comment */ /* Multi-line block comment: classic C89 style, still widely used */ printf("%d\n", x); return 0; }

C originally only had /* block */ comments in C89 (the version of C formalized in 1989). The // single-line form was added in C99 and is universally supported today. Block comments cannot be nested β€” a /* inside a block comment does not start a new comment.

Output / printing
puts "With newline automatically" print "No automatic newline\n" printf "%d items at $%.2f each\n", 3, 4.99 p [1, 2, 3] # inspect format
#include <stdio.h> int main(void) { printf("With newline\n"); printf("No newline"); printf(" here\n"); printf("%d items at $%.2f each\n", 3, 4.99); printf("%-10s %5d\n", "count:", 42); /* alignment */ fprintf(stderr, "Error message\n"); /* to stderr */ return 0; }

C's printf uses format specifiers: %d for integers, %f for floats, %s for strings, %c for characters, %zu for size_t. The width and precision modifiers (%-10s, %.2f) work exactly as in Ruby's sprintf/% operator β€” because Ruby borrowed the convention from C. fprintf(stream, format, ...) is the generalised form: printf(...) is shorthand for fprintf(stdout, ...). Writing to stderr instead of stdout sends the output to the standard error stream, which is typically unbuffered and continues to appear even when the user redirects stdout to a file.

Variables & type declarations
count = 10 # Integer inferred ratio = 3.14 # Float inferred initial = 'A' # String inferred active = true # TrueClass inferred puts count.class # Integer puts ratio.class # Float
#include <stdio.h> #include <stdbool.h> int main(void) { int count = 10; double ratio = 3.14; char initial = 'A'; bool active = true; /* C99 stdbool.h */ printf("%d\n", count); printf("%.2f\n", ratio); printf("%c\n", initial); printf("%s\n", active ? "true" : "false"); return 0; }

C is statically typed: every variable must be declared with its type. There is no type inference. The type determines how many bytes are allocated and how the bits are interpreted. bool requires #include <stdbool.h> (C99); without it, use int with 0 for false and any non-zero value for true.

sizeof operator
# Ruby abstracts memory β€” no sizeof equivalent puts 42.class # Integer puts 3.14.class # Float puts "hi".bytesize # 2 (UTF-8 bytes in the string) numbers = [1, 2, 3] puts numbers.length # 3
#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("pointer: %zu bytes\n", sizeof(void *)); int numbers[5] = {10, 20, 30, 40, 50}; size_t count = sizeof(numbers) / sizeof(numbers[0]); printf("array total: %zu bytes\n", sizeof(numbers)); printf("element count: %zu\n", count); return 0; }

sizeof is a compile-time operator (not a function) that returns the number of bytes a type or variable occupies. size_t is C's standard unsigned integer type for sizes and array indices; its actual width matches the platform's pointer size (32 or 64 bits). The idiom sizeof(array) / sizeof(array[0]) computes the element count β€” but only works in the same scope where the array was declared on the stack. When an array is passed to a function or assigned to a pointer variable, it decays: the array variable loses its size information and becomes a plain pointer to its first element. After that, sizeof returns the size of the pointer (typically 8 bytes on 64-bit systems), not the size of the original array.

Types
Integer types
small = 42 big = 9_000_000_000_000 puts small.class # Integer (arbitrary precision) puts big.class # Integer puts small + big
#include <stdio.h> int main(void) { int small = 42; long medium = 1000000L; long long large = 9000000000LL; printf("int: %d\n", small); printf("long: %ld\n", medium); printf("long long: %lld\n", large); printf("sum: %lld\n", (long long)small + large); return 0; }

Ruby's Integer is arbitrary-precision β€” it grows as large as needed. C integers are fixed-width and overflow silently (the value wraps around). The exact sizes of int, long, and long long are platform-dependent, but int is at least 16 bits, long at least 32, and long long exactly 64 on all modern platforms. The example uses an explicit cast: (long long)small + large promotes small from int to long long before adding. Without the cast, the addition would use int arithmetic and overflow on large values.

Fixed-width integers (stdint.h)
# Ruby has no fixed-width types β€” Integers grow freely signed_byte = 127 unsigned_byte = 255 # Ruby Integers are always signed but still work word = 2_147_483_647 big = 18_446_744_073_709_551_615 puts signed_byte puts unsigned_byte puts word puts big puts big.class # Integer β€” arbitrary precision, no overflow
#include <stdio.h> #include <stdint.h> #include <inttypes.h> int main(void) { int8_t signed_byte = 127; uint8_t unsigned_byte = 255; int32_t word = 2147483647; uint64_t big = 18446744073709551615ULL; printf("int8_t: %" PRId8 "\n", signed_byte); printf("uint8_t: %" PRIu8 "\n", unsigned_byte); printf("int32_t: %" PRId32 "\n", word); printf("uint64_t: %" PRIu64 "\n", big); return 0; }

The _t suffix is a C convention meaning "type" β€” it marks names that are type aliases (typedefs). Standard library types like size_t, int32_t, and time_t all follow this pattern. <stdint.h> provides exact-width types (int8_t, uint64_t, etc.) guaranteed to be exactly that many bits on every platform. The PRI format macros from <inttypes.h> β€” like PRId8 and PRIu64 β€” expand to the correct printf format specifier for each type. They look unusual because the C string literal concatenation feature joins them at compile time: "%" PRId32 "\n" becomes "%d\n" on most platforms but "%ld\n" on others where int32_t is defined as long. This is the portable way to print fixed-width integers.

Floating point
temperature = 3.14159265358979 # Float β€” always 64-bit double puts temperature puts temperature.class # Float puts 0.1 + 0.2 # 0.30000000000000004
#include <stdio.h> #include <math.h> int main(void) { float single = 3.14159265f; /* 32-bit, ~7 significant digits */ double temperature = 3.14159265358979; /* 64-bit, ~15 significant digits */ printf("float: %.7f\n", single); /* last digits unreliable */ printf("double: %.15f\n", temperature); printf("0.1+0.2: %.17f\n", 0.1 + 0.2); /* not exactly 0.3 */ printf("fabs(-5): %.1f\n", fabs(-5.0)); /* absolute value for doubles */ return 0; }

Ruby's Float is always a 64-bit double. C has both float (32-bit) and double (64-bit). Prefer double for most work; float is only worth the precision loss when memory or SIMD performance is critical. The f suffix on a literal (3.14f) makes it a float constant β€” without it, all floating-point literals are double. fabs(x) returns the absolute value of a double; use fabsf(x) for float. The Ruby equivalent is x.abs or (-5.0).abs.

Math functions
puts Math.sqrt(2.0) # 1.4142135623730951 puts 2.0 ** 10 # 1024.0 puts Math::PI # 3.141592653589793 puts 3.7.floor # 3 puts 3.2.ceil # 4
#define _GNU_SOURCE /* exposes M_PI and other POSIX extensions */ #include <stdio.h> #include <math.h> int main(void) { printf("sqrt(2): %.10f\n", sqrt(2.0)); printf("pow(2,10): %.0f\n", pow(2.0, 10.0)); printf("M_PI: %.10f\n", M_PI); printf("floor(3.7): %.1f\n", floor(3.7)); printf("ceil(3.2): %.1f\n", ceil(3.2)); printf("fabs(-5.3): %.1f\n", fabs(-5.3)); return 0; }

Math functions live in <math.h>; on some older systems linking requires -lm (the compiler flag used here). M_PI is not in the C standard but is universally available as an extension β€” the strictly portable alternative is acos(-1.0). floor and ceil return double, not int. fabs(x) is the absolute value function for double values; it corresponds to Ruby's x.abs. (There is no unary - trick like Ruby's -x.abs in C because C has no operator overloading.)

Type casting
puts 7 / 2 # 3 β€” integer division puts 7.0 / 2 # 3.5 β€” one float makes it float puts 7.to_f / 2 # 3.5 puts Integer("42") # 42 puts Float("3.14") # 3.14 puts 3.99.to_i # 3 β€” truncates toward zero
#include <stdio.h> #include <stdlib.h> int main(void) { printf("%d\n", 7 / 2); /* 3 β€” integer division */ printf("%.1f\n", 7.0 / 2); /* 3.5 */ printf("%.1f\n", (double)7 / 2); /* 3.5 β€” explicit cast */ printf("%d\n", (int)3.99); /* 3 β€” truncates, no rounding */ printf("%d\n", (int)-3.99); /* -3 β€” truncates toward zero */ printf("%d\n", atoi("42")); /* string to int */ printf("%.2f\n", atof("3.14")); /* string to double */ return 0; }

C casts are explicit: (double)7 / 2 converts 7 to a double before the division. Without the cast, 7 / 2 is integer division producing 3, even if assigned to a double. Casting a float to int always truncates toward zero β€” it does not round. The names atoi and atof stand for "ASCII to integer" and "ASCII to float" β€” "ASCII" here means a C string. Despite the f in its name, atof returns a double, not a float; the name predates the separation of float and double in C. strtol ("string to long") and strtod ("string to double") are the modern preferred alternatives because they can detect invalid input via an output pointer parameter, whereas atoi/atof silently return 0 on failure.

Strings
String literals
greeting = "Hello, World!" name = "Alice" puts greeting puts name puts greeting.length # 13 puts greeting[0] # "H" puts greeting[-1] # "!"
#include <stdio.h> #include <string.h> int main(void) { char greeting[20] = "Hello, World!"; /* mutable char array */ const char *name = "Alice"; /* read-only pointer to literal */ printf("%s\n", greeting); printf("%s\n", name); printf("%zu\n", strlen(greeting)); /* 13 β€” null byte not counted */ printf("%c\n", greeting[0]); /* 'H' */ printf("%c\n", greeting[12]); /* '!' β€” no negative indexing */ return 0; }

C strings are arrays of char terminated by a null byte ('\0'). A char array is mutable; a const char * points to a string literal in read-only memory. There is no negative indexing β€” greeting[-1] in C is undefined behavior. The null terminator is why the array must be at least strlen + 1 bytes.

String copy & concatenation
original = "Hello" copy = original.dup copy += ", World" puts original # "Hello" β€” unchanged puts copy # "Hello, World"
#include <stdio.h> #include <string.h> int main(void) { char original[20] = "Hello"; char copy[20]; /* strncpy: safe copy β€” always provide the destination size */ strncpy(copy, original, sizeof(copy) - 1); copy[sizeof(copy) - 1] = '\0'; /* guarantee null termination */ /* strncat: safe concatenate β€” provide remaining space */ strncat(copy, ", World", sizeof(copy) - strlen(copy) - 1); printf("original: %s\n", original); /* Hello β€” unaffected */ printf("copy: %s\n", copy); /* Hello, World */ return 0; }

C has no + operator for strings. Copying uses strncpy and concatenation uses strncat. The n in these names stands for the maximum number of characters to process; you pass the buffer size to prevent writing past the end of the allocated memory β€” a "buffer overflow." The unsafe originals strcpy and strcat have no limit and blindly copy until they hit a null terminator, making them a notorious source of security vulnerabilities when the source is longer than the destination. The destination buffer must always be large enough to hold the result plus the null terminator.

String comparison
puts "apple" == "banana" # false puts "apple" == "apple" # true puts "apple" <=> "banana" # -1 (apple sorts before banana) puts "apple" < "banana" # true
#include <stdio.h> #include <string.h> int main(void) { const char *first = "apple"; const char *second = "banana"; /* strcmp: 0 = equal, negative = first < second, positive = first > second */ printf("equal: %s\n", strcmp(first, second) == 0 ? "true" : "false"); printf("less: %s\n", strcmp(first, second) < 0 ? "true" : "false"); printf("result: %d\n", strcmp(first, second)); /* NEVER use == to compare strings in C */ /* It compares pointer addresses, not content */ printf("ptr ==: %s\n", first == second ? "same" : "different ptr"); return 0; }

The most common C string bug for Rubyists: using == to compare strings. In C, == compares pointer addresses, not content β€” two strings with identical characters at different memory locations will compare as unequal. Always use strcmp for content comparison. The return value mirrors Ruby's <=>: negative, zero, or positive.

String search & conversion
text = "Hello, World!" puts text.include?("World") # true puts text.index("World") # 7 puts text.upcase # HELLO, WORLD! puts Integer("42") # 42 puts "3.14".to_f # 3.14
#include <stdio.h> #include <string.h> #include <ctype.h> #include <stdlib.h> int main(void) { const char *text = "Hello, World!"; char upper[50]; /* strstr: returns pointer to match, or NULL */ const char *found = strstr(text, "World"); printf("found: %s\n", found ? "true" : "false"); printf("index: %td\n", found - text); /* pointer difference */ /* toupper operates one character at a time */ for (int index = 0; text[index]; index++) { upper[index] = (char)toupper((unsigned char)text[index]); } upper[strlen(text)] = '\0'; printf("upper: %s\n", upper); printf("int: %d\n", atoi("42")); printf("double: %.2f\n", atof("3.14")); return 0; }

C strings have no methods β€” all operations are free functions from <string.h> and <ctype.h>. strstr returns a pointer to the first occurrence of the substring, or NULL if not found. Subtracting the original pointer gives the index. toupper takes and returns int, so casting to unsigned char before passing prevents undefined behavior on systems where char is signed.

Arrays
Fixed-size arrays
numbers = [10, 20, 30, 40, 50] puts numbers[0] # 10 puts numbers[-1] # 50 (negative index) puts numbers.length # 5 numbers[2] = 99 p numbers # [10, 20, 99, 40, 50]
#include <stdio.h> int main(void) { int numbers[5] = {10, 20, 30, 40, 50}; int count = (int)(sizeof(numbers) / sizeof(numbers[0])); printf("%d\n", numbers[0]); /* 10 β€” first element */ printf("%d\n", numbers[count - 1]); /* 50 β€” last element */ printf("%d\n", count); /* 5 */ numbers[2] = 99; printf("["); for (int index = 0; index < count; index++) { printf("%d%s", numbers[index], index < count - 1 ? ", " : ""); } printf("]\n"); return 0; }

C arrays are fixed-size and allocated at declaration time β€” they cannot grow or shrink. There is no negative indexing, no bounds checking, and no .length property β€” the size must be tracked separately. Accessing beyond the array boundary is undefined behavior: the program may crash, produce garbage, or appear to work while corrupting adjacent memory.

Iterating arrays
fruits = ["apple", "banana", "cherry"] fruits.each { |fruit| puts fruit } puts "---" fruits.each_with_index { |fruit, index| puts "#{index}: #{fruit}" }
#include <stdio.h> int main(void) { const char *fruits[] = {"apple", "banana", "cherry"}; int count = (int)(sizeof(fruits) / sizeof(fruits[0])); for (int index = 0; index < count; index++) { printf("%s\n", fruits[index]); } printf("---\n"); for (int index = 0; index < count; index++) { printf("%d: %s\n", index, fruits[index]); } return 0; }

C has no iterators or block syntax β€” every collection is traversed with an explicit for loop and an index. The C standard does not provide higher-order functions like map, select, or reduce. A const char *[] is an array of pointers, each pointing to a string literal β€” not an array of character arrays.

Two-dimensional arrays
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] matrix.each { |row| p row } puts matrix[1][2] # 6
#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++) { printf("["); for (int col = 0; col < 3; col++) { printf("%d%s", matrix[row][col], col < 2 ? ", " : ""); } printf("]\n"); } printf("matrix[1][2] = %d\n", matrix[1][2]); /* 6 */ return 0; }

C two-dimensional arrays are laid out in row-major order β€” all elements of row 0 come before row 1 in memory. The outer dimension can be omitted when initializing (int matrix[][3]), but the inner dimensions must be specified for pointer arithmetic to work correctly. Unlike Ruby's array of arrays, this is a single contiguous block of memory.

Control Flow
if / else if / else
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" elsif score >= 70 puts "C" else puts "F" end
#include <stdio.h> int main(void) { int score = 85; if (score >= 90) { printf("A\n"); } else if (score >= 80) { printf("B\n"); } else if (score >= 70) { printf("C\n"); } else { printf("F\n"); } return 0; }

The logic is identical to Ruby, but the syntax requires parentheses around the condition and braces around the body. C has else if (two words); Ruby uses elsif. In C, any non-zero integer is truthy β€” there is no dedicated Boolean type in C89, though stdbool.h added bool, true, and false in C99.

while & do-while
count = 1 while count <= 5 puts count count += 1 end # Ruby has no do-while; use loop + break value = 0 loop do value += 1 puts "do: #{value}" break if value >= 3 end
#include <stdio.h> int main(void) { int count = 1; while (count <= 5) { printf("%d\n", count); count++; } /* do-while: body always executes at least once */ int value = 0; do { value++; printf("do: %d\n", value); } while (value < 3); return 0; }

C's do-while guarantees the body executes at least once before testing the condition. Ruby lacks this construct β€” the closest equivalent is loop do ... break if condition end. C also has break to exit a loop and continue to skip to the next iteration (equivalent to Ruby's next).

for loop
(1..5).each { |index| puts index } puts "---" (0...10).step(2) { |index| puts index }
#include <stdio.h> int main(void) { for (int index = 1; index <= 5; index++) { printf("%d\n", index); } printf("---\n"); for (int index = 0; index < 10; index += 2) { printf("%d\n", index); } return 0; }

C's for loop has three clauses: initializer, condition, and post-action. All three are optional β€” for (;;) is an infinite loop. Variables declared in the initializer (C99+) are scoped to the loop. The index++ post-increment runs after each body execution. This is lower-level than Ruby's iterators but extremely explicit about what the loop does.

switch / case
day = 3 case day when 1 then puts "Monday" when 2 then puts "Tuesday" when 3 then puts "Wednesday" when 6, 7 then puts "Weekend" else puts "Another day" end
#include <stdio.h> int main(void) { int day = 3; switch (day) { case 1: printf("Monday\n"); break; case 2: printf("Tuesday\n"); break; case 3: printf("Wednesday\n"); break; case 6: /* fall through */ case 7: printf("Weekend\n"); break; default: printf("Another day\n"); } return 0; }

The critical difference: C switch falls through to the next case unless stopped by break. Forgetting a break is one of the most common C bugs β€” execution continues into the next case's code. This fall-through can be intentional (cases 6 and 7 share the same handler above). C switch only works with integer types and constants β€” no strings, ranges, or patterns.

Functions
Defining functions
def square(number) number * number end def add(first, second) first + second end puts square(7) puts add(3, 4)
#include <stdio.h> /* Return type comes first, then name and parameters */ int square(int number) { return number * number; } int add(int first, int second) { return first + second; } int main(void) { printf("%d\n", square(7)); printf("%d\n", add(3, 4)); return 0; }

Every C function must declare its return type. void means no return value. Unlike Ruby, C functions cannot return multiple values β€” they return exactly one value (or nothing). There are no keyword arguments, no default values, and no splat operators. Every parameter must have an explicit type. Functions must be declared before they are called, or a prototype must appear first.

Function prototypes
# Ruby has no forward declarations β€” define before use # (inside a class, method order never matters) def greet(name) "Hello, #{name}!" end puts greet("Alice")
#include <stdio.h> /* Prototype: tells the compiler the signature before the definition */ void greet(const char *name); double average(const int *numbers, int count); int main(void) { greet("Alice"); int values[] = {10, 20, 30, 40, 50}; printf("%.1f\n", average(values, 5)); return 0; } void greet(const char *name) { printf("Hello, %s!\n", name); } double average(const int *numbers, int count) { int total = 0; for (int index = 0; index < count; index++) total += numbers[index]; return (double)total / count; }

A prototype declares a function's name, return type, and parameter types so the compiler knows the function's interface before seeing its body. Without a prototype, calling a function defined later in the file is a compile error (or in old C, silently assumes int return). In practice, prototypes go in header files (.h), and definitions go in source files (.c).

Recursion
def factorial(number) return 1 if number <= 1 number * factorial(number - 1) end def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end puts factorial(10) puts fibonacci(10)
#include <stdio.h> long factorial(int number) { if (number <= 1) return 1; return number * factorial(number - 1); } int fibonacci(int n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } int main(void) { printf("%ld\n", factorial(10)); /* 3628800 */ printf("%d\n", fibonacci(10)); /* 55 */ return 0; }

Recursion works identically in C, but C has no tail-call optimization guarantee β€” deep recursion will overflow the stack. The stack frame size is fixed at compile time, not dynamically grown like Ruby's call stack. C compilers may optimize tail calls in practice, but it is not guaranteed by the standard. For deep recursion in C, iterative solutions are safer.

Pointers
Address-of & dereference
# Ruby has no explicit pointers β€” variables are references x = 42 y = x # y refers to the same Integer value y = 99 # reassigning y doesn't change x puts x # 42 puts y # 99 puts x.object_id == y.object_id # false
#include <stdio.h> int main(void) { int x = 42; int *pointer = &x; /* & = "address of x" */ printf("value: %d\n", x); printf("address: %p\n", (void *)pointer); printf("via ptr: %d\n", *pointer); /* * = dereference */ *pointer = 99; /* modify x through the pointer */ printf("x is now: %d\n", x); /* 99 */ return 0; }

A pointer stores a memory address. The & operator gets the address of a variable. The * operator dereferences a pointer β€” it reads or writes the value at that address. Ruby's variables are implicitly references, but the mechanics are hidden. In C, every pointer operation is explicit, which makes memory relationships visible and bugs like dangling pointers and null dereferences the programmer's responsibility.

Output parameters
def swap(first, second) [second, first] end x, y = 10, 20 x, y = swap(x, y) puts "x=#{x} y=#{y}" # x=20 y=10 def min_max(numbers) [numbers.min, numbers.max] end minimum, maximum = min_max([3, 1, 4, 1, 5]) puts "min=#{minimum} max=#{maximum}"
#include <stdio.h> void swap(int *first, int *second) { int temp = *first; *first = *second; *second = temp; } void min_max(const int *numbers, int count, int *minimum, int *maximum) { *minimum = *maximum = numbers[0]; for (int index = 1; index < count; index++) { if (numbers[index] < *minimum) *minimum = numbers[index]; if (numbers[index] > *maximum) *maximum = numbers[index]; } } int main(void) { int x = 10, y = 20; swap(&x, &y); printf("x=%d y=%d\n", x, y); int values[] = {3, 1, 4, 1, 5}; int minimum, maximum; min_max(values, 5, &minimum, &maximum); printf("min=%d max=%d\n", minimum, maximum); return 0; }

Because C functions can only return one value, pointers are used as output parameters to return multiple results. The caller passes the address of a variable (&x), and the function writes to it through the pointer (*first = ...). This is the C idiom for what Ruby accomplishes with multiple return values. The standard library uses this pattern extensively β€” for example, scanf takes pointers to the variables it fills.

Pointer arithmetic
numbers = [10, 20, 30, 40, 50] numbers.each { |number| print "#{number} " } puts # Ruby Enumerables abstract all traversal
#include <stdio.h> int main(void) { int numbers[] = {10, 20, 30, 40, 50}; int *cursor = numbers; /* points to first element */ /* Incrementing a pointer moves by sizeof(int) bytes */ for (int index = 0; index < 5; index++) { printf("%d ", *cursor); cursor++; } printf("\n"); /* Pointer difference gives element count */ int *start = numbers; int *end = numbers + 5; printf("elements: %td\n", end - start); /* 5 */ /* Array indexing is pointer arithmetic in disguise */ printf("numbers[3] == *(numbers+3): %s\n", numbers[3] == *(numbers + 3) ? "true" : "false"); return 0; }

Adding 1 to a pointer advances it by sizeof(the_pointed_to_type) bytes β€” not by 1 byte. This is why pointer arithmetic is type-safe: the compiler knows the step size from the pointer's type. Array indexing (numbers[3]) is literally defined as *(numbers + 3) in the C standard. Subtracting two pointers gives the number of elements between them, as a ptrdiff_t.

NULL pointer
def find_word(words, target) words.find { |word| word == target } end result = find_word(["apple", "banana"], "banana") puts result.nil? ? "not found" : result result = find_word(["apple", "banana"], "cherry") puts result.nil? ? "not found" : result
#include <stdio.h> #include <string.h> const char *find_word(const char **words, int count, const char *target) { for (int index = 0; index < count; index++) { if (strcmp(words[index], target) == 0) return words[index]; } return NULL; /* C's equivalent of nil */ } int main(void) { const char *words[] = {"apple", "banana", "cherry"}; int count = 3; const char *result = find_word(words, count, "banana"); printf("%s\n", result != NULL ? result : "not found"); result = find_word(words, count, "mango"); printf("%s\n", result != NULL ? result : "not found"); return 0; }

NULL is C's equivalent of Ruby's nil β€” a pointer value meaning "points to nothing." Dereferencing a NULL pointer is undefined behavior, typically a segmentation fault. Always check for NULL before using a pointer returned from functions that can fail. malloc, fopen, strstr, and many other standard library functions return NULL on failure.

Memory Management
malloc & free
# Ruby allocates automatically, GC frees automatically numbers = Array.new(5) { |index| (index + 1) * 10 } numbers.each { |number| puts number } # Memory freed automatically when numbers goes out of scope
#include <stdio.h> #include <stdlib.h> int main(void) { /* malloc: allocate heap memory β€” NOT zero-initialized */ int *numbers = malloc(5 * sizeof(int)); if (numbers == NULL) { fprintf(stderr, "malloc failed\n"); return 1; } for (int index = 0; index < 5; index++) { numbers[index] = (index + 1) * 10; } for (int index = 0; index < 5; index++) { printf("%d\n", numbers[index]); } free(numbers); /* REQUIRED β€” no garbage collector in C */ numbers = NULL; /* prevent accidental use after free */ return 0; }

C has no garbage collector. Every malloc must have a matching free. Forgetting free is a memory leak β€” the memory stays allocated until the process exits. Calling free twice is undefined behavior (typically a crash or heap corruption). Setting the pointer to NULL after freeing prevents accidental reuse. Use valgrind or address sanitizer (-fsanitize=address) to find leaks and invalid accesses.

calloc & realloc
numbers = Array.new(5, 0) # [0, 0, 0, 0, 0] numbers.push(60) # grows automatically numbers.push(70) p numbers
#include <stdio.h> #include <stdlib.h> int main(void) { /* calloc: allocate AND zero-initialize */ int *numbers = calloc(5, sizeof(int)); if (!numbers) return 1; int count = 5; for (int index = 0; index < count; index++) printf("%d ", numbers[index]); printf("\n"); /* all zeros */ /* realloc: resize an existing allocation */ int *bigger = realloc(numbers, 7 * sizeof(int)); if (!bigger) { free(numbers); return 1; } numbers = bigger; /* realloc may move the block */ numbers[5] = 60; numbers[6] = 70; count = 7; printf("["); for (int index = 0; index < count; index++) { printf("%d%s", numbers[index], index < count - 1 ? ", " : ""); } printf("]\n"); free(numbers); return 0; }

calloc(count, size) allocates count * size bytes and zero-initializes every byte. realloc resizes an existing allocation and may move it to a new address, so always assign the result to a new pointer before overwriting the original β€” if realloc fails, it returns NULL and the original allocation is still valid. This is how dynamic arrays are built in C by hand.

Structs
Defining & using structs
Person = Struct.new(:name, :age) person = Person.new("Alice", 30) puts person.name puts person.age other = Person.new("Bob", 25) puts "#{other.name} is #{other.age}"
#include <stdio.h> /* typedef lets you use "Person" instead of "struct Person" */ typedef struct { char name[50]; int age; } Person; int main(void) { Person person; snprintf(person.name, sizeof(person.name), "Alice"); person.age = 30; printf("%s\n", person.name); printf("%d\n", person.age); /* Compound literal initializer (C99+) */ Person other = {"Bob", 25}; printf("%s is %d\n", other.name, other.age); return 0; }

A C struct groups named fields into a single type β€” it is the closest thing C has to a class, but with no methods or inheritance. Members are accessed with . for struct values or -> for pointers to structs. typedef struct { ... } Name creates an alias so you can write Person instead of struct Person everywhere.

Structs with functions
Point = Struct.new(:x, :y) do def distance_to(other) Math.sqrt((x - other.x)**2 + (y - other.y)**2) end def to_s = "(#{x}, #{y})" end origin = Point.new(0.0, 0.0) target = Point.new(3.0, 4.0) puts target puts origin.distance_to(target)
#include <stdio.h> #include <math.h> typedef struct { double x; double y; } Point; /* In C, functions that operate on structs are standalone */ double point_distance(Point first, Point second) { double delta_x = first.x - second.x; double delta_y = first.y - second.y; return sqrt(delta_x * delta_x + delta_y * delta_y); } void point_print(Point point) { printf("(%.1f, %.1f)\n", point.x, point.y); } int main(void) { Point origin = {0.0, 0.0}; Point target = {3.0, 4.0}; point_print(target); printf("%.1f\n", point_distance(origin, target)); return 0; }

C structs have no methods β€” functions that operate on them are written as free functions that take the struct as a parameter. By convention, such functions are prefixed with the struct name (e.g., point_distance, point_print). Structs passed by value are copied β€” large structs should be passed by pointer to avoid the copy overhead. This is the OOP-without-objects pattern that C uses everywhere.

Array of structs
Student = Struct.new(:name, :grade) students = [ Student.new("Alice", 92), Student.new("Bob", 78), Student.new("Carol", 85), ] students.sort_by(&:grade).reverse.each do |student| puts "#{student.name}: #{student.grade}" end
#include <stdio.h> #include <stdlib.h> typedef struct { char name[50]; int grade; } Student; int compare_grade_desc(const void *first, const void *second) { return ((Student *)second)->grade - ((Student *)first)->grade; } int main(void) { Student students[] = { {"Alice", 92}, {"Bob", 78}, {"Carol", 85}, }; int count = 3; qsort(students, count, sizeof(Student), compare_grade_desc); for (int index = 0; index < count; index++) { printf("%s: %d\n", students[index].name, students[index].grade); } return 0; }

The standard library's qsort sorts any array by calling a user-supplied comparator function. The comparator receives two const void * pointers and must return negative, zero, or positive β€” the same convention as Ruby's <=>. The -> operator accesses a struct member through a pointer: ptr->field is shorthand for (*ptr).field.

Enums
Basic enum
module Direction NORTH = :north SOUTH = :south EAST = :east WEST = :west end heading = Direction::NORTH puts heading puts heading == :north
#include <stdio.h> typedef enum { NORTH, SOUTH, EAST, WEST } Direction; const char *direction_name(Direction direction) { switch (direction) { case NORTH: return "NORTH"; case SOUTH: return "SOUTH"; case EAST: return "EAST"; case WEST: return "WEST"; default: return "UNKNOWN"; } } int main(void) { Direction heading = NORTH; printf("%s\n", direction_name(heading)); printf("%d\n", heading); /* NORTH == 0, SOUTH == 1, etc. */ return 0; }

C enums are named integer constants β€” they have no methods, no string representation, and no type safety. The compiler assigns sequential integer values starting at 0. You can assign specific values to any enumerator. Comparing enum values works correctly because they are just integers, but there is nothing preventing you from assigning an invalid integer to an enum variable.

Enum with explicit values (flags)
module Permission READ = 0b001 WRITE = 0b010 EXECUTE = 0b100 end access = Permission::READ | Permission::WRITE puts access # 3 puts (access & Permission::READ) != 0 # true puts (access & Permission::EXECUTE) != 0 # false
#include <stdio.h> typedef enum { PERM_READ = 1, /* 0b001 */ PERM_WRITE = 2, /* 0b010 */ PERM_EXECUTE = 4, /* 0b100 */ } Permission; int main(void) { int access = PERM_READ | PERM_WRITE; printf("access: %d\n", access); /* 3 */ printf("read: %s\n", (access & PERM_READ) ? "yes" : "no"); printf("write: %s\n", (access & PERM_WRITE) ? "yes" : "no"); printf("execute: %s\n", (access & PERM_EXECUTE) ? "yes" : "no"); return 0; }

Bit flags are a classic C pattern for combining multiple boolean options into a single integer. Assign each flag a power of 2 so each occupies a unique bit. Combine with bitwise OR (|) and test with bitwise AND (&). This is how POSIX file permissions, open() flags, and many system call parameters work. Ruby uses the same pattern but it is far more common in C code.

Error Handling
Return codes
def safe_divide(numerator, denominator) raise ArgumentError, "division by zero" if denominator == 0 numerator.to_f / denominator end begin puts safe_divide(10, 2) puts safe_divide(10, 0) rescue ArgumentError => error puts "Error: #{error.message}" end
#include <stdio.h> /* Returns 0 on success, -1 on error. Result goes through pointer. */ int safe_divide(double numerator, double denominator, double *result) { if (denominator == 0.0) return -1; /* error */ *result = numerator / denominator; return 0; /* success */ } int main(void) { double result; if (safe_divide(10.0, 2.0, &result) == 0) { printf("%.1f\n", result); } if (safe_divide(10.0, 0.0, &result) != 0) { fprintf(stderr, "Error: division by zero\n"); } return 0; }

C has no exceptions. Errors are communicated by return values: 0 for success, non-zero for failure (or a specific error code). The actual result is passed back through an output pointer parameter. This means every call site must check the return value β€” failing to check is a common source of bugs. The compiler does not enforce error checking in C (unlike languages with Result types).

errno & perror
begin File.open("nonexistent.txt", "r") rescue Errno::ENOENT => error puts error.class # Errno::ENOENT puts error.message # No such file or directory end
#include <stdio.h> #include <errno.h> #include <string.h> int main(void) { FILE *file = fopen("nonexistent.txt", "r"); if (file == NULL) { /* errno is set by the failed system call */ printf("errno: %d\n", errno); printf("message: %s\n", strerror(errno)); perror("fopen"); /* prints: "fopen: No such file or directory" */ return 0; /* printed the error info; demo exits cleanly */ } fclose(file); return 0; }

errno is a global integer set by system calls and standard library functions when they fail. strerror(errno) converts it to a human-readable string. perror(prefix) prints "prefix: error message\n" to stderr in one call. Ruby's Errno::ENOENT and friends are Ruby wrappers around the same underlying errno values β€” they share the same error numbering system as C.

Preprocessor
#define constants
MAX_RETRIES = 3 BUFFER_SIZE = 1024 PI_APPROX = 3.14159 puts MAX_RETRIES puts BUFFER_SIZE puts PI_APPROX
#include <stdio.h> #define MAX_RETRIES 3 #define BUFFER_SIZE 1024 #define PI_APPROX 3.14159 int main(void) { printf("%d\n", MAX_RETRIES); printf("%d\n", BUFFER_SIZE); printf("%.5f\n", PI_APPROX); /* sizeof works on #define values used in array declarations */ char buffer[BUFFER_SIZE]; printf("buffer: %zu bytes\n", sizeof(buffer)); return 0; }

#define is a preprocessor directive β€” it runs before the compiler and does simple text substitution. Unlike Ruby's constants, #define names have no type and no scope. The modern C alternative is const int MAX_RETRIES = 3;, which is type-checked by the compiler. The preprocessor also handles #include (file inclusion), #ifdef (conditional compilation), and many other directives.

Function-like macros
def max(first, second) = first > second ? first : second def min(first, second) = first < second ? first : second def square(value) = value * value puts max(10, 20) puts min(10, 20) puts square(7)
#include <stdio.h> /* Parenthesize everything β€” macro pitfalls lurk without it */ #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define SQUARE(x) ((x) * (x)) int main(void) { printf("%d\n", MAX(10, 20)); printf("%d\n", MIN(10, 20)); printf("%d\n", SQUARE(7)); /* ⚠ Macros expand arguments each time they appear: */ /* SQUARE(x++) expands to ((x++) * (x++)) β€” two increments! */ /* Use inline functions instead for anything with side effects */ return 0; }

Macro arguments are expanded by text substitution before compilation. This makes them work with any type β€” MAX(3.14, 2.71) works for doubles without writing a separate function. But each argument is expanded every time it appears in the body, so arguments with side effects (like x++) execute multiple times. Always parenthesize both the whole macro and each argument to avoid operator precedence surprises. Prefer static inline functions when possible.

Conditional compilation
DEBUG = ENV["DEBUG"] == "1" if DEBUG puts "[debug] Starting up" end puts DEBUG ? "debug mode" : "production mode"
#include <stdio.h> /* Define NDEBUG to disable assertions; define DEBUG to enable debug output */ /* In a real project: gcc -DDEBUG ... or gcc -DNDEBUG ... on the command line */ #define DEBUG /* comment out to disable */ #ifdef DEBUG #define LOG(msg) fprintf(stderr, "[debug] %s\n", msg) #else #define LOG(msg) /* no-op in non-debug builds */ #endif int main(void) { LOG("Starting up"); #ifdef DEBUG printf("debug mode\n"); #else printf("production mode\n"); #endif return 0; }

Conditional compilation lets you include or exclude code at compile time β€” it's the only way in C to have zero-cost debug logging. The excluded code is not compiled at all, so there is truly no runtime overhead. In practice, you define the flag on the compiler command line (gcc -DDEBUG ...) rather than in source, so you can switch modes without editing code. #ifndef HEADER_H_ guard idiom prevents header files from being included multiple times.

Standard Library
Sorting with qsort
numbers = [5, 2, 8, 1, 9, 3] p numbers.sort p numbers.sort { |first, second| second <=> first } # descending
#include <stdio.h> #include <stdlib.h> int compare_asc(const void *first, const void *second) { return *(int *)first - *(int *)second; } int compare_desc(const void *first, const void *second) { return *(int *)second - *(int *)first; } static void print_array(const int *numbers, int count) { printf("["); for (int index = 0; index < count; index++) { printf("%d%s", numbers[index], index < count - 1 ? ", " : ""); } printf("]\n"); } int main(void) { int numbers[] = {5, 2, 8, 1, 9, 3}; int count = 6; qsort(numbers, count, sizeof(int), compare_asc); print_array(numbers, count); qsort(numbers, count, sizeof(int), compare_desc); print_array(numbers, count); return 0; }

qsort is the standard library's general-purpose sort. The comparator function takes two const void * pointers and must return negative (first comes before second), zero (equal), or positive β€” the same convention as Ruby's <=>. The subtraction trick (*a - *b) works for integers but can overflow for large values; for robustness, use explicit comparisons instead. bsearch uses the same comparator convention for binary search.

snprintf β€” safe string formatting
name = "Alice" score = 98 puts "Player: %-10s | Score: %05d" % [name, score] message = "Hello, #{name}! Score: #{score}." puts message puts message.length
#include <stdio.h> #include <string.h> int main(void) { char message[100]; const char *name = "Alice"; int score = 98; snprintf(message, sizeof(message), "Player: %-10s | Score: %05d", name, score); printf("%s\n", message); snprintf(message, sizeof(message), "Hello, %s! Score: %d.", name, score); printf("%s\n", message); printf("%zu\n", strlen(message)); /* snprintf returns the number of chars that would have been written */ int needed = snprintf(NULL, 0, "Value: %d", 12345); printf("chars needed (excluding null): %d\n", needed); return 0; }

snprintf is the safe version of sprintf β€” it always null-terminates and never writes more than the specified number of bytes. The older sprintf has no bounds checking and is a common source of buffer overflow vulnerabilities. Calling snprintf(NULL, 0, format, ...) without writing anything returns the number of characters the formatted string would require β€” useful for allocating exactly the right buffer size.

⚠ Gotchas for Rubyists
Integer division truncates silently
puts 7 / 2 # 3 β€” integer division puts 7.0 / 2 # 3.5 β€” one float operand makes it float puts 7 / 2.0 # 3.5 total = 7 count = 2 # Easy to forget: this is integer division puts total / count # 3 (probably not what you want) puts total.to_f / count # 3.5 (explicit conversion)
#include <stdio.h> int main(void) { printf("%d\n", 7 / 2); /* 3 β€” integer division */ printf("%.1f\n", 7.0 / 2); /* 3.5 */ printf("%.1f\n", (double)7 / 2); /* 3.5 β€” cast first */ /* The classic bug: assigning to double doesn't help */ int total = 7, count = 2; double ratio = total / count; /* 3.0 β€” division already truncated */ printf("%.1f (WRONG)\n", ratio); double correct = (double)total / count; printf("%.1f (right)\n", correct); return 0; }

The type of an expression is determined by the operands, not by what you assign the result to. 7 / 2 is evaluated as integer division (producing 3) before the result is stored in a double. To get floating-point division, at least one operand must be a float or double β€” either via a literal suffix (7.0) or an explicit cast ((double)total). This is one of the most common silent bugs in C code written by people coming from scripting languages.

Arrays are not bounds-checked
numbers = [1, 2, 3] puts numbers[10] # nil β€” safe, returns nil puts numbers[-1] # 3 β€” safe, negative indexing
<pre>/* ⚠ This code has UNDEFINED BEHAVIOR β€” do not run */ #include <stdio.h> int main(void) { int numbers[3] = {1, 2, 3}; /* C trusts you completely β€” no bounds checking at runtime */ printf("%d\n", numbers[10]); /* reads garbage or crashes */ printf("%d\n", numbers[-1]); /* reads before the array */ /* Result: may print garbage, may segfault, may corrupt memory */ /* The behavior is undefined β€” anything can happen */ return 0; }</pre>

C performs zero bounds checking. Reading or writing past an array's end is undefined behavior β€” the program may crash, silently produce wrong results, or corrupt other variables in memory. Buffer overflows are responsible for a massive fraction of real-world security vulnerabilities. Use -fsanitize=address during development to catch them. Always track the size of every array and validate indices before using them.

Signed integer overflow
puts 2**62 # 4611686018427387904 puts 2**62 + 2**62 # 9223372036854775808 β€” no overflow puts 2**63 # still fine β€” arbitrary precision
#include <stdio.h> #include <limits.h> int main(void) { printf("INT_MAX: %d\n", INT_MAX); /* 2147483647 */ printf("INT_MIN: %d\n", INT_MIN); /* -2147483648 */ printf("LLONG_MAX: %lld\n", LLONG_MAX); /* 9223372036854775807 */ /* Unsigned overflow is defined (wraps around) */ unsigned int max_uint = 4294967295U; printf("UINT_MAX + 1 = %u\n", max_uint + 1); /* 0 β€” wraps */ /* Signed overflow is UNDEFINED BEHAVIOR in C */ /* Do NOT do: int x = INT_MAX + 1; */ /* The compiler may produce any result, or skip */ /* the overflow check entirely as an optimization */ return 0; }

Ruby integers are arbitrary-precision and never overflow. C integers are fixed-width: signed integer overflow is undefined behavior β€” the compiler is allowed to assume it never happens and optimize accordingly, producing results that are nonsensical even if you expect wraparound. Unsigned overflow is well-defined and wraps modulo 2^n. Use -fsanitize=undefined to catch signed overflow bugs during development.

Strings are not objects
text = "hello, world" puts text.length # 12 puts text.upcase # HELLO, WORLD puts text.include?(",") # true puts text.split(", ").inspect # ["hello", "world"] puts text.gsub("l", "r") # herro, worrd
#include <stdio.h> #include <string.h> #include <ctype.h> int main(void) { const char *text = "hello, world"; /* No methods β€” everything is a function call */ printf("%zu\n", strlen(text)); /* 12 */ printf("%s\n", strstr(text, ",") ? "true" : "false"); /* search */ /* toupper works one character at a time */ char upper[50]; for (int index = 0; text[index]; index++) { upper[index] = (char)toupper((unsigned char)text[index]); } upper[strlen(text)] = '\0'; printf("%s\n", upper); /* No built-in split, gsub, or replace β€” must implement manually */ printf("(no built-in split or gsub)\n"); return 0; }

Ruby's String is a rich object with over 100 methods. C strings are null-terminated char arrays with a small handful of library functions in <string.h> and <ctype.h>. There is no built-in split, no gsub, no reverse, no regex. String manipulation that takes one line in Ruby often requires 10–20 lines of C. This is not a weakness β€” C's simplicity is its strength in systems programming β€” but Rubyists should set expectations accordingly.

No garbage collector β€” memory leaks are real
def make_data(count) Array.new(count) { |index| index * index } end result = make_data(5) puts result.first # Array freed automatically when result goes out of scope
#include <stdio.h> #include <stdlib.h> /* Caller owns the returned memory β€” MUST call free() when done */ int *make_data(int count) { int *data = malloc(count * sizeof(int)); if (!data) return NULL; for (int index = 0; index < count; index++) { data[index] = index * index; } return data; } int main(void) { int *result = make_data(5); if (!result) return 1; printf("%d\n", result[0]); free(result); /* REQUIRED β€” forgetting this is a memory leak */ result = NULL; /* prevent accidental use-after-free */ return 0; }

Every byte allocated with malloc must eventually be freed with free. Ruby's garbage collector handles this automatically. In C, functions that return heap-allocated memory must document the ownership contract in their comments β€” who is responsible for calling free? Using memory after it is freed, freeing the same pointer twice, and forgetting to free are all critical bugs. Use valgrind or compile with -fsanitize=address to detect them.