c语言笔试题总结1

总结1

本文摘录网上能够找到的c语言笔试题目,回答中包含个人的理解,如有错误,希望能够得到指正,多谢。


1. main函数执行完毕后,是否可能会再执行一段代码?

答:是。main函数执行完毕后还可以执行代码,它可以通过atexit()函数来注册回调函数,回调函数的形式是void (*function)(void)。atexit()声明在stdlib.h中。具体可以参考:c语言基础函数——exit()和atexit()


2. 给一个字符串、例如 “ababc”要求返回“ab”,因为“ab”连续重复出现且最长。用C/C++语言写一函数完成该算法,给出复杂度。

答:下面是具体的参考代码:

#include 
#include 

/*
函数表示:返回给定字符串中的最大子字符串长度
参数:  src:原始字符串
		sub:最大子字符串
返回值:最大子字符串长度(不包含结尾的\0),因为strlen本来也不包含\0。
说明:
1. 如果存在多个子字符串的长度相同,返回第一个子字符串
*/
int GetSubStr(char * src, char * sub) {
	int length;		//原始字符串的长度
	int max;		//最大子字符串的长度
	int tmp_max;            //最大子字符串的长度,临时,比较时使用
	int index;
	char *p, *q;            //用来遍历字符串的两个指针
	char *k;		//指向最终的子字符串的起始位置

	length = strlen(src);
	printf("length of src is %d\n", strlen(src));
	max = 0;
	p = src;
	k = src;
	//从第一个字符开始遍历
	while (*p != '\0') {
		//与前一个字符比较
		for (q = p + 1; *q != '\0'; q++) {
			tmp_max = 0;
			//如果相等了,表示有可能出现最大连续字符串了,之后就要以p和q为起始,比较两者之后的字符串是否相等
			if (*p == *q) {
				//起始字符相等后,就开始比较两个字符串
				for (index = 0; index < q - p; index++) {
					if (*(p + index) == *(q + index)) {
						tmp_max++;
					} else {
						if (tmp_max > max) {
							max = tmp_max;
							k = p;
						}
						break;
					}
				}
			} 
		}
		p++;
	}
	strncpy(sub, k, max);
	//最后需要手动加上一个\0
	sub[max] = '\0';

	return max;
}

void main() {
	char * src = "I am a abc and he is not abc";
	char result[100];
	int max = GetSubStr(src, result);

	printf("result[%d]: %s\n", max, result);

	return 0;
}

上述代码返回的结果是" abc“,最前面有一个空格。

说明:上述代码的基本思路是使用两个指针,第一个从头开始遍历,即while (*p != '\0');第二个是从第一个指针之后的指针,从该位置开始找与第一个指针指向的内容具有相等字符串的指针位置,即for (q = p + 1; *q != '\0'; q++)循环。该算法的复杂度是O(n^2)。


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

答:这些函数的区别在于实现功能以及操作对象不同。

1. strcpy函数操作的对象是字符串,完成从源字符串到目的字符串的拷贝功能。

2. sprintf函数操作的对象不限于字符串:虽然目的对象是字符串,但是源对象可以是字符串、也可以是任意基本类型的数据。这个函数主要用来实现(字符串或基本数据类型)向字符串的转换功能。如果源对象是字符串,并且指定 %s 格式符,也可实现字符串拷贝功能。

3. memcpy函数顾名思义就是内存拷贝,实现将一个内存块的内容复制到另一个内存块这一功能。内存块由其首地址以及长度确定。程序中出现的实体对象,不论是什么类型,其最终表现就是在内存中占据一席之地(一个内存区间或块)。因此,memcpy 的操作对象不局限于某一类数据类型,或者说可适用于任意数据类型,只要能给出对象的起始地址和内存长度信息、并且对象具有可操作性即可。鉴于memcpy函数等长拷贝的特点以及数据类型代表的物理意义,memcpy 函数通常限于同种类型数据或对象之间的拷贝,其中当然也包括字符串拷贝以及基本数据类型的拷贝。

对于字符串拷贝来说,用上述三个函数都可以实现,但是其实现的效率和使用的方便程度不同:

• strcpy无疑是最合适的选择:效率高且调用方便。

• sprintf要额外指定格式符并且进行格式转化,麻烦且效率不高。

• memcpy 虽然高效,但是需要额外提供拷贝的内存长度这一参数,易错且使用不便;并且如果长度指定过大的话(最优长度是源字符串长度 + 1),还会带来性能的下降。其实strcpy函数一般是在内部调用memcpy函数或者用汇编直接实现的,以达到高效的目的。因此,使用 memcpy 和 strcpy 拷贝字符串在性能上应该没有什么大的差别。

