Output & Compiling
Hello, World
<?php
echo "Hello, World!\n"; #include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
} Every C program is a set of functions, and execution begins at
main, which returns an int exit status (0 means success). You must #include <stdio.h> to get printf — nothing is available globally the way PHP's built-ins are. There is no echo; printf writes exactly the bytes you give it, and you spell the newline as \n.Compiling vs Interpreting
<?php
// PHP is interpreted — run the source directly:
// php script.php
$name = "World";
echo "Hello, {$name}!\n"; #include <stdio.h>
// C is compiled — build first, then run the binary:
// gcc -std=c17 -Wall hello.c -o hello
// ./hello
int main(void) {
const char *name = "World";
printf("Hello, %s!\n", name);
return 0;
} The biggest shift: C is compiled ahead of time, not interpreted per request. A compiler turns your source into a native binary, catching type errors before the program ever runs — the price is a build step and no live reload. There is no interpreter reading the file at runtime, so the program is just machine code by the time it executes.
Format Specifiers
<?php
$name = "Alice";
$age = 30;
$ratio = 3.14159;
echo "Name: " . $name . "\n";
printf("Age: %d\n", $age);
printf("Ratio: %.2f\n", $ratio); #include <stdio.h>
int main(void) {
const char *name = "Alice";
int age = 30;
double ratio = 3.14159;
printf("Name: %s\n", name);
printf("Age: %d\n", age);
printf("Ratio: %.2f\n", ratio);
return 0;
} C has no string interpolation at all — you must use
printf format specifiers, and they must match the argument types exactly: %d for int, %s for a string, %f for a double, %.2f to fix two decimals. A mismatch (say %d with a double) is undefined behavior, not a friendly coercion — the compiler's -Wall warnings are your safety net.Variables & Types
Typed Declarations
<?php
$name = "Alice"; // type inferred, can change later
$age = 30;
$age = "thirty"; // legal — PHP variables are dynamic
echo "{$name}: {$age}\n"; #include <stdio.h>
int main(void) {
const char *name = "Alice"; // type fixed at declaration
int age = 30;
// age = "thirty"; // COMPILE ERROR — int can't hold a string
printf("%s: %d\n", name, age);
return 0;
} In C every variable has a static type fixed when you declare it, and it can only ever hold that type. There is no
$ sigil, but you must write the type (int, double, char, a pointer…) in front of the name. Assigning a string to an int is a compile error, not a silent re-type — the dynamic reassignment PHP allows simply does not exist.Primitive Types
<?php
$count = 42;
$ratio = 3.14;
$grade = "A"; // PHP has no char type; this is a 1-char string
echo gettype($count) . "\n"; // integer
echo gettype($ratio) . "\n"; // double
echo gettype($grade) . "\n"; // string #include <stdio.h>
int main(void) {
int count = 42;
double ratio = 3.14;
char grade = 'A'; // a single character, in single quotes
printf("%d\n", count);
printf("%f\n", ratio);
printf("%c (code %d)\n", grade, grade); // A (code 65)
return 0;
} C's primitives are low-level:
int (typically 32-bit), double (and float), and char — which is really a one-byte integer holding a character code. Note the quoting rule that trips up newcomers: "A" (double quotes) is a string, while 'A' (single quotes) is a single char. Sized types like int32_t and uint8_t live in <stdint.h> when you need exact widths.Booleans
<?php
$active = true;
$done = false;
echo ($active ? "yes" : "no") . "\n";
var_dump($done); #include <stdio.h>
#include <stdbool.h> // gives you bool, true, false
int main(void) {
bool active = true;
bool done = false;
printf("%s\n", active ? "yes" : "no");
printf("%d\n", done); // 0 — a bool is really an int
return 0;
} C had no boolean type for decades;
true/false and the bool alias arrive only when you #include <stdbool.h>, and under the hood they are just the integers 1 and 0. There is no %b format — print a bool with %d to see its numeric value. This integer-ness underlies C's truthiness rules, covered in the Gotchas.Explicit Casts
<?php
$total = 7;
$count = 2;
echo $total / $count . "\n"; // 3.5 — auto-promotes to float
$rounded = (int) (7 / 2);
echo $rounded . "\n"; // 3 #include <stdio.h>
int main(void) {
int total = 7;
int count = 2;
printf("%d\n", total / count); // 3 — integer division!
printf("%f\n", (double) total / count); // 3.5 — cast first
return 0;
} C never promotes types for your convenience.
int / int stays an int, truncating 7 / 2 to 3 — unlike PHP, which quietly produces 3.5. To get a real quotient you (double)-cast an operand before dividing. Casts are explicit, deliberate, and exactly as wide as you ask; there is no hidden juggling.Strings
Strings Are Char Arrays
<?php
$greeting = "Hello";
echo strlen($greeting) . "\n"; // 5
echo $greeting[0] . "\n"; // H #include <stdio.h>
#include <string.h>
int main(void) {
char greeting[] = "Hello"; // array of 6: H e l l o \0
printf("%zu\n", strlen(greeting)); // 5
printf("%c\n", greeting[0]); // H
printf("array size: %zu\n", sizeof(greeting)); // 6 — incl. terminator
return 0;
} A C string is not an object — it is a plain array of
char ending in a hidden \0 (null terminator). That trailing byte is why sizeof is one more than the length, and why every string function must walk to the \0 to find the end. There is no .length property: strlen (from <string.h>) counts characters by scanning for the terminator each time it is called.String Functions
<?php
$text = "Hello, World";
echo strlen($text) . "\n"; // 12
echo strtoupper($text) . "\n"; // HELLO, WORLD
var_dump(str_contains($text, "World")); // true #include <stdio.h>
#include <string.h>
#include <ctype.h>
int main(void) {
char text[] = "Hello, World";
printf("%zu\n", strlen(text)); // 12
for (size_t index = 0; text[index] != '\0'; index++) {
text[index] = (char) toupper((unsigned char) text[index]);
}
printf("%s\n", text); // HELLO, WORLD
printf("%d\n", strstr(text, "WORLD") != NULL); // 1 — found
return 0;
} PHP's rich string library shrinks to a handful of low-level helpers in
<string.h> and <ctype.h>. There is no strtoupper — you loop over the characters and uppercase each one with toupper, mutating the array in place. strstr returns a pointer to the match (or NULL), standing in for str_contains. Everything is manual, and you are responsible for not running past the buffer.Concatenation
<?php
$first = "Hello";
$last = "World";
$joined = $first . ", " . $last;
echo $joined . "\n"; // Hello, World #include <stdio.h>
int main(void) {
const char *first = "Hello";
const char *last = "World";
char joined[64]; // you must size the destination buffer yourself
snprintf(joined, sizeof(joined), "%s, %s", first, last);
printf("%s\n", joined); // Hello, World
return 0;
} There is no
. concatenation operator and no automatic allocation. To build a string you declare a destination buffer big enough to hold the result and fill it with snprintf, which (unlike the dangerous strcat/sprintf) takes the buffer size and refuses to overflow it. Forgetting to size the buffer for the result — including the \0 — is the classic C bug.Arrays
Fixed-Size Arrays
<?php
$numbers = [10, 20, 30];
$numbers[] = 40; // grows automatically
echo count($numbers) . "\n"; // 4
echo $numbers[1] . "\n"; // 20 #include <stdio.h>
int main(void) {
int numbers[3] = {10, 20, 30}; // size fixed at compile time
// numbers[3] = 40; // OUT OF BOUNDS — undefined behavior, no error
size_t count = sizeof(numbers) / sizeof(numbers[0]);
printf("%zu\n", count); // 3
printf("%d\n", numbers[1]); // 20
return 0;
} A C array has a fixed size baked in at compile time and cannot grow — there is no
$numbers[] = 40. Worse, C does not store the length or check bounds: count is computed with the sizeof(array) / sizeof(element) idiom, and indexing past the end is undefined behavior that often "works" silently until it corrupts memory. Growable arrays require manual malloc/realloc, shown in Pointers.Iterating
<?php
$numbers = [1, 2, 3, 4];
foreach ($numbers as $number) {
echo $number * 10 . "\n";
} #include <stdio.h>
int main(void) {
int numbers[] = {1, 2, 3, 4};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
for (size_t index = 0; index < count; index++) {
printf("%d\n", numbers[index] * 10);
}
return 0;
} There is no
foreach and no iterator protocol — you write an explicit index loop, and you must know the count yourself because the array does not carry its length. The loop counter is size_t (an unsigned type sized for memory offsets), which is the correct type for indices and what sizeof returns.No Built-in Maps
<?php
// PHP's array doubles as a hash map for free:
$ages = ["Alice" => 30, "Bob" => 25];
echo $ages["Alice"] . "\n"; // 30
$ages["Carol"] = 35; #include <stdio.h>
#include <string.h>
// C has no map type. A small lookup is an array of structs you scan.
struct Entry { const char *name; int age; };
int main(void) {
struct Entry ages[] = {{"Alice", 30}, {"Bob", 25}};
size_t count = sizeof(ages) / sizeof(ages[0]);
for (size_t index = 0; index < count; index++) {
if (strcmp(ages[index].name, "Alice") == 0) {
printf("%d\n", ages[index].age); // 30
}
}
return 0;
} PHP's single array type is also a hash map; C has no associative array at all. For a handful of entries you scan an array of structs by hand (as here). For real key/value work you implement a hash table or pull in a third-party library — there is no equivalent of just writing
["Alice" => 30]. This absence is one of the largest day-to-day adjustments.Pointers & Memory
What a Pointer Is
<?php
// PHP hides addresses entirely — variables just hold values.
$value = 42;
$alias = $value; // a copy
$alias = 99;
echo $value . "\n"; // 42 — untouched #include <stdio.h>
int main(void) {
int value = 42;
int *pointer = &value; // & = "address of"; pointer holds where value lives
printf("%d\n", value); // 42 — the value
printf("%p\n", (void *) pointer); // the address (hex)
printf("%d\n", *pointer); // 42 — * = "value at that address"
*pointer = 99; // write through the pointer
printf("%d\n", value); // 99 — changed the original
return 0;
} A pointer is a variable that holds a memory address.
&value takes the address of a variable; *pointer dereferences it to read or write whatever lives there. PHP hides this machinery completely, so this is genuinely new — but it underpins everything in C: passing data without copying it, dynamic memory, arrays, and strings are all pointers underneath.Pass by Pointer
<?php
// PHP passes by value; use & to let a function mutate the caller's variable.
function increment(int &$value): void {
$value += 1;
}
$count = 5;
increment($count);
echo $count . "\n"; // 6 #include <stdio.h>
// C is always pass-by-value; pass a POINTER to mutate the original.
void increment(int *value) {
*value += 1;
}
int main(void) {
int count = 5;
increment(&count); // pass the address
printf("%d\n", count); // 6
return 0;
} C functions always receive copies of their arguments. To let a function modify the caller's variable — PHP's
&$value by-reference parameter — you pass the variable's address (&count) and the function writes through the pointer (*value). This explicitness is why so many C functions take pointer arguments: it is the only way to return changes through a parameter.Manual Memory: malloc / free
<?php
// PHP allocates and frees memory for you automatically (GC).
function makeNumbers(int $size): array {
$numbers = [];
for ($i = 0; $i < $size; $i++) {
$numbers[] = $i * 10;
}
return $numbers; // freed automatically when unused
}
print_r(makeNumbers(3)); #include <stdio.h>
#include <stdlib.h>
int main(void) {
int size = 3;
int *numbers = malloc(size * sizeof(int)); // request heap memory
if (numbers == NULL) {
return 1; // allocation failed
}
for (int index = 0; index < size; index++) {
numbers[index] = index * 10;
}
printf("%d\n", numbers[1]); // 10
free(numbers); // YOU must release it
return 0;
} C has no garbage collector. Memory that must outlive a function or whose size is unknown at compile time comes from the heap via
malloc (which returns NULL if it fails — always check), and you must free it yourself. Forgetting to free leaks memory; freeing twice or using memory after freeing corrupts the program. This manual ownership discipline is the heart of systems programming.Arrays Decay to Pointers
<?php
// In PHP an array always knows its own length.
function sumAll(array $numbers): int {
return array_sum($numbers); // count is intrinsic
}
echo sumAll([1, 2, 3, 4]) . "\n"; // 10 #include <stdio.h>
// A function receives only a pointer — the length must be passed separately.
int sum_all(const int *numbers, size_t count) {
int total = 0;
for (size_t index = 0; index < count; index++) {
total += numbers[index];
}
return total;
}
int main(void) {
int numbers[] = {1, 2, 3, 4};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
printf("%d\n", sum_all(numbers, count)); // 10
return 0;
} When you pass an array to a function it "decays" to a bare pointer to its first element — the size information is lost at the boundary. That is why C functions taking arrays almost always take a companion
count parameter: inside sum_all, sizeof(numbers) would give the size of a pointer, not the array. PHP's arrays carry their length everywhere; C's do not.NULL & Segfaults
<?php
// Accessing a missing value yields null and a warning, never a crash.
$user = null;
echo ($user?->name ?? "no user") . "\n"; // no user #include <stdio.h>
int main(void) {
int *pointer = NULL; // points at nothing
if (pointer != NULL) {
printf("%d\n", *pointer);
} else {
printf("pointer is NULL — not dereferencing\n");
}
// *pointer = 5; // SEGFAULT: dereferencing NULL crashes the program
return 0;
} A pointer that points at nothing is
NULL. Dereferencing it (*pointer) does not return a tidy null with a warning as PHP does — it triggers a segmentation fault that kills the process outright. The defensive habit C demands is to check if (pointer != NULL) before every dereference, since the language offers no safety net and no optional-chaining operator.Control Flow
Conditionals
<?php
$age = 20;
if ($age >= 65) {
echo "senior\n";
} elseif ($age >= 18) {
echo "adult\n";
} else {
echo "minor\n";
} #include <stdio.h>
int main(void) {
int age = 20;
if (age >= 65) {
printf("senior\n");
} else if (age >= 18) {
printf("adult\n");
} else {
printf("minor\n");
}
return 0;
} Conditionals are almost identical — parentheses and braces included — with the one difference that C writes
else if as two words rather than PHP's fused elseif. Remember that C's condition is just an integer test: any non-zero value is true, so an accidental assignment if (x = 5) compiles and is always true (a reason compilers warn about it).switch & Fall-through
<?php
$status = 2;
$label = match ($status) {
1 => "active",
2, 3 => "pending",
default => "unknown",
};
echo $label . "\n"; #include <stdio.h>
int main(void) {
int status = 2;
const char *label;
switch (status) {
case 1: label = "active"; break;
case 2:
case 3: label = "pending"; break; // 2 and 3 share
default: label = "unknown";
}
printf("%s\n", label); // pending
return 0;
} C only has the old
switch statement, not an expression like PHP 8's match. It compares with loose integer equality, does not return a value, and — critically — falls through from one case into the next unless you write break. Stacking labels (case 2: case 3:) is how you group values, but a forgotten break is a classic bug that match made impossible.Loops
<?php
for ($i = 0; $i < 3; $i++) {
echo $i . "\n";
}
$count = 0;
while ($count < 2) {
echo "tick\n";
$count++;
} #include <stdio.h>
int main(void) {
for (int i = 0; i < 3; i++) {
printf("%d\n", i);
}
int count = 0;
while (count < 2) {
printf("tick\n");
count++;
}
return 0;
} The C-style
for and while are exactly what PHP inherited from C, so these read identically. What is missing is the high-level iteration: no foreach, no array_map, no ranges. In C you almost always count an index by hand, which is why the humble for loop does far more of the work than it does in PHP.Truthiness
<?php
// PHP falsy: 0, 0.0, "", "0", [], null, false.
$values = [0, "", "0", "hi"];
foreach ($values as $value) {
echo ($value ? "truthy" : "falsy") . "\n";
} #include <stdio.h>
int main(void) {
// In C, only the number 0 (and NULL) is false. That's it.
if (0) printf("0 is true\n"); else printf("0 is false\n");
if (-1) printf("-1 is true\n");
if ("0") printf("\"0\" pointer is true\n"); // any non-NULL pointer
return 0;
} C's truthiness is purely numeric: zero is false, every other value is true. There is no notion of an empty string or empty array being falsy — a string literal is a non-
NULL pointer, so if ("0") is true, the opposite of PHP where "0" is falsy. Negative numbers are true. This simplicity removes PHP's surprises but means you compare explicitly (strlen(text) == 0) to test emptiness.Functions
Defining Functions
<?php
function add(int $a, int $b): int {
return $a + $b;
}
echo add(2, 3) . "\n"; // 5 #include <stdio.h>
// Return type comes first; every parameter is typed.
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%d\n", add(2, 3)); // 5
return 0;
} A C function declares its return type before the name and types every parameter — the type annotations PHP makes optional are mandatory here. A function must be declared before it is called, so either define it above
main (as here) or provide a forward "prototype". There is no overloading: two functions cannot share a name, even with different parameter types.No Default Arguments
<?php
function connect(string $host, int $port = 5432): string {
return "{$host}:{$port}";
}
echo connect("db.local") . "\n"; // db.local:5432
echo connect("db.local", 6432) . "\n"; // db.local:6432 #include <stdio.h>
// No default parameters — provide a separate wrapper, or always pass all args.
void connect_to(const char *host, int port) {
printf("%s:%d\n", host, port);
}
void connect_default(const char *host) {
connect_to(host, 5432);
}
int main(void) {
connect_default("db.local"); // db.local:5432
connect_to("db.local", 6432); // db.local:6432
return 0;
} C has no default parameter values and no named arguments. Every call must pass every argument, in order. The usual workaround is to write a small wrapper function that fills in the default and forwards — verbose compared to PHP's
$port = 5432, but explicit. (Variadic functions like printf exist via <stdarg.h>, but they are clumsy and type-unsafe.)Function Pointers
<?php
function applyTwice(callable $fn, int $value): int {
return $fn($fn($value));
}
$increment = fn($n) => $n + 1;
echo applyTwice($increment, 5) . "\n"; // 7 #include <stdio.h>
int increment(int n) { return n + 1; }
// 'int (*operation)(int)' is a pointer to a function taking int, returning int.
int apply_twice(int (*operation)(int), int value) {
return operation(operation(value));
}
int main(void) {
printf("%d\n", apply_twice(increment, 5)); // 7
return 0;
} C passes behavior with function pointers — the closest thing to PHP's
callable. The declaration syntax int (*operation)(int) reads "pointer to a function taking an int and returning an int". There are no closures: a function pointer captures nothing from its surroundings, so any extra data a callback needs must be threaded through explicitly (often as a void * context argument).Structs
Structs vs Classes
<?php
class Point {
public function __construct(public int $x, public int $y) {}
}
$point = new Point(3, 4);
echo "{$point->x}, {$point->y}\n"; #include <stdio.h>
struct Point { int x; int y; };
int main(void) {
struct Point point = {3, 4}; // value on the stack, no 'new'
printf("%d, %d\n", point.x, point.y);
return 0;
} A
struct groups named fields — the data half of a PHP class, with no methods, no constructor, and no visibility. You create one as a plain value (no new, no heap), and access fields with a dot. Note you must write struct Point as the full type name unless you typedef it (shown next). Behavior lives in separate functions that take the struct, not inside it.Structs & the Arrow Operator
<?php
class Account {
public int $balance = 0;
public function deposit(int $amount): void {
$this->balance += $amount;
}
}
$account = new Account();
$account->deposit(100);
echo $account->balance . "\n"; // 100 #include <stdio.h>
struct Account { int balance; };
// "Methods" are free functions taking a pointer to the struct.
void deposit(struct Account *account, int amount) {
account->balance += amount; // -> dereferences then accesses
}
int main(void) {
struct Account account = {0};
deposit(&account, 100);
printf("%d\n", account.balance); // 100
return 0;
} Since structs have no methods, behavior is a free function whose first parameter is a pointer to the struct — exactly the explicit
$this that PHP passes for you. The -> operator is shorthand for "dereference a struct pointer and access a field": account->balance means (*account).balance. You may recognize -> from PHP, but here it specifically signals a pointer, while . is for a direct struct value.typedef
<?php
final class Coordinate {
public function __construct(
public readonly float $latitude,
public readonly float $longitude,
) {}
}
$here = new Coordinate(51.5, -0.1);
printf("%.1f, %.1f\n", $here->latitude, $here->longitude); #include <stdio.h>
// typedef gives the struct a one-word name, so you skip 'struct' everywhere.
typedef struct {
double latitude;
double longitude;
} Coordinate;
int main(void) {
Coordinate here = {51.5, -0.1};
printf("%.1f, %.1f\n", here.latitude, here.longitude);
return 0;
} typedef creates an alias for a type, most often so you can write Coordinate instead of struct Coordinate at every use. It is purely a naming convenience — there is no immutability, no constructor, no validation like PHP's readonly promoted properties. C gives you the bag of fields and nothing more; any invariants are yours to enforce by convention.Enums
Enums Are Integers
<?php
enum Status: string {
case Active = "active";
case Pending = "pending";
case Closed = "closed";
}
$status = Status::Pending;
echo $status->value . "\n"; // pending #include <stdio.h>
// A C enum is just named integer constants, starting at 0.
enum Status { ACTIVE, PENDING, CLOSED };
int main(void) {
enum Status status = PENDING;
printf("%d\n", status); // 1 — it IS an int
return 0;
} C does have enums, but they are far weaker than PHP 8's: each case is simply a named
int constant (ACTIVE = 0, PENDING = 1, …), with no string backing, no methods, and no type safety — an enum Status variable will silently accept any integer. There is no ->value or ::from(); the case is its number, which is why enums often pair with a lookup array to recover a name.Enum to String
<?php
enum Suit: string {
case Hearts = "H";
case Spades = "S";
}
// PHP keeps the name and value together:
echo Suit::Hearts->name . " = " . Suit::Hearts->value . "\n"; #include <stdio.h>
enum Suit { HEARTS, SPADES };
int main(void) {
// To print a name, keep a parallel array indexed by the enum value.
const char *names[] = {"Hearts", "Spades"};
enum Suit suit = HEARTS;
printf("%s = %d\n", names[suit], suit); // Hearts = 0
return 0;
} Because a C enum value is only an integer, there is no built-in way to get back its name —
printf("%d", suit) shows the number. The idiom is a parallel string array indexed by the enum, so names[suit] yields the label. You must keep that array in lockstep with the enum by hand, a chore PHP's backed enums eliminate entirely.Error Handling
Return Codes, Not Exceptions
<?php
function parsePort(string $text): int {
if (!ctype_digit($text)) {
throw new InvalidArgumentException("bad port");
}
return (int) $text;
}
try {
echo parsePort("abc") . "\n";
} catch (InvalidArgumentException $error) {
echo "Error: {$error->getMessage()}\n";
} #include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
// No exceptions: signal failure via the return value + an out-parameter.
int parse_port(const char *text, int *out) {
for (const char *cursor = text; *cursor != '\0'; cursor++) {
if (!isdigit((unsigned char) *cursor)) {
return -1; // failure code
}
}
*out = atoi(text);
return 0; // success
}
int main(void) {
int port;
if (parse_port("abc", &port) != 0) {
printf("Error: bad port\n");
} else {
printf("%d\n", port);
}
return 0;
} C has no exceptions — no
throw, try, or catch. The universal convention is that a function returns a status code (0 for success, non-zero for an error) and delivers its real result through an out-parameter pointer. The caller must check the return value every single time; there is no automatic propagation, so an unchecked error simply goes unnoticed.errno
<?php
// PHP raises a catchable exception or warning on a failed operation.
$handle = @fopen("/no/such/file", "r");
if ($handle === false) {
echo "Could not open file\n";
} #include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
FILE *file = fopen("/no/such/file", "r");
if (file == NULL) {
// errno is a global set by the failed library call.
printf("Error: %s\n", strerror(errno));
return 0;
}
fclose(file);
return 0;
} Many standard-library calls report why they failed through
errno, a global error number set as a side effect. After a failing call you read it immediately and translate it to text with strerror (this prints "No such file or directory"). It is a fragile mechanism — any intervening call can overwrite errno — and a world away from PHP's structured exception objects.Exit Status
<?php
// The status passed to exit() is what the shell reads (0 = success).
function check_port(int $port): int {
if ($port < 0) {
fwrite(STDERR, "invalid port\n");
return 1; // a failure status
}
echo "listening on {$port}\n";
return 0;
}
exit(check_port(-1)); // exits with status 1 #include <stdio.h>
int main(void) {
int port = -1;
if (port < 0) {
fprintf(stderr, "invalid port\n");
return 1; // a failure status
}
printf("listening on %d\n", port);
return 0;
} The integer
main returns is the process exit status that the shell and other programs inspect — 0 for success, non-zero for failure — the same contract as PHP's exit($status). Diagnostics go to standard error via fprintf(stderr, ...) so they stay separate from real output on stdout. Both examples take the invalid-port path and exit with status 1.Preprocessor & Headers
#define vs const
<?php
const MAX_USERS = 100;
define("APP_NAME", "Demo");
echo MAX_USERS . "\n";
echo APP_NAME . "\n"; #include <stdio.h>
#define MAX_USERS 100 // textual substitution before compiling
#define APP_NAME "Demo"
int main(void) {
const int retries = 3; // a real typed constant
printf("%d\n", MAX_USERS);
printf("%s\n", APP_NAME);
printf("%d\n", retries);
return 0;
} C has two notions of "constant".
#define is a preprocessor macro — the compiler literally replaces MAX_USERS with 100 as text before compilation, with no type and no scope. const int is a true typed, scoped constant and is usually the better choice. PHP's define() resembles the macro, but C's runs at compile time, not runtime.Function-like Macros
<?php
// PHP would just use a function here.
function square(int $n): int {
return $n * $n;
}
echo square(5) . "\n"; // 25 #include <stdio.h>
// A macro pastes its body in place — wrap every argument in parentheses!
#define SQUARE(n) ((n) * (n))
int main(void) {
printf("%d\n", SQUARE(5)); // 25
printf("%d\n", SQUARE(2 + 3)); // 25 — parentheses save us
return 0;
} Function-like macros take arguments but are still pure text substitution, which is powerful and dangerous: without the defensive parentheses,
SQUARE(2 + 3) would expand to 2 + 3 * 2 + 3 = 11, not 25. They also evaluate arguments more than once (avoid SQUARE(i++)). Modern C code prefers real functions — the compiler inlines small ones anyway — and reserves macros for things functions cannot do.Headers & Multi-file Builds
<?php
// math_utils.php
namespace MathUtils;
function square(int $n): int { return $n * $n; }
// main.php
// require 'math_utils.php';
// echo \MathUtils\square(5); /* math_utils.h — the interface (declarations only) */
int square(int n);
/* math_utils.c — the implementation */
// #include "math_utils.h"
// int square(int n) { return n * n; }
/* main.c — the user */
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("%d\n", square(5));
return 0;
} C splits code across files differently from PHP's
require. A header (.h) declares what exists — function prototypes and types — and a .c file provides the implementation. Other files #include the header to learn the interface, then the linker stitches the compiled pieces together. There are no namespaces, so names are global; the convention is to prefix them (mathutils_square) to avoid collisions. This multi-file example cannot run in the single-file sandbox.Standard Library
Math
<?php
echo abs(-7) . "\n"; // 7
echo pow(2, 10) . "\n"; // 1024
echo round(sqrt(2), 4) . "\n"; // 1.4142
echo max(3, 9, 2) . "\n"; // 9 #include <stdio.h>
#include <math.h> // link with -lm
#include <stdlib.h> // abs for ints
int main(void) {
printf("%d\n", abs(-7)); // 7
printf("%.0f\n", pow(2, 10)); // 1024
printf("%.4f\n", sqrt(2)); // 1.4142
// No variadic max(); compare by hand or with a macro:
int a = 3, b = 9;
printf("%d\n", a > b ? a : b); // 9
return 0;
} Math splits across headers: integer
abs is in <stdlib.h> while pow, sqrt, and friends are in <math.h> — and on many systems you must link the math library with -lm. There is no variadic max that takes any number of arguments; you compare with the ternary operator or write a macro. Note pow always returns a double, even for integer inputs.Sorting with qsort
<?php
$numbers = [5, 2, 8, 1];
usort($numbers, fn($a, $b) => $a <=> $b);
echo implode(" ", $numbers) . "\n"; // 1 2 5 8 #include <stdio.h>
#include <stdlib.h>
// The comparator receives void* and must cast back to the real type.
int compare_ints(const void *a, const void *b) {
int left = *(const int *) a;
int right = *(const int *) b;
return (left > right) - (left < right);
}
int main(void) {
int numbers[] = {5, 2, 8, 1};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
qsort(numbers, count, sizeof(int), compare_ints);
for (size_t index = 0; index < count; index++) {
printf("%d ", numbers[index]);
}
printf("\n"); // 1 2 5 8
return 0;
} qsort is the standard sort, but it is far more manual than usort. Because C has no generics, it works on raw memory: you hand it the array, the element count, the size of each element, and a comparator that receives const void * pointers it must cast back to the real type. The (left > right) - (left < right) trick yields the same -1/0/1 as PHP's spaceship without risking integer overflow.Random Numbers
<?php
$roll = random_int(1, 6);
echo "rolled between 1 and 6\n";
// random_int is cryptographically secure and seedless. #include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
srand((unsigned) time(NULL)); // seed once from the clock
int roll = rand() % 6 + 1; // 1..6 (biased; fine for casual use)
printf("rolled %s\n", roll >= 1 && roll <= 6 ? "in range" : "out");
return 0;
} C's
rand must be seeded once with srand (usually from time(NULL)) or it produces the same sequence every run — PHP seeds for you. Reducing to a range with % 6 is slightly biased and rand is not suitable for security; PHP's random_int is unbiased and cryptographically secure out of the box. For real randomness in C you reach for OS facilities like getrandom.⚠ Gotchas for PHP Devs
Never Compare Strings with ==
<?php
$a = "hello";
$b = "hel" . "lo";
var_dump($a == $b); // true — compares contents #include <stdio.h>
#include <string.h>
int main(void) {
char a[] = "hello";
char b[] = "hello";
printf("a == b: %d\n", a == b); // 0 — compares ADDRESSES
printf("strcmp: %d\n", strcmp(a, b) == 0); // 1 — compares contents
return 0;
} A dangerous trap: in C a string is a pointer, so
a == b compares the two addresses, not the characters — almost always not what you want, and it compiles without complaint. You must use strcmp, which returns 0 when the contents are equal (note: 0, not a truthy value). PHP's == compares string contents; C's does not.Integer Division Truncates
<?php
echo 7 / 2 . "\n"; // 3.5 — auto-float
echo intdiv(7, 2) . "\n"; // 3
echo 7 % 3 . "\n"; // 1 #include <stdio.h>
int main(void) {
printf("%d\n", 7 / 2); // 3 — both operands int → int result
printf("%f\n", 7.0 / 2); // 3.500000 — make one a double
printf("%d\n", 7 % 3); // 1
printf("%d\n", -7 / 2); // -3 — truncates toward zero
return 0;
} Unlike PHP, where
7 / 2 promotes to 3.5, in C dividing two ints gives an int — 3, the fractional part discarded. Make an operand a double to get a real quotient. C truncates toward zero, so -7 / 2 is -3 (matching PHP's intdiv). Mixing this up with a %d/%f format mismatch compounds the surprise, so watch both.Fixed-Width Overflow
<?php
// PHP ints are 64-bit and overflow to float automatically.
$big = PHP_INT_MAX;
echo $big . "\n";
echo ($big + 1) . "\n"; // becomes a float, no wraparound #include <stdio.h>
#include <limits.h>
int main(void) {
printf("INT_MAX = %d\n", INT_MAX); // 2147483647
// Unsigned arithmetic wraps around (well-defined):
unsigned char small = 255;
small = small + 1; // wraps to 0
printf("255 + 1 (unsigned char) = %d\n", small); // 0
return 0;
} C integers are fixed-width and do not grow. An
int tops out at INT_MAX (about 2.1 billion); pushing a signed type past its limit is undefined behavior, while unsigned types wrap around cleanly (255 + 1 becomes 0 in a byte). PHP shields you by widening its 64-bit ints to floats on overflow — C does not, so choosing the right-width type and watching for overflow is on you.Uninitialized Variables
<?php
// PHP: an unset variable reads as null (with a notice).
$total = 0;
for ($i = 1; $i <= 3; $i++) {
$total += $i;
}
echo $total . "\n"; // 6 #include <stdio.h>
int main(void) {
int total = 0; // MUST initialize — a bare 'int total;' holds garbage
for (int i = 1; i <= 3; i++) {
total += i;
}
printf("%d\n", total); // 6
return 0;
} C does not zero-initialize local variables. A declaration like
int total; leaves whatever bytes happened to be on the stack — reading it is undefined behavior, and the value is unpredictable. Always initialize at declaration (int total = 0;). PHP's habit of treating an unset variable as null/0 is exactly the assumption that produces baffling bugs in C.