Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Вопрос 14
весь реальный мир состоит из объектов. Города состоят из районов, в каждом районе есть свои названия улиц, на каждой улице находятся жилые дома, которые также состоят из объектов.
Практически любой материальный предмет можно представить в виде совокупности объектов, из которых он состоит. Допустим, что нам нужно написать программу для учета успеваемости студентов. Можно представить группу студентов, как класс языка C++. Назовем его Students.
class Students {
// Имя студента
std::string name;
// Фамилия
std::string last_name;
// Пять промежуточных оценок студента
int scores[5];
// Итоговая оценка за семестр
float average_ball;
};
Основные понятия
Классы в программировании состоят из свойств и методов. Свойства это любые данные, которыми можно характеризовать объект класса. В нашем случае, объектом класса является студент, а его свойствами имя, фамилия, оценки и средний балл.
У каждого студента есть имя name и фамилия last_name . Также, у него есть промежуточные оценки за весь семестр. Эти оценки мы будем записывать в целочисленный массив из пяти элементов. После того, как все пять оценок будут проставлены, определим средний балл успеваемости студента за весь семестр свойство average_ball.
Методы это функции, которые могут выполнять какие-либо действия над данными (свойствами) класса. Добавим в наш класс функцию calculate_average_ball(), которая будет определять средний балл успеваемости ученика.
class Students {
public:
// Функция, считающая средний балл
void calculate_average_ball()
{
int sum = 0; // Сумма всех оценок
for (int i = 0; i < 5; ++i) {
sum += scores[i];
}
// считаем среднее арифметическое
average_ball = sum / 5.0;
}
// Имя студента
std::string name;
// Фамилия
std::string last_name;
// Пять промежуточных оценок студента
int scores[5];
private:
// Итоговая оценка за семестр
float average_ball;
};
Функция calculate_average_ball() просто делит сумму всех промежуточных оценок на их количество.
Модификаторы доступа public и private
Все свойства и методы классов имеют права доступа. По умолчанию, все содержимое класса является доступным для чтения и записи только для него самого. Для того, чтобы разрешить доступ к данным класса извне, используют модификатор доступа public. Все функции и переменные, которые находятся после модификатора public, становятся доступными из всех частей программы.
Закрытые данные класса размещаются после модификатора доступа private:. Если отсутствует модификатор public, то все функции и переменные, по умолчанию являются закрытыми (как в первом примере).
Обычно, приватными делают все свойства класса, а публичными его методы. Все действия с закрытыми свойствами класса реализуются через его методы. Рассмотрим следующий код.
class Students {
public:
// Установка среднего балла
void set_average_ball(float ball)
{
average_ball = ball;
}
// Получение среднего балла
float get_average_ball()
{
return average_ball;
}
std::string name;
std::string last_name;
int scores[5];
private:
float average_ball;
};
Мы не можем напрямую обращаться к закрытым данными класса. Работать с этими данными можно только посредством методов этого класса. В примере выше, мы используем функциюget_average_ball() для получения средней оценки студента, и set_average_ball() для выставления этой оценки.
Функция set_average_ball() принимает средний балл в качестве параметра и присваивает его значение закрытой переменной average_ball. Функция get_average_ball() просто возвращает значение этой переменной.
Программа учета успеваемости студентов
Создадим программу, которая будет заниматься учетом успеваемости студентов в группе. Создайте заголовочный файл students.h, в котором будет находиться класс Students.
/* students.h */
#include <string>
class Students {
public:
// Установка имени студента
void set_name(std::string student_name)
{
name = student_name;
}
// Получение имени студента
std::string get_name()
{
return name;
}
// Установка фамилии студента
void set_last_name(std::string student_last_name)
{
last_name = student_last_name;
}
// Получение фамилии студента
std::string get_last_name()
{
return last_name;
}
// Установка промежуточных оценок
void set_scores(int student_scores[])
{
for (int i = 0; i < 5; ++i) {
scores[i] = student_scores[i];
}
}
// Установка среднего балла
void set_average_ball(float ball)
{
average_ball = ball;
}
// Получение среднего балла
float get_average_ball()
{
return average_ball;
}
private:
// Промежуточные оценки
int scores[5];
// Средний балл
float average_ball;
// Имя
std::string name;
// Фамилия
std::string last_name;
};
Мы добавили в наш класс новые методы, а также сделали приватными все его свойства. Функция set_name() сохраняет имя студента в переменной name, а get_name() возвращает значение этой переменной. Принцип работы функций set_last_name() и get_last_name() аналогичен.
Функция set_scores() принимает массив с промежуточными оценками и сохраняет их в приватную переменную int scores[5].
Теперь создайте файл main.cpp со следующим содержимым.
/* main.cpp */
#include <iostream>
#include "students.h"
int main()
{
// Создание объекта класса Student
Students student;
std::string name;
std::string last_name;
// Ввод имени с клавиатуры
std::cout << "Name: ";
getline(std::cin, name);
// Ввод фамилии
std::cout << "Last name: ";
getline(std::cin, last_name);
// Сохранение имени и фамилии в объект класса Students
student.set_name(name);
student.set_last_name(last_name);
// Оценки
int scores[5];
// Сумма всех оценок
int sum = 0;
// Ввод промеждуточных оценок
for (int i = 0; i < 5; ++i) {
std::cout << "Score " << i+1 << ": ";
std::cin >> scores[i];
// суммирование
sum += scores[i];
}
// Сохраняем промежуточные оценки в объект класса Student
student.set_scores(scores);
// Считаем средний балл
float average_ball = sum / 5.0;
// Сохраняем средний балл в объект класса Students
student.set_average_ball(average_ball);
// Выводим данные по студенту
std::cout << "Average ball for " << student.get_name() << " "
<< student.get_last_name() << " is "
<< student.get_average_ball() << std::endl;
return 0;
}
В самом начале программы создается объект класса Students. Дело в том, что сам класс является только описанием его объекта. Класс Students является описанием любого из студентов, у которого есть имя, фамилия и возможность получения оценок.
Объект класса Students характеризует конкретного студента. Если мы захотим выставить оценки всем ученикам в группе, то будем создавать новый объект для каждого из них. Использование классов очень хорошо подходит для описания объектов реального мира.
После создания объекта student, мы вводим с клавиатуры фамилию, имя и промежуточные оценки для конкретного ученика. Пускай это будет Вася Пупкин, у которого есть пять оценок за семестр две тройки, две четверки и одна пятерка.
Введенные данные мы передаем set-функциям, которые присваивают их закрытым переменным класса. После того, как были введены промежуточные оценки, мы высчитываем средний балл на основе этих оценок, а затем сохраняем это значение в закрытом свойстве average_ball, с помощью функции set_average_ball().
Скомпилируйте и запустите программу.
Отделение данных от логики
Вынесем реализацию всех методов класса в отдельный файл students.cpp.
/* students.cpp */
#include <string>
#include "students.h"
// Установка имени студента
void Students::set_name(std::string student_name)
{
Students::name = student_name;
}
// Получение имени студента
std::string Students::get_name()
{
return Students::name;
}
// Установка фамилии студента
void Students::set_last_name(std::string student_last_name)
{
Students::last_name = student_last_name;
}
// Получение фамилии студента
std::string Students::get_last_name()
{
return Students::last_name;
}
// Установка промежуточных оценок
void Students::set_scores(int scores[])
{
for (int i = 0; i < 5; ++i) {
Students::scores[i] = scores[i];
}
}
// Установка среднего балла
void Students::set_average_ball(float ball)
{
Students::average_ball = ball;
}
// Получение среднего балла
float Students::get_average_ball()
{
return Students::average_ball;
}
А в заголовочном файле students.h оставим только прототипы этих методов.
/* students.h */
#pragma once /* Защита от двойного подключения заголовочного файла */
#include <string>
class Students {
public:
// Установка имени студента
void set_name(std::string);
// Получение имени студента
std::string get_name();
// Установка фамилии студента
void set_last_name(std::string);
// Получение фамилии студента
std::string get_last_name();
// Установка промежуточных оценок
void set_scores(int []);
// Установка среднего балла
void set_average_ball(float);
// Получение среднего балла
float get_average_ball();
private:
// Промежуточные оценки
int scores[5];
// Средний балл
float average_ball;
// Имя
std::string name;
// Фамилия
std::string last_name;
};
Такой подход называется абстракцией данных одного из фундаментальных принципов объектно-ориентированного программирования. К примеру, если кто-то другой захочет использовать наш класс в своем коде, ему не обязательно знать, как именно высчитывается средний балл. Он просто будет использовать функцию calculate_average_ball() из второго примера, не вникая в алгоритм ее работы.
Над крупными проектами обычно работает несколько программистов. Каждый из них занимается написанием определенной части продукта. В таких масштабах кода, одному человеку практически нереально запомнить, как работает каждая из внутренних функций проекта. В нашей программе, мы используем оператор потокового вывода cout, не задумываясь о том, как он реализован на низком уровне. Кроме того, отделение данных от логики является хорошим тоном программирования.
В начале обучения мы говорили о пространствах имен (namespaces). Каждый класс в C++ использует свое пространство имен. Это сделано для того, чтобы избежать конфликтов при именовании переменных и функций. В файле students.cpp мы используем оператор принадлежности :: перед именем каждой функции. Это делается для того, чтобы указать компилятору, что эти функции принадлежат классуStudents.
Создание объекта через указатель
При создании объекта, лучше не копировать память для него, а выделять ее в в куче с помощью указателя. И освобождать ее после того, как мы закончили работу с объектом. Реализуем это в нашей программе, немного изменив содержимое файла main.cpp.
/* main.cpp */
#include <iostream>
#include "students.h"
int main()
{
// Выделение памяти для объекта Students
Students *student = new Students;
std::string name;
std::string last_name;
// Ввод имени с клавиатуры
std::cout << "Name: ";
getline(std::cin, name);
// Ввод фамилии
std::cout << "Last name: ";
getline(std::cin, last_name);
// Сохранение имени и фамилии в объект класса Students
student->set_name(name);
student->set_last_name(last_name);
// Оценки
int scores[5];
// Сумма всех оценок
int sum = 0;
// Ввод промежуточных оценок
for (int i = 0; i < 5; ++i) {
std::cout << "Score " << i+1 << ": ";
std::cin >> scores[i];
// суммирование
sum += scores[i];
}
// Сохраняем промежуточные оценки в объект класса Student
student->set_scores(scores);
// Считаем средний балл
float average_ball = sum / 5.0;
// Сохраняем средний балл в объект класса Students
student->set_average_ball(average_ball);
// Выводим данные по студенту
std::cout << "Average ball for " << student->get_name() << " "
<< student->get_last_name() << " is "
<< student->get_average_ball() << std::endl;
// Удаление объекта student из памяти
delete student;
return 0;
}
При создании статического объекта, для доступа к его методам и свойствам, используют операция прямого обращения «.» (символ точки). Если же память для объекта выделяется посредством указателя, то для доступа к его методам и свойствам используется оператор косвенного обращения «->».
Конструктор и деструктор класса
Конструктор класса это специальная функция, которая автоматически вызывается сразу после создания объекта этого класса. Он не имеет типа возвращаемого значения и должен называться также, как класс, в котором он находится. По умолчанию, заполним двойками массив с промежуточными оценками студента.
class Students {
public:
// Конструктор класса Students
Students(int default_score)
{
for (int i = 0; i < 5; ++i) {
scores[i] = default_score;
}
}
private:
int scores[5];
};
int main()
{
// Передаем двойку в конструктор
Students *student = new Students(2);
return 0;
}
Мы можем исправить двойки, если ученик будет хорошо себя вести, и вовремя сдавать домашние задания. А на «нет» и суда нет :-)
Деструктор класса вызывается при уничтожении объекта. Имя деструктора аналогично имени конструктора, только в начале ставится знак тильды ~. Деструктор не имеет входных параметров.
#include <iostream>
class Students {
public:
// Деструктор
~Students()
{
std::cout << "Memory has been cleaned. Good bye." << std::endl;
}
};
int main()
{
Students *student = new Students;
// Уничтожение объекта
delete student;
return 0;
}
Модификаторы уровня доступа определяют, могут ли другие классы использовать определенное поле или вызвать определенный метод. Есть два уровня управления доступом:
class может быть объявлен с модификатором public, когда, что class видим ко всем классам всюду. Если у class нет никакого модификатора (значение по умолчанию, также известное как частный на пакет), это видимо только в пределах его собственного пакета (пакеты называют группами связанных классов Вы узнаете о них в более позднем уроке.)
На задействованном уровне можно также использовать public модификатор или никакой модификатор (частный на пакет) так же, как с высокоуровневыми классами, и с тем же самым значением. Для элементов есть два дополнительных модификатора доступа: private и protected. private модификатор определяет, что к элементу можно только получить доступ в его собственном class. protected модификатор определяет, что к элементу может только получить доступ в пределах его собственного пакета (как с частным на пакет) и, кроме того, подкласс его class в другом пакете.
Следующая таблица показывает доступ к элементам, разрешенным каждым модификатором.
Уровни доступа |
||||
Модификатор |
Класс |
Пакет |
Подкласс |
Мир |
public |
Y |
Y |
Y |
Y |
protected |
Y |
Y |
Y |
N |
никакой модификатор |
Y |
Y |
N |
N |
private |
Y |
N |
N |
N |
Первый столбец данных указывает, есть ли у самого class доступ к элементу, определенному уровнем доступа. Как можно видеть, у class всегда есть доступ к его собственным элементам. Второй столбец указывает, есть ли у классов в том же самом пакете как class (независимо от их происхождения) доступ к элементу. Третий столбец указывает, есть ли у подклассов class, объявленного вне этого пакета, доступ к элементу. Четвертый столбец указывает, есть ли у всех классов доступ к элементу.
Уровни доступа влияют на Вас двумя способами. Во-первых, когда Вы используете классы, которые прибывают из другого источника, такого как классы в платформе Java, уровни доступа определяют, какие элементы тех классов Ваши собственные классы могут использовать. Во-вторых, когда Вы пишете class, Вы должны решить, какой уровень доступа каждая задействованная переменная и каждый метод в Вашем class должны иметь.
Давайте смотреть на набор классов и видеть, как уровни доступа влияют на видимость. Следующие данные показывают эти четыре класса в этом примере и как они связываются.
Следующая таблица показывает, где элементы Альфы, class видим для каждого из модификаторов доступа, которые могут быть применены к ним.
Видимость |
||||
Модификатор |
Альфа |
Бета |
Alphasub |
Гамма |
public |
Y |
Y |
Y |
Y |
protected |
Y |
Y |
Y |
N |
никакой модификатор |
Y |
Y |
N |
N |
private |
Y |
N |
N |
N |
Подсказки относительно Выбора Уровня доступа:
Если другие программисты используют Ваш class, Вы хотите гарантировать, что ошибки от неправильного употребления не могут произойти. Уровни доступа могут помочь Вам сделать это.
Статистические элементы классов
До настоящего момента каждый создаваемый вами объект имел свой собственный набор элементов данных. В зависимости от назначения вашего приложения могут быть ситуации, когда объекты одного и того же класса должны совместно использовать один или несколько элементов данных. Например, предположим, что вы пишете программу платежей, которая отслеживает рабочее время для 1000 служащих. Для определения налоговой ставки программа должна знать условия, в которых работает каждый служащий. Пусть для этого используется переменная класса state_of_work. Однако, если все служащие работают в одинаковых условиях, ваша программа могла бы совместно использовать этот элемент данных для всех объектов типа employee. Таким образом, ваша программа уменьшает необходимое количество памяти, выбрасывая 999 копий одинаковой информации. Для совместного использования элемента класса вы должны объявить этот элемент как static (статический). Этот урок рассматривает шаги, которые вы должны выполнить для совместного использования элемента класса несколькими объектами. К концу этого урока вы освоите следующие основные концепции:
СОВМЕСТНОЕ ИСПОЛЬЗОВАНИЕ ЭЛЕМЕНТА ДАННЫХ
Обычно, когда вы создаете объекты определенного класса, каждый объект получает свой собственный набор элементов данных. Однако возможны такие ситуации, при которых объектам одного и того же класса необходимо совместно использовать один или несколько элементов данных (статические элементы данных). В таких случаях объявляйте элементы данных как общие или частные, а затем предваряйте тип ключевым словом static, как показано ниже:
private:
static int shared_value;
После объявления класса вы должны объявить элемент как глобальную переменную вне класса, как показано ниже:
int class_name::shared_value;
Следующая программа SHARE_IT.CPP определяет класс book_series, совместно использующий элемент page_count, который является одинаковым для всех объектов (книг) класса (серии). Если программа изменяет значение этого элемента, изменение сразу же проявляется во всех объектах класса:
#include <iostream.h>
#include <string.h>
class book_series
{
public:
book_series(char *, char *, float);
void show_book(void);
void set_pages(int) ;
private:
static int page_count;
char title[64];
char author[ 64 ];
float price;
};
int book_series::page__count;
void book_series::set_pages(int pages)
{
page_count = pages;
}
book_series::book_series(char *title, char *author, float price)
{
strcpy(book_series::title, title);
strcpy(book_series::author, author);
book_series::price = price;
}
void book_series:: show_book (void)
{
cout << "Заголовок: " << title << endl;
cout << "Автор: " << author << endl;
cout << "Цена: " << price << endl;
cout << "Страницы: " << page_count << endl;
}
void main(void)
{
book_series programming( "Учимся программировать на C++", "Jamsa", 22.95);
book_series word( "Учимся работать с Word для Windows", "Wyatt", 19.95);
word.set_pages(256);
programming.show_book ();
word.show_book() ;
cout << endl << "Изменение page_count " << endl;
programming.set_pages(512);
programming.show_book();
word.show_book();
}
Как видите, класс объявляет page_count как static int. Сразу же за определением класса программа объявляет элемент page_count как глобальную переменную. Когда программа изменяет элемент page_count, изменение сразу же проявляется во всех объектах классаbook_series.
Совместное использование элементов класса
В зависимости от вашей программы возможны ситуации, когда вам потребуется совместно использовать определенные данные несколькими экземплярами объекта. Для этого объявите такой элемент как static. Далее объявите этот элемент вне класса как глобальную переменную. Любые изменения, которые ваша программа делает с этим элементом, будут немедленно отражены в объектах данного класса.
Как вы только что узнали, при объявлении элемента класса как static этот элемент совместно используется всеми объектами данного класса. Однако возможны ситуации, когда программа еще не создала объект, но ей необходимо использовать элемент. Для использования элемента ваша программа должна объявить его как public и static. Например, следующая программа USЕ_MBR.CPP использует элемент page_count из класса book_series,даже если объекты этого класса не существуют:
#include <iostream.h>
#include <string.h>
class book_series
{
public:
static int page_count;
private:
char title [64];
char author[64];
float price;
};
int book_series::page_count;
void main(void)
{
book_series::page_count = 256;
cout << "Текущее значение page_count равно " << book_series::page_count << endl;
}
В данном случае, поскольку класс определяет элемент класса page_count как public,программа может обратиться к этому элементу класса, даже если объекты класса book_seriesне существуют.
Предыдущая программа иллюстрировала использование статических элементов данных. Подобным образом C++ позволяет вам определить статические функции-элементы (методы). Если вы создаете статический метод, ваша программа может вызывать такой метод, даже если объекты не были созданы. Например, если класс содержит метод, который может быть использован для данных вне класса, вы могли бы сделать этот методстатическим. Ниже приведен класс menu, который использует esc-последовательность драйвера ANSI для очистки экрана дисплея. Если в вашей системе установлен драйвер ANSI.SYS, вы можете использовать метод clear_screen для очистки экрана. Поскольку этот метод объявлен как статический, программа может использовать его, даже если объекты типа menu не существуют. Следующая программа CLR_SCR.CPP использует методclear_screen для очистки экрана дисплея:
#include <iostream.h>
class menu
{
public:
static void clear_screen(void);
// Здесь должны быть другие методы
private:
int number_of_menu_options;
};
void menu::clear_screen(void)
{
cout << "33" << "[2J";
}
void main(void)
{
menu::clear_screen();
}
Так как программа объявляет элемент clear_screen как статический, она может использовать эту функцию для очистки экрана, даже если объекты типа menu не существуют. Функция clear_screen использует esc-последовательность ANSI Esc[2J для очистки экрана.
Использование в ваших программах методов класса
По мере создания методов класса возможны ситуации, когда функция, созданная вами для использования классом, может быть полезна для операций вашей программы, которые не включают объекты класса. Например, в классе menu была определена функция clear_screen, которую вы, возможно, захотите использовать в программе. Если ваш класс содержит метод, который вы захотите использовать вне объекта класса, поставьте перед его прототипом ключевое слово static и объявите этот метод как public:
public:
static void clear_screen(void);
Внутри вашей программы для вызова такой функции используйте оператор глобального разрешения, как показано ниже:
Стековые и динамические объекты
Иногда мне кажется, что C++ лучше изучать без предварительного знакомства с C. В C++ часто используются те же термины, что и в С, но за ними кроются совершенно иной смысл и правила применения. Например, возьмем примитивный целый тип.
int x = 17;
В C++ это будет экземпляр встроенного «класса» int. В С это будет... просто int. Встроенные классы имеют свои конструкторы. У класса int есть конструктор с одним аргументом, который инициализирует объект передаваемым значением. Теоретически существует и деструктор, хотя он ничего не делает и ликвидируется всеми нормальными разработчиками компиляторов в процессе оптимизации. Важно осознать, что встроенные типы за очень редкими исключениями подчиняются тем же базовым правилам, что и ваши расширенные типы.
Вы должны понимать эту теоретическую особенность C++, чтобы правильно относиться к стековым и динамическим объектам и связанным с ними переменным.
Размещение в стеке
Чтобы выделить память для стековой переменной в области действия блока, достаточно просто объявить ее обычным образом.
{
int i;
foo f(constructor_args);
// Перед выходом из блока вызываются деструкторы i и f
}
Стековые объекты существуют лишь в границах содержащего их блока. При выходе за его пределы автоматически вызывается деструктор. Разумеется, получение адреса стекового объекта - дело рискованное, если только вы абсолютно, стопроцентно не уверены, что этот указатель не будет использован после выхода за пределы области действия объекта. Все фрагменты наподобие приведенного ниже всегда считаются потенциально опасными:
{
int i;
foo f;
SomeFunction(&f);
}
Без изучения функции SomeFunction невозможно сказать, безопасен ли этот фрагмент.
SomeFunction может передать адрес дальше или сохранить его в какой-нибудь переменной, а по закону Мэрфи этот адрес наверняка будет использован уже после уничтожения объекта f. Даже если сверхтщательный анализ SomeFunction покажет, что адрес не сохраняется после вызова, через пару лет какой-нибудь новый программист модифицирует SomeFunction, продлит существование адреса на пару машинных команд и - БУМ!!! Лучше полностью исключить такую возможность и не передавать адреса стековых объектов.
Динамическое размещение
Чтобы выделить память для объекта в куче (heap), воспользуйтесь оператором new new.
foo* f = new foo(constructor_args);
Вроде бы все просто. Оператор new выделяет память и вызывает соответствующий конструктор на основании переданных аргументов. Но когда этот объект уничтожается? Подробный ответ на этот вопрос займет примерно треть книги, но я не буду вдаваться в технические детали и отвечу так: «Когда кто-нибудь вызовет оператор delete для его адреса». Сам по себе объект из памяти не удалится; вы должны явно сообщить своей программе, когда его следует уничтожить.
Указатели и ссылки
Попытки связать указатели с динамическими объектами часто приводят к недоразумениям. В сущности, они не имеют друг с другом ничего общего. Вы можете получить адрес стекового объекта и выполнить обратное преобразование, то есть разыменование (dereferencing) адреса динамического объекта. И на то, и на другое можно создать ссылку.
{
foo f;
foo* p = &f;
f.MemberFn(); // Использует сам объект
p->MemberFn(); // Использует его адрес
p = new foo;
foo& r = *p; // Ссылка на объект
r.MemberFn(); // То же, что и p->MemberFn()
}
Как видите, выбор оператора . или -> зависит от типа переменной и не имеет отношения к атрибутам самого объекта. Раз уж мы заговорили об этом, правильные названия этих операторов (. и ->) - селекторы членов класса (member selectors). Если вы назовете их «точкой» или «стрелкой» на семинаре с коктейлями, наступит гробовая тишина, все повернутся и презрительно посмотрят на вас, а в дальнем углу кто-нибудь выронит свой бокал.
Недостатки стековых объектов
Если использовать оператор delete для стекового объекта, то при большом везении ваша программа просто грохнется. А если вам (как и большинству из нас) не повезет, то программа начнет вести себя, как ревнивая любовница - она будет вытворять, всякие гадости в разных местах памяти, но не скажет, на что же она разозлилась. Дело в том, что в большинстве реализаций C++ оператор new записывает пару скрытых байтов перед возвращаемым адресом. В этих байтах указывается размер выделенного блока. По ним оператор delete определяет, сколько памяти за указанным адресом следует освободить.
При выделении памяти под стековые объекты оператор new не вызывается, поэтому эти
дополнительные данные отсутствуют. Если вызвать оператор delete для стекового объекта, он возьмет содержимое стека над вашей переменной и интерпретирует его как размер освобождаемого блока.
Итак, мы знаем по крайней мере две причины, по которым следует избегать стековых объектов - если у вас нет действительно веских доводов в их пользу:
1. Адрес стекового объекта может быть сохранен и использован после выхода за границы области действия объекта.
2. Адрес стекового объекта может быть передан оператору delete.
Следовательно, для стековых объектов действует хорошее правило: Никогда не получайте их адреса или адреса их членов.
Достоинства стековых объектов С другой стороны, память в стеке выделяется с головокружительной быстротой - так же быстро, как компилятор выделяет память под другие автоматические переменные (скажем, целые). Оператор new (по крайней мере, его стандартная версия) тратит несколько тактов на то, чтобы решить, откуда взять блок памяти и где оставить данные для его последующего освобождения. Быстродействие - одна из веских причин в пользу выделения памяти из стека. Как вы вскоре убедитесь, существует немало способов ускорить работу оператора new, так что эта причина менее важна, чем может показаться с первого взгляда.
Автоматическое удаление - второе большое преимущество стековых объектов, поэтому
программисты часто создают маленькие вспомогательные стековые классы, которые играют роль «обертки» для динамических объектов. В следующем забавном примере динамический класс Foo «упаковывается» в стековый класс PFoo. Конструктор выделяет память для Foo; деструктор освобождает ее. Если вы незнакомы с операторами преобразования, обратитесь к соответствующему разделу этой главы. В двух словах, функция operator Foo*() позволяет использовать класс PFoo везде,
где должен использоваться Foo* - например, при вызове функции g().
class PFoo {
private:
Foo* f;
public:
PFoo() : f(new Foo) {}
~PFoo() { delete f; }
operator Foo*() { return f; }
}
void g(Foo*);
{
PFoo p;
g(p); // Вызывает функцию operator Foo*() для преобразования
// Уничтожается p, а за ним - Foo
}
Обратите внимание, что этот класс не совсем безопасен, поскольку адрес, возвращаемый функцией operator Foo*(), становится недействительным после удаления вмещающего PFoo. Мы разберемся с этим чуть позже.
Мы еще не раз встретимся с подобными фокусами. Вся соль заключается в том, что стековые объекты могут пригодиться просто из-за того, что их не приходится удалять вручную. Вскоре я покажу вам, как организовать автоматическое удаление динамических объектов, но эта методика очень сложна и вряд ли пригодна для повседневного применения.
У стековых объектов есть еще одно преимущество - если ваш компилятор поддерживает ANSI-совместимую обработку исключений (exception). Когда во время раскрутки стека происходит исключение, деструкторы стековых объектов вызываются автоматически. Для динамических объектов этого не случается, и ваша куча может превратиться в настоящий хаос.