1. Regarding "void *" : No I'm not familiar with it. Can you explain in simpler words.
In C and C++, "void" is a funny thing. It looks like it is a data type, like "int" or "double". Except it really isn't. When used as a return type or the argument list of a function, it means that a function doesn't return anything, or takes no arguments. So it is not a data type, but an indicator that shows you that there is nothing there.
"int foo(double x) { ... }" is a function that takes a double argument, and returns an int.
"void bar(void) { ... }" is a function that takes no arguments and returns nothing (in isolation, such a function would be useless, but let's use it as a didactic example). Now you could ask, why didn't the people who designed C make it more logical by simply having nothing? They could have done "bar() { ... }" to indicate the same thing. Well, the lack of return type would have broken the C parser (it wouldn't know whether you are defining a function or calling it). And the lack of argument list was used early on to distinguish between ANSI and K&R style argument lists (30 years ago). So the "data type" void is really just a historical artifact.
In contrast, void can be used to define a pointer. For example, "int* pInt" is a variable of type "pointer to an integer", and "double* pDouble" is a variable of type "pointer to a double" (I'm using a naming convention here that uses the prefix "p" for every pointer, just for extra clarity). Note that C distinguishes different pointer types by the kind of thing they point to; the reasons for that are partly historical (there used to be computers where a pointer to a character was different in hardware from a pointer to an integer, like the old Cyber 6xxx hardware with 60-bit integers and 6-bit characters, and character pointers had to have more bits), and partly architectural within the language (when you dereference an int*, the compiler knows that the result is an integer, not anything else). But since C is a low-level language, where you often have to deal directly with hardware or memory, it allows using a "void*" to indicate a pointer to some form of memory, without specifying what is inside the memory. To actually use a "void*", you have to cast it to a pointer to a concrete type. In C, that casting operation is implicit in function calls: If you have some data that is for example of type "char" (like a buffer of characters, meaning of single bytes), you can pass the address of a character (perhaps the beginning of the buffer) around in a void*, and use that as an argument to functions.
5. Can you explain this a bit further. This is the part I least understand. ... But I don't understand how the "%16.16x" works.
OK, so you have a pointer, and for our discussion it doesn't matter whether it is a char* or a void*. The printf family of functions is special, in that it can interpret the arguments passed to it in many ways. If your print format contains a "%p", then it will take the argument passed to it, assume it is a pointer, and print the value of the pointer itself in a readable format. For example, if "char* pChar" points to some characters that are in memory at address 0x12345678, then printing "printf("%p", pChar) will probably return the string "0x12345678". On the other hand, if you use the print format %s, then printf will assume that the pointer is a pointer to a string in memory (where a string is nothing but a set of char's, with a terminating nun), and it will print the content of the memory pointed to by pChar, which could for example be "the quick brown fox jumps over the lazy dog".
Now, let's talk about the %x printf format. It simply assumes that the data you give it is an integer, and shall be printed in hexadecimal. So if you do "i=100; printf("%x", i)", the result will be the string "64" (since the decimal number 100 is hexadecimal 0x64). Now, let's add the two decorations. First, let's assume that we have a variable "ell", which is of a 64-bit type (like uint64). If we do "printf("%16x", ell), then it will print exactly 16 characters. If I initialize ell to the value 0x1234567812345678, then the result of the printf will be the string "1234567812345678", and if I initialize ell to zero, the result will be " 0" (there are 15 spaces before the zero). General rule for printf formats: If you put a number in front of the format specifier, it will tell printf how long to make the output, in this case 16 characters. By the way, why did I pick 16 characters? Because a 64-bit number can always be printed using 16 hex characters (every hex character can show you 4 bits). Next complication: If you use the style "%16.16x", then for integer formats (like %d, %u and %x), this means print at least 16 non-blank characters. If I had used that format, the output would be the same "1234567812345678", but for the second example it would be "0000000000000000". I like doing that for hexadecimal numbers, because it shows the user the bit pattern. Another example where this comes in useful is printing money amounts: printf("$%d.%2d", dollars, cents) works really badly: while $1.99 looks good, $2. 0" looks stupid, with the extra space in-between and missing the second zero. Another really bad version is printf("$%d.%d", dollars, cents): it is also good for $1.99, but $2.0 looks even more wrong for a two-dollar price. So for printing this, you should use printf("$%d.%2.2d", dollars, cents) to get the expected $2.00.
Warning: The style "%4.2f" for floating-point (double) numbers means something completely different: Print a number with a total of 4 characters, and exactly two digits after the decimal point. That's a sad inconsistency in C, where the second number in the printf specification means something different for integers versus floating point. In general, the C language sucks, but that's a fact of life.
About your questions on salt in modern password encryption: No idea. Not my area of expertise.