C/C++练习题(三)

时间:2023-02-23 23:59:48

1、对下面两个文件编译后,运行会输出什么?

// 第一个文件a.c
#include <stdio.h>
extern char p[];
extern void f();

int main()
{
    f();
    printf("a.c: %s\n", p);
    return 0;
}

// 第二个文件b.c
char* p = "Hello World";
void f()
{
    printf("b.c: %s\n", p);
}

打印结果:

b.c: Hello World
a.c: ل€¤※@_


分析:在我们看来,虽然使用字符数组和字符指针差不多,printf都可以打印出字符串出来,但是编译器对他们的处理完全不同。
<font color=#f0f size= face="楷体">
对于字符指针,编译器看到后,会把里边保存的值取出来,然后在去这个地址值处,将字符串取出来(进行一次寻址);对于字符数组,编译器直接到数组首地址处打印字符串。

在这里b.c定义的是字符指针,也就是说p的地址不是“helloworld”的地址,p中保存的才是“helloworld”的地址。但是到了a.c里面,却声明成了数组,所以编译的代码就不会进行寻址了,直接把p的地址当成了“helloworld”的地址打印出来。



2、下面程序输出什么?

#include <stdio.h>
int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int* p1 = (int*)(&a + 1);
    int* p2 = (int*)((int)a + 1);
    int* p3 = (int*)(a + 1);
    printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
    return 0;
}

打印结果:

5, 33554432(0x2000000), 3

分析:p1和p3没有问题,关键是p2,为什么是0x2000000呢?
我们来看a的地址分布:a[0] -> a[4] ==>低地址 -> 高地址
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
然后(int)a+1指向的地址如下:
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
然后p2就指向这个地址了,之后将这个地址当成整形输出,就是输出下面的东西:
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
组合成十六进制的形式就是0x02000000。


1、下面的代码输出什么?为什么?

#include <stdio.h>

#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)

int main()
{
    printf("%s\n", h(f(1, 2)));
    printf("%s\n", g(f(1, 2)));
    return 0;
}


结果:12 f(1, 2)

分析:
1:#在宏中表示将后面的符号变成C语言字符串;##表示将两个符号连接成一个新的符号。
2:宏和函数的调用不同,当宏定义中还使用了其他宏的时候会先扩展嵌套的宏,否则就直接进行宏替换。如h(f(1, 2))会先扩展f(1, 2)是12,然后在h(12) = "12"



2、下面的代码输出什么?为什么?

#include <stdio.h>
int main()
{
    int i = 1;
    printf("%d, %d\n", sizeof(i++), i);
    return 0;
}

这个题和编译器无关,输出一定是 4,1


分析:sizeof是编译器的武器,编译器用它只看结果不会计算的。sizeof(i++)中i++肯定是int类型,所以在编译期间这个语句就变成4了,i++没有执行。



3、下面的代码输出什么?为什么?

#include <stdio.h>    

#define PrintInt(expr) printf("%s: %d\n", #expr, (expr))

int FiveTimes(int a)
{
    int t;
    t = a << 2 + a;
    return t;
}

int main()  
{
    int a = 1;
    PrintInt(FiveTimes(a));
    return 0;
} 


结果:FiveTimes(a): 8

考点有两个:1、还是之前的#运算符的作用;
2、也是产品中最容易出现bug的地方,位运算的优先级比算术优先级要低。
对于软件开发来说,bug的原因很多时候都是基础不扎实,如果不想多加班,就多把基础搞搞。



**4、char * * string = "abcdefg";   * (*string)++; 这个表达式的值是什么?
**

分析:string是一个二级指针,却保存着“abcdefg”的地址。 * string代表到string指向的内存中取一个大小为sizeof( * string)的数据。string指向“abcdefg”,那么假设取的4字节数据就是0x12345678,之后是 * (0x12345678)++,意思是到0x12345678中取值,谁知道运行时0x12345678地址的值是什么呢???


