Форум программистов, компьютерный форум CyberForum.ru

C для начинающих

Войти
Регистрация
Восстановить пароль
 
 
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
#1

Распространенные ошибки - C (СИ)

02.10.2014, 12:49. Просмотров 36484. Ответов 57
Метки нет (Все метки)

Оглавление

Ошибки этапа компиляции
(В процессе компиляции выдается либо сообщение об ошибке, либо предупреждение)
- Попытка модифицировать константу через указатель
- Лишняя точка с запятой
- Отсутствие возврата значения из функции
- Приведение типа в стиле С++
- Использование = вместо ==


Ошибки этапа исполнения программы
(Во время исполнения программа прерывается с сообщением об ошибке)
- Возврат из функции локальной строки
- Выделение памяти без дальнейшего освобождения
- Использование неинциализированной переменной
- Использование функции strncpy как функции "безопасного" копирования строк
- Использование функций atoi/atof или sscanf для перевода строки в число
- Возврат ссылки/указателя на локальную переменную
- Выход за пределы массива
- Сравнение символьных массивов
- Использование чисел, записанных в других системах счисления.
- Работа с локальной копией объекта, вместо работы с самим объектом


Неправильное поведение программы на этапе исполнения
(Программа работает, но не так, как запланировано)
- Использование типов char, short и float при работе с va_arg
- Определение размера массива, переданного в качестве аргумента функции.
- "Неожиданное" закрытие окна.
- "Неожиданное" целочисленное деление в арифметических выражениях.
- Ошибки в логических выражениях.
- Использование символа цифры вместо числа
- Лишняя точка с запятой
- switch без break
- Сравнение вещественных чисел при вычислениях
- Проверки на принадлежность значения определенному интервалу.
- Неверный аргумент тригонометрических функций.
- Сравнение знаковой переменной с беззнаковой.
- Использование запятой для отделения дробной части
- Забытое выделение тела цикла for, while и операторов if else
- Локальная переменная экранирует переменную с таким же именем из вышестоящей области видимости
- Неправильное использование memset

Алгоритмические ошибки
(Неправильно составлен алгоритм программы)
- Двойная перестановка строк или элементов массива.

Ошибки ввода-вывода
- Ошибки при использовании функции scanf()!
- При работе с fgetc под Windows чтение файла обрывается при достижении буквы 'я'
- Оставление символа '\n' в потоке ввода
- scanf() - ввод текстовых строк

Ошибки, связанные с отклонением от стандарта языка
- Неверный тип функции main()
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
02.10.2014, 12:49     Распространенные ошибки
Посмотрите здесь:

Распространенные ошибки - C++
Оглавление Ошибки этапа компиляции Программа не компилируется или компилируется с предупреждениями. Попытка модифицировать...

безопасность и распространенные ошибки - Perl
Тут наткнулся на очень интересные тексты: http://werad.narod.ru/articles/programm6.html http://werad.narod.ru/articles/programm9.html...

.NET 2.x Распространенные ошибки SEO и ASP.NET 2.0 - C# ASP.NET
Здравствуйте, существуют несколько СЕО проблем при использовании ASP.NET, ниже я опишу эти проблемы, нужно найти простое и эффективное...

Самые распространенные строки - Delphi
type Mytype = record name:string; surname:string; end; var Students:Mytype; MyFile:file of Mytype; ...

Вывести самые распространенные мужские и женские имена - C++
Имеется массив записей о студентах, каждая из которых включает поля: фамилия, имя, отчество, пол, возраст, курс. Разработать программу,...

Вывести самые распространенные женские и мужские имена - C++
Помогите решить задачу пожалуйста! Написать программу, которая формирует файл записей данной структуры Type Student=Record ...

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

