Панель управления
Оплаченные ссылки
Главная » C++ энциклопедия » С » Сколько плюсов у C++ ?
Категория: C++ энциклопедия » С
- 85
Автор: admin
Обзор возможностей языка
Уже долгое время не прекращаются споры, что лучше: Delphi или C/C++/Visual C++. Причем в большинстве случаев сравниваются две принципиально разные вещи. Ведь до седьмой версии имя Delphi носила лишь среда разработки, а язык ее компилятора был Object Pascal (в Delphi7 борландовцы решили устранить такое упущение, и теперь и язык называется Дельфи). То же самое и с Visual C++: эта IDE "накручена" на Microsoft C/C++ Compiler (cl.exe). Поэтому корректным было бы сопоставление Delphi и Visual Studio или объектного паскаля и C++. Почему именно "си-плюс-плюс", а не C? Да потому что C - процедурно-ориентированный язык "среднего уровня", а Object Pascal - высокоуровневый, с поддержкой ООП и абстракций, т.е. это совсем разные вещи. Такая путаница в понятиях заставляет многих думать, что и C, и C++ - почти одно и то же, а ведь это совсем разные языки. Не будем погружаться в дебри Си: тут все довольно понятно, посмотрим лучше, что за зверь C++.
Что такое C++?
С этим вопросом лучше обратиться к его создателю - Бьерну Страуструпу. Думаю, он бы ответил примерно так: С++ - это язык, который лучше, чем C поддерживает абстракцию данных, объектно-ориентированное (ООП) и обобщенное программирование. В том, что все это означает, мы и будем разбираться.
Прежде всего замечу, что C++ разрабатывался с нуля с целью добавления новых средств к стандартному C. Теперь, надеюсь, понятно, почему он лучше? :) В то же время, тяжелого и убогого уродца создатели делать не хотели, поэтому они руководствовались очевидными принципами: эстетика (все должно быть понятно и элегантно), минимализм (поддержка какого-либо средства не должна вызывать дополнительных расходов в не использующих его программах) и т.п.
Важно и само понятие поддержки стиля программирования. Можно говорить, что язык поддерживают какой-то стиль, когда использование этого стиля в нем удобно, просто и эффективно. При написании объектно-ориентированной программы на C (такое тоже возможно) непередаваемые ощущения заработанного геморроя обеспечены. Поэтому C лишь предоставляет возможность использовать стиль ООП, но не поддерживает его.
Процедуры и функции
Рассмотрим непосредственно различные техники программирования и их реализации в С++. Начнем, конечно, с процедурной, поскольку она является прародителем всех остальных.
Основной принцип процедурного программирования гласит: "Реши, какие понадобятся процедуры, и используй наилучшие алгоритмы". Поддержка языком этой техники означает возможность передачи функции аргумента и возможности возврата функциями значений. Залог успеха при написании программы - подобрать оптимальные алгоритмы обработки данных и, чтобы не запутаться в них, разбить код на процедуры и функции.
Здесь C++ очень похож на C. Те же инструкции ветвления и циклы, такое же объявление переменных (правда, возможное в любом месте программы), указателей и массивов, множество встроенных типов и т.д. Хотя нововведений тоже немало: ссылки (&), операторы ввода-вывода (>> И <<), операторы для работы с памятью (new и delete), встроенный тип bool и т.д. Стоит упомянуть и обработку исключений. Вот пример функции, которая реализует вежливое, но настойчивое приглашение пользователю выйти :) ("//" - открывает комментарий до конца строки).
Модули и пространства имен
Процедурное программирование - основа основ, его стали применять в первых программах для первых ЭВМ. Но ничто не стоит на месте: сложность программ росла, и со временем важным вопросом стала организация данных. Так появились модули - набор процедур вместе с данными, которые они обрабатывают. Стал актуален принцип сокрытия: "организуй код в модулях так, чтобы скрыть в них данные".
На первый взгляд не совсем понятно, что и зачем нужно скрывать. Ответ прост: пользователю функций (хотя это такой же программер, как и их разработчик, назовем его так) не интересно, как они работают, для него главное, чтобы они действительно работали. Поэтому разработчик предоставляет пользователю некий интерфейс (пользовательский) - все, что необходимо для вызова данного набора функций (модуля). Реализация же этих функций не видна пользователю - она скрыта. По этому принципу построено большинство библиотек (например, WinAPI, где код функций находится в системных dll'ках, а программисты знают о них из заголовочных файлов типа windows.h).
На самом деле модульное программирование не новость и для С-кодеров, но они вынуждены обходиться простой раздельной компиляцией (несколько .c-файлов) и заголовочными .h-файлами. Все объявленные переменные в хидерных файлах оставались по-прежнему глобальными, и к ним можно было легко обратиться из любого места программы. В С++ появилась такая полезная вещь, как пространства имен (namespaces). Объявляя пространство имен, ты, по сути, "ограничиваешь область видимости" всему, что находится внутри него. А внутри может находиться любое объявление. Например, у нас есть пространство имен A, содержащее переменную c == 100, и есть глобальная переменная c == 0. Тогда функции f() и g() выведут 100, а h() - 0:
Здесь A::c означает, что c берется из пространства имен A. Если бы мы захотели обратиться к глобальной с из функции f (), нам пришлось бы использовать квалификатор глобального namespace'а:
Теперь нетрудно догадаться, как реализовать сокрытие данных с помощью пространств имен. Рассмотрим модуль "строка символов". На самом деле в реальных программах так делать не надо :), это лишь наглядный пример. Сначала объявим пользовательский интерфейс.
Получился довольно примитивный модуль :). Теперь посмотрим на его реализацию.
Теперь пользователю достаточно заинклудить mystring.h, и можно пользоваться нашей строкой:
Абстракция данных
Используя модуль, описанный выше, ты в какой-то момент столкнешься с проблемой реализации нескольких таких строк. Действительно, трудно представить ситуацию, где достаточно одной подобной строки. Результатом долгих и тяжелых экспериментов над нашим модулем-строкой станет некое подобие типа данных, "псевдо тип" строка. Как это чудо сделать - описывать не буду, потому что такое решение проблемы далеко от идеала. На этот случай C++ припас свое решение - возможность определения типов, которые ведут себя почти как встроенные. Такие типы называются абстрактными или типами, определяемыми пользователем (пользовательскими). Просвещенные товарищи, знакомые с ООП, думаю, уже поняли, о чем речь.
При работе с пользовательскими типами следует руководствоваться принципом "реши, какие потребуются типы, и обеспечь полный набор операций над ними". В контексте типа, определяемого пользователем, наша строка будет выглядеть примерно так:
Этот листинг демонстрирует объявление класса - пользовательского типа. Наш класс реализует строку и несколько операций над строками. По умолчанию все члены класса являются закрытыми (private), то есть доступ к ним имеют только функции-члены этого класса. Элементы, объявленные как "public", общедоступны. Сами функции-члены определяются примерно так:
Функция-член, имеющая то же название, что и класс, называется конструктором. Конструкторы помогают по-разному инициализировать объекты класса - конкретные переменные. Обычно в них выделяется необходимая память, инициализируются переменные и т.п. В MyString, как видно из листинга, два конструктора: один по умолчанию, другой преобразует C-строку в "нашенскую". Пользоваться классом MyString можно так:
Удобно? Вполне! Пользовательские типы предоставляют огромные возможности и сильно упрощают нелегкий труд программиста при решении самых разных задач, ведь операции над их объектами ничем не отличаются от операций над переменными встроенных типов (int, char и т.д.). Типы, подобные MyString, принято называть конкретными типами.
Однако в типе MyString потеряно одно свойство, которым обладал модуль MyString - реализация не отделена от интерфейса. Конечно, представление строки закрыто (private), но, тем не менее, оно "видно" пользователю. И при изменении реализации строки, программеру-юзеру класса придется перекомпилировать весь свой код. Это не есть гуд. Но что поделаешь, с конкретными типами мы хотим работать как со встроенными, и тут по-другому никак.
Если же реализацию необходимо отделить от представления, надо пользоваться абстрактными типами. Тогда интерфейс будет примерно такой:
Модификатор "virtual" означает "может быть переопределено в производном классе", а "=0" значит, что эта функция ДОЛЖНА быть переопределена в будущем. Конечно, я привел совсем бредовый пример: идея сделать виртуальной функцию, возвращающую длину строки, может родиться только в воспаленном мозгу :). Но смысл, думаю, ты уловил. Фишка в том, что производными классами можно пользоваться, не зная конкретных деталей их реализации:
При этом функция f () проглатывает объект любого класса, производного от нашего полиморфного (т.е. предоставляющего интерфейс для множества других) MyString. Например, BigStr:
Ориентируемся на объекты
Механизм наследования из предыдущего примера приводит нас еще к одной технике - объектно-ориентированному программированию. Его основы - абстракция данных и иерархия классов. Последняя представляет собой различные проявления множественного наследования. Например, класс A, класс B, производный от A, класс С, производный от B, и класс D, производный от A и B, представляют собой несложную иерархию. Производный (дочерний) класс B наследует все члены базового (родительского) класса A - это главная идея наследования.
Теперь принцип написания программы звучит так: "Реши, какие понадобятся классы, обеспечь полный набор операций над ними и вырази общность через наследование". Последнее - довольно непростая задача. Тому, кто ее решит (на этапе проектирования программы), не придется все переделывать в самый ответственный момент.
Обобщенное программирование
Тебе наверняка часто приходилось сталкиваться с такими сущностями, как список, стек и т.п. Основная их функция - хранить какие-то объекты. Классы, используемые для этих целей, называются контейнерами (классами-контейнерами). Разумеется, хотелось бы, чтобы класс "список" умел хранить что угодно: объекты любого класса, переменные любого встроенного типа - вот был бы идеальный контейнер. У такого контейнера "алгоритм хранения" должен быть представлен независимо от деталей представления хранимых данных. В C++ это достигается при помощи шаблонов (templates). Используя их универсальный стек, например, объявляется это так:
Префикс template делает тип T параметром объявления. Такой стек так же легко использовать, как и обычный:
Кроме классов, шаблонами можно объявлять и функции, что тоже очень удобно. Это позволяет писать универсальные функции сортировки, поиска и замены элементов контейнеров-шаблонов.
Шаблоны широко используются в стандартной библиотеке C++ - в STL (Standart Template Library). STL предоставляет пользователям туеву хучу всяких контейнеров (от строк до очередей с двумя концами), потоков ввода-вывода, универсальных алгоритмов и многое другое. Кроме того, она включает в себя всю стандартную библиотеку C. Вывод - must use. Пользоваться ей настоятельно рекомендую еще и потому, что писали ее не один год, постоянно улучшая и модернизируя. И если вдруг кому-то приспичит написать свой собственный вектор тихим майским вечером (лишь бы стандартный не использовать), вряд ли у него получится даже аналог STL'овского.
Размеры статьи не позволяют даже кратко описать все возможности C++, поэтому я постарался сделать обзор самого главного, основополагающего - реализации различных техник и стилей программирования в этом языке. Опять же не претендуя на полноту. Всех заинтересовавшихся отправляю прямиком в книжный магазин - за книгой Страуструпа "Язык программирования C++". Прочитав эти несчастные 12 сотен страниц, ты сможешь реально оценить безграничные возможности языка C++.
Уже долгое время не прекращаются споры, что лучше: Delphi или C/C++/Visual C++. Причем в большинстве случаев сравниваются две принципиально разные вещи. Ведь до седьмой версии имя Delphi носила лишь среда разработки, а язык ее компилятора был Object Pascal (в Delphi7 борландовцы решили устранить такое упущение, и теперь и язык называется Дельфи). То же самое и с Visual C++: эта IDE "накручена" на Microsoft C/C++ Compiler (cl.exe). Поэтому корректным было бы сопоставление Delphi и Visual Studio или объектного паскаля и C++. Почему именно "си-плюс-плюс", а не C? Да потому что C - процедурно-ориентированный язык "среднего уровня", а Object Pascal - высокоуровневый, с поддержкой ООП и абстракций, т.е. это совсем разные вещи. Такая путаница в понятиях заставляет многих думать, что и C, и C++ - почти одно и то же, а ведь это совсем разные языки. Не будем погружаться в дебри Си: тут все довольно понятно, посмотрим лучше, что за зверь C++.
Что такое C++?
С этим вопросом лучше обратиться к его создателю - Бьерну Страуструпу. Думаю, он бы ответил примерно так: С++ - это язык, который лучше, чем C поддерживает абстракцию данных, объектно-ориентированное (ООП) и обобщенное программирование. В том, что все это означает, мы и будем разбираться.
Прежде всего замечу, что C++ разрабатывался с нуля с целью добавления новых средств к стандартному C. Теперь, надеюсь, понятно, почему он лучше? :) В то же время, тяжелого и убогого уродца создатели делать не хотели, поэтому они руководствовались очевидными принципами: эстетика (все должно быть понятно и элегантно), минимализм (поддержка какого-либо средства не должна вызывать дополнительных расходов в не использующих его программах) и т.п.
Важно и само понятие поддержки стиля программирования. Можно говорить, что язык поддерживают какой-то стиль, когда использование этого стиля в нем удобно, просто и эффективно. При написании объектно-ориентированной программы на C (такое тоже возможно) непередаваемые ощущения заработанного геморроя обеспечены. Поэтому C лишь предоставляет возможность использовать стиль ООП, но не поддерживает его.
Процедуры и функции
Рассмотрим непосредственно различные техники программирования и их реализации в С++. Начнем, конечно, с процедурной, поскольку она является прародителем всех остальных.
Основной принцип процедурного программирования гласит: "Реши, какие понадобятся процедуры, и используй наилучшие алгоритмы". Поддержка языком этой техники означает возможность передачи функции аргумента и возможности возврата функциями значений. Залог успеха при написании программы - подобрать оптимальные алгоритмы обработки данных и, чтобы не запутаться в них, разбить код на процедуры и функции.
Здесь C++ очень похож на C. Те же инструкции ветвления и циклы, такое же объявление переменных (правда, возможное в любом месте программы), указателей и массивов, множество встроенных типов и т.д. Хотя нововведений тоже немало: ссылки (&), операторы ввода-вывода (>> И <<), операторы для работы с памятью (new и delete), встроенный тип bool и т.д. Стоит упомянуть и обработку исключений. Вот пример функции, которая реализует вежливое, но настойчивое приглашение пользователю выйти :) ("//" - открывает комментарий до конца строки).
bool quit ()
{
char ans;
// сюда мы сохраним ответ
for (int tries = 0; tries <= 5; tries++) {
// спрашивать будем в цикле
cout << "Вы действительно хотите выйти (y/n)? ";
// cout - стандартный поток вывода
cin >> ans;
// cin - стандартный поток ввода
switch (ans) {
case 'y':
return true; // bool может быть только true
case 'n':
cout "А зря!\n";
return false; // или false
default:
cout "Повторяю вопрос:\n";
}
}
cout "Все равно выходим!\n";
return false;
}
Модули и пространства имен
Процедурное программирование - основа основ, его стали применять в первых программах для первых ЭВМ. Но ничто не стоит на месте: сложность программ росла, и со временем важным вопросом стала организация данных. Так появились модули - набор процедур вместе с данными, которые они обрабатывают. Стал актуален принцип сокрытия: "организуй код в модулях так, чтобы скрыть в них данные".
На первый взгляд не совсем понятно, что и зачем нужно скрывать. Ответ прост: пользователю функций (хотя это такой же программер, как и их разработчик, назовем его так) не интересно, как они работают, для него главное, чтобы они действительно работали. Поэтому разработчик предоставляет пользователю некий интерфейс (пользовательский) - все, что необходимо для вызова данного набора функций (модуля). Реализация же этих функций не видна пользователю - она скрыта. По этому принципу построено большинство библиотек (например, WinAPI, где код функций находится в системных dll'ках, а программисты знают о них из заголовочных файлов типа windows.h).
На самом деле модульное программирование не новость и для С-кодеров, но они вынуждены обходиться простой раздельной компиляцией (несколько .c-файлов) и заголовочными .h-файлами. Все объявленные переменные в хидерных файлах оставались по-прежнему глобальными, и к ним можно было легко обратиться из любого места программы. В С++ появилась такая полезная вещь, как пространства имен (namespaces). Объявляя пространство имен, ты, по сути, "ограничиваешь область видимости" всему, что находится внутри него. А внутри может находиться любое объявление. Например, у нас есть пространство имен A, содержащее переменную c == 100, и есть глобальная переменная c == 0. Тогда функции f() и g() выведут 100, а h() - 0:
int c = 0;
namespace A {
int c = 100;
void f () { cout << c; }
}
void g () {
cout << A::c;
}
void h () {
cout << c;
}
Здесь A::c означает, что c берется из пространства имен A. Если бы мы захотели обратиться к глобальной с из функции f (), нам пришлось бы использовать квалификатор глобального namespace'а:
void f () { cout << ::c; }Теперь нетрудно догадаться, как реализовать сокрытие данных с помощью пространств имен. Рассмотрим модуль "строка символов". На самом деле в реальных программах так делать не надо :), это лишь наглядный пример. Сначала объявим пользовательский интерфейс.
// файл "mystring.h"
namespace MyString { // интерфейс
bool assign (char*); // присвоение значения
int length (); // возвращает длину строки
char* value (); // возвращает значение
}
Получился довольно примитивный модуль :). Теперь посмотрим на его реализацию.
// файл "mystring.с"
#include <string.h>
#include "mystring.h"
namespace MyString { // реализация
const int max_size = 1000; // максимальный размер
int len = 0; // длина
char v[max_size]; // массив символов
}
bool MyString::assign (char* str) {
if (strlen (str) > max_size - 1)
return false;
if (!strcpy (v, str))
return false;
else
return true;
}
int MyString::length () {
return len;
}
char* MyString::value () {
return v;
}
Теперь пользователю достаточно заинклудить mystring.h, и можно пользоваться нашей строкой:
// файл "user.c"
#include "mystring.h"
void f ()
{
if (!MyString::assing ("Yo!"))
cout << "Ошибка!";
else
cout << MyString::value ();
}
Абстракция данных
Используя модуль, описанный выше, ты в какой-то момент столкнешься с проблемой реализации нескольких таких строк. Действительно, трудно представить ситуацию, где достаточно одной подобной строки. Результатом долгих и тяжелых экспериментов над нашим модулем-строкой станет некое подобие типа данных, "псевдо тип" строка. Как это чудо сделать - описывать не буду, потому что такое решение проблемы далеко от идеала. На этот случай C++ припас свое решение - возможность определения типов, которые ведут себя почти как встроенные. Такие типы называются абстрактными или типами, определяемыми пользователем (пользовательскими). Просвещенные товарищи, знакомые с ООП, думаю, уже поняли, о чем речь.
При работе с пользовательскими типами следует руководствоваться принципом "реши, какие потребуются типы, и обеспечь полный набор операций над ними". В контексте типа, определяемого пользователем, наша строка будет выглядеть примерно так:
class MyString {
int length; // длина
char* v; // массив символов
public:
// создает строку из C-строки cstr
MyString (char* cstr);
// создает пустую строку по умолчанию - нулевой длины
MyString ();
MyString operator+ (MyString); // конкатенация
bool operator== (MyString); // проверка на равенство
// и так далее...
};Этот листинг демонстрирует объявление класса - пользовательского типа. Наш класс реализует строку и несколько операций над строками. По умолчанию все члены класса являются закрытыми (private), то есть доступ к ним имеют только функции-члены этого класса. Элементы, объявленные как "public", общедоступны. Сами функции-члены определяются примерно так:
bool MyString::operator== (MyString mystr)
{
return (strcmp (v, mystr.v) == 0);
}
Функция-член, имеющая то же название, что и класс, называется конструктором. Конструкторы помогают по-разному инициализировать объекты класса - конкретные переменные. Обычно в них выделяется необходимая память, инициализируются переменные и т.п. В MyString, как видно из листинга, два конструктора: один по умолчанию, другой преобразует C-строку в "нашенскую". Пользоваться классом MyString можно так:
MyString mystr; // пустая строка
MyString mystr2 ("C++ рулит");
mystr2 = mystr + MyString ("C тоже ничего");
mystr = mystr2; // теперь обе строки равны
Удобно? Вполне! Пользовательские типы предоставляют огромные возможности и сильно упрощают нелегкий труд программиста при решении самых разных задач, ведь операции над их объектами ничем не отличаются от операций над переменными встроенных типов (int, char и т.д.). Типы, подобные MyString, принято называть конкретными типами.
Однако в типе MyString потеряно одно свойство, которым обладал модуль MyString - реализация не отделена от интерфейса. Конечно, представление строки закрыто (private), но, тем не менее, оно "видно" пользователю. И при изменении реализации строки, программеру-юзеру класса придется перекомпилировать весь свой код. Это не есть гуд. Но что поделаешь, с конкретными типами мы хотим работать как со встроенными, и тут по-другому никак.
Если же реализацию необходимо отделить от представления, надо пользоваться абстрактными типами. Тогда интерфейс будет примерно такой:
class MyString {
virtual int length () = 0;
virtual char* c_str () = 0;
// и дальше в том же духе...
}Модификатор "virtual" означает "может быть переопределено в производном классе", а "=0" значит, что эта функция ДОЛЖНА быть переопределена в будущем. Конечно, я привел совсем бредовый пример: идея сделать виртуальной функцию, возвращающую длину строки, может родиться только в воспаленном мозгу :). Но смысл, думаю, ты уловил. Фишка в том, что производными классами можно пользоваться, не зная конкретных деталей их реализации:
void f (MyString& mystr)
{
cout << mystr.c_str ();
}
При этом функция f () проглатывает объект любого класса, производного от нашего полиморфного (т.е. предоставляющего интерфейс для множества других) MyString. Например, BigStr:
class BigStr: public MyString {
int length () { return 100; }
// и так далее...
}Ориентируемся на объекты
Механизм наследования из предыдущего примера приводит нас еще к одной технике - объектно-ориентированному программированию. Его основы - абстракция данных и иерархия классов. Последняя представляет собой различные проявления множественного наследования. Например, класс A, класс B, производный от A, класс С, производный от B, и класс D, производный от A и B, представляют собой несложную иерархию. Производный (дочерний) класс B наследует все члены базового (родительского) класса A - это главная идея наследования.
Теперь принцип написания программы звучит так: "Реши, какие понадобятся классы, обеспечь полный набор операций над ними и вырази общность через наследование". Последнее - довольно непростая задача. Тому, кто ее решит (на этапе проектирования программы), не придется все переделывать в самый ответственный момент.
Обобщенное программирование
Тебе наверняка часто приходилось сталкиваться с такими сущностями, как список, стек и т.п. Основная их функция - хранить какие-то объекты. Классы, используемые для этих целей, называются контейнерами (классами-контейнерами). Разумеется, хотелось бы, чтобы класс "список" умел хранить что угодно: объекты любого класса, переменные любого встроенного типа - вот был бы идеальный контейнер. У такого контейнера "алгоритм хранения" должен быть представлен независимо от деталей представления хранимых данных. В C++ это достигается при помощи шаблонов (templates). Используя их универсальный стек, например, объявляется это так:
template<class T> class Stack {
T* v;
void push (T); // добавляем элемент
T pop (); удаляем элемент
// ...
}Префикс template делает тип T параметром объявления. Такой стек так же легко использовать, как и обычный:
Stack<int> si;
si.push (24);
Stack<MyString> sm;
sm.push (MyString ("str"));
sm.pop ();
Кроме классов, шаблонами можно объявлять и функции, что тоже очень удобно. Это позволяет писать универсальные функции сортировки, поиска и замены элементов контейнеров-шаблонов.
Шаблоны широко используются в стандартной библиотеке C++ - в STL (Standart Template Library). STL предоставляет пользователям туеву хучу всяких контейнеров (от строк до очередей с двумя концами), потоков ввода-вывода, универсальных алгоритмов и многое другое. Кроме того, она включает в себя всю стандартную библиотеку C. Вывод - must use. Пользоваться ей настоятельно рекомендую еще и потому, что писали ее не один год, постоянно улучшая и модернизируя. И если вдруг кому-то приспичит написать свой собственный вектор тихим майским вечером (лишь бы стандартный не использовать), вряд ли у него получится даже аналог STL'овского.
Размеры статьи не позволяют даже кратко описать все возможности C++, поэтому я постарался сделать обзор самого главного, основополагающего - реализации различных техник и стилей программирования в этом языке. Опять же не претендуя на полноту. Всех заинтересовавшихся отправляю прямиком в книжный магазин - за книгой Страуструпа "Язык программирования C++". Прочитав эти несчастные 12 сотен страниц, ты сможешь реально оценить безграничные возможности языка C++.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.

