Содержание

Синтаксис

Переменная

Переменная - именованная область памяти. Определяется по схеме <тип ___ модификатор ____ имя>.

Синтаксис определения переменной

int a;

int a, b, c; //error! Имя а уже использовалось

int d = 1, k = 0; //Задание с инициализацией

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

Хороший тон

int a = 0; //Пример хорошего кода

int b = 2;

int c = 10;

Массив

Массив - последовательность однотипных значений, расположенных в памяти непрерывно. В массиве расположены n переменных под одним именем. Нумерация элементов производится от 0 до n-1. Размер статического массива определяется на этапе компиляции, а НЕ выполнения программы. Следовательно, размер может быть только константной величиной.

Примеры создания массивов

int m[10]; /* Массив из 10 элементов типа int. Значения всех элементов - мусор.*/

m[3]; /* Обращение к четвертому элементу массива */

int k[10] = {1, 2, 3}; /* Пример задания массива с инициализацией. Не проинициализированные элементы k[3]..k[9] заполняются нулями. */

int l[] = {1, 2, 3, 4, 5}; /* Весь массив проинициализирован. Размер равен 5 */

Двумерные массивы

int n[2][3]; /* Создана матрица из 3 столбцов и 2 строк. */

int arr[2][3] = {{0, 1, 2}, {3, 4, 5}}; /* Матрица создана и проинициализирована. */

Расположение элементов в массиве arr:

0 1 2

3 4 5

Типы данных

Тип - это набор значений, которые может принимать переменная.

Встроенные типы

Встроенные типы подразделяются на целочисленные (int, char, wchar_t) и типы с плавающей точкой (float, double). Все они могут быть использованы в сочетании с ключевыми словами long или short. При этом всегда выполняется следующее неравенство.

Соотношения между размерами типов

sizeof(short int) <= sizeof(int) <= sizeof(long int); /* sizeof возвращает размер типа */

Таблица размеров типа int в различных системах

Системаshort intint long int
16 бит224
32 бита244
64 бита2 или 44 или 88

Кроме того, используются модификаторы signed (знаковый, положительные и отрицательные значения, а так же 0) или unsigned (беззнаковый, только неотрицательные). Если он не указан, то по умолчанию задается signed. Данное ключевое слово указывается до модификатора long/short, если он есть. Например, unsigned long int или unsigned int. Диапазон значений равен 2 в степени n*8, где n - размер переменной данного типа в байтах. Так четырехбайтный int может принимать 24*8 = 232 различных значений в диапазоне -231...231-1 для signed и 0..232-1 для unsigned.

В памяти машины переменная хранится в виде соответствующей последовательности единиц и нулей (бит). Для signed int первый бит отвечает за знак числа (0 для положительных и 1 для отрицательных). Отрицательные числа записываются в дополнительном коде, полученным путем инверсии противоположного числа и прибавления единицы.

Получение дополнительного кода

0000000000000101 - число 5 в двоичном виде

1111111111111010 - инверсия

1111111111111011 - число -5 в двоичном виде

Проверка

  0000000000000101

+1111111111111011

  0000000000000000 - Действительно, 5 + (-5) = 0

Если указан один из модификаторов, но не указан тип данных, то по умолчанию он считается int.

long a = 10; //a имеет тип signed long int

В C++ можно задавать числа в десятеричной, восьмеричной и шестнадцатеричной системах исчисления. Возможность записи в двоичной систме в C++ отсутствует.

Системы счисления

int a = 10; //десятеричная система

int b = 0xA; //шестнадцатеричная (запись числа начинается с 0x)

int c = 012; //восьмеричная (запись числа начинается с 0)

int d = 0x1010b; //warning! НЕ бинарная, а шестнадцатеричная строка

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

10 - тип int

10l - long int

10u - unsigned int

10ul - unsigned long int

10.0 - double

10.0f - float

Тип char принимает значения от 0 до 255. Каждому коду соответствует символ. В таблице ASCII приведены символы с номерами от 0 до 127. Остальные зависят от кодировки. Существует так же расширенный тип wchar_t, имеющий размер 2 байта (для UCS2) или 4 (для UCS4). Здесь количество закодированных символов намного больше. Модификатор по умолчанию signed или unsigned для этих типов определяется компилятором. Поэтому, чтобы сделать цикл по char, signed указывается явно.

Определение char и wchar_t

char a = 'd'; /*Присвоен конкретный символ*/

char b = 10; /* 10 имеет тип int. Произведено преобразование, из 4 байт вырезан 1. b приняла символ, имеющий 10-ый номер в таблице ASCII */

wchar_t c = L'ф'; /* c проинициализирована символом из кириллицы */

Размер переменной типа size_t всегда совпадает с размером машинного слова. Может быть только unsigned.

