Q: O que é o "operator []"

O "operator []" é nada mais nada menos para que quem vá usar sua lista possa utilizá-la como um vetor, aumentando a legibilidade do código inclusive. Assim, seria possível acessar os elementos do array da seguinte forma:

structures::ArrayList<int> list{10u}; // Cria a lista com limite 10 elementos
// (chamando o 2º construtor que vocês
// implementaram)
list.push_back(0); // Insere um "0" no fim da lista (0)
list.push_back(5); // Insere um "5" no fim da lista (0, 5)
list.push_back(-3); // Insere um "-3" no fim da lista (0, 5, -3)
list[1] = 9; // Altera o segundo elemento para um 9 (0, 9, -3)

// Dobrando todos os valores da lista...
for (auto i = 0u; i < list.size(); ++i) {
list[i] *= 2;
}

// E para imprimir todos os elementos da lista...
for (auto i = 0u; i < list.size(); ++i) {
std::cout << list[i] << "\n";
}

Executando o programa acima, a saída dele deverá ser:

0
18
-6


Q: Qual a diferença entre o "at" e o "operator []"?

A diferença é simples:

O "operator []" tem como prioridade a performance/acesso direto, sem verificar se o índice entrege a ele está dentro dos limites do vetor ou não.

Já o método "at" faz comparação se o vetor está ou não dentro dos limites, deixando o acesso mais lento.

A ideia é, por exemplo: nos for's da questão acima, percebam que eu já faço um for de 0..size-1, ou seja, sempre estarei dentro dos limites, então não há por que perder performance comparando o tempo todo se o "i" está nos limites (imaginem um vetor com 10000 posições: são 10000 comparações a mais que eu sei que não vão ser necessárias). Pra esse caso, uso o "[]". Já em um momento em que eu não tenho garantia alguma se o índice vai estar nos limites do vetor e preciso me certificar que o Sistema Operacional não vai matar meu programa por conta de ele tentar acessar um espaço de memória o qual não deveria (por tentar acessar, por exemplo, o vetor em um índice absurdamente grande, acessando a memória de outros programas), para esse caso seria possível o "at", que deverá jogar uma exceção caso o índice não esteja dentro dos limites.

Exemplo:

structures::ArrayList<int> list{10u};
list.push_back(5);
list[0] = 3; // Acesso direto: mudo o 5 para um 3
list.at(0) = 9; // Acesso com checagem de limites: mudo o 3 para um 9
list.at(-1) = 3; // Aqui eu tomo uma exceção: -1 não está nos limites
// do vetor (que seria [0..9])
list.at(11) = 3; // Aqui também tomo uma exceção: 11 também não está
// nos limites do vetor ([0..9])
list[-1] = 3; // "Undefined behaviour": Não é previsível o que vai acontecer
list[11] = 3; // "Undefined behaviour": Não é previsível o que vai acontecer

Possivelmente nas duas últimas linhas, o programa será morto pelo sistema operacional com um "Segmentation Fault", mas não há segurança de que é exatamente isso que vai acontecer, e isso ainda varia de SO para SO.

 

Q: E quanto aos "operator ==", "operator <", "operator >"...?

Esses operadores devem ser implementados pelo objeto que a lista vai guardar, não pela própria lista (último item do slide 84).

 

Q: O que é o tal "operator *algo*"?

Em C++ você tem a possibilidade de definir operadores (+, -, *, /, <, <<, <=, >, >>, >=, etc...) para seus objetos. No caso, imaginem que eu tenho uma classe "Vector2D" (no conceito de Vetor de Geometria Analítica mesmo):

class Vector2D {
public:
Vector2D(int x, int y):
x_{x}, // inicializa "x_" com o parâmetro "x"
y_{y} // inicializa "y_" com o parâmetro "y"
{}

int x();
int y();
void move(int dx, int dy);
void set(int x, int y);

private:
int x_;
int y_;
};

Em Geometria Analítica, convenhamos que é extremamente comum que queiramos somar dois vetores, certo? Você poderia criar um método "add" que recebesse um vetor, mas em vez disso você pode fazer uma operação mais "espertinha", que seria dizer "o que acontece quando dois Vector2D são somados" (i.e. quando acontece "vector1 + vector2"):

class Vector2D {
public:
Vector2D(int x, int y):
x_{x},
y_{y}
{}

int x();
int y();
void move(int dx, int dy);
void set(int x, int y);

Vector2D operator+(const Vector2D& another) {
// Igual a G.A., apenas somam-se os valores de x e y do vetor do próprio
// objeto com os valores de x e y do segundo objeto
return Vector2D{x_ + another.x(), y_ + another.y()};
}

private:
int x_;
int y_;
};

Feito isso, em um código qualquer, posso fazer:

Vector2D u{5, 5};
Vector2D v{-5, 2};

Vector2D w = u + v; // Chama-se o operador "+" de "u",
// recebendo "v" como parâmetro
std::cout << "w = " << w.x() << ", " << w.y() << "\n";

Executando o programa acima, a saída deverá ser:

w = 0, 7

 

Q: O que significa o "const" *depois* do nome do método (e antes da {)?

Não fizeram esta pergunta ainda, mas eu imagino que vão fazer.

Um método "T foo() const {" (não confundir com "const T foo() {") é um método que não vai alterar o objeto que é dono desse método.

Por exemplo:

class A {
public:
void method() {}
void const_method() const {}
};

void consts_only(const A& a) { // "a" é recebido como "const", ou seja: dentro
// deste método, eu NÃO posso alterar "a".
a.method(); // Erro: "method" não é const, logo, eu não tenho garantia
// se ele vai ou não alterar o meu "a".
a.const_method(); // OK: "const_method" está marcado como const, ou seja,
// eu SEI que ele não vai alterar "a"
}

void anything(A& a) { // "a" não é const, logo, posso fazer o que quiser com ele
a.method(); // OK
a.const_method(); OK
}

Isso ajuda bastante inclusive o próprio programador. Por exemplo:

class A {
public:
void foo() const { // "foo" é marcado como "const"
i = 5; // Erro: eu não posso alterar nada do meu "A", porque eu garanti
// isso quando marquei "foo" como "const"
}

void bar() {
i = 5; // Aqui tranquilamente posso alterar "i", afinal "bar" não é const
}

private:
int i;
};
Última atualização: domingo, 28 ago. 2016, 08:55