Присваивание указателей друг другу 2 страница

int main(int argc, char *argv[])

{

...

}

либо так

int main(int argc, char **argv)

{

...

}

Параметр argc сообщает количество командных параметров при обращении к функции main(), учитывая в качестве нулевого параметра имя самой выполняемой программы. Другими словами параметр argc сообщает количество слов в командной строке, разделенных пробелами.

Параметр argv – массив символьных строк (слов), в который помещаются аргументы командной строки (аргумент – любой текст, не содержащий символов «пробел» или «табуляция». Если возникает необходимость передать в качестве аргумента строку, содержащую пробелы или символы табуляции, то такую строку необходимо заключить в кавычки: “Студент БГУИР”).

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

Пример программы, получающей в качестве параметров названия городов и выводящей их в обратном порядке:

#include <stdio.h>

#include <conio.h>

int main(int argc, char *argv[])

{

int i;

for (i = argc – 1; i >= 1; i--)

printf(“%s ”, argv[i]);

getch();

return 0;

}

Строка запуска программы содержит имя файла (primer.exe) и список передаваемых параметров, разделенных пробелами

c:\primer.exe Витебск Брест Минск

В результате выполнения программы на экран будет выдана следующая информация:

Минск Брест Витебск

В памяти будет содержаться следующие данные:

Элемент argv[0] может содержать не только имя вызываемого файла, но и полный путь к нему.

6. Строки

В практике программирования очень часто возникает задача хранения и использования текстовых строк. При помощи таблиц кодировки группе чисел (байт) без труда можно поставить в соответствие группу символов (букв). Но сами по себе строки довольно сложны для представления в компьютерной технике ввиду того, что их длина неизбежно должна быть переменной. Существует два простых способа хранения строк в памяти, основанных на использовании статических структур: строки с упреждающей длиной и с завершающим терминальным элементом. Т.к. размер области в памяти отведенной для хранения строки должен быть постоянным, необходимо иметь способ определять её реальную длину.

В строках с упреждающей длиной в памяти перед областью, байты которой интерпретируются как символы строки, находится число, определяющее реальную длину строки. Именно такой метод используется в языке Pascal для хранения переменных типа string. Там для строковой переменной по умолчанию отводилось 256 байт в памяти. Первый байт хранил реальную длину строки, а остальные – её символы. Именно с тем, что для хранения реальной длины использовался один байт и связано ограничение на максимальную длину строки в Pascal’е.

В языке Си вообще нет встроенного строкового типа. Это, равно как и отсутствие булевых переменных, вероятнее всего, объясняется историческими причинами. Однако среди встроенных имеются функции, делающие простой и удобной работу со строками с завершающим терминальным элементом. В строках этого типа об окончании строки свидетельствует специальная (в общем случае) последовательность символов, котороя называется терминальной. Так, в Си терминальным служит символ с кодом 0 (байт 0x00).

В виду того, что специального строкового типа нет, а Си-строка является массивом элементов типа char, логично при хранении строк использовать тип char* (или char [], что, в сущности, то же самое).

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

char *str = “Hello world”;

или аналогичное ему

char str[] = “Строка”;

(разница будет заключаться в том, что в первом случае переменную str можно будет изменить (как указатель), а во втором нельзя (имя массива)), однако, если написать так

char *const str = “Hello world”;

то и это отличие исчезает.

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

Первый способ:

#include <string.h>

char str[100]; // Любые строки до 99 символов

strcpy(str, “Строка”); // Побайтое копирование строки

Второй способ:

char *str;

str = (char*) malloc(100);

strcpy(str, “Строка”);

free(str);

Использованная стандартная функция strcpy копирует строку по адресу src, в строку по адресу dest, и возвращает указатель dest:

// Прототип функции strcpy

char* strcpy(char* dest, const char* src);

char buf[15] = “BufString”;

char str* = “String”;

strcpy(buf, str);

Предположим, что области памяти, выделенные компилятором для массива buf, и строкового литерала “String” смежные. Длина массива больше длины строки, которой он инициализирован, на 5 байт – это значит, что в них может находиться что угодно. На рисунке показано, как изменится область памяти, содержащая строки, после вызова strcpy.

Приведем пример функции, которая делает в точности то же самое, что и strcpy:

char* user_strcpy(char* dest, const char* src)

{

char *result = dest;

do

{

*dest++ = *src;

}

while (*src++);

return result;

}

Копирование прекращается лишь в том случае, если в копируемой строке встречается нулевой символ. Это значит, что, если принимающая строка (буфер) имеет недостаточную длину для приёма копируемых данных, то произойдет выход за пределы массива, что может привести к непредсказуемым и трудно обнаружимым ошибкам.

