Differences Between C and C++

Although C and C++ are often mentioned together, they are certainly not the same thing.The C language we commonly use today is based on the C89 standard, while C++ is based on the C++99 standard.C89 was established in 1989, and the latest standards are C11 and C++11.Depending on the different standards, their functionalities also vary, but newer versions tend to have less compiler support. Therefore, this article will discuss the C language based on the C89 standard and C++ based on the C++99 standard.

This article will introduce the differences between C and C++, and related articles are recommended:Imagine C and C++ as two kitchen knives.

Default Function Arguments

In C++, when defining or declaring a function, we can sometimes assign a default value to a parameter, which serves as the default value when no argument is passed. For example:

int FUN(int a = 10);

This means that if no argument is passed during the call, a will automatically be assigned an initial value of 10. However, this operation is not valid in C89; writing it this way in C will result in an error. We know that when the system calls any function, a function stack frame is created, and if the function has parameters, the actual parameters need to be pushed onto the stack. Normally, when we manually provide actual parameters, they are passed to registers in order from right to left according to the parameter list, and then pushed onto the stack using the push instruction. Now that we have provided default values for the function parameters, we only need to push the initial value when passing actual parameters, which is more efficient.

Additionally, it is important to note that initial values must be assigned starting from the right side of the parameter list; assigning from the left side will result in an error:

int sum1(int a = 10, int b);        // Error
int sum2(int a, int b = 20);        // Correct
Because if the declaration of sum1 were correct, how would we call it?
sum1( ,20) //? Unfortunately, this is a syntax error.
Since this way of calling is incorrect, it is naturally not allowed to assign initial values this way.
Conversely, let's look at the call for sum2:
sum2(20); // This makes sense, no problem at all.
In practice, when writing projects, we usually declare functions in header files rather than in the current file, and then define them in different files. Can initial values be assigned in this case?
Of course, as long as you follow the right-to-left assignment rule, it can be done.
You can even assign initial values like this:
int fun(int a, int b = 10);
int fun(int a = 20, int b);
Sharp-eyed readers may shout that the second line of code is incorrect because it assigns a value to the left side first!

In fact, this declaration is perfectly fine; both declarations refer to the same function (multiple declarations of a function are allowed). The first declaration has already assigned an initial value to b, and when it reaches the second declaration, it is equivalent to:

int fun(int a = 20, int b = 10);

However, note that the order of these two declarations cannot be reversed; otherwise, it will be incorrect.

Summary: The C89 standard does not support default function arguments, while C++ does support them, and initial values must be assigned from right to left.

Inline Functions

When it comes to inline functions, everyone should be familiar with them; they are another type of function that is not available in C under the C89 standard. Their implementation is very similar to macros, as they expand the code directly at the call site. However, macros are expanded during the pre-compilation phase, while inline functions are processed during the compilation phase. At the same time, macros do not perform type checking as they are pre-processed, whereas inline functions do perform type checking, making them “safer macros”.

The differences between inline functions and regular functions are as follows: inline functions do not create stack frames for backtracking. Generally, we write inline functions directly in header files, and after including them, they can be used. Since the code is expanded directly at the call site, we do not need to worry about redefinition issues—no symbols are generated, so there cannot be any redefinition. Regular functions generate symbols, while inline functions do not.

Additionally, it is important to note that we typically use inline functions to replace very small function bodies (1-5 lines of code). In such cases, the stack overhead of the function is relatively large compared to the size of the function body, and using inline functions can greatly improve efficiency. Conversely, if a function requires a lot of code to implement, it is not suitable to use inline functions. First, the stack overhead of the function call is negligible compared to the function body. Second, expanding a large amount of code directly can cause significant debugging difficulties. Third, if the code body reaches a certain threshold, the compiler will convert it into a regular function.

Moreover, recursive functions cannot be declared as inline functions. Ultimately, inline is merely a suggestion to the compiler, and whether it succeeds is not guaranteed. Additionally, we usually generate debug versions, and inline does not take effect in this version. It only takes effect when generating the release version.