Числа с плавающей точкой хранятся с помощью типов float, double и long double. Float занимает 4 байта, содержит 6 значащих символов и может принимать значения вплоть до 10+/-38. Размер double определяется 8 байтами, и кодируются 14-15 значащих символов при диапазоне значений до 10+/-308. Рекомендуется практически всегда использовать тип double.

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

Пример негативного влияния погрешности

for( float f = 1e-10; f != 10; f *= 10 ) printf("%f", f); /* бесконечный цикл */

Переменная типа bool может принимать два значения (true или false). Размер в стандарте не указан.

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

Преобразование типов

Перевод значения из одного типа в другой может быть явным и неявным. Явные преобразования в стиле С осуществляются с помощью синтаксической конструкции (<имя типа>). Большое количество преобразований сказывается на быстродействии и точности.

int a = 10;

double b = (double) a; /* Справа от равно явное преобразование из int в double */

int c = 1.5; /* Пример неявного преобразования. 1.5 имеет тип double. Результат: с = 1 */

short d = 10; /* Так же неявное преобразование. 10 имеет тип int, а не short int */

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

Тип результата при различных операндах

int и int --> int

int и float --> float

short и short --> int

float и double --> double

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

short i = 10, j = 20;

short l = j + i; /* Результат сложения имеет тип int, который неявно преобразуется в short int */

double k = i/j; /* Результат деления имеет тип int, поэтому в k записывается значение 0, а не 0.5, как ожидалось */

k = (double)i/j; /* Перед делением происходит явное преобразование i в тип double, поэтому результат деления так же double. Теперь в k записано 0.5 */

При сравнении двух переменных signed и unsigned не происходит преобразования к более общему типу, т.к. часть возможных значений первого операнда не влезает в диапазон второго и наоборот. Компилятор должен выдать предупреждение.

При преобразовании целочисленной переменной в bool, нулевое значение преобразуется в false, а все остальные - в true. При обратном процессе false обращается в 0, а true в 1.

Управляющие конструкции

Управляющими конструкциями обеспечиваются циклы и ветвления.

Ветвление

Тело управляющей конструкции для ветвления состоит из ключевого слова if, логического выражения в круглых скобках, блока кода в фигурных скобках, выполняемого, если логическое выражение возвращает true. Можно также обозначить вторую ветку через ключевое слово else. Код блока else выполнится, если логическое выражение вернет false. Между блоками if и else иногда добавляют несколько конструкций if else (<логическое выражение>), проверяющих дополнительные условия.

if (/*выражение*/) { /* код */ } //простейший случай ветвления

 

if (/*выражение*/) { /* код */ }

else { /* код */ } //задана альтернативная ветка

 

if (/*выражение*/) { /* код */ }

else if (/*выражение*/) { /* код */ }

else if (/*выражение*/) { /* код */ }

else { /* код */ } /*заданы несколько альтернативных веток с проверкой дополнительных условий */

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

Другой конструкцией для ветвления является switch case, но ее рекомендуется не использовать из-за сложности исполнения и отсутствия возможности сравнивать составные типы данных.

Циклы

Существуют 3 вида циклов: с предусловием (while), с постусловием (do while, код в блоке выполнится хотя бы раз), с индексацией (for).

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

while (/* условие */) { /* код */ } //цикл с предусловием

do { /* код */ } while(/* условие */); //цикл с постусловием

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

for (int i = 0; i < 10; ++i) { /* код */ }

/* Простейший случай for(<инициализация>; <условие>; <постдействие>) */

 

int i = 0;

while(i < 10) { /* код */ ++i; }

// Два этих цикла полностью аналогичны

Возможные варианты цикла for.

for(int i = 0, j = 20; i < 15; ++i, j/=2) { /* код */ } /* Цикл по двум переменным */

for( ; ; ) {} //Бесконечный, ничего не делающий цикл

Ключевые слова break и continue в теле цикла прерывают выполнение кода, и, в случае break, происходит выход из цикла, а при использовании continue, осуществляется досрочный переход на следующую итерацию.

Общие правила

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

Управляющие конструкции могут быть сколь угодно большой степени вложенности. Но хороший код требует избегать лишних вложений (лучше вынести что-нибудь в отдельную функцию).

Функции

Функция является блоком кода, заголовок которого состоит из типа возвращаемого значения, названия и списка операндов в скобках. Все функции, кроме тех, которые имеют тип void, должны возвращать значение через ключевое слово return. Если функция определена после ее первого использования, тогда ее дополнительно объявляют. Объявления так же используют в *.h файлах, где определения писать не принято.

Синтаксис функции

int foo(int a, bool flag = false); /* Объявление (declaration) */

 

