15. Модули

Програмата е съвкупност от отделно
компилирани единици, свързани от линкера.
Бьорн Строустроп


Създаване на заглавни файлове.
* Стекът е линейна структура от данни, която позволява добавяне и отстраняване на елемент само от единия си край, наречен връх на стека. Нека функцията push добавя елемент в стека s, а функцията pop изважда елемент от стека s.
 

 Изпълнение на функция Състояние на стека s
s.push("Able") "Able"
s.push("Backer") "Backer"
"Able"
s.push("Charlie") "Charlie"
"Backer"
"Able"
s.pop() -> "Charlie" "Backer"
"Able"
s.push("Delta") "Delta"
"Backer"
"Able"
s.pop() -> "Delta" "Backer"
"Able"
s.pop() -> "Backer" "Able"
s.pop() -> "Able"

* Създаване на клас за стек от низове.
class Stack  {
public:
 Stack();
 void push(string s); /* добавя нова стойност в стека */
 string pop();        /* изважда стойност от стека */
 bool is_empty() const;
private:
 vector<string> data; /* съхранява данните на стека */
 int top;             /* сочи върха на стека */
};

Stack::Stack()
{ top = 0; }          /* създава празен стек */

void Stack::push(string s)
{ if (top < data.size())
       data[top] = s;     /* има празна позиция във вектора data */
  else data.push_back(s); /* векторът data увеличава размера си  */
  top++;
}
string Stack::pop()
{ assert(top > 0);       /* стекът не е празен */
  top--;
  return data[top];
}
bool Stack::is_empty() const
{ return top == 0; }

* Разделяне на информацията между stack.h и stack.cpp.
    Заглавният файл stack.h съдържа дефиницията на класа и се включва във всеки файл, който използва стек.
// stack.h
#ifndef STACK_H
#define STACK_H
#include <vector>
#include <string>
using namespace std;

class Stack  {
public:
 Stack();
 void push(string );
 string pop();
 bool is_empty() const;
private:
 vector<string> data;
 int top;
};
#endif
    Може да се наложи един и същи заглавен файл да се включи два пъти в текста на една програма - тогава компилаторът ще даде синтактична грешка. За да се избегне тази ситуация се използва механизмът на предпазителите - чрез директивите на препроцесора ifndef, define и endif, както е показано на примера.
    Дефинициите на член-функциите на класа Stack са във файл stack.cpp.
// stack.cpp
#include <cassert>
using namespace std;
#include "stack.h"

Stack::Stack()
{ top = 0; }

void Stack::push(string s)
{ if (top < data.size()) data[top] = s;
  else data.push_back(s);
  top++;
}

string Stack::pop()
{ assert(top > 0);
  top--;
  return data[top];
}

bool Stack::is_empty() const
{ return top == 0; }
// end stack.cpp



Разделяне на програмата на няколко първични файла.
    Пример. Да се напише програма, която да може да пресмята аритметични изрази.
    Аритметичните изрази се пишат по правилата на езика С++ за действия с числа с плаваща точка и се задават като низове от входа на програмата. Добавена е операция степенуване "^". Например при вход на низа
