C 语言经典面试题笔记

文章目录

1. 指针作为函数参数的相关问题

(1)返回值为 void
// 1. 传值,只是改变了形参的指针指向内存
void getmemory1(char *p)
{
	p = (char *)malloc(100);
	strcpy(p, "hello world");	
}

// 2. 传址,真实改变实参内容,有效示例
void getmemory2(char **p)
{
	*p = (char *)malloc(100);
	strcpy(*p, "hello world");	
}

// 3. 释放已有内存,二次操作
void getmemory3(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);// 申请的内存已经被释放,但指针并不是NULL
	
	if(str == NULL)
	{
		printf("NULL\n");
	} 
	else 
	{
        strcpy(str, "world");	// str 已经是野指针了
		printf("Not NULL\n");
	}
}
(2)返回值为 char *
// 1. 返回局部变量,内存在函数退出时被清空,内存内容未知
char *getmemory1(void)
{
    char p[] = "hello world";
    return p;
}

2. 数组和链表的区别

(1)大小方面:数组大小在定义时就已经确定,而链表不是;

(2)存储地址:数组元素存储地址连续,而链表不一定是;

(3)插入与删除操作:数组的这些操作都需要移动数组中的元素,而链表只需要改变指针即可。


3. 关于结构体大小

(1)含位域
struct A
{
    char t : 4;
    char k : 4;
    unsigned short i : 8;
    unsigned long m;
};

// 定义了一个 char 类型变量,占一个字节,
// 然后根对齐规则,short 变量要存放在能被 2 整除的地址上,故空出 1 个字节;
// 前 4 位用于存放 t ,后 4 位用于存放 k;
// 定义一个 short 类型变量,占两个字节,
// 其中前 8 位用于存放变量 i;
sizeof(struct A) = 1 + 1 + 2 + 4 = 8;

4. c 和 c++ 中的 struct 有什么不同

(1)c 中的 struct 不可以含有成员函数,而 c++ 中的 struct 可以。

(2)c++ 中 struct 默认为权限为 public,而 class 默认为 private。


5. 要对绝对地址 0x100000 赋值,我们可以用 (unsigned int *)0x100000 = 1234; 那么要是想让程序跳转到绝对地址是 0x100000 去执行,应该怎么做?

(1)首先要将 0x100000 强制转换成函数指针,即:

(void (*)()) 0x100000

(2)然后再调用它:

((void (*)()) 0x100000)();

6. int a,b,c 请写函数实现c=a+b ,不可以改变数据类型,如将c改为long int,关键是如何处理溢出问题。

两个int数相加,只有两个数的符号相同的时候才会溢出:

// 1. 两个正数相加;
a > 0 && b > 0 就是判断两数都是正数;
如果没有溢出,则和 c 必定大于 a、b, 反之溢出;

// 2. 两负数相加;
a < 0 && b < 0 判断两数都是负数;
如果没有溢出,则和 c 必定小于 a、b, 反之溢出;

// 3. 综上
return (a>0 && b>0 && (*c<a||*c<b) || (a<0 && b<0 && (*c>a || *c>b)));    

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

(1)如果是要你的代码在编译时发现编译器类型,就判断 _cplusplus 或 STDC 宏,通常许多编译器还有其他编译标志宏,

#ifdef __cplusplus
	cout<<"c++";
#else
	cout<<"c";
#endif

(2)c 语言是没有重载函数的概念的,所以c编译器编译的程序里,所有函数只有函数名对应的入口。

​ 而由于 c++ 语言有重载函数的概念,如果只有函数名对应的入口,则会出现混淆,所以c++编译器编译的程序,应该是函数名+参数类型列表对应到入口。

​ 注意,因为mian函数是整个程序的入口,所以mian是不能有重载的,所以,如果一个程序只有main函数,是无法确认是c还是c++编译器。

如一个函数

int foo(int i, float j);

c编译的程序通过 nm 查看

foo          0x567xxxxxx   (地址)

c++编译程序,通过 nm 查看

foo(int, float)   0x567xxxxxx 

关于 nm 命令

nm 是 names 的缩写, nm 命令主要是用来列出某些文件中的符号(说白了就是一些函数和全局变量等)。

// nm 读取目标文件和可执行文件
[taoge@localhost learn_nm]$ nm *

8. 关键字volatile有什么含意? 并给出三个不同的例子。

​ 一个定义为volatile的变量是说,这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了

​ 精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

下面是volatile变量的几个例子:

(1) 并行设备的硬件寄存器(如:状态寄存器)

(2)一个中断服务子程序中会访问到的非自动变量

(3)多线程应用中被几个任务共享的变量


9. 要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;

10. const 关键字的作用

const修饰的数据类型是指常类型(只读)

// 1. 指向的字符串不可更改
    const char * p1;
	char const * p2; 

// 2. 指针的指向不可更改
	char * const p3;

// 3. p4和p4指针的指向不可更改
  const char* const p4;

const关键字的作用主要有以下几点:

(1)可以定义const常量,具有不可变性。 例如:

const int Max=100; 
int Array[Max];

(2)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。

​ 如果在函数体内修改了 i,编译器就会报错;

void f(const int i)
{ 
    i = 10;		//error! 
}

(3) 为 c++ 函数重载提供了一个参考。

class A {
  void f(int i) {......} 			//一个函数
  void f(int i) const {......} 		//上一个函数的重载
};

(4) 可以节省空间,避免不必要的内存分配。 例如:

#define PI 3.14159 //常量宏

const doulbe Pi = 3.14159; //此时并未将Pi放入ROM中 

double i = Pi; //此时为Pi分配内存,以后不再分配!

double I = PI; //编译期间进行宏替换,分配内存

double j = Pi; //没有内存分配

double J = PI; //再进行宏替换,又一次分配内存!

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。

(5) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。


11. static关键字的作用

在 C 中,static 主要定义全局静态变量、定义局部静态变量、定义静态函数。

(1) 定义全局静态变量 : 全局静态变量有以下特点

  • 在全局区分配内存;

  • 如果没有初始化,其默认值为 0;

  • 该变量在本文件内从定义开始到文件结束可见;

(2)定义局部静态变量:在局部变量前面加上关键字static,其特点如下:

  • 该变量在全局数据区分配内存;

  • 它始终驻留在全局数据区,直到程序运行结束;

  • 其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

(3)定义静态函数:在函数返回类型前加上static关键字,函数即被定义为静态函数,其特点如下:

  • .静态函数只能在本源文件中使用;
  • 在文件作用域中声明的inline函数默认为static类型。

12. 堆和栈的区别

  • 申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;
  • 申请大小的不同。栈获得的空间较小,而堆获得的空间较大;
  • 申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;
  • 存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。而堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;
  • 底层不同。栈是连续的空间,而堆是不连续的空间。

13. 用宏定义实现 swap 函数

#define swap(x, y)	(x) = (x) + (y);\
					(y) = (x) - (y);\
					(x) = (x) - (y);

14. 带参宏与带参函数的区别

带参宏带参函数
处理时间编译时运行时
参数类型需要定义
程序长度变长不变
占用存储空间
运行时间不占用调用和返回占时

15. C语言变量的类型和存储位置

(1)变量类型及存储位置
  • **代码段:**存放程序代码,只读的,不能修改。

  • **全局区(静态区,又称为数据段):**其中

    .data段存放的是已经初始化的全局变量和静态变量(静态全局变量和静态局部变量)。

    .bss段存放的是未初始化的全局变量和静态变量(静态全局变量和静态局部变量)。bss段会将未初始化的变量填充为0.

  • **堆:**是一种线性结构,类似链表实现,也可由其它方法实现。malloc, realloc函数一般从堆上分配内存。注意:和数据结构中的堆不同。

  • 栈: 后进先出结构。主要存储的是局部变量,函数形参,以及函数地址。

  • **常量区:**存储字符串常量。

(2) 变量的生存期和作用域
  • 全局变量: 作用域是整个源程序,即多个文件中有效。通过extern声明。
  • **静态全局变量:**和全局变量的存储域相同,都存储在数据段。但作用域不相同,用static声明的静态全局变量作用域被限制在本文件内。在文件外是无效的。函数或变量前加static,可防止命名冲突。
  • 局部变量: 定义在函数体内,当函数执行结束后,就自动释放。作用域为整个函数体。
  • **静态局部变量:**与局部变量不同的是,它存储在全局区或是静态区,局部变量存储在栈上。静态局部变量的作用域也是函数体,函数体外无效。但当函数执行完之后,静态局部变量一直存在,下次调用时可以直接利用上次保存的值,即静态局部变量就初始化1次,不会重复初始化。

​ 如果在头文件声明了静态变量,那么每个包含该头文件的源文件中的静态变量地址都是不相同的,即视为不相同的变量,这样可以防止变量重定义。也说明了静态全局变量将作用域限制在了文件内。


16. C语言编译流程

预处理 --> 编译 --> 汇编 --> 链接 以 hello.c 为例

  • 预处理编译器:cpp gcc -E hello.c -o hello.i 头文件展开,宏替换,去掉注释
  • 编译器:gcc gcc -S hello.i -o hello.s c 文件变成汇编文件
  • 汇编器:as gcc -c hello.s -o hello.o 汇编文件变成二进制文件
  • 链接器:ld gcc hello.o -o hello 将函数库中相应的代码组合到目标文件中

17. static 变量程序分析题

该程序的运行结果为 ?

int sum(int a)
{
    auto int c = 0;
    static int b = 3;
    c += 1;
    b += 2;
    
    return (a+b+c);
}

void main()
{
    int I;
    a = 2;
    for(I = 0; I < 5; I++)
    {
        printf("%d,", sum(a));
    }
}

该程序的运行结果为: 8, 10, 12, 14, 16


18. switch 分支程序分析题

该程序的运行结果为 ?

int fun(int a)
{
    int b;
    switch(a)
    {
        case 1: b = 30;
        case 2: b = 20;
        case 3: b = 16;    
        default: 0;    
    }
    
    return b;
}

int main()
{
    printf("%d", fun(1));
    
    return 0;
}

该程序的运行结果为: 16


19. 改变实参值失败的函数示例

该程序的输出结果是 ?

int modifyvalue()
{
    return (x += 10);
}
    
int changevalue(int x)
{
    return (x += 1);
}
    
void main()
{
    int x = 10;
    x++;
    
    changevalue(x);
    x++;
    
    modifyvalue();
    printf("First output:%d\n", x);
    
    x++;
    changevalue(x);
    printf("Second output:%d\n", x);
    
    modifyvalue();
    printf("Third output:%d\n", x);
}
    

运行结果:

First output:12

Second output:13

Third output:13


20. 如何只使用一条语句实现x是否为2的若干次幂的判断?

// 如果是2的若干次幂就表示它只有一位是1,m &(m-1)一定为0;
#define is2*n(x)  ( (x & (x - 1)) ? 0 : 1)

21. 关于嵌入式 32 中断的题目

​ 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字interrupt。

​ 下面的代码就使用了interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

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

**【参考答案】**这个函数有太多的错误了,以至让人不知从何说起了:

(1)ISR 不能返回一个值。

(2)ISR 不能传递参数。

(3)在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。 (4)与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。


22. 关于 unsigned int 类型与 int 类型的计算

以下代码的输出是 ?

void foo(void)
{
    unsigned int a = 6;
    int b = -20;
    (a + b > 6) ? puts("> 6") : puts("< 6");
}

​ 当表达式中存在有符号类型和无符号类型时所有的数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。


23. 实现 strcpy 函数

char *my_strcpy(char *strDest, const char *strSrc)
{
	if(strDest == NULL || strSrc == NULL)
    {
        fprintf(stderr, "src or dest is NULL\n");
        return NULL;
    }
		
	char *src = (char *)strSrc;
	char *dest = strDest;
	
	while(*src)
	{
		*dest++ = *src++;
	}
	
	return strDest;
}


24. 实现 atoi 函数

int my_atoi(const char *src)
{
	if(src == NULL)
		return 0;
	
	int res = 0;
	char *p = (char *)src;
	
	while(*p)
	{
		if(*p >= '0' && *p <= '9')
		{
			res = *p - '0'+ res * 10;	
			p++;
		}
		else
		{
			return res;
		}
	}

	return res;
}


25. 实现 itoa 函数

char *my_itoa(unsigned int num)
{
	char *str = (char *)malloc(35);			//盛装逆序转换成的字符
	memset(str, 0, 35);						//清空
	
	char *pos = str;						//定义用于操作的指针
	
	while(num)								//循环获得数字字符
	{
		*pos = (num % 10) + '0';
		num /= 10;
		pos++;
	}
	
	int len = strlen(str);					//获取字符串长度
	
	char *res = (char *)malloc(len+2);		//由于获取的字符串是逆序,定义容器装结果
	memset(res, 0, len+2);					//清空
	
	int i;
	for(pos--, i = 0; i < len; i++, pos--)	//循环反向赋值
	{
		res[i] = *pos;
	}
		
    free(str);
	return res;								//返回结果
} 


26. 实现 strcmp 函数

int my_strcmp(const char *str1, const char *str2)
{
	if(str1 == NULL || str2 == NULL)			//操作对象合法性判断
		return -2;
		
	char *p1 = (char *)str1;					//避免操作源数据时意外更改
	char *p2 = (char *)str2;
	
	while(*p1 != '\0' && *p2 != '\0')			//依次比较字符
	{
		if(*p1 != *p2)							//有决胜情况
		{
			if(*p1 > *p2)	return 1;			//str1 > str2			
			else if(*p1 < *p2)	return -1;		//str1 < str2
		}
				
		p1++, p2++;
	}
	
	if(*p1 == '\0' && *p2 == '\0')				//跳出循环说明有至少一个字符串已经到了字符串尾
	{
		return 0;
	}
	else if(*p1 == '\0' && *p2 != '\0')			//字符串长的字符串就更大,反之就小
	{
		return -1;
	}
	else
	{
		return 1;
	}	
}


27. 实现 strlen 函数

int my_strlen(const char *src)
{
	if(src == NULL)
		return 0;
		
	int length = 0;
	char *pos = (char *)src;
	
	while(*pos)
	{
		length++, pos++;
	}
	
	return length;
}


28. 实现二分查找

int half_search(int a[],int start, int end, int data)
{
  if(start > end)  return -1;
    
  int mid;
  mid = (start + end) / 2;
  
  if(data == a[mid])  return mid;
  if(data <  a[mid])  return(half_search(a, start, mid-1, data));
  else  		   	  return(half_search(a, mid+1, end, data));
}


29. 编写一个 C 函数,该函数将一个字符串逆序

char *my_reverse(char *src)
{
	if(src == NULL)							// 基操,判空
		return NULL;
		
	int length = strlen(src), i;			// 求得源字符串有效长度
	char buf[length+2] = {0};				// 申请容器中转
	char *pos = src + length - 1;			// 操作指针,从 src 尾部往前取得字符

	for(i = 0; i < length; i++, pos--)		// 循环取得
	{
		buf[i] = *pos;
	}
	
	strncpy(src, buf, length+1);			// 覆盖原有数据,已然逆序
	
	return src;
}


30. 计算字符串中子串出现的次数

/**************************************************************************
参数说明:
	@src	  查找源
	@search   要查找的内容
	@returns  目标内容在查找源中所出现的次数
***************************************************************************/
int counting_strings(const char *src, const char *search)
{
	if(src == NULL || search == NULL)			//基操,判空
	{
		return 0;
	}

	char *p = (char *)src;						//不直接操作源数据
	char *find = NULL;							//查找结果指针
	int count = 0;								//计数器

	while(p)									//循环查找
	{
		find = strstr(p, search);
		if(find != NULL)						//出现过
		{
			count ++;							//计数器自增
			p = p + (find - p) + strlen(search);//指针偏移,在剩下字符串中继续查找
		}
		else
		{
			break;								//没找到,告辞
		}
	}

	return count;
}


31. 如何判断链表中是否有环

