BUT how can the Arduino know it's a pin number 13 from Arduino or a random number 13 that we are storing there?
The Arduino does not "know" the intention of a variable. The variable "becomes" a pin number when passed as an argument to pinMode() or digitalRead().
The use of a variable to "name" a pin is part of a programming style. There are a lot of examples sketches that try to convey programming style. One of these is to reduce the amount of ripple effects of change; reduce the number of line to change for a logical change.
Continuing the LED pin number example above. The "baseline" code is:
void setup()
{
pinMode(13, OUTPUT);
...
}
void loop()
{
digitalWrite(13, HIGH);
...
digitalWrite(13, LOW);
...
}
This works just fine BUT what is the ripple effect if we change the pin "13". The example uses symbols OUTPUT, HIGH and LOW which are also just numbers.
Typically in C the tradition is to use the preprocessor and define a macro.
#define LED 13
The preprocessor will replace LED in the source code with 13 and the compiler sees the same code as above.
An alternative is using a variable.
int LED = 13;
The drawback is that memory is allocated and it is possible to change the value at run-time which is often a very bad idea especially for a pin. The definition should actually be:
const int LED = 13;
The drawback with the macro is that it is just a value. In C++ it is best to use a constant variable. This tells the compiler both what data type and value it is. A constant variable cannot be assigned (again).
The next level to strength code quality and reduce ripple effects is to use lexical scope. A lexical scope such as a function body, class or namespace defines "when" a symbol is available.
An example; first global scope where definitions are available across the source code (just as for a macro):
const size_t BUF_MAX = 32;
static char buf[BUF_MAX];
const char* read()
{
...
for (int i = 0; i < BUF_MAX; i++) buf[i] = ...
...
}
Local lexical scope; definitions within a function:
const char* read()
{
const size_t BUF_MAX = 32;
static char buf[BUF_MAX];
...
for (int i = 0; i < BUF_MAX; i++) m_buf[i] = ...
}
The symbols buffer buf and the buffer size BUF_MAX are only available in the function.
class Reader {
public:
void read()
{
...
for (int i = 0; i < BUF_MAX; i++) m_buf[i] = ...
}
protected:
const size_t BUF_MAX = 32;
char m_buf[BUF_MAX];
...
};
Again the symbols are only available within the class and member function. Here is also another example of programming style. The prefix m_ is used to show that it is member data.
Last, the same example using a namespace:
namespace Reader {
const size_t BUF_MAX = 32;
char buf[BUF_MAX];
...
void read()
{
...
for (int i = 0; i < BUF_MAX; i++) buf[i] = ...
}
};
...
for (int i = 0; i < Reader::BUF_MAX; i++) Reader::buf[i] = ...;
All symbols must have the prefix Reader when used outside the namespace.
These techniques have been developed to allow writing larger programs with reuse. It is important to use them when writing a library for Arduino and especially when sharing. Ripple effects and symbol redefinition/collisions are reduced.