关闭

C语言笔试面试总结(网上搜集)

标签: 面试c语言
1232人阅读 评论(0) 收藏 举报
分类:

1.关键字static的作用是什么?

有三个明显的作用:

1)在函数体内,一个被声明为静态的变量在这个函数被调用过程中维持其值不变

2)在模块内(但在函数体外),静态的变量可以被模块内所有函数访问,但不能被模块外其他函数访问

3)在模块内,一个被声明为静态的函数只能被这一模块内的其他函数调用,也就是,这个函数被限制在声明它的模块的本地范围内使用

2. 如何保证头文件的内容只被包含一次,请举例说明。

#ifndef
HEAD_TEST_H
#define
HEAD_TEST_H
//
在这里写你的头文件内容
#endif 

3.请写出一个实现c语言可变参数函数的实例(任意)。

#include    <stdio.h>   
 #include    <ctype.h>   
 #include  <stdlib.h>   
 #include    <stdarg.h>  

int    average(    int    first,    ...    )   
   {   
         int    count=0,

int  i=first;

int  sum=0;   
         va_list    maker;           //va_list
即是char*表明maker是一个字符型的指针。   
         va_start(maker,first);    //maker
指向first之后的第一个可变的参数,而frist是作为一个固定参数,因为它在…之前。
         while(i!=-1)   
         {   
         sum+=i;   
         count++;   
         i=va_arg(maker,int);//
返回maker列表的当前值,并指向列表的下一个位置   
         }  

va_end(maker);  

 return    sum/count;   

  }    
   void    main(void)   
   {   
     printf( "Average    is:    %d\n",    average( 2, 3, 4,4, -1 ) );   
   }   

va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,说明:argN是位于第一个可选参数之前的固定参数,函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一va函数的声明是void va_test(char a, char b, char c, …),则它的固定参数依次是a,b,c,最后一个固定参数argNc,因此就是va_start(arg_ptr, c)

va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。

va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。说明:指针arg_ptr被置无效后,可以通过调用 va_start()va_copy()恢复arg_ptr。每次调用va_start() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。

 

4.请定义一个二叉树数据结构,要求包括一个联合类型的成员。

并说明一下 联合和结构的区别。

结构和联合有下列区别:

    1)结构和联合都是由多个不同的数据类型成员组成,但在任何同一时刻,联合转只存放了一个被选中的成员,而结构的所有成员都存在。
   2
) 对于联合的不同成员赋值,将会对其它成员重写,  原来成员的值就不存在了,而对于结构的不同成员赋值是互不影响的。

 

请说明下面声明的含义

char*(*(*f(char *(*para)(char *)))[2])();

 

参考:

复杂指针解析
因为C语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既著名又常用的方法。不过,右左法则其实并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的。右左法则的英文原文是这样说的:
Theright
leftrule: Start reading the declaration from the innermost parentheses,go right, and then go left. When you encounter parentheses, thedirection should be reversed. Once everything in the parentheses hasbeen parsed, jump out of it. Continue till the whole declarationhas been parsed.
这段英文的翻译如下:
右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。
   
笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个。
 
现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:
int(*func)(int *p);

首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是int

int(*func)(int *p, int (*f)(int*));

func
被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int*int(*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int(*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int

int(*func[5])(int *p);

func
右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int


int(*(*func)[5])(int *p);

func
被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下,就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。

int(*(*func)(int *p))[5];

func
是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5int元素的数组。

要注意有些复杂指针声明是非法的,例如:

intfunc(void) [5];

func
是一个返回值为具有5int元素的数组的函数。但C语言的函数返回值不能为数组,这是因为如果允许函数返回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C语言的数组名是一个右值,它不能作为左值来接收另一个数组,因此函数返回值不能为数组。

intfunc[5](void);

func
是一个具有5个元素的数组,这个数组的元素都是函数。这也是非法的,因为数组的元素除了类型必须一样外,每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空间通常是不相同的。

 
作为练习,下面列几个复杂指针声明给读者自己来解析。

int(*(*func)[5][6])[7][8];

int (*(*(*func)(int *))[5])(int*);

int (*(*func[7][8][9])(int*))[5];

 
实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性,例如对于声明:

int(*(*func)(int *p))[5];

可以这样分解:

typedefint (*PARA)[5];
typedef PARA (*func)(int*);

这样就容易看得多了。  
 

答案,同时给出用typedef的分解方法:

int(*(*func)[5][6])[7][8];

func
是一个指向数组的指针,这类数组的元素是一个具有5X6int元素的二维数组,而这个二维数组的元素又是一个二维数组。

typedefint (*PARA)[7][8];
typedef PARA (*func)[5][6];


int(*(*(*func)(int *))[5])(int *);

func
是一个函数指针,这类函数的返回值是一个指向数组的指针,所指向数组的元素也是函数指针,指向的函数具有int*形参,返回值为int

typedefint (*PARA1)(int*);
typedef PARA1 (*PARA2)[5];
typedef PARA2(*func)(int*);

int(*(*func[7][8][9])(int*))[5];

func
是一个数组,这个数组的元素是函数指针,这类函数具有int*的形参,返回值是指向数组的指针,所指向的数组的元素是具有5int元素的数组。

typedefint (*PARA1)[5];
typedef PARA1 (*PARA2)(int*);
typedef PARA2func[7][8][9];

 

6.请从下面两方面描叙一下linux下生成coredump的配置方法

  1core文件的生成开关和大小限制     ulimit-c filesize 设置生成core文件的大小

  2core文件的名称和生成路径

proc/sys/kernel/core_pattern可以控制core文件保存位置和文件名格式

"/corefile/core-%e-%p-%t"

%p- 添加pid

%u-添加当前uid 
%g -
添加当前gid 
%s -
添加导致产生core的信号
%t-
添加core文件生成时的unix时间
%h-
添加主机名 
%e-
添加命令名

7. 给出如下C程序,在linux下使用gcc编译:

 1#include "stdio.h"
 2 #include "sys/types.h"
 3#include "unistd.h"
 4
 5  intmain()
 6 {
 7     pid_tpid1;
 8     pid_t pid2;
 9
10     pid1 = fork();
11    pid2 = fork();
12
13     printf("pid1:%d,pid2:%d\n", pid1, pid2);
14 }

 已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它新进程执行。

      1)请说出执行这个程序后,将一共运行几个进程。

四个进程

      2)如果其中一个进程的输出结果是“pid1:1001,pid2:1002”,写出其他进程的输出结果(不考虑进程执行顺序)。

        pid1:0,pid2:1002      pid1:1001, pid2:0    pid1:0, pid2:0


总体上必须清楚的:

