Архив за месяц: Август 2013

Занятие 16. Указатели.


Здравствуйте друзья.
Сегодня у нас важная и сложная тема. Возможно, самая сложная тема за все время. Указатели.

Прежде вспомним основы шестнадцатеричной системы счисления.

Любое число в 16-тиричной системе счисления записывается с помощью символов 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F. Шестнадцатеричной она называется, потому что в ней для записи различных чисел используются 16 основных цифр. Например, в привычной нами десятичной системе счисления используется десять основных цифр 0,1,2,3,4,5,6,7,8 и 9. 

Первые десять цифр в 16-тиричной системе счисления такие же, как и в десятичной системе, а вот для записи следующих шести используются буквы латинского алфавита. 
A=10
B=11
C=12
D=13
E=14
F=15.

Как перевести число из шестнадцатеричной системы счисления в десятичную.

Пусть у нас есть число 2D5. И мы хотим узнать, сколько это в нашей десятичной системе счисления.
Для этого сначала разделим число на цифры:
2 D5
Теперь заменим все буквы, на их числовые обозначения:
2 13 5

Пронумеруем эти цифры справа налево начиная с нуля.

2   13   5
2         1      0

Умножим каждую из цифр, на 16 в степени, соответствующей порядковому номеру. И сложим все это между собой.
Рис.1 Перевод шестнадцатеричного числа в десятичную систему счисления
В результате получим 725. Это и есть число 2D5 в десятичной системе счисления.

Перевод числа из десятичной в шестнадцатеричную систему счисления. 

Давайте переведем число 725 обратно в шестнадцатеричную систему счисления. В результате у нас должно получится 2D5.
Рис.2 Перевод из 10-ой в 16-ричную

Для перевода, нам необходимо поделить с остатком число 725 на 16 в столбик. Получим 45 и остаток 5. 

Теперь делим 45 на 16 с остатком. Получим 2 и остаток 13. Оба числа меньше 16 значит на этом можно закончить деление.

Теперь записываем эти числа в обратном порядке и заменяем буквами соответствующие. 

Как вы уже успели заметить, мы получили то, что и ожидали 2D5.






А теперь посмотрите на следующее число 235. Оно может быть как числом в десятичной, так и в шестнадцатеричной системе счисления. А кстати, чему будет равно число 235 в шестнадцатеричной системе счисления  при переводе в десятичную систему? Переведите самостоятельно, посмотрите сколь велико отличие.
Нужно как-то уметь отличать. Для этого используются специальные обозначения. Одним из способов является запись в которой шестнадцатеричному числу предшествует пара символов «0х». Т.е. запись 0х235 говорит нам, что 235 это число, записанное в шестнадцатеричной системе счисления.

Переменные и их адреса.

Как отмечалось во втором уроке, каждая переменная хранится в памяти. Естественно у каждой переменной в памяти есть свой адрес, по которому она записана. Мы уже даже использовали адреса переменных, когда пользовались функцией scanf().
Чтобы получить адрес переменной нам необходимо перед её именем написать символ “&”.

Память компьютера мы можем представить себе в виде таблицы с ячейками по одному байту, каждая из которых имеет свой адрес. Кстати, адреса записываются цифрами шестнадцатеричной системы. Например, это можно представить так, как показано на следующем рисунке.
Рис.3 Пример представления памяти компьютера
Как мы уже знаем, каждая переменная в зависимости от её типа, она занимает в памяти различное количество байт. Ну или в нашей интерпретации ячеек. Для того, чтобы узнать размеры  различных типов переменных можно использовать функцию sizeof(). Ниже представлена программа, иллюстрирующая её использование.

Листинг16.1
#include <stdio.h>

int main (){

printf ("razmer peremennoi tipa char %dn", sizeof(char));

printf ("razmer peremennoi tipa int %dn", sizeof(int));

printf ("razmer peremennoi tipa float %dn", sizeof(float));

printf ("razmer peremennoi tipa double %dn", sizeof(double));

return(0);

}

Результат её работы:
Рис 4. Программа иллюстрирующая работу sizeof()

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

То есть, если я объявляю в программе переменную типа int, то под нее в памяти выделяется 4 байта (ячейки).

Так как мы уже умеем получать адрес переменной, то давайте посмотрим на него. Для того, чтобы вывести число в шестнадцатеричной системе существует специальный спецификатор “%x”. И для него есть модификатор “#” при его записи, шестнадцатеричное число выводится с символами «0х».

