В заключній частині напишемо обробку останніх чотирьох прапорців. Почнемо з найпростішого - %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 і роботою операційної системи. Однак, є багато місць для подальшого покращення. Ось декілька ідей:
Розбиття функцій на окремі файли.
Створення make-файлу.
Перевірка попереджень компілятора.
Перевірка пам'яті за допомогою Valgrind.
Написання тестів.
Код можна знайти за посиланням, дякую за увагу.