Друкарня від WE.UA

Закінчуємо збирати власний 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

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

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

Це також може зацікавити:

  • Linux kernel очищується від росіян

    Останній тиждень спільноту Linux штормить від новини про несподіване усунення від доступу до розробки ядра одразу одинадцяти розробників. Ця скандальна подія розпалила спільноту не на жарт.

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

    Linux
  • Damn Small Linux - диструбитив який повертає життя старим комп’ютерам

    DSL 2024 відродився як компактний дистрибутив Linux, спеціально призначений для комп'ютерів з низькими характеристиками x86. Він вміщує багато програм у маленький пакет. Усі програми вибрані за їх функціональність, невеликий розмір та низькі вимоги до залежностей.

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

    Linux
  • Командний рядок у деяких відомих програмах

    Від початку комп’ютерної ери взаємодія з програмами відбувалась через введення команд з клавіатури. Згодом всі головні програми перейшли на графічний інтерфейс, але дещо й залишилось

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

    Linux

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

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

Це також може зацікавити:

  • Linux kernel очищується від росіян

    Останній тиждень спільноту Linux штормить від новини про несподіване усунення від доступу до розробки ядра одразу одинадцяти розробників. Ця скандальна подія розпалила спільноту не на жарт.

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

    Linux
  • Damn Small Linux - диструбитив який повертає життя старим комп’ютерам

    DSL 2024 відродився як компактний дистрибутив Linux, спеціально призначений для комп'ютерів з низькими характеристиками x86. Він вміщує багато програм у маленький пакет. Усі програми вибрані за їх функціональність, невеликий розмір та низькі вимоги до залежностей.

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

    Linux
  • Командний рядок у деяких відомих програмах

    Від початку комп’ютерної ери взаємодія з програмами відбувалась через введення команд з клавіатури. Згодом всі головні програми перейшли на графічний інтерфейс, але дещо й залишилось

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

    Linux