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.
# 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.
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.
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.
# 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.
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.
# 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.
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.
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.)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
(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.
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.
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.
# 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).
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.
# 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.
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.
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.
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.
# 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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.