Листинг16.2
#include <stdio.h>

int main (){

      int a,b;

      printf ("adres peremennoi a %#xn", &a);

      printf ("adres peremennoi b %#xn", &b);

return(0);

}

Рис.5 Адреса переменных в памяти

Мы получили два адреса 0x12ff60 и 0x12ff56. По этим адресам в памяти записаны наши переменные a и b. При этом они занимают в памяти по 4 клетки подряд, так как это переменные целого типа и из рисунка 3 видно, что их размер 4 байта. Это выглядит примерно следующим образом.
Рис.6. Пример расположения переменных в памяти
Как вы уже заметили, переменные в память записываются не одна за другой, а в произвольном месте, лишь бы там было пусто и хватило места. Исключение составляют массивы. Они записываются в память последовательно. Посмотрите на результат работы следующей программы.

Листинг16.3
#include <stdio.h>

int main (){

      int a[3];

      printf ("adres peremennoi a[0] %#xn", &a[0]);

      printf ("adres peremennoi a[1] %#xn", &a[1]);

return(0);

}

Рис.7 Расположение массива в памяти

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

Адреса это хорошо, но что с ними делать? На кой чёрт они нам сдались?
Обо всем по порядку. 

Указатели. 

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

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

Объявление указателя.

Указатель, раз это переменная, должен как-то объявляться. Делается это почти что также как и обычно.
int * p_a;

Сначала указывается тип переменной, которая будет храниться в памяти, по адресу указателя. Далее следует специальный символ «*» (звездочка), которая и указывает на то, что мы собираемся объявить не просто переменную типа int, а указатель на переменную типа int. После звездочки пишут имя указателя. Ну и естественно заключительная точка с запятой.

В нашем примере, мы объявили  указатель с именем p_a, который будет указывать на переменную типа int. Кстати, обычно, чтобы не путать указатели с другими переменными, в их имена добавляют какой-нибудь отличительный знак. Например, я вот добавляю обычно “p_”.  Когда я вижу в своей программе переменную, имя которой начинается с этих символов, я точно знаю что это у меня указатель. Кроме того, если программа большая, я помимо этого указывают после «p» ещё и тип переменной для типа int  - i, для floatf, для char – с и так далее, получается что-то типа pi_a. Это  сразу говорит мне, что это указатель, который ссылается на переменную типа int.
  

Присвоение указателю адреса.

Давайте перепишем Листинг 16.2, используя указатели.

Листинг 16.4
#include <stdio.h>

int main (){

      int a,b,*pi_a, *pi_b;

      pi_a=&a;

      pi_b=&b;

      printf ("adres peremennoi a %#xn", pi_a);

      printf ("adres peremennoi b %pn", pi_b);

return(0);

}

Рис.8. Пример хранения адреса переменной в указателе.

Как видите, после объявления с указателем можно работать так же, как и с обычной переменной. Ему можно присвоить некоторый адрес, используя оператор «=».
И заметьте, для  вывода указателя можно использовать специальный спецификатор вывода «p».

Получение значения переменной.

Мы можем получить значение, которое хранится по адресу, записанному в указателе. Для этого используется оператор «*». Ага, снова эта пресловутая звездочка. Догадайтесь, куда она записывается? Ага, именно туда, перед именем указателя. Вот такие чудеса творятся иногда.  Надо посмотреть на примере.

Давайте немного изменим Листинг 16.4.  Добавим переменным значения и попробуем получить их, используя указатели.

Листинг16.5
#include <stdio.h>

int main (){

      int a=3, b=0, *pi_a;    //объявляем переменную a

//и указатель на переменную типа int

      pi_a=&a; // присваиваем указателю адресс переменной а

      *pi_a=b; // записываем в память, по адресу который хранится в указателе

                   // значение переменной b

      printf ("adres peremennoi a %#xn", pi_a);

      printf ("znachenie po adresu zapisannomy v pi_a %dn", *pi_a);

return(0);

}

Рис.9. Использование указателей для обращения к значениям переменных,на которые они ссылаются

Как видите, используя запись *pi_a можно обращаться с указателем, как с переменной соответствующего типа. В нашем случае, как с переменной типа int.

Еще раз обсудим звездочку в указателях.
  • Если  звездочка стоит перед именем в объявлении переменной, то в этом случае она означает, что объявляется указатель.
  • Если звездочка встречается внутри программы, то в данном случае, она указывает на то, что мы обращаемся к ячейкам памяти, на которые ссылается указатель.
Еще раз внимательно перечитайте предыдущий пункт. Он очень важен, вам необходимо в этом разобраться. Задавайте вопросы в комментариях, если вам что-то непонятно. С этим обязательно нужно хорошо разобраться.

Есть еще один специальный указатель. Он имеет свое особое название – нулевой указатель NULL. Нулевой указатель не ссылается никуда. Он используется, чтобы обнулять указатели. Посмотрите на следующую программу.

Листинг16.6
#include <stdio.h>

int main (){

      int a=3,*pi_a;

      pi_a=&a; // присваиваем указателю адресс переменной а

      printf ("adres peremennoi a %#xn", pi_a);

      pi_a=NULL;

      printf ("adres peremennoi a %#xn", pi_a);

return(0);

}

Рис.10 Нулевой указатель

Это почти все основные действия, которые можно производить  с указателями. Есть еще одно интересное свойство, мы его коснемся чуть позже.

И под конец урока. Не зря же мы с вами так долго паримся с этими указателями сегодня. Один небольшой пример. Помните занятие про функции? Или недавнюю небольшую головоломку в группе во Вконтакте.
Кто не помнит, вот вам картинка.

Рис.11 Простенькая головоломка. Что выведет представленная программа?
Нам известно, когда мы передаем переменные в функцию, то передаются не сами переменные, а их копии? Иногда нам это вовсе не нужно. Иногда удобно сделать так, чтобы значения все-таки изменялись. Для этого, нужно передавать в функцию не переменную, а указатель на неё.

Давайте перепишем программу с картинки, чтобы она работала так, как и предполагается.

Листинг 16.7
#include <stdio.h>

void obmen (int *pi_a, int*pi_b){

//принимаем указатели на переменные типа int

      int temp;

      temp=*pi_a;

      *pi_a=*pi_b;

      *pi_b=temp;

}

int main (){

      int x=5, y=10;

      obmen(&x,&y);

// ВНИМАНИЕ!передаем адреса, так как функция obmen принимаем указатели

      printf ("Posle x=%d y=%dn",x,y);

return(0);

}

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

Подробный урок о том, зачем нужны указатели.

И это еще не все возможности указателей. Следующее занятие снова будет посвящено указателям. Их связью с массивами и строками.

Отдельного домашнего задания не будет. Хорошенько разберитесь с этим занятием. Вам должна быть тут понятна каждая строчка. Если интересно, можете потренироваться переводить числа из десятичной системы счисления в шестнадцатеричную, и обратно.  И раз уж мы учимся программировать, то напишите для этого программу. =)))

Если вам понравился этот или другие уроки, расскажите пожалуйста о них, своим друзьям Вконтакте,Facebook,Google+, используя кнопки социальных сетей расположенные ниже.

Алгоритм. Формы записи. Отличие алгоритма от программы.

Добрый день!
Сегодня мы поговорим об алгоритмах и способах их записи.
Что такое алгоритм?

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

1.   Понятность.Алгоритм должен быть понятным, человеку или устройству, которое его будет выполнять. Вспомните инструкции к технике на китайском или корейском языке.
2.   Конечность. Алгоритм должен иметь конечное число шагов. Пусть их будет два миллиона, но мы точно знаем, что выполнив все два миллиона шагов по порядку, то обязательно получим результат.
3.   Определенность.Алгоритм должен быть точным и не допускать неоднозначности в его понимании. На каждом шаге, исполнитель должен точно знать, какой шаг ему делать следующим, а не додумывать или предполагать.

Естественно есть и другие. Кому интересно, можете поискать информацию в  интернете.
Способы записи алгоритмов.
Есть несколько способов записи алгоритмов. Одним из наиболее распространенных, является словесное описание алгоритма. Студентов, иногда просят записывать свой алгоритм в виде блок-схем. Это тоже один наглядных способов записи алгоритмов. Кроме того, для записи алгоритмов, часто используют «формальные языки».
Дело в том, что большинство алгоритмических языков во много схожи между собой. Так, например, в большинстве из них, есть операция присвоения значения переменной, есть сами переменные, циклы,  управляющие конструкции и т.д. А раз так, то можно придумать некоторый формальный язык, которым будут описываться эти стандартные действия. Таким образом, они будут понятны всем программистам, пишущим на других языках. Такой формальный язык называют псевдокод.
Программа это, кстати, тоже не алгоритм. Программа, это уже конкретная реализация некоторого алгоритма.
Давайте рассмотрим уже знакомый нам пример, с поиском минимума в одномерном массиве.