После регистрации реклама в сообщениях будет скрыта и будут доступны все возможности форума.
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
02.10.2014, 12:55  [ТС]     Распространенные ошибки #2
1. Ошибки при использовании функции scanf().
Рассмотрим ошибки.
C
1
2
3
4
5
6
7
8
9
10
11
int k;
scanf("%d",&k);    //1
scanf("%d",k);     //2
scanf("%lf",&k);   //3
scanf("%d/n",&k);  //4
double a;
scanf("%lf",&a);   //5
char buf[100];
scanf("%s",buf);   //6
scanf_s("%s",buf); //7
scanf("%s",&buf);  //8
1. Здесь все правильно с точки зрения Си. Однако компилятор может выдать предупреждение (VS 2008) или даже ошибку (VS 2012):
1>d:\current\cpp\test\tset.cpp(13) : warning C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead.
Это нюанс исключительно микрософтовский. Предлагается использовать более защищенную функцию
scanf_s. Можете послушаться и заменить, а можете отключить это предупреждение добавив
#pragma warning(disable:4996)

или изменить уровень ошибок в свойствах проекта (VS 2008 2010):
Проект->Свойства->Свойства конфигурации->С/С++->Общие->Уровень предупреждений-> Уровень 2(W2).

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

3. Вводится целое число, но в форматной строке указано иное (lf - используется double).

4. Лишние знаки форматирования в командной строке (после ввода числа будет ожидаться еще один Enter).

5. Написано правильно. Однако, тут есть нюанс (который отсутствует в iostrem).
Если локаль языка установлена русская ( setlocale(LC_ALL,"Rus") ; ), то дробная часть отделяется от целой запятой, а не точкой.

6. По формату %s водится не строка целиком, а только очередное слово до пробельного символа. Для ввода всей строки используйте gets(buf);

7. scanf_s требует передачи размера массива в дополнительном параметре:
C++
1
scanf_s("%s",buf,100);
8. Передача указателя на адрес массива тоже выполняется правильно. Но в этом случае массив должен быть статическим и объявлен в том же адресном пространстве.
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
02.10.2014, 12:57  [ТС]     Распространенные ошибки #3
2. При работе с fgetc чтение файла обрывается при достижении буквы 'я'
(Автор Evg)

C
FILE *fp;
char c;
fp = fopen ("a.txt", "r");
while ((c = fgetc(fp)) != EOF)
{
  ...
}
Переменная 'c' должна иметь тип int, а не char.

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

Вот ссылки с пояснениями:
"я" это EOF?!
Существует ли EOF или это миф?
10 вопросов по С. part1
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
02.10.2014, 17:43  [ТС]     Распространенные ошибки #4
3. Использование типов char, short и float при работе с va_arg
(Автор Evg)
C
#include <stdio.h>
#include <stdarg.h>
 
void func (int x, ...)
{
  va_list va;
  char c1 = 0x11;
  char c2 = 0x22;
  short s1 = 0x3333;
  short s2 = 0x4444;
  float f1 = 1.25;
  float f2 = 5.75;
 
  va_start (va, x);
 
  c1 = va_arg (va, char);
  c2 = va_arg (va, char);
  s1 = va_arg (va, short);
  s2 = va_arg (va, short);
  f1 = va_arg (va, float);
  f2 = va_arg (va, float);
 
  printf ("c1=%hhx\n", c1);
  printf ("c2=%hhx\n", c2);
  printf ("s1=%hx\n", s1);
  printf ("s2=%hx\n", s2);
  printf ("f1=%f\n", f1);
  printf ("f2=%f\n", f2);
 
  va_end (va);
}
 
int main (void)
{
  char c1 = 0x11;
  char c2 = 0x22;
  short s1 = 0x3333;
  short s2 = 0x4444;
  float f1 = 1.25;
  float f2 = 5.75;
  func (0, c1, c2, s1, s2, f1, f2);
  return 0;
}
Этот код не будет работать или будет работать некорректно. Проблема кроется в том, что в языках Си/Си++ при передаче неспецифицированных аргументов начинает работать promotion для типов char, unsigned char, short, unsigned short и float. Подробнее можно почитать по ссылке:

Работа с va_arg

Правильное написание данного примера:

C
#include <stdio.h>
#include <stdarg.h>
 
