A good, general rule is that you should never compare floating-point numbers for exact equality. Famously, simple-looking code fragments like
float f1 = 1.1, f2 = 2.2;
float f3 = 3.3;
if(f1 + f2 == f3) printf("expected\n");
else printf("surprise\n");
are quite likely to behave surprisingly. The reason is that most real numbers (including ordinary decimal fractions like 1.1) can not be represented exactly in a finite-width floating-point representation.
Some numbers can be represented exactly, of course. In particular, small integers can typically be represented exactly even in low-precision floating-point representations. So to restate your example, code like
int x = 3;
float y = 3.0;
if(x == y) printf("expected\n");
else printf("surprise\n");
is virtually guaranteed to print the expected result on any practical computer.
How does it work, exactly? C supports mixed-mode arithmetic. You're perfectly allowed to write things like x == y even if x and y have different types. What typically happens is that the compiler inserts implicit conversions to a common type. So when you write x == y, the compiler says to itself. "Hmm. I don't have an instruction to compare an int and a float for equality. But if I convert the int to a float, I'll have two floats, and then I can use the machine's single-precision floating-point equality comparison operator. So I'll pretend the expression was (float)x == y."
But the general rule still stands. The fact that a floating-point variable can exactly store a small integer like 3.0 (or an exact fraction like 0.5) does not mean that it can store all integers exactly. For example, the similar-looking code
long int x = 123456789;
float y = 123456789.0;
if(x == (long int)y) printf("expected\n");
else printf("surprise\n");
is again quite likely to print a surprising result.