#+title: Curso Básico da Linguagem C #+subtitle: Aula 6: Vetores #+author: Blau Araujo #+startup: show2levels #+options: toc:3 * Aula 6: Vetores [[https://youtu.be/W5TGNQYFs4E][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/. #+begin_quote 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/. #+end_quote ** 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:* #+begin_src c 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; #+end_src *Com vetores...* #+begin_src c 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; #+end_src Nós também poderíamos inicializar o vetor na declaração: #+begin_src c 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; #+end_src 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 (=*=). - Para obter o endereço do valor associado a uma variável, nós usamos =&=. - Para obter o endereço do primeiro elemento do vetor, nós avaliamos o nome do vetor. - A operação =&= avalia o endereço de todo o conjunto de elementos do vetor. - Para obter os valores dos demais elementos, nós somamos =*( + í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 (=[í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. #+begin_quote Quantidade de dados não é, necessariamente, a mesma coisa que /volume de dados/! #+end_quote ** 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á: #+begin_src c float notas[4]; #+end_src 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: #+begin_src c float notas[4] = {7.5, 9.0, 8.3, 8.6}; #+end_src *** 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: #+begin_src c float notas[] = {7.5, 9.0, 8.3, 8.6}; // 4 elementos... #+end_src *Importante!* Isso só vale para a inicialização de vetores nas suas declarações: #+begin_src c float notas[]; // Errado! #+end_src *** 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: #+begin_src c int num[5] = {1, 2}; // => {1, 2, 0, 0, 0} #+end_src O que possibilita, inclusive, inicializar um vetor /zerado/: #+begin_src c int num[5] = {0}; // => {0, 0, 0, 0, 0} #+end_src ** 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=: #+begin_src c 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). #+end_src ** Iteração pelos elementos Nós podemos iterar os elementos de um vetor em /loops/: #+begin_src c #include 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; } #+end_src *Compilando e executando...* #+begin_example :~$ 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 #+end_example ** Vetores e funções Vetores não podem ser passados para funções, mas os seus endereços podem: #+begin_src c #include 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); } #+end_src *Compilando e executando...* #+begin_example :~$ testes.c :~$ a.out v[0] => 1 v[1] => 2 v[2] => 3 v[3] => 4 v[4] => 5 #+end_example #+begin_quote *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. #+end_quote 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: #+begin_src c 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... } #+end_src 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: #+begin_src c 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... } #+end_src *** 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. #+begin_src c #include 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; } #+end_src *Compilando e executando...* #+begin_example :~$ 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 #+end_example 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... #+begin_src c char string[] = "banana"; #+end_src É o mesmo que... #+begin_src c char string[] = {'b', 'a', 'n', 'a', 'n', 'a', '\0}; #+end_src *** Mais diferenças entre vetores e ponteiros Observe o exemplo... #+begin_src c #include int main() { char vstr[] = "bacana"; char *pstr = "calado"; printf("%p --> %s\n", vstr, vstr); printf("%p --> %s\n", pstr, pstr); return 0; } #+end_src Essas strings são diferentes: #+begin_src c char vstr[] = "bacana"; char *pstr = "calado"; #+end_src 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=: #+begin_example :~$ readelf -x .rodata a.out Despejo máximo da secção ".rodata": 0x00002000 01000200 63616c61 646f0025 70202d2d ....calado.%p -- 0x00002010 3e202573 0a00 > %s.. #+end_example 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=: #+begin_example :~$ 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 ............... #+end_example #+begin_quote As seções do binário e os segmentos do layout de memória serão assunto de uma aula futura. #+end_quote 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: #+begin_example :~$ gcc testes.c :~$ a.out 0x7ffc25b8c0b1 --> bacana <-- endereço na pilha 0x558add3cc004 --> calado <-- endereço em .rodata #+end_example