+---------------------------+
|.-------------------------.|
|| kee_reel@blog:~/ru $ cd ||
|| ссылки контакты         ||
|| c c++ linux opengl sql  ||
|| python сети             ||
||                         ||
|.-------------------------.|
+-::---------------------::-+
.---------------------------.
 // /oooooooooooooooooooooo\\ \\ 
 // /oooooooooooooooooooooooo\\ \\ 
//-------------------------------\\
\\-------------------------------//


C++. Различия между C и C++

Время чтения: 11 минут

Чтобы программировать на C++ необходимо сперва научиться программировать на C. Почему так?

Это не новый язык, который придётся заново изучать – C++ является расширением языка C, и всё, что ты изучил в курсе C, тебе пригодится и в C++. Однако, какие-то моменты там теперь можно писать иначе – я покажу какие.

Если ты не знаешь C, то ты не сможешь изучить C++. Начни с языка C.

Вот основное, что изменилось в C++ относительно Си:

Ввод/вывод

Вот пример ввода/вывода на 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 – удобная работа с файлом через потоки

В следующей статье начнём наступление на классы.