Другой необходимой и часто используемой функцией работы со строками, является функция определения их длины:

size_t user_strlen(const char* src)

{

size_t result = 0;

while (*src++) result++;

return result;

}

или аналогичная ей функция из стандартной библиотеки:

size_t strlen(const char* src);

Тип size_t определен в string.h и представляет собой псевдоним целого типа без знака.

Из всего сказанного видно, что использовать операторы присваивания и сложения для копирования и конкатенации строк (как это делалось в языке Pascal) нельзя:

char *str1 = “Студент”;

char *str2 = “ БГУИР”;

char *str3;

str3 = str1 + str2; // НЕ верно!

В данном случае выражение str1 + str2 даже не будет скомпилировано, компилятор выдаст ошибку: «Неверное использование указателей». Действительно, пусть строка «Студент» находится в памяти, например, по адресу 1000, а «БГУИР» 1009. Арифметическая сумма этих чисел даст 2009. Что находится по этому адресу сказать нельзя. Присваивание же str3 адреса строки приведет к тому, что оба указателя будут указывать на одну и ту же область в памяти, и изменение строки str3 повлечет за собой соответствующее изменение str1 и str2, чего пользователь явно не ожидает. Выражение

str3 = “Студент” + “ БГУИР”;

по сути, ничем не отличается от str3 = str1 + str2: строковые литералы размещаются в памяти по адресам, которые вместо них компилятор подставит в выражение, а складывать два адреса нельзя.

С другой стороны, в стандартной библиотеке string.h, есть целый набор функций работы со строками. Рассмотрим некоторые из них:

// Прототип функции конкатенации строк

char* strcat(char* dest, const char* src);

// Пользовательская реализация

char* user_strcat(char* dest, const char* src)

{

strcpy(dest + strlen(dest), src);

return dest;

}

// Прототип функции поиска первого вхождения символа

char* strchr(char* s, int c);

// Пользовательская реализация

char* user_strchr(char* s, int c)

{

do

{

if (*s == c) return s;

}

while (*s++);

return 0;

}

// Прототип функции создания дубликата строки

char* strdup(const char* s);

// Пользовательская реализация

char* user_strdup(const char* s);

{

/* malloc – функция динамического выделения

памяти, рассматривается в главе указатели */

return strcpy((char *) malloc(strlen(s)+1), s);

}

// Использование strdup

char* some_function(const char* str)

{

char* temp = strdup(str);

//...

free(temp); //Обязательное освобождение памяти!

}

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

// Прототип функции сравнения строк

int strcmp(const char* s1, const char* s2);

// Пользовательская реализация

int user_strcmp(const char* s1, const char* s2)

{

int r;

do

{

r = *s1 - *s2;

}

while (*s1++ && *s2++ &&!r);

return r;

}

// Использование strcmp

char* some_function(char* str1, char* str2, char* str3)

{

printf("\n%d", strcmp(str1, str2));

printf("\n%d", strcmp(str2, str1));

printf("\n%d", strcmp(str1, str1));

}

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

7. Классы хранения и видимость переменных

7.1. Общие сведения

В программе, написанной на Си, переменные относятся к одному из четырех классов хранения:

· автоматический класс хранения (auto)

· регистровый класс (register)

· статический класс (static)

· внешний класс (extern)

Каждый из приведенных классов имеет свои преимущества. Грамотное определение метода хранения переменной – необходимое условие правильности написания любой программы.

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

Объекты классов auto и register имеют локальное время жизни. Спецификаторы static и extern определяют объекты с глобальным временем жизни.

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

7.2. Автоматический класс хранения (auto)

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

Рассмотрим часть программы:

int f1(void)

{

auto int x = 10;

}

int f2(void)

{

int x = 35;

}

int main(void)

{

printf(“x=%d”, x);

return 0;

}

В ней в двух функциях f1, f2 объявлены переменные с одинаковыми именами. Необходимо понимать, что друг к другу они не имеют никакого отношения. При компиляции же будет выдана ошибка, т.к. функция main вообще не видит ни одной объявленной переменной x. В функции f1 переменная x объявлена как auto int x, а в f2 – int x. Такие объявления эквивалентны – любая локальная переменная по умолчание относится к автоматическому классу памяти, поэтому спецификатор класса auto можно опустить.

Возможна ситуация, когда глобальная и локальная переменная имеют одно и то же имя, например:

#include <stdio.h>

int num = 10; // объявление глобальной переменной

void write(void);

void main(void)

