嵌入式软件工程师面试题——2025校招社招通用(C/C++篇)(二)

说明:

  • 面试群,群号: 228447240
  • 面试题来源于网络书籍,公司题目以及博主原创或修改(题目大部分来源于各种公司);
  • 文中很多题目,或许大家直接编译器写完,1分钟就出结果了。但在这里博主希望每一个题目,大家都要经过认真思考,答案不重要,重要的是通过题目理解所考知识点,好应对题目更多的变化;
  • 博主与大家一起学习,一起刷题,共同进步;
  • 写文不易,麻烦给个三连!!!

1.用预处理指令#define声明一个常数,用以表明1年中有多少秒?

解析
通过这道题面试官想考以下几个知识点:

  • #define语法的基本知识(例如,不能以分号结束,括号的使用,等等)
  • 要懂得预处理器将为你计算常数表达式的值,因此,写出你是如何计算一年中有多
    少秒而不是计算出实际的值,会更有意义
  • 意识到这个表达式将使一个16位机的整型数溢出,因此要用到长整型符号L,告诉
    编译器这个常数是长整型数
    如果在表达式中用到UL(表示无符号长整型),那么你就有了一个好的起点。记住,
    第一印象很重要。

答案#define SECOND_EVERY_YEAR (606024*365)UL

2.写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个

解析
这个测试是为下面的目的而设的

  • 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符
    变为标准C的一部分,宏都是方便地产生嵌入代码的唯一方法。对于嵌入式系统来说,为了
    能达到要求的性能,嵌入代码经常是必须的方法
  • 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比
    if-then-else更优化的代码,了解这个用法是很重要的
  • 注意点:一定要带好括号,不要嫌麻烦,因为宏操作是直接替换,如果不带括号会出现一些问题,请看下面的例子
#define SQUARE(x) x * x

int a = 4;
int b = SQUARE(a + 1);

在预处理阶段,SQUARE(a + 1) 会被替换成 a + 1 * a + 1,由于乘法的优先级高于加法,所以实际上会计算成 a + (1 * a) + 1,结果为 9 而不是 25。

答案#define MIN(x, y) ((x) <= (y) ? (x) : (y))

3.说说const修饰指针?

解析
关于const修饰指针的情况,一般分为如下4种情况:

int main()
{   
    int b = 500;
    const int* a = &b;          // 情况1
    int const* a = &b;          // 情况2
    int* const a = &b;          // 情况3
    const int* const a = &b;    // 情况4

    return 0;
}
  • 先看情况1
    如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常
    量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。因此,1和2的
    情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下
    不允许对内容进行更改操作。
    换句话来说,如果a是一名仓库管理员的话,他所进入的仓库,里面的货物(*a)是他没权
    限允许动的,仓库里面的东西原来是什么就是什么;所以
	int b = 500;
    const int* a = &b;
    *a = 600; // 错误

但是也有别的办法去改变*a的值,一个是通过改变b的值:

	int b = 500;
    const int* a = &b;
    b = 600;
    cout << *a << endl; // 得到600

还有一种改变*a办法就是a指向别处(管理员换个仓库):

 	int b = 500, c = 600;
    const int* a = &b;
    a = &c;
    cout << *a << endl; // 得到600
  • 情况2与情况1相同
  • 情况3为指针本身是常量,这种情况下不能对指针本身进行更改操作,而指针所指向
    的内容不是常量

    举例来说:如果a是一名仓库管理员的话,他只能进入指定的某仓库,而不能去别的仓
    库(所以a++是错误的);但这个仓库里面的货物(*a)是可以随便动的,(*a=600是正确
    的)。
    此外,对于情况3:定义时必须同时初始化。
	int b = 500, c = 600;
    // int *const a; // 错误 没有初始化
    int *const a = &b; // 正确,必须初始化
    *a = 600; // 正确,允许改值
    a = &c; // 错误,指针是常量
    cout << a++ << endl; // 错误
  • 对于情况4为指针本身和指向的内容均为常量。那么这个仓库管理员只能去特定的仓库,并且仓库里面所有的货物他都没有权限去改变。
	int b = 500, c = 600;
    const int* const a = &b;
    a = &c; // 错误
    *a = 600; // 错误

4.const与#define相比有什么不同?

答案:

  • const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误(边际效应)。
  • 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
  • 存储方式不同: 定义的常量存储在程序的数据段中,有固定的地址。而 #define 定义的常量只是简单的文本替换,在编译时会直接替换成对应的值,不会分配内存空间
  • 作用域不同: 常量的作用域与其他变量类似,通常只在定义它的文件或函数内部可见。而 #define 定义的常量没有作用域限制,可以在整个程序中使用。

