PONYλM2Modula-2

C.CodeCompared.To/Ada

An interactive executable cheatsheet comparing C and Ada

C17 (GCC) Ada 2022 (GNAT 16.2)
Hello World & Compiling
Hello, World
Ada replaces C's #include plus printf with an explicit with clause that names the exact package providing the functionality. An Ada main procedure must share its name with the file it lives in. The servers that compile the Ada here always name that file example.adb, which is why the procedure is called Example; in your own project the pair is yours to choose, as long as they match.
#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;
Put_Line lives in Ada.Text_IO; the use clause makes it callable without the full Ada.Text_IO.Put_Line prefix.
Compiling and running
gnatmake is Ada's all-in-one build tool — it compiles and links in a single step.
// 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.
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
Ada has only one comment style, -- to end of line — there is no equivalent of C's /* ... */ block comment.
#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;
Every line of a long comment needs its own leading --.
Case sensitivity
Watch out: Ada identifiers are case-insensitive — Count, count, and COUNT all refer to the same declaration.
#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;
Declaring two spellings of the same name in one scope is therefore a compile error in Ada. 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
Ada declarations live in their own region, between is and begin, and Ada has a real built-in Boolean type.
#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;
There is no need for C's 0/1 convention or the <stdbool.h> header, and every variable can carry an inline initializer with :=.
No implicit conversions
C freely converts between numeric types wherever it needs to, sometimes losing precision or wrapping without warning. Ada requires an explicit conversion at every point where the type actually changes.
#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;
The conversions here are Float(Whole) and Character'Val(65) — the compiler will never silently insert one.
Integer types
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."
#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;
Underscores may separate digit groups in numeric literals for readability — C's 1000000 is written 1_000_000 in Ada. (C itself only gained digit separators, spelled with ', in C23.)
Explicit type conversion
Watch out: a C cast such as (int)pi truncates toward zero, but converting a floating-point value to Integer in Ada rounds to the nearest integer.
#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;
Integer(3.9) yields 4, not 3 — reaching for a familiar C-style cast and expecting truncation is a common mistake. When truncation is genuinely wanted, use Integer(Float'Truncation(Pi)).
Strings
Fixed strings vs char arrays
An Ada String is a fixed-length array of Character with no terminator — the length is a fixed part of the type itself.
#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; Ada's length is available instantly as the 'Length attribute.
Variable-length strings
For text whose length changes at runtime, Ada offers Unbounded_String from Ada.Strings.Unbounded — it resizes itself automatically as text is appended.
#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; Unbounded_String needs no manual allocation, reallocation, or freeing at all.
String concatenation
Ada concatenates strings with the & operator.
#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. With &, an Unbounded_String grows to fit the result automatically, with no manual buffer sizing at all.
Substrings and slicing
Ada slices an array directly with a range — Word(2 .. 4) — and the result is itself a proper bounded String.
#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; the Ada slice has no separate terminator step.
Bounds Checking & Overflow
Array bounds checking
Indexing a C array out of bounds is undefined behavior; Ada checks every array index at runtime by default.
#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;
In C 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 raises Constraint_Error the instant a bound is violated, turning a silent memory bug into an immediate, catchable exception.
Catching a bounds violation
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.
#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;
C provides no equivalent detection mechanism at all — the invalid access simply happens.
Integer overflow
Signed integer overflow in C is undefined behavior; in Ada it raises Constraint_Error the moment an arithmetic result exceeds the range of its type.
#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;
The C standard places no requirement on what happens after signed overflow, and optimizing compilers sometimes exploit that to remove overflow checks the programmer wrote. In Ada, overflow is always detected rather than silently wrapping or invoking undefined behavior.
Range-constrained subtypes
Ada's subtype bakes a range constraint into the type itself — something C cannot express at all.
#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. 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
Ada spells the "else if" chain as a single keyword, elsif, and closes the whole construct with end if;.
#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;
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
Ada's case has no fallthrough — each when arm ends automatically, so there is no break to forget — and every possible value must be covered, either explicitly or with when others.
#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;
The compiler rejects a case that accidentally misses a value; C's switch allows silent fallthrough and does not require a default case. Note the alternatives syntax: 12 | 1 | 2 lists values, 3 .. 5 covers a range.
Short-circuit operators
Watch out: C's && and || always short-circuit, but Ada's bare and/or evaluate both sides — only and then/or else short-circuit like C's 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;
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
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.
#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 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
Watch out: the range in for Index in reverse 1 .. 5 loop is still written low to high — the reverse keyword alone drives iteration backwards.
#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;
Writing 5 .. 1 directly, mirroring a C-style descending loop bound, produces an empty range (a loop that never runs) rather than an error.
While loop
Ada has no ++ or += operators — an increment is always written out fully as Count := Count + 1.
#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;
The loop body is delimited by loop ... end loop; rather than curly braces.
Do-while vs loop-exit
Ada has no dedicated do...while form — the idiom is a bare loop with exit when Condition; at the end of the body.
#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;
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
The 'Range attribute yields an array's own declared bounds, so for Index in Numbers'Range loop automatically covers every element no matter how the bounds are defined.
#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;
There is no risk of a C-style off-by-one error from a hardcoded < 5 condition — change the array's bounds and the loop follows along.
Functions & Procedures
Void functions vs procedures
Ada splits C's single "function" notion in two at the language level: a procedure never returns a value, matching C's void functions, while a function (next example) always must.
#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;
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
An Ada function declares its return type with return Integer in the signature and must return a value on every path.
#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;
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
Where C mutates a caller's variable by passing a pointer, Ada declares the intent in the signature: in out mode lets a parameter be both read and written.
#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;
The call site uses ordinary variable syntax — Swap(X, Y), with no & address-of operators — and the body needs no * dereferences. C has only the one parameter-passing mode, so the pointer machinery appears at every use.
Write-only out parameters
Ada's out mode marks a parameter as write-only — the subprogram must not depend on the caller's initial value. C has no way to express this on a pointer parameter.
#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;
The mode both documents the intent and, in many compilation modes, enforces it. 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
Ada's record plays the same structural role as C's struct, including the same dot-notation field access.
#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;
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
Nested records work exactly like nested structs, with the same chained dot access: Box.Top_Left.X.
#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;
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
Both languages pass structs and records to subprograms by value by default, copying the whole aggregate.
#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;
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
C allocates with malloc and releases with free; Ada declares an access type and allocates with new.
#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() here: this short program simply lets the allocation -- live until it exits. Explicit deallocation exists, but goes -- through the deliberately loud Ada.Unchecked_Deallocation. end Example;
The malloc/free pair requires the programmer to release every allocation exactly once — forgetting leaks memory, and freeing twice or using the pointer afterward corrupts the heap. Ada is not garbage-collected: by default an allocation simply lives until the program ends (or until its access type's storage pool is reclaimed), and explicit freeing requires instantiating Ada.Unchecked_Deallocation — the "unchecked" in the name makes every deliberate free-equivalent loudly visible in the source.
Dereferencing pointers
Ada spells pointer dereference .all where C uses unary *.
#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;
It reads more like ordinary field access: Pointer.all gets or sets the integer the access value designates, on both the reading and writing side of an assignment.
Null pointers
Both languages have a null pointer value, spelled NULL in C and null in Ada — but the behavior on dereference differs sharply.
#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;
Dereferencing a null pointer in C is undefined behavior, typically a segmentation fault with no recovery. Dereferencing a null access value in Ada raises Constraint_Error, which the program can catch and handle.
Access to records
Ada has no arrow operator: an access value is automatically dereferenced for record field access, so Pointer.Name is shorthand for Pointer.all.Name.
#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, pointer->name, to access a field through a pointer — a separate syntax from the dot used on the struct value directly. In Ada there is only the dot.
Headers vs Packages
Header files vs package specs
Ada's package spec (.ads) plays the role of a C header natively — a declared public interface — while the body (.adb) holds the implementation.
/* 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 is textually pasted into every including file by the preprocessor, protected from double inclusion only by manual #ifndef guards. Ada needs no preprocessor or include guards, and the compiler itself enforces that the body actually matches the spec it implements.
#include vs with
Ada's with clause is a real compiler-level dependency declaration on a compiled package — not C's textual, preprocessor-level #include pasting.
#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;
There is no preprocessor step at all, and referencing a package that does not exist is an ordinary compile error rather than a header search failure.
Macros & Constants
#define vs constant
A C #define constant is untyped text substitution; Ada's constant is a genuinely typed, compiler-checked declaration.
#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;
Because the preprocessor substitutes text before compilation even starts, the C compiler cannot type-check a macro's uses. 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
Ada has no macro system at all — where C reaches for a function-like macro, Ada uses an ordinary function.
#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 macro is textual substitution and can evaluate its argument multiple times, producing surprising bugs when the argument has side effects, such as n++. An Ada function evaluates each argument exactly once, the same as any normal subprogram call.
Error Handling
Return codes / errno vs exceptions
C has no built-in exception mechanism — errors travel through a return code, a global like errno, or an out-parameter, and it is entirely up to the caller to remember to check. Ada errors are exceptions that propagate until handled.
#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;
Ada raises Constraint_Error automatically for division by zero, and the exception propagates on its own until something catches it — it cannot be silently overlooked the way a C caller can skip an error-code check.
Exception handlers
Every begin...end block in Ada can end with an exception section — the try/catch that plain C simply does not have.
#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;
Multiple when clauses match specific exceptions, and when others is the catch-all.
Raising a custom exception
Declaring a custom exception in Ada is a single line — Invalid_Age : exception; — and raise ... with "message" attaches a description without building an error-info struct by hand.
#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; with Ada.Exceptions; 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;
The handler names the exception occurrence (when Error : Invalid_Age) so the message can be read back with Ada.Exceptions.Exception_Message, which is why the with Ada.Exceptions; clause appears at the top.
Function Pointers
Function pointers vs access to subprograms
Ada's counterpart to a C function pointer is a named access-to-subprogram type, and taking a subprogram's address requires the explicit 'Access attribute.
#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 will not let a subprogram's address be taken silently.
A restriction C does not have
Ada restricts which subprograms may have 'Access taken for a given access-to-subprogram type — a rule with no C equivalent.
#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;
The restriction exists primarily to prevent a dangling reference to a nested subprogram whose enclosing frame has already returned. Ordinary C function pointers carry no such rule — 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 =
Ada uses := for assignment and plain = for comparison.
#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;
No single character means two different things depending on context, so the classic C bug of writing if (count = 5) when == was intended cannot happen — = alone is never a valid assignment.
&& / || become and then / or else
Watch out: writing bare and instead of and then is the single most common Ada gotcha for a C programmer — and always evaluates both operands.
#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;
A guard condition written with bare and, 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 /=
Ada spells "not equal" as /=, borrowed from the mathematical ≠ notation, rather than C's !=.
#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;
There is no ! operator for logical negation at all — Ada uses the keyword not instead.
1-based array conventions
Watch out: C array indices are always 0-based; Ada bounds are whatever the declaration says, and existing Ada code very commonly starts at 1.
#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;
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
Watch out: Ada refuses to mix Integer and Float in an expression at all — both operands must be the same type.
#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, applying its own conversion rules that can produce a surprising result — here, integer division happens before the conversion to double, so C prints 2.0. In Ada the programmer writes the conversion explicitly and therefore sees exactly where it happens.
end repeats the block name
An Ada procedure, function, package, or record closes with end Name; — repeating the identifier being closed is optional but strongly conventional, and if written, the compiler checks that it matches.
#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;
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.