Продовжуємо збирати велосипед або писати власний 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

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

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

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

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

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

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

🔸https://twitter.com/drukarniaua

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

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