План лекции
  • Различия между C и C++
    • Идеология gcc
    • Стандартные расширения файлов
    • Подключение стандартных библиотек (-lstdc++)
    • Статические и разделяемые библиотеки
    • Простой путь: g++ вместо gcc
    • Необходимость объявления функций в C++
  • Типы данных в C/C++ и неявные приведения типов
    • Все есть число, целочисленные типы данных, неявные приведения, выполнение операций
    • Вещественные числа
  • Указатели и ссылки
    • Понятие указателя
    • Адрес по переменной и переменная по адресу
    • Как посмотреть значение указателя
    • Арифметика указателей, различие между разными видами указателей
    • Указатели и массивы


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

Идеология gcc

GCC = GNU C Compiler.
Кроме C и C++, GCC компилирует с языка Фортран, Паскаль и других (для поддержки языков устанавливаются соответствующие дополнения)
Так как компоновщик работает только с объектными файлами, ему не важно, на каком языке был написан исходный файл.

Стандартные расширения файлов

GCC распознает, на каком языке написан исходный файл, по его расширению:
gcc –c file.c
gcc –c file.cpp
gcc –c file.p
В первом случае файл будет скомпилирован как файл на языке C, во втором на C++, в третьем – на Паскале.
Так же, можно заставить скомпилировать файл, написанный на C, как файл на C++:
g++ -c file.c
– в этом случае файл будет скомпилирован как C++ файл, несмотря на расширение

Подключение стандартных библиотек (-lstdc++)

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

Одна из стандартных библиотек является libc, которую компоновщик подключает  автоматически.
Также, если программа написана на языке C++, то библиотеки libc недостаточно, кроме нее нужно использовать стандартную библиотеку C++   libstdc++
Она подключается с помощью ключа –l:
gcc main.o –o program –lstdc++
Можно использовать команду g++ вместо gcc, в этом случае файл компилируется как программа на языке C++, а библиотека libstdc++ подключается автоматически:
g++ main.o -o program

Статические и динамические библиотеки

Есть два типа библиотек: статические и динамические (другое название -  разделяемые).
В Linux статические библиотеки имеют расширение ".a" (что означает archive – библиотека-архив объектных файлов), а динамические – ".so" (shared objects – библиотека с разделяемыми объектами)
Аналогично, в Windows статические библиотеки имеют расширение “.lib”, динамические –".dll"

Статическая сборка

Во время компоновки при статической сборке происходит следующее:
1. Обрабатываются объектные файлы программы.
2. Если есть неизвестные символы, которые в программе не указаны, то компоновщик ищет соответствующие объектные файлы (например, printf.o для функции printf) в библиотеках и добавляет код нужных функций  в код программы.
3. Неизвестные символы из объектных файлов библиотеки могут потянуть за собой другие объектные файлы (из этой библиотеки или других).
Этот процесс продолжается до тех пор, пока остаются неизвестные символы.

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

Динамическая сборка

Идея такой сборки заключается в том, что скомпилированные коды функций не включается в программу. Вместо этого в программу помещаются только название динамической библиотеки и адрес нужной функции в ней. Таким образом, в момент запуска программы в память загружаются необходимые библиотеки и используются программой.
Недостаток динамической сборки заключается в том, что файл подключаемой динамической библиотеки “.so” тоже должен быть на компьютере, на котором запускается программа.

По умолчанию GCC использует динамическую сборку, но с помощью ключа –static задать использование статических библиотек, если они есть:
gcc main.o –o program –static

Необходимость объявления функций в C++

С++ строже , чем С в некоторых вопросах. Один из них заключается в том, что в C++, в отличие от C, компилятор заставляет подключать заголовочные файлы. 

Типы данных в C/C++ и неявные приведения типов

Все есть число, целочисленные типы данных

С точки зрения языка C, все есть число.

Целочисленные типы данных:

Название типа Кол-во байт
для
хранения
Диапазон
char 1 -2^7..2^7-1
short 2 -2^15..2^15-1
int 4 -2^31..2^31-1
long 4 -2^31..2^31-1
long long 8 -2^63..2^63-1
unsigned char 1 0..2^8-1
unsigned short 2 0..2^16-1
unsigned int 4 0..2^32-1
unsigned long 4 0..2^32-1
unsigned long long 8 0..2^64-1
Примечание: int является естественным типом в C++, это означает, что кол-во байт для его хранения зависит от архитектуры процессора - используется размер, с которым процессору проще работать
Размер типов short и long определяются относительно размера int следующим образом:
Это же относится и к типам, начинающимся с "unsigned".

Вещественные числа

Название типа Кол-во байт для хранения Диапазон
float 4 1,4 * 10^-45.. 3.4 * 10^38
double 8 4,94 * 10^-324.. 1.79 * 10^308

Кроме того, в C++ появился тип bool. Он принимает значения «1» (true) или «0» (false). Количество байт для его хранения определяется компилятором..

Неявные и явные приведения, выполнение операций

