C 858993460 что это означает
БлогNot. Лекции по C/C++: указатели и строки Си
Лекции по C/C++: указатели и строки Си
Указатель — это переменная, содержащая адрес некоторого объекта в оперативной памяти (ОП). Смысл применения указателей — косвенная адресация объектов в ОП, позволяющая динамически менять логику программы и управлять распределением ОП.
- работа с массивами и строками;
- прямой доступ к ОП;
- работа с динамическими объектами, под которые выделяется ОП.
Описание указателя имеет следующий общий вид:
тип *имя;
то есть, указатель всегда адресует определённый тип объектов! Например,
int *px; // указатель на целочисленные данные char *s; //указатель на тип char (строку Си)
Опишем основные операции и действия, которые разрешены с указателями:
1. Сложение/вычитание с числом:
px++; //переставить указатель px на sizeof(int) байт вперед s--; //перейти к предыдущему символу строки //(на sizeof(char) байт, необязательно один)
2. Указателю можно присваивать адрес объекта унарной операцией » & «:
int *px; int x,y; px=&x; //теперь px показывает на ячейку памяти со // значением x px=&y; //а теперь – на ячейку со значением y
3. Значение переменной, на которую показывает указатель, берется унарной операцией » * » («взять значение»):
x=*px; //косвенно выполнили присваивание x=y (*px)++; //косвенно увеличили значение y на 1
Важно! Из-за приоритетов и ассоциативности операций C++ действие
имеет совсем другой смысл, чем предыдущее. Оно означает «взять значение y ( *px ) и затем перейти к следующей ячейке памяти ( ++ )»
Если px по-прежнему показывал на y , он означает «записать значение y в x и затем перейти к ячейке памяти, следующей за px «. Именно такой подход в классическом Си используется для сканирования массивов и строк.
Вот пример, с точностью до адресов памяти показывающий это важное различие. Комментарием приведены значения и адреса памяти переменных x и y , а также значение, полученное по указателю px и адрес памяти, на который он показывает. Обратите внимание, что после выполнения второго варианта кода значение, полученное по указателю, стало «мусором», так как он показывал на переменную, а не на нулевой элемент массива.
#include int main() < int x=0,y=1; int *px=&y; printf ("\nx=%d on &%p, y=%d on &%p, *px=%d on &%p",x,&x,y,&y,*px,px); x=(*px)++; //после первого запуска замените на x=*px++; printf ("\nx=%d on &%p, y=%d on &%p, *px=%d on &%p",x,&x,y,&y,*px,px); /* Действие (*px)++ x=0 on &002CFC14, y=1 on &002CFC08, *px=1 on &002CFC08 x=1 on &002CFC14, y=2 on &002CFC08, *px=2 on &002CFC08 Действие *px++ x=0 on &0021F774, y=1 on &0021F768, *px=1 on &0021F768 x=1 on &0021F774, y=1 on &0021F768, *px=-858993460 on &0021F76C */ getchar(); return 0; >
Приведём пример связывания указателя со статическим массивом:
int a[5]=; int *pa=&a[0]; for (int i=0; ifor (int i=0; iЭти записи абсолютно эквиваленты, потому что в Си конструкция a[b] означает не что иное, как *(a+b) , где a - объект, b – смещение от начала памяти, адресующей объект. Таким образом, обращение к элементу массива a[i] может быть записано и как *(a+i) , а присваивание указателю адреса нулевого элемента массива можно бы было записать в любом из 4 видов
int *pa=&a[0]; int *pa=&(*(a+0)); int *pa=&(*a); int *pa=a;Важно! При любом способе записи это одна и та же операция, и это - не "присваивание массива указателю", это его установка на нулевой элемент массива.
4. Сравнение указателей (вместо сравнения значений, на которые они указывают) в общем случае может быть некорректно!
int x; int *px=&x, *py=&x; if (*px==*py) . //корректно if (px==py) . //некорректно!Причина – адресация ОП не обязана быть однозначной, например, в DOS одному адресу памяти могли соответствовать разные пары частей адреса "сегмент" и "смещение".
5. Указатели и ссылки могут использоваться для передачи функциям аргументов по адресу (то есть, для "выходных" параметров функций), для этого есть 2 способа:
Способ 1, со ссылочной переменной C++
void swap (int &a, int &b) < int c=a; a=b; b=c; >//. int a=3,b=5; swap (a,b);Этот способ можно назвать "передача параметров по значению, приём по ссылке".
Способ 2, с указателями Cи
void swap (int *a, int *b) < int c=*a; *a=*b; *b=c; >//. int a=3,b=5; swap (&a,&b); int *pa=&a; swap (pa,&b);Передача параметров по адресу, прием по значению.
Указатели и строки языка Си
Как правило, для сканирования Си-строк используются указатели.
char *s="Hello, world";Это установка указателя на первый байт строковой константы, а не копирование и не присваивание!
1. Даже если размер символа равен одному байту, эта строка займёт не 12 (11 символов и пробел), а 13 байт памяти. Дополнительный байт нужен для хранения нуль-терминатора, символа с кодом 0 , записываемого как '\0' (но не '0' – это цифра 0 с кодом 48). Многие функции работы с Си-строками автоматически добавляют нуль-терминатор в конец обрабатываемой строки:
char s[12]; strcpy(s,"Hello, world"); //Вызвали стандартную функцию копирования строки //Ошибка! Нет места для нуль-терминатора сhar s[13]; //А так было бы верно!2. Длина Си-строки нигде не хранится, её можно только узнать стандартной функцией strlen(s) , где s – указатель типа char * . Для строки, записанной выше, будет возвращено значение 12, нуль-терминатор не считается. Фактически, Си-строка есть массив символов, элементов типа char .
Как выполнять другие операции со строками, заданными c помощью указателей char * ? Для этого может понадобиться сразу несколько стандартных библиотек. Как правило, в новых компиляторах C++ можно подключать и "классические" си-совместимые заголовочные файлы, и заголовки из более новых версий стандарта, которые указаны в скобках.
Файл ctype.h ( cctype ) содержит:
1) функции с именами is* - проверка класса символов ( isalpha , isdigit , . ), все они возвращают целое число, например:
char d; if (isdigit(d)) < //код для ситуации, когда d - цифра >Аналогичная проверка "вручную" могла бы быть выполнена кодом вида
if (d>='0' && d<='9') <2) функции с именами to* - преобразование регистра символов ( toupper , tolower ), они возвращают преобразованный символ. Могут быть бесполезны при работе с символами национальных алфавитов, а не только латиницей.
Модуль string.h ( cstring ) предназначен для работы со строками, заданными указателем и заканчивающимися байтом '\0' ("строками Си"). Имена большинства его функций начинаются на "str". Часть функций ( memcpy , memmove , memcmp ) подходит для работы с буферами (областями памяти с известным размером).
Примеры на работу со строками и указателями
1. Копирование строки
char *s="Test string"; char s2[80]; strcpy (s2,s); //копирование строки, s2 - буфер, а не указатель!2. Копирование строки с указанием количества символов
char *s="Test string"; char s2[80]; char *t=strncpy (s2,s,strlen(s)); coutФункция strncpy копирует не более n символов ( n - третий параметр), но не запишет нуль-терминатор, в результате чего в конце строки t выведется "мусор". Правильно было бы добавить после вызова strncpy следующее:
t[strlen(s)]='\0';то есть, "ручную" установку нуль-терминатора.
3. Копирование строки в новую память
char *s="12345"; char *s2=new char [strlen(s)+1]; strcpy (s2,s);Здесь мы безопасно скопировали строку s в новую память s2 , не забыв выделить "лишний" байт для нуль-терминатора.
4. Приведём собственную реализацию стандартной функции strcpy :
char *strcpy_ (char *dst, char *src) < char *r=dst; while (*src!='\0') < *dst=*src; dst++; src++; >*dst='\0'; return r; >Вызвать нашу функцию можно, например, так:
char *src="http://blog.kislenko.net/%D0%A1%D1%82%D1%80%D0%BE%D0%BA%D0%B0%20%D1%82%D0%B5%D0%BA%D1%81%D1%82%D0%B0"; char dst[80]; strcpy_ (&dst[0],&src[0]);Сократим текст функции strcpy_:
char *strcpy_ (char *dst, char *src)
5. Сцепление строк – функция strcat
char *s="Test string"; char *s2; char *t2=strcat (s2,strcat(s," new words"));Так как strcat не выделяет память, поведение такого кода непредсказуемо!
А вот такое сцепление строк сработает:
char s[80]; strcpy (s,"Test string"); char s2[80]; strcat (s," new words"); strcpy (s2,s); char *t2=strcat (s2,s);То есть, всегда должна быть память, куда писать - статическая из буфера или выделенная динамически.
6. Поиск символа или подстроки в строке.
char *sym = strchr (s,'t'); if (sym==NULL) puts ("Не найдено"); else puts (sym); //выведет "t string" //для strrchr вывод был бы "tring" char *sub = strstr (s,"ring"); puts (sub); //выведет "ring"7. Сравнение строк – функции с шаблоном имени str*cmp - "string comparing"
char *a="abcd",*b="abce"; int r=strcmp(a,b); //r=-1, т.к. символ 'd' предшествует символу 'e' //Соответственно strcmp(b,a) вернет в данном случае 1 //Если строки совпадают, результат=08. Есть готовые функции для разбора строк - strtok , strspn , strсspn - см. пособие, пп. 8.1-8.3
9. Преобразование типов между числом и строкой - библиотека stdlib.h ( cstdlib )
char *s="qwerty"; int i=atoi(s); //i=0, исключений не генерируется!Из числа в строку:
1) itoa , ultoa - из целых типов
char buf[20]; int i=-31189; char *t=itoa(i,buf,36); //В buf получили запись i в 36-ричной с.с.2) fcvt , gcvt , ecvt - из вещественных типов
Работа с динамической памятью
Как правило, описывается указатель нужного типа, который затем связывается с областью памяти, выделенной оператором new или си-совместимыми функциями для управления ОП.
1. Описать указатель на будущий динамический объект:
int *a; //Надёжнее int *a=NULL;2. Оператором new или функциями malloc , calloc выделить оперативную память:
a = new int [10];#include //stdlib.h, alloc.h в разных компиляторах //. a = (int *) malloc (sizeof(int)*10);a = (int *) calloc (10,sizeof(int));В последнем случае мы выделили 10 элементов по sizeof(int) байт и заполнили нулями '\0' .
Важно! Не смешивайте эти 2 способа в одном программном модуле или проекте! Предпочтительней new , кроме тех случаев, когда нужно обеспечить заполнение памяти нулевыми байтами.
3. Проверить, удалось ли выделить память - если нет, указатель равен константе константе NULL из стандартной библиотеки (в ряде компиляторов null , nullptr , 0 ):
if (a==NULL) < //Обработка ошибка "Не удалось выделить память" >4. Работа с динамическим массивом или строкой ничем не отличается от случая, когда они статические.
5. Когда выделенная ОП больше не нужна, её нужно освободить:
delete a; //Если использовали newfree (a); //Пытается освободить ОП, //если использовали malloc/callocВажно! Всегда старайтесь придерживаться принципа стека при распределении ОП. То есть, объект, занявший ОП последним, первым её освобождает.
Пример. Динамическая матрица размером n*m.
const int n=5,m=4; int **a = new (int *) [n]; for (int i=0; iПосле этого можно работать с элементами матрицы a[i][j] , например, присваивать им значения. Освободить память можно было бы так:
for (int i=n-1; i>-1; i--) delete a[i]; delete a;Рекомендуемые задачи
1. Написать собственную функцию работы со строкой, заданной указателем, сравнить со стандартной.
2. Написать собственную функцию для работы с одномерным динамическим массивом, заданным указателем.
3. Написать свои версии функций преобразования строки в число и числа в строку.
05.11.2015, 08:12 [23917 просмотров]
1.6 – Неинициализированные переменные и неопределенное поведение
В отличие от некоторых языков программирования, C/C++ не инициализирует большинство переменных автоматически заданным значением (например, нулем). Таким образом, когда компилятор выделяет переменной место в памяти, значением по умолчанию для этой переменной является любое (мусорное) значение, которое уже находится в этой области памяти! Переменная, которой не было присвоено известное значение (обычно посредством инициализации или присваивания), называется неинициализированной переменной.
Примечание автора
Многие читатели ожидают, что термины «инициализированный» и «неинициализированный» будут строго противоположными, но это не совсем так! Инициализация означает, что объекту было предоставлено начальное значение в точке определения. Неинициализированный означает, что объекту не было присвоено известное значение (каким-либо образом, включая присваивание). Следовательно, объект, который не инициализирован, но которому затем было присвоено значение, больше не является неинициализированным (потому что ему было присвоено известное значение).
- инициализация = объекту присваивается известное значение в точке определения;
- присваивание = объекту присваивается известное значение в точке, выходящей за рамки определения;
- неинициализированный = объекту еще не присвоено известное значение.
В качестве отступления.
Отсутствие инициализации является оптимизацией производительности, унаследованной от C, когда компьютеры были медленными. Представьте себе случай, когда вы собираетесь прочитать 100 000 значений из файла. В таком случае вы можете создать 100 000 переменных, а затем заполнить их данными из файла.
Если бы C++ инициализировал все эти переменные при создании значениями по умолчанию, это привело бы к 100 000 инициализаций (что было бы медленно) и к небольшой выгоде (поскольку вы всё равно перезапишете эти значения).
На данный момент вы всегда должны инициализировать свои переменные, потому что затраты на это ничтожны по сравнению с выгодой. Как только вы освоите язык, тогда могут быть определенные случаи, когда вы можете пропустить инициализацию в целях оптимизации. Но делать это всегда нужно выборочно и намеренно.
Использование значений неинициализированных переменных может привести к неожиданным результатам. Рассмотрим следующую короткую программу:
#include int main() < // определяем целочисленную переменную с именем x int x; // эта переменная не инициализирована, потому что мы не присвоили ей значение // выводим значение x на экран std::cout
В этом случае компьютер выделит для x некоторую неиспользуемую память. Затем он отправит значение, находящееся в этой ячейке памяти, в std::cout , который напечатает значение (интерпретируемое как целое число). Но какое значение он напечатает? Ответ – «кто знает!», и ответ может (или не может) меняться каждый раз, когда вы запускаете программу. Когда автор запускал эту программу в Visual Studio, std::cout в первый раз вывел значение 7177728, а во второй раз – 5277592. Не стесняйтесь компилировать и запускать программу самостоятельно (ваш компьютер не взорвется).
В качестве отступления.
Некоторые компиляторы, такие как Visual Studio, при использовании конфигурации отладочной сборки будут инициализировать содержимое памяти некоторым предустановленным значением. Этого не произойдет при использовании конфигурации сборки выпуска. Поэтому, если вы хотите запустить указанную выше программу самостоятельно, убедитесь, что вы используете конфигурацию сборки выпуска (чтобы вспомнить, как это сделать, смотрите урок «0.9 – Настройка компилятора: конфигурации сборки»). Например, если вы запустите приведенную выше программу в конфигурации отладки в Visual Studio, она будет неизменно печатать -858993460 , потому что с помощью этого значения (интерпретируемого как целое число) Visual Studio инициализирует память в конфигурациях отладки.
Большинство современных компиляторов пытаются определить, используется ли переменная без присваивания значения. Если они смогут это обнаружить, они обычно выдадут ошибку времени компиляции. Например, компиляция приведенной выше программы в Visual Studio выдала следующее предупреждение:
c:\VCprojects\test\test.cpp(11) : warning C4700: uninitialized local variable 'x' used
Если ваш компилятор по этой причине не позволяет вам скомпилировать и запустить приведенную выше программу, вот возможное решение этой проблемы:
#include // Пока не беспокойтесь о том, что такое &, мы просто используем его, // чтобы заставить компилятор думать, что переменная x используется void doNothing(int&) < >int main() < // определяем целочисленную переменную с именем x int x; // эта переменная не инициализирована // заставляем компилятор думать, что мы присваиваем значение этой переменной doNothing(x); // выводим значение x на экран (кто знает, что мы получим, потому что x не инициализирован) std::cout
Использование неинициализированных переменных – одна из наиболее распространенных ошибок, которые совершают начинающие программисты, и, к сожалению, она также может быть одной из самых сложных для отладки (потому что программа всё равно может работать нормально, если неинициализированное значение было присвоено определенной области памяти, которая содержала приемлемое значение, например, 0).
Это основная причина использования оптимальной практики «всегда инициализировать переменные».
Неопределенное поведение
Использование значения из неинициализированной переменной – наш первый пример неопределенного поведения. Неопределенное поведение – это результат выполнения кода, поведение которого не определено языком C++. В этом случае в языке C++ нет правил, определяющих, что произойдет, если вы используете значение переменной, которой не было присвоено известное значение. Следовательно, если вы действительно сделаете это, результатом будет неопределенное поведение.
Код, реализующий неопределенное поведение, может проявлять любые из следующих симптомов:
- ваша программа при каждом запуске дает разные результаты;
- ваша программа постоянно дает один и тот же неверный результат;
- ваша программа ведет себя непоследовательно (иногда дает правильный результат, иногда нет);
- кажется, что ваша программа работает, но позже выдает неверные результаты;
- ваша программа вылетает сразу после запуска или позже;
- ваша программа работает с одними компиляторами, но не работает с другими;
- ваша программа работает до тех пор, пока вы не измените какой-нибудь другой, казалось бы, несвязанный код.
Или ваш код в любом случае может действительно вести себя правильно. Природа неопределенного поведения заключается в том, что вы никогда не знаете точно, что получите, будете ли вы получать это каждый раз, и изменится ли это поведение, когда вы внесете какие-либо изменения.
C++ содержит множество случаев, которые могут привести к неопределенному поведению, если вы не будете осторожны. Мы будем указывать на них в будущих уроках всякий раз, когда с ними столкнемся. Обратите внимание на эти случаи и убедитесь, что вы их избегаете.
Правило
Старайтесь избегать всех ситуаций, которые приводят к неопределенному поведению, например, использование неинициализированных переменных.
Примечание автора
Один из наиболее распространенных типов комментариев, которые мы получаем от читателей, гласит: «Вы сказали, что я не могу делать X, но я всё равно сделал это, и моя программа работает! Почему?".
Есть два общих ответа. Наиболее распространенный ответ заключается в том, что ваша программа на самом деле демонстрирует неопределенное поведение, но это неопределенное поведение в любом случае дает желаемый результат… пока. Завтра (или на другом компиляторе или машине) этого может и не быть.
В качестве альтернативы, иногда авторы компиляторов допускают вольность к требованиям языка, когда эти требования могут быть более строгими, чем необходимо. Например, в стандарте может быть сказано: «Вы должны сделать X перед Y», но автор компилятора может счесть это ненужным и заставить Y работать, даже если вы сначала не выполните X. Это не должно влиять на работу правильно написанных программ, но в любом случае может привести к тому, что неправильно написанные программы будут работать. Таким образом, альтернативный ответ на вышеупомянутый вопрос заключается в том, что ваш компилятор может просто не следовать стандарту! Такое случается. Вы можете избежать этого, если отключили расширения компилятора, как описано в уроке «0.10 – Настройка компилятора: расширения компилятора».
Небольшой тест
Вопрос 1
Что такое неинициализированная переменная? Почему вам следует избегать их использования?
Неинициализированная переменная – это переменная, которой программа не присвоила значение (обычно посредством инициализации или присваивания). Использование значения, хранящегося в неинициализированной переменной, приведет к неопределенному поведению.
Вопрос 2
Что такое неопределенное поведение и что может произойти, если вы сделаете что-то, что демонстрирует неопределенное поведение?
Неопределенное поведение – это результат выполнения кода, поведение которого не определяется языком. Результатом может быть что угодно, в том числе и то, что ведет себя правильно.
Паппас К., Мюррей У. - Visual C++ 6. Руководство разработчика - 2000
Рис. 3.11. Помечанная стрелкой строка программного кода, содержащая ошибку Продолжение отладки После того как вы внесли исправления, программа готова к новой попытке построить исполняемый файл. Перейдите в меню Project и вновь выберите команду RebuildAll. На рис. 3.12 показано обновленное окно сообщений. 41
Рис.3.12. Окно сообщений во время третьей попытки построить исполняемый файл Теперь обнаруживаем, что та же строка, которая содержала некорректное имя функции (Printf()),заключает в себе еще одну ошибку. В C/C++ все строковые значения должны браться в двойные кавычки. Значит, в нашем случае открывающие кавычки в функции printf () следует поставить сразу после открывающей круглой скобки, т.е. строка должна начинаться следующим образом: printf(". Сохраните файл и попытайтесь еще раз построить программу. Как выглядит окно сообщений после очередного обновления, показано на рис. 3.13. 42
Рис. 3.13. Окно сообщений во время четвертой попытки построить исполняемый файл На этот раз выдается такое сообщение об ошибке: syntax error: missing ';'before ')' В C/C++, в отличие от Pascal, символ точки с запятой используется для обозначения окончания выражения, а не в качестве разделителя. Таким образом, в конце второй проверки в цикле for необходимо ввести точку с запятой. После исправления сохраните файл и вновь выполните команду RebuildAll. Все в порядке? Судя по окну сообщений, у компилятора нет больше претензий к вашей программе, и команда RebuildAll успешно сгенерировала исполняемый файл ERROR.EXE. 43
В окне сообщений должны отсутствовать сообщения об ошибках, но присутствовать одно предупреждение, которое можно проигнорировать. Если это не так, значит, вы допустили ошибки при вводе программы. Проверьте программный код и исправьте его. Запуск программы Чтобы запустить программу, просто выберите в меню Project команду Execute. Если в ответ на запрос программы Would you like to continue(Y/N) вы нажмете клавишу [Y], а затем [Enter], на экране отобразится следующее: -858993460 0 -858993460 -858993460 -858993460 -858993460 -858993460 Welcome to a trace demonstration! Would you like to continue (Y/N)у На рис. 3.14 показано, что произойдет далее. Рис. 3.14. Сообщение об ошибке выполнения программы Использование встроенного отладчика Созданная нами программа в начале своей работы отображает на экране исходное содержимое массива данных, после чего спрашивает, хотите ли вы продолжить работу. Ответ Y (yes— да) сигнализирует о том, что вы хотите заполнить массив собственными данными и отобразить их на экране. Из рис. 3.14 можно сделать вывод о том, что хотя программный код набран совершенно правильно, т.е. в нем нет синтаксических ошибок, программа работает не так, как нам бы хотелось. Ошибки такого рода называются логическими. К счастью, встроенный в VisualC++ отладчик содержит ряд средств, которые послужат для вас спасательным кругом в подобной ситуации. Во-первых, вы можете выполнять программу пошагово, строка за строкой. Вовторых, вам предоставляется возможность анализировать значения переменных в любой момент выполнения программы. Разница между командами StepInto и StepOver Когда вы начинаете процесс отладки, появляется панель инструментов Debug. Из множества представленных на ней кнопок наиболее часто задействуются StepInto (четвертая справа в верхнем ряду) и StepOver(третья справа). В обоих случаях программа будет запущена на выполнение в пошаговом режиме, а в тексте программы выделяется та строка, которая сейчас будет выполнена. Различия между командами StepInto и StepOver проявляются только тогда, когда в программе встречается вызов функции. Если выбрать команду StepInto, то отладчик войдет в функцию и начнет выполнять шаг за шагом все ее операторы. При выборе команды StepOver отладчик выполнит функцию как единое целое и перейдет к строке, следующей за вызовом функции. Эту команду удобно применять в тех случаях, когда в программе делается обращение к стандартной функции или созданной вами подпрограмме, которая уже была протестирована. Давайте выполним пошаговую отладку нашей программы.
Как видно из рис. 3.15, в окне редактирования появилась стрелка (ее называют индикатором трассировки), указывающая на строку программы, которая будет выполнена на следующем шаге. В данный момент она указывает на функцию print_them(). Рис. 3.15. Окно редактирования после того, как трижды была выполнена команда StepInto или StepOver Имеет смысл выполнить эту функцию как одно целое. Для этого выберем команду StepOver. Функция будет выполнена, и индикатор трассировки укажет на первый вызов функции printf(). Теперь три раза нажмите клавишу [F10], пока стрелка не остановится напротив функции scanf(). В этот момент вам нужно перейти в окно программы и в ответ на приглашение Would you like to continue(Y/N) ввести Y и нажать [Enter] (рис. 3.16). Сразу после этого на экране появится сообщение об ошибке (рис. 3.17). Это сообщение было сгенерировано программой после попытки выполнит функцию scanf(). Давайте попытаемся разобраться, в чем, собственно, состоит проблема.
Рис. 3.16. Введите "Y" и нажмите [Enter], чтобы продолжить выполнение программы Рис. 3.17. Отладчик сообщает об ошибке в программе Ошибка связана с некорректным использованием функции scanf() . Функция scanf () ожидает указания адреса ячейки памяти для заполнения. Рассмотрим такое выражение: scanf("%C", continu); Как видите, здесь указывается не адрес переменной, а сама переменная. Чтобы указать адрес, нужно поместить оператор взятия адреса (&) перед continu. Внесите исправления в выражение, чтобы оно выглядело следующим образом: scanf("%C", &continu); Сохраните файл и вновь выберите команду RebuildAll. Дополнительные средства отладки Вы, очевидно, слышали о точках останова, которые применяются в программе при необходимости прервать ее выполнение в определенных местах. Смысл использования точек останова состоит в том, что отладчик не тратит времени на пошаговое выполнение программы вплоть до указанной точки, по достижении которой переходит в пошаговый режим. Точки останова проще всего расставлять с помощью кнопки Breakpoint (первая справа) панели инструментов Build. Для этого достаточно установить курсор на нужной строке программы и щелкнуть на указанной кнопке. Если же выделенная строка уже содержит точку останова, то после щелчка на кнопке Breakpoint она, точка останова, будет удалена. При выборе команды Go программа будет выполняться от текущего местоположения курсора до ближайшей точки останова. Обратимся к нашей программе. Мы знаем, что все строки программы до вызова функции scanf() отлично работают. Чтобы не тратить время на пошаговое выполнение всех строк, 46
которые уже были проверены ранее, поставим точку останова на 20-й строке, содержащей вызов функции scanf(). Имеется и другой способ задания точек останова — с помощью диалогового окна Breakpoints (рис. 3.18), вызываемого командой Breakpoints. из меню Edit. По умолчанию при щелчке на кнопке со стрелкой открывается контекстное меню, в котором первым пунктом указывается команда создания точки останова на той строке, где в данный момент в окне редактирования находится курсор. В нашем случае это строка 20. Рис. 3.18. Задание точки останова Работа с точками останова Предположим, что вы поставили точку останова в строке программы, содержащей вызов функции scanf<). Теперь выберите команду Go— либо из меню, либо нажав клавишу [F5]. Обратите внимание, что выполнение программы прерывается не на первой строке программы, а на строке, содержащей точку останова. Далее можно продолжить выполнение программы в пошаговом режиме либо проанализировать текущие значения переменных. Нас интересует, будет ли функция scanf() работать корректно после того, как в программный код были внесены изменения. Выберите команду StepOver, перейдите к окну программы, введите букву Y в верхнем регистре и нажмите клавишу [Enter]. (Мы применили команду StepOver для того, чтобы избежать пошагового анализа отладчиком всех операторов функции scanf(). При выборе команды StepIn появляется предложение указать местонахождение файла SCANF.C) Все отлично! Отладчик не выдал окна с сообщением об ошибке. Но означает ли это, что все проблемы разрешены? Чтобы ответить на этот вопрос, достаточно будет проанализировать текущее значение переменной continu. Окно QuickWatch Команда QuickWatch. открывает диалоговое окно QuickWatch(рис. 3.19), которое позволяет по ходу выполнения программы анализировать значения переменных. Простейший способ определить значение переменной с помощью данного окна состоит в том, что курсор помещается на имени переменной в окне редактирования, а затем нажимается комбинация клавиш [Shift+F9]. Проделайте указанную операцию с переменной continu.
Рис. 3.19. Диалоговое окно QuickWatch Теперь, когда мы определили, что переменная continu имеет правильное значение, можно продолжить выполнение программы до конца, выбрав в меню Debug команду Go(рис. 3.20). Рис. 3.20. Исправленная версия программы 48
Глава 4. Введение в С и C++ • Из истории языка С o Отличия С от других ранних языков высокого уровня o Достоинства языка С o Недостатки языка С o Язык С — не для любителей! • Стандарт ANSI С • Переход от С к C++ и объектно-ориентированному программированию • Из истории языка C++ o Эффективность объектно-ориентированного подхода o Незаметные различия между С и C++ o Ключевые различия между С и C++ • Основные компоненты программ на языках C/C++ o Простейшая программа на языке С o Простейшая программа на языке C++ o Получение данных от пользователя в языке С o Получение данных от пользователя в языке C++ o Файловый ввод-вывод Знакомство с языками программирования С и C++ мы начнем с истории. Узнать историю появления языков С и C++ будет полезно, поскольку так нам легче будет понять концепции, положенные в основу этих языков, а также ответить на вопрос, почему С на протяжении вот уже ряда десятилетий остается столь популярным среди программистов, а его более молодой "родственник" C++ не уступает ему по популярности. Приводимые далее примеры можно набирать и запускать в среде MicrosoftVisualC++. Подразумевается, что в предыдущей главе вы научились выполнять элементарные операции по компиляции и отладке программ. Из истории языка С Наше исследование происхождения языка С начнется с операционной системы UNIX, поскольку она сама и большинство программ для нее написаны на С. Тем не менее, это не означает, что С предназначен исключительно для UNIX. Благодаря популярности UNIX язык С был признан в среде программистов как язык системного программирования, который можно использовать для написания компиляторов и операционных систем. В то же время он удобен для создания многих прикладных программ. Операционная система UNIX была разработана в 1969 г. на маломощном, по современным представлениям, компьютере DEC PDP-7 в компании BellLaboratories, город Мюррей Хилл, штат Нью-Джерси. Система была полностью написана на языке ассемблера для PDP-7 и претендовала на звание "дружественной для программистов", поскольку содержала довольно мощный набор инструментов разработки и являлась достаточно открытой средой. Вскоре после создания UNIX Кен Томпсон (KenThompson) написал компилятор нового языка В. С этого момента мы можем начать отслеживать историю языка С, поскольку язык В Кена Томпсона был непосредственным его предшественником. Рассмотрим родословную языка С: Algol 60 Разработан международным комитетом в 1960 г.
| CPL | Combined Programming Language — комбинированный язык программирования. Разработан в 1963 г. | |
| группой программистов из Кембриджского и Лондонского университетов | ||
| BCPL | BasicCombinedProgrammingLanguage — базовый комбинированный язык программирования. | |
| Разработан в Кембридже Мартином Ричардсом (MartinRichards) в 1967 г. | ||
| В | Разработан в 1970 | г. Кеном Томпсоном, компания BellLabs |
| С | Разработан в 1972 | г. Деннисом Ритчи (DennisRitchie), компания BellLabs |
Позже, в 1983 г., при Американском институте национальных стандартов (American National Standards Institute — ANSI) был создан специальный комитет с целью стандартизации языка С, в результате чего был разработан стандарт ANSI С. Язык Algol 60 появился всего на пару лет позже языка FORTRAN. Это был значительно более мощный язык, который во многом предопределил пути дальнейшего развития большинства последующих языков программирования. Его создатели уделили много внимания логической целостности синтаксиса команд и модульной структуре программ, с чем, собственно, впоследствии и ассоциировались все языки высокого уровня. К сожалению, Algol 60 не стал популярным в США. Многие считают, что причиной тому была определенная абстрактность этого языка. Разработчики языка CPL попытались приблизить "возвышенный" Algol 60 к реалиям конкретных компьютеров. Тем не менее, данный язык остался таким же трудным для изучения и практического применения, как и Algol 60, что предопределило его судьбу. В наследство от CPL остался язык BCPL, представляющий собой упрощенную версию CPL, сохранившую лишь основные его функции. Когда Кен Томпсон взялся за разработку языка В для ранней версии UNIX, он попытался еще больше упростить язык CPL. И действительно, ему удалось создать довольно интересный язык, который эффективно работал на том оборудовании, для которого был спроектирован. Однако языки В и BCPL, очевидно, были упрощены больше, чем нужно. Их использование было ограничено решением весьма специфических задач. Например, когда Кен Томпсон закончил создание языка В, появился новый компьютер PDP-11. Система UNIX и компилятор языка В были сразу же модернизированы под новую машину. Хотя PDP-11 был, несомненно, мощнее, чем его предшественник PDP-7, возможности этого компьютера оставались все еще слишком скромными по сравнению а современными стандартами. Он имел только 24 Кб оперативной памяти, из которых 16 Кб отводились операционной системе, и 512 Кб на жестком диске. Возникла идея переписать UNIX на языке В. Но В работал слишком медленно, поскольку оставался языком интерпретирующего типа. Была и другая проблема: язык В ориентировался на работу со словами, тогда как компьютер PDP-11 оперировал байтами. Стала очевидной необходимость усовершенствования языка В. Работа над более совершенной версией, которую назвали языком С, началась в 1971 г. Деннис Ритчи, который известен как создатель языка С, частично восстановил независимость языка от конкретного оборудования, что было во многом утеряно в языках BCPL и В. Так, были успешно введены в практику типы данных и в то же время сохранена возможность прямого доступа к оборудованию — идея, заложенная еще в языке СPL. Многие языки программирования, разработанные отдельными авторами (С, Pascal, Lisp и APL), отличались большей целостностью, чем те, над которыми трудились группы разработчиков (Ada, PL/I и Algol 60). Кроме того, для языков, разработанных одним автором, характерна большая специализированность в решении тех вопросов, в которых автор разбирался лучше всего. Деннис Ритчи был признанным специалистом в области системного программирования, а именно: языков программирования, операционных систем и генераторов программ. Учитывая профессиональную направленность автора, нетрудно понять, почему С был признан прежде всего разработчиками системных программ. Язык С представлял собой язык программирования относительно низкого уровня, что позволяло контролировать каждую мелочь в работе алгоритма и достигать максимальной эффективности. Но в то же время в С заложены принципы языка высокого уровня, что позволяло избежать зависимости программ от особенностей архитектуры конкретного компьютера. Это повышало эффективность процесса программирования. Отличия С от других ранних языков высокого уровня Вам, конечно, интересно узнать, какое место занимает С в ряду других языков программирования. Чтобы ответить на этот вопрос, рассмотрим примерную иерархию языков, показанную на рис. 4.1, где точками представлены промежуточные этапы развития. К примеру,
C 858993460 что это означает
Здравствуйте, уважаемые программисты! Приходилось ли Вам копаться в "мусоре"? Мне приходилось.
Началось всё с того, как мне захотелось написать собственную реализацию strcat() и strlen() (разумеется в собственном пространстве имён) и я столкнулся с следующей проблемой.
В программе, тестирующей эти функции я объявил три строки:
char str1[80] = "Hallo!"; char str2[] = " How are you?"; char str3[80];
Как видите, первые две строки тут же инициализированы, а третья - нет (она вводится с клавиатуры).
Вот так я реализовал strcat:
void strcat(char* str1, const char* str2)
Вот strlen:
int strlen(const char *str)
Перед определением функций есть прототипы, так что, функции видят друг друга с этим проблем нет.
Для первых двух строк strcat сработала как надо, но когда я применил её к третьей:
strcat(str3, str2);
я узнал, что такое "мусор".
Оказалось, что str3 не содержит ни одного "завершающего нуля"! При объявлении, 3-я строка содержит 80 значений -52, беззнаковое представление которых есть 204 в ASCII это русская 'M' или '╠'. При в воде в строку str3 n символов (n<80), первые n 'М' заменяются на символы, введённые с клавы, а остальные не меняются! А при инициализации первой строки все "оставшиеся" символы равны 0, поэтому с ним проблем нет. В strcat пришлось дописать
str1[pos] = '\0';
Или нужно было объявить str3 как нуль-строку
char str3[80] = "";
Теперь всё работает, но остались вопросы.
Итак, как я понял, значение -52 для char есть ничто иное как "мусор". Эксперимент даёт следующие результаты для "мусора" других типов:
| char: -52 'М' | short: -13108 | int: -858993460 | float: -107374176. | doule: -9.2559631349317831e+061 |
Как я понял здесь int и double расширяются до long-значений, т.к. для long и long double "мусор" идентичен соответствующим значениям для int и double.
Внимание! Bопросы:
1) Почему "мусор", который как я читал и считал должен иметь неопределённый результат имеет вполне определённые отрицательные значения?
2) Почему они именно такие?
3) Зависят ли эти значения от модели процессора, компилятора ОС или ещё чего?
Очень надеюсь на ответы. Заранее благодарен.
Последний раз редактировалось Haric_110; 06.08.2014 в 18:29 .