Семинар 5 (02.10.2015)
! Обновление (07.10.2015)
Обратите внимание на результаты компиляции и запуска тестов
Типичные ошибки
Обращайте внимание не только на ошибки, выдаваемые компилятором, но и на предупреждения (warnings).
Поищите, как в вашей рабочей среде настроить компилятор так, чтобы он выдавал предупреждения.
Например, при использовании gcc (g++
) под Linux или при использовании mingw в Windows,
вывод всех предупреждений включается опцией -Wall
:
g++ -std=c++14 -Wall source1.cpp source2.cpp ...
Многое из того, что сообщает компилятор указывает на логические ошибки в коде. Например:
В функции-члене «LinkedList::iterator& LinkedList::iterator::operator++()»:
предупреждение: присваивание, используемое как логическое выражение, рекомендуется [-Wparentheses]
if (this->element = NULL)
^
Скорее всего, вместо присваивания (=
) в этой строке подразумевалось сравнение (==
).
В таком виде блок кода идущий после if (...)
никогда не будет выполнен.
Ещё пример:
В функции-члене «LinkedList::iterator LinkedList::iterator::operator++(int)»:
предупреждение: control reaches end of non-void function [-Wreturn-type]
}
^
Нет возвращаемого значения при выходе из не-void функции, что будет делать программа при вызове этого метода скорее всего неопределено.
Не используйте функцию memcpy
для копирования объектов, для этого есть оператор присваивания (operator=
)
и возможность его перегрузить.
При копировании итератора (с использованием констуктора iterator(const iterator &)
или оператора присваивания
iterator & operator=(const iterator&)
) не нужно копировать узел (Node), на который указывает итератор.
Например, если iterator
содержит поля:
class iterator {
Node * node;
}
то при копировании итератора нужно просто скопировать указатель:
iterator(const iterator & other): node(other.node) {
}
iterator & operator=(const iterator & other) {
this->node = other.node;
}
Кстати, в точности то же самое сделает default
:
iterator(const iterator & other): node(other.node) = default;
iterator & operator=(const iterator & other) = default;
Объяснение достаточно простое: итератор не владеет "узлом" с данными, на который он указывает.
Данными владеет класс LinkedList
.
По этой же причине при деструктор итератора (~iterator()
) не должен удалять "узел".
Операторы +
и +=
. Оператор +
не изменяет содержимое ни одного из двух аргументов
и возвращает новый объект. Пример для чисел:
int x = 10;
int y = 15;
int z = x + y;
// сожержимое x и y не изменилось. z содержит новое значение.
Оператор +=
изменяет содержимое первого аргумента, но не трогает второе:
int x = 10;
int y = 15
x += y;
// x содержит новое значение, значение y не изменилось.
Теперь к классам. Обратите внимание на сигнатуру операторов:
List operator+(const List & other) const;
List& operator+=(const List & other);
operator+
возвращает List
, то есть новый объект, operator+=
возвращает ссылку List&
.
Семантически это "ссылка на самого себя".
Что это значит? Это значит, то можно написать следующий код:
string x = "one";
(x += "two") += "three";
cout << x << endl;
// резутьтат: onetwothree
Если бы operator+=
возвращал новый объект (List
) вместо ссылки (List&
),
то при вызове += "three"
менялось содержимое не x
, а этого нового объекта и результат был бы onetwo
.
Можно определить оператор +
через +=
и конструктор копирования:
List& operator+=(const List & other) {
for (List::iterator it = other.begin(); it != other.end(); ++it) {
this->push_back(*it);
}
return *this;
}
List operator+(const List & other) const {
List result(*this); // создаём копию "этого" списка
result += other; // добавляем к копии "другой" список
return result; // возвращаем результат
}
Сравнение итераторов. В коде часто встречается конструкция it != list.end()
.
Как правильно сравнить два итератора?
Во-первых, нужно помнить, что итераторы можно копировать.
Во-вторых, что значит "два итератора равны"?
В случае со списком это означает, что два итератора указывают на один и тот-же узел.
Как понять, что итераторы указывают на один и тот же узел?
Использовать "хитрый хак" - сравнить два указателя :)
bool operator==(const iterator & other) const {
return this->node == other.node;
}
bool operator!=(const iterator & other) const {
return !((*this) == other);
}
Как узнать, что iterator++
дошел до конца списка?
Если внутри итератора хранить длину списка и позицию, как в классе Str,
но для связного списка это не будет работать.
Например:
struct iterator {
Node * node;
int len;
int pos;
};
int main() {
List l{1, 2};
List::iterator it = l.begin();
l.push_back(3);
cout << *it << endl; // 1
++it;
cout << *it << endl; // 2
++it;
cout << *it << endl; // Должно быть 3, но it не знает, что длина списка изменилась
}
Можно сказать "но я же могу использовать ссылку на длину, следующим образом:"
class List {
// ...
int len
// ...
};
struct iterator {
Node * node;
int & len; // len указывает на List::len
int pos;
};
но тогда я предложу следующий вариант, который всё испортит:
int main() {
List l{1, 2};
List::iterator it = l.begin();
l.push_front(0);
l.pop_back();
// Длина списка не изменилась, но изменилась относительная позиция
}
Вообще, самым лучшим решением будет не хранить длину списка (?!). Как понять, что итератор дошёл до конца списка? Очень просто: если у текущего узла нет следующего, значит это конец:
struct iterator {
Node * node;
};
iterator & operator++() {
if (this->node->next) {
this->node = this->node->next;
}
return *this;
}
Что возвращает метод end()?
Метод end()
возвращает итератор, указывающий на фиктивный элемент (узел), являющийся первый после последнего реального элемента в списке.
Покажу на примере. Допустим, список содержит элементы 1, 2 и 3.
Тогда список будет выглядеть примерно следующим образом:
begin() end()
| |
V V
iterator iterator
| |
V V
NULL <- node <-> node <-> node <-> node -> NULL
| | |
V V V
1 2 3
В самом классе List
достаточно иметь два поля: Node * _begin
и Node * _end
,
при этом _end
всегда будет указывать на узел с "первым после последнего элементом",
а _begin
- на узел с первым элементом.
Как тогда должен выглядеть пустой список?
_end
должен указывать на "фиктивный" узел, а _begin
должен указывать на _end
:
class List {
// ...
private:
Node * _begin;
Node * _end;
};
List::List() {
_end = new Node();
_begin = _end;
}
Не забывайте при добавлении и удалении узлов перестраивать указатели соседних узлов.
Нельзя возвращать ссылки (точно так же, как и указатели) на локальные переменные из методов, функций. Например, следующий код будет неправильным:
int & getReference() {
int x = 13;
return x;
}
Компилятор будет (должен) выдавать предупреждение для такого кода:
В функции-члене «int& getReference()»:
предупреждение: возвращена ссылка на локальную переменную «x» [-Wreturn-local-addr]
int x = 13;
^
В следующем коде та же самая ошибка:
class Node {
// ...
public:
value_type & getValue();
// ...
};
class List {
// ...
public:
value_type & front() {
value_type result = this->_begin->getValue(); // создаётся локальная переменная
return result; // ошибка: возвращается ссылка на локальную переменную
}
private:
Node * _begin;
};
Использовать статические переменные для решения этой проблемы не нужно. В данном случае можно написать просто:
value_type & front() {
return this->_begin->getValue();
}
Или, если нужно использовать локальную переменную, то объявить её как ссылку:
value_type & front() {
value_type & result = this->_begin->getValue();
return result;
}
Из константного метода нельзя вызвать неконстантный метод. Также нельзя вызвать неконстантный метод поля класса. Объяснение простое: const-методы не должны изменять состояние объекта. Поэтому следующий код работать не будет:
class Node {
// ...
public:
value_type & getValue();
// ...
};
class List {
// ...
public:
const value_type & front() const {
return this->_begin->getVanue(); // ошибка: вывзов non-const метода из const
}
private:
Node * _begin;
};
В таком случае можно объявить два метода getValue
- const и не-const:
class Node {
// ...
public:
value_type & getValue();
const value_type & getValue() const;
// ...
};
Тогда из const-метода front
будет вызвать const-метод getValue
.
Этот подход используют коллекции из стандартной библиотеки, например метод
operator[] класса std::vector.
В случае, если от метода getValue
требуется только доступ "на чтение", можно оставить только const-версию метода.
Обн. 07.10.2015
Важно, чтобы код соответствовал спецификации.
В частности, публичные методы должны называться так, как указано в задании
и иметь точно такую же сигнатуру (если метод обозначен как const
, то const
убирать нельзя).
Самый простой способ этого добиться - полностью скопировать объявление класса LinkedList
из текста задания в заголовочный файл.
Константные итераторы традиционно называются const_iterator
.
Пожелания
Структуры с стиле C. Вместо
typedef struct _MyStruct {
int some_field;
} MyStruct;
можно писать просто
struct MyStruct {
int some_field;
};
Поскольку ключевое слово struct
так же, как слово class
, определяет класс, но с доступом public
к членам класса по-умолчанию, последняя запись аналогична:
class MyStruct {
public:
int some_field;
};
Имена файлов.
Старайтесь придерживаться следующей схемы разбиения кода на модули (файлы).
Каждому классу с именем SomeClass
должна соответствовать пара файлов
SomeClass.h
и SomeClass.cpp
.
Вложенные классы (например SomeClass::iterator
) можно определять в тех же файлах.
функцию main()
лучше всего располагать в отдельном файле main.cpp
.
Тесты располагаются в отдельном файле (файлах), например: SomeClass_test.cpp
- функции для тестирования класса SomeClass
.
Таким образом, код первой задачи можно разбить на 4 файла:
LinkedList.h
, LinkedList.cpp
, LinkedList_test.cpp
и main.cpp
.
Продолжение следует