Закінчуємо збирати власний printf

Частина 1

Частина 2

В заключній частині напишемо обробку останніх чотирьох прапорців. Почнемо з найпростішого - %u. В минулій частині ми описали обробку типу int, тепер наше завдання трохи спрощене, тому що нам не потрібно обробляти негативні значення. %u використовується для форматування змінних типу unsigned int, але використовувати конвертер для типу int ми не можемо, оскільки ми можемо отримати неочікувані результати. Спочатку створимо просту програму, котра виведе діапазони значень з котрими нам доведеться працювати:

#include <stdio.h>
// бібліолтека котра містить в собі діапазони для всіх стандартних типів
#include <limits.h>

int main(void){

    printf("Maximum Integer Value: %d\n", INT_MAX);
    printf("Minimum Integer Value: %d\n", INT_MIN);
    printf("Maximum Unsigned Integer Value: %u\n", UINT_MAX);

	return 0;
}

Після компіляції отримаємо:

Maximum Integer Value: 2147483647
Minimum Integer Value: -2147483648
Maximum Unsigned Integer Value: 4294967295

Unsigned int зберігає лише позитивні значення, тому мінімальне значення для цього типу - це 0. Тому, якщо ми будемо оперувати позитивними значеннями в межах від 0 до 2147483647, то все буде працювати нормально. Однак, коли ми вийдемо за ці межі, відбудеться дещо цікаве. Спробуємо обробити різні значення змінних типу unsigned int за допомогою нашої програми:

...
int main() {
		unsigned int a = 0;
		unsigned int b = 2147483647;
		unsigned int c = 2147483648;
		unsigned int d = 4294967295;


		myprintf("0 == %d\n", a);
		myprintf("2147483647 == %d\n", b);
		myprintf("2147483648 == %d\n", c);
		myprintf("4294967295 == %d\n", d);	


		return 0;
}

Отримаємо:

0 == 0
2147483647 == 2147483647
2147483648 == -2147483648
4294967295 == -1

Зовсім не те на що ми очікували. Для охочих дізнатися, чому так відбувається варто познайомитись з тим в якому виді представляються значення типу int (посилання).

Напишемо функції для обробки прапорця %u використовуючи аналогічні функції, що ми написали раніше для прапорця %d:

int uint_len(unsigned int n){

	int	count = 1;

	while (n > 9) {
		count++;
		n /= 10;
	}

	return count;
}


char *uint_toa(unsigned int n){
	int n_len;
	char *result;

	n_len = uint_len(n);
	result = malloc((n_len + 1) * sizeof(char));
	if (!result)
		return (NULL);
	result[n_len] = '\0';
	while (--n_len >= 0)
	{
		result[n_len] = (n % 10) + '0';
		n /= 10;
	}
	return (result);
}


int printu(unsigned int n){
	char *number;
	int result = 0;
	
	number = uint_toa(n);
	printstring(number);
	result = str_len(number);
	free(number);
	return (result);
}

// не забуваєсо додати новий прапорець 'u'
int	handle_flag(va_list args, char flag)
{
    if (flag == 'u')
	return (printu(va_arg(args, unsigned int)));
    if (flag == 'd' || flag == 'i')
        return (printdecimal(va_arg(args, int)));
    if (flag == 'c')
	return (printchar((char)va_arg(args, int)));
    if (flag == 's')
	return (printstring(va_arg(args, char *)));
    if (flag == '%')
	return (printchar('%'));
    return (0);
}

Скомпілюємо попередній приклад використовуючи %u:

int main() {
		unsigned int a = 0;
		unsigned int b = 2147483647;
		unsigned int c = 2147483648;
		unsigned int d = 4294967295;


		myprintf("0 == %u\n", a);
		myprintf("2147483647 == %u\n", b);
		myprintf("2147483648 == %u\n", c);
		myprintf("4294967295 == %u\n", d);	

    
    return 0;
}

Отримаємо:

0 == 0
2147483647 == 2147483647
2147483648 == 2147483648
4294967295 == 4294967295

Принаймні для тестових значень змінних отримали вірний результат.

Перейдемо до наступного прапорця - %x (%X). Цей прапорець відрізняється від %u тим, що нам потрібно представити число у шістнадцятковому форматі і в залежності від регістру виводити великі (%X) або малі (%x) літери в результаті. Ми також будемо працювати з цілими позитивними числами, але розширимо діапазон до unsigned long, щоб мати підтримку значень від 0 до 2^64-1 (це нам знадобиться пізніше). Використовуючи функції обробки %u як основу, напишемо аналогічні функції для %x:

int uhex_len(unsigned long n)
{
	int count = 1;
	
	while (n > 15) {
		count++;
		n /= 16;
	}
	return count;
}

// base використовуєтьс для форматування регістру літер
char *uhex_toa(unsigned long n, char base)
{
	int tmp;
	int n_len;
	char *result;

	n_len = uhex_len(n);
	result = (char *)malloc((n_len + 1) * sizeof(char));
	result[n_len] = '\0';
	while (--n_len >= 0)
	{
		tmp = (n % 16);
		if (tmp > 9)
			result[n_len] = base + tmp - 10;
		else
			result[n_len] = tmp + '0';
		n /= 16;
	}
	return result;
}

int printhex(unsigned long n, char base)
{
	char *number;
	int	 result;

	result = 0;
	number = uhex_toa(n, base);
	printstring(number);
	result = str_len(number);
	free(number);

	return result;
}

Додамо дві нові умови до обробника прапорців:

...
  if (flag == 'x')
    return (printhex(va_arg(args, unsigned long), 'a'));
  if (flag == 'X')
    return (printhex(va_arg(args, unsigned long), 'A'));
