Лекции по С++. Часть 1

1  Отличия синтаксиса языка С++ от С.

Язык С++ является дальнейшим развитием языка С, поэтому все конструкции языка С поддерживаются в С++. Однако в С++ появились новые возможности синтаксиса, не имеющиеся в С. Далее будут рассмотрены кратко эти отличия, но вначале обратим внимание на организацию заголовочных файлов разрабатываемого приложения. Проблемы, возникающие с заголовочными файлами при разработке достаточно сложных приложений, присущи как языку С, так и С++.

1.1  Заголовочные файлы приложения

Типичной ошибкой начинающих программистов является включение в заголовочные файлы определения глобальных переменных, массивов, а иногда даже и функций. Согластно принятому стилю программирования в заголовочных файлах должны содержаться только директивы включения других заголовочных файлов, описания типов переменных и функций. Однако, существует риск включения заголовочных файлов в один и тот же файл больше одного раза. Приведем пример. Пусть наше приложение состоит из следующих файлов.
//файл HEADER.HPP
class MyClass {/* ... */};  // содержит описание некоторого
...                         // класса
//файл DESKTOP.HPP
#include "HEADER.HPP" // подключает описанный ранее файл
class DeskTop {/* ... */}; //и определяет собственные типы
...
//файл DRIVER.CPP
#include "HERADER.HPP"
#include "DESKTOP.HPP"
class Driver {/* ... */};
...

При попытке откомпилировать файл DRIVER.CPP во второй строке компилятор выдаст ошибку, поскольку файл HEADER.HPP включен дважды и, соответственно, типы переменных, описанные в этом файле, будут описаны дважды.

Для предотвращения таких случаев можно организовать достаточно простую "ловушку", использую директиву условной компиляции. Измененный файл HEADER.HPP выглядит следующим образом.

//файл HEADER.HPP
#ifndef __HEADER_HPP //если перем. __HEADER_HPP не описана,
#define __HEADER_HPP //то определить ее пустым значением
 class MyClass {/* ... */} // определение требуемых типов
#endif                // конец директивы

Лучше всего любые заголовочные файлы в приложении организовывать приведенным выше способом.

Отметим, что громоздкое имя переменной __HEADER_HPP

создается по имени файла и такое имя практически позволяет избежать ошибки, связанной с совпадением имен.

1.2  Комментарии

В С++ наряду со старым форматом (/* ... */) используется новый формат комментариев, для комментария, состоящего из одной строки. Строка комментариев начинается символами //, а заканчивается в конце строки. Для комментирования нескольких строк подряд можно пользоваться старым форматом.

1.3  Переменные

Напомним определение переменной. Переменная - именованная область памяти для хранения информации. Язык С++ позволяет объявлять переменные почти в любом месте программы, например:
  ...
  for (int i=0; i<10; i++) {//объявление перем. i снаружи
           /* ... */       //блока, содержащего оператор for  
  }
  ...
  {
  int i = GetValue(); //объявление другой переменной i
  char* cp = NextString(); 
  ...
  }

Время "жизни" любой переменной в С++ определяется блоком {...}, в котором она объявлена, поэтому пeременной i, объявленной в цикле for, не существует при выходе из блока, содержащего оператор цикла. Целесообразно объявлять переменную в том месте, где она впервые понадобилась.

1.4  Использование const вместо #define

Использование ключевого слова const вместо директивы #define позволяет компилятору С++ контролировать типы, например:
 const int TRUE = 1;  //определение типизованных констант
 const int FALSE = 0;
 const float ALT = 1.5;
 ...
 while (TRUE) {
   ...
   int a = FALSE; //использование констант с контролем типа
   float b = TRUE;//нет предупреждения о различных типах
   int  i = ALT;  //предупреждение о различных типах
   ...
 }

Поскольку константные объекты в С++ по умолчанию не доступны из других модулей, при необходимости следует использовать ключевое слово extern. Пример.

  extern const int FALSE = 0;
  extern const int TRUE = 1;

1.5  Объявления функций

При объявлении функций можно не указывать имя входного параметра, а только его тип, например
  void func(char*, int, float*);//верное объявление функции

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

 int func (int a, int /* b */) {
   return (a-1);
 }

Можно описать функцию с параметрами по умолчанию, например

  int f(int a = 0) { return a+1;}

Вызов такой функции без параметров, означает, что входной параметр равен нулю, а результат, возвращенный функцией равен единице.

1.6  Указатели и ссылки

Известно, что указатели в языке С и С++ содержат адрес объекта. Указатели не нужно инициализировать во время объявления, но они должны быть проинициализированы до использования. Указатель типа void* в С++ обладает важным свойством. Типизованный указатель может быть присвоен указателю типа void, но не наоборот. В противном случае требуется явное преобразование типов.

Переменные ссылки - нововведение в С++. Переменные ссылки также содержат адрес объекта, но подчиняются другим правилам. Ссылки необходимо инициализировать при объявлении. Рассмотрим пример.

 int a = 3;  //объявление и инициализация переменной
 int& ia = a;//объявление и инициализация ссылки на
             //переменную

Переменная ссылка ссылается на объект, ее можно использовать так, как будто она является альтернативным именем объекта.

  int& i = 3;
  int j;

  if (i == 3) {
    i--;
    j = i;
  }

  i = strlen("title");

Нельзя объявить ссылку типа void.

  void& a = 3; //неверно!

Значение ссылки во время ее существования можно изменить, если только она не объявлена как константа.

 const int& number = 10;
 ...
 number = 5;   // ошибка!
 number = 10;  // ошибка!
 ...

1.7  Управление памятью

В С++ появились три встроенных оператора для управления памятью: new, delete, delete[]. Оператор new по своему назначению близок к функции malloc(). Однако, преобразование типов при его использовании не требуется. Следующий пример показывает использование этого оператара.
  int i = 10;
  int*  ip = new int[i]; // выделение памяти 
                         //для массива из 10 целых чисел

Объем выделяемой памяти должен быть известен во время выполнения программы. Для освобождения памяти используется оператор delete, а для освобождения памяти распределенной под массив используется delete[], например

  char* cp = new char[100];//выделение памяти
  ...
  delete[] cp;//освобождение памяти

2  Обзор языка С++

С++ является языком объектно-ориентированного программирования (ООП). Объект - абстрактная сущность, наделенная характеристиками объектов реального мира. Как и любой другой язык ООП, С++ использует три основные идеи ООП - инкапсуляцию, наследование и полиморфизм.

Инкапсуляция - сведение кода и данных воедино в одном объекте, получившим название класс.

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

Полиморфизм - дословный перевод с греческого "много форм". В С++ полиморфизм реализуется с помощью виртуальных функций, которые позволяют в рамках всей иерархии классов иметь несколько версий одной и той же функции. Решение о том, какая именно версия должна выполняться в данный момент, определяется на этапе выполнения программы и носит название позднего связывания.

2.1  Классы, данные и методы класса

Основное отличие С++ от С состоит в том, что в С++ имеются классы. С точки зрения языка С классы в С++ - это структуры, в которых вместе с данными определяются функции. Это и есть инкапсуляция в терминах ООП.

Класс (class) - это тип, определяемый пользователем, включающий в себя данные и функции, называемые методами или функциями-членами класса.

Данные класса - это то, что класс знает.

Функции-члены (методы) класса - это то, что класс делает.

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

2.2  Обьявление класса

Приведем пример объявления класса.
 class TCounter {
    long count; // данные класса
 public:
    long GetValue();      //функции-члены класса
    void SetValue(long);
 };

Определение класса начинается с ключевого слова class за которым следует имя класса. Имя класса в BC 4.5 может иметь до 32 символов, причем различаются строчные и прописные буквы. Открывающая и закрывающая фигурные скобки определяют тело класса, в которое включено описание данных и функций класса. Заканчивается описание класса символом ;. Класс имеет столько переменных (данных), сколько необходимо. Переменные могут быть любого типа, включая другие классы, указатели на классы и указатели на динамически распределяемые объекты. Переменные объявленные внутри класса имеют область видимости класса, т.е. от точки объявления переменной до конца класса.

2.3  Определение функций-членов класса

