Една програма на С++ може да бъде разбита в множество изходни файлове(.cpp), които се компилират независимо един от друг, т.е. осъществява се разделна компилация.
Преди самата компилация изходният файл бива подаден на препроцесора, който изпълнява всички директиви (започващи с #).
Пример: при всяко срещане на #include препроцесорът го заменя със съответстващото му парче код (хедър файл, съдържащ декларации).
В резултат на компилацията се получават няколко обектни файла (файлове с разширение .obj), те представляват машинен код.
Изпълнимият код на програмата (файл с разширение .ехе) се получава след свързване на обектните файлове от linker-a (Linking).
Той асоциира всички референции към имена (на променливи, функции, класове и т.н.) на един обектов файл към съответните им дефиниции, които могат да се намират и в други изходни файлове.
Понякога се случва дефинициите да не се намират в никой от обектните файлове, като в този случай компилаторът претърсва стандартната C++ библиотека (libcp.lib), стандартната C библиотека (libc.lib), а също и всяка ръчно указана такава от програмиста. Ако такава дефиниция не бъде открита линкерът дава грешка.
За да се възползваме максимално от разделната компилация, разделяме класовете на .h и .cpp файлове.
Навсякъде където ще работим с класа, ще включваме само .h файла. По този начин, ако променим реализацията на някоя от функциите на класа, ще се прекомпилира само този файл.
Проект на C/C++ е нещо, което се използва за генериране на един от тези три артефакта:
- Статична библиотека (.lib, .a)
- Динамична библиотека (.dll, .so)
- Изпълнима програма (.exe)
Естеството на генерирания артефакт няма голямо значение, но идеята за проект е все пак важна, защото ни казва какво трябва да бъде групирано в единица за анализ.(Analysis Unit).
За съжаление, няма една стандартизирана система за изграждане или един формат на проект за C++, а много различни системи (autoconf, cmake, makefiles, vcxproj, xcodeproj…).
Всяка от тези системи е много конфигурируема, което прави автоматичното извличане на информация в тези проекти много трудна задача. Така че трябва да разберете по-добре какво прави даден проект, преди да можете да конфигурирате анализ.
- във фазата на предварителна обработка (преди компилацията) се правят различни промени в текста на файла с код
- не променя оригиналните файлове с код - временно в паметта или чрез временни файлове
- обработва препроцесорните директиви - инструкции, които започват със символа # и завършват с нов ред, НЕ с точка и запетая (имат собтвен синтаксис)
#include
- препроцесорът заменя директивата със съдържанието на включения файл
- след това включеното съдържание се преработва (което може да доведе до рекурсивна преработка на допълнителни #includes), след което се преработва останалата част от файла
#define
- може да се използва за създаване на макрос - правило, което определя как входният текст се преобразува в заместващ изходен текст
- бърз, но не може да се дебъгва
-
Kомпилаторът проверява дали кодът спазва правилата на езика C++. Ако не е така, компилаторът ще даде съобщение за грешка (и съответния номер на реда). Процесът на компилиране също така ще бъде прекъснат, докато грешката не бъде отстранена.
-
Компилаторът превежда кода на C++ в инструкции на машинен език. Тези инструкции се съхраняват в междинен файл, наречен обектен файл (с разширение
.obj
)
- Linker-ът прочита всеки от файловете с обекти, генерирани от компилатора, и се уверява, че те са валидни.
- Linker-ът гарантира, че всички междуфайлови зависимости са разрешени правилно. Например, ако дефинираме нещо в един файл, а след това го използваме в друг, линкерът свързва двата файла заедно. Ако не е в състояние да свърже референцията към нещо с неговата дефиниция, ще се получи грешка на linker-а и процесът на свързване ще се прекъсне.
- Linker-ът може да свързва и библиотечни файлове. Библиотечният файл е колекция от предварително компилиран код, който е "пакетиран" за повторна употреба в други програми.
- След като linker-ът завърши свързването на всички обектни файлове и библиотеки, ще получим изпълним файл, който можем да стартирате (разширение
.exe
)
- можем да разбием нашата програма на повече от един изходни файлове (
.cpp
) - те се компилират независимо един от друг, затова, ако направим промяна само в един от тях, другите не се компилират наново
- ако добавим един
cpp
файл в друг, ще се получи колизия, защото и двата файла се компилират - осъществяваме връзките между файловете чрез header файлове (
.h
), в който включваме само ДЕКЛАРАЦИИТЕ на функции
#ifndef SOME_UNIQUE_NAME
#define SOME_UNIQUE_NAME
// your declarations (and certain types of definitions) here
#endif
- когато header-ът е #included, препроцесорът проверява дали SOME_UNIQUE_NAME е било дефинирано преди това
- ако включваме header-а за първи път, SOME_UNIQUE_NAME няма да е дефинирано, следователно той дефинира SOME_UNIQUE_NAME и включва съдържанието на файла
- ако header-ът бъде включен отново в същия файл, SOME_UNIQUE_NAME вече ще е дефинирано и съдържанието на хедъра ще бъде игнорирано (благодарение на #ifndef).
- по конвенция се задава пълното име на header-а, изписано с главни букви, като се използва долна черта за разделител (името трябва да е уникално)
#ifndef SQUARE_H
#define SQUARE_H
int getSquareSides() {
return 4;
}
#endif
#ifndef WAVE_H
#define WAVE_H
#include "square.h"
#endif
#include "square.h"
#include "wave.h"
int main() {
return 0;
}
#ifndef SQUARE_H
#define SQUARE_H
// съдържанието се включва във файла
int getSquareSides() {
return 4;
}
#endif // SQUARE_H
#ifndef WAVE_H
#define WAVE_H
#ifndef SQUARE_H
// SQUARE_H вече е дефинирано, затова се игнорира
#define SQUARE_H
int getSquareSides() {
return 4;
}
#endif // SQUARE_H
#endif // WAVE_H
int main() {
return 0;
}
#pragma once
има същата цел като header guards: да се избегне многократното включване на даден хедър файл- изискваме от компилатора да пази хедъра
- Пример за лоша абстракция:
struct Triangle {
int x1;
int y1;
int x2;
int y2;
int x3;
int y3;
};
int getPer(const Triangle& t) {
return
sqrt( (t.x1-t.x2)*(t.x1-t.x2) + (t.y1-t.y2)*(t.y1-t.y2) +
sqrt( (t.x2-t.x3)*(t.x2-t.x3) + (t.y2-t.y3)*(t.y2-t.y3) +
sqrt( (t.x3-t.x1)*(t.x3-t.x1) + (t.y3-t.y1)*(t.y3-t.y1);
}
- Пример за по-добра абстракция:
struct Point {
int x, y;
double getDistTo(const Point& other) const {
return sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y));
}
};
struct Triangle {
Point p1;
Point p2;
Point p3;
};
int getPer(const Triangle& t) {
return t.p1.getDistTo(t.p2) + t.p2.getDistTo(t.p3) + t.p3.getDistTo(t.p1);
}
- отношение, при което вътрешния клас (B) няма предназначение в системата извън външния (A)
- A "притежава" B
- А отговаря за жизнения цикъл на B
- комания <- акаунти
class A {
B obj;
}
- отношение, при която вътрешния клас (B) може да съществува независимо от външния (A)
- A "използва" B
- A не отговаря за жизнения цикъл на B
- комания <- хора
class A {
B& obj;
}
//или
class A {
B* obj;
}
Заедно с конструктора по подразбиране и деструктора във всеки клас се дефинират и следните член-функции:
- Копиращ конструктор - конструктор, който приема обект от същия клас и създава новият обект като негово копие.
- Оператор= - функция/оператор, който приема обект от същия клас и променя данните на съществуващ обект от същия клас (обектът от който извикваме функцията).
При липсата на дефиниран/и копиращ конструктор и/или оператор=, компилаторът автоматично създава такива по подразбиране. Забележка: Копиращият конструктор създава нов обект, а оператор= модифицира вече съществуващ такъв!
#include <iostream>
struct Test {
Test() {
std::cout << "Default constructor\n";
}
Test(const Test& other) {
std::cout << "Copy constructor\n";
}
Test& operator=(const Test& other) {
std::cout << "operator=\n";
return *this;
}
~Test() {
std::cout << "Destructor\n";
}
};
void f(Test object) {
//do Stuff
}
void g(Test& object) {
//do Stuff
}
int main() {
Test t; //Default constructor;
Test t2(t); // Copy constructor
Test t3(t2); // Copy constructor
t2 = t3; // operator=
t3 = t; // operator=
Test newTest = t; //Copy constructor !!!!!!!
f(t); // Copy constructor
g(t); // nothing. We are passing it as a reference. We are not copying it!
Test* ptr = new Test(); // Default constructor // we create a new object in the dynamic memory. The destructor must be invoked explicitly (with delete)
delete ptr; // Destructor
} //Destructor Destructor Destructor Destructor
Относно следващия пример:
В рамките на курса ще възприемаме, че няма да се извикват нито излишни copy constructor-и(защото връщаме по копие), нито destructor-и(в scope на функцията обектът умира, но преди това би трябвало да се копира, за да се върне по копие) в scope-a на функцията, защото се случва RVO - return value optimization, което ни спестява излишни копирания, тоест единствено ще се извикат constructor и destructor на съответние места индикирани с коментари.
struct Test {
Test() {
std::cout << "Consturctor";
}
Test(const Test& other) {
std::cout << "Copy consturctor";
}
Test& operator=(const Test& other) {
std::cout << "operator=";
return *this;
}
~Test() {
std::cout << "Destuctor";
}
};
Test create() {
return Test(); // default constructor
}
int main() {
Test t = create();
} // destructor
Задача 1: Напишете клас, който е за работа със събитие. Събитието се характеризира с име (низ до 20 символа), дата, начален час и краен час.
Задача 2: Напишете клас за работа с колекция от събития (най-много 20). Трябва да имате:
- Добавяне на събитие.
- Намиране на най-дългото събитие.
- Приемане на дата и връщане на максималния брой събития, които може да се посетят в този ден. (за да се посетят 2 събития, те трябва да не се пресичат).
- Премахване на събитие по име.