Executando verificação de segurança...
Em resposta a Ponteiros
7

Só um adendo - e eu sei que o texto está fazendo simplificações para ficar mais didático, mas não tem jeito, eu sou chato e pedante, então vamos lá:

Essa questão do array ser um ponteiro para o primeiro elemento é verdade em C. Mas em JavaScript e Python (e em várias outras linguagens de mais alto nível), não é bem assim. Essas linguagens abstraem as estruturas de forma que, na prática, não necessariamente será um ponteiro direto para o array.

Por exemplo, vejamos a implementação mais comum do Python (que é escrita em C, e chamada de CPython). Neste caso, a lista é implementada como um struct:

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;

Ou seja, é uma estrutura que contém o tamanho alocado e um array de ponteiros para os elementos da lista. Isso quer dizer que na verdade, por debaixo dos panos, não estamos passando um ponteiro para o array, e sim um ponteiro para o struct (que internamente manipulará o array de fato).

Mas isso é no nível mais interno da implementação. No código Python em si, tudo que vemos são variáveis, que no fundo são referências para objetos (e referência pode ser só o ponteiro/endereço, ou ainda alguma outra estrutura que abstrai esses detalhes). Até mesmo se fizermos x = 1, x será uma referência para o valor 1 (que é implementando como um objeto int). E claro que em outras linguagens pode ser diferente.

Quanto ao fato de ser mutável ou imutável, não tem relação direta com passagem por valor e referência. Vc pode ter tipos imutáveis que são passados por referência, só que aí não vai ser possível mudá-lo porque a imutabilidade está no tipo, não no fato de vc ter uma referência ao mesmo.


Passagem por valor ou por referência

Aliás, sobre passagem por valor e por referência, tem um detalhe bem sutil que muita gente confunde. E pra isso copiei os exemplos abaixo daqui.

Primeiro, este código em C:

void function2(int *param) {
    printf("I've received value %d\n", *param);
    (*param)++;
}

int main(void) {
    int variable = 111;
    function2(&variable);
    printf("variable %d\n", variable);
    return 0;
}

Saída:

I've received value 111
variable=112

Repare que o valor da variável foi alterado dentro da função, dando a impressão de que ela foi passada por referência. Mas na verdade, o que foi passado para a função foi um ponteiro para a variável (&variable, o endereço da variável). E esse endereço é passado por valor (é feita uma cópia do endereço, e a cópia que é usada na função).

O que acontece é que, dentro da função, o operador * desreferencia o ponteiro (pega o valor que está no endereço para o qual ele aponta) e aí o valor que está lá pode ser modificado. Mas isso não quer dizer que a variável foi passada por referência, e sim que o ponteiro/endereço para a variável foi passado por valor.

E como ele comprova isso? Com este código:

void function2(int *param) {
    printf("address param is pointing to %d\n", param);
    param = NULL;
}

int main(void) {
    int variable = 111;
    int *ptr = &variable;
    function2(ptr);
    printf("address ptr is pointing to %d\n", ptr);
    return 0;
}

A saída é:

address param is pointing to -1846583468
address ptr   is pointing to -1846583468

Se o ponteiro fosse passado por referência, então depois da função ele seria nulo, mas não é o caso. Isso porque a função recebe o ponteiro por valor (ela recebe uma cópia do endereço), e qualquer alteração feita neste ponteiro dentro da função não é refletida fora dela, por isso o ponteiro continua apontando para o mesmo endereço depois que a função é chamada. Então em C vc tem apenas a ilusão de passagem por referência (ou uma simulação, feita de maneira "esperta" com os ponteiros).

Aliás, se fizer assim, também dá o mesmo resultado:

function2(&variable);
printf("address ptr   is pointing to %d\n", &variable);

Pois a função recebe uma cópia do ponteiro/endereço, e a alteração feita dentro dela (param = NULL) não se reflete fora dela.


Algo similar ocorre em Java, JS, Pyhton, etc. A função a grosso modo recebe uma referência (outro nome bonito para o ponteiro ou endereço, mas pode ter mais detalhes de implementação que os abstraem), mas na verdade, ela recebe uma cópia desta referência (ou uma cópia do endereço). E como as operações são feitas no endereço, o resultado é o objeto original sendo modificado. Por exemplo, em Python:

def f(x):
    x.append(4)

lista = [1, 2, 3]
f(lista)
print(lista) # [1, 2, 3, 4]

A função recebe a referência à lista (uma "cópia do endereço" dela) e atua em cima desta cópia. O que acontece é que métodos são sempre chamados no objeto para o qual a referência aponta. E como foi feita uma "cópia do endereço", o método é chamado no mesmo endereço do objeto original, e por isso este é modificado.

Mas se fizer assim:

def f(x):
    x = [4, 5, 6]

lista = [1, 2, 3]
f(lista)
print(lista) # [1, 2, 3]

Aí a lista não é modificada. Isso porque a atribuição x = [4, 5, 6] foi feita na cópia, e não na referência original. Afinal, a função recebe uma cópia da referência, e uma atribuição cria um objeto novo (com um novo endereço, e portanto, outra referência). Mas como x é uma cópia, a atribuição a ele não altera a lista fora da função. Para mais detalhes, veja aqui.

E vale lembrar que a função sempre receberá uma "cópia do endereço", mesmo se o tipo for imutável. A imutabilidade não tem relação direta com o tipo de passagem, são caraterísticas independentes.


Então o que é passagem por referência?

Uma linguagem que tem passagem por referência de fato é C#:

public class Test
{
    public static void Main()
    {
        int[] array = { 1, 2, 3 };
        f(ref array);
        foreach (int n in array)
            Console.WriteLine(n);
    }
    public static void f(ref int[] array)
    {
        array = new[]{4, 5, 6};
    }
}

O código acima imprime 4 5 6, pois a função recebe a referência do array (indicado por ref), e não uma cópia. Por isso é possível atribuir outro array dentro da função, e esta mudança se reflete fora dela.

Se removermos os ref's do código, aí passa a ser passagem por valor normal: é feita uma cópia da referência, igual ao exemplo anterior com Python. E por isso a atribuição dentro da função é feita na cópia e isso não altera o array fora da função. Por isso, se remover os ref's, o código imprimirá 1 2 3.

Só pra dar outro exemplo, PHP também tem passagem por referência, bastando colocar & antes do parâmetro:

function f(&$var) { // & indica que $var é passada por referência
    $var = 1;
}

$x = 'oi';
f($x);
echo $x; // 1

Se removermos o & antes de $var, aí deixa de ser passagem por referência e o código imprime "oi".

Carregando publicação patrocinada...
1
1

De fato como você mencionou a intenção era ser simples e prático, mas isso não muda o fato de que o seu comentário agrega bastante. Parte da ideia do artigo era que outras pessoas também contribuissem, portanto, muito obrigado!