void func (int x, ...)
{
  va_list va;
  char c1 = 0x11;
  char c2 = 0x22;
  short s1 = 0x3333;
  short s2 = 0x4444;
  float f1 = 1.25;
  float f2 = 5.75;
 
  va_start (va, x);
 
  c1 = (char) va_arg (va, int);
  c2 = (char) va_arg (va, int);
  s1 = (short) va_arg (va, int);
  s2 = (short) va_arg (va, int);
  f1 = (float) va_arg (va, double);
  f2 = (float) va_arg (va, double);
 
  printf ("c1=%hhx\n", c1);
  printf ("c2=%hhx\n", c2);
  printf ("s1=%hx\n", s1);
  printf ("s2=%hx\n", s2);
  printf ("f1=%f\n", f1);
  printf ("f2=%f\n", f2);
 
  va_end (va);
}
 
int main (void)
{
  char c1 = 0x11;
  char c2 = 0x22;
  short s1 = 0x3333;
  short s2 = 0x4444;
  float f1 = 1.25;
  float f2 = 5.75;
  func (0, c1, c2, s1, s2, f1, f2);
  return 0;
}
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
02.10.2014, 17:49  [ТС]     Распространенные ошибки #5
4. Определение размера массива, переданного в качестве аргумента функции.
(Автор BumerangSP):
C
1
2
3
4
5
6
7
8
9
10
11
int f(int *a) 
{
    return sizeof(a) / sizeof(*a);
}
int main() 
{
    int a[] = {1, 2, 3};
    printf("%d\n",sizeof(a) / sizeof(*a)  ); 
    printf("%d\n",f(a));
        return 0;
}
Решение: передавать размер массива как параметр функции:
C++
1
2
3
4
5
6
7
8
9
10
int f(int *a, int size) {
    return size;
}
int main()
{
    int a[] = {1, 2, 3};
    printf("%d\n",  ); 
    printf("%d\n",f(a,sizeof(a) / sizeof(*a)));
        return 0;
}
Evg
Эксперт CАвтор FAQ
17462 / 5700 / 361
Регистрация: 30.03.2009
Сообщений: 15,636
Записей в блоге: 26
02.10.2014, 21:33     Распространенные ошибки #6
7. Возврат из функции локальной строки

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

C
char* gen_string (void)
{
  char str[100];
  strcpy (str, "hello world");
  return str;
}
 
void func (void)
{
  p = gen_string ();
  printf ("%s\n", p);
}
В языке Си с массивом нельзя работать напрямую, как с целиковым объектом. Когда мы пишем имя переменной-массива, то семантически это означает взятие адреса на самый первый элемент массива. Т.е. "return str;" на самом деле исполнится как "return &str[0];". Т.е. из функции будет возвращён указатель на локальную переменную функции gen_string. Ну а после выхода из функции виртуально можно считать, что все её локальные переменные разрушаются

Как это дело лечить?

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

C
char* gen_string (char *str, int len)
{
  strncpy (str, "hello world", len);
  return str;
}
 
void func (void)
{
  char str[100], *p;
  p = gen_string (str, sizeof (str));
  printf ("%s\n", p);
}
2. Условно честный способ - внутри функции gen_string использовать static переменную. Условная честность заключается в том, что значение в строке проживёт лишь до следующего вызова функции. В большинстве случаев в реальной жизни этого достаточно, но бывает, что и нет

C
char* gen_string (void)
{
  static char str[100];
  strcpy (str, "hello world");
  return str;
}
 
void func (void)
{
  p = gen_string ();
  printf ("%s\n", p);
}
3. Честный, но плохой способ - внутри функции выделать память malloc'ом. Плохость данного метода в том, что потом кто-то должен освобождать память. А когда память выделяется на одном уровне, а освобождается на другом - такие коды часто подвержены ошибкам или утечкам памяти

C
char* gen_string (void)
{
  char *str;
  str = malloc (100);
  strcpy (str, "hello world");
  return str;
}
 
