2014 dxdy logo

Научный форум dxdy

Математика, Физика, Computer Science, Machine Learning, LaTeX, Механика и Техника, Химия,
Биология и Медицина, Экономика и Финансовая Математика, Гуманитарные науки




Начать новую тему Ответить на тему На страницу 1, 2  След.
 
 Вопросы к профессионалам в C++.
Сообщение24.12.2005, 11:31 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
Хотелось бы узнать мнение тех, кто постоянно и давно использует C++, по следующим вопросам:

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

2) Используете ли вы в своих классах константные функции? Если да - то что, по вашему, изменилось в лучшую строну после того, как Вы их стали использовать?

3) Часто ли Вы используете множественное наследование? Сталкивались ли Вы с задачами, где использование множественного наследования было нужным и целесообразным (Мнение А.Голуба по этому вопросу мне известно, хотелось бы узнать Ваше :) ).

 Профиль  
                  
 
 
Сообщение24.12.2005, 17:02 


27/11/05
183
Северодонецк
1) Довод в пользу стековых объектов

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

2) Доводы против

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

#include <memory.h>

class Y
{

public:
void Set(void)
{
memset(m, 0, 100);
}

private:
char m[20];
};

int main(int argc, char* argv[])
{
char m1[100];
Y y;
char m2 = 2;
int i;

for(i = 0; i < 100; ++i)
m1[i] = char(i + 1);

printf("%d-%d-%d-%d-%d\n", m1[0], m1[79], m1[80], m1[99], m2);

y.Set();

printf("%d-%d-%d-%d-%d\n", m1[0], m1[79], m1[80], m1[99], m2);

return 0;
}

В результате выполнения первого printf вы получите ожидаемый результат
1-80-81-100-2

Но после отработки Set, который пишет за пределы своей памяти (размер
внутренней памяти объекта 20 байт, а вызов функции memset зацепил лишние
80 байт), в результате второй printf напечатает
0-0-81-100-2

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

 Профиль  
                  
 
 
Сообщение24.12.2005, 18:04 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
bekas писал(а):
В результате выполнения первого printf вы получите ожидаемый результат
1-80-81-100-2

Но после отработки Set, который пишет за пределы своей памяти (размер
внутренней памяти объекта 20 байт, а вызов функции memset зацепил лишние
80 байт), в результате второй printf напечатает
0-0-81-100-2

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


По-моему, это довод скорее ЗА использование стекового объекта, нежели против. Поскольку в данном случае ошибку легко локализовать: значения локальных переменных меняются сразу после вызова определенной функции-члена определенного объекта. А если подобные изменения произойдут где-то в куче и поменяют какое-нибудь поле другого объекта, причем поле, объявленное private? Да, может быть если прога написана в Borland C++ Builder, возможно там какое-нибудь исключение и вылезет при этом.. Но скажу по своему опыту: почти всегда при нарушении границ массива в куче это проявляется не сразу. А как правило, где-то совсем далеко. Например, нарушение границ массива в одном объекте вызывает исключение при вызове конструктора другого объекта. Или даже при вызове деструктора (выполнении оператора delete).

 Профиль  
                  
 
 
Сообщение24.12.2005, 21:16 
Заслуженный участник
Аватара пользователя


17/10/05
3709
:evil:
Скажу по поводу объектов на стеке (локальных объектов) vs динамические, что думаю.

1) Свойства оных понятны - автоматически размещаются, автоматически удаляются. Плюс не тратиться время на динамическое распределение памяти. Менее очевидным является то, что они участвуют в обработке исключительных ситуаций. Я надеюсь, меня поправят коли не прав, но автоматического отката new нет. Это значит, что динамически размещенный объект "провисает" и его ресурсы могут потеряться. С локальным объектом этого не происходит.

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

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

4) Основным недостатком динамических объектов является отсутствие автоматической сборки мусора в C++. Отсюда идут все smart pointer'ы и иже с ними - они считают за программиста ссылки. (Забавно, что такой smart pointer обычно суть стековый объект. Т.е. чтобы работать с динамическим, мы создаем стековый. Правда, гораздо более простой и предсказуемый.) Тема сия велика, но я считаю, что современная практика программирования уходит от доступности экземпляра объекта в программе (кроме как по ссылке). Примеров тьма: Visual Basic, Java, Python, C# (компромис C# мне особо нравиться). И если в ранние дни я удивлялся почему MFC wizard создает приложение динамически, и исправлял на создание на стеке, теперь понимаю... У "сосланных" объектов слишком много достоинств. Недостаток один - чуть больше ресурсов (памяти и времени). Зато контейнеры работают куда там STL. А с точки зрения разработчика - куда более простая модель (а значит сокращается вемя разработки и сопровождения).

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

 Профиль  
                  
 
 
Сообщение24.12.2005, 21:57 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
незванный гость писал(а):
3) С локальными объектами связанны еще пара неприятностей. Во-первых, время жизни указателя на такой объект может оказаться дольше функции (и такую ошибку ловить - большое удовольствие).

:) Сам однажды такой баг ловил (очень долго не мог понять, что происходит :) ). Дня три потратил, наверное! С тех пор старался использовать динамические объекты (до сегодняшнего дня...) Подробнее - напишу попозднее, дабы народ не забыл про другие два вопроса. Или оставшиеся два - риторические? :)

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


А энто меня даже больше устраивает. Чем раньше ошибка себя проявит - тем лучше! :)

 Профиль  
                  
 
 Re: Вопросы к профессионалам в C++.
Сообщение24.12.2005, 22:08 
Заслуженный участник
Аватара пользователя


17/10/05
3709
Sanyok писал(а):
3) Часто ли Вы используете множественное наследование? Сталкивались ли Вы с задачами, где использование множественного наследования было нужным и целесообразным (Мнение А.Голуба по этому вопросу мне известно, хотелось бы узнать Ваше :) ).


Редко. Частично потому, что вопрос о множественном наследовании решен в C++ не так, как хотелось бы. Вообще, обратите внимание, насколько сложен вопрос о наследовании в С++ стал.

Что такое "тип A наследует из B и C"? Это означает, что объект типа A является одновременно и объектом типа B и типа C. Имеет свойства обоих. То есть, свойства наследника есть объединение свойств. Множественное наследование (не virtual) задает декартово произведение свойств. Это особенно заметно, когда и B и C суть наследники D. В этом случае в A появляются два экземпляра D, что не есть хорошо. И, чтобы уйти от этих проблем, рождаются монстры иерархий объектов (ничего общего с реальной иерархией не имеющих).

Хотя мне известны примеры, когда "декартовость" работает хорошо - когда B и C задают ортогональные фасеты поведения. Например, B может описывать графические свойства, а C - задавать программную механику (управление памятью, временем жизни, и т.п.) Видел, например, в ATL.

Виртуально же наследовать каждый раз тоже тяжело - overhead мешает.

В целом, ситуация все-таки относительна редка (за исключением очень больших библиотек).

Выход Жабы мне тоже не импонирует. Имеет смысл посмотреть на Python, там сделано очень нетривиально и хорошо, но у Python динамическая модель, в которой многое проще.

 Профиль  
                  
 
 
Сообщение25.12.2005, 00:55 
Заслуженный участник
Аватара пользователя


17/10/05
3709
:evil:
Цитата:
Во вторых, уже помянутое переполнение на стеке опаснее. В большинстве систем на стеке кроме данных находиться адрес возврата. Это значит, что выход за границы объекта может привести к передаче управления в очень странные места. Сие уже вопрос security, то есть проходит по совсем другому ведомству. Там могут и бамбуковыми палками по пяткам...

Sanyok писал(а):
А энто меня даже больше устраивает. Чем раньше ошибка себя проявит - тем лучше! :)

Это если Вы поймаете. А если вирусописатели? А Ваш продукт на О(10^7) компов по всему миру? И радостные письма зараженных пользователей с мнением о компании вообще и Вас как профессионале в частности? Как и было сказано, это по другому ведомству проходит. С Секуритате, однако, шутки плохи. :D

Если Вас так пугают незамеченные ошибки в куче, есть стандартное решение - временный custom allocator. Который a) ведет список всех захваченных и возвращенных областей памяти (и тем самым, проверяет утечки памяти, двойные возвращения и т.п.) и b) контролирует (или пытается контролировать) переполнение при помощи создания буферных зон с сигнатурами. Ловит проблемы куда лучше стековых переменных, дает более осмысленную диагностику, и исчезает по мановению волшебного #define DEBUG_OFF. Ресурсы, конечно, поджирает, так при отладке же...

 Профиль  
                  
 
 
Сообщение25.12.2005, 11:37 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
незванный гость писал(а):
Если Вас так пугают незамеченные ошибки в куче, есть стандартное решение - временный custom allocator. Который a) ведет список всех захваченных и возвращенных областей памяти (и тем самым, проверяет утечки памяти, двойные возвращения и т.п.) и b) контролирует (или пытается контролировать) переполнение при помощи создания буферных зон с сигнатурами. Ловит проблемы куда лучше стековых переменных, дает более осмысленную диагностику, и исчезает по мановению волшебного #define DEBUG_OFF. Ресурсы, конечно, поджирает, так при отладке же...


Ладно, оставим Security в покое... Слава богу, я про "них" знаю только по наслышке (пока что) :). Говоря о временном custum allocator, Вы имеете в виду перегрузку операторов new и delete или что-то другое? Где нибудь в литературе есть реализация того, как это делается и внятное описание?

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

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

