[C++] Detailed explanation of essential knowledge for getting started with C++

First of all, we must first know that C++ is based on C, accommodates the idea of ​​​​object-oriented programming, and adds many useful libraries. This chapter will take you to understand that C++ complements the lack of C language grammar, and how C++ optimizes the unreasonable design of C language.

1. Namespace

1. namespace

In C/C++, there are a large number of variables, functions, etc., and the names of these variables, functions, and classes will all exist in the global scope, which may cause many conflicts. The purpose of using namespaces is to localize identifier names to avoid naming conflicts. The namespace keyword appears to address this problem.

For example, if we want to define a variable sqrt, it can be directly defined in a global variable and then compiled, as shown in the figure below:

Insert image description here
However, we know that sqrt is actually a library function, which is included in the header file of math.h. If we add the header file of math.h, can it still be compiled? The answer is no, because they have the same name. If the header file of math.h is included, the compilation will not pass, and the error in the figure below will be reported:
Insert image description here

So is there a good solution? The answer is yes. C++ has added keywords such as namespace to solve such problems. For example, we can put the variables we need to define into the namespace of namespace, and then use it to let the compiler search in the specified namespace; if the compiler is not specified, the compiler will first search for variables in the global domain; namespace use:

		#include <stdio.h>
		#include <math.h>
		
		// 命名空间的名字
		namespace Young
		{
			int sqrt = 10;
		}
		
		int main()
		{
			printf("%d\n", Young::sqrt);
			return 0;
		}

The use of the above code is to let the compiler find the variable sqrt in the specified namespace Young and then use this variable, so that there will be no naming conflict with the sqrt function in the library function; Young is a namespace that can be named by itself The name can be any name, not necessarily Young.

In printf("%d\n", Young::sqrt);the image, the :: symbol in front of sqrt is called a scope qualifier , which means that the compiler uses things defined in the namespace before the scope qualifier.

2. Usage scenarios of namespace

In addition to the above we use namespace to define variables in the namespace, we can also define functions, structures, etc.; in addition, they can also be nested. For example the following code:

		namespace Young
		{
			//变量
			int sqrt = 10;
		
			// 函数
			int Add(int a, int b)
			{
				return a + b;
			}
		
			// 结构体
			struct ListNode
			{
				int data;
				struct ListNode* next;
			};
		
			// 嵌套使用 
			namespace Y
			{
				int a = 10;
			}
		
		}
		
		int main()
		{
			int ret = Young::Add(1, 2);
			printf("%d\n", ret);
		
			struct Young::ListNode node;
		
			printf("%d\n", Young::Y::a);
		
			return 0;
		}

In the main function part of the above code, the domain qualifier in the structure should be used before ListNode, not before struct; when using nested namespace, look from right to left and search in the specified namespace;

Although this method can effectively avoid naming conflict problems, is it troublesome to add a domain qualifier in front of it every time it is used? It is indeed true, but there is another way to solve it, which is to expand the namespace ; take the above namespace as an example, such as the following code:

		// 将命名空间展开
		using namespace Young;
		using namespace Y;
		
		int main()
		{
			int ret = Add(1, 2);
			printf("%d\n", ret);
		
			struct ListNode node;
		
			printf("%d\n",a);
		
			return 0;
		}

The above code expands the content in the Young and Y namespaces, so there is no need to use domain qualifiers; in addition, we can also expand the content in some namespaces, for example, I only expand the Add function come out:

		// 展开部分
		using Young::Add;
		
		int main()
		{
			int ret = Add(1, 2);
			printf("%d\n", ret);
		
			struct Young::ListNode node;
		
			printf("%d\n", Young::Y::a);
		
			return 0;
		}

The above is the namespace of the expanded part. Usually when working on projects, we will not expand the namespace, because expansion will become unsafe; but when we usually write code exercises, we can expand the namespace. It is more conducive to our practice.

2. Understand input and output in C++