1、用二进制来编码字符串“abcdabaa”,需要能够根据编码,解码回原来的字符串,那么最少需要多长的二进制字符串?(原google笔试题)

思路:考虑 霍夫曼树


2、有20个数组,每个数组里面有500个数组,降序排列,每个数字都是32位的uint,求出这10000个数字中最大的500个数?(原百度笔试题)

思路:这个问题是考对归并排序的理解,这些数组都是排序好的,只需要直接归并即可。还可以考虑到竞争树,在效率上基本上同级别。


3、有个数组a[100]存放了100个数,这100个数取自1-99,且只有两个相同的数,剩下的98个数都是互不相同的,写出一个搜索算法找出相同的那个数?

思路:根据题目的意思,我们只需要扫描数组一遍就OK了,不需要辅助空间。只需要把所有的数组元素相加,然后减掉1+2+···+99就可以了。


1、死锁发生的必要条件是(ABCD)

A:互斥条件
B:请求和保持
C:不可剥夺
D:循环等待

思路:这个问题是考对归并排序的理解,这些数组都是排序好的,只需要直接归并即可。还可以考虑到竞争树,在效率上基本上同级别。



2、Android 内核和 Linux 内核的区别?


Android 是执行于 Linux kernel 之上,但并不是 GNU/Linux。因为在一般 GNU/Linux 里支持的功能,Android 大都没有支持,包括 Cairo、X11、 Alsa、 FFmpeg、 GTK、 Pango 及Glibc 等都被移除掉了。
Android又以 bionic 取代 Glibc、以 Skia 取代 Cairo、再以 opencore 取代FFmpeg 等等。
Android 为了达到商业应用,必须移除被 GNU GPL授权证所约束的部份,例如 Android 将驱动程序移到 userspace,使得 Linux driver 与 Linux kernel 彻底分开。bionic/libc/kernel/ 并非标准的 kernel header files。Android 的 kernel header 是利用工具由Linux kernel header 所产生的。
Android 对 Linux 内核的主要剪裁部分:
Goldfish
YAFFS2
蓝牙
调度器
Android 新增驱动
电源管理
以及新增的额外调试功能、键盘背光控制、TCP 网络管理等。



3、下面代码输出什么?为什么?

int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
    {
        a[i] = (char)(-1 - i);
    }
    printf("%d\n", strlen(a));
    return 0;
}


输出结果为 255

分析:
我们来逐一计算 a 中元素的值:
a[0] = -1, a[1] = -2, a[2] = -3, … , a[126] = -127, a[127] = -128, a[128]= 127, a[129] = 126, … , a[255] = 0; ……
在 C 语言中 char 的取值范围是[-128, 127],所以当 i=127 的时候 a中的元素 a[127]将存储 char 类型所能存储的最小值-128;当 i=128的时候(-1 - 128)的值为-129,这个时候 char 类型将无法表示这个数值,于是产生了溢出(为何溢出后的值为 127,请参考《计算机组成原理》的相关内容),其结果反转为 char 所能表示的最大值 127,以此类推,当 i=255 的时候,a 数组中出现第一个 0 值(就是'\0')。所以,将数组 a 作为参数调用 strlen 得到的结果为 255。


1、逆波兰表达式


正常的表达式称为中缀表达式,运算符在中间,主要是给人阅读的,机器求解并不方便。
例如: 3 + 5 * (2 + 6) - 1
而且,常常需要用括号来改变运算次序。
相反,如果使用逆波兰表达式(前缀表达式)表示,上面的算式则表示为:

    • 3 * 5 + 2 6 1
      不再需要括号,机器可以用递归的方法很方便地求解。


2、实现下面函数

写一个函数,它的原形是 int continumax(char outputstr,char intputstr)
功能:
在字符串中找出连续最长的数字串,并把这个串的长度返回,并把这个最长数字串付给其中一个函数参数 outputstr 所指内存。
例如: "abcd12345ed125ss123456789"的首地址传给 intputstr 后,函数将返回 9。outputstr 所指的值为 123456789。


