C++ and C side by side

C++ grew out of C, and the same program can often be written in both. The difference is where the work sits: C leaves the algorithm, the size and the lifetime to the programmer, and C++ moves them into the type system and the library.

Each section below has two files, one compiled with gcc and one with g++, and both are run. The C version is not wrong; it is the same task with fewer guarantees, which is what a C programmer trades for control and a smaller runtime.

The C version of each task is shorter and the C++ version says more. Neither is free: the C programmer carries the size and the lifetime, and the C++ programmer carries the abstractions that keep them right.

qsort against std::sort

The C version calls qsort with a comparison over const void*, a size and a count. The comparison casts the pointers back, and the compiler cannot check that the cast is right.

The C++ version sorts a std::vector with std::sort and a typed comparator that the compiler instantiates. Both print 1 2 3, and the C++ call has no void pointer, no element size and no chance of a wrong cast.

The C interface exists for any type, which is why it takes a size and a void pointer. C++ gets the same generality from a template, and the type is recovered at compile time instead of at the top of a callback.

sort.c

#include <stdio.h> #include <stdlib.h> int compare(const void* left, const void* right) { return *(const int*)left - *(const int*)right; } int main(void) { int values[] = {3, 1, 2}; qsort(values, 3, sizeof(values[0]), compare); for (int i = 0; i < 3; ++i) { printf("%d ", values[i]); } printf("\n"); return 0; }

sort.cpp

#include <algorithm> #include <iostream> #include <vector> int main() { std::vector<int> values{3, 1, 2}; std::sort(values.begin(), values.end()); for (int value : values) { std::cout << value << ' '; } std::cout << '\n'; }

gcc -std=c17 -Wall -Wextra sort.c -o sort_c
./sort_c
g++ -std=c++23 -Wall -Wextra sort.cpp -o sort_cpp
./sort_cpp

1 2 3
1 2 3

A struct and functions against a class

The C rectangle is a struct with two fields and a function that takes a pointer to it. The data and the operation are separate, and nothing stops a caller from passing a rectangle with a negative width.

The C++ rectangle is a class with a constructor and a const member function. Both programs print 12, and the C++ one has one place that builds a rectangle and one place that computes its area.

The gain is not the syntax of class. It is that the compiler now knows the invariant, and the constructor can enforce it, which the C version does with a comment or not at all.

rectangle.c

#include <stdio.h> typedef struct { int width; int height; } rectangle; int area(const rectangle* shape) { return shape->width * shape->height; } int main(void) { rectangle shape = {3, 4}; printf("%d\n", area(&shape)); return 0; }

rectangle.cpp

#include <iostream> class rectangle { public: rectangle(int width, int height) : width_(width), height_(height) {} int area() const { return width_ * height_; } private: int width_; int height_; }; int main() { std::cout << rectangle{3, 4}.area() << '\n'; }

gcc -std=c17 -Wall -Wextra rectangle.c -o rectangle_c
./rectangle_c
g++ -std=c++23 -Wall -Wextra rectangle.cpp -o rectangle_cpp
./rectangle_cpp

12
12

Strings by hand and std::string

The C version reserves an array of twenty chars, copies into it with strcpy and strcat, and reads the length with strlen. Every one of those calls has a size the programmer has to keep right.

The C++ version concatenates two std::string objects with + and reads size(). Both print hello world and 11, and the C++ version allocates and frees the buffer itself, which lesson 023 covered.

The C version is not slower in any interesting way. It is a promise that the buffer is large enough, and the C++ version is the same promise kept by the type.

strings.c

#include <stdio.h> #include <string.h> int main(void) { char text[20]; strcpy(text, "hello"); strcat(text, " world"); printf("%s %zu\n", text, strlen(text)); return 0; }

strings.cpp

#include <iostream> #include <string> int main() { std::string text = "hello"; text += " world"; std::cout << text << ' ' << text.size() << '\n'; }

gcc -std=c17 -Wall -Wextra strings.c -o strings_c
./strings_c
g++ -std=c++23 -Wall -Wextra strings.cpp -o strings_cpp
./strings_cpp

hello world 11
hello world 11

The same file under g++

A C file is not always a C++ file. The program below erases a pointer to void and assigns it back to an int pointer, which C allows and C++ rejects.

The gcc command accepts the file and produces a program that returns 1. The g++ command reports the invalid conversion from void* to int* and stops, which is the language difference the previous lesson's malloc section met from the other side.

The fix is a cast, static_cast<int*>(erased), or the C++ type that keeps the information. A file that has to compile as both languages needs the cast and the headers that both provide, which is why a shared header is a careful piece of work.

int main(void) { int value = 1; void* erased = &value; int* back = erased; return *back; }

gcc -std=c17 -Wall -Wextra erase.c -o erase_c
g++ -std=c++23 -Wall -Wextra erase.c -o erase_cpp

erase.c: In function 'int main()':
erase.c:5:17: error: invalid conversion from 'void*' to 'int*' [-fpermissive]
5 | int* back = erased;
| ^~~~~~
| |
| void*

Exercises

  1. Add a descending comparison to both sort programs and check the order.
  2. Give the C rectangle a function that validates the width and call it before area.
  3. Compile the strings.c file with g++ and explain the result.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why does qsort take a size and a count?

    Answer

    Because it works on void pointers and cannot know the element type

    The C interface is untyped, so the caller supplies the layout of the elements.

  2. What does std::sort use instead of a size and a comparison over void pointers?

    Answer

    A typed range and a comparator template

    The template recovers the element type at compile time, so no cast is needed.

  3. What does a class add to a C struct with functions?

    Answer

    A place to keep the invariant and enforce it in the constructor

    The data and its operations live together, and the constructor is the one gate into a valid object.

  4. What does the C sort program print?

    Answer

    1 2 3

    qsort orders the three ints and the loop prints them in ascending order.

  5. What does the C rectangle program print?

    Answer

    12

    The area is 3 times 4, and the C++ version prints the same number.

  6. What do both string programs print?

    Answer

    hello world 11

    Both build the same eleven-character string, and the length excludes the terminator.

  7. What does g++ say about assigning a void pointer to an int pointer?

    Answer

    That the conversion from void* is invalid

    C++ requires an explicit cast from void* to another object pointer.

  8. Does the C erase program compile with gcc?

    Answer

    Yes, the implicit conversion is allowed in C

    The C standard permits the conversion, and the program returns the value it points at.

  9. What is the C++ replacement for a char buffer and strcat?

    Answer

    std::string

    The string owns its buffer, grows it and knows its own length.

  10. What does a header shared between C and C++ need?

    Answer

    Only constructs that both languages accept, and often an extern C block

    The two languages agree on a subset, and linkage names have to be said explicitly in the shared header.

The code on this page was built with g++ 15.2.0 and gcc 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev