Cópia vs Referência em programação + outras dúvidas :P

Boa tarde. Alguém poderia explicar a diferença dos dois ? Se possível com exemplos, pode ser qualquer linguagem, e também de uma forma que um animal racional com falta de habilidade intelectual consiga entender, que é o meu caso :P.

Outra dúvida sobre compilação:
Resolvido pelo @romulopb

1 curtida
int main() {
    int numero = 6;
    somarUm(numero);
}

void somarUm(int num) {
    num++;
    cout << "Valor = " << num << endl;
}

No exemplo acima passamos o valor da variável numero para a função somarUm, certo? Quando essa função recebe o valor de numero, ela CRIA OUTRA variável chamada num que é uma cópia da variável numero. Isso significa que mais memória será utilizada.

int main() {
    int numero = 6;
    somarUm(numero);
}

void somarUm(int &num) {
    num++;
    cout << "Valor = " << num << endl;
}

Agora passamos o argumento para a função por referência. Nesse caso o num é um APELIDO para a variável numero, ou seja, não é armazenada mais memória, pois a referência está indicando qual é o endereço de memória da variável numero.

2 curtidas

Dá para entender melhor a diferença entre referências e cópias pensando na versão mais “raiz” das referências, os ponteiros. Os ponteiros são basicamente variáveis contendo o local de memória onde uma variável está, e o tamanho deles é (simplificando um pouco) a quantidade de bits da máquina.

A principal consequência tanto de ponteiros quando de referências é que é possível uma função editar variáveis de outra:

#include <stdio.h>

int somarUm(int *numero) {
  // podemos alterar o conteúdo da memória na localização apontada por numero
  // usando o operador *.
  // OBS: usando int& no C++, podemos omitir o *.
  (*numero)++;
}

int main(void) {
  int numero = 2;
  somarUm(&numero);
  printf("Número: %d\n", numero);
}

Numa cópia (só int numero em vez de int *numero ou int &numero), a alteração só se refletiria na própria função. Por isso, bibliotecas no geral pedem ponteiros/referências.


Como o @Melk citou, outra propriedade é que referências e ponteiros consomem muito menos memória. Não é tão importante assim para int (na verdade, teoricamente compensaria fazer a cópia, pois na arquitetura x86_64 o int tem 32 bits e ponteiros têm 64 bits), mas para arrays ou estruturas de dados mais compridas, compensa muito não ter que ler os dados de um local, transcrevê-los para outro, para ler uma ou outra coisinha e descartá-los.


Antecipando: a diferença entre ponteiros e referências é que referências possuem salvaguardas adicionais. Ponteiros “brutos” podem mudar para onde apontam, e podem inclusive apontar para variáveis que não existem, sendo esse último item a causa clássica dos erros “Segmentation Fault” (no Linux) ou “Access Violation” (no Windows). Já referências devem apontar para uma variável que exista, e dependendo da linguagem, não podem mudar de variável.


Em linguagens que não têm esses conceitos de cópia e ponteiro/referência explícitos, como Python, precisamos ver a documentação. Por exemplo, sabendo que em Python listas são sempre referências, e que ao usar [x,y,z] você na verdade está criando uma lista do nada e puxando a referência dela, o fato do código a seguir soltar [1, 2, 3, 4] está explicado:

super_lista = [1,2,3]

def coisa(l):
    # É uma referência, vai se refletir na super_lista de fora da função.
    l.append(4)
    # Mudei a referência, agora l aponta para essa lista que eu criei agora.
    l = [1, 2]
    # Não vai afetar a super_lista.
    l.append(44)

coisa(super_lista)

print(super_lista)
2 curtidas

Desculpe-me pela ignorância, ponteiros em C++ e C para mim é um “bicho de sete cabeças”, irei estudar mais a fundo sobre isso. Obrigado pela ajuda @Melk e @Capezotte :heart:

1 curtida

Pessoal, desculpa por não criar outro tópico, não quero “floodar” de tópicos :P. Como eu faço para auto-compilar um código C/C++ no neovim, principalmente quando eu salvo ele? Dei uma pesquisada e vi que muitas pessoas utilizam o makefile, porém não faço ideia de como usar essa bomba :P.

