Como uso matrizes em C ++?


480

C ++ herdou matrizes de C, onde são usadas praticamente em qualquer lugar. C ++ fornece abstrações que são mais fáceis de usar e menos propenso a erros ( std::vector<T>desde C ++ 98 e std::array<T, n>desde C ++ 11 ), de modo que a necessidade de matrizes não surge tão frequentemente como faz em C. No entanto, quando você lê legado codificar ou interagir com uma biblioteca escrita em C, você deve ter uma noção firme de como as matrizes funcionam.

Este FAQ está dividido em cinco partes:

  1. matrizes no nível de tipo e elementos de acesso
  2. criação e inicialização de array
  3. atribuição e passagem de parâmetros
  4. matrizes multidimensionais e matrizes de ponteiros
  5. armadilhas comuns ao usar matrizes

Se você sentir que algo importante está faltando nesta FAQ, escreva uma resposta e vincule-a aqui como uma parte adicional.

No texto a seguir, "matriz" significa "matriz C", não o modelo de classe std::array. O conhecimento básico da sintaxe do declarador C é assumido. Observe que o uso manual de newe deletecomo demonstrado abaixo é extremamente perigoso diante de exceções, mas esse é o tópico de outra FAQ .

(Observação: isso deve ser uma entrada para as Perguntas frequentes sobre C ++ do Stack Overflow . Se você quiser criticar a idéia de fornecer uma FAQ neste formulário, a postagem na meta que iniciou tudo isso seria o lugar para isso. essa pergunta é monitorada na sala de bate-papo do C ++ , onde a ideia das Perguntas frequentes começou em primeiro lugar; portanto, é muito provável que sua resposta seja lida por quem a teve.)


Eles seria ainda melhor se os ponteiros sempre apontou para a começando em vez de em algum lugar no meio do seu alvo embora ...
Deduplicator

Você deve usar o vetor STL porque ele oferece maior flexibilidade.
Moiz Sajid 30/08/2015

2
Com a disponibilidade combinada de std::arrays, std::vectors e gsl::spans - Eu francamente esperar um FAQ sobre como usar matrizes em C ++ para dizer "Até agora, você pode começar a considerar que, bem, não usá-los."
Einpoklum 19/03

Respostas:


302

Matrizes no nível de tipo

Um tipo de matriz é indicado como T[n]onde Testá o tipo de elemento e né um tamanho positivo , o número de elementos na matriz. O tipo de matriz é um tipo de produto do tipo de elemento e do tamanho. Se um ou ambos os ingredientes diferirem, você obtém um tipo distinto:

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

Observe que o tamanho faz parte do tipo, ou seja, tipos de matriz de tamanho diferente são tipos incompatíveis que não têm absolutamente nada a ver um com o outro. sizeof(T[n])é equivalente a n * sizeof(T).

Decaimento de matriz para ponteiro

A única "conexão" entre T[n]e T[m]é que ambos os tipos podem ser implicitamente convertidos em T*, e o resultado dessa conversão é um ponteiro para o primeiro elemento da matriz. Ou seja, em qualquer lugar que T*seja necessário, você pode fornecer um T[n]e o compilador silenciosamente fornecerá esse ponteiro:

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

Essa conversão é conhecida como "decaimento de matriz para ponteiro" e é uma importante fonte de confusão. O tamanho da matriz é perdido nesse processo, pois não faz mais parte do tipo ( T*). Pro: esquecer o tamanho de uma matriz no nível de tipo permite que um ponteiro aponte para o primeiro elemento de uma matriz de qualquer tamanho. Con: dado um ponteiro para o primeiro (ou qualquer outro) elemento de uma matriz, não há como detectar o tamanho dessa matriz ou para onde exatamente o ponteiro aponta em relação aos limites da matriz. Os ponteiros são extremamente estúpidos .

Matrizes não são ponteiros

O compilador silenciosamente irá gerar um ponteiro para o primeiro elemento de uma matriz sempre que for considerado útil, ou seja, sempre que uma operação falhar em uma matriz, mas for bem-sucedida em um ponteiro. Essa conversão de matriz para ponteiro é trivial, pois o valor resultante do ponteiro é simplesmente o endereço da matriz. Observe que o ponteiro não é armazenado como parte da própria matriz (ou em qualquer outro lugar na memória). Uma matriz não é um ponteiro.

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

Um contexto importante em que uma matriz não se decompõe em um ponteiro para seu primeiro elemento é quando o &operador é aplicado a ela. Nesse caso, o &operador gera um ponteiro para toda a matriz, não apenas um ponteiro para o primeiro elemento. Embora nesse caso os valores (os endereços) sejam os mesmos, um ponteiro para o primeiro elemento de uma matriz e um ponteiro para toda a matriz são tipos completamente distintos:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

A seguinte arte ASCII explica essa distinção:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

Observe como o ponteiro para o primeiro elemento aponta apenas para um único número inteiro (representado como uma caixa pequena), enquanto o ponteiro para a matriz inteira aponta para uma matriz de 8 números inteiros (representados como uma caixa grande).

A mesma situação surge nas aulas e é talvez mais óbvia. Um ponteiro para um objeto e um ponteiro para seu primeiro membro de dados têm o mesmo valor (o mesmo endereço), mas são tipos completamente distintos.

Se você não estiver familiarizado com a sintaxe do declarador C, os parênteses no tipo int(*)[8]são essenciais:

  • int(*)[8] é um ponteiro para uma matriz de 8 números inteiros.
  • int*[8]é uma matriz de 8 pontos, cada elemento do tipo int*.

Acessando elementos

O C ++ fornece duas variações sintáticas para acessar elementos individuais de uma matriz. Nenhum deles é superior ao outro, e você deve se familiarizar com os dois.

Aritmética do ponteiro

Dado um ponteiro ppara o primeiro elemento de uma matriz, a expressão p+igera um ponteiro para o i-ésimo elemento da matriz. Ao remover a referência desse ponteiro posteriormente, é possível acessar elementos individuais:

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

Se xdenota uma matriz , o decaimento da matriz para o ponteiro entra em ação, porque adicionar uma matriz e um número inteiro não faz sentido (não há mais operação nas matrizes), mas a adição de um ponteiro e um número inteiro faz sentido:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(Observe que o ponteiro gerado implicitamente não tem nome, então escrevi x+0para identificá-lo.)

Se, por outro lado, xdenota um ponteiro para o primeiro (ou qualquer outro) elemento de uma matriz, o decaimento de matriz para ponteiro não é necessário, porque o ponteiro ao qual iserá adicionado já existe:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

Observe que, no caso representado, xé uma variável de ponteiro (discernível pela caixa pequena ao lado de x), mas também poderia ser o resultado de uma função retornando um ponteiro (ou qualquer outra expressão do tipo T*).

Operador de indexação

Como a sintaxe *(x+i)é um pouco desajeitada, o C ++ fornece a sintaxe alternativa x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;

Devido ao fato da adição ser comutativa, o código a seguir faz exatamente o mesmo:

std::cout << 3[x] << ", " << 7[x] << std::endl;

A definição do operador de indexação leva à seguinte equivalência interessante:

&x[i]  ==  &*(x+i)  ==  x+i

No entanto, &x[0]geralmente não é equivalente a x. O primeiro é um ponteiro, o segundo uma matriz. Somente quando o contexto dispara a deterioração da matriz para o ponteiro pode xe pode &x[0]ser usado de forma intercambiável. Por exemplo:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

Na primeira linha, o compilador detecta uma atribuição de um ponteiro para um ponteiro, o que é trivialmente bem-sucedido. Na segunda linha, ele detecta uma atribuição de uma matriz para um ponteiro. Uma vez que este é o sentido (mas ponteiro para atribuição ponteiro faz sentido), pontapés decaimento matriz-a-ponteiro em como de costume.

Gamas