提示:这个问题可以在遍历一般字符串后得到答案,就是一个统计的问题。

#include <iostream>
#include <string>
using namespace std;

int continumax(char *outputstr, char *inputstr);
int main()
{
    char *s = "abcd12345ed125ss123456789";
    char ss[256];
    int n = continumax(ss,s);
    return 0;
}
int continumax(char *outputstr, char *inputstr)
{
    int n = 0;
    int maxs = 0;
    int len = 0;
    char *tmp;
    char *poi;
    while (*inputstr != '\0')
    {
        if ((*inputstr >= '0')&&(*inputstr <= '9'))
        {
            tmp = inputstr;
            ++n;
            inputstr++;
            
            while (*inputstr != '\0')
            {
                if ((*inputstr >= '0')&&(*inputstr <= '9'))
                {
                    ++n;
                }
                else
                {
                    break;
                }
                inputstr++;
            }
            if (n > maxs)
            {
                maxs = n;
                poi = tmp;
            }
        }
        else
        {
            n = 0;
        }
        
        inputstr++;
    }
    for (int i = 0; i < maxs; ++i)
    {
        *outputstr++ = *poi++;
    }
    *outputstr = '\0';
    return maxs;
}

1、下面程序有没有问题,为什么?

int main()
{
    int i = 10;
    int array[i];
    return 0;
}


在 C99 规范之后 C 语言可以定义变量数组,也就是定义时下标用变量来定义的数组。而 GNU 一直都紧跟规范,所以在最新的 linux 版本中自带的 gcc 和 g++都支持变长数组了。
然而,事实上的 C 语言规范是 C89 ,目前绝大多数公司的产品都是用 C89 编译器的,极少有公司使用 C99,对于 C++ 它的存在有一个使命 就是兼容 C 语言,所以 C99 支持变长数组后
C++也支持变长数组。因此 我们在做软件时以 C89 为规范写代码。这其实很简单的,规范和实际的工程有不同的,目前好多硬件生产厂商是很不愿意去升级自己提供的编译器。C89 用得好好的,为什么要大力气去支持
C99 呢?费时费力但对财政报表没有帮助,而软件厂商会觉得自己的大多数代码都是 C89 写的,为什么要吃螃蟹呢?所以很多规范制定得好,但是执行得不一定好。



2、下面程序应该输出什么呢?
int main() { int a = 10; int b = a; char c; int d = b; int& array[2] = {a, d}; printf("%d\n", &array[1] - &array[0]); return 0; }

从 C 语言的角度 这个程序应该输出 1 原因很简单 因为这个就是一个典型的 指针运算(两个数组的下标差)
我们假设 C++支持这么写 那么从 C++的角度来说 应是计算变量 a 和变量 d 之间的地址差 因此输出绝对不会是 1
那么大家想想 这样子 是不是就改变了 C 语言本来的语义 也就是说如果支持这么写 那么就没有兼容 C 语言的特性了
大家知道 C++不支持引用数组的原因了吗
一旦支持那么将改变原有的 C 语言语义 兼容性被破坏。


1、下面程序输出什么?

#include <stdio.h>
int main()
{
    int a[5][5];
    int (*p)[4];
    p = a[0];
    printf("%d\n", &p[3][3] - &a[3][3]);
    return 0;
}


2、编写一个函数,该函数的参数是一个长整型数,返回值是长整型,且函数的返回值是参数中各个十进制位的从大到小排序的整数(性能要求:以最少的空间和时间完成)如:原型: long f(long n);调用: long num = f(1302181);函数返回后 num 的值为 8321110


主要思想:
十进制数的每一位出现的数字只可能为 0-9,因此可以先统计各个位上的数字出现的次数,然后根据这些统计信息重新组合为一个符合要求的十进制数返回。

实现代码如下:

long f(long n)
{
    int ret = 0;
    int i = 0;
    int fact = 1;
    int s[10] = {0}; // 辅助空间
    int sign = (n > 0) ? 1 : -1;

    n = (n > 0) ? n : -n;
    
    while(n)
    {
        s[n%10]++;
        n /= 10;
    }
    for(i=1; i<10; i++)
    {
        while( s[i] )
        {
            ret += i * fact;
            fact *= 10;
            s[i]--;
        }
    }
    while( s[0] )
    {
        ret *= 10;
        s[0]--;
    }
    return ret * sign;
}


在这个算法中,循环次数为 2n,所需要的执行时间与问题规模 n 成线性关系,算法复杂度为 O(n)。


1、下面程序输出结果是什么?为什么?

int main()
{
    int i;
    int a[5];
    
    for(i=0; i<=5; i++)
    {
        a[i] = -i;
        printf("%d, %d\n", i, a[i]);
    }
    return 0;
}


答案:死循环

相信大家一眼就可以看出本题的主要问题是数组 a 只有 5 个元素,访问其元素的下标分别是0, 1, 2, 3, 4。当循环变量 i 的值为 5 的时候将访问 a[5],这个时候产生了一个数组越界的错误。但问题是,为什么会产生死循环,当 i 的值为 5 的时候,程序究竟做了什么?在 C语言中产生死循环只有一个原因,就是循环条件一直为真。在本题中也就是 i<=5 将一直成立。可问题是,循环变量 i 在每次循环结束后都做了 i++。理论上,不可能产生死循环。为了弄清楚问题的本质,我们先弄清楚 a[5]究竟代表什么?在 C 中 a[5]其实等价于(a+5),也就是说当 i 的值为 5 的时候,将对 a+5 这个内存空间进行赋值。很不幸,在本题中 a+5 这个内存空间正好就是 i 的地址,也就是说当 i 的值为 5 的时候,在 for 循环中的赋值语句其实
等价于 i=-i,即将 i 赋值为-5,因此 i<=5 永远满足。更进一步,为什么 i 正好在 a+5 这个位置呢?


据推测,PC机采用的是减栈。

空栈:栈指针指向空位,每次存入直接存入然后栈指针移动一格,每次取出则先移动一个才能取出

满栈:栈指针指向栈中最后一格数据,每次存入需要先移动栈指针一格再存入,取出直接取出,然后在移动栈指针

增栈:栈指针移动时向地址增加的方向移动的栈

减栈:栈指针移动时向地址减少的方向移动的栈


在分配栈空间的时候,先分配i的地址(处于栈的高地址),然后分配a[5]的栈地址(处于栈的低地址),但是在a[5]内部的访问从a[0] -> a[4]却是从低地址 -> 高地址的,这样如果越界,就会与i的地址冲突。

我们看下面的例子:

int main()
{
    int i;
    int a[5];
    
    printf("%X\n", &i);
    printf("%X\n", a+4);
    printf("%X\n", a+3);
    printf("%X\n", a+2);
    printf("%X\n", a+1);
    printf("%X\n", a);
}

最后得到的结果是:
BFA5F240
BFA5F23C
BFA5F238
BFA5F234
BFA5F230
BFA5F22C
从运行结果可以知道:局部变量 i 确实紧跟着数组中的第 4 个元素 a[4]在栈上分配空间,因此 a[5]就是 i。

(类似的还有之前《C/C++练习1》的第二小题,也是一样的道理。)


1、进程和线程的描述,哪些是对的 (C )
A. 操作系统的一个程序必须有一个进程,但是不必须有一个线程
B. 进程有自己的栈空间,而线程只共享父进程的栈空间
C. 线程必从属于一个进程
D. 线程可以更改从属的进程


程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序 健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

