Как известно, целые числа без знака кодируются в памяти компьютера прямым двоичным кодом, отрицательные целые числа - дополнительным кодом, а форма представления действительных чисел содержит и знак, и смещенный порядок, и мантиссу. Операций для визуализации битовых последовательностей в языке Си нет. Это избыточные операции, не нужные для разработки приложений.
Однако хочется посмотреть, как будет выглядеть то или иное число в двоичной форме. Это можно рассчитать, то есть буквально перевести любое число в двоичную систему известным алгоритмом, получить обратный, дополнительный коды, а также по правилам получить код действительного числа. Речь пойдет не об этом.
Речь пойдет о том, как прочесть переменную в памяти по битам и вывести результат на консоль. Условие при этом следующее: использовать только стандартные возможности языка Си, не использовать никакие нестандартные библиотеки функций.
Итак, приступим. Решение данной задачи будет основано на том, что есть в Си побитовые операции, в частности побитовая конъюнкция. Мы знаем, что результат конъюнкции признается истинным, если истинны все аргументы данной операции. Для двух аргументов Истина И Истина = Истина, остальные комбинации логических значений аргументов приводят к ложности конъюнкции.
Побитовая конъюнкция производится над каждой парой битов двух аргументов. Её результат будет равным единице в том бите результата, если в таких же битах аргументов содержатся единицы. Вот пример в восьмиразрядном коде. 9 И 8.
9: 00001001
8: 00001000
00001000 - результат побитового И.
Еще пример. 9 И 4.
9: 00001001
4: 00000100
00000000 - результат побитового И, в данном примере нет ни одного разряда двух исходных чисел, в котором бы одновременно содержались единицы.
Таким образом, если по очереди выполнить побитовую конъюнкцию с числами равными степеням двойки, мы получим ненулевой результат при единицах в одних и тех же разрядах. Если же в исследуемом числе в данном разряде степени двойки единицы не было, а в том же разряде во втором числе-маске единица была, то получим ноль. Это можно записать следующим образом на языке Си.
if ( (x & mask) != 0 ) z=1; else z=0;
x - исследуемое число, mask - второй операнд конъюнкции, равный степени двойки, то есть или 1, или 2, или 4, или 8 и так далее.
Если х=1 и mask=1, то z =1. Если x=1 и mask=2 или любой другой степени двойки, то z=0.
Для целых чисел этот код сработает. Но если исследуемое число будет типа float или double, то компилятор выдаст ошибку. Дело в том, что побитовые операции определены только для целых чисел. Здесь на помощь приходят указатели.
При объявлении переменной типа double, где будет храниться исследуемое число, сразу объявляем указатель на массив типа char (массив байтов).
double a;
char *u=(char*)&a; //указатель установлен на первый байт массива, в котором будет храниться переменная a
char mask=1; // второй операнд конъюнкции, будет равен степеням двойки.
Для перебора степеней двойки будет использована операция побитового сдвига влево на одну позицию.
mask=mask<<1;
Эта операция из единицы сделает двойку, из двойки - четверку и так далее, передвигая установленный бит поочередно по всем битам числа.
Далее осталось достроить код перебора байтов в массиве, где хранится исследуемое число, и код перебора битов внутри каждого байта. В целом программа на языке Си в вечном цикле будет выглядеть следующим образом. (В код заложено, что если вводим число, то получаем его компьютерный код, если символ, то программа завершится.)
#include<stdio.h>
#define BUFFER double
void main(){
BUFFER a;
char *u=(char*)&a;
char mask=1;
char s[sizeof(BUFFER)*8];
for( ; scanf("%lf",&a)!=0; ){
int ps=0;
for(int i=0; i<sizeof(BUFFER); i++){
for(int j=1; j<=8; j++){
int z;
if( (u[i] & mask) !=0 ) s[ps]='1';
else s[ps]='0';
mask=mask<<1; ps++;
}
mask=1;
}
for(ps--; ps>=0; ps--) printf("%c", s[ps]);
printf("\n");
}
}
С помощью данной программы можно исследовать любые простые типы, исправляя их имена под директивой #define. Неудобство в том, что нужно менять модификатор типа в форматной строке функции scanf. Оба места в листинге выделены жирным шрифтом.
С типом char связана одна особенность работы программы. После ввода символа выводится не только его код, но и код клавиши "Enter", нажатием которой ввод данных завершается.
Я так думаю, что можно и структуры исследовать, если структуру определить до директивы #define, но ввод придется исправлять. Сам я не проверял: лень.
Чтобы исправить имя типа данных для исследования только в одном месте, нужно перейти на Си++, но в этом случае программу нужно будет закрывать насильно, так как в потоковом вводе не содержится никаких признаков, которые можно было бы использовать для завершения программы.
#include<iostream>
#define BUFFER char
void main(){
BUFFER a;
char *u=(char*)&a;
char mask=1;
char s[sizeof(BUFFER)*8];
char b;
for( ; ; ){
cin>>a;
int ps=0;
for(int i=0;i<sizeof(BUFFER);i++){
for(int j=1; j<=8; j++){
int z;
if( (u[i] & mask) !=0 ) s[ps]='1';
else s[ps]='0';
mask=mask<<1; ps++;
}
mask=1;
}
for(ps--; ps>=0; ps--) cout<<s[ps];
cout<<endl;
}
}