int foo(int a, bool flag) { /* Определение (defenition) */

    if (flag) return a*a;

    return a; /* Можно не писать else, т.к. при flag==true выполнение блока прерывается на предыдущем return */

}

Для некоторых параметров функции можно сразу указывать значения по умолчанию (см. параметр flag в предыдущем примере). Рекомендуется указывать их только в объявлении.

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

Перегрузка функций

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

Перегрузка - это подбор наиболее подходящей функции из списка. Происходит в момент компиляции.

Пример

int foo(int a); // I

int foo(long a); // II

 

foo(10); // Вызов I

foo(10l); //Вызов II

foo(1.0); // error!

Преобразование имён функций

При компиляции происходит преобразование имён функций. Так функция I из предыдущего листинга будет переименована в что-то вроде foo@i, а II - в foo@s. Утилита objdump показывает списки секций в объектном файле, а c++filt выводит все сигнатуры (название функции + принимаемые операнды) из заданного объектного файла.

Совместимость с C

Язык С, в отличие от С++, не поддерживает перегрузку функций. Поэтому встает вопрос об обеспечении совместимости кода на С и С++. Для его решения используется блок extern "C", внутри которого пишут функции без перегрузок для дальнейшего использования в программах на С.

Синтаксис

extern "C" {

       /* Функции без перегрузок */

}

Операции

В языке программирования С++ определен набор операций. Одни из них (математические, логические, побитовые) возвращают некое значение, не меняя при этом переменную-операнд, другие же, например, все операции присваивания, всегда ее меняют.

Таблица операций

Категория СимволНазначениеСимволНазначение

Математические

+Сложение*Умножение
-Вычитание/Деление
%

Остаток от деления

(только для целых чисел)

  

Присваивание (меняют значение переменной)

= Простое присваивание /= Деление с присваиванием
-= Вычитание с присваиванием ++ Инкремент (постфиксный или префиксный)
*= Умножение с присваиванием -- Декремент (постфиксный или префиксный)
+= Сложение с присваиванием  
Сравнение (в качестве результата возвращают значение типа bool) == Сравнение на равенство < Меньше
>= Больше или равно > Больше
<= Меньше или равно != Не равно
Логические (операнды приводятся к типу bool, результат также этого типа) && AND ! NOT
|| OR    
Логические побитовые(операция производится над каждым битом операндов) & AND ~ NOT
| OR ^ XOR
Побитовые сдвиги (сдвигает биты в машинном представлении переменной) << n влево на n позиций >> n вправо на n позиций
Тернарный оператор (a > b) ? 2 : 3 возвращает 2, если верно выражение в скобках, а иначе 3

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

int a = 10;

a = a++ + ++a; // Undefined behaviour

Операции над значениями

Примеры использования

int a = 9; /* Создание переменной и присваивание ей значения 9 */

a = a * 2; /* Выражение выполняется с право на лево. Сначала операция умножение возвращает число 18, а затем происходит присваивание. Теперь a = 18 */

a *= 2; /* Выполняет то же, что и предыдущая строка. Теперь а = 36 */

int k = ++a; /* Сначала выполнен инкремент, потом присваивание. Результат: k = 37, a = 37 */

k = a++; /* Сначала присваивание, затем постфиксный инкремент. Результат: k = 37, a = 38 */

a = (a > 0) ? 0 : k; /* В данном случае а > 0, поэтому тернарный оператор вернет значение до : , в противном случае вернулось бы k. Результат: а = 0 */

bool b = ((a != 0) && (k/a >= 7)); /* b = false */

Замечание: операторы && и || ленивые. Это значит, что если первый операнд однозначно определяет возвращаемое значение, то второй уже не важен. Таким образом, в последнем примере программа никогда не упадет от ошибки деления на ноль, так как при а == 0 первый операнд false, и выражение после && не выполняется.

Использование тренарного оператора при работе со сложными типами данных считается плохим тоном.

Операции над битами

Пусть m - некая битовая маска из четырех единиц или нулей, где старший бит отвечает за то, горит ли лампа. Тогда для проверки состояния лампы необходимо воспользоваться битовыми операциями.

bool flag = (m & (1 << 4)); /* flag примет значение true, если лампа включена. */

Число 1 есть последовательность из 31 нуля и одной единицы в младшем бите. Побитовый сдвиг влево перемещает ее на 4 бита. Теперь, если старший бит в маске так же равен единице, то выражение в скобках всегда будет возвращать не нулевое значение.

Операция sizeof()

Операция sizeof() не является классическим вызовом функции, она выполняется в момент компиляции и возвращает размер переданной переменной или типа. При этом никакие операции или функции внутри скобок sizeof() произведены не будут.

int num = 9;

int size = sizeof(++num); /* size = 4 (размер типа int); num не увеличился, по-прежнему, равен 9 */