A,创建进程的时候就会有一个线程存在 B,线程有自己的栈空间
老师分析:
程序是一个可执行的文件,进程是一个可执行文件执行后在内存中的实体。进程是操作系统分配资源的基本单位,如,分配内存,分配 CPU 周期等资源,而线程是执
行流的基本单元
因此,每个进程必然有一个线程才可能运行得起来,否则就只有一堆资源,没有执行过程了。
按照分析 显然 A D 就错了
D 错的原因是 线程执行需要资源 因此必然属于一个进程而不能更改
B 这个最容易搞错 线程被创建后必须有自己的栈空间 而不能共享 因为多个线程同时运行后 CPU 的调度需要知道线程的执行流 共享的话 无法多线程了



2、下面哪些是稳定排序? (AD )
A. 冒泡儿排序
B. 快速排序
C. 堆排序
D. 归并排序
E. 选择排序


排序的稳定性指的是,如果两个数据元素的值相同,那么排序后原来排在前面的总是排在前面,否则就不稳定。(冒泡 归并 插入 都是稳定的)



3、下面程序输出什么?

int main()
{
    int i = 43;
    int n = printf("%d\n", i);
    printf("%d\n", n);
    return 0;
}


好,大家深入考虑一下,为什么返回是3 。这背后有什么鲜为人知的秘密,到底是C语言离奇的规定,还是深思熟虑后的决定?

相信大家都在学习驱动的时候应该知道有一种字符设备驱动。在 linux 中一切东东都是文件,外设也是文件,也就是说显示器也是文件,那么 printf 的实现其实就是调用显示器的驱动程序往这种外设写入数据,所以我们来考虑一下,显示器属于什么设备呢,字符型设备。所以 printf 返回的其实不应该是输出的字符个数,准确的说应该是向字符设备写入的数据的字节数。因为 char 占用一个字节,所以碰巧“printf 返回输出字符的个数”,这个说法正确了。



4、#define中用到了array,但是array在后面才定义的,合法吗?为什么?最后程序输出什么?

#include <stdio.h>
#define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0]))
int array[] = {23,34,12,17,204,99,16};
int main()
{
    int d;
    for(d=-1;d <= (TOTAL_ELEMENTS-2);d++)
    printf("%d\n",array[d+1]);
    return 0;
}


第一个: #define 中用到了 array, 但是 array 在后面才定义的,合法吗?为什么?
其实合法的,在编译之前是预编译,预编译会处理#define之流的东东,在编译时这个define就没了。
第二个:程序输出什么?
程序不会输出任何东西。因为 int 和 unsigned int 比较时会被转换为无符号的,因此-1就直
接被看成 0xFFFFFFFF 了,这样 d 不可能小于条件中的表达式。自然 for 不会执行。 注意一点 sizeof 是编译的工具,它的计算结果是无符号的。



5、char p, a[16][8];问:p=a 是否会导致程序在以后出现问题?为什么?**


p[1][2]和 a[1][2]代表一样的意思吗?
我们应该知道 二维数组在内存中也是一维排布的
所以 a[1][2] 代表 第 11 个元素
我们再来看 p[1][2] 因为 p 的类型为 char**p 所以 p[1][2] 代表的意思 ((p+1) + 2) ,p指向一个 char* 所以 p+1 向前移动 4 个字节 后面+2 再向前移动 2 个字节 一共 6 个字节。所以说 不一样。



**5、int d[3][5] = {{1}, {2}, {3}}; (*p)[5] = d; 下面表达式中值不为0的是?为什么?**
A. &d[1][2] B. p[1][2] C. (p+15+2) D. (*(p+1)+2)

B和D统一,C是野指针了。



6、下面函数有没有问题?如果有,如何修改?

int square(int* ptr)
{
    return *ptr * *ptr;
}


对于这个题目主要的问题是 ptr 所指向的地址内容很可能被意想不到的改变 而 ptr 就可能反映不出这个改变了。所以 square 就必须解决这个,因此,改变的第一步就应该是
int square(volatile int ptr)
{
return
ptr * ptr;
}
但是,这样改也有问题,如果 ptr 真的被意想不到的改变了,那么 square 就显得不会是某个数的平方。所以最后的方案应该是:
int square(volatile int
ptr)
{
int a = ptr;
return a
a;
}


1、下面程序有没有问题,为什么?

__interrupt double compute_area (double radius)
{
    double area = PI * radius * radius;
    printf("/nArea = %f", area);
    return area;
}


中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准 C支持中断。具代表事实是,产生了一个新的关键字__interrupt。上面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR)。
这个是经典的面试题,它的问题如下:
1.ISR 不能返回一个值。
2.ISR 不能传递参数。
3.由于重效率问题不要在 ISR 中做浮点运算。
4.不要在 ISR 中做 IO 操作。
(printf 这个函数是一个 IO 函数 很多 IO 函数都是不可重入的。也就是说如果在执行 printf 的时候这个中断产生了,那么是不是 printf 又被调用了?
另外的原因就是 printf 实在太慢了( printf 的实现其实还调用了 sprintf 和 write 系统调用))
下面就有一个问题了,既然 ISR 有那么多限制,那么如果我们真的需要做这些事,那么怎么办呢?
好,下面就要提一下系统设计的一些技巧。在一些大型系统中 ISR 一般只是做一些投递消息的操作。比如发生了中断,然后就只是向一个队列中插入一个中断号,这样就可以在其他的线程中处理这个中断,这样就绕开了限制。



2、编译期和运行期的疑问?
).一个程序哪些东西可以在编译期就确定?
2).一个程序哪些东西要等到运行期才确定?
3).比如:int x = 100; x是在什么时间被赋值的?


1、静态的赋值,比如 int a = 2; sizof的计算等。编译器会尽自己最大的努力在编译阶段确定足够多的东西
2、实在是在编译阶段无法确定的东西,比如指针的多态表现,对内存的动态分配等
3、int x = 100 显然是编译期
编译期分配内存并不是说在编译期就把程序所需要的空间在内存里面分配好,而是说在程序生成的代码里面产生一些指令,由这些指令控制程序在运行的时候把内存分配好。不过分配的大小在编译的时候就是知道的。
而运行期分配内存则是说在运行期确定分配的大小,存放的位置也是在运行期才知道的。



3、为什么ABC不行?

#define ABC #pragma message("aaa")
int main()
{
    //#pragma message("aaa") //这样是可以的
    ABC //这样就不行
    return 0;
}

当然不行,#define 中,不能嵌套其它预编译的代码。



**4、题目:实现一个袖珍型计算器,假设已经从界面编辑框读入两个数 op1 和 op2,和一个操作符 per( +, -, *, /) ,可以用 switch 来实现,代码如下:
switch(per)
{
case ADD:
result = add(op1,opt2);
break;
case SUB:
result = sub(op1,op2);
break;
……
}
如果对于一个新奇的有上百个操作符的计算器,这个 switch 将会很长。有没有什么办法可以缩短这个代码量,看起来简洁明了呢?**


大家想想如何做 这个确实在大型软件系统中常用。
老师:
第 0 步:
定义操作函数

int add(int p1, int p2)
{
    return p1 + p2;
}
int sub(int p1, int p2)
{
    return p1 - p2;
}

第1步:
定义函数指针

typedef int(*pFunc)(int, int);

第2步
定义结构体

struct Operation
{
    int op;
    pFunc func;
};

第3步
定义操作列表

static struct Operation OpArray[] =
{
    { '+', add },
    { '-', sub },
};

第四步
定义 run 函数

int run(int op, int p1, int p2)
{
    int ret = 0;
    int i = 0;
    for(i=0; i<sizeof(OpArray)/sizeof(*OpArray); i++)
    {
        if( OpArray[i].op == op )
        {
            ret = OpArray[i].func(p1, p2);
        }
    }
    return ret;
}

