ฉันรู้ว่ามีมาตรฐานอยู่เบื้องหลังการใช้งานคอมไพเลอร์ C ทั้งหมดดังนั้นจึงไม่มีคุณสมบัติที่ซ่อนอยู่ แม้จะเป็นเช่นนั้นฉันมั่นใจว่านักพัฒนา C ทุกคนมีกลอุบายที่ซ่อนอยู่ / ความลับที่พวกเขาใช้ตลอดเวลา
ฉันรู้ว่ามีมาตรฐานอยู่เบื้องหลังการใช้งานคอมไพเลอร์ C ทั้งหมดดังนั้นจึงไม่มีคุณสมบัติที่ซ่อนอยู่ แม้จะเป็นเช่นนั้นฉันมั่นใจว่านักพัฒนา C ทุกคนมีกลอุบายที่ซ่อนอยู่ / ความลับที่พวกเขาใช้ตลอดเวลา
คำตอบ:
พอยน์เตอร์ฟังก์ชั่น คุณสามารถใช้ตารางของตัวชี้ฟังก์ชันเพื่อนำไปใช้เช่นล่ามโค้ดทางอ้อมอย่างรวดเร็ว (FORTH) หรือตัวกระจายรหัสไบต์หรือเพื่อจำลองวิธีเสมือน OO
จากนั้นจะมีอัญมณีที่ซ่อนอยู่ในไลบรารีมาตรฐานเช่น qsort (), bsearch (), strpbrk (), strcspn () [สองหลังหลังมีประโยชน์สำหรับการนำ Strtok () มาใช้แทน)
misfeature ของ C คือ overflow เลขคณิตที่ลงนามแล้วนั้นเป็นพฤติกรรมที่ไม่ได้กำหนด (UB) ดังนั้นเมื่อใดก็ตามที่คุณเห็นนิพจน์เช่น x + y ทั้งคู่มีการลงชื่อ int ก็อาจเกิดการล้นและทำให้เกิด UB
เคล็ดลับเพิ่มเติมเกี่ยวกับคอมไพเลอร์ GCC แต่คุณสามารถให้คำแนะนำการบ่งชี้สาขาแก่คอมไพเลอร์ (พบได้ทั่วไปในเคอร์เนล Linux)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
ดู: http://kerneltrap.org/node/4705
สิ่งที่ฉันชอบเกี่ยวกับเรื่องนี้คือมันยังเพิ่มความหมายบางอย่างให้กับฟังก์ชั่นบางอย่าง
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
สิ่งเหล่านี้เป็นรายการทางเลือกในมาตรฐาน แต่ต้องเป็นคุณลักษณะที่ซ่อนอยู่เนื่องจากผู้คนกำลังกำหนดใหม่อยู่ตลอดเวลา ฐานรหัสเดียวที่ฉันได้ทำไป (และยังทำอยู่ตอนนี้) มีการกำหนดซ้ำหลายครั้งทั้งหมดมีตัวระบุที่แตกต่างกัน ส่วนใหญ่แล้วมาพร้อมกับมาโครตัวประมวลผลล่วงหน้า:
#define INT16 short
#define INT32 long
และอื่น ๆ มันทำให้ฉันต้องการดึงผมออกมา เพียงใช้ typedefs จำนวนเต็มมาตรฐานที่บ้าแล้ว!
เครื่องหมายจุลภาคไม่ได้ใช้กันอย่างแพร่หลาย มันสามารถถูกทารุณกรรมได้อย่างแน่นอน แต่ก็มีประโยชน์มาก การใช้งานนี้เป็นสิ่งที่พบได้บ่อยที่สุด:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
แต่คุณสามารถใช้โอเปอเรเตอร์นี้ได้ทุกที่ สังเกต:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
แต่ละคำสั่งถูกประเมิน แต่ค่าของนิพจน์จะเป็นของคำสั่งสุดท้ายที่ประเมิน
เริ่มต้นโครงสร้างเป็นศูนย์
struct mystruct a = {0};
สิ่งนี้จะเป็นศูนย์องค์ประกอบโครงสร้างทั้งหมด
memset
/ calloc
do "all bytes zero" (เช่นศูนย์ทางกายภาพ) ซึ่งแน่นอนไม่ได้กำหนดไว้สำหรับทุกประเภท { 0 }
รับประกันได้ว่าจะทำให้ทุกอย่างลงตัวด้วยค่าศูนย์ตรรกะที่เหมาะสม ตัวชี้ตัวอย่างเช่นมีการค้ำประกันที่จะได้รับค่า null 0xBAADFOOD
ที่เหมาะสมของพวกเขาแม้ว่าโมฆะค่าบนแพลตฟอร์มที่ได้รับคือ
memset
ทำ (ด้วย0
อาร์กิวเมนต์ที่สอง) คุณได้รับศูนย์ตรรกะเมื่อคุณเริ่มต้น / กำหนด0
(หรือ{ 0 }
) ไปยังวัตถุในรหัสที่มา ศูนย์สองชนิดนี้ไม่จำเป็นต้องให้ผลลัพธ์เหมือนกัน เช่นเดียวกับในตัวอย่างด้วยตัวชี้ เมื่อคุณทำmemset
กับตัวชี้คุณจะได้รับ0x0000
ตัวชี้ แต่เมื่อคุณกำหนดให้0
กับตัวชี้คุณจะได้รับค่าตัวชี้ nullซึ่งในระดับกายภาพอาจเป็น0xBAADF00D
หรือสิ่งอื่นใด
double
. โดยปกติจะมีการใช้งานตามมาตรฐาน IEEE-754 ซึ่งเป็นศูนย์ตรรกะและศูนย์ทางกายภาพเหมือนกัน แต่ภาษานั้นไม่จำเป็นต้องใช้ IEEE-754 ดังนั้นจึงอาจเกิดขึ้นได้เมื่อคุณทำdouble d = 0;
(โลจิคัลศูนย์) ฟิสิคัลบิตบางส่วนในหน่วยความจำที่ครอบครองโดยd
จะไม่เป็นศูนย์
ค่าคงที่แบบหลายตัวอักษร:
int x = 'ABCD';
ชุดนี้x
เป็น0x41424344
(หรือ0x44434241
ขึ้นอยู่กับสถาปัตยกรรม)
แก้ไข:เทคนิคนี้ไม่ได้พกพาโดยเฉพาะอย่างยิ่งถ้าคุณเป็นอนุกรม int อย่างไรก็ตามมันมีประโยชน์อย่างยิ่งในการสร้าง enums เอกสารด้วยตนเอง เช่น
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
สิ่งนี้ทำให้ง่ายขึ้นมากถ้าคุณดูที่การถ่ายโอนข้อมูลหน่วยความจำดิบและจำเป็นต้องกำหนดค่าของ enum โดยไม่ต้องค้นหา
ฉันไม่เคยใช้บิตฟิลด์แต่มันฟังดูเจ๋งสำหรับสิ่งที่ต่ำมาก
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;
}
ซึ่งหมายความว่าสามารถมีขนาดเล็กเป็นsizeof(cat)
sizeof(char)
C มีคอมไพเลอร์มาตรฐาน แต่คอมไพเลอร์ C ทั้งหมดนั้นไม่ได้มาตรฐานอย่างสมบูรณ์ (ฉันยังไม่เห็นคอมไพเลอร์ C99 ที่เข้ากันได้เลย!)
ที่กล่าวว่าเทคนิคที่ฉันชอบคือสิ่งที่ไม่ชัดเจนและสามารถพกพาข้ามแพลตฟอร์มได้เนื่องจากพวกเขาใช้ความหมายของ C พวกเขามักจะเกี่ยวกับมาโครหรือเลขคณิตบิต
ตัวอย่างเช่น: การสลับสองจำนวนเต็มที่ไม่ได้ลงชื่อโดยไม่ใช้ตัวแปรชั่วคราว:
...
a ^= b ; b ^= a; a ^=b;
...
หรือ "การขยาย C" เพื่อแทนเครื่องสถานะ จำกัด เช่น:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
ที่สามารถทำได้ด้วยมาโครต่อไปนี้:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
โดยทั่วไปแล้วฉันไม่ชอบกลอุบายที่ฉลาด แต่ทำให้โค้ดมีความซับซ้อนเกินความจำเป็นในการอ่าน (เป็นตัวอย่างการสลับ) และฉันรักสิ่งที่ทำให้โค้ดชัดเจนขึ้นและถ่ายทอดเจตนาโดยตรง (เช่นตัวอย่างของ FSM) .
โครงสร้างพัวพันเช่นอุปกรณ์ของ 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);
}
}
ฉันชอบ initializers ที่กำหนดเพิ่มใน C99 (และรองรับเป็น gcc เป็นเวลานาน):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
การกำหนดค่าเริ่มต้นของอาร์เรย์ไม่ขึ้นอยู่กับตำแหน่งอีกต่อไป หากคุณเปลี่ยนค่าของ FOO หรือ BAR การเริ่มต้นอาร์เรย์จะสอดคล้องกับค่าใหม่โดยอัตโนมัติ
C99 มีโครงสร้างเริ่มต้นที่ยอดเยี่ยม
struct foo{
int x;
int y;
char* name;
};
void main(){
struct foo f = { .y = 23, .name = "awesome", .x = -38 };
}
โครงสร้างและอาร์เรย์ที่ไม่ระบุชื่อเป็นสิ่งที่ฉันโปรดปราน (cf. http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html )
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
หรือ
void myFunction(type* values) {
while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
มันยังสามารถใช้เพื่อสร้างรายการเชื่อมโยง ...
GCC มีจำนวนของส่วนขยายของภาษา C ที่ฉันชอบซึ่งสามารถพบได้ที่นี่ บางส่วนของรายการโปรดของฉันมีคุณลักษณะที่ฟังก์ชั่น ตัวอย่างหนึ่งที่มีประโยชน์อย่างยิ่งคือแอตทริบิวต์รูปแบบ สิ่งนี้สามารถใช้ถ้าคุณกำหนดฟังก์ชั่นที่กำหนดเองที่ใช้สตริงรูปแบบ printf หากคุณเปิดใช้งานคุณลักษณะของฟังก์ชั่นนี้ gcc จะทำการตรวจสอบข้อโต้แย้งของคุณเพื่อให้แน่ใจว่าสตริงรูปแบบและข้อโต้แย้งของคุณตรงกันและจะสร้างคำเตือนหรือข้อผิดพลาดตามความเหมาะสม
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
คุณสมบัติ (ซ่อนอยู่) ที่ "ทำให้ตกใจ" เมื่อฉันเห็นครั้งแรกเกี่ยวกับ printf คุณสมบัตินี้ช่วยให้คุณใช้ตัวแปรสำหรับการจัดรูปแบบตัวระบุรูปแบบเอง มองหารหัสคุณจะเห็นดีกว่า:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
อักขระ * บรรลุเอฟเฟกต์นี้
ดี ... ฉันคิดว่าหนึ่งในจุดแข็งของภาษา C คือความสะดวกในการพกพาและความเป็นมาตรฐานดังนั้นเมื่อใดก็ตามที่ฉันพบ "เคล็ดลับที่ซ่อนอยู่" ในการใช้งานที่ฉันใช้อยู่ในปัจจุบันฉันพยายามไม่ใช้เพราะพยายามเก็บไว้ รหัส C เป็นมาตรฐานและพกพาได้มากที่สุด
ยืนยันเวลารวบรวมเป็นที่กล่าวถึงอยู่ที่นี่แล้ว
//--- 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);
การต่อสตริงอย่างต่อเนื่อง
ฉันค่อนข้างประหลาดใจที่ไม่เห็นมันทั้งหมดในคำตอบเนื่องจากคอมไพเลอร์ทั้งหมดที่ฉันรู้จักสนับสนุน แต่โปรแกรมเมอร์จำนวนมากดูเหมือนจะเพิกเฉย บางครั้งมันมีประโยชน์จริงๆและไม่เพียง แต่เมื่อเขียนมาโคร
ใช้กรณีที่ฉันมีในรหัสปัจจุบันของฉัน: ฉันมี#define PATH "/some/path/"
ในไฟล์การกำหนดค่า (จริงๆมันถูก setted โดย makefile) ตอนนี้ฉันต้องการสร้างเส้นทางแบบเต็มรวมถึงชื่อไฟล์เพื่อเปิดแหล่งข้อมูล มันไปที่:
fd = open(PATH "/file", flags);
แทนที่จะน่ากลัว แต่เป็นเรื่องธรรมดามาก:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
ขอให้สังเกตว่าทางออกที่น่ากลัวทั่วไปคือ:
ฉันไม่เคยใช้มันและฉันก็ไม่แน่ใจว่าจะแนะนำให้กับทุกคนหรือไม่ แต่ฉันรู้สึกว่าคำถามนี้จะไม่สมบูรณ์หากไม่ได้กล่าวถึงเคล็ดลับร่วมประจำของ Simon Tatham
เมื่อเริ่มต้นอาร์เรย์หรือ enums คุณสามารถใส่เครื่องหมายจุลภาคหลังรายการสุดท้ายในรายการ initializer เช่น:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
การทำเช่นนี้จะช่วยให้คุณไม่ต้องกังวลกับการลบเครื่องหมายจุลภาคสุดท้าย
การกำหนดโครงสร้างนั้นยอดเยี่ยม หลายคนดูเหมือนจะไม่ตระหนักว่า structs เป็นค่านิยมเช่นกันและสามารถกำหนดได้โดยไม่จำเป็นต้องใช้memcpy()
เมื่อการมอบหมายอย่างง่ายทำกลอุบาย
ตัวอย่างเช่นลองพิจารณาห้องสมุดกราฟิก 2D ในจินตนาการมันอาจกำหนดประเภทเพื่อแสดงพิกัดหน้าจอ (จำนวนเต็ม):
typedef struct {
int x;
int y;
} Point;
ตอนนี้คุณทำสิ่งที่อาจดูเหมือน "ผิดปกติ" เช่นเขียนฟังก์ชั่นที่สร้างจุดเริ่มต้นจากอาร์กิวเมนต์ของฟังก์ชันและส่งคืนดังนี้:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
มีความปลอดภัยตราบเท่าที่ (แน่นอน) เนื่องจากค่าส่งคืนถูกคัดลอกโดยค่าโดยใช้การกำหนด struct:
Point origin;
origin = point_new(0, 0);
ด้วยวิธีนี้คุณสามารถเขียนโค้ดที่ค่อนข้างสะอาดและ object-oriented-ish ทั้งหมดนี้เป็นมาตรฐานธรรมดา C
การจัดทำดัชนีเวกเตอร์แปลก ๆ :
int v[100]; int index = 10;
/* v[index] it's the same thing as index[v] */
คอมไพเลอร์ C ใช้หนึ่งในหลายมาตรฐาน อย่างไรก็ตามการมีมาตรฐานไม่ได้หมายความว่าทุกแง่มุมของภาษาจะถูกกำหนดไว้ อุปกรณ์ของดัฟฟ์ยกตัวอย่างเช่นนั้นเป็นฟีเจอร์ที่ 'ซ่อน' ซึ่งเป็นที่ชื่นชอบซึ่งได้รับความนิยมอย่างมากในขณะที่คอมไพเลอร์สมัยใหม่มีรหัสการจดจำวัตถุประสงค์พิเศษเพื่อให้แน่ใจว่าเทคนิคการปรับให้เหมาะสมจะไม่บดบัง
โดยทั่วไปคุณสมบัติที่ซ่อนอยู่หรือกลเม็ดภาษาจะหมดกำลังใจเมื่อคุณใช้งานบนขอบใบมีดโกนของ C มาตรฐานใด ๆ ที่คอมไพเลอร์ของคุณใช้ เทคนิคดังกล่าวจำนวนมากไม่ทำงานจากคอมไพเลอร์หนึ่งไปยังอีกและบ่อยครั้งที่ฟีเจอร์เหล่านี้จะล้มเหลวจากคอมไพเลอร์สวีทเวอร์ชันหนึ่งโดยผู้ผลิตที่กำหนดให้เป็นเวอร์ชันอื่น
เทคนิคต่าง ๆ ที่มีรหัส C แตกรวมถึง:
ปัญหาและปัญหาอื่น ๆ ที่เกิดขึ้นเมื่อโปรแกรมเมอร์กำหนดสมมติฐานเกี่ยวกับแบบจำลองการดำเนินการที่ระบุไว้ในมาตรฐาน C ส่วนใหญ่เป็นพฤติกรรม 'ขึ้นอยู่กับคอมไพเลอร์'
เมื่อใช้ sscanf คุณสามารถใช้% n เพื่อค้นหาตำแหน่งที่คุณควรอ่านต่อ:
sscanf ( string, "%d%n", &number, &length );
string += length;
เห็นได้ชัดว่าคุณไม่สามารถเพิ่มคำตอบอื่นได้ดังนั้นฉันจะรวมคำตอบที่สองที่นี่คุณสามารถใช้ "&&" และ "||" ตามเงื่อนไข:
#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 );
}
รหัสนี้จะส่งออก:
สวัสดี ROFL
การใช้ INT (3) เพื่อกำหนดจุดพักที่รหัสคือสิ่งที่ฉันโปรดปรานตลอดเวลา
คุณสมบัติ "ซ่อน" ที่ฉันโปรดปรานของ C คือการใช้% n ใน printf เพื่อเขียนกลับไปที่สแต็ก โดยปกติ printf จะแสดงค่าพารามิเตอร์จากสแต็กตามสตริงรูปแบบ แต่% n สามารถเขียนกลับมาได้
ตรวจสอบส่วน 3.4.2 ที่นี่ สามารถนำไปสู่ช่องโหว่ที่น่ารังเกียจมากมาย
การรวบรวมเวลาตรวจสอบสมมติฐานโดยใช้ enums: ตัวอย่างโง่ ๆ แต่มีประโยชน์จริง ๆ สำหรับไลบรารีที่มีค่าคงที่ที่กำหนดเวลารวบรวมได้
#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) มีฟีเจอร์สนุก ๆ ที่คุณสามารถเปิดใช้งานได้เช่นการประกาศฟังก์ชันที่ซ้อนกันและรูปแบบ?: b ของตัวดำเนินการ?: ซึ่งจะส่งกลับ a ถ้าไม่ใช่เท็จ
ฉันค้นพบ 0 bitfields เร็ว ๆ นี้
struct {
int a:3;
int b:2;
int :0;
int c:4;
int d:3;
};
ซึ่งจะให้รูปแบบของ
000aaabb 0ccccddd
แทนที่จะเป็น: 0;
0000aaab bccccddd
ฟิลด์ความกว้าง 0 บอกว่าบิตฟิลด์ต่อไปนี้ควรถูกตั้งค่าบนเอนทิตีอะตอมถัดไป (char
)
แมโครอาร์กิวเมนต์ตัวแปร C99 สไตล์หรือที่รู้จัก
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
ซึ่งจะใช้เช่น
ERR(errCantOpen, "File %s cannot be opened", filename);
ที่นี่ฉันยังใช้ตัวดำเนินการ stringize และ string concatentation อย่างต่อเนื่องคุณสมบัติอื่น ๆ ที่ฉันชอบ
ตัวแปรอัตโนมัติขนาดตัวแปรยังมีประโยชน์ในบางกรณี เหล่านี้ถูกเพิ่มเข้ามาฉัน nC99 และได้รับการสนับสนุนใน gcc เป็นเวลานาน
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
คุณจบลงด้วยบัฟเฟอร์บนสแต็คที่มีห้องพักสำหรับส่วนหัวของโปรโตคอลขนาดคงที่รวมถึงข้อมูลขนาดตัวแปร คุณสามารถได้รับผลกระทบเดียวกันกับ alloca () แต่ไวยากรณ์นี้มีขนาดเล็กกว่า
คุณต้องตรวจสอบให้แน่ใจว่า extraPadding เป็นค่าที่เหมาะสมก่อนที่จะเรียกรูทีนนี้หรือคุณจบการสแต็ก คุณต้องมีสติตรวจสอบข้อโต้แย้งก่อนโทร malloc หรือเทคนิคการจัดสรรหน่วยความจำอื่น ๆ ดังนั้นจึงไม่ผิดปกติ