【秋招】嵌入式面试八股文- C++ 基础
本文为C语言部分,具体整篇目录可以看前言!
第一部分(纯八股)
1 C语言
关于C语言部分内容见此篇文章【秋招】嵌入式面试八股文-C语言01篇
2 C++部分
2.1 C++中类成员的访问权限?
- 无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部 (定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
(1)protected:受保护的
- 类内和子类可直接访问,也就是说,基类中有protected成员,子类继承于基类,那么也可以访问基类的protected成员,要是基类是private成员,则对于子类也是隐藏的,不可访问。
(2)private:私有的
- 只有类内的成员函数才可以访问。
2.2 什么是构造函数?
- 构造函数是一种特殊的函数,用于创建和初始化对象。它在创建对象时被调用,用于设置对象的初始状态和属性。构造函数的名称通常与类的名称相同,且没有返回类型声明。
- 构造函数可以有多个重载版本,每个版本允许接受不同类型和数量的参数。通过调用不同的构造函数,可以根据需要创建不同种类的对象。
构造函数的主要功能包括:
- 分配内存空间:构造函数负责为对象分配足够的内存空间,以存储对象的数据成员。
- 初始化对象:构造函数可以对对象的数据成员进行初始化,确保对象的属性处于正确的初始状态。
- 设置默认值:构造函数可以为对象的属性设置默认值,以避免对象在创建时出现未定义的行为。
在C++中,构造函数名称与类名称相同,没有返回类型声明,并且可以是公有、私有或受保护的。当创建对象时,会自动调用适当的构造函数来初始化对象。如果未明确定义构造函数,编译器将提供一个默认的无参数构造函数。
2.3 构造函数的分类是怎样的?
(1)无参构造 Person( ) {}
(2)有参构造 Person(int a) {}
(3)拷贝构造函数 Person( const Person& p) {}
2.4 构造函数的调用规则是怎样的?
C++编译器至少给一个类添加3个函数:
(1)默认构造函数(无参)
(2)默认析构函数(无参)
(3)默认拷贝构造函数,对属性进行值拷贝
- 如果用户定义了有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造;
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数。
2.5 什么是析构函数?
需要自己定义构造函数和析构函数的情况有以下几种:
- 当需要在对象创建时进行一些初始化操作时,可以定义构造函数来实现。比如,需要在对象创建时给成员变量赋初值或者打开一些资源。
- 当需要在对象销毁时进行一些清理操作时,可以定义析构函数来实现。比如,需要在对象销毁时释放一些资源或者关闭一些文件。
- 当需要控制对象的生命周期时,可以定义构造函数和析构函数来实现。比如,需要在对象创建时进行一些操作,在对象销毁时进行一些清理操作,这样可以确保对象的正确使用。
总之,需要自己定义构造函数和析构函数的情况主要是为了实现一些特定的需求,比如初始化、清理、控制对象的生命周期等。
注意:构造函数可以有参数,因此可以重载,析构函数不能有参数,因此不可以发生重载。
2.6 引用的注意事项?
- 引用格式: 数据类型 &别名 = 原名
- 引用必须要进行初始化;
- 引用在初始化后不可以改变;
- 函数传参时,可以利用引用让形参修饰实参;
- 引用可以作为函数的返回值,但是不要返回局部变量
- 引用的本质是在C++内部实现一个指针常量。
2.7 函数重载是什么?
重载满足条件:
- 同一个作用域下;
- 函数名称相同;
- 函数参数类型不同或者个数不同或者顺序不同。
2.8 什么是深拷贝与浅拷贝?
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
- 注意:当在类里面涉及到指针操作时,如果采用浅拷贝,则执行拷贝构造函数后。会导致拷贝出两个指针指向同一个内存空间,则进行析构函数时,就会对该空间释放两次,然后导致报错。因此需要进行深拷贝,对于指针重新再开辟一段空间。
2.9 静态成员归纳
(1)静态成员变量:
- 所有对象共享同一份数据; 在编译阶段分配内存;类内声明,类外初始化
(2)静态成员函数:
- 所有对象共享同一个函数;静态成员函数只能访问静态成员变量
(3)关于两者内存:
- 如果只声明了类而未定义对象,则类的一般成员变量是不占用内存空间的,只有在定义对象的时候,才为对象的成员变量分配空间。
静态成员不占用类内空间;静态成员函数在类内声明,类外初始化。
2.10 继承是什么?
- 语法: class 子类 : 继承方式 父类 比如:class A :public B
- 多继承语法:class 子类 : 继承方式 父类1,继承方式 父类1
- 继承过程中,父类中的私有成员也被子类继承,只是由编译器隐藏后访问不到。
继承同名成员处理方式:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。
2.11 菱形继承是什么?
- 两个派生类继承同一个基类;又有某个类同时继承了两个派生类;这种继承称为菱形继承。
- 羊继承了动物数据;马继承了动物数据;草泥马继承了羊和马的数据,则动物数据被继承了两份。
采用虚继承的方法解决该问题。
2.12 虚函数是什么?
- 虚函数只能是类的成员函数, 而不能将类外的普通函数声明为虚函数。虚函数的作用是允许在派生类中对基类的虚函数重新定义 (函数覆盖), 只能用于类的继承层次结构中.
- 虚函数能有效减少空间开销。 当一个类带有虚函数时, 编译系统会为该类构造一个虚函数表 (一个指针数组), 用于存放每个虚函数的入口地址。
什么时候应该使用虚函数:
- 判断成员函数所在的类是不是基类, 非基类无需使用虚函数
- 成员函数在类被继承后有没有可能被更改的功能, 如果希望修改成员函数功能, 一般在基类中将其声明为虚函数;
- 我们会通过对象名还是基类指针访问成员函数, 如果通过基类指针过引用去访问, 则应当声明为虚函数。
2.13 静态函数和虚函数的区别?
- 多态分为两类:静态多态和动态多态
- 什么是静态多态:函数重载和运算符重载属于静态多态
- 虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。
2.14 什么是多态?
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。
在继承中要想构成多态需要满足两个条件:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
2.15 纯虚函数是什么?
- 纯虚函数不需要在父类中实现,必须在子类中实现
- 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
- 纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0
当类中有了纯虚函数,这个类也称为抽象类,抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
class Base { public: virtual void func() { cout << "Base::func" << endl; } virtual void pureFunc() = 0; // 纯虚函数 };
2.16 重载和覆盖有什么区别?
- 覆盖是子类和父类之间的关系,垂直关系;重载同一个类之间方法之间的关系,是水平关系。
- 覆盖只能由一个方法或者只能由一对方法产生关系;重写是多个方法之间的关系。
- 覆盖是根据对象类型(对象对应存储空间类型)来决定的;而重载关系是根据调用的实参表和形参表来选择方法体的。
2.17 析构函数可以为 virtual 型,构造函数则不能,为什么?
- 虚函数的主要意义在于被派生类继承从而产生多态。派生类的构造函数中,编译器会加入构造基类的代码,如果基类的构造函数用到参数,则派生类在其构造函 数的初始化列表中必须为基类给出参数,就是这个原因。虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。
2.18 数组下标可以为负数吗
- 可以,因为下标只是给出了一个与当前地址的偏移量而已,只要根据这个偏移量能定位得到目标地址即可。
2.19 结构体和类的区别
区别:
(1)类型不同
- 结构体是一种值类型,而类是引用类型。
- 值类型用于存储数据的值,引用类型用于存储对实际数据的引用。那么结构体就是当成值来使用的,类则通过引用来对实际数据操作
(2)存储不同
- 结构使用栈存储,而类使用堆存储
- 栈的空间相对较小,但是存储在栈中的数据访问效率相对较高,栈由系统自动分配,速度较快。但程序员是无法控制的。
- 堆的空间相对较大,但是存储在堆中的数据的访问效率相对较低,堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便,
- 堆栈的执行效率要比堆的执行效率高,可是堆栈的资源有限,不适合处理大的逻辑复杂的对象。所以结构处理作为基类型对待的小对象,而类处理某个商业逻辑
(3)作用不同
- 类是反映现实事物的一种抽象
- 结构体的作用只是一种包含了具体不同类别数据的一种包装;类可以继承,结构体不具备类的继承多态特性
(4)初始化不同
- 类可以在声明的时候初始化
- 结构不能在声明的时候初始化(不能在结构中初始化字段),否则报错。
(5)类的实质
- 类的实质是一种数据类型,类似于int、char等基本类型,不同的是它是一种复杂的数据类型。因为它的本质是类型,而不是数据,所以不存在于内存中,不能被直接操作,只有被实例化为对象时,才会变得可操作
(6)构造函数
- 不能为结构体提供无参构造函数(类中如果提供了自定义构造函数,就不会再提供默认无参构造函数。)
(7)默认访问权限
- 结构体的成员默认private,类的成员默认public
(8)结构体和类的使用场景
- 因为结构体是值类型,自身存储在栈上,主要用于轻量级对象,用来存储简单的数据(此时结构体的成员也大部分是值类型)。
- 因为类是引用类型,可以抽象、继承等,适合存储重量级对象,拥有复杂逻辑(结构体不能被继承,也不能继承自其他,不能用abstract关键字等)
- 在表现抽象和多级别的对象层次时,类是最好的选择
- 大多数情况下该类型只是一些数据时,结构体是最佳的选择
3 C++11/14/17/20新特性
3.1 C++11
- auto类型推导
- 范围for循环
- lambda表达式
- nullptr
- 智能指针
- 右值引用和移动语义
- 线程库
3.2 C++14
- 泛型lambda
- 变量模板
- constexpr函数改进
3.3 C++17
- 结构化绑定
- if/switch语句初始化
- std::optional, std::variant, std::any
- 并行算法
3.4 C++20
- 概念(Concepts)
- 协程(Coroutines)
- 范围(Ranges)
- 模块(Modules)
4 编译与链接
4.1 编译过程
- 预处理:处理#include, #define等
- 编译:生成汇编代码
- 汇编:生成目标文件
- 链接:连接多个目标文件和库
4.2 内联函数 (用关键字 inline)
- 减少函数调用开销
- 编译器决定是否真正内联
- 类内定义的成员函数默认内联
inline int add(int a, int b) { return a + b; }
4.3 宏与内联函数的区别
- 宏是文本替换,内联函数是函数调用优化
- 宏不进行类型检查,内联函数有类型检查
- 宏可能导致意外的副作用,内联函数不会
4.4 explicit关键字
防止隐式类型转换:
class MyClass { public: explicit MyClass(int x) { /* ... */ } }; MyClass obj1 = 10; // 错误:不允许隐式转换 MyClass obj2(10); // 正确:显式构造
5 并发编程
5.1 线程
C++11引入了std::thread类:
#include <thread> void func(int x) { // 线程执行的函数 } std::thread t(func, 42); // 创建线程 t.join(); // 等待线程结束
5.2 互斥量
防止多个线程同时访问共享资源:
#include <mutex> std::mutex mtx; void func() { mtx.lock(); // 临界区 mtx.unlock(); // 或使用锁守卫 std::lock_guard<std::mutex> lock(mtx); // 临界区,离开作用域自动解锁 }
5.3 条件变量
线程间的同步机制:
#include <condition_variable> std::mutex mtx; //互斥锁,用于保护共享资源(这里是ready变量) std::condition_variable cv; //条件变量,用于线程间的通知和等待 bool ready = false; //共享标志,指示数据是否准备好 void producer() { // 准备数据 //使用lock_guard自动加锁和解锁 //在临界区内将ready设为true,表示数据已准备好 //大括号限定锁的作用域,离开时自动解锁 { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // 通知消费者 } void consumer() { std::unique_lock<std::mutex> lock(mtx); //这里的wait 下面做了详细解释过程 cv.wait(lock, [] { return ready; }); // 等待数据准备好 // 处理数据 processData(); }
(1) 进入wait
前
std::unique_lock<std::mutex> lock(mtx); // 加锁 cv.wait(lock, [] { return ready; }); // 进入wait
- 锁状态:已加锁(
mtx
被当前线程持有) - 作用:确保在检查
ready
标志前,其他线程无法修改它
(2)进入wait
时
当执行cv.wait(lock, ...)
时,内部会自动执行以下操作:
- 释放锁:原子性地释放
mtx
(相当于调用lock.unlock()
) - 阻塞线程:将当前线程放入条件变量的等待队列中
- 等待唤醒:线程暂停执行,直到其他线程调用
cv.notify_one()
或cv.notify_all()
- 锁状态:释放状态(其他线程可以获取
mtx
) - 关键点:释放锁后,生产者线程才能进入临界区修改ready并调用notify_one()如果不释放锁,生产者会被阻塞,导致死锁
(3)退出wait
时
当条件变量被唤醒后,线程会:
- 重新加锁:自动获取
mtx
(相当于调用lock.lock()
) - 检查谓词:执行
[] { return ready; }
如果返回false:再次释放锁并继续等待(回到步骤 2)如果返回true:退出wait,保持锁的持有状态
- 锁状态:已加锁(
mtx
被当前线程持有) - 作用:退出
wait
后,锁仍然保持,确保后续的数据处理代码可以安全访问共享资源,不会被其他线程干扰。
(4)退出大括号时
锁被自动释放
#嵌入式##嵌入式秋招##秋招##校招##秋招提前批,你开始投了吗#双非本,211硕。本硕均为机械工程,自学嵌入式,在校招过程中拿到小米、格力、美的、比亚迪、海信、海康、大华、江波龙等offer。八股文本质是需要大家理解,因此里面的内容一定要详细、深刻!这个专栏是我个人的学习笔记总结,是对很多面试问题进行的知识点分析,专栏保证高质量,让大家可以高效率理解与吸收里面的知识点!掌握这里面的知识,面试绝对无障碍!