mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-09 10:16:35 -03:00
444 lines
12 KiB
Org Mode
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
|
|
|