First of all, we need to know that C++ introduces input and output different from C language. In C language, we use scanf and printf as input and output, but in C++, cout standard output object (console) and cin standard input are added. Object (keyboard); let's look at their use first:

Insert image description here

We can learn that cout and cin in the above code are called stream insertion operator and stream extraction operator respectively . We will introduce more about these two in future studies; cout and cin must include the <iostream> header. Files and use std according to the namespace usage method, where std is the namespace name of the C++ standard library. C++ puts the definition and implementation of the standard library into this namespace. So we can expand the std namespace:

		#include <iostream>
		using namespace std;
		
		int main()
		{
			int input;
			double d;
			// 自动识别类型
			cin >> input >> d;
		
			cout << input << endl << d << endl;
			return 0;
		}

In addition, cin and cout can also automatically identify the type of variables, such as the above code, and its output is as follows:

Insert image description here

3. Default parameters

Default parameters specify a default value for the function's parameters when declaring or defining a function. When calling this function, if no actual parameter is specified, the default value of the formal parameter is used, otherwise the specified actual parameter is used. Let’s first look at the use of default parameters:

Insert image description here
In the above usage, the Add function uses default parameters. In the Add function definition, it specifies a = 100, b = 200, which means that when the Add function is called, if no parameters are passed in, it will be used. Variables defined by yourself; when passing parameters, use the specified actual parameters, as shown below:

Insert image description here
Of course, you can also pass only part of the parameters, but when multiple parameters appear, the parameters must be given sequentially from right to left, and cannot be given at intervals; for example:

		#include <iostream>
		using namespace std;
		
		int Add(int a = 100, int b = 200, int c = 300)
		{
			return a + b + c;
		}
		
		int main()
		{
			int a = 10, b = 20, c = 30;
		
			int ret = Add(a);
			cout << ret << endl;
			return 0;
		}

The output result of the above code is 510, so for example, int ret = Add(a,,c);this kind of parameter passing is not allowed.

Then we can classify the default parameters . Like in the above code, Add()those that don't pass anything are called full default parameters ; Add(a)those Add(a,b)that only pass part of them are called semi-default parameters .

Finally, we should note that default parameters cannot appear in both function declaration and definition . If they appear in function declaration and function at the same time, we only need to give the default value in the declaration.

4. Function overloading

1. The concept of function overloading

Function overloading : It is a special case of functions. C++ allows several functions of the same name with similar functions to be declared in the same scope . These functions with the same name have different formal parameter lists (number of parameters or types or type order). This is often used to deal with Implement functions similar to the problem of different data types. Let’s look at using it first:

		#include <iostream>
		using namespace std;
		
		void Add(int a ,double b)
		{
			// 打印数据方便观察
			cout << "void Add(int a ,double b)" << endl;
		}
		
		
		void Add(double a, int b)
		{
			// 打印数据方便观察
			cout << "void Add(double a, int b)" << endl;
		}
		
		
		int main()
		{
		
			Add(3, 3.14);
			Add(3.14, 3);
			
			return 0;
		}

The results of the operation are as follows:

Insert image description here
In the above code, we print data in the function to show that the compiler called this function; we defined two functions with the same name, but their parameter types are different, and when we use these two functions, we pass The parameters are also different, so they will call their corresponding functions;

2. The principle of C++ supporting function overloading

The reason why C++ supports function overloading is because C++ has its own function name modification rules .
We know that .cpp files or .c files must go through the process of preprocessing, compilation, assembly, and linking before generating an executable program. Specifically review previous blogs: Preprocessing and Programming Environment ;

Among them, during the compilation process of C language, the symbol summary summarizes the function names of all .c files . Note that it is the function name. Therefore, in C language, function names with the same name will conflict during the compilation process, and the compilation will not be successful. pass;

However, in the function name modification rules in C++, C++ is not summarized with function names, but has its own modification rules. The specific modification rules have different modification rules in different compilers, for example:

		void func(int i, double d)
		{}
		
		void func(double d, int i)
		{}

