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

Помощь дата->рейтинг Поиск Почта RSS канал Статистика nickolay.info Домой

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

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

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

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

Оглавление

1. Ошибки в расчётах и применении стандартных средств языка
Несоблюдение отступов в тексте программы
Неправильная запись чисел
= вместо == (присваивание вместо сравнения)
Использование неинициализированных переменных
Градусы вместо радианов у тригонометрических функций
Забыли, что в строке всегда должен быть нуль-терминатор
Использование strncpy без добавления нуль-терминатора
Сцепление Си-строк без выделения памяти
Неверный #define с параметрами

2. Ошибки с типизацией данных
"Неожиданный" результат деления целых чисел
Неверное использование char как int
Перепутаны символ char и строка char *
Сравнение знаковых и беззнаковых значений
Сравнение вещественных значений как целых
Функции atoi/atof или sscanf для перевода строки в число
Неправильный кастинг (приведение типов)

3. Ошибки в условных операторах
"Опасная" проверка булевой переменной в условии
& и | вместо && и || (побитовые операции вместо логических)
Принадлежит ли значение интервалу?
Неверный составной условный оператор
Переключатель switch без операторов break
Объявление и инициализация переменной в ветви case
Неверное использование тернарного оператора

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

5. Ошибки работы с вводом/выводом
Чтение данных "в никуда"
Пути к файлам без двойного бэкслеша
"Удвоение" последней строки файла
"Лишние" пустые строки при построчном выводе данных
Не срабатывает останов в программе или "не работает" оператор ввода
Окно программы закрывается, я не успеваю увидеть результаты работы программы?
Посимвольное чтение файла обрывается при достижении буквы 'я'
Из текстового файла по формату читается только первое число и всё зацикливается!

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

7. Ошибки работы с динамической памятью и указателями
Использование указателя как массива
Неверное выделение/освобождение динамической памяти
Применение delete к объекту из стека
Попытка модифицировать константный указатель
Ссылка или указатель на "переехавший" объект

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

Попытка модифицировать константный указатель
#include <iostream>

void f (char *s) {
 s[0]='P'; //Так делать нельзя, s в новых компиляторах - константный указатель
}

int main () {
 char *s = "Hello"; //Теперь это в C++ означает константный указатель!
 f(s);
 return 0;
}
//(Visual C++)

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

0xC0000005: Нарушение прав доступа при записи "0x00415830".

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

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

#include <iostream>
#include <string.h>
using namespace std;

void f (char *s) {
 s[0]='P';
}

int main () {
 char *s = new char [6];
 strcpy (s,"Hello");
 f(s);
 cout << s << endl;
 system ("pause"); return 0;
}
//(Visual C++)

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

#include <iostream>
using namespace std;

void f (char *s) {
 s[0]='P';
}

int main () {
 char s[] = "Hello"; 
 f(s);
 cout << s << endl;
 system ("pause"); return 0;
}
//(Visual C++)

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

См. также эту заметку.

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

char *s="Hello";
cin.getline(s,255);

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

char s[255]="Hello";
cin.getline(s,255);

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

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

char *s=new char[255]; 
cin.getline(s,255);
//...
delete[] s;
Двойная перестановка элементов массива

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

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

#include <iostream>
#include <string.h>
using namespace std;
int main () {
 char *string = new char [6];
 strcpy(string,"Hello");
 int L = strlen (string);
 for (int i=0; i<L/2; i++) {
  char temp = string[i];
  string[i] = string [L-i-1];
  string [L-i-1] = temp;
 }
 cout << string << endl;
 system("pause"); return 0;
}
//(Visual C++)

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

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

#include <iostream>
#include <string.h>
using namespace std;

void print_matrix (int **a,int n) {
 for (int i=0; i<n; i++) {
  for (int j=0; j<n; j++) cout << a[i][j] << " ";
  cout << endl;
 }
}

int main () {
 const int n=3;
 int i,j;
 int **a = new int * [n];
 for (i=0; i<3; i++) {
   a[i] = new int [n];
  for (j=0; j<3; j++) a[i][j] = i*n+j;
 }
 cout << "Source matrix" << endl;
 print_matrix (a,n);

 for (i=0; i<n; i++)
 for (j=i+1; j<n; j++) {
  int temp = a[i][j];
  a[i][j] = a[j][i];
  a[j][i] = temp;
 }

 cout << "Transposed matrix" << endl;
 print_matrix (a,n);
 system("pause"); return 0;
}
//(Visual C++)

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

for (i=0; i<n; i++)
 for (j=0; j<n; j++)

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

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

#include <iostream>
#include <string.h>
using namespace std;

int main () {
 const int n=5;
 int b[n] = { 1, 2, 3, 4, 5 };
 for (int i=0; i<n-1; i++)
 for (int j=i+1; j<n; j++) {
  cout << i << " " << j << endl;
 }
 system("pause"); return 0;
}
//(Visual C++)

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

