Нарушение прав доступа с помощью указателей? - С++

Я написал простую программу токенизации строк с использованием указателей для недавнего школьного проекта. Однако у меня возникли проблемы с моим методом StringTokenizer::Next(), который при вызове должен возвращать указатель на первую букву следующего слова в массиве char. Я не получаю ошибок времени компиляции, но я получаю ошибку времени выполнения, которая гласит:

Unhandled exception at 0x012c240f in Project 5.exe: 0xC0000005: Access violation reading location 0x002b0000.

В настоящее время программа токенизирует массив символов, но затем останавливается, и появляется эта ошибка. Мне кажется, это связано с NULL проверкой, которую я выполняю в своем Next() методе.

Итак, как я могу это исправить?

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

Спасибо!


StringTokenizer.h:

#pragma once

class StringTokenizer
{
public:
StringTokenizer(void);
StringTokenizer(char* const, char);
char* Next(void);
~StringTokenizer(void);
private:
char* pStart;
char* pNextWord;
char delim;
};

StringTokenizer.cpp:

#include "stringtokenizer.h"
#include <iostream>
using namespace std;

StringTokenizer::StringTokenizer(void)
{
pStart = NULL;
pNextWord = NULL;
delim = 'n';
}

StringTokenizer::StringTokenizer(char* const pArray, char d)
{
pStart = pArray;
delim = d;
}

char* StringTokenizer::Next(void)
{
pNextWord = pStart;
if (pStart == NULL) { return NULL; }

while (*pStart != delim) // access violation error here
{
    pStart++;
}

if (pStart == NULL) { return NULL; }

*pStart = '\0'; // sometimes the access violation error occurs here
pStart++;

return pNextWord;
}

StringTokenizer::~StringTokenizer(void)
{
delete pStart;
delete pNextWord;
}

Main.cpp:

// The PrintHeader function prints out my
// student info in header form
// Parameters - none
// Pre-conditions - none
// Post-conditions - none
// Returns - void
void PrintHeader();

int main ( )
{
const int CHAR_ARRAY_CAPACITY = 128;
const int CHAR_ARRAY_CAPCITY_MINUS_ONE = 127;

// create a place to hold the user's input
// and a char pointer to use with the next( ) function
char words[CHAR_ARRAY_CAPACITY];
char* nextWord;

PrintHeader();

cout << "\nString Tokenizer Project";
cout << "\nyour name\n\n";
cout << "Enter in a short string of words:";
cin.getline ( words, CHAR_ARRAY_CAPCITY_MINUS_ONE );

// create a tokenizer object, pass in the char array
// and a space character for the delimiter
StringTokenizer tk( words, ' ' );

// this loop will display the tokens
while ( ( nextWord = tk.Next ( ) ) != NULL )
{
    cout << nextWord << endl;
}


system("PAUSE");
return 0;
}


ИЗМЕНИТЬ:

Хорошо, у меня теперь программа работает нормально, пока разделителем является пробел. Но если я передам `/' в качестве разделителя, он снова выдаст ошибку нарушения прав доступа. Любые идеи?

Функция, работающая с пробелами:

char* StringTokenizer::Next(void)
{
pNextWord = pStart;

if (*pStart == '\0') { return NULL; }

while (*pStart != delim)
{
    pStart++;
}

if (*pStart = '\0') { return NULL; }

*pStart = '\0';
pStart++;

return pNextWord;
}

person Alex    schedule 08.02.2010    source источник


Ответы (4)


Этот ответ предоставлен на основе отредактированного вопроса и различных комментариев/замечаний в других ответах...

Во-первых, каковы возможные состояния pStart при вызове Next()?

  1. pStart имеет значение NULL (конструктор по умолчанию или иным образом установлен в NULL)
  2. *pStart равен '\0' (пустая строка в конце строки)
  3. *pStart является разделителем (пустая строка рядом с разделителем)
  4. *pStart — это что-то еще (токен непустой строки)

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

if (pStart == NULL) { return NULL; }

Почему нам пока не нужно беспокоиться о случаях 2 или 3? Вы, вероятно, захотите рассматривать соседние разделители как содержащие токен пустой строки между ними, в том числе в начале и в конце строки. (Если нет, отрегулируйте по вкусу.) Цикл while справится с этим за нас, при условии, что вы также добавите проверку '\0' (необходимую независимо):

while (*pStart != delim && *pStart != '\0')

После цикла while вам нужно быть осторожным. Каковы возможные состояния сейчас?

  1. *pStart равен '\0' (маркер заканчивается в конце строки)
  2. *pStart является разделителем (маркер заканчивается на следующем разделителе)

Обратите внимание, что сам pStart не может быть здесь NULL.

Вам нужно вернуть pNextWord (текущий токен) для обоих этих условий, чтобы не удалить последний токен (т. е. когда *pStart равен '\0'). Код правильно обрабатывает случай 2, но не случай 1 (исходный код опасно увеличивал pStart после '\0', новый код возвращал NULL). Кроме того, важно правильно сбросить pStart для случая 1, чтобы следующий вызов Next() вернул NULL. Я оставлю точный код в качестве упражнения для читателя, так как в конце концов это домашнее задание;)

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

Наконец, я заметил, что у вас есть вызовы удаления как для pStart, так и для pNextWord в вашем деструкторе. Во-первых, для удаления массивов нужно использовать delete [] ptr; (т. е. удаление массива). Во-вторых, вы не стали бы удалять и pStart, и pNextWord, потому что pNextWord указывает на массив pStart. В-третьих, к концу pStart больше не указывает на начало памяти, поэтому вам потребуется отдельный член для хранения исходного начала для вызова delete []. Наконец, эти массивы размещаются в стеке, а не в куче (т. е. с использованием char var[], а не char* var = new char[]), и поэтому их нельзя удалять. Поэтому вам следует просто использовать пустой деструктор.

Еще один полезный совет — подсчитать количество вызовов new и delete; их должно быть одинаковое количество. В этом случае у вас нет вызовов new и два вызова delete, что указывает на серьезную проблему. Если бы было наоборот, это указывало бы на утечку памяти.

person Jason Govig    schedule 08.02.2010
comment
Спасибо, это было действительно очень полезно! - person Alex; 08.02.2010

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

Рассмотрим цикл while в Next():

while (*pStart != delim) // access violation error here
{
    pStart++;
}

Допустим, строка "blah\0". Обратите внимание, что я включил завершающий ноль. Теперь спросите себя: как этот цикл узнает, что нужно остановиться, когда он достигает конца строки?

Что еще более важно: что происходит с *pStart, если цикл не удается остановиться в конце строки?

person DK.    schedule 08.02.2010

Внутри ::Next вам нужно проверить символ разделителя, но вам также нужно проверить конец буфера (который, как я предполагаю, обозначается \0).

while (*pStart != '\0' && *pStart != delim) // access violation error here
{
    pStart++;
}

И я думаю, что эти тесты в ::Next

if (pStart == NULL) { return NULL; }

Вместо этого должно быть это.

if (*pStart == '\0') { return NULL; }

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

person John Knoeller    schedule 08.02.2010
comment
Обратите внимание, что он устанавливает указатели в NULL в конструкторе без аргументов. - person Anon.; 08.02.2010
comment
@Anon: да, но он использует другой конструктор, поэтому тест NULL может быть задуман как пуленепробиваемый, а может и нет. - person John Knoeller; 08.02.2010
comment
тогда я должен просто избавиться от проверки NULL? Это действительно бессмысленно? - person Alex; 08.02.2010
comment
Хорошо, теперь моя программа работает, но если я передам ей другой разделитель, она снова завершится с нарушением прав доступа. Странный. - person Alex; 08.02.2010
comment
@Alex: проверка NULL не лишена смысла, но в этом случае было важнее проверить конец строки, и было неясно, имеете ли вы в виду проверку нуля как проверку конца строки. - person John Knoeller; 08.02.2010
comment
Есть две точки зрения на проверку NULL. Некоторые считают, что вы всегда должны проверять. другие (например, я) считают, что вы должны проверять только тогда, когда нуль является действительной возможностью - лучше сбой раньше, чем ошибка, замаскированная нулевой проверкой. - person John Knoeller; 08.02.2010

Нарушение прав доступа обычно означает плохой указатель.

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

person Anon.    schedule 08.02.2010
comment
что было бы лучшим способом добавить проверку ошибок для этого в моей программе? - person Alex; 08.02.2010
comment
Пока вы повторяете свою строку, а также проверяете разделитель, проверьте завершающий нуль, который означает конец строки. Если вы его найдете, остановитесь на этом и верните строку. - person Anon.; 08.02.2010