1)程序结构是三种:顺序结构 ,循环结构(三个循环结构),选择结构(ifswitch)
2
)读程序都要从main()入口,然后从最上面顺序往下读(碰到循环做循环,碰到选择做选择)
3
)计算机的数据在电脑中保存是以二进制的形式.数据存放的位置就是 他的地址.
4
bit是位是指为0或者1byte是指字节,一个字节 =八个位.
5
)一定要记住二进制 如何划成 十进制。

概念常考到的:

编译预处理不是C语言的一部分,不占运行时间,不要加分号。C语言编译的程序称为源程序,它以ASCII数值存放在文本文件中。

每个C语言程序中main函数是有且只有一个。

在函数中不可以再定义函数

4)算法是一定要有输出的,他可以没有输入。
5
break可用于循环结构和switch语句。
6
)逗号运算符的级别最低。
第一章
1
)合法的用户标识符考查:
合法的要求是由字母,数字,下划线组成。有其它元素就错了。
并且第一个必须为字母或者是下划线。第一个为数字就错了。
关键字不可以作为用户标识符号。maindefine scanf printf都不是关键字。迷惑你的地方If是可以做为用户标识符。因为If中的第一个字母大写了,所以不是关键字。
2
)实型数据的合法形式:
2.333e-1
就是合法的,且数据是2.333×10-1
考试口诀:ee后必有数,e后必为整数.
3
)字符数据的合法形式:
’1′
是字符占一个字节,1″是字符串占两个字节(含有一个结束符号)
’0′
ASCII数值表示为48,’a’ASCII数值是97,’A'ASCII数值是65
一般考试表示单个字符错误的形式:’65′“1″
字符是可以进行算术运算的,记住: ’0′-0=48
大写字母和小写字母转换的方法: ’A'+32=’a’相互之间相差32
4
)整型一般是两个字节,字符型是一个字节,双精度一般是4个字节:
考试时候一般会说,在16位编译系统,或者是32位系统。碰到这种情况,不要去管,一样做题。掌握整型一般是两个字节,字符型是一个字节,双精度一般是4个字节就可以了。
5
转义字符的考查:
在程序中inta = 0x6d,是把一个十六进制的数给变量a注意这里的0x必须存在。
在程序中inta = 06d, 是一个八进制的形式。
在转义字符中,’\x6d’才是合法的,0不能写,并且x是小写。
‘\141’
是合法的,0是不能写的。
‘\108’
是非法的,因为不可以出现8
6
算术运算符号的优先级别:
同级别的有的是从左到右,有的是从右到左。
7
)强制类型转换:
一定是(inta不是 inta),注意类型上一定有括号的。
注意(int)(a+b)和(inta+b的区别。前是把a+b转型,后是把a转型再加b
8
)表达式的考查:
是表达式就一定有数值。
赋值表达式:表达式数值是最左边的数值,a=b=5;该表达式为5,常量不可以赋值。
自加、自减表达式:假设a=5++a(是为6),a++(为5);
运行的机理:++a是先把变量的数值加上1,然后把得到的数值放到变量a中,然后再用这
++a表达式的数值为6,而a++是先用该表达式的数值为5,然后再把a的数值加上16
再放到变量a中。进行了++aa++后在下面的程序中再用到a的话都是变量a中的6了。
考试口诀:++在前先加后用,++在后先用后加。
逗号表达式:优先级别最低;表达式的数值逗号最右边的那个表达式的数值。
234)的表达式的数值就是4
9
)位运算的考查
会有一到二题考试题目。
总的处理方法:几乎所有的位运算的题目都要按这个流程来处理(先把十进制变成二进制再变成十进制)。
1: chara = 6, b;
b = a<<2;
这种题目的计算是先要把a的十进制6化成二进制,再做位运算。
2: 一定要记住,异或的位运算符号。0异或 1得到1
0
异或 0得到0
1
异或1得到0
考试记忆方法:相同为0,不同为1
3: 在没有舍去数据的时候,<<左移一位表示乘以2>>右移一位表示除以2

10018的数值是非法的,八进制是没有8的,逢81
11
%符号两边要求是整数。不是整数就错了。
12)
 三种取整丢小数的情况:
1、inta =1.6
2、(int)a
3、1/23/2
13)
字符型和整数是近亲:
   chara = 65 ;
printf(“%c”, a);
得到的输出结果:a
printf(“%d”,a);
 得到的输出结果:65

第二章
1
printf函数的格式考查:
%d
对应整型;%c对应字符;%f对应单精度等等。宽度的,左对齐等修饰。
%ld
对应longint%lf对应double
2
scanf函数的格式考察:
注意该函数的第二个部分是&a这样的地址,不是a
scanf(“%d%d%*d%d”,&a,&b,&c);
跳过输入的第三个数据。
3
putchar,getchar 函数的考查:
chara = getchar()
是没有参数的,从键盘得到你输入的一个字符给变量a
putchar(‘y’)
把字符y输出到屏幕中。
4
)如何实现两个变量xy中数值的互换(要求背下来)
不可以把x=y,y=x; 要用中间变量t=xx=yy=t
5
)如何实现保留三位小数,第四位四舍五入的程序,(要求背下来)
这个有推广的意义,注意x= intx这样是把小数部分去掉。

第三章
特别要注意:c语言中是用非0表示逻辑真的,用0表示逻辑假的
1
)关系表达式:
表达式的数值只能为1(表示为真),或0(表示假)
当关系的表达是为真的时候得到1。如9>8这个是真的,所以表达式的数值就是1
2
)逻辑表达式:
只能为1(表示为真),或0(表示假)

共有&&|| ! 三种逻辑运算符号。

>&&>||优先的级别。

注意短路现象。考试比较喜欢考到。

要表示 x是比0大,比10小的方法。0<x<10是不可以的(一定记住)是先计算0<x得到的结果为1或则0;再用0,或110比较得到的总是真(为1)。所以一定要用(0<x)&&(x<10)表示比0大比10小。

3)if语句
else
是与最接近的if且没有else的相组合的。
4
)条件表达式:
表达式1?表达式2:表达式3
注意是当0时候是表达式2的数值,当0时就是表达式3的数值。
考试口诀:真前假后。
5
switch语句
a)
一定要注意 有break和没有break的差别,书上(34页)的两个例子,没有break时候,只要有一个case匹配了,剩下的都要执行,有break则是直接跳出了swiche语句。
b)switch
只可以和break一起用,不可以和continue用。
c)switch(x) x
:是整型常量,字符型常量,枚举型数据。
{case1: ….
不可以是变量。
case2: ….
}

第四章 
1
)三种循环结构:
a
for(); while()do-while()三种。
b
for循环当中必须是两个分号,千万不要忘记。
c
)写程序的时候一定要注意,循环一定要有结束的条件,否则成了死循环。
d)do-while()
循环的最后一个while();的分号一定不能够丢。(当心上机改错),dowhile循环是至少执行一次循环。
2)break
continue的差别
记忆方法:
break
:是打破的意思,(破了整个循环)所以看见break就退出整个一层循环。
continue
:是继续的意思,(继续循环运算),但是要结束本次循环,就是循环体内剩下的语句不再执行,跳到循环开始,然后判断循环条件,进行新一轮的循环。
3
)嵌套循环
就是有循环里面还有循环,这种比较复杂,要一层一层一步一步耐心的计算,一般记住两层是处理二维数组的。
4)while
((c=getchar()!=’\n’)和 whilec=getchar()!=’\n’)的差别
先看a= 3 != 2 和 (a=3)!=2的区别:
(!=号的级别高于=号所以第一个先计算3=2) 第一个a的数值是得到的1;第二个a的数值是3
考试注意点: 括号在这里的重要性。

第五章
函数:是具有一定功能的一个程序块;是C语言的基本组成单位。
1)
函数的参数,返回数值(示意图):

2
)一定要注意参数之间的传递
实参和形参之间传数值,和传地址的差别。(考试的重点)
传数值的话,形参的变化不会改变实参的变化。
传地址的话,形参的变化就会有可能改变实参的变化。
3
)函数声明的考查:
一定要有:函数名,函数的返回类型,函数的参数类型。
不一定要有:形参的名称。
4
)要求掌握的库函数:
sqrt() fabs( ) pow( ) sin( )
其中pow(ab)是重点。23是由pow(23)表示的。

第六章
指针变量的本质是用来放地址,而一般的变量是放数值的。
int*p
*pp的差别:
*p
可以当做变量来用;*的作用是取后面地址p里面的数值
p
是当作地址来使用。
*p++
和 (*p++的之间的差别:改错题目中很重要
*p++
是地址会变化。
*p++是数值会要变化。
三名主义:(考试的重点)
数组名:表示第一个元素的地址。数组名不可以自加,他是地址常量名。(考了很多次)
函数名:表示该函数的入口地址。
字符串常量名:表示第一个字符的地址。
考试重要的话语:
指针变量是存放地址的。并且指向哪个就等价哪个,所有出现*p的地方都可以用它等价的代替。
例如:inta=2*p=&a
*p=*p+2;
(
由于*p指向变量a,所以指向哪个就等价哪个,这里*p等价于a,可以相当于是a=a+2)
指针变量两种初始化
方法一:inta=2*p=&a(定义的同时初始化)
方法二:inta=2*p;  (定义之后初始化)
p=&a

第七章
1
一维数组的重要概念:
a[10]这个数组的讨论。
1、a表示数组名,是第一个元素的地址,也就是元素a[0]的地址。
2、a是地址常量,所以只要出现a++,或者是a=a+2赋值的都是错误的。
3、a是一维数组名,所以它是列指针,也就是说a+1是跳一列。
a[3][3]的讨论。
1、a表示数组名,是第一个元素的地址,也就是元素a[0][0]的地址。
2、a是地址常量,所以只要出现a++,或者是a=a+2赋值的都是错误的。
3、a是二维数组名,所以它是行指针,也就是说a+1是跳一行。
4、a[0]a[1]a[2]也都是地址常量,不可以对它进行赋值操作,同时它们都是列指针,a[0]+1a[1]+1a[2]+1都是跳一列。
5、注意aa[0]a[1]a[2]是不同的,它们的基类型是不同的。前者是一行元素,后三者是一列元素。
2)
二维数组做题目的技巧
如果有a[3][3]={1,2,3,4,5,6,7,8,9}这样的题目。
步骤一:把他们写成:      第一列 第二列 第三列
a[0]?
 1   2   3>第一行
a[1]?4
  5   6 —>第二行
a[2]?7
  8   9 ->第三行
步骤二:这样作题目间很简单:
*(a[0]+1)
我们就知道是第一行的第一个元素往后面跳一列,那么这里就是a[0][1]元素,所以是1。
*(a[1]+2)
我们就知道是第二行的第一个元素往后面跳二列。那么这里就是a[1][2]元素,所以是6
一定记住:只要是二维数组的题目,一定是写成如上的格式,再去做题目,这样会比较简单。
3)
数组的初始化,一维和二维的,一维可以不写,二维第二个一定要写
inta[]={1
2}合法。 inta[][4]={234}合法。但inta[4][]={234}非法。
4)
二维数组中的行指针
inta[1][2]

其中a现在就是一个行指针,a+1跳一行数组元素。搭配(*p[2]指针
a[0]
a[1]现在就是一个列指针。a[0]+1跳一个数组元素。搭配*p[2]指针数组使用
5)
还有记住脱衣服法则:
a[2]
变成 *a+2a[2][3]变成*a+2[3]再可以变成**a+2+3
这个思想很重要!

其它考试重点
文件的复习方法:
把上课时候讲的文件这一章的题目要做一遍,一定要做,基本上考试的都会在练习当中。
1
)字符串的strlen()strcat()和strcmp()和strcpy()的使用方法一定要记住。他们的参数都是地址。其中strcat()和strcmp()有两个参数。

2strlensizeof的区别也是考试的重点;

3definefx)(x*x)和 definefxx*x之间的差别。一定要好好的注意这写容易错的地方,替换的时候有括号和没有括号是很大的区别。
4
int*p
p=
int*malloc2);
p=
int*mallocsizeofint));以上两个等价
当心填空题目,malloc的返回类型是void*

5)还有mainintargcchar**argv{}这种含有参数的题目,是很呆板的题目。第一个参数是表示输入的字符串的数目,第二个参数是指向存放的字符串。

6函数的递归调用一定要记得有结束的条件,并且要会算简单的递归题目。要会作递归的题目

7结构体和共用体以及链表要掌握最简单的typedef考的很多,而且一定要知道如何引用结构体中的各个变量,链表中如何填加和删除节点,以及如何构成一个简单的链表,一定记住链表中的节点是有两个域,一个放数值,一个放指针。


8
函数指针的用法(*f)()记住一个例子:
intadd(int x, int y)
{….}
main()
{ int
*f)();
f=add;
}
赋值之后:合法的调用形式为1、add(23)
2、f(23)
3、(*f)(23
9
两种重要的数组长度:
chara[]={‘a’,’b’,’c’};
  数组长度为3,字符串长度不定。sizeof(a)为3。
chara[5]={ ‘a’,’b’,’c’}
  数组长度为5,字符串长度3。sizeof(a)为5。
10
scanf和 gets的数据:
如果输入的是 goodgood study
那么scanf(“%s”,a);只会接收 good. 考点:不可以接收空格。
gets(a);
会接收 goodgood study! 考点:可以接收空格。
11
)共用体的考查:
unionTT
{ int a;
char ch[2];}
考点一: sizeof(struct TT) = 2;
考点二: TT t1; t1=0×1234;
那么 ch[0]=0x34; ch[1]=0×12

12)“文件包含”的考查点:

这里一个C语言程序是有两个文件组成,分别是no1.cno2.c。那么no1.c中最开始有个#include”no2.c”他表示把第二个文件的内容给包含过来,那么no1.c中调用add()函数的时候就可以了把数值传到no2.c中的被调用函数add()了。
一个文件必须要有main函数。 这句话错了。 例如:no2.c就没有。
头文件一定是以.h结束的。 这句话错了。例如:no1.c中就是#include”no2.c”.c结尾的。

面试题1:变量的声明和定义有什么区别

为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
面试题2:写出boolintfloat、指针变量与“零值”比较的if语句
bool
型数据:if(flag ) { A; } else { B}
int
型数据:if(0 != flag ) { A; } else { B}
指针型数:if(NULL == flag ) { A; } else { B}
float
型数据:if( ( flag >= NORM ) && ( flag <= NORM ) ) {A
2
}
注意:应特别注意在int、指针型变量和“零值”比较的时候,把“零值”放在左边,这样当把“==”误写成“=”时,编译器可以报错,否则这种逻辑错误不容易发现,并且可能导致很严重的后果。

面试题3sizeofstrlen的区别

sizeofstrlen有以下区别:
sizeof
是一个操作符,strlen是库函数
sizeof
的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0‘的字符串作参数。
编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
数组做sizeof的参数不退化,传递给strlen就退化为指针了
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是sizeof

面试题4C语言的关键字staticC++的关键字 static有什么区别

Cstatic用来修饰局部静态变量和外部静态变量、函数。而C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
注意:编程时static的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而C++的静态成员则可以在多个对象实例间进行通信,传递信息。

面试题5:C中的malloc和C++中的new有什么区别

mallocnew有以下不同:
1newdelete操作符,可以重载,只能在C++中使用。
2mallocfree函数,可以覆盖,CC++中都可以使用。
3new可以调用对象的构造函数,对应的delete调用相应的析构函数。
4malloc仅仅分配内存,free仅仅回收内存,并不执行构造和析构函数
5newdelete返回的是某种数据类型指针,mallocfree返回的是void指针
注意:malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混用。因为两者实现的机理不同。

面试题6写一个“标准”宏MIN#define min(a,b)((a)<=(b)?(a):(b))

注意:在调用时一定要注意这个宏定义的副作用,如下调用:((++*p)<=(x)?(++*p):(x)
p
指针就自加了两次,违背了MIN的本意。
3

面试题7:一个指针可以是volatile

可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个buffer的指针时,必须用volatile来修饰这个指针。
说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。

面试题8a&a有什么区别

请写出以下代码的打印结果,主要目的是考察a&a的区别。#include<stdio.h>void main( void ) { int a[5]={1,2,3,4,5}; int *ptr=(int *)(&a+1);printf(“%d,%d”,*(a+1),*(ptr-1)); return;}
输出结果:25
注意:数组名a可以作数组的首地址,而&a是数组的指针。思考,将原式的int*ptr=(int *)(&a+1);改为int*ptr=(int*)(a+1);时输出结果将是什么呢?

面试题9:简述CC++程序编译的内存分配情况

CC++中内存分配方式可以分为三种:
1)从静态存储区域分配:
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static变量等。
2)在栈上分配:
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3)从堆上分配:
即动态内存分配。程序在运行的时候用mallocnew申请任意大小的内存,程序员自己负责在何时用freedelete释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
一个CC++程序编译时内存分为5大存储区:堆区、栈区、全局区、文字常量区、程序代码区
4

面试题10:简述strcpysprintfmemcpy的区别

三者主要有以下不同之处:
1)操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
2)执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低。
3)实现功能不同,strcpy主要实现字符串变量间的拷贝,sprintf主要实现其他数据类型格式到字符串的转化,memcpy主要是内存块间的拷贝。
说明:strcpysprintfmemcpy都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

面试题11:设置地址为0x67a9的整型变量的值为0xaa66int *ptr; ptr = (int *)0x67a9; *ptr =0xaa66;

说明:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。

面试题12:面向对象的三大特征

面向对象的三大特征是封装性、继承性和多态性:
封装性:将客观事物抽象成类,每个类对自身的数据和方法实行protectionprivateprotectedpublic)。
继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)
多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
说明:面向对象的三个特征是实现面向对象技术的关键,每一个特征的相关技术都非常的复杂,程序员应该多看、多练。

面试题13C++的空类有哪些成员函数

缺省构造函数。
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符const
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
5

面试题14:谈谈你对拷贝构造函数和赋值运算符的认识

拷贝构造函数和赋值运算符重载有以下两个不同之处:
1)拷贝构造函数生成新的类对象,而赋值运算符不能。
2)由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

面试题15:用C++设计一个不能被继承的类

template<typename T>

classA

{

friendT;

private:

A(){}

~A(){}

};

classB : virtual public A<B>

{

public:

B(){}

~B(){}

};

classC : virtual public B

{public:

C(){}

~C(){}

};

voidmain( void )

{

Bb; //C c; return;

}
注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。

面试题17:简述类成员函数的重写、重载和隐藏的区别

1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
virtual
的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被
7
virtual
修饰,也可以没有。
2)隐藏和重写、重载有以下几点不同。
与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。
说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
面试题18简述多态实现的原理
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针vptr(对vc编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptrvtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系了起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,。如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态C++面试的重要考点之一,而虚函数是实现多态的基础。

面试题19:链表和数组有什么区别

数组和链表有以下几点不同:
1)存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
3)数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
4)越界问题:链表不存在越界问题,数组有越界问题。
说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。

面试题20:怎样把一个单链表反序

1)反转一个链表。循环算法。Listreverse(List n) { if(!n) //判断链表是否为空,为空即退出。{return n; } list cur = n.next; //保存头结点的下个结点listpre = n; //保存头结点 listtmp;
8
pre.next = null; //
头结点的指针指空,转换后变尾结点while( NULL != cur.next ) //循环直到cur.next为空{tmp = cur; //实现如图10.3—10.5所示tmp.next= pre pre = tmp; cur = cur.next; } return tmp; //f返回头指针}
2)反转一个链表。递归算法。List*reverse( List *oldList, List *newHead = NULL ) { List *next =oldList-> next; //记录上次翻转后的链表oldList->next = newHead; //将当前结点插入到翻转后链表的开头newHead= oldList; //递归处理剩余的链表 return( next==NULL )? newHead: reverse( t, newHead );}
说明:循环算法就是图10.2—10.5的移动过程,比较好理解和想到。递归算法的设计虽有一点难度,但是理解了循环算法,再设计递归算法就简单多了。

面试题 21:简述队列和栈的异同

队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”。
注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

面试题22:能否用两个栈实现一个队列的功能

面试题23:计算一颗二叉树的深度

面试题24:编码实现直接插入排序

面试题25:编码实现冒泡排序

面试题26:编码实现直接选择排序

面试题27编程实现堆排序

面试题28:编程实现基数排序

面试题29:谈谈你对编程规范的理解或认识

编程规范可总结为:程序的可行性,可读性、可移植性以及可测试性。
说明:这是编程规范的总纲目,面试者不一定要去背诵上面给出的那几个例子,应该去理解这几个例子说明的问题,想一想,自己如何解决可行性、可读性、可移植性以及可测试性这几个问题,结合以上几个例子和自己平时的编程习惯来回答这个问题。

面试题30shorti = 0; i = i +1L;这两句有错吗

代码一是错的,代码二是正确的。
说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。
面试题31&&&|||有什么区别
1&|对操作数进行求值运算,&&||只是判断逻辑关系。
19
2&&||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&||替换成&|没有出错,但是其逻辑是错误的,可能会导致不可预想的后果(比如当两个操作数一个是1另一个是2时)。

面试题32C++的引用和C语言的指针有什么区别

指针和引用主要有以下区别:
1引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。

2)引用初始化以后不能被改变,指针可以改变所指的对象。
3)不存在指向空值的引用,但是存在指向空值的指针。
注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误。所以使用时一定要小心谨慎。

面试题33:在二元树中找出和为某一值的所有路径

输入一个整数和一棵二元树。从树的根结点开始往下访问,一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。例如,输入整数9和如下二元树:
3
/\
2 6
/ \
5 4
则打印出两条路径:36324
【答案】typedefstruct path { BiTNode* tree; //结点数据成员structpath* next; //结点指针成员}PATH,*pPath;
初始化树的结点栈:voidinit_path( pPath* L ) { *L = ( pPath )malloc( sizeof( PATH ) );//创建空树 (*L )->next = NULL; }
树结点入栈函数:voidpush_path(pPath H, pBTree T) { pPath p = H->next; pPath q = H;while( NULL != p )
20
{ q = p; p = p->next; } p = ( pPath)malloc( sizeof( PATH ) ); //
申请新结点p->next= NULL; //初始化新结点 p->tree= T; q->next = p; //新结点入栈}
树结点打印函数:voidprint_path( pPath L ) { pPath p = L->next; while( NULL != p )//打印当前栈中所有数据 {printf(“%d, “, p->tree->data); p = p->next; } }
树结点出栈函数:voidpop_path( pPath H ) { pPath p = H->next; pPath q = H; if( NULL ==p ) //检验当前栈是否为空 {printf(“Stack is null!\n”); return; } p = p->next; while( NULL!= p ) //出栈 {q = q->next; p = p->next; } free( q->next ); //释放出栈结点空间q->next= NULL; }
判断结点是否为叶子结点:intIsLeaf(pBTree T) { return ( T->lchild == NULL )&&(T->rchild==NULL ); }
查找符合条件的路径:intfind_path(pBTree T, int sum, pPath L)
21
{ push_path( L, T);record += T->data; if( ( record == sum ) && ( IsLeaf( T )) ) //
打印符合条件的当前路径 {print_path( L ); printf( “\n” ); } if( T->lchild != NULL )//递归查找当前节点的左孩子 {find_path( T->lchild, sum, L); } if( T->rchild != NULL )//递归查找当前节点的右孩子 {find_path( T->rchild, sum, L); } record -= T->data;pop_path(L); return 0;}
注意:数据结构一定要活学活用,例如本题,把所有的结点都压入栈,而不符合条件的结点弹出栈,很容易实现了有效路径的查找。虽然用链表也可以实现,但是用栈更利于理解这个问题,即适当的数据结构为更好的算法设计提供了有利的条件。

面试题34写一个“标准”宏MIN

写一个“标准”宏MIN,这个宏输入两个参数并且返回较小的一个。
【答案】#definemin(a,b)((a)<=(b)?(a):(b))
注意:在调用时一定要注意这个宏定义的副作用,如下调用:((++*p)<=(x)?(++*p):(x)
p
指针就自加了两次,违背了MIN的本意。

面试题35typedefdefine有什么区别

1)用法不同:typedef用来定义一种数据类型的别名,增强程序的可读性。define主要用来定义常量,以及书写复杂使用频繁的宏。
2)执行时间不同:typedef编译过程的一部分,有类型检查的功能。define是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
3)作用域不同:typedef有作用域限定define不受作用域约束,只要是在define声明后的引用都是正确的。
4)对指针的操作不同:typedefdefine定义的指针时有很大的区别。
注意:typedef定义是语句,因为句尾要加上分号。而define不是语句,千万不能在句尾加分号。

面试题36:关键字const是什么

const用来定义一个只读的变量或对象。主要优点:便于类型检查、同宏定义一样可以方便地进行参数的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。
说明:const修饰函数参数,是一种编程规范的要求,便于阅读,一看即知这个参数不能被改变,实现时不易出错。

面试题37static有什么作用

staticC中主要用于定义全局静态变量、定义局部静态变量、定义静态函数。在C++中新增了两种作用:定义静态数据成员、静态函数成员。
注意:因为static定义的变量分配在静态区,所以其定义的变量的默认值为0,普通变量的默认值为随机数,在定义指针变量时要特别注意。

面试题38extern有什么作用

extern标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。
面试题39:流操作符重载为什么返回引用
在程序中,流操作符>><<经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
注意:除了在赋值操作符和流操作符之外的其他的一些操作符中,如+-*/等却千万不能返回引用。因为这四个操作符的对象都是右值,因此,它们必须构造一个对象作为返回值。

面试题40简述指针常量与常量指针区别

指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

面试题41数组名和指针的区别

请写出以下代码的打印结果:#include<iostream.h> #include <string.h> void main(void) { charstr[13]=”Hello world!”;
23
char *pStr=”Hello world!”;cout<<sizeof(str)<<endl; cout<<sizeof(pStr)<<endl;cout<<strlen(str)<<endl; cout<<strlen(pStr)<<endl;return; }
【答案】
打印结果:134 1212
注意:一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富的多。但是当数组名当做参数传递给函数后,其失去原来的含义,变作普通的指针。另外要注意sizeof不是函数,只是操作符。

面试题42:如何避免“野指针”

野指针”产生原因及解决办法如下
1)指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL
2)指针pfree或者 delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL
3)指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL
注意:“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。

