2014-10-03 3 views
3

Использует while(*p++) для проверки того, содержит ли массив больше элементов?Есть (* p ++) опасно?

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

Этот простой код:

#include <stdio.h> 

void f(float *p) { 
    printf("%p : %.f\n", p, *p); 
    while(*p++){ 
     printf("%p : %.f\n", p, *p); 
    } 
    printf("%p : %.f\n", p, *p); 
} 

int main() { 
    float p[] = {2.2, 3.2}; 
    f(p); 
    return 0; 
} 

дает этот вывод:

0x7fffed4e8f10 : 2 
0x7fffed4e8f14 : 3 
0x7fffed4e8f18 : 0 
0x7fffed4e8f1c : 0 

Таким образом, если на 0x7fffed4e8f18 значение было ≠ 0 будет это сделать мой код не так?

+4

Короткий ответ: да. Вы можете создать указатель на один элемент за концом массива, но разыменование этого указателя дает неопределенное поведение. –

+2

Хорошо, если вы * знаете *, что в вашем массиве будет 0 (т. Е. * Require * это по контракту вашей функции), то он будет таким же безопасным или небезопасным, как и полагающийся на параметр длины, предоставленный пользователем. В отсутствие такого контракта это, конечно, безумие. – 5gon12eder

+1

О, и в вашем случае вам повезло, что в массиве было 0. Предполагая, что это полагается на неопределенное поведение. – 5gon12eder

ответ

3

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

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

void 
f(float * p) 
{ 
    do 
    { 
     printf("% .01f\n", *p); 
    } 
    while(*p++); 
} 

int 
main() 
{ 
    float p[] = {-1.0f, -0.5f, 0.0f, 0.5f, 1.0f}; 
    f(p); 
    return 0; 
} 

Выведет

-1.00 
-0.50 
0.00 

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

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

  1. не содержат байты NUL и
  2. прекращается путем размещения байт NUL после последнего байта.

Если оба требования выполнены, делая

void 
roll_all_over_it(char * string) 
{ 
    while(*string); 
    { 
     putchar(*string++); 
    } 
} 

безопасен по контракту.

Это делает использование строк немного более удобным, так как нам не нужно поддерживать целое число рядом с каждой строкой, чтобы отслеживать ее длину. С другой стороны, в нем сделано немало (небрежно написанных) программ, уязвимых для переполнения буфера, и есть другие проблемы, которые возникают из этого предположения. Смотри, например, обсуждение fgets в GNU libc manual:

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

+0

Это просто доказывает, насколько опасен (* p ++).благодаря – Igor

8

Как только p выходит за пределы массива, разыменование его вызывает неопределенное поведение. Так что да, делать это опасно.

+2

Вот почему вы видите, что каждый api, который принимает массив в качестве параметра, также принимает длину как второй параметр. –

2

Да, это: ваша функция принимает аргумент указателя, но вы не проверяете, не является ли это NULL-указателем. Выделение нулевого указателя не допускается.
Как указывали другие, разыменование недопустимого указателя (за пределами) приводит к неопределенному bahviour, что плохо.

Кроме того, вы используете правильную строку формата для печати указателя (%p), но скомпилируйте свой код с помощью -Wall -pedantic. печать значения указателя является одним из немногих случаев, когда у вас есть, чтобы наложить указатель на void *. IE изменение:

printf("%p : %.f\n", p, *p); 

в

printf("%p : %.f\n", (void *) p, *p); 

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

void f(float *arr, size_t arr_len) 
{ 
    if (arr == NULL) 
     exit(EXIT_FAILURE);//handle error 
    //do stuff 
} 

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

//example setup 
float foo = 123.4f 
float *bar = malloc(123 * sizeof *foo);//<-- uninitialized memory, contains junk 
//omitting if (bar == NULL) check, so bar might be null 
//dangerous calls: 
f(&foo); 
f(bar);//<-- bar could be null if malloc failed, and contains junk if it didn't dangerous 
f(NULL); 
+2

Я думаю, что «вы не проверяете, чтобы убедиться, что это не часть NULL-указателя», это немного вводит в заблуждение. Это не то, о чем беспокоит вопросник и честно, «NULL» - всего лишь одно из очень многих недействительных указателей. Мы не можем проверить их все. C не является Java. – 5gon12eder

+0

Итак, как бы вы решили проверить, что массив закончен, если вы не знаете размер массива, или это невозможно? Есть ли свет в конце туннеля? – Igor

+1

@ 5gon12eder: Я не говорю, что вы можете проверить все недействительные указатели. Но 'NULL' легко проверить, и в этом случае очевидный недостаток в коде –

2

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

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

Если вы настаиваете на переходе вашего массива f как float * указателю, неплохо было бы передать исходный размер массива вручную от вызывающего (т.е. от main), в качестве дополнительного параметра функции f.

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

1

Да, способ, которым вы написали свой код, дает неопределенное поведение , если в вашем массиве нет нулевого элемента.

То есть while(*p++) сам по себе не плох, но применение его к массиву, который вы явно не завершили с помощью нулевого элемента. I. e. следующая версия безопасна:

#include <stdio.h> 

//p must point to a zero terminated array 
void f(float *p) { 
    printf("%p : %.f\n", p, *p); 
    while(*p++){ 
     printf("%p : %.f\n", p, *p); 
    } 
    //printf("%p : %.f\n", p, *p); //p points past the end of the array here, so dereferencing it is undefined behavior 
} 

int main() { 
    float array[] = {2.2, 3.2, 0}; //add a terminating element 
    f(array); 
    return 0; 
} 

Однако, я обычно предпочитаю явно передавая размер массива:

#include <stdio.h> 

void f(int floatCount, float *p) { 
    printf("%p : %.f\n", p, *p); 
    for(int i = 0; i < floatCount; i++) { 
     printf("%p : %.f\n", p+i, p[i]); 
    } 
} 

int main() { 
    float array[] = {2.2, 3.2}; 
    int count = sizeof(array)/sizeof(*array); 
    f(count, array); 
} 

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

1

То, что вы сделали, действительно очень опасно!

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

Вот что вы должны сделать:

#include <stdio.h> 

void f(float *p) { 
    printf("%p : %.f\n", p, *p); 
    while(*p++){ 
     printf("%p : %.f\n", p, *p); 
    } 
    /* because of ++, p is now one element to far : stop and do not print ... */ 
} 

int main() { 
    float p[] = {2.2, 3.2, 0}; /* force a 0 as a last element marker */ 
    f(p); 
    return 0; 
} 
+0

Проверка наличия нулевого значения не имеет эффекта. – Igor

+0

@Igor: не могли бы вы уточнить? Я явно помещаю элемент 0 в последний, проверяю его присутствие и никогда не отменю ссылку на указатель outsite массива: он имеет эффект! –

+0

Я имел в виду: вы не контролируете последний элемент массива равным 0: – Igor

Смежные вопросы