c and c++ some tips for the c language

  • 2020-05-17 06:02:49
  • OfStack

1. Variable-length array

Strictly speaking, the implementation of variable-length arrays is not a hassle in c++. vector in Stl is itself a variable-length array and has the ability to automatically manage memory.
But in c, implementing variable-length arrays is a bit trickier. With the implementation of C, it is necessary to have a structure, in which there should be a pointer, which allocates a segment of memory space. The space size is determined according to the needs, and there must be another field to record exactly how much and how long the space is opened up.
The general description is as follows:

Struct MutableLenArray
  Int count;
  Char* p;

P = new Char[Count];

No problem, but one of the greatest prides of C speakers is their mastery of efficiency and spatial use. If count=0, they would wonder, then p would be unnecessary. It would take up 4 (8 bits on a 64-bit system) bytes of space, which would be a waste.
Is there a better way to meet these requirements and still keep the space reasonable? The answer is yes, I have length 0

Struct MutableLenArray 
Int count; 
Char p[0]; 

Use method 1 with the above structure, but we can use sizeof to try to read its size and find that only the length of count field is 4 bytes, p has no space allocated. Perfect!

2. The use of macros

1. # and

The "#" symbol converts a symbol directly to a string, such as:

#define TO_STRING(x) #x 
const char *str = TO_STRING( test ); 

The content of str is "test", which means that # will put double quotation marks directly after the symbol.
This feature provides great convenience for the implementation of c++ reflection, which can be referred to in the next article of the blogger, a simple implementation of c++ reflection.

The ## symbol joins two symbols to create a new symbol (lexical level), such as:

#define SIGN( x ) INT_##x 
  int SIGN( 1 ); 

When the macro is expanded, it will be: int INT_1;
You can think of ## as a hyphen, which facilitates the creation of new symbols. Google's Gtest framework makes clever use of hyphens to generate new test cases.

2. Variable and macro

#define LOG( format, ... ) printf( format, __VA_ARGS__ ) 
  LOG( "%s %d", str, count ); 

VA_ARGS is a system predefined macro that is automatically replaced with a parameter list.
You often need to format the output, and you can use these techniques when redefining it.

3. prescan for macro parameters

Definition of prescan: when a macro parameter is put into the macro body, the macro parameter is first expanded in its entirety (with exceptions, see below). When the expanded macro parameters are put into the macro body, the preprocessor scans the newly expanded macro body for the second time and continues to expand. Such as:

#define PARAM( x ) x 
  #define ADDPARAM( x ) INT_##x 
  PARAM( ADDPARAM( 1 ) ); 

Since ADDPARAM(1) is a macro parameter for PARAM, expand ADDPARAM(1) to INT_1, then put INT_1 into PARAM.
The exception is that if the PARAM macro USES # or ## for the macro parameter, the macro parameter will not be expanded:

#define PARAM( x ) #x 
  #define ADDPARAM( x ) INT_##x 

PARAM(ADDPARAM(1)); Will be expanded as "ADDPARAM(1)".

So to get the result of "INT_1", you must add an intermediate macro:

#define PARAM(x) PARAM1(x)
#define PARAM1( x ) #x 

PARAM(ADDPARAM(1)); The result will be "INT_1". According to prescan principle, when ADDPARAM(1) is passed in, it will expand to get INT_1, then put INT_1 into PARAM1 macro, and finally get the result of "INT_1".

4. The interface macros

The following section, excerpted from an online blog, is for disclaimers only.
One of the goals of C++ is to separate the declaration and definition of a class, which is extremely beneficial for the development of a project -- it allows developers to know what a class does without having to see its implementation. However, the way C++ implements the separation of class declarations from class definitions results in some extra work -- each non-inline function representation needs to be written twice, once in the class declaration and once in the class definition.
The code is as follows:

// .h File 
class Element 
void Tick (); 

// .cpp File 
void Element ::Tick () 
// todo 

Since the Tick logo appears in both places, we need to change both places if we need to change the method's arguments (change the function name, return type, or add const).
Of course, this is usually a small amount of work, but in some cases this feature can cause a lot of trouble.
For example, if we have a base class called BaseClass, we have three subclasses inherited from BaseClass -- D1, D2, and D3. BaseClass declares a virtual function Foo() and has a default implementation, and D1, D2, and D3 override the Foo() function.
Now, let's say we added a parameter to BaseClass::Foo(), but we forgot to modify D3 accordingly. Here's the rub -- the compiler will pass, and it will put BaseClass::Foo(...) And D3::Foo() as two completely different functions. This can be problematic when we want to call Foo of D3 via the virtual function mechanism.
In UE4, there are thousands of classes inherited from AActor class alone. If we need to make one change to AActor class, then if we use the traditional method, we need to modify for thousands of derived classes, and one of the derived classes in 10000 is not modified, the compiler will not report an error!
Ideally, then, we would want the representation of a function to exist in only one place, if only declare BaseClass::Foo() once, and then not have to declare Foo in its derived class.
Moreover, in terms of efficiency, when using inheritance in C++, we often use many shallow class inheritance relationships. One parent class often has 1 heap subclass. Many times we just need to integrate many unrelated functions into a single class inheritance family.
For shallow inheritance, we just declare the starting parent class as an interface -- that is, it declares some (mostly pure) virtual functions. In most cases, we will have one base class and the rest of the derived classes in the class family.
If our base class has 10 functions, and we derive 20 classes from this base class, then we need to make an additional 200 function declarations. But the purpose of these declarations is often only for the methods in the Implement base class, which makes header files more or less difficult to maintain.
Implementation of traditional methods
If we have a class of Animal, which is considered a base class, we want to derive different subclasses from this base class. There are three pure demand functions in Animal, as follows:

class Animal 
virtual std :: string GetName () const = 0 ; 
virtual Vector3f GetPosition () const = 0; 
virtual Vector3f GetVelocity () const = 0; 

At the same time, this base class has three derived classes -- Monkey, Tiger, Lion.
Then each of our three methods will exist in seven places: once in Animal, once in the declaration and definition of Monkey, Lion, and Tiget.
Then let's say we make a small change -- we want to change the return type of GetPosition and GetVelocity to Vector4f to accommodate the Transform transformation, so we have to change it in seven places: Animal's.h file, Lion's, Tiger's, Monkey's.h file and cpp's.cpp file.
Implementation using macros
One nice way to do this is to wrap these methods in the form of so-called interface macros. We can try:

Struct MutableLenArray 
Int count; 
Char p[0]; 

It is worth mentioning that the ## symbol represents the join, and the \ symbol represents the join of the next line.
With these macros, we can greatly simplify the declaration of Animal and all the classes derived from it:

Struct MutableLenArray 
Int count; 
Char p[0]; 

Related articles: