АЙТИШНЫЙ САЙТ ПЕТРА СЕМИЛЕТОВА

ПЕТР СЕМИЛЕТОВ

ПРОСТО С++: УРОК 13

Указатели и строки

До сих пор мы пользовались строками, принимая их как есть. Мы просто писали тип переменной std::string, а как он работает, что такое текст вообще - оставалось за кадром. Пришло время разобраться.

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

Что такое символы в представлении компьютера? Очевидно, это числа. Которые хранятся в отдельных переменных или ячейках массива. Но что именно будет храниться в таких ячейках?

Какие именно числа? Коды символов, букв.

Зачем и как это работает?

Упрощенно так. Когда программе надо вывести на экран строку, программа перебирает, по одному, элементы массива, содержащего в себе строку. В каждом элементе находится число с кодом, соответствующим определенной букве.

Например латинская большая “A” имеет код 65, “B” - 66, и так далее. В программе также есть эдакая внутренняя таблица, массив с картинками, изображениями букв. Картинки берутся из файла со шрифтом. И вот программа берет код буквы А - 65 - и по этому коду берет из массива с изображениями букв нужную картинку, а затем показывает ее на экране.

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

А вот как ХРАНИТЬ символы в строках, какие строки, какого типа и в какой кодировке - использовать - выбор программиста.

Кодировка - это таблица соответствий кодов и букв. Исторически сложилось, что поначалу популярность набрала кодировка ASCII - это сокращение от American Standard Code for Information Interchange. Каждый символ имеет свой номер - буквы, числа, знаки препинания, математические операции - всё пронумеровано. Коды для больших и маленьких букв различны. В кодировку ASCII помещался латинский алфавит - фактически два его набора, с большими и маленькими вариантами букв, и прочие символы - как упомянутые ранее, так и например знак доллара, копирайта и некоторые другие. Эта кодировка была размером в 128 символов, чего явно недостаточно, чтобы вместить еще и другие алфавиты, ту же кириллицу.

Поэтому возникло расширение кодировки ASCII, ANSI (American National Standards Institute), уже на 255 символов, где первые 128 отводились под ту же старую добрую ASCII, а остальные номера до 255 - под символы какого-нибудь другого алфавита, и таким образом возникли разные варианты ANSI, например, известная всем пользователям Windows кодировка Windows 1251, она же CP 1251. CP это сокращение от code page, кодовая страница. А в Европе была популярна Windows-1252, отражающая особенности немецкого, испанского и французского языков.

В CP 1251 русская заглавная “А” имеет код 192, “Б” - 193, и так далее. Маленькая “а” - 224, “б” - 225, “в” - 226. То есть код каждой маленькой буквы на 32 номера больше соответствующей ей большой. Таким образом прибавив к коду 32, мы получим из большой буквы маленькую, а отняв от маленькой 32 - превратим ее код в код той же буквы, но большой.

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

Разработчикам этой кодировки пришло в голову не размещать весь алфавит последовательно, а некоторые буквы поместить в части таблицы ПЕРЕД алфавитом. Так, заглавная “Ё” стоит в ней прежде “А” и имеет код 168, маленькая “ё” - код 184. В той же области номеров нашлось место и украинской “е”, “i” и украинской твердой “ґ”. Я не знаю, почему так случилось, может разработчики просто сначала забыли некоторые буквы, а потом вспомнили и задним числом добавляли уже в область кодов до так сказать основного алфавита.

Были и другие кодировки для русского языка, DOS 866 - популярная в системе MS DOS, и KOI8-R, весьма распространенная на заре становления Линукса и в ранней электронной почте. Однако вы можете прочесть об этом сами, я же побегу дальше.

Итак, кодировка - это договоренность, определенный стандарт, где расписано, каким символам какие номера соответствуют. Или, если угодно, определенная нумерация букв. Можно пронумеровать так, а можно эдак. Чтобы не было разнобоя, держимся какого-то стандарта.

С некоторых пор восьмибитных кодировок - а диапазон чисел кодировки вроде CP 1251 именно такой - стало не хватать. Что, если хочется использовать в одном тексте русский, английский и греческий? В кодировочной таблице не хватит места!

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

В Юникоде нашлось место не только всем мыслимым алфавитам, но и различным специальным символам, смайликам и тому подобным широко используемым пиктограммам - карточные масти, знаки зодиака и тому подобное. Главное, чтобы их картинки еще были в шрифте, которым вы хотите отобразить текст в кодировке Юникода, иначе вы увидите вместо них квадратики.

Юникод, однако, определяет таблицу соответствий кодов и символов, однако не определяет как именно коды представлены в программе. Определение задается в стандартах, называемых UTF-8, UTF-16, UTF-32.

Я не буду сейчас пускаться в подробности. Наиболее популярен UTF-8, а наиболее прост для программиста - UTF-16.

Отмечу, что первые 128 кодов Юникода повторяют 128 кодов ASCII.

Предшественником языка программирования С++ является язык Си. Он кстати еще широко используется в программировании, на нем написана даже система Linux!

В си, единицей хранения кода Си по умолчанию является переменная типа char - чар. Вы можете услышать также именование “кэр”, что неправильно - произношение “кэр” возникло, кажется, еще в Советском Союзе, когда программирование учили по письменному, по книжкам, не у кого было услышать, как этот тип произносят англоязычные программисты. И зная, что char произошло от character, наши стали говорить сокращенно - кэр, что не лишено логики, но противоречит истинному произношению - чар.

Тип char, напомню, может быть беззнаковым и знаковым, signed и unsigned. В первом случае его диапазон равен -128..127, во втором от 0 до 255. По умолчанию char знаковый, поэтому в нем с грехом пополам умещались символы кодировки ASCII - латинский алфавит и некоторые другие символы.

Пример переменной типа char:

char symbol;

Как присвоить ей значение?

symbol = 'a';

Надо просто присвоить некую букву, заключив ее в одинарные кавычки. Одинарные кавычки - для одинарных символов. Двойные кавычки - для строк. Таковы правила Си и С++.

Можно написать и так, напрямую указав код символа:

symbol = 65;

Массивы из элементов типа char называют си-строками, они в С++ считаются старорежимными, но всё еще широко используются.

Пример такой строки:

  char a[6] = {"hello"};

Почему мы объявили массив из 6 элементов, если в слове “hello” всего 5 букв? А попробуйте объявить размер массива 5. Вы получите сообщение об ошибке.

После знака равенства мы инициализируем массив, присваиваем его элементам значение “hello”. При этом компилятор автоматически добавит в конец такой строки ноль. Не символ “0”, а число 0. Оно обозначает, для строки, ее конец. Поэтому нам нужно предусмотреть место для этого последнего элемента, где будет ноль.

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

Есть целый набор функций для работы со строками языка Си, они разбросаны в разных стандартных библиотечных файлах, таких как string.h и stdio.h. Чтобы подключить их, надо написать:

#include <string.h>
#include <stdio.h>  

Например, функция strlen, которой передается в параметре строка, возвращает длину строки. Делает она это просто - включает счетчик и начинает искать в строке искомый ноль. Где найдет, там возвращает текущее значение счетчика.

А вот функция printf, которая выводит строку на консоль, подобно объекту cout.

Например:
  char a[6] = {"hello"};
  printf (a);  

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

  char a[13] = {"привет"};

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

Пример 13-01.cpp:

#include <stdio.h>

int main (int argc, char *argv[])
{
  char a[12] = {"hello\n"};
  char *p = a;

  printf (p); 
  
  return 0;
}

Это пример примечателен тем, что написан он на языке Си, а не С++. Вернее, он написан на той общей основе, что лежит в обоих языках. Пример этот можно откомпилировать как компилятором C++, так и компилятором обычного Си, для чего вместо “g++” надо указать компилятор “gcc”.

Для компиляции компилятором C++ даем команду:

g++ 13-01.cpp

Для компиляции компилятором языка Си даем команду:

gcc 13-01.cpp

Разница в исходнике именно здесь невелика - для вывода на консоль мы используем не объект cout из библиотечного файла iostream библиотеки С++, а функцию print из stdio.h, которая присуща не только Си, но и досталась в наследие С++.

Также мы добавили интересную штуку в строку hello - “\n”. Это особый символ “new line”, новая линия, указывающий, что мы хотим после вывода этой строки перевести курсор на новую линию, как бы программно нажимаем Энтер. То же, как если бы мы посылали на cout перевод строки endl.

