Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Разделна компилация. Абстракция. Копиращ конструктор и оператор за присвоявяне (=)

Разделна компилация.

Една програма на С++ може да бъде разбита в множество изходни файлове(.cpp), които се компилират независимо един от друг, т.е. осъществява се разделна компилация.
Преди самата компилация изходният файл бива подаден на препроцесора, който изпълнява всички директиви (започващи с #).
Пример: при всяко срещане на #include препроцесорът го заменя със съответстващото му парче код (хедър файл, съдържащ декларации).
В резултат на компилацията се получават няколко обектни файла (файлове с разширение .obj), те представляват машинен код.
Изпълнимият код на програмата (файл с разширение .ехе) се получава след свързване на обектните файлове от linker-a (Linking).
Той асоциира всички референции към имена (на променливи, функции, класове и т.н.) на един обектов файл към съответните им дефиниции, които могат да се намират и в други изходни файлове.
Понякога се случва дефинициите да не се намират в никой от обектните файлове, като в този случай компилаторът претърсва стандартната C++ библиотека (libcp.lib), стандартната C библиотека (libc.lib), а също и всяка ръчно указана такава от програмиста. Ако такава дефиниция не бъде открита линкерът дава грешка.
За да се възползваме максимално от разделната компилация, разделяме класовете на .h и .cpp файлове.
Навсякъде където ще работим с класа, ще включваме само .h файла. По този начин, ако променим реализацията на някоя от функциите на класа, ще се прекомпилира само този файл.

enter image description here

Проект на C/C++ е нещо, което се използва за генериране на един от тези три артефакта:

  • Статична библиотека (.lib, .a)
  • Динамична библиотека (.dll, .so)
  • Изпълнима програма (.exe) Естеството на генерирания артефакт няма голямо значение, но идеята за проект е все пак важна, защото ни казва какво трябва да бъде групирано в единица за анализ.(Analysis Unit).

За съжаление, няма една стандартизирана система за изграждане или един формат на проект за C++, а много различни системи (autoconf, cmake, makefiles, vcxproj, xcodeproj…).
Всяка от тези системи е много конфигурируема, което прави автоматичното извличане на информация в тези проекти много трудна задача. Така че трябва да разберете по-добре какво прави даден проект, преди да можете да конфигурирате анализ.

Разделна компилация

Препроцесор

  • във фазата на предварителна обработка (преди компилацията) се правят различни промени в текста на файла с код
  • не променя оригиналните файлове с код - временно в паметта или чрез временни файлове
  • обработва препроцесорните директиви - инструкции, които започват със символа # и завършват с нов ред, НЕ с точка и запетая (имат собтвен синтаксис)

Препроцесорни директиви

#include

  • препроцесорът заменя директивата със съдържанието на включения файл
  • след това включеното съдържание се преработва (което може да доведе до рекурсивна преработка на допълнителни #includes), след което се преработва останалата част от файла

#define

  • може да се използва за създаване на макрос - правило, което определя как входният текст се преобразува в заместващ изходен текст
  • бърз, но не може да се дебъгва

Компилация

  1. Kомпилаторът проверява дали кодът спазва правилата на езика C++. Ако не е така, компилаторът ще даде съобщение за грешка (и съответния номер на реда). Процесът на компилиране също така ще бъде прекъснат, докато грешката не бъде отстранена.

  2. Компилаторът превежда кода на C++ в инструкции на машинен език. Тези инструкции се съхраняват в междинен файл, наречен обектен файл (с разширение .obj)

Linking

  1. Linker-ът прочита всеки от файловете с обекти, генерирани от компилатора, и се уверява, че те са валидни.
  2. Linker-ът гарантира, че всички междуфайлови зависимости са разрешени правилно. Например, ако дефинираме нещо в един файл, а след това го използваме в друг, линкерът свързва двата файла заедно. Ако не е в състояние да свърже референцията към нещо с неговата дефиниция, ще се получи грешка на linker-а и процесът на свързване ще се прекъсне.
  3. Linker-ът може да свързва и библиотечни файлове. Библиотечният файл е колекция от предварително компилиран код, който е "пакетиран" за повторна употреба в други програми.
  4. След като linker-ът завърши свързването на всички обектни файлове и библиотеки, ще получим изпълним файл, който можем да стартирате (разширение .exe)

Разделна компилация

  • можем да разбием нашата програма на повече от един изходни файлове (.cpp)
  • те се компилират независимо един от друг, затова, ако направим промяна само в един от тях, другите не се компилират наново
  • ако добавим един cpp файл в друг, ще се получи колизия, защото и двата файла се компилират
  • осъществяваме връзките между файловете чрез header файлове (.h), в който включваме само ДЕКЛАРАЦИИТЕ на функции

Header guards

#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 събития, те трябва да не се пресичат).
  • Премахване на събитие по име.