Класс, представляющий собой координаты точки
Код:
class TCoord
{
    int x;
    int y;
public:
    TCoord(){};
    .....
};


Класс "линия" #1:
Код:
class TLine1
{
    softarray<TCoord *> *line;
    .....
};


Класс "линия" #2:
Код:
class TLine2
{
    softarray<TCoord> *line;
    .....
};


Класс "линия" #3:
Код:
class TLine3
{
    softarray<TCoord> line;
    .....
};


softarray - это шаблон класса, который реализует "безопасные" массивы объектов того типа, который является параметром этого шаблона. Сначала я не хотел его описывать, но уж на всякий случай тоже приведу:

Код:
template <class Type>
class softarray  //Dynamic array of Type
{
public:
    softarray();                 //Конструктор
    softarray(const softarray<Type> &dv);//Конструктор копирования.
    int setlength(int length); //установить длину массива
    int add(Type el);//добавить элемент массива.
    int length(void) const;//Получить длину массива
    ~softarray();     //Деструктор
    softarray<Type> &operator = (const softarray<Type> &dv);//Присваивание
    Type &operator [](int index);//Оператор индексирования массива
    operator Type*(void) const; //Оператор преобразования указателя.
private:
    Type *ptr;
    int len;
};


Какая из реализаций класса линии удобнее? Я прихожу к выводу, что третья... Если после этого мы создадим экземпляр класса TLine3 динамически через new, то он весь (и все его поля) будет размещатся в куче, и нас не побьют палками по пяткам! :)

 Профиль  
                  
 
 
Сообщение26.12.2005, 01:35 
Заслуженный участник
Аватара пользователя


17/10/05
3709
:evil:

В С++ я принимаю решение исходя из того, кто владеет объектом.
Код:
TLIne1 l1;
TLine2 l2;
TLine3 l3;
(Sanyok определил TLine... выше ).
l3 владеет l3.line, которая в свою очередь владеет всеми координатами. l2 ссылается на l2->line, но l2->line владеет координатами. В первом случае мы имеем одни ссылки.

Различий между владением и ссыланием лежит в нескольких плоскостях:
1) время жизни подчиненного объекта всегда совпадает с временем жизни владеющего (с точностью до порядка конструкции/деструкции.
2) в нормальных условиях владеимыми объектами "не делятся", то есть на них не оздается постоянных внешних ссылок. В то же время совместное использование "сосланных объектов" является нормальной практикой. Естественно, в этом случае время жизни "сосланного объекта" должно быть не меньше всех его пользователей, а потому пользователи обычно ненесут ответственности за его создание / разрушение.

То есть, в нормальных условиях я бы выбрал третий вариант.

В С# ситуация решается несколько по другому. Владения в смысле С++ там нет (и слава Богу). Всяк объект существует по ссылке, и только по ссылке. Но в C# координаты очень может быть не стоит делать объектом. Я бы сделал их структурой, то есть "value type". В это случае они обрабатываются по модели обращения с целыми (литералами).

 Профиль  
                  
 
 
Сообщение26.12.2005, 10:17 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
Меня мучит еще один вопрос. :)
У меня была (и пока остается пока остается на вооружении) следующая практика. Пусть, к примеру, у нас есть некий объект, который управляет графиками на экране или в окне (определения для TLine см. выше):
Код:
class TGraph:
{
    softarray <TLine3 *> LinesArray;
public:
    int AddLine(TLine3 *);//Добавляем кривую.
    bool EnableLine(bool on, int linenum); //Включаем/отключаем отображение кривой.
    bool RemoveLine(int linenum);//Удаляем кривую с графика.
//Ну и конструктор с деструктором:
    TGraph();
    ~TGraph();
};

Обычно в подобных классах при добавлении элемента (в данном случае кривой) я не создавал его копии, а просто добавлял указатель в массив, т.е.
Код:
int TGraph::AddLine(TLine3 *line)
{
    return LinesArray.add(line);
}

Подобная тактика как мне казалось, себя оправдывала, поскольку объект line мог содержать несколько тысяч элементов! Добавлялся элемент примерно след. образом:
Код:
void func(TGraph *graph)
{
....
TLine3 *line = new TLine3();

....//Заполняем line элементами.

    graph->AddLine(line);
}

Пока все хоккей - line у нас не "повисает" - ссылка на него сохраняется во внутреннем массиве LinesArray, и занимаемая line память освободится при автоматическом вызове деструктора для LinesArray. Проблемы начинаются тогда, когда пользователь класса TGraph не знает ничего о его внутренней реализации (не знает, что после добавления line в graph память, занимаемую line уже нельзя освобождать).