2*(1 + 2^2*3) + 2^10 =
програмата трябва да изведе числото 1050.
    Алгоритъм за решаване на задчата.
    Създават се два стека - стек за числата - num_s и стек за операциите - op_s. Четат се последователно елементи от низа и в зависимост от прочетения елемент се прави следното:
    1. Число - добавя се в num_s.
    2. ( - добавя се в op_s.
    3. Аритметична операция - сравнява се приоритетът й с приоритета на операцията от op_s.
      - Ако е с по-висок приоритет се добавя в op_s.
      - В противен случай се извършва "пресмятане" и след това операцията се добавя в op_s.
    "Пресмятане" наричаме изваждане на две числа от num_s и една операция от op_s; изпълнение на операцията с аргументи двете числа и добавяне на получения резултат в num_s.
    4. ) - извършват се "пресмятания", докато в op_s се появи (.
    5. = - извършват се "пресмятания", докато има елементи в op_s.
Когато стекът op_s е празен, в num_s има единствено число - стойността на пресметнатия аритметичен израз.
    В таблицата е реализиран описания алгоритъм за пресмятане на израза
2*(1 + 2^2*3) + 2^10 =
Елемент
от низа
2 * ( 1 + 2 ^ 2 * 3 ) + 2 ^ 10 =
"пресмятане" 2^2
=4
4*3
=12
1+12
=13
2*13
=26
2^10
=1024
26+1024
=1050
num_s 2 2 2 2
1
2
1
2
1
2
2
1
2
2
1
2
2
2
1
4
2
1
4
3
2
13
26 26
2
26
2
26
2
10
1050
op_s * *
(
*
(
*
(
+
*
(
+
*
(
+
^
*
(
+
^
*
(
+
*
*
(
+
*
+ + +
^
+
^

Текстът на програмата за пресмятане на аритметични изрази е разделен на няколко файла, връзките между които са описани в следната таблица:
 

Файл (модул) #include компилация
stack.h - -
stack.cpp stack.h stack.obj
error.h - -
error.cpp error.h error.obj
input.h - -
input.cpp error.h
input.h
input.obj
eval.h stack.h -
eval.cpp stack.h
error.h
eval.h
eval.obj
calc.cpp stack.h
input.h
eval.h
error.h
calc.exe

// error.h
#ifndef ERROR_H
#define ERROR_H
#include <string>
using namespace std;
void error(string );
#endif

// input.h
#ifndef INPUT_H
#define INPUT_H
#include <string>
using namespace std;
bool is_operator(string);
string next_token();
#endif

// eval.h
#ifndef EVAL_H
#define EVAL_H
#include <string>
#include "stack.h"
using namespace std;
int precedence(string);
void evaluate(Stack &, string);
#endif

// error.cpp
#include <iostream>
#include <string>
#include "error.h"
using namespace std;

void error(string message)
{ cout << "ERROR: " << message << ".\n";
  exit(1);
}
// end error.cpp

// eval.cpp
#include <string>
#include <sstream>
#include <cmath>
using namespace std;

#include "stack.h"
#include "error.h"
#include "eval.h"

int precedence(string s)
{ if (s == "+" or s == "-")      return 1;
  else if (s == "*" or s == "/") return 2;
  else if (s == "^")             return 3;
  else                           return 0;
}

double string_to_double(string s)
{ istringstream instr(s);
  double x;
  instr >> x;
  return x;
}

string double_to_string(double x)
{ ostringstream outstr;
  outstr << x;
  return outstr.str();
}

void evaluate(Stack &num, string op)
{ if (num.is_empty()) error("Syntax error");
  double y = string_to_double(num.pop());
  if (num.is_empty()) error("Syntax error");
  double x = string_to_double(num.pop());
  double z;
  if (op == "^") z = pow(x, y);
  else if (op == "*") z = x * y;
  else if (op == "/")
  { if (y == 0) error("Divide by 0");
    else z = x / y;
  }
  else if (op == "+") z = x + y;
  else if (op == "-") z = x - y;
  else error("Syntax error");
  num.push(double_to_string(z));
}
// end eval.cpp

// input.cpp
#include <iostream>
#include <string>
#include <cctype>
using namespace std;

#include "error.h"
#include "input.h"

bool is_operator(string s)
{ return s == "+" or s == "-" or
         s == "*" or s == "/" or s == "^";
}

void skip_whitespace()
{ char ch;
  do
  { cin.get(ch);
    if (cin.fail()) return;
  }
  while (isspace(ch));
  cin.unget();
}

string next_number()
{ string r;
  bool more = true;
  while (more)
  { char ch;
    cin.get(ch);
    if (cin.fail())  more = false;
    else if (isdigit(ch)) r = r + ch;
    else
    { cin.unget();
      more = false;
    }
  }
  return r;
}

string next_token()
{ skip_whitespace();
  char ch;
  cin.get(ch);
  if (cin.fail()) return "";
  if (isdigit(ch)) { cin.unget(); return next_number(); }
  else
  { string token;
    token = token + ch;
    if (is_operator(token) or token == "(" or
              token == ")" or token == "=") return token;
    else
    { string message = "Unexpected input ";
      error(message + ch);
      return "";
    }
  }
}
// end input.cpp

// calc.cpp
#include <iostream>
#include <string>
using namespace std;

#include "stack.h"
#include "input.h"
#include "eval.h"
#include "error.h"

int main()
{ Stack numstack;
  Stack opstack;

  while (true)
  { string s = next_token();
    if (is_operator(s))
    { if (opstack.is_empty()) opstack.push(s);
      else
      { string old_op = opstack.pop();
        if (precedence(s) > precedence(old_op)) opstack.push(old_op);
        else evaluate(numstack, old_op);
        opstack.push(s);
      }
    }
    else if (s == "(") opstack.push(s);
    else if (s == ")")
    { bool more = true;
      while (more)
      { if (opstack.is_empty()) error("No matching (");
        string old_op = opstack.pop();
        if (old_op == "(") more = false;
        else evaluate(numstack, old_op);
      }
    }
    else if (s == "=")
    { while (not opstack.is_empty())
      { string old_op = opstack.pop();
        if (old_op == "(") error("No matching )");
        else evaluate(numstack, old_op);
      }
      if (numstack.is_empty()) error("Syntax error");
      cout << numstack.pop() << "\n";
      if (not numstack.is_empty()) error("Syntax error");
    }
    else if (s == "") return 0;
    else numstack.push(s);
  }
}
// end calc.cpp
 

2*(1 + 2^2*3) + 2^10 =
1050
(25/5 + 5)*10 - 4*(5 - 4/(2^2)) - 50 =
34

Проекти и make файлове.
    Когато програмата е разделена на няколко модула, можем да компилираме всеки от тях отделно. Компилаторът създава обектни файлове за всеки модул. Свързващата програма (link) обработва всички модули и създава изпълним файл. В интегрирана среда за програмиране, обединяването на файловете от една програма става със създаване на проекти.
    Например в среда на Dev-C++ трябва да отворим нов проект и да добавим всички файлове от програмата. За програмата за пресмятане на  аритметични изрази, това са файловете:
    calc.cpp
    stack.cpp
    error.cpp
    input.cpp
    eval.cpp
    Самият проект се записва във файла project.dev.
    Когато се работи с извикване на компилатора от командния ред (извън интегрирана среда),  най-често се използва специален файл, наречен make-файл. Той е текстов файл и описва модулите на програмата и тяхното взаимодействие.
    Например програманата среда Dev-C++ създава такъв файл и по-долу е даден този файл  за задачата за пресмятане на аритметични изрази (операционна система Windows 98).

Makefile.win

# Project: Project1
# Makefile created by Dev-C++ 4.9.8.0

CPP  = g++.exe
CC   = gcc.exe
WINDRES = windres.exe
RES  = 
OBJ  = calc.o stack.o input.o eval.o error.o $(RES)
LINKOBJ  = calc.o stack.o input.o eval.o error.o $(RES)
LIBS =  -L"C:/DEV-CPP/lib" 
INCS =  -I"C:/DEV-CPP/include" 
CXXINCS =  -I"C:/DEV-CPP/include/c++"  -I"C:/DEV-CPP/include/c++/mingw32"  -I"C:/DEV-CPP/include/c++/backward"  -I"C:/DEV-CPP/include"  -I"C:/ccc2book" 
BIN  = Pro.exe
CXXFLAGS = $(CXXINCS) 
CFLAGS = $(INCS) 

.PHONY: all all-before all-after clean clean-custom

all: all-before Pro.exe all-after

clean: clean-custom
 rm -f $(OBJ) $(BIN)

$(BIN): $(LINKOBJ)
 $(CPP) $(LINKOBJ) -o "Pro.exe" $(LIBS)

calc.o: calc.cpp
 $(CPP) -c calc.cpp -o calc.o $(CXXFLAGS)

stack.o: stack.cpp
 $(CPP) -c stack.cpp -o stack.o $(CXXFLAGS)

input.o: input.cpp
 $(CPP) -c input.cpp -o input.o $(CXXFLAGS)

eval.o: eval.cpp
 $(CPP) -c eval.cpp -o eval.o $(CXXFLAGS)

error.o: error.cpp
 $(CPP) -c error.cpp -o error.o $(CXXFLAGS)

Поделяне на променливи и константи между модули.
    Функции/класове се използват от няколко модула като се декларират/дефинират в заглавни файлове и тези файлове се включват с директивата на препроцесора #include. Такава техника се използва и за поделяне на константи и променливи, дефинирани в заглавни файлове. Когато константа или променлива е дефинирана в някой .cpp файл и трябва да се използва от други модули, тя се дефинира там като външен обект (с ключовата дума extern).
Пример.
// файл first.cpp
...
int global_size;
void f()
{ cout << global_ size;
...
}
// файл second.cpp
...
extern int global_size;
...
int main()
{ global_size = 100;
...
}
Следва разделна компилация на двата файла (модула) и свързването им в една програма.