Имена,
разделители, комментарии
Операторы (команды) =, { }, if, switch, goto, for,
while, do, break, continue
Способы
образования новых типов
Преобразование
числовых типов и указателей
Хранение в
памяти локальных и глобальных переменных
Функции с
переменным количеством аргументов
Объявление
(предопределение) функций и типов
Ограничение
доступа к членам. Классы.
Представление
классов в памяти
Распределение
программы по файлам
Экспортирование
и импортирование функций
Данное описание языка
программирования C++ излагает почти все конструкции этого языка, не включает описание
стандартных библиотечных функций и функций для работы в операционной системе Windows.
a, ABC, An95b6, __N5_a8_ - возможные имена (идентификаторы) переменных,
функций, типов и т.п. Имя состоит из латинских больших и маленьких букв, цифр и
знаков подчеркивания и начинается с буквы или
подчеркивания. Имена различны, если состоят из разных символов, причем
считается, что большая буква отличается от маленькой.
5a, a$8, ф - неправильные имена.
Текст программы делится на раздельные слова (токены) – имена, названия
операторов,
отдельные символы (скобки, запятые и т. д.). Эти слова могут быть
разделены любым количеством пробелов и переходов на следующую строку без
изменения содержания текста (за исключением перехода на следующую строку в
командах препроцессора, начинающихся с #, и в записи
строковых констант, см. ниже). Если можно разделить два слова,
даже если между ними нет пробела, то пробел можно не писать. Например:
int a=b+3; Это кусок программы состоит из 7
слов (“int”, “a”, “=”, “b”, “+”, “3”, “;”). Пробел между “int” и “a” нельзя убрать.
Между любыми словами можно вставлять комментарии
(произвольный текст, не влияющий на смысл программы). Комментарии бывают
строчными - начиная с символов “//”, до конца строки – и скобочными - все, что стоит между символами “/*” и “*/”. Например:
int a=b+3; //Переменная a
int a/*Переменная a*/=/*
Переменная b*/b+3;
эквивалентно
int a=b+3;
A a,b,c; Переменные a, b, c имеют тип A.
bool,char,short,int,long,float,double - предопределенные типы. Здесь и далее ключевые слова C++ выделяются жирным шрифтом.
bool - логический тип,
имеет два значения - 0 - false, 1 - true.
char - символ,
например, ’a’.
char,short,int,long - целые числа со
знаком, занимающие 1,2,4,4 байта. Так char от -128 до 127.
unsigned int - целые числа могут быть без знака, если к ним приписать unsigned.
unsigned char - от 0 до 255.
float,double - числа с
плавающей точкой (имеют экспоненциальное представление), занимают 4,8 байт.
Пример
определения переменных:
int n,m; n и m - 4-байтовые целые
числа со знаком.
int n=3,m=5; определение n и m и задание им
соответствующих начальных значений (инициализация).
Любая переменная хранится в памяти по какому-то адресу. Адрес является 4-байтным
целым числом без знака. Вся память - последовательность
байт. Адрес A указывает на
какой-то байт. A+1 указывает на следующий байт.
Никакие данные, которые использует программа не располагаются по нулевому
адресу. Данное, находящееся по адресу A может занимать только целое число байт. Размер данного измеряется в байтах. Байт состоит из 8 бит. Бит может
принимать два значения - 0 или 1. Байт может принимать 28=256 значений, n-байтное данное - N=256n значений: 2-байтное - N=65 536 значений, 4-байтное - N=4 294 967 296.
Целые числа без знака представляются в двоичной системе счисления, заполняя все биты,
поэтому они имеют диапазон от 0 до N-1. При суммировании таких чисел k и m компьютер получает новое двоичное число, которое не
уместится в n байт, если k+m³N. Более старшие
разряды откидываются. Если мы прибавим таким способом
к N-1 единицу, то получим 0. Аналогично, при вычитании из 0 единицы получится N-1. При умножении тоже откидываются
старшие разряды. При целочисленном делении откидываются цифры после запятой.
Над целыми числами со знаком арифметические операции производятся так же, как и с числами без
знака, если смотреть на их двоичное представление в компьютере. Если старший
бит числа равен 0, то оно считается положительным,
если 1, то -
отрицательным. Такое число имеет диапазон от -N/2 до N/2-1. Если
число k положительное, то
оно имеет такое же побитовое представление, как k без знака, если отрицательное - то такое же
представление, как N-k без знака. Если n-байтное целое число расположено по адресу A, то будет ли по адресу A расположен старший или
младший байт этого числа зависит от машинной реализации.
Число с плавающей точкой хранится в виде ±1,abc...×2±k. В нем какие-то биты отводятся
на мантиссу (±1,abc...), а какие-то на порядок (±k). У такого числа количество значащих цифр не зависит от
порядка. Заметим, что если целое число или число с
плавающей точкой равны 0, то каждый их байт равен 0.
5,-63,’a’ - числа типа int. ’a’=97 - числовое значение символа.
5.0, -14e-5, 5e3 - числа типа double. -14e-5=-14×10-5. Буква e может быть
заглавной.
0xf43ab - 16-ричное представление числа типа
int (спереди приписывается ‘0x’). Буквы x,a-f могут быть заглавными. Например, 0x10=16, 0xFF=255, 0x100=256.
5u,0x10u - числа типа unsigned int (сзади
приписывается ‘u’ или ‘U’).
((n+5)*3+2*m)/8 - пример выражения. Выражение -
это то, что имеет значение. Например, 3+5 имеет значение 8.
+, -, *, /, %, >>, <<, ++, --, !, &&, ||, &, |, ^, ~, ==, !=, <, >, >=, <=, =, +=, -=, *=, /=, %=, >>=, <<=,
^=, &=, |=, ? : - операторы, участвующие в выражениях.
Далее вместо n и m могут стоять любые выражения.
n+m - пример бинарного оператора.
-n, n++ - примеры унарного оператора.
n+m,n-m,+n,-n - бинарные
операторы сложения и вычитания и унарные - знак числа ‘+’, ‘-’.
n*m,n/m - умножение, деление. Для целых чисел результат деления, как и всех
других операций - целое число.
n%m - остаток n при делении на m.
n>>m, n<<m - сдвиги вправо и влево целого
числа n на m бит, в первом случае старшие, во втором -
младшие биты заполняются нулями. Так 5>>1=2, так как 5 имеет двоичное представление 1012, при сдвиге вправо
получается 102=4. 5<<2=20=101002. Сдвиг вправо
эквивалентен целому делению, а сдвиг
влево - умножению на 2m (только
осуществляется быстрее).
n++, ++n, n--, --n - операции пост и пред инкрементации и декрементации целого числа, то есть прибавления
или вычитания единицы из n после или до
взятия значения. Так, если n=5, то после присваивания m=n++, будет m=5 (то есть n перед приращением), а n=6. Если б было m=++n, то m=6, n=6. Можно написать без присваивания: n++.
!n, n&&m, n||m - логические отрицание, ‘и’ и ‘или’. n или m считается
логическим 0, если оно равно
нулю и логической 1, если не равно. n и m могут иметь любой
числовой тип или быть указателем (определяется ниже). Результат имеет логический тип (0 или 1).
&n - взятие адреса переменной. Значение будет
указателем на переменную n.
*p - взятие значения,
находящегося по адресу, хранящемуся в переменной p.
n&m, n|m, n^m, ~n - побитовые
операции над целыми числами ‘и’, ‘или’, исключающее ‘или’, отрицание. Эти операции возвращают целое число, k-ый бит которого получается соответствующей
операцией над k-ыми битами n и m.
n==m, n!=m, n<m, n>m, n>=m, n<=m - операции
сравнения, выдают логический тип - проверка на равенство, не равенство, меньше,
больше, больше либо равно, меньше либо равно.
n=m; - присваивание n значения m.
n+=m*5; - то же, что и n=n+m*5.
n@=m; - аналогично - n=n@m; где @ - знак операции.
=,@= - операторы присваивания.
n?m1:m2 - если n не равно 0, то значение выражения - m1, иначе - m2.
(5>3)?2:4 возвратит 2.
Заметим, что все промежуточные операции над целыми числами
производятся через тип int или unsigned int, а все операции с
числами с плавающей точкой - через тип double.
Вычисление операций
в выражении производится в некотором порядке. Порядок
определяется поставленными в выражении скобками и старшинством операторов. Старшинство
операторов следующее:
!,~,+,-,++,--,&,* - унарные;
*,/,% - бинарные;
+,- - бинарные;
>>,<<;
<,>,>=,<=;
==,!=;
& - бинарный;
^;
|;
&&;
||;
? :;
=,+=,-=,*=,/=,%=,>>=,<<=,^=,&=,|=
- операторы присваивания.
Последовательность выполнения операторов одного старшинства
- слева направо, кроме операторов присваивания, которые выполняются справа
налево.
n=m=k=p; - цепочка присваиваний k=p; m=k; n=m; Можно сказать, что
оператор присваивания возвращает новое значение левого аргумента.
n+m+k*p - (n+m)+(k*p).
n<m || k==p - (n<m)||(k==p ).
!n%2 - (!n)%2.
При вычислении выражения n1&&n2&&n3&&... вычисление производится слева
направо и прерывается, как только промежуточный
результат равен 0, тогда возвращается 0. Если n1, n2, n3,... - выражения,
и, например, n1=0, то n2 и n3 не вычисляются.
Аналогично, в выражении n1||n2||n3||..., если промежуточный результат
равен 1, то вычисление
прерывается и возвращается 0.
int x,y,z;
x=2; y=3; - последовательность из двух операторов
присваивания.
if(x) y=3; else y=5; - условный
оператор. x может быть любым выражением числового типа или
указателем. Будем говорить, что x сводится к логическому типу. Если x не равен нулю, то выполняется оператор,
стоящий сразу после “if(x)”, иначе - оператор после else. Данная запись
эквивалентна записи y=x?3:5;. Часть с else может
отсутствовать.
if(x) y=3; z=4; - y присваивается 3 только если x не равно 0. Второй оператор присваивания
производится в любом случае.
if(3<x && x<=5) {y=2; z=3;} else {y=1; z=2;} - в фигурные скобки объединяют
несколько операторов в один составной оператор. Точку с запятой после закрывающейся фигурной скобки составного оператора можно как
ставить, так и не ставить.
if(x) goto a; - оператор
перехода. После него выполняется оператор, предшествуемый меткой
y=2; a (после которой
следует двоеточие). Меткой может быть любое имя, даже
a:z=2; начинающееся с
цифры. В данном случае если x, то выполняем z=2;. Переход
по goto должен быть в
пределах одной функции, он не должен перескакивать
через
определения локальных переменных.
a:; Лишняя точка с запятой обозначает
пустой оператор. Перед ней можно поставить метку для перехода.
switch(x)
{ case 1: y=2;
case 2: y+=3; break;
case 4: case -6: y=7; break;
default: y=8;
} - оператор выбора. x должно быть выражением
целого типа. После каждого слова case стоит одно константное выражение целого типа. Ищется то из
них, значение которого равно x. Далее выполняются все подряд операторы, пока
не встретится break; в том числе операторы следующей метки case. Если ни одно значение не совпало со значением x, то выполняются операторы, следующие за меткой
default, которая может
стоять после всех меток case или отсутствовать. В последнем случае ничего не
выполняется.
switch(x+1)
{ case ’a’ : y=2; break;
case ’a’+2: y=3; break;
} - Пример.
for(x=0; x<y; x++) z+=x; - оператор цикла for. Сначала выполнить
оператор x=0;, затем повторять,
пока x<y, операторы
z+=x; x++.
for(A;B;C) D; эквивалентно
A; a:if(B) {D; C; goto a;} , где A - оператор начала цикла - один оператор, который записывается одним выражением, (может
отсутствовать, но точка с запятой обязательна), B - условие продолжения цикла – выражение,
значение которого сводится к логическому типу (может отсутствовать, но точка с
запятой обязательна, в этом случае условие не
проверяется), С – оператор завершения витка цикла, должн записываться одним
выражением (может отсутствовать), D – тело цикла, может быть составным оператором.
for(;;) if(x) goto a; else x++; - пример.
for(int x=0, y=0; A; x++, y+=2) B; - вводятся переменные x и y с соответствующими начальными значениями. x++, y+=2 – одно выражение, при котором
выполняются два стоящих через запятую выражения, например:
x=(z*=5, x++, y+=2)+1; - чтобы получить
значение выражения, стоящего в скобках вычисляются все три выражения, перечисленных через запятую, и берется значение последнего
выражения, то есть новое значение у (после прибавления к нему 2).
for(int n=0; n<10; m+=n, n++); - тело цикла может быть пустым
оператором “;”.
while(A) B; - оператор цикла while. Пока выполняется условие A исполнять оператор B.
a:if(A) {B; goto a;} – эквивалентное действие.
do A while B; - оператор цикла do. Пока исполнять
оператор A, пока выполняется B.
a:A; if(B) goto a; – эквивалентное действие.
for(x=0; x<5; x++) {if(y>5) break; y++;} x=0; – оператор break осуществляет выход
из циклов for, while, do и переход к следующей за циклом
команде (в данном случае x=0;) . break может стоять
только во внутренней части этих циклов (или в операторе switch).
for(x=0; x<5; x++) {if(y>5) continue; y++;} – оператор continue осуществляет
переход к следующему витку циклов for, while и do. В данном случае оператор y++; не будет выполняться, а
завершающий виток оператор x++ будет выполняться).
float f(int a, bool b)
{ x=a;
float c=a;
if(b) c/=3;
else return c+5;
return c;
} Определение функции f, имеющей два
аргумента - типа int a и типа bool b и возвращающей результат типа float. Внутри функции
находится последовательность операторов и определений переменных. Внутри
функции в любом месте может вызываться оператор return со значением, имеющим тип,
который возвращает функция. return заканчивает выполнение функции и возвращает ее значение.
Переменная x определена вне
функции.
float x=f(n+1, Z>3)+1; Вызов функции f от конкретных значений
аргументов. При этом функция выполняется и возвращает значение, которое
подставляется в выражение вместо f(…).
f(x,true); Вызов функции без
использования ее значения.
int f(int x, int y) {return x+y;} нельзя писать int f(int x, y) {…}.
void f() {x++; if(x>5) return; y++;} Функция, не возвращающая значения
и не имеющая аргументов. В этом случае оператор return записывается без возвращаемого
значения. Можно писать:
void f(void) {…}
f(int a) {return a+1;} Если возвращаемый
тип не указан, то, считается, что он int.
void f() {} Эта функция ничего не делает.
void f(int x, int y=5, float z=4.5) {...} Начиная с какого-то
аргумента, все аргументы могут иметь значения по умолчанию. Они записываются
так же, как при инициализации переменных. При вызове такой функции часть этих аргументов, начиная с какого-то, до
последнего может быть пропущена. При этом им будут присвоены значения,
указанные при инициализации.
f(5,3,1);
f(5,3); Подразумевается f(5,3,4.5).
f(5); Подразумевается f(5,5,4.5).
int x;
void f(int y, float) {x=y;} Если какие-то из аргументов
функции не используются внутри нее, то их имена можно не писать в списке
аргументов.
void f() {...}
void f(int x) {...}
void f(double x) {...} Эти три функции
перегружают друг друга, то есть имеют одно имя и разные
наборы аргументов. Наборы аргументов считаются равными при совпадении
количества и типов аргументов.
Нельзя: void f(int y=0) {...} Так как это
повторяет второе определение.
f(); Вызов первой
функции.
f(5); Вызов второй
функции.
f(5.0); Вызов третьей функции. Выбор нужной функции произошел по типу аргумента.
Функция может быть от переменного количества аргументов.
Этот случай будет описан ниже.
int x,y;
float f(int a)
{ float b=3.5;
b+=a;
int z=2;
return y+z;
}
int g(int a) {return f(a);}
int z;
main()
{ int a=5;
z=a+g(z);
return 0;
} Программа является
последовательностью определений переменных и функций. Одна из этих функций
имеет имя main, ее спецификация (типы
аргументов) будет описана ниже. Сейчас она использована без аргументов. Те переменные, которые определены вне функций,
называются глобальными, а которые являются аргументами функции или определены
внутри функции – локальными. Внутри функции можно вызывать другие функции и
использовать глобальные переменные и локальные
переменные этой функции, но не локальные переменные других функций. Внутри
функции нельзя определять функции.
int x;
void f(int x) {…}
void g() {int x; …} Локальная переменная может иметь
такое же имя, что и глобальная. Тогда при обращении к этой переменной будет иметься в виду локальная переменная, она
переопределяет глобальную. Чтобы обратиться к глобальной переменной, надо перед
ее именем поставить ::, например:
int x;
void f() {float x=5; ::x=x;}
int x;
x+=y;
{ int x=5;
y=x;
{ int x=4;}
::x=5;
}
x=3; Внутри составного
оператора (внутри фигурных скобок) можно вводить новые переменные, которые
будут доступны (другими словами, видны) только внутри этого оператора. Они
будут переопределять внешние переменные с такими же именами, но только внутри своих скобок. Составные операторы могут быть любой
вложенности. Тело функции является таким же составным оператором и ведет себя
так же с точки зрения определения
локальных переменных. Каждое определенное имя имеет свой уровень определения.
Имена, определенные на каком-то уровне,
переопределяют такие же имена, определенные на внешнем по отношению к этому
уровню уровне. Другая терминология следующая: пространство внутри скобок можно
называть областью или блоком. Блоки могут быть вложены.
неправильно:
int x; x=3;
int x; Имя не может быть
переопределено тем же или другим образом (типом) внутри одного блока.
Исключением является перегрузка функций, когда одному имени функции
соответствуют функции с разными наборами аргументов.
неправильно:
x=3;
int x; Если x не определена до этого места во
внешнем блоке, так как переменная видна только после ее определения. Также
функция не может вызываться в тексте, предшествующем ее объявлению. Как
объявлять функцию, не определяя ее будет описано ниже. Функцию можно вызывать саму из себя, без дополнительных объявлений.
int f(int x) {return x;}
int f(float x) {return x+1;} Можно определять функции с одинаковыми именами,
но разными типами аргументов или разным их количеством. При вызове функции
будет выбрана та, у которой аргументы имеют типы, к
которым могут быть преобразованы передаваемые в нее данные.
int x;
x=f(0); Вызывается первая
функция.
x=f(0.0); Вызывается вторая
функция.
нельзя: int f; void f() {} Имя функции не может совпадать с
именами не функций, видыми в этом месте и
определенными на том же уровне. Вообще такие имена могут совпадать только в
случае функций с различающимися аргументами.
const float a=3.5; a – константа, ее значение нельзя
изменять, она должна быть инициализированна при
определении.
неверно: a=4.1;
const a=4; Если тип константы не указан, то
он считается int.
enum T {a,b=5,c,d=3}; Здесь определяется новый тип T. Переменная этого типа может принимать
значения a, b, c, d, являющиеся целыми константами.
Значения этих констант можно как определять так и не определять. Для констант,
значения которым не присвоены, значения определяются как значение предыдущей
константы плюс 1, если такой константы еще нет или 0, если эта константа первая. Так a=0, c=6. Имена констант становятся видны
в области видимости типа T, поэтому не могут совпадать с другими именами,
определенными на том же уровне.
T x=a;
int y=b;
struct T
{ int x,y;
char z;
}; Здесь определяется новый тип T. Данные этого типа представляют собой совокупность членов (полей) этой
структуры – x и y типа int и z типа char. В памяти эти члены записываются
подряд и структура (данное типа T) занимает в памяти размер, равный сумме
размеров данных. Точка с запятой после определения структуры (после фигурных скобок) обязательна. Определение членов
в структуре такое же, как определение локальных
или глобальных переменных, кроме того, что их нельзя инициализировать
при их определении:
нельзя: struct T {int x=5,y;};
Это
определение можно размещать внутри функций или вне их
(так же как определения переменных). Доступность (наличие видимости) имени T и его переопределение во вложенных блоках аналогично
видимости и переопределению имен переменных. То же верно и для имени типа. То
же верно и для всех ниже описываемых видов
определений типа. Заметим, что во внутреннем блоке определение переменной с
каким-то именем может переопределить такое же имя не только переменной, но и
функции или типа.
T x; x – данное типа T.
x.x=5; x.y=x.x; x.z=’*’; Члену x данного x присваивается 5 и т.д. Имя поля структуры может совпадать с
любым другим именем в программе, кроме имени другого поля в этой же структуре.
T a={3,4,’*’}; Инициализация
структуры значениями ее полей. Такое выражение допустимо только при определении переменной.
struct T {int a:2,b:3; char c; unsigned int d:5;}; Для полей a, b, d указано
количество занимаемых каждым из них бит - 2, 3, 5, они называются битовыми полями. Такие поля могут
быть только целого типа, в том числе перечислимого типа. Эти поля заполняют байты структуры последовательно, без
пропусков. Поля, для которых не указано
количество занимаемых бит (с помощью двоеточия), начинаются с целого числа байт
от начала структуры, при этом какие-то промежуточные биты могут не
использоваться. Так же вся структура занимает целое
число байт.
struct {int a,b;} x,y; Структура может не иметь имени.
Ее определение может фигурировать вместо имени уже определенного типа. x и y – переменные типа этой структуры.
struct T {int a,b;};
struct T1 {T x,y;} t;
t.x.a=t.x.b; Обращение к члену
члена структуры.
struct {int a,b;} x={5,3}, y=x, z;
z=x; Копирование всех данных структуры
x в структуры y и z.
T1 b={{1,2},{3,4}}; Пример с инициализацией
подструктур.
нельзя: T a; T1 b={a,{3,4}}; Особенность существующего стандарта С++.
union T {int a; float b; double c;}; T – новый тип, являющийся
объединением полей a, b, c. Эти поля записываются в одно и то же место
памяти, начиная от начала объединения (данного типа T). Так как они занимают различный размер, то заканчиваются в разных местах. Данное типа T имеет размер, равный размеру самого большого
поля. Обращение к полям объединения записывается так же, как и полям структуры:
T x; x.a=5;
struct T
{ union {int a; double b;} x;
int y;
};
T t; t.x.a=t.y; t.x.b=3.5
int a[5],b,c[3][4]; a – массив из 5 элементов типа int, b – переменная типа int, c- двумерный массив 3x4 элементов типа int. c – как бы массив массивов. Вместо int мог стоять любой
тип. При определении массива указывается его длина (по каждому измерению) в виде константного целого выражения (то
есть такого, которое может быть вычислено до выполнения программы).
a[0]=1; a[4]=2; Нулевому элементу
массива a присвоить 1,… В массиве длины N элементы нумеруются от 0 до N-1, поэтому в массиве a максимальный элемент –
четвертый.
for(int n=0; n<5; n++) a[n]=n+1; Присвоение значений
всем элементам массива a.
for(int n=0; n<3; n++) for(int m=0; m<4; m++) c[n][m]=n+m; Присвоение значений
всем элементам массива b.
int a[5]={1,2,3,4,5}; Инициализация всех элементов массива a. Такое выражение допустимо только при
определении переменной.
int a[5]={1,2,3}; Инициализация первых трех
элементов массива a.
int a[3][2]={{0,1},{2,3},{4,5}}; Инициализация
двумерного массива.
int a[][2]={{0,1},{2,3},{4,5}}; Эквивалентно
предыдущему.
нельзя: int a[][]={{0,1},{2,3},{4,5}}; Особенность существующего
стандарта C++.
struct T{int a,b;};
T a[2]={{0,1},{2,3}}; Инициализация
массива структур.
int a[]={3,2,5}; Длина массива a становится равной 3 – длине инициализирующего массива.
нельзя копировать массивы: int a[2], b[2]; b=a;
void f(int x[], int y[3]) {x[2]=y[2];} Передача массивов в качестве
аргументов.
int X[3],Y[3];
f(X,Y);
int *a, b, *c, d; a и c – указатели на данные типа int, c и d – переменные типа int. Вместо int может стоять любой тип.
Переменная типа указатель занимает 4 байта, если адрес памяти 4-байтный.
a=&b; c=a; a присваивается
адрес b, c присваивается значение a, то есть адрес b.
*a=5; В данное типа int по адресу a (то есть b) записывается 5.
d=*a; d присваивается
данное типа int по адресу a, то есть 5.
struct T {int a,b;} x;
T* p=&x;
x.a=5;
p->a=5; Присваивание члену структуры, на
который указывает p.
(*p).a=5; Последние три присваивания
эквивалентны.
int a[5],*b,*c; a – массив int, b – указатель на int.
b=a; b присвоить адрес
начала массива (его нулевого элемента). a воспринимается как этот адрес.
b[3]=3; b воспринимается как массив int, начинающийся там,
куда указывает b.
*a=2; Присвоение нулевому
элементу массива a.
*(b+2)=3; эквивалентно b[2]=3; Результат сложения
указателя с целым числом n – указатель,
смещенный на n размеров типа, на
который указывает указатель.
b=a+3; эквивалентно b=&a[3]; b присваивается указатель на a[3].
b[-1]=5; эквивалентно a[2]=5; индекс может быть отрицательным.
нельзя: a=b , так как a считается константным указателем. Присваивать массивы так
же как структуры тоже нельзя.
int* c=b+2; int n=c-b; (n=2)
bool d=c>b; (d=true;) Указатели на один тип можно вычитать и
сравнивать (==, !=, >, >=, <, <=).
const int* a; Указатель
на константные данные типа int.
можно: a=b; нельзя: *a=5; нельзя: a[3]=5; , так как нельзя изменять
значение по указателю. a[3] эквивалентно *(a+3) и a+3 тоже имеет тип const int*.
int a[5],b[5];
int * const c=a; , c – константный указатель, равный a.
нельзя: c=b; , так как c - константа.
можно: C[1]=2;
const char* a;
char* b;
можно: a=b;
нельзя: b=a; , так как мы получаем доступ к *a.
void f(const int* a) {…} В теле функции нельзя менять *a.
const int* x;
void f(int* x) {…}
нельзя: f(x); так как функция f допускает изменение *x.
int a,b;
int& c=a; c синоним a.
c=3; b=c; эквивалентно a=3; b=a; В c хранится указатель на a (адрес a), но в тексте программы переход по адресу ставить не
нужно.
эквивалентный вариант последних
двух строк:
int* c=&a;
*c=3; b=*c;
struct T{int a,b;} x;
T *y=&x, &z=*y; y – указатель на x, z синоним *y, то есть x.
z.a=5; эквивалентно x.a=5;
void f(int* x) {*x=3;}
int a;
f(&a); эквивалентно a=3.
эквивалентный вариант:
void f(int& x) {x=3;}
int a=5;
f(a);
struct T{int a,b;} x,y;
T& f(T&
x) {x.a++; x.b++; return x;}
y=f(x);
эквивалено:
T* f(T* x)
{x->a++; x->b++; return x;}
y=*f(x);
int f(char a, float b) {return a+b;}
int (*g)(char a, float b); g имеет тип указатель на функцию int(char,float). Имена аргументов a и b можно не писать.
g=f; g присваивается
адрес функции f. Под f подразумевается адрес функции, то есть начала исполняемых
команд функции. g имеет такой же тип, что и f - указатель на функцию.
x=g(’*’,5.3); Вызов функции по адресу g от таких аргументов, то есть
вызов f.
нельзя: f=g; Так как f – константный указатель на функцию.
int x,y;
void f() {x=5;}
void g(void(*h)(), int a) {h(); y=a;} Первым аргументом функции g является указатель на функцию, который используется в теле
g.
g(f,3); эквивалентно x=5; y=3;
typedef T1 T2 тип T2 определяется как
равный типу T1.
typedef int* T T – новый тип, эквивалентный int*.
int a[5];
T b=a; b[2]=1;
typedef int[5] T; T - массив int из 5 элементов.
typedef int(*T)(char,float); T – указатель на функцию int(char,float).
typedef struct {T1 x; T2 y;} T; эквивалентно struct T {T1 x; T2 y;};
T* a,b; * относится только к a, b будет типа T. Тип, который относится ко всем переменным в следующем за
ним списке, должен записываться одним именем (за
исключением разных приписок типа unsigned, const). Для любого типа
можно определить имя через typedef.
typedef T* T1;
T1 a,b; Здесь a и b будут указателями
на T.
нельзя:
(T*) a,b;
T *a[5], **b=a; a – массив указателей на тип T, b – указатель на указатель на тип T, значит b можно присвоить a.
T ***a,
***&b=a; b – синоним a.
int*& (*f[5])(int**); f – массив из 5 указателей на функции, имеющих
аргументом указатель на указатель на int, и возвращающей синоним
указателя на int.
нельзя заводить массив синонимов.
нельзя, чтобы аргументом функции был массив, кроме случая, если он определен
как typedef.
T a, *b;
int x=sizeof(T);
x=sizeof(a); x=sizeof(*b); оператор sizeof(A)возвращает размер данного типа A, если A – тип, а если A – данное или выражение, то –
размер этого данного или данного, являющегося значением выражения. Этот размер
вычисляется до выполнения программы, то есть выражение sizeof(A) является константным.
T x; T1 y;
x=T(y);
x=(T)y; Два варианта записи
преобразования типа. При этом данное типа T1 преобразуется к типу T. Преобразование типа является оператором, оно может
участвовать в выражениях. Этот оператор меняет тип выражения.
int x=int(3.5);
x=(int)3.5;
x=3.5; В данном случае можно не писать преобразование типа. Для числовых типов оно
производится автоматически.
T* x, T1* y, y=(T1*)x; Преобразуется
указатель на T к указателю на T1. При этом он
остается тем же адресом.
нельзя: y=T*(x); Такая форма записи преобразования
типа возможна только для типов, записанных одним
словом.
x=*(T*)&y; Присваивается x то, что стоит на месте y (начиная с &y), если это считать типом T.
*(T1*)&x=y; Присваивается
значение y тому, что стоит на месте x, если считать это типом T1.
T* x;
int y=int(x); Адрес
можно преобразовать к любому целому типу. Если целый тип занимает 4 байта, то
число остается фактически тем же.
x=(T*)y; и, наоборот.
(T*)x->y.z+5 эквивалентно (T*)(x->y.z)+5. Приоритет оператора
преобразования типа ниже операторов ., ->, () - взятие функции,[] - взятие элемента массива и выше
всех бинарных и унарных операций: +,*,++ и т.д.
(T2)(T1)x Последовательное преобразование x сначала к типу T1, затем – к T2.
T *x; x – указатель на T.
x=new T; x присваивается
указатель на свободное (не занятое другими данными)
место в памяти размером sizeof(T). Это место в памяти теперь можно
использовать, обращаясь по указателю x:
T y; *x=y;
delete x – освобождение участка памяти, на
который указывает x. Если x=0, то ничего не
делается. Нельзя дважды освобождать занятый участок
памяти или вызывать delete x для x, не являющегося указателем на память, выделенную
оператором new. Если не освобождать эту память после того, как она больше
не используется, то свободная память может кончиться.
int n;
T* x=new T[n]; Здесь оператор new[] возвращает указатель на
свободную память размером n*sizeof(T), являющуюся массивом T[n]. Вместо n может стоять целое выражение.
Если n=0, то возвращается тоже ненулевой
указатель, который потом нужно удалить.
T b;
x[3]=b;
delete[] x; удаление памяти, выделенной
оператором new[]. Аналогичен оператору delete (без []).
int** x=new int*; Выделение памяти под int*.
int** x=new int*[5]; Выделяет массив указателей на int.
typedef int T[3][5];
T* a=new T[n][3][5]; Использование оператора new для создания многомерных
массивов. Первая размерность может не быть константой.
new и delete работают следующим образом. Пусть new (или new[]) должен выделить n байт памяти. Среди свободных отрезков памяти компьютера
ищется кусок длины не меньшей n+4 (этот размер еще округляется до кратного 4 в большую
сторону). От этого отрезка кусок такой длины занимается. В первые 4 байта
записывается его размер. Возвращается указатель на 5-ый байт (начиная с него по край
ней мере n байт свободны). При вызове delete x из указателя x вычитаются 4
байта, с этого адреса начинается кусок, который надо освободить. По этому же
адресу лежит размер этого куска. Далее этот кусок добавляется к свободным
кускам.
T1 x; T2 y; T3 z; x, y, z - глобальные переменные. Для
всех глобальных переменных выделяется сплошной кусок памяти, в котором эти
переменные последовательно занимают место. Неинициализированные глобальные
переменные перед выполнением программы заполняются
нулями.
int x[5],y[5];
x[5]=1; эквивалентно y[0]=1; , так как сразу за массивом x в памяти следует массив y.
T f(T x,y) {T
a,b; return b;}
T f1(T x1,y1) {T
a1,b1; return b1;}
T g(T u,v) {T
c,d; c=f(x,y); d=f1(x,c); return d;}
Когда из функции g вызвана и исполняется функция f, локальные переменные и аргументы этих функций
располагаются в специальной области памяти – стеке в следующем порядке:
b a x y retf d c
u v retg,
где
retg и retf – куски памяти,
куда записываются возвращаемые функциями f и g значения. До вызова f в стеке было:
d c u v retg.
После
возврата из f:
retf d c u v
retg.
После
присвоения c=f(x,y):
d c u v retg.
Во
время исполнения f1:
b1 a1 x1 y1
retf1 d c u v retg.
Мы видим, что стек заполняется в сторону уменьшения адресов
памяти, исключение составляют аргументы функции,
которые заталкиваются в стек в обратном порядке, оказываясь расположенными в
порядке возрастания адресов. Кроме хранения локальных переменных стек
используется для хранения промежуточных значений при вычислении выражений, например: (n+m)*(k+l). Когда вычисляется k+l, тогда уже полученная сумма n+m хранится в стеке.
Рост стека в обратном направлении остался со времени
отсутствия страничной адресации памяти. Раньше при такой адресации для
программы выделялся фиксированный кусок памяти. С
одной стороны этого куска выделялась динамическая память (память, выделяемая
оператором new), а с другой рос стек. Переполнение памяти возникало,
когда граница стека встречалась с границей динамической памяти. Благодаря такой
системе можно было не фиксировать распределение
свободной памяти между стеком и динамической памятью. При страничной адресации
памяти обращение к каждой странице памяти – блоку размером, например, 4Кб (4096
байт) происходит через таблицу адресов страниц. При этом подряд идущие адреса памяти, используемые в программе
(математические адреса), могут соответствовать не подряд идущим страницам в
физической памяти (физические адреса). Для стека сразу выделяется
пространство математических адресов с
запасом, например, 1Мб, по мере заполнения стека под
них выделяются страницы физической памяти.
При вызове функции в стек еще записывается адрес следующей
команды, к которой нужно перейти после исполнения этой функции. Так же в стек
записывается адрес в стеке, на котором начинаются локальные
переменные текущей функции. Этот адрес нужно не забыть после вызова вложенной
функции, так как обращение к локальным переменным, аргументам и результату
текущей функции производится через смещение относительно этого адреса, а не по
абсолютному адресу памяти этих переменных, который
неизвестен до вызова функции.
Аргументы функции перед ее выполнением вычисляются, если
они заданы в виде выражений, и их значения копируются в стек. Если аргумент
имеет числовой тип, то в стек он записывается в виде 4 байт (в формате int или unsigned int) для целого типа и
в виде double (8 байт) для чисел с плавающей
точкой (float и double). Такое же
преобразование числовых типов используется при вычислений выражений и для их
промежуточных результатов.
Локальные переменные функций не инициализируются, если не
указана инициализация при их определении.
int _stdcall f(int a,b) {…}
int _fastcall f(int a,b) {…}
int _pascal f(int a,b) {…}
Существует несколько способов ведения стека при вызове
функции. Чтобы обозначить способ, отличный от стандартного для C++, перед названием функции пишется специальное
слово. Некоторые аргументы могут быть переданы через регистр процессора, а не
через стек, или переданы по ссылке, а не по значению – это зависит от
выбранного способа.
void f()
{ int x[3],y[3];
y[3]=1; эквивалентно x[0]=1; , так как массив y расположен в стеке сразу перед массивом x.
x[3]=2; После этого присваивания, если
функция f была вызвана из другой функции,
}
испортится стек, возврат из функции
f будет некорректным.
int* f()
{ int x=5;
return &x; Возврат адреса локальной
переменной неправилен, так как стек изменится
} после возврата из функции.
int g()
{ int *a;
a=f();
f1(); f2();
return *a; Будет возвращено не 5 (так как локальные переменные функций f1 и f2 могли
}
затереть x), или вообще
произойдет ошибка (если освободившийся кусок стека будет помечен, как незанятая
память).
int sum(int n, ...) Функция,
вычисляющая сумму n следующих аргументов, предполагая,
что они имеют тип int.
{ int s=0;
for(int k=0; k<n; k++) s+=(&n)[k+1]; Здесь достаются значения
аргументов,
} следующих после n. Эти аргументы
располагаются в стеке в порядке возрастания адресов,
что и используется. Подобным образом, преобразуя адреса можно было бы доставать
значения аргументов любых типов.
f(3,4,5,6); Должно вернуть сумму чисел 4, 5,
6.
Функция с переменным количеством аргументов может иметь
любое количество определенных аргументов, то есть
идущих до троеточия, в том числе – нулевое. При вызове функции типы
определенных аргументов должны подходить, типы же следующих аргументов и их
количество может быть любым. Причем информация об этом не передается в функцию специально, просто значения всех аргументов кладется в
стек. Внутри функции можно по предыдущим аргументам определить тип следующих и
их количество.
char a[]=”abc”; эквивалентно следующему:
char a[]={’a’,’b’,’c’,0}; После последнего символа строки идет ноль, показывающий конец строки. Строка –
указатель на последовательность символов, кончающихся нулем.
char* a=”abc”; a присваивается указатель на
заведенную до выполнения программы область памяти, где хранится {’a’,’b’,’c’,0}.
a=””; ”” – пустая строка – указатель на нулевой символ.
a=”sadf\
sdfsd”; Обратная косая черта обозначает
продолжение символов строки на следующей строке текста. Переносы можно
продолжать на несколько строк.
’\\’ – символ \.
’””’ –
”.
’\’’ – '.
’\r’,’\n’,’\t’ – символы перехода в начало строки, перехода на следующую строку,
табуляции.
’\0’,’\53’ – символы с
номерами 0 и 53 (так можно записать символ с
любым номером).
a=”””\n\’\\\\\n””””\0abc0\n” – строка: двойная кавычка,
переход на следующую строку, одинарная кавычка, две обратные
косые черты, переход на следующую стоку, две двойных кавычки, ноль, символы ‘a’, ‘b’, ‘c’, ‘0’, переход на следующую строку.
bool f(float x); Объявление функции f без ее определения. Оно выглядит так же, как и определение функции без тела функции, но после
закрывающейся скобки должна стоять точка с запятой. Определение функции так же
ее объявляет.
bool g() {… f(5);…} Функция g использует ниже
определяемую функцию f. Это можно делать,
так как функция f перед этим объявлена.
int f(float x) {… g();…} Определение функции f. Так как функции f и g вызываются друг из
друга, то без предопределения одной из них нельзя было бы обойтись.
Аналогично, с объявлением структур:
struct T; Объявление структуры T.
struct T1 {T* A,B;};
void f(T* x) {…}
struct T {T1* C,D;};
После объявления, но до определения структуры T можно использовать только указатель на тип T, но нельзя использовать переменные типа T или обращаться к членам T.
Аналогично struct, объявляется union.
inline int f(int x, bool y) {if y return x; else return x+1;} f – inline функция.
int g(bool a) {return f(5,a);} Тело функции f подставляется в тело функции g. При этом делаются преобразования и
подстановки переменных, сокращающие последовательность
команд программы. inline-функция всегда
подставляется, а не вызывается, кроме случая вызова этой функции по указателю
на нее.
При объявлении inline функции слово inline обязательно
писать, а при последующем ее определении его можно как писать так и не писать.
struct T
{ int a,b;
int f(int x);
int g(int x); Объявление функций членов
(методов) f и g структуры T.
};
int T::f(int x) {a=x; return g(b);} Определение метода структуры T.
T t; t – данное (объект)
структры T.
int n=t.f(5); Вызов метода f объекта t.
Эквивалентно: int n; t.a=5; n=t.g(t.a);
T* p=&t; p->f(5); Вызов метода через
указатель аналогичен обращению к полю через указатель.
В методе структуры видны (то есть могут быть использованы)
все глобальные имена (функции, не являющиеся членами
структур, переменные и т.д.) и имена, определенные в этой структуре. Причем,
при обращении к ним, мы обращаемся к данным или методам объекта, вызвавшего эту
функцию. Методы осуществляются следующим образом – это обычные функции, в которые передается еще один аргумент (он идет перед
всеми явными аргументами), являющийся ссылкой на объект класса, его вызвавший.
Этот аргумент затем помещается в регистр процессора, обеспечивая быстроту
обращения к членам структуры.
void f() {...}
float a;
int T::g(int x) {::a=a; ::f(); f(5);} Через :: производится обращение к
глобальному имени (в данном случае к переменной и к функции), если оно
переопределено локальным именем (то есть именем внутри структуры).
void g(T* x) {…}
struct T
{ int a;
void f() {g(this);} inline функция, так как
ее тело определено при объявлении.
} this обозначает
указатель на объект, для которого вызван метод. this можно использовать
только внутри метода (не важно inline или нет).
эквивалентно:
struct T
{ int a;
inline void f();
}
void T::f() {g(this);} Определение inline-функции может
быть вынесено из определения класса. Перед void можно было тоже
написать inline, что не играет роли.
struct T
{ int a;
static int b; Статический член структуры. Он
является глобальной переменной и один для всех
объектов структуры T.
void f(int x) {a=x;}
static void g(int x) {b=x;} Статическая функция структуры T. Она не использует нестатические переменные
этой структуры. Поэтому в нее не передается еще одним аргументом указатель на вызвавший ее объект.
Если она определяется вне определения класса, то
модификатор static при ее определении
можно не писать: void T::g(int x) {b=x;}
const int h(int x) {return a+x;}
}
int T::b=1; Необходимо определение
статических данных, наряду с их объявлением в
определении структуры. Оно производится в глобальной области (вне функций и
определений) так же как определение глобальных переменных. Вместе с определением их можно
проинициализировать (иначе они инициализируются нулями).
T x; x.g(2); эквивалентно: T::g(2); Вызов статической функции можно
производить как метод объекта той же структуры или как просто функцию, указав с
помощью :: к какой структуре
она относится.
struct T
{ int a;
int f() {return a;}
float f(int x) {return x+a;}
} Определение функции f для разных наборов аргументов - так же как и
для глобальных функций.
T x; x.f(); x.f(5); Вызов этих двух функций. Так же
их можно вызывать внутри методов структуры T.
struct T1:T
{ int c,d;
void h();
void f();
} T1 расширяет структуру T членами c и d и методами h и f (f
переопределяет f стуктуры T), то есть в T1 есть кроме них члены и методы T.
T1 x; x.a=x.c;
x.f(); Вызов функции f структуры T1.
x.T::f(); Вызов функции f структуры T. Если бы вызывалась функция, не переопределяемая в классе T1, то уточнение членом какой
структуры является функция (T::) не требовалось бы.
Структура T1 – потомок, дочерняя структура
структуры T. T1 наследует члены и методы родительской структуры T. T – базовая структура структуры T1.
void T1::h() {f(); ::f(); T::f();} В теле функции h три вызова функций. Первый – функции f структуры T1, второй и третий эквивалентны –
вызов функции f структуры T.
struct T:T1,T2,T3 {…}; Структура T включает в себя члены и методы структур T1, T2, T3. Это называется прямым
множественным наследованием. Никакие две
структуры из T1, T2, T3 не должны совпадать. Кроме того,
эти структуры не должны совпадать с их родителями (или родителями их родителей
и т.д.). Если T1, T2, T3 имеют общего родителя (может быть, не непосредственного), то он будут включаться в
структуру T несколько раз, в
отличие от виртуального наследования.
T x,y; T2 u,v;
u=x; присваивание структуре u подструктуры T2 структуры x.
x=u; присваивание подструктуре T2 структуры x структуры u.
(T2&)x=y; присваивание
подструктуре T2 структуры x подструктуры T2 структуры y.
x=(T2&)y; присваивание
подструктуре T2 структуры y подструктуры T2 структуры x.
T* x; T2* y;
y=x; y присваивается
указатель на подструктуру T2 в структуре, на которую указывает x.
x=(T*)y; x присваивается
указатель на то место, где начиналась бы структура T, если адрес ее подструктуры типа T2 был бы y.
struct T1 {int a,b;};
struct T2 {int c,d;};
struct T3: virtual T1, T2 {…};
struct T4: virtual T1, T2 {…};
struct T5: T3,T4 {…}; Структура T5 виртуально наследует структуру T1 и не виртуально T2. Это означает, что данные структуры T1 будут входить только один раз в состав T5, а данные T2 – дважды – с данными T3 и с данными T4. В общем случае ситуация следующая. Структура
может иметь несколько виртуальных родителей. Если
структура A несколько раз
наследует структуру B, возможно, через
цепочку родительских связей, то B будет наследоваться структурой A один раз для случаев, когда В указана как виртуальный родитель своих
непосредственных потомков, являющихся родителями (не
обязательно непосредственными) для A.
T5 x;
x.T3::с=1; Присвоить 1 члену c базовой структуры T3.
x.T4::с=1; Присвоить 1 члену c базовой структуры T4.
нельзя: x.с=1; так как непонятно, какое a имеется в виду.
можно: x.a=1; так как a входит в x только один раз.
struct T {virtual int f() {…}}; f - виртуальная
функция. При ее определении вне класса модификатор virtual можно не писать: int T::f() {…}
struct T1:T {int f() {…}}; Виртуальное переопределение
функции f структуры T.
struct T2:T {virtual int f() {…}}; Виртуальное переопределение
функции f
структуры T.
struct T3:T1 {int f() {…}}; Не виртуальное переопределение
функции f структуры T1.
struct T4:T2 {int f() {…}}; Виртуальное переопределение функции f структуры T2.
T *t,t0; T1 t1;
T2 t2; T3 t3; T4 t4;
t=&t0; t->f(); Вызов метода f структуры T.
t=&t1; t->f(); Вызов метода f структуры T1.
t=&t2; t->f(); Вызов метода f структуры T2.
t=&t3; t->f(); Вызов метода f структуры T3
t=&t4; t->f(); Вызов метода f структуры T2.
t1.T::f(); Вызов метода f структуры T. Так достижим вызов переопределенного метода
базовой структуры.
Любой метод структуры можно определить как виртуальный. Это
позволяет вызывать одноименный метод с такими же
аргументами у первого потомка, у которого такой метод есть, через указатель или
ссылку на эту структуру. При вызове виртуального метода через указатель или
ссылку на структуру его адрес достается из таблицы адресов виртуальных функций
для этой структуры.
struct T {virtual int f() {…}};
struct T1:T {…}; Функция f не
переопределяется, то есть остается такой
же, как в
структуре T.
struct T2:T1 {int f() {…}}; Виртуальное переопределение
функции f.
struct T {virtual int f()=0;}; f – чисто виртуальная функция – функция, у которой отсутствует
определение (тело). T – абстрактная структура – структура, в которой
есть хотя бы одна чисто виртуальная функция. Нельзя создавать переменных с
типом этой структуры, а можно только с типом не абстрактных потомков этой структуры.
struct T {int f();};
struct T1:T
{ int f(int x);
using T::f; Обеспечивает видимость метода f базового класса из
членов этого класса. Иначе, он перекрыт одноименным методом, хотя с другими
аргументами этого класса – особенность существующего
стандарта С++.
};
struct T1; Предопределение структуры T1.
void g(); Предопределение
функции g.
struct T
{public: До следующей
директивы protected: или private: идут публичные члены и методы структуры. В структуре по умолчанию (если нет директив public, protected или private)
все члены и методы публичные. Публичность означает их видимость из любых
других функций или структур. То есть допустимость доступа через .A, ->A, .F(), ->F(). public, protected и private могут стоять в любой
последовательности и повторяться, возможно, подряд.
int A;
void F();
protected: Защищенные
члены и методы структуры. Видимы только из методов производных по отношению к
этой структур и дружественных структур и функций (см.
ниже).
int A1;
void F1();
private: Приватные
члены и методы. Видимы только из дружественных структур и функций.
int A2;
void F2();
friend T1; Указание на то, что T2 - дружественная структура по
отношению к этой структуре, то есть из методов T2 видны все члены и методы T. Указать на это можно только внутри
определения этой структуры. Определения дружественных структур и функций могут
располагаться в любом порядке вместе с другими определениями внутри определения
структуры. Слово friend относится только к одной функции
или структуре в отличие от public.
friend void g(int n); g – дружественная глобальная функция, из нее видны члены и
методы T.
friend void T2::G(); G – дружественный метод структуры T2.
};
struct T4: protected T1, T2, private T2, public T3 {…}; Методы и члены T1 и T3 наследуются с такой же степенью
защиты, как при их определении в T1 и T3 (public как public и т. д.), методы и члены T1 наследуются как protected и private (public, protected->protected, private->private), методы и члены T2 все наследуются как private. Модификаторы видимости при
наследовании указываются независимо для каждого базового класса. В структурах
наследование по умолчанию публичное (когда не указан модификатор видимости).
class T {public: … protected: … private … friend …}; Определение класса.
class T3: public T, T2, public T3 {…}; Определение класса, производного
от T,
T2, T3. T2 наследуется как protected.
Классы полностью
совпадают со структурами (при замене struct на class), кроме
того, что в классе по умолчанию внутренние члены и методы являются protected (видны только из
потомков) и наследование по умолчанию имеет модификатор видимости protected. Класс был
придуман как структура, обладающая методами. Терминология методов, наследования и видимости членов более характерна в
применении для классов, а не для структур. Переменная, имеющая тип, который
является классом, называется объектом. Объект может обладать методами и
членами. Далее в примерах будут использоваться в основном
структуры (чтобы не писать везде public), но называться они будут
классами, так как используемые понятия более привычно звучат применительно к
классам.
void g();
struct T
{ friend void g();
protected: Далее идут вложенные в класс T определения типов.
enum TE {E1,E2,E3};
union TU {int A; double B;};
struct TS {int A,B; void f(){A=B;}};
public:
class TC {int A,B; void f();};
typedef int TT;
TS X;
int A,B,C;
};
void T::TC::f() Определение тела метода вложенного класса.
{ A=B; TS x; x.f();} Имеются в виду A и B класса T::TC.
неверно: void T::TC::f() {A=C;} Так как C не член класса T::TC, а член класса T.
void g() {T::TS x;
x.A=5;}
Все виды определений типа (enum, union, struct, class, typedef) могут
находиться внутри определения структуры или класса. Вложенность определений
может быть многократной. Видимость вложенных определений управляется
модификаторами public, protected и private аналогично членам и методам
класса. Вложенный в T тип T1 вне T виден как T::T1, а внутри T его имя видно непосредственно (как T1). Тип T1 ничем, кроме видимости, не
отличается от такого же типа, определенного вне T, в частности, он не содержит ничего от T. Заметим, что в T1 видны имена других определений,
вложенных в T.
struct T
{ int A,B;
T(): A(0),B(0) {…} Конструктор класса
– метод, вызываемый при создании объекта класса, он имеет имя как у класса, не
имеет возвращаемого значения и не может быть виртуальным. У класса может быть
несколько конструкторов с отличающимися наборами
аргументов. Это пустой конструктор - без аргументов. Перед телом конструктора
допустима инициализация его членов и базовых классов в приведенной форме. Здесь
инициализируются A и В значением 0.
эквивалентно: T() {A=0; B=0; …} Определение конструктора можно
вынести из класса следующим образом внутри класса: T(); вне класса: T::T(): A(0),B(0) {}
T(int a, int b=0): A(a), B(b) {…}
T(double a) {…}
T(T& x): A(x.A), B(x.B) {…} Конструктор
копирования. Может иметь спецификацию T(const T& x) или, например, T(T& x, int n=0), то есть иметь еще несколько
аргументов со значениями по умолчанию.
~T() {…} Деструктор класса –
метод, вызываемый при уничтожении объекта класса, его имеет имя вид ~T, где T – имя класса, не имеет возвращаемого
значения и аргументов. Может быть виртуальным (virtual ~T() {…}, затем переопределяется в
производном классе). У класса может быть только один деструктор.
};
Далее следуют объявления переменных и их инициализация с
помощью конструкторов.
T x1, Первый конструктор
(пустой конструктор).
x2(3,5), Второй конструктор.
x3=T(3,5), Эквивалентно предыдущему.
x4(3), Второй конструктор.
x5(1.0), Третий конструктор.
x6(x1), Четвертый
конструктор (конструктор копирования).
x7=x1, Эквивалентно предыдущему.
*x8=new T(3,5), Создание переменной и ее
инициализация с помощью оператора new.
Используется второй
конструктор.
X9[10], Элементы массива инициализируются пустым
конструктором. Нет способа их инициализировать
конструктором с параметрами.
X10=new T[10], То же, только с помощью оператора
new.
X11[]={T(3,5),T(1.0),T()}; Инициализация элементов массива
разными конструкторами. Так же можно инициализировать элементы структуры.
Деструкторы этих переменных (для
массивов – для каждого элемента), кроме тех, которые созданы оператором new, вызовутся при
выходе из блока, где эти переменные определены.
delete x8; delete[] x10; Удаление будет сопровождаться
вызовом деструктора.
delete (void*)x10; Удаление не будет сопровождаться
вызовом деструктора.
void f(T x) {…}
f(T(5)); f(*new T(5));
void f(T& x) {…}
f(T(5));
void f(T* x) {…} f(&T(5)); f(new T(5)); В выше приведенных примерах, где
аргумент функции создается без оператора new, его деструктор
будет вызываться при выходе из функции.
T f() {…}
T x;
x=f() После выполнения функции f вызовется конструктор копирования для x, затем – деструктор для возвращаемого
значения.
struct T1:T
{ T1(int a, int b): T(a,b), X(a+b) {…}
T X;
} В конструкторе производного
класса можно после двоеточия вызывать конструкторы
базовых классов и конструкторы членов. Порядок вызова инициализаций базовых
классов и членов, указанных после двоеточия не зависит от порядка, в котором
они написаны, а зависит только от их порядка в определении класса. Причем конструкторы виртуально наследуемых классов
вызываются раньше не виртуально наследуемых.
struct T {explicit T(int n); …}
T x=T(5); (неверно: T x=5;) Слово explicit перед
конструктором указывает, что конструктор можно вызвать только явно.
Конструктор может вызываться
только при инициализации – внутри тела процедуры или в конструкторе
производного класса или класса, содержащего этот класс в качестве члена.
T x; x.~T(); Деструктор может вызываться явно.
int n(5) эквивалентно: int n=5; То есть,
для всех простых типов можно тоже записывать инициализацию в скобочной форме.
Если инициализация некоторых членов или базовых классов не
указана, то они инициализируются пустыми конструкторами. У каждого класса есть
предопределенный пустой конструктор, который вызывает
пустые конструкторы базовых классов и членов и предопределенный деструктор,
который вызывает деструкторы базовых классов и членов.
неверно:
struct T {T(int n){…}}; T x; Если в классе определен хотя бы
один конструктор, то предопределенный конструктор
больше не может использоваться, и должен
быть вызван какой-то из определенных конструкторов.
неверно:
class T {T(){…}}; T x; В области, где создается объект, обязательно
должен быть виден используемый конструктор. В данном случае он является protected, так как в классе по умолчанию
внутренние определения являются protected.
неверно:
class T {~T(){…} }; … {T x; …} Так как определенный деструктор
является protected.
На схемах под T::f подразумевается адрес функции f, он занимает 4 байта. Блок с этими адресами называется
таблицей виртуальных функций.
Случай.
Нет виртуальных функций, нет наследования.
struct T {void f(); void g(); int A,B};
Случай.
Есть виртуальные функции, наследование не множественное.
struct T {virtual void f(); virtual void g(); void h(); int A,B};
struct T1:T {virtual void f(); virtual void u(); int C,D};
struct T2:T1 void {f(); void g(); int E};
Случай.
Наследование множественное, но не виртуальное.
struct T1 {virtual void f(); virtual void g(); int A};
struct T2 {virtual void u(); virtual void v(); int B};
struct T3:T1,T2 {void f(); void u(); int C};
Случай.
Виртуальное наследование.
struct T {virtual void f(); int A};
struct T1: virtual T {int B};
struct T2: virtual T {int C};
struct T3:T1,T2 {int D};
Видно, что во всех случаях данные, относящиеся к любому
базовому классу, достаются по какому-то адресу таким же образом, как в базовом
классе, причем, если базовый класс один, то этот
адрес совпадает с адресом самого данного. Это делает возможным преобразование
указателя от базового класса к потомку и, наоборот. Если наследование
множественное, то такое преобразование изменит не только тип, но и значение указателя, см. следующий пункт.
y=const_cast<T>(x); Преобразование выражения x к типу T, если тип x отличаются от T только наличием или отсутствием модификатора const.
y=reinterpret_cast<T>(x); Замена типа выражения x на тип T без проверки совместимости типов.
y=dynamic_cast<T>(x); Если x имеет тип T1* (или T1&), а T=T2* (или T2&) и T1 c T2 имеют общий базовый класс или
один является базовым для другого, то возвращается указатель или ссылка на
данное типа T2.
y=static_cast<T>(x); То же самое при не виртуальном
наследовании можно сделать на этапе компиляции и сводится к изменению указателя
на константу.
Операторы a+b, a=b, a++ и т.д. можно считать функциями от одного или
двух аргументов. Язык C++ позволяет их определять и переопределять для новых типов первого
аргумента. Тип первого аргумента может являться struct, class, enum, union.
Можно определять
следующие операторы:
Унарные: !, *, ++, --, ~, -, +.
Бинарные: +, -, *, /, %, <<, >>, &, |, ^, &&, ||, =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, <, >, <=, >=, ==, !=.
Другие: new, new[],
delete, delete[], ->, ()- вызов функции, [] – индексный
оператор.
Операторы преобразования типа.
1. Оператор – не метод
класса. Нельзя переопределять операторы =, (),
[], -> и операторы преобразования типа.
T operator +(T1 x, T2 y)
{… return …} Определения
бинарного оператора “+”. Подобным образом определяются другие бинарные
операторы. Если T, T1 и T2 содержат более 4 байт, то лучше
их передавать по ссылке. То же касается следующих
случаев.
T x; T1 y; T2 z; x=y+z; Использование этого оператора.
T operator !(T1 x) {… return …} Определения
бинарного оператора “+”. Подобным образом определяются другие унарные
операторы.
T x; T1 y; x=!z; Использование этого оператора.
T operator ++(T1 x, int n) {…} Если второй
аргумент равен –1, то выполняется оператор прединкрементации (++x), иначе – постинкрементации (x++). Аналогично, для оператора --.
void* operator new(size_t size) {…} Можно
переопределить предопределенный оператор new, выделяющий память для всех
типов. Тип size_t является целочисленным (unsigned int) и определен в
файле stddef.h. Аргумент size определяет размер данного.
Возвращаемое значение указывает на выделенную память.
T* x=new T; Вызовется operator new(sizeof(T)).
void* operator new(size_t, void* buf) {return buf;} Оператор new может содержать несколько
дополнительных аргументов и быть определен для разных их наборов.
T* x=new T(buf); Дополнительные аргументы
указываются в скобках при использовании оператора new.
void* operator new[](size_t size) {…} Определение
оператора new[] такое же, как и оператора new.
T* x=new T[5]; Использование оператора new[].
void operator delete(void* x) {…} Можно
переопределить преопределенные операторы delete и delete[]. Эти операторы не могут иметь дополнительных аргументов. В качестве
аргумента передается указатель на память, подлежащую удалению.
void operator delete[](void* x) {…}
2. Оператор – метод
класса. В этом случае первый аргумент оператора – объект этого класса, этот
аргумент не записывается в объявлении функции. Эти операторы могут
наследоваться как обычные функции, за исключением операторов присваивания и
вызова функции.
struct T
{ T1 operator +(T2& x)
{…} Пример бинарного оператора. Подобным образом определяются
другие бинарные операторы. Тело функции оператора
может быть определено внутри определения класса, как здесь, или вне него.
T1 operator !(); Пример унарного оператора.
Подобным образом определяются другие унарные операторы.
T& operator =(T1& x); или T& operator =(const T2& x); Оператор присваивания. Если бы аргумент имел тип T, то этот оператор переопределял бы
преопределенный оператор присваивания.
T1 operator [](T2 z); Индексный оператор. Индекс имеет
тип T2.
T1 operator ->(); Оператор доступа к члену по ссылке.
T1 operator ()(T2 x, T3 y); Оператор функции. В данном
случае, от двух аргументов типов T2 и T3. В качестве имени функции будет
фигурировать выражение типа T.
operator T1() {… return …} Оператор
преобразования к типу T1 объекта этого класса. Возвращаемое значение должно быть типа T1, но оно не указывается в
объявлении.
void* operator new(size_t); Для операторов new и new[] допустимы дополнительные
параметры.
void* operator new[](size_t);
void operator delete(void* x, size_t size); В операторах delete и delete[] можно не писать второй аргумент: void operator delete(void* x);. Первый указывает на удаляемое
данное.
void operator delete[](void* x, size_t size);
};
T1 T::operator !() {…} Пример определения оператора вне
определения класса.
T x; T1 y; T2 z; T3 u;
y=x+z; Использование
оператора +.
y=!x; Использование
оператора !.
x=z; Использование
оператора =.
y=x[z]; Использование оператора [].
y=x(z,u); y=(*(T*)(void*)&x)(z,u); Использование оператора функции.
y=T1(x); y=x; Использование оператора преобразования типа в явном и неявном виде.
T* x=new T; delete x; x=new T[5]; delete[] x; Использование методов класса T new, delete, new[], delete[].
y=x.operator+(z); эквивалентно: y=x+z; Явный вызов оператора +. Таким же
образом можно вызывать все другие операторы,
являющиеся методами класса.
template <struct T1, struct T2, struct T3>
struct T {T1 A; T2 F(T3* x);} Определяется шаблон структуры T, зависящий от типов T1, T2, T3. Эти типы могут участвовать в
определениях внутри T. Точно так же определяется шаблон класса.
template <struct T1, struct T2, struct T3> T2 T::F<T1,T2,T3>(T3* x)
{ return x->Next();} Определение метода
класса, являющегося шаблоном, вне класса. Предполагается, что класс T3 имеет метод Next().
T<TA,TB,TC> Новая структура, является подстановкой в шаблон T типов TA, TB, TC.
T<TA,TB,TC> x,y; Определение объектов этой структуры.
typedef T<TA,TB,TC> TNew; Другое
использование нового типа.
template <struct T1, struct T2> T F(T2 x, T3 y)
{ return
x.Next()+y.Prev();} Определение шаблона функции F.
T x; TA y; TB z; x=F(y,z); По типам x и y определяются параметры шаблона и компилятор генерирует код
соответствующей функции, если его еще нет.
try
{ …
throw(); или throw(x); или throw(x,y); и т.п. Оператор throw может встретиться внутри функции, которая была вызвана из
текущей функции. Он обеспечивает выход из всех вложенных блоков, в том числе,
из функций вплоть до выхода из текущего блока try без выполнения
оставшегося кода, только вызываются деструкторы всех
локальных переменных. Оператор throw может иметь различные наборы аргументов. Будем говорить,
что оператор throw генерирует
исключительную ситуацию или исключение с типом, соответствующим набору
аргументов. Блоки try могут быть вложены, в том числе, располагаться в разных функциях, которые вызывают
друг друга.
}
catch(T1 x1, T2 x2) {…} После блока try может быть один
или несколько блоков catch от разных наборов аргументов.
Если блок try был выполнен без исключений, то
они пропускаются. Выполняется тело только первого
блока, к аргументам которого могут быть преобразованы аргументы исключения,
если такой есть. Внутри тела оператора catch может встречаться
оператор throw, он перехватится
из catch внешнего блока try. Блок catch можно называть
обработчиком исключения.
catch(T x) {…}
catch(...) {…} Если в операторе catch вместо аргументов
стоит троеточие, то он выполняется для любого набора аргументов.
Если был вызван оператор throw, то производится переход к
операторам catch с выходом из всех функций и вызовом деструкторов всех локальных объектов.
Выполняется первый оператор catch, у которого типы
аргументов подходят к типам аргументов throw. Затем выполняются операторы после catch. Если throw не было, то после
выполнения операторов блока try операторы catch пропускаются.
#define A Определяется пустой макрос A. Как и другие операторы препроцессора эта
директива может быть в любом месте программы (между определений, внутри тела
функции). Кроме того, все директивы начинаются с #, который должен быть первым символом в строке,
возможно, после пробелов. Правила образования имен макросов такие же, как у
других идентификаторов.
#undef A Макрос A делается неопределенным.
…
#ifdef A Далее следующие строки включаются в программу,
если макрос A определен.
(или #ifndef A Включало бы следующие строки,
если A не определен.)
x=5;
#else Следующие до #endif строки включаются,
если условие #ifdef или #ifndef не выполнено.
x=4;
#endif
#define A x=5;\
y=4; Определяется макрос A. Он подставляет
всюду в ниже следующем тексте вместо слова A: “x=5”, переход на следующую строку, “y=4”. Если два раза подряд (без #undef) определить макрос с одним
именем, но разной подстановочной частью, то компилятор выдаст предупреждение.
aaa A bbb
эквивалентно
aaa x=5;
y=4; bbb
#define A(x,y,z)
AAA##z+(((x)+(y))>>1) Макрос с параметрами. Вместо параметров должны
подставляться регулярные выражения. Оператор ## сращивает то, что стоит справа
от него и слева без пробелов. При подстановке выражений
a=A(z+2,3*u,BB)+8; эквивалентно a=AAABB+(((z+2)+(3*u))>>1)+8;
#define A B+C
#define D A+A эквивалентно #define D B+C+B+C То есть, макросы
подставляются так же внутрь других макросов.
#if A>B+1 || !defined(C) Если значение следующего после #if константного выражения равно true, то следующие строки включаются
в программу. Оператор defined(…) может использоваться только в операторе #if или #elif (см. далее).
…
#elif C>D Если предыдущее
условие не выполнено, а выполнено это, то следующие строки включаются в
программу.
…
#elif E>H Операторов #elseif может быть несколько, но
выполняется не более одного.
…
#else Следующие строки включаются, если не выполнено
ни одно из вышестоящих условий.
…
#endif
#include ”a.cpp” Включение файла “a.cpp” в текст в то место, где стоит эта директива.
#include <stdio.h> Такое же включение файла “stdio.h”, только этот файл ищется сначала в директории
со стандартными файлами, поставляемыми вместе с компилятором.
__FILE__ Предопределенный макрос. Он
вставляет имя текущего файла.
__LINE__ Вставляет номер текущей строки.
Программа состоит из
файлов с расширением “.cpp” и с расширением “.h” (заголовочные, “header” файлы). Компилятору указывается множество файлов
cpp.
Заголовочные файлы включаются через #include в cpp
файлы и друг в друга. В них помещают определения, которые должны быть видны в
нескольких файлах, и в них не должно быть определений (не объявлений)
переменных и не inline функций. Определения inline функций допустимы. При компиляции компилируются
последовательно все cpp файлы (в произвольном порядке), при компиляции каждого из них включаются
все файлы, указанные в нем через #include. Сначала рассмотрим случай без
заголовочных файлов.
Файл “a.cpp”:
int g(); Предопределение
функции, определяемой в другом файле.
extern int b; Объявление переменной,
находящейся в другом файле.
int a;
int f() {return g()+b;} Использование функции и
переменной, определенных в другом файле.
Файл “b.cpp”:
int f(); Аналогично.
extern int a;
int g() {return a+1;}
int b;
Если нужно использовать одно и то
же определение типа в нескольких файлах, то без заголовочных файлов обойтись
нельзя.
Файл “a.h”:
#ifndef __A_H Эта директива
предотвращает, повторения компиляции этого файла, так как недопустимо
дублирование определения типа.
#define __A_H
struct T
{ int A,B;
int F(int x); Не inline функция.
int G(int x) {return x+A;} inline-функция. В заголовочном
файле допустимо определение inline-функций.
inline int H(int x); inline-функция.
};
inline int T::G(int x);
extern T a,b;
int f(T* x);
int g(T* x);
inline int h(T* x) {return x->A;}
#endif
Файл “a.cpp”:
#include ”a.h”
int a;
int f(T* x) {return x->A+g(x)+b;}
int T::F(int x) {return A-x;}
Файл “b.cpp”:
#include ”a.h”
int b;
int