Tôi biết có một tiêu chuẩn đằng sau tất cả các cài đặt trình biên dịch C, vì vậy không nên có các tính năng ẩn. Mặc dù vậy, tôi chắc chắn rằng tất cả các nhà phát triển C đều có những thủ thuật ẩn / bí mật mà họ sử dụng mọi lúc.
Tôi biết có một tiêu chuẩn đằng sau tất cả các cài đặt trình biên dịch C, vì vậy không nên có các tính năng ẩn. Mặc dù vậy, tôi chắc chắn rằng tất cả các nhà phát triển C đều có những thủ thuật ẩn / bí mật mà họ sử dụng mọi lúc.
Câu trả lời:
Chức năng con trỏ. Bạn có thể sử dụng một bảng các con trỏ hàm để thực hiện, ví dụ, các trình thông dịch mã luồng gián tiếp nhanh (FORTH) hoặc các bộ gửi mã byte hoặc để mô phỏng các phương thức ảo giống như OO.
Sau đó, có những viên ngọc ẩn trong thư viện tiêu chuẩn, chẳng hạn như qsort (), bsearch (), strpbrk (), strcspn () [hai cái sau hữu ích để thực hiện thay thế strtok ()].
Một sai lầm của C là tràn số học đã ký là hành vi không xác định (UB). Vì vậy, bất cứ khi nào bạn thấy một biểu thức như x + y, cả hai đều được ký int, nó có thể có khả năng tràn và gây ra UB.
Thêm một mẹo của trình biên dịch GCC, nhưng bạn có thể đưa ra gợi ý về chỉ thị nhánh cho trình biên dịch (phổ biến trong nhân Linux)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
xem: http://kerneltrap.org/node/4705
Những gì tôi thích về điều này là nó cũng thêm một số biểu cảm cho một số chức năng.
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
Đây là một mục tùy chọn trong tiêu chuẩn, nhưng nó phải là một tính năng ẩn, bởi vì mọi người liên tục định nghĩa lại chúng. Một cơ sở mã mà tôi đã làm việc (và hiện vẫn đang làm) có nhiều định nghĩa lại, tất cả đều có các định danh khác nhau. Hầu hết thời gian với các macro tiền xử lý:
#define INT16 short
#define INT32 long
Và như thế. Nó làm cho tôi muốn kéo tóc ra. Chỉ cần sử dụng typedefs số nguyên tiêu chuẩn kỳ dị!
Toán tử dấu phẩy không được sử dụng rộng rãi. Nó chắc chắn có thể bị lạm dụng, nhưng nó cũng có thể rất hữu ích. Việc sử dụng này là phổ biến nhất:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
Nhưng bạn có thể sử dụng toán tử này ở bất cứ đâu. Quan sát:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
Mỗi câu lệnh được ước tính, nhưng giá trị của biểu thức sẽ là giá trị của câu lệnh cuối cùng được ước tính.
khởi tạo cấu trúc về không
struct mystruct a = {0};
điều này sẽ bằng không tất cả các yếu tố cấu trúc.
memset
/ calloc
do "all byte zero" (nghĩa là các số 0 vật lý), thực sự không được xác định cho tất cả các loại. { 0 }
được đảm bảo để giới thiệu mọi thứ với các giá trị logic hợp lý . Các con trỏ, chẳng hạn, được đảm bảo để có được các giá trị null thích hợp của chúng, ngay cả khi giá trị null trên nền tảng đã cho là 0xBAADFOOD
.
memset
không (với 0
đối số thứ hai). Bạn nhận được số 0 logic khi bạn khởi tạo / gán 0
(hoặc { 0 }
) cho đối tượng trong mã nguồn. Hai loại số không không nhất thiết tạo ra cùng một kết quả. Như trong ví dụ với con trỏ. Khi bạn làm memset
trên một con trỏ, bạn nhận được một 0x0000
con trỏ. Nhưng khi bạn gán 0
cho một con trỏ, bạn sẽ nhận được giá trị con trỏ null , ở mức vật lý có thể là 0xBAADF00D
hoặc bất cứ thứ gì khác.
double
. Thông thường, nó được thực hiện theo tiêu chuẩn IEEE-754, trong đó số 0 logic và số 0 vật lý là như nhau. Nhưng ngôn ngữ không yêu cầu IEEE-754. Vì vậy, có thể xảy ra rằng khi bạn làm double d = 0;
(số 0 logic), một số bit trong bộ nhớ chiếm dụng d
sẽ không bằng không.
Hằng số nhiều ký tự:
int x = 'ABCD';
Điều này đặt x
thành 0x41424344
(hoặc 0x44434241
, tùy thuộc vào kiến trúc).
EDIT: Kỹ thuật này không khả dụng, đặc biệt nếu bạn tuần tự hóa int. Tuy nhiên, nó có thể cực kỳ hữu ích để tạo ra các enum tự ghi lại. ví dụ
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
Điều này làm cho nó đơn giản hơn nhiều nếu bạn đang xem một bãi chứa bộ nhớ thô và cần xác định giá trị của một enum mà không cần phải tìm kiếm nó.
Tôi không bao giờ sử dụng các trường bit nhưng chúng có vẻ tuyệt vời cho những thứ cực thấp.
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;
}
Điều này có nghĩa là sizeof(cat)
có thể nhỏ như sizeof(char)
.
C có một trình biên dịch chuẩn nhưng không phải tất cả các trình biên dịch C đều tuân thủ đầy đủ (Tôi chưa thấy trình biên dịch C99 tuân thủ đầy đủ nào!).
Điều đó nói rằng, các thủ thuật tôi thích là những thủ thuật không rõ ràng và có thể di động trên các nền tảng vì chúng dựa vào ngữ nghĩa C. Chúng thường là về macro hoặc số học bit.
Ví dụ: hoán đổi hai số nguyên không dấu mà không sử dụng biến tạm thời:
...
a ^= b ; b ^= a; a ^=b;
...
hoặc "mở rộng C" để thể hiện các máy trạng thái hữu hạn như:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
có thể đạt được với các macro sau:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
Tuy nhiên, nói chung, tôi không thích các thủ thuật thông minh nhưng làm cho mã trở nên phức tạp không cần thiết để đọc (như ví dụ hoán đổi) và tôi thích các thủ thuật làm cho mã rõ ràng hơn và truyền đạt trực tiếp ý định (như ví dụ về FSM) .
Các cấu trúc xen kẽ như Thiết bị của 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);
}
}
Tôi rất thích các trình khởi tạo được chỉ định, được thêm vào C99 (và được hỗ trợ trong gcc trong một thời gian dài):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
Việc khởi tạo mảng không còn phụ thuộc vào vị trí. Nếu bạn thay đổi các giá trị của FOO hoặc BAR, việc khởi tạo mảng sẽ tự động tương ứng với giá trị mới của chúng.
cấu trúc ẩn danh và mảng là một yêu thích của tôi. (xem http://www.run.montefiore.ulg.ac.be/~martin/resource/kung-f00.html )
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
hoặc là
void myFunction(type* values) {
while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
nó thậm chí có thể được sử dụng để cung cấp danh sách liên kết ...
gcc có một số phần mở rộng cho ngôn ngữ C mà tôi thích, có thể tìm thấy ở đây . Một số mục yêu thích của tôi là thuộc tính chức năng . Một ví dụ cực kỳ hữu ích là thuộc tính định dạng. Điều này có thể được sử dụng nếu bạn xác định một chức năng tùy chỉnh có chuỗi định dạng printf. Nếu bạn bật thuộc tính hàm này, gcc sẽ kiểm tra các đối số của bạn để đảm bảo rằng chuỗi định dạng và đối số của bạn khớp với nhau và sẽ tạo ra các cảnh báo hoặc lỗi khi thích hợp.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
tính năng (ẩn) đã "gây sốc" cho tôi khi tôi lần đầu tiên nhìn thấy là về printf. tính năng này cho phép bạn sử dụng các biến để định dạng định dạng định dạng. tìm mã, bạn sẽ thấy tốt hơn:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
nhân vật * đạt được hiệu ứng này.
Chà ... tôi nghĩ rằng một trong những điểm mạnh của ngôn ngữ C là tính di động và tính chuẩn của nó, vì vậy bất cứ khi nào tôi tìm thấy một "mánh khóe ẩn giấu" nào đó trong triển khai tôi hiện đang sử dụng, tôi cố gắng không sử dụng nó vì tôi cố gắng giữ Mã C là tiêu chuẩn và di động càng tốt.
Biên dịch xác nhận thời gian, như đã thảo luận ở đây .
//--- 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);
Nối chuỗi liên tục
Tôi khá ngạc nhiên khi không thấy nó sẵn sàng trong các câu trả lời, vì tất cả các trình biên dịch mà tôi biết đều hỗ trợ nó, nhưng nhiều lập trình viên dường như bỏ qua nó. Đôi khi nó thực sự tiện dụng và không chỉ khi viết macro.
Trường hợp sử dụng tôi có trong mã hiện tại của mình: Tôi có một #define PATH "/some/path/"
tệp cấu hình (thực sự nó được giải quyết bởi tệp thực hiện). Bây giờ tôi muốn xây dựng đường dẫn đầy đủ bao gồm tên tệp để mở nguồn. Nó chỉ đi đến:
fd = open(PATH "/file", flags);
Thay vì kinh khủng, nhưng rất phổ biến:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Lưu ý rằng giải pháp khủng khiếp phổ biến là:
Chà, tôi chưa bao giờ sử dụng nó, và tôi không chắc liệu tôi có từng giới thiệu nó cho bất kỳ ai không, nhưng tôi cảm thấy câu hỏi này sẽ không đầy đủ nếu không đề cập đến thủ thuật đồng quy của Simon Tatham .
Khi khởi tạo mảng hoặc enum, bạn có thể đặt dấu phẩy sau mục cuối cùng trong danh sách khởi tạo. ví dụ:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
Điều này đã được thực hiện để nếu bạn tự động tạo mã, bạn không cần phải lo lắng về việc loại bỏ dấu phẩy cuối cùng.
Phân công cấu trúc là mát mẻ. Nhiều người dường như không nhận ra rằng các cấu trúc cũng là các giá trị và có thể được chỉ định xung quanh, không cần sử dụng memcpy()
, khi một phép gán đơn giản thực hiện thủ thuật.
Ví dụ, hãy xem xét một số thư viện đồ họa 2D tưởng tượng, nó có thể xác định một loại để thể hiện tọa độ màn hình (số nguyên):
typedef struct {
int x;
int y;
} Point;
Bây giờ, bạn làm những việc có thể trông "sai", như viết một hàm tạo một điểm được khởi tạo từ các đối số của hàm và trả về nó, như vậy:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
Điều này là an toàn, miễn là (tất nhiên) khi giá trị trả về được sao chép theo giá trị bằng cách sử dụng phép gán struct:
Point origin;
origin = point_new(0, 0);
Theo cách này, bạn có thể viết mã ish khá sạch và hướng đối tượng, tất cả đều theo tiêu chuẩn C.
Lập chỉ mục vector lạ:
int v[100]; int index = 10;
/* v[index] it's the same thing as index[v] */
Trình biên dịch C thực hiện một trong một số tiêu chuẩn. Tuy nhiên, có một tiêu chuẩn không có nghĩa là tất cả các khía cạnh của ngôn ngữ được xác định. Ví dụ, thiết bị của Duff là một tính năng 'ẩn' yêu thích đã trở nên phổ biến đến mức các trình biên dịch hiện đại có mã nhận dạng mục đích đặc biệt để đảm bảo rằng các kỹ thuật tối ưu hóa không làm mờ hiệu ứng mong muốn của mẫu thường được sử dụng này.
Nói chung, các tính năng ẩn hoặc thủ thuật ngôn ngữ không được khuyến khích khi bạn đang chạy trên cạnh dao cạo của bất kỳ (các) tiêu chuẩn C nào mà trình biên dịch của bạn sử dụng. Nhiều thủ thuật như vậy không hoạt động từ trình biên dịch này sang trình biên dịch khác và thường các loại tính năng này sẽ thất bại từ một phiên bản của bộ trình biên dịch bởi một nhà sản xuất nhất định sang phiên bản khác.
Nhiều thủ thuật đã phá vỡ mã C bao gồm:
Các vấn đề và vấn đề khác phát sinh bất cứ khi nào lập trình viên đưa ra các giả định về các mô hình thực thi, tất cả được chỉ định trong hầu hết các tiêu chuẩn C là hành vi 'phụ thuộc vào trình biên dịch'.
Khi sử dụng sscanf, bạn có thể sử dụng% n để tìm nơi bạn nên tiếp tục đọc:
sscanf ( string, "%d%n", &number, &length );
string += length;
Rõ ràng, bạn không thể thêm câu trả lời khác, vì vậy tôi sẽ đưa câu trả lời thứ hai vào đây, bạn có thể sử dụng "&&" và "||" như điều kiện:
#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 );
}
Mã này sẽ xuất ra:
Chào ROFL
sử dụng INT (3) để đặt điểm dừng tại mã là sở thích của tôi
Tính năng "ẩn" yêu thích của tôi về C, là việc sử dụng% n trong printf để ghi lại vào ngăn xếp. Thông thường printf bật các giá trị tham số từ ngăn xếp dựa trên chuỗi định dạng, nhưng% n có thể ghi lại chúng.
Kiểm tra phần 3.4.2 tại đây . Có thể dẫn đến rất nhiều lỗ hổng khó chịu.
Kiểm tra giả định thời gian biên dịch bằng enums: Ví dụ ngu ngốc, nhưng có thể thực sự hữu ích cho các thư viện có hằng số cấu hình thời gian biên dịch.
#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]
)
Gcc (c) có một số tính năng thú vị mà bạn có thể kích hoạt, chẳng hạn như khai báo hàm lồng nhau và dạng a ?: B của toán tử ?:, Trả về a nếu a không sai.
Các macro đối số biến kiểu C99, aka
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
mà sẽ được sử dụng như
ERR(errCantOpen, "File %s cannot be opened", filename);
Ở đây tôi cũng sử dụng toán tử chuỗi và nối chuỗi liên tục, các tính năng khác tôi thực sự thích.
Biến tự động kích thước biến cũng hữu ích trong một số trường hợp. Chúng đã được thêm i nC99 và đã được hỗ trợ trong gcc trong một thời gian dài.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
Bạn kết thúc với một bộ đệm trên ngăn xếp có chỗ cho tiêu đề giao thức kích thước cố định cộng với dữ liệu kích thước thay đổi. Bạn có thể có được hiệu ứng tương tự với alloca (), nhưng cú pháp này nhỏ gọn hơn.
Bạn phải chắc chắn rằng ExtraPadding là một giá trị hợp lý trước khi gọi thủ tục này, hoặc cuối cùng bạn sẽ thổi tung ngăn xếp. Bạn phải tỉnh táo kiểm tra các đối số trước khi gọi malloc hoặc bất kỳ kỹ thuật cấp phát bộ nhớ nào khác, vì vậy điều này thực sự không bình thường.