5.下列程序的输出结果是什么?

#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;

struct {
    short a1;
    short a2;
    short a3;
}A;

struct {
    long a1;
    short a2;
}B;

int main()
{
    char *ss1 = "0123456789";
    char ss2[] = "0123456789";
    char ss3[100] = "0123456789";
	int ss4[100];
	char q1[] = "abc";
	char q2[] = "a\n";
	char *q3 = "a\n";
	char *str1 = (char *)malloc(100);
	void *str2 = (void *)malloc(100);

    cout << sizeof(ss1) << " ";
    cout << sizeof(ss2) << " ";
    cout << sizeof(ss3) << " ";
    cout << sizeof(ss4) << " ";
    cout << sizeof(q1) << " ";
    cout << sizeof(q2) << " ";
    cout << sizeof(q3) << " ";
    cout << sizeof(A) << " ";
    cout << sizeof(B) << " ";
    cout << sizeof(str1) << " ";
    cout << sizeof(str2) << " ";
    return 0;
}

解析

  • ss1是一个字符指针,指针的大小是一个定值,就是4字节,所以sizeof(ss1)是4字节
  • ss2是一个字符数组,这个数组最初未定大小,由具体填充值来定。填充值
    是“0123456789”。1个字符所占空间是1字节,10个就是10字节,再加上隐含的“\0”,所以一
    共是11字节
  • ss3也是一个字符数组,这个数组开始预分配100,所以它的大小一共是100字节
  • ss4也是一个整型数组,这个数组开始预分配100,但每个整型变量所占空间是4,所以
    它的大小一共是400字节
  • q1与ss2类似,所以是4字节
  • q2里面有一个“\n”,“\n”算做一位,所以它的空间大小是3字节
  • q3是一个字符指针,指针的大小是一个定值,就是4,所以sizeof(q3)是4字节
  • 结构体A中有3个short类型变量,各自以2字节对齐,结构体对齐参数按默认的8字节对齐,则a1、a2、a3都取2字节对齐,sizeof(A)为6,其也是2的整数倍
  • B中a1为4字节对齐,a2
    为2字节对齐,结构体默认对齐参数为8,则a1取4字节对齐,a2取2字节对齐;结构体大小为6字节,6不为4的整数倍,补空字节,增到8时,符合所有条件,则sizeof(B)为8
  • str1与str2与ss1类似,都是指针的大小,所以为4

答案: 4 11 100 400 4 3 4 6 8 4 4

6.以下代码为32位机器编译,数据是以4字节为对齐单位,这两个类的输出结果是什么,为什么?

#include <iostream>
using namespace std;

class A {
private:
    bool m_bTemp;
    int m_nTemp;
    bool m_bTemp2;
};

class B {
private:
    int m_nTemp;
    bool m_bTemp;
    bool m_bTemp2;
};

int main()
{   
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    return 0;
}

解析
在访问内存时,如果地址按4字节对齐,则访问效率会高很多。这种现象的原因在于访问内存的硬件电路。一般情况下,地址总线总是按照对齐后的地址来访问的。例如你想得到0x00000001开始的4字节内容,系统首先需要以0x00000000读4字节,从中取得3字节,然后再用0x00000004作为开始地址,获得下一个4字节,再从中得到第一个字节,两次组合出你想得到的内容。但是如果地址一开始就是对齐到0x00000000,则系统只要一次读写即可。
在这里插入图片描述

答案:12 8

7.说明sizeof和strlen之间的区别

解析
由以下几个例子我们说明sizeof和strlen之间的区别。
第1个例子:

char * ss = "0123456789";

sizeof(ss)结果为4,ss是指向字符串常量的字符指针。
sizeof(*ss)结果为1,*ss是第一个字符。

第2个例子:

char ss[] = "0123456789";

sizeof(ss)结果为11,ss是数组,计算到“\0”位置,因此是(10+1)。
sizeof(*ss)结果为1,*ss是第一个字符。

第3个例子:

char ss[100] = "0123456789";

sizeof(ss)结果为100,ss表示在内存中预分配的大小,100×1。
strlen(ss)结果为10,它的内部实现是用一个循环计算字符串的长度,直到“\0”为止。

第4个例子:

int ss[100] = "0123456789";

sizeof(ss)结果为400,ss表示在内存中的大小,100×4。
strlen(ss)错误,strlen的参数只能是char*,且必须是以“\0”结尾的。

第5个例子:

class X
{
	int i;
	int j;
	char k;
};
X x;

cout<<sizeof(X)<<endl;结果为12,内存补齐。
cout<<sizeof(x)<<endl;结果为12,理由同上。

答案

