PONY λ M2 Modula-2

C.CodeCompared.To/Ada

An interactive executable cheatsheet comparing C and Ada

C17 (GCC) Ada 2022 (GNAT 16.1)
Hello World & Compiling
Hello, World
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is begin Put_Line("Hello, World!"); end Example;
Ada replaces C's #include plus printf with an explicit with clause that names the exact package providing the functionality. On Compiler Explorer the source file is always named example.adb, so the main procedure must be named Example.
Compiling and running
// Single file: // gcc -Wall -Wextra -o hello hello.c && ./hello // With optimization: // gcc -O2 -o hello hello.c
-- gnatmake example.adb -- ./example -- -- gnatmake compiles and links in one step, tracking -- dependencies between package specs and bodies automatically.
gnatmake is Ada's all-in-one build tool: it figures out which source files a program depends on, compiles only what changed, and links the executable. There is no separate object-file bookkeeping to manage by hand, unlike a typical C Makefile.
Comments
#include <stdio.h> int main(void) { // This is a single-line comment. /* This is a multi-line comment. */ printf("Comments compiled away\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is -- This is a comment. It always runs to the end of the line. -- Ada has no multi-line comment syntax at all. begin Put_Line("Comments compiled away"); end Example;
Ada has only one comment style, -- to end of line — there is no equivalent of C's /* ... */ block comment. Every line of a long comment needs its own leading --.
Case sensitivity
#include <stdio.h> int main(void) { int Count = 1; int count = 2; // a genuinely different variable in C printf("%d %d\n", Count, count); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Count : Integer := 1; -- Count2 : Integer := 2; -- would NOT collide with Count above; -- but "count" and "Count" ARE the same identifier in Ada. begin Ada.Integer_Text_IO.Put(Count, Width => 0); New_Line; end Example;
Ada identifiers are case-insensitive: Count, count, and COUNT all refer to the same declaration, and declaring two of them in the same scope is a compile error. C is case-sensitive, so Count and count are unrelated variables — a common source of confusion when porting code between the two languages.
Variables & Types
Variable declarations
#include <stdio.h> int main(void) { int count = 10; double ratio = 3.14; int flag = 1; // C has no native boolean before <stdbool.h> printf("%d %.2f %d\n", count, ratio, flag); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; with Ada.Float_Text_IO; procedure Example is Count : Integer := 10; Ratio : Float := 3.14; Flag : Boolean := True; begin Ada.Integer_Text_IO.Put(Count, Width => 0); New_Line; Ada.Float_Text_IO.Put(Ratio, Fore => 1, Aft => 2, Exp => 0); New_Line; Put_Line(Boolean'Image(Flag)); end Example;
Ada has a real Boolean type built in, so there is no need for C's 0/1 convention or the <stdbool.h> header. Declarations live between is and begin, and every variable can carry an inline initializer with :=.
No implicit conversions
#include <stdio.h> int main(void) { int whole = 5; double fraction = whole; // implicit int-to-double, silently allowed char letter = 65; // implicit int-to-char, silently allowed printf("%.1f %c\n", fraction, letter); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Float_Text_IO; procedure Example is Whole : Integer := 5; Fraction : Float := Float(Whole); -- explicit conversion required Letter : Character := Character'Val(65); -- explicit conversion required begin Ada.Float_Text_IO.Put(Fraction, Fore => 1, Aft => 1, Exp => 0); New_Line; Put_Line(Character'Image(Letter)); end Example;
C freely converts between numeric types wherever it needs to, sometimes losing precision or wrapping without warning. Ada requires an explicit conversion, such as Float(Whole) or Character'Val(65), at every point where the type actually changes — the compiler will not silently insert one.
Integer types
#include <stdio.h> #include <stdint.h> int main(void) { int8_t small = 100; int medium = 1000000; int64_t big = 9000000000LL; unsigned int positive = 42; printf("%d %d %lld %u\n", small, medium, big, positive); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Small : Short_Short_Integer := 100; Medium : Integer := 1_000_000; Big : Long_Long_Integer := 9_000_000_000; Natural_N : Natural := 0; -- >= 0 Positive_N : Positive := 42; -- >= 1 begin Ada.Integer_Text_IO.Put(Medium, Width => 0); New_Line; end Example;
Ada provides Natural (values ≥ 0) and Positive (values ≥ 1) as predefined subtypes, giving a built-in replacement for C's convention of using unsigned int to mean "non-negative." Underscores may separate digit groups in numeric literals for readability, just like C's 1000000 written as 1_000_000 in Ada.
Explicit type conversion
#include <stdio.h> int main(void) { double pi = 3.9; int truncated = (int)pi; // explicit cast, truncates toward zero printf("%d\n", truncated); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Pi : Float := 3.9; Truncated : Integer := Integer(Pi); -- rounds to nearest, not toward zero! begin Ada.Integer_Text_IO.Put(Truncated, Width => 0); New_Line; end Example;
A C cast such as (int)pi truncates toward zero. Converting a floating-point value to Integer in Ada instead rounds to the nearest integer — Integer(3.9) yields 4, not 3. Reaching for a familiar C-style cast and expecting truncation is a common mistake.
Strings
Fixed strings vs char arrays
#include <stdio.h> #include <string.h> int main(void) { char name[11] = "Alice"; // null-terminated, 10 usable chars + '\0' printf("%s length=%zu\n", name, strlen(name)); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Name : String(1 .. 10) := "Alice "; -- padded to exact length, no terminator begin Put_Line(Name); Ada.Integer_Text_IO.Put(Name'Length, Width => 0); New_Line; end Example;
A C string is a char array terminated by a '\0' byte, and its length must be discovered with strlen. An Ada String is a fixed-length array of Character with no terminator — the length is a fixed part of the type itself and is available instantly as the 'Length attribute.
Variable-length strings
#include <stdio.h> #include <string.h> #include <stdlib.h> int main(void) { char *name = malloc(6); strcpy(name, "Alice"); name = realloc(name, 7); strcat(name, "!"); printf("%s length=%zu\n", name, strlen(name)); free(name); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; procedure Example is Name : Unbounded_String := To_Unbounded_String("Alice"); begin Append(Name, "!"); Put_Line(To_String(Name)); Ada.Integer_Text_IO.Put(Length(Name), Width => 0); New_Line; end Example;
Growing a C string means manually tracking a buffer size, calling realloc, and remembering to free it. Ada's Unbounded_String from Ada.Strings.Unbounded resizes itself automatically as text is appended, with no manual allocation, reallocation, or freeing required.
String concatenation
#include <stdio.h> #include <string.h> int main(void) { char full[20]; strcpy(full, "Ada"); strcat(full, " "); strcat(full, "Lovelace"); printf("%s\n", full); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; procedure Example is First : Unbounded_String := To_Unbounded_String("Ada"); Last : Unbounded_String := To_Unbounded_String("Lovelace"); Full : Unbounded_String; begin Full := First & " " & Last; Put_Line(To_String(Full)); end Example;
C string concatenation goes through strcat, which writes past the end of the destination buffer with no bounds check if the buffer is too small — a classic source of buffer overflows. Ada uses the & operator, and an Unbounded_String grows to fit the result automatically with no manual buffer sizing at all.
Substrings and slicing
#include <stdio.h> #include <string.h> int main(void) { char word[] = "Hello"; char sub[4]; strncpy(sub, word + 1, 3); // manual pointer arithmetic sub[3] = '\0'; printf("%s\n", sub); return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Word : constant String := "Hello"; Sub : constant String := Word(2 .. 4); -- "ell" begin Put_Line(Sub); end Example;
C substrings require manual pointer arithmetic (word + 1) plus remembering to add a null terminator by hand. Ada slices an array directly with a range, Word(2 .. 4), and the result is itself a proper bounded String with no separate terminator step.
Bounds Checking & Overflow
Array bounds checking
#include <stdio.h> int main(void) { int numbers[5] = {10, 20, 30, 40, 50}; // numbers[10] is undefined behavior in C -- it may print // garbage, crash, or silently corrupt other memory. printf("%d\n", numbers[4]); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Numbers : array(1 .. 5) of Integer := (10, 20, 30, 40, 50); begin Ada.Integer_Text_IO.Put(Numbers(5), Width => 0); New_Line; -- Numbers(10) would raise Constraint_Error at runtime instead -- of reading past the end of the array silently. end Example;
Indexing a C array out of bounds is undefined behavior: the compiler is free to do anything, and in practice the program often reads or writes memory that does not belong to the array. Ada checks every array index at runtime by default and raises Constraint_Error the instant a bound is violated, turning a silent memory bug into an immediate, catchable exception.
Catching a bounds violation
#include <stdio.h> int main(void) { int numbers[5] = {10, 20, 30, 40, 50}; int index = 10; // C offers no built-in mechanism to detect this before it happens. // printf("%d\n", numbers[index]); -- undefined behavior printf("would read out of bounds at index %d\n", index); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Numbers : array(1 .. 5) of Integer := (10, 20, 30, 40, 50); Index : Integer := 10; begin begin Ada.Integer_Text_IO.Put(Numbers(Index), Width => 0); New_Line; exception when Constraint_Error => Put_Line("Constraint_Error: index out of bounds"); end; end Example;
Because Ada raises a real exception for an out-of-range index, the program can catch it with an ordinary exception when Constraint_Error handler and recover gracefully. C provides no equivalent detection mechanism at all — the invalid access simply happens.
Integer overflow
#include <stdio.h> #include <limits.h> int main(void) { int max_value = INT_MAX; int overflowed = max_value + 1; // signed overflow: undefined behavior printf("%d\n", overflowed); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Max_Value : Integer := Integer'Last; begin begin Ada.Integer_Text_IO.Put(Max_Value + 1, Width => 0); New_Line; exception when Constraint_Error => Put_Line("Constraint_Error: integer overflow"); end; end Example;
Signed integer overflow in C is undefined behavior — the standard places no requirement on what happens, and optimizing compilers sometimes exploit that to remove overflow checks the programmer wrote. Ada raises Constraint_Error the moment an arithmetic result exceeds the range of its type, so overflow is always detected rather than silently wrapping or invoking undefined behavior.
Range-constrained subtypes
#include <stdio.h> // C has no built-in way to say "this int must stay between 0 and 100" -- // it takes a manual check on every assignment. int set_grade(int value) { if (value < 0 || value > 100) { printf("invalid grade\n"); return -1; } return value; } int main(void) { int grade = set_grade(87); printf("%d\n", grade); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is subtype Grade_Type is Integer range 0 .. 100; Grade : Grade_Type := 87; begin Ada.Integer_Text_IO.Put(Grade, Width => 0); New_Line; -- Grade := 150; -- would raise Constraint_Error automatically, -- with no manual "if out of range" check needed anywhere. end Example;
In C, enforcing that a value stays within a range means writing and remembering a manual check at every assignment site. Ada's subtype bakes the constraint into the type itself: Grade_Type can never legally hold a value outside 0 .. 100, and the compiler and runtime enforce it everywhere the type is used, not just where the programmer remembered to check.
Control Flow
If / elsif
#include <stdio.h> int main(void) { int score = 75; if (score >= 90) { printf("A\n"); } else if (score >= 80) { printf("B\n"); } else if (score >= 70) { printf("C\n"); } else { printf("F\n"); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Score : Integer := 75; begin if Score >= 90 then Put_Line("A"); elsif Score >= 80 then Put_Line("B"); elsif Score >= 70 then Put_Line("C"); else Put_Line("F"); end if; end Example;
Ada spells the "else if" chain as a single keyword, elsif, and closes the whole construct with end if;. There are no curly braces anywhere in Ada — indentation is purely a style convention, not a syntactic requirement, and the explicit end if is what actually delimits the block.
Switch vs case
#include <stdio.h> int main(void) { int month = 6; switch (month) { case 12: case 1: case 2: printf("Winter\n"); break; case 3: case 4: case 5: printf("Spring\n"); break; case 6: case 7: case 8: printf("Summer\n"); break; default: printf("Autumn\n"); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Month : Integer := 6; begin case Month is when 12 | 1 | 2 => Put_Line("Winter"); when 3 .. 5 => Put_Line("Spring"); when 6 .. 8 => Put_Line("Summer"); when others => Put_Line("Autumn"); end case; end Example;
Ada's case has no fallthrough and therefore needs no break — each when arm ends automatically. It also requires every possible value to be covered, either explicitly or with when others, so the compiler rejects a case that accidentally misses a value; C's switch allows silent fallthrough and does not require a default case.
Short-circuit operators
#include <stdio.h> int main(void) { int numbers[5] = {42, 0, 0, 0, 0}; int index = 1; if (index >= 1 && index <= 5 && numbers[index - 1] > 0) { printf("Valid positive: %d\n", numbers[index - 1]); } else { printf("Not valid\n"); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Numbers : array(1 .. 5) of Integer := (42, 0, 0, 0, 0); Index : Integer := 1; begin if Index >= 1 and then Index <= 5 and then Numbers(Index) > 0 then Ada.Integer_Text_IO.Put(Numbers(Index), Width => 0); New_Line; else Put_Line("Not valid"); end if; end Example;
C's && and || always short-circuit. Ada instead has two separate forms: bare and/or always evaluate both sides, while and then/or else short-circuit like C's operators. Reaching for bare and where a guard condition must run first, such as an index check before an array access, is a common mistake for a C programmer new to Ada.
Loops
For loop and 1-based ranges
#include <stdio.h> int main(void) { for (int index = 0; index < 5; index++) { printf("%d ", index); } printf("\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is begin for Index in 1 .. 5 loop Ada.Integer_Text_IO.Put(Index, Width => 0); Put(" "); end loop; New_Line; end Example;
Ada's for Index in Low .. High loop has no separate initializer, condition, or increment clause to get wrong — the loop variable is declared implicitly, is read-only inside the loop, and stops existing once the loop ends. Ada array and range conventions commonly start at 1 rather than C's near-universal 0-based indexing, so a direct 0 .. 4 translation of a C loop is often not idiomatic Ada.
Counting down
#include <stdio.h> int main(void) { for (int index = 5; index >= 1; index--) { printf("%d ", index); } printf("\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is begin for Index in reverse 1 .. 5 loop Ada.Integer_Text_IO.Put(Index, Width => 0); Put(" "); end loop; New_Line; end Example;
The range in for Index in reverse 1 .. 5 loop is still written low to high; the reverse keyword alone drives iteration backwards. Writing 5 .. 1 directly, mirroring a C-style descending loop bound, produces an empty range instead of an error.
While loop
#include <stdio.h> int main(void) { int count = 1; while (count <= 5) { printf("%d ", count); count++; } printf("\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Count : Integer := 1; begin while Count <= 5 loop Ada.Integer_Text_IO.Put(Count, Width => 0); Put(" "); Count := Count + 1; end loop; New_Line; end Example;
Ada has no ++ or += operators — an increment is always written out fully as Count := Count + 1. The loop body is delimited by loop ... end loop; rather than curly braces.
Do-while vs loop-exit
#include <stdio.h> int main(void) { int count = 1; do { printf("%d ", count); count++; } while (count <= 5); printf("\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Count : Integer := 1; begin loop Ada.Integer_Text_IO.Put(Count, Width => 0); Put(" "); Count := Count + 1; exit when Count > 5; end loop; New_Line; end Example;
Ada has no dedicated do...while form. The idiom is a bare loop ... exit when Condition; end loop;, and because exit when may appear anywhere in the body — not only at the end — the same construct also covers loops that need to check their exit condition in the middle.
Iterating over an array
#include <stdio.h> int main(void) { int numbers[5] = {10, 20, 30, 40, 50}; for (int index = 0; index < 5; index++) { printf("%d ", numbers[index]); } printf("\n"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Numbers : array(1 .. 5) of Integer := (10, 20, 30, 40, 50); begin for Index in Numbers'Range loop Ada.Integer_Text_IO.Put(Numbers(Index), Width => 0); Put(" "); end loop; New_Line; end Example;
The 'Range attribute yields an array's own declared bounds, so a loop written as for Index in Numbers'Range loop automatically covers every element no matter how the array's bounds are defined — there is no risk of a C-style off-by-one error from a hardcoded < 5 condition.
Functions & Procedures
Void functions vs procedures
#include <stdio.h> void greet(const char *name) { printf("Hello, %s!\n", name); } int main(void) { greet("Alice"); return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is procedure Greet(Name : String) is begin Put_Line("Hello, " & Name & "!"); end Greet; begin Greet("Alice"); end Example;
Ada makes the C distinction between "returns a value" and "returns nothing" explicit at the language level: a procedure never returns a value, matching C's void functions, while a function (below) always must. Parameters default to in mode, meaning read-only, unlike C where every non-pointer parameter is silently copied and freely mutable inside the function body.
Functions with return values
#include <stdio.h> int square(int number) { return number * number; } int main(void) { printf("%d\n", square(7)); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is function Square(Number : Integer) return Integer is begin return Number * Number; end Square; begin Ada.Integer_Text_IO.Put(Square(7), Width => 0); New_Line; end Example;
An Ada function declares its return type with return Integer in the signature and must return a value on every path — the compiler rejects a function with a path that falls off the end without a return statement, unlike C where a missing return is merely undefined behavior if the value is used.
Pass-by-pointer vs in out parameters
#include <stdio.h> void swap(int *first, int *second) { int temp = *first; *first = *second; *second = temp; } int main(void) { int x = 5, y = 10; swap(&x, &y); printf("%d %d\n", x, y); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is procedure Swap(First : in out Integer; Second : in out Integer) is Temp : Integer := First; begin First := Second; Second := Temp; end Swap; X : Integer := 5; Y : Integer := 10; begin Swap(X, Y); Ada.Integer_Text_IO.Put(X, Width => 0); Put(" "); Ada.Integer_Text_IO.Put(Y, Width => 0); New_Line; end Example;
C has only one parameter-passing mode, so mutating a caller's variable requires manually passing a pointer and dereferencing it with * at every use. Ada's in out mode lets a parameter be both read and written using ordinary variable syntax, with no pointers or address-of operators anywhere at the call site.
Write-only out parameters
#include <stdio.h> // C has no way to mark a pointer parameter "write-only" -- // the function COULD read *result before writing it, and nothing // stops that, even though the caller's initial value is meaningless. void compute_square(int number, int *result) { *result = number * number; } int main(void) { int answer; compute_square(9, &answer); printf("%d\n", answer); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is procedure Compute_Square(Number : Integer; Result : out Integer) is begin Result := Number * Number; -- Reading Result here before assigning it would be a compile -- error in strict Ada modes -- "out" means write-only. end Compute_Square; Answer : Integer; begin Compute_Square(9, Answer); Ada.Integer_Text_IO.Put(Answer, Width => 0); New_Line; end Example;
Ada's out mode documents, and in many compilation modes enforces, that a parameter is write-only: the subprogram must not depend on the caller's initial value. A C pointer parameter used the same way carries no such guarantee — nothing in the language stops the function from reading the pointee before writing it.
Structs vs Records
Struct vs record
#include <stdio.h> struct Point { int x; int y; }; int main(void) { struct Point origin = {0, 0}; printf("%d %d\n", origin.x, origin.y); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Point is record X, Y : Integer; end record; Origin : Point := (X => 0, Y => 0); begin Ada.Integer_Text_IO.Put(Origin.X, Width => 0); Put(" "); Ada.Integer_Text_IO.Put(Origin.Y, Width => 0); New_Line; end Example;
Ada's record plays the same structural role as C's struct, including the same dot-notation field access. Named aggregates, (X => 0, Y => 0), initialize every field in one expression — C's equivalent, {0, 0}, relies on positional order and gives no field-name safety net.
Nested structs vs records
#include <stdio.h> struct Point { int x; int y; }; struct Rectangle { struct Point top_left; int width; int height; }; int main(void) { struct Rectangle box = {{0, 0}, 10, 20}; printf("%d %d %d %d\n", box.top_left.x, box.top_left.y, box.width, box.height); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Point is record X, Y : Integer; end record; type Rectangle is record Top_Left : Point; Width, Height : Integer; end record; Box : Rectangle := (Top_Left => (X => 0, Y => 0), Width => 10, Height => 20); begin Ada.Integer_Text_IO.Put(Box.Top_Left.X, Width => 0); Put(" "); Ada.Integer_Text_IO.Put(Box.Top_Left.Y, Width => 0); Put(" "); Ada.Integer_Text_IO.Put(Box.Width, Width => 0); Put(" "); Ada.Integer_Text_IO.Put(Box.Height, Width => 0); New_Line; end Example;
Nested records work exactly like nested structs, with the same chained dot access, Box.Top_Left.X. The named aggregate form makes the nesting explicit at the initializer, unlike C's positional {{0, 0}, 10, 20}, which relies entirely on remembering field declaration order.
Passing records to subprograms
#include <stdio.h> struct Person { char name[10]; int age; }; void print_person(struct Person person) { printf("%s age %d\n", person.name, person.age); } int main(void) { struct Person alice = {"Alice", 30}; print_person(alice); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Person is record Name : String(1 .. 5); Age : Integer; end record; procedure Print_Person(The_Person : Person) is begin Put_Line(The_Person.Name & " age" & Integer'Image(The_Person.Age)); end Print_Person; Alice : Person := (Name => "Alice", Age => 30); begin Print_Person(Alice); end Example;
Both languages pass structs and records to subprograms by value by default, copying the whole aggregate. Ada makes that copy read-only unless the parameter mode is explicitly in out, whereas a C function can freely mutate its local copy of a struct parameter — the caller's original is unaffected either way, but only Ada enforces the read-only intent at compile time.
Pointers & Memory Management
malloc/free vs access types
#include <stdio.h> #include <stdlib.h> int main(void) { int *pointer = malloc(sizeof(int)); if (pointer == NULL) { return 1; } *pointer = 42; printf("%d\n", *pointer); free(pointer); // forgetting this leaks memory; // using pointer after this is a use-after-free return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Int_Access is access Integer; Pointer : Int_Access := new Integer'(42); begin Ada.Integer_Text_IO.Put(Pointer.all, Width => 0); New_Line; -- No free() call: the storage pool manages the allocation's -- lifetime, so there is no manual deallocation step to forget. end Example;
C's malloc/free pair requires the programmer to track every allocation and release it exactly once — forgetting leaks memory, and freeing twice or using the pointer afterward corrupts the heap. Ada's access types allocate with new, and there is no direct equivalent of free in ordinary use; the language leaves deallocation to a storage pool rather than an explicit call the programmer must remember.
Dereferencing pointers
#include <stdio.h> #include <stdlib.h> int main(void) { int *pointer = malloc(sizeof(int)); *pointer = 10; *pointer = *pointer + 5; // read-modify-write through the pointer printf("%d\n", *pointer); free(pointer); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Int_Access is access Integer; Pointer : Int_Access := new Integer'(10); begin Pointer.all := Pointer.all + 5; Ada.Integer_Text_IO.Put(Pointer.all, Width => 0); New_Line; end Example;
C's unary * dereferences a pointer on both the reading and writing side of an assignment. Ada spells the same operation .all, which reads more like ordinary field access: Pointer.all gets or sets the integer the access value designates.
Null pointers
#include <stdio.h> #include <stdlib.h> int main(void) { int *pointer = NULL; if (pointer == NULL) { printf("pointer is null\n"); } // *pointer would crash (or worse, silently misbehave) return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is type Int_Access is access Integer; Pointer : Int_Access := null; begin if Pointer = null then Put_Line("pointer is null"); end if; -- Pointer.all would raise Constraint_Error, a catchable -- exception, instead of C's undefined-behavior crash. end Example;
Both languages have a null pointer value, spelled NULL in C and null in Ada. The behavior on dereference differs sharply though: dereferencing a null pointer in C is undefined behavior, typically a segmentation fault with no recovery, while dereferencing a null access value in Ada raises Constraint_Error, which the program can catch and handle.
Access to records
#include <stdio.h> #include <stdlib.h> #include <string.h> struct Person { char name[6]; int age; }; int main(void) { struct Person *pointer = malloc(sizeof(struct Person)); strcpy(pointer->name, "Bob"); pointer->age = 25; printf("%s %d\n", pointer->name, pointer->age); free(pointer); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Person is record Name : String(1 .. 3); Age : Integer; end record; type Person_Access is access Person; Pointer : Person_Access := new Person; begin Pointer.Name := "Bob"; Pointer.Age := 25; Put_Line(Pointer.Name); Ada.Integer_Text_IO.Put(Pointer.Age, Width => 0); New_Line; end Example;
C requires the arrow operator, pointer->name, to access a field through a pointer, distinct from the dot used on the struct value directly. Ada automatically dereferences an access value for record field access, so Pointer.Name is shorthand for Pointer.all.Name — there is no separate arrow syntax to remember.
Headers vs Packages
Header files vs package specs
/* geometry.h */ #ifndef GEOMETRY_H #define GEOMETRY_H struct Point { double x, y; }; double distance(struct Point a, struct Point b); #endif /* geometry.c */ #include "geometry.h" #include <math.h> double distance(struct Point a, struct Point b) { double dx = b.x - a.x; double dy = b.y - a.y; return sqrt(dx * dx + dy * dy); }
-- geometry.ads (specification -- the public interface) package Geometry is type Point is record X, Y : Float; end record; function Distance(A, B : Point) return Float; end Geometry; -- geometry.adb (body -- the implementation) with Ada.Numerics.Elementary_Functions; package body Geometry is function Distance(A, B : Point) return Float is Delta_X : Float := B.X - A.X; Delta_Y : Float := B.Y - A.Y; begin return Ada.Numerics.Elementary_Functions.Sqrt(Delta_X * Delta_X + Delta_Y * Delta_Y); end Distance; end Geometry;
A C header declares an interface that is textually pasted into every including file by the preprocessor, protected from double inclusion only by manual #ifndef guards. Ada's package spec (.ads) plays the same role natively, without a preprocessor or include guards, and the compiler itself enforces that the body (.adb) actually matches the spec it implements.
#include vs with
#include <stdio.h> #include <math.h> #include <string.h> // The preprocessor textually pastes the entire contents of each // header file in, before the compiler ever sees the source. int main(void) { printf("%.2f\n", sqrt(16.0)); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Float_Text_IO; with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions; -- "with" names an actual compiled package -- no textual pasting, -- no preprocessor, and the compiler checks the package really exists. procedure Example is begin Ada.Float_Text_IO.Put(Sqrt(16.0), Fore => 1, Aft => 2, Exp => 0); New_Line; end Example;
C's #include is a textual, preprocessor-level operation with no understanding of the language underneath it. Ada's with clause is a real compiler-level dependency declaration on an already-compiled package — there is no preprocessor step, and referencing a package that does not exist is a compile error rather than a header search failure.
Macros & Constants
#define vs constant
#include <stdio.h> #define MAX_SIZE 100 #define GREETING "Hello" // #define values have no type -- the preprocessor just substitutes text, // so MAX_SIZE could accidentally be compared against any type at all. int main(void) { printf("%d\n", MAX_SIZE); printf("%s\n", GREETING); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Max_Size : constant Integer := 100; Greeting : constant String := "Hello"; begin Ada.Integer_Text_IO.Put(Max_Size, Width => 0); New_Line; Put_Line(Greeting); end Example;
A C #define constant is untyped text substitution performed before compilation even starts, so the compiler cannot type-check its uses. Ada's constant is a genuinely typed, compiler-checked declaration — Max_Size is an Integer and using it where an Integer is not expected is a compile error, not a silent substitution.
Function-like macros vs functions
#include <stdio.h> #define SQUARE(n) ((n) * (n)) // Careless callers can trip subtle bugs: SQUARE(x++) expands to // ((x++) * (x++)), incrementing x twice instead of once. int main(void) { printf("%d\n", SQUARE(5)); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is function Square(Number : Integer) return Integer is begin return Number * Number; end Square; begin Ada.Integer_Text_IO.Put(Square(5), Width => 0); New_Line; -- Square(X) always evaluates X exactly once -- no macro -- expansion, no double-evaluation surprises. end Example;
A C function-like macro is textual substitution and can evaluate its argument multiple times, producing surprising bugs when the argument has side effects, such as n++. Ada has no macro system at all — an ordinary function evaluates each argument exactly once, the same as any normal subprogram call.
Error Handling
Return codes / errno vs exceptions
#include <stdio.h> #include <errno.h> int divide(int numerator, int denominator, int *result) { if (denominator == 0) { return -1; // caller MUST remember to check this } *result = numerator / denominator; return 0; } int main(void) { int result; if (divide(10, 0, &result) != 0) { printf("Error: division by zero\n"); } else { printf("%d\n", result); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is function Divide(Numerator, Denominator : Integer) return Integer is begin return Numerator / Denominator; end Divide; Numerator : Integer := 10; Denominator : Integer := 0; begin begin Put_Line(Integer'Image(Divide(Numerator, Denominator))); exception when Constraint_Error => Put_Line("Error: division by zero"); end; end Example;
C has no built-in exception mechanism — errors are communicated through a return code, a global like errno, or an out-parameter, and it is entirely up to the caller to remember to check it. Ada raises Constraint_Error automatically for division by zero, and the exception propagates on its own until something catches it — there is no way to silently ignore it the way a C caller can skip an error-code check.
Exception handlers
#include <stdio.h> int main(void) { int numerator = 10; int denominator = 0; // C has no try/catch. The only options are a manual check // beforehand, or letting the crash (SIGFPE) happen. if (denominator == 0) { printf("cannot divide by zero\n"); } else { printf("%d\n", numerator / denominator); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Numerator : Integer := 10; Denominator : Integer := 0; Result : Integer; begin begin Result := Numerator / Denominator; Put_Line(Integer'Image(Result)); exception when Constraint_Error => Put_Line("Numeric error (division by zero)"); when others => Put_Line("Unexpected error"); end; end Example;
Every begin...end block in Ada can end with an exception section listing one or more when handlers, playing the role of C++'s try/catch — a facility plain C simply does not have. Multiple when clauses match specific exceptions, and when others is the catch-all.
Raising a custom exception
#include <stdio.h> // C simulates a "custom error" with a return code plus a message, // but nothing forces the caller to check either one. int check_age(int age, const char **error_message) { if (age < 0) { *error_message = "negative age"; return -1; } *error_message = NULL; return 0; } int main(void) { const char *error_message; if (check_age(-1, &error_message) != 0) { printf("Error: %s\n", error_message); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Invalid_Age : exception; procedure Check_Age(Age : Integer) is begin if Age < 0 then raise Invalid_Age with "negative age"; end if; end Check_Age; begin begin Check_Age(-1); exception when Error : Invalid_Age => Put_Line("Error: " & Ada.Exceptions.Exception_Message(Error)); end; end Example;
Declaring a custom exception in Ada is a single line, Invalid_Age : exception;, and raise Invalid_Age with "message" attaches a description without building a whole error-info struct by hand. Reading the message back requires Ada.Exceptions.Exception_Message, so a fully worked example needs that package as well.
Function Pointers
Function pointers vs access to subprograms
#include <stdio.h> int add(int first, int second) { return first + second; } int subtract(int first, int second) { return first - second; } int main(void) { int (*operation)(int, int) = add; // any matching signature works printf("%d\n", operation(3, 4)); operation = subtract; printf("%d\n", operation(3, 4)); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is function Add(First, Second : Integer) return Integer is begin return First + Second; end Add; function Subtract(First, Second : Integer) return Integer is begin return First - Second; end Subtract; type Binary_Operation is access function(First, Second : Integer) return Integer; Operation : Binary_Operation := Add'Access; begin Ada.Integer_Text_IO.Put(Operation(3, 4), Width => 0); New_Line; Operation := Subtract'Access; Ada.Integer_Text_IO.Put(Operation(3, 4), Width => 0); New_Line; end Example;
C lets any function whose signature matches be assigned to a function-pointer variable with no explicit marker. Ada requires a named access-to-subprogram type, such as Binary_Operation, and the target subprogram must be marked explicitly with the 'Access attribute — the language will not let a subprogram's address be taken silently.
A restriction C does not have
#include <stdio.h> int add(int first, int second) { return first + second; } int main(void) { // C freely takes the address of ANY function, whether it is // file-scope or (via nested-function GCC extensions) local. int (*operation)(int, int) = add; printf("%d\n", operation(3, 4)); return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is type Binary_Operation is access function(First, Second : Integer) return Integer; function Add(First, Second : Integer) return Integer is begin return First + Second; end Add; -- A subprogram nested more deeply than Binary_Operation's own -- declaration cannot always have 'Access taken safely, because -- its access value could otherwise outlive the enclosing frame -- that holds its non-local variables. Operation : Binary_Operation := Add'Access; begin Ada.Integer_Text_IO.Put(Operation(3, 4), Width => 0); New_Line; end Example;
Ada restricts which subprograms may have 'Access taken for a general access-to-subprogram type, primarily to prevent a dangling reference to a nested subprogram whose enclosing frame has already returned. Ordinary C function pointers carry no such restriction — pointing at a function is always allowed, though pointing at a stack-allocated nested function via GCC's non-standard extension carries the same lifetime danger without the compiler stopping it.
Gotchas for C Programmers
Assignment is := not =
#include <stdio.h> int main(void) { int count = 5; // '=' assigns if (count == 5) { // '==' compares -- easy to typo as '=' printf("five\n"); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Count : Integer := 5; -- ':=' assigns begin if Count = 5 then -- '=' compares -- no separate operator to confuse Put_Line("five"); end if; end Example;
Ada uses := for assignment and plain = for comparison, so there is no single character that means two different things depending on context — the classic C bug of writing if (count = 5) when == was intended cannot happen, because = alone is never a valid assignment.
&& / || become and then / or else
#include <stdio.h> int main(void) { int denominator = 0; int numerator = 10; if (denominator != 0 && numerator / denominator > 1) { printf("big ratio\n"); } else { printf("safe\n"); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Denominator : Integer := 0; Numerator : Integer := 10; begin if Denominator /= 0 and then Numerator / Denominator > 1 then Put_Line("big ratio"); else Put_Line("safe"); end if; end Example;
Writing bare and instead of and then is the single most common Ada gotcha for a C programmer: and always evaluates both operands, so a guard condition meant to prevent a division by zero would not actually prevent it. Only and then/or else short-circuit the way C's &&/|| always do.
!= becomes /=
#include <stdio.h> int main(void) { int count = 5; if (count != 0) { printf("nonzero\n"); } return 0; }
with Ada.Text_IO; use Ada.Text_IO; procedure Example is Count : Integer := 5; begin if Count /= 0 then Put_Line("nonzero"); end if; end Example;
Ada spells "not equal" as /=, borrowed from mathematical notation, rather than C's !=. There is also no ! operator for logical negation at all — Ada uses the keyword not instead.
1-based array conventions
#include <stdio.h> int main(void) { int numbers[5] = {10, 20, 30, 40, 50}; printf("%d\n", numbers[0]); // first element is always index 0 return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; procedure Example is Numbers : array(1 .. 5) of Integer := (10, 20, 30, 40, 50); -- Ada array bounds are whatever the programmer declares them -- to be; 1-based is a common convention, but 0-based -- (array(0 .. 4)) and any other range are equally legal. begin Ada.Integer_Text_IO.Put(Numbers(1), Width => 0); New_Line; end Example;
C array indices are always 0-based — the language gives no choice. Ada array bounds are chosen explicitly by the programmer in the type or object declaration, and existing Ada code very commonly starts at 1, so porting a C loop or index expression verbatim, without adjusting for the different base, is a frequent source of off-by-one bugs when moving between the two languages.
No automatic coercion between numeric types
#include <stdio.h> int main(void) { int count = 5; double half = count / 2; // integer division happens first, // THEN the int result converts to double printf("%.1f\n", half); // prints 2.0, probably not what was wanted return 0; }
with Ada.Text_IO; use Ada.Text_IO; with Ada.Float_Text_IO; procedure Example is Count : Integer := 5; Half : Float; begin -- Half := Count / 2; -- compile error: Integer and Float can't mix Half := Float(Count) / 2.0; -- explicit conversion required Ada.Float_Text_IO.Put(Half, Fore => 1, Aft => 1, Exp => 0); New_Line; end Example;
C silently mixes int and double in an expression, applying its own conversion rules that can produce a surprising result, such as integer division happening before the conversion to double. Ada refuses to mix Integer and Float in an expression at all; both operands must be the same type, so the programmer must write the conversion explicitly and therefore see exactly where it happens.
end repeats the block name
#include <stdio.h> void greet(const char *name) { printf("Hello, %s!\n", name); } // a bare closing brace -- no name repetition, no way to catch // a mismatched end at compile time
with Ada.Text_IO; use Ada.Text_IO; procedure Example is procedure Greet(Name : String) is begin Put_Line("Hello, " & Name & "!"); end Greet; -- repeating "Greet" here is optional, but if present, -- must match -- the compiler catches a mismatch begin Greet("Alice"); end Example;
Closing an Ada procedure, function, package, or record with end Name; — repeating the identifier being closed — is optional but strongly conventional, and if written, the compiler checks that the name actually matches. C's closing } carries no name at all, so a misplaced or mismatched brace is caught only indirectly, often far from the real location of the mistake.