Add MIPT CXX lecture notes

This commit is contained in:
thematdev 2023-09-21 19:42:51 +03:00
parent 3bff372afe
commit 9f11ec59c4
Signed by: thematdev
GPG Key ID: D12878639B090D90
5 changed files with 748 additions and 0 deletions

View File

@ -0,0 +1,126 @@
# $I$. Introduction to C++
## § 1.2 Types and their supported operations.
static typization $:=$ тип переменной определяется на этапе компиляции
Integral types:
```C++
int x; //4
long x; //4
long long x; //8
short x; //2
char x; //? 1
bool x; //?? 1
int8_t x;
int16_t x;
int32_t x;
int64_t x; //стандартизованные типы про которые гарантируется их ширина
int128_t x; //есть аналоги с u
std::byte x; //аналогично char
size_t x; //==ull
```
Floating point types:
```C++
float x; //4b
double x; //8b
long double x; //16b
```
Хранятся они в формате sign (1 bit) mantissa exponent, то есть число хранится как mantissa * 2^exponent * sign
Все, что выше - это фундаментальные типы. Рассмотрим что-то более комплексное
```C++
std::string s = "abc";
s[1] == 'b';
s[3] == (char)0; //string - это null termindated string - после данных стандарт гарантирует присутствие символа 0
s[4]; //ub
.at()
+ +=
.push_back()
.pop_back()
.front()
.back()
.size()
std::vector<int> v;
//push_back работает амортизированно за O(1), но гарантируется за O(n)
//аналогично string без + и +=
.shrink_to_fit()
.clear()
//просто делает из старых элементов ub, но память никак не освобождает
.reserve(n)
//предывделить память до n, но не кладет туда элементы (обращение туда все еще ub)
.resize(n, x)
//дропунть лишнее, если есть, если нет, то заполнить все остальное нулями
.capacity()
//size под выделенную память
std::list<int> l; //useless
std::forward_list<int> fl; //useless
std::deque<int> d; //страшная вещь внутри, которую нужно будет реализовать
std::stack<int> s; //deque внутри
std::queue<int> q; //deque внутри
std::priority_queue<int> pq; //deque с make_heap-ом внутри
std::map<int, int> m; //rbt
m.at(key); //возвращает exception, если нет ключа
m.find(key); //возвращает итератор или m.end, если не находит
m.insert(pair<key, val>);
erase(map::iter);
std::multimap<key, val> mm; //это multiset, который map.........
std::unordered_map<key, val> um; //хэш-таблица
//работает В СРЕДНЕМ (не амортизированно) за O(1)
//то есть гарантируется, что работает за O(1) на рандомном наборе данных, а амортизированно - это значит, что работает быстро гарантированно
std::istream is; //поток ввода
std::ostream os; //поток вывода
```
"Эффективность дороже безопасности". Любая проверка на ub отсутствует, потому что это дорого).
Литералы
```C++
5u;
1'000'000'057;
0.2f;
"abc";
'a';
//integer promotions - это когда ты складываешь int и long long, то получится long long и все будет корректно
//Есть аналогичное floating point promotion
//int + uint = uint
0xff == 255; //16-иричный
0123 == 83; //8-иричный
0b101 == 5; //2-ичный
```
## §1.1 Знакомство с терминалом
```
pwd - print working directory
cd - вернуться в предыдущее пространство
```
## §1.3 Declarations, definitions and scopes.
На глобальном уровне (вне какой-либо другой сущности) все действия - это объявления чего либо (класса, функции, переменной etc)
```C++
#include <iostream>
int a;
void f(int x);
class C;
struct S;
enum E;
union U;
namespace N {
int x;
}
namespace N {
int y;
}
using ll = long long;
int main() {
;
}
```

372
pages/mipt_cxx1/02.md Normal file
View File