如果你现在要增加一个新的操作乘法,你会如何修改上面的代码呢?只需要修改第0和3步就可以了。这样来写代码是不是对于后面的扩展很有好处了,只需要多定义一个操作函数 和 把操作函数添加到操作列表中就可以了。

其他的解法:
1、申明并实现单个功能函数:

Type add(Type, Type);
Type sub(Type, Type);
Type mul(Type, Type);
……

2、初始化函数指针数组:

Type (*oper_func[])(Type,Type) = {add,sub,mul,...};

3、求计算结果:

result = oper_func[add/sub/mul](op1,op2);

1、关于 int a[10]; 问下面哪些不可以表示 a[1] 的地址?

A. a+sizeof(int) 
B. &a[0]+1 
C. (int*)&a+1 
D. (int*)((char*)&a+sizeof(int))

分析:
A. a+sizeof(int)
// No, 在32位机器上相当于指针运算 a + 4
B. &a[0]+1
// Yes,数组首元素地址加1,根据指针运算就是a[1]的地址
C. (int)&a+1
// Yes,数组地址被强制类型转换为int
,然后加1,这样和B表示的一个意思
D. (int)((char)&a+sizeof(int))
// Yes,数据地址先被转换为char,然后加4,根据指针运算公式,向前移动4 sizeof(char),之后被转换为int*,显然是a[1]的地址


2、基于比较的排序的时间复杂度下限是多少?

A. O(n) 
B. O(n^2) 
C. O(nlogn) 
D. O(1)

**大家记住这个结论就好 在当前 计算机科学界 对于基于比较的排序 最快只是O(n*logn)**


3、完成字符串拷贝可以使用 sprintf、strcpy 及memcpy 函数,请问这些函数有什么区别,你喜欢使用哪个,为什么?

strcpy是最初C库中的字符串处理函数,只能用于以0结束的字符串,甚至不能用于字符数组的处理,因为strcpy不带长度信息因此是不安全的函数,很多黑客都是从这个函数入手做很多事。

memcpy是内存拷贝函数,可以拷贝任意的内存,带长度信息,属于安全的函数。

sprintf是字符串格式化函数,可以将几乎任意的基础类型值格式化为字符串,因为不带长度信息,所以这个函数也是不安全的.

这三个函数都可以做字符串的拷贝,但是三个函数的设计出发点不同,这个题目主要是考察面试者的对C标准库的熟悉程度。


4、是地址之差还是地址下标之差?

int* p = 0;
printf("%d\n", &(p[9]));
printf("%d\n", &(p[9])-p);

打印:36, 9

地址相减则为下标差。


1、进程和线程的描述,哪些是对的( C )

A. 操作系统的一个程序必须有一个进程,但是不必须有一个线程
B. 进程有自己的栈空间,而线程只共享父进程的栈空间
C. 线程必从属于一个进程
D. 线程可以更改从属的进程

我们先来看看程序和进程的区别:
程序是一个可执行的文件,进程是一个可执行文件执行后在内存中的实体
进程是操作系统分配资源的基本单位,如,分配内存,分配CPU周期等资源,而线程是执行流的基本单元。因此,每个进程必然有一个线程才可能运行得起来,否则就只有一堆资源,没有执行过程了。
按照我刚才分析 显然 A D就错了

D错的原因是 线程执行需要资源 因此必然属于一个进程而不能更改

B这个最容易搞错,线程被创建后必须有自己的栈空间而不能共享。因为多个线程同时运行后,CPU的调度需要知道线程的执行流。共享的话无法多线程了。线程切换后怎么切换回来呢?如果每个线程没有自己的存储执行流的栈的话,是无法实现线程切换的。


2、下面程序选什么?(D)

int main()
{
    int* p;
    int a = 10;
    *p = a;
    printf("*p = %d\n", *p);
    return 0;
}

A. 10
B. a的地址值
C. 编译错误
D. 运行异常