Summary: C89 does not have inline functions, which expand directly at the call site, do not generate symbols, do not create stack frames for backtracking, and only take effect in release versions. They are generally written in header files.

Function Overloading

The rule for generating function symbols in C is based on the name, which means that C does not have the concept of function overloading. In C++, function symbols are generated based on the function name, number of parameters, and parameter types. It is important to note that the return value of a function cannot be used as a basis for function overloading; that is, int sum and double sum cannot constitute an overload!

Function overloading is also a form of polymorphism, known as static polymorphism.

  • Static Polymorphism: Function Overloading, Function Templates

  • Dynamic Polymorphism (Runtime Polymorphism): Polymorphism in Inheritance (Virtual Functions)

When using overloading, it is important to pay attention to scope issues. Please see the following code:

Differences Between C and C++

I defined two functions in the global scope, and they can be overloaded due to different parameter types.
At this point, the main function can correctly call each respective function.

However, consider the commented-out line of code in the main function. If I uncomment it, a warning will be raised: converting double type to int type may lose data. This means that the compiler called the parameter type int’s compare function for both of the following calls. Thus, it can be seen that the compiler prioritizes searching for functions in the local scope when calling functions; if it finds a match, it will call that function according to its standard. If it does not find a match, it will then search in the global scope.

Summary: C does not have function overloading, while C++ determines overloading based on function name, number of parameters, and parameter types, which is static polymorphism. Overloading must occur within the same scope.

const

This section is very important. In another blog of mine, “32 Keywords in C Language,” I also discussed const in C. It mentioned an issue: variables modified by const in C are not constants; they are called constant variables or read-only variables, which cannot be used as array subscripts. However, in C++, variables modified by const can be used as array subscripts, becoming true constants. This is an extension of const in C++.

In C, const: once modified, cannot be an lvalue, can be uninitialized, but cannot be reinitialized afterward. It cannot be used as an array subscript, but can be modified through pointers. In simple terms, the only difference from ordinary variables is that it cannot be an lvalue; everything else is the same.

In C++, const: is a true constant. It must be initialized at the time of definition and can be used as an array subscript. The compilation rule for const in C++ is replacement (similar to macros), so it is considered a true constant. It can also be modified through pointers. It is important to note that C++ pointers may degenerate into C language pointers. For example:

int b = 20;
const int a = b;
In this case, a is just an ordinary C language const variable and can no longer be used as an array subscript. (It references a value that is uncertain at compile time)

const generates local symbols when generating symbols. That is, it is only visible within the current file. If you want to use it in other files, declare it at the top of the file: extern const int data = 10; this way, the generated symbol will be a global symbol.

Summary: In C, const is called a read-only variable, which is just a variable that cannot be an lvalue; in C++, const is a true constant, but it can also degenerate into a C language constant, and it generates local symbols by default.

References

When we talk about references, our first reaction is to think of their sibling: pointers. Related articles: C Language Interview – Usage Scenarios of Pointers and References?References, at a low level, are essentially the same as pointers, but their characteristics are completely different in the compiler.

int a = 10;
int &b = a;
int *p = &a;//b = 20;//*p = 20;
First, we define a variable a = 10, then we define a reference b and a pointer p pointing to a. Let's look at the disassembly to see the underlying implementation:

Differences Between C and C++

We can see that the underlying implementation is completely consistent; the address of a is placed into the eax register, and then the value in eax is stored in the memory of reference b/pointer p. Thus, we can say (at a low level) that a reference is essentially a pointer. Understanding the underlying implementation, we return to the compiler. We see that modifying the value of a using pointer p is done with *p = 20; that is, dereferencing and replacing the value. The underlying implementation:

Differences Between C and C++

Now let’s look at modifying the reference:

Differences Between C and C++

We see that the method of modifying the value of a is also the same; it is also dereferencing. However, the way we call it is different: when calling p, we need to dereference it with *p, while b can be used directly. Thus, we deduce that a reference is dereferenced directly when used. The memory allocated for the reference is not accessible. If used directly, it is dereferenced. Even printing &b will output the address of a.

Note: The “*” serves to reference the value of the variable pointed to by the pointer; a reference actually refers to the address of that variable, and “dereferencing” means to open the package corresponding to that address, which is the value of that variable, hence the term “dereferencing”. In other words, dereferencing returns the object corresponding to the memory address.

Here is a small trick for converting pointers to references:

int *p = &a/* We move the reference symbol to the left and replace * with : */
int &p = a

Next, let’s see how to create a reference to an array:

int array[10] = {0}; // Define an array

We know that using array gives us the address of the first element of the array, which is of type int *. So what does &array mean? It is of type int **, used to point to the address of array[0]? Don’t assume; &array is the entire array type.

To define a reference to an array, let’s first write a pointer to the array:

int (*q)[10] = &array;

Now we replace the & on the right with * on the left:

int (&q)[10] = array;

Testing sizeof(q) = 10. We have successfully created a reference to the array. From the above detailed explanation, we know that a reference is essentially an address. We also know that an immediate value has no address, that is:

int &b = 10;

This code cannot compile. However, if you really want to reference an immediate value, there is a way:

const int &b = 10;

By using const to modify this immediate value, it can be done. Why?

This is because variables modified by const will generate a temporary variable to store this data, which naturally has an address to reference.

Summary: References are essentially pointers, and when used, they are dereferenced directly. They can be combined with const to reference an immediate value.

malloc, free & new, delete

This issue is very interesting and is a key point that needs attention. malloc() and free() are standard library functions in C for dynamically allocating and freeing memory. On the other hand, new and delete are operators and keywords in C++. Under the hood, new and delete still call malloc and free. The differences between them are as follows:

1: malloc and free are functions, while new and delete are operators.

2: malloc requires the size before allocating memory, while new does not.

For example:

int *p1 = (int *)malloc(sizeof(int));
int *p2 = new int;     //int *p3 = new int(10);

When using malloc, the size must be specified, and type conversion is required. With new, the size does not need to be specified because it can infer from the given type, and it can also assign an initial value at the same time.

3: malloc is unsafe and requires manual type conversion, while new does not require type conversion.

See the previous point.

4: free only releases space, while delete first calls the destructor and then releases space (if needed).

5: new first calls the constructor and then allocates space (if needed).

When we call new (for example, int *p2 = new int;), the underlying implementation of this line of code is: first push 4 bytes (the size of int), then call the operator new function to allocate memory. Since this line of code does not involve complex types, such as class types, there is no constructor call. Below is the source code of operator new, which is also an important function for implementing new:

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc){       // try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0){       // report no memory
_THROW_NCEE(_XSTD bad_alloc, );}
return (p);}
We can see that it first calls malloc(size) to allocate memory of the specified byte size.
If it fails (malloc returns 0), it enters the judgment:
If _callnewh(size) also fails, it throws a bad_alloc exception.
_callnewh() checks whether the new handler is available.
If available, it will free some memory and return to malloc to continue the allocation.
If the new handler is not available, it will throw an exception.

6: The handling of insufficient memory (allocation failure) is different.

malloc returns 0 on failure, while new throws a bad_alloc exception.

7: new and malloc allocate memory in different locations.

malloc allocates in the heap, while new allocates in the free store.

8: new can call malloc(), but malloc cannot call new.

new is implemented using malloc(), while malloc cannot call new.

Scope

In C, there are only two scopes: local and global. In C++, there are three: local scope, class scope, and namespace scope.

The so-called namespace is defined as namespace. When we define a namespace, we are defining a new scope. Accessing it requires the following method, taking std as an example:

std::cin << "123" << std::endl;

For example, if we have a namespace called Myname, which contains a variable called data. If we want to use data elsewhere, we need to declare at the top of the file: using Myname::data; this way, data will use the value from Myname. However, declaring each symbol like this can be tedious. We can simply use using namespace Myname; to import all symbols from it. Therefore, we often see code like:

using namespace std;

资料与群

Leave a Comment