Продовжуємо збирати велосипед або писати власний printf

У попередній частині ми створили основу для власної імплементації функції printf, використовуючи функцію write з бібліотеки unistd.h. Зараз ми спробуємо реалізувати обробку прапорців %i та %d, які використовуються для виводу значень типу int. Ці прапорці мають однакову поведінку в функції printf, а відмінність між ними проявляється лише при використанні їх у функції scanf (детальніше).

Почнемо з простого і використаємо функцію write для виводу значення змінної типу int:

#include <unistd.h>

int main(void){

	int a  = 123;

	write(1, &a, sizeof(a));

	return 0;
}

Скомпілюємо і отримаємо:

[cr1m3s@pc:~]$ gcc foo.c
[cr1m3s@pc:~]$ ./a.out
{

Ми отримали трохи дивний результат "{". Це не є помилкою функції і не просто випадковим значенням з пам'яті. Давайте відкриємо таблицю ASCII і перевіримо:

│ Oct   Dec   Hex   Char
...
│ 173   123   7B    {

Отже, просто так вивести значення типу int у нас не вийде. Для обробки цілочисельних значень нам потрібно написати власний конвертор типу int до типу char або char*.

Давайте почнемо з простого перетворення числа на літеру. Знову звернемось до таблиці ASCII:

       Oct   Dec   Hex   Char
       060   48    30    0                   
       061   49    31    1                       
       062   50    32    2                       
       063   51    33    3                      
       064   52    34    4                     
       065   53    35    5                       
       066   54    36    6                     
       067   55    37    7                      
       070   56    38    8                       
       071   57    39    9   

Тобто щоб вивести числа нам потрібні значення int починаючи від 48 до 57.

#include <unistd.h>

int main(void){

	int a  = 48;
	while (a < 58) {
		write(1, &a, sizeof(a));
		a++;
	}

	return 0;
}

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

[cr1m3s@pc:~]$ gcc foo.c
[cr1m3s@pc:~]$ ./a.out
0123456789

Отже, щоб отримати представлення числа типу int у форматі char, достатньо до значення int додати 48. Напишемо просту функцію для цього:

int write_int(int a){

	if (a < 0 || a > 9)
		return -1;

	// тип char в С зберігає значення в діапазоні від -128 до 127
        // тому ми можемо проводити математичні операції між int та char
        a += '0';        
        write(1, &a, sizeof(a));

	return 0;
}

Це непоганий початок, але таким чином ми можемо виводити лише числа в діапазоні від 0 до 9. Для перетворення більших (або менших:)) чисел нам потрібна значно складніша функція.

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

int int_len(int a){

	int result = 0;
	int n = a;

	if (n == 0)
		return 1;

	if (n < 0)
		result++;

	while (n != 0){
		result++;
		n /= 10;
	}

	return result;
}

Знаючи кількість символів в цілочисленній змінній, ми можемо створити стрічку використовуючи malloc (не забуважємо підключити бібліотеку stdlib.h).

char* int_toa(int a){

	int num;
	int sign;
	int i_len;
	char* result;

	i_len = int_len(a);
        // не забуваємо про '\0' на кінці стрічки
	result = malloc((i_len + 1) * sizeof(char));
        // sign виступає як костиль
        // можна було б просто помножити негативне значення на -1
        // але ми можемо прийти до переповнення
        // INT_MAX --> 2147483647
        // INT_MIN --> -2147483648
        // INT_MIN * -1 > INT_MAX
	sign = 1;
	num = a;

	if (!result)
		return NULL;

	if (num < 0)
		sign = -1;
        // починаємо з кінця
	result[i_len] = '\0';

	while (--i_len >= 0){
                // використовуємо sign щоб отримувати коректне значення
                // тим самим уникаємо використання abs 
		result[i_len] = (num % 10) * sign + '0';
		num /= 10;
	}

	if (sign == -1)
		result[0] = '-';

	return result;
}

Тепер потрібно написати функцію котра буде власно друкувати конвертоване значення у вигляді стрічки і повертати кількість надрукованих символів.

int printdecimal(int n)
{
	char *number;
	int result;

	number = int_toa(n);
        // використаємо функцію printstring написану в минулій частині
	result = printstring(number);
        // не забуваємо звільнити пам'ять після використання
	free(number);

	return (result);
}

Додамо нові прапорці до функції handle_flag:

int handle_flag(va_list args, char flag)
{
        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);
}

Напишемо невеличкий тест:

int main() {
    char * str = "%cello %s!%%\n";
    myprintf(str, 'H', "world");
 
    int a = myprintf("%d", -2147483648);
    myprintf("\n");
    int b = myprintf("%d", 0);
    myprintf("\n");
    int c = myprintf("%d", 2147483647);
    myprintf("\n");
 
    myprintf("%i\n", a);
    myprintf("%i\n", b);
    myprintf("%i\n", c);
    
    return 0;
}

Отримаємо:

Hello world!%
-2147483648
0
2147483647
11
1
10

[Execution complete with exit code 0]

Код успішно компілюється, виводить потрібні значення і навіть повертає 0, незважаючи на спроби маніпулювати пам'яттю вручну з використанням malloc і free. Тепер наша функція може обробляти 5 з 9 прапорців. В останній частині розглянемо обробку 16-річних чисел (%x, %X), позитивних цілих чисел (%u) і найцікавіше - вказівників (%p).

Повний код можна знайти за посиланням.

Дякую за те, що дочитали до кінця.

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

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

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

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

  • Лінукс й opensource та проект у 2023 році.

    Світ технологій у 2023 році шаленів від штучного інтелекту, продовжував гратися із криптовалютами та дивуватися витівкам Ілона Маска. Натомість у Лінукс середовищі та FOSS спільноті все йшло своєю течією.

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

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

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

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

    Linux

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

Вітаю! Вашу роботу опубліковано в Twitter та на Facebook Друкарні.

🔸https://www.facebook.com/drukarniaua

🔸https://twitter.com/drukarniaua

Ваш довгочит дуже цікавий! 🫶

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

  • Лінукс й opensource та проект у 2023 році.

    Світ технологій у 2023 році шаленів від штучного інтелекту, продовжував гратися із криптовалютами та дивуватися витівкам Ілона Маска. Натомість у Лінукс середовищі та FOSS спільноті все йшло своєю течією.

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

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

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

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

    Linux