...

Випробуємо наші функції на попередніх прикладах:

int main() {
  unsigned long a = 0;
  unsigned long b = 2147483647;
  unsigned long c = 2147483648;
  unsigned long d = 4294967295;
  
  		
  myprintf("%x\n", a);
  myprintf("%x\n", b);
  myprintf("%x\n", c);
  myprintf("%x\n", d);
  		
  myprintf("%X\n", a);
  myprintf("%X\n", b);
  myprintf("%X\n", c);
  myprintf("%X\n", d);
    
  return 0;
}

/*
0
7fffffff
80000000
ffffffff
0
7FFFFFFF
80000000
FFFFFFFF
*/

Отже, ми підходимо до фінішу, і залишився лише один прапорець - %p, який використовується для виводу вказівників. Вказівники виводяться у форматі шістнадцяткових чисел з префіксом "0x". Ми вже створили функцію для конвертації десяткових чисел у шістнадцятковий формат, тому залишилося лише створити функцію, яка буде додавати префікс "0x". Вказівник може вказувати на практично будь-яку комірку пам'яті. З огляду на те, що більшість сучасних процесорів мають 64-бітну архітектуру, на попередньому кроці, ми розширили діапазон оброблюваних значень до unsigned long. Отримаємо:

int printpointer(unsigned long n)
{
  char *number;
  int result = 0;
  
  // використовуємо раніше створені функції
  number = uhex_toa(n, 'a');
  // виводимо префікс
  printstring("0x");
  printstring(number);
  result = str_len(number) + 2;
  free(number);
	
  return result;
}

Залишилась лише невелика деталь - обробка випадків, коли вказівник вказує на звільнену комірку пам'яті або вказівник ще не має жодного значення. Додамо обробку для NULL-вказівників і оновимо функцію printPointer:

int printpointer(unsigned long n)
{
  char *number;
  int result;

  result = 0;
  if (!n) {
    // наша функція повинна відтворювати поведінку
    // printf, саму тому для NULL вказівників ми виводмио (nil)
    write(1, "(nil)", 5);
    result = 5;
  }
  else {
    number = uhex_toa(n, 'a');
    printstring("0x");
    printstring(number);
    result = str_len(number) + 2;
    free(number);
  }
  return result;
}

В раніше створених функціях нам залишилось ще одне місце куди слід додати обробку NULL-вказівників, а саме функція printstring:

int printstring(char *str){
    int len  = 0;
    
    if (!str) {
	write(1, "(null)", 6);
	len = 6;
    }
    else {
        len = str_len(str);
        write(1, str, len);
    }
    return len;
}

Додамо нову умову для обробника прапорців:

...
    if (flag == 'p')
	return (printpointer(va_arg(args, unsigned long)));
...

Протестуємо роботу функції:

int main() {
  unsigned long a = 0;
  unsigned long b = 2147483647;
  unsigned long c = 2147483648;
  unsigned long d = 4294967295;


		
  myprintf("%p\n", a);
  myprintf("%p\n", b);
  myprintf("%p\n", c);
  myprintf("%p\n", d);
  // подивимось де в пам'яті знаходяться наші змінні
  myprintf("%p\n", &a);
  myprintf("%p\n", &b);
  myprintf("%p\n", &c);
  myprintf("%p\n", &d);
  return 0;
}
/*
(nil)
0x7fffffff
0x80000000
0xffffffff
0x7ffc3a970228
0x7ffc3a970230
0x7ffc3a970238
0x7ffc3a970240
*/

Таким чином, нам вдалося створити аналог printf, познайомитись ближче з деякими стандартними бібліотеками C і роботою операційної системи. Однак, є багато місць для подальшого покращення. Ось декілька ідей:

  1. Розбиття функцій на окремі файли.

  2. Створення make-файлу.

  3. Перевірка попереджень компілятора.

  4. Перевірка пам'яті за допомогою Valgrind.

  5. Написання тестів.

Код можна знайти за посиланням, дякую за увагу.

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
O P
O P@cr1m3s

226Прочитань
4Автори
9Читачі
На Друкарні з 30 квітня

Більше від автора

Вам також сподобається

  • Види (дистрибутиви) Linux та який обрати

    Linux дистрибутиви бувають різні і у кожної своя задача. Деякі з них є спеціалізованими, інші розраховані на звичайного користувача. Проблема в тому що їх багато і обрати щось серед них складно. Ми спробуємо розповісти про основні типи дистрибутивів Linux та який краще обрати.

    Теми цього довгочиту:

    Linux
  • Утиліта duf

    duf — проста утиліта для зручного показу зайнятого та використаного місця на дисках.

    Теми цього довгочиту:

    Лінукс
  • Як встановити FTP-сервер?

    Розбираємо як встановити свій власний FTP-сервер та як його налаштувати, щоб усе працювало

    Теми цього довгочиту:

    Ftp

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається

  • Види (дистрибутиви) Linux та який обрати

    Linux дистрибутиви бувають різні і у кожної своя задача. Деякі з них є спеціалізованими, інші розраховані на звичайного користувача. Проблема в тому що їх багато і обрати щось серед них складно. Ми спробуємо розповісти про основні типи дистрибутивів Linux та який краще обрати.

    Теми цього довгочиту:

    Linux
  • Утиліта duf

    duf — проста утиліта для зручного показу зайнятого та використаного місця на дисках.

    Теми цього довгочиту:

    Лінукс
  • Як встановити FTP-сервер?

    Розбираємо як встановити свій власний FTP-сервер та як його налаштувати, щоб усе працювало

    Теми цього довгочиту:

    Ftp