PONYλM2Modula-2

C.CodeCompared.To/Perl

An interactive executable cheatsheet comparing C and Perl

C17 (GCC) Perl 5.44
Output & Basics
Hello, World
C requires an explicit #include, a typed int main(void) entry point, and an explicit return 0. A Perl script is just a sequence of statements — none of that ceremony.
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
use v5.38; say "Hello, World!";
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
Both languages simply execute statements top to bottom — a genuine structural match.
#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 are imperative languages, so no special sequencing construct is needed at all.
Variables & Sigils
Sigils: a genuinely new syntax concept for C
Perl prefixes every variable with a sigil indicating its access type: $ for a single (scalar) value, @ for an array, % for a hash — one of the first genuinely new syntax ideas a C developer meets.
#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. $count here means "the scalar value named count," not a special operator.
`my` gives block scope — closer to C than you might expect
Perl's my declares a lexically-scoped variable — confined to the enclosing block, exactly like a C variable declared inside { }.
#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;
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
Perl has no compile-time type checking at all, where C checks every argument and return type before the program can run.
#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);
add("two", 3) would run anyway: Perl coerces "two" to 0 during numeric addition and produces 3. Under use v5.38 (which enables warnings) that coercion emits a runtime "isn't numeric" warning — but only a warning; nothing rejects the call up front the way C's compiler would.
A single scalar variable can hold any type, at any time
A Perl scalar can hold a number at one moment and a string the next — the $ sigil describes the ACCESS pattern (single value), not a fixed data type.
#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; the Perl reassignment shown here needs no re-declaration or cast.
Strings
String concatenation — no more manual buffer management
Perl's . operator concatenates strings directly, growing the result automatically — no buffer to size, no buffer to overflow.
#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.
String interpolation — no format specifiers needed
Perl interpolates variables directly inside a double-quoted string — no format specifiers, no type matching to get wrong.
#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 type-specific placeholders (%s, %d) that must match the argument types exactly; a mismatch is undefined behavior, and older C standards offered no compiler enforcement at all.
Fixed Arrays vs. Dynamic Arrays
Arrays: fixed size versus dynamic
A C array's size is fixed at declaration and baked into the type; a Perl array grows and shrinks dynamically at runtime.
#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";
push/pop/shift/unshift resize a Perl array freely, with no manual reallocation ever required. Interpolating "@numbers" in a double-quoted string joins the elements with spaces.
Growing an array at runtime — impossible for a C array
Growing an array at runtime is something a plain C array structurally cannot do at all — int numbers[5] is a fixed block of exactly 5 ints, full stop.
#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";
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
Perl arrays always know their own length — scalar @numbers returns it directly.
#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-division 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).
Hashes: No C Equivalent
Hashes: a data structure C has no built-in equivalent for
Hashes are a genuinely new capability for a C developer: Perl's % sigil and => syntax build a hash table directly into the language.
// 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};
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.
Checking for a key and iterating
exists checks for key presence, and keys returns every key for iteration — routine, everyday Perl operations.
// 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 without triggering autovivification (accidentally creating the key by looking it up). Both operations would require substantial hand-written code in C.
Pointers vs. References
Address-of and reference creation — a deliberate, close parallel
One of the closest conceptual bridges on the whole site: Perl references were deliberately designed to echo C pointers. &value becomes \$value; *pointer becomes $$reference.
#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;
The underlying idea — a value that identifies another value's location rather than holding it directly — transfers almost unchanged from C.
Modifying the original through a reference/pointer
Assigning through a dereferenced pointer/reference mutates the ORIGINAL value in both languages: *pointer = 99 in C, $$reference = 99 in Perl.
#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;
Neither language copies the value first — both are genuinely operating on the same storage the reference/pointer identifies.
Anonymous array references versus array-to-pointer decay
Perl's [1, 2, 3] creates an explicit ARRAY REFERENCE — a single scalar value that always knows how many elements it points to.
#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 well-known footgun requiring a separately-passed length parameter. $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
Likely the single biggest relief for a C developer reading Perl: there is no malloc/free pair to manage at all.
#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
Perl tracks a reference count on every value and reclaims it automatically the instant nothing refers to it anymore — no leaks from a forgotten free, no double-frees, no use-after-free dangling pointers.
No dangling pointers, no use-after-free
Perl structurally cannot have a use-after-free bug: a value is only reclaimed once its reference count hits zero, meaning nothing anywhere still refers to it.
#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. There is no way to "free early by mistake" in Perl the way a stray C free() call allows.
Control Flow
if/else
The structure is nearly identical — one small spelling difference: C's else if is two words; Perl contracts it to one, elsif.
#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"; }
Braces are mandatory in Perl, unlike C, which permits omitting them for single statements.
Iterating a collection: index-based versus foreach
Perl's for my $number (@numbers) iterates the elements directly — no index variable, no bounds check, no off-by-one risk.
#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.
Functions vs. Subroutines
Typed parameters versus the @_ argument array
Perl subroutines have no parameter list in the signature — every call's arguments arrive in the special array @_, and my ($x, $y) = @_; is the idiomatic first line unpacking them.
#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);
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
Every Perl subroutine already accepts any number of arguments for free via @_ — no special declaration or machinery.
#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.
Regular Expressions: Built In
Regex matching: built into the language, not a library call
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 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"; }
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
Perl's s/// substitution operator finds a pattern and replaces it in one operation, resizing the string automatically.
// 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;
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
Perl's record idiom is a hash with conventional keys — where a C struct declares a fixed set of typed fields enforced by the compiler.
#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}";
The hash 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
This is a genuine tradeoff, not just a syntax difference: C guarantees an identical layout for every instance; Perl lets every hash carry its own set of keys.
#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;
sizeof(struct Point) is a compile-time constant — a real efficiency and correctness guarantee. Perl hashes trade that for flexibility: two hashes built with the same intent can end up with entirely different keys, since nothing enforces a common "shape" across them.
No OOP vs. bless-Based OOP
Simulating objects in C versus Perl's bless
Perl's bless associates a reference (usually a hash reference) with a package name, making method calls via -> dispatch to that package's subroutines.
#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 hand-wired function pointers — even more manual than Perl's explicit approach. my $self = shift is the idiomatic first line of every method, extracting the invocant the same way a hand-rolled C "method" takes self as its first parameter.
Return Codes vs. die/eval
Return codes and errno versus die/eval
Perl's die raises an exception that propagates until caught by an enclosing eval { } block, with the error message landing in $@.
#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. An uncaught die is much harder to ignore.
die with a custom message: no errno lookup table needed
Perl's die "message" carries the actual error text with the error — no numeric code, no separate lookup table.
#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 Perl message arrives verbatim in $@ at the catch site.
Gotchas for C Developers
Context sensitivity: the same expression means different things
Watch out: the same @numbers expression means "how many elements" in scalar context but "the elements themselves" in list context — a Perl concept with no C parallel at all.
// 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;
my $count = @numbers is scalar context (5); my ($first) = @numbers is list context (the first element). In C, an expression's meaning never shifts based on the surrounding syntax this way.
Truthiness rules: "0" the string is false, unlike most falsy strings
Watch out: the single-character string "0" is FALSE in a Perl boolean context, even though it is a non-empty string.
#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! }
Perl's falsy values are a short list: undef, 0, "", and "0". C has nothing resembling this — a C string is simply a pointer, and string CONTENTS never factor into truthiness at all.