Eu tenho uma keybind para compilar, porém como eu disse acima, eu gostaria de auto-compilar quando eu salvo o arquivo.
Minha keybind:

-- Mapeando F5 para compilar códigos C ou C++ {{{
vim.cmd([[
  augroup c_cpp_compile
    autocmd!
    autocmd FileType c,cpp nnoremap <F5> :lua if vim.fn.expand("%:e") == "c" then vim.cmd("!gcc -o %:r %") elseif vim.fn.expand("%:e") == "cpp" then vim.cmd("!g++ -o %:r %") end<CR>
  augroup END
]])

--}}}

Não é uma prática comum em código C/C++, mas você poderia fazer um shellscript com inotify que ficaria observando certos arquivos para compilar automaticamente. Algo como:

#!/bin/bash

while true; do
    inotifywait -e modify programa.c
    gcc programa.c -o programa
done

Também deve existir talvez alguma extensão no VSCode para isso.

Já ferramentas como Makefile ou CMake tem objetivos diferentes. São uma prática comum, pois projetos crescem e passam a envolver bibliotecas externas, muitos arquivos, variáveis de ambiente e demandar compilações diferenciadas com flags específicas em cada etapa do desenvolvimento (debugging/test/staging/production…). Também, usar uma ferramenta de building ajuda a não reconstruir partes que não mudaram e facilita na hora de paralelizar o build de partes independentes do projeto.

1 curtida

Valeu meu chanceler @romulopb, deu super certo aqui :slightly_smiling_face:
Esse utilitário “ìnotify” é muito bacana para criação de scripts!

Que bom que se achou. No tocante ao Makefile, a linguagem meio que funciona como uma especificação de regras com dependências, por exemplo, para obter main.o precisamos de main.c:

main.o: main.c
    gcc -c main.c

e para compilar um programa que depende de main.c e util.c:

prog: main.o util.o
    gcc -o prog main.o util.o

main.o: main.c
    gcc -c main.c

util.o: util.c
    gcc -c util.c

Podemos ter receitas/targets , que não são arquivos:

clean:
    rm -f *.o prog

.PHONY: clean

Você pode criar quantas receitas quiser, install, uninstall, clean, etc.

Pode usar regras genéricas:

# Regra genérica para compilar qualquer arquivo .c em um arquivo .o
%.o: %.c
    gcc -c $< -o $@

Pode definir variáveis e usar nas regras:

# Arquivos fonte
SRC := main.c

# Alvo padrão (quando você simplesmente executa 'make')
all: debug

# Alvo para modo de desenvolvimento
debug: CFLAGS := -g -Wall
debug: gcc $(CFLAGS) $(SRC) -o prog

# Alvo para modo de release
release: CFLAGS: = -O2 -Wall
release: gcc $(CFLAGS) $(SRC) -o prog

# Alvo para limpar os artefatos de compilação
clean:
	rm -f prog

.PHONY: debug release clean

onde você pode usar com make clean, e assim por diante. Lembre que são apenas exemplos, vá estudando e procurando padrões e boas práticas, explore projetos consagrados no github, assista aulas de grandes nomes no C, C++… Por exemplo, é uma boa prática gerar artefatos de build em uma pasta separada, chamada build, guardar o código em uma pasta src, etc.

2 curtidas

Entendi boa parte. Maioria dos programas que eu baixo pelo código fonte, por exemplo o cmus ou o dwm, eles utilizam o makefile para compilar o projeto, por exemplo o uninstall que você citou acima, no projeto do dwm o “make uninstall” irá remover os binários compilados do /usr/local/bin, caso tenha instalado como root, e caso o programa não tenha o uninstall, teria que colocar essa opção no makefile e criar a receita para tal.

Não entendi muito bem sobre os arquivos “.o”, oque seriam eles? Já vi esses arquivos em código fontes, até mesmo os “.h” e etc, eu comecei direto no C++, então não sei muito bem.
Desde já, agradeço pela ajuda e tempo de responder :slight_smile:

Arquivos .o (podem aparecer como .obj em projetos do Windows) são arquivos objeto, são como pedaços de um programa já transformado em código de máquina, mas que sozinhos não formam um executável, representam diretamente um arquivo.c compilado e podem ser usados para gerar o programa/biblioteca final.