These two functions, after function modification by the g++ compiler, become [_Z+function length+function name+type first letter], as shown in the figure:

Insert image description here
Insert image description here
So they can be distinguished when compiling and summarizing.

5. Quote

1. The concept of citation

A reference does not define a new variable, but gives an alias to an existing variable. The compiler will not allocate memory space for the reference variable. It shares the same memory space with the variable it refers to. Let’s look at a simple example first:

		#include <iostream>
		using namespace std;
		
		int main()
		{
			int a = 10;
			int& b = a;
			return 0;
		}

The above code int& b = a;is defining the reference type, b is the alias of a, a and b actually point to the same space, changes in a will affect b, and changes in b will also affect a.

2. Citation characteristics

  1. References must be initialized when defined

  2. A variable can have multiple references

  3. Once a reference refers to one entity, it cannot refer to other entities.

     	void Test()
     	{
     		int a = 10;
     		// int& ra;   // 该语句编译时会出错
     		int& ra = a;
     		int& rra = a;
     	}
    

int& ra; Compilation errors occur because there is no initialization during definition; in the above code, rra is an alias of ra and an alias of a. These three variables use the same space, and changes between them will affect each other.

3. Often cited

We must abide by a rule when using references, that is, during the reference process, permissions can be shifted and permissions can be reduced, but permissions cannot be enlarged. For example:

		int main()
		{
			const int a = 0;
			// 权限的放大,不允许
			//int& b = a;
		
			// 不算权限的放大,因为这里是赋值拷贝,b修改不影响a
			//int b = a; 
		
			// 权限的平移,允许
			const int& c = a;
		
			// 权限的缩小,允许
			int x = 0;
			const int& y = x;
		
			return 0;
		}

In the above code, the amplification of permissions means that const int a = 0;the const-modified a variable is constant, cannot be modified, and is read-only, but int& b = a;the value representing b can be modified, and the modification of the value of b will affect a, and b is readable and writable. , but a is only read-only, so this is an amplification of permissions; but int b = a; it does not count as an amplification of permissions, because this is an assignment copy, and the modification of b does not affect a.

The translation of permissions means that everyone has the same permissions. For example, const int& c = a;c and a here in the above code are modified by const. Everyone has const, so it is a translation of permissions, which is possible.

The reduction of permissions in the above code int x = 0; const int& y = x;means that x is readable and writable, but y is modified by const and is only read-only, but the change from readable and writable to read-only is allowed. This is called permission reduction. .

So let’s take a look at what the following statements belong to?

		void test()
		{
			int i = 0;
			double& d = i;
		}

First of all, we should understand clearly that if it is, it is int i = 0; double d = i;also possible, because integer promotion will occur between them; then we must be clear that during the process of integer promotion, a copying process will occur, and d takes a temporary copy of i. As shown in the figure below, this temporary copy is permanent and cannot be modified, so this is an enlargement of permissions and is not allowed.
Insert image description here
So the correct statement should be as follows:

		void test()
		{
			int i = 0;
			const double& d = i;
		}

If the attribute of d is also made unmodifiable, then there is a translation relationship between them.

4. Reference usage scenarios

(1) Making parameters (passing parameters by reference)

Our common method of passing parameters by reference is the exchange function. Write a commonly used exchange function as follows:

		#include <iostream>
		using namespace std;
		
		void Swap(int* p1, int* p2)
		{
			int tmp = *p1;
			*p1 = *p2;
			*p2 = tmp;
		}
		
		int main()
		{
			int a = 10, b = 20;
		
			Swap(&a, &b);
			
			return 0;
		}

In this exchange function, we need to pass the address of a and the address of b to change the values ​​of a and b; in C++, we can use references to complete the same exchange, the code is as follows:

		void Swap(int& p1, int& p2)
		{
			int tmp = p1;
			p1 = p2;
			p2 = tmp;
		}
		
		int main()
		{
			int a = 10, b = 20;
		
			Swap(a, b);
		
			return 0;
		}

After using references, the overall code looks very comfortable. There is no need to pass addresses and dereferences like pointers. At the same time, passing references and parameters can also improve the efficiency of parameter passing, because every time the address or value is passed, it is a copy. One copy is required at one time, which is very inefficient; while a reference does not need to be copied, because the formal parameter is an alias of the actual parameter, so there is no need to copy.

In addition, the most comfortable place for passing references and parameters is in the singly linked list we have learned before. For example, in the singly linked list in previous blogs, whether it is head insertion or tail insertion, etc., you need to pass a secondary pointer. Change the overall structure of the linked list, and after C++ introduces references , there is no need to pass secondary pointers, as shown in the following code:

		void SLTPushBack(SLTNode*& phead, SLTDateType x)
		{
		    // ...
		    if (phead == NULL)
		    {
		        phead = newnode;
		    }
		    else
		    {
		        //...
		    }
		}
		
		int main()
		{
		    SLTNode* plist = NULL;
		
		    SLTPushBack(plist, 1);
		    SLTPushBack(plist, 2);
		    SLTPushBack(plist, 3);
		
		    return 0;
		}

(2) Return value (return by reference)

When using return by reference, you need to pay attention. Unlike pass by reference, return by reference can only be used if the object is still in the function scope, and cannot be used if the object is not in the function scope; such as the following code:

		int& func()
		{
			int n = 0;
			n = 10;
		
			return n;
		}
		
		int main()
		{
			int ret = func();
			
			return 0;
		}

In this code, a variable n is defined in the function func, but its life cycle is only in this function, and its space will be destroyed when it goes out of the scope of the function. Draw a picture to understand it better:

Insert image description here
As shown in the figure above, after func is destroyed, n will also be destroyed, and the space will be returned to the operating system, but in the main function, ret is actually equivalent to accessing the destroyed n, which is strictly speaking equivalent to the wild pointer problem. That is, cross-border access.

But in different compilers, the results are different. In vs2019, the value of n can be obtained, as shown in the figure below:
Insert image description here

However, in the gcc/g++ compiler, an error is reported, as shown in the figure below:
Insert image description here
The reason is that it depends on whether the compiler initializes the destroyed space after the stack frame is destroyed. If the destroyed space is initialized, and To continue to access it is out of bounds. Compilers like gcc/g++ obviously initialize the space when the space is reclaimed, so it causes out of bounds; while vs2019 does not have strict checks.

Expansion : If the code is changed to the following, can it still be compiled?

		int& func()
		{
			int n = 0;
			n = 10;
		
			return n;
		}
		
		int main()
		{
			int& ret = func();
			cout << ret << endl;
			cout << ret << endl;
		
			return 0;
		
		}

Here, the reception of ret is changed to a reference, that is, ret is an alias for the returned n, let's see the execution result:

Insert image description here
The second execution is a random value, why? The reason is that ret is an alias of n, and they share the same space. When the cout statement is executed, a series of function stack frames will also be created, so the new space will overwrite the space where the previous func is located, that is, n The space of is covered, that is, the space of ret is covered, so the value of n becomes a random value; the reason why the first time is 10 is that the original space is not covered.

So another topic is introduced. If the space of n is not covered, is it still 10? Then we modify the code to the following code:

		int& func()
		{
			int a[1000];
			int n = 0;
			n = 10;
		
			return n;
		}
		
		int main()
		{
			int& ret = func();
			
			cout << ret << endl;
			cout << ret << endl;
		
			return 0;
		}

In the func function, we added an array with a length of 1000, let's look at the running results first:

Insert image description here

At this time, it becomes 10 again. This is because the space in the stack frame of the function is created downwards, so in the func function, 1000 spaces are first created, and then space is created for n. The position of n at this time is below ; if func is destroyed, if there is new space coverage, it depends on whether this space is larger than the original space of func. If this space is large and covers n, then n will become a random value. Otherwise, n is still the original value.