Задача:  Дан массив целых чисел. Найти минимальный элемент в массиве.
Идея решения: Сравнить между собой все элементы и найти минимальный.

Алгоритм: (словесное описание)
1.   Принимаем в качестве минимального первый элемент предложенного массива.
2.   Начиная со второго, последовательно сравниваем каждый элемент с минимальным значением, пока не достигнем конца массива.
2.1. Если текущий элемент меньше минимального, принимаем его значение в качестве минимума.
2.2. В противном случае, переходим к следующей итерации.

Теперь представим блок-схему данного алгоритма. Имеем массив чисел arr[N], N – длина массива.

Рис 1. Блок-схема алгоритма поиска  

 

Все эти квадратики, кружочки и ромбики это не моя прихоть, а специальные обозначения. Существует даже специальный государственный документ, который определяет наклон линий, размеры этих фигур, подписи и т.д. (кому интересно поищите стандарт ЕСПД).  Я уже давно не занимался этим, и стандарты давно не читал, поэтому в тонкостях могу и ошибаться, но общий вид блок схемы правильный. К тому же соблюдать эти стандарты требуется только в официальной документации, студентов обычно не просят делать этого. Быть может, я еще подробнее остановлюсь на составлении блок-схем к алгоритмам, но пока рассмотрим основные элементы. 
Скругленные квадраты в начале и в конце обозначают начало и конец программы.
Овал – ввод или вывод данных.
В прямоугольнике записывают вычисления и присвоения.
Ромб – это условие, буквально оператор if-else. Из него две ветви одна выполняется, когда условие истинно, другая - когда ложно.
Шестигранник используется для обозначения цикл со счетчиком, хотя это и так понятно. После окончания цикла выполняется правая веточка этого значка (хотя я встречал и другие способы записи циклов).

Запись алгоритма на псевдокоде.
Вы можете встретить различные виды псевдокода, синтаксис некоторых может быть похож на синтаксис языка программирования Pascal. Я привожу здесь тот, что используется в задания типа ЕГЭ. 

Листинг 1.
АЛГ
НАЧ
ЦЕЛ N, min;
МАССИВ arr[N];
ЦЕЛ i;
min = arr[0];
НЦ  (i=0;i < N;i++)  
                 ЕСЛИ (arr[i]<min)  
                 ТО min=arr[i];
КЦ
ВЫВОД min;
КОН

Листинг 2.
#include <stdio.h>
int main(){
      int min, arr[11]={5,14,7,4,11,2,6,12,8,7,3};
      min = arr[0];
      for (int i=1; i<11; i++){
            if (arr[i]<min)
                  min = arr[i];
      }
      printf("%dn", min);
return(0);
}

Не правда ли очень похоже? 

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

Урок 15. Некоторые стандартные функции работы со строками.


Всем доброго дня.
Продолжаем изучать строки. Сегодня у нас праздник. Почему? Да просто мы познакомимся с новым заголовочным файлом – string.h. Нетрудно сообразить, что в нём описываются стандартные функции, предназначенные для работы со строками.  Естественно не следует забывать, что для использования этих функций необходимо подключить этот заголовочный файл. Я надеюсь вы не забыли как это делается.

Функция strlen().

В практическом задании к прошлому занятию, я просил вас написать  программу, которая вычисляет длину строки без учета нулевого элемента.  Длина строки - это один из самых важных параметров, характеризующих любую строку, и необходимый при работе со строками.  Для определения длины строки в заголовочном файле string.h описана функция strlen().
Данная функция имеет простой синтаксис. Она принимает один параметр строку символов, и возвращает в результате своей работы целое число - длину этой строки.
Листинг15.1
#include <stdio.h> 
#include <string.h> 
int main(void) { 
      char str[]="Hello world"
      int n=strlen(str); 
      printf("Dlina stroki %d simvolovn",n); 
return(0);
}
Рис.1 Программа, иллюстрирующая работу функции strlen().

Как видите ничего неожиданного, функция вернула в результате своей работы число 11.
Теперь, на основании знаний, полученных на прошлом занятии, объясните результат работы следующей программы.
Листинг15.2
#include <stdio.h> 
#include <string.h> 
int main(void) { 
      char str[100]; 
      fgets(str,100,stdin); 
      int n=strlen(str); 
      printf("Dlina stroki %d simvolovn",n);
return(0); 
}

Рис.2. Иллюстрация работы программы Листинг 15.2
Почему, когда мы ввели Hello world и нажали Enter, наша функция вернула нам число 12, а не 11 как в прошлом случае?

Функции сравнения строк.

Еще одной задачей, предложенной для решения в прошлом занятии, было написание программы, которая посимвольно сравнивает между собой две строки.  Для выполнения этой операции, тоже существует готовая функция –strcmp().
Данная функция принимает в качестве аргументов две строки, которые необходимо сравнить. В результате своей работы, если строки одинаковые, он возвращает нуль, и если разные, то либо целое число положительное или отрицательное. Сравнение идет по коду символов в таблице ASCII.
Рассмотрим пример.
Листинг15.3
#include <stdio.h> 
#include <string.h> 
int main(void) { 
       char  str1[] = "hello world",
             str2[] = "hello world"
             str3[] = "hello World"
       int n12 = strcmp(str1,str2); 
       int n13 = strcmp(str1,str3); 
       int n31 = strcmp(str3,str1); 
       printf(" %s i %s %dn", str1, str2,n12); 
       printf(" %s i %s %dn", str1, str3,n13); 
       printf(" %s i %s %dn", str3, str1,n31); 
       printf("%c/%dn",str1[6],str1[6]); 
       printf("%c/%dn",str3[6],str3[6]);
return(0); 
}
Результат работы данной программы:
Рис.3. Сравнение строк функцией strcmp()

 
Строки str1 и str2 одинаковые и потому, при их сравнении получилось 0. А вот строки str1 и str3 различаются, регистром символа ‘w’ поэтому результат их сравнения не равен нулю. Обратите внимание, что в одном случае он равен единице, а в другом минус единице. Почему так получается, мы сейчас разберемся.
Строки сравниваются посимвольно, использую соответствие между символом и его кодом в таблице ASCII.  Ниже сравнения, в нашей программе указаны коды символов ‘w’ и ‘W’.  Как видите, код символа ‘w’ –  119, больше чем код символа ‘W’ – 87. Значит и строка str1 больше чем строка str3.

Основное правило:  Если, в функции strcmp() строка записанная в первом аргументе, больше чем строка во втором аргументе, то функция возвращает положительное число.  Если меньше – отрицательное.
Есть и еще одна функция, предназначенная для сравнения строк strncmp(). Её отличие лишь в том, что добавляется третий параметр – целое число, указывающее, сколько символов с начала строки необходимо сравнить.
Немного преобразуем предыдущую программу
Листинг 15.4
#include <stdio.h> 
#include <string.h> 
int main(void) { 
       char  str1[] = "hello world"
             str2[] = "hello World"
       int n1 = strncmp(str1,str2,6); 
       int n2 = strncmp(str1,str2,7); 
       printf("%s i %s %dn", str1, str2,n1); 
       printf("%s i %s %dn", str1, str2,n2);
return(0);
}

Рис.4. Сравнение частей строк, с помощью функции strncmp().
Как видите, когда мы сравнили первые шесть символов двух строк, то получили 0, так как они совпадают. Если же мы сравниваем первые семь символов, то первая строка больше второй, и в ответе получается положительное целое число. Причем в данном случае, это разность между кодами соответствующих символов (119-87 = 32). Пользоваться этим обстоятельством нужно очень осторожно. Не факт, что в других средах разработки, эти функции реализованы точно таким же образом. Т.е. может быть, что где-то strcmp будет тоже возвращать  32, а не 1, как в нашем примере. А где-то возможно strncmp не будет возвращать 32. Как это реализовано в вашей среде программирования проверьте самостоятельно.   

Функции изменения регистра строки.

Как вы помните, в задаче к прошлому уроку требовалось написать функцию, которая переводит все символы строки в нижний регистр. Для этой задачи тоже есть готовая функция – strlwr().  Ко всему прочему имеется и функция, которая переводит все символы в верхний регистр – strupr().
Данные функции принимают один параметр – строку, которую необходимо перевести в тот или иной регистр. Рассмотрим пример их использования.
Листинг 15.5
#include <stdio.h> 
#include <string.h> 
int main(void) { 
      char str[] = "HeLLo WoRlD"
      printf ("%sn",str); 
      strlwr(str); //переводим строку в нижний регистр
      printf ("%sn",str); 
      strupr(str); //переводим строку в верхний регистр
      printf ("%sn",str);
return(0);
}
Результат работы программы
Рис.5. Изменение регистра строки функциями strupr() и strlwr().

Функции объединения строк.

В файле string.h описаны две функции для объединения строк.
Первая strcat() принимает две строки, и приписывает вторую в конец первой. Т.е. результат склеивания двух строк окажется в той, что записана в первом аргументе. Причем нулевой символ ‘’ добавляется автоматически в конец новой, получившейся строки.  Небольшой иллюстрирующий пример:
Листинг15.6
#include <stdio.h> 
#include <string.h> 
int main(void) { 
      char  str1[50] = "hello"
            str2[]="world"
      strcat(str1," "); //приклеиваем к str1 пробел получаем
                        //в str1 строку "hello "
      strcat(str1,str2);//приклеиваем к str1 строку str2 
      printf("%sn", str1); //выводим str1
return(0); 
}
Рис.6. Склеивание двух строк функцией strcat().

Вторая функция strncat() отличается от предыдущей, наличием еще одного аргумента, означающего, какое количество символов с начала второй строки следует приклеить к первой строке. Немного переделаем наш предыдущий пример.
Листинг 15.7
#include <stdio.h> 
#include <string.h> 
int main(void) { 
      char  str1[50] = "hello"
            str2[]="world"
      strcat(str1," "); //приклеиваем к str1 пробел получаем                              //в str1 строку "hello "
      strncat(str1,str2,3); //приклеиваем к str1 3 первых                                     //символа из str2

      printf("%sn", str1); //выводим str1

return(0);
}

Ну и соответственно результат её работы.
Рис.7. Использование функции strncat().


Ничего удивительного. Все работает именно так, как и ожидалось.
При работе с этими двумя функциями стоит внимательно следить за тем, чтобы в строке, к которой мы приклеиваем, хватило места, иначе произойдет ошибка переполнения. С ней мы уже сталкивались с прошлом занятии. Вот попробуйте выполнить следующую программу.
 Листинг 15.8
#include <stdio.h> 
#include <string.h> 
int main(void) { 
      char  str1[10] = "hello"
            str2[]=" world"
      strcat(str1,str2); 
      printf("%sn", str1); 
      return(0);
}

Хотя программа и скомпилируется, но при выполнении возникнет ошибка.
На этом сегодняшний урок мы закончим. Мы рассмотрели лишь некоторые функции, описанные в заголовочном файле string.h. Есть там еще несколько очень интересных и полезных функций, но о них мы поговорим чуть позже, когда познакомимся с указателями.  

Отдельного практического задания к этому уроку не будет.  Проверьте с помощью стандартных функций программы, написанные к прошлому занятию. А 3,4 и 5 задачи перепишите с использованием стандартных функций.


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

Занятие 14. Строки и символьные массивы в языке Си.


Добрый день друзья. На прошлом занятии мы разобрались с одномерными массивами. Сегодня разберемся с одним их частным случаем - символьными строками.  С ними мы уже даже сталкивались. В самом первом уроке. Помните, выводили на экран строчку Hello world. Каждый символ в этой строке это элемент одномерного массива.

Рис.1. Символьный массив

Как мы уже знаем, для хранения символов используются элементы типа  char.  Значит для хранения строки, мы можем использовать массив таких элементов. Единственным отличием такого массива, будет являться последний элемент. Компьютеру же как-то надо понимать, когда строка заканчивается? Это мы с вами видим, а у компьютера мозги кремниевые, он только нулики и единички понимает, и в них же и хранит всю информацию. Поэтому выбрали специальный символ, которым обозначается конец строки. Это символ с кодом нуль , его обозначают следующим символом –  '’. Помните, я рассказывал про то, что первые 32 значения в таблице кодов ASCII застолблены под служебные символы. Вот это пример одного из них, самого первого.  Изменим пример на картинке выше, чтобы он больше соответствовал действительности. 
Рис 2. Символьная строка
 