分析:
1、p为野指针;
2、int* p = NULL;也是错误的,p未指向任何地方,却被赋值10,错误;
3、改为p = &a;

野指针的成因主要有三种:

  1. 指针变量没有被初始化,任何指针变量刚被创建的时候不会自动成为NULL,它的缺省值是随机的,它会乱指一气。
  2. 指针被free或者delete之后,没有置为NULL,让人误以为是合法的指针。
  3. 指针操作超越了变量的作用范围。比如不要返回指向栈内存的指针或者引用,因为栈内存在函数调用结束时会被释放。

3、下面代码输出的结果为?

#define a 10
void foo();
int main()
{
    printf("%d.", a);
    foo();
    printf("%d", a);
}
void foo()
{
    #undef a
    #define a 50
}

答案:10.10
分析:#undef 是在后面取消以前定义的宏定义。define在预处理阶段就把main中的a全部替换为10了.
另外,不管是在某个函数内,还是在函数外,define都是从定义开始知道文件结尾,所以如果把foo函数放到main上面的话,则结果会是50 ,50


1、下面程序在80x86架构下,输出什么值?

union Test
{
    char a[4];
    short b;
}
Test t;
t.a[0] = 256;
t.a[1] = 255;
t.a[2] = 254;
t.a[3] = 253;
printf("%d\n", t.b);

答案: -256
分析:
char类型的取值范围是-128~127,unsigned char的取值范围是0~256
这里a[0]=256,出现了正溢出,将其转换到取值范围内就是0,即a[0]=0;
同理,a[1]=-1, a[2]=-2, a[3]=-3,在C语言标准里面,用补码表示有符号数,故其在计算机中的表示形式如下:
a[0]=0, 0000 0000
a[1]=-1, 1111 1111(反码+1 = -1)
a[2]=-2, 1111 1110
a[3]=-3, 1111 1101
short是2字节(a[0]和a[1]),由于80X86是小端模式,即数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中,在本例中,a[0]中存放的是b的低位,a[1]中存放的是b的高位,即b的二进制表示是:1111 1111 0000 0000,表示-256


1、宏的使用

#define NDEBUG
#define PRINTF(s)
#else
#define PRINTF(s) printf("%s\n", #s); s

2、下面程序输出什么?

int main()
{
    int a;
    int *p;
    p = &a;
    *p = 0x500;
    a = (int )(*(&p));
    a = (int )(&(*p));
    if(a == (int)p)
        printf("equal !\n");
    else
        printf("not equal !\n");
    return 0;
}

答案:equal !

a = (int )((&p));和a = (int )(&(p));打印的都是p的地址。


3、什么是可重入函数?C语言中写可重入函数,应注意的事项?

答案:可重入函数是指能够被多个线程“同时”调用的函数,并且能保证函数结果的正确性的函数。
在编写可重入函数时通常要注意如下的一些问题:
1、尽量不要使用全局变量,静态变量,如果使用了应该注意对变量访问的互斥。通常可以根据具体的情况采用:信号量机制,关调度机制,关中断机制等方式来保证函数的可重入性。
2、不要调用不可重入的函数,调用了不可重入的函数会使该函数也变为不可重入的函数。
3、注意对系统中的临界资源,互斥资源的访问方式,防止使函数成为不可重入的函数。
4、一般驱动程序都是不可重入的函数,因此在编写驱动程序时一定要注意重入的问题。



答案是 -3,而不是 0,为什么呢?

数组 int a[5][5]; 可以等价于
typedef int(AA)[5];
AA a[5];

a[3][3]的地址是:
(unsigned int)a + 3 * sizeof(AA) + 3 * sizeof(int) ==>
(unsigned int)a + 3 * 20 + 3 * 4

p[3][3]的地址:
(unsgined int)a + 3 * 16 + 3 * 4

于是:&p[3][3] - &a[3][3]就等于:
(3 * 20 - 3 * 16)/4 = -3