Первое знакомство с С++
Первое знакомство с С++
Программа С++ состоит из одной или нескольких единиц компиляции. Каждая единица компиляции представляет собой отдельный файл исходного кода, обычно имеющий расширение .cpp (другими распространенными расширениями являются .cc и .cxx); она обрабатывается компилятором за один шаг. Для каждой единицы компиляции компилятор генерирует объектный файл с расширением .obj (в Windows) или .о (в Unix и Mac OS X). Объектный файл — это бинарный файл, содержащий машинный код для той архитектуры, на которой будет выполняться программа.
После компиляции всех файлов .cpp мы можем собрать все объектные файлы для создания исполняемого модуля, используя специальную программу, называемую компоновщиком (linker). Компоновщик соединяет объектные файлы в единое целое и назначает адреса памяти функциям и другим символическим ссылкам, которые содержатся в единицах компиляции.
Рис. Б.1. Процесс компиляции программы на С++ (в Windows).
При создании программы только одна единица компиляции должна иметь функцию main(), которая является точкой входа в программу. Эта функция не принадлежит никакому классу — она является глобальной функцией.
В отличие от Java, где каждый исходный файл должен содержать точно один класс, С++ позволяет организовать единицу компиляции удобным для нас способом. Можно реализовать несколько классов в одном файле .cpp или распространить реализацию класса на несколько файлов .cpp; имена исходных файлов могут быть любыми. При внесении изменений в один конкретный файл .cpp потребуется перекомпилировать этот файл и затем повторно скомпоновать приложение для создания нового исполняемого модуля.
Прежде чем мы пойдем дальше, давайте рассмотрим очень простую программу на С++, вычисляющую квадрат целого числа. Эта программа состоит из двух единиц компиляции: main.cpp и square.cpp.
Ниже показан файл square.cpp:
01 double square(double n)
02 {
03 return n * n;
04 }
Этот файл содержит лишь глобальную функцию с именем square(), которая возвращает квадрат своего параметра.
Ниже показан файл main.cpp:
01 #include <cstdlib>
02 #include <iostream>
03 using namespace std;
04 double square(double);
05 int main(int argc, char *argv[])
06 {
07 if (argc != 2) {
08 cerr << "Usage: square <number>" << endl;
09 return 1;
10 }
11 double n = strtod(argv[1], 0);
12 cout << "The square of " << argv[1] << " is " << square(n) << endl;
13 return 0;
14 }
Исходный файл main.cpp содержит определение функции main(). В С++ эта функция принимает в качестве параметров int и char * (массив символьных строк). Имя программы находится в argv[0], а аргументы командной строки — в argv[1], argv[2], … argv[argc — 1]. Параметры имеют стандартные имена argc («argument count» — количество аргументов) и argv («argument values» — значения аргументов). Если программа не использует аргументы командной строки, функцию main() можно определить без параметров.
Функция main() использует из стандартной библиотеки С++ функции strtod() («string to double» — преобразование строки в переменную двойной точности), cout (стандартный поток вывода С++) и cerr (стандартный поток вывода сообщений об ошибках С++) для преобразования аргумента командной строки в тип double и для вывода текста на консоль. Строки, числа и маркеры конца строки (endl) выводятся с помощью оператора <<, который также используется для сдвига битов. Чтобы воспользоваться этой стандартной функциональностью, необходимо включить директивы #include, расположенные в строках 1 и 2.
Директива using namespace в строке 3 указывает компилятору на то, что мы хотим импортировать в глобальное пространство имен все идентификаторы, объявленные в пространстве имен std. Это позволяет нам пользоваться записью strtod(), cout, cerr и endl вместо указания полных имен: std::strtod(), std::cout, std::cerr и std::endl. В С++ оператор :: разделяет компоненты сложного имени.
В строке 4 объявляется прототип функции. Он указывает компилятору на то, что существует функция с данными параметрами и возвращаемым значением. Реальное определение функции может находиться в той же или в другой единице компиляции. Без прототипа функции компилятор не позволил бы нам вызвать эту функцию в строке 12. Имена параметров функции указывать необязательно.
Процедура компиляции программы зависит от платформы. Например, для компиляции программы в Solaris с использованием компилятора С++ компании «Sun» мы могли бы задать следующие команды:
CC -с main.cpp
CC -с square.cpp
ld main.o square.o -о square
Первые две строки вызывают компилятор, чтобы сгенерировать файлы .о для соответствующих файлов .cpp. Третья строка вызывает компоновщик и формирует исполняемый модуль с именем square, который может запускаться следующим образом:
./square 64
Эта программа выводит на консоль следующее сообщение:
The square of 64 is 4096
(Квадрат числа 64 равен 4096)
Чтобы скомпилировать программу, вы, возможно, попросите помощи у местного опытного программиста С++. Если это не удастся сделать, можете прочитать остальную часть приложения, ничего не компилируя, и воспользоваться инструкциями в главе 1 по компиляции вашего первого приложения C++/Qt. В Qt предусмотрены утилиты, позволяющие легко создавать приложения на любой платформе.
Вернемся к нашей программе. В реальном приложении, как правило, мы размещали бы прототип функции square() в отдельном файле и включали бы этот файл во все единицы компиляции, в которых вызывается эта функция. Такой файл называется заголовочным; он обычно имеет расширение .h (часто встречаются также расширения .hh, .hpp и .hxx). Если переделать наш пример, используя заголовочный файл, то можно было бы создать файл с именем square.h, который содержит следующие строки:
1 #ifndef SQUARE_H
2 #define SQUARE_H
3 double square(double);
4 #endif
В начале и в конце заголовочного файла задаются препроцессорные директивы (#ifndef, #define и #endif). Эти директивы гарантируют однократное выполнение заголовочного файла, даже если он несколько раз включается в одну и ту же единицу компиляции (такая ситуация возникает, когда одни заголовочные файлы включают в себя другие заголовочные файлы). По принятым соглашениям используемый для этого препроцессорный символ строится на основе имени файла (в нашем примере это символ SQUARE_H). Позже в этом приложении мы вернемся к рассмотрению препроцессора.
Новый файл main.cpp будет иметь следующий вид:
01 #include <cstdlib>
02 #include <iostream>
03 #include "square.h"
04 using namespace std;
05 int main(int argc, char *argv[])
06 {
07 if (argc != 2) {
08 cerr << "Usage: square <number>" << endl;
09 return 1;
10 }
11 double n = strtod(argv[1], 0);
12 cout << "The square of " << argv[1] << " is " << square(n) << endl;
13 return 0;
14 }
Используемая в строке 3 директива #include разворачивает содержимое файла square.h. Директивы, начинающиеся с символа #, рассматриваются препроцессором С++ до фактической компиляции. В прежние дни препроцессор являлся отдельной программой, которую программист вызывал вручную перед выполнением компилятора. В современных компиляторах этап препроцессорной обработки выполняется автоматически.
Директивы #include в строках 1 и 2 разворачивают содержимое заголовочных файлов cstdlib и iostream, которые являются частью стандартной библиотеки С++. Стандартные заголовочные файлы не имеют суффикса .h. Угловые скобки вокруг имен файлов говорят о том, что заголовочные файлы располагаются в стандартном месте системы, в то время как кавычки заставляют компилятор просматривать текущий каталог. Директивы #include обычно собирают вместе и располагают в верхней части файла .cpp.
В отличие от файлов .cpp, заголовочные файлы сами по себе не являются единицей компиляции и не приводят к созданию объектных файлов. Они могут только содержать объявления, позволяющие различным единицам компиляции взаимодействовать друг с другом. Следовательно, было бы неправильно помещать реализацию функции square() в какой-нибудь заголовочный файл. Если бы мы это сделали в нашем примере, ничего плохого не случилось бы, потому что square.h включается только однажды, однако если бы мы включали square.h в несколько файлов .cpp, то получили бы несколько реализаций функции square() (по одной на каждый файл .cpp, который включает этот заголовочный файл). После этого компоновщик пожаловался бы на существование нескольких (идентичных) определений функции square() и отказался бы генерировать исполняемый модуль. И наоборот, если мы объявляем функцию, но нигде ее не реализуем, компоновщик пожалуется на наличие «неразрешенного символа».
До сих пор мы предполагали, что исполняемый модуль состоит только из объектных файлов. На практике они компонуются также с библиотеками, которые реализуют готовую функциональность. Существует два основных типа библиотек:
• статические библиотеки непосредственно помещаются в исполняемый модуль, как будто они являются объектными файлами. Это гарантирует невозможность потери библиотеки, но увеличивает размер исполняемого модуля;
• динамические библиотеки (называемые также совместно используемыми библиотеками или библиотеками DLL) располагаются в стандартном месте на машине пользователя и автоматически загружаются во время запуска приложения.
Программу square мы компонуем со стандартной библиотекой С++, которая реализована как динамическая библиотека на большинстве платформ. Сами средства разработки Qt представляют собой коллекцию библиотек, которые могут создаваться как статические или как динамические библиотеки (по умолчанию они создаются как динамические библиотеки).