C++. Различия между C и C++
Время чтения: 11 минут
Чтобы программировать на C++ необходимо сперва научиться программировать на C. Почему так?
Это не новый язык, который придётся заново изучать – C++ является расширением языка C, и всё, что ты изучил в курсе C, тебе пригодится и в C++. Однако, какие-то моменты там теперь можно писать иначе – я покажу какие.
Если ты не знаешь C, то ты не сможешь изучить C++. Начни с языка C.
Вот основное, что изменилось в C++ относительно Си:
- Ввод/вывод
- Потоки ввода/вывода
- Пространства имён
- Переменные и типы данных
- Новый тип
bool
- Ключевое слово
auto
- Новый тип
- Условия
- Циклы
- Указатели
new
,new[]
,delete
,delete[]
nullptr
- Массивы
- Строки
std::string
- Функции
- Параметры по умолчанию
- Перегрузка функций
- Передача по ссылке
- Структуры
- Файлы
fstream
Ввод/вывод
Вот пример ввода/вывода на C:
// Подключаем библиотеку стандартного ввода/вывода
#include <stdio.h>
int main()
{
int x;
// Используем scanf (подключенный из stdio.h) для ввода
scanf("%d", &x);
// Используем printf (подключенный из stdio.h) для вывода
printf("Value = %d\n", x);
return 0;
}
Точно такую же программу можно написать в C++, и она будет прекрасно работать. Надо, разве что, изменить название библиотеки с stdio.h
на cstdio
(добавляем ‘c’ в начале, убираем “.h” в конце):
#include <cstdio>
Но, такой способ ввода/вывода называется теперь “C style”, а мы же хотим быть теперь “C++ style”! Вот как такая же программа будет выглядеть в новом “C++ style”:
// Подключаем библиотеку потокового ввода/вывода
#include <iostream>
int main()
{
int x;
// Используем std::cin (подключенный из iostream) для ввода
std::cin >> x;
// Используем std::cout (подключенный из iostream) для вывода
// В конце выводим std::endl вместо '\n'
std::cout << "Value = " << x << std::endl;
return 0;
}
Потоки ввода/вывода
Потоки это нововведение C++, которое облегчает работу с вводом/выводом в консоль, файлы и т.д.
-
cin
это поток стандартного ввода – из него мы можем получать данные, введённые с клавиатуры. -
cout
– поток стандартного вывода, который может выводить данные в консоль.
Чтобы получить из потока данные, используют оператор >>
(стрелочки наружу), а чтобы отправить в поток данные – <<
(стрелочки внутрь).
Пространства имён
Ещё, в примере используется какая-то непонятная конструкция std::
– что это?
Представь, что у тебя есть две функции с одинаковым названием, и ты вот ни в какую не хочешь их переименовывать. Что делать?
// Возвращает введённую строку
char* input()
{
// ...
}
// Возвращает введённое число
int input()
{
// ...
}
В языке C ты бы ничего не смог бы поделать, а в C++ для разрешения таких ситуаций появляются пространства имён:
namespace string_utils
{
char* input()
{
// ...
}
};
namespace number_utils
{
int input()
{
// ...
}
};
Они позволяют разделить определение переменных, функций и прочего так, чтобы они не конфликтовали друг с другом. Обратиться к функциям из примера можно так:
char str = string_utils::input();
int number = number_utils::input();
В случае с cin
и cout
используется пространство имён std, поэтому они так и записываются: std::cin
, std::cout
.
Переменные и типы данных
В языке C использовались следующие примитивные типы данных:
- char
- int
- float
- double
В комплекте с модификаторами:
- unsigned – для беззнаковых
- short – для сокращённого размера
- long – для увеличенного размера
Тип bool
В языке C++ используются такие же типы данных, плюс добавляется новый тип данных – bool
:
bool value_t = true;
bool value_f = false;
if(value_t)
{
// ...
}
В языке C мы использовали для этих задач char
, в который записывали 1
или 0
. Теперь у нас есть более подходящий для этого тип данных.
“Тип” auto
Одним из нововведений C++ является ключевое слово auto
– оно позволяет нам не указывать тип переменной, если компилятор может понять его из контекста:
#include <iostream>
#include <typeinfo>
int main()
{
int x = 10;
auto y = 20;
auto z = 30.0;
auto w = 40.0f;
std::cout << typeid(x).name() << std::endl; // Выведет i -- int
std::cout << typeid(y).name() << std::endl; // Выведет i -- int
std::cout << typeid(z).name() << std::endl; // Выведет d -- double
std::cout << typeid(w).name() << std::endl; // Выведет f -- float
}
Чтобы узнать тип переменной я воспользовался C++ библиотекой typeinfo – не пугайся, она не используется повсеместно, и здесь она нужна только для демонстрации.
Для простых типов auto
не очень помогает, но в дальнейшим он позволит нам сократить количество кода (когда будут названия классов).
Условия
Условия в C и C++ работают одинаково:
int x;
std::cin >> x;
if(x > 0)
{
std::cout << "+";
}
else if(x < 0)
{
std::cout << "-";
}
else
{
std::cout << "=";
}
Циклы
Циклы в C и C++ работают одинаково:
// Цикл for
for(int i = 0; i < 10; i++)
std::cout << i;
// Цикл while
int i = 0;
while(i < 10)
std::cout << i++;
// Цикл do while
i = 0;
do {
std::cout << i++;
} while(i < 10);
Забегая вперёд, for теперь может записываться через двоеточие при итерировании по STL контейнеру:
std::vector<int> vs = {1, 2, 3}; for (int s : vs) std::cout << s << std::endl;
Но об этом будем говорить потом.
Указатели
Указатели в C и C++ работают одинаково:
int x = 42;
int *p1 = &x; // Сохранит адрес переменной x
p1++; // Переместит указатель на 4 байта вправо
p1 -= 2; // Переместит указатель на 8 байт влево
char *p2 = p1; // Сохранит адрес, который сейчас записан в p1 (4 байта влево от переменной x)
p2 += 4; // Переместит указатель на 4 байта вправо, на первый байт переменной x
std::cout << *p2; // Выведет значение первого байта x
Если ты понял что тут написано, то ты понимаешь что такое указатели и как с ними работать.
Для быстрых и смелых – в библиотеке STL добавятся “умные” указатели. В реальных проектах никто не работает с “сырыми” указателями, а пользуются “умными”. Если хочешь ознакомиться, то вот.
Выделение памяти
В Си для динамического выделения памяти использовались функции malloc
, calloc
, realloc
и free
:
struct Example {
int a;
float b;
};
int main() {
// Выделяем память под структуру
Example *ex = malloc(sizeof(Example));
// Что-то с ней делаем
ex->a = 42;
ex->b = 42;
// Освобождаем выделенную память
free(ex);
// Выделяем массив из 10 int'ов
int n = 10;
int *arr = calloc(n, sizeof(int));
// Что-то с ним делаем
for(int i = 0; i < n; i++)\
arr[i] = i;
// Освобождаем выделенную память
free(arr);
}
В C++ для этого появились операторы new
, new[]
, delete
и delete[]
:
struct Example {
int a;
float b;
};
int main() {
// Выделяем память под структуру через оператор "new"
Example *ex = new Example();
// Что-то с ней делаем
ex->a = 42;
ex->b = 42;
// Освобождаем выделенную память через оператор "delete"
free(ex);
// Выделяем массив из 10 int'ов через оператор "new[]"
int n = 10;
int *arr = new int[n];
// Что-то с ним делаем
for(int i = 0; i < n; i++)\
arr[i] = i;
// Освобождаем выделенную память через оператор "delete[]"
delete[] arr;
}
Обрати внимание, что если мы выделяем память для массива через new int[10]
, то освободить её мы должны через delete[]
– если будет использован просто delete
, то поведение программы будет непредсказуемым.
Кроме выделения памяти эти операторы ещё вызвают конструкторы объектов соответствующего типа – в следующей статье про это уже будет рассказано.
nullptr
Из нового – помнишь NULL
? Так вот, вместо него давай теперь использовать nullptr
. Объясняю для чего:
int x = NULL;
Такой код успешно скомпилируется. NULL
это нулевой адрес (void*)0
, который может быть неявно приведён в тип int: (void*)0
-> 0
.
nullptr
лишён такого недостатка:
int x = nullptr;
Такой код не скомпилируется, так как nullptr
запрещает неявное приведение в любые типы, отличные от указалелей или bool.
Возможность неявного приведения в тип bool нужна для использования в условиях:
int *x = nullptr;
if(x)
{
// Выполнится только если x не равен nullptr
}
Массивы
Массивы в C и C++ работают одинаково:
int x[10];
for(int i = 0; i < 10; i++)
std::cin >> x[i];
int *y = malloc(sizeof(int) * 10);
for(int i = 0; i < 10; i++)
std::cin >> x[i];
Строки
В C строки были представлены char массивами с нуль-терминатором '\0'
в конце:
char str[50] = "Hello world!";
Для работы со строками использовались функции из библиотеки string.h
:
- strlen(char *str) – получить длину строки
- strcat(char *str1, char *str2) – конкатенация двух строк
- strtok(char *str, char *delimiter) – разделить строку
- strstr(char *str, char *str_to_find) – найти подстроку в строке
- strchr(char *str, char char_to_find) – найти символ в строке
std::string
В C++ всё ещё можно работать со строками так же, но это не приветствуется. В C++ вся работа со строками происходит через класс std::string
из библиотеки string
:
std::string str = "C++ style string.";
str.length(); // Длина строки
std::string another_str = "Another string";
std::string concatenated_str = str + another_str; // Конкатенация строк
Это просто пример использования – мы подробнее познакомимся с ними в дальнейшем.
Функции
Функции в C и C++ работают одинаково:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int result = add(1, 2);
std::cout << result << std::endl; // Выведет "3"
}
Параметры по умолчанию
Теперь для параметров функции можно указать значения по умолчанию, которые будут использованы, если ничего не было указано:
#include <iostream>
int add(int a=2, int b=2)
{
return a + b;
}
int main()
{
// Тут можно обойтись и без временной переменной
// a = 1, b = 1
std::cout << add(1, 1) << std::endl; // Выведет "2"
// a = 0, b = 2 (по умолчанию)
std::cout << add(0) << std::endl; // Выведет "2"
// a = 2 (по умолчанию), b = 2 (по умолчанию)
std::cout << add() << std::endl; // Выведет "4"
}
Из-за того, что параметры передаются последовательно, нельзя первый параметр взять “по умолчанию”, а второй передать – придётся ручками указать первый:
add(, 1); // ОШИБКА
add(2, 1); // ОК
Ещё, не обязательно чтобы все параметры были по умолчанию – но при этом есть ограничение, что аргументы по умолчанию надо указывать начиная с последнего и далее, к началу:
int add(int a = 2, int b); // ОШИБКА
int add(int a, int b = 2); // ОК
Перегрузка функций
Перегрузка функций позволяет определить несколько функций с одинаковым названием, но различными аргументами:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
int main()
{
std::cout << add(1, 2) << std::endl; // Выведет "3"
std::cout << add(1.1, 2.2) << std::endl; // Выведет "3.3"
}
Это работает так – компилятор, по типам указанных аргументов, старается подобрать самую подходящую функцию.
В некоторых случаях он не может выбрать подходящий вариант:
add(1.1, 2);
// test.cpp: In function ‘int main()’:
// test.cpp:107:21: error: call of overloaded ‘add(int, double)’ is ambiguous
// 107 | std::cout << add(1.1, 2) << std::endl;
// | ~~~^~~~~~~~
И ещё, перегружаемые функции должны различаться аргументами, а не возвращаемыми значениями:
int add(int a, int b);
double add(double a, double b);
// test.cpp:100:5: error: ambiguating new declaration of ‘double add(int, int)’
// 100 | double add(int a, int b)
// | ^~~
// test.cpp:96:8: note: old declaration ‘int add(int, int)’
// 96 | int add(int a, int b)
// | ^~~
Передача по ссылке
Представь что в примере выше мы хотели бы изменять значение переменной result
прямо внутри функции add
. Для этого мы бы передавали переменную result
через указатель:
#include <iostream>
void add(int *result, int a, int b)
{
*result = a + b;
}
int main()
{
int result;
add(&result, 1, 2);
std::cout << result << std::endl; // Выведет "3"
}
В C++ теперь мы можем не возиться с этими указателями, а передать переменную по ссылке (by reference):
#include <iostream>
void add(int &result, int a, int b)
{
result = a + b;
}
int main()
{
int result = 0;
add(result, 1, 2);
std::cout << result << std::endl; // Выведет "3"
}
Чтобы передать переменную по ссылке, надо указать символ имперсанта &
при объявлении типа.
Структуры
Структуры в C и C++ работают одинаково:
#include <iostream>
struct MyStruct
{
int age;
char name[50];
};
int main()
{
MyStruct s;
s.age = 21;
strcpy(s.name, "John Snow");
}
Единственное отличие в том, что теперь не надо писать struct
перед названием структуры (вот так не надо struct MyStruct s
).
Файлы
Файлы в C и C++ работают одинаково:
#include <cstdio>
int main()
{
char *text = "Some text to fill the file\nAnd some more text";
FILE *f = fopen("some-file.txt", "w");
fprintf(f, "%s", text);
fclose(f);
return 0;
}
Потоки при работе с файлами
Потоки в C++ можно использовать не только для вывода в консоль, но и для вывода в файл:
#include <fstream>
int main()
{
char *text = "Some text to fill the file\nAnd some more text";
std::ifstream f("some-file.txt");
f << text;
f.close();
}
Так же можно считывать из файла (тут уже нужен std::string
):
#include <fstream>
int main()
{
std::string text;
std::ifstream f("some-file.txt");
f >> text;
f.close();
}
Согласись – стало удобнее :)
Заключение
Итого, мы вспомнили и узнали новое по этим темам:
- Ввод/вывод
- Потоки –
std::cout
для вывода,std::cin
для ввода - Пространства имён –
namespace
чтобы избегать пересечений названий
- Потоки –
- Переменные и типы данных
- Новый тип
bool
– можно присваиватьtrue
иfalse
вместо1
и0
- Ключевое слово
auto
– чтобы компилятор сам додумывал тип переменной
- Новый тип
- Условия
- Циклы
- Указатели
new
,new[]
,delete
,delete[]
– операторы, которые надо использовать при выделении памятиnullptr
– более безопасная заменаNULL
- Массивы
- Строки
std::string
– удобная замена “сырым” char массивам
- Функции
- Параметры по умолчанию – для параметров функции можно указать значения по умолчанию
- Перегрузка функций – теперь можно делать функции с одним именем, но разными параметрами
- Передача по значению – можно не заморачиваться с передачей через указатель
- Структуры
- Файлы
fstream
– удобная работа с файлом через потоки
В следующей статье начнём наступление на классы.