嵌入式面试指南
文章目录
- 嵌入式面试指南
- 嵌入式系统
- C/C++面试题
- 操作系统
嵌入式系统
C/C++面试题
1、C/C++程序基础
1.1 基本概念
面试题1:什么是C语言语句
什么是c语言语句,预处理是不是语句?
-
所涉及知识点
- C、C++语句
- 预处理指令不是C语句
-
分析问题
在C\C++中语句主要包括五大类:
-
表达式语句
-
函数调用语句
-
控制语句
条件语句、循环语句、结束本次循环语句(continue)、终止循环语句(break)、分支语句(swith…case)、跳转语句(goto)、返回语句(return)
-
复合语句
是由花括号“{}”括起来的语句组合
-
空语句
-
答案:
语句是以分号“
;
”作为分隔符,编译后产生机器指令的代码。预处理指令不是语句。
面试题2:变量的声明和定义有什么区别
变量的声明和定义有什么区别?
分析问题
变量的声明是指给编译器提交变量的标识符,作用是把这个变量声明的位置和变量的类型交给编译器。定义则是要编译器在变量定义的地方为其分配内存空间,编译器要根据它的大小在内存中开辟出空间来保存它的数据。
答案:
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入
extern
修饰的是变量的声明,说明此变量将在文件外或在文件后面部分定义
面试题3:下列字符中,那些不是C语言关键字
在以下各组中全部都不是C语言关键字的是?
A:int char extern B:printf include define
C:sizeof go cout D:while for sizeof
-
所涉及知识点
- C语言关键字
- C语言库函数
-
分析问题
由ANSI标准定义的C语言关键字共32个如下图所示:
选择B。因为printf
是个标准库函数不是关键字,include
和define
是预处理指令,也不是C语言的关键字
面试题4:下列变量的定义中,哪些是合法的
下列变量定义中哪些是合法的?
A:short _b a = 1-.le-1; B:double b=1+5e2.5
C:long d=0xfdaL; D:float 2_and=1-e-3
答案 B
说明:所谓标识符,是指为变量(
variable
)、宏(macro
)或者函数(function
)等取的名字。例如:int num;这个语句中的num就是一个标识符。
1.2 编程规范
面试题5:谈谈你对编程规范的理解和认识
谈谈你对编程规范的理解或认识?
编程规范可总结为:程序的可行性、可读性、可移植性以及可测试性
面试题6:函数、变量等命名都有哪些规则
函数、变量的命名都有哪些规则?
含数字、ASCII字母、下划线,以字母或下划线开头,不能用保留字作为变量名的情况下,还要有很高的可读性
面试题7:写出bool、int、float、指针变量与“零值”比较的if语句
写出bool、int、float、指针变量与“零值”比较的语句。
答案:
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;
}
- 注意:
应特别注意在int、指针变量和“零值”比较的时候,把“零值”放在左边,这样当把“==”误写成“=”时,编译器可以报错,否则这种逻辑不容易被发现,并且导致很严重的后果。
1.3 数据类型
面试题8:写出代码的输出结果
根据下列代码写出输出结果:
#include <stdio.h>
//#include <conio.h>
void main(void)
{
int a = 167;
int b = 025;
printf("a:%d b:%d\n",a,b);
getchar();
}
答案:
a:167 b:21
面试题9:C语言中不合法的整形常数
下列数值中,不是C语言合法int型常数有哪些?
A:32768 B:0 C:037 D:0xAF
答案:A
面试题10:short i = 0;i = i + 1L;这两句有错吗
一下这两段代码都是正确的吗?
代码一:
short s1 = 1;
s1 = s1 + 1L;
代码二:
short s1 = 1;
s1 += 1L;
答案:
代码一是错的,代码二十正确的
面试题11:char x[] = {“abcd”} 和 char y[] = {‘a’,‘b’,‘c’,‘d’}有不同吗
在下列代码中数组x和数组y相同吗?
char x[] = {"abcd"};
char y[] = {'a','b','c','d'};
答案:
不同。因为char x[] = {“abcd”}这种写法,是把字符串abcd包括结束符\0存到x;而char y[] = {‘a’,‘b’,‘c’,‘d’}这种是存储4个字符a、b、c、d到数组y。
面试题12:char 型数据在内存中的存储形式
在c语言中,char型数据在内存中的存储形式是()。
A:补码 B:反码 C:原码 D:ASII码
答案:D
1.4 运算符
面试题13:请写出下列代码的输出内容
请写出下列代码的输出内容
#include <stdio.h>
void main(void)
{
int a,b,c,d;
a = 10;
b = a++;
c = ++ a;
d = 10*a++;
printf("b: %d\nc: %d\nd: %d\n",b,c,d);
return;
}
输出结果:10 12 120
注意运算符优先级问题(*++)
面试题14:运算符优先级的问题
给出下面程序的运行结果:
#include <stdio.h>
void main(void)
{
if(0 & 1 == 0)
{
printf("0 & 1 == 0\n");
}
else
{
printf("0 & 1 != 0\n");
}
if(0 & 1 != 0)
{
printf("0 & 1 != 0\n");
}
else
{
printf("0 & 1 == 0\n");
}
return;
}
答案:
0 & 1 != 0 0 & 1 == 0
注意:== 和 != 的优先级要高于&、~、|、&&、||运算符
面试题15:&&和&,||和|有什么区别
&&和&,||和|有什么区别?
- &和|对操作数进行求值运算(位运算),&&和||只是判断逻辑关系
- &&和||在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
面试题16:什么是左值,什么是右值
什么是左值,什么是右值?
答案:
左值:存储数据值的那块内存的地址。也称为变量的地址值。
右值:存储在某内存地址中的数据。也称为变量的数据。
面试题17:请写出程序的运行结果
请写出这段程序的运行结果。
#include <stdio.h>
void main(void)
{
char x = 'a';
char *p = "abcd";
char y[4] = {'a','b','c','d'};
int a = 10;
int *b = &a;
int c[4] = {1,2,3,4};
printf("%d %d %d\n",sizeof(x),sizeof(p),sizeof(y));
printf("%d %d %d\n",sizeof(a),sizeof(b),sizeof(c));
return;
}
在32位机上的输出结果是:
1 4 4
4 4 16
面试题18:sizeof和strlen的区别
sizeof和strlen都用来返回其参数的长度,那它们有什么不同吗?如果有,请举例说明。
答案:sizeof和strlen有以下区别:
- sizeof是一个操作符,strlen是库函数。
- sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串做参数
- 编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据的类型占内存的大小,而strlen计算的是字符串的实际长度。
- 数组做sizeof的参数不退化,传递给strlen就退化为指针了。
1.5 结构体
面试题19:结构体是什么样的数据类型
这是一个面试填空题,原题如下:
结构体是____数据类型。
答案:
结构体是够造数据类型的数据。
面试题20:结构体可以直接赋值吗
结构体在声明是可以直接赋值吗,在同一个结构体对象之间可以直接赋值吗?如果可以,请举例说明。
答案:
声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值,但是当结构体中含有指针“成员”是一定要小心。
面试题21:计算学生不及格的人数并打印他们的性别、姓名和成绩
编程实现记录7和学生的姓名、性别、成绩、计算不及格的人数,并打印出不及格人的姓名、性别和成绩。
答案:
#include <stdio.h>
struct student{
char *name;
int score;
char sex;
};
void main(void)
{
int totle = 0;
struct student stu[7] = {{"张三",99,'M'},{"李四",88,'F'},{"王五",77,'M'},{"露娜",66,'M'},{"李白",55,'F'},{"亚索",44,'F'},{"女警",33,'M'}};//结构体数组
for(int i = 0;i < 7;++i)
{
if(stu[i].score < 60)
{
printf("name:%s sex:%c score:%d\n",stu[i].name,stu[i].sex,stu[i].score);
totle++;
}
}
printf("the totle is:%d\n",totle);
getchar();
return;
}
面试题体22:结构体内存对齐问题
请写出以下代码的输出结果:
struct s1{
int i:8;
char j:4;
int a:4;
double b;
};
struct s2{
int i:8;
int j:4;
double b;
int a:4;
};
struct s3{
int i;
char j;
double b;
int a;
};
printf("sizeof(s1) = %d\n",sizeof(s1));
printf("sizeof(s2) = %d\n",sizeof(s2));
printf("sizeof(s3) = %d\n",sizeof(s3));
答案:
sizeof(s1) = 16
sizeof(s2) = 24
sizeof(s1) = 32
1.6 C和C++的区别
面试题23:关键字static在c和c++中的区别
c和c++中都有关键字static关键字,那么关键字在c和c++的使用有什么区别?请简述之。
在c中static用来修饰局部静态变量和外部静态变量、函数。而c++中除了上述功能外,还用来定义类的成员变量和函数,即静态成员变量和静态成员函数。
面试题24:c语言的结构体和c++的有什么区别
c语言的结构体和c++的有什么区别?
答案:
C语言的结构体和c++的结构体主要有一下区别:
- C语言的结构体是不能有成员函数的,而c++的类可以有。
- C语言的结构体中数据成员时没有private、public和protected访问权限的,而c++的类的成员有这些访问限定。
- C语言的结构体是没有继承关系的,而c++的类却有丰富的继承关系。
说明:
虽然c的结构体和c++的类有很大的相似度,但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。
面试题25:C中的malloc和C++中的new有什么区别
C中的malloc和C++中的new有什么区别?
答案:
- new和delete是操作符,可以重载,只能在c++中使用。
- malloc和free是函数,可以覆盖,C、C++都可以使用。
- new可以调用对象的构造函数,对应的delete调用相应的析构函数。
- malloc仅仅分配内存,free仅仅回收内存,并不执行构造函数和析构函数。
- new、delete返回的是某种数据类型的指针,malloc、free返回的是void指针。
面试题26:C++的引用和C语言的指针有什么区别
指针和引用有什么区别?
答案:
指针和引用主要有以下区别:
- 引用必须初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配内存空间。
- 引用初始化以后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
2、预处理、保留字
2.1 预处理
面试题1:简述#ifdef
、#else
、#endif
和#ifndef
的作用
简述#ifdef
、#else
、#endif
和#ifndef
的作用。
这些编译指令主要有以下功能:
- 利用
#ifdef
、#endif
可将某程序功能模块包括进去,以向特定用户提供该功能。- 用于在子程序前加上标记,以便于追踪和调式。
- 应对硬件的限制。
面试题2:宏定义和函数
宏和函数有何区别?
宏的引用只占编译时间,不占运行时间。
宏的引用没有返回值。如果需要,需给整个表达式加上括号。
宏的形参无类型。函数的形参必须要有类型
实参为表达式的情况:如果没有给表达式加上括号,可能会导致错误的结果,而函数不会。
宏直接替代有可能导致副作用,函数不会。
面试题3:用#define声明一个常数
用宏定义实现一个常数,用来表明一年有多少秒,忽略闰年的问题。要注意数据长度的问题。(16位机)
答案:
#define SECONDS_PER_YEAR (365*24*60*60)UL
面试题4:写一个“标准”宏MIN
写一个“标准”宏MIN,这个宏输入两个参数并且返回较小的一个。
#define MIN(a,b)((a) <= (b)?(a):(b))
面板试5:typedef和define有什么区别
typedef和define有什么区别?
答案:
- 用法不同:typedef用来定义一种数据类型的别名,增强程序的可读性;define主要用来定义常量,以及书写复杂、使用频繁的宏。
- 执行时间不同:typedef是编译过程的一部分,有类型检查的功能;define是宏定义,是预编译的部分;其发生在编译之前,只是简单地进行字符串的替换,不进行类型的检查。
- 作用域不同:typedef有作用域限定;define不受作用域约束,只要是在define声明后的引用都是正确的。
- 对指针的操作不同:typedef和define定义的指针有很大的区别。
- 注意:typedef定义是语句,因为句尾要加分号;而define不是语句,千万不能在句尾加分号。
面试题6:#define CHAR char*
和typedef char* CHAR
#define CHAR char*和typedef char* CHAR
两者的作用是否相同,各有什么优劣?
- 由define定义的类型别名可以被其他修饰符扩展(如:unsigned),而typedef不可以。
- define定义的类型别名代表指针时,其连续声明的变量中只有第一个是指针,其他的均为非指针的普通变量。而typedef能保证连续声明的所有变量均为同一类型。
面试题7:谈谈你对typedef的认识
谈谈你对typedef的认识。
typedef共有四种用途:
(1)定义一种类型的别名。
(2)用在旧的C代码中辅助声明struct。
(3)定义与平台无关的类型。
(4)为复杂的声明定义一个新的简单的别名。
还有两个注意事项:
(1)定义了一种类型的新别名后不能被其他的修饰符扩展。
(2)typedef是并不真正影响对象的存储特性的存储类关键字。
2.2 const(常量)
面试题8:关键字const是什么
const是非常常见的关键字,那么const是什么意思?它有什么作用呢?
答案:const用来定义一个只读的变量或对象。
主要优点:便于类型检查、同宏定义一样可以方便进行参数的修改和调整、节省空间,避免不必要的内存分配,为函数重载提供参考。
面试题9:说明以下a声明的含义
请说明下面a的各种声明的含义。
const int a;
int const a;
const int *a;
int *const a;
int const *a const;
答案:
- const int a;和int const a;的意义是一样的,定义一个常整型数a。
- const int *a;定义了一个指向常整型数据的指针a。
- int const *a;定义了一个指向整型数据的常指针a。
- int const *a const;定义了一个指向常整型数据的常指针a.
面试题10:const、define定义常量的区别
常量有什么用?const和define都可以用来定义常量,两者有什么区别?
答案:
常量的引入可以增强程序的可读性,可以使程序的维护和调试更加方便,使书写简便。
const和define都可以定义常量。但是两者还是有区别的:
(1)const定义的常量有数据类型,而define没有。
(2)很多集成开发环境只支持对const定义的常量的调试,而不支持define定义的常量。
(3)const定义的常量是要分配内存空间的,而define定义的常量却不分配空间。
所以const定义的常量比define定义的常量有更多的优势,可以说const是对define的优化。
2.3 static(静态)和extern
面试题11:static有什么作用
static是比较常见的关键字,引入它有什么作用?
答案:
static在C中主要用于定义全局静态变量、局部静态变量、静态函数。在C++中新增了两种作用:定义静态数据成员、静态函数成员。
-
注意:
因为static定义的变量分配在静态区,所以其定义的变量默认值为0,普通变量的默认值为随机数,在定义指针变量时要特别注意。
例子程序:
//main.cpp
int a = 0;//全局初始化区
char *p1;//全局未初始化区
int main(void)
{
int b;//栈
char s[] = "abc";//栈
char *p2;//栈
char *p3 = "123456";//123456\0在常量区,p3在栈上
static int c = 0;//全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);//分配得来的10和20字节的区域就在堆区
strcpy(p1,"123456");//123456\0放在常量区,编译器可能会将它与p3指向"123456"优化成一个地方
}
描述内存分配方式以及他们的区别?
- 从静态存储区域分配,内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
- 在栈上创建,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集。
- 从堆上分配,亦称为动态内存分配,程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时使用free或delete释放内存。动态内存的生存周期由程序员决定,使用非常灵活,但问题也最多
全局变量可不可以定义在可被多个.c文件包含的头文件中?为什么?
可以,在不同的.c文件中以static形式来声明同名全局变量;不同的.c文件都会有static int a;的定义,使用的是本文件内部定义的,互不影响。
面试题12:extern有什么用
extern是常见的关键字,引入它有什么作用?
答案:extern标识的变量函数声明定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找定义。
面试题13:简述变量存储类型
请简述C、C++中变量的存储类型。
答案:
变量的存储类型是对变量的作用域、存储空间、生存周期的规定。
变量的存储类型分为四种:
自动类型(auto)、寄存器类型(register)、外部类型(extern)、静态类型(static)。这四种类型声明的变量分别称为自动变量、寄存器变量、外部变量和静态变量。
2.4 volatile
volatile的含义一般有两种说法:“动态的”、“易变得”,无论是“动态的”还是“易变的”,都强调了变量的易变星。
在嵌入式编程中状态寄存器变量会随时被外界条件改变,属于这种类型的变量。在多线程编程中临界变量被几个线程共享,也是“易变的”,也应定义为这种类型的变量。
面试题14:volatile有什么作用
简述volatile的作用。
- 所涉及的知识点
- volatile
- 线程
- 嵌入式
- 分析问题
volatile定义变量的值是易变的,一般编译器不去假设这个变量的值。优化器每次用到这个变量的时候都要去重新读取这个变量的值,而不是直接读取寄存器内的备份。主要用于以下几种情况:
(1)状态寄存器一类的并行设备硬件寄存器。
(2)一个中断服务子程序会访问到的非自动变量。
(3)多线程间被几个任务共享的变量。
答案:
volatile有如下作用:
- 状态寄存器一类的并行设备硬件寄存器
- 一个中断服务子程序会访问到的非自动变量
- 多线程间被几个任务共享的变量
- 说明
虽然volatile在嵌入式方面应用会比较多,但在PC软件的多线程中,volatile修饰的临界变量也是非常实用的。
面试题15:一个参数可以既是const又是volatile吗
一个参数可以既是const又是volatile吗,为什么?
答案:
可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。
- 说明
在此一定要注意const的意思,const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。
面试题16:一个指针可以是volatile吗
一个指针可以是volatile吗?解释为什么?
答案:
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个buffer的指针时,必须用volatile来修饰这个指针。
- 说明
指针是一个普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存的地址。
3、引用和指针
3.1 引用
面试题1:什么是引用
什么是引用,引用有什么作用?
-
所涉及的知识点
- 引用
- 指针
-
分析问题
引用就是一个目标变量的别名。对引用的一切操作和对变量的直接操作是一样的。引用的声明如下:
【数据类型标识符】【&】【定义的引用名称】=【目标变量名】
引用的定义应用实例如下:
#include <stdio.h> void main(void) { char a = 1; char &b = a; printf("&a = %d,&b = %d\n",&a,&b); printf("a = %d,b = %d\n",a,b); b++; printf("a = %d,b = %d\n",a,b); return; }
输出结果:
&a = 1234052,&b = 1234052 a = 1,b = 1 a = 2,b = 2
char &b = a;定义了字符型引用b为a的别名。在这里要注意以下几点:
-
&不是取地址运算符,只是表示b为引用
-
类型标识符char是指目标变量的数据类型
-
引用的声明一定要初始化
-
定义了a的引用b后,相当于一个变量有两个名字:原变量a和引用变量b。不能再把该引用b作为其他变量的别名。
-
声明了引用b,并不是有重新定义了一个变量,它只是a的别名,不是一种数据类型,并不占用存储单元。所以求引用b的地址就是求原变量a的地址。
-
没有数组的引用,因为数组不是单个元素,所以无法给若干元素定义一个别名。
引用有什么作用?
(1)可用做函数参数
在C中一般的参数都是值传递,大块的数据一般采用指针作为参数,以避免大块的数据全部压入栈中,这样可以提高程序的效率。在C++中引用基本替代了指针的作用,参数传递时大块的数据都采用引用。请看如下代码:
#include <iostream.h> void swap(char &x,char &y) { int temp = 0; temp = x; x = y; y = temp; } void main(void) { char a = 0; char b = 0; cin >> a >> b; cout << "Before swap" << endl; cout << "a:" << a << endl << "b:" << b << endl; swap(a,b); cout << "After swap" << endl; cout << "a:" << a << endl << "b:" << b << endl; return; }
(2) 常引用
常引用的定义如下:
【const】【数据类型标识符】【&】【引用名】=【目标变量】;
其实常引用的定义就是在普通引用定义前面加上一个只读标识符const。如下例:
char x = 1; const char &rx = x; rx = 2;//错误,rx是只读的 x = 2;//正确
(3)引用作为函数返回值
引用作为函数返回值时,定义格式如下:
【数据类型标识符】【&】【函数名】(形式参数列表){函数体}
实例代码如下:
#include <iostream.h> int temp;//定义全局变量temp int func1(int x)//定义函数func1,它以返回值的方法返回函数值 { temp = (int)(x*x*3); return temp; } int func2(int x)//定义函数func2,它以引用方式返回函数值 { temp = (int)(x*x*3); return temp; } void main(void) { int a = func1(5);//情况1,系统生成要返回值的副本即临时变量 int &b = func1(100);//情况2,有时会出错(在不同c++编译器有不同的规定),不能从被调函数中返回一个临时变量或局部变量的引用 int c = func2(5);//情况3,系统不生成返回值的副本,可以从被调函数中返回一个全局变量的引用 int &d = func2(5);//情况4,系统不生成返回值的副本,可以从被调函数中返回一个全局变量的引用 cout << a << endl; cout << b << endl; cout << c << endl; cout << d << endl; }
情况2在VC6.0下编译会出错,注释掉情况下输出结果如下:
75
75
75
引用在作为函数返回值时应该注意以下几点:
-
不能返回局部变量的值。
局部变量在被调用函数返回后就被销毁掉了。那么它返回的引用所指向的内存已经没有任何意义,运行时会出现不可预知的错误。
-
可以返回类成员的引用,但最好是const的。
-
不能返回函数内部new分配的内存的引用。
因为被函数返回的引用只是作为临时变量出现的,没有被赋予一个实际的变量空间,所以这个引用所指的空间就无法释放,造成内存泄漏。
-
-
答案:
引用就是一个目标变量的别名,对引用的一切操作和对变量的直接操作是一样的。主要用做函数的参数、函数返回值和常应用。
- 说明
引用可以理解为常量指针,也就是指针值一经确定就不能再改的特殊指针,并且这种特殊指针没有自己的内存空间。
但有些时候指针并不能实现引用的功能,比如运算符重载时返回值应为引用,而不是指针。
面试题2:常引用有什么作用
简述为什么引入常引用,常引用有什么作用。
答案:
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。
常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外修改。
面试题3:流操作符重载为什么返回引用
- 所涉及的知识点
- 引用
- 流操作符cout、cin(<<、>>)
答案:
在程序中,流操作符>>和<<经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
- 注意
在除赋值操作符和流操作符之外的其他一些操作符中,如+、-、*、/等千万不能返回引用。因为这四个操作符的对象都是右值,因此,它们必须构造一个对象作为返回值。
3.2 指针
面试题4:说明以下声明的含义
说明以下声明的含义:
A:int(**p)[10];
B:char*(*p)[10];
C:float(*p[10])();
D:double*((*p)[10]);
E:short:(*p)(char);
F:long(*(*p)(int,int))(int)
答案:
A:p为数组指针的指针,数组有10个元素,元素为int型数据。 B:p为数组的指针,数组有10个元素,元素为char*型数据。 C:p为数组,数组有10个元素,元素为函数指针类型,这个函数没有参数,返回值为float型数据。 D:p为数组的指针,数组有10个元素,元素为double*型数据。 E:p为函数指针,它所指向的函数有一个char型的参数,返回值为short型数据。 F:p为函数指针,它所指向的函数有两个int型的参数,返回值为函数指针型数据,这个返回的函数指针指向的函数有一个int型的参数,返回值为long型数据。
面试题5:简述指针常量与常量指针的区别
什么是指针常量?什么是常量指针?两者有什么区别?
答案:
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。
常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
- 注意
无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变性。
面试题6:写出以下代码的输出结果
写出以下代码的输出结果:
#include <iostream.h>
void main(void)
{
char str1[] = "abc";
char str2[] = "abc";
const char str3[] = "abc";
const char str4[] = "abc";
const char *str5 = "abc";
const char *str6 = "abc";
char *str7 = "abc";
char *str8 = "abc";
cout << (str1 == str2) << endl;
cout << (str3 == str4) << endl;
cout << (str5 == str6) << endl;
cout << (str7 == str8) << endl;
return;
}
答案:
0 0 1 1
面试题7:找出代码的错误
找出下列代码的错误并改正。
#include <stdio.h>
void swapxy(char *a,char *b)
{
char x = *a,y = *b;
x = x + y;
y = x - y;
x = x - y;
*a = x,*b = y;
return;
}
void main(void)
{
char a = 'a',b = 'b';
char &x = a,&y = b;
printf("a = %c and b = %c before swap.\n",a,b);
swapxy(x,y);
printf("a = %c and b = %c after swap.\n",a,b);
return;
}
答案:
#include <stdio.h> void swapxy(char &a,char &b) { char x = a,y = b; x = x + y; y = x - y; x = x - y; a = x,b = y; return; } void main(void) { char a = 'a',b = 'b'; char &x = a,&y = b; printf("a = %c and b = %c before swap.\n",a,b); swapxy(x,y); printf("a = %c and b = %c after swap.\n",a,b); return; }
3.3 指针和数组
面试题8:写出代码的输出结果
请写出以下代码的输出结果:
#include <iostream>
using namespace std;
int main(void)
{
int b = 0;
int a[5] = {0,1,2,3,4};
for(int i = 0;i < 5;)
{
i = a[i + 1];
cout << a[i] << endl;
}
return 0;
}
答案:g++编译器结果
1 2 3 4 段错误(核心已转储)
编写实现数组排序的一种算法,说明为什么你会选择这样的方法?
数组排序方法很多,比如 冒泡、选择、快速排序,后面的问题主要是问你对该排序的理解,理解原理,能现场写出来
答案:数组冒泡排序算法
/*************************************************************************
> File Name: bubbleSort.cpp
> Author: faker
> System: Ubuntu 64bit
> created Time: 2020年10月02日 17时21分56秒 CST
************************************************************************/
#include<iostream>
using namespace std;
void swap(int *a,int *b)
{
int temp;
if(a == b) return;
temp = *a;
*a = *b;
*b = temp;
}
void bubbleSort(int *arr,int len)
{
for(int i = 0;i < len - 1;++i)
{
for(int j = 0;j < len - i - 1;++j)
{
if(arr[j] > arr[j + 1])
swap(&arr[j],&arr[j + 1]);
}
}
}
void show(int *arr,int len)
{
cout << "arr:";
while(len--)
cout << *arr++ ;
cout << "\n";
}
int main(void)
{
int arr[] = {5,4,3,2,1};
show(arr,5);
bubbleSort(arr,5);
show(arr,5);
}
面试题9:请问这段程序有问题吗
下面这段程序有问题吗?如果有,请写出打印结果。
#include <stdio.h>
#define MAX 255
void main(void)
{
unsigned char str[MAX],i;
for(i = 0;i <= MAX;i++)
{
str[i] = i;
printf("%d\n",str[i]);
}
return;
}
答案:
这个程序是死循环加数组越界访问(C/C++不进行数组越界检查)。
面试题10:a和&a有什么区别
请写出以下代码的打印结果。
#include <stdio.h>
void main(void)
{
int a[5] = {1,2,3,4,5};
int *ptr = (int *)(&a + 1);
printf("%d %d\n",*(a + 1),*(ptr - 1));
return;
}
答案:
输出结果:2 5
面试题11:请问代码有什么问题
以下代码有什么问题?
#include <iostream>
using namespace std;
void main(void)
{
char *p1,*p2;
char ch[12];
char **pp;
p1 = ch;
pp = &ch;
p2 = *pp;
cout << p1 << endl;
cout << pp << endl;
cout << p2 << endl;
return;
}
答案:
- 数组ch没有初始化
- 把数组指针赋值给字符的二级指针
- 注意
数组名是个常量,不能当做变量来用。比如对于数组ch:ch++、ch–、ch += 5、ch -= 5;之类的操作都是不允许的。
面试题12:数组名和指针的区别
请写出以下代码的打印结果:
#include <iostream>
#include <cstring>
using namespace std;
void main(void)
{
char str[13] = "Hello World!";
char *pStr = "Hello World!";
cout << sizeof(str) << endl;
cout << sizeof(pStr) << endl;
cout << strlen(str) << endl;
cout << strlen(pStr) << endl;
return;
}
答案:
打印结果:
13 4 12 12
- 注意
一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富得多。但是当数组名当做参数传递给函数后,其失去了原来的含义,变为普通指针。另外要注意sizeof不是函数,只是操作符。
3.4 函数指针
面试题13:请解析(*(void(*)())0)()
的含义
请解析下面这条语句:
(*(void(*)())0)()
;
答案:
从头到尾解析结果如下:
void (*0)()
:是一个返回值为void,参数为空的函数指针。(void(*)())0
:把0转变成一个返回值为void,参数为空的函数指针。*(void(*)())0
:在上句的基础上加*表示整个是一个返回值为void,无参数,并且起始地址为0的函数的名字。(*(void(*)())0)()
:这就是上句的函数名所对应的函数的调用。
- 说明
函数指针在PC软件开发中使用较少,在嵌入式行业使用较多,但是无论是PC软件还是嵌入式软件,理解函数指针的定义和使用,对于理解程序设计都是很有好处的。
面试题14:指出程序的错误
指出下面这段程序的错误并改正:
#include <stdio.h>
void main(void)
{
int max(x,y);//函数声明错误,改为:int max(int,int);
int *p = max;//指针定义错误,改为:int (*p)(int,int) = max;
int a,b,c,d;
scanf("%d %d %d"a,b,c);//库函数使用错误,&a,&b,&c
d = p(p(a,b),c);//函数指针调用函数错误,改为:d = (*p)((*p)(a,b),c)
printf("d:%d\n",d);
return;
}
int max(int x,int y)
{
return (x > y?x:y);
}
3.5 “野指针”
面试题15:如何避免”野指针“
”野指针”是如何产生的?应该如何避免?
答案:
“野指针”产生的原因及解决办法如下:
- 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可以让它指向NULL
- 指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL
- 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间让指针指向NULL
- 在使用指针前一定要检验指针的合法性
面试题16:程序是否正确
下面的这段程序是否正确?如果正确,请写出打印结果:
#include <iostream>
using namespace std;
typedef int *pInt;
void swap(int *p1,int *p2)
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
void main(void)
{
int a = 1,b = 3;
pInt p1 = &a,p2 = &b;
cout << "a:" << *p1 << "b:" << *p2 << endl;
swap(p1,p2);
cout << "a:" << *p1 << "b:" << *p2 << endl;
return;
}
答案:
不正确,指针p没有初始化就直接使用,其为“野指针”会导致不可预知的错误。
面试题17:指出程序的错误
指出下列程序的错误:
#include <stdio.h>
class A{
public:
A(){
number = 0;
};
void func1(void){
printf("In func1 number is:%d\n",number);
}
void func2(void){
printf("In func2\n");
}
public:
int number;
};
void func(A*p,int i);
void main(void)
{
A*p;
func(p,5);
if(p != NULL)
{
p->func1();
p->func2();
}
}
void func(A *p,int i)
{
if(p == NULL)
{
A a;
a.number = i;
p = &a;
}
}
答案:
在主函数中调用func后,p是“野指针”,用p调用func1时,会出现内存错误,因为在func1中用到了类A的成员变量number,而类的成员变量属于类的成员对象,在类成员对象作用域外对其的所有访问都是非法的。
3.6 动态内存
面试题18:简述C、C++程序编译的内存分配情况
在c、c++中不同性质的数据在编译时存放在不同的位置,请简述c、c++程序编译时的内存分配情况。
答案:
- 栈区(stack):由编译器自动分配释放,存放为运行函数而分配得局部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的栈
- 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由系统回收。分配方式类似于链表
- 全局区(静态区)(static):存放全局变量、静态数据、常量。程序结束后由系统释放。
- 文字常量区:常量字符串就是存放在这里的。程序结束后由系统释放
- 程序代码区:存放函数体(类成员函数和全局函数)的二进制代码
面试题19:以下四段代码中哪段没有错误
请看这四段代码:
代码一:
void GetMemory(char *p){
p = (char *)malloc(100);
}
void test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str,"hello world");
printf(str);
}
代码二:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void test(void)
{
char *str = NULL;
str GetMemory()();
printf(str);
}
代码三:
void GetMemory(char **p,int num)
{
*p = (char *)malloc(num);
}
void test(void);
{
char *str = NULL;
GetMemory(&strm,100);
strcpy(str,"hello world");
printf(str);
}
代码四:
void test(void)
{
char *str = (char *)malloc(100);
strcpy(str,"hello world");
free(str);
}
并指出错误段中的错误。
答案:
四段代码全有错误:
- 代码一:GetMemory(char *)的参数为字符型指针,在这个函数修改参数p的值并不能真正修改实参的值
- 代码二:其中的p[]数组是函数GetMemory中的局部变量,函数返回后,p就被释放掉,str便指向了一段无用的内存区域
- 代码三:没有判断动态内存申请是否成功而直接使用,没有释放动态申请的内存,造成内存泄漏
- 代码四:没有判断动态内存申请是否成功而直接使用,动态内存释放后没有将指针置空
- 注意
申请动态内存是一定要判断是否申请成功,失败时要进行失败处理;动态内存使用后要及时释放,不要造成内存泄漏;释放后将原先指向动态内存的指针置空,一面造成“野指针”。
4、字符串
4.1 数组字符串
面试题1:编码实现数字转换为字符串
编码实现函数itoa(),设计一个程序,把一个数字转换为字符串存储到一个缓冲区。例如数字:5486321,转化成字符串:“5486321”。
答案:
int myitoa(int num,char *p,int n) { if(p == NULL) { return -1;//校建指针的有效性 } if(num < 0)//判断数值是否为负值 { *p++ = '-'; num = 0 - num;//将负值转换成正值 } int temp = 0; *p = 0; if(n == 16)//按16进制转换 { for(int i = 0;i < 8;i++) { temp = int(num / pow(16,7 - i)); if(temp != 0) { *p = 1; } if(*p != 0) { if(temp >= 0 && temp <= 9) { *p++ = temp + '0'; } else if(temp >= 10 && temp <= 15) { *p++ = temp - 10 + 'A'; } num = num % int(pow(16,7 - i)); } } } esle if(n == 10)//按十进制转换 { for(int i = 0;i < 10;i++) { temp = int(num / pow(10,9 - i)); if(temp != 0) { *p = 1; } if(*p != 0) { *p++ = temp + '0'; num = num % int(pow(10,9 - i)); } } } else if(n == 2) { for(int i = 0;i < 32;i++) { temp = int(num / pow(2,31 - i)); if(temp != 0) { *p = 1; } if(*p != 0) { *p++ = temp + '0'; num = num % int(pow(2,31 - i)); } } } else if(n == 8) { for(int i = 0;i < 16;i++) { temp = int(num / pow(8,15 - i)); if(temp != 0) { *p = 1; } if(*p != 0) { *p++ = temp + '0'; num = num % int(pow(8,15 - i)); } } } *p += '\0'; return 0; }
面试题2:编码实现字符串转换为数字
编码实现函数atoi(),设计一个程序,把一个字符串转换成一个整数值。例如数字:“5486321”,转换成字符:5486321.
vint Invert(char *str)
{
int num=0;
while(*str!='\0')
{
int digital=*str-48;
num=num*10+digital;
str=str+1;
}
return num;
}
4.2 字符串函数
面试题3:编写一个标准strcpy函数
试编码实现标准库函数strcpy函数。
答案:
实现strcpy函数
#include<stdio.h> char *Mystrcpy(char *dst,const char *src) { if((dst == NULL) || (src == NULL)) return NULL; char *ret = ret; while((*dst++ = *src++) != '\0'); return ret; } int main(void) { char arr1[] = "Hello "; char arr2[] = "World!"; Mystrcpy(arr1,arr2); printf("%s",arr1); return 0; }
主要看你的功底,对字符串的理解(\0作为结束)和对异常问题(参数为null)的处理,还有strcpy本身的理解【拷贝\0吗??】
参数使用const修饰,防止函数内部修改
参数需要检测,测量你的代码规范问题
字符串会拷贝\0,先赋值在判断
返回值,可以方便调用者
自己编写的strcpy函数:
char *strcpy(char *str1,const char *str2) { //校检指针的合法性 assert((str1 != NULL) && (str2 != NULL)); char *address = str1;//记录目标指针所指向的地址 while((*str++ = *str2++) != '\0');//拷贝直到str2结束 return address;//返回目标地址 }
- 注意
编程时,一定要注意编程细节,注意对形参指针合法性的检验,以及程序可扩展性的设计。
实现反转字符串
编写反转字符串的程序,比如:“abcdefghi”->“ihfedcba”,要求优化速度、优化空间。
答案:
#include <stdio.h>
#include <string.h>
void Reverse(char *str)
{
if(str == NULL) return;
int len = strlen(str);
int i = 0;
int j = len - 1;
while(i < j)
{
char temp = str[i];
str[i] = str[j];
str[j] = temp;
++i;
--j;
}
}
int main(void)
{
char arr[] = "chengxing";
Reverse(arr);
printf("%s",arr);
return 0;
}
找错题:请找出下面代码中的所有错误
答案:
#include <stdio.h>
main()
{
char *src = "Hello,world";
char *dest = NULL;
int len = strlen(src);
//char *dest = (char*)malloc(len + 1);
dest = (char*)malloc(len);
char *d = dest;
//char *s = &src[len - 1];指向最后一个字符
char *s = src[len];
while(len-- != 0)
d++ = s--;//*d = '\0';尾部要加'\0'
printf("%s",dest);//free(dest);释放空间,避免内存泄漏
//dest = NULL;防止产生野指针
return 0;
}
面试题4:简述strcpy、sprintf与memcpy的区别
函数strcpy、memcpy和sprintf都可以完成从“源资费从”到“目标字符串”的拷贝功能。简述这三个函数的异同。
答案:
三者主要有以下不同之处:
- 操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memory的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
- 执行效率不同,memory最高,strcpy次之,sprintf的效率最低。
- 实现功能不同,strcpy主要实现字符串变量间的拷贝,sprintf主要实现其他数据类型格式到字符串的转化,memory主要是内存块间的拷贝。
4.3 字符串与数组
面试题5:找出程序的错误之处
指出以下三段程序的错误,并改正。
void test1()
{
char string[10];//数组str1越界,改为:char string[11];
char *str1 = "0123456789";
strcpy(string,str1);
}
void test2()
{
char string[10],str1[10];//改为:char string[11],str1[11];
for(int i = 0;i < 10;i++)
{
str1[i] = 'a';
}
// 加上:str[i] = '0';
strcpy(string,str1);
}
void test3(char *str1)
{
char string[10];
if(strlen(str1) <= 10)//改为:strlen(str1) < 10;
{
strcpy(string,str1);
}
}
答案:
- test1()中,数组str1越界
- test2()中,strcpy不会结束,直到内存错误
- test3()中,str1数组下标越界
面试题6:判断程序会出现什么问题
判断以下程序会有什么问题,运行后有什么结果:
#include <stdio.h>
#include <string.h>
void main(void)
{
char s[] = "123456789";
char d[] = "123";
strcpy(d,s);
printf("%s\n%s\n",d,s);
}
答案:
在进行字符串拷贝是导致数组d越界,输出结果如下:
123456789 56789
5、嵌入式编程
面试题1:编码实现某一变量某位清0或置1
给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清a的bit 3,在以上两个操作中,要保持其他位不变。
答案:
#define BIT3 (0x1 << 3) Satic int a;
设置a的bit 3:
void set_bit3(void) { a |= BIT3;//将a的第3位置1 }
清a的bit 3:
void empty_bit3(vooid) { a &= ~BIT3;//将a第3位清零 }
面试题2:用C编写一个死循环程序
嵌入式系统中经常要用到无限循环,怎样用C编写死循环呢?
答案:
(1)用while实现
while(1){}
(2)用for实现
for(;;){}
(3)用goto实现
Loop; .. goto Loop;
面试题3:用变量给出下面的定义
用变量给出下面的定义
A:一个整型数(An integer)
B:一个指向整型数据的指针(A pointer to an integer)
C:一个指向指针的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)
D:一个有10个整型数据的数组(An array of 10 integers)
E:一个有10个指针的数组,该指针是指向整型数据(An array of 10 pointers to integers)
F:一个指向有10个整型数据数组的指针(A pointer to an array of 10 integers)
G:一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
H:一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(An array of ten pointers to functions that take an integer argument and return an integer)
答案:
A:int a; B:int *a; C:int **a; D:int a[10]; E:int *a[10]; F:int (*a)[10]; G:int (*a)(int); H:int (*a[10])(int);
面试题4:设置地址为:0x67a9的整形变量的值为0xaa66
在某工程中,要求设置某一绝对地址为0x67a9的整形变量的值为0xaa66。编译器是一个纯碎的ANSI编译器。写代码去完成这一任务。
答案:
int *ptr; ptr = (int *)0x67a9; *ptr = 0xaa66;
- 说明
这道题就是强制类型转换的典型例子,无论在什么平台上,地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。
面试题5:评论下面这个中断函数
中断是嵌入式系统中重要的组成部分,这导致了很多编译器开发商提供一种扩展----让标准C支持中断。具体代表事实是,产生了一个新的关键字__interrupt
。下面的代码就使用了__interrupt
关键字去定义一个中断服务子程序(ISR),请评论以下这段代码。
__interrupt double compute_area(double radius)
{
double area = PI * radius * radius;
printf("Area = %f",area);
return area;
}
答案:
这段中断服务子程序主要有以下四个问题:
- ISR不能返回一个值
- ISR不能传递参数
- 在ISR中做浮点运算是不明智的
- printf()经常有重入和性能上的问题
面试题6:评价一个代码段
评价下面的代码片段:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
答案:
应将源代码改为:
unsigned int zero = 0; unsigned int compzero = ~0;
- 注意
在嵌入式编程中一定要注意程序的可移植性。
6、面向对象
6.1 面向对象的基本概念
面试题1:谈谈你对面向对象思想的理解和认识
谈谈你对面向对象思想的理解和认识。
答案:
面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加简洁、清晰。
- 说明
编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析、设计和编程技术,而且需要借助必要的建模和开发工具。
面试题2:面向对象的三大特征
面向对象的三大特征是什么?请简述之。
答案:
面向对象的三大特征是封装性、继承性和多态性。
封装性:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private,protected,public)。
·继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无须额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。
多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
面试题3:面向过程和面向对象有什么区别
简述面向过程和面向对象的区别。
答案:
面向过程就是指分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题步骤中的行为。
- 说明
面向对象面向的是数据结构,面向过程面向的是算法。虽然面向对象在数据构造方面优于面向过程,但是面向过程是算法设计的实现基础,面向对象的程序设计最终还要转化为面向过程。
6.2 类的成员变量和成员函数
面试题4:简述类public、protected和private的作用
简述类public、protected和private的作用。
答案:
public的变量和函数在类的内部外部都可以访问,private修饰的变量和函数只有在类的内部可以访问,protected修饰的变量和函数在类的内部可以访问,还可以在派生类中访问。
面试题5:写出代码的打印结果
写出下列代码的打印结果:
#include <stdio.h>
class A{};
class B{};
class C:public A,public B{};
class D:virtual public A{};
class E:virtual public A,virtual public B{};
void main(void)
{
printf("%d\n",sizeof(A));
printf("%d\n",sizeof(B));
printf("%d\n",sizeof(C));
printf("%d\n",sizeof(D));
printf("%d\n",sizeof(E));
}
答案:
打印结果:
1 1 1 4 8
面试题6:写出程序的打印结果
写出以下程序的打印结果:
#include <stdio.h>
class A{
public:
virtual void func();
};
void A::func()
{
printf("output A");
return;
}
class B:A
{
public:
virtual void func();
B(int,char);
int a;
char b;
void function(){}
};
B::B(int a1,char b1)
{
a = a1;
b = b1;
}
void B::func()
{
printf("output B");
return;
}
int main(void)
{
B test(258,'y');
B *p;
p = &test;
char ch = *((char *)p + 5);
printf("%d\n",ch);
int size_test = sizeof(test);
printf("%d\n",size_test);
return 0;
}
答案:
输出结果:
1
12
面试题7:C++的空类有哪些成员函数
C++中的空类,默认产生哪些类成员函数?
答案:
- 缺省构造函数
- 缺省拷贝构造函数
- 缺省析构函数
- 缺省赋值运算符
- 缺省取址运算符
- 缺省取址运算符const
6.3 构造函数和析构函数
面试题8:构造函数能否为虚函数
构造函数能否为虚函数,能否为纯虚函数、析构函数呢?
答案:
构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必需的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
面试题9:简述子类与父类的析构、构造函数的调用顺序
答案:
构造函数调用的次序是先基类后派生类,即定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数。而析构过程恰好相反,先析构子类对象,再调用父类的析构函数。
面试题10:编写类String的构造函数、析构函数和赋值函数
已知String的原型为:
class String
{
public:
String(const char *str = NULL);
String(const String &other);
~String(void);
String & operator = (const String &other);//赋值函数
private:
char *m_data;//用于保存字符串
};
请编写String的上述4个函数。
String::String(const char *str)
{
int length = strlen(str);
m_data = new char[length + 1];
//此处可加上NULL判断
strcpy(m_data,str);
}
String::~String(void)
{
delete [] m_data;//也可写成:delete m_data;
}
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
//此处可加上NULL判断
strcpy(m_data,other.m_data);
}
String & String::operator = (const String &other)//赋值函数
{
if(this == &other)//检查赋值
return *this;
if(m_data != NULL)
delete [] m_data;//释放原有内存资源
int length = strlen(other.m_data);
//分配新的内存资源
m_data = new char[length + 1];
//此处可加上NULL判断
strcpy(m_data,other.m_data);//赋值内容
return *this;//返回本对象的引用
}
6.4 拷贝构造函数
面试题11:谈谈对拷贝构造函数和赋值运算符的认识
谈谈你对拷贝构造函数和赋值运算符的认识,两者有何异同?
答案:
拷贝构造函数和赋值运算符重载有以下两个不同之处:
(1)拷贝构造函数生成新的类对象,而赋值运算符不能。
(2)由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配,要先把内存释放掉
-
注意
当类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。
面试题12:写出当定义#define_INMAIN 0和不定义时代码打印结果
写出当定义#define_INMAIN 0和不定义时代码打印结果:
#include <iostream.h>
#define _INMAIN 0
class B
{
public:
B():data(0)
{
cout<<"default constructor"<<endl;
}
~B()
{
cout<<"destructed by parameter " << data <<endl;
}
B(int i):data(i)
{
cout<<"constructed by parameter " << data <<endl;
}
private:
int data;
};
B Play( B b)
{
return b ;
}
#ifdef _INMAIN
void main( void )
{
B t1=Play( 5 );
B t2=Play( t1 );
return;
}
#else if
void main( void )
{
B t1=Play( 5 );
B t2=Play( 10 );
return;
}
#endif
答案:
(1)定义#define _INMAIN 0。输出结果:
constructed by parameter 5 destructed by parameter 5 destructed by parameter 5 destructed by parameter 5 destructed by parameter 5
(2)不定义#define _INMAIN 0。输出结果:
constructed by parameter 5 destructed by parameter 5 constructed by parameter 10 destructed by parameter 10 destructed by parameter 10 destructed by parameter 5
- 说明
一个类可以有多个构造函数,但是每个构造函数的参数各不相同,可以使数据类型不相同,也可以使参数个数不相同。这就是覆盖,多态的实现技术之一。另外应注意用一个类对象初始化另外一个类对象,调用的是拷贝构造函数。
7、继承与多态
7.1 继承
面试题1:指出程序的错误
指出下段程序的错误,并解释它为什么是错的。
#include <iostream>
using namespace std;
class Base{
public:
int val;
Base(){
val = 1;
};
};
class Derive:Base//错误,默认的继承关系为私有继承,私有继承时子类的对象不能访问父类成员,导致coout << d.Base::val << endl << d.val << endl;出错,应改为:class Derive:public Base
{
public:
int val;
Derive(int i)
{
val = Base::val + i;
};
};
int main(int,char**,char**)
{
Derive d(10);
coout << d.Base::val << endl << d.val << endl;
return 0;
}
面试题2:用C++设计一个不能被继承的类
template <typename T> class A
{
friend T;
private:
A() {}
~A() {}
};
class B:virtual public A<B>
{
public:
B() {}
~B() {}
};
class c:virtual public B
{
public:
C() {}
~C() {}
};
void main(void)
{
B b;
//C c;
return;
}
- 注意
构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。
7.2 虚函数和纯虚函数
面试题3:下面说法中正确的是哪个
class A{
virtual void func1(){};
void func2();
};
class B:public virtual A
{
void func1(){
cout << "func1 in class B" << endl;
}
virtual void func2()
{
cout << "func2 in class B" << endl;
}
};
根据这段代码,判断以下说法中正确的是:
A:A中的func1和B中的func2都是虚函数。
B:A中的func1和B中的func2都不是虚函数。
C:A中的func2是虚函数,B中的func1不是虚函数。
D:A中的func2和B中的func1都是虚函数。
答案:
A是正确的
- 说明
虚函数为了重载和多态的需要在基类中定义,可以定义成空虚函数,在子类中可以重写也可以不写基类中的函数。纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像Java中的接口函数。
面试题4:写出程序的打印结果
写出以下程序的打印结果:
#include <iostream>
using namespace std;
class A{
public:
virtual void print(void)
{
cout << "A::print()" << endl;
}
};
class B:public A{
public:
virtual void print(void)
{
cout << "B::print()" << endl;
}
};
class C:public B{
public:
virtual void print(void)
{
cout << "C::print()" << endl;
}
};
void printf(void)(A a){
a.print();
}
void main(void)
{
A a,*pa,*pb,*pc;
B b;
C c;
pa = &a;
pb = &b;
pc = &c;
a.print();
b.print();
c.print();
pa->print();
pb->print();
pc->print();
print(a);
print(b);
print(c);
}
答案:
打印结果:
A::print() B::print() C::print() A::print() B::print() C::print() A::print() A::print() A::print()
面试题5:访问基类的私有虚函数
写出以下程序的输出结果:
#include <iostream>
using namespace std;
class A{
virtual void g(){
cout << "A::g" << endl;
}
private:
virtual void f()
{
cout << "A::f" << endl;
}
};
class B:public A{
void g()
{
cout << "B::g" << endl;
}
virtual void h(){
cout << "B::h" << endl;
}
};
typedef void(*Fun)(void);
void main(void)
{
B b;
Fun pFun;
for(int i = 0;i < 3;i++)
{
pFun = (Fun)*((int *) * (int *)(&b) + i);
pFun();
}
}
答案:
输出结果:
B::g A::f B::h
- 注意
本题注意考察了面试者对虚函数的理解程度。一个虚函数不了解的人很难正确地做出本题。在学习面向对象的多态性时一定要深刻理解函数表的工作原理。
7.3 多态
面试题6:简述类成员函数的重写、重载和隐藏的区别
答案:
(1)重写和重载主要有以下几点不同。
· 范围的区别:被重写函数和重写函数在两个类中,而重载和被重载的函数在同一个类中。
· 参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
· virtual的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。
· 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
· 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。
面试题7:简述多态的实现原理
答案:
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针vptr(对VC编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系了起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable。如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
注意
一定要区分虚函数、纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态是C++面试的重要考点之一,而虚函数是实现多态的基础。
8、数据结构
8.1 链表
面试题1:链表和数组有什么区别
-
所涉及的知识点
- 数据物理存储结构
- 链表节点指针
-
分析问题
数组中的数据在内存中,按顺序存储在一块连续的空间上,声明时就要确定长度。而链表动态地进行存储分配,链表可以是连续的,也可以是不连续的,链表的存储结构如下图所示:
答案:
数组和链表有以下几点不同。
- 存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个节点要保存相邻节点指针。
- 数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
- 数据插入或删除:链表可以快速插入和删除结点,而数组可能需要大量数据移动。
- 越界问题:链表不存在越界问题,数组有越界问题。
面试题2:寻找单链表中间节点
快速查找单链表中间节点。
-
所涉及的知识点
- 算法的设计
- 链表数据结构
-
分析问题
要查找中间结点,最容易想到的方法就是通过头到尾遍历链表得到链表的长度,再根据长度遍历得到中间结点,但是过程麻烦。
如果不信浪费CPU资源,只能开动脑筋来想办法。如果是个双向链表,可以首尾并行,利用两个指针一个从头到尾,一个从尾到头,当两个指针相遇时就找到了中间元素。受此启发可以想到单链表也可以用双指针来实现中间结点的快速查找:
- 有两个指针同时从头开始遍历。
- 一个快指针一次走两步,一个慢指针一次走一步。
- 快指针先到链表尾部,而慢指针应该则恰好到达链表中部。
- 注意
快指针到链表尾部时,当链表长度为奇数时,慢指针指向的即是链表中间指针。当链表长度为偶数时,慢指针指向的结点和慢指针指向结点的下个结点都是链表的中间结点。这一点读者需要注意,并没有在代码中体现。
答案:
typedef struct NODE//定义一个节点结构体 { int a; struct node *next; }NODE; NODE *middle(lnode *head) { NODE *fast,*slow,*p;//定义三个结点指针 if(head == NULL) { return NULL; } fast = slow = head;//快和慢指针同时指向链表首结点 while(!(p = fast->next) && !p->next)//循环,直到快指针下个结点或下下个结点为空 { slow = slow->next;//慢指针走一步 fast = p->next;//快指针走两步 } return slow;//返回中间结点 }
面试题3:怎样把一个单链表反序
对整个反转过程有一个清晰的理解:
把头指针指向空,后面的结点的next指针指向它前面的一个元素,最后尾结点成了头结点,问题就解决了。详细步骤如下图:
答案:
(1)反转一个链表。循环算法:
List reversr(List n) { if(!n)//判断链表是否为空,为空即退出 { return n; } list cur = n.next;//保存头结点的下个结点 list pre = n;//保存头结点 list = temp; pre.next = NULL;//头结点的指针指向空,转换后变尾结点 while(NULL != cur.next)//循环直到cur.next为空 { temp = cur; temp.next = pre; pre = temp; cur = cur.next; } return temp;//返回头指针 }
(2) 反转一个链表。递归算法:
List *reverse(List *oldList,List *newHead = NULL) { List *next = oldList->next;//记录上次翻转后的链表 oldList->next = newHead;//将当前结点插入到翻转后链表开头 newHead = oldList;//递归处理剩余的链表 return (next == NULL)?newHead:reverse(t,newHead); }
8.2 单循环链表
面试题4:根据需求建立一个单向循环链表
设计一个程序,用键盘输入的一些数字创建一个单向循环链表,求出链表长度,打印链表各个元素。之后可以根据需求把这个循环链表改装成单向链表或带环的单向链表。
-
所涉及的知识点
- 单向循环链表
- 结构体
-
分析问题
本题要求完成以下几个任务:
链表创建、链表长度计算、链表元素打印、链表改装。
-
创建一个链表:根据键盘输入的数据创建一个单向循环链表。
首先创建一个结点的结构体,这个结构体包含两个元素:一个数据元素,一个是本结构体指针元素。
-
答案:
面试题5:检测一个较大的单向链表是否带环
8.3 双向链表
面试题6:按要求构造一个双向链表
面试题7:编程实现双链表插入新结点
面试题8:编程实现双链表删除指定结点
8.4 栈和队列
面试题9:简述栈和队列的异同
栈和队列是两种不同的数据结构,它们之间相互联系又相互区别,请简述两者的异同。
答案:
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”
注意
区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构的栈。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。
面试题10:建立一个链式栈
建立一个链式栈,实现入栈和出栈功能。
面试题11:建立一个链式队列
建立一个链式队列,实现入队和出队功能。
面试题12:能否用两个栈实现一个队列的功能
能否用两个栈实现一个队列的功能
8.5 二叉树
面试题13:建立一个二叉树
编码实现二叉树的创建,二叉树的先序、中序和后序遍历。
面试题14:计算一颗二叉树的深度
设计一个算法,实现计算二叉树的深度的功能。
面试题15:在二元二叉树中找出和为某一值的所有途径
9、排序
9.1 插入排序
面试题1:编码实现直接插入排序
面试题2:编码实现希尔(Shell)排序
9.2 交换排序
面试题3:编码实现冒泡排序
面试题4:编码实现快速排序
交换两个变量的值,不使用第三个变量
答:有两种解法,算术算法,用^(异或)
a = a + b;
b = a - b;
a = a - b;
或者
a = a ^ b;
b = a ^ b;
a = a ^ b;
或者
a ^= b ^= a;
9.3 选择排序
面试题5:编码实现选择排序
面试题6:编码实现堆排序
9.4 基数排序
面试题7:编码实现基数排序
操作系统
编译原理
体现结果&组成原理
数据结构与算法
请编写代码测试一个机器是大端还是小端
这个问题主要考察你对字节序的理解,涉及到一个自己类型(比如 short)在内存中如何存放的问题,可以使用共用体来实现
带参宏与带参函数的区别(至少说出5点)?
处理时间 | 编译时 | 运行时 |
---|---|---|
参数类型 | 无 | 需定义 |
程序长度 | 变长 | 不变 |
占用存储空间 | 否 | 是 |
运行时间 | 否 | 调用和返回时占用 |
有符号、无符号运算的问题:
当有符号数据和无符号数据运算的时候(包括±*/比较),有符号被系统强制转换为无符号来运算,导致有符号数据变得很大
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a + b > 6)?puts(">6"):pcuts("<6");
}
输出结果: > 6
如果换成
(a > b)?puts(">6"):puts("<6");
类型转换 运算符只允许通类型操作,如果不同类型
-
隐式类型转换(类型不匹配,机器自动转换)
低精度向高精度、有符号向无符号自动转换 复制:右侧无条件转为左侧
-
强制类型转换(类型不匹配,手动转换)
(类型)数据
将物理内存地址0x20345678写入数据0x10110101
这一问题测试你是否知道为了访问一绝对地址把一个整数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同,典型的类似代码如下:
int *ptr;
ptr = (int *)0x20345678;
*ptr = 0xaa66;
记住,如果是寄存器,别忘了加上volatile
一个单向链表,不知道头结点,一个指针指向其中一个节点,问如何删除该节点?
将这个指针指向的next节点值copy到本节点,将next指向next->next,并随后删除原next指向的节点。
这个问题主要考验技巧,类似的题目还会有删除一个节点,插入一个节点
写出sizeof(struct name1) = ,sizeof(struct name2) = 的结果
自然对齐:变量所在的地址是变量大小的整数倍,比如int a,能存放的地址是0,4,8,12,…
这类结构体对齐的问题非常多,机器一般32bit
struct name1{
char str;
short x;
int num;
};
struct name2{
char str;
int num;
short x;
};
sizeof(struct name1) = 8
sizeof(struct name2) = 12
在第二个结构体中,为保证num按四个字节对齐,char后必须留出3字节的空间;同时为保证整个结构的自然对齐(这里是4字节对齐),在x后还要补齐2个字节,这样就是12字节。
下面来解释内存的对齐规则:
- 结构的第一个成员都永远放在0偏移出;[任何数据都是0的整数倍]
- 从第二个成员开始,都要保证自然对齐
- 结构体的总大小必须是CPU字长的整数倍(如果整个结构体长度小于字长,则不需要对齐,结构体大小就是实际大小);
- 不考虑:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大成员的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍
合并两个(有序、无序)链表,(参考数据结构笔记)
linux特色的题目
$$$结构体成员数组大小为0
结构体数组成员的大小为0 是 GNU C的一个特性,好处是可以在结构体中分配不定长的大小,如:
#define SIZE 100
typedef struct{
int a;
int b;
char c[0];
}st_t;
//sizeof(st_t)等于8,即char c[0]的大小为0
//可以通过s->c来访问这片可以动态分配的内存空间
st_t *s = (st_t *)malloc(sizeof(st_t) + SIZE);
位运算(这种问题涉及到单片机寄存器问题,是底层的必考题!)
寄存器register,是软件访问硬件的接口,一个寄存器地址为0x12345678,
- 请将该寄存器写入数值0x56712345;
- 请将该寄存器的第7bit置1
- 请将该寄存器的第8bit置0
- 请将该寄存器8-11bit置1
- 请将该寄存器8-11bit写入 0101
答案:参考ARM课程寄存器操作(比如LED PWM)
寄存器,必须使用volitle!
GPX *((volatile unsigned long *)0x12345678)
GPX = 0X56712345;
GPX |= (1 << 7);
GPX &=~(1 << 8);
GPX |= (0Xf << 8);
GPX &=(~(0Xf << 8));//GPX |= (5 << 8)
请编写一个 C 函数,该函数给出一个字节中被置 1 的位的个数
unsigned int TestAsOne0(char log)
{
int i;
unsigned int num=0, val;
for(i=0; i<8; i++)
{
val = log >> i; //移位
val &= 0x01; //与1相与
if(val)
num++;
}
return num;
}
硬件技术相关
中断是什么,简述中断工作流程。请描述中断的边沿触发和电平触发的区别,中断函数处理器需要注意什么?
简述串口 i2c spi 总线的区别
简述ADC原理,ADC是如何测量电压的,有时候会出计算题,求解电压数值
定时器,pwm原理是什么?
c语言关键字valatile有什么作用?请举例说明使用场景。
请实现一个函数让程序执行此函数时能跳转到指定地址0x0运行。