So what are the application scenarios for returning by reference? Our common return by reference can be used to modify the return object. For example, in a singly linked list, the search function and the modification function can be written together and used to return by reference, so that the data you want to find can be found and modified. The value you want to modify. For example the following code:

		int& SLFindOrModify(struct SeqList& ps, int i)
		{
			assert(i < ps.size);
			// ...
			return (ps.a[i]);
		}
		
		int main()
		{
			// 定义对象
			struct SeqList s;
			
			// 查找 10 这个数据,并将它修改成 20
			SLFindOrModify(s, 10) = 20;
		
			return 0;
		}

(3) The difference between references and pointers

Now that we have all learned about pointers and references, we can find that references and pointers are actually very similar. In many uses, pointers can replace references, and references can also replace pointers. So what is the difference between them? Let’s analyze them one by one:

The difference between references and pointers:

  1. A reference conceptually defines an alias for a variable, and a pointer stores a variable address.
  2. References must be initialized when defined, pointers are not required

  3. After a reference refers to an entity during initialization, it can no longer refer to other entities, and a pointer can point to any entity of the same type at any time.
  4. No NULL reference, but NULL pointer
  5. The meaning in sizeof is different: the reference result is the size of the reference type, but the pointer is always the number of bytes occupied by the address space (
    4 bytes on a 32-bit platform)
  6. Reference self-increment means that the referenced entity is increased by 1, and pointer self-increment means that the pointer is offset backward by the size of the type.
  7. There are multi-level pointers but no multi-level references
  8. The way to access entities is different. The pointer needs to be explicitly dereferenced, and the reference compiler handles it by itself.
  9. References are relatively safer to use than pointers

6. Inline functions

1. #define defines macros

We have learned #define to define macros before. For example, in the previous blog #define to define macros , macros bring us many benefits. For example, for small functions that are frequently called, there is no need to create stack frames, which improves efficiency; such as the following code:

		#define ADD(a,b) ((a)+(b))
		
		int main()
		{
			int ret = ADD(10, 20);
		
			cout << ret << endl;
			return 0;
		}

The above macro defines the addition of two numbers. Note that the macro definition here cannot be ((a)+(b))written as Executing | and & again is not the result we want.(a+b)ADD(1 | 2 + 1 & 2)

The above macro definition is directly expanded and replaced in the preprocessing stage, so no stack frame is created, which greatly improves efficiency.

However, while macros bring us benefits, they will inevitably bring inconveniences. If you use macro definitions, it will be easy to make mistakes. Just like the macro for adding two numbers above, it will not work without even one parentheses, so there are many syntax pitfalls in macros.

Finally, let’s summarize the advantages and disadvantages of macros:

advantage:

  1. There are no strict restrictions on types.
  2. There is no establishment of function stack frame, which improves efficiency.

shortcoming:

  1. It is not convenient to debug macros. (Because of the replacement during the pre-compilation stage)
  2. This results in poor code readability.
  3. There are no type safety checks.
  4. It is easy to make mistakes and has many grammatical pitfalls.

2. The concept of inline functions

Therefore, C++ introduced inline functions . Functions modified with inline are called inline functions. When compiling, the C++ compiler will expand the place where the inline function is called. There is no overhead of function calls to establish stack frames. Inline functions improve the efficiency of program operation. .

For example, the following inline function for adding two numbers:

		inline int Add(int a, int b)
		{
			return a + b;
		}
		
		int main()
		{
			int ret = Add(10, 20);
		
			cout << ret << endl;
			return 0;
		}

In the above code, the inline function for adding two numbers does not create a function stack frame, which shows good performance, nor does it need to add a lot of parentheses due to operator problems, so the inline function combines the advantages and disadvantages of macros and functions. Designed.

2. Characteristics of inline functions