@ -0,0 +1,372 @@
В прошлый раз мы остановились на пункте
Declarations, definitions and scopes
Объявления
```C++
int a;
void f(int x);
class C;
struct S;
enum E;
union U;
namespace N {
int x;
}
namespace N {
int y;
}
```
Чтобы сделать элемент видимым из другого namespace,
можно использовать using
```C++
int main() {
// std::cout << x << std::endl // CE
std::cout << N::x << std::endl // OK
using N::x; // теперь объявление x есть в нашей области видимости(scope)
std::cout << x << std::endl; // OK
}
```
Также using можно также добавить в глобальную область видимости и в другой namespace
Можно использовать сразу все пространство имен через `using namespace N`.
Один из примеров использования: `using namespace std`, но это **крайне не рекомендуется**,
по крайней мере глобально.
Причина тому то, что `std` крайне огромен, соответственно в нем очень много имен,
особенно опасно привнести функцию с одинаковым именем и другими типами,
например, `std::distance`.
Еще один вид конструкции `using` это алиасинг типов, например, некоторые из смертных грехов
```C++
using vi = std::vector<int>;
using ll = long long;
```
Стоит понимать, что `using` не создает новый тип. `using` также можно использовать с шаблонами
```C++
template <typename T>
using v = std::vector<T>;
```
Ключевые слова `typedef` и `using` семантически делают одно и то же, но `typedef` это C-style,
а значит, например, не умеет в шаблоны.
Напомним, что _ключевыми словами_ называются слова, смысл которых закреплен компилятором,
и как следствие, их нельзя использовать, например, в качестве имён.
Каждый блок(из фигурных скобок) создает новую область видимости, то есть
они образуют дерево по вложенности фигурных скобок(прим. лектора:
концептуально это так, но как всегда в C++, наверняка можно найти
несколько крайних случаев)
По умолчанию определения берется из минимальной по включению области видимости,
но всегда можно обратиться к глобальной через `::`, например
```C++
#include <iostream>
int x = 0;
int main() {
int x = 1;
for (int i = 0; i < 10; ++i) {
int x = i;
// int x = 5; // CE, redeclaration in same scope
std::cout << x << std::endl; // i
std::cout << ::x << std::endl; // 0, if global doesn't exist will throw CE
}
}
```
Обратим внимание, что всегда можно создать блок и затемнить объекты
```C++
int x = 5;
{
int x = 6;
// now x is 6
}
```
Стоит обратить внимание на следующую "подлянку":
```C++
namespace N {
int x = 7;
}
using namespace N;
// WARNING: uncommenting this line will result to 0 as output
// By standard this is not redeclaration but using N::x is
// int x = 0;
int main() {
int x = 1;
{
int x = 5;
std::cout << x << std::endl; // will print 7, as global scope includes x
}
}
```
Если `x` объявлен в двух различных пространствах `N, M`, подключенных
через `using namespace N; using namespace M;`, то при попытке использования `x`
компилятор выкинет ошибку `ambigious reference to variable x`.
В первом приближении есть условно два типа, которые разрешаются в следующем приоритете:
1. Имена объявленные "явно", через `T x;` или `using N::x`.
2. Имена подключенные из другого пространства имен через `using namespace N;`
Также можно получить ошибку `different kind of entity`, если определить
символ как тип и как переменную одновременно, например
```C++
using N::x; // int
using x = std::vector<int>; // Type
```
Если начать заниматься еще большей херней, например `int x = x;`, то из-за
такой концепции как point of declaration, данный код эквивалентен `int x;`,
соответственно в `x` будет лежать мусор, а совсем не то, что будет в высшей
области видимости или пространстве имен.
**One definition rule(ODR)**. Каждая сущность в программе должна быть определена ровно один раз.
Это верно в C, но не совсем верно для классов. Класс можно определить несколько раз, если
все определения абсолютно идентично.
_Объявлений_ функций может быть сколько угодно, но _определение_ ровно одно.
Любое определение является объявлением.
C++ разрешает более одной функции с одним и тем же именем.
```C++
void f();
int f(int x) {
return x + 1;
}
int f(double x) {
return x + 2;
}
```
`f(0.5) = 2`, так как вызовется от `double`, `f(0) = 1`, так как вызовется от `int`.
`f(0.0f) = 2`, ведь `float -> double` -- promotion, а `float -> int` -- conversion.
**promotion лучше, чем стандартный conversion, лучше чем тот, что определен пользователем**.
Если бы определили `int f(float x);`, а вызывали бы `f(0.0) // no suffix defines double`,
то компилятор бы выкинул ошибку, так как он не может выбрать между двумя conversion.
Следующий код также вызовет ошибку компиляции
```
void f();
int f();
```
Это переобъявление(redeclaration) с одинаковым типом принимаемых значений, но разным возвращаемых.
Необходимо различать (не)квалифицированные идентификаторы.
```
x; // unqualified-id
N::x; // qualified-id
```
По стандарту `int x;` также считается определением, только если не стоит
ключевое слово `extern`. Выражение `extern int x;` является объявлением,
но не считается определением, линковщик будет искать этот символ.
`static` делает переменную видимой только в данном translation unit,
но также является определением(которое затемняет другие).
# 1.4 Expressions and operators
В локальных областях видимости появляются такие сущности, как
_выражения_(expressions) и _инструкции_(control flow statements), про
последние мы поговорим в следующем параграфе.
Формальное определение выражения занимает одну-две страницы.
Неформально это просто набор некоторых переменных и литералов,
соединенных между собой операторами и скобками.
**Пример.** `x + 5` или `cout << x` являются выражениями.
Все мы знаем (бинарные) операторы `+, -, *, /, %, <<, >>, |, &, ^, &&, ||, <, >, !=, ==, <=, >=, <=>(since C++-20)`
Побитовые операторы(`&, |, ^`) вычисляют выражение побитово.
Логические операторы(`&&, ||`) принимают `bool` и возвращают `bool`, **и притом вычисляются лениво**.
Стандарт гарантирует, что если `a` ложно, то в выражении `a && b` выражение `b` вычислено не будет.
Аналогично, если `a` истинно, то в выражении `a || b` выражение `b` вычислено не будет.
**Пример.** Выражение `v.size() >= 5 && v[4] == 1` безопасно и не вызывает UB из-за ленивости `&&`.
Рассмотрим оператор присваивания `=` и операторы составного присваивания `+=, -=, *=, /=, %=, <<=, >>=, |=, ^=, &=`.
`x = y` возвращает `y`. `(x = y) = z` возвращает `z`, все они lvalue.
На интуитивном уровне, lvalue можно что-то присвоить, а rvalue нет, то есть `x + y` это rvalue, а `x = y` это lvalue.
Первая группа операторов возвращает lvalue, а вторая rvalue.
Операторы присваивания правоассоциативны, то есть `x = y = z` эквивалентно `x = (y = z)`.
Вроде как все операторы из первой группы левоассоциативны, то есть `x + y + z` эквивалентно `(x + y) + z`.
Для `float` уже есть разница в каком порядке их складывать из-за точности(лучше в отсортированном).
Лево(право)ассоциативность сохраняется в независимости от того, как оператор был перегружен.
До C++-17 операторы `&&, ||` при перегрузке теряет ленивость и гарантию на порядок вычисления.
Начиная с C++-17 гарантируется, что при вычислении `a && b` выражение `b` будет
вычислено после выражения `a`.
Ещё один класс операторов -- это декременты и инкременты, `a++, ++a, --a, --a`.
Разница между `a++, ++a` заключается в том, что первый(постфиксный) создает новое
значение и кладет туда результат, тем самым возвращает rvalue. Второй(префиксный) увеличивает
переменную "на месте" и возвращает lvalue. Аналогично `a--, --a`.
Инкременты и декременты применимы только к lvalue, то есть выражение `--(4 + 5)` некорректно,
а `++(x=5)` корректно.
По стандарту постфиксные операции приоритетнее, и `++a++` эквивалентно `++(a++)`, а следовательно
некорректно.
**Важный пример**. Чему эквивалентно выражение `a+++++b`?
**Лексический парсер(лексер) всегда пытается взять наибольший "осмысленный" токен**,
поэтому он разобъет его следующим образом: `((a++)++)+b`, но выражение `(a++)` является rvalue,
а инкремент можно применить только к lvalue.
**Тернарный оператор**. `a ? b : c`. Пытается преобразовать `a` к `bool`, если истинно, то вычисляет `b` и
возвращает его, иначе вычисляет `c` и возвращает его. Гарантируется, что будет вычисляться ровно один из них.
Вид возвращаемого value зависит от вида value `b` и `c`. Но вид value, как и тип, должен быть
известен на этапе компиляции, поэтому если хоть одно из них rvalue, то вид всего выражения
это rvalue, иначе lvalue.
**Пример**. `(false ? a++ : ++a) = 1`, даже несмотря на то, что оно всегда равно `++a`, которое lvalue,
из рассуждения выше левая часть все равно будет rvalue, откуда присваивание некорректно.
Если для `b` и `c` не найдется общий тип, но будет ошибка компиляции, иначе тип выражения равен общему типу.
**Пример**. `f(b ? 1 : 0.5)` всегда вызовет `f(double)`.
Оператор `,` делает следующее: выражение `a, b` вычисляет сначала `a`, потом `b`,
и возвращает `b`. Тип value такой же как у `b`.
**Не всякая запятая как символ является оператором**. Запятая в `void f(int, int)` очевидно не
оператор, ведь это объявление, а не выражение, равно как и `<` в `#include <header.h>`, которая
вообще является командой препроцессора.
**Важное замечание**. `T x = y` это не оператор присваивания и не выражения, это синтаксис объявления.
**Важный пример**. При вызове функции `f(x, y, z)` **нет никакой гарантии на порядок вычисления аргументов**,
`x, y, z` это не выражение, а перечисление аргументов. А если написать `f((x, y, z))`, то это будет
эквивалентно вызову функции `f` от одного аргумента `z`, выражения `x`, `y`, `z` будут вычислены
именно в таком порядке.
Оператор `sizeof`. Возвращает количество байт, которое занимает переменная/типа. Так как тип
переменной `x` известен во время компиляции, то `sizeof(x)` также известен во время компиляции.
`sizeof` от VLA(variable length array) является неопределенным поведением. VLA есть в
стандарте C99, но нет в стандарте C++, тем не менее, компиляторы его поддерживают.
**Замечание**. `sizeof(v)` это не `v.size()`. `sizeof(arr)`, где `arr` --
статически выделенный массив, это сумма размеров его элементов,
но от указателя он равен размеру самого указателя непосредственно.
[Приоритеты операторов](https://en.cppreference.com/w/cpp/language/operator_precedence)
[Порядок выполнения](https://en.cppreference.com/w/cpp/language/eval_order)
Ниже приведены некоторые примеры:
Нельзя сказать, чему равно значение `x++ * y++ + ++x`. Несмотря на то, что
умножение "выполняется" раньше, чем сложение, порядок вычисления операндов
не гарантируется.
```C++
int f() { std::cout << 1; return 1;}
int g() { std::cout << 2; return 2;}
int h() { std::cout << 3; return 3;}
```
В таком случае значение `f() * g() + h()` имеет однозначно определенное значения,
но вот результат его вычисления не определен.
В выражении `a*b + c*d + e*f` нет никаких гарантий, что сначала вычислится результат всех умножений,
а только потом они будут складываться.
Есть шедевральная глава "Sequenced before" rules, которое задает некоторые правила
порядка вычислений внутри одного потока, более точно, транзитивное антисимметричное бинарное отношение.
Порядок выражений стал более строгим начиная с C++-17, тем самым некоторые выражения, вызывающие
неопределнное поведение до C++-17, хорошо определены после.
Control statements
if:
```C++
if (/* bool-expression */) {
} else if (...) {
} else {
}
```
switch:
```C++
switch (/* bool-expression */) {
case 1:
std::cout << "AAAA";
/* FALLTHROUGH */
case 2:
std::cout << "BBBB";
break;
case 3:
std::cout << "CCCC";
default:
std::cout << "DEFAULT";
}
```
**Замечание**. Cases in switch **fall through**, например при `expression == 1` он выведет `AAAABBBB`, на `2` выведет `BBBB`, на
`3` выведет `CCCCDEFAULT`, иначе `DEFAULT`.
loops:
```C++
// First check, then do
while (/* bool-expression */) {
}
// First do, then check
do {
} while (/* bool-expression */);
// first follows declaration, then check expression, and if true
// evaluates expression and inner block and repeats it
for (declaration | expression; bool-expression; expression) {
}
// empty bool-expression is always true
```
Начиная с C++-17 можно делать объявление в ife: `if (int x = 0; y > x)`.
Метки:
```
label:
// do some stuff
if (bool-expression) {
goto label;
}
```
Нельзя прыгать с пропуском инициализации.

228
pages/mipt_cxx1/03.md Normal file
View File

@ -0,0 +1,228 @@
# 1.6. Compile-time errors, runtime errors and undefined behaviour
Мы уже встречались с ошибками компиляции.
Ошибки можно условно разделить на лексические, синтаксические и
семантические.
Мы уже затрагивали лексический парсер, он разбивает программу на
токены, например `std::cin >> x;` разбивается на 6 токенов
`std :: cin >> x ;`.
Далее происходит синтаксический разбор, и компилятор
начинает заниматься семантикой.
**Пример**.
* `\\;` будет лексической ошибкой, потому что лексер не справится разобрать символ `\` с `error: stray '\' in program`
* `6abcde;` будет семантической ошибкой, потому что компилятор подумает, что `abcde` -- литеральный суффикс, а потом его не найдет(начиная
с C++-11 литеральные суффиксы можно определять свои)
* `std::cout << x + ;` будет синтаксической ошибкой, `expected primary-expression before ';' token`, так как нет выражения после плюса.
* `"abc" + 5.0f;` будет семантической ошибкой, `invalid operands of types ...`.
_Ошибка времени выполнения_ или runtime error это когда
программа успешно скомпилирована, но непредвиденно завершается во время
выполнения(падает).
Одна из самых частых -- segmentation fault.
```C++
#include <iostream>
#include <vector>
int main() {
std::vector<int> v(10);
v[50'000] = 1;
}
```
Возникает из-за обращения к памяти, которую мы не имеем права читать.
Floating point exception (FPE), например, деление на ноль, идёт от процессора.
Aborted, вызов функции `abort()` из libc, её вызов приводит к аварийному завершению.
**Неопреденное поведение** или undefined behaviour -- некорректный код, который
приводит к "непредсказуемым" последствиям во время выполнения, в том смысле,
что компилятор не может дать никаких гарантий.
```C++
std::vector<int> v(10);
v[10] = 1; // UB
// Может упасть, а может и повредить очень важную область памяти
// у другой переменной
int x;
std::cout << x; // UB
x++ + ++x; // UB, как говорилось раньше, нет гарантий на порядок
```
Signed integer overflow это также UB. В том смысле, что **компилятор вправе
считать, что в коде нет переполнений знаковых чисел и делать оптимизации
исходя из этого**.
```C++
for (int i = 0; i < 300; ++i) {
std::cout << i << ' ' << i * 12345678 << std::endl;
}
```
при компиляции с `g++ -O2` получается вечный цикл. Но почему? Ведь при проверке не возникает переполнение
Компилятор считает, что выражение `i * 12345678` не переполняется, а значит `i < 174`, а значит **проверку можно убрать**.
Также компилятор может убрать `assert(a + 100 > a)`, так как он вправе считать, что `a + 100 > a` всегда.
Бесконечный цикл без побочных эффектов тоже является UB, компилятор имеет право сделать с ним что угодно,
см [C Compilers Disprove Fermats Last Theorem](https://blog.regehr.org/archives/140).
Помимо неопределенного поведения есть ещё **unspecified behaviour**. Это значит, что
стандарт не говорит, что конкретно должно быть, например, порядок вычислений.
То есть `f(a(), b(), c())` это не undefined behaviour, но unspecified, потому
что неясен порядок вычислений.
**Implementation-defined behaviour** это то, что зависит от реализации и окружения, например,
от компилятора, локали, архитектуры, etc.
За счёт undefined behaviour, компилятор может делать агрессивные оптимизации,
что ускоряет "корректный" код, зато с "некорректным" компилятор может
сделать что угодно.
**Лирическое отступление**.
**Warning** -- замечание компилятора относительно вашего кода, не обязательно нарушение стандарта.
**Примеры**:
```C++
if (x = 0) {
// do something
}
```
Это формально корректный код, и даже используемая идиома, но `clang` кидает предупреждение, и
просит обернуть в скобки, если это сделано специально.
```C
pid_t pid;
if ((pid = fork()) == -1) {
// error while forking, should handle
}
```
Ещё один пример, это unused value. Компилятор предупреждает, не забыли ли вы случайно использовать
значение, например `f();` если `f` возвращает не `void`.
**Некоторые флаги**
Флаг `-Wall` позволяет предупреждать "обо всём", `-Wextra` о том, о чём не предупреждает `-Wall`.
Флаг `-pedantic` говорит компилятору строго чтить стандарт и не использовать свои расширения, по типу
VLA в C++.
Флаг `-Werror` превращает все предупреждения в ошибки.
Подробнее см. `man gcc`, `info gcc`(боже упаси) или [онлайн-документацию](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html).
# II. Compound types
## 2.1. Pointers
Пусть есть какая-то переменная `T x`. У нее есть какой-то адрес, `&x`. Это не буквально её адрес на плашке
оперативной памяти, но по этому числу программа может обращаться к памяти. Для этого не нужно
вдаваться в подробности того, как работает операционная система.
```C++
int main() {
int x;
std::cout << &x << std::endl; // may be different at different runs
```
А какой тип у `&x`? Это `T*`. Ясно, что должна существовать обратная операция к взятию адреса, _разыменование_.
`*p` по указателю `T*` возвращает то, что лежит под ним.
```C++
int x = 0;
int* p = &x;
*p = 2;
```
**Важно**. Несмотря на то, что `*` это часть типа, при объявлении нескольких указателей
её надо каждый раз проставлять заново
```C++
int *a, *b, c;
// a, b are pointers, c is integer
```
Самое главное, **к указателям можно прибавлять числа**. Этим и отличаются типы `T*, U*`, если `T` и `U` разных размеров.
Указатель `p + n` ссылается "на `n` элементов типа `T` дальше, чем `p`". То есть к адресу прибавляется `n * sizeof(T)`.
```C++
std::vector<int> v = {1, 2, 3, 4, 5}; // std::vector guarantees that they're aligned in a row
int *p = &v[0];
std::cout << *(p + 3) << std::endl; // 4
std::cout << *++p << std::endl; //2
```
Указатели можно вычитать(конечно, если они одного типа),
например `&v[3] - &v[0]` равно `3`, а `&v[0] - &v[4]` равно `-4`.
Но если вычесть два указателя, которые лежат не в одном куске, то получится
абсолютно случайное значение.
Грубо говоря, разность это `(address(p) - address(q)) / sizeof(T)`.
Ничего не мешает брать указатели на указатели.
На 64-битной архитектуре размер указателя это 8 байт. Размер указателя не зависит
от типа, на который он указывает.
Унарная `*` это lvalue(объект), а `&` это rvalue(его адрес), и его аргумент
должен быть lvalue(чтобы у него был адрес).
```C++
int a = 1;
int *p = &a;
{
int b = 2;
p = &b;
} // lifetime of b ends here, so p points to trash
std::cout << *p << std::endl; // UB, but most likely 2
```
Память может также переиспользоваться, если потом создать `int c = 3;`, например.
Если вы работаете с указателями разных типов, то скорее всего вы делаете что-то не то.
Их нельзя даже сравнить, будет ошибка компиляции.
Есть особый тип -- `void*`, указатель на непонятно что, любой указатель можно привести к нему,
но обратно без явного приведения нельзя. Но у него нет операций прибавления, разности, разыменовывания.
В C++ есть ключевое слово `nullptr`(константа `NULL` в Си, равная нулю), указатель в никуда.
При его разыменовывании происходит неопреденное поведение.
## 2.2 Kinds of memory
data, text, stack
data -- область памяти, в которой лежат глобальные переменные
text -- область памяти, куда загружена сама программа
stack -- область памяти, где хранятся локальные переменные
Она называется stack, потому что она работает как стек, когда программа входит в функцию,
на стек кладутся аргументы, далее другие локальные переменные. После выхода из функции/области видимости
они извлекаются из стека, и.т.д. В рамках одной локальной области компилятор может класть переменные на
стек в произвольном порядке и делать промежутки.
При входе в функцию также кладется "адрес возврата", откуда надо продолжить выполнять код, после того как
мы выйдем из функции.

22
pages/mipt_cxx1/Makefile Normal file
View File

@ -0,0 +1,22 @@
BUILDDIR ?= build
PAGE := $(shell basename $(shell pwd))
PREFIX := $(BUILDDIR)/page/$(PAGE)
MARKDOWN_FILES := $(wildcard *.md)
MARKDOWN_TARGETS = $(patsubst %.md,$(PREFIX)/%.html,$(MARKDOWN_FILES))
all: $(MARKDOWN_TARGETS) index
$(PREFIX)/%.html: %.md
@mkdir -p $(@D)
pandoc $< --to html --output $@ --standalone
$(PREFIX)/index.html: index.html
@mkdir -p $(@D)
cp $< $@
index: $(PREFIX)/index.html
.PHONY: all index

View File