Uma matriz do tipo T[n]possui nelementos indexados de 0para n-1; não há nenhum elemento n. E, no entanto, para oferecer suporte a intervalos semi-abertos (onde o começo é inclusivo e o final é exclusivo ), o C ++ permite o cálculo de um ponteiro para o n-ésimo elemento (inexistente), mas é ilegal desreferenciar esse ponteiro:

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

Por exemplo, se você deseja classificar uma matriz, os dois itens a seguir funcionam igualmente bem:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

Observe que é ilegal fornecer &x[n]como o segundo argumento, já que isso é equivalente a &*(x+n), e a subexpressão *(x+n)invoca tecnicamente um comportamento indefinido em C ++ (mas não em C99).

Observe também que você pode simplesmente fornecer xcomo o primeiro argumento. Isso é um pouco conciso demais para o meu gosto e também torna a dedução do argumento do modelo um pouco mais difícil para o compilador, porque nesse caso o primeiro argumento é uma matriz, mas o segundo argumento é um ponteiro. (Novamente, a deterioração da matriz para o ponteiro entra em ação.)


Casos em que a matriz não se decompõe em um ponteiro são ilustrados aqui para referência.
precisa saber é o seguinte

@fredoverflow Na parte Access ou Ranges, vale a pena mencionar que as matrizes C funcionam com C ++ 11 com base no intervalo para loops.
gnzlbg

135

Os programadores geralmente confundem matrizes multidimensionais com matrizes de ponteiros.

Matrizes multidimensionais

Muitos programadores estão familiarizados com as matrizes multidimensionais nomeadas, mas muitos desconhecem o fato de que a matriz multidimensional também pode ser criada anonimamente. Matrizes multidimensionais são frequentemente chamadas de "matrizes de matrizes" ou " matrizes multidimensionais verdadeiras ".

Matrizes multidimensionais nomeadas

Ao usar matrizes multidimensionais nomeadas, todas as dimensões devem ser conhecidas no momento da compilação:

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

É assim que uma matriz multidimensional denominada se parece na memória:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

Observe que as grades 2D, como as anteriores, são apenas visualizações úteis. Do ponto de vista do C ++, a memória é uma sequência "plana" de bytes. Os elementos de uma matriz multidimensional são armazenados na ordem principal da linha. Ou seja, connect_four[0][6]e connect_four[1][0]são vizinhos na memória. De fato, connect_four[0][7]e connect_four[1][0]denotam o mesmo elemento! Isso significa que você pode pegar matrizes multidimensionais e tratá-las como matrizes unidimensionais grandes:

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

Matrizes multidimensionais anônimas

Com matrizes multidimensionais anônimas, todas as dimensões, exceto a primeira, devem ser conhecidas em tempo de compilação:

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

É assim que uma matriz multidimensional anônima se parece na memória:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

Observe que o próprio array ainda está alocado como um único bloco na memória.

Matrizes de ponteiros

Você pode superar a restrição de largura fixa introduzindo outro nível de indireção.

Matrizes nomeadas de ponteiros

Aqui está uma matriz nomeada de cinco ponteiros que são inicializados com matrizes anônimas de diferentes comprimentos:

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

E aqui está como fica na memória:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

Como cada linha é alocada individualmente agora, visualizar matrizes 2D como matrizes 1D não funciona mais.

Matrizes anônimas de ponteiros

Aqui está uma matriz anônima de 5 (ou qualquer outro número de) ponteiros que são inicializados com matrizes anônimas de diferentes comprimentos:

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

E aqui está como fica na memória:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

Conversões

A deterioração de matriz para ponteiro se estende naturalmente a matrizes de matrizes e matrizes de ponteiros:

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

No entanto, não há conversão implícita de T[h][w]para T**. Se tal conversão implícita existisse, o resultado seria um ponteiro para o primeiro elemento de uma matriz de hponteiros para T(cada um apontando para o primeiro elemento de uma linha na matriz 2D original), mas essa matriz de ponteiros não existe em nenhum lugar memória ainda. Se você deseja essa conversão, deve criar e preencher manualmente a matriz de ponteiros necessária:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

Observe que isso gera uma visão da matriz multidimensional original. Se você precisar de uma cópia, crie matrizes extras e copie os dados:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;

Como sugestão: você deve salientar que int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];assim como int (*p)[W] = new int[6][W];e int (*p)[W] = new int[H][W];são declarações válidas, quando He Wsão conhecidas em tempo de compilação.
RobertS apoia Monica Cellio

88

Tarefa

Por nenhum motivo específico, as matrizes não podem ser atribuídas uma à outra. Use em std::copyvez disso:

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

Isso é mais flexível do que o que a verdadeira atribuição de matriz poderia fornecer porque é possível copiar fatias de matrizes maiores em matrizes menores. std::copygeralmente é especializado em tipos primitivos para oferecer desempenho máximo. É improvável que tenha um std::memcpydesempenho melhor. Em caso de dúvida, meça.

Embora você não possa atribuir matrizes diretamente, é possível atribuir estruturas e classes que contêm membros da matriz. Isso ocorre porque os membros da matriz são copiados membro a membro pelo operador de atribuição, que é fornecido como padrão pelo compilador. Se você definir o operador de atribuição manualmente para seus próprios tipos de estrutura ou classe, deverá voltar à cópia manual para os membros da matriz.

Passagem de parâmetro

Matrizes não podem ser passadas por valor. Você pode passá-los por ponteiro ou por referência.

Passe pelo ponteiro

Como as próprias matrizes não podem ser passadas por valor, geralmente um ponteiro para o primeiro elemento é passado por valor. Isso geralmente é chamado de "passar pelo ponteiro". Como o tamanho da matriz não é recuperável por meio desse ponteiro, é necessário passar um segundo parâmetro indicando o tamanho da matriz (a solução C clássica) ou um segundo ponteiro apontando após o último elemento da matriz (a solução do iterador C ++) :

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

Como alternativa sintática, você também pode declarar parâmetros como T p[]e significa exatamente a mesma coisa que apenas T* p no contexto das listas de parâmetros :

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

Você pode pensar no compilador como reescrever T p[]a T *p no contexto de apenas listas de parâmetros . Essa regra especial é parcialmente responsável por toda a confusão sobre matrizes e ponteiros. Em qualquer outro contexto, declarar algo como uma matriz ou como um ponteiro faz uma enorme diferença.

Infelizmente, você também pode fornecer um tamanho em um parâmetro de matriz que é ignorado silenciosamente pelo compilador. Ou seja, as três assinaturas a seguir são exatamente equivalentes, conforme indicado pelos erros do compilador:

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

Passe por referência

As matrizes também podem ser passadas por referência:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

Nesse caso, o tamanho da matriz é significativo. Como escrever uma função que aceita apenas matrizes de exatamente 8 elementos é de pouca utilidade, os programadores geralmente escrevem funções como modelos:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

Observe que você só pode chamar esse modelo de função com uma matriz real de números inteiros, não com um ponteiro para um número inteiro. O tamanho da matriz é inferido automaticamente e, para cada tamanho n, uma função diferente é instanciada no modelo. Você também pode escrever modelos de função bastante úteis que abstraem do tipo de elemento e do tamanho.


2
Pode valer a pena acrescentar uma nota que, mesmo void foo(int a[3]) aque pareça estar passando a matriz por valor, a modificação ainterna de foomodificará a matriz original. Isso deve ficar claro, porque as matrizes não podem ser copiadas, mas pode valer a pena reforçar isso.
gnzlbg

C ++ 20 temranges::copy(a, b)
LF

int sum( int size_, int a[size_]);- a partir de (eu acho) C99 em diante
Chef Gladiator

73

5. Armadilhas comuns ao usar matrizes.

5.1 Armadilha: Confiando em links não seguros do tipo.

OK, você foi informado ou descobriu que globals (variáveis ​​de escopo de namespace que podem ser acessadas fora da unidade de tradução) são Evil ™. Mas você sabia como eles são realmente maus? Considere o programa abaixo, composto por dois arquivos [main.cpp] e [numbers.cpp]:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

No Windows 7, isso compila e vincula bem ao MinGW g ++ 4.4.1 e ao Visual C ++ 10.0.

Como os tipos não correspondem, o programa falha quando você o executa.

A caixa de diálogo de falha do Windows 7

Explicação formal: o programa tem comportamento indefinido (UB) e, em vez de travar, pode simplesmente travar, ou talvez não fazer nada, ou enviar e-mails ameaçadores aos presidentes dos EUA, Rússia, Índia, China e Suíça, e faça com que os Daemons Nasais voem pelo nariz.

Explicação na prática: na main.cppmatriz é tratada como um ponteiro, colocado no mesmo endereço da matriz. Para executável de 32 bits, isso significa que o primeiro intvalor na matriz é tratado como um ponteiro. Isto é, main.cppna numbersvariável contém, ou parece conter, (int*)1. Isso faz com que o programa acesse a memória na parte inferior do espaço de endereço, que é convencionalmente reservado e causa trapping. Resultado: você recebe um acidente.

Os compiladores têm pleno direito de não diagnosticar esse erro, porque C ++ 11 §3.5 / 10 diz sobre o requisito de tipos compatíveis para as declarações,

[N3290 §3.5 / 10]
A violação desta regra sobre identidade de tipo não requer diagnóstico.

O mesmo parágrafo detalha a variação permitida:

… As declarações para um objeto de matriz podem especificar tipos de matriz que diferem pela presença ou ausência de um limite de matriz principal (8.3.4).

Essa variação permitida não inclui declarar um nome como uma matriz em uma unidade de tradução e como um ponteiro em outra unidade de tradução.

5.2 Armadilha: Fazendo otimização prematura ( memsete amigos).

Ainda não escrito

5.3 Armadilha: Usando o idioma C para obter o número de elementos.

Com profunda experiência em C, é natural escrever…

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

Como um arraydecaimento para apontar para o primeiro elemento, quando necessário, a expressão sizeof(a)/sizeof(a[0])também pode ser escrita como sizeof(a)/sizeof(*a). Significa o mesmo, e não importa como está escrito, é o idioma C para encontrar os elementos numéricos da matriz.

Armadilha principal: o idioma C não é seguro. Por exemplo, o código…

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

passa um ponteiro para N_ITEMSe, portanto, provavelmente produz um resultado errado. Compilado como um executável de 32 bits no Windows 7, produz…

7 elementos, chamando a exibição ...
1 elementos.

  1. O compilador reescreve int const a[7]para apenas int const a[].
  2. O compilador reescreve int const a[]para int const* a.
  3. N_ITEMS é, portanto, invocado com um ponteiro.
  4. Para um executável de 32 bits sizeof(array)(tamanho de um ponteiro) é então 4.
  5. sizeof(*array)é equivalente a sizeof(int), que para um executável de 32 bits também é 4.

Para detectar esse erro no tempo de execução, você pode…

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 elementos, chamando a exibição ...
Falha na asserção: ("N_ITEMS requer uma matriz real como argumento", typeid (a)! = Typeid (& * a)), arquivo runtime_detect ion.cpp, linha 16

Este aplicativo solicitou que o Runtime o encerrasse de uma maneira incomum.
Entre em contato com a equipe de suporte do aplicativo para obter mais informações.

A detecção de erros em tempo de execução é melhor do que nenhuma detecção, mas desperdiça um pouco de tempo do processador e talvez muito mais tempo do programador. Melhor com detecção em tempo de compilação! E se você estiver feliz por não suportar matrizes de tipos locais com C ++ 98, poderá fazer isso:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

Compilando essa definição substituída no primeiro programa completo, com g ++, obtive…

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Na função 'void display (const int *)':
compile_time_detection.cpp: 14: erro: nenhuma função correspondente para chamar 'n_items (const int * &)'

M: \ count> _

Como funciona: a matriz é passada por referência e n_items, portanto, não se deteriora para apontar para o primeiro elemento, e a função pode simplesmente retornar o número de elementos especificado pelo tipo.

Com o C ++ 11, você também pode usá-lo para matrizes do tipo local, e é o idioma C ++ seguro para encontrar o número de elementos de uma matriz.

5.4 Armadilha C ++ 11 e C ++ 14: Usando uma constexprfunção de tamanho de matriz.

Com o C ++ 11 e posterior, é natural, mas como você verá perigoso !, substituir a função C ++ 03

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

com

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

onde a mudança significativa é o uso de constexpr, que permite que essa função produza uma constante de tempo de compilação .

Por exemplo, em contraste com a função C ++ 03, essa constante de tempo de compilação pode ser usada para declarar uma matriz do mesmo tamanho que outra:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

Mas considere este código usando a constexprversão:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

A armadilha: em julho de 2015, o acima foi compilado com o MinGW-64 5.1.0 com -pedantic-errors, e testando com os compiladores on-line em gcc.godbolt.org/ , também com o clang 3.0 e o clang 3.2, mas não com o clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) ou 3.7 (experimental). E importante para a plataforma Windows, ele não é compilado com o Visual C ++ 2015. O motivo é uma instrução C ++ 11 / C ++ 14 sobre o uso de referências em constexprexpressões:

C ++ 11 C ++ 14 $ 5,19 / 2 de nove th traço

Uma expressão condicional e é uma expressão constante central , a menos que a avaliação de e, seguindo as regras da máquina abstrata (1.9), avalie uma das seguintes expressões:
        ⋮

  • uma expressão id que se refere a uma variável ou membro de dados do tipo de referência, a menos que a referência tenha uma inicialização anterior e
    • é inicializado com uma expressão constante ou
    • é um membro de dados não estáticos de um objeto cuja vida útil começou dentro da avaliação de e;

Pode-se sempre escrever o mais detalhado

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

… Mas isso falha quando Collectionnão é uma matriz bruta.

Para lidar com coleções que podem ser não matrizes, é necessário sobrecarregar uma n_itemsfunção, mas também, para uso em tempo de compilação, é necessário uma representação em tempo de compilação do tamanho da matriz. E a solução clássica C ++ 03, que também funciona bem em C ++ 11 e C ++ 14, é permitir que a função relate seu resultado não como um valor, mas por meio do tipo de resultado da função . Por exemplo, assim:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

Sobre a escolha do tipo de retorno para static_n_items: este código não usa std::integral_constant porque com std::integral_constanto resultado é representado diretamente como um constexprvalor, reintroduzindo o problema original. Em vez de uma Size_carrierclasse, pode-se deixar que a função retorne diretamente uma referência a uma matriz. No entanto, nem todo mundo está familiarizado com essa sintaxe.

Sobre a nomeação: parte desta solução para o problema constexpr-invalid devido à referência é tornar explícita a escolha do tempo de compilação constante.

Esperamos que o oops - houve uma referência envolvida no seu constexprproblema - seja corrigido com o C ++ 17, mas até então uma macro como a STATIC_N_ITEMSacima gera portabilidade, por exemplo, para os compiladores clang e Visual C ++, mantendo o tipo segurança.

Relacionado: macros não respeitam escopos, portanto, para evitar colisões de nomes, pode ser uma boa ideia usar um prefixo de nome, por exemplo MYLIB_STATIC_N_ITEMS.


1
+1 Ótimo teste de codificação C: Passei 15 minutos no VC ++ 10.0 e GCC 4.1.2 tentando consertar o Segmentation fault... Finalmente descobri / entendi depois de ler suas explicações! Por favor, escreva sua seção §5.2 :-) Cheers
olibre

Boa. Um nit - o tipo de retorno para countOf deve ser size_t em vez de ptrdiff_t. Provavelmente vale a pena mencionar que em C ++ 11/14 deve ser constexpr e noexcept.
Ricky65 27/05

@ Ricky65: Obrigado por mencionar as considerações do C ++ 11. O suporte para esses recursos chegou atrasado para o Visual C ++. Em relação a size_tisso, não tenho vantagens que conheço para plataformas modernas, mas há vários problemas devido às regras implícitas de conversão de tipos de C e C ++. Ou seja, ptrdiff_té usado de forma muito intencional, para evitar os problemas com size_t. No entanto, deve-se estar ciente de que o g ++ tem um problema com a correspondência do tamanho da matriz com o parâmetro do modelo, a menos que seja size_t(não acho que esse problema específico do compilador com não size_tseja importante, mas com o YMMV).
Saúde e hth. - Alf

@Alf. No rascunho de trabalho padrão (N3936) 8.3.4 eu li - O limite de uma matriz é ... "uma expressão constante convertida do tipo std :: size_t e seu valor deve ser maior que zero".
Ricky65 27/05

@ Ricky: Se você está se referindo à inconsistência, essa afirmação não existe no padrão C ++ 11 atual, por isso é difícil adivinhar o contexto, mas a contradição (uma matriz alocada dinamicamente pode ter um limite 0, por C + +11 §5.3.4 / 7) provavelmente não terminará em C ++ 14. Os rascunhos são exatamente isso: rascunhos. Se você está perguntando sobre o que "its" se refere, refere-se à expressão original, não à expressão convertida. Se, por outro lado, você menciona isso porque acha que talvez essa frase signifique que alguém deva usar size_tpara indicar tamanhos de matrizes, é claro que não.
Saúde e hth. - Alf

72

Criação e inicialização de matrizes

Como em qualquer outro tipo de objeto C ++, as matrizes podem ser armazenadas diretamente nas variáveis ​​nomeadas (o tamanho deve ser uma constante em tempo de compilação; o C ++ não suporta VLAs ) ou podem ser armazenadas anonimamente na pilha e acessadas indiretamente via ponteiros (somente então o tamanho pode ser calculado em tempo de execução).

Matrizes automáticas

Matrizes automáticas (matrizes que vivem "na pilha") são criadas sempre que o fluxo de controle passa pela definição de uma variável de matriz local não estática:

void foo()
{
    int automatic_array[8];
}

A inicialização é realizada em ordem crescente. Observe que os valores iniciais dependem do tipo de elemento T:

  • Se Tfor um POD (como intno exemplo acima), nenhuma inicialização ocorrerá.
  • Caso contrário, o construtor padrão Tinicializa todos os elementos.
  • Se Tnão fornecer um construtor padrão acessível, o programa não será compilado.

Como alternativa, os valores iniciais podem ser especificados explicitamente no inicializador da matriz , uma lista separada por vírgulas, entre colchetes:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

Como nesse caso o número de elementos no inicializador da matriz é igual ao tamanho da matriz, a especificação manual do tamanho é redundante. Ele pode ser deduzido automaticamente pelo compilador:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

Também é possível especificar o tamanho e fornecer um inicializador de matriz mais curto:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

Nesse caso, os elementos restantes são inicializados com zero . Observe que o C ++ permite um inicializador de matriz vazio (todos os elementos são inicializados com zero), enquanto o C89 não (pelo menos um valor é necessário). Observe também que os inicializadores de matriz podem ser usados ​​apenas para inicializar matrizes; eles não podem mais tarde ser usados ​​em atribuições.

Matrizes estáticas

Matrizes estáticas (matrizes que vivem "no segmento de dados") são variáveis ​​de matriz locais definidas com a staticpalavra - chave e variáveis ​​de matriz no escopo do espaço para nome ("variáveis ​​globais"):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(Observe que as variáveis ​​no escopo do espaço para nome são implicitamente estáticas. Adicionar a staticpalavra-chave à sua definição tem um significado completamente diferente e obsoleto .)

Aqui está como matrizes estáticas se comportam de maneira diferente das matrizes automáticas:

  • Matrizes estáticas sem um inicializador de matriz são inicializadas com zero antes de qualquer potencial inicialização adicional.
  • As matrizes POD estáticas são inicializadas exatamente uma vez e os valores iniciais são tipicamente inseridos no executável; nesse caso, não há custo de inicialização no tempo de execução. Porém, essa nem sempre é a solução com mais eficiência de espaço e não é exigida pelo padrão.
  • Matrizes não POD estáticas são inicializadas na primeira vez em que o fluxo de controle passa por sua definição. No caso de matrizes estáticas locais, isso pode nunca acontecer se a função nunca for chamada.

(Nenhuma das opções acima é específica para matrizes. Essas regras se aplicam igualmente bem a outros tipos de objetos estáticos.)

Membros de dados da matriz

Os membros de dados da matriz são criados quando o objeto de propriedade é criado. Infelizmente, o C ++ 03 não fornece meios para inicializar matrizes na lista de inicializadores de membros , portanto, a inicialização deve ser falsificada com atribuições:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

Como alternativa, você pode definir uma matriz automática no corpo do construtor e copiar os elementos sobre:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

No C ++ 0x, as matrizes podem ser inicializadas na lista de inicializadores de membros, graças à inicialização uniforme :

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

Essa é a única solução que funciona com tipos de elementos que não possuem construtor padrão.

Matrizes dinâmicas

Matrizes dinâmicas não têm nomes; portanto, o único meio de acessá-las é por meio de ponteiros. Como eles não têm nomes, vou me referir a eles como "matrizes anônimas" a partir de agora.

Em C, matrizes anônimas são criadas via malloce amigos. No C ++, matrizes anônimas são criadas usando a new T[size]sintaxe que retorna um ponteiro para o primeiro elemento de uma matriz anônima:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

A arte ASCII a seguir descreve o layout da memória se o tamanho for calculado como 8 em tempo de execução:

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

Obviamente, matrizes anônimas requerem mais memória que matrizes nomeadas devido ao ponteiro extra que deve ser armazenado separadamente. (Também há alguma sobrecarga adicional na loja gratuita.)

Observe que não decaimento de matriz para ponteiro acontecendo aqui. Embora a avaliação new int[size]realmente crie uma matriz de números inteiros, o resultado da expressão new int[size] é um ponteiro para um único número inteiro (o primeiro elemento), não uma matriz de números inteiros ou um ponteiro para uma matriz de números inteiros de tamanho desconhecido. Isso seria impossível, porque o sistema de tipo estático exige que os tamanhos de matriz sejam constantes em tempo de compilação. (Portanto, não anotei a matriz anônima com informações de tipo estático na imagem.)

Em relação aos valores padrão para elementos, matrizes anônimas se comportam de maneira semelhante às matrizes automáticas. Normalmente, matrizes POD anônimas não são inicializadas, mas há uma sintaxe especial que aciona a inicialização de valor:

int* p = new int[some_computed_size]();

(Observe o par de parênteses à direita antes do ponto e vírgula.) Novamente, o C ++ 0x simplifica as regras e permite especificar valores iniciais para matrizes anônimas, graças à inicialização uniforme:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

Se você terminar de usar uma matriz anônima, precisará liberá-la novamente para o sistema:

delete[] p;

Você deve liberar cada matriz anônima exatamente uma vez e nunca mais tocá-la novamente. Não liberá-lo em todos os resultados em um vazamento de memória (ou mais geralmente, dependendo do tipo de elemento, um vazamento de recursos), e tentar liberá-lo várias vezes resulta em um comportamento indefinido. Usar o formulário não matriz delete(ou free) em vez de delete[]liberar a matriz também é um comportamento indefinido .


2
A descontinuação do staticuso no escopo do espaço para nome foi removida no C ++ 11.
Legenda2k 17/05

Por newser um operador am, certamente poderia retornar a matriz allcated por referência. Simplesmente não faz sentido ...
Deduplicator

@Duplicador Não, não podia, porque historicamente, newé muito mais antigo que as referências.
Fredoverflow

@FredOverflow: Portanto, há uma razão pela qual não foi possível retornar uma referência, é apenas completamente diferente da explicação escrita.
Deduplicator

2
@ Reduplicador Não acho que exista uma referência a uma matriz de limites desconhecidos. Pelo menos g ++ se recusa a compilarint a[10]; int (&r)[] = a;
fredoverflow
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.