cblc/aulas/06-vetores/README.org

12 KiB

Curso Básico da Linguagem C

Aula 6: Vetores

Vídeo desta aula

Vetores são estruturas de dados que agrupam valores do mesmo tipo na forma de listas.

  • Cada valor em um vetor é um elemento.
  • O nome do vetor é o endereço do seu primeiro elemento.
  • Cada elemento é identificado por um índice escrito entra colchetes após o nome.
  • A representação do índice, incluindo os colchetes, é chamada de subscrito.

Embora os termos sejam intercambiáveis, um vetor é, mais precisamente, uma matriz unidimensional, ou seja, uma lista de valores. Já o termo matriz e mais utilizado quando nos referimos a um vetor cujos elementos são outros vetores, o que forma uma matriz multidimensional. Finalmente, array é só o termo em inglês para matriz.

Utilidade dos vetores

Quando estamos lidando com conjuntos de dados relacionados, é preferível trabalhar com vetores em vez de variáveis, porque isso possibilita uma melhor organização e estruturação dos dados, além de facilitar a sua manipulação.

Exemplo:

float nota1 = 7.5;
float nota2 = 9.0;
float nota3 = 8.3;
float nota4 = 8.6;

float soma = nota1 + nota2 + nota3 + nota4;
float media = soma / 4;

Com vetores…

float notas[4];

notas[0] = 7.5;
notas[1] = 9.0;
notas[2] = 8.3;
notas[3] = 8.6;

float soma = 0;
for (int i = 0; i < 4; i++) soma += notas[i];

float media = soma / 4;

Nós também poderíamos inicializar o vetor na declaração:

float notas[4] = {7.5, 9.0, 8.3, 8.6};

float soma = 0;
for (int i = 0; i < 4; i++) soma += notas[i];

float media = soma / 4;

As diferenças entre os exemplos com vetores e com variáveis separadas são…

Semanticamente:

  • Uma variável expressa e dá significado a um valor.
  • Um vetor expressa e dá significado a um conjunto relacionado de valores.

Sintaticamente:

  • Para obter o valor associado à variável, nós avaliamos o nome da variável.
  • Para obter o valor do primeiro elemento de um vetor, nós derreferenciamos o nome do vetor (*<vetor>).
  • Para obter o endereço do valor associado a uma variável, nós usamos &<var>.
  • Para obter o endereço do primeiro elemento do vetor, nós avaliamos o nome do vetor.
  • A operação &<vetor> avalia o endereço de todo o conjunto de elementos do vetor.
  • Para obter os valores dos demais elementos, nós somamos *(<vetor> + índice).
  • A linguagem C implementa uma notação mais semântica para acesso aos valores dos elementos na forma de um subscrito para a escrita de um índice após o nome do vetor (<vetor>[índice]).

Pragmaticamente:

Como os índices são numéricos, nós podemos iterar os elementos através de operações com expressões que avaliem valores compatíveis com o tipo int. Isso facilita muito o acesso e a manipulação de valores, especialmente em situações onde temos que lidar com grandes quantidades de dados.

Quantidade de dados não é, necessariamente, a mesma coisa que volume de dados!

Declaração e inicialização de vetores

Para declarar um vetor, nós definimos os tipos dos seus elementos, seu nome e um subscrito (um par de colchetes) contendo a quantidade de elementos que o vetor receberá:

float notas[4];

Enquanto não forem definidos, os valores nos elementos será o que houver na quantidade de bytes reservada a partir do endereço do vetor (lixo de memória).

Nós também podemos inicializar um vetor na sua declaração:

float notas[4] = {7.5, 9.0, 8.3, 8.6};

Tamanho de um vetor em tempo de compilação

Quando um vetor é inicializado sem a definição da quantidade de elementos em seu subscrito, o compilador obtém essa quantidade da lista de valores:

float notas[] = {7.5, 9.0, 8.3, 8.6}; // 4 elementos...

Importante!

Isso só vale para a inicialização de vetores nas suas declarações:

float notas[];  // Errado!

Inicialização parcial

Quando a quantidade de elementos é definida na inicialização do vetor, a ausência de valores correspondentes na lista de elementos é preenchida com zeros:

int num[5] = {1, 2}; // => {1, 2, 0, 0, 0}

O que possibilita, inclusive, inicializar um vetor zerado:

int num[5] = {0}; // => {0, 0, 0, 0, 0}

Acesso aos elementos de um vetor

O acesso para manipulação dos elementos de um vetor é feita através de seus índices, levando em conta que o primeiro elemento sempre terá índice 0:

int num_list[5];
/* ... */
num_list[0] = 23; // Definição do valor do elemento 0 (primeiro elemento).
/* ... */
num_list[4] = 19; // Alteração do elemento 4 (último elemento).

Iteração pelos elementos

Nós podemos iterar os elementos de um vetor em loops:

#include <stdio.h>

int main() {

    int v[5] = {0};

    v[0] = 23;
    v[4] = 19;

    for (int i = 0; i < 5; i ++)
        printf("v[%d]: %d\n", i, v[i]);

    putchar('\n');

    for (int i = 0; i < 5; i ++) {
        v[i] = i + 1;
        printf("v[%d]: %d\n", i, v[i]);
    }

    return 0;
}

Compilando e executando…

:~$ testes.c
:~$ a.out
v[0]: 23
v[1]: 0
v[2]: 0
v[3]: 0
v[4]: 19

v[0]: 1
v[1]: 2
v[2]: 3
v[3]: 4
v[4]: 5

Vetores e funções

Vetores não podem ser passados para funções, mas os seus endereços podem:

#include <stdio.h>

void print_array(int v[], int size) {
    for (int i = 0; i < size; i++)
        printf("v[%d] => %d\n", i, v[i]);
}

int main() {

    int vetor[5];

    for (int i = 0; i < 5; i ++) vetor[i] = i + 1;

    print_array(vetor, 5);
}

Compilando e executando…

:~$ testes.c
:~$ a.out
v[0] => 1
v[1] => 2
v[2] => 3
v[3] => 4
v[4] => 5

Importante! É preciso ter cuidado com a quantidade de elementos de um vetor, pois o gcc não emite avisos nem erros quando tentamos ler ou escrever dados fora dos limites do vetor, o que pode causar muitos problemas.

Ainda no exemplo, observe que a função print_array tem o parâmetro int v[], que se parece com a notação de um vetor, mas esta é só uma sintaxe utilizada na linguagem C para representar que o argumento esperado é o endereço de um valor de determinado tipo (no caso, int) e que esse endereço, semanticamente, será o de um vetor.

Essa sintaxe tem exatamente o mesmo efeito de um ponteiro como parâmetro:

void print_array(int *v, int size) {
    for (int i = 0; i < size; i++)
        printf("v[%d] => %d\n", i, v[i]); // Notação de vetor...
}

Isso também vale para todas as expressões que utilizem a notação de vetores, porque nome[índice] é uma representação semântica que será interpretada como *(nome + índice) pelo compilador.

Logo, a função também poderia ser escrita assim:

void print_array(int *v, int size) {
    for (int i = 0; i < size; i++)
        printf("v[%d] => %d\n", i, *(v + i)); // Notação de ponteiro...
}

Relação entre vetores e ponteiros

Um equívoco muito comum, é a ideia de que vetores "decairiam" para ponteiros. Isso simplesmente não é verdade: ponteiros são variáveis que recebem endereços como valores, enquanto vetores são, e sempre serão, endereços, onde quer que seus nomes sejam utilizados.

#include <stdio.h>

int main() {

    int v[4] = {0};
    int *p = v;

    printf("vetor    : %p\n", v);
    printf("&vetor   : %p\n", &v);
    printf("ponteiro : %p\n", p);
    printf("&ponteiro: %p\n", &p);

    return 0;
}

Compilando e executando…

:~$ gcc testes.c
:~$ ./a.out
vetor    : 0x7fff16832290 <-- Endereço de v[0]
&vetor   : 0x7fff16832290 <-- Endereço expresso por v
ponteiro : 0x7fff16832290 <-- Valor no ponteiro (endereço expresso por v) 
&ponteiro: 0x7fff16832288 <-- Endereço do valor no ponteiro

Nos parâmetros de funções, TIPO nome[] é uma sintaxe alternativa para TIPO *nome e ambas representam a declaração de um ponteiro que receberá, como sempre, um endereço, não uma cópia de um vetor.

Do mesmo modo, o uso da notação de vetores em expressões é apenas uma sintaxe alternativa para operações realizadas com endereços: é por isso que vetor[i] é o mesmo que *(vetor + i).

Vetores de caracteres (strings)

Strings são vetores de caracteres (cadeias de bytes) terminados com o caractere nulo (\0).

Portanto…

char string[] = "banana";

É o mesmo que…

char string[] = {'b', 'a', 'n', 'a', 'n', 'a', '\0};

Mais diferenças entre vetores e ponteiros

Observe o exemplo…

#include <stdio.h>

int main() {

    char vstr[] = "bacana";
    char *pstr = "calado";

    printf("%p --> %s\n", vstr, vstr);
    printf("%p --> %s\n", pstr, pstr);
    
    return 0;
}

Essas strings são diferentes:

char vstr[] = "bacana";
char *pstr = "calado";

Enquanto o vetor vstr recebeu uma lista de caracteres, o ponteiro pstr recebeu o endereço para um dado numa região do espaço de memória, atribuído pelo sistema operacional ao programa, chamada de .rodata, onde são armazenados os dados que não podem ser alterados (read only).

Isso pode ser verificado despejando o conteúdo do binário do programa com o utilitário readelf:

:~$ readelf -x .rodata a.out

Despejo máximo da secção ".rodata":
  0x00002000 01000200 63616c61 646f0025 70202d2d ....calado.%p --
  0x00002010 3e202573 0a00                       > %s..

Veja que apenas a string calado aparece nesta seção do binário.

A string bacana, por sua vez poderá ser encontrada na seção .text do binário, que é onde nós encontramos o código da função main:

:~$ readelf -x .text a.out

Despejo máximo da secção ".text":
  0x00001050 31ed4989 d15e4889 e24883e4 f0505445 1.I..^H..H...PTE
  0x00001060 31c031c9 488d3dce 000000ff 154f2f00 1.1.H.=......O/.
  0x00001070 00f4662e 0f1f8400 00000000 0f1f4000 ..f...........@.
  0x00001080 488d3d91 2f000048 8d058a2f 00004839 H.=./..H.../..H9
  0x00001090 f8741548 8b052e2f 00004885 c07409ff .t.H.../..H..t..
  0x000010a0 e00f1f80 00000000 c30f1f80 00000000 ................
  0x000010b0 488d3d61 2f000048 8d355a2f 00004829 H.=a/..H.5Z/..H)
  0x000010c0 fe4889f0 48c1ee3f 48c1f803 4801c648 .H..H..?H...H..H
  0x000010d0 d1fe7414 488b05fd 2e000048 85c07408 ..t.H......H..t.
  0x000010e0 ffe0660f 1f440000 c30f1f80 00000000 ..f..D..........
  0x000010f0 f30f1efa 803d1d2f 00000075 2b554883 .....=./...u+UH.
  0x00001100 3dda2e00 00004889 e5740c48 8b3dfe2e =.....H..t.H.=..
  0x00001110 0000e829 ffffffe8 64ffffff c605f52e ...)....d.......
  0x00001120 0000015d c30f1f00 c30f1f80 00000000 ...]............
  0x00001130 f30f1efa e977ffff ff554889 e54883ec .....w...UH..H..
  0x00001140 10c745f1 62616361 c745f461 6e610048 ..E.baca.E.ana.H <-- Aqui!
  0x00001150 8d05ae0e 00004889 45f8488d 55f1488d ......H.E.H.U.H.
  0x00001160 45f14889 c6488d05 9f0e0000 4889c7b8 E.H..H......H...
  0x00001170 00000000 e8b7feff ff488b55 f8488b45 .........H.U.H.E
  0x00001180 f84889c6 488d0580 0e000048 89c7b800 .H..H......H....
  0x00001190 000000e8 98feffff b8000000 00c9c3   ...............

As seções do binário e os segmentos do layout de memória serão assunto de uma aula futura.

As consequências disso, são:

  • Os caracteres de calado não poderão ser alterados por aritmética de ponteiros.
  • A tentativa de alterar dados em .rodata não resultará em erros de compilação, mas causará falha de segmentação (escrita em segmentos não autorizados da memória).
  • Enquanto o endereço em pstr será de um dado em .rodata, o endereço de vstr[0] estará na pilha do espaço de memória, onde alterações são permitidas.

A compilação e execução do exemplo deixa isso bem claro:

:~$ gcc testes.c
:~$ a.out
0x7ffc25b8c0b1 --> bacana   <-- endereço na pilha
0x558add3cc004 --> calado   <-- endereço em .rodata