Как быть? С одной стороны - есть правило, что память должна освобождаться там, где она выделяется. Тогда при добавлениии line надо создавать его копию (в функции AddLine), а созданный по new объект после добавления (вызова функции graph->AddLine(line)) удалять. Вроде бы это хорошо, поскольку мухи получаются отдельно от котлет (класс graph - сам по себе, функция, которая добавляет в него линию - сама по себе). С другой стороны - делать все это весьма и весьма накладно в смысле производительности и поэтому очень не хочется.

Все, что мне удалось пока придумать - это следовать такому соглашению: если некая функция создает копию объекта - она должна принимать параметр по ссылке, вот так:
Код:
AddLine(TLine3 &line);

Ежели не создает, то должна принимать параметр-указатель на объект:
Код:
AddLine(TLine3 *line);

Но такой вариант мне самому не очень нравится... :(

Интересно, как принято решать данную проблему в C++?

 Профиль  
                  
 
 
Сообщение26.12.2005, 19:43 


13/09/05
153
Москва
To Sanyok:
Если, я правильно понял, речь идет о графике, то тут по-идее нужно использовать патерн "фасад" со всеми вытекающими следствиями.
Есть график, у него есть подграфики, легенда, рамка, оси и т.д.
Ко всему доступ осуществляется через вспомогательный класс CGraph.
И тогда для добавления кривой на график мы используем что-нить типа такого
Код:
CGraph::AddCurve(int nSubplot, vector<data>& data)
{
     CSubplot* pPlot = GetSubplot(nSubplot);
     pPlot->AddCurve(data);
}
..............
CSubplot::AddCurve(vector<data>& data)
{
     CGraphCurve* pCurve = new CGraphCurve(data);
     AddCurve(pCurve);// добавляем кривую в этот подграфик
}
..............
CGraphCurve::CGraphCurve(vector<data>& data)
{
// копируем данные
............
}

Хотя можно вместо vector<data>& data использовать vector<data*>& data, но тогда где-то во внешней проге создавать данные, передавать, а потом убивать. При этом CGraphCurve::CGraphCurve(vector<data*>& data) именно создавать копии данных, т.к. они не "наши". Тогда все и получается - где создали, там и убили - и кривую, и данные.

 Профиль  
                  
 
 
Сообщение28.12.2005, 10:44 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
Не совсем понял - "паттерн" в ваших устах означает то же, что "шаблон" или что-то еще?

И еще одно:

Код:
CGraph::AddCurve(int nSubplot, vector<data>& data)
{
     CSubplot* pPlot = GetSubplot(nSubplot);
     pPlot->AddCurve(data);
}


В предложении CGraph::AddCurve(int nSubplot, vector<data>& data) data - это идентификатор типа или ссылки на экземпляр объекта? Вроде как понятно, что ссылки на экземпляр объекта, просто мне как-то непривычно в качестве параметров шаблонов видеть имена переменных, а не типов.

 Профиль  
                  
 
 
Сообщение28.12.2005, 12:29 


13/09/05
153
Москва
Патерны - это из "шаблоны" проектирования, есть такая книга "Приемы объектно-ориентированного программирования. Патерны проектирования". Есть сайт с подборкой патернов + несколько других, которых нет в книге - http://ooad.asf.ru/Patterns/ViewPattList.asp?Cat=1.

А по поводу vector<data>& data - я здесь имел ввиду, что может быть vector<СData>& data или vector<СData*>& data, просто не то написал:))

 Профиль  
                  
 
 
Сообщение28.12.2005, 13:28 
Заслуженный участник
Аватара пользователя


12/10/05
478
Казань
За книжку - спасибо! Такие книжки надо иметь в бумажном варианте! Постараюсь поискать в магазинах.. :)

Насколько я понял, в приведенном Вами примере задача решается просто - тот объект, который получается большим (объект типа CGraphCurve) является внутренним по отношению к CSubplot и CGraph и создается внутри них.
У меня же он являлся внешним. Получается, надо просто строить классы так, что бы такого не было... Т.е. ни к чему создавать метод наподобие

Код:
CSubplot::AddCurve(const CGraphCurve *curve);


- похоже это бесполезно, и более того - вредно и надо без подобных методов обходится. Но вот всегда ли такое возможно?

 Профиль  
                  
 
 
Сообщение28.12.2005, 14:14 


13/09/05
153
Москва
Сам подграфик хранит кривые - отсюда следует, что и управлять ими должен он - создавать, удалять, связывать разные объекты с кривыми и т.д. Тогда все просто получается.

А книга - просто рулит.

 Профиль  
                  
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ Сообщений: 30 ]  На страницу 1, 2  След.

Модераторы: Karan, Toucan, PAV, maxal, Супермодераторы



Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group