10 типичных ошибок программирования


Содержание

10 типичных ошибок программирования

БлогNot. Типичные ошибки начинающего разработчика на C++: проблемы и их решения

Типичные ошибки начинающего разработчика на C++: проблемы и их решения

сейчас в списке: 61 ошибка Проводя гораздо больше времени, чем мне хотелось бы, за разного рода учебными мероприятиями по программированию, я поневоле начал собирать коллекцию типовых ошибок, допускаемых начинающими разработчиками. Попробую приспособить для неё эту запись, надеюсь, список постепенно будет пополняться. до 50-60 самых типовых проблем, ну, уж никак не больше 100 :)

Конечно, я не ставлю цели рассказывать обо всех возможных синтаксических ошибках — для этого есть компилятор. Я обращу внимание скорее на типовые алгоритмические и логические неточности, допускаемые начинающими. Весь приведённый код проверен, в основном, для этого служили бесплатные сборки Microsoft Visual C++ версии 2010 или выше.

Страница длинная, поэтому воспользуйтесь оглавлением или нажмите комбинацию клавиш Ctrl+F для поиска нужного текста на странице.

Оглавление
Попытка модифицировать константный указатель

Ошибка особенно коварна тем, что всплывёт на этапе исполнения программы:

а при компиляции всё может выглядеть нормально.

Первый способ исправления — выделять память под s динамически:

Второй способ — заменить исходный указатель массивом.

Некоторые компиляторы, например, от Borland, выполняют и исходный код. Это их проблемы, а в стандарте языка такого нет.

К тому же виду ошибки можно отнести и такое:

Так нельзя, в s нет столько места, и после ввода строки возникнет ошибка. Правильно было бы сделать константный указатель на изменяемую строку, указав её размер сразу:

Теперь ввод в s будет работать, если не указывать вторым параметром метода getline размерность, большую количества выделенной памяти (255 байт в нашем случае).

Ещё правильнее выделить новую память, а затем её освободить:

Двойная перестановка элементов массива

Ошибка встречается при транспонировании матрицы, инверсии (переворчивании) символов строки и т.п. Суть дела в том, что вы применяете полные циклы там, где нужны только их «половинки» :)

Пример: перевернём символы строки правильно.

Типичная ошибка: при верхней границе цикла, равной L , строка останется неизменённой, так как каждая пара символов будет переставлена дважды.

Пример: транспонирование матрицы.

Типичная ошибка: если сделать в main полный двойной цикл

то ничего транспонировано не будет.

Пример: сравнить попарно все элементы вектора (программа только напечатает номера пар сравниваемых элементов)

Типовая ошибка: если сделать полный двойной цикл

то сравнения будут выполняться дважды, более того, каждый элемент будет сравниваться «сам с собой».

Пример. Меняем местами максимальные и минимальные элементы массива. Неверно:

После оператора, помеченного «1», бывший максимум уже заменён минимальным значением. Идущий следом второй оператор меняет всё обратно. Правильно, например, так:

Окно программы закрывается, я не успеваю увидеть результаты работы программы?

Если Вы не программировали остановки в программе, её и не будет!

Вставляйте оператор, ожидающий ввода с клавиатуры перед оператором return главной функции.

Варианты такого кода:

Неправильный кастинг (приведение типов)

Как известно, C++ допускает преобразования типов данных с потерей точности, в этом случае компилятор может просто «промолчать» или сгенерировать предупреждение:

Для таких случаев вполне подойдёт static_cast :

Все скобки — и треугольные, и круглые, здесь обязательны!

Если же компилятор выдаёт ошибку преобразования типа, например, при попытке присваивания структур разных типов

то можно рискнуть применить

— но лишь потому, что мы понимаем, что структуры type1 и type2 на самом деле полностью совместимы по типам данных, хотя и имеют разные теги имени структурного типа и разные наименования полей:

Обратите внимание, что присваивание через указатель var2 , как и положено, изменит значение поля исходной переменной var1 .

= вместо == (присваивание вместо сравнения)

Головная боль начинающих, особенно после Паскаля :)

Компилятор не найдёт в этом никаких проблем, максимум, сгенерирует предупреждение. Меж тем, переменной i будет присваиваться «двойка», а так как результат этой операции равен true , то всегда будет выполняться ветвь 1.

Решение очевидно — пишите правильно операцию сравнения, а именно, ==

& и | вместо && и || (побитовые операции вместо логических)

Первый оператор выполнит логическое умножение над отдельными битами значений n1 и n2 , так что для младшего байта получится

Второй оператор каждую переменную приведёт к типу bool , так что true && true = true (напомню, что true считаются все ненулевые значения).

Второй оператор мог бы быть более наглядно выполнен условием вида n1!=0 && n2!=0

Неверное использование char как int

Вот этот код ошибочно интерпретирует значение c как число (а не код символа), пытаясь прибавить к нему значение 10. В результате получится отнюдь не 11, а 59, то есть символ » ; «

Происходит это потому, что код символа «ноль» равен 48, а «единицы» 49. Правильным решением было бы сначала отнять от символа-цифры код нуля, равный 48. Неявно используется предположение, что цифровые символы закодированы идущими подряд числовыми значениями (это так во всех распространённых кодировках).

Неверное выделение/освобождение динамической памяти

Одна из самых трудноуловимых проблем времени исполнения программы. Чтобы не тратить время на попытки починить непредсказуемо работающий код, соблюдайте несложные правила:

1. Вся динамически выделяемая память должна освобождаться, в идеале «по принципу стека», то есть, занявший память последним объект освобождает её первым:

2. Если память выделялась через new , она освобождается через delete , если через new[] — через delete[] , то есть, последним оператором нужно освобождать память от массивов:

3. Не смешивайте в одной программе разные способы выделения и освобождения памяти. Если вы выделяете память с помощью функции malloc или calloc , то освобождайте её с помощью функции free , при выделении через операторы new/new[] , освобождайте с помощью delete/delete[] . В целом использование в проектах C++ «сишных» функций malloc , calloc и free не одобряется стандартом.

4. Бойтесь повторных delete , применённых к уже удалённым объектам — это одна из самых трудноуловимых ошибок. Для многих компиляторов действует неформальный «хак» — явно занулять указатель после delete , присваивая ему 0 или NULL , что зависит от компилятора:

Теперь повторные delete не опасны.

5. По возможности избегайте выделения памяти на одном уровне (например, в теле функции), а освобождения — на другом (например, в функции main ). Даже если речь о «куче» и операторах new / delete . Например:

При вызове функции f в неё передаётся копия указателя Ptr . При выделении памяти адрес помещается в эту копию, то есть, значение переменной Ptr не изменится. После возврата из функции адрес потеряется, соответственно, потеряется выделенная память и возникнет ошибка времени выполнения. Как вариант, можно делать так:

(передавать в функцию адрес указателя) или так:

(принимать в функции ссылку на указатель), но всё равно выглядит вычурно :)

Возврат из функции ссылки или указателя на локальную переменную

Да, если вам повезёт, эти 2 функции вернут результаты 1 и 2 соответственно:

На самом деле, так поступать нельзя. Локальные переменные, созданные в стеке, уничтожаются при выходе из их области видимости (в нашем случае — при выходе из функции). Таким образом, память получает статус свободной и туда могут быть записаны какие-либо новые данные. Если это не успело произойти, Вы свои 1 и 2 получите. но в сложном коде почти гарантированно однажды произойдёт сбой. Чтобы не попасть впросак, используйте естественное

Функция good2 имеет смысл в предположении, что оператор new выделяет память в куче (heap), а под локальные объекты память выделяется в стеке (stack). Также потенциально опасно, если память выделяется на одном уровне вложенности кода, а освобождается на другом.

Та же проблема имеется и с возвратом локальных строк:

Самый естественный способ исправить — в функцию передавать буфер, куда копируем строку. По возможности, при всех операциях с буфером нужно контролировать возможный выход за его границу, особенно когда работаем с заранее неизвестными размерами (типовое и не лучшее решение — использование strncpy вместо strcpy внутри функции f ).

Использование неинициализированных переменных

. то есть, вы делаете что-то с переменной, которая просто описана, но не получала никакого значения:

Результат может быть любым — от генерации ошибки времени исполнения до неверного расчёта, сделанного программой. Особенно часто забывают давать начальное значение количеству, сумме, максимуму или минимуму, демонстрируя тем самым, что типовые алгоритмы не учили :)

Пока мы не начали суммировать элементы массива, переменная-сумма sum отнюдь не обязана быть равной нулю!

Нулями автоматически инициализируются только:

  • статические переменные, описанные с указанием ключевого слова static ;
  • в некоторых компиляторах — глобальные переменные, описанные вне всех функций (полагаться не стоит);
  • динамические области памяти, выделяемые устаревшей сишной функцией calloc .

Два добрых совета, экономящих кучу времени:

1. Привыкнуть инициализировать объявленные переменные всегда. А указатели тоже инициализировать значением 0 или NULL (да ещё и сбрасывать обратно в эти значения после освобождения динамической памяти, связанной с указателем).

2. Объявлять переменные как можно ближе к месту их использования, благо, C++ это позволяет.

Пустой цикл из-за точки с запятой.

Часто ставят лишнюю точку с запятой, например, после открывающей части цикла:

Результат выполнения этого кода будет поистине страшен! Во-первых, здесь не написано «суммировать n элементов массива a «. Здесь написано » n раз выполнить пустой цикл, а затем прибавить к переменной sum значение i -го элемента массива a «. Во-вторых, после выхода из цикла по i , значение этой переменной может оказаться равным 3 , что приведёт ещё и к выходу за границы массива при выполнении a[i] .

Делают так и для функций. хотя вот это, к счастью, выловит любой современный компилятор:

Чтобы не совершать этой глупой ошибки, помните — точка с запятой завершает действие (цикл, объявление, оператор), а не начинает его.

Функция main не имеет типа int

Вроде бы и не ошибка, но. распространённейшее

— неверно. Согласно стандарту, функция main() должна возвращать целочисленное значение, правильно вот так:

Определение структурного типа или класса не заканчивается точкой с запятой

Вот эта маленькая программа сгенерирует, как минимум, 3 ошибки:

То же самое и при объявлении классов — не забывайте про символ ; в конце.

В нашем примере можно было и сразу же описать структурный тип и определить структуру:

Сравнение вещественных значений как целых

Проблема возникает при сравнении вещественных значений a , b в виде

Дело в том, что арифметические вычисления для чисел с плавающей запятой выполняются с некоторой погрешностью, обусловленной тем, что хранить все знаки дробной части числа в памяти бывает невозможно, например, 2/3=0.66666666666. — ряд шестёрок неизбежно будет где-то обрезан.


Правильный путь — сравнивать вещественные числа с некоторой точностью, которая зависит от поставленной задачи, например

Аналогичная проблема возникает в циклах с вещественной управляющей переменной, скажем, в этом цикле

переменная x рискует «потерять» своё последнее значение, равное 1 , всё из-за тех же погрешностей. Правильный путь — прибавлять к правой границе диапазона изменения x некое малое значение, заведомо меньшее шага: for (x=0; x char ) с помощью операций отношения , == , >= и т.д. — глубоко неправильное для C++ действие. Так мы будем сравнивать не содержимое, а указатели (адреса памяти, где начинаются строки). Правильный путь — стандартная функция сравнения строк strcmp (подключить заголовки string.h ).

Неправильная запись чисел

Да-да, такое тоже бывает. Например, посмотрите, что напечатает этот код:

Просто лидирующий ноль означает число в восьмеричной системе счисления. Добавлять ноль перед числом не нужно никогда. Перед строкой — пожалуйста:

Вторая «детская» проблема, способная, тем не менее, создать жуткий головняк — применение запятой вместо точки для отделения дробной части вещественного числа.

Правило — всегда использовать точку, а не запятую при записи чисел! C++ — не Microsoft Office, а средство его написания :)

В длинных математических выражениях ошибка очень трудноуловима. Максимум, что скажет компилятор — предупреждение о неиспользуемой переменной при включённом флаге -Wunused-value . Поэтому программирование математики должно быть особенно тщательным.

Принадлежит ли значение интервалу?

Проверка, которая выполняется очень часто. Допустим, есть переменная x , нужно проверить, попадает ли её значение в интервал [a,b] . Единственный правильный путь сделать это — записать

Самые распространённые неправильные способы записи собраны, некоторые из них даже компилируются, но это ничего не значит :)

Так как операция сравнения левоассоциативна (выполняется слева направо), это эквивалентно записи

дающей истинный результат при любом a .

Тоже истина для любого x .

Результатом операции «запятая» является последнее выражение, то есть, здесь проверяется только условие x

Неверный составной условный оператор

Проблема возникает, когда вы хотите запрограммировать более 2 вариантов расчёта. Обычно всё дело — в непонимании того, как работает условный оператор с несколькими ветвями расчёта.

Вот простейший пример, здесь мы определяем знак z целого числа a , по правилу:

Вот типичная ошибка такого расчёта:

Применение одного короткого и одного полного условных операторов является здесь грубой ошибкой — ведь после завершения короткого условного оператора для всех ненулевых значений a будет выполнено присваивание z=1 . Правильных вариантов этого расчета, по меньше мере, два:

— с помощью 3 коротких условных операторов, вариант не очень хорош тем, что проверяет лишние условия даже тогда, когда знак уже найден.

— с помощью составного условного оператора, этот вариант лучше.

Вторая типичная проблема — неправильный порядок ветвей в составном операторе, из-за которого некоторые условия срабатывают «досрочно» или не срабатывают вовсе. Например, в показанном ниже фрагменте, где нужно было вывести, является ли значение x чётным или нечётным и отдельно учесть значение x=12 , напечатав для него слово «Дюжина», эта самая «дюжина» не будет напечатана никогда:

Проблема в том, что значение 12 — тоже чётное, и сработавшая ветвь «Чётное» не даст сработать ветви «Дюжина». Да и проверка условий организована явно избыточно, правильно было так:

Чтобы не допускать таких ошибок, нужно понимать, как работает составной условный оператор. Его общий вид может включать произвольное число условий и ветвей расчета:

При использовании оператора последовательно проверяются условия 1, 2, . ,N , если некоторое условие истинно, то выполняется соответствующий оператор и управление передается на оператор, следующий за условным. Если все условия ложны, выполняется ветвь0 , если она задана, или не выполняется ни одной ветви, если ветвь0 отсутствует. Число ветвей N не ограничено. Существенно то, что если выполняется более одного условия из N , обработано всё равно будет только первое истинное условие.

