Hello World & Compiling
Hello, World
Three small differences in five lines, and each one is a habit you will have to unlearn rather than a thing to look up.
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}Note
main(void) rather than main(): in C an empty parameter list means "unspecified arguments", not "no arguments", so main() declines a check the compiler would otherwise do for you. The newline is part of the format string because printf has no std::endl, and it does not flush — which matters when a program crashes with output still buffered. And printf is variadic and type-unchecked at the language level, so the format string and the arguments can disagree; GCC's -Wformat catches the common cases but it is a warning, not a rule.gcc vs g++, and name mangling
The absence of name mangling is not a compiler detail — it is the reason several C++ features cannot exist in C, and it is worth putting first because the rest of the page keeps referring back to it.
// g++ example.cpp -o example
//
// The compiler MANGLES every function name, encoding the parameter types
// into the symbol so that overloads can coexist in one object file:
//
// void log(int) → _Z3logi
// void log(const char*) → _Z3logPKc
//
// It also links libstdc++ automatically.
#include <iostream>
void log(int value) { std::cout << "int " << value << std::endl; }
void log(const char* text) { std::cout << "text " << text << std::endl; }
int main() {
log(42);
log("hello");
return 0;
}/* gcc example.c -o example
*
* No mangling: the symbol for log_int is exactly "log_int". That is why
* two functions cannot share a name — there would be one symbol for two
* definitions, and the linker would reject it.
*
* Nothing is linked beyond libc; math needs an explicit -lm.
*/
#include <stdio.h>
void log_int(int value) { printf("int %d\n", value); }
void log_text(const char *text) { printf("text %s\n", text); }
int main(void) {
log_int(42);
log_text("hello");
return 0;
}One name, one symbol. That single constraint is what rules out function overloading, default arguments, namespaces, and member functions, all of which need more than one entity to share a source-level name. It is also what makes C the lingua franca between languages: a C symbol is just its name, so every other language can call it without knowing anything about C's type system. Note the comment style too —
// is legal in C99 and later, but /* */ remains the convention in most C codebases.Scoping & Organization
No namespaces — prefixes instead
C has exactly one global namespace for functions, and every C library you have ever linked shares it with you.
#include <iostream>
namespace geometry {
double area_of_square(double side) { return side * side; }
}
namespace statistics {
double area_of_square(double side) { return side * side; }
}
int main() {
std::cout << geometry::area_of_square(3.0) << std::endl;
std::cout << statistics::area_of_square(3.0) << std::endl;
return 0;
}#include <stdio.h>
/* The prefix IS the namespace. It is a convention, enforced by nothing
* except the linker complaining when two names collide.
*/
double geometry_area_of_square(double side) { return side * side; }
double statistics_area_of_square(double side) { return side * side; }
int main(void) {
printf("%g\n", geometry_area_of_square(3.0));
printf("%g\n", statistics_area_of_square(3.0));
return 0;
}This is why C library authors prefix everything —
SDL_, png_, sqlite3_, pthread_ — and why short generic names in a C header are considered rude. There is no using, no nested namespace, and no way to shorten a prefix at the call site; a local #define is the only approximation and it is a bad one, because it is textual and does not respect scope. Plan the prefix before you write the library, because changing it later touches every caller.static instead of an anonymous namespace
C++ style guides tell you to prefer an anonymous namespace over file-scope
static. In C the advice reverses, because there is no anonymous namespace.#include <iostream>
namespace {
// Anonymous namespace: internal linkage, the modern C++ spelling.
int helper_calls = 0;
int double_it(int value) {
helper_calls += 1;
return value * 2;
}
}
int main() {
std::cout << double_it(21) << " calls=" << helper_calls << std::endl;
return 0;
}#include <stdio.h>
/* static at file scope means internal linkage: invisible to the linker,
* so another translation unit may define its own double_it with no clash.
*/
static int helper_calls = 0;
static int double_it(int value) {
helper_calls += 1;
return value * 2;
}
int main(void) {
printf("%d calls=%d\n", double_it(21), helper_calls);
return 0;
}File-scope
static is the whole mechanism, and it does the same job: the symbol does not reach the linker, so two .c files can each have a private double_it. This is the closest thing C has to private, and it operates on files rather than types. Beware that static means something completely different inside a function (a variable with lifetime beyond the call) — the keyword is overloaded exactly as it is in C++.Header guards, and no #pragma once guarantee
Both languages use the same preprocessor, so headers work the same way — but the idiom for putting a function body in one differs, and getting it wrong produces link errors rather than compile errors.
// A C++ header, in the style most projects use:
//
// // shape.hpp
// #pragma once
// struct Shape { double side; };
// inline double area(Shape shape) { return shape.side * shape.side; }
//
// "inline" here means "may be defined in several translation units",
// which is what lets a function body live in a header at all.
#include <iostream>
struct Shape { double side; };
inline double area(Shape shape) { return shape.side * shape.side; }
int main() {
std::cout << area(Shape{3.0}) << std::endl;
return 0;
}/* A C header, in the style most projects use:
*
* / * shape.h * /
* #ifndef SHAPE_H
* #define SHAPE_H
* struct Shape { double side; };
* static inline double area(struct Shape shape) { return shape.side * shape.side; }
* #endif
*
* "static inline" is the portable spelling for a function body in a
* header: static gives each translation unit its own copy, which
* sidesteps C's much fussier rules about bare inline.
*/
#include <stdio.h>
struct Shape { double side; };
static inline double area(struct Shape shape) { return shape.side * shape.side; }
int main(void) {
struct Shape square = { 3.0 };
printf("%g\n", area(square));
return 0;
}C's
inline is not C++'s. In C++ an inline function may be defined in every translation unit and the linker picks one; in C, bare inline requires exactly one translation unit to also provide an extern declaration, and forgetting it yields an undefined-symbol error at link time. static inline avoids the whole question and is what you will see in real C headers. #pragma once works in GCC and Clang but is not in the standard, so portable C keeps the #ifndef guard.Types & Declarations
struct tags live in their own namespace
This is the first thing a C++ programmer trips over, usually within the first ten minutes:
Point origin; does not compile.#include <iostream>
struct Point {
int x;
int y;
};
int main() {
// The struct name is a type name. No typedef needed.
Point origin{0, 0};
Point corner = {3, 4};
std::cout << corner.x - origin.x << std::endl;
return 0;
}#include <stdio.h>
/* "struct Point" is the type name; bare "Point" is not a type at all.
* The typedef is what most C code adds to get the C++ spelling.
*/
typedef struct Point {
int x;
int y;
} Point;
int main(void) {
struct Point origin = { 0, 0 }; /* the built-in spelling */
Point corner = { 3, 4 }; /* the typedef spelling */
printf("%d\n", corner.x - origin.x);
return 0;
}C keeps struct tags in a separate namespace from ordinary identifiers, so the type is spelled
struct Point in full. The near-universal fix is the typedef above, which is why so much C code declares structs that way — and why the tag and the typedef name are usually identical, which looks redundant but is not. A side effect worth knowing: because the two namespaces are separate, a variable named Point and a struct Point can coexist, which is legal, confusing, and occasionally deliberate.No auto — every type is written out
C17 has no
auto in the C++11 sense (the keyword exists, but it means the storage class nobody has written since 1978). The interesting part is that this matters less than you expect.#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> stock{{"widget", 7}};
// auto exists precisely because this type is unbearable to write.
for (const auto& entry : stock) {
std::cout << entry.first << " " << entry.second << std::endl;
}
return 0;
}#include <stdio.h>
#include <string.h>
/* No auto, no range-for, and no map — so the type is short because the
* data structure is something you wrote yourself.
*/
struct StockEntry {
const char *name;
int count;
};
int main(void) {
struct StockEntry stock[] = { { "widget", 7 } };
size_t entry_count = sizeof(stock) / sizeof(stock[0]);
for (size_t index = 0; index < entry_count; index += 1) {
printf("%s %d\n", stock[index].name, stock[index].count);
}
return 0;
}Type names in C stay short because the type system stays small: there are no templates, so there is no
std::map<std::string, std::vector<int>>::const_iterator to abbreviate. The idiom you will actually miss is the sizeof(array) / sizeof(array[0]) line above, which computes an element count and silently produces nonsense the moment the array decays to a pointer — for instance after being passed to a function. C23 adds auto and typeof, but this site targets C17.bool and NULL are library, not language
Two of these are a header away and cost nothing. The third is a genuine behavioral difference that has been quietly surprising people since 1989.
#include <iostream>
int main() {
bool ready = true; // a built-in type since C++98
int* nothing = nullptr; // a typed null since C++11
std::cout << std::boolalpha << ready << " " << (nothing == nullptr) << std::endl;
std::cout << sizeof('a') << std::endl; // 1 — char is char
return 0;
}#include <stdio.h>
#include <stdbool.h> /* bool, true, false — macros, not keywords */
#include <stddef.h> /* NULL */
int main(void) {
bool ready = true;
int *nothing = NULL; /* usually ((void*)0), sometimes just 0 */
printf("%s %s\n", ready ? "true" : "false", nothing == NULL ? "true" : "false");
printf("%zu\n", sizeof('a')); /* 4 — a character constant is an int */
return 0;
}sizeof('a') is 4 in C and 1 in C++: a character constant has type int in C and type char in C++. It rarely matters, but it is the classic proof that C is not a subset of C++. NULL is a macro rather than a typed keyword, so it does not distinguish a null pointer from the integer zero the way nullptr does — which is one reason C compilers cannot catch passing NULL where an int was wanted. C23 promotes bool, true, false and nullptr to real keywords; C17 does not.const is not a constant expression
This one bites when porting a header. The declaration is byte-for-byte legal in both languages and means something different in each.
#include <iostream>
int main() {
const int buffer_size = 64;
// A const int initialized by a constant IS a constant expression in
// C++, so it can size an array and be a case label.
char buffer[buffer_size];
buffer[0] = 'x';
std::cout << sizeof(buffer) << " " << buffer[0] << std::endl;
return 0;
}#include <stdio.h>
/* In C a const int is a read-only VARIABLE, not a constant expression.
* Sizing an array with one produces a variable-length array (legal, but
* a different thing), and it cannot be a case label at all.
* The constant-expression tool in C is the enum, or a macro.
*/
enum { BUFFER_SIZE = 64 };
int main(void) {
char buffer[BUFFER_SIZE];
buffer[0] = 'x';
printf("%zu %c\n", sizeof(buffer), buffer[0]);
return 0;
}C++ made
const imply internal linkage and constant-expression-ness precisely so that it could replace #define for named constants. C did neither, so a const int at file scope is an ordinary variable with external linkage — put one in a header included twice and you get duplicate symbols. The C idioms are the anonymous enum above, which is a genuine compile-time constant with a type, or #define, which has no type at all. This is also why array sizes in C headers are almost always macros.void* converts implicitly
Here the direction of the difference reverses: this is a place where C is more permissive than C++, and the C++ habit is actively wrong in C.
#include <cstdlib>
#include <iostream>
int main() {
// C++ REFUSES the implicit void* → int* conversion, so the cast is
// mandatory. This is one of the ways C++ is stricter than C.
int* numbers = static_cast<int*>(std::malloc(3 * sizeof(int)));
if (numbers == nullptr) { return 1; }
numbers[0] = 7;
std::cout << numbers[0] << std::endl;
std::free(numbers);
return 0;
}#include <stdio.h>
#include <stdlib.h>
int main(void) {
/* void* converts to any object pointer implicitly, so casting the
* result of malloc is unnecessary — and casting it is mildly harmful,
* because it hides a missing #include <stdlib.h>.
*/
int *numbers = malloc(3 * sizeof(int));
if (numbers == NULL) { return 1; }
numbers[0] = 7;
printf("%d\n", numbers[0]);
free(numbers);
return 0;
}Casting
malloc is a well-known C style error, and the reason is specific: before C99, calling an undeclared function made the compiler assume it returned int, and the cast would silence the resulting warning while the truncated pointer stayed broken. Modern C makes that an error anyway, but the convention stuck for the second reason — the cast is noise that has to be updated whenever the type changes. Write malloc(count * sizeof(*pointer)) and the size follows the declaration automatically.Manual Memory Management
No RAII — the goto cleanup pattern
This is the single largest difference on the page, and the one that will change how you write code most. Everything C++ automates about lifetime, C makes you write.
#include <iostream>
#include <memory>
#include <vector>
int process() {
// Every one of these frees itself, on every exit path, including
// an exception. There is nothing to remember and nothing to forget.
auto first = std::make_unique<int[]>(100);
std::vector<int> second(100);
if (first[0] != 0) { return 1; } // early return: both freed
if (second[0] != 0) { return 2; } // early return: both freed
return 0;
}
int main() {
std::cout << process() << std::endl;
return 0;
}#include <stdio.h>
#include <stdlib.h>
int process(void) {
int *first = NULL;
int *second = NULL;
int status = 0;
first = calloc(100, sizeof(int));
if (first == NULL) { status = 3; goto cleanup; }
second = calloc(100, sizeof(int));
if (second == NULL) { status = 3; goto cleanup; }
if (first[0] != 0) { status = 1; goto cleanup; }
if (second[0] != 0) { status = 2; goto cleanup; }
cleanup: /* ONE exit path, so one place to free */
free(second); /* free(NULL) is defined and does nothing */
free(first);
return status;
}
int main(void) {
printf("%d\n", process());
return 0;
}The
goto cleanup idiom is not a code smell in C — it is the accepted answer, used throughout the Linux kernel and every serious C codebase, because the alternative is duplicating the free list at each early return and eventually missing one. The rules that make it work: initialize every pointer to NULL up front, have exactly one label, and free in reverse order of acquisition. free(NULL) is explicitly defined to do nothing, which is what lets the single cleanup block run no matter how far the function got.malloc/free instead of new/delete
Four differences here, and the one most likely to burn you is that
malloc does not initialize.#include <iostream>
struct Reading {
int value;
Reading() : value(42) {} // a constructor runs on new
};
int main() {
Reading* single = new Reading(); // allocates AND constructs
Reading* many = new Reading[3]; // constructs all three
std::cout << single->value << " " << many[2].value << std::endl;
delete[] many; // destructs, then frees
delete single;
return 0;
}#include <stdio.h>
#include <stdlib.h>
struct Reading {
int value;
};
int main(void) {
/* malloc allocates BYTES. Nothing is initialized and nothing is
* constructed, so the fields hold garbage until you assign them.
*/
struct Reading *single = malloc(sizeof(*single));
struct Reading *many = malloc(3 * sizeof(*many));
if (single == NULL || many == NULL) { free(many); free(single); return 1; }
single->value = 42;
for (int index = 0; index < 3; index += 1) { many[index].value = 42; }
printf("%d %d\n", single->value, many[2].value);
free(many); /* one free for arrays and singles alike */
free(single);
return 0;
}A freshly
malloc'd struct holds indeterminate bytes, so reading a field before assigning it is undefined behavior — use calloc when you want zeros, which is why the previous row used it. There is no new[]/delete[] distinction because there are no destructors to run, so one free serves both. malloc returns NULL on failure rather than throwing, so the check is on every call and cannot be skipped. And sizeof(*single) rather than sizeof(struct Reading) keeps the size correct if the declaration's type ever changes.realloc, and the aliasing trap
Growing an array by hand is the most common thing a C++ programmer will find themselves writing in C, and it has a specific trap in its most obvious spelling.
#include <iostream>
#include <vector>
int main() {
std::vector<int> readings;
// push_back reallocates when it must; the vector owns the details,
// and a failed allocation throws rather than corrupting anything.
for (int value = 0; value < 5; value += 1) {
readings.push_back(value * value);
}
for (int value : readings) { std::cout << value << " "; }
std::cout << "\ncount is " << readings.size() << std::endl;
return 0;
}#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *readings = NULL;
size_t count = 0;
for (int value = 0; value < 5; value += 1) {
/* Assigning realloc's result straight back to readings LEAKS the
* old block when realloc fails and returns NULL. Use a temporary.
*/
int *grown = realloc(readings, (count + 1) * sizeof(*readings));
if (grown == NULL) { free(readings); return 1; }
readings = grown;
readings[count] = value * value;
count += 1;
}
for (size_t index = 0; index < count; index += 1) { printf("%d ", readings[index]); }
printf("\ncount is %zu\n", count);
free(readings);
return 0;
}readings = realloc(readings, …) reads naturally and leaks: when realloc fails it returns NULL without freeing the original block, so the only pointer to it has just been overwritten. Always land the result in a temporary first. Two more things to carry over: growing one element at a time is O(n²) in copies, so real code doubles a separate capacity field, and every pointer into the old block is dangling after a successful realloc — the same invalidation rule std::vector has, with nothing to warn you.Structs Instead of Classes
No member functions — pass the struct explicitly
A member function is a free function with a hidden first parameter. C makes the parameter visible, and once you see that, most of the translation is mechanical.
#include <iostream>
class Counter {
public:
void increment(int by) { total_ += by; }
int total() const { return total_; }
private:
int total_ = 0;
};
int main() {
Counter counter;
counter.increment(5);
counter.increment(7);
std::cout << counter.total() << std::endl;
return 0;
}#include <stdio.h>
typedef struct Counter {
int total;
} Counter;
/* "this" was never magic: it is a pointer parameter, and here you write
* it yourself. Const-correctness becomes const on that pointer.
*/
static void counter_increment(Counter *counter, int by) { counter->total += by; }
static int counter_total(const Counter *counter) { return counter->total; }
int main(void) {
Counter counter = { 0 };
counter_increment(&counter, 5);
counter_increment(&counter, 7);
printf("%d\n", counter_total(&counter));
return 0;
}The naming convention is load-bearing, since the prefix is all that groups these functions with their type:
counter_ here, fclose/fread/fwrite in the standard library. A const member function becomes const Type *, which is the same guarantee written one level out. Note also Counter counter = { 0 }; — there is no default constructor, so a struct left uninitialized contains garbage, and = { 0 } is the idiom that zeroes every field.Constructor and destructor become a function pair
The pair of functions is easy. What is gone is the guarantee that the second one runs.
#include <cstring>
#include <iostream>
class Buffer {
public:
explicit Buffer(size_t size) : data_(new char[size]), size_(size) {
std::memset(data_, 0, size_);
}
~Buffer() { delete[] data_; } // runs automatically at scope exit
size_t size() const { return size_; }
private:
char* data_;
size_t size_;
};
int main() {
Buffer buffer(16);
std::cout << buffer.size() << std::endl;
return 0; // destructor runs here
}#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Buffer {
char *data;
size_t size;
} Buffer;
/* Returns 0 on success — the constructor's job, minus the guarantee
* that it runs, plus the ability to report failure without throwing.
*/
static int buffer_init(Buffer *buffer, size_t size) {
buffer->data = calloc(size, 1);
if (buffer->data == NULL) { return -1; }
buffer->size = size;
return 0;
}
static void buffer_destroy(Buffer *buffer) {
free(buffer->data);
buffer->data = NULL; /* so a double destroy is harmless */
buffer->size = 0;
}
int main(void) {
Buffer buffer;
if (buffer_init(&buffer, 16) != 0) { return 1; }
printf("%zu\n", buffer.size);
buffer_destroy(&buffer); /* YOU must remember this line */
return 0;
}Nothing calls
buffer_destroy for you: not scope exit, not an early return, and nothing at all on a path that gotos past it. This is the discipline the previous goto cleanup row exists to support. Two conventions worth adopting: return a status code from the init function rather than leaving failure unreported, since there is no constructor exception to throw, and null the pointer in the destroy function so a second call is safe — C has no way to mark an object as destroyed.No private — opaque pointers instead
C has no
private, but it does have real encapsulation — stronger than C++'s, in one specific respect.#include <iostream>
class Connection {
public:
Connection() : socket_handle_(3) {}
int id() const { return socket_handle_; }
private:
int socket_handle_; // callers cannot touch this
};
int main() {
Connection connection;
// connection.socket_handle_ = 9; // error: private
std::cout << connection.id() << std::endl;
return 0;
}#include <stdio.h>
#include <stdlib.h>
/* In the header, callers see only this — an INCOMPLETE type. They can
* hold a Connection*, and can do nothing else with it, because they do
* not know what is inside or even how big it is.
*/
typedef struct Connection Connection;
Connection *connection_open(void);
int connection_id(const Connection *connection);
void connection_close(Connection *connection);
/* In the .c file, the definition. This is the only place the fields exist. */
struct Connection {
int socket_handle;
};
Connection *connection_open(void) {
Connection *connection = malloc(sizeof(*connection));
if (connection != NULL) { connection->socket_handle = 3; }
return connection;
}
int connection_id(const Connection *connection) { return connection->socket_handle; }
void connection_close(Connection *connection) { free(connection); }
int main(void) {
Connection *connection = connection_open();
if (connection == NULL) { return 1; }
printf("%d\n", connection_id(connection));
connection_close(connection);
return 0;
}An opaque pointer hides the fields from the compiler, not merely from the programmer: a caller that never sees
struct Connection's definition cannot reach a field even by trying, and — unlike a C++ private member — changing the layout does not force callers to recompile. That is why this pattern is everywhere in C library design (FILE, sqlite3, pthread_mutex_t in some implementations). The cost is that the type can no longer be stack-allocated by callers, so the library must hand out heap objects and take them back, which is exactly the _open/_close pair above.Hand-Rolled Polymorphism
virtual becomes a struct of function pointers
Dynamic dispatch is not magic and never was — it is an indirect call through a table. Writing that table by hand is the clearest possible explanation of what
virtual compiles to.#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
double area() const override { return side_ * side_; }
private:
double side_;
};
class Circle : public Shape {
public:
explicit Circle(double radius) : radius_(radius) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Square>(3.0));
shapes.push_back(std::make_unique<Circle>(1.0));
for (const auto& shape : shapes) {
std::cout << shape->area() << std::endl;
}
return 0;
}#include <stdio.h>
/* The vtable, written out. The compiler builds exactly this for you in
* C++; here it is a struct, and the object holds a pointer to it.
*/
typedef struct Shape Shape;
typedef struct ShapeVTable {
double (*area)(const Shape *shape);
} ShapeVTable;
struct Shape {
const ShapeVTable *vtable; /* by convention, the FIRST member */
};
typedef struct Square { Shape base; double side; } Square;
typedef struct Circle { Shape base; double radius; } Circle;
static double square_area(const Shape *shape) {
const Square *square = (const Square *)shape; /* safe: base is first */
return square->side * square->side;
}
static double circle_area(const Shape *shape) {
const Circle *circle = (const Circle *)shape;
return 3.14159 * circle->radius * circle->radius;
}
static const ShapeVTable square_vtable = { square_area };
static const ShapeVTable circle_vtable = { circle_area };
int main(void) {
Square square = { { &square_vtable }, 3.0 };
Circle circle = { { &circle_vtable }, 1.0 };
Shape *shapes[] = { &square.base, &circle.base };
for (int index = 0; index < 2; index += 1) {
printf("%g\n", shapes[index]->vtable->area(shapes[index]));
}
return 0;
}Two conventions make the cast legal rather than merely lucky. Putting the base struct first means the standard guarantees a pointer to the struct and a pointer to its first member have the same address, so the downcast is well-defined. And the vtable is
static const, so one copy is shared by every instance — exactly what C++ does. This pattern is how GTK, the Linux kernel's file_operations, and COM all work. What you give up is any type checking on the cast: get the base wrong and there is no diagnostic, just wrong memory.Lambdas become a function pointer plus void*
A C++ lambda with captures is a struct with an
operator(). In C the struct and the function separate, and the void* is the seam between them.#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> readings{5, 3, 9, 1};
int threshold = 4;
// The lambda CAPTURES threshold; the closure carries it along.
auto above = std::count_if(readings.begin(), readings.end(),
[threshold](int value) { return value > threshold; });
std::cout << above << std::endl;
return 0;
}#include <stdio.h>
/* A function pointer captures nothing, so anything the callback needs
* travels beside it as a void* the caller passes straight through.
*/
static size_t count_if(const int *values, size_t count,
int (*predicate)(int value, void *context),
void *context) {
size_t matches = 0;
for (size_t index = 0; index < count; index += 1) {
if (predicate(values[index], context)) { matches += 1; }
}
return matches;
}
static int above_threshold(int value, void *context) {
return value > *(const int *)context;
}
int main(void) {
int readings[] = { 5, 3, 9, 1 };
int threshold = 4;
size_t above = count_if(readings, 4, above_threshold, &threshold);
printf("%zu\n", above);
return 0;
}The
void *context parameter — variously called user_data, closure, or arg — is the standard C answer to closure capture, and once you recognize it you will see it in every callback API you have ever used. When you design one, always put it there even if today's callbacks need nothing: adding it later breaks every caller. The cautionary counter-example is qsort, whose comparator takes no context, which is why sorting by a runtime-chosen key needs a global variable or the non-standard qsort_r.Strings
char* and the null terminator
A C string is not a type. It is a convention: a pointer to characters that ends at the first zero byte, and everything follows from that.
#include <iostream>
#include <string>
int main() {
std::string greeting = "hello";
greeting += ", world"; // grows itself
std::cout << greeting << std::endl;
std::cout << greeting.size() << std::endl; // O(1), stored
return 0;
}#include <stdio.h>
#include <string.h>
int main(void) {
/* The buffer is yours, its size is fixed, and the length is found
* by scanning for the terminating zero byte every single time.
*/
char greeting[32] = "hello";
strncat(greeting, ", world", sizeof(greeting) - strlen(greeting) - 1);
printf("%s\n", greeting);
printf("%zu\n", strlen(greeting)); /* O(n), computed on each call */
return 0;
}Because the length is not stored,
strlen walks the whole string — so the innocent-looking for (i = 0; i < strlen(text); i++) is quadratic. Because the buffer is fixed, every write needs a bound, which is why strncat appears above rather than strcat, and why its size argument is the remaining room rather than the total. And because the terminator is what makes it a string, a buffer that fills exactly leaves no room for it — the commonest single cause of C string bugs.Building a string safely
snprintf is the one string function to internalize, because its return value carries information the obvious reading throws away.#include <format>
#include <iostream>
#include <string>
int main() {
std::string product = "widget";
int quantity = 7;
// The result sizes itself; overflow is not a thing that can happen.
std::string line = std::format("{} x{}", product, quantity);
std::cout << line << std::endl;
return 0;
}#include <stdio.h>
int main(void) {
const char *product = "widget";
int quantity = 7;
char line[64];
/* snprintf never writes past the size, always terminates, and
* returns the length it WANTED — which is how truncation is detected.
*/
int wanted = snprintf(line, sizeof(line), "%s x%d", product, quantity);
if (wanted < 0 || (size_t)wanted >= sizeof(line)) {
printf("truncated\n");
return 1;
}
printf("%s\n", line);
return 0;
}It returns the length the formatted string would have been, not the number of bytes written — so
wanted >= sizeof(line) is exactly the truncation test, and code that ignores the return value silently produces a shortened string. Never use sprintf, which has no bound at all, and prefer snprintf to strcpy/strcat chains even when the latter would fit, because one call with one bound is easier to audit than four. Note also that %s with an int argument compiles and crashes; the format string is a second, unchecked type system.Growable Arrays & Lookup
There is no standard container library
This is worth stating plainly rather than discovering: C has no vector, no map, no set, no list, and no string class.
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
int main() {
std::vector<int> readings{3, 1, 2};
std::set<int> unique{3, 1, 2, 1};
std::map<std::string, int> stock{{"widget", 7}};
std::cout << readings.size() << " "
<< unique.size() << " "
<< stock.at("widget") << std::endl;
return 0;
}#include <stdio.h>
/* The C standard library offers: arrays, qsort, bsearch. That is the
* whole list. Everything else you write, vendor, or do without.
*/
int main(void) {
int readings[] = { 3, 1, 2 };
size_t count = sizeof(readings) / sizeof(readings[0]);
printf("%zu\n", count);
return 0;
}What C does ship is
qsort and bsearch, both taking a comparator, and that pair covers a surprising amount of ground: sort once, then binary-search, and you have a read-mostly map without writing a hash table. When that is not enough the options are to write the structure (a few hundred lines for a decent hash table), to vendor a single-header library, or to restructure so you do not need one. Real C codebases lean on the third far more than a C++ programmer expects — fixed-size arrays and linear scans are genuinely fine at the sizes most code actually handles.std::sort vs qsort
This is the concrete cost of having no templates, and it is measurable rather than theoretical.
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> readings{5, 3, 9, 1};
// Knows the type. Inlines the comparison. Typically 2-3x faster.
std::sort(readings.begin(), readings.end());
for (int value : readings) { std::cout << value << " "; }
std::cout << std::endl;
return 0;
}#include <stdio.h>
#include <stdlib.h>
/* The comparator takes const void*, so every call costs two casts, two
* dereferences and an indirect call that cannot be inlined.
*/
static int compare_ints(const void *left, const void *right) {
int first = *(const int *)left;
int second = *(const int *)right;
return (first > second) - (first < second);
}
int main(void) {
int readings[] = { 5, 3, 9, 1 };
size_t count = sizeof(readings) / sizeof(readings[0]);
qsort(readings, count, sizeof(readings[0]), compare_ints);
for (size_t index = 0; index < count; index += 1) { printf("%d ", readings[index]); }
printf("\n");
return 0;
}Note the comparator body:
(first > second) - (first < second) rather than the tempting first - second, which overflows for large-magnitude inputs and returns the wrong sign — a real and frequently-shipped bug. qsort also gives no stability guarantee (std::stable_sort has no C counterpart) and no way to pass context to the comparator, which is why sorting by a runtime-chosen field needs a file-scope variable or the non-standard qsort_r. The type erasure is the whole difference: std::sort monomorphizes and inlines, qsort cannot.Flexible array members
A rare row where the C version is the one with the better tool: this is a C99 feature that C++ has never standardized.
#include <iostream>
#include <memory>
// A header plus its payload means two allocations, or a vector member
// that allocates separately from the object it lives in.
struct Packet {
int length;
std::unique_ptr<char[]> payload;
};
int main() {
Packet packet;
packet.length = 4;
packet.payload = std::make_unique<char[]>(4);
packet.payload[0] = 'a';
std::cout << packet.length << " " << packet.payload[0] << std::endl;
return 0;
}#include <stdio.h>
#include <stdlib.h>
/* The array has no size, and must be last. The struct and its payload
* then live in ONE allocation, contiguous, with one free.
*/
struct Packet {
int length;
char payload[];
};
int main(void) {
int length = 4;
struct Packet *packet = malloc(sizeof(*packet) + (size_t)length);
if (packet == NULL) { return 1; }
packet->length = length;
packet->payload[0] = 'a';
printf("%d %c\n", packet->length, packet->payload[0]);
free(packet); /* one allocation, one free */
return 0;
}One allocation instead of two means one cache line rather than a pointer chase, and one
free rather than an ownership question — which is why this appears throughout networking code and the Linux kernel, where a header followed by a variable payload is the shape of nearly everything. The rules: the flexible array must be the last member, the struct must have at least one other member, and sizeof the struct does not include it, which is what makes the sizeof(*packet) + length arithmetic correct. C++ has no standard equivalent; the closest is the non-standard "struct hack" with a one-element array.Generic Code Without Templates
Templates become void* and a size
The
void*-plus-size approach is one of two ways to write generic C. It produces one copy of the code, and gives up every type guarantee to do it.#include <iostream>
template <typename Element>
void swap_values(Element& first, Element& second) {
Element held = first;
first = second;
second = held;
}
int main() {
int left = 1;
int right = 2;
swap_values(left, right); // type-checked, inlined, no copies
std::cout << left << " " << right << std::endl;
return 0;
}#include <stdio.h>
#include <string.h>
/* One function for all types, at the cost of every guarantee: the size
* is passed by hand, the types are unchecked, and it copies bytes.
*/
static void swap_values(void *first, void *second, size_t size) {
unsigned char *left = first;
unsigned char *right = second;
for (size_t index = 0; index < size; index += 1) {
unsigned char held = left[index];
left[index] = right[index];
right[index] = held;
}
}
int main(void) {
int left = 1;
int right = 2;
swap_values(&left, &right, sizeof(left));
printf("%d %d\n", left, right);
return 0;
}Nothing checks that the two pointers have the same type, or that
size matches either of them — swap_values(&an_int, &a_double, sizeof(double)) compiles and corrupts memory. It also cannot be inlined through the indirection, so it is slower than the template for small types. This is the trade every void* API in C makes, including qsort and memcpy. The alternative is the macro in the next row, which keeps the speed and loses different things.Templates become macros
The second way to write generic C keeps the performance of a template and gives up the type checking, the scoping, and single evaluation.
#include <iostream>
template <typename Element>
Element largest(Element first, Element second) {
return first > second ? first : second;
}
int main() {
std::cout << largest(3, 9) << std::endl;
std::cout << largest(1.5, 0.5) << std::endl;
return 0;
}#include <stdio.h>
/* Textual substitution: works on any type with >, generates no code of
* its own, and evaluates each argument TWICE.
*/
#define LARGEST(first, second) ((first) > (second) ? (first) : (second))
int main(void) {
printf("%d\n", LARGEST(3, 9));
printf("%g\n", LARGEST(1.5, 0.5));
return 0;
}Every argument appears twice in the expansion, so
LARGEST(next(), 0) calls next() twice — the classic macro bug, and the reason the parentheses around every parameter are not optional either. There is no scoping, so any temporary the macro introduces can collide with a caller's variable, which is why C macros use __ugly_names. And the argument types are never checked: mixing an int and a double here silently applies the usual arithmetic conversions. C11 _Generic can dispatch on type inside a macro, which recovers some safety at considerable cost in readability.Functions
No overloading and no default arguments
Both of these follow directly from one symbol per name, which the compiling row set up. The workarounds are the ones the C standard library itself uses.
#include <iostream>
#include <string>
void report(int value) { std::cout << "int " << value << std::endl; }
void report(double value) { std::cout << "double " << value << std::endl; }
void report(const std::string& value) { std::cout << "text " << value << std::endl; }
void connect(const std::string& host, int port = 80) {
std::cout << host << ":" << port << std::endl;
}
int main() {
report(1);
report(1.5);
report(std::string("hi"));
connect("example.com");
return 0;
}#include <stdio.h>
/* One symbol per name, so the type goes IN the name. The standard
* library does the same: abs, labs, llabs, fabs, fabsf.
*/
void report_int(int value) { printf("int %d\n", value); }
void report_double(double value) { printf("double %g\n", value); }
void report_text(const char *value) { printf("text %s\n", value); }
/* No default arguments either — write the wrapper. */
void connect_on_port(const char *host, int port) { printf("%s:%d\n", host, port); }
void connect(const char *host) { connect_on_port(host, 80); }
int main(void) {
report_int(1);
report_double(1.5);
report_text("hi");
connect("example.com");
return 0;
}The naming convention is worth copying rather than inventing: a suffix for the type (
abs/fabs/fabsf) or a descriptive verb phrase (connect_on_port). Default arguments become a wrapper function, which has one genuine advantage — the default lives in one place at link time, rather than being baked into every caller's compiled code the way a C++ default argument is. C11's _Generic can fake overloading inside a macro, and that is exactly how <tgmath.h> makes sqrt work for floats and doubles alike.No references — pointers, and they can be null
Losing references costs two things: the null-free guarantee, and the invisible call site.
#include <iostream>
// A reference cannot be null and cannot be reseated, so the function
// needs no check and the caller cannot pass "nothing".
void double_in_place(int& value) { value *= 2; }
int main() {
int reading = 21;
double_in_place(reading); // no & at the call site
std::cout << reading << std::endl;
return 0;
}#include <stdio.h>
/* A pointer can be null, so a defensive function checks — and the
* caller must remember the &.
*/
void double_in_place(int *value) {
if (value == NULL) { return; }
*value *= 2;
}
int main(void) {
int reading = 21;
double_in_place(&reading); /* the & is visible, and required */
printf("%d\n", reading);
return 0;
}The visible
& is arguably an improvement — a C++ call double_in_place(reading) gives no hint that the argument may change, while &reading says so. What is genuinely lost is the guarantee: a reference parameter cannot be null, so C++ needs no check, whereas every C function taking a pointer must decide whether null is a caller error (document it and let it crash) or a supported input (check it). Pick one per function and say which in a comment; the standard library is inconsistent about this and it causes real bugs.Returning a struct by value
Worth a row precisely because so many C++ programmers assume otherwise and reach for out-parameters that are not needed.
#include <iostream>
struct Point {
double x;
double y;
};
Point midpoint(Point first, Point second) {
// Copy elision means the result is built directly in the caller.
return Point{(first.x + second.x) / 2, (first.y + second.y) / 2};
}
int main() {
Point result = midpoint(Point{0, 0}, Point{4, 6});
std::cout << result.x << " " << result.y << std::endl;
return 0;
}#include <stdio.h>
typedef struct Point {
double x;
double y;
} Point;
Point midpoint(Point first, Point second) {
/* Structs pass and return by value in C too — this is NOT one of
* the things C is missing, and it surprises people who expect it to be.
*/
Point result = { (first.x + second.x) / 2, (first.y + second.y) / 2 };
return result;
}
int main(void) {
Point origin = { 0, 0 };
Point corner = { 4, 6 };
Point result = midpoint(origin, corner);
printf("%g %g\n", result.x, result.y);
return 0;
}Struct assignment, struct arguments and struct returns have all been in C since C89, and the compiler applies the same return-value optimization it does in C++ — the result is built in the caller's storage, not copied. The one thing that does not come along is deep copying: a struct containing a
char * copies the pointer, not the string, because there is no copy constructor to run. Arrays are also still special, and still decay to pointers when passed, which is why a struct wrapping an array is sometimes used to get by-value semantics for one.Error Handling
No exceptions — return codes
With no exceptions, the return value has to carry two things at once — whether it worked, and what the answer was — and C resolves that by moving the answer to an out-parameter.
#include <iostream>
#include <stdexcept>
#include <string>
int parse_port(const std::string& text) {
int value = std::stoi(text); // throws on failure
if (value < 0) { throw std::out_of_range("negative"); }
return value;
}
int main() {
try {
std::cout << parse_port("8080") << std::endl;
std::cout << parse_port("-1") << std::endl;
} catch (const std::exception& problem) {
std::cout << "failed" << std::endl;
}
return 0;
}#include <stdio.h>
#include <stdlib.h>
/* The return value carries the STATUS; the answer goes to an
* out-parameter. This is the dominant C convention.
*/
static int parse_port(const char *text, int *out_port) {
char *stopped_at = NULL;
long value = strtol(text, &stopped_at, 10);
if (stopped_at == text || *stopped_at != '\0') { return -1; }
if (value < 0) { return -1; }
*out_port = (int)value;
return 0;
}
int main(void) {
int port = 0;
if (parse_port("8080", &port) == 0) { printf("%d\n", port); }
if (parse_port("-1", &port) != 0) { printf("failed\n"); }
return 0;
}Note
strtol rather than atoi: atoi returns 0 for both "0" and "nonsense" and cannot tell you which, so it has no place in code that cares. The stopped_at pointer is how strtol reports where parsing ended, which is what makes "trailing garbage" detectable. The bigger consequence is one the C++ reader will feel constantly: an ignored return value is silent, so there is no equivalent of an unhandled exception terminating the program — a failure nobody checked simply proceeds with garbage.errno, and why it is not a return value
C's standard library splits failure reporting in two: the return value says that something failed, and a global says why.
#include <iostream>
#include <stdexcept>
int main() {
try {
// The exception object carries the reason with it, typed and
// scoped to the call that threw.
throw std::runtime_error("No such file or directory");
} catch (const std::runtime_error& problem) {
std::cout << "open failed: " << problem.what() << std::endl;
}
return 0;
}#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
/* The call reports FAILURE; errno reports WHY, out of band.
* Read it immediately — the next library call may overwrite it.
*/
FILE *file = fopen("/no/such/path/at/all", "r");
if (file == NULL) {
printf("open failed: %s\n", strerror(errno));
return 0;
}
fclose(file);
return 0;
}errno is thread-local in any modern implementation, so it is not the data race it looks like, but the timing rule is strict: read it immediately after the failing call, because a successful library call is permitted to clobber it and many do. It is also only meaningful after a failure — a successful call may leave any value behind, so testing errno without first testing the return value is a bug. strerror turns it into a message; perror prints one directly. Nothing forces you to look at either, which is the recurring theme of C error handling.Compile-time and runtime assertions
Both assertions carry over, with one syntactic catch that will stop your first build.
#include <cassert>
#include <iostream>
struct Header {
int length;
char kind;
};
int main() {
// static_assert has been a keyword since C++11, message optional
// since C++17.
static_assert(sizeof(int) == 4);
int value = 7;
assert(value > 0); // compiled out when NDEBUG is defined
std::cout << sizeof(Header) << std::endl;
return 0;
}#include <assert.h>
#include <stdio.h>
struct Header {
int length;
char kind;
};
int main(void) {
/* _Static_assert is the C11 keyword; assert.h defines static_assert
* as a macro for it. In C17 the message is REQUIRED.
*/
_Static_assert(sizeof(int) == 4, "this code assumes 32-bit int");
int value = 7;
assert(value > 0); /* also compiled out when NDEBUG is defined */
printf("%zu\n", sizeof(struct Header));
return 0;
}C17's
_Static_assert requires the message argument — the single-argument form C++17 allows is a C23 addition and does not compile here. Including <assert.h> gets you static_assert as a macro spelling of the same thing, which is what most C code uses. Runtime assert behaves identically to C++'s, including being erased by -DNDEBUG, which means the usual warning applies twice over: never put an expression with side effects inside one, because in a release build it will not run at all.What C Has That C++ Does Not
Designated initializers, out of order
C++20 finally adopted this C99 feature, and adopted a restricted version of it.
#include <iostream>
struct Config {
int port;
int timeout;
bool verbose;
};
int main() {
// C++20 allows designated initializers, but ONLY in declaration
// order and with no gaps skipped arbitrarily.
Config config{.port = 8080, .verbose = true};
std::cout << config.port << " " << config.timeout << " "
<< config.verbose << std::endl;
return 0;
}#include <stdio.h>
#include <stdbool.h>
struct Config {
int port;
int timeout;
bool verbose;
};
int main(void) {
/* C99 allows ANY order, and every field you skip is zeroed. */
struct Config config = { .verbose = true, .port = 8080 };
printf("%d %d %d\n", config.port, config.timeout, config.verbose);
return 0;
}C++ requires the designators to appear in declaration order, so
{.verbose = true, .port = 8080} above is a C++ error and valid C. C also allows mixing designated and positional initializers, and array designators ([3] = 7), neither of which C++ has. The shared and genuinely valuable part is the zeroing: any field you do not name is zero-initialized, so a struct that gains a member does not silently leave it as garbage in existing code — which makes this the right default for configuration structs in both languages.Compound literals
These look like C++ temporaries and are a meaningfully different thing: they are objects, not values.
#include <iostream>
struct Point {
int x;
int y;
};
void report(Point point) {
std::cout << point.x << "," << point.y << std::endl;
}
int main() {
// C++ spells this with a constructor call or braced init; the
// object is a temporary with no address you may keep.
report(Point{3, 4});
return 0;
}#include <stdio.h>
struct Point {
int x;
int y;
};
void report(struct Point point) {
printf("%d,%d\n", point.x, point.y);
}
int main(void) {
/* A compound literal is an unnamed OBJECT with a real address and
* automatic storage duration — so taking a pointer to it is fine.
*/
report((struct Point){ 3, 4 });
struct Point *pointer = &(struct Point){ 5, 6 };
printf("%d,%d\n", pointer->x, pointer->y);
return 0;
}A compound literal has an address and a storage duration — automatic inside a function, static at file scope — so
&(struct Point){5, 6} is legal C and has no C++ equivalent. That makes it genuinely useful for passing a one-off struct to a function taking a pointer, which otherwise needs a named local. The lifetime rule is the one to respect: an automatic compound literal dies at the end of its enclosing block, so returning that pointer is the same dangling-reference bug it would be for a local.restrict — the no-alias promise
C99 added a keyword for the aliasing question, and C++ never standardized it — every C++ compiler that supports it does so as an extension spelled
__restrict.#include <iostream>
// No standard "restrict" in C++. The compiler must assume these two
// may overlap, so it reloads after every store.
void add_arrays(const double* left, const double* right, double* out, int count) {
for (int index = 0; index < count; index += 1) {
out[index] = left[index] + right[index];
}
}
int main() {
double left[3] = {1, 2, 3};
double right[3] = {10, 20, 30};
double out[3] = {0, 0, 0};
add_arrays(left, right, out, 3);
std::cout << out[0] << " " << out[2] << std::endl;
return 0;
}#include <stdio.h>
/* restrict promises these pointers do not alias, which lets the
* compiler keep values in registers and vectorize the loop.
*/
void add_arrays(const double *restrict left, const double *restrict right,
double *restrict out, int count) {
for (int index = 0; index < count; index += 1) {
out[index] = left[index] + right[index];
}
}
int main(void) {
double left[3] = { 1, 2, 3 };
double right[3] = { 10, 20, 30 };
double out[3] = { 0, 0, 0 };
add_arrays(left, right, out, 3);
printf("%g %g\n", out[0], out[2]);
return 0;
}This is a promise you make, and nothing checks it: call
add_arrays(values, values, values, 3) and the behavior is undefined, with the usual symptom of correct debug builds and wrong optimized ones. That is the opposite of Rust's &mut, which gives the compiler the same information but proves it. Used correctly on numeric kernels the payoff is real — often several times faster, because the compiler can hoist loads and vectorize instead of assuming every store may have invalidated every load.Variable-length arrays
A C99 feature C++ deliberately declined, and one where the C version really is doing something C++ cannot.
#include <iostream>
#include <vector>
int main() {
int count = 4;
// C++ has no VLAs. The heap-allocating vector is the standard
// answer; alloca exists but is non-standard and hazardous.
std::vector<int> scratch(count);
for (int index = 0; index < count; index += 1) { scratch[index] = index * index; }
std::cout << scratch[3] << " " << scratch.size() << std::endl;
return 0;
}#include <stdio.h>
int main(void) {
int count = 4;
/* The length is a runtime value and the storage is on the STACK.
* No malloc, no free, and no allocation failure to check for.
*/
int scratch[count];
for (int index = 0; index < count; index += 1) { scratch[index] = index * index; }
printf("%d %zu\n", scratch[3], sizeof(scratch) / sizeof(scratch[0]));
return 0;
}No heap allocation means no
free and no failure path, and sizeof on a variable-length array is computed at runtime and gives the real size. The reason C++ said no is the reason to be careful in C too: the length is unchecked, so a large or attacker-influenced count blows the stack with no diagnostic — several CVEs have exactly this shape. Use them for small, bounded sizes only. Note also that C11 made VLAs an optional feature, so strictly portable C cannot rely on them; GCC and Clang both support them.Type punning through a union
This is the sharpest "C is not a subset of C++" row on the page: identical code, defined in one language and undefined in the other.
#include <cstring>
#include <cstdint>
#include <iostream>
int main() {
float source = 1.0f;
std::uint32_t bits = 0;
// Reading a union member other than the one last written is
// undefined behavior in C++. memcpy is the defined way, and
// optimizes to the same instruction.
std::memcpy(&bits, &source, sizeof(bits));
std::cout << std::hex << bits << std::endl;
return 0;
}#include <stdint.h>
#include <stdio.h>
union FloatBits {
float value;
uint32_t bits;
};
int main(void) {
/* Reading a member other than the one last written is EXPLICITLY
* permitted in C — the value is reinterpreted, as you would expect.
*/
union FloatBits converter;
converter.value = 1.0f;
printf("%x\n", converter.bits);
return 0;
}C explicitly blesses reading a union member other than the last one written; C++ does not, and although every mainstream C++ compiler does the expected thing in practice, the standard permits it not to. The portable answer in C++ is
std::memcpy, which every optimizer recognizes and turns into the same single instruction, so it costs nothing — and C++20 added std::bit_cast as a typed spelling of it. When porting C to C++, union punning is one of the specific things to look for, because it compiles silently and only misbehaves under optimization.Mixing C and C++
extern "C" and the dual-language header
The asymmetry is the point of this row: all the work happens on the C++ side, and the C side is unaware any of it is going on.
#include <iostream>
// A header meant for BOTH languages wraps its declarations, guarded by
// __cplusplus so a C compiler never sees the C++-only syntax:
//
// #ifdef __cplusplus
// extern "C" {
// #endif
//
// int library_add(int first, int second);
//
// #ifdef __cplusplus
// }
// #endif
extern "C" int library_add(int first, int second);
extern "C" int library_add(int first, int second) {
return first + second;
}
int main() {
std::cout << library_add(2, 3) << std::endl;
return 0;
}#include <stdio.h>
/* The C side needs nothing at all: this is simply how C works, and the
* whole mechanism exists to make C++ behave this way on request.
*/
int library_add(int first, int second) {
return first + second;
}
int main(void) {
printf("%d\n", library_add(2, 3));
return 0;
}extern "C" tells the C++ compiler to emit an unmangled symbol with the C calling convention — it does not change the language of the code inside, which is still C++. That means an extern "C" function may not be overloaded (there would be one symbol for two definitions) and must not let an exception escape, since the C caller has no unwinding machinery. The __cplusplus guard is the standard way to write one header both compilers can read, and you will find it at the top of essentially every C library header in existence.C is not a subset of C++
Worth closing on, because the assumption that C is a subset of C++ is common, load-bearing in people's planning, and false.
#include <cstdlib>
#include <iostream>
int main() {
// Each of these is legal C and rejected or changed by C++:
//
// int *p = malloc(4); // C++: no implicit void* conversion
// int new = 1; // C++: "new" is a keyword
// struct Point { int x; };
// Point origin; // C++: fine. C: needs "struct Point"
// sizeof('a') // C: 4 (int). C++: 1 (char)
//
// So "just compile it as C++" is not a porting strategy.
std::cout << sizeof('a') << std::endl;
return 0;
}#include <stdio.h>
#include <stdlib.h>
int main(void) {
/* All of these are ordinary C. */
int *numbers = malloc(4); /* no cast needed */
int new_count = 1; /* "new" would be legal too */
if (numbers == NULL) { return 1; }
numbers[0] = new_count;
printf("%zu %d\n", sizeof('a'), numbers[0]);
free(numbers);
return 0;
}The two languages share an enormous amount and diverged in 1983; C has kept moving since (C99, C11, C17, C23) and not always toward C++. Practical consequences: compiling a C file as C++ is a porting exercise rather than a no-op, C++ keywords like
new, class, template and this are ordinary identifiers in C and appear in real headers, and a header meant for both must avoid every construct either language rejects. The reliable way to use C from C++ is to keep compiling the C as C and cross the boundary with extern "C".