cblc/aulas/06-vetores/README.org

444 lines
12 KiB
Org Mode

#+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 (=*<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.
#+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 <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;
}
#+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 <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);
}
#+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 <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;
}
#+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 <stdio.h>
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