Функция не возвращает значения всегда или в некоторых случаях

Если функция не имеет тип void , она должна возвращать значение указанного в заголовке типа при любых комбинациях своих входных параметров. Например, следующая функция

ошибочна: для нулевого или отрицательного значения x возвращаемое значение отсутствует.

Простейший способ избежать проблемы — всегда возвращать из функции некое «значение по умолчанию»:

Некоторые компиляторы, например, Visual Studio, могут предупредить об этой проблеме:

Но для этого надо установить соответствующий уровень предупреждений компилятора. В Visual Studio это делается через команду меню Проект, Свойства, Свойства конфигурации, С/С++, Общие, Уровень предупреждений, Уровень 4. По умолчанию принят уровень 3.

Не срабатывает останов в программе или «не работает» оператор ввода

Очень частая проблема. Например, почти во всех компиляторах второй ввод через scanf будет пропущен:

А вот в таком виде всё сработает верно:

В ряде компиляторов могут быть проблемы и с таким кодом:

На самом деле, всё очень просто. «Проскакивание» ожидания ввода (паузы) происходит из-за того, что в потоке после предшествующего ввода остаются символы. Чаще всего остаётся символ новой строки \n , который попадает в поток при нажатии Enter. Оператор cin >> его просто проигнорирует, а такие функции, как getchar() , cin.get() и т.п. считывают его как первый символ в потоке и код идёт дальше, а ожидания ввода символа не происходит. Резюме — перед применением этих функций поток нужно очищать — fflush (stdin) на С или cin.sync() на C++

Вот в этом коде, если не повезёт, могут быть проблемы:

А в этом — вряд ли:

Удобней всего просто синхронизировать поток перед операциями ввода:

Экзотика. Вместо cin.get() можно просто пропустить символы:

Можно также в цикле прочитать оставшиеся символы

Выход за границы массива

Индексы массивов в C++ начинаются с нуля. Для массива из n элементов допустимые индексы лежат в диапазоне [0..n-1] . Чтение или запись информации вне выделенной памяти приводит к неопределенному поведению программы.

Приведённая ниже ошибка, скорей всего, сравнительно безобидна, а вот запись элемента в несуществующий элемент массива способна привести к краху программы.

Со строками данная ошибка обусловлена, чаще всего, отсутствием в них нуль-терминатора (байта с кодом 0, пишется ‘\0’, не путать с цифрой 0 (‘0’), имеющей код 48!)

Переключатель switch без операторов break

Если проверяемая величина принимает заранее известный фиксированный набор значений, оператор-переключатель удобнее, чем составной условный. Но если забыть про то, что каждую ветвь case нужно завершать оператором break , позволяющим выйти из переключателя, результаты могут быть плачевны:

Здесь для i , равного 0 , будет выведено zero one other . При i , равном 1 , будет выведено one other . То есть, без break происходит «проваливание» к следующей метке.

Бывает, что отсутствие break — не баг, а фича. Например, вот эта функция умеет возвращать 1-ю, 2-ю или 3-ю степень «двойки», а в остальных случаях результат будет равен единице.

Просто укажите комментарием, если break вам не нужны и ветви переключателя должны «проваливаться».

Градусы вместо радианов у тригонометрических функций

Очень часто к неожиданным результатам расчётов приводят вот такие ляпы в коде: Получится значение x = 0.893997, а отнюдь не 1. Просто человек имел в виду 90 градусов, а компьютер принимает аргументы тригонометрических функций в радианах. Если входная величина задана в градусах, переведите её в радианы:

Ещё лучше написать маленькую функцию, делающую такой перевод.

Неверный вызов конструктора базового класса из конструктора производного

В показанном ниже коде производный класс B пытается воспользоваться конструктором своего родителя A .

Правильный способ — использовать списки инициализации в конструкторе потомка.

Сравнение знаковых и беззнаковых значений

Вот в этом коде -1 «неожиданно» окажется больше 1:

Знаковое x приводится к беззнаковому, отчего возрастает до значения UINT_MAX — 1 (само UINT_MAX равно max(unsigned int)-1 , в разных компиляторах может обозначаться разными именами).

Решение — не сравнивать знаковые и беззнаковые значения, заранее приводя их к одному типу данных.

Вызов виртуального метода из конструктора класса

Класс A хочет для инициализации свойства x воспользоваться виртуальным методом f :

У класса-потомка B виртуальный метод реализован, но всё равно ничего не выходит. Проблема в том, что при создании объекта B , сначала создается базовая часть (класс A ), а в конструкторе базового класса ничего о классе B ещё не известно, поскольку он еще не создан, в том числе, не заполнена таблица виртуальных функций.

Решение: не вызывать виртуальные методы из конструкторов.

Неправильное выделение тела цикла или ветви условного оператора

Если в теле цикла находится более одного оператора, его следует заключать в фигурные операторные скобки.

Если же повторяемый оператор всего один, допустима запись тела как с операторными скобками, так и без них:

К сожалению, начинающие разработчики часто путаются и не ставят операторных скобок даже если в цикле больше одного оператора. Получается вот что:

Оператор вывода cout не имеет отношения к циклу, в цикле находится только оператор a[i]= , а выравнивание отступами для компилятора С++ ничего не значит.

Делать его всё равно нужно, это облегчает чтение программы человеком

Для исправления ошибки достаточно заключить тело цикла в операторные скобки:

Аналогичная проблема характерна и для ветвей условных операторов. Вот в этом коде

несмотря на визуальное выравнивание, ветка else относится не к первому if , а ко второму, скобки же способны решить проблему и показать разветвление кода наглядно:

Без указания дополнительных операторных скобок else всегда относится к ближайшему сверху if, для которого ветка else ещё не указана

Самый простой способ не запутаться — всегда ставить операторные скобки после ключевых слов for , while , do , if , else if или else и закрывать открытые скобки сразу же, так, чтобы закрывающая скобка была непосредственно под открывающей

или же под первой буквой ключевого слова, к которому относится открывающая скобка:

В любом случае, код, помещённый внутри цикла или ветви алгоритма сдвигается вправо пробелами и выравнивается, так что одному уровню вложенности всегда соответствует одинаковое число пробелов. Я лично для скорости печати обхожусь одним, но часто испольуют несколько пробелов или табуляцию, тем более, что в современных редакторах кода этот параметр легко настраивается. В частности, в Visual C++ пройдите в меню Сервис, Параметры, Текстовый редактор, C/C++, Табуляция и настройте так, как вам удобно.

Функция не может узнать размер параметра-массива

Функция f неудачно пытается воспользоваться конструкцией sizeof для определения размера массива-параметра:

Решение 1, самое простое и верное, подходит и для статических, и для динамических массивов. Передать размерность массива как параметр функции.

Решение 2, только для статического массива. Размер статического массива можно узнать, если принять в функции ссылку на массив и использовать template для типа данных. Увы, это сработает не во всех компиляторах и обратная совместимость кода с Си нарушается.

Решение 3, тоже подойдёт для статического массива, но не сработает с динамическим, память под который выделялась оператором new . Размер массива можно задать при использовании шаблона, вот образец:

Но это уже совсем жутик по сравнению с универсальным и обладающим обратной совместимостью способом 1 :)

Неверный порядок свойств класса при использовании списка инициализации

Есть класс со свойствами x и y , проинициализируем их с использованием списка в конструкторе:

Проблема состоит в том, что в списке инициализации конструктора объекты инициализируются в том порядке, в котором они объявлены в классе или структуре, а не в том, который указан в списке инициализации. В нашем случае сначала будет проинициализирована переменная x , а только потом y , что приведёт к неопределённости значения x .

Выход — следить за порядком объявления переменных в классе.

Функция работает с локальной копией объекта вместо объекта

Представим, что мы написали пару функций для удобной инициализации динамического массива и его удаления из памяти:

Показанный код в корне неверен тем, что при вызове функции init_array в нее передается копия указателя a , описанного в главной функции. Этот указатель ещё не проинициализирован на момент вызова функции. При выделении памяти, адрес массива будет помещён в локальную копию a внутри функции, значение a в функции main от этого не изменится. Соответственно, после выхода из функции выделенная память будет потеряна.


Первый способ решения — передать в функцию адрес указателя, используя конструкцию ** :

Второй способ — принимать в функции ссылку на указатель, конструкцию *& :

Это удобнее, не так ли? :)

Наконец, вот такая версия программы тоже способна отработать успешно —

Несоблюдение правила Большой Тройки при разработке класса или структуры

Правило Большой Тройки состоит вот в чём: если класс или структура определяет любой из следующих трёх методов:

  1. Деструктор
  2. Конструктор копирования
  3. Оператор присваивания

то он должен определить и два остальных метода! По меньшей мере, если в классе идёт работа с памятью, её динамическое выделение!

В любом случае, эти три метода — особые члены класса и будут созданы компилятором автоматически. Но наверняка автоматически созданные версии не удовлетворят потребности класса, например, по копированию объектов или освобождению ранее выделенной памяти. Приведём классический пример класса-массива, в котором хранится его размерность, а память выделяется динамически.

В рассмотренном случае конструктор по умолчанию тоже не создается автоматически.

Чтение данных «в никуда»

В основном, этим грешат начинающие при использовании scanf , забывая, что ей нужно передавать адрес переменной:

Выведется бред или возникнет Runtime Error. Правильно так:

Использование указателя как массива

Гораздо чаще, чем хотелось бы, приходится видеть в коде такое:

Конечно, так нельзя — предварительно выделите память под a. Указатель — только переменная, предназначенная для хранения адреса памяти, места под элементы массива там нет! Правильно, например, так:

Ещё надёжнее проверять, удалось ли выделить память, особенно если размерности большие:

Перепутаны символ char и строка char *

Отдельный символ (величина типа char ) не есть строка символов, заданная указателем на тип char ( char * )

Скорее уж char похож на целое число int , представляя собой код символа.

При работе с однобайтовыми кодировками char занимает 1 байт памяти.

Строка char * занимает столько байт, сколько в ней символов, плюс ещё один, необходимый для хранения нуль-терминатора — байта с кодом ноль, записываемого как ‘\0’ . Не путать с цифрой ‘0’ , имеющей код 48 !

Вот практический пример. Во избежание синтаксической ошибки «находчивый» новичок явно преобразовал тип char к char * при вызове стандартного метода strcat . Всё, что он получил — Runtime-ошибку «Нарушение прав доступа».

Правильным путём было либо «присобачить» символ к строке «вручную», не забыв про нуль-терминатор:

либо создать массив из 2 символов и тогда уже использовать strcat :

Разумеется, всё это имеет смысл при условии, что в str есть «свободное место», зарезервированное статически или выделенное динамически.

Забыли, что в строке всегда должен быть нуль-терминатор

. и предусмотрен дополнительный символ (байт) для него. Вот этот код в Visual Studio вполне способен привести к краху программы.

Деструктор базового класса должен быть виртуальным!

Вот здесь я приводил эту великую мысль как пример типичной сиплюсплюснутой заумности :) Но что поделать, если это действительно так. Посмотрим простейшую иерархию из родителя A и потомка B , конструкторы и деструкторы которых печатают в консоль факт своего вызова:

Эта программа напечатает: A()B()

То есть, деструктор класса B не вызывался, соответственно, он мог не освободить память, если таковая выделялась конструктором!

Изменив деструктор класса A на

получим вывод A()B()

Таким образом, виртуальный деструктор нужен для правильного полиморфного удаления объектов.

Во избежание проблем всегда следует как минимум:

1. При наличии хотя бы одного виртуального метода объявлять виртуальным и деструктор.

2. Также его надо явно объявлять виртуальным, если класс предполагается в будущем сделать базовым.

Вызов функции, изменяющей некие величины, стоит в одном операторе с изменёнными величинами

Приведём пример простой программы, справа от операторов вывода написано, что печатается.

Как видим, обе функции успешно меняют то, что должны изменить: init — свои параметры, переданные по ссылке, а init2 — глобальную переменную global .

Но попытка вывести изменённые функциями значения в одном операторе с вызовом функций оба раза оказалась безуспешна.

Проблема в том, что до точки с запятой (конца текущего оператора) значения остаются в рамках одной инструкции и не меняются.

Также никогда нельзя рассчитывать на то, что вычисление значений аргументов в списке параметров функции выполняется слева направо.

Ссылка или указатель на «переехавший» объект

На словах эту распространённую проблему можно описать так:

1. В динамический объект с лимитом памяти, например, в вектор, добавляются объекты (элементы) по значению.

2. В какой-то момент запоминается ссылка или указатель на какое-либо из значений вектора.

3. В вектор добавляют ещё некоторое количество объектов.

4. В какой то момент резерв памяти иссякает, и вектор делает realloc , то есть, расширяет буфер, перенося туда свои объекты. В результате объекты меняют адрес.

5. Ранее выданные «наружу» ссылки или указатели становятся недействительными, но программа никак не отследит это.

6. Обращение по неверному указателю или ссылке приводит к ошибке времени исполнения и краху программы. Ну или просто к потере указателя, если читаем данные.

Ошибка очень трудноуловима, потому что «авария» может произойти очень далеко от места причины. Для вектора часто резервируют память ( vector::reserve ) и обычно её хватает. а однажды, при каких-то сочетаниях исходных данных, не хватит.

Цукерберг рекомендует:  Новые возможности PHP 5.6

Совсем простой пример без векторов и классов:

Вывод — не полагаться на ссылку (указатель) на объект, являющийся элементом некой динамической структуры данных.

Неверный вызов конструктора по умолчанию

В показанном ниже коде программист хотел создать объект a класса A и написал круглые скобки для указания того, что нужен конструктор без параметров:

Но эта конструкция была проинтерпретирована как объявление прототипа функции без параметров с именем a , которая возвращает значение типа A .

Правильным было бы объявление

Неявный вызов конструктора по умолчанию базового класса вместо его конструктора копирования

При копировании объектов вместо конструктора копии, у базового класса запускается конструктор по умолчанию. Вот код примера:

Проблема состоит в том, что при наследовании конструктор копирования не будет порождать вызов конструктора копирования базового класса. Программист должен явно указать, какой базовый конструктор нужно запустить. А компилятор запускает только «конструкторы по умолчанию». Правильно было бы заменить строчку

Изменение управляющей переменной цикла в теле цикла

Глупая ошибка, но встречается очень часто, вот она в немного утрированном виде:

Всего лишь помним — внутри цикла for его управляющая переменная не должна меняться иначе, чем в заголовке цикла. Для циклов do .. while и while . do счётчик меняйте всегда последним оператором тела цикла, тогда не возникнет ошибок.

Повторное вычисление границ цикла

Проблема, часто замедляющая быстродействие программы. Цикл for создавался для выполнения заранее известного числа шагов. Не надо заставлять его вычислять это число шагов заново на каждом шаге:

У меня это выполнилось в среднем за 0.875 ms, на каждом шаге заново вычислялись верхняя граница цикла и шаг. Исправим отмеченную комментариями // часть на

и стабильно получим заметно меньшее время. В книгах и инете распространён миф, что компилятор самостоятельно умеет оптимизировать расчёты в условии цикла.

Это не совсем так. В общем случае, компилятор может оптимизировать только константы в условии цикла. Но у меня здесь не произошло и этого. Внешнюю переменную в границе цикла компилятор тем более не будет оптимизировать, как раз потому, что в теле цикла эта переменная может быть изменена.

При удачном стечении обстоятельств компилятор может оптимизировать и внешнюю переменную, если в теле цикла доступна вся информация о ней и есть гарантия, что оптимизация не изменит логики программы. но полагаться на это не стоит.

А вот если вы пользуетесь готовой и адекватной реализацией контейнера, она сама сделает подстановку нового размера. В показанном ниже коде перебор элементов вектора выполняется с помощью итератора i , верхняя граница изменения которого «каждый раз вычисляется заново» методом end :

Равноценна по времени исполнения будет замена выделенного комментариями // цикла на

При этом важно, что в теле такого цикла нельзя менять размерность вектора, удаляя или добавляя элементы, иначе заранее вычисленная константа перестанет соответствовать новой размерности.

Неверный итератор после изменения контейнера внутри цикла

Посмотрим на наполнение выделенного комментариями // цикла for .

Синтаксически вполне корректный, он приведёт к краху программы. Дело в том, что после удаления элемента, итератор i начинает ссылаться на несуществующий элемент и становится невалидным. Следующее i++ уже делается «к ничему» и приводит к краху.

Типовое неверное исправление могло бы быть таким:

Останется крах при размерности списка, кратной 3, то есть, предполагающей удаление последнего элемента. То есть, ошибка станет ещё трудноуловимей. А всё дело в том, что метод erase присваивает итератору ссылку на элемент, идущий следом за удаляемым. А за удаляемым последним ничего нет, и опять инкремент ведёт за разрешённую область памяти.

Правильно будет так:

Обратите внимание, что в заголовке цикла for теперь не делается «автоматического» шага.

Можно было сделать и

Почему? Различия в работе префискного и постфиксного инкрементов, которые нужно хорошо понимать.

Неверный #define с параметрами

Вот пример для этой распространённой проблемы:

Директива #define в этой программе неверна. И ответы будут разные, причём, верен второй, полученный без #define . Дело в том, что макрос — это совсем не функция, а только чисто синтаксическая подстановка, SQUARE(x+1) превратится в x+1 * x+1 или, с учётом старшинства операций, x + x +1 .

Всегда берите аргументы #define в круглые скобки!

На всякий случай, следует избегать и строчных комментариев // при определении #define , хотя современные компиляторы обычно справляются:

А вот всё тело макроса тоже лучше всегда брать в круглые скобки, иначе выйдет вот что:

А вот так всё правильно:

Типичной проблемой является также наличие пробела между именем макроса и открывающей скобкой при описании директивы #define . Это неправильно, открывающая скобка должна идти «впритык» к имени макроса.


Вот так всё правильно:

Объявление и инициализация переменной в ветви case

Нельзя одновременно объявлять и инициализировать переменную в ветви case . Можно поместить ветвь оператора switch в операторные скобки < . >, но это чревато другими ошибками.

Объявляя переменную в одном блоке case , вы делаете её доступной для всех последующих case . Если засунуть в < . >, то область видимости ограничится фигурными скобками.

Компилятор не всегда «увидит» такую ошибку, более того, ряд компиляторов вообще не найдёт проблем в приведённом коде, но это не значит, что так нужно делать.

Несоблюдение отступов в тексте программы

Само по себе не является ошибкой, но создаёт для начинающих массу проблем, основная из которых на C++ — «потерянные» операторные скобки, порождающие кучу самых невероятных ошибок от компилятора.

Чтобы не попасть впросак и не терять времени, всегда соблюдайте несколько несложных правил.

1. Структурируйте и выравнивайте код, чтобы сделать его читаемым. Суть в том, что блоки закрываются скобкой > в том же столбце текстового редактора, в котором они открыты скобкой < . Существует, по меньшей мере, два осмысленных способа сделать это:

Мне лично нравится второй, так как экономит мне одно нажатие Enter и минимум одно нажатие пробела (если редактор текста не понимает автоматических отступов). Закрывающая скобка находится не под открывающей, но под первом символом открывающего блок оператора — это воспринимается как то же самое, стоит Вам написать 10-20 тысяч строк кода. Этот стиль неизменен практически во всех моих листингах.

Что касается количества пробелов в отступах, здесь вкусы тоже разнятся. Мне лично нравится одно нажатие пробела, как самое экономичное:

Мне не нравится также использование символа табуляции для отступа. В большинстве встроенных в IDE редакторов размер отступа табуляции невелик, скажем, равен 4, а в «простых» текстовых редакторах, вроде Блокнота, встроенного редактора Far или Notepad++, в которых тоже приходится открывать листинги, он классически равен 8. В результате программа «разъезжается» далеко вправо на сложных блоках, а вид текста зависит от размера отступа табуляции.

Поэтому в том же Visual Studio я делаю так: меню Сервис — Параметры — Все языки — Табуляция, Отступы = Блок (тогда курсор выравнивает следующую строку по предыдущей), Размер интервала табуляции и Размер отступа = 1, выбрана опция Вставлять пробелы.

2. Для соблюдения правила 1 любые блоки всегда закрывайте сразу. Такой блок, как

пишется сразу, а только потом ветвь алгоритма или тело цикла наполняются содержимым. Это поможет не запутаться в сложном коде и облегчит соблюдение следующего принципа.

3. Если делать всё правильно, некомпилируемый код никогда не остаётся «на завтра». Блочно-модульная структура программы позволяет всегда избежать этого. Подпрограмма может быть пустой «заглушкой», вы можете использовать ничего не делающие условия, пустые блоки, комментарии, но текущий код должен компилироваться, если завтра вы не хотите половину рабочего дня затратить на восстановление в памяти недоделанного сегодня.

4. В редакторе кода используется только моноширинный шрифт, как в старых консолях или современном Studio (загляните в меню Сервис — Параметры — Среда — Шрифты и цвета). Без этого все рассуждения об отступах теряют смысл.

«Неожиданный» результат деления целых чисел

Несмотря на её банальность, проблема встречается довольно часто.

Деление целых в C++ даёт целое, забывать об этом нельзя. Правильно так:

То есть, применяем один из способов:

  • хотя бы один из аргументов превратить в вещественный тип, добавив к числу символ точки;
  • явно привести хотя бы один из аргументов к вещественному типу оператором (float)a или (double)a .
Посимвольное чтение файла обрывается при достижении буквы ‘я’

Проблема не в букве ‘я’, а в байте со значением 255. Именно это значение, рассмотренное, как char ( 0xff ) и приведённое к типу int ( 0xffffffff ) совпадёт со значением EOF , которое равно -1 . При работе под Windows с файлами в кодировке Windows-1251 код номер 255 имеет русская буква ‘я’ маленькая. В других кодировках этому коду может соответствовать другая буква. Пример неверного кода:

Решение — изменить тип переменной c :

Использование strncpy без добавления нуль-терминатора

Стандартная функция strncpy часто используется для «безопасного» копирования строк — её третий аргумент задаёт «максимальное количество символов» (на самом деле, длину буфера).

Проблема в том, что функция «забывает» ставить нуль-терминатор в конец целевой строки:

Правильно было бы поставить перед printf «ручное» завершение строки:

Сама по себе реализация этой функции такова: если исходная строка короче целевого буфера, то strncpy будет заполнять всю хвостовую часть оставшегося буфера нулями. Если строка длиннее целевого буфера, strncpy забудет выполнить её нуль-терминацию. Увы, адекватной замены методу копирования ограниченного количества символов в стандарте нет. Есть strlcpy , но это только для unix-систем.

Обяснение проблемы такое: изначально strncpy предназначалась для перевода обычных нуль-терминированных строк в строки фиксированной ширины.

Строки фиксированной ширины использовались в файловой системе Unix.

В формате фиксированной ширины строка хранится в соответствии со следующими правилами:

  • Все строки хранятся в буфере заранее заданной фиксированной ширины N;
  • Если строка имеет длину меньше N, то все лишние позиции в строке заполняются нулевыми символами;
  • Если строка имеет длину ровно N, то она полностью заполняет буфер и не заканчивается нулевым символом.
Функции atoi/atof или sscanf для перевода строки в число

Указанные функции очень популярны при преобразованиях строки в число. Тем не менее, есть 2 серьёзных проблемы, связанных с ними:

  • они не формируют отчёта об ошибочных ситуациях;
  • они имеют неопределённое поведение при переполнении.

Покажем это на примере:

Стандарт рекомендует использовать для преобразования строк в числа функции группы strto. : strtol , strtoul , strtod . Они умеют сообщать в вызывающий код о неправильном формате входных данных и устойчивы к переполнению, при котором сообщают о нём через стандартную переменную errno :

Кроме того, любую некорректную запись числа atoi / atof считают нулём.

Применение delete к объекту из стека

Вот маленькая программа, на которой хорошо видна проблема:

Суть дела в том, что оператор new выделяет память в «куче» (heap) — см. объект c . Тогда применим и delete .

Объект d создаётся в стеке ( stack ), к нему delete неприменим. Хотя при создании объекта строкой кода

конструктор будет вызван.

Каждому способу выделения памяти для переменой соответствует свой способ освобождения, например

Следует учесть, что при выделении памяти через malloc конструктор не вызывается.

Отметим также, что если для объекта, расположенного в стеке, конструктор выделяет память под какие-либо поля — например, в классе есть поле

а конструктор выделяет для него память

то в классе всё равно пишется деструктор и в нём должен быть delete[] для name . Деструктор в этом случае вызовется автоматически в конце области видимости. Нужно проектировать программу так, чтобы подобный вызов был к месту.

Если же время жизни объекта нужно контролировать независимо от текущей области видимости и освобождать его директивно, явным вызовом деструктора, то такой объект следует размещать в «куче» и выделять память оператором new .

Наконец, в классе можно создать метод с условным именем erase , который закроет все внутренние объекты и освободит занимаемую ими память. Деструктор просто вызывает метод erase , но erase можно вызвать и без деструктора.

См. также в этой заметке про стек и кучу.

Пути к файлам без двойного бэкслеша

Очень частая проблема у начинающих, особенно, если они «пришли» с другого, не си-подобного языка:

В этой программе файл никогда не будет открыт и прочитан, даже если он существует и содержит данные. Дело в том, что обратный слеш внутри двойных кавчек для C и C++ — спецсимвол, например, \t означает табуляцию, а \n — перевод строки. Если внутри двойных кавчек нужен символ «обратный слэш», как известно, служащий в Windows для разделения имён папок при записи пути к файлам, он удваивается: \\.

Поэтому правильно будет так:

«Удвоение» последней строки файла

Возьмём небольшую программу, построчно читающую текстовый файл:

Сам файл C:\temp\data.txt состоит из пары строк и пустой строки в конце:

«Удивительным образом» наша программа «удвоит» string 2 , которая будет напечатана дважды. Проблема в том, что после чтения string 2 конец файла ещё не достигнут, а ввод-вывод через stdio буферизован. последний шаг цикла прочитает пустую строку, а в буфере всё ещё будет string 2 , которая и выведется повторно вместо пустой строки. Проверьте сами — если удалить из файла данных пустую строку в конце —

— никакого «удвоения» не будет. Встречается вот такое решение — — но оно грозит, наоборот, «потерять» последнюю строку файла, если после неё нет пустой. Хотя при форматном чтении данных, скажем, целых чисел из файла, такой подход бывает применим:

(лучше посмотреть здесь про чтение чисел scanf’ом из файла со «смешанным» форматом).

Что же до нашего исходного примера — всегда полезен дополнительный анализ прочитанных строк (например, исключение из вывода пустых) или хоть зануление буфера после того, как он использован:

«Лишние» пустые строки при построчном выводе данных

С программкой из этого примера связана ещё одна типовая проблема — прочитанные из файла строки при выводе почему-то содержат лишние пустые строки между строками данных. Файл данных: Вывод:

Всё объясняется просто — fgets читает строку файла вместе с символом перевода строки (точней, под Windows — с парой символов \r\n , интерпретируемых как один), а puts добавляет к выводимой строке ещё один перевод строки. Так что выводите другим методом или удаляйте из прочитанной fgets ‘ом строки последний символ:

Переопределённый оператор не возвращает экземпляр или ссылку на экземпляр класса

. хотя должен это делать. Попросту говоря, это нужно для того, чтобы работали вычисления по цепочке. Посмотрим внимательно на листинг:

Оператор += изменяет только текущий объект, но тоже может встретиться в цепочке вычислений, скажем, A d=*a+=*b; Вполне достаточно, если он вернёт *this для возможности выполнения дальнейших расчётов.

А вот бинарному сложению после выполнения *a + *b нужно вернуть именно новый объект класса, чтобы можно было, например, продолжить цепочку вычисления, прибавив той же функцией-оператором ещё c : *a + *b + c .

Формально не запрещено, сделать и такое бинарное сложение:

Но выражение над экземплярами класса *a + *b + с работать уже не будет, только *a + *b .

Если в коде примера вы замените тип возвращаемого значения у операторных функций с A & на A , тоже будет работать.

Из текстового файла по формату читается только первое число и всё зацикливается!

Вот эта программа в VS будет зацикливаться на чтении из файла

если файл data.txt в текущей папке вот такой:

(прочитается только первое число).

Решение — либо убрать русскую локаль, для которой разделитель целой и дробной части числа — не точка, а запятая:

(закомментировали оператор), либо в файле заменить . в числах на ,

Если читать средствами C++ (потоки), а Си-совместимыми — аналогично.

Сцепление Си-строк без выделения памяти

Сначала воспроизведем типичную ошибку начинающих:

Так как функция strcat не выделяет память, поведение такого кода непредсказуемо и с вероятностью 99% приведёт к ошибке времени исполнения программы! А вот такое сцепление строк сработает:

То есть, всегда должна быть память, куда писать — статическая из буфера или выделенная динамически самой программой.

Из той же оперы — использование указателя как массива. Не забывайте также, что вместо динамических массивов и строк Си практичнее использовать контейнер vector и класс string.

Неверное использование тернарного оператора

Тернарный условный оператор (conditional expression) вида условие?оператор1:оператор2; очень удобен, но способен создать ряд проблем при неаккуратном его применении.

При работе с ним нужно учесть 2 момента.

Во-первых, типом тернарного оператора будет наиболее общий тип его двух последних операндов. Что значит наиболее общий? Например, для int и short общим типом будет int , то есть, наиболее общий тип — это такой тип, к которому могу быть приведены оба операнда.

Вполне возможны ситуации, когда общего типа нет и возникает ошибка, например:

Кроме того, если в тернарном операторе происходит преобразование типов к наиболее общему, то тернарный оператор является rvalue . Если же нет, то lvalue (что это значит?).


Во-вторых, нужно учитывать приоритеты операций, самый надёжный путь сделать это — брать тернарный оператор в дополнительне круглые скобки, если он записан внутри выражения или другого оператора.

Например, вот эта маленькая программа корректно использует тернарный оператор для вывода пробела или перевода строки в cout (массив печатается по 2 элемента в одной строке консоли):

Без дополнительных скобок в операторе вывода, то есть, при коде

вывод будет воспринят как

и окажется неправильным.

Присваивание объектов класса, содержащих динамические свойства, без написания конструктора копирования

Если в классе есть динамическое свойство, заданное указателем, при копировании объекта класса, под который выделялась динамическая память в «куче», скопируется только адрес памяти, содержащийся в указателе.

Если затем объект 2, бывший справа от знака «=», будет удалён, перестанет работать и ссылка на память из объекта 1:

Для корректной работы нужно опеределить в классе оператор присваивания и конструктор копирования (см. правило большой тройки).

Тогда можно переписать динамическое свойство s в новую память, которую мы выделим в куче (код простейший, не содержит проверок того, удалось ли выделить память):

«Опасная» проверка булевой переменной в условии

Безобидное на первый взгляд

может привести к трудноуловимой ошибке. Например, в ряде компиляторов константа true==-1 , а в C++ принято, что всё, что не 0 , равно true и только 0 — это false .

Соответственно, всегда безопасней

Неверное выделение памяти под динамическую матрицу

Отметим как отдельную ошибку. Правильный путь таков:

  • описать двойной указатель на данные нужного типа;
  • выделить память под указатели на строки матрицы;
  • каждый указатель на строку связать с новой областью памяти;
  • после этого можно заполнять матрицу данными, передавать двойной указатель в качестве аргумента функции и т.п.

Освобождать память, в идеале, нужно в порядке, обратном выделению. Вот пример кода.

12.02.2015, 12:38; рейтинг: 81204

10 типичных ошибок программирования

Существуют различные типы программных ошибок, которые могут возникать на этапе разработки программы программного обеспечения и каждый программист должен знать о них.

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

Если вы абсолютный новичок в области программирования то эта статья непременно будет вам интересна: Основы программирования для начинающих.

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

Основные виды ошибок в программировании

Компьютерное программирование это огромное поле с сотнями языков, которые используют миллионы приложений. Это программирование операционной системы, прикладное программирование, встроенное кодирование системы, веб-разработка, приложения для мобильных платформ, развитие программ, развернутых в интернете, научные вычисления. В таблице представлены основные виды ошибок.

Тип ошибок программирования

Описание

Логическая ошибка

Это, пожалуй, наиболее серьезная из всех ошибок. Когда написанная программа на любом языке компилирует и работает правильно, но выдает неправильный вывод, недостаток заключается в логике основного программирования. Это ошибка, которая была унаследована от недостатка в базовом алгоритме. Сама логика, на которой базируется вся программа, является ущербной. Чтобы найти решение такой ошибки нужно фундаментальное изменение алгоритма. Вам нужно начать копать в алгоритмическом уровне, чтобы сузить область поиска такой ошибки.

Синтаксическая ошибка

Каждый компьютерный язык, такой как C, Java, Perl и Python имеет специфический синтаксис, в котором будет написан код. Когда программист не придерживаться «грамматики» спецификациями компьютерного языка, возникнет ошибка синтаксиса. Такого рода ошибки легко устраняются на этапе компиляции.

Ошибка компиляции

Компиляция это процесс, в котором программа, написанная на языке высокого уровня, преобразуется в машиночитаемую форму. Многие виды ошибок могут происходить на этом этапе, в том числе и синтаксические ошибки. Иногда, синтаксис исходного кода может быть безупречным, но ошибка компиляции все же может произойти. Это может быть связано с проблемами в самом компиляторе. Эти ошибки исправляются на стадии разработки.

Ошибки среды выполнения (RunTime)

Программный код успешно скомпилирован, и исполняемый файл был создан. Вы можете вздохнуть с облегчением и запустить программу, чтобы проверить ее работу. Ошибки при выполнении программы могут возникнуть в результате аварии или нехватки ресурсов носителя. Разработчик должен был предвидеть реальные условия развертывания программы. Это можно исправить, вернувшись к стадии кодирования.

Арифметическая ошибка

Многие программы используют числовые переменные, и алгоритм может включать несколько математических вычислений. Арифметические ошибки возникают, когда компьютер не может справиться с проблемами, такими как «Деление на ноль», или ведущие к бесконечному результату. Это снова логическая ошибка, которая может быть исправлена только путем изменения алгоритма.

Ошибки ресурса

Ошибка ресурса возникает, когда значение переменной переполняет максимально допустимое значение. Переполнение буфера, использование неинициализированной переменной, нарушение прав доступа и переполнение стека — примеры некоторых распространенных ошибок.

Ошибка взаимодействия

Они могут возникнуть в связи с несоответствием программного обеспечения с аппаратным интерфейсом или интерфейсом прикладного программирования. В случае веб-приложений, ошибка интерфейса может быть результатом неправильного использования веб-протокола.

Интенсивное тестирование и фаза отладки неотъемлемая часть цикла разработки программного обеспечения, которое может помочь пресечь эти ошибки в зародыше, прежде чем произойдет полномасштабное развертывание программного обеспечения. Много ошибок можно избежать с помощью предварительного планирования во время стадии кодирования. Большинство ошибок можно исправить в процессе разработки программного обеспечения через практику и строгие процедуры отладки. Ошибки являются частью обучения, и их никогда нельзя полностью избежать, Тем не менее, у вас могут появляться новые ошибки, но повторять старые вы не должны!

10 типичных ошибок программирования

When? This feed was archived on April&nbsp25,&nbsp2020&nbsp04:40 ( 1+&nbspy&nbspago ). Last successful fetch was on March&nbsp15,&nbsp2020&nbsp17:15 ( 1+&nbspy&nbspago )

Why? Inactive feed status. Our servers were unable to retrieve a valid podcast feed for a sustained period.

What now? You might be able to find a more up-to-date version using the search function. This series will no longer be checked for updates. If you believe this to be in error, please check if the publisher’s feed link below is valid and contact support to request the feed be restored or if you have any other concerns about this.

Welcome to Player FM!

Player FM is scanning the web for high-quality podcasts for you to enjoy right now. It’s the best podcast app and works on Android, iPhone, and the web. Signup to sync subscriptions across devices.

Take it with you

Start listening to #My list on your phone right now with Player FM’s free mobile app, the best podcasting experience on both iPhone and Android. Your subscriptions will sync with your account on this website too. Podcast smart and easy with the app that refuses to compromise.

«»THE best podcast/netcast app. Brilliantly useful, fantastically intuitive, beautiful UI. Developers constantly update and improve. No other podcast/netcast app comes close.»»

«»Excellent app. Easy and intuitive to use. New features frequently added. Just what you need. Not what you don’t. Programmer gives this app a lot of love and attention and it shows.»»

«»Thank you for giving me a beautiful, podcast streaming app with a great library»»

«»This is \»the\» way to handle your podcast subscriptions. It’s also a great way to discover new podcasts.»»

«»It’s perfect. So easy to find shows to follow. Six stars for Chromecast support.»»

10 распространенных ошибок при программировании на C# и как их избежать.

Каждый разработчик совершает ошибки, никто от них не застрахован.

Каждый разработчик совершает ошибки, никто от них не застрахован. Большинство программистов учатся методом проб и ошибок. Это часть пути от Junior C# Developer до Senior C# Developer. Тем не менее, не обязательно совершать самому все эти ошибки, чтобы пройти этот путь. Ниже приведены типичные ошибки, с которыми можно встретиться, программируя на C# и пути их обхода.

1. Итерация значений вместо использования LINQ.

Очень часто при написании приложения стоит задача считать множество значений и сохранить их как список (List) или другую коллекцию. Сделать это можно, например, простой итерацией. Рассмотрим создание списка клиентов. Если клиентов сотня тысяч, создать специфический набор данных, перебирая все эти записи — не совсем эффективно. Вместо использования операторов for или foreach, можно использовать LINQ (Language-Integrated Query). Данный язык запросов, синтаксис которого напоминает SQL, фирма Microsoft добавила в языки программирования платформы .NET Framework в конце 2007 г. LINQ изначально был спроектирован для того, чтобы облегчить работу с такими объектами как коллекции.

foreach (Customer customer in CustomerList) <
if (customer.State == «FL») <
tax += customer.Balance;
>
>

var customers = (from customer in CustomerList
where customer.State == «FL»
select customer.Balance).Sum();

Одна строчка кода на LINQ сразу возвращает набор данных, состоящий из 1000 клиентов, вместо перебора 100 000 объектов. Работать одновременно с 1000 значений более эффективно, чем перебирать 100 000.

2. Нелогичное использование «var», если известен тип данных.

С момента появления ASP.NET MVC многие программисты стали использовать LINQ для работы с коллекциями. В большинстве случаев тип получаемых данных неизвестен. В этом случае оператор «var» помогает избежать ошибок при выполнении кода, в случае, если результатом является NULL или неожиданный тип данных.

Тем не менее, желательно, явно декларировать тип данных, если он известен. Это улучшает читаемость кода, и помогает другим программистам работать с ним без лишних усилий.

Рассмотрим предыдущий пример:

var customers = (from customer in CustomerList
where customer.State == «FL»
select customer.Balance).Sum();

В этом примере, возможно, Sum должен иметь тип decimal. Если это точно известно, нужно задекларировать переменную как decimal. Когда другой программист будет читать этот код, он будет знать, что значение будет иметь тип decimal, а не int, например.

3. Использование глобальных переменных класса вместо свойств.

Свойства являются обычными для объектно-ориентированных языков программирования. Они предоставляют гибкий механизм для чтения, записи или вычисления значения частного поля. Но зачем их использовать, если любые переменные класса можно объявить глобальными? Ответ один — свойства дают возможность контролировать как они будут использоваться, чего нельзя отнести к глобальным переменным.

Рассмотрим следующий код:

public decimal Total

В данном случае только сам класс или производные от него классы смогут его использовать. Рассмотрим класс Order, который подсчитывает общее количество заказов клиента. Не хотелось бы, чтобы какой-то внешний класс изменял что-то в заказах, а вот класс Order и производные от него могли бы совершать операции с переменными типа Total. Если просто сделать переменную глобальной, любой класс сможет сделать с ней все, что угодно, без каких-либо ограничений.

4. Забыть освободить объект.

Нехватка памяти и других ресурсов компьютера — реальная проблема для различных приложений. C# предоставляет довольно удобный путь использовать метод Dispose, когда работа с объектом закончена. Для этого даже не обязательно этот метод указывать явно. Об этом позаботиться оператор «using».

file.Read(buffer, 0, 100);

Вышеописанный код, если явно не указать закрытие объекта, может стать причиной нехватки оперативной памяти. Это легко предотвратить используя оператор «using».

using (FileStream file = File.OpenRead(«numbers.txt»)) <
file.Read(buffer, 0, 100);
>

Теперь приложение считает информацию из файла и закроет объект по окончании.

5. Использование «» вместо string.Empty.

Это всего лишь небольшая досада для программиста, она больше влияет на читаемость кода, нежели на его эффективность. Например, «» может быть ошибочно понят как символ пробела « », а это уже совершенно другое значение. Используя string.Empty вместо «» для инициализации строковой переменной, можно избежать любую ошибочную двусмысленность.

6. Использование общих исключений Try – Catch

Многие начинающие программисты используют общий класс Exception вместо того чтобы явно указывать тип перехваченного исключения. Конечно, все существующие в C# классы исключений являются производными от общего класса Exception и можно создать собственный класс исключений, который будет наследовать общий класс. Но в любом случае предпочтительно использование конкретных исключений вместо общих.

Цукерберг рекомендует:  Обучение - Что такое вирусы,и как с этим лучше бороться

try <
newInteger = int.Parse(astring);
> catch (Exception e) <
// do something here
>

try <
newInteger = int.Parse(astring);
> catch (FormatException) <
// do something here
>

Второй вариант кода уже явно указывает тип перехваченного исключения. Таким образом будет проще понять, что вызвало ошибку, и исправить ее. Конечно, можно использовать общий класс Exception для неизвестных возможных ошибок, но делать это надо, по возможности, реже.

7. Использование методов внутри блока Try – Catch.

Ме́тоды являются основой объектно-ориентированного программирования.. Выше был приведен пример простого блока Try — Catch, который содержит только один оператор с обработчиком исключения. Обычная ошибка, которую совершают разработчики, — окружить метод блоком исключения Try – Catch .

Блоки Try – Catch лучше располагать в логической секции кода. Правильно закодированный метод, должен сам выбрасывать исключение, чтобы передать его на верхний уровень для обработки.

Кроме того, не нужно использовать только один блок, если метод совершает несколько логически разных процедур, например, читает файл, распределяет его содержимое по переменным и затем записывает результат в базу данных. Разбейте блоки так, чтобы были видны: чтение файла, занесение данных, сохранение информации в базу данных.


8. Некорректное соединение строковых переменных.

В старых версиях языка для соединения двух строковых переменных достаточно было использовать знак «+». Однако реализация данного пути не всегда была эффективной, и Microsoft представил класс StringBuilder, созданный для оптимизации скорости и памяти при работе со строковыми величинами.