В приведенном выше описании класса функции класса только объявлены, приведем их реализацию. Обычно описания классов включают в заголовочные файлы (*.HPP), а реализацию функций-членов классов - в файлы *.CPP.
 // установить значение счетчика
 void TCounter::SetValue(long val) {
   count = val;
 }
 //получить значение счетчика
 long TCounter::GetValue() { return count; }

В приведенной реализации запись TCounter:: сообщает компилятору, что реализация функций принадлежит классу TCounter. Символ :: является операцией определения области действия.

2.4  Использование класса

Следует четко понимать, что в момент объявления класса и определения его функций-членов самих объектов или экземпляров класса не существует. Классы - это не объекты. Объектами являются переменные, экземпляры классов которые должны создаваться в программе. Приведем пример использования класса.
void main(void) {
  TCounter cnt; //создание объекта cnt типа TCounter

  cnt.SetValue(10);  //вызов метода для инициализации
  //определение и инициализация указателя на объект
  TCounter* p = &cnt;
  int i = p->GetValue();//использование указателя
  //определение ссылки
  TCounter& Rp = &cnt;
  i = Rp.GetValue();  //использование ссылки
  //Определение массива указателей
  TCounter* m[10];
  //Создание и инициализация объектов
  for (int k = 0; k < 10; k++) {
    m[k] = new TCounter;
    m[k]->SetValue(0);
  }
  //Освобождение памяти
  for (i = 0; i < 10; i++) delete m[i];
}

2.5  Управление доступом к классу

Для ограничения уровня доступа к данным и функциям-членам класса в С++ существуют три ключевых слова private: (частный), protected: (защищенный), public: (общедоступный), задающие разделы поступа в классе. Каждый раздел в классе начинается с одного из приведенных слов. Если ни одно из ключевых слов не использовалось, то все объявления в классе считаются частными. Разделы с разными привилегиями доступа могут появлятся в любом порядке и в любом колличестве. Рассмотрим пример.
class Example {
  int x1;        // частные по умолчанию
  int f1(void);
protected:
  int x2;        // защищенные
  int f2(void);
private:
  int x3;        // опять частные
  int f3(void);
public:
  int x4;        // общедоступные
  inf f4(void);
};

1  Частные члены класса

Частные члены класса имеют наиболее ограниченный доступ. К частным членам класса имеют доступ только функции-члены данного класса или классы и функции объявленные как дружественные (friend) к данному классу, например:
class TPrivateClass {
  int value;
  int GetValue(void);
};
int TPrivateClass::GetValue(void) {
  return value; //доступ разрешен
}
void main(void) {
 TPrivateClass cl; //создание объекта

 int i = cl.value; //ошибка! Нет доступа
 i = cl.GetValue();//ошибка! Нет доступа
}

2  Защищенные члены класса

Члены и функции объявленные в защищенном (protected) разделе класса доступны только для функций производных классов. Обычно в этом возникает необходимость тогда, когда разрабатываемый класс является базовым классом для других классов. В этом случае он ограничивает доступ к данным внешним пользователям и разрешает доступ для классов наследников. Рассмотрим пример иерархии объектов.
class A {
protected:
 int val;
};

class B: public A { //наследуется от A
public:
 void fb();
};
void B::fb() { val = 0; } //доступ разрешен

class C:public B { //наследуется от B
public:
 void fc();
};
void C::fc() {val = 10;} //доступ разрешен

В данном примере приведена иерархия классов A->B->C. Свойство защищенности распространяется вниз по иерархии до тех пор пока производный класс объявляет свой базовый общедоступным (public). При этом любые функции-члены в классах C и B имеют доступ к члену данных val базового класса. Если функция-член производного класса в качестве входного параметра имеет указатель или ссылку на объект базового класса, то правила становятся другими. Модифицируем класс C следующим образом.

class C:public B {
 public:
   void fc(A&); //Входной параметр ссылка на базовый класс
};
void C::fc(A& a) {
  val = 10; //доступ разрешен
  a.val = 10; //ошибка! нарушение прав доступа
}

3  Общедоступные члены класса

C общедоступными членами класса все обстоит намного проще. Доступ к общедоступным членам класса разрешен функциям-членам самого класса, производных классов и обычным пользователям класса.

2.6  Указатель this

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