CPP Design - Impossible

1. Ambiguity and nonsense

​ We all know that programs cannot accept ambiguity. When a statement can have two execution results, the program will not accept it. But we often ignore "nonsense". This is the concept I got, which means that the execution result of this statement is uncertain. This kind of uncertainty does not mean that the result is uncertain. For example, the value of a local variable in C is the same when it is just declared. Uncertain, but it doesn't matter, but if the rules of execution are uncertain, then it is not allowed.

​ However, due to the high degree of freedom of CPP, he hopes that all the rules are formulated by the user, but before such custom rules are formulated, there must be certain rules, but this rule needs to be understood by ourselves.


2. Declaration, definition, initialization

2.1 Concept

​ First, explain the meaning of the declaration . The declaration is to tell the CPP the name and type of this thing. It is equivalent to **"registering"** on the symbol table. But what exactly this thing is like is actually not stipulated.

​ As for the definition , it means to tell CPP what the specifics of this thing are, its memory space must exist (it is not necessary to declare it), and there must be certain rules for the data in the memory space (the inside Things are not necessarily fixed, but there must be rules).

2.2 Declaration and definition of function

​ In fact, the above two concepts are easy to understand, especially when "this thing" is a function , it is easy for us to distinguish these two concepts:

// function declaration
int sum(int, int);

// function defination
int sum(int a, int b)
{
    
    
	return a + b;
}

2.3 Declaration and definition of variables

But for variables , things get complicated. Namely in CPP. Only externthe keyword can complete the operation of "declaring but not defining" variables . That is, even if we write the following statement

int a;

​ I didn't "define" whether athe content is 1 or 2. But it's still a statement with both a "declaration" and a "definition". In other words, after executing this statement, a4 bytes of storage space have been allocated. Although the content inside is uncertain, the rules are certain. For example, if the value is a global variable, then the value ain is 0. If ais a local variable, then athe value of is the corresponding value of the previous virtual space position on the function stack.

​ Let me talk about it after the introduction extern. when such a sentence appears

extern int a;

​ We only know that a is an int variable, but no storage space and value are aallocated .

​ This is a very simple cognition, but when it comes to complex types, it becomes a bit vague. This may be because the memory space of complex types becomes larger, and there is interference from other languages, such as Java

Student cnx;

​ This statement does not cnxallocate Studenta space for , let alone the value of each variable Studentin . But CPP is different. For CPP, the same statement actually allocates memory space for cnxthis variable, and even the contents inside are determined according to fixed rules (basically still the above rules). This statement is still a statement that is both a declaration and a definition .

2.4 Initialization of variables

​ Initialization refers to the situation where the "rule" is customized when we define it. An initialization statement such as:

int a(3);

