Разминка для мозгов: строка со счётчиком

Всем привет.

Итак, n дней назад, Cranium предложил тему для нашей супер рублики «Разминка для мозгов».
ТЗ звучит так:

аккуратно написать класс, реализующий строку со счётчиком (или «строка в стиле Pascal») без использования STL и других библиотек. Примечание: строка со счётчиком НЕ завершается '\0'.

Посмотреть и оценить моё решение можно здесь.( devcpp проект )

Давайте делиться оценками и решениями. Вместе мы сделаем идеальный класс для представления строк со счётчиком! :)

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

Сразу в глаза операторы сравнения бросаются. Попробуйте:

PascalString strF = "one" ;
PascalString strS = "two" ;
std::cout << (strF!=strS) << std::endl ;

почему operator += возвращает временный объект?

PascalString operator+= ( const PascalString& );

Действительно, косяк вышел.
Вот исправленный оператор сравнения:


bool PascalString::operator!= ( const PascalString &ob )
{
    if ( len != ob.len )
       return true;
    else
       return !(_memcmp( str, ob.str, len ) );
}

почему operator += возвращает временный объект?

Разве?:

PascalString PascalString::operator+= ( const PascalString &ob )
{
    str = (char*)realloc( str, len + ob.len );
    _memcpy( str + len, ob.str, ob.len );
    len += ob.len;
    return *this;
}

Разве?:

у Вас operator[] не ссылку же возвращает, так что будет ненужное копирование, ну и получим rvalue(xvalue)

bool PascalString::operator> ( const PascalString &ob )
{
    return len > ob.len;
}

bool PascalString::operator< ( const PascalString &ob )
{
    return len < ob.len;
}

длины строк сравниваете, а не сами строки?

    PascalString PascalString::operator= ( const PascalString &ob )
{
    if ( !(*this == ob) ) //<-- Сравнение строк зачем? Может имелось ввиду this!=&ob ?
    {
        if ( str )
            free( str );
        len = ob.len;
        str = (char*)malloc( len );//Если здесь не удалось выделить память, то старая строка будет потеряна
        _memcpy( str, ob.str, len );//Здесь, да и нигде вообще, нет проверок успешного выделения памяти
    }
    return *this;//функция возвращает копию объекта, поэтому цепочка str1=str2=str3 работать не будет, ну и лишнее копирование присутствует
}

И вообще, может использовать operator new, вместо malloc, мы же всё-таки на c++ работаем?

int PascalString::size() const

const? Ок! Почему тогда

bool operator> ( const PascalString& );
bool operator< ( const PascalString& );
bool operator!= ( const PascalString& );
bool operator== ( const PascalString& );
PascalString operator+ ( const PascalString& );

без const? И где const версия для

char& operator[] ( int );

без этого Вы не сможете сравнивать и «складывать» константные строки, а так же «брать» отдельные символы константных строк.

длины строк сравниваете, а не сами строки?

да, а что ещё делать?

у Вас operator[] не ссылку же возвращает, так что будет ненужное копирование, ну и получим rvalue(xvalue)

char& PascalString::operator[] ( int index )
{
    if ( index < 0 || index >= len )
    {
        PascalStringExeption e;
        throw e;
    }
    else
        return str[index];
}

Вроде ссылку возвращает, или я что-то напутал?

/<— Сравнение строк зачем? Может имелось ввиду this!=&ob ?

Да, там имелось ввиду *this != ob, просто на момент когда я писал оператор присваивания, оператор != сравнивал количество символов, а тут сравнение нужно для того, что бы избежать присвоение строки, если строка, которой присваивают новое значение уже равна этому значению.

//функция возвращает копию объекта, поэтому цепочка str1=str2=str3 работать не будет, ну и лишнее копирование присутствует

Вот тестовый код:


    PascalString str = "This is sample from operator= and operator +=";
    PascalString str2, str3;
    str2 = "\nGreat! operator += work!";
    str3 = str += str2;
    cout << str << endl << str2 << endl <<str3 << endl;
    PascalString str4 = "This is end message";
    str = str2 = str3 = str4;

    cout << "str = " << str << endl;
    cout << "str2 = " << str2 << endl;
    cout << "str3 = " << str3 << endl;
    cout << "str4 = " << str4 << endl;

Вот что он даёт в результате:

This is sample from operator= and operator +=
Great! operator += work!

Great! operator += work!
This is sample from operator= and operator +=
Great! operator += work!
str = This is end message
str2 = This is end message
str3 = This is end message
str4 = This is end message

//Если здесь не удалось выделить память, то старая строка будет потеряна

И вообще, может использовать operator new, вместо malloc, мы же всё-таки на c++ работаем?

Да, с памятью серьёзный прокол получился. Сейчас переделаю, выложу новую версию.

malloc я использовал потому, что можно использовать realloc

Вроде ссылку возвращает, или я что-то напутал?

Это я напутал. Думал про operator+=, а написал operator[]

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

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

Вот тестовый код

ну так усложните тест:

int main()
{
    PascalString str1 = "one" ;
    PascalString str2 = "two" ;
    PascalString str3 = "three" ;
    std::string stdStr1 = "one" ;
    std::string stdStr2 = "two" ;
    std::string stdStr3 = "three" ;
    (str1=str2)=str3 ;
    std::cout << "\nTest:\n" ;
    std:: cout << str1 << std::endl ;
    std:: cout << str2 << std::endl ;
    std:: cout << str3 << std::endl ;
    std::cout << "\nStandart:\n" ;
    (stdStr1=stdStr2)=stdStr3 ;
    std::cout << stdStr1 << std::endl ;
    std::cout << stdStr2 << std::endl ;
    std::cout << stdStr3 << std::endl ;
    return 0;
}

или вот:

const PascalString & foo ( const PascalString & refString )
{
    return refString ;
}

int main()
{
    PascalString str1 = "one" ;
    PascalString str2 = "two" ;
    const PascalString & refStr1 = foo(str1=str2) ;
    std::cout << str1 << std::endl ;
    std::cout << str2 << std::endl ;
    std::cout << refStr1 << std::endl ;
}

Вышла новая версия класса PascalString :).
Исправлены:

  • Наплевательское отношение к памяти
  • Перегруженный оператор присваивания(operator=), теперь он работает как надо( по моим тестам )
  • Так же другие мелкие недочёты

Версию 3.0 можно скачать здесь.

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

В объявление класса:

    private:
        static int counter;
        int id;

        void iReg();
        void iLog(const char *);

И, соответственно, в реализацию:

#include <iostream>
#include <iomanip>

using namespace std;

#include "PascalString.h"

#include <cstdlib>

#include "memfunc.h"

int PascalString::counter = 0;

void PascalString::iReg() {
    id = counter++;
}

void PascalString::iLog(const char *comment)   {
    cout << "- [" << id << "] - \"";
    if (str == NULL)
        cout << "<NULL>";
    else
        for (int i = 0; i < len; ++i)
            if (*(str + i) >= ' ')
                cout << *(str + i);
            else
                cout << "\\x" << hex << setw(2) << setfill('0') << (int)*(str + i) << dec;

    cout << "\"(" << len << ") : " << comment << endl;
}


PascalString::PascalString(): len(0), str(NULL)
{
    iReg();
    iLog("PascalString()");
}

PascalString::PascalString( const char smb )
{
    len = 1;
    str = (char*)malloc( len );
    str[len-1] = smb;

    iReg();
    iLog("PascalString(const char)");
}

PascalString::PascalString( const char *init )
{
    len = strlen( init );
    str = (char*)malloc( len  );
    _memcpy( str, init, len );

    iReg();
    iLog("PascalString(const char *)");
}

PascalString::PascalString( const PascalString &ob )
{
    len = ob.len;
    str = (char*)malloc( len );
    _memcpy( str, ob.str, len );

    iReg();
    iLog("PascalString(const PascalString &)");
}

PascalString::~PascalString()
{
    iLog("destructor");
    if ( str )
       free ( str );
}

int PascalString::size() const
{
    return len;
}

char* PascalString::c_str( char *ret ) const
{
    _memcpy( ret, str, len );
    *(ret + len) = 0;
    return ret;
}

bool PascalString::operator> ( const PascalString &ob )
{
    return len > ob.len;
}

bool PascalString::operator< ( const PascalString &ob )
{
    return len < ob.len;
}

bool PascalString::operator!= ( const PascalString &ob )
{
    return len != ob.len;
}

bool PascalString::operator== ( const PascalString &ob )
{
    if ( *this != ob )
       return false;
    else
        return _memcmp( str, ob.str, len ); 
}


PascalString PascalString::operator+ ( const PascalString &ob )
{
    PascalString ret = *this;
    ret.str = (char*)realloc( ret.str, ret.len + ob.len );
    _memcpy( ret.str + ret.len, ob.str, ob.len );
    ret.len += ob.len;
    iLog("operator+ (const PascalString &)");
    return ret;
}

PascalString PascalString::operator+= ( const PascalString &ob )
{
    str = (char*)realloc( str, len + ob.len );
    _memcpy( str + len, ob.str, ob.len );
    len += ob.len;
    iLog("operator+= (const PascalString &)");
    return *this;
}

PascalString PascalString::operator= ( const PascalString &ob )
{
    if ( !(*this == ob) )
    {
        if ( str )
           free( str );
        len = ob.len;
        str = (char*)malloc( len );
        _memcpy( str, ob.str, len );
    }
    iLog("operator= (const PascalString &)");
    return *this;
}

char& PascalString::operator[] ( int index )
{
    if ( index < 0 || index >= len )
    {
        PascalStringExeption e;
        throw e;
    }
    else
        return str[index];
}

ostream &operator<< ( ostream& stream, const PascalString &ob )
{
    for ( int i = 0; i < ob.len; i++ )
        stream << ob.str[i];
    return stream;
}

void PascalString::get( istream &stream, int end)
{
    char read;
    *this = "";
    while ( stream >> noskipws >> read )
    {
        if ( read == end )
           break;
        *this += read;
    }
}

Переменная класса counter, посредством вызова iReg() в конструкторах, подсчитывает количество созданных объектов (деструктор не уменьшает counter!).

Переменная экземпляра класса id, посредством того же вызова iReg() в конструкторах, получает уникальную метку для каждого экземпляра.

Функция iLog() показывает потроха экземпляра класса: ид, содержимое строки, длину строки, плюс пользовательский комментарий.

UPD. В iLog() добавил манипулятор перехода в 10-чную систему после 16-ричной.

Это — так называемая «тестовая печать».

Некоторые методы класса (как правило конструкторы и операторы приведения типа) могут вызываться неявно. Тестовая печать помогает проследить что же происходит на самом деле.

porshe, по версии 3.0.

Улучшения по сравнению со второй версией очевидные. Но есть и над чем поработать.

(1) Не понял финта с перехватом исключения bad_alloc и последующим выбросом исключения PascalStringExeption. Какой смысл?

(2) Вместо

        PascalStringExeption e;
        e.error = MEMORY_OUT;
        throw e;

Можно просто написать

        throw PascalStringExeption(MEMORY_OUT);

Если добавить конструктор:

class PascalStringExeption
{
    public:
        PascalStringExeption(PascalStringError e) : error(e) {}
        PascalStringError error;
};

(3) При компиляции даёт ошибку на таком тесте:

PascalString ps0 = "ps0";
PascalString ps1;
ps1 = ps0 + " tail";    // ok
ps1 = "head " + ps0;    // ERROR!

operator+ лучше делать другом, чем членом (замечательная фраза!).

(4) Мне почему-то кажется, что ты не воспользовался советом из моего предыдущего поста по поводу отладочной печати.

Ещё могу предложить два макроса:

#define LOG(op) cout << "** executing : `" #op "` -> " << (op) << endl;
#define LOG2(op) cout << "** executing : `" #op "`" << endl; op;

// пример применения:
    LOG2(PascalString ps0);
    LOG2(PascalString ps1("const c-string"));
    LOG2(PascalString ps2("const c-string2"));
    LOG2(ps2 = ps0 = ps1);

    LOG(ps0 == ps0);
    LOG(ps0 == ps1);

Они выводят на консоль выполняемые операторы (которые передаются в качестве аргументов.

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

Лог получается примерно такой (фрагмент):


+++ initializations +++

** executing : `PascalString ps0`
- [0] - "<NULL>"(0) : PascalString()
** executing : `PascalString ps1("const c-string")`
- [1] - "const c-string"(14) : PascalString(const char*)
** executing : `PascalString ps2("const c-string2")`
- [2] - "const c-string2"(15) : PascalString(const char*)

+++ assignments +++

** executing : `ps2 = ps0 = ps1`
- [0] - "const c-string"(14) : operator= (const PascalString &)
- [2] - "const c-string"(14) : operator= (const PascalString &)
const c-string
ps2.size() = 14
- [2] - "const c-string"(14) : char& PascalString::operator[] ( int index )
ps2[4] = t
** executing : `ps2[4] = 'T'`
- [2] - "const c-string"(14) : char& PascalString::operator[] ( int index )
** executing : `cout << ps2 << endl`
consT c-string
** executing : `ps0 = "test0-"`
- [3] - "test0-"(6) : PascalString(const char*)
- [0] - "test0-"(6) : operator= (const PascalString &)
- [3] - "test0-"(6) : destructor
** executing : `ps1 = ncstring`
- [4] - "abcdefghi"(9) : PascalString(const char*)
- [1] - "abcdefghi"(9) : operator= (const PascalString &)
- [4] - "abcdefghi"(9) : destructor

Не понял финта с перехватом исключения bad_alloc и последующим выбросом исключения PascalStringExeption. Какой смысл?

Ну что бы у класса было своё исключение. Вдруг кто-нибудь попробует такой код:

PascalString str = "this is string";
int *i;
try
{
    i = new int[/*тут находится такое большое число, что память не выделится*/];
    str += "aaaaaaa...aaaa";//тут находится строка такой длинны, что память опять не выделится.

}
catch ( bad_alloc )
{
    cout << "Bad alloc exeption!" << endl;
    //Неоднозначность. Что породило исключение?
    //выделение памяти или добавление строки?

}

operator+ лучше делать другом, чем членом (замечательная фраза!).

То есть придётся создавать два оператора? Один метод, а другой дружественный?

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

Ну просто ещё не успел :). Ведь версия 3.0 ещё не знала макросов.

Кстати про макросы. Как они работают? Что выводит надпись +++ initializations +++?

UPD P.S.: Версия 4.0 в разработке, и я обещаю, там будут использоваться ваши функции и макросы( если я пойму как они работают ) для отладки :)

Создавать свой инстанс исключения на каждый чих — плохая практика.

В твоем случае, логичнее было бы использовать bad_alloc. То место, где исключение было выброшено, будет записано в traceback.

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

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

Ответить

Вы можете использовать разметку markdown для оформления комментариев и постов. Используйте функцию предпросмотра для проверки корректности разметки.

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

Либо производите оформление кода вручную, следующим образом:

``` #include <iostream> using namespace std; int main() { // ... } ```

Предпросмотр сообщения

Ваше сообщение пусто.