• 嵌入式软件工程师面试题——2025校招社招通用(十一)


    说明:

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

    1.什么是内存泄露,如何检测与避免

    答案:
    内存泄露
    一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
    避免内存泄露的几种方式

    • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
    • 一定要将基类的析构函数声明为虚函数
    • 对象数组的释放一定要用delete []
    • 有new就有delete,有malloc就有free,保证它们一定成对出现

    2.对象复用的了解,零拷贝的了解

    答案:
    对象复用
    对象复用其本质是一种设计模式:Flyweight享元模式。
    通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。

    零拷贝
    零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。
    零拷贝技术可以减少数据拷贝和共享总线操作的次数。
    在C++中,vector的一个成员函数 emplace_back() 很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。举个例子:

    #include 
    #include 
    #include 
    using namespace std;
    
    struct Person
    {
        string name;
        int age;
        //初始构造函数
        Person(string p_name, int p_age): name(std::move(p_name)), age(p_age)
        {
             cout << "I have been constructed" <<endl;
        }
         //拷贝构造函数
         Person(const Person& other): name(std::move(other.name)), age(other.age)
        {
             cout << "I have been copy constructed" <<endl;
        }
         //转移构造函数
         Person(Person&& other): name(std::move(other.name)), age(other.age)
        {
             cout << "I have been moved"<<endl;
        }
    };
    
    int main()
    {
        vector<Person> e;
        cout << "emplace_back:" <<endl;
        e.emplace_back("Jane", 23); //不用构造类对象
    
        vector<Person> p;
        cout << "push_back:"<<endl;
        p.push_back(Person("Mike",36));
        return 0;
    }
    //输出结果:
    //emplace_back:
    //I have been constructed
    //push_back:
    //I have been constructed
    //I am being moved.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    3.C++的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast

    答案:
    reinterpret_cast:
    用于将一个对象的内存表示重新解释为另一个类型的对象。它可以在不改变对象值的情况下,将一个类型转换为另一个类型。例如:

    int a = 1;
    double b = reinterpret_cast<double&>(a); // 将整数a的内存表示转换为双精度浮点数b
    
    • 1
    • 2

    const_cast:
    该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外.
    用于移除对象的const属性。它可以在需要修改const对象时使用,例如:

    const int a = 1;
    int* p = const_cast<int*>(&a); // 将const整数a的地址转换为非const整数指针p
    *p = 2; // 修改p指向的值
    
    • 1
    • 2
    • 3

    static_cast:
    没有运行时类型检查来保证转换的安全性
    用于在编译时进行类型转换。它可以在需要显式类型转换时使用,例如:

    int a = 1;
    double b = static_cast<double>(a); // 将整数a转换为双精度浮点数b
    
    • 1
    • 2

    dynamic_cast:
    有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全
    用于在运行时进行类型转换。它可以在需要根据对象的实际类型进行类型转换时使用,例如:

    class Base { virtual void print() {} };
    class Derived : public Base {};
    
    Base* basePtr = new Derived();
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 将基类指针basePtr转换为派生类指针derivedPtr
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.C++函数调用的压栈过程

    答案:
    函数的调用过程:
    1)从栈空间分配存储空间
    2)从实参的存储空间复制值到形参栈空间
    3)进行运算
    形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。
    数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。
    当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

    #include 
    using namespace std;
    
    int f(int n) 
    {
    	cout << n << endl;
    	return n;
    }
    
    void func(int param1, int param2)
    {
    	int var1 = param1;
    	int var2 = param2;
    	printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
    }
    
    int main(int argc, char* argv[])
    {
    	func(1, 2);
    	return 0;
    }
    //输出结果
    //2
    //1
    //var1=1,var2=2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    5.谈一谈移动构造函数

    答案:

    • 我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
    • 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
    • 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。
    class Person {
    public:
        Person() : name(""), age(0) {}
        Person(const std::string& n, int a) : name(n), age(a) {}
        Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
            other.name = "";
            other.age = 0;
        }
    
    private:
        std::string name;
        int age;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    6.如何获得结构成员相对于结构开头的字节偏移量

    答案:
    使用头文件中的,offsetof宏。
    示例如下:

    #include 
    #include 
    using namespace std;
    
    struct  S
    {
    	int x;
    	char y;
    	int z;
    	double a;
    };
    int main()
    {
    	cout << offsetof(S, x) << endl; // 0
    	cout << offsetof(S, y) << endl; // 4
    	cout << offsetof(S, z) << endl; // 8
    	cout << offsetof(S, a) << endl; // 12
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    7.静态类型和动态类型,静态绑定和动态绑定的介绍

    答案:

    • 静态类型:对象在声明时采用的类型,在编译期既已确定;
    • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
    • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
    • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

    从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。 举个例子:

    #include 
    using namespace std;
    
    class A
    {
    public:
    	/*virtual*/ void func() { std::cout << "A::func()\n"; }
    };
    class B : public A
    {
    public:
    	void func() { std::cout << "B::func()\n"; }
    };
    class C : public A
    {
    public:
    	void func() { std::cout << "C::func()\n"; }
    };
    int main()
    {
    	C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
    	B* pb = new B(); //pb的静态类型和动态类型也都是B*;
    	A* pa = pc;      //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
    	pa = pb;         //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
    	C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
        
        pa->func();      //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
    	pc->func();      //C::func() pc的动、静态类型都是C*,因此调用C::func();
    	pnull->func();   //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    如果将A类中的virtual注释去掉,则运行结果是:

    a->func();      //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
    pc->func();      //C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
    pnull->func();   //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;
     
    
    • 1
    • 2
    • 3
    • 4

    在上面的例子中,

    • 如果基类A中的func不是virtual函数,那么不论pa、pb、pc指向哪个子类对象,对func的调用都是在定义pa、pb、pc时的静态类型决定,早已在编译期确定了。
    • 同样的空指针也能够直接调用no-virtual函数而不报错(这也说明一定要做空指针检查啊!),因此静态绑定不能实现多态;
    • 如果func是虚函数,那所有的调用都要等到运行时根据其指向对象的类型才能确定,比起静态绑定自然是要有性能损失的,但是却能实现多态特性;

    至此总结一下静态绑定和动态绑定的区别:

    • 静态绑定发生在编译期,动态绑定发生在运行期;
    • 对象的动态类型可以更改,但是静态类型无法更改;
    • 要想实现动态,必须使用动态绑定
    • 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

    8.指针加减计算要注意什么?

    答案:

    • 对于指针的减法运算,其结果表示两个指针之间间隔了多少个“对应类型”的数据。例如,如果有两个整数指针p和q,它们之间的差值就是它们所指向的整数在内存中的距离(以字节为单位)
    • 关于指针的加法运算,它通常用于遍历数组或移动到下一个元素的位置。然而,必须注意的是,对指针进行加法运算并没有直接的意义。例如,不能简单地将一个整数加到一个指针上,也不能将两个指针相加得到一个新的指针。此外,当我们试图通过指针加减数字来移动指针位置时,这个数字必须是指针所指向数据类型的大小。例如,如果我们有一个整型指针p并想要将其向前移动4个位置,我们应该写成p += 4 * sizeof(int)。
    • 要避免访问未分配的内存或已经被释放的内存。这可能会导致程序崩溃或其他未定义的行为。
      举个栗子:
    include <iostream>
    using namespace std;
    
    int main()
    {
    	int *a, *b, c;
    	a = (int*)0x500;
    	b = (int*)0x520;
    	c = b - a;
    	printf("%d\n", c); // 8
    	a += 0x020;
    	c = b - a;
    	printf("%d\n", c); // -24
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    首先变量a和b都是以16进制的形式初始化,将它们转成10进制分别是1280(5162=1280)和1312(5*162+216=1312), 那么它们的差值为32,也就是说a和b所指向的地址之间间隔32个位,但是考虑到是int类型占4位,所以c的值为32/4=8

    a自增16进制0x20之后,其实际地址变为1280 + 2164 = 1408,(因为一个int占4位,所以要乘4),这样它们的差值就变成了1312 - 1408 = -96,所以c的值就变成了-96/4 = -24

    9.C++怎样判断两个浮点数是否相等?

    答案:
    在计算机编程中,由于浮点数的精度问题,直接比较两个浮点数是否相等可能会得到错误的结果。因此,通常采用一种称为“浮点数比较”的方法来判断两个浮点数是否相等。
    以下是一种常见的方法:

    • 定义一个非常小的正数(例如0.00001),称为机器精度或epsilon。
    • 如果两个浮点数的差的绝对值小于等于epsilon,则认为这两个浮点数相等。

    举个栗子:

    #include 
    #include 
    
    bool float_equal(float a, float b, float epsilon = 1e-6) {
        return std::abs(a - b) < epsilon;
    }
    
    int main() {
        float num1 = 0.1 + 0.2;
        float num2 = 0.3;
    
        if (float_equal(num1, num2)) {
            std::cout << "两个浮点数相等" << std::endl;
        } else {
            std::cout << "两个浮点数不相等" << std::endl;
        }
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    10.类如何实现只能静态分配和只能动态分配

    • 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建
    • 建立类的对象有两种方式:
      • 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
      • 动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;
    • 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有。

    11.继承机制中对象之间如何转换?指针和引用之间如何转换?

    答案:

    对象之间的转换主要有两种类型:向上类型转换和向下类型转换。

    向上类型转换
    将派生类指针或引用转换为基类的指针或引用被称为向上类型转换。这种转换会自动进行,是安全的。例如,如果我们有一个指向派生类对象的基类指针,我们可以直接使用这个指针来操作派生类对象,无需进行任何额外的转换。
    向下类型转换:
    将基类指针或引用转换为派生类指针或引用被称为向下类型转换。这种转换需要显式进行,且只有在运行时才能确定是否成功。向上类型转换和向下类型转换的主要区别在于,向上类型转换是自动的、安全的,而向下类型转换需要显式进行,并可能存在风险。

    指针和引用之间的转换主要有两种形式:指针转引用和引用转指针。
    指针转引用
    这是将一个指针赋值给一个引用。例如,当我们有一个指向某个对象的指针p时,我们可以创建一个引用r,使得r指向与p相同的对象。具体来说,我们可以通过使用*操作符来完成这个转换,如:int a = 10; int *p = &a; int &r = *p;。
    引用转指针:
    这是将一个引用赋值给一个指针。例如,如果我们有一个引用r,我们可以创建一个指针p,使得p指向与r相同的对象。为了实现这一点,我们可以使用&操作符来获取r的地址,如:int a = 10; int &r = a; int *p = &r;。

  • 相关阅读:
    Python操作Excel、Word、PPT、PDF、复杂文件、通信软件(微信、邮件、飞书、钉钉)、图片集合大全
    以数智化驱动为核心,构建研发效能增长动力
    ABC分析做法、步骤、Pareto图制作方法解说
    利用C++开发一个迷你的英文单词录入和测试小程序-升级版本
    Linux系统 (二)- 指令学习2
    MyBatis中的StrictMap类
    【附源码】计算机毕业设计SSM视频网站
    Tainted kernels
    Selenium3.0基础 — 自动化测试概述
    完美解决新旧R版本冲突ggplot包不能安装问题
  • 原文地址:https://blog.csdn.net/weixin_45257157/article/details/134289752