PONY λ M2 Modula-2

C.CodeCompared.To/Perl

An interactive executable cheatsheet comparing C and Perl

C17 (GCC) Perl 5.40
Output & Basics
Hello, World
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
use v5.38; say "Hello, World!";
C requires an explicit #include, a typed int main(void) entry point, and an explicit return 0 on success. Perl needs none of that ceremony — a script is just a sequence of statements, and say (added in Perl 5.10, enabled here by use v5.38) prints with a trailing newline automatically, unlike C's printf, which needs an explicit \n.
Sequencing multiple outputs
#include <stdio.h> int main(void) { printf("First\n"); printf("Second\n"); printf("Third\n"); return 0; }
use v5.38; say "First"; say "Second"; say "Third";
Both languages simply execute statements top to bottom — a genuine structural match, since both are imperative languages with no special sequencing construct needed at all.
Variables & Sigils
Sigils: a genuinely new syntax concept for C
#include <stdio.h> int main(void) { int count = 42; printf("%d\n", count); return 0; }
use v5.38; my $count = 42; say $count;
C has no sigil system at all — a variable's name alone identifies it. Perl prefixes every variable with a sigil indicating its access type: $ for a single (scalar) value, @ for an array, % for a hash — $count here means "the scalar value named count," not a special operator. This is one of the first genuinely new syntax ideas a C developer encounters in Perl.
`my` gives block scope — closer to C than you might expect
#include <stdio.h> int main(void) { int x = 10; { int x = 20; printf("%d\n", x); } printf("%d\n", x); return 0; }
use v5.38; my $x = 10; { my $x = 20; say $x; } say $x;
Perl's my declares a lexically-scoped variable — confined to the enclosing block, exactly like a C variable declared inside { }. This is a closer match than a C developer might expect from a "scripting language": shadowing an outer $x inside a nested block behaves identically in both languages.
Static Types vs. Fully Dynamic
No compile-time type checking at all
#include <stdio.h> int add(int x, int y) { return x + y; } int main(void) { printf("%d\n", add(2, 3)); return 0; }
use v5.38; sub add { my ($x, $y) = @_; return $x + $y; } say add(2, 3);
C checks add's argument and return types at compile time — passing a pointer where an int is expected is a compile error (or at least a warning). Perl has no compile-time type checking at all: add("two", 3) would run, silently coerce "two" to 0 during numeric addition, and produce 3 — no error, just quiet, possibly surprising, coercion.
A single scalar variable can hold any type, at any time
#include <stdio.h> int main(void) { // C requires a fixed type per variable, decided at compile time — // this variable can ONLY ever hold an int, never a string: int value = 42; printf("%d\n", value); return 0; }
use v5.38; my $value = 42; say $value; $value = "now a string"; say $value;
A C variable's type is fixed permanently at compile time based on its declaration. A Perl scalar variable — despite always being declared the same way, with $ — can hold a number at one moment and a string the next, with no re-declaration or cast required; the sigil describes the ACCESS pattern (single value), not a fixed data type.
Strings
String concatenation — no more manual buffer management
#include <stdio.h> #include <string.h> int main(void) { char buffer[64]; strcpy(buffer, "Hello, "); strcat(buffer, "World!"); printf("%s\n", buffer); return 0; }
use v5.38; my $greeting = "Hello, " . "World!"; say $greeting;
C string concatenation requires manually allocating a big-enough buffer and calling strcpy/strcat, with a buffer overflow if the destination is too small — a classic, genuinely dangerous C bug category. Perl's . operator concatenates directly, growing the result automatically, with no buffer to size or overflow at all.
String interpolation — no format specifiers needed
#include <stdio.h> int main(void) { const char *name = "Ada"; int age = 36; printf("%s is %d\n", name, age); return 0; }
use v5.38; my $name = "Ada"; my $age = 36; say "$name is $age";
C's printf requires a format string with type-specific placeholders (%s, %d) that must match the argument types exactly, with no compiler enforcement in older C standards — a real source of undefined behavior if they mismatch. Perl interpolates variables directly inside a double-quoted string, with no format specifiers or type matching to get wrong.
Fixed Arrays vs. Dynamic Arrays
Arrays: fixed size versus dynamic
#include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5}; for (int i = 0; i < 5; i++) { printf("%d ", numbers[i]); } printf("\n"); return 0; }
use v5.38; my @numbers = (1, 2, 3, 4, 5); say "@numbers";
A C array's size is fixed at declaration and baked into the type — growing it means manually allocating a bigger block and copying every element over. Perl's @numbers array grows and shrinks dynamically at runtime with push/pop/shift/unshift, with no manual reallocation ever required from the programmer.
Growing an array at runtime — impossible for a C array
#include <stdio.h> int main(void) { // A fixed-size C array cannot grow — this would require // realloc'ing a heap-allocated array instead, and manually // tracking both its current size and its allocated capacity: int numbers[5] = {1, 2, 3, 4, 5}; printf("%d\n", numbers[4]); return 0; }
use v5.38; my @numbers = (1, 2, 3, 4, 5); push @numbers, 6; push @numbers, 7; say "@numbers";
This is something a plain C array structurally cannot do at all: numbers[5] declares a fixed block of exactly 5 ints, full stop. Achieving the same growth in C means switching to a heap-allocated, manually-managed buffer with malloc/realloc and separately tracked size/capacity fields. Perl's push just works, handling the underlying reallocation invisibly.
Array length is always known — no sizeof trick needed
#include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5}; int length = sizeof(numbers) / sizeof(numbers[0]); printf("%d\n", length); return 0; }
use v5.38; my @numbers = (1, 2, 3, 4, 5); say scalar @numbers;
C has no built-in way to ask an array its length at runtime — the common sizeof(arr) / sizeof(arr[0]) idiom only works for a genuine stack array still in scope, and silently breaks the moment that array decays to a pointer (e.g., when passed to a function). Perl arrays always know their own length; scalar @numbers (or simply using @numbers in numeric context) returns it directly, correct in every context.
Hashes: No C Equivalent
Hashes: a data structure C has no built-in equivalent for
// C has no built-in key/value map at all — getting this behavior // means hand-rolling a hash table (an array of buckets, a hash // function, collision handling) or reaching for a third-party // library. There is no standard, language-level syntax for it: #include <stdio.h> int main(void) { printf("C has no map literal syntax\n"); return 0; }
use v5.38; my %ages = (Ada => 36, Alan => 41); say $ages{Ada};
This is a genuinely new capability for a C developer, not just different syntax for an old one. C's standard library has no hash table or map type at all — implementing one means writing the bucket array, hash function, and collision resolution yourself, or depending on a third-party library. Perl's % sigil and => syntax build a hash table directly into the language, with no data structure to implement.
Checking for a key and iterating
// Again, this would require a hand-rolled hash table in C — // shown here only as a comment describing the shape of the problem: #include <stdio.h> int main(void) { printf("no built-in key-existence check exists in C\n"); return 0; }
use v5.38; my %ages = (Ada => 36, Alan => 41); if (exists $ages{Ada}) { say "found Ada"; } for my $name (sort keys %ages) { say "$name: $ages{$name}"; }
exists checks for key presence without triggering autovivification (accidentally creating the key by looking it up), and keys returns every key so you can iterate — both routine, everyday Perl operations that would require substantial hand-written code in C.
Pointers vs. References
Address-of and reference creation — a deliberate, close parallel
#include <stdio.h> int main(void) { int value = 42; int *pointer = &value; printf("%d\n", *pointer); return 0; }
use v5.38; my $value = 42; my $reference = \$value; say $$reference;
This is one of the closest conceptual bridges on the whole site — Perl references were deliberately designed to echo C pointers. C's &value ("address of") becomes Perl's \$value (backslash, "reference to"); C's *pointer (dereference) becomes Perl's $$reference (double sigil, dereference). The underlying idea — a value that identifies another value's location rather than holding it directly — transfers almost unchanged.
Modifying the original through a reference/pointer
#include <stdio.h> int main(void) { int value = 42; int *pointer = &value; *pointer = 99; printf("%d\n", value); return 0; }
use v5.38; my $value = 42; my $reference = \$value; $$reference = 99; say $value;
Assigning through a dereferenced pointer/reference mutates the ORIGINAL value in both languages, with nearly identical syntax: *pointer = 99 in C, $$reference = 99 in Perl. Neither language copies the value first — both are genuinely operating on the same memory location the reference/pointer identifies.
Anonymous array references versus array-to-pointer decay
#include <stdio.h> void printFirst(int *numbers) { // A C array parameter silently "decays" into a pointer to its // first element — the function has no idea how many elements // follow without a separately-passed length: printf("%d\n", numbers[0]); } int main(void) { int numbers[] = {1, 2, 3}; printFirst(numbers); return 0; }
use v5.38; sub print_first { my ($numbers_ref) = @_; say $numbers_ref->[0]; } my $numbers_ref = [1, 2, 3]; print_first($numbers_ref);
A C array parameter silently decays into a bare pointer to its first element, losing all length information — a genuine, well-known C footgun requiring a separately-passed length parameter. Perl's [1, 2, 3] creates an explicit ARRAY REFERENCE, a single scalar value that always knows how many elements it points to — $numbers_ref->[0] dereferences and indexes in one step via the arrow operator, with no silent information loss.
malloc/free vs. Reference Counting
No malloc/free at all — the single biggest relief for a C developer
#include <stdio.h> #include <stdlib.h> int main(void) { int *numbers = malloc(5 * sizeof(int)); for (int i = 0; i < 5; i++) { numbers[i] = i * i; } printf("%d\n", numbers[4]); free(numbers); // forgetting this line leaks memory return 0; }
use v5.38; my @numbers = map { $_ * $_ } (0 .. 4); say $numbers[4]; # No free() call anywhere — Perl's reference counter reclaims # @numbers automatically once nothing refers to it anymore
This is likely the single biggest relief a C developer feels reading Perl: there is no malloc/free pair to manage, and no possibility of a memory leak from a forgotten free, a double-free, or a use-after-free dangling pointer. Perl tracks a reference count on every value and reclaims it automatically the instant nothing refers to it anymore — the programmer never manages memory lifetime directly.
No dangling pointers, no use-after-free
#include <stdio.h> #include <stdlib.h> int main(void) { // This is a genuine, classic C bug — using memory after it has // already been freed. The behavior is UNDEFINED, not a clean // error; it might print garbage, crash, or appear to "work": int *value = malloc(sizeof(int)); *value = 42; free(value); // printf("%d\n", *value); // undefined behavior — commented out printf("would be use-after-free if uncommented above\n"); return 0; }
use v5.38; my $reference = \42; say $$reference; # There is no way to "free" $reference early and then accidentally # use it afterward — Perl's reference counting guarantees the # underlying value stays valid for as long as any reference to it # exists, full stop, with no manual lifetime tracking required
Use-after-free in C is undefined behavior — the freed memory might be reused by something else entirely, producing garbage, a crash, or (worst of all) apparently correct output that later breaks under different conditions. Perl structurally cannot have this bug category: a value is only ever reclaimed once its reference count hits zero, meaning nothing anywhere still holds a reference to it — there is no way to "free early by mistake" the way a stray C free() call allows.
Control Flow
if/else
#include <stdio.h> int main(void) { int n = -5; if (n == 0) { printf("zero\n"); } else if (n > 0) { printf("positive\n"); } else { printf("negative\n"); } return 0; }
use v5.38; my $n = -5; if ($n == 0) { say "zero"; } elsif ($n > 0) { say "positive"; } else { say "negative"; }
The structure is nearly identical — braces are mandatory in both languages (unlike some C style that permits omitting them for single statements) — with one small spelling difference: C's else if is two words; Perl contracts it to one, elsif.
Iterating a collection: index-based versus foreach
#include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5}; for (int i = 0; i < 5; i++) { printf("%d ", numbers[i]); } printf("\n"); return 0; }
use v5.38; my @numbers = (1, 2, 3, 4, 5); for my $number (@numbers) { print "$number "; } print "\n";
C's idiomatic loop is index-based, since it has no built-in iteration protocol — you manage the counter and the bounds check yourself. Perl's for my $number (@numbers) iterates the elements directly, with no index variable, no bounds check, and no off-by-one risk to manage.
Functions vs. Subroutines
Typed parameters versus the @_ argument array
#include <stdio.h> int add(int x, int y) { return x + y; } int main(void) { printf("%d\n", add(2, 3)); return 0; }
use v5.38; sub add { my ($x, $y) = @_; return $x + $y; } say add(2, 3);
C declares a fixed, typed parameter list as part of the function signature. Perl subroutines have no parameter list syntax at all — every call's arguments arrive in the special array @_, and my ($x, $y) = @_; is the idiomatic first line unpacking them into named variables. This also means a Perl subroutine can be called with any number of arguments; nothing enforces exactly two the way C's int add(int x, int y) signature does.
Variable-length argument lists — no va_list machinery needed
#include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list args; va_start(args, count); int total = 0; for (int i = 0; i < count; i++) { total += va_arg(args, int); } va_end(args); return total; } int main(void) { printf("%d\n", sum(3, 1, 2, 3)); return 0; }
use v5.38; sub total_sum { my $total = 0; $total += $_ for @_; return $total; } say total_sum(1, 2, 3);
C requires the explicit, error-prone <stdarg.h> machinery (va_list/va_start/va_arg/va_end, plus a separately-passed count since C cannot otherwise know how many arguments were passed) to accept a variable number of arguments. Every Perl subroutine already accepts any number of arguments for free via @_ — no special declaration or machinery needed at all.
Regular Expressions: Built In
Regex matching: built into the language, not a library call
// C has no regular-expression support in the language or its // standard library at all — POSIX systems provide <regex.h>, but // it is a separate, C-string-based API with its own compile/exec/ // free lifecycle, not language syntax: #include <stdio.h> int main(void) { printf("C needs regex.h or a third-party library for this\n"); return 0; }
use v5.38; my $text = "The year is 2026"; if ($text =~ /(\d+)/) { say "Found: $1"; }
Perl regular expressions are language syntax, not a library call: =~ applies a pattern directly, and a successful capture group is available immediately afterward as $1. C has no regex support in the language itself; POSIX's <regex.h> exists but requires explicitly compiling a pattern, executing it, and freeing it — three separate steps with their own lifecycle, a genuinely heavier-weight API than Perl's built-in operator.
Substitution: s/// with no C equivalent at all
// Substituting a regex match in place would require, in C, manually // finding the match with regex.h, computing the new string length, // allocating a new buffer, and copying the pieces around it — there // is no single-operation equivalent: #include <stdio.h> int main(void) { printf("substitution requires manual buffer surgery in C\n"); return 0; }
use v5.38; my $text = "Hello, World!"; $text =~ s/World/Perl/; say $text;
Perl's s/// substitution operator finds a pattern and replaces it in one operation, resizing the string automatically. The equivalent in C means manually finding the match, computing the new total length, allocating a correctly-sized buffer, and copying the unchanged prefix, the replacement, and the unchanged suffix around it by hand — no single operation covers this the way s/// does.
Structs vs. Hash-Based Records
A fixed-shape struct versus a flexible hash
#include <stdio.h> struct Point { int x; int y; }; int main(void) { struct Point point = {3, 4}; printf("%d and %d\n", point.x, point.y); return 0; }
use v5.38; my %point = (x => 3, y => 4); say "$point{x} and $point{y}";
A C struct has a fixed set of typed fields, declared once and enforced by the compiler for every instance — adding a field at runtime is impossible. Perl's hash-as-record idiom (%point with keys x/y) has no fixed shape at all: fields can be added or removed from any individual hash at runtime, with no compiler enforcing that every "point" hash actually has both an x and a y.
Fixed memory layout versus per-instance flexibility
#include <stdio.h> struct Point { int x; int y; }; int main(void) { // Every "struct Point" has the EXACT same memory layout and // size — sizeof(struct Point) is a compile-time constant: printf("%zu\n", sizeof(struct Point)); return 0; }
use v5.38; my %point3d = (x => 3, y => 4, z => 5); # this one has 3 keys my %point2d = (x => 3, y => 4); # this one has only 2 say scalar keys %point3d; say scalar keys %point2d;
This is a genuine tradeoff, not just a syntax difference. C's struct Point guarantees every instance has the identical memory layout and size, checked at compile time — a real efficiency and correctness guarantee. Perl hashes trade that guarantee for flexibility: two hashes built with the same intent can end up with entirely different sets of keys, since nothing enforces a common "shape" across them the way a C struct type does.
No OOP vs. bless-Based OOP
Simulating objects in C versus Perl's bless
#include <stdio.h> #include <string.h> // C has no OOP at all — the closest simulation is a struct holding // data plus function pointers acting as "methods," wired up by hand: struct Animal { char name[32]; void (*speak)(struct Animal *self); }; void dogSpeak(struct Animal *self) { printf("%s says Woof\n", self->name); } int main(void) { struct Animal dog; strcpy(dog.name, "Rex"); dog.speak = dogSpeak; dog.speak(&dog); return 0; }
use v5.38; package Animal; sub new { my ($class, %args) = @_; return bless { name => $args{name} }, $class; } sub speak { my $self = shift; say "$self->{name} says Woof"; } package main; my $dog = Animal->new(name => "Rex"); $dog->speak;
C has no object system at all; the closest simulation is a struct holding data plus function pointers wired up by hand to act as "methods" — genuinely more manual than even Perl's explicit approach. Perl's bless associates a reference (usually a hash reference) with a package name, making it dispatchable via ->; my $self = shift is the idiomatic first line of every method, extracting the invocant the same way a hand-rolled C "method" would take self as its first parameter.
Return Codes vs. die/eval
Return codes and errno versus die/eval
#include <stdio.h> #include <errno.h> int main(void) { FILE *file = fopen("/does/not/exist.txt", "r"); if (file == NULL) { printf("Error: could not open file (errno %d)\n", errno); return 1; } fclose(file); return 0; }
use v5.38; eval { open(my $fh, "<", "/does/not/exist.txt") or die "Could not open file: $!"; }; if ($@) { say "Error: $@"; }
C signals failure through a special return value (here, a NULL file pointer) that the caller must remember to check, plus the global errno for details — nothing forces the check, and a forgotten one silently continues with invalid data. Perl's die raises an exception that propagates until caught by an enclosing eval { } block, with the error message landing in $@ — closer to exception handling in higher-level languages, and harder to silently ignore than a C return-code check.
die with a custom message: no errno lookup table needed
#include <stdio.h> int divide(int x, int y, int *result) { if (y == 0) { return -1; // caller must know -1 means "divide by zero" } *result = x / y; return 0; } int main(void) { int result; if (divide(10, 0, &result) != 0) { printf("Error: divide by zero\n"); return 1; } printf("%d\n", result); return 0; }
use v5.38; sub my_divide { my ($x, $y) = @_; die "divide by zero\n" if $y == 0; return $x / $y; } eval { say my_divide(10, 0); }; if ($@) { print "Error: $@"; }
C's -1 return value means "divide by zero" only by convention documented separately from the code itself — the caller must already know what each error code means. Perl's die "divide by zero\n" carries the actual error message directly, available verbatim in $@ at the catch site, with no separate lookup table or documentation needed to interpret it.
Gotchas for C Developers
Context sensitivity: the same expression means different things
// C has no equivalent to context sensitivity — an expression's // meaning never changes based on where it appears; a value is // simply the type it was declared as, full stop: #include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5}; int length = sizeof(numbers) / sizeof(numbers[0]); printf("%d\n", length); return 0; }
use v5.38; my @numbers = (1, 2, 3, 4, 5); my $count = @numbers; # numeric context: 5 (the length) my ($first) = @numbers; # list context: 1 (the first element) say $count; say $first;
This is a real, well-known Perl surprise with no C parallel at all: the exact same @numbers expression means "how many elements" in scalar/numeric context (my $count = @numbers) but "the list of elements, destructured" in list context (my ($first) = @numbers). C has nothing like this — an expression's meaning never shifts based on the surrounding syntax the way Perl's context sensitivity does.
Truthiness rules: "0" the string is false, unlike most falsy strings
#include <stdio.h> #include <string.h> int main(void) { // C has no separate notion of a "falsy string" at all — only // an actual 0 integer (or NULL pointer) is false in a condition: const char *text = "0"; if (strlen(text) > 0) { printf("non-empty, so this always runs in C\n"); } return 0; }
use v5.38; my $text = "0"; if ($text) { say "truthy"; } else { say "falsy"; # this one actually runs! }
This is a genuinely surprising Perl-specific rule: the single-character string "0" is FALSE in a boolean context, even though it is a non-empty string — one of only a handful of falsy values in Perl (alongside undef, 0, and the empty string ""). C has nothing resembling this distinction: a C string is simply a pointer, and only a genuinely null/zero pointer or the integer 0 is false in a condition — string CONTENTS never factor into truthiness at all.