Se não me engano, as structs são armazenadas em um espaço contínuo na memória. Desta forma, ao declarar uma variável do tipo st_func, o que você tem em mãos é uma região contínua de 60 bytes na memória.
Temos que &func e func.nome apontam para a primeira posição dessa região, func.end aponta para a posição 30 e func.prof aponta para a posição 50.
Assim, quando você passa o endereço de func para a função fread, ele não sabe que você tá passando uma struct. Ele tá vendo um endereço e vai escrever 60 bytes do arquivo a partir desse endereço. Desta forma ele acabará escrevendo exatamente o que você queria.
Entretanto, para imprimir um vetor de caracteres com %s, a função precisa saber qual o tamanho do vetor ou um caractere que marca o final da string. Em C++ a classe string armazena o tamanho da string, mas em C precisamos colocar o caractere de final de string. Neste caso é o '\0' ou simplemente o valor 0. Do modo como você está fazendo, não tá colocando esse caracter. Para isso você teria que declarar cada campo com uma posição a mais do que vai realmente utilizar, mas ia bugar a sua leitura que depende do tamanho da sua struct.
Não sei uma maneira muito enxuta de se contornar o problema. A primeira coisa que me vem em mente é ler uma linha inteira em uma string e depois usando 'for's preencher cada campo da sua struct.
EDITADO:
Na verdade você poderia fazer 3 leituras, lendo cada campo de uma vez e depois "fechando" a string. Observe que mesmo dessa forma você deve declarar seus campos com uma posição a mais:
include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *arq;
int qtd;
struct st_func
{
char nome[31];
char end[21];
char prof[11];
};
struct st_func func;
if ( (arq = fopen("arq01.txt", "r") ) == NULL )
{
printf("Erro ao abrir arquivo... ");
exit(0);
}
qtd = fread(func.nome, 30, 1, arq);
qtd += fread(func.end, 20, 1, arq);
qtd += fread(func.prof, 10, 1, arq);
func.nome[30] = func.end[20] = func.prof[10] = 0;
printf("\nNome: %s \nEnd: %s \nProf: %s\n", func.nome, func.end, func.prof);
fclose(arq);
}