Используйте StringBuilder всякий раз, когда нужно соединить строки и для других операций над строковыми переменными. Конечно, для работы с совсем простыми строчками это делать не совсем обязательно, но для обработки данных полученных из файла или базы данных класс StringBuilder незаменим.

9. Не забудьте про журнал ошибок.

Что произойдет, если Вам позвонит пользователь и скажет, что приложение выдало ошибку. Как узнать, что произошло и что вызвало ошибку? Именно для этого и существуют журналы ошибок. Все ошибки всегда должны сохраняться в журнале. Для этого можно использовать собственный код или настроенный под себя сторонний инструмент. Поскольку сторонних инструментов для создания журналов ошибок существует великое множество, возможно разумнее будет использовать уже существующий и проверенный, чем создавать свой собственный.

10. Наконец, не забудьте обновить свой код.

Это ошибка не только программистов на C#, но на других языках. Visual Studio объединена с Team Foundation Server (TFS). Это хорошая среда для команды разработчиков, но это не будет правильно работать, если программист забудет скачать утром обновленный код, перед тем как продолжить свою работу. Несомненно, можно объединить 2 различных версии кода потом, но это не всегда делается корректно и может привести к неожиданным ошибкам.

Не нужно забывать скачивать последнюю версию кода перед началом работы и загружать на сервер Ваши последние наработки в конце. Никогда не загружайте код с ошибками в нем. Только тот, который может быть скомпилирован. TFS имеет систему позволяющую компилировать код «на лету». Безошибочное компилирование позволяет быть уверенным, что другие программисты могут скачать этот код и использовать его.

Заключение.

C# — достаточно сложный язык. Когда начинаешь изучать объектно-ориентированное программирование, сталкиваешься с большим количеством трудностей. Здесь так много различных правил и стандартов, что непросто избежать ошибок. Однако, делая выводы из своих и чужих ошибок, можно стать хорошим программистом, который создает эффективные и продуктивные приложения.

Если вам необходима помощь разработчиков с опытом — обращайтесь!

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

Программирование – процесс непростой, и без ошибок в процессе не обходится создание ни одной программы. Часть багов, самые неприятные, носят логический характер. Их крайне сложно искать, и нередко такие ошибки «всплывают» уже в процессе эксплуатации. Ошибки синтаксиса и опечатки – самые безобидные. Почти во всех языках их выявляют интерпретаторы и компиляторы. Что-то удается вычислить при автоматическом или бета-тестировании.

Но сейчас мы решили поговорить о самых распространенных ошибках, которые делают практически все. Новичкам эта памятка поможет обратить особое внимание на самые популярные виды багов. Опытным разработчикам этот список просто освежит память. Ведь все мы люди, и малейшая невнимательность может привести к «танцам по старым и всем известным граблям».

Работа с необъявленными переменными

Суть ошибки проста. Вы начинаете пользоваться переменной, которая не была указана в блоке объявления переменных и не получила собственный тип. Как на это отреагирует программа, зависит от выбранного языка:

  1. Если синтаксис ЯП требует жесткого объявления и типизации переменных, интерпретатор «вылетит» из процесса компиляции с указанием ошибки. Это хороший случай, так как вы, скорей всего, быстро поймете, в чем дело, ведь необъявленный объект будет присутствовать в указанной строке ошибки.
  2. При использовании языков с менее жесткой структурой, переменная «объявится» автоматически с момента ее появления в коде. Казалось бы, это удобно. На самом деле, отладка в случае подобной ошибки значительно усложняется. В вашу переменную может быть записано любое значение, в том числе, не предусмотренного программой типа. В результате код будет «вылетать» в строке с попыткой выполнить какие-то вычисления или другой вид обработки с участием этой переменной, что усложнит поиск проблемы. В худшем случае, ошибка будет неявной, логической, т.е. программа будет работать, но выдаст неверный результат.

Не забывайте проверить все переменные, убедиться, что вы их объявили. А при неявном объявлении желательно использовать какие-то дополнительные возможности улучшения стиля. Например, комментарии.

Инициализация переменных без начального значения

Мало объявить переменные, необходимо следить за их начальными значениями. В большинстве языков до того, как вы что-то «положите» в выделенную область памяти, там будет храниться остаточный «мусор», т.е. любой двоичный код, который остался в ячейках до начала работы программы. Это приводит к неприятным казусам.

Приведем пример такого кода:

В результате его исполнения вы можете ввести, например, числа 2 и 5, а в качестве результата получить 2384.

Если вы внимательно прочитали код, то причина ошибки очевидна. Программа сначала производит суммирование num1 и num2, и только потом запрашивает ввод их значений. Что хранилось в ячейках памяти до того, то и компьютер и сложил. Переместите строки ввода данных выше операции сложения, и проблема будет решена.

Необъявленные функции

Эта ошибка особенно часто возникает в случае использования готовых функций, хранящихся в отдельных файлах или библиотеках. Ни один компилятор не пропустит подобный баг. Но поиск причины постоянного «вылета» на строке с функцией нередко занимает много времени. Просто потому, что разработчик видит строку с ошибкой и начинает искать причину где-то рядом.

Казалось бы, ситуация настолько очевидная, что и говорить не о чем. Но вспомните, сколько раз вы тратили время на попытки найти подобный баг? И как долго до вас «доходило», что вы в самом начале программы забыли прописать подключение файла или библиотеки? Именно потому эта популярнейшая ошибка и занимает место в нашем списке.

Переполнение типа

Иногда бывает, что код выглядит логично, а программа вылетает по ошибке из-за проблем с выделением объемов памяти для того или иного типа переменной.

Приведем пример на C++:

Эти строки программы выполняют сложение двух переменных, после чего для переменной G выделяется определенный объем памяти, значение которого хранится в A. Вроде бы, проблем быть не должно.

Но если значения B и C будут большими, их сумма «не поместится» в объем памяти, который занимает A. В результате такого переполнения вместо ожидаемого положительного значения, в переменной A окажется отрицательное число. И на строке выделения памяти для G программа покажет ошибку.

Избежать этого просто: не забывайте добавлять проверку значений на максимально допустимые.

Переполнение буфера

Ошибка переполнения буфера стала одной из самых легендарных в программировании, так как эта уязвимость привела к созданию целой серии вирусов-«червей», начиная с «червя» Морриса. Часть современных языков защищены от этой уязвимости, а потому в результате переполнения программа просто вылетает по ошибке. Другие и сейчас подвержены такому багу, в результате пользователь получает «дыры» в защите компьютера, через которые может проникать вредоносный код.

Самый простой пример подобной проблемы можно описать так: в строковую переменную, которая может вмещать максимум 255 символов, записывают текст большего размера. Если последние из символов, которые уже не вмещаются в выделенный стек памяти, компилятор «прочитывает» как дополнительные адреса ячеек памяти, он обращается к ним. И готов записывать в эту область памяти «излишки» информации. Этим пользуются «черви», которые закидывают в переменную «мусорную информацию», следом за которой идет вредоносный код, который программа «дисциплинированно» размещает в памяти компьютера.

Возникают проблемы и в случае попытки считать что-то из такой переменной. Даже если уязвимость не использована вредоносным кодом, при чтении программа обращается в том числе к ячейкам памяти, расположенным вне буфера, и программа начинает обрабатывать дополнительные ячейки. Но в них уже давно могут оказаться любые «мусорные» данные, т.е. случайная информация, которая хранилась по указанным адресам.

Бороться с этим явлением помогает обычная «защита от дурака», установленная для каждого случая получения данных от пользователя или из внешней утилиты. Т.е. проверка на соответствие типа, диапазона значений, отсутствие исполняемого кода и другие важные для бесперебойной работы программы параметрам.

Ошибки в оценке границ массива

Бытует мнение, что такую ошибку можно допустить только в C или C++. На самом деле, обращение к несуществующему элементу массива возможно в Python, Java и многих других языках. Суть проблемы заключается в том, что программист по причине невнимательности или из-за ошибки в расчетах обращается к элементу массива с несуществующим номером.

Самый просто пример:

  1. Вы определили массив из 10 элементов.
  2. Нумерация в массиве начинается с нуля, т.е. существуют номера с 0 до 9.
  3. Вы забываете об этой особенности массива и обращаетесь к элементу с индексом 10.

При этом программа обращается к неиспользуемой области памяти, присваивает это случайное значение элементу с индексом 10. Результат обработки «мусорной» информации, само собой, непредсказуем.

Чтобы избежать проблем, не поленитесь написать дополнительную проверку на граничные значения. Лучше дописать пару строк кода, чем столкнуться с невразумительными результатами из-за случайного бага.

«Забытые» ограничения ресурсов

Эта проблема возникает при ручном управлении памятью, а также при работе с базами данных или при создании безразмерных массивов. Без строгого контроля и указания ограничений вы рискуете получить один из двух вариантов:

  1. Программа игнорирует ячейки памяти, занятые полезной информацией. В результате в работе возникают критические ошибки.
  2. Приложение «съедает» практически всю оперативную память, «виснет» само и «подвешивает» систему.

Аккуратно относитесь к ограничителям массивов, проверяйте базы данные при слиянии, а при прямом обращении к ОЗУ обязательно указывайте граничные значения.

Обращение к освобожденной памяти

Ошибка наиболее популярна у Си-программистов, так как здесь после завершения работы с блоком памяти ячейки обязательно освобождаются. Но и в других языках случаются подобные проблемы, например, в случае принудительной очистки ради экономии ресурсов.

Суть проблемы заключается в том, что программа обращается к освобожденной памяти уже после очистки. И, естественно, не получает ожидаемых данных.

Казалось бы, об этой ошибке знают все. И проверить на такой баг программу совсем просто. На самом деле, баг крайне популярен даже среди опытных разработчиков. Не зря постоянно появляются новости о «заморозке» крупных программных продуктов из-за такого сбоя.

Инъекции SQL и команд ОС

Эти два типа инъекций на самом деле являются хакерскими атаками, которые оканчиваются доступом злоумышленника либо к базам данных (SQL), либо к root-доступу на компьютере пользователя.

Причины SQL-инъекций – низкий уровень защиты сайта. Чаще всего, их проводят через отправку сообщений от пользователей (форма обратной связи, добавление записи на форум, обращение в чат и т.д.). Если «дыра» в безопасности не закрыта, злоумышленник отправляет через эти формы вредоносный код, сервер начинает его исполнять. И хакер получает доступ ко всем базам данных.

С командами ОС ситуация сходная. Если вы даете разрешение программе пользоваться готовыми командами системы, обязательно нужно поставить защиту от злоумышленников, чтобы приложение исполняло только те команды, которые были заданы разработчиком, но не могло применять ничего больше.

Рискованные алгоритмы

Одна из «любимых» ошибок джуниоров. Заключается она в том, что программист либо начинает «изобретать велосипед» при попытке защитить личные данные пользователей или другую важную информацию, либо, наоборот, использует первый найденный вариант, даже не проверив его на предмет уязвимостей.

С первым случаем все понятно. Защита данных – не та область, где стоит полагаться только на свои, довольно скромные возможности. Пример второго случая – использование алгоритма хэштегирования SHA-1. Если вы воспользуетесь поиском, то очень быстро узнаете, что этот алгоритм уже устарел, в нем найдено множество уязвимостей, под которые написан не один вирус. А потому лучше пользоваться SHA-2 или SHA-3.

Как видите, в большинстве случаев причины багов и уязвимостей – обычная невнимательность к деталям и нежелание лишний раз протестировать код. Будьте внимательны, не забывайте о своих «любимых ошибках» и примерах из нашего рейтинга. При таком подходе ваш код будет работать, как задумано!

​10 распространенных ошибок при программировании на C# и как их избежать.

Каждый разработчик совершает ошибки, никто от них не застрахован. Большинство программистов учатся методом проб и ошибок. Это часть пути от Junior C# Developer до Senior C# Developer. Тем не менее, не обязательно совершать самому все эти ошибки, чтобы пройти этот путь. Ниже приведены типичные ошибки, с которыми можно встретиться, программируя на C# и пути их обхода.

1. Итерация значений вместо использования LINQ.

Очень часто при написании приложения стоит задача считать множество значений и сохранить их как список (List) или другую коллекцию. Сделать это можно, например, простой итерацией. Рассмотрим создание списка клиентов. Если клиентов сотня тысяч, создать специфический набор данных, перебирая все эти записи — не совсем эффективно. Вместо использования операторов for или foreach, можно использовать LINQ (Language-Integrated Query). Данный язык запросов, синтаксис которого напоминает SQL, фирма Microsoft добавила в языки программирования платформы .NET Framework в конце 2007 г. LINQ изначально был спроектирован для того, чтобы облегчить работу с такими объектами как коллекции.

Приведем пример:

foreach (Customer customer in CustomerList) <
if (customer.State == «FL») <
tax += customer.Balance;
>
>

var customers = (from customer in CustomerList
where customer.State == «FL»
select customer.Balance).Sum();

Одна строчка кода на LINQ сразу возвращает набор данных, состоящий из 1000 клиентов, вместо перебора 100 000 объектов. Работать одновременно с 1000 значений более эффективно, чем перебирать 100 000.

2. Нелогичное использование «var», если известен тип данных.

С момента появления ASP.NET MVC многие программисты стали использовать LINQ для работы с коллекциями. В большинстве случаев тип получаемых данных неизвестен. В этом случае оператор «var» помогает избежать ошибок при выполнении кода, в случае, если результатом является NULL или неожиданный тип данных.

Тем не менее, желательно, явно декларировать тип данных, если он известен. Это улучшает читаемость кода, и помогает другим программистам работать с ним без лишних усилий.

Рассмотрим предыдущий пример:

var customers = (from customer in CustomerList
where customer.State == «FL»
select customer.Balance).Sum();

В этом примере, возможно, Sum должен иметь тип decimal. Если это точно известно, нужно задекларировать переменную как decimal. Когда другой программист будет читать этот код, он будет знать, что значение будет иметь тип decimal, а не int, например.

3. Использование глобальных переменных класса вместо свойств.

Свойства являются обычными для объектно-ориентированных языков программирования. Они предоставляют гибкий механизм для чтения, записи или вычисления значения частного поля. Но зачем их использовать, если любые переменные класса можно объявить глобальными? Ответ один — свойства дают возможность контролировать как они будут использоваться, чего нельзя отнести к глобальным переменным.

Рассмотрим следующий код:

В данном случае только сам класс или производные от него классы смогут его использовать. Рассмотрим класс Order, который подсчитывает общее количество заказов клиента. Не хотелось бы, чтобы какой-то внешний класс изменял что-то в заказах, а вот класс Order и производные от него могли бы совершать операции с переменными типа Total. Если просто сделать переменную глобальной, любой класс сможет сделать с ней все, что угодно, без каких-либо ограничений.

4. Забыть освободить объект.

Нехватка памяти и других ресурсов компьютера — реальная проблема для различных приложений. C# предоставляет довольно удобный путь использовать метод Dispose, когда работа с объектом закончена. Для этого даже не обязательно этот метод указывать явно. Об этом позаботиться оператор «using».

Вышеописанный код, если явно не указать закрытие объекта, может стать причиной нехватки оперативной памяти. Это легко предотвратить используя оператор «using».

using (FileStream file = File.OpenRead(«numbers.txt»)) <
file.Read(buffer, 0, 100);
>

Теперь приложение считает информацию из файла и закроет объект по окончании.

5. Использование «» вместо string.Empty.

Это всего лишь небольшая досада для программиста, она больше влияет на читаемость кода, нежели на его эффективность. Например, «» может быть ошибочно понят как символ пробела « », а это уже совершенно другое значение. Используя string.Empty вместо «» для инициализации строковой переменной, можно избежать любую ошибочную двусмысленность.

6. Использование общих исключений Try – Catch

Многие начинающие программисты используют общий класс Exception вместо того чтобы явно указывать тип перехваченного исключения. Конечно, все существующие в C# классы исключений являются производными от общего класса Exception и можно создать собственный класс исключений, который будет наследовать общий класс. Но в любом случае предпочтительно использование конкретных исключений вместо общих.

try <
newInteger = int.Parse(astring);
> catch (Exception e) <
// do something here
>

try <
newInteger = int.Parse(astring);
> catch (FormatException) <
// do something here
>

Второй вариант кода уже явно указывает тип перехваченного исключения. Таким образом будет проще понять, что вызвало ошибку, и исправить ее. Конечно, можно использовать общий класс Exception для неизвестных возможных ошибок, но делать это надо, по возможности, реже.

7. Использование методов внутри блока Try – Catch.


Ме́тоды являются основой объектно-ориентированного программирования.. Выше был приведен пример простого блока Try — Catch, который содержит только один оператор с обработчиком исключения. Обычная ошибка, которую совершают разработчики, — окружить метод блоком исключения Try – Catch .

Блоки Try – Catch лучше располагать в логической секции кода. Правильно закодированный метод, должен сам выбрасывать исключение, чтобы передать его на верхний уровень для обработки.

Кроме того, не нужно использовать только один блок, если метод совершает несколько логически разных процедур, например, читает файл, распределяет его содержимое по переменным и затем записывает результат в базу данных. Разбейте блоки так, чтобы были видны: чтение файла, занесение данных, сохранение информации в базу данных.

8. Некорректное соединение строковых переменных.

В старых версиях языка для соединения двух строковых переменных достаточно было использовать знак «+». Однако реализация данного пути не всегда была эффективной, и Microsoft представил класс StringBuilder, созданный для оптимизации скорости и памяти при работе со строковыми величинами.

Используйте StringBuilder всякий раз, когда нужно соединить строки и для других операций над строковыми переменными. Конечно, для работы с совсем простыми строчками это делать не совсем обязательно, но для обработки данных полученных из файла или базы данных класс StringBuilder незаменим.

9. Не забудьте про журнал ошибок.

Что произойдет, если Вам позвонит пользователь и скажет, что приложение выдало ошибку. Как узнать, что произошло и что вызвало ошибку? Именно для этого и существуют журналы ошибок. Все ошибки всегда должны сохраняться в журнале. Для этого можно использовать собственный код или настроенный под себя сторонний инструмент. Поскольку сторонних инструментов для создания журналов ошибок существует великое множество, возможно разумнее будет использовать уже существующий и проверенный, чем создавать свой собственный.

10. Наконец, не забудьте обновить свой код.

Это ошибка не только программистов на C#, но на других языках. Visual Studio объединена с Team Foundation Server (TFS). Это хорошая среда для команды разработчиков, но это не будет правильно работать, если программист забудет скачать утром обновленный код, перед тем как продолжить свою работу. Несомненно, можно объединить 2 различных версии кода потом, но это не всегда делается корректно и может привести к неожиданным ошибкам.

Не нужно забывать скачивать последнюю версию кода перед началом работы и загружать на сервер Ваши последние наработки в конце. Никогда не загружайте код с ошибками в нем. Только тот, который может быть скомпилирован. TFS имеет систему позволяющую компилировать код «на лету». Безошибочное компилирование позволяет быть уверенным, что другие программисты могут скачать этот код и использовать его.

Заключение.

C# — достаточно сложный язык. Когда начинаешь изучать объектно-ориентированное программирование, сталкиваешься с большим количеством трудностей. Здесь так много различных правил и стандартов, что непросто избежать ошибок. Однако, делая выводы из своих и чужих ошибок, можно стать хорошим программистом, который создает эффективные и продуктивные приложения.

Если вам необходима помощь разработчиков с опытом — обращайтесь!

50 серьезных ошибок в программировании и дизайне, которые стоили жизней, времени и денег. Часть 1

Когда-то я заинтересовался вопросом ответственности в IT. К финансовым потерям мы уже привыкли. Не проданный продукт, ошибка в расчете и недовольный клиент — это те последствия, о которых мы в курсе. Но что, если наши ошибки приведут к непоправимым последствиям. Я несколько раз читал лекции на тему ошибок в программировании, которые привели к человеческим жертвам.

1. Элаи Херцберг попала в историю как первый человек, который погиб под колесами беспилотного автомобиля. Весной 2020 года в темное время суток машина Uber засекла преграду. В начале она подумала, что это мусор, потом — что животное, и только за пару метров поняла, что это человек.

К сожалению, машина не успела затормозить, что привело к смерти человека. Тестирование проходило на модифицированной Volvo XC90, у которой была отключена система экстренного торможения, чтобы не мешать ПО Uber управлять машиной. Не может быть два короля в одном королевстве. Задача тормозить в экстренных случаях была возложена на плечи водителя, который страховал автопилот. Он же в это время смотрел Netflix.

2. Аппарат Therac-25 стал самым резонансным случаем в истории программирования для медицинских девайсов. В силу ошибки race condition, при быстром переключении между магнитным и рентгеновским режимами работы девайса заслонка для рентгеновских лучей не успевала установиться. Из-за этого у 10 пациентов диагностировали лучевую болезнь, что привело к смерти или ампутации пораженных частей тела.

3. 25 февраля 1991 года установка ПВО Patriot не смогла перехватить ракету, пущенную со стороны сил Саддама Хусейна. Ракета попала в барак солдат США, что привело к 28 смертям. Расследование показало, что 24-битные процессоры перехватчика при переводе времени совершают ошибку в 0.013 секунды каждый час. Patriot не перезагружали более 100 часов, что привело к ошибкам вычисления положения ракеты на 600 метров. Вот уж где перезагрузка спасла бы жизни.

4. В 2020 году актер Антон Ельчин был раздавлен собственной машиной при въезде домой. Антон многим запомнился как актер, сыгравший навигатора Чехова в полнометражках «Start Trek». Причиной смерти послужил не интуитивный дизайн ручки передач, представленный Jeep в новых моделях машин.

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

5. В 1992 году ошибки системы распределения маршрутов в Лондонской скорой помощи привели к смерти 30-45 человек (разброс большой, потому что не ясно, смогла бы скорая спасти того или иного человека). Все произошло, когда в Лондоне решили заменить людей операторов на компьютерную систему.

В результате система была введена в эксплуатацию без нагрузочного тестирования и с 81! известным багом. Добавьте к этому еще и то, что интегратор решил сэкономить и купил дешевое оборудование, которое сломалось через пару часов после активного пользования системой. Хаос был настолько безумным, что бывали случаи, когда человек, не дождавшись скорой, умирал, его увозили в морг, и только тогда приезжала скорая.

6. 1979 год. Пенсильвания чуть не стала еще одним Чернобылем, и это самый большой прецедент в истории атомной энергетики США. Внешний дизайн датчиков системы там был очень плохо продуман и был расположен по кругу комнаты. Смена, которая следила за состоянием, не заметила всех показателей, которые указывали на то, что есть утечка, и значение температуры реактора приблизилось к критическому значению. Ситуацию спасла следующая смена, которая выходила на вахту и начинала считывать показания датчиков.

Чтоб считать показатели, надо было пройти по круглой комнате и отдельно смотреть на каждый показатель во многих разных местах. Утечку нашли, реактор отключили, но еще 14 лет проводили работы по очищению территории. Это стало поводом огромных протестных митингов и очень поменяло общественное мнение про «мирный атом».

7. 1992 год, самолет под управлением опытной команды потерпел крушение возле Страсбурга. 87 из 92 человек погибли. Анализ черного ящика показал, что опытные пилоты перепутали настройки автопилота: угол и скорость снижения.

Дизайнер приборной панели очень стремился сэкономить место и расположил эти два индикатора друг возле друга. При том, что даже не смотря на то, что единицы измерения совершенно разные (пилоты хотели задать 3.3 градуса спуска, а задали 3300 футов в минуту). Но для экономии места оба показателя показывались как 3.3. Кому в голову придет показывать 3300 как 33?

8. Моя любимая история. Станислав Петров в 1983 году спас мир, не сделав ничего. Во время разгара холодной войны между США и СССР он служил в штабе антиракетной обороны. Так как две страны обладали атомным оружием, между ними была заключена доктрина взаимного гарантированного уничтожения. Это значило, что как только одна ракета полетит со стороны одной страны в другую, другая может ответить, как хочет. Грубо говоря — начало третьей мировой.

Станислав Петров в 1983 году как раз наблюдал за системой раннего обнаружения ракетного удара. И как же он удивился, когда увидел на экране 5 ракет, которые летели со стороны США в сторону СССР. По всем правилам Петров должен был отдать указания полномасштабного ракетного удара по США. Но, как он сказал: «У него была чуйка». Он предположил, что нападать на СССР всего лишь 5 ракетами — не логично, и решил подождать. Внезапно ракеты пропали, он сделал рапорт.

Расследование определило, что эти 5 ракет — edge case того, как лучи солнца падают на спутник на орбите Молния. Таким образом, Петров, не сделав ничего, подарил нам с вами мир, в котором мы живем. Хотя злые языки говорят, что он тогда был в стельку пьян. Но это не отменяет того, что даже будучи пьяным, ты можешь спасти миллиарды людей.

9. Пациентка Jenny (имя заменено в отчетах) погибла от обезвоживания после химиотерапии. Причина оказалась до ужаса идиотской. Дизайн медицинской системы был настолько плох, что опытные медсестры просто не заметили пункта, который говорил, что для этого пациента необходимо вводить дополнительные элементы сразу после операции.

10. Шестеро велосипедистов сошли с дистанции гонки в Осло в 2014 году. Оказалось, что велосипедисты выпили жидкость для мытья пола, решив, что это энергетик. Дизайн упаковки тоже имеет значение.

11. Два солдата армии США погибли во время учений артиллерийских подразделений. Солдаты понадеялись на данные тактической системы, но не знали, что, если не задать высоту цели — система считает ее равной 0. Вот тебе и рокетджамп.

12. 1994 год, борт авиакомпании China Airlines потерпел крушение и унес жизни более чем 250 человек. При приземлении второй пилот случайно включил автопилот, который начал вносить коррективы в действия пилотов. При снижении пилоты заметили это и смогли выключить автопилот, но, к сожалению, рассинхронизация действий людей и автопилота привела к крушению. При расследовании также была найдена ошибка, патч для которой уже был написан. Борт, потерпевший крушение, был запланирован на апдейт системы сразу после окончания рейса, который, к сожалению, так и не закончился удачно.

13. Knight Capital в свое время перепутали деплои и вместо тестового енва задеплоили новую версию на продакшен. Система думала, что тратятся виртуальные доллары, а на самом деле Knight потеряла 460 миллионов долларов за 45 минут. Скидывались на спасение Knight всем селом.

14. 2015 год, грузовой самолет испанских военно-воздушных сил потерпел крушение около Севильи. Авария была вызвана ошибкой ПО и унесла жизни 4 человек. Airbus после этого случая отозвала все самолеты A400 на проверку. Проблема заключалась в новой версии софта по контролю топлива в двигателях. Система подавала топливо, но очень медленно, от чего 3 из 4 двигателей отключились.

Цукерберг рекомендует:  Ide - Как организовать среду разработки без проблем.

15. 1998 год, один из спутников Nasa, отправленный на Марс, достиг орбиты красной планеты и взорвался. Была ошибка в двух модулях ПО спутника: один ждал данные в метрической системе, а другой отдавал в имперской :) Не додебажили на 327 лямов.

16. Как рассказывает SIG, один из самолетов F-14 разбился вследствие того, что случилась системная ошибка, но программист не обернул ее в catch. Это привело к полному отключению бортового компьютера. Пилот катапультировался, но самолет, конечно же, не спасли.

17. Кибервойна, или взрыв, который было видно из космоса. В 1982 ЦРУ внедрило шпиона в канадскую фирму по разработке софта для газопроводных систем, потому как знало, что этот софт будет использован СССР. Программист-шпион написал методы, из-за которых в 1982 году газопроводная труба взорвалась так сильно, что взрыв можно было наблюдать из космоса. К счастью, никто кроме оленей не пострадал.

18. Новая, в 1996 году, ракета Ariane 5, разработка которой стоила около 8! миллиардов долларов, должна была вывести на орбиту несколько спутников и другое оборудование. Полет ракеты завершился через 4 секунды после взлета. Она взорвалась. Один из модулей системы попытался сконвертировать 64-битное число в 16-битное. Оно оказалось больше, чем влезало в память, и модуль завалился. Но ведь есть дублирующий модуль, которому было передано управление! На нем была та же версия ПО, которая попытался сделать то же самое. Результат — самоликвидация и горящие осколки на площади в 12 км2 на потеху зрителям. Во время расследования инцидента была проведена симуляция полета Ariane 5 с использованием другой инерциальной платформы — баг воспроизвели. Программа сбойнула точь-в-точь как в случае с реальной ракетой.