/**************************************************************************
    设置两个速度不同的指针同时从链表的第一个节点开始遍历链表,
    一个快指针 fp 每次移动两个节点,一个慢指针sp每次移动一个节点,
    若两个指针能相遇,则存在环。
**************************************************************************/
int hasCycle(struct ListNode* head)
{
    struct ListNode *p1 = head, *p2 = head;
     
    while((p2 != NULL) && (p2->next != NULL))
    {
        p1 = p1->next;
        p2 = p2->next->next;
         
        if(p1==p2)
        {
            return 1;
        }
    }
    
    return 0;    
}


32. 将二维数组行列元素互换,存到另一个数组中

/**************************************************************************
参数说明:
	@arr	要交换的原数组	
	@dest	目标数组
	这里的考点应该是在二维数组作为形参,笼统的用二级指针是过不了的。
**************************************************************************/
void change_arr(int arr[][4], int dest[][4])
{
	if(*arr == NULL || *dest == NULL)
		return ;
		
	int i, j;
	
	for(i = 0; i < 4; i++)
	{
		for(j = 0; j < 4; j++)
		{
			dest[i][j] = arr[j][i];
		}
	}
}


33. 输入一行字符,统计其中有多少个单词

/***********************************
	字符串中的单词个数为空格数 + 1
************************************/
int count_words(const char *src)
{
    if(src == NULL)
    	return -1;
    
    int count = 0;
    char *p = (char *)src;
    
    while(*p)
    {
        if(*p == ' ')	count ++;
        p++;
    }
    
    return count+1;
}


34. 写一个内存拷贝函数,不用任何库函数

(1)拷贝长度为length字符串

char *myMemcpy(char* dest, const char* source, size_t length)
{   
 	// 判是否为空
    if (!source || !dest)
    {   
    	return dest;  
    }  
    
    char* ret = dest;
    
	while (*source != '\0' && length) 
    { 
        *dest = *source;  
   		source ++;  
        dest ++;    
        length --;   
    } 
    
    *dest='\0'; 
    
    return ret;  
}

(2)通用

char *strcpy(char *str1, const char *str2)
{
	if(str1 == NULL || str2 == NULL || str1 == str2)	
		return str1;	

      while (*str1++ == *str2++)

      return str1;
}


35. 取一个整数a从右端开始的 4~7 位

/**************************************************
取一个整数a从右端开始的4-7位,可以这样考虑:
	1.先使a右移4位
    2.取一个低4全为1,其余为0的数,可以这样~(~0<<4)
    3.两者相& 
***************************************************/
unsigned int getFour_Seven(unsigned int num)
{
    unsigned num1, num2, result;
    
    num1 = num >> 4;
    num2 = ~(~0 << 4);
    
    result = num1 & num2;
 
    return result;
}


36. 打印 杨辉三角形

#include <stdio.h>
#define N 14
int main()
{
    int i, j, k, n=0, a[N][N];  /*定义二维数组a[14][14]*/
    while(n<=0||n>=13){  /*控制打印的行数不要太大,过大会造成显示不规范*/
        printf("请输入要打印的行数:");
        scanf("%d",&n);
    }
    printf("%d行杨辉三角如下:\n",n);
    for(i=1;i<=n;i++)
        a[i][1] = a[i][i] = 1;  /*两边的数令它为1,因为现在循环从1开始,就认为a[i][1]为第一个数*/
    for(i=3;i<=n;i++)
        for(j=2;j<=i-1;j++)
            a[i][j]=a[i-1][j-1]+a[i-1][j];  /*除两边的数外都等于上两顶数之和*/ 
    for(i=1;i<=n;i++){
        for(k=1;k<=n-i;k++)
            printf("   ");  /*这一行主要是在输出数之前打上空格占位,让输出的数更美观*/
        for(j=1;j<=i;j++)  /*j<=i的原因是不输出其它的数,只输出我们想要的数*/
            printf("%6d",a[i][j]);
        
        printf("\n");  /*当一行输出完以后换行继续下一行的输出*/
    }
    printf("\n");
}

以上就是关于C语言经典面试题的笔记了,以上代码均可运行通过,但本人水平终究有限,若有勘误还望不吝指出,谢谢。





Edit By Huangzhihui

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值