это учебник по С++ для начинающих, которые хотят изучить основы С++

и быстро перейти на продвинутый уровень, и его можно использовать в качестве учебника по обновлению С++, который кратко проходит по различным темам, мы говорим о печати, переменных, условных операторах, массивах, циклах for, ранжированных циклах, циклах While Loops, выполнении циклов while, функциях, шаблонах функций , перегрузка функций и разрешение перегрузки, специализация шаблонов, итераторы, векторы, ссылки, указатели, динамическое размещение, интеллектуальные указатели, Span, структуры

что такое С++

c++ — это скомпилированный язык со статической типизацией.

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

пример

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

статически типизированный → внутри программы у нас есть много значений, и для каждого значения есть связанный тип, который указывает, что это за значение, является ли оно значением int, значением с плавающей запятой или двойным, поэтому, когда мы говорим, что язык статически типизирован, мы имеем в виду что как только мы назначаем тип значению, мы не можем изменить его во время выполнения, это статично

печать

Печать некоторой информации на экране, чтобы лучше понять, что происходит, используя стандартную библиотеку С++ с использованием std::cout, cout определен в заголовке с именем iostream

мы начнем с включения определения cout iostream

что такое директива включения, выполняющая свою функцию?

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

когда говорить о заголовках нас интересует предварительная обработка и препроцессор

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

поэтому, когда мы включаем iostream, мы говорим препроцессору найти этот заголовочный файл с именем iostream и заменить оператор включения содержимым заголовочного файла, чтобы иметь определение вроде std:: cout

используя g++ -E print.cpp -0 print.ii

мы можем выполнить только предварительную обработку, мы заметим, что файл содержит около 32 тысяч строк кода, и все это содержимое файла iostream, который мы включили.

std::cout<<"hello world";

что такое оператор ‹‹ для объекта выходного потока, который мы используем ‹‹ для выполнения нашей печати

и мы можем связать эти операторы ‹‹

std::cout<<"hello world"<<"whaaat !";

переменные

переменные - это просто имена для значений программы, связанных с типом

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

например

#include<iostream>
using namespace std;
int main(){
    int var1=0;
    int var2=1;
    int var3=var1+var2;
    cout<<var1<<endl;
    
    return 0;
}

поэтому мы можем использовать авто

автоматический вывод типа означает, что мы оставляем тип для определения компилятором

#include<iostream>
using namespace std;
int main(){
    auto var1=0;
    auto var2=1;
    auto var3=var1+var2;// note that the compiler knows that var3 must be an int
    cout<<var1<<endl;
    return 0;
}

auto заставляет нас выполнять инициализацию и определение одновременно

поэтому мы не можем сделать что-то вроде

#include<iostream>
using namespace std;
int main(){
    auto var1;//ERROR:declaration of ‘auto var1’ has no initializer
    return 0;
}

Условный оператор

#include<iostream>
using namespace std;
int main(){
    int a = 10;
    int b = 20;
    if(a>b){
        cout<<"a is bigger"<<endl;
    }else if(a==b){
      cout<<"a is equal to b"<<endl;
    }
    else{
        cout<<"b is bigger"<<endl;
    }
    return 0;
}

массивы

#include<iostream>
#include<array>
using namespace std;
int main(){
    array<int,3> my_arr1={1,2,3};//aggregate intialzation
    array<int,3> my_arr2{1,2,3};//both work

    cout<<my_arr1.at(0)<<endl;//for safe access

    my_arr1.fill(480);
    cout<<my_arr1.front()<<endl;
    cout<<my_arr1.back()<<endl;
    cout<<my_arr1.size()<<endl;//very useful by the way

    return 0;
}

для петель

ранжированные для петель

и он используется для работы с диапазоном значений, таких как все элементы в контейнере, например, контейнер шаблона STL, такой как std:: array или std::vector

#include<array>
#include<iostream>
using namespace std;
int main(){
    array<int,3> my_arr1={1,2,3};
    for(int val:my_arr1){
        cout<<val<<endl;
    }
}

Циклы while и выполнение циклов while

#include<iostream>
int main(){
    int nums = 10;
    while(nums!=0){
        std::cout<<"Nums: "<<nums<<std::endl;
        nums--;
    }
    do{
        std::cout<<"Nums: "<<nums<<std::endl;
        nums++;
    }while(nums!=10);
    return 0;
}

Функции

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

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

#include<iostream>
void/*<-function return type*/ func(/*parameter list*/){
    std::cout<<"function"<<std::endl;
}
int main(){
    func();//calling the function
    return 0;
}

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