С одной стороны, причиной сбоя можно назвать оптимизацию кода, который не проверял допустимые значения для той конкретной переменной — программисты не предполагали, что число, описывающее горизонтальную скорость, может быть настолько великим.

С другой стороны, провинились и тестировщики — хоть и были произведены сотни тестов и тысячи последующих корректировок, никто не сделал полноценного анализа опорной инерциальной системы. Проблема была в том, что на Ariane 5, чья траектория полета сильно отличалась от Ariane 4, поставили навигационный софт от Ariane 4. Тот случай, когда железо проапгрейдили, а софтину обновить забыли.

10 самых дорогих ошибок в разработке ПО

Ещё совсем свежа память об уязвимости Heartbleed в Open SSL, поставившей под удар сотни тысяч приложений по всему миру. Пока дотошные багоискатели продолжают исследовать код Open SSL в поисках новой ошибки, способной с не меньшей силой заставить сердца специалистов по безопасности ныть от тревожного предчувствия, мы решили оглянуться назад и вспомнить самые дорогостоящие и ужасные по последствиям технологические катастрофы, случившиеся из-за ошибок разработчиков программного обеспечения. От потерянных в глубинах космоса спутников, стоимостью в годичный ВВП небольшого островного государства, до печально известного червя Морриса, мутировавшего из безобидного эксперимента в напасть года, что, кстати, не помешало его автору стать уважаемым человеком – профессором MIT и сооснователем Y Combinator.

Momento bagus, software engineer!

В 1998 году агентство NASA потеряло в космосе спутник «Mars Climate Orbiter». Эта катастрофа озадачила инженеров – удивительно, как такое могло поизойти. В результате оказалось, что субподрядчик, который работал над многими инженерными задачами, не выполнил простейшего преобразования английских единиц измерения в метрическую систему. Из-за фатальной ошибки аппарат стоимостью 125 миллионов долларов оказался слишком близко к поверхности Марса. Диспетчеры пришли к выводу, что спутник на большой скорости вошел в марсианскую атмосферу, где из-за возникших перегрузок его системы связи вышли из строя. Неуправляемый Mars Climate Orbiter попал на околосолнечную орбиту, миссия была провалена.

На новейшей французской беспилотной ракете-носителе «Ariane 5» решили использовать то же программное обеспечение, которое было разработано для более ранней модели – Ariane 4. К сожалению, более мощный двигатель Ariane 5 спровоцировал баг, не встречавшийся в предыдущих версиях ПО. Через тридцать шесть секунд после первого запуска ракеты пришлось активировать систему самоуничтожения, так как возникла целая череда программных ошибок. В сущности, программа попыталась записать 64-разрядное число в 16-разрядное пространство. Возникло переполнение, в результате которого отказал и основной, и резервный компьютер (поскольку на обоих компьютерах выполнялись одни и те же программы). На разработку Ariane 5 было потрачено около 8 миллиардов долларов. Общая стоимость спутников, которые должна была вывести на орбиту эта ракета, составляла 500 миллионов долларов. В следующем ролике мы видим ошеломленного инженера, наблюдающего взрыв ракеты. Затем специалист записывает на бумажке какое-то короткое слово – F…A…I…L , вероятно .

В 2004 году компания EDS разработала сложную компьютерную систему по выплате пособий для британского агентства помощи детям (CSA). В то же время Министерство труда и пенсионного обеспечения (DWP) приняло решение реорганизовать это агентство. Две программные системы оказались полностью несовместимы, в результате были спровоцированы необратимые ошибки. Система переплатила 1,9 миллионам человек и недоплатила семистам тысячам. В итоге накопилось 7 миллиардов долларов, не попавших на социальные счета, 239 000 нерассмотренных дел, 36 000 новых дел, «застрявших» в системе. Все эти ошибки обошлись британским налогоплательщикам в сумму более 1 миллиарда долларов.

Советская газотранспортная система была настолько сложной, что управлять ею можно было лишь с помощью продвинутого автоматизированного ПО. Которого в стране, конечно, не было. В ЦРУ узнали, что советские агенты собираются украсть планы подобной системы, и вышли на контакт с канадской компанией, разрабатывавшей ПО такого рода. Сотрудникам компании было поручено специально внести в код определённые ошибки, чтобы СССР получил дефектную программу. В июне 1982 года на участке газопровода произошел мощный взрыв, который, по некоторым данным, был крупнейшим неядерным взрывом в истории планеты.

Незадолго до открытия пятого терминала в аэропорту Хитроу персонал тестировал новейшую систему для транспортировки больших объемов багажа, поступающего в аэропорт ежедневно. Перед открытием терминала она была тщательно протестирована на 12 000 пробных «единицах» багажа. Все испытания прошли безупречно, но в день открытия терминала оказалась, что система неработоспособна. Вероятно, причиной тому стали непредусмотренные практические ситуации. Например, пассажир мог забыть в сумке какой-то важный предмет, и багаж вручную забирали из транспортной системы. Весь процесс обработки нарушался, и система отказывала. В течение следующих десяти дней около 42 000 мест багажа не были доставлены владельцам, из-за этого пришлось отменить более 500 рейсов.

В 1962 году космический корабль «Mariner 1» должен был отправиться к Венере. Однако едва ракета успела оторваться от космодрома на мысе Канаверал, как угрожающе отклонилась от курса. Возникла угроза падения на землю. Инженеры NASA, управлявшие полетом с Земли, активировали систему самоуничтожения ракеты. Позже ревизионная комиссия пришла к выводу: авария возникла из-за того, что в программных инструкциях был пропущен дефис. В результате корабль получал неверные управляющие сигналы. Стоимость ракеты составляла 18 миллионов долларов без учёта инфляции.

Червь Морриса – под таким названием стала известна программа, разработанная в 1988 году аспирантом Корнеллского университета Робертом Тэппеном Моррисом. Автор утверждал, что задумал его как «безобидный эксперимент», но из-за ошибки в коде она вышла из-под контроля и начала стремительно распространяться, выводя из строя тысячи компьютеров. Общая стоимость устранения ущерба составила около 100 миллионов долларов. Роберта Морриса обвинили в компьютерном преступлении и оштрафовали на 10 000 долларов. На суде адвокат заявил, что созданная подзащитным программа помогла усовершенствовать компьютерную безопасность. Стоит отметить, что Моррис был сооснователем инкубатора стартапов Y Combinator, а в настоящее время является адъюнкт-профессором Массачусетского технологического института. Дискета с исходным кодом червя Морриса хранится в Бостонском университете.

В феврале 1991 года американский комплекс противоракетной обороны «Patriot», установленный в Саудовской Аравии, пропустил ракетный удар по армейским казармам. Правительственная комиссия обнаружила, что это произошло из-за программного сбоя, который привел к «неточности в процедуре отслеживания, усугубившейся в ходе долговременной эксплуатации системы». К моменту катастрофической ошибки комплекс «Patriot» беспрерывно проработал в течение более 100 часов. Накопилась достаточно серьезная погрешность, в результате которой комплекс ПВО неправильно вычислил курс атакующей ракеты. В результате прямого попадания погибли 28 американских солдат. Ещё до инцидента американские инженеры успели исправить в программе баг, из-за которого в работе «Patriot» возникали неточности. Исправленная версия ПО была доставлена на пострадавшую военную базу на следующий день после атаки.

В 1994 году профессор математики обнаружил баг в популярном процессоре Intel от Pentium и опубликовал об этом статью. Компания Intel в ответ на это замечание заявила, что готова заменять процессоры по требованию пользователей, которые смогли бы доказать, что пострадали в результате этой ошибки. По расчетам Intel, вероятность её возникновения была столь низкой, что подавляющее большинство пользователей даже не заметили бы её. Возмущённые клиенты потребовали заменить процессоры всем желающим, и Intel пришлось на это пойти. Финансовые потери Intel в результате составили около 475 миллионов долларов.

Компания Knight, один из ключевых игроков американского фондового рынка, едва не обанкротилась в результате одной программной ошибки. Из-за возникшего бага компания всего за полчаса потеряла около 440 миллионов долларов. В течение 2 дней, когда неисправное ПО наводнило рынок незапланированными сделками, котировки акций компании упали на 75 процентов. Предполагается, что содержавший ошибку биржевой алгоритм Knight стал совершать незапланированные сделки примерно на 150 торговых площадках, просто парализовав их.

Список был бы неполным без упоминания о спутнике NOAA-19 – ещё одной аварии, которая, правда, не связана с программными ошибками. 6 сентября 2003 года в ходе сборки на заводе Lockheed Martin Space Systems был сильно поврежден спутник. Аппарат рухнул на пол, когда техники устанавливали его в горизонтальное положение. Расследование этого происшествия показало, что случившееся было вызвано плохой производственной дисциплиной. Как оказалось, из тележки, которую использовали в этой процедуре, ранее техник извлёк 24 болта, закреплявших переходную платформу, но не задокументировал этого. Рабочие, которые воспользовались этой тележкой при изменении положения спутника, не проверили болты, хотя обязаны были это сделать. Стоимость ремонта спутника составила 135 миллионов долларов.

SavePearlHarbor

Ещё одна копия хабора

10 распространенных ошибок при программировании на C# и как их избежать

Каждый разработчик совершает ошибки, никто от них не застрахован.

Большинство программистов учатся методом проб и ошибок. Это часть пути от Junior C# Developer до Senior C# Developer. Тем не менее, не обязательно совершать самому все эти ошибки, чтобы пройти этот путь.

Ниже приведены типичные ошибки, с которыми можно встретиться программируя на C#, и пути их обхода.

1. Итерация значений вместо использования LINQ

Очень часто при написании приложения стоит задача считать множество значений и сохранить их как список (List) или другую коллекцию. Сделать это можно, например, простой итерацией.

Рассмотрим создание списка клиентов. Если клиентов сотня тысяч, создать специфический набор данных, перебирая все эти записи — не совсем эффективно. Вместо использования операторов for или foreach можно использовать LINQ (Language-Integrated Query). Данный язык запросов, синтаксис которого напоминает SQL, фирма Microsoft добавила в языки программирования платформы .NET Framework в конце 2007 г. LINQ изначально был спроектирован для того, чтобы облегчить работу с такими объектами как коллекции.

Одна строчка кода на LINQ сразу возвращает набор данных, состоящий из 1000 клиентов, вместо перебора 100 000 объектов. Работать одновременно с 1000 значений более эффективно, чем перебирать 100 000.

2. Нелогичное использование «var», если известен тип данных

С момента появления ASP.NET MVC многие программисты стали использовать LINQ для работы с коллекциями. В большинстве случаев тип получаемых данных неизвестен. В этом случае оператор «var» помогает избежать ошибок при выполнении кода, в случае, если результатом является NULL или неожиданный тип данных.

Тем не менее, желательно явно декларировать тип данных, если он известен. Это улучшает читаемость кода, и помогает другим программистам работать с ним без лишних усилий.

Рассмотрим предыдущий пример:

В этом примере, возможно, Sum должен иметь тип decimal. Если это точно известно, нужно задекларировать переменную как decimal. Когда другой программист будет читать этот код, он будет знать, что значение будет иметь тип decimal, а не int, например.

3. Использование глобальных переменных класса вместо свойств

Свойства являются обычными для объектно-ориентированных языков программирования. Они предоставляют гибкий механизм для чтения, записи или вычисления значения частного поля. Но зачем их использовать, если любые переменные класса можно объявить глобальными? Ответ один — свойства дают возможность контролировать как они будут использоваться, чего нельзя отнести к глобальным переменным.

Рассмотрим следующий код:

В данном случае только сам класс или производные от него классы смогут его использовать. Рассмотрим класс Order, который подсчитывает общее количество заказов клиента. Не хотелось бы, чтобы какой-то внешний класс изменял что-то в заказах, а вот класс Order и производные от него могли бы совершать операции с переменными типа Total. Если просто сделать переменную глобальной, любой класс сможет сделать с ней все, что угодно, без каких-либо ограничений.

4. Забыть освободить объект

Нехватка памяти и других ресурсов компьютера — реальная проблема для различных приложений. C# предоставляет довольно удобный путь использовать метод Dispose, когда работа с объектом закончена. Для этого даже не обязательно этот метод указывать явно. Об этом позаботиться оператор «using».

Вышеописанный код, если явно не указать закрытие объекта, может стать причиной нехватки оперативной памяти. Это легко предотвратить используя оператор «using».

Теперь приложение считает информацию из файла и закроет объект по окончании.

5. Использование «» вместо string.Empty

Это всего лишь небольшая досада для программиста, она больше влияет на читаемость кода, нежели на его эффективность. Например, «» может быть ошибочно понят как символ пробела « », а это уже совершенно другое значение. Используя string.Empty вместо «» для инициализации строковой переменной, можно избежать любую ошибочную двусмысленность.

6. Использование общих исключений Try – Catch

Многие начинающие программисты используют общий класс Exception вместо того, чтобы явно указывать тип перехваченного исключения. Конечно, все существующие в C# классы исключений являются производными от общего класса Exception и можно создать собственный класс исключений, который будет наследовать общий класс. Но в любом случае предпочтительно использование конкретных исключений вместо общих.

Второй вариант кода уже явно указывает тип перехваченного исключения. Таким образом будет проще понять, что вызвало ошибку, и исправить ее. Конечно, можно использовать общий класс Exception для неизвестных возможных ошибок, но делать это надо, по возможности, реже.

7. Использование методов внутри блока Try – Catch

Ме́тоды являются основой объектно-ориентированного программирования. Выше был приведен пример простого блока Try — Catch, который содержит только один оператор с обработчиком исключения.

Обычная ошибка, которую совершают разработчики, — окружить метод блоком исключения Try – Catch .

Блоки Try – Catch лучше располагать в логической секции кода.
Правильно закодированный метод, должен сам выбрасывать исключение, чтобы передать его на верхний уровень для обработки.

Кроме того, не нужно использовать только один блок, если метод совершает несколько логически разных процедур, например, читает файл, распределяет его содержимое по переменным и затем записывает результат в базу данных. Разбейте блоки так, чтобы были видны: чтение файла, занесение данных, сохранение информации в базу данных.

8. Некорректное соединение строковых переменных

В старых версиях языка для соединения двух строковых переменных достаточно было использовать знак «+». Однако реализация данного пути не всегда была эффективной и Microsoft представил класс StringBuilder, созданный для оптимизации скорости и памяти при работе со строковыми величинами.


Используйте StringBuilder всякий раз, когда нужно соединить строки и для других операций над строковыми переменными. Конечно, для работы с совсем простыми строчками это делать не совсем обязательно, но для обработки данных полученных из файла или базы данных класс StringBuilder незаменим.

9. Не забудьте про журнал ошибок

Что произойдет, если вам позвонит пользователь и скажет, что приложение выдало ошибку? Как узнать, что произошло и что вызвало ошибку? Именно для этого и существуют журналы ошибок. Все ошибки всегда должны сохраняться в журнале.

Для этого можно использовать собственный код или настроенный под себя сторонний инструмент. Поскольку сторонних инструментов для создания журналов ошибок существует великое множество, возможно разумнее будет использовать уже существующий и проверенный, чем создавать свой собственный.

10. Наконец, не забудьте обновить свой код

Это ошибка не только программистов на C#, но на других языках.

Visual Studio объединена с Team Foundation Server (TFS). Это хорошая среда для команды разработчиков, но это не будет правильно работать, если программист забудет скачать утром обновленный код, перед тем как продолжить свою работу.

Несомненно, можно объединить 2 различных версии кода потом, но это не всегда делается корректно и может привести к неожиданным ошибкам.

Не нужно забывать скачивать последнюю версию кода перед началом работы и загружать на сервер ваши последние наработки в конце. Никогда не загружайте код с ошибками в нем. Только тот, который может быть скомпилирован. TFS имеет систему, позволяющую компилировать код «на лету». Безошибочное компилирование позволяет быть уверенным, что другие программисты могут скачать этот код и использовать его.

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

Введение

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

Переполнение буфера

ереполнение буфера является очень распространенным видом уязвимости приложений и сводится к записи в некий буфер программы информации, объем которой превышает размер буфера. Подобная ситуация возникает исключительно из-за ошибок программистов, чаще всего в результате использования в программе буферов статического размера для хранения данных динамической длины. Например, выделяя буфер для хранения введенного пароля, программист рассуждает: «Длина пароля — не более 16 символов, поэтому я выделю 30 символов, и этого точно хватит».

Переполнение буфера может применяться злоумышленником для решения ряда задач, из которых можно выделить две основные:

  • срыв стека. Осуществляется за счет переполнения буфера, расположенного в стеке. Как известно, в стеке размещается адрес возврата из текущей подпрограммы, который при переполнении буфера окажется поврежденным. В простейшем случае это приведет к сбою в работе программы, а при применении специально сформированного буфера появится возможность выполнения произвольного кода, созданного злоумышленником;
  • несанкционированная модификация данных программы. Этот метод основан на том, что при переполнении буфера запись информации происходит в память, расположенную за правой границей буфера. В случае размещения в этой памяти других переменных произойдет их перезапись данными из подготовленного злоумышленником буфера.

На нашем компакт-диске можно найти простейший пример, позволяющий изучить переполнение буфера. На первый взгляд текст примера не содержит грубых ошибок:

int _tmain(int argc, _TCHAR* argv[])

bool AuthComplete = false;

char Passwd[10];

char Login[10];

int Cnt = 0;

// Запрос имени пользователя

printf(“Enter login:”);

// Запрос пароля

printf(“Enter password:”);

// Увеличение счетчика попыток

Cnt++;

// Проверка имени и пароля

if (strcmp(&Login[0], “Admin”) == 0 &&

strcmp(&Passwd[0], “TopSecret”) == 0)

AuthComplete = true;

> while ((Cnt

Данный код вставляет в генерируемую HTML-страницу текст, переданный ему в параметре запроса с именем text_var1. Однако никакого контроля содержимого text_var1 не предусмотрено, а потому злоумышленник вместо текста получает возможность передать произвольный HTML-код, который может содержать вредоносный скрипт.

  • контроль типа параметров, в результате которого должно выявляться соответствие переданных данных ожидаемому типу данных;
  • создание единых алгоритмов проверки содержимого параметров, содержащих текстовые данные.

Другой тип ошибки состоит в том, что приложение подготавливает текст запроса к базе данных динамически, вставляя в него некоторые параметры. Отсутствие контроля параметров может привести к тому, что злоумышленник получит возможность модифицировать выполняемый запрос или вообще выполнить в базе произвольные SQL-команды. Предположим, что приложение выполняет запрос к базе Oracle, для чего в разработанном на Delphi приложении имеется код вида:

OracleQuery1.SQL.Clear;

OracleQuery1.SQL.Add(‘SELECT EMPNO, ENAME, JOB, SAL’);

OracleQuery1.SQL.Add(‘FROM SCOTT.EMP’);

OracleQuery1.SQL.Add(‘Where DEPTNO = 20’);

if GetParam(‘EMPNO’) <> ‘’ then

OracleQuery1.SQL.Add(‘ AND EMPNO=’+ GetParam(‘EMPNO’);

OracleQuery1.Execute;

На первый взгляд код не содержит грубых ошибок: подготавливается текст запроса, выборка всегда ограничена отделом номер 20, при отсутствии параметра с табельным номером сотрудника выводятся данные по всему отделу, а при его наличии добавляется условие фильтрации по табельному номеру. Данный код будет прекрасно работать, но сделает приложение крайне уязвимым для атаки. Например, передача параметра EMPNO, равного «1 or 1=1», приведет к выводу всей таблицы SCOTT.EMP (причина этого очевидна: описанный выше программный код соберет запрос с условием «Where DEPTNO = 20 and EMPNO=1 or 1=1»). Немного усложнив передаваемую строку, можно получить доступ к другим таблицам базы данных, например передача параметра «-1 union all select sid, username||’ — ‘||osuser||’ — ‘||machine, null, null from v$session» приведет к выводу данных о текущих сессиях базы данных вместо данных о сотрудниках 20-го отдела. Подобные ошибки часто встречаются как в web-приложениях, так и в клиентских приложениях в архитектуре «клиент-сервер». Иногда случаются более опасные уязвимости подобного типа, позволяющие злоумышленнику выполнить произвольные SQL-операторы.

  • обязательно контролировать типы, размерность и содержимое всех параметров, применяемых при конструировании запросов. Например, в описанном выше примере для устранения данной уязвимости достаточно было применить конструкцию вида «OracleQuery1.SQL.Add(‘ AND EMPNO=’+ IntToStr(StrToIntDef(GetParam(‘EMPNO’), -1));»;
  • по возможности следует избегать динамической сборки и модификации запросов. Помимо повышения уязвимости приложения и затруднения его отладки, динамическая модификация запросов отрицательно сказывается на быстродействии из-за затрат времени и ресурсов на повторный парсинг запросов, построения для них планов выполнения и т.п.;
  • работа приложения с базой данных должна производиться из-под учетной записи пользователя, обладающего минимально необходимыми для работы задачи привилегиями;
  • при разработке клиентских приложений необходимо учитывать возможность модификации приложения злоумышленником — текст запросов достаточно легко найти и изменить в откомпилированной программе. Поэтому желательно снабдить клиентское приложение защитой от модификации;
  • для ответственных задач следует применить дополнительные средства ограничения доступа (например, доступ к таблицам через представления, обработку и модификацию данных при помощи хранимого программного кода, разработка триггеров для расширенного аудита и т.п.).

Утечка информации через средства регистрации ошибок

азличные средства протоколирования и регистрации ошибок являются неотъемлемой частью многих крупных проектов. Однако доступность отладочной информации и информации об ошибках может привести к существенному снижению защищенности приложения.

  • пользователь приложения должен получать необходимый минимум информации об ошибке, достаточный для принятия решения о дальнейших действиях. Все детализированные данные (тексты запросов, коды ошибок, информация о том, в какой строке и в каком модуле возникла ошибка, и т.п.) должны регистрироваться в недоступных для пользователя местах;
  • в случае отображения информации об ошибке необходимо учитывать, что многие системы помещают в текст сообщения достаточно большое количество технических данных, которые могут применяться злоумышленником.

Хранение паролей в теле программы

анная ошибка является весьма распространенной и состоит в том, что где-то в программе размещаются параметры для идентификации на некотором сервере, для доступа к базе данных и т.п. Эти данные могут быть обнаружены и использованы злоумышленниками. Другим вариантом этой ошибки является хранение паролей в INI-файле программы, реестре или ином незащищенном хранилище.

Следует учитывать, что утечка паролей и иной конфиденциальной информации может произойти без ведома программиста. Например, ряд предназначенных для работы с базами данных VCL-компонентов для Delphi содержат свойства для хранения строки соединения, имени и пароля. Задание этих свойств во время дизайна проекта упрощает его разработку и отладку, но значения этих свойств сохраняются в ресурсах в открытом виде и могут быть обнаружены злоумышленником.

  • изучение применяемых компонентов и правил их использования. Например, в VCL-компонентах DOA для доступа к серверу Oracle имеется свойство DesignConnection, блокирующее сохранение параметров соединения с базой данных в ресурсах создаваемого EXE-файла (рис. 4);
  • анализ приложения на предмет наличия в нем конфиденциальной информации.

Рис. 4. Фрагмент ресурсов EXE-файла, использующего компонент DOA

Собственная реализация идентификации пользователя и ограничения его прав

анная ошибка широко распространена и состоит в том, что идентификация пользователя средствами базы данных подменяется его идентификацией при помощи обрабатывающей запросы пользователя программы, которая, в свою очередь, поддерживает соединение или пул соединений с базой данных. Применяемая в таком приложении методика идентификации пользователей зачастую не выдерживает никакой критики и сводится к наличию в базе данных таблицы с полями «Имя пользователя», «Пароль», «Уровень доступа». В клиент-серверных приложениях данная ошибка нередко усугубляется тем, что пароль пользователя хранится в базе в виде открытого текста (или зашифрован примитивным алгоритмом типа XOR) и запрашивается для проверки на сторону клиента.

Другим аспектом подобной реализации защиты является то, что приложение соединяется с базой данных от имени некоторого пользователя, имеющего права доступа к необходимым объектам базы данных, и независимо от того, кто работает с приложением — администратор или пользователь с гостевым доступом, вся защита и разграничение доступа производится исключительно на логическом уровне.

Утечка данных из таблицы пользователей и их подмена позволяют взломать подобную систему. В случае с приложением типа «клиент-сервер» возможен взлом клиентской программы, сводящийся к отключению проверки идентификационных данных пользователя или нейтрализации программного ограничения его прав. Третьим путем атаки, часто применяемым в случае с клиент-серверным приложением, является соединение с базой данных при помощи неких посторонних программ — это возможно, поскольку выяснить параметры соединения с базой, являющиеся общими для всех пользователей, нетрудно.

  • в случае применения подобных способов необходимо предпринять дополнительные меры защиты базы данных — в частности создать учетную запись с минимальными правами доступа;
  • по возможности использовать механизмы идентификации, защиты и аудита базы данных. Многие разработчики игнорируют подобную возможность (в частности, добиваясь совместимости своего приложения с несколькими базами данных различных производителей), что, естественно, снижает защищенность системы. Например, для проверки имени или пароля пользователя можно применить хранимый программный код, доступный приложению только на выполнение. Хранимый программный код, в свою очередь, будет оперировать таблицами, содержащими данные о пользователях, но эти таблицы не будут доступны приложению напрямую;
  • применение особых механизмов защиты базы данных, основанных на блокировке доступа к объектам по определенным условиям, на динамическом назначении привилегий и на установке поведенческих ловушек, различающих почерк работы легитимного приложения и посторонних программ. Описание этих механизмов выходит за рамки данной статьи, однако можно отметить, что у современных серверов баз данных (например, Oracle последних версий) предусмотрена масса средств, позволяющих реализовать подобные методы защиты.

Применение криптографических алгоритмов собственной разработки

рограммисты часто применяют алгоритмы собственной разработки для защиты некоторых данных. Общий недостаток таких алгоритмов состоит в том, что зачастую они обладают низкой криптостойкостью. Часто встречаются различные виды подстановки и перестановки, шифрование при помощи XOR по некоторому ключу сравнительно небольшой длины. В случае применения подобных алгоритмов для защиты несущественных данных от «любопытного пользователя» особой опасности, естественно, нет, но для защиты конфиденциальной информации подобный подход неприменим.

  • применение в случае необходимости современных алгоритмов шифрования и цифровой подписи. Реализации большинства из известных алгоритмов доступны в виде исходных текстов, поэтому особых проблем с их применением быть не должно;
  • привлечение профессиональных криптоаналитиков для решения задач криптозащиты.

Передача конфиденциальной информации по открытому каналу

одобные случаи можно часто наблюдать в различных Web-приложениях, немного реже — в приложениях в архитектуре «клиент-сервер». Передаваемые открытым текстом конфиденциальные данные могут быть перехвачены злоумышленником при помощи сниффера и использованы для последующего доступа к системе. В качестве простейшего примера можно рассмотреть стандартную идентификацию пользователя в HTTP-запросе:

GET / HTTP/1.1

Accept: */*

Accept-Language: ru

Accept-Encoding: gzip, deflate

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)

Connection: Keep-Alive

Authorization: Basic QWRtaW46VG9wU2VjcmV0

В строке Authorization содержатся имя и пароль (Admin:TopSecret), закодированные по алгоритму Base64.

Идентификация при помощи Web-форм еще более уязвима, например:

POST / HTTP/1.1

Accept-Language: ru

Content-Type: application/x-www-form-urlencoded

Accept-Encoding: gzip, deflate

Content-Length: 51

Connection: Keep-Alive

Cache-Control: no-cache

  • применение защищенных протоколов (например, HTTPS) для передачи конфиденциальных данных. В качестве альтернативного варианта возможно применение собственных реализаций защиты на основе несимметричного шифрования;
  • передача вместо имени и пароля некоторой вычисленной по ним хеш-функции.

Выводы

данной статье рассмотрены некоторые наиболее распространенные ошибки, приводящие к появлению в программах различных уязвимостей. Как показывает практика, наиболее эффективными мерами защиты (применимыми в основном для корпоративной среды) являются:

  • обучение программистов, ибо нередки ситуации, когда профессиональные разработчики прикладных программ не задумываются о вопросах безопасности создаваемых приложений;
  • разработка корпоративных стандартов, регламентирующих основные аспекты разработки программ и их защиты;
  • проведение тестирования программ с элементами моделирования действий хакеров. При этом изучение программы должно включать анализ исходных текстов и тщательное тестирование самого продукта.

Представленные в статье примеры вы найдете на нашем CD-ROM.

Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих