Намедни случился занятный случай. Попросила меня одна мамаша, сын которой учится на программиста,
посоветовать учебник по C++.
Хорошенькое дело: книг по "плюсам" издано немало и которую из них посоветовать? Ну конечно же это старый добрый Герберт Шилдт и его знаменитый
"Самоучитель С++" 3-е изд. 2003 года! Любой просветленный это скажет. Увы, не скажет, ибо старовата книга сия...
Из известных современных книг по "плюсам" больше всего лично мне понравилась
"«Язык программирования C++. Лекции и упражнения» (описан C++11), Стивен Прата, 6-е издание, 1248 стр., ISBN 978-5-8459-1778-2, «ВИЛЬЯМС», 2012. Именно ее я и предложил в качестве учебника.
Материнская любовь к своему сыну-студенту настолько велика, что книга, объемом 1248 страниц, а именно столько страниц в книге Стивена Прата, показалась ей слишком тяжелой то ли для освоения C++, то ли по своим весовым характеристикам, но меня попросили предложить что-нибудь "полегче" и в частности высказать свое мнение о другой, в два раза более тонкой книге, а именно о книге
«Освой самостоятельно C++ за 21 день» (описан стандарт C++11) , Сиддхартха Рао, 7-е издание, 688 стр., ISBN 978-5-8459-1825-3, «ВИЛЬЯМС», 2013.
Было острое желание сказать, что книга хорошая и тем самым быстренько отделаться от "клиента", который книги
оценивает исключительно по их весовым качествам, но я решил все-таки фрагментарно посмотреть эту книгу и избрал
для этого ЗАНЯТИЕ 26 "
Понятие интеллектуальных указателей".
Приступил к чтению и сразу стало интересно, ибо "странности" нашлись совсем недалеко от начала главы.
Замечу, что в данном контексте стиль программирования меня не волнует. Стиль трогать не будем. Мне не случалось видеть, чтобы в спорах о стиле один программист в чем-то убедил другого. Исходя из этого, любые дискуссии на эту тему считаю бесполезной тратой времени. Приемлемый стиль должен раздражать всех читателей в равной мере. :)
Начало повествования было бодрым и незамедлительно стало ясно почему интеллектуальный указатель способен
функционировать как обычный указатель, но потом начался ЛИСТИНГ 26.1. "
Минимально необходимые компоненты класса
интеллектуального указателя". Взгляд зацепился за очевидную Erratum в теле деструктора класса smart_pointer. Да вот этот листинг:
template <typename T> class smart_pointer {
private:
T* m_pRawPointer;
public:
smart_pointer (T* pData) : m_pRawPointer (pData) {} // constructor
~smart_pointer () {delete pData;}; // destructor
smart_pointer (const smart_pointer & anotherSP); // copy constructor
smart_pointer& operator= (const smart_pointer& anotherSP); // copy assignment operator
T& operator* () const { return *(m_pRawPointer); } // dereferencing operator
T* operator-> () const { return m_pRawPointer; } // member selection operator
};
// empty stub to enable compilation
int main() {
return 0;
}
Неужели вот так сразу и ошибка? Не может быть! А давайте-ка спросим у компилятора:
$ g++ -Wall MinimalSmartPtr.cpp -o MinimalSmartPtr
MinimalSmartPtr.cpp: In destructor «smart_pointer::~smart_pointer()»:
MinimalSmartPtr.cpp:8:31: ошибка: нет декларации «pData» в этой области видимости
~smart_pointer () {delete pData;}; // destructor
^
Не понравилось ему. Поправим:
~smart_pointer () {delete /* pData; */ m_pRawPointer;}; // destructor
и опять спросим. Теперь все в порядке. Ничего не поделаешь. Всякое случается.
Дальше читаем. Вот подраздел "
Анализ" и что видим? Цитирую:
Например, чтобы использовать интеллектуальный указатель на объект класса Tuna, вы создали бы его экземпляр
так:
smart_pointer <Tuna> pSmartTuna (new Tuna);
pSmartTuna->Swim ();
// Альтернатива:
(*pSmartDog).Swim ();
Конец цитаты.
А pSmartDog, который тоже плавать умеет, здесь при чем? Может разумнее было бы показать осмысленный фрагмент кода, расширяющий ЛИСТИНГ 26.1, например, так:
class Tuna {
public:
void swim() { std::cout << "Tuna can swim." << std::endl; }
};
int main() {
smart_pointer <Tuna> pSmartTuna (new Tuna);
pSmartTuna->swim();
(*pSmartTuna).swim();
return 0;
}
Этот код и компилироваться будет, и сомнительных "
указателей на умных псин" (*pSmartDog) здесь не будет,
и при запуске программа выдаст понятный вывод:
$ ./MinimalSmartPtr.exe
Tuna can swim.
Tuna can swim.
Должен заметить, что у Стивена Прата все как-то доступнее и более предсказуемо описано, и нет нужды неискушенному читателю гадать о том, с чего это у автора книги тунец и собака плавают.
А чего это я тут брюзжать вздумал? Не брюзжание это, а грусть за набитые шишки у тех, кто будет C++ по такой, местами загадочной, книге изучать. И ведь книга не так уж и плоха. It’s nothing personal. Ей надо отдать должное, но мне почему-то не хотелось бы, чтобы мое чадо лишний раз по граблям при изучении языка топталось.
Дальше автор как-то сразу перешел к беглому обзору типов интеллектуальных указателей и в частности к глубокому
копированию, введя в обсуждение базовый класс Fish. А зачем, хочется спросить, была вся эта суета
около вышеприведенного класса smart_pointer?
Давайте эту незаконченную авторскую мысль попробуем все-таки закончить. Для этого добавим базовый класс и поглядим на получившуюся картину, но сначала все предельно упростим и на минуточку вспомним о необходимости определения виртуального деструктора в интерфейсе:
#include <iostream>
class Fish {
public:
virtual void swim() = 0;
};
class Tuna: public Fish {
public:
void swim() { std::cout << "Tuna can swim." << std::endl; }
};
int main() {
Fish *pTuna = new Tuna();
pTuna->swim();
(*pTuna).swim();
// The behaviour of the next line is undefined. It probably
// calls Fish::~Fish, not Tuna::~Tuna
delete pTuna;
return 0;
}
$ g++ -Wall tuna1.cpp -o tuna1
tuna1.cpp: В функции «int main()»:
tuna1.cpp:21:12: предупреждение: deleting object of abstract class type «Fish» which
has non-virtual destructor will cause undefined behaviour [-Wdelete-non-virtual-dtor]
delete pTuna;
^
Классика предмета - "Why should I declare a virtual destructor for an abstract class in C++?",
а о ней ни слова! Как на мой взгляд, напомнить о ней здесь было бы совсем даже нелишне. Теперь можно показать вариант лечения данной проблемы. Например, вот такой:
#include <iostream>
class Fish {
public:
virtual ~Fish() {}; //You should always use a virtual destructor with interfaces.
virtual void swim() = 0;
};
class Tuna: public Fish {
public:
~Tuna() { std::cout << "Tuna destructor." << std::endl; };
void swim() { std::cout << "Tuna can swim." << std::endl; }
};
int main() {
Fish *pTuna = new Tuna();
pTuna->swim();
(*pTuna).swim();
delete pTuna;
return 0;
}
Либо, чтобы быть уже совсем в фарватере обсуждаемой темы и забегая чуть вперед, - вот такой вариант:
#include <iostream>
#include <memory>
class Fish {
public:
virtual ~Fish() {}; //You should always use a virtual destructor with interfaces.
virtual void swim() = 0;
};
class Tuna: public Fish {
public:
~Tuna() { std::cout << "Tuna destructor." << std::endl; };
void swim() { std::cout << "Tuna can swim." << std::endl; }
};
int main() {
//Fish *pTuna = new Tuna();
std::shared_ptr<Fish> pTuna(new Tuna());
pTuna->swim();
(*pTuna).swim();
return 0;
}
Здесь нужно не забыть о флаге c++11:
$ g++ -std=c++11 -Wall tuna.cpp -o tuna
И чтобы вся эта суета вокруг smart_pointer была осмысленной, покажем полный пример с smart_pointer:
#include <iostream>
template <typename T> class smart_pointer {
private:
T* m_pRawPointer;
public:
smart_pointer (T* pData) : m_pRawPointer (pData) {} // constructor
~smart_pointer () {delete m_pRawPointer;}; // destructor
smart_pointer (const smart_pointer & anotherSP); // copy constructor
smart_pointer& operator= (const smart_pointer& anotherSP); // copy assignment operator
T& operator* () const { return *(m_pRawPointer); } // dereferencing operator
T* operator-> () const { return m_pRawPointer; } // member selection operator
};
class Fish {
public:
virtual ~Fish() {}; //You should always use a virtual destructor with interfaces.
virtual void swim() = 0;
};
class Tuna: public Fish {
public:
~Tuna() { std::cout << "Tuna destructor." << std::endl; };
void swim() { std::cout << "Tuna can swim." << std::endl; }
};
int main() {
smart_pointer <Fish> pTuna (new Tuna);
pTuna->swim();
(*pTuna).swim();
// delete pTuna;
// Когда интеллектуальный указатель выходит из области видимости,
// он освобождает память, на которую указывает (при помощи деструктора).
return 0;
}
$ g++ -Wall tuna1.cpp -o tuna1
$ ./tuna1.exe
Tuna can swim.
Tuna can swim.
Tuna destructor.
Вот теперь можно и о глубоком копировании рассказывать...
Как на мой взгляд, примеры кода лучше нескольких страниц поясняющего текста и в частности текста, который не отличается особой прозрачностью для изучающего язык. Неужели автору книги было так сложно сделать осмысленные примеры к книге? При печати книги эти примеры, будучи помещенными в прилагаемый архив, перерасход бумаги не создали бы, а читатель сэкономил бы немало драгоценного времени.
Все. Надоело. Будет настроение, почитаю дальше эту книгу. Есть такое смутное ощущение, что ERRATизма там еще хватает...