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 anymorePerl 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 requiredUse-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.