面试题43:常引用有什么作用

常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。
说明:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。

面试题44:编码实现字符串转化为数字

面试题45:简述strcpysprintfmemcpy的区别

三者主要有以下不同之处:
1)操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,
25
目的操作对象是字符串,memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
2)执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低。
3)实现功能不同,strcpy主要实现字符串变量间的拷贝,sprintf主要实现其他数据类型格式到字符串的转化,memcpy主要是内存块间的拷贝。
说明:strcpysprintfmemcpy都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

面试题46:用C编写一个死循环程序

while(1){}
说明:很多种途径都可实现同一种功能,但是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间较小,所以时间和空间优势是选择各种方法的首要考虑条件。

面试题47:编码实现某一变量某位清0或置1

给定一个整型变量a,写两段代码,第一个设置abit3,第二个清abit3,在以上两个操作中,要保持其他位不变。
【答案】#defineBIT3 (0×1 << 3 ) Satic int a;
设置abit3: void set_bit3( void ) { a |= BIT3; //a3位置1}
abit3 void set_bit3( void ) { a &= ~BIT3; //a3位清零}
说明:在置或清变量或寄存器的某一位时,一定要注意不要影响其他位。所以用加减法是很难实现的。

面试题48:评论下面这个中断函数

中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展——让标准C支持中断。具体代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt关键字去定义一个中断服务子程序(ISR),请评论以下这段代码。__interruptdouble compute_area (double radius) { double area = PI * radius *radius; printf(” Area = %f”, area); returnarea;
}
【答案】
这段中断服务程序主要有以下四个问题:
1ISR不能返回一个值。
2ISR不能传递参数。
3)在ISR中做浮点运算是不明智的。
4printf()经常有重入和性能上的问题。
注意:本题的第三个和第四个问题虽不是考察的重点,但是如果能提到这两点可给面试官留下一个好印象。

面试题49:构造函数能否为虚函数

构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
说明:虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。

面试题50:谈谈你对面向对象的认识

面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。
说明:编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。


归并排序:

//将有二个有序数列a[first...mid]a[mid...last]合并。 

    void mergearray(int a[], int first, int mid, int last, int temp[])  
    {  
        int i = first, j = mid + 1;  
        int m = mid,   n = last;  
        int k = 0;     

        while (i <= m && j <= n)  
        {  
            if (a[i] <= a[j])  
                temp[k++] = a[i++];  
            else  
                temp[k++] = a[j++];  
        }  

        while (i <= m)  
            temp[k++] = a[i++];  
        while (j <= n)  
            temp[k++] = a[j++];  
        for (i = 0; i < k; i++)  
            a[first + i] = temp[i];  
    }  
    void mergesort(int a[], int first, int last, int temp[])  
    {  
        if (first < last)  
        {  
            int mid = (first + last) / 2;  
            mergesort(a, first, mid, temp);    //左边有序  
            mergesort(a, mid + 1, last, temp); //右边有序  
            mergearray(a, first, mid, last, temp); //再将二个有序数列合并  
        }  

    }  

      


四种进程或线程同步互斥的控制方法

   1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
   2
、互斥量:为协调共同对一个共享资源的单独访问而设计的。
   3
、信号量:为控制一个具有有限数量用户资源而设计。
   4
、事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

  
  

临界区(CriticalSection


保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区包含两个操作原语:
EnterCriticalSection
()进入临界区
LeaveCriticalSection
()离开临界区
EnterCriticalSection
()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
MFC
提供了很多功能完备的类,我用MFC实现了临界区。MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的。只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。Lock()后代码用到的资源自动被视为临界区内的资源被保护。UnLock后别的线程才能访问这些资源。

互斥量(Mutex

  
互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
  
互斥量包含的几个操作原语:
CreateMutex
()创建一个互斥量
OpenMutex
()打开一个互斥量
ReleaseMutex
()释放互斥量
WaitForMultipleObjects
()等待互斥量对象
  
同样MFC为互斥量提供有一个CMutex类。使用CMutex类实现互斥量操作非常简单,但是要特别注意对CMutex的构造函数的调用
CMutex(BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL,LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
不用的参数不能乱填,乱填会出现一些意想不到的运行结果。

信号量(Semaphores


信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。
PV
操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。
P
操作申请资源:
  
  (1S1
  
  (2)若S1后仍大于等于零,则进程继续执行;
  
  (3)若S1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
  V
操作 释放资源:
  
  (1S1
  
  (2)若相加结果大于零,则进程继续执行;
  
  (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。
  
  
  信号量包含的几个操作原语:
  
  CreateSemaphore()创建一个信号量
  
  OpenSemaphore()打开一个信号量
  
  ReleaseSemaphore()释放信号量
  
  WaitForSingleObject()等待信号量

事件(Event

  
事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。
信号量包含的几个操作原语:
  
  CreateEvent()创建一个信号量
  
  OpenEvent()打开一个事件
  
  SetEvent()回置事件
  
  WaitForSingleObject()等待一个事件
  
  WaitForMultipleObjects()        等待多个事件
  
    WaitForMultipleObjects函数原型:
  
     WaitForMultipleObjects
  
     INDWORD nCount, // 等待句柄数
  
     INCONST HANDLE *lpHandles, //指向句柄数组
  
     INBOOL bWaitAll, //是否完全等待标志
  
     INDWORD dwMilliseconds //等待时间
  
     )
参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT

总结:
1
互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
2
.互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。
3
.通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。



四种进程或线程同步互斥的控制方法

1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
2
、互斥量:为协调共同对一个共享资源的单独访问而设计的。
3
、信号量:为控制一个具有有限数量用户资源而设计。
4
、事件:用来通知线程有一些事件已发生,从而启动后继任务的开始

临界区(CriticalSection同一个进程内,实现互斥
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

互斥量(Mutex可以跨进程,实现互斥
互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。

信号量(Semaphores主要是实现同步,可以跨进程
信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出

事件(Event实现同步,可以跨进程
事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。


二、进程调度算法

1.先进先出算法(FIFO)按照进程进入就绪队列的先后次序来选择。即每当进入进程调度,总是把就绪队列的队首进程投入运行。

2.时间片轮转算法(RR)分时系统的一种调度算法。轮转的基本思想是,将CPU的处理时间划分成一个个的时间片,就绪队列中的进程轮流运行一个时间片。当时间片结束时,就强迫进程让出CPU,该进程进入就绪队列,等待下一次调度,同时,进程调度又去选择就绪队列中的一个进程,分配给它一个时间片,以投入运行。

3.最高优先级算法(HPF)进程调度每次将处理机分配给具有最高优先级的就绪进程。最高优先级算法可与不同的CPU方式结合形成可抢占式最高优先级算法和不可抢占式最高优先级算法。

4.多级队列反馈法:几种调度算法的结合形式多级队列方式。



单例模式

//Singleton.h  

  1. class Singleton    

  2. {  

  3. public:  

  4.     static Singleton* GetInstance();  

  5. private:  

  6.     Singleton() {}  

  7.     static Singleton *m_pInstance;  

  8. };  

  9. //Singleton.cpp  

  10. Singleton* Singleton::m_pInstance = NULL;  

  11. Singleton* Singleton::GetInstance()  

  12. {  

  13.     if(m_pInstance == NULL)  

  14.         m_pInstance = new Singleton();  

  15.     return m_pInstance;  

  16. }  

class CSingleton  
    {  
    private:  
        CSingleton()  
       {  
        }  
        static CSingleton *m_pInstance;  
        class CGarbo   //它的唯一工作就是在析构函数中删除CSingleton的实例  
        {  
        public:  
            ~CGarbo()  
            {  
                if(CSingleton::m_pInstance)  
                   delete CSingleton::m_pInstance;  
            }  
        };  

        static CGarbo Garbo;  //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数  
    public:  
        static CSingleton * GetInstance()  
        {  
            if(m_pInstance == NULL)  //判断是否第一次调用  
                m_pInstance = new CSingleton();  
            return m_pInstance;  
        }  
    }; 

exit()函数与_exit()函数的区别

_exit函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核的各种数据结构;exit函数则在这些基础上做了一些小动作,在执行退出之前还加了若干道工序。exit()函数与_exit()函数的最大区别在于exit()函数在调用exit 系统调用前要检查文件的打开情况,把文件缓冲区中的内容写回文件。也就是图中的“清理I/O缓冲”。



C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?

C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为:voidfee(int x,inty);

该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。

C++提供了C连接交换指定符号extern“C”来解决名字匹配的问题。

实现string类

#include<utility>
#include<string.h>

classString
{
 public:
  String()
    :data_(new char[1])
  {
    *data_= '\0';
  }
  String(constchar* str)
    :data_(new char[strlen(str) + 1])
  {
    strcpy(data_,str);
  }
  String(constString& rhs)
    :data_(new char[rhs.size() + 1])
  {
    strcpy(data_,rhs.c_str());
  }
  /*Delegate constructor in C++11
  String(constString& rhs)
    :String(rhs.data_)
  {
  }
  */
  ~String()
  {
    delete[]data_;
  }
  /*Traditional:
  String&operator=(const String& rhs)
  {
    Stringtmp(rhs);
    swap(tmp);
    return*this;
  }
  */
  String&operator=(String rhs) // yes, pass-by-value
  {
    swap(rhs);
    return*this;
  }
  //C++ 11
  String(String&&rhs)
    :data_(rhs.data_)
  {
    rhs.data_= nullptr;
  }
  String&operator=(String&& rhs)
  {
    swap(rhs);
    return*this;
  }
  //Accessors
  size_tsize() const
  {
    returnstrlen(data_);
  }
  constchar* c_str() const
  {
    returndata_;
  }
  voidswap(String& rhs)
  {
    std::swap(data_,rhs.data_);
  }
 private:
  char*data_;
};



1.一个系统里面有多个线程,其中一个线程想终止另外一个线程,该怎么实现.

2.为什么要用virtualdestructor

3.什么是heapcorruption

4.Semaphore VS Mutex

5.C++中的static,怎么用,内存在哪里分配,等等

6.sizeof一个指针和sizeof一个指针指向的空间有什么不同?

7.char* char[]有什么区别?

8.操作系统的Page是怎么做的?

9.什么是Thrashing

10.什么是volatileu关键字?在什么情况下需要使用?

11.什么是criticalsection?

 

12.C++里面的dynamic_cast如何实现,为什么dynamic_cast要求被cast的指针指向的类
至少要有一个virtualmethod?

13.virtual inheritance 为什么能够避免导出类中有多于一个的virtualbase class
copy?

14.deadlock’s four condition

15.关于C++处理异常的方法

16.static 变量最大的问题是什么(考虑多线程)?

17.state Machine 的几种实现方法, 各自的优缺点是什么

18.多线程程序里,你最喜欢哪种同步操作?Mutex是大家用得最多的,为什么?Mutex有什么不好?(overheadserializedeadlock),给例子。如何避免这些坏处?(lock-free算法,比如lock-freequeuestackhashtable)一般用什么办法来写lock-free算法?(CAS等原子操作,等等),如何实现一个lock-freequeue?(coding ?)

19.strlen(“hello”)=? , sizeof(“hello”)=?

20.如果main函数只有如下结构
intmain(…)
{
Try{…}
catch(){…}
}
并且这个catch语句catch了所有的exception,但是这个程序还是coredump了,问可能是什么问题引起的。

21.singleton, 多线程,如何做doublecheck?

22.Command Pattern

23.同步/异步IO和阻塞/非阻塞IO的区别?

24.写一个threadsafe lazy initialization singleton

25.Map ReduceMulti-threadtrade-off



1、智能指针,com实现的原理,
2
printf()可变参数如何实现
3
、标准模板库vector追加数据如何实现。是底层如何实现,不能用现有的东东。
4
、还有,java的垃圾收集机制如何实现为什么?如果是你自己实现垃圾收集机制,如何实

现? 用什么数据结构。转摘请注明:www.pghome.net
5
、二叉排序树和哈希表那个查找效率高,实用于pda
6
.net的底层实现机制。
7
、进程间通信如何实现。
8
、还有迭代问题,什么问题用迭代,迭代在操作系统中如何实现的。
9
、如何交换两个变量,不能用中间变量。
10
cc++static函数的区别???
11
const函数的作用,如何实现钩子函数。
12
、两层容错技术怎么实现?
13
、写出函数指针,函数返回指针,const指针,指向const的指针,指向constconst指针

14
、函数调用如何实现,注意什么问题。
15
、指针和引用的差别,
16
、拷贝构造函数如何实现,什么情况下会用到。



野指针”不是NULL指针,而是指向“垃圾”的内存指针,其主要成因是:指针变量没有被初始化,或者指针pfree或者delete之后,没有置为NULL



预编译又称为预处理,是做些代码文本的替换工作。处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。
c
编译系统在对程序进行通常的编译之前,先进行预处理。c提供的预处理功能主要有以下三种: 1)宏定义 2)文件包含 3)条件编译



什么是预编译,何时需要预编译:总是使用不经常改动的大型代码体。
程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个预编译头。


C++程序中调用被 C编译器编译后的函数,为什么要加extern“C”?(5分)
答:C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为:voidfoo(int x, inty);
该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
C++
提供了C连接交换指定符号extern“C”来解决名字匹配问题。


Windows是一个消息(Message)驱动系统。Windows的消息提供了应用程序之间、应用程序与Windows系统之间进行通信的手段。应用程序想要实现的功能由消息来触发,并且靠对消息的响应和处理来完成。
      Windows
系统中有两种消息队列:系统消息队列和应用程序消息队列计算机的所有输入设备由Windows监控。当一个事件发生时,Windows先将输入的消息放入系统消息队列中,再将消息拷贝到相应的应用程序消息队列中。应用程序的消息处理程序将反复检测消息队列,并把检测到的每个消息发送到相应的窗口函数中。这便是一个事件从发生至到达窗口函数必须经历的过程。
      
必须注意的是,消息并非是抢占性的,无论事件的缓急,总是按照到达的先后派对,依次处理(一些系统消息除外),这样可能使一些实时外部事件得不到及时处理

Windows中的消息是放在对应的进程的消息队列里的。可以通过GetMessage取得,并且对于一般的消息,此函数返回非零值,但是对于WM_QUIT消息,返回零。可以通过这个特征,结束程序。当取得消息之后,应该先转换消息,再分发消息。所谓转换,就是把键盘码的转换,所谓分发,就是把消息分发给对应的窗口,由对应的窗口处理消息,这样对应窗体的消息处理函数就会被调用。两个函数可以实现这两个功能:TranslateMessageDispatchMessage
      
另外,需要注意,当我们点击窗口的关闭按钮关闭窗口时,程序并没有自动退出,而是向程序发送了一个WM_DESTROY消息(其实过程是这样的,首先向程序发送WM_CLOSE消息,默认的处理程序是调用DestroyWindow销毁窗体,从而引发WM_DESTROY消息),此时在窗体中我们要响应这个消息,如果需要退出程序,那么就要向程序发送WM_QUIT消息(通过PostQuitMessage实现)。一个窗体如果想要调用自己的消息处理函数,可以使用SendMessage向自己发消息。
      
如上所述,大部分(注意是大部分)的消息是这样传递的:首先放到进程的消息队列中,之后由GetMessage取出,转换后,分发给对应的窗口。这种消息成为存储式消息。存储式消息基本上是使用者输入的结果,以击键(如WM_KEYDOWNWM_KEYUP讯息)、击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)的形式给出。存储式消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。但是也有的消息是直接发送给窗口的,它们被称为非存储式消息。例如,当WinMain调用CreateWindow时,Windows将建立窗口并在处理中给窗口消息处理函数发送一个WM_CREATE消息。当WinMain调用ShowWindow时,Windows将给窗口消息处理函数发送WM_SIZEWM_SHOWWINDOW消息。当WinMain调用UpdateWindow时,Windows将给窗口消息处理函数发送WM_PAINT消息。



语言定义中说明,每一种指针类型都有一个特殊值——“空指针” —— 它与同类型的其它所有指针值都不相同,它“与任何对象或函数的指针值都不相等”。也就是说,取地址操作符&永远也不能得到空指针,同样对malloc()的成功调用也不会返回空指针,如果失败,malloc() 的确返回空指针,这是空指针的典型用法:表示“未分配”或者“尚未指向任何地方”的指针。
空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数;而未初始化指针则可能指向任何地方。
如上文所述,每种指针类型都有一个空指针,而不同类型的空指针的内部表示可能不尽相同。尽管程序员不必知道内部值,但编译器必须时刻明确需要那种空指针,以便在需要的时候加以区分


根据语言定义,在指针上下文中的常数0会在编译时转换为空指针。也就是说,在初始化、赋值或比较的时候,如果一边是指针类型的值或表达式,编译器可以确定另一边的常数0为空指针并生成正确的空指针值。因此下边的代码段完全合法:
char*p = 0;
if(p != 0)
然而,传入函数的参数不一定被当作指针环境,因而编译器可能不能识别未加修饰的0“表示” 指针。在函数调用的上下文中生成空指针需要明确的类型转换,强制把0看作指针。例如,Unix 系统调用execl接受变长的以空指针结束的字符指针参数。它应该如下正确调用:
execl(“/bin/sh”,“sh”, “-c”, “date”, (char *)0);
如果省略最后一个参数的(char*) 转换,则编译器无从知道这是一个空指针,从而当作一个0传入。(注意很多Unix手册在这个例子上都弄错了。
如果范围内有函数原型,则参数传递变为“赋值上下文”,从而可以安全省略多数类型转换,因为原型告知编译器需要指针,使之把未加修饰的0正确转换为适当的指针。函数原型不能为变长参数列表中的可变参数提供类型。在函数调用时对所有的空指针进行类型转换可能是预防可变参数和无原型函数出问题的最安全的办法。


死锁deadlock

产生死锁的四个必要条件:

1)互斥条件:一个资源每次只能被一个进程使用。
2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

linux下进程间通信的几种主要手段简介:

  1. 管道(Pipe)及有名管道(namedpipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

  2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

  3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  4. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

  5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

  6. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:LinuxSystemV的变种都支持套接字。

一般来说,linux下的进程包含以下几个关键要素:

  • 有一段可执行程序;

  • 有专用的系统堆栈空间;

  • 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;

  • 具有独立的存储空间

普通的虚函数提供了接口与缺省实现。(公有继承的)派生类继承接口的同时可以自动继承接口的缺省实现(免费的午餐),或者选择重新定义该函数来特化自己的行为。风险是以后需要定制此行为的派生类可能因为忘记重定义而错误地使用缺省实现。

  • 纯虚函数只提供了接口。但是具有函数体的纯虚函数同时还提供了缺省实现。派生类必须显式定义接口。可以通过显式调用基类函数来完成缺省行为。

  • 为了完整,顺便补充一下,公有非虚函数提供了接口与强制实现。也就是说所有的派生类必须继承这个接口及其实现。


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:11397次
    • 积分:527
    • 等级:
    • 排名:千里之外
    • 原创:42篇
    • 转载:7篇
    • 译文:0篇
    • 评论:0条