for (int i=0; i<n; i++)
 for (int j=0; j<n; j++) {

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

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

#include <iostream>
using namespace std;

int max (int n, int *a) {
  int m=a[0]; 
  for (int i=1; i<n; i++) if (a[i]>m) m=a[i];
  return m;
}

int min (int n, int *a) {
  int m=a[0]; 
  for (int i=1; i<n; i++) if (a[i]<m) m=a[i];
  return m;
}

int main() {
 const int n=5;
 int a[n] = {1,2,3,2,1};
 int i, mini = min(5,a), maxi = max(5,a);
 for (i=0; i<n; i++) {
   if (a[i]==maxi) a[i]=mini; //1
   if (a[i]==mini) a[i]=maxi; //2
 }

 for (i=0; i<n; i++) cout << a[i] << " ";
 system("pause"); return 0;
} //Visual С++

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

for (i=0; i<n; i++) {
 if (a[i]==maxi) a[i]=mini; //1
 else if (a[i]==mini) a[i]=maxi; //2
}

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

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

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

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

#include <iostream>
int main () {
 system("pause");
 return 0;
}
//(Visual C++)


#include <iostream>
using namespace std;
int main () {
 cout << "Press Enter to exit";
 cin.sync();
 cin.get();
 return 0;
}
//(Visual C++)


#include <stdio.h>
int main () {
 printf ("\nPress Enter to exit");
 fflush (stdin);
 getchar();
 return 0;
}
//Любой старый Си-совместимый компилятор
Неправильный кастинг (приведение типов)

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

double d=1.5;
int n=d;

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

int n=static_cast<int>(d);

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

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

struct type1 { int t1; };
struct type2 { int t2; };
type1  var1= { 1 };
type1 var11 = var1; //Это верно
type2 var2 = var1; //Ошибка!

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

reinterpret_cast <новый_тип> (выражение)

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

struct type1 { int t1; };
struct type2 { int t2; };
type1  var1= { 1 };
type2 *var2 = reinterpret_cast <type2 *> (&var1);
var2->t2 = 2; 
cout << var1.t1 << endl << var2->t2; //2 2

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

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

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

int i=1;
if (i=2) cout << "branch 1";
else cout << "branch 2";

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

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

& и | вместо && и || (побитовые операции вместо логических)
char n1 = 1, n2 = 2;
cout << ( n1 & n2 ? "true" : "false" ); //false
cout << ( n1 && n2 ? "true" : "false" ); //true

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

00000001
*
00000010
=
00000000

т.е., ложь.

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

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

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

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

#include <iostream>
#include <stdio.h>

int main () {
 char c='1';
 int n = c + 10;
 printf ("%c (%d)",(char)n,n); // ; (59)
 system ("pause"); return 0;
}
//(Visual C++)

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

int n = c - '0' + 10;
printf ("%d",n); //11

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

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

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

#include <iostream>
using namespace std;

int main () {
 int *a = new int;
 int *b = new int;
 int *c = new int;
 *a = *b = 1; *c = *a + *b;
 cout << *c << endl; //2
 delete c;
 delete b;
 delete a;
 system ("pause"); return 0;
}
//(Visual C++)

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

int *a = new int [10];
int *b = new int;
*b = 1; 
for (int i=0; i<10; i++) a[i]=i+1;
//... 
delete b;
delete[] a;

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

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

Animal *pDog = new Animal;
delete pDog;
pDog = 0;

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

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

#include <iostream>
using namespace std;

void f(int * Arr) {
 Arr = new int[10];
 for (int i = 0; i < 10; i++) Arr[i] = i;
}

int main() {
 int * Ptr=0;
 f(Ptr);
 for (int i = 0; i < 10; i++) cout << Ptr[i] << "  ";
 cin.get(); return 0;
}

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

#include <iostream>
using namespace std;

void f(int ** Arr) {
 *Arr = new int[10];
 for (int i = 0; i < 10; i++) (*Arr)[i] = i;
}

int main() {
 int * Ptr=0;
 f(&Ptr);
 for (int i = 0; i < 10; i++) cout << Ptr[i] << "  ";
 cin.get(); return 0;
}

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

#include <iostream>
using namespace std;

void f(int * & Arr) {
 Arr = new int[10];
 for (int i = 0; i < 10; i++) Arr[i] = i;
}

int main() {
 int * Ptr=0;
 f(Ptr);
 for (int i = 0; i < 10; i++) cout << Ptr[i] << "  ";
 cin.get(); return 0;
}

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

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

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

#include <iostream>
using namespace std;

int &bad1() {
 int x=1; return x;
}
 
int *bad2() { 
 int x=2; return &x; 
}

int main () {
 int x1=bad1();
 int *x2=bad2();
 cout << x1 << " " << *x2 << endl; //?
 system ("pause"); return 0;
}
//(Visual C++)

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

#include <iostream>
using namespace std;

int good1() {
 int x=1; return x;
}
 
int *good2() { 
 int *x=new int; *x = 2; return x; 
}

int main () {
 int x1=good1();
 int *x2=good2();
 cout << x1 << " " << *x2 << endl;
 system ("pause"); return 0;
}
//(Visual C++)

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

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

#include <windows.h>
#include <stdio.h>
#include <string.h>

char *f() {
 char str[100];
 strcpy (str, "hello, world!");
 return str;
}

int main () {
 char *p = f(); //Так нельзя!
 printf ("%s\n", p); 
 system("pause");
 return 0;
}

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

#include <windows.h>
#include <stdio.h>
#include <string.h>

char *f(char *str, int len) {
 strncpy (str, "hello, world!", len);
 str[len]='\0';
 return str;
}

int main () {
 char str[100];
 char *p = f(str,sizeof(str)); //Так можно!
 printf ("%s\n", p); 
 system("pause");
 return 0;
}

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

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

int x;
cout << x;

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

const int n = 3;
int a[n] = {1, 2, 3};
int sum; //правильно int sum=0;
for (int i=0; i < n; i++) sum += a[i]; 
cout << sum << endl; //результат м.б. любым!

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

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

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

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

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

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

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

#include <iostream>
using namespace std;

int main () {
 const int n = 3;
 int a[n] = {1,2,3};
 int i;
 int sum=0;
 for (i=0; i<n; i++); //лишняя точка с запятой!
   sum += a[i];
 cout << sum << endl; //опять что попало!
 system ("pause"); return 0;
}
//(Visual C++)

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

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

#include <iostream>
using namespace std;

int f (int x); //Здесь не нужна точка с запятой,
{              //это определение, а не объявление функции!
  return x*x;
}

int main () {
 cout << f(2) << endl; 
 system ("pause"); return 0;
}
//(Visual C++)

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

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

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

void main() {
 //...
}

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

int main() {
 //...
 return 0;
}

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

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

#include <iostream>
using namespace std;
struct point { int x,y; } //забыта ;
int main () {
 point a = {1,2};
 system ("pause"); return 0;
} //(Visual C++)

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

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

#include <iostream>
using namespace std;
struct point { int x,y; } a = {1,2};
int main () {
 cout << a.x << " " << a.y << endl; //1 2
 system ("pause"); return 0;
} //(Visual C++)
Сравнение вещественных значений как целых

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

if (a==b)

или же

if (a-b==0)

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

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

if (fabs(a-b)<1e-6)

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

float x;
for (x=0; x<=1; x+=0.1) ...

переменная x рискует "потерять" своё последнее значение, равное 1, всё из-за тех же погрешностей. Правильный путь - прибавлять к правой границе диапазона изменения x некое малое значение, заведомо меньшее шага: for (x=0; x<=1+1e-6; x+=0.1) ...

Сравнение символьных массивов как переменных

Сравнение Си-строк (массивов типа char) с помощью операций отношения <, ==, >= и т.д. - глубоко неправильное для C++ действие. Так мы будем сравнивать не содержимое, а указатели (адреса памяти, где начинаются строки). Правильный путь - стандартная функция сравнения строк strcmp (подключить заголовки string.h).

#include <iostream>
#include <string.h>
using namespace std;
int main () {
 char c2[] = "aaa", c1[] = "bbb";
 if (c1 < c2) // ошибка!
    cout << "Error, c1<c2" << endl;
 int cmp = strcmp(c1, c2);
 if (cmp < 0) 
    cout << "OK, c1<c2" << endl;
 else if (cmp > 0) 
    cout << "OK, c1>c2" << endl;
 else cout << "c1==c2" << endl;
 system ("pause"); return 0;
} //(Visual C++)
Неправильная запись чисел

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

int x = 123; cout << x << endl;  //123
int y = 0123; cout << y << endl; //83

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

int x = 123; 
char buf[6];
sprintf (buf,"%05d", x);
cout << buf << endl;  //00123

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

double x;
x = 3,2; // x будет равен не 3.2, а 3

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

x = 3.2;

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

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

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

if (x>=a && x<=b)

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

if (a <= x <= b)

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

if ((a <= x) <= b)

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

if (x>=a || x<=b)

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

if(x>a , x<=b)

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

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

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

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

z = -1, если a<0
z = 0, если a==0
z = 1, если a>0

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

if (a<0) z=-1;
if (a==0) z=0;
else z=1;

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

if (a<0) z=-1;
if (a==0) z=0;
if (a>0) z=1;

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

if (a<0) z=-1;
else if (a==0) z=0;
else z=1;

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

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

int x=12;
if (x%2==0) cout << "Чётное";
else if (x%2==1) cout << "Нечётное";
else if (x==12) cout << "Дюжина";

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

if (x==12) cout << "Дюжина";
else if (x%2==0) cout << "Чётное";
else cout << "Нечётное";

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

if (условие1) ветвь1;
else if (условие2) ветвь2;
...
else if (условиеN) ветвьN;
else ветвь0;

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

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

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

int f (int x) {
 if (x>0) return 1;
}

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

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

int f (int x) {
 if (x>0) return 1;
 return 0;
}

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

warning C4715: f: значение возвращается не при всех путях выполнения

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

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

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

char x='0',y='0';
printf ("\ninput char x"); scanf ("%c",&x);
printf ("\ninput char y"); scanf ("%c",&y);
printf ("\nx=%c,y=%c",x,y);

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

char x='0',y='0';
printf ("\ninput char x"); scanf ("%c",&x);
printf ("\ninput char y"); fflush(stdin); scanf ("%c",&y);
printf ("\nx=%c,y=%c",x,y);

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

#include <conio.h>
#include <stdio.h>
int main () {
 printf ("ok\n");
 char c=getch();
 return 0;
}

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

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

#include <iostream>
using namespace std;
int main () {
 cout << "Input n:";
 int n;
 cin >> n; // вводится число после нажатия Enter
 cout << "Input c:";
 char c;
 cin >> c; // хотим ввести символ на новой строки, но вместо него вводится '\n'
 system("pause"); return 0;
}

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

#include <iostream>
using namespace std;
int main () {
 cout << "Input n:";
 int n;
 (cin>>n).get(); // вводим число и пропускаем один символ
 cout << "Input c:";
 char c;
 cin >> c; // хотим ввести символ на новой строке, но вместо него вводится '\n'
 system("pause"); return 0;
}

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

cout << "Input n:";
int n;
cin >> n; 
cin.sync(); // вводим число и сбрасываем остаток строки
cout << "Input c:";
char c;
cin >> c; 
cin.sync(); // вводим символ на новой строке и тоже сбрасываем остаток строки.

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

cin.ignore(k,'\n'); // k -  пропускаемое к-во символов,  параметр '\n' можно опускать 
cin.ignore(cin.rdbuf()->in_avail()); // пропустить все оставшиеся символы

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

while (cin.get() != '\n');

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

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

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

#include <iostream>
using namespace std;
int main () {
 int a[5] = {1,2,3,4,5};
 for( int i=1; i<=5; i++ ) cout << a[i] << " ";
  //Выведет 2, 3, 4, 5, а потом - что угодно
 system("pause"); return 0;
} //Visual С++

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

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

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

switch( i ) {
 case 0: cout << "zero ";
 case 1: cout << "one ";
 default: cout << "other"; 
}

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

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

#include <iostream>
using namespace std;
int f(int n) {
 int r=1;
 switch (n) { //break в этом операторе не нужны!
  case 3: r*=2;
  case 2: r*=2;
  case 1: r*=2;
 }
 return r;
}
int main () {
 cout << f(1) << endl; //2
 cout << f(2) << endl; //4
 cout << f(3) << endl; //8
 cout << f(5) << endl; //1
 system("pause"); return 0;
} //Visual С++

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

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

Очень часто к неожиданным результатам расчётов приводят вот такие ляпы в коде:

x = sin(90);
Получится значение x = 0.893997, а отнюдь не 1. Просто человек имел в виду 90 градусов, а компьютер принимает аргументы тригонометрических функций в радианах. Если входная величина задана в градусах, переведите её в радианы:
x = sin(90 * M_PI / 180);

где M_PI - константа со значением числа пи.

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

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

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

#include <iostream>
using namespace std;

class A {
 public:
  int x;
  A () { x = 1; }
  A (int x) { this->x = x; }
};
 
class B : public A {
 public:
  B () {}
  B (int x) {
    A::A(x); //один неверный способ
    A(); //ещё один
    A::A(); //и ещё один!
  }
};

int main () {
 B b(2); //хотим создать объект со значением x=2
 cout << b.x << endl; //всё равно печатает 1!
 system("pause"); return 0;
} //Visual С++

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

#include <iostream>
using namespace std;

class A {
 public:
  int x;
  A () { x = 1; }
  A (int x) { this->x = x; }
};
 
class B : public A {
 public:
  B () {}
  B (int x): A (x) {}
};

int main () {
 B b(2);
 cout << b.x << endl; //вот теперь 2
 system("pause"); return 0;
} //Visual С++
Сравнение знаковых и беззнаковых значений

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

#include <iostream>
using namespace std;

int main () {
 int x = -1;
 unsigned int y = 1;
 if (x<y) cout << "x<y" << endl; 
 else if (x>y) cout << "x>y" << endl; 
  //выведет x>y !
 system("pause"); return 0;
} //Visual С++

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

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

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

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

#include <iostream>
using namespace std;

class A {
 int x;
 virtual int f() { return 0; } 
public:
  A() { x = f(); } //Вот оно!
  int getX() { return x; }
};
class B : public A {
 virtual int f() { return 1; } 
};
 
int main() {
 B b; 
 cout << b.getX() << endl; // Выводит 0, а не 1
 system("pause"); return 0;
} //Visual С++

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

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

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

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

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

const int n=5;
int a[n];
for (int i=0; i<n; i++)
 a[i] = i+1; //так делать можно
const int n=5;
int a[n];
for (int i=0; i<n; i++) {
 a[i] = i+1; //и так тоже
}

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

#include <iostream>
using namespace std;
int main() {
 const int n=5;
 int a[n],i;
 for (i=0; i<n; i++)
  a[i] = i+1;
  cout << a[i] << " "; //будет напечатано одно случайное число, взятое за границей массива
 system("pause"); return 0;
} //Visual С++

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

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

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

for (i=0; i<n; i++) {
 a[i] = i+1;
 cout << a[i] << " "; //теперь всё хорошо
}

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

if (x==0)
 if (y==0) cout << "X==Y==0";
else cout << "X!=0"; 

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

if (x==0) {
 if (y==0) cout << "X==Y==0";
}
else {
 cout << "X!=0"; 
}
Без указания дополнительных операторных скобок else всегда относится к ближайшему сверху if, для которого ветка else ещё не указана

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

int x=1;
while (x<10)
{
  y=x*x;
  x++;
  //такой стиль используют
}

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

int x=1;
while (x<10) {
  y=x*x;
  x++;
  //и такой тоже
}

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

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

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

#include <iostream>
using namespace std;
int f (int *a) {
 int n = sizeof(a)/sizeof(a[0]);
 return n;
}
int main() {
 int a[] = {1,2,3};
 cout << f(a) << endl; //Будет выведено 1
 system("pause"); return 0;
} //Visual С++

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

#include <iostream>
using namespace std;
int f (int n,int *a) {
 return n;
}
int main() {
 int a[] = {1,2,3};
 cout << f(3,a) << endl;
 int *b = new int [5];
 cout << f(5,b) << endl;
 system("pause"); return 0;
} //Visual С++

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

#include <iostream>
using namespace std;
template <typename T> int f (T &a) {
 return sizeof(a)/sizeof(a[0]);
}
int main() {
 const int n=3;
 int a[n]={1,2,3};
 cout << f(a) << endl; //Выведет 3
 int *b = new int [5];
 cout << f(b) << endl; //Выведет 1 - для динамического массива способ неверен!
 system("pause"); return 0;
} //Visual С++

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

#include <iostream>
using namespace std;
template <typename T, size_t size> size_t f(T(&)[size]) { return size; }

int main() {
 int a[]={1,2,3};
 cout << f(a) << endl; //Выведет 3
 system("pause"); return 0;
} //Visual С++

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

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

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

#include <iostream>
using namespace std;
class A {
 public:
  int x,y;
  A (int y_) : y(y_) , x(y*2) {}
};

int main() {
 A A(1);
 cout << A.x << " " << A.y << endl; //Выведет дикий бред и 1
 system("pause"); return 0;
} //Visual С++

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

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

#include <iostream>
using namespace std;
class A {
 public:
  int y,x;
  A (int y_) : y(y_) , x(y*2) {}
};

int main() {
 A A(1);
 cout << A.x << " " << A.y << endl; //Выведет 2 и 1
 system("pause"); return 0;
} //Visual С++
Функция работает с локальной копией объекта вместо объекта

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

#include <iostream>
using namespace std;

void init_array (int n,int *a){
 a=new int[n];
 for (int i=0; i<n; i++) a[i]=i+1;
}
void delete_array(int *a){
 delete [] a;
 a=NULL;
}

int main() {
 const int n=3;
 int *a;
 init_array (n,a); //выдаст runtime-ошибку или результат будет непредсказуем
 for (int i=0; i<n; i++) cout << a[i] << " ";
 delete_array (a);
 system("pause"); return 0;
} //Visual С++

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

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

#include <iostream>
using namespace std;

void init_array (int n,int **a){
 *a=new int[n];
 for (int i=0; i<n; i++) (*a)[i]=i+1;
}
void delete_array(int **a){
 delete [] *a;
 *a=NULL;
}

int main() {
 const int n=3;
 int *a;
 init_array (n,&a); //всё работает
 for (int i=0; i<n; i++) cout << a[i] << " ";
 delete_array (&a);
 system("pause"); return 0;
} //Visual С++

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

#include <iostream>
using namespace std;

void init_array (int n,int *&a){
 a=new int[n];
 for (int i=0; i<n; i++) a[i]=i+1;
}
void delete_array(int *&a){
 delete [] a;
 a=NULL;
}

int main() {
 const int n=3;
 int *a;
 init_array (n,a); //и так работает
 for (int i=0; i<n; i++) cout << a[i] << " ";
 delete_array (a);
 system("pause"); return 0;
} //Visual С++

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

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

#include <iostream>
using namespace std;

int *init_array (int n) {
 //просто вернём указатель на выделенную в "куче" память
 int *a=new int[n];
 for (int i=0; i<n; i++) a[i]=i+1;
 return a;
}

void delete_array(int *&a) {
 //а вот здесь ссылка в заголовке нужна
 delete [] a;
 a=NULL;
}

int main() {
 const int n=3;
 int *a = init_array (n); //теперь метод вызовется так
 for (int i=0; i<n; i++) cout << a[i] << " ";
 delete_array (a);
 system("pause"); return 0;
} //Visual С++

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

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

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

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

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

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

#include <iostream>
using namespace std;

class A {
 private:
  int *x; // указатель на данные
  int n; //размерность
 public:
  A () : x(NULL), n(0) {} //конструктор без параметров
  A (int n_) : n(n_) { //конструктор с указанной размерностью
   x=new int[n];  //выделяем память - понадобится деструктор для освобождения
   for (int i=0; i<n; i++) x[i]=i+1;
  }
  ~A () { //деструктор
   delete[] x; x = NULL;
  }
  A (const A &a):n(a.n) {  //конструктор копирования, раз есть деструктор
   x = new int[a.n];
   n = a.n;
   for (int i=0;i<a.n;i++) x[i]=a.x[i];
  }
  A& operator = (const A& a) { //оператор присваивания, раз есть деструктор
   if (this == &a) return *this;  //не надо присваивать самому себе
   if (x!=NULL) delete[] x;
   n = a.n;
   x = new int [a.n];
   for (int i=0; i<a.n; i++) x[i]=a.x[i];
   return *this;
  }
  A operator+(A& a) { //объединение массивов переопределённой операцией "+"
   A c;
   c.n = this->n + a.n;
   c.x = new int [c.n];
   int i=0;
   for(; i<this->n; i++) c.x[i]=this->x[i];
   int j=0;
   for(; i<c.n; i++,j++) c.x[i]=a.x[j];
   return c; 
   // для передачи объекта по значению будет использован конструктор копирования!
  }
  void show (char *msg) { //просто вывод массива
   cout << msg << ": ";
   for (int i=0; i<n; i++) cout << x[i] << " ";
   cout << endl;
  }
};

int main() {
 A a1(2),a2(3);
 a1.show ("a1");
 a2.show ("a2");
 A a3=a1+a2;
 a3.show ("a3");

 A a4(4),a5(5),a6(6);
 a4=a5=a6;
 a4.show("a4");
 a5.show("a5");
 a6.show("a6");

 system("pause"); return 0;
} //Visual С++

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

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

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

int x;
printf ("\nInput x ");
fflush (stdin);
scanf ("%d",x);
printf ("\nx=%d",x);

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

scanf ("%d",&x);

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

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

int *a;
for (int i=0; i<10; i++) a[i]=i+1; //полный крах, пишем данные в никуда!

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

int *a = new int [10];
for (int i=0; i<10; i++) a[i]=i+1;

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

int *a = new int [100000];
if (a==NULL) {
 //Здесь обработка ошибки и выход из программы
}

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

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

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

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

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

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

#include <iostream>
using namespace std;
int main () {
 char c = '*';
 char str[256];
 strcpy (str,"Hello");
 strcat (str, (char *)c); 
 cout << str << endl;
 system ("pause");
 return 0;
} //Visual Studio

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

int len = strlen (str);
str[len] = c;
str[len+1] = '\0';

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

char c[2] = { '*','\0' };
char str[256];
strcpy (str,"Hello");
strcat (str, c);

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

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

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

char s[5];
strcpy (s,"Hello");
в Visual Studio вполне способен привести к краху программы.

Правильно

char s[6];
strcpy (s,"Hello");

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

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

#include <iostream>
using namespace std;

class A {
 public:
  A() { cout << "A()"; }
  ~A() { cout << "~A()"; }
};
 
class B : public A {
 public:
  B() { cout << "B()"; }
  ~B() { cout << "~B()"; }
};

int main () { 
 A *a = new B;
 delete a;
 system ("pause");
 return 0;
}

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

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

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

virtual ~A() { cout << "~A()"; }

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

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

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

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

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

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

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

#include <iostream>
using namespace std;

bool init (int &x, int &y) { x=y=42; return true; }

int global=0;
bool init2 () { global+=10; return true; }

int main () { 
 int x=0,y=0;
 cout << init(x,y) << " " << x << " " << y << endl; //1 0 0
 cout << x << " " << y << endl; //42 42
 cout << init2() << " " << global << endl; //1 0
 cout << global << endl; //10
 system ("pause");
 return 0;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Пример:

#include <iostream>
#include <vector>
using namespace std;

class papa {
 public:
  virtual ~papa() {}
  virtual void f()=0;
};
    
 
class child : public papa {
 public:
  int a;
  child () { a=0; }
  virtual void f(){ 
   a++; cout << "a = "<< a << endl;
  }
};

int main() {
 vector <child> vec;
 const child vec0;
 vec.emplace_back(vec0);
 child &s = vec.back(); //2
 s.f();
 for (int i=0; i<10; i++) vec.push_back( child() ); //1
 s.f(); //вот тут сдохло
 system("pause"); return 0;
} //Visual С++

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

#include <iostream>
#include <malloc.h>
using namespace std;

int main() {
 int *a = (int *)malloc (100*sizeof(int));
 for (int i=0; i<100; i++) a[i]=i+1;
 int *b = &a[49];
 cout << *b << endl; //вывелось 50
 realloc(a,200*sizeof(int));
 cout << *b << endl; //вывелась полная бяка
 system("pause"); return 0;
} //Visual С++

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

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

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

#include <iostream>
using namespace std;

class A {
 public:
  int val;
  A () { val=1; }
};

int main() {
 A a();                          //Ошибка!
 cout << a.val << endl; //Теперь a.val не скомпилируется!
 system("pause"); return 0;
} //Visual С++

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

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

A a;

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

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

#include <iostream>
using namespace std;

class A {
 public:
  ~A() { delete [] data; } //деструктор
   A() : size(0),data(NULL) {} //конструктор по умолчанию
   A (const int size) : size(size),data(new int[size]) {}
    //конструктор с параметром - выделяет память
   A (const A & a) : size(a.size),data(new int[size]) {}
    //конструктор копирования - копирует только размерность
 int size;
 int *data;
};
  
class B : public A {
  int bdata;
 public:
  B () :  bdata(0) {} //конструктор 1 (по умолчанию)
  B (const int size, const int val = 0) : A(size) , bdata(val) {}
   //конструктор 2 (с параметрами)
  B (const B &a) : bdata(a.bdata) {} ////конструктор 3 (копирования)
  void view() const {
   cout << "data=" << bdata << " size=" << size << endl;
  }
};

int main() {
 B b1 (10,10); 
 b1.view();     //data=10 size=10
 B b2 = b1;    
 b2.view();     //data=10 size=0
 system("pause"); return 0;
} //Visual С++

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

B b2 = b1;    

например, на

B b2(10);
b2 = b1;    

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

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

#include <iostream>
using namespace std;

int main(void) {
 for (int i=0; i<10; i++) {
  cout << i << " "; //0 2 4 6 8
  i++; //вот оно!
 }
 cin.sync(); cin.get();
 return 0;
}

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

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

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

#include <iostream>
#include <time.h>
#include <math.h>
#define M_PI 3.1415926535897932384626433832795
using namespace std;

int main(void) {
 clock_t start = clock();
 //
 const int n = 100;
 for (double x=0.; x<=M_PI*n; x+=M_PI/n/1000) {
  double y=sin(x);
 } //0.875ms
 //
 float duration = (((float)clock() - (float)start) / 1000000.0F ) * 1000;  
 cout << duration << "ms" << endl; 
 cin.sync(); cin.get();
 return 0;
} //Visual C++

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

//
const int n = 100;
double x2=M_PI*n;
double xstep=M_PI/n/1000;
for (double x=0.; x<=x2; x+=xstep) {
 double y=sin(x);
} //0.797ms
//

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

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

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

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

#include <iostream>
#include <vector>
#include <time.h>
using namespace std;

int main(void) {
 int size=1000;
 vector <int> vec;
 for (int i=0; i<size; i++) vec.push_back(i+1);
 clock_t start = clock();
 //
 for (vector<int>::iterator i = vec.begin(); i<vec.end(); i++) { 
  cout << *i << " ";
 } 
 //
 float duration = (((float)clock() - (float)start) / 1000000.0F ) * 1000;  
 cout << duration << "ms" << endl; 
 cin.sync(); cin.get();
 return 0;
} //Visual C++

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

const auto e = vec.end();
for (vector<int>::iterator i = vec.begin(); i<e; i++) { 
 cout << *i << " ";
}

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

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

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

#include <iostream>
#include <list>
using namespace std;

int main(void) {
 list <int> mylist;
 for(size_t n=0; n<15; n++) mylist.push_back(n+1);

 cout << "List = { ";
 for (list<int>::iterator i=mylist.begin(); i!=mylist.end(); i++) cout << *i << " ";
 cout << "}" << endl;
    
 //Пытаемся удалить в контейнере все элементы с ключом, кратным 3
 for (list<int>::iterator i=mylist.begin(); i!=mylist.end(); i++) {
  if(*i%3==0) mylist.erase(i); //ошибка!
 }
 //
 cout << "New list = { ";
 for (list<int>::iterator i=mylist.begin(); i!=mylist.end(); i++) cout << *i << " ";
 cout << "}" << endl;    
 
 cin.sync(); cin.get();
 return 0;
} //Visual C++

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

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

if (*i%3==0) i=mylist.erase(i); //крах при размерности, кратной 3

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

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

for (list<int>::iterator i=mylist.begin(); i!=mylist.end(); ) {
 if (*i%3==0) i=mylist.erase(i);
 else i++;
}

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

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

for (list<int>::iterator i=mylist.begin(); i!=mylist.end(); ) {
 if (*i%3==0) mylist.erase(i++);
 else ++i;
}

но не

for (list<int>::iterator i=mylist.begin(); i!=mylist.end(); ) {
 if (*i%3==0) mylist.erase(++i);
 else ++i;
}

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

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

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

#include <iostream>
#include <stdio.h>
#define SQUARE(val) val * val
using namespace std;

int main () {
 int x = 2;
 cout << SQUARE(x+1) << endl; //5
 cout << (x+1)*(x+1) << endl; //9
 system ("pause"); return 0;
}
//(Visual C++)

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

Правильно так:

#define SQUARE(val) (val)*(val)

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

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

#define SQUARE(val) ((val)*(val)) //так тоже не надо

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

#define DOUBLE(val) (val) + (val)
//...
cout << DOUBLE(x) * DOUBLE(x) << endl; //8
cout << (x+x)*(x+x) << endl; //16

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

#define DOUBLE(val) ((val) + (val))

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

#define SQUARE (val) val * val /* Это неверно! */
//...
cout << SQUARE (x) << endl; //Ошибка "Необъявленный идентификатор val"

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

#define SQUARE(val) ((val)*(val))

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

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

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

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

#include <iostream>
using namespace std;

int main () {
 char x;
 cout << "X=";
 cin >> x;
 int y3;
 switch (x) {
  case '1':
   int y=1; //ошибка
  break;
  case '2':
   int y2; //нет ошибки
  break;
  case '3':
   y3=3; //нет ошибки
  break;
  default:
   int z=2; //не найдёт ошибки, но лучше так не делать
  break;
 }
 system("pause");
 return 0;
} //Visual C++
Несоблюдение отступов в тексте программы

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

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

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

оператор
{
    вложенный оператор 
    {
        вложенный оператор
        {
            //...
        }
    }
}

или

оператор {
    вложенный оператор {
        вложенный оператор {
            // ...
        }
    }
}

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

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

оператор {
 вложенный оператор {
  вложенный оператор {
   // ...
  }
 }
}

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

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

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

if (условие) {
}
else {
}

или

while (условие) {
}

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

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

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

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

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

int n=2;
double x,y=2.,z;
x = 1/n; // x будет равен нулю
y = pow(y, 1/2 ); // возведение в нулевую степень
z = y + n/(n+1); // к y прибавляется ноль
cout << x << " " << y << " " << z << endl; //0 1 1

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

int n=2;
double x,y=2.,z;
x = 1./n;
y = pow(y, 1./2 );
z = y + n/(double)(n+1);
cout << x << " " << y << " " << z << endl; //верные ответы с дробной частью

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

Посимвольное чтение файла обрывается при достижении буквы 'я'

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

#include <locale.h>
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
using namespace std;

int main () {
 setlocale(LC_ALL,"Russian");
 FILE *fp = fopen ("data.txt","wt");
 if (fp==NULL) {
  printf ("Не могу создать файл, проверьте права\n");
  system("pause"); exit (1);
 }
 fprintf (fp,"Яша присоединил Аляску");
 fclose (fp);
 fp=fopen ("data.txt","rt");
 char c;
 while ((c = fgetc(fp)) != EOF) {
  printf("%c",c); //Оборвётся на 'я' маленькой!
 }
 printf("\n");
 system("pause");
 return 0;
}

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

int c;

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

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

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

#include <windows.h>
#include <stdio.h>
#include <string.h>

int main () {
 const int len=10;
 char str[len+1];
 for (int i=0; i<len; i++) str[i]='1';
 str[len]='\0';
 strncpy (str,"Hello",5);
 printf ("%s\n",str); //Hello11111
 system("pause");
 return 0;
}

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

str[5]='\0';

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

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

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

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

Функции atoi/atof или sscanf для перевода строки в число

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

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

#include <windows.h>
#include <stdio.h>
#include <string.h>

int main () {
 const char *s = "999999999999999999";
 int i = atoi (s);
 printf ("%d\n",i); //стандартного поведения нет
 sscanf (s,"%d",&i);
 printf ("%d\n",i); //то же самое; кстати, ответы разные
 system("pause");
 return 0;
}

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

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main () {
 const char *s = "999999999999999999";
 char *p;
 int i = strtol (s,&p,10);
 if (errno == ERANGE) printf ("Overflow!\n");
 else printf ("%d\n",i); 
 system("pause");
 return 0;
} //Visual C++

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

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

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

#include <windows.h>
 
class Class {
 int n;
public:
 Class (int n) { this->n = n; }
};
 
int main (void){
 Class *c = new Class(5);
 delete c; //работает
 Class d(10); //так тоже можно создать объект
 delete &d; //вот здесь программа "упадёт"
 system("pause");
 return 0;
} //Visual C++

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

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

Class d(10);

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

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

int sa[4];
Class sc(20);
 
// выделили через new
int *nm=new int[4];
Class *nc=new Class(30);
// поработали...
// освободили
delete nc;
delete[] nm;
 
// выделили через malloc
int *mm=(int*) malloc(4*sizeof(int));
Class *mc=(Class*)malloc(sizeof(Class));
// поработали...
// освободили
free(mc);
free(mm);

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

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

char *name;

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

name = new char [30];

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

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

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

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

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

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

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

int main (void){
 FILE *fp = fopen ("C:\temp\data.txt", "r");
 if (fp == NULL) {
  printf ("Can't open file!\n");
  fflush (stdin);
  getchar();
  exit(1);
 }
 const int MAXLEN = 128;
 char buf[MAXLEN+1];
 fgets (buf, MAXLEN, fp);
 puts (buf);
 fclose (fp);
 system("pause");
 return 0;
} //Visual Studio

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

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

FILE *fp = fopen ("C:\\temp\\data.txt", "r");

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

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

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

int main (void){
 FILE *fp = fopen ("C:\\temp\\data.txt", "r");
 if (fp == NULL) {
  printf ("Can't open file!\n");
  fflush (stdin);
  getchar();
  exit(1);
 }
 const int MAXLEN = 128;
 char buf[MAXLEN+1];
 while (!feof(fp)) { //последняя строка файла может быть "удвоена"
  fgets (buf, MAXLEN, fp);
  puts (buf);
 }
 fclose (fp);
 system("pause");
 return 0;
} //Visual Studio

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

string 1
string 2

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

string 1
string 2

- никакого "удвоения" не будет. Встречается вот такое решение -

while (1) {
 fgets (buf, MAXLEN, fp);
 if (feof(fp)) break;
 puts (buf);
}
- но оно грозит, наоборот, "потерять" последнюю строку файла, если после неё нет пустой. Хотя при форматном чтении данных, скажем, целых чисел из файла, такой подход бывает применим:
int a;
while (1) {
 fscanf (fp, "%d", &a);
 if (feof(fp)) break;
 printf ("%d ",a);
}

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

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

while (!feof(fp)) {
  fgets (buf, MAXLEN, fp);
  puts (buf);
  buf[0]='\0';
 }

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

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

string 1
string 2
Вывод:
string 1

string 2
Для продолжения нажмите любую клавишу . . .

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

const int MAXLEN = 128;
char buf[MAXLEN+1];
while (!feof(fp)) {
 fgets (buf, MAXLEN, fp);
 int len = strlen(buf);
 if (buf[len-1]=='\n') buf[len-1]='\0';
 puts (buf);
 buf[0]='\0';
}
Переопределённый оператор не возвращает экземпляр или ссылку на экземпляр класса

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

#include <iostream>
#include <windows.h>
using namespace std;

class A {
	int n; //приватное числовое поле
public:
	A (int n=0) { this->n = n; } //конструктор
	A (A &p) { this->n = p.n; } //конструктор копирования
	A & operator + (A &p2); //бинарное сложение
	A & operator += (A &p2); //унарный оператор +=
	A & A::operator + (); //унарный оператор + (знак числа)
	A & A::operator - (); //унарный оператор - (знак числа)
	void show();
};

void A::show() { cout << this->n << endl; }

A & A::operator + (A &p2) { //бинарное сложение - реализация
	A *p = new A();
	p->n = this->n + p2.n;
	return *p;
}

A & A::operator += (A &p2) { //операция += - реализация
	this->n += p2.n;
	return *this;
}

A & A::operator + () { //знак + перед объектом - реализация
	if (this->n < 0) this->n = - this->n;
	return *this;
}

A & A::operator - () { //знак - перед объектом - реализация
	if (this->n > 0) this->n = - this->n;
	return *this;
}

int main () { //Демка
	A *a=new A(5);
	A *b=new A(3);
	A c = *a + *b;
	c.show();
	c+=c;
	A d = *a + *b + c;
	d.show();
	system("pause");
	return 0;
} //Visual С++

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

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

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

int A::operator + (A &p2) {
	A *p = new A();
	p->n = this->n + p2.n;
	return p->n;
}

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

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

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

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

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
 setlocale(LC_ALL,"Rus"); 
 SetConsoleCP(1251); SetConsoleOutputCP(1251);
 
 FILE *fp = fopen ("text.txt","r");
 if (fp==NULL) {
  printf ("\nне удалось открыть файл");  getchar(); exit (1); 
 }
 float a;
 while (1) {
  fscanf (fp,"%f",&a);
  if (feof(fp)) break; //Если файл кончился, выйти из цикла 
  //здесь выполняется обработка очередного значения a, например:
  printf ("%.2f ",a);
 }
 fclose(fp);
 
 fflush(stdin); getchar();  return 0;
}

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

1 1.5 -3.5    
2 3.5

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

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

// setlocale(LC_ALL,"Rus");

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

1 1,5 -3,5    
2 3,5

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

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

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

char *s="Test string";
char *p = strcat(s," + new words");

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

char *s="Test string";
char s2[80];
strcpy (s2,s);
char *p=strcat (s2, " + new words");

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

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

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

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

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

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

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

int result = func();
cout << (result == -1 ? "Error!" : result); //Ошибка - нет общего типа!

//...
int result = func();
if (result == -1) cout << "Error!";
else cout << result; //А так работает

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

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

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

#include <iostream>
using namespace std;

int main() {
 const int n=5;
 int a[n] = {1,2,3,4,5};
 for (int i=0; i<n; i++) cout << a[i] << ((i+1)%2?" ":"\n");
 system("PAUSE");
 return 0;
}

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

for (int i=0; i<n; i++) cout << a[i] << (i+1)%2?" ":"\n";

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

for (int i=0; i<n; i++) (cout << a[i] << (i+1)%2) ?" ":"\n";

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

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

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

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

#include <iostream>
#include <cstring>
using namespace std;

class Class {
 char *s;
public: 
 Class(char *); //Конструктор
 ~Class(); //Деструктор
 void show(); //Метод для вывода строки
};

Class::Class(char *s) {
 int n = sizeof(char)*(strlen(s) + 1);
 this->s = new char [n];
 strcpy_s (this->s, n, s);
}

Class::~Class() {
 delete[] s;
}

void Class::show() {
 fputs("\n",stdout); fputs (this->s, stdout);
}

int main () {
 Class *c1 = new Class("Hello, world!");
 Class c2 = *c1;
 c1->show(); c2.show(); //кажется, что всё ОК
 delete c1;
 c2.show(); //в c2.s - мусор, возможен крах программы
 cin.get(); return 0;
}

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

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

#include <iostream>
#include <cstring>
using namespace std;

class Class {
 char *s;
public: 
 Class(char *); //Конструктор
 Class(Class &); //Конструктор копирования
 ~Class(); //Деструктор
 void show(); //Метод для вывода строки
};

Class::Class(char *s) {
 int n = sizeof(char)*(strlen(s) + 1);
 this->s = new char [n];
 strcpy_s (this->s, n, s);
}

Class::Class(Class &that) {
 int n = sizeof(char)*(strlen(that.s) + 1);
 this->s = new char[n];
 strcpy_s(this->s, n, that.s);
}

Class::~Class() {
 delete[] s;
}

void Class::show() {
 fputs("\n",stdout); fputs (this->s, stdout);
}

int main () {
 Class *c1 = new Class("Hello, world!");
 Class c2 = *c1;
 c1->show(); c2.show();
 delete c1;
 c2.show(); //всё работает
 cin.get(); return 0;
}
"Опасная" проверка булевой переменной в условии

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

bool tmp;
//...
if ( tmp == true ) {
 //...
}

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

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

if ( tmp ) {
 //...
}


теги: программирование список c++ ошибка

показать комментарии (2)

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

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

Наверх Яндекс.Метрика
© PerS
вход