Arrays em funções

Quando sua função main está enorme com inúmeras linhas de código e você tem a brilhante ideia de separar em funções menores, infelizmente essa árdua tarefa pode ter um comportamento inesperado ao passar um array para uma função.

Por exemplo, o código a seguir funciona perfeitamente na main:

int main() {
  int array[] = {5, 1, 2, 4, 3};

  std::size_t length = sizeof(array) / sizeof(array[0]);
  for (int i = 0; i < length; i++) {
    std::println("{}° elemento: {}", i+1, array[i]);
  }
  return 0;
}
/*
resultado:

1° elemento: 5
2° elemento: 1
3° elemento: 2
4° elemento: 4
5° elemento: 3 */

Mas se você abstrair para um função print_array o resultado pode ser inesperado ao passar seu array como parametro da função:

void print_array(int array[]) {
  std::size_t length = sizeof(array) / sizeof(array[0]);
  for (int i = 0; i < length; i++) {
    std::println("{}° elemento: {}", i+1, array[i]);
  }
}

int main() {
  int array[] = {5, 1, 2, 4, 3};
  print_array(array);
  return 0;
}
/*
resultado:

1° elemento: 5
2° elemento: 1 */

Bem… Por que isso acontece?

Vamos comparar os tamanhos nas funções main e print_array:

void print_array(int array[]) {
  std::println("\nPRINT_ARRAY");
  std::println("tamanho da array: {} bytes", sizeof(array));
  std::println("tamanho do elemento: {} bytes", sizeof(array[0]));
  return;
  //
}

int main() {
  int array[] = {5, 1, 2, 4, 3};
  std::println("MAIN");
  std::println("tamanho da array: {} bytes", sizeof(array));
  std::println("tamanho do elemento: {} bytes", sizeof(array[0]));

  print_array(array);
  return 0;
}
/*
resultado:

MAIN
tamanho da array: 20 bytes
tamanho do elemento: 4 bytes

PRINT_ARRAY
tamanho da array: 8 bytes
tamanho do elemento: 4 bytes */

Ao passar meu array para uma função, o array decai para um ponteiro.
Ou seja, ambas as assinaturas são equivalentes:

void print_array(int array[])

void print_array(int* array)

Na minha máquina, 64 bits, um ponteiro tem 8 bytes, por isso ainda mostra 2 elementos da array. Em uma máquina de 32 bits talvez mostrasse menos elementos.
Esse comportamento é inconsistente, se meu array tiver 2 elementos eu vou achar que o código está correto, quando não está.

Algumas maneiras de passar arrays em c++

Passe o tamanho para a função

Passe a array em si e o tamanho que ela tem para sua função. Por exemplo:

void print_array(int array[], int size) {
  for (int i = 0; i < size; i++) {
    std::println("{}° elemento: {}", i+1, array[i]);
  }
}

int main() {
  int array[] = {5, 1, 2, 4, 3};
  int size = sizeof(array) / sizeof(array[0]);
  print_array(array, size);
  return 0;
}
/*
resultado:

1° elemento: 5
2° elemento: 1
3° elemento: 2
4° elemento: 4
5° elemento: 3 */

Referências

Referências não decaem para ponteiros (embora a sintaxe seja um pouco estranha a principio: type (&name)[size]), então é possível receber uma referência de um array como abaixo:

void print_array(int (&array)[5]) {
  for (int i = 0; i < 5; i++) {
    std::println("{}° elemento: {}", i+1, array[i]);
  }
}

int main() {
  int array[] = {5, 1, 2, 4, 3};
  print_array(array);
  return 0;
}
/*
resultado:

1° elemento: 5
... */

Infelizmente nessa abordagem é preciso saber o tipo e tamanho do array que a função deve receber e isso nem sempre é prático.

Templates e referências

É aí que entram Templates e Referências, ferramentas poderosas que consegue resolver alguns dos “problemas” citados acima. Templates permitem que o compilador deduza o tipo do parâmetro e o tamanho recebido.

template<typename T, std::size_t N>
void print_array(T (&array)[N]) {
  for (int i = 0; i < N; i++) {
    std::println("{}° elemento: {}", i+1, array[i]);
  }
}

int main() {
  int array[] = {5, 1, 2, 4, 3};
  print_array(array);
  return 0;
}
/*
resultado:

1° elemento: 5
...*/

E por ser uma referência, não decai para ponteiros e mantêm a informação do tamanho ao usar sizeof

template<typename T, std::size_t N>
void print_array(T (&array)[N]) {
  std::println("\nPRINT_ARRAY");
  std::println("tamanho da array: {} bytes", sizeof(array));
  std::println("tamanho do elemento: {} bytes", sizeof(array[0]));
  ...
}

/*
resultado:

PRINT_ARRAY
tamanho da array: 20 bytes  // mesmo que na main!
tamanho do elemento: 4 bytes
1° elemento: 5
... */

Outra vantagem dessa abordagem é que a função é genérica o suficiente, aceita diversos tipos e tamanhos diferentes, sem precisar alterar seu código.
Por exemplo, agora passarei um int[5] e double[3] normalmente:

template<typename T, std::size_t N>
void print_array(T (&array)[N]) {
  for (int i = 0; i < N; i++) {
    std::println("{}° elemento: {}", i+1, array[i]);
  }
}

int main() {
  int array[] = {5, 1, 2, 4, 3};
  double double_array[] = {3.14, 1.6, 1.33};
  print_array(array);
  print_array(double_array);
  return 0;
}
/*
1° elemento: 5
2° elemento: 1
3° elemento: 2
4° elemento: 4
5° elemento: 3
1° elemento: 3.14
2° elemento: 1.6
3° elemento: 1.33*/

C++ tem std::array e std::vector!!

Em C++ moderno é preferível usar std::array se você não precisar alocar dinamicamente o array ou std::vector se o tamanho precisar ser dinâmico. Entretanto isso é assunto para outra postagem :)