Já os .h são arquivos de cabeçalho, eles contem as interfaces ou assinaturas, é o que você usa no include, você teria algo como:

// soma.h
int add(int a, int b);
// soma.c
int add(int a, int b) {
    return a + b;
}
// main.c
#include "soma.h"

int main() {
    int resultado = add(3, 4);
    ...
}

também temos bibliotecas estáticas (.a) e dinâmicas (.so). No Windows são .lib e .dll

1 curtida

Bom dia. Vocês sabem dizer a diferença de *(arr + i)
para o arr[i] ?
Segue o código:

#include <iostream> // saída e entrada padrão

// Prototipando {{{
int somaArray(int *arr, int tamanho);
// }}}

// main {{{
int main() {

  int lista[5] = {1, 2, 3, 4, 5};
  int soma = somaArray(lista, 5);

  std::cout << "soma da lista: " << soma << '\n';

  std::cout << "endereço de lista: " << &lista << '\n';

  return 0;
} // main fim }}}


// Funções {{{
int somaArray(int *arr, int tamanho) {
  int soma = 0;
  
  // for genérico
  for (int i = 0; i < tamanho; i++) {
    soma += arr[i]; // NOTE: alternativamente *(arr + i);
  }

  std::cout << "endereço apontado por *arr: " << arr << '\n';
  return soma;
}
// }}}

Não tem diferença, o padrão C literalmente define x[y] dizendo que deve equivaler a *(x+y).

E sim, você pode pegar um ponteiro e ficar incrementando ele para avançar nos elementos de uma lista/string; por exemplo:

// Usamos *x como critério de continuar porque toda string em C deve terminar com um byte de valor zero.
for (char *x = "nome"; *x; x++) printf("ASCII: %hhd\n", *x);
2 curtidas

Eu realmente não entendi. Por que arr + i e por que o uso dos parenteses, e por que o * tem que estar fora dos parenteses? Resumindo, não entendi nada kkkkkkkkkkkkkkk. Desculpe-me pela ignorância, eu gosto de ir até os últimos detalhes :P.

  1. *(arr + i)
  • ‘arr’ é uma variável que contem um endereço de memória.
  • Pegue esse endereço e adicione ‘i’ variáveis a frente (seguindo o mesmo tipo da variável ‘arr’)
  • ‘*’ indica retornar o conteúdo do endereço que vem depois dele, no caso é endereço de memória calculado anteriormente, por isso precisa dos parenteses. Se fosse *arr + i retornaria o conteúdo do endereço de ‘arr’, daí somaria o valor de ‘i’.
  1. arr[i]
  • Retorna o conteúdo do ‘i’-ésimo termo da array. Internamente o compilador vai procurar o tipo de ‘arr’ e adicionar ‘i’ vezes o tamanho desse tipo para retornar o valor do endereço de memória do local.

Quando o compilador está fazendo operações com ponteiros, ele precisa saber o tipo da variável para poder avançar corretamente o local. Isso quer dizer que se a variável ‘arr’ for do tipo ponteiro para ‘char’ (um byte), e ‘arr’ for o endereço 0x0300, e ‘i’ for 2, então arr + i retorna 0x3002. Mas se ‘arr’ for tipo ponteiro para inteiro (um inteiro são 4 bytes), então arr + i retorna 0x3008.

Do primeiro jeito é explícito o fato do que o compilador deve fazer, enquanto no segundo jeito fica “invisível” pro programador o que está acontecendo na máquina.

2 curtidas

O que o pessoal te explicou se trata de aritmética de ponteiros. É um comportamento implícito, onde você pode “navegar” entre endereços usando múltiplos do tamanho do tipo do ponteiro, porém existem várias pequenas nuances:

  • para int arr[n];
int arr[10];
int* ptr = arr + 2;   // OK
arr = arr + 1;   // ERRO
  • Para int* ptr;
int x = 5;
int* ptr = &x;
ptr = ptr + 1;  // OK
  • para int** ints_ptr;
int x = 5;
int* ptr = &x;
int** ints_ptr = &ptr;
ints_ptr = ints_ptr + 1;  // OK, avança em multiplos de sizeof(int*), não sizeof(int)

Claro, esteja sempre atento ao fato que em C e C++ não existe bondary check para aritmética de ponteiros. Durante o desenvolvimento pode ser interessante usar as flags -fsanitize=boundse -fsanitize=address para detectar sempre que possível se você está tentando acessar algo fora dos limites.

E sim, como qualquer aritmética, você pode dividir, multiplicar, subtrair…


Um caso interessante é com void*, não é possível fazer aritmética com estes ponteiros sem ter um tipo definido:

void* ptr;
char* char_ptr;
...
char_ptr = (char*)ptr + 5;
ptr = (void*)char_ptr;

Estes ponteiros são geralmente usados para tipos opacos dentre outras coisas, por exemplo:

void foo(int x) { 
    ... 
}

void call_func(void (*f)(int)) { 
    f(42);
}

Acho que você já percebeu que ponteiros dão para C um “gostinho” de orientação a objetos. :slightly_smiling_face:

1 curtida

Boa noite! Por que eu não preciso passar o endereço do vetor na função ptar() ?
Segue o código como exemplo:

#include <iostream>

// prototipando
void ptar(float *v);


int main () {
  // {{{  ...

  float vetor[5];

  ptar(vetor);

  // for ranged-base para imprimir os elementos do
  // array (vetor).
  for (float number : vetor) {
    std::cout << number << '\n';
  }

  std::cout << "\n\n";

  // mostrando o endereço dos elementos de (vetor)
  for (int i = 0; i < 5; i++) {
    std::cout << &vetor[i] << '\n';
  }

  return 0;
  // end }}}
}

void ptar(float *v) {
  // mudando os elementos para 0
  for (int i = 0; i < 5; i++) {
    v[i] = 0;
  }

  // mostrando os endereços apontado por *v
  for (int i = 0; i < 5; i++) {
    std::cout << &v[i] << '\n';
  }
}

Em C e C++, vetores já servem como ponteiros para o primeiro elemento deles. A intenção é que, a partir do endereço do primeiro elemento, basta usar o operador de índice [n] (ou de modo mais “cru”, aritmética de ponteiros), para achar os demais.

Passando o endereço do vetor, você vai ter de constantemente usar * de “acessar ponteiro” em conjunto com o [] de índice.

2 curtidas

Arrays são entidades especiais, em diversas situações eles se comportam como ponteiro do seu tipo base, ou melhor dizendo, decaem para um ponteiro do seu tipo base:

int arr[5];
int *ptr = arr;

Mas &arr é um ponteiro de int? Não, veja:

int arr[5];
int (*arr_ptr)[5] = &arr;
int *ptr = arr
// ptr++ não vai ter o mesmo comportamento de arr_ptr++

Você também vai ver peculiaridades como sizeof(arr) devolvendo um valor diferente de sizeof(*ptr).

3 curtidas

De começo eu tentei usar a lógica ( argv[1] == "--help" ), porém sem converter para o tipo std::string, o que me retornou um erro, nisso eu converti o tipo, porém mesmo assim é confuso, pois eu vi que o argv tem o [ ], o que significa que é um array, afinal que bruxaria é essa? :thinking: Também tenho outra dúvida, eu transformei em string o argv em geral ou apenas o o 1 índice, que seria o “–help” passado?

Se estiver confuso de entender, me avise para eu poder melhorar a pergunta :D.

Eu não entendi bem o que você está achando que é uma bruxaria ou é confuso.

str::string → Classe string do C++
char *argv[ ] → array de C-style strings

Você está esperando muita abstração de algo em estilo C, na prática quando você faz (argv[1] == "--help") você está comparando um ponteiro com uma string literal (que na prática, aqui decai para um ponteiro também). (*argv[1] == "--help") também não daria certo, pois *argv[1] derreferencia para o valor do primeiro endereço dessa string estilo C, um caractere. Se você quer continuar no reino do C, deve usar (strcmp(argv[1], "--help") == 0).

Classes em C++ tem suporte ao que chamamos de operator overloading, onde implementamos um método para um operador, no caso de std::string, existe algo como:

bool operator==(const string& stringObj_direita) const { ... }
bool operator==(const char* stringC_direita) const { ... }
...
1 curtida