​ This sentence means that when the space aof is allocated, the value in this space will be written 3. For simple types , the above statement is equivalent to the following, which is also initialization (we're most used to this):

int a = 3;

​ But such a statement is not an initialization (strictly speaking, it is anot 3a statement that initializes to )

int a;
a = 3;

​ When it comes to the discussion of complex types , things get even more magical, such as this structure

#include <iostream>

using namespace std;

class Node
{
    
    
private:
	int a;
public:
	Node(int);
	void Show() const;
};

Node::Node(int aa)
{
    
    
	a = aa;
}

void Node::Show() const
{
    
    
	cout << a << endl;
}

int main()
{
    
    
	Node node(3);
	
	node.Show();
	
	return 0;	
} 

​ Inside this sentence (or the following sentence):

Node node(3);
Node node = node(3);

​ I don't know if it is called initialization, because if you understand it according to simple types, the above two are called initialization, and to some extent they are. Because we have an indicator to check whether the initialization is to constmodify , constthe variable can only be initialized, but not assigned. So such a statement would have the following result

const int a; // Wrong! uninitialized

const int a = 3;
a = 2; // Wrong! assigement of read-only variable

​ For complex types, the following statements are all acceptable, indicating that these statements are initialization statements

const Node node1(3);   			// AC
const Node node2 = Node(2); 	// AC

But we have another initialization list thing in the complex structure . What this thing says is that the initialization list will be executed before the constructor is called. That is to say, what is executed in the initialization list is the initialization. The above statements are all constructor statements, and it seems that they should not be initialization (the initialization position is robbed by the initialization list), but it (the constructor) is an initialization statement.

​ What I understand now is that the initialization list provides the initialization of all data members in the class, and the constructor provides the initialization of the class object. Although the initialization of an object appears to be the initialization of all data members in the object. However, if these two concepts are distinguished, logically self-consistency can be achieved.

2.5 Initialization of parameters and return values

​ The above is not very rigorous. In fact, there is another place where variable declaration and initialization are separated, that is, the working principle of function parameters and return values. I have always felt that for the parameters of the function, what happens is a process of assignment, that is, for the function

int sum(int a, int b)
{
    
    
	return a+b;
}

What actually happens with the statement is (my imagination)

// 语句
sum(1, 2);

// 实际
int a;
int b;
a = 1;
b = 2;
int ret; 
ret = a + b; 

but actually it is (true)

int a(1); 
int b(2);
int ret(a + b);

Therefore, whether it is a function parameter or a return value, it is an initialization operation, not a process of defining first and then assigning a value. Only then can constkeywords .

2.6 Type declarations

​ In addition to function declarations and variable declarations, there is another type of declaration, such as these two:

// class declaretion
class Node
{
    
    
private:
	int a;
public:
	Node(int);
	void Show() const;
};

// typedef declaretion
typedef int int_32;

// enum declaretion
enum bits{
    
    one = 1, two = 2, four = 4, eight = 8};

It is also important to note that this is just a declaration and does not open up memory space for anything (except static members of the class).


Three, const

3.1 New types

​ I think constthe best is that it is not a modifier, but a new type. To make it clearer,

int a;
const int b = 3;

​ It is good to directly distinguish the two into different types. This is because this keyword plays a very important role (especially to determine certain properties), and it will appear in various declarations and definitions (for example, friendthe keyword used once in the declaration).

We can regard the mutual assignment of the two as a type conversion.

3.2 Pointer constants and constant pointers

In fact, it is very simple to record it.

const int *pa;		// 常量指针
int * const pa		// 指针常量

A constant pointer means that the content pointed to by this pointer cannot be modified through this (note that it can still be modified in other ways). Pointer constant means that the content pointed to by the pointer can be modified, but the value of the pointer itself cannot be modified.

3.3 Constants and pointers

In fact, it is very complicated to say, but I don't have much time. I can only say a few conclusions.

For a pointer constant, the address of an ordinary variable or the address of a constant can be used for initialization

const int a = 1;
int b = 2;
const int *pa = &a;		// VALID
const int *pb = &b;		// VALID

But there is no way for an ordinary pointer to accept the address of a constant

const int a = 1;
int *pa = &a;			// INVALID

For the mutual assignment of pointers, non-constant pointers can be assigned to constant pointers, that is, the following statements can be compiled.

int a = 1;
int *pa = &a;
const int *pb = pa;

But constant pointers cannot be assigned to constant pointers. That is, the following statement cannot pass:

int a = 1;
const int *pa = &a;
int *pb = pa;

3.4 Explicit restrictions on constants

It constis really a strong limitation, and the most easily overlooked limitation occurs when the function is passed, such as the following code

#include <iostream>

using namespace std;

int sum(int *a, int *b)
{
    
    
	return *a + *b;
}

int main()
{
    
    
	const int a = 2;
	int b = 3;
	
	const int *pa = &a;
	
	cout << sum(pa, &b);
	
	return 0;	
} 

The reason for the error is invalid conversion from ‘const int *’ to int *. In other words, even sumif is no change in the content, it is still not allowed.

3.5 Quote syntactic sugar

​Syntactic sugar only refers to a certain syntax added to a computer language. This syntax has no effect on the function of the language, but it is more convenient for programmers to use. Generally speaking, the use of syntactic sugar can increase the readability of the program, thereby reducing the chance of program code errors.

​References are one such syntactic sugar. It can actually be understood as a constant pointer , something like the following

int a = 2;
int &b = a;
int *const c = &a;

​ So when we consider the many limitations of references, we can actually think about them in the form of constant pointers. For example, it can modify the content. This is because it is a pointer. The reference must be initialized, this is because the constant pointer is a constant, and the constant must be initialized.

3.6 Essential summary

After I finished writing these things, one thing suddenly became clear, which is to use the idea of ​​​​type conversion to understand const. constIn essence, it provides a restriction, we can only do several restricted type conversions, as follows:

int = const int				// VALID
const int = int				// VALID
    
int * = const int *			// INVALID
cosnt int * = int *			// VALID
    
&int = cosnt int *			// INVALID
&const int = int *			// VALID
    
int & = const int &			// INVALID
const int & = int &			// VALID

​ In fact, it shows one thing, that is, constants are things that cannot be modified . The reason why the second one in the first group is possible is because although it is not possible from the perspective of type conversion, the situation where this conversion occurs is that a constant is assigned to a variable, and the constant itself is not converted. But this is not the case for the last three groups, which are essentially pointer assignments. If the content pointed to by the pointer is a constant, when the pointer is copied, it must ensure that the object it is assigned to will not modify the constant. This requires that the pointer must also be a constant pointer. This is why the second of the last three groups is not allowed.

​ Not just intThe above relationship is true for all types, including structures and classes.


4. Assign "value"

4.1 Array copy

Array assignment is the thing I want to discuss the most. I used to think that there is no way to assign an array. That's it:

int a[3] = {
    
    1, 2, 3};
int b[3];
b = a;

It is because the array is a large data, so there is no way to assign it. This idea is a very wrong idea, it can be said to be my biggest wrong idea. This idea puts the judgment of whether the sentence is correct to a criterion called "very large", which is obviously unreasonable.

This kind of wrong idea makes me often have many illusions in various studies later, such as

struct node
{
    
    
	int array[3];
};

struct node a;
struct node b;
a = b;

​ I always think this is wrong. First of all, I think that a should not exist. nodeIt is an array inside. How could it be declared so easily. a = bIt is even more unacceptable, how can such a big thing be copied?

​ But in fact, the assignment of CPP is very simple. In fact, it is to write a certain value in a piece of memory space, but certain rules must be followed. But writing is just writing, and there is no situation where you can’t write if you write too much. If you want to write, you can write as many arrays as you want. That is to say, when a = bthis , athe content of the array in is actually copied bto , so what is completed is a deep copy . We can verify this with the following program

#include <bits/stdc++.h>

using namespace std;

struct ArrayWrapper
{
    
    
	int array[3];
};

int main() 
{
    
    
	ArrayWrapper a;
	a.array[0] = 5;
	a.array[1] = 1;
	a.array[2] = 4;
	ArrayWrapper b = a;
	
	// 514
	cout << b.array[0] << b.array[1] << b.array[2] << endl;
	
	// change the b.array
	b.array[0] = 4;

	// 514, a doesn't changed
	cout << a.array[0] << a.array[1] << a.array[2] << endl;
	// 414, b.array is different from a.array
	cout << b.array[0] << b.array[1] << b.array[2] << endl;
	return 0;
}

​ So what caused the above illusion is actually the name of the array, such as the following

int a;
int b[10]

​ We know that the integer in the first line is called a, and a is its name. We make it equal to 1, that is, let a equal to 1. But what is the name of the integer array below, is it b? No, b is a constant pointer pointing to the first element of this array. Let's not talk about constants. Letting a pointer assign a value to another pointer, of course, cannot copy the array, because a is not an array. In other words, all the arrays in C and CPP are **"anonymous"**, they all have no names, so naturally they cannot be assigned. Who can manipulate something that cannot be manipulated? The array is a characteristic of "constant" pointers, and it is even guaranteed that once a value is assigned, an error will be reported. This is the so-called array that cannot be assigned.

4.2 Forever value transfer

​ After elaborating this point of view, we can lead to a more important thing, which is how the assignment happens. In fact, think about it, this is actually a lot of changes. For example in Java

Student cnx = qs;

​ This kind of thing does not actually copy the entity qsof cnx, but assigns something similar to a pointer to it cnx, and the entity of the qsdata is not assigned to cnx, but cnxalso points to qsthe data entity pointed to by . This raises a deep copy problem. However, Java has trouble dealing with this aspect. Because for all complex structures (classes), Java does not provide the name of the entity, but only provides pointers.

​ But CPP is not like this, for a complex structure

struct node
{
    
    
	int array[3];
};

struct node a;
struct node b;
a = b;

Speaking of assignment, it means opening up a new memory space, and then changing the content in this space to an rvalue. That is, passed by value . So what is the so-called pointer passing and reference passing? In fact, it is still passed by value . It's just that this value becomes the value of the pointer, not the value of the content pointed to by the pointer. As explained earlier, references are just a special kind of pointer. A pointer is just a special kind of value.


Five, heap

5.1 Common usage

​ This part is very simple, that is, CPP discards malloc and free, and uses newand delete, basically the usage is the same. The space on the heap is still managed by pointers. As an example:

// 分配一个数组
int *a = new int[5];

// 分配一个对象
Student *pcnx = new Student("cnx", 19);

​ Pay attention to newmatching , that is,

// 释放数组
delete [] a;
// 释放对象
delete pcnx

5.2 positioning new

​ This is a new usage newof , in fact, it is possible to specify the allocation location new. (Before it was allocated on the heap, it was the responsibility of the operating system). His usage is as follows

#include<new>

char buffer[512];

pd1 = new(buffer) double[3];

​ The code above allocates some space in the buffer.


6. Namespace

6.1 Linkability

​ This content applies not only to CPP, but also to C. The way we physically organize code is typically files. We say that there are related codes in a file. But there is a problem that we often need to organize the code in multiple files into a program. So the symbols in these files may conflict. So we have to measure the visibility of each symbol for each file, and how to deal with this conflict.

​ The "visibility of files" we describe is actually **"linkability"**. About variables. We have a total of three kinds of connectivity, see the table below

Linkage Condition explain
no link code block will not be seen outside of this code block
Internal Linkage not in any function, usestatic Can be seen by this file, but not by other files
external connectivity not in any function can be seen by all files

​ The conflict of the same name we are talking about can actually be subdivided into two types, one is to cover up . This happens when the linkability is different. For example, when there is a variable called in the code block a, there is a variable called globally a. Then in this global awill abe covered by in this code block.

​ The other is a real conflict . This happens when the linkage is the same. For example, if a global variable is defined in one file aand the same definition is made in another file, then the compiler will report an error. In order to avoid this situation (this situation generally only occurs on external link variables, because if there are other conflicts, it is generally the programmer's own problem, he defines a variable twice). We have introduced externthe keyword , so that variables can be declared multiple times and defined once .

​ For the case staticwhere appears in a code block, there are the following interesting features to note:

  • The storage feature is static, that is, it still exists when the code block is not active, so the value of the static local variable remains unchanged between two function calls
  • Static local variables are only initialized once when the program starts, and will not be initialized when the function is called again later

​ Take the following code as an example

#include <iostream>

using namespace std;

void increase()
{
    
    
	static int ans = 3;
	ans++;
	cout << ans << endl;
}
 
int main()
{
    
    
	increase();
	increase();
	
	return 0;	
} 

Its output is

4
5

​ For functions, they are globally linked by default, but their multiple declarations do not need to use externkeywords (of course they can be used). They do not have unlinkability because they cannot be defined inside code blocks. But you can add before the function staticto make it internally linked, that is, only this file is visible.

​ For inline functions, they do not need to obey the rules of "can be declared multiple times, but can only be defined once" that functions and variables follow. They can be defined multiple times, as long as the content of each definition is the same.

6.2 Namespaces

​ But the above methods do not really solve the problem of name conflicts. They are just avoiding the name of a function or variable with external linkage to be different from other functions and variables. In other words, it consumes the programmer's effort to Dealing with a problem that shouldn't be a problem.

​ In order to really improve the problem of name conflicts, we have come up with many solutions. For example, Javain , we fully adopted the idea of ​​OOP. No variable is globally visible. The largest visible domain of a variable is the class. When we want to use a variable, we must use this form

cnx.grade;
qs.grade;

​ So although there are two grade, because the previous objects are different, it becomes two different variables. But this method is not a very perfect method, because it means that in order to distinguish these variables, we must OOP, which is obviously a price that is not worth the candle.

​ The essence of our extractor is actually to add modifiers before variables with the same name so that they no longer have the same name. This is the concept of namespace. We can declare variables and functions in this namespace, and these things will not conflict with things of the same name in another namespace, such as

namespace Jack
{
    
    
    double pail;
    void fectch();
}

namespace Jill
{
    
    
    int pail;
    float fetch();
}

​ These two things will not conflict. When we need to use them, we only need to add the parsing operator :: .

Jack::fetch();
Jill::pail;

​ Of course, we don’t want to type so many words every time, so we have using the command

using Jack;					// using 编译指令
using Jill::fetch();		// using 声明指令

​ The scope of the two of them depends on the position of the instruction, which is no different from the general variable declaration.

​ If we take it out, we can see that the namespace essentially provides a new hierarchical structure, which is under the global. CPP should have no file hierarchy and is not perfect, we can't specify a certain file.

Regarding the usage details of the namespace, there are the following questions:

  • The program itself will correspond to a global namespace , and its use method is, for example ::a, no need to add a name in front.
  • A namespace can be global or located within another namespace.
  • By default, the linkability of names declared in a namespace is global (meaning they can be used everywhere plus the parser).
  • For classes, we still use the resolution operator :: .
  • Unnamed namespaces can be substituted for internal static variables. Because other files can't use it (no name)

おすすめ

転載: blog.csdn.net/living_frontier/article/details/129982193