三十七.面试分析
1.指针运算
#include <stdio.h> void main() { int TestArray[5][5] = { {11,12,13,14,15}, {16,17,18,19,20}, {21,22,23,24,25}, {26,27,28,29,30}, {31,32,33,34,35} }; int* p1 = (int*)(&TestArray + 1);// int* p2 = (int*)(*(TestArray + 1) + 6); printf("Result: %d; %d; %d; %d; %d\n", *(*TestArray), *(*(TestArray + 1)), *(*(TestArray + 3) + 3), p1[-8], p2[4]); }
输出结果:
Result: 11; 16; 29; 28; 26
考点:数组名的指针运算:p + n;<---------->(unsigned int)p+n*sizeof(*p);p1指向这个二维数组后的地址
a和&a的区别: a为数组是数组首元素的地址 &a为整个数组的地址 a和&a的意义不同其区别在于指针运算 a+1--->(unsigned int)a + sizeof(*a) &a+1---->(unsigned int)(&a) + sizeof(*&a)=(unsigend int)(&a) + sizeof(a)
二维数组在内存中是以一维数组形式存放,*(TestArray + 1)代表第二个数组的地址,*(TestArray
+ 1)+6是指向后移动六个单位
2.调试经验
#include<stdio.h> void main() { char* p = "hello world!"; int a = (int)p; short s = 'c'; printf("%c\n", (long)(*((int*)p))); printf("%s\n", a); printf("%s\n", &s); }
输出:
h hello world! c
1.p是字符串的首地址,p是一个32位的整数,是一个地址值,(int*)p转化成为一个指向整数的指针,*((int*)p)取出从这个地址的开始的四个字节,得到hell,(printf丢失类型信息)这个值在转化成为long型,以c来解析,就是h
2.以字符串的类型,打印a代表的地址开头的字符串
3.以字符串类型打印,&s所代表的地址开始的字符串,short是两个字节,c占一个字节,另外一个字节是0,所以只打印c(和大小端有关系)
总结:可变参数的函数,无法对参数的类型进行检测,要靠传入的参数来来解析
3.安全编程
#include<stdio.h> int main(int argc, char *argv[]) { int flag = 0; char passwd[10]; memset(passwd,0,sizeof(passwd)); strcpy(passwd, argv[1]); if(0 == strcmp("LinuxGeek", passwd)) { flag = 1; } if( flag ) { printf("\n Password cracked \n"); } else { printf("\n Incorrect passwd \n"); } return 0; }
解析:输入超过10字节的密码就会密码正确,因为函数栈上是从高地址到低地址增长,strcpy复制超过10个字符会覆盖flag的值
安全性总结:
1.检查传入参数
2.检查函数调用中的返回值是否正确
3.assert断言
4.使用带n的字符串库函数,例如strncpy代替strcpy
5.不要使用会有副作用的表达式(例如一个表达式中同个变量多次使用后++),不要使用在不同编译器会有不同运算顺序的表达式。
答疑:
1.
#include<stdio.h> int main() { printf("%f\n",5); printf("%d\n",5.01); } 输出:两个都不是 5
第一 printf是一个可变参数函数 我在课程中说过可变参数函数中变化的部分 是没有参数类型信息的 因此printf中的 "%d" "%c" 等等是用于标识参数类型的
第二 在计算机中 整数 和 浮点数的 内部表示不同,并且 在C中 5 默认是int 的 5.01默认是double的 它们的内部表示截然不同 然而
printf("%f\n",5); 这里用浮点的方式解释一个int的表示
printf("%d\n",5.01); 这里用整数的方式解释一个float的表示
2.
#include <stdio.h> int main() { int k; while(1) { scanf("%d",&k); printf("%d\n",k); if(k==2) break; } return 0; }
解析:
修改成:
#include <stdio.h> int main() { int k=9; while(1) { scanf("%d",&k); printf("%d\n",k); if(k==2) break; fflush(stdin); } return 0;
总结:
scanf()函数接收输入数据时,遇以下情况结束一个数据的输入:(不是结束该scanf函数,scanf函数仅在每一个数据域均有数据,并按回车后结束)。
① 遇空格、“回车”、“跳格”键。
② 遇宽度结束。
③ 遇非法输入
3.关于const和volatile
const和volatile是可以同时修饰一个变量的 ,const只是表示变量只读,不能出现在赋值号左边,防止程序“意外”修改,并且编译器一定会做优化 ,
不会每次去内存取值 。这个时候如果外部事件如中断服务程序 ,改了这个变量的内存值 ,那么由于编译器优化就不会出有反应,这样会导致错误。
加上volatile就告诉编译器,不要做任何优化,并且每次都去内存取值 而且这个变量不可以当左值使用
void f() { const int count = 5; int i = 0; for(i=0; i<count; i++) { sleep(500); } }
对于这个程序,是不是可以确认 这个循环一定做5次呢?
现代编译器对于这个情况一定是5次,在编译的时候,编译器就觉得都是const的了,在这个函数中又不可能改变值了,直接用5代替了
所有的局部变量都是在内存中而已 他们都有地址,对于像单片机这样子无操作系统的嵌入式程序,局部变量的地址甚至可以直接“计算”得到
4.sizeof
看下面的程序,输出是什么?
int f() { static int i = 0; i++; return i; } int main() { int j = sizeof(f()); printf("%d\n", f()); return 0; }
我强调的是sizeof不是函数 而是关键字 是编译器用的关键字 因此其值在编译期就确定了,所以第一个f() 没有调用
f()只被调用了一次,而f返回值的长度 在编译时就可以确定 不需要等到运行时
5.系统对齐
32位系统默认4字节对齐,64为系统默认8字节对齐
#include<stdio.h> struct TestC { char a; char mac[6]; }cc; struct TestD { char a; char mac[6]; char pad[1]; }dd; struct x{ char a; int b; char c; }ee; int main() { printf("%d\n",sizeof(cc));//7 printf("%d\n",sizeof(dd));//8 printf("%d\n",sizeof(ee));//12 }
要按照结构体成员类型大小和对齐参数的最小值来对齐(空间对齐和地址对齐)
5.
问题一:
下面函数有什么本质区别?你会选择哪种?为什么?
void fun1(int a[], int x, int b[], int y, int n) { int i = 0; for(i=0; i<n; i++) { a[i * x] = b[i * y]; } } void fun2(int a[], int x, int b[], int y, int n) { int i = 0; for(i=0; i<n; i++) { *a = *b; a += x; b += y; } }
其实 单从题面来看 我们肯定选择2了 因为第二种的效率高 计算机做加法的效率远高于程序
但是 我们不要忽略可读性 其实从C的角度第一种方法的可读性高
如果我们要考虑程序的移植性和效率 那么我们选择第二种 原因是第二种从源码的级别直接用加法替换乘法 提高了效率 因此到任何C编译器上 都是高效的
第一种写法 是可读性比较好的写法 但是效率不好 不过现在很多C编译器 有了优化选项 当我们编译时打开了优化选项 那么两者之间的想率其实是一样的 但我们无法保证所有C编译器都可以优化编译
问题二:
下面两段程序有什么区别?为什么?
程序一: for(i=0; i<m; i++) { for(j=0; j<n; j++) { for(k=0; k<p; k++) { c[i][j] += a[i][k] * b[k][j]; } } } 程序二: for(i=0; i<m; i++) { for(k=0; k<p; k++) { for(j=0; j<n; j++) { c[i][j] += a[i][k] * b[k][j]; } } }
从程序来看 两者实现了同样的功能 区别只是第二层和第三层循环交换了位置
但是他们的差距却是巨大的 这个需要从CPU的cache来说了, cpu每次访问内存的时候都会先从内存将数据读入cache 然后以后都从cache取数据
但是cache的大小是有限的 因此只会有部分进入cache,我们每次都一段内存进cache后 只用其中一个数组 其附近的数据没用到
我们来看这个程序 c[i][j] = a[i][k] * b[k][j];
我们都知道C中二维数组是在内存中一维排列的 如果 我们把k循环放在第三层 那么cache基本没有用了 每次都需要重新到内存取数据
交换后 每次取到cache的数据都可以复用多次
所以说第二种写法效率高
问题三:
函数strcmp用于判断字符串是否相等,同样的memcpy也能用于字符串的相等判断,两者有什么不同?为什么?
们一起从 C语言的角度 和 问题二中cache的角度来考虑
第一 strcmp的比较也结束符'\0'密切相关 也就是说它不够安全 因为memcmp在比较的时候需要提供长度的 相对来说安全多了 这个是从C安全编程的角度来分析
第二 效率
strcmp就是一个字符一个字符的比较, 也就是说CPU要“想办法”只比较一个字节 而memcmp是根据硬件平台的特性 每次比较4个字节或者8个字节
那么我们从cache来看 strcmp每次将一块数据读入cache后只取一个字节进行比较 而memcmp每次都一块内存进入cache并且 根据CPU“喜好”进行块比较
6.分享一个技巧
int a; int* p = &a; void* pv = p; printf("%d\n", *pv);
pv就是指向a的地址的 如果 我们不想知道让 别人知道指针的具体指向的数据的类型 我们就可以用void* 指针来指向具体的地址 在我的数据结构课程中用的就是这个技巧 因为我不想使用我创建的数据结构的人 知道我具体如何表示数据结构的 如 链表的头 等等 所以我用void* 指针来返回