1、常见地址解释
-
0xABADCAFE:初始化所有空闲内存以捕获错误指针的启动值
-
0xBAADF00D:由Microsoft的LocalAlloc(LMEM_FIXED)用于标记未初始化的已分配堆内存
-
0xBADCAB1E:当断开与调试器的连接时返回给Microsoft eVC调试器的错误码
-
0xBEEFCACE:被Microsoft .NET用作资源文件中的magic number
-
0xCCCCCCCC:由Microsoft的c++调试运行库用于标记未初始化的堆栈内存
由Microsoft的c++调试运行库用于标记未初始化的堆内存 -
0xDEADDEAD:当用户手动启动崩溃时使用的Microsoft Windows STOP错误代码
-
0xFDFDFDFD:用于微软的c++调试堆标记“无人之地”保护字节在分配堆内存之前和之后
-
0xFEEEFEEE:由微软的HeapFree()用来标记释放的堆内存
对于0xcccccccc和0xcdcdcdcd,在 Debug 模式下,VC 会把未初始化的栈内存上的指针全部填成 0xcccccccc ,当字符串看就是 “烫烫烫烫……”;会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd,当字符串看就是 “屯屯屯屯……”。那么调试器为什么要这么做呢?VC的DEBUG版会把未初始化的指针自动初始化为0xcccccccc或0xcdcdcdcd,而不是就让取随机值,那是为了方便我们调试程序,如果野指针的初值不确定,那么每次调试同一个程序就可能出现不一样的结果,比如这次程序崩掉,下次却能正常运行,这样显然对我们解bug是非常不利的,所以自动初始化的目的是为了让我们一眼就能确定我们使用了未初始化的野指针了。
对于0xfeeefeee,是用来标记堆上已经释放掉的内存。注意,如果指针指向的内存被释放了,变量本身的地址如未做改动,还是之前指向的内存的地址。如果该指针是一个类的指针,并且类中包含有指针变量,则内存被释放后(对于C++类,通常是执行delete操作),类中的指针变量就会被赋值为0xfeeefeee。如果早调试代码过程中,发现有值为0xfeeefeee的指针,就说明对应的内存被释放掉了,我们的代码已经出问题了。
在c代码中,有时候需要对具体内存进行判断,通过指针地址与0xFEEEFEEE的比对,指定代码使用刚释放的堆内存。
2、利用指针地址偏移填结构体值
void *data = malloc(sizeof(int) * 4);
*(int*)(data + sizeof(int) * 0) = 1;
*(int*)(data + sizeof(int) * 1) = 2;
*(int*)(data + sizeof(int) * 2) = 3;
*(int*)(data + sizeof(int) * 3) = 4;
printf("%d\t%d\n", *(int*)data, *(int*)(data + sizeof(int) * 3));
这里的int可以转化成任意的struct,这样做的好处是不需要在代码中额外定义struct来赋值,只需要一个地址,一般是使用内存池开辟一块空间。
3、矩阵相乘
随机产生一个mn的100以内的正整数矩阵,输出这个矩阵,再随机产生一个no的100以内的正整数矩阵,输出这个矩阵,求这两个矩阵的乘积,放在一个新的矩阵中,输出所得矩阵。(m、n、o由键盘输入,需要判断m、n、o的合法性)
#include<stdio.h>
#include<time.h>
#define MATRIX_MAXSIXE 10
int main()
{
//定义三个矩阵
int a[MATRIX_MAXSIXE][MATRIX_MAXSIXE],b[MATRIX_MAXSIXE][MATRIX_MAXSIXE],c[MATRIX_MAXSIXE][MATRIX_MAXSIXE];
//用于输入两个矩阵的行和列
int m,n,p,q;
//用于for循环
int i,j,k;
//输入第一个矩阵的行和列
printf("Enter rows and columns of first matrix:");
scanf("%d%d",&m,&n);
输入第二个矩阵的行和列
printf("Enter rows and columns of second matrix:");
scanf("%d%d",&p,&q);
//矩阵容错
if(m==0 || n==0 || p==0 || q==0
|| m>MATRIX_MAXSIXE || n>MATRIX_MAXSIXE
|| p>MATRIX_MAXSIXE || q>MATRIX_MAXSIXE)
{
printf("\nSorry!!!! Matrix multiplication can't be done");
return 0;
}
//求乘的两个矩阵满足条件:第一个矩阵的列为第二个矩阵的行
if(n==p)
{
printf("\nfirst matrix:\n");
for(i=0;i<m;++i)
{
for(j=0;j<n;++j)
{
printf("%3d",a[i][j]=rand()%100);
}
printf("\n");
}
printf("\nsecond matrix:\n");
for(i=0;i<p;++i)
{
for(j=0;j<q;++j)
{
printf("%3d",b[i][j]=rand()%100);
}
printf("\n");
}
printf("\nThe new matrix is:\n");
for(i=0;i<m;++i)
{
for(j=0;j<q;++j)
{
c[i][j]=0;
for(k=0;k<n;++k)
{
c[i][j]=c[i][j]+(a[i][k]*b[k][j]);
printf("%d ",c[i][j]);
}
}
printf("\n");
}
}
//否则,输出矩阵不能相乘
else
printf("\nSorry!!!! Matrix multiplication can't be done");
return 0;
}
4、C语言位域
位域的定义和使用
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:
struct bs
{
int a:8; // 这里8后可以跟 , 或 ; 都是合法的。
int b:2;
int c:6;
};
5、运行时修改代码
#pragma comment(linker,"/SECTION:.text,RW")
#include <stdio.h>
#ifdef _DEBUG
#define OFFSET 0x0C
#else
#define OFFSET 0x01
#endif
int *p;
int p2() {
int a;
a=2;
return a;
}
int main() {
p=(int *)((char *)p2+OFFSET);
printf("p2==0x%08x,p==0x%08x,*p==%d\n",(char *)p2,p,*p);
*p=3;
printf("p2()==%d\n",p2());
return 0;
}
//p2==0x00401000,p==0x0040100c,*p==2
//p2()==3
//
6、确定被调用的宿主问题
#include <stdio.h>
void whocallme();
void fun1() {
whocallme();
}
void fun2() {
whocallme();
}
void fun3() {
fun1();
}
void whocallme() {
int *_esp,i;
__asm {
mov eax,esp
mov _esp,eax
}
printf("\nfun1,fun2,fun3,_esp=%08x,%08x,%08x,%08x\n",(int)fun1,(int)fun2,(int)fun3,(int)_esp);
for (i=0;i<100;i++) if (_esp[i]==(int)_esp) break;
if (i<100) {
printf("ret addr=%08x\n",_esp[i+2]);
if ((int)fun1<=_esp[i+2] && _esp[i+2]<(int)fun2) printf("fun1 callme\n");
if ((int)fun2<=_esp[i+2] && _esp[i+2]<(int)fun3) printf("fun2 callme\n");
}
}
void main() {
fun2();
fun1();
}
//C:\tmp\tmp\Debug>tmp
//
//fun1,fun2,fun3,_esp=00401000,00401020,00401040,0012fe84
//ret addr=0040102e
//fun2 callme
//
//fun1,fun2,fun3,_esp=00401000,00401020,00401040,0012fe84
//ret addr=0040100e
//fun1 callme
//
//C:\tmp\tmp\Debug>cd ..\release
//
//C:\tmp\tmp\Release>tmp
//
//fun1,fun2,fun3,_esp=00401000,0040100a,00401014,0012ff5c
//ret addr=00401012
//fun2 callme
//
//fun1,fun2,fun3,_esp=00401000,0040100a,00401014,0012ff5c
//ret addr=00401008
//fun1 callme
//
7、while
尽量不要使用
while (条件)
更不要使用
while (组合条件)
要使用
while (1) {
if (条件1) break;
//...
if (条件2) continue;
//...
if (条件3) return;
//...
}
因为前两种写法在语言表达意思的层面上有二义性,只有第三种才忠实反映了程序流的实际情况。
典型如:
下面两段的语义都是当文件未结束时读字符
while (!feof(f)) {
a=fgetc(f);
//...
b=fgetc(f);//可能此时已经feof了!
//...
}
而这样写就没有问题:
while (1) {
a=fgetc(f);
if (feof(f)) break;
//...
b=fgetc(f);
if (feof(f)) break;
//...
}
类似的例子还可以举很多。
8、结构体初始化
Linux Kernel的例子
static struct usb_driver usb_storage_driver = {
.owner = THIS_MODULE,
.name = "usb-storage",
.probe = storage_probe,
.disconnect = storage_disconnect,
.id_table = storage_usb_ids,
};
9、c鲜为人知的运算符<----
#include<stdio.h>
int main()
{
int x = 10;
while (0<----x)
{
printf("%d",x);
}
return 0;
}
8 6 4 2
#include <stdio.h>
int main(int argc, char** argv) {
int x = 10;
while (x --> 0) {
printf("%d ", x);
}
return 0;
}
9 8 7 6 5 4 3 2 1 0
10、glibc中高度优化的strlen
正常的strlen
size_t strlen(const char *p) {
const char *s = p;
while (*p) p++;
return (size_t)(p - s);
}
glibc中高度优化的strlen。可以一次性计算 8 个字节内有没有包含 0,写性能极致优化的代码,要考虑这些局部优化的手段。
size_t strlen (const char *str) {
const char *char_ptr;
const unsigned long int *longword_ptr;
unsigned long int longword, himagic, lomagic;
for (char_ptr = str; ((unsigned long int) char_ptr & (sizeof (longword) - 1)) != 0; ++char_ptr)
if (*char_ptr == '\0')
return char_ptr - str;
longword_ptr = (unsigned long int *) char_ptr;
himagic = 0x80808080L;
lomagic = 0x01010101L;
if (sizeof (longword) > 4) {
himagic = ((himagic << 16) << 16) | himagic;
lomagic = ((lomagic << 16) << 16) | lomagic;
}
for (;;) {
longword = *longword_ptr++;
if (((longword - lomagic) & ~longword & himagic) != 0) {
const char *cp = (const char *) (longword_ptr - 1);
if (cp[0] == 0) return cp - str;
if (cp[1] == 0) return cp - str + 1;
if (cp[2] == 0) return cp - str + 2;
if (cp[3] == 0) return cp - str + 3;
if (sizeof (longword) > 4) {
if (cp[4] == 0) return cp - str + 4;
if (cp[5] == 0) return cp - str + 5;
if (cp[6] == 0) return cp - str + 6;
if (cp[7] == 0) return cp - str + 7;
}
}
}
}
11、快速范围判断
if (j >= min && j <= max)
存在的两次分支判断,可以减少为一次,写成:
if ((int32_t)((j - min) | (max - j)) >= 0)
进一步多个变量范围判断还可以继续优化成,注意下面都是无符号整数:
if ( ( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0)
让四次判断减少为 1 次判断,这背后的道理很简单,当范围是变量时,多算两次减法完全不影响,但是多一两次判断,对性能的影响是很大的 https://godbolt.org/z/eN_GK-
链接:https://zhuanlan.zhihu.com/p/147039093
12、整数快速除以255
整数快速除以 255 这个事情非常常见,例如图像绘制/合成,音频处理,混音计算等。网上很多比特技巧,
#define div_255_fast(x) (((x) + (((x) + 257) >> 8)) >> 8)
当 x 属于 [0, 65536] 范围内,该方法的误差为 0。过去不少人简略的直接用 >> 8 来代替,然而这样做会有误差,连续用 >>8 代替 / 255 十次,误差就累计到 10 了。上面的宏可以方便的处理 8-16 位整数的 /255 计算,经过测试 65536000 次计算中,使用 /255的时间是 325ms,使用div_255_fast的时间是70ms,使用 >>8 的时间是 62ms,div_255_fast 的时间代价几乎可以忽略。进一步可以用 SIMD 写成:// (x + ((x + 257) >> 8)) >> 8
static inline __m128i _mm_fast_div_255_epu16(__m128i x) {
return _mm_srli_epi16(_mm_adds_epu16(x,
_mm_srli_epi16(_mm_adds_epu16(x, _mm_set1_epi16(0x0101)), 8)), 8);
}
这样可以同时对 8 对 16 bit 的整数进行 / 255 运算,照葫芦画瓢,还可以改出一个 / 65535 ,或者 / 32767 的版本来。对于任意大于零的整数,他人总结过定点数的方法,x86 跑着一般,x64 下还行:
static inline uint32_t fast_div_255_any (uint32_t n) {
uint64_t M = (((uint64_t)1) << 32) / 255; // 用 32.32 的定点数表示 1/255
return (M * n) >> 32; // 定点数乘法:n * (1/255)
}
这个在所有整数范围内都有效,但是精度有些不够,所以要把 32.32 的精度换成 24.40 的精度,并做一些四舍五入和补位:static
inline uint32_t fast_div_255_accurate (uint32_t n) {
uint64_t M = (((uint64_t)1) << 40) / 255 + 1; // 用 24.40 的定点数表示 1/255
return (M * n) >> 40; // 定点数乘法:n * (1/255)
}
该方法能够覆盖所有 32 位的整数且没有误差,有些编译器对于常数整除,已经可以生成类似 fast_div_255_accurate 的代码了,整数除法是现代计算机最慢的一项工作,动不动就要消耗 30 个周期,常数低的除法除了二次幂的底可以直接移位外,编译器一般会用定点数乘法模拟除法。编译器生成的常数整除代码主要是使用了 64 位整数运算,以及乘法,略显复杂,对普通 32 位程序并不是十分友好。因此如果整数范围属于 [0, 65536] 第一个版本代价最低。
13、浮点数格式C代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
int main() {
float f;
double d;
char bs[65];
char b[65];
char s[80];
unsigned char *p;
char e[12];
char *t;
int ex;
while (1) {
printf("Input a float point number:");fflush(stdout);
rewind(stdin);
fgets(s,80,stdin);
if (1==sscanf(s,"%f",&f) && 1==sscanf(s,"%lf",&d)) break;
}
printf("f=%g\n",f);
p=(unsigned char *)&f;
printf("hex=%02X %02X %02X %02X\n",p[3],p[2],p[1],p[0]);
ltoa(*(long *)&f,b,2);
sprintf(bs,"%032s",b);
printf("bin=%s\n",bs);
printf("bin=%.1s %.8s %s\n",bs,bs+1,bs+9);
strncpy(e,bs+1,8);e[8]=0;
ex=strtol(e,&t,2);
printf(" %c %-4d-127 1.%s\n",(bs[0]=='0')?'+':'-',ex,bs+9);
ex-=127;
printf(" %c %-8d 1.%s\n",(bs[0]=='0')?'+':'-',ex,bs+9);
printf("\nd=%lg\n",d);
p=(unsigned char *)&d;
printf("hex=%02X %02X %02X %02X %02X %02X %02X %02X\n",p[7],p[6],p[5],p[4],p[3],p[2],p[1],p[0]);
_i64toa(*(__int64 *)&d,b,2);
sprintf(bs,"%064s",b);
printf("bin=%s\n",bs);
printf("bin=%.1s %.11s %s\n",bs,bs+1,bs+12);
strncpy(e,bs+1,11);e[11]=0;
ex=strtol(e,&t,2);
printf(" %c %-6d-1023 1.%s\n",(bs[0]=='0')?'+':'-',ex,bs+12);
ex-=1023;
printf(" %c %-11d 1.%s\n",(bs[0]=='0')?'+':'-',ex,bs+12);
return 0;
}
//Input a float point number:0.125
//f=0.125
//hex=3E 00 00 00
//bin=00111110000000000000000000000000
//bin=0 01111100 00000000000000000000000
// + 124 -127 1.00000000000000000000000
// + -3 1.00000000000000000000000
//
//d=0.125
//hex=3F C0 00 00 00 00 00 00
//bin=0011111111000000000000000000000000000000000000000000000000000000
//bin=0 01111111100 0000000000000000000000000000000000000000000000000000
// + 1020 -1023 1.0000000000000000000000000000000000000000000000000000
// + -3 1.0000000000000000000000000000000000000000000000000000
//
14、谁来跟我挑战效率?从文件d.txt中逐个读出能读的浮点数,比如"1.0,3.5,2.2 …"
#include <stdio.h>
int n,r;
double d;
FILE *f;
void main() {
f=fopen("d.txt","r");
n=0;
while (1) {
r=fscanf(f,"%lf",&d);
if (1==r) {
n++;
printf("[%d]==%lg\n",n,d);//可以试试注释掉这句以后的速度
} else if (0==r) {
fscanf(f,"%*c");
} else break;
}
fclose(f);
}
15、^L
例如,main在hello.c GNU中,Hello包的结尾如下:
#include<stdio.h>
int main()
{
exit (EXIT_SUCCESS);
}
^L
从字面上看,它是一个分页符(“换页”)字符。编译器将其视为普通的空格。但它对于打印源代码非常有用 - 它会启动一个新页面(例如,^L在函数之间使用以强制每个调用获取自己的页面)。
在基于Vim / vi的编辑器中,您可以通过键入Ctrl+ V后跟Ctrl+ 来在编辑模式下插入这样的字符L。它看起来像^L在编辑器中,但它实际上只是一个字符(ASCII值:12或0x0C)。
16、受 SQLite 多年青睐,C 语言到底好在哪儿?
SQLite 近日发表了一篇博文,解释了为什么多年来 SQLite 一直坚持用 C 语言来实现,以下是正文内容:
C 语言是最佳选择
从2000年5月29日发布至今,SQLite 一直都是用 C 语言实现。C 一直是实现像 SQLite 这类软件库的最佳语言。目前,还没有任何计划要采用另外一门语言对 SQLite 进行重新开发。
为什么 C 语言是实现 SQLite 的最佳选择?原因主要体现在这几个方面:
性能
兼容性
低依赖性
稳定性
1、性能
像 SQLite 这类库要求速度必须要快。SQLite 的速度就很快,它比文件系统快 35%(详情可以参考这两个示例:Internal Versus External BLOBs 和 35% Faster Than The Filesystem)。
而 C 语言就能实现快速编写代码。C 语言通常被描述为“可移植性的汇编语言”。它使开发人员能够尽可能靠近底层硬件进行编码,同时仍然可以跨平台保持可移植性。
平常,我们可能会看到有人描述某种语言“像 C 语言一样快”,却不会看到有人说,作为通用目的编程时,会有一门语言“比 C 语言快”,因为这种语言真的不存在。
2、兼容性
几乎所有系统都能调用 C 语言编写的库,但其他语言就不尽然。例如,用 Java 编写的 Android 应用能够调用 SQLite(通过适配器)。 如果用 Java 编写 SQLite,那么对 Android 来说可能会更方便,因为这会使接口更简单。但在 iPhone 上,应用程序是用 Objective-C 或 Swift 编写的,它们都不能调用用 Java 编写的库。 因此,如果用 Java 编写,SQLite 将无法在 iPhone 上使用。
3、低依赖性
用 C 语言编写的库对运行时没有很强的依赖。SQLite 的最低配置也只要求 C 库中的这些方法:
memcmp()
memcpy()
memmove()
memset()
strcmp()
strlen()
strncmp()
在更完整的构建中,SQLite 也使用诸如 malloc() 和 free() 之类的库例程以及用于打开,读取,写入和关闭文件的操作系统接口。 但即便如此,依赖的数量也很少。
4、稳定性
C 语言易于理解,契合了 SQLite 的要求,适合 SQLite 的开发。
为什么 SQLite 不使用面向对象的语言?
开发人员可能无法想象用“非面向对象”来开发一个像 SQLite 这样复杂的系统会是什么样子。所以 SQLite 为什么不使用 C++ 或者 Java 来开发呢?
1、用 C++ 或 Java 编写的库通常只能由以相同语言编写的应用程序使用。 使用 Haskell 或 Java 编写的应用程序很难调用用 C++ 编写的库。 另一方面,用 C 语言编写的库可以从任何编程语言调用。
2、面向对象是设计模式,而不是编程语言。 你可以使用任何所需语言(包括汇编语言)进行面向对象编程。 某些语言(例如:C++ 或 Java)可以使面向对象更容易,但你仍然可以用像 C 这样的语言进行面向对象的编程。
3、面向对象不是唯一有效的设计模式。对象通常是分解问题的好方法。 但不是唯一的方法,也不总是分解问题的最佳方法。 有时好的旧程序代码更容易编写,更易于维护和理解,并且比面向对象的代码更快。
4、SQLite 进行开发时,Java 还不是一门成熟的语言,C++ 会成熟一点,但当时要找到两种能以 相同方式工作的 C++ 编译器比较困难。相比之下,C 语言是个不错的选择。虽然,这种情况现在有所改善,但为此对 SQLite 重新开发并没有什么好处。
为什么 SQLite 不使用"安全"语言编写?
使用“安全”语言不易发生内存泄露、数组溢出等的安全问题。最近,许多人好像对 Rust 和 Go 这样的“安全”语言感兴趣。但 SQLite 为什么不使用呢?
1、SQLite 出现后的 10 年时间里,所谓的“安全”语言还不存在。虽然 SQLite 可以用 Rust 或者 Go 重新编写,但这样可能会引入更多难以修复的 Bug,进而会影响编码速度。
2、“安全”编程语言解决简单的问题:像内存泄露、数组溢出等。在解决 SQL 计算结果这类的问题上,并不如 C 语言好用。
3、“安全”语言可防止安全漏洞,但 SQLite 并非一个对安全敏感的库。如果应用运行了不受信任的 SQL,那它可能已经存在更大的安全问题,而这是“安全”语言无法修复的问题。
4、一些“安全”语言(如 Go 语言)不喜欢使用 assert(),但这是保持 SQLite 可维护性的重要前提。
5、“安全”语言会插入额外的机器分支来执行其他操作。但在正确的代码中,这些分支并不会被采用。所以机器代码不能 100% 被测试到,可这恰恰是 SQLite 质量检测的重要组成部分。
6、“安全”语言会在内存不足(OOM)时请求终止,而 SQLite 的设计是遇到 OOM 时能重新恢复。目前,还不知道如何利用“安全”语言实现这一点。
7、现有的“安全”语言都比较新,SQLite 开发员对它们的出现表示赞赏,但依然认为 C 语言更适合目前的开发工作。
文章最后表示,SQLite 可能会考虑使用 Rust 重新开发,但不太可能使用 Go 语言,因为它对 assert() 不友好。但其实 Rust 目前的条件并不足以对 SQLite 进行重新开发,它还需要继续发展,详情请查看原文。