Объявление и инициализация строк.

Как мы уже разобрались, необходимо использовать для хранения строк массив символов. Никаких фокусов тут нет, и объявление массива символов стандартное.  
 
Листинг 14.1

char str[10];

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

Листинг14.2
#include <stdio.h> 
int main(){
      char str[17];
      printf("%sn", str);
      return(0);
}

Рис.3. Символьный массив после объявления
Как видите, для вывода строк можно использовать стандартную функцию printf(), для которой предусмотрен специальный спецификатор %s. В остальном никаких отличий здесь нет.

Теперь разберемся, как присвоить значение строке. Есть несколько способов. 

  • Как и любая переменная, строка может быть инициализирована (т.е. ей присвоено некоторое значение) непосредственно при её объявлении.
Листинг14.3
#include <stdio.h> 
int main(){
      char str[17]="Hello world";
      printf("%sn", str);
      return(0);
}

При таком объявлении и инициализации,  все свободные символы, так и останутся заполненными мусором либо обнулятся. Тут в зависимости от компилятора. Вы можете это проверить самостоятельно. Это будет первым заданием для самостоятельного выполнения к этому уроку.
  • Мы можем не задавать при инициализации размер массива.

Листинг 14.4
#include <stdio.h> 
int main(){
      char str[]="Hello world";
      printf("%sn", str);
      return(0); 
}

В этом случае, компилятор сам посчитает количество символов для хранения строки в кавычках, и сам учтет заключительный символ ‘’. В данном случае массив strбудет из 12 элементов.

Кстати, посмотрите на следующую программу.  Она наглядно иллюстрирует, что массив символов, без завершающего ‘’  это не строка.

Листинг 14.5
#include <stdio.h> 
int main(){
      char str[5];
      str[0]='a';
      str[2]='b';
      printf("%sn", str);
      return(0); 
}

Результат работы этой программы представлен на следующем рисунке.
Рис. 4. Вывод на экран символьного массива без нулевого элемента в конце

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

Ввод и вывод строк.

Теперь рассмотрим, как можно ввести  строку символов с клавиатуры.
Возникает естественное желание воспользоваться стандартной функцией scanf(). Попробуем.

Листинг14.6
#include <stdio.h> 
int main(){
      char str[17];
      scanf("%s", str);
      printf("%sn",str);
      return(0); 
}

Выполним эту программу. Введем, уже привычное нам, Hello world.
Результат работы программы на следующем рисунке.

Рис.5 Ввод строки с использованием функции scanf().

Как видите, в строку сохранилось лишь первое слово. Все дело в реализации функции scanf. Она считает пробел разделителем. Получается, таким способом мы можем считать лишь строку, которая не содержит пробелов, т.е. мы можем считать одно слово.
Кстати, вы обратили внимание, что я не поставил перед именем массива знак &, чтобы получить его адрес?  Это не ошибка. Просто имя массива, без указания номера элемента в квадратных скобках, ссылается на адрес первого элемента массива. Т.е. нам тут не нужно получать адрес, он и так у нас есть.  Это касается не только символьных массивов, а любых. Но пока это не особо важно. С этим мы столкнемся, когда будем изучать указатели.   
Вернемся к нашим баранам, как говорится.  Мы хотели считать строку. Раз scanf() надежд возложенных на неё не оправдала, должна быть какая-то другая функция. И она, конечно же, есть. 

Функция gets().

Мы снова не будем углубляться в развернутый синтаксис, кому интересно, тот может подробно посмотреть в любом справочнике.
Функция getsпринимает в качестве своего аргумента массив символов, в который она и записывает считываемую из стандартного потока ввода строку. Концом ввода строки является  символ перенос строки ‘n’, т.е. когда мы нажмем Enter на клавиатуре.   Этот символ последним считывается и при записи в массив заменяется  символом конца строки ’’.
Следующая программа читает введенную строку и выводит её на экран.

Листинг 14.7
#include <stdio.h> 
int main(){
      char str[17];
      gets(str);
      printf("%sn",str);
      return(0); 
}

Результат работы программы, на следующем рисунке.
Рис. 6. Ввод строки с использованием функции gets()
Как видите, мы избавились от нашей проблемы. Но есть более важная проблема. Когда мы предоставляем пользователю вводить строку, мы не знаем, сколько он символов введет. Может так случиться, что мы объявили массив на 10 элементов, а пользователь ввел 30. Тогда возникнет ошибка – переполнение буфера. Поэтому использовать эту функцию использовать нужно очень осторожно, либо не использовать вообще. А что же тогда использовать?

Функция fgets().
 Сразу рассмотрим пример её использования.

Листинг 14.8
#include <stdio.h>
int main(){
      char str[10];
      fgets(str,10,stdin);
      printf("%sn",str);
      return(0); 
}

Функция принимает три аргумента.
  1. Массив символов, в который необходимо записать  вводимую строку.
  2. Количество символов, которые может считать функция с учетом символа конца строки. В нашем случае это 10, т.е. рабочих из них девять, и один зарезервирован для конца строки.
  3. Откуда читать данные. В нашем случае указан стандартный поток ввода.
Попробуем в нашей программе Листинг 14.8 ввести нашу строку Hello world. В ней 12 символов.
Результат выполнения.
Рис.7. Ввод с помощью функции fgets().
Как видите, даже если мы введем больше, то функция считает только определенное ей количество символов. Не больше.
Есть еще одно отличие. Функция gets() глотала наш перенос строки, превращая его в символ конца строки. А вот функция fgets() его не «проглатывает». Введем, какую-нибудь строку меньше 10 символов. И посмотрим что будет.
Рис.8. Иллюстрация особенностей ввода функции fgets().
Как видите, получилось два переноса строки. Один из самой строки, другой из-за формат строки вывода.  
С вводом разобрались. Теперь поколдуем над выводом. Кроме стандартного printf() есть еще несколько функций. По аналогии с функциями ввода.

Функции puts(), fputs().

Синтаксис будет понятен из следующего примера

Листинг 14.9
#include <stdio.h> 
int main(){
      char str[12];
      fgets(str,12,stdin);
      puts(str);
      fputs(str, stdout);
      return(0); 
}

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

Рис.9. Использование стандартных функций вывода строки puts(), fputs(). Особенности вывода.
Как видите, функция puts является обратной к функции gets(). Она выводит строку заменяя символ конца строки на символ переноса строки. И поэтому то, что выводи функция fputs() оказывается на новой строке. А вот функция fputs()дополнительного переноса строки не делает. 

На этом на сегодня всё. На следующем уроке будем разбирать стандартные функции, которые работают со строками.

Задания для самостоятельной практической работы.

  •  На вход программе подается строка, длинной не более 100 символов. Напишите программу, которая определяет длину этой строки, без учета нулевого символа.
  • Напишите программу, которая переводит данную строку в нижний регистр. Т.е. и строки "Hello WorlD" должна получиться строка "hello world"
  • На вход программе подается строка, длинной не более 100 символов. Определить, является ли она перевертышем. Например, как слово "шалаш" или "топот". Учтите что строка может содержать пробелы, их учитывать не нужно. Например, строка "А роза упала на лапу Азора" будет являться перевертышем.
  • Пользователь вводит две строки. Необходимо сравнить их между собой, и вывести yes если строки полностью совпадают, или no в противном случае. Регистр учитывать не нужно. Т.е строки "Hello WorlD" и "hello world" считаются двумя одинаковыми строками.
  • Напишите программу, которая читает из файла строку, длинной не более 200 символов. И считает количество вхождения в строку всех используемых в ней символов,без учета регистра. На вход поступает строка состоящая из букв латинского алфавита и пробелов. Программа должна вывести в первой строке длину введенной строки. В следующих строках встречаемые символы и их количество. Например, для строки "hello world", вывод будет следующим.
11
d - 1
e - 1

h - 1
l - 3
o - 2
r - 1
w - 1
probel - 1


  • Капитан Флинт зарыл клад на Острове сокровищ. Есть описание, как найти клад. Описание состоит из строк вида: "North 5", где первое слово – одно из "North", "South", "East", "West", а второе целое число – количество шагов, необходимое пройти в этом направлении. Напишите программу, которая по описанию пути к кладу определяет точные координаты клада, считая, что начало координат находится в начале пути, ось OX направлена на восток, ось OY – на север. На вход подается последовательность строк указанного формата. На выходе программа должна вывести координаты клада – два целых числа через пробел
Например:
Вход:

North 5
East 3
South 1
Выход: 3 4.

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