(1) Inline is a method of exchanging space for time. If the compiler treats a function as an inline function, it will replace the function call with the function body during the compilation phase. Disadvantage: it may make the target file larger. Advantages: less Reduce calling overhead and improve program running efficiency.

(2) inline is just a suggestion for the compiler. Different compilers may have different implementation mechanisms for inline. The general suggestion is to make the function smaller (that is, the function is not very long. There is no exact statement, and it depends on the internal implementation of the compiler. ), non-recursive, and frequently called functions should be inline modified, otherwise the compiler will ignore the inline feature.

In other words, assuming you use inline, the compiler may not necessarily regard this function as an inline function, because if the function is large in size and the amount of code is large, it will cause code expansion. Therefore, considering overall performance, if we Use inline functions and try to simplify your code.

(3) It is not recommended to separate declaration and definition in inline, as separation will cause link errors. Because inline is expanded, there is no function address and the link will not be found.

For example, I define a Test.hheader file that contains the declaration of the Add function:

		inline int Add(int a, int b);

Define another Test.cppfile containing the implementation of the Add function:

		#include "Test.h"
		int Add(int a, int b)
		{
			return a + b;
		}

Then main.cppcall the Add function in the function:

		#include "Test.h"

		int main()
		{
			int ret = Add(10, 20);
		
			cout << ret << endl;
			return 0;
		}

The final compilation error occurred, as shown below:

Insert image description here
What's the reason for this? The reason is because the header file #include "Test.h" will be expanded in the main.cpp file in the preprocessing stage. After the expansion, there will be a declaration of the function Add, and the Add function is preceded by an inline inline. The compiler will consider it an inline function. It will be expanded directly, so if it is not given a valid address in the compilation stage, it will not enter the symbol table; and the Add function is called in the main function, it does not find the address of its corresponding function in the symbol table, so it will A link error occurred.

7. auto keyword

In C++11, auto means that variables declared with auto must be deduced by the compiler at compile time. In other words, auto is a keyword that automatically deduce the type based on the variable.

For example:

8. Range-based for loop (C++11)

When we need to traverse an array, we usually use the following method:

		int main()
		{
			int arr[] = { 1,2,3,4,5,6,7,8 };
		
			for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
			{
				cout << arr[i] << " ";
			}
		
			return 0;
		}

For a ranged collection, it is redundant and sometimes error-prone for the programmer to specify the range of the loop. So range-based for loops were introduced in C++11. The parentheses after the for loop are divided into two parts by the colon ":": the first part is the variable used for iteration in the range, and the second part represents the range to be iterated. Using range for we can combine it with the auto keyword we learned above, such as the following code:

Insert image description here
If we need to change the values ​​in the array, should we use it like the following code?

Insert image description here

Obviously, the answer is no, because e is just a temporary copy of the data in the array. Changing the value of the temporary copy does not affect the original value in the array, so we need to add a reference:

		int main()
		{
			int arr[] = { 1,2,3,4,5,6,7,8 };
		
			for (auto& e : arr)
			{
				e *= 2;
			}
		
			for (auto e : arr)
			{
				cout << e << " ";
			}
			return 0;
		}

After adding the reference, e is the alias of the data in the array, and changing e means changing the content in the array.

9. Pointer null value nullptr

When the NULL pointer was designed in the early days, NULL was actually 0, so using NULL in some places would cause ambiguous function calls, for example:

Insert image description here
In the above code, func constitutes a function overload. We expected NULL to call void func(int*)the function, but it called another one, so this resulted in an ambiguous function call.

Therefore, in C++11, nullptr was introduced, and its type is an untyped pointer (void*), which effectively avoids the above situation. For example, in the figure below, nullptr calls a function with a pointer type:

Insert image description here

Finally, all the content of Getting Started with C++ has been shared. Friends who feel it is helpful to you, please like and save it~Thank you for your support!

Guess you like

Origin blog.csdn.net/YoungMLet/article/details/131802354