Приведение типа - преобразование значения переменной одного типа в значение другого типа.
Типы можно приводить друг к другу неявно:
float f=1.5;
int a=f; //неявное приведение
f=a;
в конце выполнения этого кода f будет равно 1.
Также, можно делать явное приведение:
Для явного приведения типов некоторой переменной перед ней следует указать в круглых скобках имя нового типа. Пример:
long l;
short s;
s=(short)l;
При приведении типов, которые используют большее количество байт для хранения, к типам, которые используют меньше байт для хранения, могут возникать ошибки. Это связано с тем, что в этой ситуации копируется столько битов, сколько можно скопировать, начиная с младшего. Например,
int A=128;
char B=A;
A=B;
в конце выполнения этого кода, в A будет находиться число -128. Это происходит потому, что переменная типа char кодируется 8 битами, а int кодируется большим количеством бит (количество бит зависит от архитектуры). На строчке “char B=A;” в B записывается только 8 младших бит числа A, 8 бит равен 1, поэтому компилятор считает, что в B хранится отрицательное число. На строчке «A=B» в A значение B копируется как отрицательное число.

Указатели и массивы

Массивы

Объявление массива:
int array[10];
место, выделяемое под этот массив – 10*sizeof(int)
число в квадратных скобках обязательно должно быть константой, так как место, выделяемое под массив, определяется на этапе компиляции программы.
другие способы объявления массива:
int array[10]={0,1,2,3,4,5,6,7,8,9}; // массив заполняется указанными числами
int array[10]={0}; // весь массив заполняется нулями
int array[]={0,1,2,3,4,5,6,7,8,9}; //в этом случае размер массива автоматически вычисляется компилятором
для типа char:
char array[]={0,1,2,3,4,5,6,7,8,9};
char array[]={‘H’,’e’,’l’,’l’,’o’};
char array[]=”Hello”; //в этом случае размер массива будет равен 6, так как еще добавляется нуль-символ, обозначающий конец строки
Запись с использованием { и } может быть использована только для инициализации массива.
Двумерные массивы.
int M[10][10];
Двумерный массив по-другому можно назвать массивом массивов.
Инициализируется двумерный массив аналогично одномерному:
int M[2][2] = { {1,2} , {3,4} };
Обращение к элементам массива происходит следующим образом:
int array[10]={0,1,2,3,4,5,6,7,8,9};
int a=array[index];
где index – любое целое число, задающее смещение от первого элемента массива.
Например index[3] при данном объявлении будет равен 3.
Обращением к элементам массива не контролируется компилятором, поэтому может привести к определенным проблемам.
Если к массиву  обратиться  с отрицательным индексом или индексом, большим индекса последнего элемента в массиве, то в этом случае будет сделано обращение к числу из памяти, находящейся раньше массива или после массива соответственно. В результате, например, может быть непредвиденно изменена часть памяти, которая не относится к массиву.

Понятие указателя

Указатель – число, адрес соответствующего элемента в памяти.
Адрес указывает смещение от начала оперативной памяти.
Количество байт для хранения указателя зависит от архитектуры компьютера.
Указатель объявляется следующим образом:
int *p;
В этом случае был объявлен указатель p на число типа int.

Адрес по переменной и переменная по адресу

Чтобы получить адрес какой-либо переменной, применяется оператор "&":
int *p;
int a;
p=&a; //записать в переменную p адрес a
Чтобы обратиться по адресу, находящемуся в указателе, применяется оператор "*":
int b=*p // в переменную b запишется значение по адресу, хранящемуся в указателе p

Как посмотреть значение указателя

Вывод на экран значения указателя с помощью функции printf делается так:
printf(“%p”,p);
Формат вывода указателя зависит от архитектуры компьютера и компилятора.

Арифметика указателей.

К указателям можно прибавлять/вычитать числа. Пример:
int i[5]={1,2,3,4,5};
char c[]=’hello’;
int *pi=i;
char *pc=c;
pi+=1; // будет сделан сдвиг адреса на размер одного int
pc+=1; // будет сделан сдвиг адреса на размер одного char
Сдвиг зависит от типа объекта, на который указывает указатель.
Выражения типа a[b] компилятор заменяет на *(a+b), так как выражение симметрично, то с точки зрения компилятора следующие выражения идентичны:
array[i] и i[array], так как будет заменены одинаково: *(array+i);
Указатель с указателем складывать нельзя!

Различие между разными видами указателей

char c[4];
char *p=&c[0]; //&c[0] – адрес первого элемента массива, или адрес массива
int *pi=p; // на C эта строка считается нормальной, на C++ будет выдана ошибка, так как указатели pi и p разных типов.
int *pi=(int*)p; //эта строка будет правильной.

Связь указателей с массивами.

Указателю какого-либо типа можно присваивать массив этого типа, но не наоборот. Пример:
char array[10];
char *p;
p=array; // в p передается адрес массива (адрес нулевого элемента)
array=p; //это ошибка
Это объясняется тем, что массив – это фиксированная область в памяти, а указатель – это адрес в памяти.

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