обратите внимание на дублированный цикл for, который является плохой практикой кодирования

#include<iostream>
#include<array>
void func(){
    std::cout<<"function"<<std::endl;
}
int main(){
    std::array<int,5> my_arr1{1,2,3,4,5};
    std::array<int,5> my_arr2{5,4,3,2,1};

    for(int val : my_arr1){
        std::cout<<val<<std::endl;
    }
    
    for(int val : my_arr2){
        std::cout<<val<<std::endl;
    }
    return 0;
}

с помощью функций

#include<iostream>
#include<array>
void print_array(std::array<int,5>arr){
    for(int val : arr){
        std::cout<<val<<" ";
    }
    std::cout<<std::endl;
}
int main(){
    std::array<int,5> my_arr1{1,2,3,4,5};
    std::array<int,5> my_arr2{5,4,3,2,1};
    print_array(my_arr1);
    print_array(my_arr2);
    return 0;
}

функции: возвращаемое значение

#include<iostream>
int sum(std::array<int,5>arr){
    int sum = 0 ; 
    for(int val : arr){
        sum+=val;
    }
    return sum;
}
int main(){
    std::array<int,5> my_arr1{1,2,3,4,5};
    auto val = sum(my_arr1);
    std::cout<<val<<std::endl;
    return 0;
}

функции: перегрузка и разрешение перегрузки

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

было бы удобнее, если бы у нас было унифицированное имя для этих функций

#include<iostream>
#include<array>
using namespace std;
void print_int_array(array<int,5> arr){
    for(auto val : arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
void print_float_array(array<float,5> arr){
    for(auto val : arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
int main(){
    std::array<int,5> my_arr1{1,2,3,4,5};
    std::array<float,5> my_arr2{1.1f,2.2f,3.3f,4.4f,5.5f};
    print_int_array(my_arr1);
    print_float_array(my_arr2);
}

как поступить в случае, когда у нас есть несколько функций, которые делают одно и то же, но с разным количеством параметров, должны ли мы создавать несколько функций с разными именами, которые делают одно и то же, ответ, конечно, не

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

#include<iostream>
#include<array>
using namespace std;
void print_array(array<int,5> arr){
    for(auto val : arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
void print_array(array<float,5> arr){
    for(auto val : arr){
        cout<<val<<" ";
    }
    cout<<endl;
}

int main(){
    std::array<int,5> my_arr1{1,2,3,4,5};
    std::array<float,5> my_arr2{1.1f,2.2f,3.3f,4.4f,5.5f};
    print_array(my_arr1);
    print_array(my_arr2);
}

Функция:Шаблоны

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

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

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

#include<iostream>
#include<array>
using namespace std;

template<typename T>
void print_array(T arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}

int main(){
    array<int,3> int_arr{1,2,3};
    //print_array<array<int,3>>(int_arr);
    print_array(int_arr);

    array<float,3> float_arr{1.1,2.2,3.3};
    //print_array<array<float,3>>(float_arr);
    print_array(float_arr);

}

и с auto мы можем сделать то же самое, но эта функция есть только в c++ 20

#include<iostream>
#include<array>
using namespace std;


void print_array(auto arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}

int main(){
    array<int,3> int_arr{1,2,3};
    print_array(int_arr);

    array<float,3> float_arr{1.1,2.2,3.3};
    print_array(float_arr);

}

специализация шаблона

что, если мы хотим сделать что-то немного отличающееся от одного из типов, обрабатываемых шаблоном

#include<iostream>
#include<array>
using namespace std;


void print_array(auto arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}

template<>
void print_array(array<int,3> arr){
    cout<<"from the specialized template function"<<endl;
}

int main(){
    array<int,3> int_arr{1,2,3};
    print_array(int_arr);

    array<float,3> float_arr{1.1,2.2,3.3};
    print_array(float_arr);
}

Итераторы

поэтому итератор обеспечивает общий способ итерации контейнеров с застрявшими библиотеками.

мы начинаем с создания массива с пятью элементами, мы можем перебирать массив с помощью итераторов, используя встроенную функцию array.begin(), чтобы вернуть итератор, который указывает на первый элемент массива, и мы можем увеличить его, чтобы указать для следующего элемента с помощью оператора ++, и мы можем проверить, достигли ли мы конца массива с помощью array.end() и получить доступ к значениям, указанным итератором, с помощью оператора *

#include<array>
#include<iostream>
using namespace std;
int main(){
    array<int,5> my_arr = {1,2,3,4,5};
    for(auto itr = my_arr.begin();itr<my_arr.end();itr++){
        cout<<*itr<<" ";
    }
    cout<<endl;
}

обратный итератор

мы просто используем rbegin() и rend()

#include<array>
#include<iostream>
using namespace std;
int main(){
    array<int,5> my_arr = {1,2,3,4,5};
    for(auto itr = my_arr.rbegin();itr<my_arr.rend();itr++){
        cout<<*itr<<" ";
    }
    cout<<endl;
}

STL-сортировка

#include<array>
#include<iostream>
#include<algorithm>
using namespace std;

void print_array(auto arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
int main(){
    array<int,5> my_arr{115,99,350,1,3};
    print_array(my_arr);
    sort(my_arr.begin(),my_arr.end());
    print_array(my_arr);
}

векторы

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

#include<iostream>
#include<vector>
using namespace std;
void print_vec(auto arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
int main(){
    //note that we only specify the type
    vector<int> my_vec = {5,6,4,1,2};
    print_vec(my_vec);

    //some vector member functions
    my_vec.push_back(5);
    print_vec(my_vec);

    my_vec.pop_back();
    print_vec(my_vec);
}

что на самом деле произошло в памяти, когда мы добавили больше элементов

#include<iostream>
#include<vector>
using namespace std;
void print_vec(auto arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
int main(){
    //note that we only specify the type
    vector<int> my_vec;
    for(int i = 0;i<10;i++){
        //Size = number of elements in the vector
        //capacity =number of elements that we can actaully store
        cout<<"Size: "<<my_vec.size()<<endl;
        cout<<"Capacity: "<<my_vec.capacity()<<endl;
        my_vec.push_back(i);
    }
}

выход

Size: 0
Capacity: 0
Size: 1
Capacity: 1
Size: 2
Capacity: 2
Size: 3
Capacity: 4
Size: 4
Capacity: 4
Size: 5
Capacity: 8
Size: 6
Capacity: 8
Size: 7
Capacity: 8
Size: 8
Capacity: 8
Size: 9
Capacity: 16

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

мы можем использовать резервный метод, чтобы решить это

#include<iostream>
#include<vector>
using namespace std;
void print_vec(auto arr){
    for(auto val:arr){
        cout<<val<<" ";
    }
    cout<<endl;
}
int main(){
    //note that we only specify the type
    vector<int> my_vec;
    my_vec.reserve(10);
    for(int i = 0;i<10;i++){
        //Size = number of elements in the vector
        //capacity =number of elements that we can actaully store
        cout<<"Size: "<<my_vec.size()<<endl;
        cout<<"Capacity: "<<my_vec.capacity()<<endl;
        my_vec.push_back(i);
    }
}
Size: 0
Capacity: 10
Size: 1
Capacity: 10
Size: 2
Capacity: 10
Size: 3
Capacity: 10
Size: 4
Capacity: 10
Size: 5
Capacity: 10
Size: 6
Capacity: 10
Size: 7
Capacity: 10
Size: 8
Capacity: 10
Size: 9
Capacity: 10

и если емкость настолько больше, что нам действительно нужно, мы можем использовать метод уменьшить_до_фита

Рекомендации

иногда нам нужно скопировать переменную в другую переменную, и это нормально делать с переменными небольшого размера, но что, если у нас есть массив из 10 тысяч элементов, нам нужно использовать ссылки, чтобы избежать копирования

#include<iostream>
using namespace std;
int main(){
    int a = 5;
    int b = a; //note that this line copies a to b
    b+=1;
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    return 0;
}

мы хотим создать ссылку на int вместо создания b мы хотим создать ссылку на a вместо того, чтобы запрашивать новую часть памяти

ссылка - это имя для чего-то, что уже существует, b будет псевдонимом для a, поэтому, когда я делаю b+=1, на самом деле я обновляю a, потому что это то же самое пространство памяти.

#include<iostream>
using namespace std;
int main(){
    int a = 5;
    int &b = a;
    b+=1;//
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    return 0;
}

пройти по ссылке

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

#include<iostream>
#include<vector>
using namespace std;
void add_elements(vector<int> vec,int N){
    for(int i =0;i<N;i++){
        vec.push_back(i);
    }
}
int main(){
    vector<int> my_vec;
    add_elements(my_vec,10);
    for(auto i :my_vec){
        cout<<i<<" ";
    }
 }

что случилось, что мы скопировали значения my_vec в параметр функции и работали с копией, поэтому my_vec остался неинициализированным

#include<iostream>
#include<vector>
using namespace std;
void add_elements(vector<int> &vec,int N){
    for(int i =0;i<N;i++){
        vec.push_back(i);
    }
}
int main(){
    vector<int> my_vec;
    add_elements(my_vec,10);
    for(auto i :my_vec){
        cout<<i<<" ";
    }
}

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

указатели

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

#include<iostream>
using namespace std;
int main(){
    int a = 5;
    int*b = &a;// we have a pointer a that stroes the addres of varible a 
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;

    cout<<"a = "<<&a<<endl;
    cout<<"b = "<<*b<<endl;
    
}

разыменование указателя дает значение, хранящееся в адресе памяти

динамическое размещение

поэтому иногда в наших программах нам нужно выделить память во время выполнения, и для этого мы будем использовать новое выражение

поэтому новое выражение выделяет часть памяти и возвращает указатель на нее

#include<iostream>
using namespace std;
int main(){
    int * int_ptr = new int;//allocates one integer
    *int_ptr = 50;//changes the value of it
    cout<<*int_ptr<<endl;
    delete int_ptr;//free memory

    int * int_ptr = new int[10];//allocates ten elements
    *int_ptr = 50;//changes the first element of the array
    int_ptr[3] = 50;//changes third element of array
    cout<<*int_ptr<<endl;
    delete[] int_ptr;//free entrie array
}

unique_ptr

уникальный указатель — это один из типов так называемых интеллектуальных указателей, который имеет уникальное право собственности на объект.

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

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

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

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

#include<memory>
#include<iostream>
using namespace std;
int main(){
    unique_ptr<int[]> ptr(new int[10]);
    for(int i = 0 ;i<10;i++){
        ptr[i]=i;
    }
    cout<<ptr[4]<<endl;
    cout<<ptr[5]<<endl;
}

и используйте ptr как обычную динамически выделяемую память, но она будет автоматически освобождена

другой способ создать уникальный указатель

#include<memory>
#include<iostream>
using namespace std;
int main(){
    auto ptr = make_unique<int[]>(10);
    for(int i = 0 ;i<10;i++){
        ptr[i]=i;
    }
    cout<<ptr[4]<<endl;
    cout<<ptr[5]<<endl;
}

общий указатель

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

#include<iostream>
#include<memory>
using namespace std;
int main(){
    //creating a shared pointer that manages array of ints
    //passing a pointer to the object to be managed
    shared_ptr<int[]> ptr1(new int[10]);
    auto ptr2 = ptr1;
    //now we have 2 shared pointers to the same pieace of memeory
    //so the array of 10 intergers will not be freed until the two smart pointers go out of scoope
    cout<<"Referance count: "<<ptr1.use_count()<<endl;
    //anther way to create shared pointer
    auto ptr3 = make_shared<int[]>(10);

}

Охватывать

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

так что мы можем сделать это с интервалом

span описывает объект, который может ссылаться на непрерывную последовательность объектов

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

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

#include<iostream>
#include<span>
#include<vector>
using namespace std;

void print_subVector(span<int> Span){
    for(auto val : Span){
        cout<<val<<" ";
    }
    cout<<endl;
}

int main(){
    vector<int> my_vec= {1,2,3,4,5,6,7,8,9,10};
    //note im not coping the vector to the function i'm just looking at the content
    print_subVector(my_vec);
    //span(iterator to the begining of container,num of elemtents)
    print_subVector(span(my_vec.begin(),2));

    print_subVector(span(my_vec.begin()+1,3));
}

структуры

#include<iostream>
using namespace std;
struct Point{
    double x; 
    double y;
    void print(){
        cout<<"x = "<<x<<endl;
        cout<<"y = "<<y<<endl;
    }
};
int main(){
    Point p1;
    p1.x = 10;
    p1.y=20;
    p1.print();
}

параллельные алгоритмы STL

#include<numeric>
#include<vector>
using namespace std;
int main (){
    vector<int> my_vec(1<<30);
    auto sum = reduce(execution::par_unseq,my_vec.begin(),my_vec.end(),0);
}

пространства имен

#include<iostream>
using namespace std;
namespace A{
    void print(){
        cout<<"print namespace A"<<endl;
    }
}
namespace B{
    void print(){
        cout<<"print namespace B"<<endl;
    }
}

void print(){
    cout<<"print global namespace"<<endl;
}
int main(){
    print();
    A::print();
    B::print();
}