{

// вывод значения глобальной переменной

printf("\nnum=%d", num);

int num = 20; // объявление локальной переменной

// с тем же именем, что и глобальная

printf("\nnum=%d", num);

write();

}

void write(void)

{

printf("\nnum=%d", num);

}

Результатом выполнения этой программы будет

num=10

num=20

num=10

Когда объявляется локальная переменная num в функции main, глобальная переменная с тем же именем num скрывается. Она становится временно, до выхода из блока, недоступной. При выходе же из main – при выполнении блока функции write, глобальная переменная с именем num становится опять доступной.

7.3. Регистровый класс хранения (register)

В чем несомненный минус использования стека для хранения информации? Дело в том, что стек представляет собой специальный участок оперативной памяти, работа с которым осуществляется с использованием двух операций: push (помещения данных в стек) и pop (извлечения данных из стека). С одной стороны каждая из этих операций состоит из нескольких действий, с другой – работает с оперативной памятью, скорость доступа к которой ниже, чем скорость доступа к элементам памяти, расположенным внутри процессора. Значительно лучше было бы хранить переменные прямо в процессоре, а точнее – в его регистрах, это позволило бы сэкономить несколько тактов. В ситуациях, когда с переменной нужно проводить простые действия (например – побитовый сдвиг), хранение в регистре сократило бы работу в несколько раз. Поэтому локальные (не глобальные) переменные, используемые в счетчиках целесообразно хранить как регистровые. Количество доступных регистров ограничено архитектурой процессора и возможностями компилятора. Если регистров не хватает, компилятор игнорирует спецификатор register и переменная объявляется как автоматическая. По этой же причине без надобности регистры занимать не рекомендуется. Регистры не адресуются, поэтому к регистровым переменным неприменима операция взятия адреса.

Пример:

#include <stdio.h>

void main()

{

register long sum = 0;

for (register int i = 1; i <= 100; i++)

sum = sum + i;

printf("\nsum[100]=%d", sum);

}

Данная программа вычисляет и выводит на консоль сумму первых ста элементов арифметической прогрессии (1, 2, 3, 4,…).

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

7.4. Статический класс хранения (static)

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

Различают локальные и глобальные переменные статического класса хранения.

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

Пример:

#include <stdio.h>;

int print1(void);

int print2(void);

void main(void)

{

int i;

for (i = 1; i <= 5; i++)

{

printf("\n%d %d", print1(), print2());

}

}

int print1(void)

{

static int i = 0;

i++;

return i;

}

int print2(void)

{

int i = 0;

i++;

return i;

}

Все переменные здесь носят одно и то же имя – i, однако объявлены они по-разному: в функциях main и print2 объявлены автоматические переменные, которые при вызове функции возникают и инициализируются каждый раз вновь, а в print1 переменная объявлена как статическая, она сохраняет свое значение. Это приводит к тому, что на экран будет выведено следующее:

1 1

2 1

3 1

4 1

5 1

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

Необходимо сделать еще несколько замечаний по объявлению глобальных статических переменных: они автоматически инициализируются нулем, т.е., объявления static int i=0 и static int i будут эквивалентны; если глобальная переменная объявлена без спецификатора класса хранения, то этим классом по умолчанию будет статический, т.е., записи static int i и int i – эквивалентны.

7.5. Внешний класс хранения (extern)

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

Приведем пример объявления переменной после ее первого использования:

#include <stdio.h>

void main(void)

{

printf("\ni=%5.2f", i);

}

float i = 10.25;

При компиляции будет выдана ошибка: неопределенный символ i. И правда, область видимости глобальной переменной – от момента объявления до окончания выполнения программы. В примере – ошибка, которую можно исправить путем добавления в функцию main перед строкой

printf("\ni=%5.2f", i);

строки

extern float i;

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

Однако чаще всего ключевое слово extern используется, если область видимости глобальной переменной, описанной в одном файле, нужно расширить на функцию, находящуюся в другом файле:

/* исходный файл file1.cpp */

main()

{

...

}

funс1()

{

extern double i;

...

}

/* исходный файл file2.cpp */

double i = 9.36;

funс2()

{

...

}

Переменная i описана в файле file2.cpp, по умолчанию она – static, т.е. в других файлах не видна. Чтобы сделать ее видимой для функции func1 объявляется i в этой функции как extern. Необходимо отметить, что для функции main эта переменная по-прежнему будет недоступна. То есть, описан с выделением памяти один объект (в нашем случае переменная) может быть только единожды, объявлять же ее как внешнюю можно сколь угодно большое число раз. При разработке больших программных проектов обычно включают объявления extern в заголовочные файлы, которые затем подключаются к каждому файлу исходного текста программы. Это – очевидное упрощение по отношению к ручному объявлению переменных, которое может привести к ошибкам.