Что же происходит в нашей программе?

Вот мы объявляем обычный массив а, размером в 12 элементов, и инициализируем его значением “hello\n”. Это значит, что слово hello будет содержаться в ячейках массива с нулевой по четвертую, а в пятую попадает “\n”.

  char a[12] = {"hello\n"};

Мы объявляем указатель - на английском указатель pointer, поэтому часто используют имя переменной p (английская буква “пи”):

char *p = a;

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

Проверим, что теперь будет в p, выведем его на экран:

  printf (p); 

Программа отобразит слово “hello”, потому что указатель p указывает на строковой массив a.

Теперь посмотрим, какая выгода от этого может быть.

Пример 13-02.cpp:

#include <stdio.h>

int main (int argc, char *argv[])
{
  char a[12] = {"hello\n"};
  char b[12] = {"world\n"};

  char *p;

  p = a;
  printf (p);

  p = b;
  printf (p);
  
  return 0;
}

Объявили два массива, a и b. В одном слово “hello”, в другом “world”. Объявили указатель p.

Сначала присваиваем указателю p адрес массива a. Выводим указатель на консоль. Пишется слово “hello”, которое содержится в массиве a. Потому, что указатель p указывает сейчас на массив a.

Затем присваиваем указателю p адрес массива b. Снова выводим указатель на консоль. Теперь пишется слово world. Потому, что оно содержится в массиве b, а указатель p указывает сейчас на массив b.

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

Например, мы, чтобы вывести с ее помощью некое число, а хоть бы 13, не можем написать:

printf (13);

Вместо этого надо будет написать:

  printf ("%i\n", 13);

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

То же самое с cout выглядит так:

cout << 13 << endl;

Мы снова сталкиваемся с грозным призраком объектно-ориентированного программирования.

Ощутите разницу подходов. Следующий пример 13-03.cpp:

#include <stdio.h>
#include <string.h>

int main (int argc, char *argv[])
{
  char a[21] = {"hello"};
  char b[6] = {"world"};

  strcat (a, b);

  printf (a);
  printf ("\n");

  return 0;
}

являет старый, сишный подход. В заголовочном файле string.h есть функция strcat, которая складывает две строки-параметра, записывая вторую в конец первой. Поэтому первая должна быть массивом достаточного размера, чтобы уместить обе строки. Заголовок функции strcat таков:

char *strcat (char *str1, const char *str2)

Это значит, что мы передаем ей указатели на строки. Второй параметр, str2 предварен словом const, что подсказывает программисту - второй параметр, хотя и указатель, изменен не будет. Функция возвращает то же, что внутри функции получится в str1. То есть в str1 допишется str2, а вдобавок после этого str1 еще и выдается функцией как результат ее выполнения.

Мы складываем строки a и b:

  strcat (a, b);

Печатаем строку a - получится слитное слово “helloworld”

  printf (a);

И переводим курсор на новую строку

  printf ("\n");

Итак, это было решение задачи в духе языка Си. Как это делается на С++? Очень изящно, при помощи объектов-переменных типа string:

#include <iostream>

using namespace std;

int main (int argc, char *argv[])
{
  string a = "hello";
  string b = "world";

  a = a + b;

  cout <<  a << endl;
  
  return 0;
}

Вместо вызова функции для склеивания строк, мы просто прибавляем одну строку к другой. За этой простотой и стоят мощные механизмы объектно-ориентированного программирования.

Но это будет уже в новой части нашего цикла. Под конец практический пример, зачем нужно склеивание строк.

Пример 13-05.cpp:

#include <iostream>

using namespace std;

int main (int argc, char *argv[])
{
  
  string name;

  cout << "Введите имя: "; 
  cin >> name;

  string lozung1 = "Голосуй за " + name + "!";
  string lozung2 = name + " в президенты!";

  cout <<  lozung1  << endl;
  cout <<  lozung2  << endl;
  
  return 0;
}

Программа просит ввести имя, оно записывается в переменную name, затем на основе этого имени мы формирует два лозунга, в переменных lozung1 и lozung2, а затем выводим их на консоль.

Если пользователь введет ругательства, получится смешно.

Поддержать курс:

Навигация:

Оглавление

Предыдущий урок