Eu sei que há um padrão por trás de todas as implementações do compilador C, portanto, não deve haver recursos ocultos. Apesar disso, tenho certeza de que todos os desenvolvedores de C têm truques ocultos / secretos que usam o tempo todo.
Eu sei que há um padrão por trás de todas as implementações do compilador C, portanto, não deve haver recursos ocultos. Apesar disso, tenho certeza de que todos os desenvolvedores de C têm truques ocultos / secretos que usam o tempo todo.
Respostas:
Ponteiros de função. Você pode usar uma tabela de ponteiros de função para implementar, por exemplo, aceleradores de código de thread indireto (FORTH) rápidos ou distribuidores de código de bytes, ou para simular métodos virtuais semelhantes a OO.
Depois, existem gemas ocultas na biblioteca padrão, como qsort (), bsearch (), strpbrk (), strcspn () [as duas últimas sendo úteis para implementar uma substituição de strtok ()].
Um erro de C é que o estouro aritmético assinado é um comportamento indefinido (UB). Portanto, sempre que você vê uma expressão como x + y, ambas sendo assinadas, ela pode potencialmente estourar e causar UB.
Mais um truque do compilador GCC, mas você pode fornecer dicas de indicação de ramificação para o compilador (comum no kernel do Linux)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
veja: http://kerneltrap.org/node/4705
O que eu gosto sobre isso é que ele também acrescenta alguma expressividade a algumas funções.
void foo(int arg)
{
if (unlikely(arg == 0)) {
do_this();
return;
}
do_that();
...
}
int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t
Esse é um item opcional no padrão, mas deve ser um recurso oculto, porque as pessoas os redefinem constantemente. Uma base de código em que trabalhei (e ainda o faço, por enquanto) tem várias redefinições, todas com identificadores diferentes. Na maioria das vezes, é com macros de pré-processador:
#define INT16 short
#define INT32 long
E assim por diante. Isso me faz querer arrancar meus cabelos. Basta usar os inteiros padrão typedefs!
O operador de vírgula não é amplamente utilizado. Certamente pode ser abusado, mas também pode ser muito útil. Esse uso é o mais comum:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
Mas você pode usar esse operador em qualquer lugar. Observar:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
Cada instrução é avaliada, mas o valor da expressão será o da última instrução avaliada.
inicializando a estrutura para zero
struct mystruct a = {0};
isso zerará todos os elementos da estrutura.
memset
/ calloc
do "todos os bytes zero" (ou seja, zeros físicos), o que de fato não é definido para todos os tipos. { 0 }
é garantido para intilizar tudo com valores lógicos zero adequados . Os ponteiros, por exemplo, são garantidos para obter seus valores nulos adequados, mesmo que o valor nulo na plataforma fornecida seja 0xBAADFOOD
.
memset
isso que faz (com 0
o segundo argumento). Você obtém zero lógico ao inicializar / atribuir 0
(ou { 0 }
) ao objeto no código-fonte. Esses dois tipos de zeros não produzem necessariamente o mesmo resultado. Como no exemplo com ponteiro. Quando você faz memset
um ponteiro, recebe um 0x0000
ponteiro. Porém, quando você atribui 0
a um ponteiro, obtém um valor nulo , que no nível físico pode ser 0xBAADF00D
ou qualquer outra coisa.
double
,. Geralmente é implementado de acordo com o padrão IEEE-754, no qual o zero lógico e o zero físico são os mesmos. Mas o IEEE-754 não é exigido pelo idioma. Portanto, pode acontecer que, quando você fizer double d = 0;
(zero lógico), fisicamente alguns bits na memória ocupada por d
não sejam zero.
Constantes com vários caracteres:
int x = 'ABCD';
Isso define x
como 0x41424344
(ou 0x44434241
, dependendo da arquitetura).
Edição: Esta técnica não é portátil, especialmente se você serializar o int. No entanto, pode ser extremamente útil criar enums de auto-documentação. por exemplo
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
Isso torna muito mais simples se você estiver olhando para um despejo de memória bruta e precisar determinar o valor de uma enumeração sem precisar procurar.
Eu nunca usei campos de bits, mas eles parecem legais para coisas de nível ultra baixo.
struct cat {
unsigned int legs:3; // 3 bits for legs (0-4 fit in 3 bits)
unsigned int lives:4; // 4 bits for lives (0-9 fit in 4 bits)
// ...
};
cat make_cat()
{
cat kitty;
kitty.legs = 4;
kitty.lives = 9;
return kitty;
}
Isso significa que sizeof(cat)
pode ser tão pequeno quanto sizeof(char)
.
Comentários incorporados de Aaron e leppie , obrigado pessoal.
C possui um padrão, mas nem todos os compiladores C são totalmente compatíveis (ainda não vi nenhum compilador C99 totalmente compatível!).
Dito isso, os truques que eu prefiro são aqueles que não são óbvios e são portáveis entre plataformas, pois contam com a semântica C. Eles geralmente são sobre macros ou aritmética de bits.
Por exemplo: trocando dois números inteiros não assinados sem usar uma variável temporária:
...
a ^= b ; b ^= a; a ^=b;
...
ou "estendendo C" para representar máquinas de estados finitos como:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
isso pode ser alcançado com as seguintes macros:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
Em geral, porém, eu não gosto dos truques que são inteligentes, mas tornam o código desnecessariamente complicado de ler (como o exemplo de troca) e adoro os que tornam o código mais claro e transmitem diretamente a intenção (como o exemplo do FSM) .
Estruturas de entrelaçamento como o dispositivo de Duff :
strncpy(to, from, count)
char *to, *from;
int count;
{
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
Eu gosto muito de inicializadores designados, adicionados no C99 (e suportados no gcc por um longo tempo):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
A inicialização do array não depende mais da posição. Se você alterar os valores de FOO ou BAR, a inicialização do array corresponderá automaticamente ao novo valor.
estruturas e matrizes anônimas é a minha favorita. (cf. http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html )
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
ou
void myFunction(type* values) {
while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
pode até ser usado para instanciar listas vinculadas ...
O gcc possui várias extensões para o idioma C que eu gosto, que podem ser encontradas aqui . Alguns dos meus favoritos são atributos de função . Um exemplo extremamente útil é o atributo format. Isso pode ser usado se você definir uma função personalizada que usa uma sequência de caracteres no formato printf. Se você habilitar esse atributo de função, o gcc fará verificações nos seus argumentos para garantir que a sequência e os argumentos do formato sejam correspondentes e gerará avisos ou erros, conforme apropriado.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
o recurso (oculto) que me "chocou" quando vi pela primeira vez é sobre printf. esse recurso permite usar variáveis para formatar os especificadores de formato. procure o código, você verá melhor:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
o caractere * atinge esse efeito.
Bem ... acho que um dos pontos fortes da linguagem C é a portabilidade e a padronização; portanto, sempre que encontro algum "truque oculto" na implementação que estou usando atualmente, tento não usá-lo porque tento manter meu Código C o mais padrão e portátil possível.
Asserções em tempo de compilação, como já discutido aqui .
//--- size of static_assertion array is negative if condition is not met
#define STATIC_ASSERT(condition) \
typedef struct { \
char static_assertion[condition ? 1 : -1]; \
} static_assertion_t
//--- ensure structure fits in
STATIC_ASSERT(sizeof(mystruct_t) <= 4096);
Concatenação de cadeia constante
Fiquei bastante surpreso por não ter visto tudo nas respostas, pois todos os compiladores que conheço o apóiam, mas muitos programadores parecem ignorá-lo. Às vezes, é realmente útil e não apenas ao escrever macros.
Caso de uso que tenho no meu código atual: Eu tenho um #define PATH "/some/path/"
em um arquivo de configuração (realmente é definido pelo makefile). Agora eu quero construir o caminho completo, incluindo nomes de arquivos para abrir recursos. Apenas vai para:
fd = open(PATH "/file", flags);
Em vez do horrível, mas muito comum:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Observe que a solução horrível comum é:
Bem, eu nunca o usei e não tenho certeza se recomendaria a alguém, mas acho que essa pergunta seria incompleta sem uma menção ao truque co-rotineiro de Simon Tatham .
Ao inicializar matrizes ou enumerações, você pode colocar uma vírgula após o último item na lista de inicializadores. por exemplo:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
Isso foi feito para que, se você estiver gerando código automaticamente, não precise se preocupar em eliminar a última vírgula.
A atribuição de estruturas é legal. Muitas pessoas parecem não perceber que as estruturas também são valores e podem ser atribuídas ao redor, não há necessidade de usarmemcpy()
, quando uma tarefa simples faz o truque.
Por exemplo, considere alguma biblioteca gráfica 2D imaginária, ela pode definir um tipo para representar uma coordenada da tela (inteira):
typedef struct {
int x;
int y;
} Point;
Agora, você faz coisas que podem parecer "erradas", como escrever uma função que cria um ponto inicializado a partir dos argumentos da função e a retorna da seguinte maneira:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
Isso é seguro, desde que, é claro, o valor de retorno seja copiado por valor usando a atribuição de estrutura:
Point origin;
origin = point_new(0, 0);
Dessa forma, você pode escrever um código ish bastante limpo e orientado a objetos, tudo no padrão C.
Indexação de vetor estranho:
int v[100]; int index = 10;
/* v[index] it's the same thing as index[v] */
Os compiladores C implementam um dos vários padrões. No entanto, ter um padrão não significa que todos os aspectos da linguagem sejam definidos. Dispositivo de Duff , por exemplo, é um recurso 'oculto' favorito que se tornou tão popular que os compiladores modernos têm código de reconhecimento de finalidade especial para garantir que as técnicas de otimização não obtenham o efeito desejado desse padrão frequentemente usado.
Em geral, os recursos ocultos ou os truques de linguagem são desencorajados, enquanto você está rodando na borda dos padrões C, que seu compilador usa. Muitos desses truques não funcionam de um compilador para outro e, geralmente, esses tipos de recursos falham de uma versão de um conjunto de compiladores de um determinado fabricante para outra versão.
Vários truques que quebraram o código C incluem:
Outros problemas e questões que surgem sempre que os programadores fazem suposições sobre os modelos de execução especificados na maioria dos padrões C como comportamento 'dependente do compilador'.
Ao usar o sscanf, você pode usar% n para descobrir onde deve continuar lendo:
sscanf ( string, "%d%n", &number, &length );
string += length;
Aparentemente, você não pode adicionar outra resposta, então incluirei uma segunda aqui, você pode usar "&&" e "||" como condicionais:
#include <stdio.h>
#include <stdlib.h>
int main()
{
1 || puts("Hello\n");
0 || puts("Hi\n");
1 && puts("ROFL\n");
0 && puts("LOL\n");
exit( 0 );
}
Este código produzirá:
Oi ROFL
usar INT (3) para definir o ponto de interrupção no código é o meu favorito de todos os tempos
Meu recurso "oculto" favorito de C é o uso de% n no printf para gravar de volta na pilha. Normalmente printf exibe os valores dos parâmetros da pilha com base na string de formato, mas% n pode gravá-los de volta.
Confira a seção 3.4.2 aqui . Pode levar a muitas vulnerabilidades desagradáveis.
Verificação de suposições em tempo de compilação usando enumerações: exemplo estúpido, mas pode ser realmente útil para bibliotecas com constantes configuráveis em tempo de compilação.
#define D 1
#define DD 2
enum CompileTimeCheck
{
MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)),
MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0)
};
#define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1]
)
O Gcc (c) possui alguns recursos divertidos que você pode ativar, como declarações de funções aninhadas e a forma a?: B do operador?: Que retorna a se a não for falso.
Descobri recentemente 0 campos de bits.
struct {
int a:3;
int b:2;
int :0;
int c:4;
int d:3;
};
que dará um layout de
000aaabb 0ccccddd
em vez de sem o: 0;
0000aaab bccccddd
O campo de largura 0 indica que os seguintes campos de bits devem ser definidos na próxima entidade atômica ( char
)
Macros de argumento variável no estilo C99, também conhecido como
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
que seria usado como
ERR(errCantOpen, "File %s cannot be opened", filename);
Aqui eu também uso o operador stringize e a concatentação constante de strings, outros recursos que realmente gosto.
Variáveis automáticas de tamanho variável também são úteis em alguns casos. Estes foram adicionados no nC99 e são suportados no gcc há muito tempo.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
Você acaba com um buffer na pilha, com espaço para o cabeçalho de protocolo de tamanho fixo mais dados de tamanho variável. Você pode obter o mesmo efeito com alloca (), mas essa sintaxe é mais compacta.
Você deve garantir que extraPadding seja um valor razoável antes de chamar essa rotina, ou você acaba explodindo a pilha. Você precisaria verificar os argumentos antes de chamar o malloc ou qualquer outra técnica de alocação de memória, portanto isso não é realmente incomum.