7.6. Заключение

Подведем итоги.

Глобальные переменные могут объявляться со спецификаторами классов хранения static и extern. Локальные переменные – с любым из четырех спецификаторов.

Любая локальная переменная по умолчанию – auto, любая глобальная – static. Локальные переменные auto и register автоматически не инициализируются никакими осмысленными значениями; и локальные, и глобальные static инициализируются нулем.

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

Подводя итог, можно составить таблицу 7.1, суммирующую информацию о классах хранения.

Таблица 7.1. Информация о классах хранения

Класс хранения Место хранения Время жизни Область видимости
автоматический (auto) сегмент стека равно времени выполнения программного блока внутри блока программы, в котором объявлена
регистровый (register) регистры процессора (если доступны) --//-- --//--
локальный статический (static) сегмент данных равно времени выполнения всей программы --//--
глобальный статический (static) --//-- --//-- файл, в котором глобально объявлена
внешний (extern) --//-- --//-- внутри всего проекта (даже, если программа состоит из нескольких файлов)

8. Структуры, объединения
и перечисления

8.1. Общие сведения

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

Рассмотрим типовую структуру на примере примитивной базы данных:

№ п/п Наименование (name) Стоимость (cost) Количество (quant) Примечание (note)
.. .. .. генератор … … … … … 100,5 … … … … … … … … … … частота 460 Гц … … … … …

Объявление структурной переменной состоит из:

1. задания шаблона структуры или типа структуры

2. собственно объявления структурной переменной.

Описание структуры начинается с ключевого слова s truct. В нашем примере объявление шаблона имеет следующий вид:

struct base

{

int n;

char name[20];

float cost;

int quant;

char note[100];

};

После объявления структуры ставится точка с запятой, поскольку оно (объявление) является оператором.

base определяет имя создаваемого таким образом типа структурной переменной; n, name, cost, quant, note – список элементов (члены, поля) структуры, которые могут иметь любой основной тип, включая тип других структурных переменных.

Если структура описана внутри функции, то она будет видна только в функции. Если вне функции, то шаблон будет доступен после объявления. Структурный шаблон сообщает компилятору, какой вид должна иметь структурная переменная, но сама структурная переменная не создаётся. Чтобы её создать, необходимо её объявить:

struct base m1; // создание переменной m1 типа base.

Когда объявляется структурная переменная, компилятор автоматически выделяет количество памяти, необходимое, чтобы разместить все её члены (рис. 5.1).

Рис. 5.1. Расположение элементов структуры в памяти

Объявление шаблона и одной (или нескольких) структурных переменных можно объединить:

struct base

{

int n;

char name[20];

float cost;

int quant;

char note[100];

} m1, m2, m3;

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

Общий вид объявления структуры:

struct [имя_типа_структуры]

{

тип_имя члена;

тип_имя члена;

.

.

.

тип_имя члена;

} [одна или более переменных-структур];

Неявно структурная переменная выравнивается на границу байта. В зависимости от опции IDE может быть задано выравнивание на границу байта или слова (2 байта).

При выравнивании на границу байта данные могут начинаться как с чётного, так и с нечётного адреса.

Если задан режим выравнивания на границу байта, то размер всей переменой равен сумме длин всех полей. Если задан режим выравнивания на границу слова, то все несимвольные данные выравниваются по чётным адресам, а все символьные (char) – по любым.

Исходя из этого видно, что внутри переменной могут появиться дополнительные байты для выравнивания на границу слова.

8.2. Инициализация структурных переменных

Инициализацию структуры можно осуществить двумя способами. Можно при объявлении структуры проинициализировать её элементы или произвести её инициализацию в теле программы. Вследствие того, что не всегда известны данные, которые должна содержать структура, чаще используется второй способ. Пример инициализации структурной переменной m1:

struct base m1 =

{1, “генератор”, 100.5, 100, “частота 460 Гц”};

Инициализация структуры во время объявления:

struct base

{

int n;

char name[20];

float cost;

int quant;

char note[100];

} m1 = {1, “генератор”, 1.2, 100, “сдан в ремонт”};

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

. обращение к элементу структуры;

-> обращение к элементу структуры через указатель на структуру;

В следующем выражении полю n уже объявленной ранее структурной переменной m1 присваивается значение 1:

m1.n = 1;

m1.name = “генератор”; // неправильно, т.к. name – массив,

// т.е. указатель-константа

strcpy(m1.name, “генератор”); // правильно

m1.cost = 1.2;

Чтобы вывести на экран поле структуры n нужно написать следующее выражение:


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: