Збираємо велосипед або пишемо власний printf

Один з небагатьох проектів, який мені справді було цікаво реалізовувати під час навчання в школі 42, - це написання власної імплементації функції printf.

printf - функція, що входить до бібліотеки stdio.h і використовується для виводу тексту до стандартного виводу. Імплементована у вигляді:

int printf ( const char * format, ... );

Приймає стрічку (const char *) і значення котрі ми хочемо додати до стрічки (…), повертає int зі значенням кількості надрукованих символів. Для форматування використовуються прапори у вигляді %d, %s, %c… Наприклад:

#include <stdio.h>

int main() {
    printf("Hello %s %c %d\n", "world",'!', 123);
    return 0;
}

Після компіляції і виконання виведе:

[username@:~]$ gcc hello_world.c
[username@:~]$ ./a.out
Hello world ! 123

Перейдемо до самого завдання проекту:

Опис

Написати бібліотеку з власною імлементацією функції printf

Функції дозволені для використання

malloc, free, write,
va_start, va_arg, va_copy, va_end

Підтримувані прапори

cspdiuxX%

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

#include <unistd.h>

ssize_t write(int fildes, const void *buf, size_t nbyte)

Функція write входить до бібліотеки unistd.h і використовується для виводу інформації до дескриптора файлу. В unix-подібних системах, дескриптор стандартного виводу дорівнює 1.

#include <unistd.h>

int main() {
    char c = '+';
// записуємо до stdout, 
// значення що знаходиться зі посиланням &c
// розміром 1 байт
    write(1, &c, 1); 

    return 0;
}

Зробимо перший крок і напишемо функцію, що зможе виводити стрічку. Створимо файл myprintf.c і запишемо до нього наступний код:

#include <unistd.h>

int myprintf(const char * str, ...){
    int counter = 0;
    // через цикл проходимо по стрічці,
    // поки не зустрінемо нул-термінатор -
    // '\0' значення котрим повинні закінчуватись всі стрічки в С
    while(str[counter] != '\0'){
        // виводимо по одному символу за крок циклу
        write(1, &str[counter], 1);
        counter++;
    }

    // повертаємо кількість надрукованих символів
    return counter;
}

int main() {
    char * str = "Hello world!";
    myprintf(str);
    
    return 0;
}

Компілюємо за допомогою gcc і виконуємо отриману програму:

[username@:~]$ gcc myprintf.c
[username@:~]$ ./a.out
Hello world!

Рядок з викликом функції write винесемо в окрему функцію printchar, котру потім будем вокористовувати як для простого виводу, так і обробки прапорів, отримаємо:

#include <unistd.h>

int printchar(char c){
    write(1, &c, 1);
    return 1;
}

int myprintf(const char * str, ...){
    int counter = 0;
    while(str[counter] != '\0'){
        printchar(str[counter]);
        counter++;
    }
    return counter;
}

int main() {
    char * str = "Hello world!";
    myprintf(str);
    
    return 0;
}

Тепер переходимо до найцікавішої частини - форматування виводу. Спробуємо обробити значення для вставки у вихідний рядок та найпростіший прапорець %c. Для цього внесемо зміни у функцію myprintf:

#include <unistd.h>

int printchar(char c){
    write(1, &c, 1);
    return 1;
}
// функція тепер приймає два аргементи
// стрічка з текстом і прапором котрий вказує 
// тип значення для вставки 
int myprintf(const char * str, char c){
    int counter = 0;
    while(str[counter] != '\0'){
        if (str[counter] == '%'){
            counter++;
            if (str[counter] == 'c'){
                printchar(c);
            }
        }
        else {
            printchar(str[counter]);
        }
        counter++;
    }
    return counter;
}

int main() {
    // використовуємо %с - прапор дл япозначення літери
    char * str = "Hello world!%c";
    myprintf(str, '+');
    
    return 0;
}

// Отримаємо:
// Hello world!+

Додамо обробку прапору стрічки - %s. Для цього напишемо функцію printstring.

int printstring(char *str){
    int len = str_len(str);
    
    write(1, str, len);
    
    return len;
}

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

int str_len(char * str){
    int counter = 0;
    while(str[counter] != '\0'){
        counter++;
    }
    
    return counter;
}

Перепишемо функцію myprintf так, щоб вона могла форматувати рядок, використовуючи прапорці %c та %s. Згідно з документацією, функція printf повинна повертати довжину виведеного тексту, крім самого виводу. Ми не можемо цього зробити, використовуючи лічильник (counter), оскільки довжина форматованого рядка може бути будь-якою, а лічильник відповідає лише за індексацію форматованого рядка. На щастя, у мові С стрічки це вказівники, тому ми можемо ітеруватись за вказівником (*str у нашому випадку).

#include <unistd.h>

int printchar(char c){
    write(1, &c, 1);
    return 1;
}

int str_len(char * str){
    int counter = 0;
    while(str[counter] != '\0'){
        counter++;
    }
    
    return counter;
}

int printstring(char *str){
    int len = str_len(str);
    
    write(1, str, len);
    
    return len;
}

int myprintf(const char * str, char c, char * s){
    int counter = 0;

    while(*str != '\0'){
        if (*str == '%'){
            str++;
            if (*str == 'c'){
                counter += printchar(c);
            }
            if (*str == 's'){
                counter += printstring(s);
            }
        }
        else {
            counter += printchar(*str);
        }
        str++;
    }
    
    return counter;
}

int main() {
    char * str = "%cello %s!";
    myprintf(str, 'H', "world");
    
    return 0;
}

// Отримаємо:
// Hello world!

Наша функція тепер може форматувати стрічку використовуючи по одному значенню типу char і char *, але нам потрібно підтримувати невизначену кількісь значень різних типів. Для цього подивимось на список дозволених для використання функцій і знайдемо там va_start, va_arg, va_copy, va_end - ці функції входять до бібліотеки stdarg.h. Вчергове перепишемо myprintf, тепер використовуючи va_ функції.

// підключаємо бібліотеку
#include <stdarg.h>
...
// використовуємо ... як аргумент функції
int myprintf(const char * str, ...){
    // створюємо список аргументів
    va_list args;
    int counter;
    
    // ініціалізуємо список аргументів
    // args - список аргументів
    // str - назва аргумента після котрого починається перелік args
    va_start(args, str);
    counter = 0;
    while(*str != '\0'){
        if (*str == '%'){
            str++;
            if (*str == 'c'){
                // передаємо аргумент приводячи до відповідного типу
                // передаємо аргумент як int
                // потім явно приводимо до типу char
                // не можемо одразу приводити до char
                // бо отримаємо попередження:
                // warning: 'char' is promoted to 'int' when passed through '...'
                counter += printchar((char)va_arg(args, int));
            }
            if (*str == 's'){
                // приводимо аргументдо типу char * 
                // і передаємо функції printstring
                counter += printstring(va_arg(args, char *));
            }
        }
        else {
            counter += printchar(*str);
        }
        str++;
    }
    // закінчуємо використання аргументів
    va_end(args);
    
    return counter;
}

Винесемо обробку прапорів в окрему функцію і додамо обробку ще одного прапору %%:

int handle_flag(va_list args, char flag)
{
    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 myprintf(const char * str, ...){
    va_list args;
    int counter;
    
    va_start(args, str);
    counter = 0;
    while(*str != '\0'){
        if (*str == '%'){
            str++;
            counter += handle_flag(args, *str);
        }
        else {
            counter += printchar(*str);
        }
        str++;
    }
    va_end(args);
    
    return counter;
}

Таким чином ми почали створювати власний аналог функції printf. Описали обробку 3 прапорів з 9 (cspdiuxX%), створили базу для подальшого розширення функції. Наступним кроком буде імплементація обробки прапорів %i і %d, котрі будуть вимагати написання конвертеру типу int до стрічки.

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

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

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

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

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

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

Потужно

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