void func (void)
{
  p = gen_string ();
  printf ("%s\n", p);
  free (p);
}
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
03.10.2014, 20:52  [ТС]     Распространенные ошибки #7
5. "Неожиданное" закрытие окна под Windows.
Когда консольное приложение запускается непосредственно из среды программирования,
то после выполнения последнего оператора программы ( return 0; ) окно закрывается.
Вставляйте оператор, ожидающий ввода символа с клавиатуры перед return:
C
1
2
3
4
#include <stdlib.h>
.....
system("pause");
return 0;
или
C
1
2
3
4
5
#include <stdio.h>
.....
puts("Нажмите Enter для завершения");
getchar();
return 0;
или с использованием библиотеки низкоуровневого ввода conio.h
(на *NIX системах она не используется):
C
1
2
3
4
#include <conio.h>
.....
getch();
return 0;
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
04.10.2014, 10:24  [ТС]     Распространенные ошибки #8
6. Оставление символа '\n' в потоке ввода:
C
1
2
3
4
5
int n;char c;
puts("введите n:");
scanf("%d",&n; // вводится число после нажатия Enter
puts("введите символ:");
scanf("%c",&c); // хотим ввести символ на новой строке, но вместо него вводится '\n'
исправление ошибки
C
1
2
3
4
puts("введите n:");
scanf("%d\n",&n;  // вводим число и пропускаем конец строки
puts("введите символ:");
scanf("%c",&c); // вводится символ на новой строке ( !!!  '\n' второй строки еще не прочитали).
или
C
1
2
3
4
5
puts("введите n:");
scanf("%d",&n;  // вводим число 
getchar(); // пропускаем конец строки
puts("введите символ:");
scanf("%c",&c);
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
04.10.2014, 10:30  [ТС]     Распространенные ошибки #9
8. Выделение памяти без дальнейшего освобождения
При выделении памяти обязательно освобождайте её перед выходом из функции,
во избежание утечки памяти:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <memory.h>
#include <locale.h>
int f(int n)
{
    int i,sum;
    int *a;
    a=(int*)malloc(n*sizeof(int)); // выделена память
    sum=0;
    for(i=0;i<n;i++)
    {
        a[i]=i*i;
        sum+=a[i];
    }
    free(a); // освобождена память
    return sum;
}
int main()
{
    int s;
    s=f(5);     
    printf("s=%d\n",s);
    s=f(10);
    printf("s=%d\n",s);
    setlocale(LC_ALL,"Rus");
    printf("\nНажмите Enter для завершения");
    getchar();
    return 0;
}
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
04.10.2014, 10:57  [ТС]     Распространенные ошибки #10
9. Использование неинциализированной переменной

C
1
2
int x; // предполагалось, например, int x=1;
printf("%d",x); // выведет мусор (то, что находилось в переменной x)
Часто встречается вариант с использованием неинициализированного счётчика или переменной для подсчёта суммы:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <locale.h>
#define arrSize  3
int main()
{
    int i,arr[arrSize] = {1, 2, 3};
    int sum;                // надо писать int sum=0;
    for (i=0; i < arrSize; i++)
        sum += arr[i]; // мы к "мусору" добавляем значение элемента массива
    printf("%d\n",sum); // выведет не то, что вы ожидали
    setlocale(LC_ALL,"Rus");
    printf("\nНажмите Enter для завершения");
    getchar();
    return 0;
}
Такой код может привести к ошибке на этапе исполнения:
Run-Time Check Failure #3 - The variable 'sum' is being used without being initialized.
Также компилятор может выдать предупреждение об ошибке:
1>d:\current\cpp\test\test.c(9) : warning C4700: использована неинициализированная локальная переменная "sum"
Для этого надо установить соответствующий уровень предупреждений компилятора.
В Visual Studio это делается через команду меню
Проект->Свойства->Свойства конфигурации->С/С++->Общие->
Уровень предупреждений->Уровень 4 (/W4).
Evg
Эксперт CАвтор FAQ
17462 / 5700 / 361
Регистрация: 30.03.2009
Сообщений: 15,636
Записей в блоге: 26
12.10.2014, 14:06     Распространенные ошибки #11
10. "Неожиданное" целочисленное деление в арифметических выражениях.
Следует помнить, что результат деления целого на целое - тоже целое:
C
1
2
3
4
5
int n;double y;
n=2;
y = 1/n;// y будет равен нулю
y = pow(y, 1/3 ); // возведение в нулевую степень
z = y + n/(n+1); // прибавляется ноль
Рекомендации:
Все константы, которые явно не должны быть целыми, делайте действительными
(записывайте с точкой, первое время можете даже после точки ставить нуль 1.0 ):
C
1
2
3
4
5
int n;double y;
n=2;
y = 1./n;// y будет равен 0.5
y=pow(y, 1./3. ); //извлечение корня кубического
z=y + n/(n+ 1. ); // прибавляется 0.666666666666
Там, где в выражении нет констант, используйте оператор приведения
C
1
2
3
4
int n,m;
n=1;
m=2;
double y = (double)n/m;

Пояснения, почему так происходит.

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

В случае различных типов один из типов является более "широким", чем другое. А потому в двухаргументной операции всегда аргумент более "узкого" типа приводится к более "широкому" типу. Проверка идёт примерно в таком порядке:
  • Вещественный тип всегда считается более широким, чем целочисленный
  • Когда оба типа вещественные или оба целочисленные, то тип с большим размером всегда считается более широким, чем тип с меньшим размером
  • В случае равенства размеров для целочисленных типов беззнаковый тип считается более широким, чем знаковый

В итоге код

C
float f;
int i;
unsigned u;
long long ll;
x + f;
u + f;
i + u;
ll + u;
эквивалентен

C
int i;
unsigned u;
float f;
((float)x) + f; /* тип результата - float */
((float)u) + f; /* тип результата - float */
((unsigned)i) + u; /* тип результата - unsigned */
ll + ((long long)u); /* тип результата - long long */
Если мы вернёмся к делению, то следующий код

C
int a, b;
float f;
f = a / b;
эквивалентен

C
int a, b, tmp;
float f;
/* сначала выполняем целочисленное деление
 * (при котором отбрасывается остаток) */
tmp = a / b;
/* и только потом полученный результат (с утерянной
 * дробной частью) преобразуется в вещественный тип */
f = (float) tmp;
А потому, чтобы выполнить нормальное вещественное деление нужно хотя бы один из аргументов превратить в вещественный тип, после чего второй аргумент превратится в вещественный тип автоматически по выше описанным правилам. Т.е. явно написать

C
int a, b;
float f;
f = ((float)a) / b;
или

C
int a, b;
float f;
f = a / ((float)b);
что будет эквивалентно

C
int a, b;
float f;
f = ((float)a) / ((float)b);
Если один из аргументов является константой, то можно написать её в виде "3.0" (с вещественной дробной частью), что автоматически приведёт её тип к вещественному
TheCalligrapher
С чаем беда...
Эксперт CЭксперт С++
3635 / 1910 / 503
Регистрация: 18.10.2014
Сообщений: 3,476
24.10.2014, 11:30     Распространенные ошибки #12
11.Использование функции strncpy как функции "безопасного" копирования строк

Функция strncpy появилась в стандартной библиотеке языка С для решения одной узкоспециализированной задачи - перевода классических нуль-терминированных строк в так называемые строки фиксированной ширины. Строки фиксированной ширины - это особый способ представления строк, который использовался в файловой системе Unix во времена, когда язык С был еще очень молодым.

В формате фиксированной ширины строка хранится в соответствии со следующими правилами:
1. Все строки хранятся в буфере заранее заданной фиксированной ширины N
2. Если строка имеет длину меньше N, то все лишние позиции в строке заполняются нулевыми символами
3. Если строка имеет длину ровно N, то она полностью заполняет буфер и не заканчивается нулевым символом

В частности, файловая система Unix System V использовала такие строки длины 14 для представления имен файлов. Такой формат представления строк был выбран с одной-единственной целью - сэкономить один байт при представлении имен максимальной длины. При использовании классических нуль-терминированных строк имя длины 14 потребовало бы 15 байт, в то время как в соответствии с соглашениями формата фиксированной ширины такая строка умещается в 14 байт.

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

Так вот, функция strncpy предназначалась именно для инициализации таких буферов. Например, приведенные ниже вызовы функции strncpy формируют правильно проинициализированные строки фиксированной ширины 14 в своих целевых буферах

C
1
2
3
4
char target1[14], target2[14];
 
strncpy(target1, "hello", sizeof target1);
strncpy(target2, "hello, world !", sizeof target2);
Обратите внимание, что во втором случае целевой буфер не будет нуль-терминирован, в полном соответствии со спецификацией строк фиксированной ширины. А в первом случае он будет, неформально выражаясь, нуль-терминирован 9 раз.

Однако времена поменялись, необходимость экономить один байт из пятнадцати практически отпала и функция strncpy стала почти бесполезной... Но тут случилось страшное. Кто-то малознакомый и с конкретной спецификацией функции strncpy и с изначальным предназначением вдруг пришел к выводу, что эта функция предназначена (и пригодна) для "безопасного" копирования обыкновенных нуль-терминированых строк. Такого человека можно понять, ибо функция strncpy названа довольно неудачно: ее название как-будто намекает на то, что эта функция предназначена именно для ограниченного по длине копирования строк. А документация... Кто ж читает документацию, когда и из названия функции все понятно?

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

C
1
2
3
4
char target[BUFFER_SIZE];
...
strncpy(target, source, BUFFER_SIZE);
target[BUFFER_SIZE - 1] = '\0';
Т.е. после использования strncpy надо обязательно не забыть выполнить нуль-терминацию целевой строки "на всякий случай". Именно в рамках такого применения функция strncpy обрела вторую жизнь и даже умудрилась войти в некоторые учебники как пример "безопасной" работы со строками.

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

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

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

Но, если уж вам все таки хочется пользоваться strncpy, то помните - эта функция не гарантирует того, что ваша строка будет правильно нуль-терминирована. Не забываете обеспечить корректную нуль-терминацию самостоятельно.
Evg
Эксперт CАвтор FAQ
17462 / 5700 / 361
Регистрация: 30.03.2009
Сообщений: 15,636
Записей в блоге: 26
24.10.2014, 16:38     Распространенные ошибки #13
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
По какой-то необъяснимой причине эти стандартные функции малоизвестны среди начинающих программистов
Причина вполне стандартная, как и для многих других ошибок - неправильно рассказывали в школе/институте из-за того, что преподаватель сам плохо разбирается в вопросе

Добавлено через 1 час 33 минуты
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Однако, к сожалению, как и atoi/atof, функция sscanf приводит к неопределенному поведению в случае арифметического переполнения
Тут есть ещё дополнительная засада. Выдержка из linux'ового man'а:

ERRORS
...
ERANGE The result of an integer conversion would exceed the size that can be stored in the corresponding integer type.
и действительно, на практике это имеет место быть:

C
#include <stdio.h>
#include <errno.h>
 
int main (void)
{
  int x;
  sscanf ("99999999999999999999999", "%d", &x);
  printf ("x=%d, errno=%d\n", x, errno);
  return 0;
}
Код
$ gcc t.c
$ ./a.out
x=2147483647, errno=34
Однако под solaris'ом ничего такого в man'е не указано, и поведение иное:

Код
$ gcc t.c
$ ./a.out
x=-159383553, errno=0
Т.е. имеет место быть дополнительный источник проблем, когда человек, прочитав документацию и запуская на одной платформе, видя, что всё работает, делает предположение, что на других платформах тоже будет работать
TheCalligrapher
С чаем беда...
Эксперт CЭксперт С++
3635 / 1910 / 503
Регистрация: 18.10.2014
Сообщений: 3,476
24.10.2014, 18:25     Распространенные ошибки #14
Цитата Сообщение от Evg Посмотреть сообщение
и действительно, на практике это имеет место быть:
Да, конечно, если заглянуть в реализацию стандартной библиотеки, то обнаружится, что 'atoi' "почти всегда" просто напросто вызывает 'strtol'. Таким образом хорошее поведение 'strtol' тут же наследуется и 'atoi'. Я не удивлюсь, если увижу аналогично реализованную 'sscanf'.

Но спецификация языка продолжает настаивать на том, что 'atoi' ведет себя (или может вести себя) плохо:

7.20.1 Numeric conversion functions
1 The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undefined.
Более того, даже спецификация POSIX в этом отношении тоже согласна со стандартом и открыто говорит, что 'atoi' ведет себя как 'strtol', но не обнаруживает ошибок.

Аналогичное утверждение сделано о функциях группы 'scanf'.

Если заглянуть в такой документ, как Rationale for International Standard-Programming Languages-C, то там открыто пишут, что уже в времена первого стандарта языка С (C89/90) опасность функция группы 'ato...' была всем понятна. Именно по этой причине, в частности, первое дополнение к стандарту, вышедшее в в 1995 году и добавившее, кроме прочего, в стандартную библиотеку функции работы с wide character строками, не ввела "широких" версий для функций этой группы. Там же сказано, что функции группы 'ato...' считаются "поглощенными" (subsumed) функциями группы 'strto...', но не были исключены из языка полностью по причине их широкого использования в старом коде (см. 7.20.1.1/25).

Другими словами, мне не совсем понятно, почему комитет по стандартизации не хочет "спасти" функции группы 'ato...'. Ведь на первый взгляд не составит никакого труда просто уточнить их спецификацию и сделать их безопасными аналогами функций группы 'strto...'. Но этого не происходит. Да, с одной стороны, как сказано в rationale, эти функции считаются "ненужными". Но с другой, группа продолжает поддерживаться: 'atoll' была добавлена относительно "недавно".
Evg
Эксперт CАвтор FAQ
17462 / 5700 / 361
Регистрация: 30.03.2009
Сообщений: 15,636
Записей в блоге: 26
24.10.2014, 18:50     Распространенные ошибки #15
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Но спецификация языка продолжает настаивать на том, что 'atoi' ведет себя (или может вести себя) плохо
Корень подобных проблем ты уже озвучивал выше - стандарты никто не читает, все пытаются делать выводы на основании того, что на их собственной системе программирования всё работает. Я тут просто наглядно продемонстрировал к чему может привести такой неправильный подход. Для начинающих само понятие другой платформы уже тяжело к восприятию, так что может быть тут уже будет перебором засовывать всё это в ошибки для начинающих, т.к. это уже по сути следующий уровень
TheCalligrapher
С чаем беда...
Эксперт CЭксперт С++
3635 / 1910 / 503
Регистрация: 18.10.2014
Сообщений: 3,476
24.10.2014, 22:57     Распространенные ошибки #16
12.Использование функций atoi/atof или sscanf для перевода строки в число

Функции atoi/atof (и другие функции группы ato...), несмотря на свою исключительную популярность, практически не пригодны для надёжного решения задачи преобразования десятичной записи числа в его внутреннее представление. Эти функции страдают от двух серьёзных проблем: они не формируют осмысленного отчёта об ошибочных ситуациях и они вызывают неопределённое поведение при переполнении.

Во-первых, функция atoi, как известно, возвращает нулевое значение, если входная строка не содержит корректного десятичного представления. Понятно, что получив нулевой результат, вызывающий код не может немедленно ответить на вопрос о том, произошла ли какая-то ошибка или входная строка действительно содержала ноль. В практическом коде, основанном на использовании atoi, зачастую можно встретить попытки дополнительного анализа входной строки в ситуациях, когда atoi вернула ноль. Т.е. код пытается выполнить пост-разбор входной строки чтобы выяснить, содержится ли там ноль или какая-то абракадабра. Несмотря на то, что для функций целочисленного преобразования такой разбор выполнить несложно, он зачастую бывает реализован с ошибками (не говоря уже о более сложной с точки зрения входного формата функции atof). В любом случае, решение это в корне порочно, ибо вся прелесть использования готовых стандартных функций преобразования строк в числа во многом и заключается в том, что разбор не надо делать вручную.

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

Но это ещё не всё.

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

C
1
2
int i = atoi("999999999999");
/* Неопределённое поведение на платформе с 32-битным int*/
Я не буду рассказывать вам стандартные байки о том, что неопределённое поведение взорвет вашу ЭВМ или отформатирует ваш НЖМД. Но тем не менее код, вызывающий неопределённое поведение в программе - это всегда очень плохо.

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

Так как же правильно и безопасно преобразовать строковую запись числа во внутреннее представление?

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

Ещё в девяностых годах прошлого столетия функции группы strto... были признаны замещающими функции группы ato.... Последние были оставлены в стандартной библиотеке в первую очередь для совместимости со старым кодом.

Другими словами, функциям группы ato... не место в серьёзном коде. Эти функции применимы лишь в одноразовом экспериментальном коде, написанном "на салфетке". В серьёзном коде для выполнения рассматриваемого преобразования следует использовать функции группы strto....
Evg
Эксперт CАвтор FAQ
17462 / 5700 / 361
Регистрация: 30.03.2009
Сообщений: 15,636
Записей в блоге: 26
24.10.2014, 23:52     Распространенные ошибки #17
После того, как часть поста #12 вынесли в пост #16, пост #13 стал бессмысленным, т.к. он должен по смыслу быть после поста #16
TheCalligrapher
С чаем беда...
Эксперт CЭксперт С++
3635 / 1910 / 503
Регистрация: 18.10.2014
Сообщений: 3,476
28.10.2014, 17:11     Распространенные ошибки #18
Цитата Сообщение от zss Посмотреть сообщение
8. Передача указателя на адрес массива почему-то срабатывает правильно.
Заинтересовало. О чем именно идет речь в данном случае?
zss
Модератор
Эксперт С++
6276 / 5879 / 1902
Регистрация: 18.12.2011
Сообщений: 15,076
Завершенные тесты: 1
28.10.2014, 18:41  [ТС]     Распространенные ошибки #19
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
О чем именно идет речь в данном случае?
C++
1
2
3
char mass[N];
scanf("%s",mass); // так правильно
scanf("%s",&mass); // почему-то тоже срабатывает
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
28.10.2014, 19:01     Распространенные ошибки
Еще ссылки по теме:

Найти и вывести самые распространенные женские те мужские имена - Pascal ABC
О каждом студенте факультета доступна следующая информация: фамилия, имя, отчество, пол, возраст, курс. Найти и вывести самые...

Вывести самые распространенные мужское и женское имена среди студентов - Pascal
Во входном файле записана следующая информация о каждом из 20 студентов некоторого вуза: &lt;фамилия&gt;, &lt;имя&gt;, &lt;отчество&gt;, &lt;пол&gt;, &lt;возраст&gt;,...

Какие самые распространенные бесплатные спам-фильтры Вам известны? - Безопасность сайтов
Всем привет понимаю что не заслуживаю как новенький о помощи но все же попрошу помогите . Вопрос жизни . Какие самые распространенные...

Тема: составить программу Справочная экспертная система «Словарь» (содержит наиболее распространенные терми - Программирование
Тема Помогите написать программу Справочная экспертная система «Словарь» (содержит наиболее распространенные термины и понятия по...

Найти ошибки в данной программе (известно что есть только 2 ошибки) - Assembler
У меня в модуле с програмирования задание: Найти ошибки в данной программе (известно что есть только 2 ошибки) .model small .data ...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
Evg
Эксперт CАвтор FAQ
17462 / 5700 / 361
Регистрация: 30.03.2009
Сообщений: 15,636
Записей в блоге: 26
28.10.2014, 19:01     Распространенные ошибки #20
"mass" семантически есть &(mass[0]), т.е. указатель на char
"&mass" семантически хз как написать, но по смыслу это есть указатель на конструкцию, которая есть массив из N char'ов. Т.е. тип указателя другой, но значение то же самое

C
1
2
3
4
5
6
7
8
9
10
void foo (void)
{
  char a[10];
  char *p; /* указатель на char */
  char (*pp)[10]; /* указатель на char[10] */
 
  p = a;
  p = &a; /* warning */
  pp = &a;
}
Код
$ gcc t.c -Wall -c
t.c: In function 'foo':
t.c:8: warning: assignment from incompatible pointer type
Yandex
Объявления
28.10.2014, 19:01     Распространенные ошибки
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru