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.
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 the second declaration is reached, 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.

It is also 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.

Additionally, recursive functions cannot be declared as inline functions. Ultimately, inline is merely a suggestion to the compiler, and whether it succeeds is not guaranteed. Furthermore, 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, please note 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. It can be seen that the compiler prioritizes searching 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 my other blog “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++.

Const in C: Once modified, it cannot be an lvalue, can be uninitialized, but cannot be reinitialized afterward. It cannot be used as an array subscript and 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.

Const in C++: True constants. Must be initialized at the time of definition and can be used as array subscripts. 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;
At this point, 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 creating 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: Const in C is called a read-only variable, which is just a variable that cannot be an lvalue; const in C++ is a true constant, but it may also degenerate into a C language constant, generating local symbols by default.

References

When we talk about references, we immediately think of their counterpart: pointers. Related articles: C Language Interview – Usage Scenarios of Pointers and References? References are fundamentally the same as pointers at a low level, but their characteristics in the compiler are completely different.

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 taken and 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 a’s value is also the same, which is also dereferencing. However, the way we call them 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 when used directly. The pointer p, when used directly, refers to its own address. This also helps us understand that the memory allocated for the reference is not accessible. If we use it directly, it will dereference immediately. 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 address corresponding to that object, just like opening a package, which reveals 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 *: */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 type of the entire array.

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 *:

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 taking an address. We all know that an immediate value has no address, that is:

int &b = 10;

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

const int &b = 10;

This way, the immediate value is modified with const, which will generate a temporary variable to store this data, thus providing an address to reference.

Summary: A reference is fundamentally a pointer, and when used, it will dereference directly. It can be combined with const to reference an immediate value.

malloc, free & new, delete

This topic is very interesting and requires careful 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).

Corresponding to point 4, if a complex type is used, it first destructs and then calls operator delete to reclaim memory.

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.

Namespace refers to the namespace; defining a namespace creates a new scope. Accessing it requires the following format, taking std as an example:

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

For example, if we have a namespace called Myname with a variable called data, and 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 can be tedious; we can simply use using namespace Myname; to import all symbols. Therefore, we often see the following code:

using namespace std;

资料与群

Leave a Comment