现在的位置: 首页 > 综合 > 正文

c语言要点摘录(面试题分析以及疑点分析)(完)

2013年10月11日 ⁄ 综合 ⁄ 共 4569字 ⁄ 字号 评论关闭

三十七.面试分析

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;
}

解析:

scanf输入非法数据会跳出  直接执行下一句printf k的值就一直是上次输入的值  但是键盘缓冲区里面的值一直是非法字符  所以每次scanf都会跳出
你在循环最后添加一句fflush(stdin);这个程序就不会死


修改成:

#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* 指针来返回








【上篇】
【下篇】

抱歉!评论已关闭.