Один з небагатьох проектів, який мені справді було цікаво реалізовувати під час навчання в школі 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, |
Підтримувані прапори | 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. код можна знайти за посиланням.