Recent from talks
Nothing was collected or created yet.
Compatibility of C and C++
View on WikipediaThe C and C++ programming languages are closely related but have many significant differences. C++ began as a fork of an early, pre-standardized C, and was designed to be mostly source-and-link compatible with C compilers of the time.[1][2] Due to this, development tools for the two languages (such as IDEs and compilers) are often integrated into a single product, with the programmer able to specify C or C++ as their source language.
However, C is not a subset of C++,[3] and nontrivial C programs will not compile as C++ code without modification. Likewise, C++ introduces many features that are not available in C and in practice almost all code written in C++ is not conforming C code. This article, however, focuses on differences that cause conforming C code to be ill-formed C++ code, or to be conforming/well-formed in both languages but to behave differently in C and C++.
Bjarne Stroustrup, the creator of C++, has suggested[4] that the incompatibilities between C and C++ should be reduced as much as possible in order to maximize interoperability between the two languages. Others have argued that since C and C++ are two different languages, compatibility between them is useful but not vital; according to this camp, efforts to reduce incompatibility should not hinder attempts to improve each language in isolation. The official rationale for the 1999 C standard (C99) "endorse[d] the principle of maintaining the largest common subset" between C and C++ "while maintaining a distinction between them and allowing them to evolve separately", and stated that the authors were "content to let C++ be the big and ambitious language."[5]
Several additions of C99 are not supported in the current C++ standard or conflicted with C++ features, such as variable-length arrays, native complex number types and the restrict type qualifier. On the other hand, C99 reduced some other incompatibilities compared with C89 by incorporating C++ features such as // comments and mixed declarations and code.[6]
Constructs valid in C but not in C++
[edit]C++ enforces stricter typing rules (no implicit violations of the static type system[1]), and initialization requirements (compile-time enforcement that in-scope variables do not have initialization subverted)[7] than C, and so some valid C code is invalid in C++. A rationale for these is provided in Annex C.1 of the ISO C++ standard.[8]
- One commonly encountered difference is C being more weakly-typed regarding pointers. Specifically, C allows a
void*pointer to be assigned to any pointer type without a cast, while C++ does not; this idiom appears often in C code usingmallocmemory allocation,[9] or in the passing of context pointers to the POSIX pthreads API, and other frameworks involving callbacks. For example, the following is valid in C but not C++:void* ptr; // Implicit conversion from void* to int* int* i = ptr;
or similarly:
int* j = malloc(5 * sizeof *j); // Implicit conversion from void* to int*
In order to make the code compile as both C and C++, one must use an explicit cast, as follows (with some caveats in both languages):[10]
void* ptr; int* i = (int*)ptr; int* j = (int*)malloc(5 * sizeof *j);
- C++ has more complicated rules about pointer assignments that add qualifiers as it allows the assignment of
int**toconst int* const*but not the unsafe assignment toconst int**while C allows neither of those (although compilers will usually only emit a warning). - C++ changes some C standard library functions to add additional overloaded functions with
consttype qualifiers, e.g.strchrreturnschar*in C, while C++ acts as if there were two overloaded functionsconst char* strchr(const char*)and achar* strchr(char*). In C23 generic selection is used to make C's behaviour more similar to C++'s.[11] - C++ is also more strict in conversions to enums: ints cannot be implicitly converted to enums as in C. Also, prior to C23, enumeration constants (
enumenumerators) were always of typeintin C, whereas they are distinct types in C++ (and C23 or later) and may have a size different from that ofint.[needs update] - In C++ a
constvariable must be initialized; in C this is not necessary. - C++ compilers prohibit goto or switch from crossing an initialization, as in the following C99 code:
void fn(void) { goto flack; int i = 1; flack: printf("Reached flack"); }
- While syntactically valid, a
longjmp()results in undefined behaviour in C++ if the jumped-over stack frames include objects with nontrivial destructors.[12] The C++ implementation is free to define the behaviour such that destructors would be called. However, this would preclude some uses oflongjmp()which would otherwise be valid, such as implementation of threads or coroutines switching between separate call stacks withlongjmp()— when jumping from the lower to the upper call stack in global address space, destructors would be called for every object in the lower call stack. No such issue exists in C. - C allows for multiple tentative definitions of a single global variable in a single translation unit, which is invalid as an ODR violation in C++.
int n; int n = 10;
- In C, declaring a new type with the same name as an existing
struct,unionorenumis valid, but it is invalid in C++, because in C,struct,union, andenumtypes must be indicated as such whenever the type is referenced whereas in C++, all declarations of such types carry the typedef implicitly.enum Bool { FALSE, TRUE }; typedef int Bool; // in C, Bool and enum Bool are distinct
- Non-prototype ("K&R-style") function declarations are invalid in C++; they are still valid in C until C23,[13][14] although they have been deemed obsolescent since C's original standardization in 1990. (The term "obsolescent" is a defined term in the ISO C standard, meaning a feature that "may be considered for withdrawal in future revisions" of the standard.) Similarly, implicit function declarations (using functions that have not been declared) are not allowed in C++, and have been invalid in C since 1999.
- In C until C23,[15] a function declaration without parameters, e.g.
int foo();, implies that the parameters are unspecified. Therefore, it is legal to call such a function with one or more arguments, e.g.foo(42, "hello world"). In contrast, in C++ a function prototype without arguments means that the function takes no arguments, and calling such a function with arguments is ill-formed. In C, the correct way to declare a function that takes no arguments is by using 'void', as inint foo(void);, which is also valid in C++. Empty function prototypes are a deprecated feature in C99 (as they were in C89). - In both C and C++, one can define nested
structtypes, but the scope is interpreted differently: in C++, a nestedstructis defined only within the scope/namespace of the outerstruct, whereas in C the inner struct is also defined outside the outer struct. - C allows
struct,union, andenumtypes to be declared in function prototypes, whereas C++ does not.
C99 and C11 added several additional features to C that have not been incorporated into standard C++ as of C++20, such as complex numbers, variable length arrays (complex numbers and variable length arrays are designated as optional extensions in C11), flexible array members, the restrict keyword, array parameter qualifiers, and compound literals.
- Complex arithmetic using the
float complexanddouble complexprimitive data types was added in the C99 standard, via the_Complexkeyword andcomplexconvenience macro. In C++, complex arithmetic can be performed using the complex number class, but the two methods are not code-compatible. (The standards since C++11 require binary compatibility, however.)[16] - Variable length arrays. This feature leads to possibly non-compile time sizeof operator.[17]
void foo(size_t x, int a[*]); // VLA declaration void foo(size_t x, int a[x]) { printf("%zu\n", sizeof a); // same as sizeof(int*) char s[x * 2]; printf("%zu\n", sizeof s); // will print the value of x * 2 }
- The last member of a C99 structure type with more than one member may be a flexible array member, which takes the syntactic form of an array with unspecified length. This serves a purpose similar to variable-length arrays, but VLAs cannot appear in type definitions, and unlike VLAs, flexible array members have no defined size. ISO C++ has no such feature. Example:
struct X { int m; int n; char bytes[]; };
- The
restricttype qualifier defined in C99 was not included in the C++03 standard, but most mainstream compilers such as the GNU Compiler Collection,[18] Microsoft Visual C++, and Intel C++ Compiler provide similar functionality as an extension. - Array parameter qualifiers in functions are supported in C but not C++.
int foo(int a[const]); // equivalent to int* const a int bar(char s[static 5]); // annotates that s is at least 5 chars long
- The functionality of compound literals in C is generalized to both built-in and user-defined types by the list initialization syntax of C++11, although with some syntactic and semantic differences.
struct X a = (struct X){4, 6}; // The equivalent in C++ would be X{4, 6}. The C syntactic form used in C99 is supported as an extension in the GCC and Clang C++ compilers. foo(&(struct X){4, 6}); // The object is allocated in the stack and its address can be passed to a function. This is not supported in C++. if (memcmp(d, (int[]){8, 6, 7, 5, 3, 0, 9}, n) == 0) { // ... } // The equivalent in C++ would be: using Digits = int[]; if (std::memcmp(d, Digits{8, 6, 7, 5, 3, 0, 9}, n) == 0) { // ... }
- Designated initializers for arrays are valid only in C:
char s[20] = { [0] = 'a', [8] = 'g' }; // allowed in C, not in C++
- Functions that do not return can be annotated using a
[[noreturn]]attribute in C++ whereas C uses a distinct keyword,_Noreturn. In C23, the attribute syntax is adopted while_Noreturnis deprecated.[19] - C2Y introduces named loops and named break statements (similar to Java). This feature has not been added to C++ as of C++26.[20]
- C uses a
_Genericselection to allow for compile-time deciding of expressions, which can be seen as emulating function overloading. C++ has no need for this as C++ supports function overloading and template specialization. - C23 introduces the
typeofoperator which produces a type of either a type or an expression. In C++, the equivalent feature isdecltype.
C++ adds numerous additional keywords to support its new features. This renders C code using those keywords for identifiers invalid in C++. For example, the following snippet is valid C code, but is rejected by a C++ compiler, since the keywords template, new and class are reserved.
struct template {
int new;
struct template* class;
};
Constructs that behave differently in C and C++
[edit]There are a few syntactic constructs that are valid in both C and C++ but produce different results in the two languages.
- Character literals such as
'a'are of typeintin C and of typecharin C++, which means thatsizeof 'a'will generally give different results in the two languages: in C++, it will be1, while in C it will besizeof(int). As another consequence of this type difference, in C,'a'will always be a signed expression, regardless of whether or notcharis a signed or unsigned type, whereas for C++ this is compiler implementation specific. - C++ assigns internal linkage to namespace-scoped
constvariables unless they are explicitly declaredextern, unlike C in whichexternis the default for all file-scoped entities. In practice this does not lead to silent semantic changes between identical C and C++ code but instead will lead to a compile-time or linkage error. - In C, use of inline functions requires manually adding a prototype declaration of the function using the extern keyword in exactly one translation unit to ensure a non-inlined version is linked in, whereas C++ handles this automatically. In more detail, C distinguishes two kinds of definitions of
inlinefunctions: ordinary external definitions (whereexternis explicitly used) and inline definitions. C++, on the other hand, provides only inline definitions for inline functions. In C, an inline definition is similar to an internal (i.e. static) one, in that it can coexist in the same program with one external definition and any number of internal and inline definitions of the same function in other translation units, all of which can differ. This is a separate consideration from the linkage of the function, but not an independent one. C compilers are afforded the discretion to choose between using inline and external definitions of the same function when both are visible. C++, however, requires that if a function with external linkage is declaredinlinein any translation unit then it must be so declared (and therefore also defined) in every translation unit where it is used, and that all the definitions of that function be identical, following the ODR. Static inline functions behave identically in C and C++. - Both C (since C99) and C++ have a Boolean type
boolwith constantstrueandfalse, but they are defined differently.- In C++,
boolis a built-in type and a reserved keyword. - In C99, a new keyword,
_Bool, is introduced as a new built-in Boolean type. The headerstdbool.hprovides macrosbool,trueandfalsethat are defined as_Bool,1and0, respectively. Therefore,trueandfalsehave the typeint. - In C23 however,
bool(and its valuestrueandfalse) are keywords.
- In C++,
- C++ has the types
char8_t,char16_tandchar32_tto encode a single UTF code unit. C23 includes these, but as typedefs to other integer types rather than distinct built-in types, such that their names are not reserved keywords. - In C it is implementation-defined whether a bit field of type
intis signed or unsigned while in C++ it is always signed to match the underlying type.
Several of the other differences from the previous section can also be exploited to create code that compiles in both languages but behaves differently. For example, the following function will return different values in C and C++:
extern int T;
int size(void) {
struct T {
int i;
int j;
};
return sizeof(T);
// C: return sizeof(int)
// C++: return sizeof(struct T)
}
This is due to C requiring struct in front of structure tags (and so sizeof(T) refers to the variable), but C++ allowing it to be omitted (and so sizeof(T) refers to the implicit typedef). Beware that the outcome is different when the extern declaration is placed inside the function: then the presence of an identifier with same name in the function scope inhibits the implicit typedef to take effect for C++, and the outcome for C and C++ would be the same. Observe also that the ambiguity in the example above is due to the use of the parenthesis with the sizeof operator. Using sizeof T would expect T to be an expression and not a type, and thus the example would not compile with C++.
Linking C and C++ code
[edit]While C and C++ maintain a large degree of source compatibility, the object files their respective compilers produce can have important differences that manifest themselves when intermixing C and C++ code. Notably:
- C compilers do not name mangle symbols in the way that C++ compilers do.[21]
- Depending on the compiler and architecture, it also may be the case that calling conventions differ between the two languages.
For these reasons, for C++ code to call a C function foo(), the C++ code must prototype foo() with extern "C". Likewise, for C code to call a C++ function bar(), the C++ code for bar() must be declared with extern "C".
A common practice for header files to maintain both C and C++ compatibility is to make its declaration be extern "C" for the scope of the header:[22]
In foo.h
// If this is a C++ compiler, use C linkage
#ifdef __cplusplus
extern "C" {
#endif
// These functions get C linkage
void foo();
struct Bar {
// implementation here
};
// If this is a C++ compiler, end C linkage
#ifdef __cplusplus
}
#endif
Differences between C and C++ linkage and calling conventions can also have subtle implications for code that uses function pointers. Some compilers will produce non-working code if a function pointer declared extern "C" points to a C++ function that is not declared extern "C".[23]
For example, the following code:
void myFunction();
extern "C" void foo(void (*fnPtr)(void));
void bar() {
foo(myFunction);
}
Using Sun Microsystems' C++ compiler, this produces the following warning:
$ CC -c test.cc
"test.cc", line 6: Warning (Anachronism): Formal argument fnPtr of type
extern "C" void(*)() in call to foo(extern "C" void(*)()) is being passed
void(*)().
This is because myFunction() is not declared with C linkage and calling conventions, but is being passed to the C function foo().
See also
[edit]References
[edit]- ^ a b Stroustrup, Bjarne. "An Overview of the C++ Programming Language in The Handbook of Object Technology (Editor: Saba Zamir). CRC Press LLC, Boca Raton. 1999. ISBN 0-8493-3135-8" (PDF). p. 4. Archived (PDF) from the original on 16 August 2012. Retrieved 12 August 2009.
- ^ B.Stroustrup. "C and C++: Siblings. The C/C++ Users Journal. July 2002" (PDF). Retrieved 17 March 2019.
- ^ "Bjarne Stroustrup's FAQ – Is C a subset of C++?". Retrieved 22 September 2019.
- ^ B. Stroustrup. "C and C++: A Case for Compatibility. The C/C++ Users Journal. August 2002" (PDF). Archived (PDF) from the original on 22 July 2012. Retrieved 18 August 2013.
- ^ Rationale for International Standard—Programming Languages—C Archived 6 June 2016 at the Wayback Machine, revision 5.10 (April 2003).
- ^ "C Dialect Options - Using the GNU Compiler Collection (GCC)". gnu.org. Archived from the original on 26 March 2014.
- ^ "N4659: Working Draft, Standard for Programming Language C++" (PDF). §Annex C.1. Archived (PDF) from the original on 7 December 2017. ("It is invalid to jump past a declaration with explicit or implicit initializer (except across entire block not entered). … With this simple compile-time rule, C++ assures that if an initialized variable is in scope, then it has assuredly been initialized.")
- ^ "N4659: Working Draft, Standard for Programming Language C++" (PDF). §Annex C.1. Archived (PDF) from the original on 7 December 2017.
- ^ "IBM Knowledge Center". ibm.com.
- ^ "FAQ > Casting malloc - Cprogramming.com". faq.cprogramming.com. Archived from the original on 5 April 2007.
- ^ "Qualifier-preserving standard library functions, v4" (PDF).
- ^ "longjmp - C++ Reference". www.cplusplus.com. Archived from the original on 19 May 2018.
- ^ "WG14 N2432 : Remove support for function definitions with identifier lists" (PDF).
- ^ "2011 ISO C draft standard" (PDF).
- ^ "WG14 N 2841: No function declarators without prototypes".
- ^ "std::complex - cppreference.com". en.cppreference.com. Archived from the original on 15 July 2017.
- ^ "Incompatibilities Between ISO C and ISO C++". Archived from the original on 9 April 2006.
- ^ Restricted Pointers Archived 6 August 2016 at the Wayback Machine from Using the GNU Compiler Collection (GCC)
- ^ "WG14-N2764 : The noreturn attribute" (PDF). open-std.org. 21 June 2021. Archived (PDF) from the original on 25 December 2022.
- ^ "Information technology — Programming languages — C" (PDF). open-std.org. 4 May 2025.
- ^ "IBM Knowledge Center". ibm.com.
- ^ "IBM Knowledge Center". ibm.com.
- ^ "Oracle Documentation". Docs.sun.com. Archived from the original on 3 April 2009. Retrieved 18 August 2013.
External links
[edit]- Detailed comparison, sentence by sentence, from a C89 Standard perspective.
- Incompatibilities Between ISO C and ISO C++, David R. Tribble (August 2001).
- Oracle (Sun Microsystems) C++ Migration Guide, section 3.11, Oracle/Sun compiler docs on linkage scope.
- Oracle: Mixing C and C++ Code in the Same Program, overview by Steve Clamage (ANSI C++ Committee chair).
Compatibility of C and C++
View on GrokipediaOverview
Historical Context
The C programming language was developed by Dennis Ritchie at Bell Laboratories between 1969 and 1973, evolving in parallel with the early stages of the Unix operating system; the most innovative phase occurred in 1972, when Ritchie introduced key features such as a structured type system and the close relationship between arrays and pointers.[6] Derived from the typeless BCPL language via Ken Thompson's B, C was created as a system implementation language to support Unix development, providing a balance of low-level control and higher-level abstractions suitable for operating system programming.[6][7] C played a pivotal role in Unix's evolution, as Ritchie and Thompson rewrote the Unix kernel in C by 1973, enabling greater portability and facilitating the system's spread across diverse hardware platforms.[8] This rewrite transformed Unix from an assembly-language-based system into one implementable in a high-level language, contributing to its widespread adoption; by 1978, over 600 Unix installations existed.[8] The language's standardization began with the ANSI X3.159-1989 ratification on December 14, 1989, published in spring 1990 and adopted internationally as ISO/IEC 9899:1990, which unified variations in C implementations and established a common baseline.[9] In 1979, Bjarne Stroustrup at Bell Labs initiated the project that became C++, originally termed "C with Classes," to extend C with Simula-inspired object-oriented features like classes, inheritance, and constructors while preserving C's efficiency and flexibility for systems programming.[10] Renamed C++ in 1983—reflecting its incremental evolution from C—the language reached its first commercial release in 1985, with the initial reference manual published that year.[10] A core design principle was maximizing backward compatibility with C, encapsulated in the guideline "as close to C as possible—but no closer," ensuring that most existing C code could compile under a C++ compiler with minimal modifications to support type safety and abstraction without gratuitous incompatibilities.[10][2] C++'s standardization culminated in 1998 with the publication of ISO/IEC 14882:1998, the first international standard, which formalized the language's features including templates and the Standard Template Library while aligning closely with the contemporaneous ISO C standard to facilitate interoperability. This milestone followed years of committee work starting in the early 1990s, building on Stroustrup's foundational efforts to evolve C into a multiparadigm language without severing ties to its C heritage.[10]Design Philosophy and Compatibility Goals
The design of C++ was fundamentally shaped by Bjarne Stroustrup's vision to extend C with object-oriented and generic programming features while preserving its core strengths, positioning C++ not as a strict superset of C but as a language sharing a substantial common subset to facilitate seamless integration of existing C codebases.[2] This philosophy emphasized evolving C into a "better C" by adding high-level abstractions such as classes and templates, with the explicit goal that the vast majority of well-written C programs could compile and run unchanged under a C++ compiler, minimizing disruption for developers already invested in C.[11] Stroustrup articulated this in his seminal work, highlighting the intent to build upon C's established ecosystem without requiring wholesale rewrites.[12] Central to these compatibility goals were the retention of C's renowned efficiency and portability, which C++ sought to maintain through zero-overhead abstractions that do not introduce runtime costs unless explicitly invoked, such as virtual functions or exceptions.[2] By design, C++ allows low-level programming styles identical to C, including direct memory manipulation and inline assembly, ensuring that performance-critical code remains viable across both languages while enhancing portability via shared tools, libraries, and compiler infrastructures.[13] This approach enabled C++ to leverage the vast body of portable C software, fostering a unified community where code and expertise could be reused, as Stroustrup noted in discussions on maximizing mutual benefits between the languages.[2] However, achieving these goals involved deliberate trade-offs, particularly in adopting stricter type checking to catch common errors and promote safer code, even if it rendered certain lax C constructs invalid in C++.[2] For instance, C++ mandates explicit function prototypes and disallows implicit conversions that C permits, prioritizing compile-time error detection over absolute backward compatibility for edge cases that could lead to subtle bugs.[11] Additionally, C++ discourages overreliance on the C preprocessor—such as extensive macro use for constants or inline functions—by providing type-safe alternatives like theconst keyword, inline functions, and templates, which reduce portability issues and maintenance burdens while still supporting the preprocessor for interoperability needs.[2] These decisions reflect a balanced philosophy: enhancing C's expressiveness without sacrificing its efficiency, though at the cost of requiring minor adjustments for the remaining fraction of C code that relies on deprecated or ambiguous features.[13]
Syntactic Compatibility
Constructs Valid in C but Invalid in C++
C++ imposes stricter syntactic rules than C in several areas, leading to certain constructs that compile successfully in C but fail in C++. These differences stem from C++'s emphasis on type safety and scope management, requiring explicit declarations and qualifications that C permits implicitly or through looser rules. This section examines key examples, including handling of void expressions, implicit function declarations, and bit-field types. Void expressions in C allow greater flexibility in contexts where no value is expected, such as function calls that return void. For instance, a function declared asvoid f(void); can be invoked in an expression where the return value is discarded, and C compilers typically accept such usage without error if the expression is not used for a value-requiring operation. However, attempting to assign the result of a void function to a variable, like int x = f();, is a constraint violation in standard C but may compile with warnings in some implementations. In C++, such assignments are strictly invalid, producing a compilation error like "invalid use of void expression," because void is an incomplete type with no valid values, and expressions of type void cannot be used where a scalar or object type is required. This stricter enforcement ensures type safety but breaks C code that relies on lenient handling, such as legacy code discarding void returns in assignments or conditional contexts without explicit casting to void. For compatibility, C code using void functions in value contexts must be refactored, often by separating the call or using the comma operator to sequence it with a valid expression, as per the C++ standard's rules on expression value categories.[1]
Implicit function declarations represent a significant compatibility hurdle, as C historically permitted calling functions without prior declarations, assuming an implicit return type of int and unspecified parameters. Under the C89 standard, code like the following compiles:
void call_foo() {
int result = foo(42); // foo undeclared, implicit int foo(...);
}
int foo(int x) { return x; }
void call_foo() {
int result = foo(42); // foo undeclared, implicit int foo(...);
}
int foo(int x) { return x; }
int types with implementation-defined signedness, whereas C++ mandates more explicit type specifications to ensure predictable behavior. In C, a bit-field like struct { int flag : 1; }; is valid, but whether flag is treated as signed or unsigned is implementation-defined, potentially leading to varying promotion rules in expressions (ISO/IEC 9899:2011, section 6.7.2.1). Compilers may choose unsigned for efficiency, affecting arithmetic operations where the bit-field promotes to int or unsigned int. In C++, bit-fields require an integral type, and plain int is interpreted as signed int, making unsigned behavior explicit only with unsigned int flag : 1;; using plain int without qualifiers compiles but assumes signedness, and attempts to rely on implementation-specific unsigned treatment fail under strict conformance. Moreover, C++ disallows bit-fields wider than the underlying type's capacity in value terms, though padding bits are permitted (ISO/IEC 14882:2020, section 11.4). Legacy C code using plain int bit-fields for unsigned semantics must be updated with qualifiers to compile reliably in C++, avoiding subtle bugs from differing signedness assumptions during integral promotions.[1]
Constructs Valid in C++ but Invalid in C
C++ introduces reference types as a safer alternative to pointers for aliasing objects, a feature absent in C, which relies solely on pointers for indirect access. A reference is declared using the& symbol and must be initialized to refer to an existing object, after which it cannot be reseated to another object or be null. For example:
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20; // Modifies x to 20
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20; // Modifies x to 20
class Point {
private:
int x, y;
public:
Point(int a, int b) : x(a), y(b) {} // Constructor
int getX() const { return x; } // Member function
};
class Point {
private:
int x, y;
public:
Point(int a, int b) : x(a), y(b) {} // Constructor
int getX() const { return x; } // Member function
};
template keyword followed by parameters in angle brackets. For instance:
template<typename T>
T add(T a, T b) {
return a + b;
}
// Usage: int result = add(3, 4); // Instantiates add<int>
template<typename T>
T add(T a, T b) {
return a + b;
}
// Usage: int result = add(3, 4); // Instantiates add<int>
try, catch, and throw keywords, which C lacks, forcing reliance on error codes or global variables for error management. Exceptions allow functions to signal errors by throwing objects, which are caught in matching handlers. An example is:
try {
if (some_condition) {
throw std::runtime_error("Error occurred");
}
} catch (const std::exception& e) {
// Handle the exception
std::cerr << e.what() << std::endl;
}
try {
if (some_condition) {
throw std::runtime_error("Error occurred");
}
} catch (const std::exception& e) {
// Handle the exception
std::cerr << e.what() << std::endl;
}
Semantic and Behavioral Differences
Type System Variations
One key difference in the type systems of C and C++ lies in the handling of qualifiers, particularlyconst correctness. In C++, the const qualifier is deeply integrated into the type system, enforcing immutability at compile time for objects and parameters; for instance, a pointer to a const object, such as const int* p, prohibits modification of the pointed-to value through p, and passing a non-const pointer to a function expecting a const one results in a type mismatch error. This strict enforcement supports features like function overloading based on constness and enables compiler optimizations assuming immutability. In contrast, C treats const more as a hint for optimization rather than a strict type constraint; while modifying a const-qualified object invokes undefined behavior, the language allows casting away constness more permissibly, and compilers may not always diagnose violations as errors, leading to potential runtime issues in mixed-code scenarios.[3][14]
C++ extends the type system with pointer-to-member types, which are essential for object-oriented programming but entirely absent in C. These types, declared as return_type (class_name::*pointer_name)(args), store the address of a non-static class member (function or data) relative to the class instance, requiring an object to invoke, such as obj.*ptr() for member functions. This design accommodates polymorphism and inheritance by adjusting offsets at runtime, but attempting to compile similar constructs in C fails due to the lack of classes and member concepts, rendering C code incompatible when porting to C++ without refactoring.[15][1]
The bool type also diverges significantly between the languages. C++ provides a native bool keyword as a fundamental type, with literal values true (1) and false (0), that does not implicitly convert to integers in most contexts and supports logical operations without promotion; for example, bool b = true; ensures type-safe boolean logic throughout the program. Pre-C99 C lacks any built-in boolean type, relying on integers (non-zero as true, zero as false), which can lead to unintended arithmetic mixing, while C99 introduces _Bool for similar semantics but without C++'s keyword integration or restrictions on implicit conversions, potentially causing portability issues when linking C and C++ code.[1][3]
Regarding enumerations, C leaves the underlying type of an enum implementation-defined, usually the smallest integer type that can hold all enumerators (often int), which may vary across compilers and architectures, complicating binary compatibility. C++ inherits this behavior pre-C++11 but introduces scoped and fixed underlying types in C++11, allowing declarations like enum class Color : uint8_t { Red, Green, Blue }; to specify the exact integral type, ensuring consistent sizing (e.g., 1 byte here) and preventing implicit conversions to integers for better type safety. This enhancement aids interoperability by allowing C++ enums to match C's flexible sizing when needed, though mismatches can still arise without explicit specification.[16][1]
Expression and Operator Evaluation
One key difference in expression evaluation arises from array-to-pointer decay and the behavior of thesizeof operator. In both C and C++, when an array is used in most expressions, it decays to a pointer to its first element, losing size information. However, the sizeof operator, being unevaluated, does not trigger this decay: for an array identifier a of type T[N], sizeof a yields N * sizeof(T) in both languages. This preservation holds in the defining scope or unevaluated contexts, allowing array length computation via (sizeof a) / sizeof(a[0]). Yet, if the array has already decayed—such as when passed as a function parameter, where the parameter is adjusted to a pointer type—sizeof returns the size of the pointer, not the original array, leading to potential portability issues in mixed C/C++ code.[17][18]
void func(int arr[10]) {
printf("%zu\n", sizeof arr); // Prints sizeof(int*), e.g., 8 on 64-bit systems (same in C and C++)
}
int main() {
int a[10];
printf("%zu\n", sizeof a); // Prints 40 (assuming int is 4 bytes) in both C and C++
}
void func(int arr[10]) {
printf("%zu\n", sizeof arr); // Prints sizeof(int*), e.g., 8 on 64-bit systems (same in C and C++)
}
int main() {
int a[10];
printf("%zu\n", sizeof a); // Prints 40 (assuming int is 4 bytes) in both C and C++
}
sizeof as preserving the array type in unevaluated operands, though C's looser type system may allow more implicit decays in extensions, while C++ enforces stricter compile-time checks.[17][18]
Another behavioral variance occurs in conditional expressions involving assignments, such as if (x = y). In C, the condition expects a scalar expression implicitly compared to zero: a non-zero result (the assigned value of y) evaluates to true, with no required conversion. This permits assignments without explicit casting, though it risks common errors like using = instead of ==. In C++, the condition undergoes contextual conversion to bool, where non-zero integers become true and zero false, yielding identical runtime results for integer assignments. However, C++ compilers typically issue warnings for assignments in conditions to prevent mistakes, and since C++11, structured bindings or declarations in conditions (e.g., if (auto p = expr)) introduce scoped variables, enhancing safety but differing from C's statement-only model.[19][20]
int x = 0, y = 5;
if (x = y) { // In C: non-zero true; in C++: converts to true, often warned
// Executes in both
}
int x = 0, y = 5;
if (x = y) { // In C: non-zero true; in C++: converts to true, often warned
// Executes in both
}
volatile qualifier exhibits nuanced differences in access sequencing and optimization guarantees. In C++, volatile ensures that every access to a volatile-qualified object via a glvalue is a visible side effect, prohibiting reordering relative to other sequenced-before or sequenced-after side effects within a single thread; this provides strict intra-thread ordering for hardware interactions like memory-mapped I/O. In C, volatile similarly mandates observable side effects for each access, preventing elimination or reordering relative to sequence points (or sequenced relations in C11), but the guarantees are looser without C++'s formal memory model, allowing more aggressive optimizations across compilers and leading to behavioral variances in optimization-heavy code. Neither language intends volatile for inter-thread synchronization, where atomics are preferred.[21][22]
volatile int *reg = (volatile int *)0x40000000;
*reg = 1; // Guaranteed side effect and sequencing in both
int val = *reg; // Accesses treated as distinct side effects in both standards
volatile int *reg = (volatile int *)0x40000000;
*reg = 1; // Guaranteed side effect and sequencing in both
int val = *reg; // Accesses treated as distinct side effects in both standards
inline specifier permits multiple identical definitions across translation units, allowing the compiler to inline the function wherever used without requiring a single external definition; function-local statics are shared, and the function has the same address everywhere, facilitating whole-program optimization. In C (since C99), inline functions with external linkage require exactly one external definition (often via extern inline in a separate unit), while other instances serve only as inline candidates; without this, linking fails if the function is odr-used. This C++ flexibility enhances modularity in headers but demands identical definitions to avoid ill-formed programs, contrasting C's stricter external linkage model.[23][24]
// header.h
inline int add(int a, int b) { return a + b; } // Definable in multiple TUs, inlined across
// In C, this would need an external def in one .c file for external linkage
// header.h
inline int add(int a, int b) { return a + b; } // Definable in multiple TUs, inlined across
// In C, this would need an external def in one .c file for external linkage
Interoperability Mechanisms
Linking C and C++ Objects
Linking C and C++ object files into a single executable or shared library requires ensuring compatibility between the compiled outputs, including symbol names, calling conventions, and runtime dependencies. The C++ compiler is typically used as the linker driver to automatically include the C++ standard library and handle initialization, even when mixing C objects. This process leverages standard object file formats such as ELF on Unix-like systems and COFF (or PE/COFF on Windows), which support combining code from both languages without format conflicts. However, C++ objects often incorporate additional runtime support for features like dynamic memory management, which must be resolved during linking.[25] A key mechanism for interoperability is the use of compiler flags and declarations to align linkage specifications. In C++ code, functions from C sources must be declared withextern "C" to adopt C linkage, preventing the C++ compiler from applying name mangling to symbols and ensuring they match the unmangled names in C object files. For instance, to call a C function, one might declare it as extern "C" void c_func(int param);, allowing seamless invocation from C++ without linker errors. This declaration can wrap entire header includes for C libraries, such as #include <stdio.h> within an extern "C" { ... } block, to maintain compatibility across files. Failure to use extern "C" often leads to unresolved external symbols during linking, a common pitfall in mixed-language projects.[26][27]
When integrating C libraries, such as the standard C library (libc), into C++ programs, headers must avoid C++-specific constructs like classes or templates to prevent compilation errors or ABI mismatches. C library functions are typically declared with C linkage in system headers, but custom C headers included in C++ should be guarded with extern "C" to ensure proper symbol resolution. Linking proceeds by passing C object files or archives to the C++ linker, which resolves dependencies against both C and C++ runtimes; for example, using g++ with options like -lc explicitly adds libc if not automatically included. Pitfalls here include inadvertent use of C++ features in interface headers, leading to redefinition errors or incompatible types.[25][27]
Toolchain support varies but generally facilitates mixed compilation. GCC and Clang allow compiling C sources with the C++ driver (g++ or clang++), using flags like -x c for Clang to treat files as C, and automatically linking the C++ runtime (libstdc++ or libc++) alongside C libraries. For MSVC, the cl.exe compiler in Visual Studio projects handles both C and C++ files within the same build, producing COFF objects linked by link.exe, though project settings must enable C++ exceptions (/EHsc) or disable RTTI (/GR-) if not needed to avoid bloat or compatibility issues with pure C code. In all cases, using compilers from the same vendor and compatible versions minimizes ABI discrepancies, such as differing exception handling models.[28][29]
Name Mangling and Calling Conventions
Name mangling, also known as name decoration, is a process employed by C++ compilers to encode additional information about functions, such as parameter types and namespaces, into the symbol names used for linking. This ensures uniqueness for overloaded functions and scoped entities, which C does not support. In contrast, C compilers do not perform name mangling, preserving the original identifier names as symbols in the object files.[30][31] For example, under the Itanium ABI commonly used by GCC on Unix-like systems, a function declared asint func(int) is mangled to _Z4funci, where _Z is the mangling prefix, 4 indicates the length of the function name "func", and i encodes the int parameter type. On Windows with MSVC, the same function might be mangled to ?func@@YAHH@Z, incorporating a question mark prefix, type codes for the return value (H for int), parameter (H for int), and a suffix indicating the calling convention. These differences arise because C++ must distinguish between multiple functions sharing the same base name due to overloading, while C forbids such overloading, relying on plain names like func for linkage.[32][30]
Application Binary Interfaces (ABIs) define how functions are called, including argument passing, stack management, and return values. C typically adheres to platform-specific conventions like cdecl (caller cleans the stack) on most Unix systems or stdcall (callee cleans the stack) for Windows API calls. C++ compilers extend these conventions to accommodate features like exceptions and virtual functions but maintain compatibility with C ABIs when functions are declared with extern "C", which disables name mangling and enforces C-style linkage. Without extern "C", C++ functions use mangled names and may incorporate additional ABI elements, such as exception handling tables, potentially causing mismatches when linking with C code.[33][25]
Function overloading in C++ relies on signature-based mangling to resolve calls at link time; attempting to link a C++-mangled symbol to a plain C symbol results in linker errors due to unresolved references. For instance, a C compiler expecting func will fail to match _Z4funci, highlighting the incompatibility without explicit linkage specifications. Platform-specific variations exacerbate this: GCC on Linux follows the Itanium ABI for mangling, while MSVC on Windows uses a distinct scheme, preventing direct binary interoperability between binaries compiled by different compilers even within C++.[34][35]
Standards Evolution
C Standard Features in C++
C++98 was designed to be largely compatible with the C89 standard (ISO/IEC 9899:1990), serving as a near-superset while introducing stricter type checking and other enhancements.[11] This compatibility allows most valid C89 code to compile as C++ with minimal modifications, though certain legacy constructs are not supported. For instance, C++98 prohibits K&R-style function definitions, which lack explicit parameter types and were common in pre-ANSI C but deprecated in C89 itself.[11] Additionally, C++ disallows implicit declarations of functions and variables, requiring explicit type specifications where C89 permitted an implicitint return or type.[11] These exceptions stem from C++'s emphasis on type safety, but they affect only a small subset of C89 programs, ensuring broad interoperability.[11]
Support for C99 features (ISO/IEC 9899:1999) in early C++ standards is partial, with C++98 incorporating some elements as extensions but omitting others due to conflicts with C++ semantics or design goals.[4] For example, C99's variable-length arrays and compound literals are not natively supported in C++98, as they raise concerns over memory management and exception safety; instead, C++ favors dynamic allocation via new or standard containers.[4] The _Complex type qualifier and <complex.h> header from C99 are absent in C++98's core language, though the <complex> header provides a class-based std::complex for similar functionality since C++98. Later, C++11 fully standardized 64-bit integer types like long long, which originated in C99, integrating them into the core language with fixed-width variants in <cstdint>.[36] Variadic macros, another C99 innovation allowing macros with variable arguments, were not standard in C++98 but were fully adopted in C++11, aligning preprocessor behavior more closely with C99.
C11 features (ISO/IEC 9899:2011) see analogous but distinct support in C++, reflecting parallel evolution rather than direct porting. Atomic operations, introduced in C11 via the <stdatomic.h> header and _Atomic qualifier, are handled in C++ through the <atomic> header since C++11, providing thread-safe primitives like std::atomic<int> without requiring C11-specific headers in standard C++ mode. However, some C++ implementations offer <stdatomic.h> as an extension for mixed C/C++ codebases, ensuring binary compatibility where possible.[37] For threading, C11's <threads.h> is not directly available in C++; instead, C++11 introduced the <thread> header with classes like std::thread, offering higher-level abstractions over platform-specific APIs. This approach maintains compatibility for linking but diverges in API design to leverage C++'s object-oriented features.
Alongside adoptions, C++ standards have deprecated certain C-inherited functions for safety reasons. In C++11, std::gets from C's <cstdio>—noted for its vulnerability to buffer overflows—was marked as deprecated, and it was fully removed in C++14, encouraging safer alternatives like std::getline. These changes promote secure coding practices without breaking existing C linkages, as deprecated features remain usable in transitional code.
