Wiki

Распространенные ошибки при программировании

Авторы: Zack Rusin, Till Adam, Richard Moore Перевод: Andi Peredri

Введение

Цель этого документа - обобщить рекомендации многих опытных KDE-разработчиков по использованию библиотек Qt и KDE. Обычно новичкам этот опыт доставался в результате публичной критики их ошибок. После прочтения этого документа вы будете меньше опасаться того, что ваш код может увидеть один из ворчливых KDE-разработчиков :) Конструкции, которые мы обсудим, не обязательно ошибочны, они могут быть либо медленными, либо нечитаемыми.

Общие вопросы C++

В этом разделе мы расскажем о некоторых "подводных камнях" в C++, которые побуждают разработчиков к неправильному использованию языка или просто приводят к ошибкам.

Неименованные пространства имен и статические данные

Если функция-член не обращается к данным класса и может вызываться без создания экземпляра класса, сделайте ее статической. Если к тому же она является закрытой вспомогательной функцией, то определите ее на уровне файла. В результате она будет видима лишь в его пределах. Помните, что вызов статической функции осуществляется быстрее, так как компилятору не нужно неявно передавать указатель this. - прим. пер. Функции, определенные в неименованном пространстве имен имеют не внутреннюю, а внешнюю компоновку. Это объясняется тем, что компилятор C++ самостоятельно присваивает такому пространству уникальное в пределах всей программы имя. Поэтому если вы хотите, чтобы имена ваших функций не экспортировались, делайте их статическими. Однако учтите, что типы с внутренней компоновкой не могут использоваться в качестве параметров шаблонов.

Работа с NULL-указателями

Первое и самое главное замечание: нулевой указатель может быть удален. Поэтому проверка условия в следующей конструкции избыточна:     if (ptr) delete ptr; Во-вторых, вы можете встретить три различных способа определения нулевых указателей: 0, 0L и NULL. Значение NULL не рекомендуется использовать, так как в C оно определено как (void*)0, и такое его определение не соответствует принятому в C++. Однако все известные реализации C++ корректно определяют NULL, поэтому с этим обычно проблем не возникает. Для правильной трактовки 0 в вызовах функций с переменным числом аргументов часто использовался 0L. Но теперь это тоже уже в прошлом. И, скорее, является делом привычки. По мере перемещения кода в CVS все чаще вместо NULL будет использоваться 0. При удалении объекта убедитесь, что вы обнулили соответствующий указатель:
    delete ptr;
    ptr = 0;

Данные-члены

В KDE вы можете встретить четыре основных способа именования данных-членов:
  • m_variable - строчная m, символ подчеркивания и имя переменной со строчной буквы
  • mVariable - строчная m и имя переменной с заглавной буквы
  • variable_ - имя переменной со строчной буквы и символ подчеркивания
  • _variable - символ подчеркивания и имя переменной со строчной буквы. Обычно такое именование не одобряется. Будьте внимательны: такая нотация может использоваться для именования параметров функций.
Как это часто случается, не существует единого правильного способа именования переменных. Поэтому старайтесь придерживаться того стиля, который уже применяется в данном приложении или библиотеке.

Статические переменные

Старайтесь ограничиться небольшим числом статических переменных в своих программах, особенно в библиотеках. Создание и инициализация большого числа статических переменных заметно увеличивают время загрузки. Я не рекомендую использовать статические объекты классов не только в библиотеках и загружаемых модулях, но даже в программах. Неопределенность в порядке создания и уничтожения таких объектов приводит к многочисленным ошибкам (падениям). Вместо них используйте статические указатели вместе с KStaticDeleter.

Неполные объявления

Вы можете уменьшить время компиляции программы, по возможности заменив включение заголовочных файлов классов их неполными объявлениями. Например:
    #include <qwidget.h>     // Плохой стиль
    #include <qstringlist.h> // Плохой стиль
    #include <qstring.h>     // Плохой стиль
    class SomeInterface
    {
    public:
        void widgetAction( QWidget *widget ) =0;
        void stringAction( const QString& str ) =0;
        void stringListAction( const QStringList& strList ) =0;
    };
Этот код следует переписать следующим образом:
    class QWidget;     // Правильно
    class QStringList; // Правильно
    class QString;     // Правильно
    class SomeInterface
    {
        // По аналогии
    };

Итераторы

Несколько замечаний в произвольном порядке: При организации цикла для работы с большими контейнерами предварительно сохраните результат вызова метода end(). Например:
    QValueList<SomeClass&gtcontainer;
    // Заполняем контейнер большим числом элементов
    QValueListConstIterator end( container.end() );
    for( QValueConstIterator itr( container.begin() ); itr != end; ++itr ) 
    {
    }
Это предотвратит многократное создание временных объектов функцией end() и значительно повысит скорость работы. По мере возможности используйте константные итераторы (const_iterators) вместо обычных. Контейнеры с неявным совместным доступом обычно создают копию данных при вызове неконстантных методов begin() и end() (QValueList - пример такого контейнера). При работе с константными итераторами убедитесь, что вы действительно используете константные версии begin() и end(). Если сам контейнер не является константным, то может произойти нежелательное копирование данных. Поэтому для надежности константные итераторы инициализируйте с помощью функций constBegin() и constEnd(). При работе с итераторами старайтесь использовать префиксные, а не постфиксные операторы. Таким образом вам удастся избежать создания ненужных временных переменных.

Структуры данных

В этом разделе мы обсудим наиболее распространенные ошибки при работе с данными в Qt/KDE приложениях.

QPtrList

Как следует из имени класса, QPtrList является списком указателей. Список QPtrList предоставляет явный совместный доступ к данным класса. Это значит, что при каждом вызове метода returnList()
    class SomeClass
    {
    public:
        QPtrList returnList() const;
    private:
        QPtrList m_list;
    };
вам придется поэлементно копировать весь список во вновь созданный объект QPtrList. Поэтому мы рекомендуем вместо QPtrList использовать QValueList. Вы лишитесь возможности автоматического удаления объектов при уничтожении списка, но соответствующий код несложно добавить в деструктор вашего класса.

QCString

Одной из самых распространенных ошибок является использование метода QCString::length() в циклах. Для определения длины строки QCString::length() вызывает функцию strlen(), поэтому использование данного метода в подобных циклах:
    QCString someCString = ...; 
    for( int i = 0; i < someCString.length(); ++i ) {
        // Что-то сделать
    }
будет приводить к strlen(someCString) + 1 вызовам функции strlen(). Если вы планируете использовать результат вызова QCString::length() в цикле, предварительно сохраните его.

QByteArray

Часто у разработчиков возникает необходимость преобразования данных из массива QByteArray в строку QCString. Для этого многие пишут такой код:
    void someFunc( const QByteArray& data ) 
    {
        QCString str( data ); // Неправильно!
    }
Проблема в том, что QByteArray не обязательно завершается нулем. Это приводит к появлению трудно обнаруживаемых ошибок. Вот корректный код для создания строки QCString из массива QByteArray:     QCString str( data, data.length() + 1 );

QString

Часто возникает необходимость проверки строки на равенство нулю. Сравнение ее со статическим значением QString::null происходит медленнее, чем вызов метода isNull():
   if ( mystring.isNull() ) {          // Быстрее
   }
   if ( mystring == QString:: null ) { // Медленнее
   }
Если вам необходимо удостовериться, что строка не содержит каких-либо символов, используйте для этого метод isEmpty(). Во многих случаях это предпочтительнее проверки на нуль:
   if ( mystring.isEmpty() ) {  // Рекомендуется
   }
   if ( mystring == "" ) {      // Не рекомендуется
   }
Конвертирование файла из локальной кодировки в уникод (QString) происходит гораздо быстрее, если он читается сразу целиком, а не построчно. Для этого хорошо подходит метод QIODevice::readAll(), который позволяет обойтись одним экземпляром строки. Учтите, что большие файлы нужно читать и конвертировать блоками. Помимо всего прочего такой подход позволяет обеспечить доступность интерфейса программы. Для этого вы можете либо периодически вызывать метод qApp->processEvents(), либо использовать таймер для фонового чтения блоками. Передавайте строки QString как const QString &. Несмотря на то что класс QString обеспечивает неявный совместный доступ к данным, передача строки по константной ссылке является более эффективной и безопасной в сравнении с ее передачей по значению. Вот каноническое объявление метода, принимающего в качестве параметров две строки:     void myMethod( const QString & foo, const QString & bar ); Обычно, если есть возможность, всегда старайтесь передавать данные по константной ссылке. Хотя класс QString вне конкуренции по возможностям обработки строк, бывают ситуации, когда он совершенно неэффективен. Если вы работаете с данными типа QCString и QByteArray, не используйте для их обработки функции, принимающие в качестве параметров строки QString и затем снова преобразующие их в QCString или QByteArray. Например:
    QCString myData;
    QString myNewData = mangleData( myData );
 
    QString mangleData( const QString& data ) 
    {
        QCString str = data.latin1();
        // Обработка
        return QString(str);
    }
В этом случае происходит дорогостоящее конвертирование в строку QString с внутренним уникодным представлением. Это излишне, так как первое, что делает функция - это конвертирует ее обратно в latin1(). Если конвертирование в уникод вам точно не нужно, постарайтесь избежать использования QString, переписав этот пример так:
    QCString myData;
    QCString myNewData = mangleData( myData );
 
    QCString mangleData( const QCString& data )
    ...

QDomElement

Обычно при синтаксическом анализе XML-документа выполняется последовательный просмотр всех его элементов. Для этого вы можете захотеть использовать следующий код:
    for ( QDomElement e = baseElement.firstChild().toElement(); !e.isNull();
	e = e.nextSibling().toElement() )
    {
	...
    }
Это некорректное решение, поскольку данный цикл может преждевременно завершиться при обработке узла QDomNode, если он будет комментарием, а не элементом. Вот верное решение:
    for ( QDomNode n = baseElement.firstChild(); !n.isNull();
	n = n.nextSibling() )
    {
	QDomElement e = n.toElement();
	if(e.isNull()) continue;
	...
    }
Источник: Common Programming Mistakes