Массивы и структуры

1. Описание массивов
 2. Массивы в качестве параметров функций
 3. Многомерные массивы
 4. Структуры

1. Описание массивов

Пример описания одномерного массива:
const int n = 12;
int c[n];
Это описание вводит в программу массив c, состоящий из 12-ти элементов, для обращения к которым используются индексы из диапазона 0..11 - c[0], c[1], ..., c[11]. Индексы могут быть выражениями целого типа.

В описании массива можно выполнить его инициализацию. Примеры:
int a[4] = {1,2,3,4};   // Массив содержит четыре элемента с значениями 1, 2, 3, 4.
int b[] = {32,64,128};  // Массив содержит три элемента с значениями 32, 64, 128.
int c[10] = {0};        // Массив содержит десять элементов с значениями 0.
Если в списке инициализации элементов меньше, чем элементов в массиве, то оставшиеся элементы массива обнуляются.

Есть особенности в описании массивов с элементами типа char. Такие массивы в C++ служат для представления строк. Для указания места окончания строки за её последней литерой должен стоять терминатор '\0' - байт с нулевым значением. При указании размера строки всегда следует предусмотривать место и для её терминатора. Примеры:
char str[] = {'L','o','g','\0'}; // Строка длиной три литеры занимает четыре байта.
char str[] = "Log";    // Этот способ инициализации приводит к тому же результату.
char str2[20] = "Log"; // Строка длиной три литеры занимает двадцать байтов.

2.Массивы в качестве параметров функций

Функциям массивы по умолчанию передаются по ссылке, а их элементы по значению. Пример:

// ArrTest.cpp
#include <iostream.h>

const int m = 10, n = 4;
int a[m] = {1,2,3,4,5,6,7,8,9,10};
int b[n] = {1,2,4,8};

int SumArr(int a[], int m);

void main() {
   int Sa = SumArr(a,m);
   int Sb = SumArr(b,n);
   cout << "Sa = " << Sa
        << "Sb = " << Sb << endl;
}

int SumArr(int a[], int m) {
   int S = 0;
   for (int i=0;i<m;i++) S+=a[i];
   return S;
}

Выход программы ArrTst.cpp:
Sa = 55  Sb = 15

3. Многмерные массивы

Примеры описания многомерных массивов:
int a[2][3];
int b[2][3] = {1,2,3},{4,5,6}};
При определении функции, которой передаётся в качестве параметра многомерный массив, необходимо указывать размерность по всем измерениям кроме первого. Пример:

const int m =
3, m = 4;
int a[m][n];
...
int Sum(int a[][n], int m, int n) {
int S = 0;
for (int i=0;i<m;i++)
for (int j=0;j<n;j++)
S+=a[i][j];
return S;
}

Пример вызова функции Sum: Sa = Sum(a,m,n);
Имена массивов не являются l-value.

4. Структуры

Структуры в C++ похожи на записи в АЯ Pascal. Пример описания типа структуры:
struct SRec {
   char name[10];
   int  age;
};
Далее в программе можно объявлять переменные типа SRec:
SRec s = {"Атос", 30};
Для обращения к полю структуры её имя квалифицируют именем поля: s.name, s.age. Описание структуры не должно быть рекурсивным. По умолчанию структуры передаются функциям по значению.

Далее рассмотрим пример, в котором реализуется функция поиска записи в таблице. Таблица - это массив структур. Пример:

const int m = 7;
struct SRec { char name[10]; int age; };
SRec Tab[m];
...
int Search(char name[]) {
   for (int i=0;i<m;i++) {
      if (!strcmp(Tab[i].name,name)) return i;
   return -1;
}
...
void main() {
...
   int p = Search("Арамис");
   if (p>=0) cout << "Age = " << Tab[p].age << endl;
   else cout << "Нет в таблице." << endl;
...
}

Библиотечная функция strcmp(s1, s2) выполняет сравнение строк s1 и s2 и возвращает значение 0 (строки равны), <0 (s1 < s2) или >0 (s1 > s2). Для вызова этой функции надо включить заголовочный файл string.h .

Copyright г Барков Валерий Андреевич, 2000


Наследование

Содержание

Композиция

1. Интерфейс класса CBank
 2. Модуль реализации класса CBank
 3. Клиент класса CBank

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

Для иллюстрации этого метода мы рассмотрим пример организации класса CBank, представляющего сильно упрощённую модель банка, счета в котором будут представлять объекты класса CAccount. Модули интерфейса и реализации класса CAccount включёны в этот проект в последней редакции и здесь не приводятся.

1. Интерфейс класса CBank

Может быть предложен такой интерфейс класса CBank.

// Bank.h - Интерфейс класса CBank

#ifndef BANK_H
#define BANK_H
#include "Account.h"

class CBank {
public:
    CBank(char *NameOfBank,int AccCnt);
    ~CBank();
    void printReport();
    char *getNameOfBank();
    int getCapital();
    int Deposit(int AccNum, int Amount);
    int Withdraw(int AccNum, int Amount);
    int OpenAcc();
    int CloseAcc(int AccNum);
private:
    CAccount **mArrAcc;
    char mName[10];
    const int mCnt;
    int mLastAccNum;
    int AccNumToIndex(int AccNum);
    int IndexOfEmpty();
};

#endif

Обсудим атрибуты и методы класса CBank. Сначала рассмотрим методы раздела public, которые и образуют фактический интерфейс класса. Конструктор CBank устанавливает имя банка и максимальное количество счетов, которое в нём могут находиться. Назначение методов getNameOfBank, printReport и getCapital очевидно. Остальные методы раздела связаны с жизненным циклом каждого счёта, который может быть представлен такой схемой:
OpenAcc { Deposit | Withdraw } CloseAcc.
OpenAcc "открывает счёт" и сообщает его номер. Номер счёта это его единственный идентификатор, по которому к нему можно обращаться. Аргументы операций Deposit ("положить на счёт") и Withdraw ("снять со счёта") предусматривают указание номера счёта и суммы, участвующей в операции. Оба эти метода возвращают значение новой суммы на счёте. Метод CloseAcc "закрывает счёт". Естественно предположить, что каждый номер счёта должен быть уникальным. Это означает, что номера закрытых счетов не должны использоваться в будущем.

Атрибуты и методы раздела private более тесно связаны с реализацией класса. Основная образующая структура данных класса это атрибут mArrAcc - массив указателей на хранящиеся счета. Атрибут mCnt устанавливает размер массива mArrAcc. Этот атрибут является константным. Атрибут mName предназначен для хранения имени банка. Атрибут mLastAccNum содержит номер последнего открытого счёта и необходим для генерации уникальных номеров счетов. Методы AccNumToIndex и IndexOfEmpty - это утилиты. Первая определяет индекс элемента массива mArrAcc, который содержит указатель на счёт с заданным номером. Вторая - определяет индекс свободного элемента массива mArrAcc.

Устанавливая общие принципы построения класса, мы должны принять и некие общие принципы обработки ошибок. В практике создания реальных систем этому вопросу уделяется очень большое внимание, но в данном случае мы ограничимся техникой формирования кодов завершения - если метод возвращает -1, то это говорит об ошибке клиента или нарушения какого-нибудь ограничения. Мы при этом исходим из того, что при отсутствии ошибок методы не могут возвращать отрицательных значений.

После рассмотрения интерфейса класса CBank естественно перейти к рассмотрению его реализации.

2. Модуль реализации класса CBank

// Bank.cpp - Реализация класса CBank

#include <iostream.h>
#include <string.h>
#include "Account.h"
#include "Bank.h"

CBank::CBank(char *NameOfBank,int AccCnt):mCnt(AccCnt) {
    strcpy(mName,NameOfBank);
    mLastAccNum=0;
    mArrAcc = new CAccount*[mCnt];
    for (int i=0;i<mCnt;i++) mArrAcc[i]=0;
}

CBank::~CBank() {
    for (int i=0;i<mCnt;i++)
        if (mArrAcc[i]) delete mArrAcc[i];
    delete [] mArrAcc;
}

void CBank::printReport() {
    cout << "*** " << mName << " Bank Report ***\n";
    for (int i=0;i<mCnt;i++)
        if (mArrAcc[i]) mArrAcc[i]->printAccount();
    cout << "   Capital = " << getCapital() << endl;
}

char *CBank::getNameOfBank() {return mName;}

int CBank::getCapital() {
    int cap=0;
    for (int i=0;i<mCnt;i++)
        if (mArrAcc[i]) cap+=mArrAcc[i]->getBalance();
    return cap;
}

int CBank::Deposit(int AccNum, int Amount) {
    int ind=AccNumToIndex(AccNum);
    if (ind<0) return -1;
    return mArrAcc[ind]->Deposit(Amount);
}

int CBank::Withdraw(int AccNum, int Amount) {
    int ind=AccNumToIndex(AccNum);
    if (ind<0) return -1;
    return mArrAcc[ind]->Withdraw(Amount);
}

int CBank::OpenAcc() {
    int ind=IndexOfEmpty();
    if (ind<0) return -1;
    mArrAcc[ind]=new CAccount(++mLastAccNum);
    return mLastAccNum;
}

int CBank::CloseAcc(int AccNum) {
    int ind=AccNumToIndex(AccNum);
    if (ind<0) return -1;
    int b=mArrAcc[ind]->getBalance();
    delete mArrAcc[ind];
    mArrAcc[ind]=0;
    return b;
}

int CBank::AccNumToIndex(int AccNum) {
    for (int i=0;i<mCnt;i++)
        if (mArrAcc[i])
            if (mArrAcc[i]->getNumber() == AccNum) return i;
    return -1;
}

int CBank::IndexOfEmpty() {
    for (int i=0;i<mCnt;i++)
        if (mArrAcc[i] == 0) return i;
    return -1;
}

Обсудим некоторые моменты реализации. Память для массива mArrAcc выделяется динамически при выполнении конструктора CBank. Следует обратить внимание на способ инициализации константного атрибута класса mCnt - обычный способ для этого неприемлем. Освобождение памяти, которую занимал массив mArrAcc, выполняет деструктор ~CBank. Предварительно он должен освободить память, зарезервированную для размещения объектов-счетов.

3. Клиент класса CBank

Клиент, используя методы класса CBank, выполняет операции необходимые пользователю. Взаимодействие с пользователем происходит в режиме диалога, который реализуется с помощью меню.

// BankClient.cpp - Клиент класса CBank

#include <iostream.h>
#include "Bank.h"

void main() {
    CBank b("Omega",3);
    int accnum,amount,rep;
    char ans;
    do {
        cout << endl << endl <<
            "====== MENU ======\n"
            "e. Exit\n" <<
            "p. Print Report\n"
            "o. Open Account\n"
            "c. Close Account\n"
            "d. Deposit\n"
            "w. Withdraw\n"
            "------------------\n"
            "Answer: ";

        cin >> ans;

        switch (ans) {
        case 'e':   return;

        case 'p':   b.printReport(); rep=1; break;

        case 'o':   rep=b.OpenAcc();
                    if (rep>=0)
                        cout << "Account opened!" << endl <<
                            "Account number = " << rep << endl;
                    break;

        case 'c':   cout << "Account number: ";
                    cin >> accnum;
                    rep=b.CloseAcc(accnum);
                    if (rep>=0) cout << "Account closed!" << endl <<
                        "Rest balance = " << rep << endl;
                    break;

        case 'd': case 'w':
                    cout << "Account number and Amount: ";
                    cin >> accnum >> amount;
                    if (ans=='d') rep=b.Deposit(accnum,amount);
                    else rep=b.Withdraw(accnum,amount);
                    if (rep>=0) cout << "New balance = " <<
                        rep << endl;
                    break;
        }
        if (rep<0) cout << "===  Access error!\n";
        cout << "==================\n";
    } while (1);
}


Классы и абстракция данных

1. Определение класса
 2. Инкапсуляция
 3. Конструкторы и деструкторы
4. Отделение интерфейса от реализации

1. Определение класса

Для языков программирования большое значение имеет понятие типа. Тип переменной определяет её внутреннюю организацию и множество операций, в которых переменная может участвовать. До ООП возможности пользователя по созданию новых типов выражались в конструировании новых типов данных. Для реализации операций предназначались процедуры, но их связь с данными сводилась к использованию последних в качестве параметров. Можно утверждать, что полноправных типов пользователь тогда создавать не мог.

В C++ возможность создания новых типов связана с понятием класс (синоним слова тип). Определение класса очень похоже на определение обычной структуры языка C, но может включать и определения функций. Рассмотрим пример программы с классом CAccount:

// Account1.cpp - Простой класс CAccount

#include <iostream.h>

class CAccount {
public:
    int mNumber;
    int mBalance;
    void printAccount();
};

void CAccount::printAccount() {
    cout << "Account # " << mNumber <<
        ". Balance = $" << mBalance << "." << endl;
}

void main() {
    CAccount a;
    CAccount *pa = new CAccount;

    a.mNumber=1;
    a.mBalance=120;
    a.printAccount();

    pa->mNumber=2;
    pa->mBalance=150;
    pa->printAccount();

    pa->mBalance+=30;
    pa->printAccount();
}

Выход программы Account1.cpp:

Account # 1. Balance = $120.
Account # 2. Balance = $150.
Account # 2. Balance = $180.

Синтаксис определения простого класса: class <Имя класса> { <Тело класса> }; . Тело класса содержит определения членов класса - членов-данных, будем их называть атрибутами, и членов-функций, будем их называть методами.

В теле класса можно объявлять любые атрибуты, кроме типа <Имя класса>. Но можно объявлять указатели на данные типа <Имя класса>.

Есть два способа определения метода. При первом способе определение метода полностью включается в определение класса. В этом случае метод компилируется как inline-функция. При втором способе в определение класса включают только прототип метода, а его полное определение приводится на внешнем уровне. В этом случае имени метода должна предшеcтвовать конструкция <Имя класса>:: (см. пример) . При этом метод тоже может быть объявлен как inline-функция, но это указание уже должно быть явным. На практик первый способ используется только для коротких функций.

После определения класса можно объявлять объекты класса. Их также называют экземплярами класса. Способы объявления новых объектов и способы обращения к их членам очевидны из примера.

Слово public определяет класс доступа к его членам как общий. Существует ещё два класса доступа: private (приватный) и protected (защищённый). По умолчанию устанавливается класс доступа private. Соответствующие разделы классов доступа тела класса могут следовать в произвольном порядке. О назначении классов доступа мы расскажем в следующем разделе.

Для членов класса вводится понятие области видимости типа класс - все члены класса видимы в определении класса и в определениях его методов.

В нашем примере метод printAccount настолько тесно связан с классом CAccount, что без него теряет смысл. Это позволяет трактовать методы как операции.

2. Инкапсуляция

В примере предыдущего раздела все атрибуты класса доступны для изменений. Мы даже можем снять со счёта сумму превышающую ту, что на нём имеется. Чтобы избежать таких недоразумений, целесообразно защитить важные атрибуты класса от прямого доступа. Сделать это можно, объявив их с классом доступа private. После этого доступ к ним возможен только через методы. При этом, очевидно, методы могут контролировать доступ к приватным данным и предотвращать любые недоразумения. Такая организация классов называется инкапсуляцией. Суть инкапсуляции состоит в том, что от пользователя сознательно "прячется" внутренняя организация класса и ему предоставляется только интерфейс класса в виде его общих членов. Таким образом язык программирования позволяет создавать абстрактные конструкции, в терминах которых удобно выполнять разработку программы. Эти идеи иллюстрирует программа Account2.cpp.

// Account2.cpp - Инкапсуляция

#include <iostream.h>

class CAccount {
public:
    void printAccount();
    void init(int number) {
        mNumber=number;
        mBalance=0;
    }
    int getBalance() {return mBalance;}
    int Deposit(int amount);
    int Withdraw(int amount);
private:
    int mNumber;
    int mBalance;
};

void CAccount::printAccount() {
    cout << "Account # " << mNumber <<
        ". Balance = $" << mBalance << "." << endl;
}

inline int CAccount::Deposit(int amount) {
    if (amount<=0) return -1;
    mBalance+=amount;
    return mBalance;
}

int CAccount::Withdraw(int amount) {
    if (amount<=0 || amount>mBalance) return -1;
    mBalance-=amount;
    return mBalance;
}

void main() {
    CAccount a;

//  a.mNumber=1;
//  error C2248: 'mNumber' : cannot access private member
//                              declared in class 'CAccount'
    a.init(1);
    a.printAccount();

    cout << "a.Deposit(100): " << a.Deposit(100) << endl;
    cout << "a.Deposit(50):  " << a.Deposit(50) << endl;
    a.printAccount();

    cout << "a.Withdraw(-100): " << a.Withdraw(-100) << endl;
    cout << "a.Withdraw(100):  " << a.Withdraw(100) << endl;
    cout << "a.Withdraw(100):  " << a.Withdraw(100) << endl;
    a.printAccount();
}

Выход программы Account2.cpp:

Account # 1. Balance = $0.
a.Deposit(100): 100
a.Deposit(50):  150
Account # 1. Balance = $150.
a.Withdraw(-100): -1
a.Withdraw(100):  100
a.Withdraw(100):  -1
Account # 1. Balance = $50.

Метод init необходим для установки атрибутов созданного объекта в начальное состояние. Здесь следует заметить, что обычная инициализация для атрибутов не разрешается. Обратите внимание на то, что в методах Deposit и Withdraw выполняется проверка правильности их параметров.

Инкапсуляция вместе с удобствами приносит и некоторые осложнения. Так как прямой доступ к защищённым данным невозможен, то появляется необходимость в большом количестве методов типа "set" / "get".

Для методов тоже может устанавливаться класс доступа private. Такие методы называются утилитами. Они могут вызываться только методами класса и носят вспомогательный характер.

3. Конструкторы и деструкторы

Для инициализации атрибутов создаваемых объектов предусмотрены специальные методы, называемые кострукторами. Конструктор позволяет совместить инициализацию объекта с его созданием. Имя конструктора совпадает с именем класса. Тип возвращаемого значения для конструктора не указывается. В остальном конструктор - это обычный метод (он может быть перегружаемым, иметь параметры по умолчанию и т.д.). Параметры конструктора записывают в скобках при объявлении или явном создании объекта. Пример Account3.cpp иллюстрирует использование конструктора. От применения метода init пришлось, естественно, отказаться.

// Account3.cpp - Конструктор

#include <iostream.h>

class CAccount {
public:
    CAccount(int number);
    void printAccount();
    int getBalance() {return mBalance;}
    int Deposit(int amount);
    int Withdraw(int amount);
private:
    int mNumber;
    int mBalance;
};

CAccount::CAccount(int number) {
    mNumber=number;
    mBalance=0;
}

void CAccount::printAccount() {
    cout << "Account # " << mNumber <<
        ". Balance = $" << mBalance << "." << endl;
}

int CAccount::Deposit(int amount) {
    if (amount<=0) return -1;
    mBalance+=amount;
    return mBalance;
}

int CAccount::Withdraw(int amount) {
    if (amount<=0 || amount>mBalance) return -1;
    mBalance-=amount;
    return mBalance;
}

void main() {
    CAccount a(2);

    a.printAccount();

    cout << "a.Deposit(100): " << a.Deposit(100) << endl;
    a.printAccount();
}

Выход программы Account3.cpp:

Account # 2. Balance = $0.
a.Deposit(100): 100
Account # 2. Balance = $100.

Для сложных объектов обычно возникает необходимость в выполнении некоторых действий и перед прекращением существования объекта. Эту задачу выполняет специальный метод - деструктор. Имя деструктора начинается с знака ~, за которым следует имя класса. У деструктора нет параметров. Он вызывается непосредственно перед прекращением существования объекта.

Порядок вызова конструкторов и деструкторов определяется местом объявления (или явного создания) объекта и его классом памяти. Общее правило гласит, что деструкторы выполняются в порядке обратном выполнению конструкторов. Конструкторы объектов, объявляемых на внешнем уровне, выполняются до, а деструкторы - после выполнения функции main. Для локальных объектов конструктор выполняется в момент достижения описания, а деструктор при выходе из соответствующего блока, если объект имеет класс памяти auto, или после выполнения функции main, если объект имеет класс памяти static. Пример WhenCalled.cpp демонстрирует выполнение этих правил.

// WhenCalled.cpp - Когда вызываются конструкторы и деструкторы

#include <iostream.h>

class C {
public:
    char mName;
    C(int name) {
        mName=name;
        cout << " " << mName;
    }
    ~C() {
        cout << " ~" << mName;
    }
};

C a('A');
C b('B');

void func() {
    C c('C');
    static C d('D');
    C *pe=new C('E');
}

void main() {
    cout << " main";
    C *pf=new C('F');
    func();
    C g('G');
    delete pf;
    func();
    cout << " ~main";
}

Выход программы WhenCalled.cpp:

 A B main F C D E ~C G ~F C E ~C ~main ~G ~D ~B ~A

Если объект создаётся в динамической памяти (с помощью оператора new), то для его уничтожения необходимо обязательно выполнить оператор delete. В примере объект с именем F "живёт" от момента создания до момента уничтожения, которые соответственно определяются моментами вызова операторов new и последующего delete. А вот для объекта с именем E, который создаётся в теле функции func вызовом оператора new, вызов оператора delete совсем не предусмотрен. Это явная ошибка в программе и, к сожалению, очень распространённая. В данном случае при выходе из тела функции переменная-указатель pe прекращает своё существование, и объект, который с ней связан, становится недостижимым. Он существует, занимает память, но доступа к нему нет! Такие объекты называют "мусором". Более того, при втором вызове функции func создаётся ещё один такой объект. Как видно из выходных данных программы, деструкторы для этих объектов вообще не вызываются. Этот пример наглядно показывает куда может "исчезать" память в ходе выполнения программы.

4. Отделение интерфейса от реализации

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

В ходе разработки программы каждый класс представляется в виде двух исходных модулей. Первый модуль - это заголовочный файл, который содержит определение класса. Этот модуль называют модулем интерфейса класса. Второй модуль - это модуль реализации. Он содержит код, реализующий методы класса. Главная программа, которая создаёт объекты класса и выполняет с ними необходимые операции, составляет ещё один отдельный модуль. Его называют модулем клиента. Рассмотрим пример такой организации программы.

// Account.h - Интерфейс класса CAccount

#ifndef ACCOUNT_H
#define ACCOUNT_H

class CAccount {
public:
    CAccount(int number);
    void printAccount();
    int getBalance() {return mBalance;}
    int getNumber() {return mNumber;}
    int Deposit(int amount);
    int Withdraw(int amount);
private:
    int mNumber;
    int mBalance;
};

#endif

// Account.cpp - Реализация класса CAccount

#include <iostream.h>
#include "Account.h"

CAccount::CAccount(int number) {
    mNumber=number;
    mBalance=0;
}

void CAccount::printAccount() {
    cout << "Account # " << mNumber <<
        ". Balance = $" << mBalance << "." << endl;
}

int CAccount::Deposit(int amount) {
    if (amount<=0) return -1;
    mBalance+=amount;
    return mBalance;
}

int CAccount::Withdraw(int amount) {
    if (amount<=0 || amount>mBalance) return -1;
    mBalance-=amount;
    return mBalance;
}

// AccountClient.cpp - Отделение интерфейса от реализации

#include <iostream.h>
#include "Account.h"

void main() {
    CAccount a(21);

    cout << "a.Deposit(100): " << a.Deposit(100) << endl;
    cout << "a.Withdraw(75): " << a.Withdraw(75) << endl;
    cout << "a.Withdraw(-5): " << a.Withdraw(-5) << endl;
    a.printAccount();
}

Выход программы AccountClient.cpp:

a.Deposit(100): 100
a.Withdraw(75): 25
a.Withdraw(-5): -1
Account # 21. Balance = $25.

Отметим узловые мометы построения примера.

В модуле интерфейса Account.h с помощью директивы ifndef препроцессора проверяется определено ли имя ACCOUNT_H. Если оно не определено, то такое имя определяется директивой define препроцессора с последующим включением текста определения класса в программу. В противном случае определение класса в программу не включается. Директива препроцессора endif формально определяет границу включаемого текста. Этот механизм необходим, чтобы исключить повторное включение описания, что может случиться в сложной программе.

В начале модуля реализации Account.cpp должна быть директива препроцессора #include "Account.h" для включения заголовочного файла с определением класса. Кавычки, в которые заключается имя заголовочного файла, указывают, что этот файл не является системным и находится в каталоге вместе с другими файлами проекта. Точно такую же директиву надо включить и в начало модуля клиента. В обоих случаях это необходимо для того, чтобы компилятор мог использовать определение класса при компиляции соответствующих модулей в объектные файлы. Объектный файл модуля реализации может быть передан потребителю, но при этом, очевидно, надо передать и модуль интерфейса в форме заголовочного файла, чтобы его можно было включить в модуль клиента для компиляции последнего. Объектный модуль реализации подключается к программе клиента при её компоновке.


Вв

1.
 2.
 3.
 4.
 5.
 6.
 7.

1.

2.

3.

4.

5.

6.

7.

Copyright г Барков Валерий Андреевич, 2000


Темы, которые предстоит раскрыть:
- Потоки ввода-вывода
- Обработка исключительных ситуаций
- Перегрузка операторов
- Детали классов (указатель this, константные члены классов, статические члены классов)
- Множественное наследование
- Шаблоны функций
- Шаблоны классов
- Стандартные библиотеки классов (в частности строковый в BCppB)


Динамические структуры данных

1. Операторы new и delete
 2. Строки в динамической памяти
 3. Циклические списки

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

1. Операторы new и delete

Оператор new резервирует память в куче для экземпляра переменной заданного типа и возвращает указатель на неё или 0, если необходимая память не может быть выделена. Оператор delete выполняет обратное действие - освобождает память в куче связанную с указателем. Пример:

int *aPtr = new int;
. . .
delete aPtr;

Для обращения к переменной, связанной с указателем, используется операция * - разименовывания указателя. Для обращения к полям структур предусмотрена специальная операция - ->. Запись вида aRec->Field эквивалентна записи (*aRec).Field. Скобки здесь необходимы, так как приоритет операции .выше приоритета операции *.

Для создания и удаления динамических массивов предусмотрена специальная форма операторов new и delete:

int *Arr = new int[7];
. . .
Arr[4]=137;
. . .
delete [] Arr;

2. Строки в динамической памяти

Программа DynStr.cpp иллюстрирует работу со строками, размещаемыми в динамической памяти. Функция dyncpy создаёт динамическую копию заданной строки. Функция dyncat выполняет конкатенацию двух динамических строк. Для этого она сначала резервирует необходимую область памяти, затем копирует в неё первую строку, выполняет конкатенацию этой строки со второй и, наконец, освобождает память занимаемую исходными строками. Эта функция имеет преимущество перед стандартной функцией strcat, которое выражается в том, что не надо беспокоиться о переполнении литерного массива, в котором собирается результирующая строка. На зато на выполнение dyncat требуется значительно больше времени.

// DynStr.cpp - Динамические строки
#include <iostream.h>
#include <string.h>

char *dyncpy(const char *s) {
    char *p = new char[strlen(s)+1];
    strcpy(p,s);
    return p;
}

char *dyncat(char *s1, char *s2) {
    char *p = new char[strlen(s1)+strlen(s2)+1];
    strcpy(p,s1);
    strcat(p,s2);
    delete [] s1;
    delete [] s2;
    return p;
}

void main() {
    char str1[] = "Queen Elizabeth I";
    char str2[] = " of England";

    char *ds1 = dyncpy(str1);
    char *ds2 = dyncpy(str2);
    char *ds  = dyncat(ds1,ds2);

    cout << ds << endl;
}

Выход программы DynStr.cpp:

Queen Elizabeth I of England

3. Циклические списки

Организация связных списков - классический пример применения динамических структур данных. Программа CycleList.cpp иллюстрирует работу с циклическим списком. Циклический список задаётся указателем на последний элемент, а в поле связи последнего элемента помещают указатель на первый элемент списка. Такая организация очень удобна, так как позволяет легко получить доступ к обоим концевым элементам списка. Некоторые издержки связаны с необходимостью поддержания списка в зацикленном состоянии, но они незначительны. В программе DynStr.cpp функция Append обеспечивает включение нового элемента в конец списка, а функция Print выполняет вывод элементов списка.

// CycleList.cpp - Циклические списки
#include <iostream.h>

struct SElem {int Info; SElem *Link;};

SElem *List = 0;

void Append(int v) {
    SElem *p = new SElem;
    p->Info = v;
    if (List) {
        p->Link = List->Link;
        List->Link = p;
        List = p;
    } else {
        List = p;
        p->Link = p;
    }
}

void PrintList() {
    cout << "[";
    if (List) {
        SElem *p = List->Link;
        cout << p->Info;
        while (p != List) {
            p = p->Link;
            cout << "," << p->Info;
        }
    }
    cout << "]" << endl;
}

void main() {
    int a[] = {2,3,5,7};
    PrintList();
    for (int i=0;i<4;i++) {
        Append(a[i]);
        PrintList();
    }
}

Выход программы DynStr.cpp:

[]
[2]
[2,3]
[2,3,5]
[2,3,5,7]


Управление программой

1. Основные операторы
 2. Синтаксис структурных операторов
 3. Оператор цикла do
 4. Опрератор цикла for
 5. Оператор switch
 6. Определение именованных констант
 7. Операторы ввода-вывода

1. Основные операторы

Основной оператор - это выражение за которым стоит знак ;. Просто знак ; - это пустой оператор.

2. Синтаксис структурных операторов

Составной оператор: { S1 S2 ... Sn } .
Условный оператор (if): if (B) S1 [ else S2 ] .
Оператор цикла while: while (B) S .
Оператор цикла do: do S while (B); .
Оператор цикла for: for (E1;E2;E3) S .
Оператор выбора switch:
switch (E)
{
   case C1: [ S11 S12 ... ]
   case C2: [ S21 S22 ... ]
   ...
  [default: S1 [ S2 ...] ]
}

Обозначения: S - операторы, E - выражения, С - константы.

3. Оператор цикла do

Логическое выражение B определяет условие продолжения цикла.

4. Опрератор цикла for

Его семантика легко выражается через оператор while: E1; while (E2) { S E2; } . Но есть одно ислючение - если выражение E2 пропущено, то его значение в схеме замещения полагается отличным от нуля, то есть цикл будет работать бесконечно долго. Для преждевременого выхода из цикла (любого типа) предусмотрен специальный оператор break; . Оператор continue; выполняет передачу управления на следующий по порядку цикл. Пример:
int s=0;
for (int i=1; i<=10; i++) s+=i;
На месте выражений E1 и E2 могут стоять списки выражений. Пример:
for (int i=1,j=5, i<=5; i++,j--) cout << i+j << endl;
Описатель типа int в последнем примере относится к обеим переменным. Такой способ описания типа параметров оператора for равноценен обычному описанию типа на уровне оператора for, то есть в последующих операторах for на том же самом уровне эти же переменные описыват уже не надо. И ещё один пример:
int i=0; for (;;) { i++; if (i==5) break; }

5. Оператор switch

Выражение E и константы C1, C2, ... должны быть целого типа. Управление передаётся той группе операторов, которые следуют за константой с значением равным значению выражения E. Далее выполняются операторы следующей группы и так далее до конца оператора. Чтобы не допустит передачи управления на следующую группу операторов необходимо текущую группу завершить оператором break; . операторы группы default выполняются, если нет константы с значением равным значению E. Пример:
switch (mon)
{
   case 4: case 6: case 9: case 11: d=30; break;
   case 2: d=28; break;
   default: d=31;
}

6. Определение именованных констант

Пример: const double d=1.37; . Именованные константы не могут быть l-value.

7. Операторы ввода-вывода

Основаны на операциях >> и <<. Операнами операции >>, принимающими вводимые значения, могут быть только переменные. Данные из входного потока автоматически приводятся к типам этих переменных. Разделителями данных во входном потоке служат пробельные символы (пробел, символ перевода каретки, символ табуляции).

Операндами операции <<, значения которых выводятся в выходной поток, могут быть выражения. Выводимые значения автоматически приводятся к строковому типу. Кроме того операндами операции << могут быть манипуяторы потока, которые управляют форматом и размещением данных в потоке вывода. С манипулятором eoln мы уже знакомы. Для использования других манипуляторов в программу необходимо включить заголовочный файл iomanip.h . Для начала рассмотрим только два манипулятора:
setw(w) - устанавливает ширину поля вывода равной w знакам,
setprecision(d) - устанавливает размер значащей части числа равным d знакам.
Действие этих манипуляторов распространяется на вывод только одного значения.

Пример:

#include <iostream.h>
#include <iomanip.h>

void main() {
    int i;
    double d;
    char s[20];

    cout << "Enter i(int), d(double) and s(string): ";
    cin >> i >> d >> s;

    cout << "i = " << setw(4) << i+1
        << " d = " << setw(12) << setprecision(4) << d
        << " s = " << s << '.' << endl;
}

Пример диалога:

Enter i(int), d(double) and s(string): 137 123.4567 Моцарт
i =  138 d =        123.5 s = Моцарт.


Функции

1. Стандартные математические функции
 2. Определение функции
 3. Рекурсия
 4. Встраиваемые функции
 5. Передача параметра по ссылке
 6. Аргументы по умолчанию
 7. Перегружаемые функции
8. Пример разработки программы
 9. Классы памяти
10. Модульность

Функции - это основной механизм структурного программирования. Их использование повышает уровень программирования с помощью абстракции.

1. Стандартные математические функции

<math.h>: sqrt(x), exp(x), log(x), log10(x), fabs(x), ceil(x), floor(x), pow(x,y), fmod(x,y), sin(x), cos(x), tan(x) .
Все параметры и результаты типа double.

2. Определение функции

Пример: int Sqr(int x) { return x*x; }

Терминология: заголовок, тип параметра, тип возвращаемого значения, тело функции, блок, оператор прототипа.

Оператор прототипа необходим в тех случаях, когда вызов функции выполняется до её определения. Пример: int Sqr(int x); или просто int Sqr(int); .

Тип возвращаемого значения указывается в начале определения функции.Если функция не возвращает значения, то надо указать тип void. Этот же тип на месте списка формальных параметров означает, что у функции нет формальных параметров.

Последним исполняемым оператором функции должен быть оператор return E; , где E - выражение, значение которого возвращается функцией. Если функция не возвращает значения, то последним должен быть оператор return; или естественное окончание определения функции.

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

В C++ все функции определяются на одном уровне. Кроме локальных переменных, определяемых в блоках функций, можно определять переменные и на внешнем уровне - их называют глобальными.

Вызов функции: a = Sqr(b); . По умолчанию выполняется автоматическое приведение типов фактических параметров к типам формальных параметров. При этом, если преобразование выполняется в нисходящем порядке ( long double, double, float, unsigned long [int], long [int], unsigned [int], int, unsigned short [int], short [int], unsigned char, char), то возможны искажения значений. Рекомендуется всегда использовать явные преобразования типов.

3. Рекурсия

Допускаются рекурсивные обращения в определении функции. Пример:

int fact(int n) {
    if (n<=1) return 1;
    return n*fact(n-1);
}

4. Встраиваемые функции

Пример: inline double cube(double x) { return x*x*x; }
В этом случае тело функции непосредственно встраивается в последовательность команд вызывающей программы, что приводит к увеличению производительности, но и, к сожалению, к увеличению объёма программы при большом теле функции и многократных вызовах.

5. Передача параметра по ссылке

По умолчанию скалярные параметры передаются по значению. Передача параметра по ссылке даёт возможность возвращать значения через параметры. Пример:
void sqr(int &n) { n*=n; }

Ссылки можно использовать и независимо от функций. Пример:
int Count = 0;
int &cRef = Count;
Инициализация ссылочной переменной при определении обязательна! Ссылочная переменная может использоваться наряду с основной переменной как её второе имя.

6. Аргументы по умолчанию

Пример:
int VolBox(int l=1, int w=1, int h=1) { return l*w*h; }
Вызов: v=VolBox(3,4);
Если при вызове какой-либо параметр опущен, то и все последующие должны быть опущены.

7. Перегружаемые функции

Компилятор C++ распознаёт функции по их сигнатуре. Сигнатура составляется из имени функции и списка типов параметров в порядке их перечисления. Две функции считаются разными, если различаются их сигнатуры. Пример:
int sqr(int x) { return x*x; }
double sqr(double x) { return x*x; }
Следует быть осторожным при использовании параметров по умолчанию - компилятор всегда должен иметь возможность определить к какой функции относится тот или иной вызов.

8. Пример разработки программы

Составим программу, которая моделирует поведение игрока в craps по следующим правилам. Игрок бросает две кости. Если выпало в сумме 7 или 11, то игрок выиграл. Если выпало 2, 3 или 12 - проиграл (craps). Любое другое значение запоминается как point и игрок бросает кости, пока не выпадет снова point или 7. В первом случае игрок выиграл, во втором - проиграл.

При составлении программы мы следуем структурному программированию и выделяем отдельную функцию для моделирования процесса бросания костей - int roollDice(void); . Попутно вводится ряд новых понятий: перечисляемый тип, функции time, srand и rand. Программа:

// Craps.cpp
// Program to simulate the game of craps
#include <iostream.h>
#include <stdlib.h>    // for srand(), rand()
#include <time.h>      // for time()

int rollDice(void);

void main()
{
    enum Status { CONTINUE, WON, LOST };
    int sum, myPoint;
    Status gameStatus;
    char Ch;
    do
    {
    srand(time(NULL));
    sum=rollDice();

    switch (sum)
        {
    case 7: case 11: gameStatus=WON; break;
    case 2: case 3: case 12: gameStatus=LOST; break;
    default:
        gameStatus=CONTINUE;
            myPoint=sum;
        cout << "Point is " << myPoint << endl;
            break;
        }

        while (gameStatus == CONTINUE)
        {
            sum=rollDice();
        if (sum==myPoint)
            gameStatus=WON;
        else
        if (sum==7) gameStatus=LOST;
        }

    if (gameStatus==WON)
        cout << "Player wins" << endl;
    else
        cout << "Player loses" << endl;
    cout << "Entry 'e' to exit or other to continue" << endl;
    cin >> Ch;
    } while (Ch !='e');
}

int rollDice(void)
{
    int die1=rand() % 6 +1;
    int die2=rand() % 6 +1;
    int workSum=die1+die2;
    cout << "Player rolled " << die1 << " + "
        << die2 << " = " << workSum << endl;
    return workSum;
}

Пример выхода программы Craps.cpp:

Player rolled 5 + 6 = 11
Player wins
Entry 'e' to exit or other to continue
c
Player rolled 1 + 4 = 5
Point is 5
Player rolled 6 + 5 = 11
Player rolled 5 + 6 = 11
Player rolled 4 + 3 = 7
Player loses
Entry 'e' to exit or other to continue
e

Дадим необходимые пояснения к программе. Оператор enum позволяет вводить перечислимый тип в программу с значениями, задаваемыми именами в скобках. Фактически используются константы целого типа с значениями 0, 1, 2 и так далее. Функция time используется для инициализации (srand) генератора псевдослучайных чисел (rand) различающимися значениями при каждом запуске программы. Если этого не сделать, то последовательность генерируемых чисел будет в точности повторяться при каждом запуске.

9. Классы памяти

Всякая переменная имеет тип, размер и значение. В ходе компиляции эти атрибуты связываются с именем переменной. Кроме того есть ещё три атрибута - класс памяти (storage class), область видимости (scope) и компонуемость (linkage), которые мы будем относить к имени. В C++ для указания этих атрибутов предусмотрены четыре описателя класса памяти: auto, register, extern и static. Класс памяти определяет время существования имени (понимай объекта с данным именем) в памяти. Область видимости - это область текста программы, из которой возможно обращение к имени. Компонуемость определяет в многомодульных программах известно ли имя только в текущем модуле, или ещё в каком-нибудь модуле, в котором есть соответствующее описание.

Классы auto и register относят к автоматическому классу, а extern и static - к статическому.

К автоматическому классу могут принадлежать только локальные переменные функций. Память для переменных автоматического класса выделяется при входе в блок, в котором они описаны, и освобождается при выходе из блока. По умолчанию локальные переменные блока имеют класс auto. Класс register воспринимается компилятором как рекомендация разместить переменную в регистре процессора, чтобы сократить время выполнения программы. По умолчанию инициализация переменных автоматического класса не выполняется, а если она предусмотрена, то выполняется при каждом входе в блок.

Память для статических переменных резервируется на всё время выполнения программы. Глобальные переменные и имена функций по умолчанию имеют класс памяти extern. С классом памяти static можно объявить локальную переменную блока. Эта переменная будет сохранять своё значение и после выхода из функции. Инициализация переменных статического класса выполняется обязательно, но лишь только один раз при запуске программы. По умолчанию для числовых данных инициализация выполняется нулевыми значениями.

Для имён в C++ существует четыре вида областей видимости: функция (function scope), файл (file scope), блок (block scope) и прототип функции (function-prototype scope).

Функция является областью видимости для меток оператора goto.

Область видимости блок означает, что имя видимо с момента его описания до конца блока, за исключением вложенных блоков, в которых описаны такие же имена. Такую область видимости имеют имена локальных переменных, в том числе и переменных класса памяти static, не смотря на то, что они сохраняют свои значения и после выхода из функции.

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

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

10. Модульность

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

Модуль в C++ это файл. Описатели extern и static определяют атрибут компонуемости имени. Описатель extern означает, что ниже в текущем файле или в каком-нибудь другом файле программы определена глобальная переменная и она доступна в текущем файле везде после описания. Аналогичным образом оператор прототипа функции означает, что ниже в текущем файле или в каком-нибудь другом файле определена функция, которая может быть вызвана в теле любой функции текущего файла, расположенной ниже оператора прототипа. Такое межмодульное связывание называется внешней компоновкой (external linkage). Описатель static определяет внутримодульную компоновку (internal linkage), при которой связывание ограничено текущим файлом. Значение глобальной переменной с описателем static не доступно в других файлах. Это относится и к функциям. Для функций описатель static указывается только в операторе прототипа функции или в её определении, в зависимости от того, что приводится раньше.

В качестве примера приведём программу, состоящую из двух модулей - MainM.cpp и SubM.cpp:

// MainM.cpp
#include <iostream.h>

int a;

static void fa(void);
void fb(void);
extern int b;

void main()
{
    a = 17;
    fa();
    fb();
    cout << "b = " << b << endl;
}

void fa(void)
{
    cout << "Hello from fa() in MainM.cpp" << endl;
}
// SubM.cpp
#include <iostream.h>

static void fa(void);
extern a;

int b = 21;

void fb(void)
{
    fa();
}

void fa(void)
{
    cout << "Hello from fa() in SubM.cpp" << endl;
    cout << "a = " << a << endl;
}

Изучение выхода программы, составленной из модулей MainM.cpp и SubM.cpp, поможет понять порядок вызова функций в ней и обращения к внешним данным:

Hello from fa() in MainM.cpp
Hello from fa() in SubM.cpp
a = 17
b = 21

В практике программирования на C++ общие для нескольких модулей описания принято выносить в отдельный заголовочный файл, как это показано в следующем примере, построенном на основе предыдущего:

// Decl.h
#include <iostream.h>
extern int a;
extern int b;
void fb(void);
// MainM.cpp
#include "Decl.h"

int a;
static void fa(void);

void main() {...}
void fa(void) {...}
// SubM.cpp
#include "Decl.h"

static void fa(void);
int b = 21;

void fb(void) {...}
void fa(void) {...}

Тела функций в примере остались без изменения. Кавычки, в которые заключено имя h-файла в операторе include, означают, что данный заголовочный файл находится в каталоге проекта программы, в котором находятся и другие исходные файлы. Угловые же скобки показывают, что заголовочный файл является системным. Общий заголовочный файл - это также удобное место для размещения описаний типов общих для нескольких модулей и по сути он содержит описание общего интерфейса нескольких модулей. В этой связи определения функций класса static разумно оставить в собственных модулях, так как они не относятся к общему интерфейсу.


C++

(Конспект лекций)

Содержание

1. Введение в C++
 2. Управление программой
 3. Функции
 4. Массивы и структуры
 5. Указатели
6. Обработка строк
 7. Динамические структуры данных
8. Классы и абстракция данных
9. Композиция
10. Наследование
11. Полиморфизм и виртуальные функции

Справочник

1. Приоритеты операций в C++
2. Ключевые слова C++


Наследование

1. Определение производного класса
 2. Оператор присваивания для объектов классов и приведение типов указателей
 3. Пример оращения к объектам базового и производных классов

Наследование - это ещё один метод построения нового класса, называемого производным, на основе некоторого класса, называемого базовым. Определение производного класса может содержать новые атрибуты и методы. Таким образом, производный класс, сохраняя функциональность базового класса (наследование), получает ещё и дополнительную функциональность. Так как от базового класса могут наследовать несколько производных классов, а те, в свою очередь, могут выступать в роли базовых для образования новых производных классов и так далее, то могут создаваться очень мощьные иерархии классов с развитыми возможностями. Родовая схема порождения классов в другом направлении может рассматриваться как схема обобщения классов. Для графического изображения этих отношения классов используют обозначения двух видов: A <- B <- C или ( ( ( A) B ) C ), где A порождает B, а B порождает C. Стрелка в первой схеме означает обобщение. Скобками во второй схеме демонстрируется, что конкретные классы имеют "больший объём" и что базовый класс содержит то общее, что присуще всем его производным классам.

1. Определение производного класса

Выше мы определили класс CAccount для представления счетов в банке. Предположим, что банк решил ввести новый вид счетов, например, таких, что на них "положить сумму" можно только один раз. Дадим новому классу имя CAccountM. Ясно, что многие члены класса CAccountM могут быть унаследованы от класса CAccount, но должны появиться и новые. Можно предложить такое описание производного класса CAccountM.

// AccountM.h - Интерфейс класса CAccountM

#ifndef ACCOUNTM_H
#define ACCOUNTM_H
#include "Account.h"

class CAccountM: public CAccount {
public:
    CAccountM(int number);
    void printAccount();
    int Deposit(int amount);
    int Withdraw(int amount);
private:
    int mInit;
};

#endif

Имя базового класса указывается в заголовке определяемого производного класса. Слово public определяет способ наследования, при котором все члены базового класса сохраняют свои классы доступа и в производном классе, кроме членов класса private, которые в производном классе непосредственно не доступны, но доступ к ним может быть получен через методы базового класса с классом доступа public.

Всего для членов класса существует три класса доступа, которые означают следующее:
public - члены класса доступны везде в файле;
protected - члены класса могут быть доступны в методах данного класса и в методах производных классов;
private - члены класса доступны только в методах данного класса.

Кроме способа наследования public могут быть заданы также способы protected и private, но они на практике применяются очень редко и рассматривать их здесь мы не будем.

В этой связи, желая члены класса CAccount сделать непосредственно доступными для методов класса CAccountM, мы раздел private в определении класса CAccount преобразуем в раздел protected. Желая создать возможность порождения от класса CAccount производных классов, мы должны были позаботиться об этом ещё при его создании, но тогда мы ещё не знали о такой возможности. Во всём остальном мы сохраняем модули интерфейса и реализации класса CAccount в их последней редакции.

В класс CAccountM мы вводим один новый атрибут mInit для отметки факта выполнения операции Deposit и три новых метода printAccount, Deposit и Withdraw с тем же самым назначением, что и в базовом классе. Так получилось, что и сигнатуры этих методов полностью совпадают с сигнатурами соответствующих методов в базовом классе. В таких случаях говорят о перекрытии методов. Аналогичным образом могут быть перекрыты и атрибуты. Не смотря на перекрытие возможность обращения с членам базового класса сохраняется - для этого достаточно использовать следующий синтаксис: <Имя базового класса> :: <Обращение к члену базового класса>. В случае отсутствия перекрытия для обращения к членам базового класса можно использовать обычный синтаксис.

Теперь посмотрим на модуль реализации класса CAccountM.

// AccountM.cpp - Реализация класса CAccountM

#include <iostream.h>
#include "Account.h"
#include "AccountM.h"

CAccountM::CAccountM(int number): CAccount(number) {
    mInit=0;
}

int CAccountM::Deposit(int amount) {
    if (amount<=0 || mInit>0) return -1;
    mBalance+=amount;
    mInit++;
    return mBalance;
}

int CAccountM::Withdraw(int amount) {
    return -1;
}

void CAccountM::printAccount() {
    cout << "M";
    CAccount::printAccount();
}

Отметим особенности реализации производного класса. Главное это вызов конструктора базового класса - он записывается сразу за заголовком конструктора производного класса через ":". Другая особенность имеет отношение к рассматриваемому примеру. В новом методе printAccount мы используем функциональность метода printAccount базового класса.

В результате мы c минимальными затратами получили новый класс CAccountM.

2. Оператор присваивания для объектов классов и приведение типов указателей

В С++ переменной-объекту можно присвоить значение переменной-объекта того же или производного типов. Надо помнить о том, что такое присваивание выполняется как побитовое, то есть путём буквального копирования битов памяти. В этой связи следует проявлять осторожность и особенно при использовании в качестве атрибутов указателей - данные, которые связаны с указателями при таком присваивании, естественно, не копируются. Присваивание переменной-объекту производного типа зачения переменной-объекта базового типа не допускается, так как является бессмысленным.

Аналогичным образом выполняется и присваивание указателей классовых типов. В случае необходимости приведения указателя базового типа к одному из производных типов следует использовать оператор явного приведения типа, но разименовывать такой указатель можно только в полной уверенности, что с ним фактически связан объект соответствующего типа. Пример корректной работы с указателем объекта.

CAccountM *pam = new CAccountM(12);
CAccount  *pa = pam;
. . .
(CAccountM *)pa -> printAccount();

Следует помнить, что при обращении по указателю к объекту некоторого производного класса фактическое обращение выполняется к членам класса в строгом соответствии с типом указателя.

3. Пример обращения к объектам базового и производного классов

Рассмотрим на примере клиента классов CAccount и CAccountM.

// AccountMClient.cpp - Клиент классов CAccount и CAccountM

#include <iostream.h>
#include "Account.h"
#include "AccountM.h"

void main() {
    cout << "1.\n"; // Операции с объектом базового класса
    CAccount a(21);
    cout << "a.Deposit(200): " << a.Deposit(200) << endl;
    cout << "a.Deposit(100): " << a.Deposit(100) << endl;
    cout << "a.Withdraw(50): " << a.Withdraw(50) << endl;
    a.printAccount();

    cout << "\n2.\n"; // Операции с объектом порождённого класса
    CAccountM am(37);
    cout << "am.Deposit(200): " << am.Deposit(200) << endl;
    cout << "am.Deposit(100): " << am.Deposit(100) << endl;
    cout << "am.Withdraw(50): " << am.Withdraw(50) << endl;
    am.printAccount();

    cout << "\n3.\n"; // Выбор метода в зависимости от типа указателя
    CAccountM *pam = new CAccountM(77);
    CAccount *pa = pam;
    cout << "pam->Deposit(200): " << pam->Deposit(200) << endl;
    cout << "pa->Deposit(100): " << pa->Deposit(100) << endl;
    cout << "pa->Withdraw(50): " << pa->Withdraw(50) << endl;
    pam->printAccount();
    pa->printAccount();
    delete pam;
}

Выход программы AccountMClient.cpp:

1.
a.Deposit(200): 200
a.Deposit(100): 300
a.Withdraw(50): 250
Account # 21. Balance = $250.

2.
am.Deposit(200): 200
am.Deposit(100): -1
am.Withdraw(50): -1
MAccount # 37. Balance = $200.

3.
pam->Deposit(200): 200
pa->Deposit(100): 300
pa->Withdraw(50): 250
MAccount # 77. Balance = $250.
Account # 77. Balance = $250.

Анализ выхода программы AccountMClient.cpp показывает, что в третьем случае поведение одного и того же объекта зависит от типа указателя, по которому происходит обращение.


Введение в C++

1. Общие замечания
 2. Простые примеры
 3. Лексика
 4. Некоторые простые типы данных
 5. Операции в порядке уменьшения приоритета
 6. Выражения

1. Общие замечания

C++ (Bjarne Stroustrup, 1980) сложный язык ООП, многое заимствовол от C (Dennis Ritchie, 1972, от B). Оба языка возникли в недрах Bell Laboratories в рамках ОС UNIX.
Изучению подлежит сам C++, его библиотека стандартных классов и ANSI C standard library.
Процесс подготовки и выполнения программы: редактирование исходных модулей (*.h, *.cpp), препроцессорная обработка, компиляция (*.obj), связывание (*.exe, *.dll), загрузка, выполнение (cin ("see-in"), cout ("see-out"), cerr).

2. Простые примеры

// First.cpp - первый пример
#include <iostream.h>

void main()
{
   cout << "Привет из C++!\n";
}

В примере:
include - оператор препроцессора;
main - главная функция;
void - указывает, что функция не возвращает
значения;
cout - объект стандартного потока вывода;
<< - операция включения;
"..." - строковая константа;
\n - escape-символ.
Жирным шрифтом выделены ключевые слова.

// Second.cpp - второй пример
#include <iostream.h>

void main()
{
   int a, b, sum;

   cout << "Введите а и b: ";
   cin >> a >> b;
   sum = a + b;
   cout << a << " + " << b << " = " << sum << endl;
}

3. Лексика

Идентификатор - последовательность из знаков A..Z, a..z, 0..9 (не м.б. первыми), _. C++ регистрозависимый.
Ключевое слово - int, void, ... .
Escape-символы - \n (новая строка), \t (табуляция), \\ - backslash, \" - кавычка и др. Применяются в литерных данных. Пример: "Скажите\n\"Да\"" .
Комментарии - концевой (//), многострочный (/* ... */).

4. Некоторые простые типы данных

Целые: char (1), short (2), int (4), long (4).
Вещественные: float (4), double (8), long double (8).

5. Операции в порядке уменьшения приоритета

Операции | Тип операций | Примеры
()[] | Скобки | a * (b + c)
++ -- + - ! (type) | Унарные (<-) | ++a, -b
* / % | Мультипликативные | a % b
+ - | Адитивные | a + b
<< >> | Включения / извлечения | cout << a
< <= > >= | Отношения | a <= b
== != | Эквивалентности | a != b
&& | Логическое И | a && b
|| | Логическое ИЛИ | a || b
?: | Условная (<-) | a > b ? a : b
= += -+ *= /+ %= | Присваивания (<-) | a += 2
, | Запятая | (a = b, b = c - 1)

Операции инкрементирования и декрементирования (++ и --). Обе операции ++a и a++ увеличивают значение a и возвращают результат. В первом случае результат - это новое значение переменной, а во втором - старое.

Операция (type) выполняет явное преобразование типа. Пример: double d = 2.56; int i = (int)d;

Операция / даёт целый результат, если оба операнда целого типа. Операция % - деление по модулю.

Логические операции (! - отрицание, || - дизъюнкция и && - конъюнкция) возвращают одно из двух значений: 0 - "ложь" и 1 - "истина". В логических выражениях целочисленные значения интерпретируются как догические: 0 - "ложь" и все остальные - "истина". Операции отношения и эквивалентности возвращают логические значения.

Условная операция B?E1:E2 возвращает значение выражения E1, если значение выражения B - "истина", и E2 - в противном случае.

Опрерации присваивания изменяют состояние памяти программы. Операция вида a+=b - это сокращённая запись операции a=a+b.

Операция , позволяет строить списки выражений. Выражения списка выполняются слева направо. Результат списка операций - результат последнего выражения.

6. Выражения

Выражение - это комбинация констант, переменных, вызовов функций и знаков операций. Из ограничений, кроме очевидных, которым должны удовлетворять выражения, отметим лишь, что левым опрерандом операции присваивания должно быть так называемое l-value. Аргументы опрераций ++ и -- тоже должны быть l-value. Точное определение l-value дать достаточно сложно. Простые переменные и переменные с индексами - это l-value. На первом этапе изучения языка можно ограничиться этим списком. Экзотический пример: (a>b?a:b) = d. Порадок выполнения операций в выражении определяется их приоритетом. Порядок выполнения операций одного типа определяется их ассоциативностью. Различают левоассоциативные и правоассоциативные операции. Первые выполняются слева направо. Вторые - справа налево (они в таблице операций отмечены знаком <- ).


Ключевые слова C++

C и C++

auto      break     case     char       const     continue
default   do        double   else       enum      extern
float     for       goto     if         int       long
register  return    short    signed     sizeof    static
struct    switsh    typedef  union      unsigned  void
volatile  while

Только C++

asm       catch     class    delete     friend    inline
new       operator  private  protected  public    template
this      throw     try      virtual


Вв

Содержание

Приоритеты операций в C++

Операции в таблице приведены в порядке уменьшения приоритета сверху вниз.

Операция | Ассоциативность
:: | <- (для унарной) и -> (для бинарной)
() [] -> . | ->
++ -- + - ! ~ (type) * & sizeof | <-
.* ->* | ->
* / % | ->
+ - | ->
<< >> | ->
< <= > >= | ->
== != | ->
& | ->
^ | ->
| | ->
&& | ->
|| | ->
?: | <-
= += -+ *= /= %= &= |= <<= >>= | <-
, | ->


Указатели

1. Описание указателей
 2. Передача параметров функции по ссылке
 3. Использование описателя const с указателями
 4. Оператор sizeof
 5. Арифметика указателей
 6. Массив указателей
 7. Указатели на функции

1. Описание указателей

Указатели - это одно из наиболее мощных средств C++. Значение переменной-указателя - это адрес. Принято считать, что имя переменной является прямой ссылкой на её значение. С именем переменной ассоциирован адрес той области памяти, в которой хранится её значение. Но эта ассоциация существует лишь в ходе компиляции программы. Указатели же дают возможность косвенной ссылки на значение переменной, которую можно установить или изменить в ходе выполнения программы - именно это обстоятельство придаёт программированию с использованием указателей большую гибкость.

Для описания переменной-указателя используется оператор *. Пример:
int a;
int *aPtr = &a;
В примере описана переменная-указатель aPtr, которая инициализирована значением адреса переменной a, то есть можно сказать, что указатель aPtr указывает на переменную a. Можно описать указатель без инициализации, но тогда нельзя сказать на что он указывает, до тех пор пока ему не будет присвоено какое-нибудь разумное значение. Знак & - это оператор адреса. Он возвращает адрес переменной, которой предшествует. Указатели в C++ типированные. В примере aPtr - это указатель на переменную типа int и только int. Это важно, так как некоторые свойства указателя определяются свойствами его базового типа.

Для обращения к значению переменной, на которую указывает указатель, существует оператор разименовывания указателя - *. Конструкцию *aPtr можно использовать также как имя переменной a, за тем отличием, что в ходе выполнения программы с указателем aPtr можно связать другую переменную и тогда *aPtr ,будет уже обращением к значению другой переменной.

Указателю любого типа можно присвоить значение 0, что обозначает пустую связь. Разименовывать указатель пустой связи нельзя. Для указания пустой связи можно также использовать константу NULL, но при этом в программу надо включить заголовочный файл с её определением (можно iostream.h).

2. Передача параметров функции по ссылке

Указатели можно использовать для передачи параметров по ссылке. Этот вариант передачи параметров был привнесён из языка C. В этом случае программе передаётся по значению указатель на ту переменную, к которой предполагается выполнять доступ в полпрограмме. Программа CallByRef.cpp иллюстрирует эту возможность.

// CallByRef.cpp - Передача параметра функции по ссылке
#include <iostream.h>
void myswap(int *x, int *y) {
    int w = *x; *x=*y; *y=w;
}

void main() {
    int a=5, b=7;
    cout << "Before: " << a << " , " << b << endl;
    myswap(&a,&b);
    cout << "After: " << a << " , " << b << endl;
}

Выход программы CallByRef.cpp:

Before: 5 , 7
After: 7 , 5

3. Использование описателя const с указателями

При передаче указателя в качестве параметра функции мы предоставляем функции все возможности доступа к соответствующим данным. Разработчики C++ предусмотрели возможность ограничения такого доступа с помощью описателя const. Причём правильно расставляя описатели const можно предотвратить возможность изменения как связанных с указателем данных, так и значения самого указателя. Компилятор выдаст сообщение об ошибке при попытке сделать это. Программа UseConst.cpp иллюстрирует четыре варианта использования описателя const c указателями:
1) Нет ограничений на изменение данных и указателя.
2) Запрет на изменение данных.
3) Запрет на изменение значения указателя.
4) Запрет на изменение данных и значения указателя.

// UseConst.cpp - Использование const с указателями
#include <iostream.h>
void Sub1() {
    int a = 13, b = 17;
    int *p = &a;

    cout << "1: " << *p;
    *p=19;
    cout << " " << *p;
    p = &b;
    cout << " " << *p;
    cout << endl;
}

void Sub2() {
    int a = 13, b = 17;
    const int *p = &a;

    cout << "2: " << *p;
//  *p=19;
//  error C2166: l-value specifies const object
    p = &b;
    cout << " " << *p;
    cout << endl;
}

void Sub3() {
    int a = 13, b = 17;
    int * const p = &a;

    cout << "3: " << *p;
    *p=19;
    cout << " " << *p << endl;
//  p = &b;
//  error C2166: l-value specifies const object
}

void Sub4() {
    int a = 13, b = 17;
    const int * const p = &a;

    cout << "4: " << *p << endl;
//  *p=19;
//  error C2166: l-value specifies const object
//  p = &b;
//  error C2166: l-value specifies const object

}

void main() {
    Sub1();
    Sub2();
    Sub3();
    Sub4();
}

Выход программы UseConst.cpp:

1: 13 19 17
2: 13 17
3: 13 19
4: 13

4. Оператор sizeof

Оператор sizeof возвращает размер аргумента в байтах. В качестве аргумента можно использовать имя типа. В последнем случае оператор sizeof надо записывать только в функциональной форме (со скобками). Программа Sizeof.cpp иллюстрирует эти возможности.

// Siseof.cpp - Оператор sizeof
#include <iostream.h>

void main()
{
    int Arr[20], *Ptr = Arr;

    cout << "       sizeof(char) = " << sizeof(char) << endl
         << "      sizeof(short) = " << sizeof(short) << endl
         << "        sizeof(int) = " << sizeof(int) << endl
         << "       sizeof(long) = " << sizeof(long) << endl
         << "      sizeof(float) = " << sizeof(float) << endl
         << "     sizeof(double) = " << sizeof(double) << endl
         << "sizeof(long double) = " << sizeof(long double) << endl
         << "         sizeof Arr = " << sizeof Arr << endl
         << "         sizeof Ptr = " << sizeof Ptr << endl
         << endl;
}

Выход программы Sizeof.cpp:

       sizeof(char) = 1
      sizeof(short) = 2
        sizeof(int) = 4
       sizeof(long) = 4
      sizeof(float) = 4
     sizeof(double) = 8
sizeof(long double) = 8
         sizeof Arr = 80
         sizeof Ptr = 4

5. Арифметика указателей

Над указателями можно выполнять арифметические операции, но в ограниченных рамках. Так к указателю можно прибавить или от него отнять константу. Разрешается находить разность двух указателей одного типа. Допустимы операции ++, -- и сравнения. Интерпретацию результатов операций над указателями легко понять в связи с их связью с индексами элементов массивов. В C++ принято, что имя массива - это указатель на его первый элемент. Так, если a - это имя массива, то *a - это значение его первого элемента. Принято, что (a + 1) - это указатель на второй элемент массива, и тогда *(a + 1) - значение второго элемента массива. И так далее. Запись *(a + 4) имеет совершенно тот же смысл, что и запись a[4]. Аналогичным образом фактическое приращение адреса при выполнении операции ++ над указателем, зависит от типа указателя и равно размеру переменной базового типа. Программа PtrArithm.cpp иллюстрирует возможности арифметики указателей.

// PtrArithm.cpp - Арифметика указателей
#include <iostream.h>

void main() {
    int a[9] = {10,21,32,43,54,65,76,87,98};
    int *p = &a[0];

    cout << "a = " << a << "  p = " << p
         << "  &a[0] = " << &a[0] << endl;
    cout << "1. *p = " << *p << endl;
    p++;
    cout << "2. *p = " << *p << endl;
    p += 2;
    cout << "3. *p = " << *p << endl;
    cout << "*a     = " << *a     << endl;
    cout << "*a+4   = " << *a+4   << endl;
    cout << "*(a+4) = " << *(a+4) << endl;

    p = &a[2];
    int *q = &a[5];
    int d = q - p;
    cout << "q = " << q << "  p = " << p
         << "  d = " << d << endl;
}

Выход программы PtrArithm.cpp:

a = 0x0065FDD4  p = 0x0065FDD4  &a[0] = 0x0065FDD4
1. *p = 10
2. *p = 21
3. *p = 43
*a     = 10
*a+4   = 14
*(a+4) = 54
q = 0x0065FDE8  p = 0x0065FDDC  d = 3

6. Массив указателей

Программа ArrOfPtr.cpp иллюстрирует использование массива указателей для моделирования двумерного массива с переменным количеством элементов в его строках.

// ArrOfPtr.cpp - Массив указателей
#include <iostream.h>
#include <iomanip.h>

void print(int *ap[],int n[], int m) {
    for (int i=0;i<m;i++) {
        cout << i+1;
        for (int j=0;j<n[i];j++)
            cout << setw(5) << *(ap[i]+j); // проще ap[i][j]
        cout << endl;
    }
}

void main() {
    const int m = 3;

    int a1[] = {11,12,13};
    int a2[] = {21,22,23,24};
    int a3[] = {31,32};

    int n[] = {sizeof a1/sizeof(int),
               sizeof a2/sizeof(int),
               sizeof a3/sizeof(int)};

    int *ap[] = {a1,a2,a3};

    print(ap,n,m);
}

Выход программы ArrOfPtr.cpp:

1   11   12   13
2   21   22   23   24
3   31   32

Описание массива указателей имеет вид int *ap[] . В программе массив указателей передаётся функции print в качестве параметра.

7. Указатели на функции

Имя функции в C++ рассматривается как указатель на функцию. Значение указателя в этом случае можно рассматривать как адрес точки входа в функцию. Представление функции в форме указателя позволяет организовать передачу функции другой функции в качестве параметра (фактически передаётся адрес точки входа).

В программе Integral.cpp (см. ниже) функция интегрирования Integral составлена в общем виде. Интегрируемая функция передаётся ей в качестве третьего аргумента. Описание этого аргумента имеет вид double (*func)(double) . Часть описания (*func) символизирует имя функции. Ей предшествует указание типа возвращаемого значения. За ней в скобках следует список типов аргументов функции. Оператор вызова функции, которая в данном случае задаётся указателем, имеет вид: (*func)(x);

Программа Integral.cpp иллюстрирует также использование массива указателей на функции. В ней функция MakeTabula получает в качестве аргумента массив указателей на функции, для которых она строит таблицы значений. Описание массива указателей на функции имеет вид: double (*f[m])(double) .

При изученнии программы Integral.cpp следует обратить внимание на принятые способы инициализации указателей на функции, a также на возможность использования стандартных функций наряду с функциями пользователя.

// Integral.cpp - Указатели на функции
#include <iostream.h>
#include <iomanip.h>
#include <math.h>

double sqr(double x) { return x*x; }

double cube(double x) {return x*x*x; }

double Integral(double a, double b, double(*func)(double)) {
    int n = 1000;
    double x=a, s=0, h=(b-a)/n;

    for (int i=0; i<n; i++) { s+=(*func)(x); x+=h; }
    return s*h;
}

const int m=3;
double (*f[m])(double) = {sqr,cube,exp};

void MakeTabula(double (*f[])(double), int m) {
    double x=0;
    for (int i=0; i<5; i++) {
        cout << setw(4) << x;
        for (int j=0; j<m; j++) cout << setw(10) << (*f[j])(x);
        cout << endl;
        x+=1;
    }
}

void main() {
    cout << "I(0,2,x^2) = " << Integral(0,2,sqr) << endl;
    cout << "I(0,3.14159,sin) = " << Integral(0,3.14159,sin) << endl;
    cout << endl;
    MakeTabula(f,3);
}

Выход программы Integral.cpp:

I(0,2,x^2) = 2.66627
I(0,3.14159,sin) = 2

   0         0         0         1
   1         1         1   2.71828
   2         4         8   7.38906
   3         9        27   20.0855
   4        16        64   54.5982


Обработка строк

1. Общие сведения
 2. Стандартные функции для работы со строками
 3. Пример обработки строк

1. Общие сведения

В C++ массив литер служит основой для размещения строки. Признаком завершения строки является нуль-литера ('\0').

Есть некоторые особенности инициализации строковых данных. Описание вида
char s[] = "Sentence";
предусматривает выделение памяти, достаточной для размещения всех литер инициализирующей строки и дополнительно одного байта для размещения завершающей нуль-литеры. Описание вида
char *as[] = {"once", "upon","a", "time"};
создаёт массив указателей на строки из инициализирущей части, причём все строки размещаются в одном непрерывном массиве литер без каких-либо пропусков.

2. Стандартные функции для работы со строками

Функции этого раздела входят в состав стандартной библиотеки языка C, но оказались достаточно популярными и в C++.

Функции для работы со строками описаны в заголовочном файле string.h. Рассмотрим некоторые из них.

char *strcpy(char *s1, const char *s2) - Копирует строку s2 в массив литер s1. Возвращает значение s1.

char *strcat(char *s1, const char *s2) - Добавляет строку s2 к строке s1. Первая литера строки s2 записывается на место завершающей нуль-литеры строки s1. Возвращает значение s1. Массив литер s1 должен быть достаточной длины для размещения обеих строк. Нарушение этого условия функцией не контролируется и может привести к искажению данных в соседней области памяти. Надо быть очень внимательным при работе с этой функцией!

int *strcmp(const char *s1, const char *s2) - Сравнивает строки s1 и s2. Возращает значение 0, меньше нуля и больше нуля, если соответственно строка s1 равна, меньше и больше строки s2.

size_t strlen(const char *s) - Определяет длину строки s. Возвращает количество литер, которые предшествуют завершающей нуль-литере. Тип size_t в зависимости от системы определён как unsigned int или unsigned long.

char *strtok(char *s1, const char *s2) - Последовательный вызов strtok разбивает строку s1 на лексемы - последовательности литер, отличающихся от литер строки s2, которые считаются разделителями. Во втором и последующих вызовах на месте аргумента s1 должен быть NULL. При каждом вызове возвращается указатель на текущую лексему. Если лексем больше нет, то возвращается NULL. Интересная особенность этой функции состоит в том, что все строки-лексемы размещаются в массиве литер s1. Функция strtok изменяет строку s1 - за каждой лекскмой размещается нуль-литера.

char *strchr(const char s, int c) - Определяет первое вхождение литеры с (преобразуется в char) в строке s. Возвращает указатель на найденную литеру или NULL, если искомая литера в строке отсутствует. Завершающая нуль-литера тоже включена в область поиска.

char *strrchr(const char s, int c) - Аналогична функции strchr, но ищет последнее вхождение литеры c.

Из других функций заметную роль играет функция sprintf, которая описана в заголовочном файле stdio.h. Эта функция часто используется, когда надо выполнить преобразование арифметических данных в форматированное строковое представление.

int sprintf(char *s, const char format, ...) - Данные из списка ... преобразовывает в строковый формат и размещает в литерном массиве s в форме строки. Строка format определяет способ форматирования. Возвращает длину сформированной строки s (без учёта завершающей литеры).

Строка format может содержать текст и коды форматирования. Текст непосредственно включается в результирующую строку. Коды форматирования позиционно соответствуют даннам из списка: первый код определяет форматирование первого данного, второй - второго и так далее. Детальное рассмотрение кодов форматирования здесь не представляется целесообразным и мы приведём лишь наиболее популярные. В общих чертах синтаксис кода форматирования имеет вид
%[-][w][.d]<Символ преобразования> ,
где знак "-" определяет выравнивание влево при размещении данного в отведённом поле. По умолчанию выполняется выравнивание вправо. w - ширина поля вывода. d - точность, которая для вещественных чисел определяет длину дробной части. Символы преобразования:
с - вывод символа;
s - вывод строки;
d - вывод целого числа в десятичном формате со знаком;
u - вывод целого числа без знака в десятичном формате;
x - вывод целого числа без знака в шестнадцатеричном формате;
f - вывод вещественного числа в формате с фиксированной точкой;
e или E - вывод вещественного числа в научном формате.

Программа Format.cpp иллюстрирует использование форматных кодов.

// Format.cpp - Форматирование данных
#include <iostream.h>
#include <stdio.h>

void main() {
    int i=137;
    double d=137.731;
    char c='A';
    char s[]="ABC...";

    char buf[80];

    sprintf(buf,"c=|%-3c| s=|%-8s| s=|%8s|\n", c,s,s);
    cout << buf;

    sprintf(buf,"i=|%4d| i=|%4u| i=|%06x| c=|%3d|\n", i,i,c);
    cout << buf;

    sprintf(buf,"d=|%12.4f| d=|%12.3E|\n", d,d);
    cout << buf;
}

Выход программы Format.cpp:

c=|A  | s=|ABC...  | s=|  ABC...|
i=| 137| i=|0137| i=|000089| c=| 65|
d=|    137.7310| d=|  1.377E+002|

Вывод специально организован так, что знаки "|" обозначает границы полей вывода. Обратите внимание на 0 в коде %04u - он определяет необходимость вывода ведущих нулей.

3. Пример обработки строк

Программа примера выполняет ввод строки BufIn, строит массив строк-слов, входящих во введённую строку, сортирует его по алфавиту (sort) и собирает их в этом же порядке в строке BufOut (tostr). Функция print выводит строки массива.

// StrProc.cpp - Обработка строк
#include <iostream.h>
#include <string.h>

void sort(char *a[], int m);
void print(char *a[], int m);
void tostr(char *a[], int m, char *s);

void main() {
    char BufOut[80],BufIn[80]; //="chickens like to run and play";
    char *ArrStr[10];
    int i,Cnt;
    char *tokPtr;

    cout << "Sentence:" << endl;
    cin.getline(BufIn,80);

    cout << endl << BufIn << endl << endl;

    i=0;
    tokPtr=strtok(BufIn," ");
    while (tokPtr) {
        ArrStr[i++]=tokPtr;
        tokPtr=strtok(0," ");
    }
    Cnt=i;

    print(ArrStr,Cnt);
    sort(ArrStr,Cnt);
    cout << endl;
    print(ArrStr,Cnt);
    tostr(ArrStr,Cnt,BufOut);
    cout << endl << BufOut << endl;
}

void print(char *a[], int m) {
    for (int i=0;i<m;i++)
        cout << i << ". " << a[i] << endl;
}

void sort(char *a[], int m) {
    char *r;
    int j;
    for (int i=1;i<m;i++) {
        r=a[i];
        j=i-1;
        while (j>=0 && strcmp(r,a[j])<0) {
            a[j+1]=a[j];
            j--;
        }
        a[j+1]=r;
    }
}

void tostr(char *a[], int m, char *s) {
    strcpy(s,a[0]);
    for (int i=1;i<m;i++) {
        strcat(s,":");
        strcat(s,a[i]);
    }
}

Пример диалога программы ProcStr.cpp:

Sentence:
chickens like to run and play

chickens like to run and play

0. chickens
1. like
2. to
3. run
4. and
5. play

0. and
1. chickens
2. like
3. play
4. run
5. to

and:chickens:like:play:run:to

При анализе программы следует обратить внимание на место расположения в памяти строк-слов. Они располагаются в поле строковой переменной StrIn. Также следует отметить, что в ходе сортировки строки-слова никуда не перемещаются, а переставляются только указатели на них. В ходе сборки строки BufOut происходит копирование строк-слов из BufIn в BufOut.