对于非字符串类型的数据的复制来说,strcpy和snprintf一般就无能为力了,可是对memcpy却没有什么影响。但是,对于基本数据类型来说,尽管可以用memcpy进行拷贝,由于有赋值运算符可以方便且高效地进行同种或兼容类型的数据之间的拷贝,所以这种情况下memcpy几乎不被使用。memcpy 的长处是用来实现(通常是内部实现居多)对结构或者数组的拷贝,其目的是或者高效,或者使用方便,甚或两者兼有。


4. 变量的定义和声明有什么区别?

答:定义包含为变量分配存储空间和指定初始值;而声明仅用于向编译器告知变量的名称和类型。


5. 请写出下面代码在32位平台上的运行结果,并说明 sizeof 的性质。

答:直接看代码,注释部分为结果:

#include 
#include 
int main(void)
{
	char a[30];
	char *b = (char *)malloc(20 * sizeof(char));
	printf("%d\n", sizeof(a));		// 30,a是一个数组,这里应该计算所有的值
	printf("%d\n", sizeof(b));		// 4,b就是一个char指针
	printf("%d\n", sizeof(a[3]));		// 1,一个char
	printf("%d\n", sizeof(b + 3));		// 4,一个char指针
	printf("%d\n", sizeof(*(b + 4)));	// 1,一个char
	return 0;
}

结果已经有了,sizeof的性质也就很明了了,不写了。


6. 请编写一个C函数,该函数给出一个字节中被置1的位的个数,并请给出该题的至少一个不同解法。

答:下面是具体的代码。两种不同的解法中,第一种依赖的是移位,第二种依赖的是2的除法和取余:

#include 
#include 

/*
函数说明:返回参数中比特位值是1的总个数
函数参数:value:待测试的值
返回值  :待测试值中的比特位值是1的总个数
*/
int BitOneTest1(char value) {
	int index;
	int result;	//返回结果
	char tmp;	//存放value及其移位值的临时变量

	result = 0;
	for (index = 0; index < 8; index++) {
		tmp = value >> index;
		result += tmp & 1;
	}

	return result;
}

int BitOneTest2(char value) {
	int result;		//返回结果
	unsigned char tmp;	//这里需要将value转换成unsigned char,否则当value最高位是1时后面除法和取余操作会使结果发生错误

	result = 0;
	tmp = (unsigned char)value;

	while (tmp) {
		if ((tmp % 2) == 1) {
			result++;
		}
		tmp = tmp / 2;
	}

	return result;
}

int main(void)
{
	char num = 0b11111111;
	printf("[方法1]bit置为1的个数: %d\n", BitOneTest1(num));
	printf("[方法2]bit置为1的个数: %d\n", BitOneTest2(num));

	return 0;
}

又看到一种方法:

#include
#define TRUE	1
#define FALSE	0

int BitOneTest3(char value) {
	int count = 0;
	while (value)
	{
		count++;
		value = value & (value - 1);
	}
	return count;
}

int main(void)
{
	char num = 0b11110111;
	printf("[方法3]bit置为1的个数: %d\n", BitOneTest3(num));
	return 0;
}

7. 实现自己的itoa和atoi函数。

答:参见http://blog.csdn.net/jiangwei0512/article/details/50668937。


8. 定义 int **a[3][4], 则变量占有的内存空间为多少。

答:这题不确定要考什么东西......要是是确定变量a的大小,那么就是4,因为它就是一个指针。如果是数组的大小,那么是4*3*4=48,它实际上是一个指针的指针的二维数组。


9. 编写一个函数,要求输入年月日时分秒,输出该年月日时分秒的下一秒。如输入2004年12月31日23时59分59秒,则输出2005年1月1日0时0分0秒。

答:参见具体代码:

void NextTime (
	int *year, 
	int *month, 
	int *date, 
	int *hour, 
	int *minute, 
	int*second
)
{
	int dayOfMonth[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
	if (*year < 0 || *month < 1 || *month > 12 ||
		*date < 1 || *date > 31 || *hour < 0 || *hour > 23 ||
		*minute < 0 || *minute > 59 || *second <0 || *second >60)
		return;

	//处理闰年
	if ((*year % 400 == 0) || ((*year % 100 != 0) && (*year % 4 == 0)))
		dayOfMonth[1] = 29;

	*second = *second + 1;
	if (*second >= 60)
	{
		*second = 0;
		*minute += 1;
		if (*minute >= 60)
		{
			*minute = 0;
			*hour += 1;
			if (*hour >= 24)
			{
				*hour = 0;
				*date += 1;
				if (*date > dayOfMonth[*month - 1])
				{
					*date = 1;
					*month += 1;
					if (*month > 12)
					{
						*month = 1; 
						*year += 1;
					}
				}
			}
		}
	}
	return;
}

10. 写一个函数,判断一个int型的整数是否是2的幂,即是否可以表示成2^X的形式(不可以用循环)。

答:这里指定了不能用循环,那么可以考虑使用递归,下面是具体的代码:

#define TRUE	1
#define FALSE	0
int IsBinrayPower(int num) {
	int tmp = num;
	//0的时候返回FALSE
	if (tmp == 0) {
		return FALSE;
	} else if (tmp == 1) {
		// 1 = 2 ^ 0,所以返回TRUE;
		return TRUE;
	}
	else {
		if (tmp % 2 == 0) {
			return IsBinrayPower(num / 2);
		} else {
			return FALSE;
		}
		
	}
}

还有一种方法,一句代码就搞定了:

#define TRUE	1
#define FALSE	0
int IsBinrayPower(int num) {
	return (num & (num - 1)) ? FALSE : TRUE;
}

11. int (* (*f)(int, int))(int)表示什么含义?

答:f是一个函数指针,它的参数是(int ,int),返回值是一个函数指针(形式是int *(g)(int))。虽然看上去很复杂,但是在实际的应用中确实也有类似的,比如在signal.h中就有这样的定义:

void	(*signal(int, void (*)(int)))(int);

(以上来自OS X系统,其它系统可能略有不同)


12. x=x+1,x+=1,x++,为这三个语句的效率排序。并说明为什么。

答:从网上找到的结果是,x=x+1 < x+1 < x++,因为:

x=x+1的执行过程如下:1. 读取右x的地址; 2. x+1; 3. 读取左x的地址; 4. 将右值传给左边的x(编译器并不认为左右x的地址相同)。

x+=1的执行过程如下:1. 读取右x的地址; 2. x+1; 3. 将得到的值传给x(因为x的地址已经读出)。

x++的执行如下:1. 读取右x的地址; 2. x自增1。

但实际的情况是:VS2015下反汇编的结果如下:

	x = x + 1;
00D81685  mov         eax,dword ptr [x]  
00D81688  add         eax,1  
00D8168B  mov         dword ptr [x],eax  
	x += 1;
00D8168E  mov         eax,dword ptr [x]  
00D81691  add         eax,1  
00D81694  mov         dword ptr [x],eax  
	x++;
00D81697  mov         eax,dword ptr [x]  
00D8169A  add         eax,1  
00D8169D  mov         dword ptr [x],eax  

实际上根本没有什么区别,编译器会去优化的。


13. 写一算法检测单向链表中是否存在环(whether there is a loop in a link list),要求算法复杂度(Algorithm's complexity是O(n)) 并只使用常数空间(space is O(c))。

答:使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。具体的代码如下:

#define TRUE	1
#define FALSE	0
typedef struct _Node{
	int data;
	struct _Node * next;
} Node;
int IsLoop(Node *head)
{
	Node *pSlow = head;
	Node *pFast = head;
	while (pSlow != NULL && pFast != NULL)
	{
		pSlow = pSlow->next;
		pFast = pFast->next->next;
		if (pSlow == pFast)
			return TRUE;
	}
	return FALSE;
}

14. static全局变量与普通全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?

答:

1. static全局变量和普通的全局变量,在内存中的位置是一样的,区别在于static全局变量只在当前的文件有效,而普通全局变量在所有的文件中都有效。

2. static局部变量和普通局部变量在内存中的存储就不一样了,使得普通全局变量每次都会重新初始化,而static局部变量只会初始化一次,之后就沿用上一次的值。

3. static函数和普通函数的差别是,static函数只在当前文件有效,而普通函数默认是extern的,因此在其它文件也有效。


15. 下面程序的打印结果是什么?

#include
main()
{
	char *a = "hello";
	char *b = "hello";
	if (a == b)
		printf("YES");
	else
		printf("NO");
}

答:粗的来看应该是打印NO,但是因为hello是存放在静态存储区的,编译器有可能进行优化,将a和b指向同一个hello,此时a==b。在vs2015上的测试结果也是YES。


16. 求a b c的结果。

答:参见具体的代码,这题主要考察的是一个就近原则,即a+++b实际上是(a++)+b;还有就是后++发生在+=等操作之后:

#include
void main()
{
	int a = 5, b = 7, c;
	c = a+++b;
	printf("a = %d, b = %d, c= %d\n", a, b, c);	//a = 6, b = 7, c= 12
}

17. What does the following function return?

#include
char foo(void)
{
	unsigned int a = 6;
	int b = -20;
	printf("%d\n", ((a + b) > 6));
	char c;
	(a + b > 6) ? (c = 1) : (c = 0);
	return c;
}

void main()
{
	printf("%d\n", foo());	//1
}

答:这里的考察的是一个类型的转换,在《c++ primer》中文版第五版中,P34页有这么一句:“当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数。”因此就可以看到这里的a+b中,-20会被转换成无符号数,所以a+b > 6成立,因此c=1。


18. 有1,2,....一直到n的无序数组,求排序算法,并且要求时间复杂度为O(n),空间复杂度O(1),使用交换,而且一次只能交换两个数。

答:参见具体代码:

#include
void main()
{
	int a[] = { 9, 3, 4, 5, 6, 8, 2, 7, 1 };
	//排序后的数组应该是{1,2,3,4,5,6,7,8,9},也就是说元素值和下标是一一对应的,这个就是算法的基础

	int tmp;//临时存放数据
	int index;
	int length = sizeof(a) / sizeof(int);//数组长度

	for (index = 0; index < length; ) {
		tmp = a[a[index] - 1];//a[index]是原值,原值应该放的位置是a[index]-1,这个位置对应的值是a[a[index] - 1]
		a[a[index] - 1] = a[index];
		a[index] = tmp;
		//index++的条件是该位置的值已经正确了
		if (a[index] == index + 1) {
			index++;
		}
	}

	for (index = 0; index < length; index++) {
		printf("%d ", a[index]);
	}
	printf("\n");
}

19. 写一个函数判断计算机的字节存储顺序是升序(little-endian)还是降序(big-endian)。

答:需要记住的是,小端模式下低字节内存放低位数据,即,对于int类型的数据0x12345678,最低地址放的是0x78。这个可以作为编程的依据:

#include 

int main(void)
{
	unsigned int a = 0x12345678;
	if (*((unsigned char *)&a) == 0x78) {
		printf("little end\n");
	}
	else {
		printf("big end");
	}
	return 0;
}

20. 有个数组a[100]存放了100个数,这100个数取自1-99,且只有两个相同的数,剩下的98个数不同,写一个搜索算法找出相同的那个数的值。(注意空间效率时间效率尽可能要低)。

答:参见第18题,两者非常的相似。如果这里按照18题的代码那样进行排序,并将原来的int a[] = { 9, 3, 4, 5, 6, 8, 2, 7, 1 };更换成int a[] = { 7, 3, 4, 5, 6, 8, 2, 7, 1 };代码将会挂死。原因就是因为两个数相同之后,导致这两个相同的一直再做交换。只要修改一下代码,就可以用到这一题上来。

#include
void main()
{
	int a[] = { 7, 3, 4, 5, 6, 6, 1, 2, 8 };

	int tmp;//临时存放数据
	int index;
	int length = sizeof(a) / sizeof(int);//数组长度

	for (index = 0; index < length; ) {
		tmp = a[a[index] - 1];//a[index]是原值,原值应该放的位置是a[index]-1,这个位置对应的值是a[a[index] - 1]
		a[a[index] - 1] = a[index];
		a[index] = tmp;
		//index++的条件是该位置的值已经正确了
		if (a[index] == index + 1) {
			index++;
		}
		//新增代码
		else if (a[index] == a[a[index] - 1]) {
			printf("found: %d\n", a[index]);//found 6
			break;
		};
	}
}

21. 写出判断ABCD四个表达式的是否正确, 若正确, 写出经过表达式中a的值。int a = 4;

(A)a += (a++);   (B) a += (++a) ;   (C) (a++) += a;   (D) (++a) += (a++);

答:AB,CD错误。具体的值参见代码:

#include
void main()
{
	int a = 4;
	a += (a++);
	printf("a = %d\n", a);//9

	a = 4;
	a += (++a);
	printf("a = %d\n", a);//10

	a = 4;
//	(a++) += a;//错误
	printf("a = %d\n", a);

	a = 4;
//	(++a) += (a++);//错误
	printf("a = %d\n", a);
}

a++和++a不能作为左值。a+=(a++),这里的a实际上只有一次自增会被计算进去,第二次没有,所以结果是4+5=9。


22. switch中允许的数据类型是?

答:整型常量表达式。重点是整型和常量。


23. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?

答:参见具体的代码:

while (1);
for (;;);

24. Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子: 

#define dPS struct s * 
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么? 

答:typedef好。具体见下面的代码:

#include
struct s {
	int a;
	int b;
};
#define dPS struct s * 
typedef struct s * tPS;
void main()
{
	dPS p1, p2;
	tPS p3, p4;
	printf("p1: %d, p2: %d, p3: %d, p4: %d\n", sizeof(p1), sizeof(p2), sizeof(p3), sizeof(p4));	//p1: 4, p2: 8, p3: 4, p4: 4
}

注意结果中sizeof(p2)=8,原因是对于#define,编译器只是在编译时展开宏,所以得到的结果是struct s *p1, p2。p2实际上就是一个结构体,这个可能不是代码真实需要的。


25. 如何判断一段程序是由C编译程序还是由C++编译程序编译的?

答:使用__cplusplus宏:

#include 
using std::cout;
using std::endl;

void main()
{
#if __cplusplus
	cout << "c++" << endl;
#else
	cout << "c" << endl;
#endif
}

26. 用宏定义写出swap(x, y)。

答:具体参见代码:

#define SWAP(x, y) (x = (x)+(y), y =(x)-(y), x = (x)-(y))

27. int(*s[10])(int);表示的是什么。

答:表示的是一个函数指针数组,每一个元素是一个形如int (*f)(int)的函数指针。


28. 关于struct位域的题目。

答:下面是几个例子:

struct s1 {
	int i : 1;	//0偏移,占1位
	int j : 2;	//1位偏移,占2位
	int a : 9;	//3位偏器,占9位
	//int b : 33;
};//补齐int大小,总共4个字节
int main(void)
{
	struct s1 s1 = { 1, 1, 1 };
	printf("%d\n", *(unsigned char *)(&s1));	//取结构体的第一个字节,得到的值是11,即1011b,证明了元素是紧紧排列的。没有位对齐这么一说。

	return 0;
}

网上有几个说法:

1. 位域不能跨字节存储。但是实际上是可以的,如上例中的s1.a,但是确实不能大于原始数据类型的大小,这里就不能大于32。

2. 位域也需要对齐,比如这里的s1.j需要按2位对齐,所以偏移从2位开始。实际上也是错的,同类型位域就是紧挨着的。

另外一个例子:

#include

struct s1 {
	int i : 8;	//0偏移,占8位
	int j : 4;	//8位偏移,占4位
	int a : 3;	//12位偏器,占3位
};//补齐int大小,总共4个字节

struct s2 {
	int i : 8;	//0偏移,占8位
	int j : 4;	//8位偏移,占4位
	int a : 3;	//12位偏器,占3位
	double b;	//不同类型还是需要对齐的,4字节偏移,占8字节
};//照理说总共就12个字节可以了,但是还需要按照最大元素类型的大小来补齐,所以需要时8字节的倍数,所以总大小是16


struct s3 {
	int i : 8;	//0偏移,占8位
	int j : 4;	//8位偏移,占4位
	double b;	//不同类型还是需要对齐的,4字节偏移,占8字节
	int a : 3;	//不同类型还是需要对齐的,12字节偏移,占3位,再补齐到4个字节
};//照理说总共就16个字节可以了,但是还需要按照最大元素类型的大小来补齐,所以需要时8字节的倍数,所以总大小是24

struct s4 {
	int i;
	double j;
};//按最大元素大小整数倍,所以总大小是16

int main(void)
{
	printf("sizeof(int) = %d\n", sizeof(int));		//4
	printf("sizeof(double) = %d\n", sizeof(double));	//8
	printf("sizeof(s1) = %d\n", sizeof(struct s1));		//4
	printf("sizeof(s2) = %d\n", sizeof(struct s2));		//16
	printf("sizeof(s3) = %d\n", sizeof(struct s3));		//24
	printf("sizeof(s4) = %d\n", sizeof(struct s4));		//16
	return 0;
}

29. 下面的代码有什么错误:

	swap(int* p1, int* p2)
	{
		int *p;
		*p = *p1;
		*p1 = *p2;
		*p2 = *p;
	}

答:p是一个野指针,不能*p = *p1,有可能踩到不该踩的区域。


30. 求1000!的未尾有几个0。

答:1-1000中,能够整除5的有200个,能够整除25的有40个,能够整除125的有8个,能够整除625的有1个,总共是249个,所以有249个0。


 

你可能感兴趣的:(编程基础)