通过对sizeof与strlen的深入理解,得出两者区别如下:

  • sizeof操作符的结果类型是size_t,它在头文件中的typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。
  • sizeof是运算符,strlen是函数。
  • sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以“\0”结尾的。sizeof还可以用函数做参数。
  • 数组做sizeof的参数不退化,传递给strlen就退化为指针。
  • 大部分编译程序在编译的时候就把sizeof计算过了,是类型或是变量的长度。这就是sizeof(x)可以用来定义数组维数的原因。
  • strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,而不是类型
    占内存的大小。
  • sizeof后如果是类型必须加括号,如果是变量名可以不加括号。这是因为sizeof是
    个操作符而不是个函数。
  • 当使用了一个结构类型或变量时,sizeof返回实际的大小。当使用一静态的空间数
    组时,sizeof返回全部数组的尺寸。sizeof操作符不能返回被动态分配的数组或外部的数组的
    尺寸。
  • 数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,如
    fun(char [8])、fun(char [])都等价于fun(char *)。在C++里传递数组永远都是传递指向数组首
    元素的指针,编译器不知道数组的大小。如果想在函数内知道数组的大小,需要这样做:进
    入函数后用memcpy将数组复制出来,长度由另一个形参传进去。代码如下:
void fun (unsigned char *p1, int len)
{
    unsigned char* buf = new unsigned char[len+1];
    memcpy(buf, p1, len);
}

  • sizeof操作符不能用于函数类型、不完全类型或位字段。不完全类型指具有未知
    存储大小数据的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void
    类型等。

8.说明sizeof的使用场合

答案

  • sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信。例如:
void *malloc(size_t size);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 用它可以看看某种类型的对象在内存中所占的单元字节
  • 在动态分配一对象时,可以让系统知道要分配多少内存
  • 便于一些类型的扩充。在Windows中有很多结构类型就有一个专用的字段用来存放该类型的字节大小
  • 由于操作数的字节数在实现时可能出现变化,建议在涉及操作数字节大小时用sizeof代替常量计算。
  • 如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小

9. 这个数组占据多大空间?

int **a[3][4]  = ?

A. 64
B. 12
C. 48
D. 128

解析:sizeof问题,3×4×4=48。

答案:C

10.一个空类占多少空间?多重继承的空类呢?

解析:我们用程序来实现一个空类和一个多重继承的空类。看看它们的大小是多少。代
码如下:

#include <iostream>
#include <memory.h>
#include <assert.h>
using namespace std;

class A {};
class A2 {};

class B : public A {};
class C : public virtual B {};
class D : public A, public A2 {};


int main()
{   
    cout << "sizeof(A): " << sizeof(A) << endl;
    cout << "sizeof(B): " << sizeof(B) << endl;
    cout << "sizeof(C): " << sizeof(C) << endl;
    cout << "sizeof(D): " << sizeof(D) << endl;

    return 0;
}

空类所占空间为1,单一继承的空类空间也为1,多重继承的空类空间还是1。
但是虚继承涉及虚表(虚指针),所以sizeof©的大小为4。

答案:1 1 4 1

11.内联函数和宏的差别是什么?

答案:

  • 内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。而宏只是一个简单的替换。
  • 内联函数要做参数类型检查,这是内联函数跟宏相比的优势。
  • inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但是inline在和宏相比没有付出任何额外代价的情况下更安全。至于是否需要inline函数,就需要根据实际情况来取舍了。
  • inline一般只用于如下情况:
    一个函数不断被重复调用。
    函数只有简单的几行,且函数内不包含for、while、switch语句。

一般来说,我们写小程序没有必要定义成inline,但是如果要完成一个工程项目,当一个简单函数被调用多次时,则应该考虑用inline。

  • 宏在C语言里极其重要,而在C++里用得就少多了。关于宏的第一规则是绝不应该去使用它,除非你不得不这样做。几乎每个宏都表明了程序设计语言里、程序里或者程序员的一个缺陷,因为它将在编译器看到程序的正文之前重新摆布这些正文。宏也是许多程序设计工具的主要麻烦。所以,如果你使用了宏,就应该准备只能从各种工具(如排错系统、交叉引用系统、轮廓程序等)中得到较少的服务。
  • 宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
  • 宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。
  • 关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。如下风格的函数Foo不能成为内联函数:
inline void Foo(int x, int y); // inline仅与函数声明放在一起
void Foo(int x, int y) {}

而如下风格的函数Foo则成为内联函数:

void Foo(int x, int y); 
inline void Foo(int x, int y) {} // inline与函数定义放在一起

所以说,inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。内联能提高函数的执行效率,至于为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

以下情况不宜使用内联:
1 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
2 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值