Полиморфизм и виртуальные функции

1. Виртуальные функции
 2. Абстрактные классы
 3. Раннее и позднее связывание
 4. Пример построения класса для работы с циклическими списками

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

Операциям в классах соответствуют методы. В примере предыдущего раздела мы создали производный класс CAccountM c методом printAccount. Метод с такой же сигнатурой есть и у базового класса CAccountM (так специально было задумано, хотя совпадение сигнатур у методов базового и производного классов в общем случае не обязательно). Рассмотрим следующий фрагмент программы.

CAccountM *pam = new CAccountM(13);
CAccount  *pa  = new CAccount(17);
CAccount *p;
p = pam; p -> printAccount();
p = pa;  p -> printAccount();

В приведённом фрагменте программы в обоих случаях вызова метода printAccount будет вызван метод базового класса, хотя реально с указателем p в каждом случае связаны объекты разных типов. Возникает вопрос о возможности сделать так, чтобы в этих случаях вызывались бы методы соответствующие реальным объектам. Это привело бы к тому, что операция printAccount стала бы полиморфной, то есть выполнялась бы сообразно типу объекта. Создание такой привлекательной возможности предусматривается в ООП и достигается это с помощью так называемых виртуальных функций.

1. Виртуальные функции

Если в определении базового класса перед определением метода поставить ключевое слово virtual, то такая функция становится виртуальной. Это означает, что если в производном классе будет определена функция с такой же сигнатурой, то она автоматически будет считаться тоже виртуальной. В С++ предусмотрено, что при обращении к виртуальной функции по указателю базового типа вызывается метод того класса, к которому принадлежит объект, связанный с указателем.

В следующем примере три метода класса CAccount объявлены виртуальными.

// Account.h - Интерфейс класса CAccount

#ifndef ACCOUNT_H
#define ACCOUNT_H

class CAccount {
public:
    CAccount(int number);
    virtual void printAccount();
    int getBalance() {return mBalance;}
    int getNumber() {return mNumber;}
    virtual int Deposit(int amount);
    virtual int Withdraw(int amount);
protected:
    int mNumber;
    int mBalance;
};

#endif

Модуль реализации класса CAccount остаётся без изменений и здесь не приводится. Так же можно оставить без изменений модули интерфейса и реализации для класса CAccountM - три метода в нём автоматически станут виртуальными. Но существует традиция явно указывать виртуальные методы и в производных классах. В этой связи слегка изменим модуль интерфейса класса CAccountM.

// AccountM.h - Интерфейс класса CAccountM

#ifndef ACCOUNTM_H
#define ACCOUNTM_H
#include "Account.h"

class CAccountM: public CAccount {
public:
CAccountM(int number);
virtual void printAccount();
virtual int Deposit(int
amount);
virtual int Withdraw(int
amount);
private:
int mInit;
};

#endif

Модуль реализации класса CAccountM, как уже указывалось, можно оставить без изменений. Так же без изменений оставим и модуль клиента классов CAccount и CAccountM. Но выход программы AccountMClient.cpp изменится:

1.
a.Deposit(200): 200
a.Deposit(100): 300
a.Withdraw(50): 250
Account # 21. Balance = $250.

2.
am.Deposit(200): 200
am.Deposit(100): -1
am.Withdraw(50): -1
MAccount # 37. Balance = $200.

3.
pam->Deposit(200): 200
pa->Deposit(100): -1
pa->Withdraw(50): -1
MAccount # 77. Balance = $200.
MAccount # 77. Balance = $200.

В случае 1 и 2 изменений нет. Это означает, что при использовании точечной формы при вызове виртуального метода вызывается метод в соответствии с типом переменной. В случае 3, когда вызов виртуального метода выполняется по указателю, вызываются методы соответствующие классу объекта.

2. Абстрактные классы

Базовый класс может содержать определение чистых виртуальных функций, как это сделано в примере определения класса CFig - "фигура".

// Fig.h - Интерфейс и реализация класса CFig

#ifndef FIG_H
#define FIG_H

class CFig {
public:
    virtual double Square() = 0;
    virtual void print() = 0;
    virtual ~CFig() {}
};

#endif

Определение чистой виртуальной функции состоит из заголовка, который завершается комбинацией " = 0;". Класс, в определении которого есть чистыие виртуальные функции, называется абстрактным. Это означает, что создавать объекты этого класса нельзя, но он может быть базовым классом для порождения нормальных производных классов. В нашем примере это классы CRect ("прямоугольник") и CCircle ("круг").

// Rect.h - Интерфейс и реализация класса CRect
#ifndef RECT_H
#define RECT_H
#include <iostream.h>
#include "Fig.h"

class CRect: public CFig {
public:
    CRect(double w, double h) {mw=w;mh=h;}
    virtual ~CRect() { cout << "Rect out\n";}
    virtual double Square() {return mw*mh;}
    virtual void print() {
        cout << "r(" << mw << "," << mh << ")";
    }
private:
    double mw,mh;
};

#endif
// Circle.h - Интерфейс и реализация класса CCircle

#ifndef CIRCLE_H
#define CIRCLE_H
#include <iostream.h>
#include "Fig.h"

class CCircle: public CFig {
public:
    CCircle(double r) {mr=r;}
    virtual ~CCircle() { cout << "Circle out\n";}
    virtual double Square() {return 3.14159*mr*mr;}
    virtual void print() {
        cout << "c(" << mr << ")";
    }
private:
    double mr;
};

#endif

// Client.cpp - Абстрактные классы

#include <iostream.h>
#include "Fig.h"
#include "Rect.h"
#include "Circle.h"

void main() {
    const int m=4;
    CFig *pFig[m] = {
        new CRect(3,4),
        new CCircle(1),
        new CRect(4,5),
        new CCircle(2)
    };

    for (int i=0;i<m;i++) {
        pFig[i]->print();
        cout << " S = " << pFig[i]->Square() << endl;
    }

    for (i=0;i<m;i++) delete pFig[i];
}

Выход программы Client.cpp:

r(3,4) S = 12
c(1) S = 3.14159
r(4,5) S = 20
c(2) S = 12.5664
Rect out
Circle out
Rect out
Circle out

В модуле клиента организован массив указателей на объекты типа CFig, а фактически CRect и CCircle. Мы видим проявление полиморфизма методов print и Square.

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

Пример с классами CFig, CRect и CCircle демонстрирует также использование виртуальных деструкторов. В классах с виртуальными функциями и деструкторы рекомендуется делать виртуальными. Причём, в этом случае различие сигнатур деструкторов базового и производного классов во внимание не принимается. Предусмотрено, что перед прекращением существования объекта автоматически выполняются все виртуальные деструкторы вверх по линии иерархии, начиная с деструктора соответствующего классу объекта.

Отметим также, что в данном примере необходимости в деструкторах нет и они введены только для иллюстрации идеи виртуальных деструкторов.

3. Раннее и позднее связывание

Конструкция вида "p->func()", где func не виртуальная функция, предусматривает передачу управления вполне конкретной функции и поэтому её компиляция сводится к подстановке соответствующей команды перехода к подпрограмме. В случае, если func это виртуальная функция, может существовать уже несколько функций, которым может быть передано управление. Причём решение о выборе нужной можно принять только в ходе выполнения программы, так как выбор зависит от класса объекта, с которым связан указатель p. Об этих двух случаях говорят, соответственно, как о раннем и позднем связывании.

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

Наравне с терминами раннее и позднее связывание в литературе употребляют термины статическое и динамическое связывание.

4. Пример построения класса для работы с циклическими списками

Циклический список - структура данных достаточно универсального применения и вполне достойна реализации в форме класса. Желая решить проблему наиболее универсальным образом, мы должны позаботится о возможности включения в список элементов разных типов, в том числе и ещё не известных. Вот одно из возможных решений.

// List.h - Интерфейс классов CAtom и CList

#ifndef LIST_H
#define LIST_H
#include <iostream.h>

class CAtom {
public:
    ~CAtom() {cout << "Atom out\n";}
    void * Info;
    CAtom * Link;
};

class CList {
public:
    CList() {mList=0;}
    ~CList();
    void Append(void *pInfo);
    void Iterator(void (*func)(void *));
private:
    CAtom *mList;
};

#endif

// List.cpp - Реализация класса CList
#include "List.h"
#include <iostream.h>

CList::~CList() {
    if (mList) {
        CAtom *p = mList;
        do {
            CAtom *q = p;
            p = p->Link;
            delete q;
        } while (p!=mList);
    }
    cout << "List out\n";
};

void CList::Append(void *pInfo) {
    CAtom *p = new CAtom;
    p->Info = pInfo;
    if (mList) {
        p->Link = mList->Link;
        mList->Link = p;
        mList = p;
    } else {
        mList = p;
        p->Link = p;
    }
}

void CList::Iterator(void (*func)(void *)) {
    if (!mList) return;
    CAtom *p = mList;//->Link;
    //(*func)(p->Info);
     do{
        p = p->Link;
        (*func)(p->Info);
    }while (p != mList);
}

// ListClient.cpp - Клиент класса CList

#include <iostream.h>
#include "List.h"
#include "Fig.h"
#include "Rect.h"
#include "Circle.h"

double S = 0;

void print(void *p);
void delFig(void *p);

void main () {
    CList List;

    List.Append((void *)new CRect(3,4));
    List.Append((void *)new CCircle(5));
    List.Append((void *)new CRect(4,5));

    List.Iterator(print);

    cout << "S = " << S << endl;

    List.Iterator(delFig);

    cout << "Main out\n";

}

void print(void *p) {
    CFig *pf = (CFig *)p;
    pf->print();
    cout << " s = " << pf->Square() << endl;
    S+=pf->Square();
}

void delFig(void *p) {
    delete (CFig *)p;
}

Выход программы ListClient.cpp:

r(3,4) s = 12
c(5) s = 78.5397
r(4,5) s = 20
S = 110.54
Rect out
Circle out
Rect out
Main out
Atom out
Atom out
Atom out
List out

Список состоит из элементов класса CAtom, поле Info которого имеет тип void *, что вытекает из требования создания возможности включения в список элементов разных типов. Предполагается, что класс CList необходимо пополнить определением ещё ряда методов: включение элемента в начало списка, включение элемента за элементом с заданным значением указателя, удаление элемента, следующего за элементом с заданным значением указателя и так далее.

Вместе с решением проблемы создания класса CList, как универсальной структуры данных, мы получили другую: "Каким образом выполнить сканирование элементов списка?" Для решения этой проблемы предусмотрен метод Iteratot, единственный аргумент которой это указатель на функцию с прототипом void (*)(void*). Метод Iteratot последовательно вызывает функцию-параметр с передачей последней указателя на очередной элемент списка.

В частности, метод Iteratot оказался необходимым для освобождения памяти зарезервированной под элементы списка. В рамках класса CList эту задачу решить нельзя из-за использования нетипированных указателей в поле Info элементов списка.

Клиент класса CList иллюстрирует создание списка элементов абстрактного класса CFig, рассмотренного ранее.

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