C++说爱你不容易-3

C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)


本章讲解点

  • 1.1 C++与C的区别——看看你的理解是否深刻
  • 1.2 从代码到可执行文件的过程
  • 1.3 extern "C"
  • 1.4 宏——到底是什么
  • 1.5 内联函数
  • 1.6 条件编译
  • 1.7 字节对齐详解
  • 1.8 Const——今天必须把它搞懂
  • 1.9 Static作用
  • 1.10 volatile和mutable
  • 1.11 volatile在嵌入式里的应用
  • 1.12 原子操作
  • 1.13 指针与引用的区别
  • 1.14 右值引用
  • 1.15 面向对象的编程思想
  • 1.16 类
  • 1.17 类的成员
  • 1.18 友元函数
  • 1.19 初始化列表
  • 1.20 this指针
  • 1.21 继承
  • 1.22 多态
  • 1.23 虚函数与重写
  • 1.24 虚构造函数与虚析构函数
  • 1.25 函数重载
  • 1.26 操作符重载
  • 1.27 迭代器与指针
  • 1.28 模板
  • 1.29 C++智能指针
  • 1.30 四种cast转换
  • 1.31 Lambda
  • 1.32 function和bind

受众:本教程适合于C/C++已经入门的学生或人士,有一定的编程基础。

本教程适合于互联网嵌入式软件求职的学生或人士。

img

故事背景

img

蒋 豆 芽:小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。

隔壁老李:大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。

导 师:蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。

img

故事引入

img

蒋 豆 芽:(疑惑)老李,你怎么一直在我身边晃啊。

隔壁老李:(嘻嘻)没什么。你在睡午觉啊,那你好好睡。

蒋 豆 芽:老李,你快说,你神神秘秘的,我也睡不好了。

隔壁老李:豆芽,我们上一次的内容其实没有讲完,我们接着讲。

蒋 豆 芽:(晕)我就知道,害,谁叫我在找工作呢?哪里配拥有午睡?

img

1.10 volatile和mutable

img

隔壁老李:没事啦,豆芽,加油!先讲一个面试常问的问题——volatile和mutable

mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。mutable在类中只能够修饰非静态数据成员。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器每次会从内存里重新读取这个变量的值,而不是从寄存器里读取。特别是多线程编程中,变量的值在内存中可能已经被修改,而编译器优化优先从寄存器里读值,读取的并不是最新值。这就是volatile的作用了。

隔壁老李:volatile和mutable知识点比较简单,豆芽你记下来没有?这就是标准答案啊。还不赶紧请我吃饭?

蒋 豆 芽:好好好,老李,一定请你吃大餐。(请你吃个鬼哦)

img

1.11 volatile在嵌入式的应用

img

隔壁老李:不要着急,我们重点介绍下volatile在嵌入式的应用。

Volatile主要有三个应用场景:

(1)外围设备的特殊功能寄存器。

(2)在中断服务函数中修改全局变量。

(3)在多线程中修改全局变量。

隔壁老李:(1)外围设备的特殊功能寄存器。在嵌入式偏硬件方面的程序,我们经常要控制一些外围硬件设备,就拿I/O端口来说,我们会去操作映射到对应IO端口的寄存器。假设某一个寄存器的地址为0x1234,在C语言中,我们可以定义一个指针pRegister指向这个地址:

unsigned int *pRegister = (unsigned int *)0x1234;

在实际运用中(例如uart、ADC等等),我们经常会去判断一个寄存器中的值(或者寄存器中某一位)为‘0’还是‘1’。例如下面程序:

unsigned int *pRegister = (unsigned int *)0x1234;  

//wait  
while(*pRegister == 0){
    //不改变*pRegister的值
}  

//Code...  

我们的代码目的是不断的判断*pRegister的值是否为‘0’。如果*pRegister的值(值由硬件改变)在中途变为‘1’,则跳出死循环。

因为上面的循环中,*pRegister的值并没有发生改变,因为我们的编译器会对上述代码进行优化,如下:

unsigned int *pRegister = (unsigned int *)0x1234;  

//wait  
if (*pRegister == 0){
    while(1){
        //不改变*pRegister的值
    }  
}

//Code... 

经过优化后,在上面的循环中,*pRegister的值不会发生改变,所以循环中就不再判断*pRegister的值了,运行效率提升。但是pRegister指向的特殊功能寄存器,其值是由硬件改变的,而软件却不再判断*pRegister的值了,那么就进入死循环了,即使*pRegister的值发生了改变,软件也察觉不到了。我们来查看下编译结果(反汇编):

30:     b     30  

可以看到,编译器好心办坏事,经过优化,缺少了cmp指令,软件不再判断*pRegister的值。这样自然将出现bug

那么改进就是加上volatile关键字

volatile unsigned int *pRegister = (unsigned int *)0x1234;

再次查看编译结果:

28:   ldr   r3, [r2, #564]  
2c:   cmp   r3, #0  
30:   bne   28  

可以看到,加上volatile关键字后,编译器不再优化,有了cmp指令,软件持续判断*pRegister的值,当*pRegister的值发生了改变,软件自然就能及时作出反应。

蒋 豆 芽:(恍然大悟)原来是这样。

隔壁老李:(嘿嘿)(2)在中断服务函数中修改全局变量也是容易出问题的。我们来看一个例子:

static int flag = 1;  

void main(void){  

   while (flag == 1){  
       //code ...  
   }  
   //code ...  
}  

void do_interrupt(void){  //中断服务程序
   //code...  
   flag = 0;  
}  

上面的代码简单,只要flag的值为‘1’,就会一直运行循环里面的程序。刚才我们已经讲了,因为flag值在循环里没有改变,编译器就将对其优化。如下:

static int flag = 1;  

void main(void){  

   if (flag == 1){  
       while (1){  
           //code ...   
       }  
   }  
   //code ...   
}  

void do_interrupt(void){  
   //code...  
   flag = 0;  
}  

我们来查看下编译结果(反汇编):

10:  eafffffe    b   10  

发现问题了吗?同样少了cmp指令,而当发生中断时,flag值发生改变,但main函数中却察觉不到flag的改变,就陷入了死循环,明白了吧,改进同理,加上volatile关键字。

volatile static int flag = 1;  

隔壁老李:(3)我们再来看看在多线程中修改全局变量。如下:

int  cnt;  

void task1(void){  
    cnt = 0;  
    while (cnt == 0) {  
        sleep(1);  
    }  
}  

void task2(void){  
    cnt++;  
    sleep(10);  
}  

豆芽,这段代码会出现什么问题应该不用我多解释了吧,同理。解决办法依然是加上volatile关键字。

蒋 豆 芽:(晕晕沉沉)嗯嗯。。。嗯

隔壁老李:(敲脑袋)醒醒,豆芽,我们还要接着讲。

img

1.12 原子操作

img

隔壁老李:好了,volatile关键字的应用场景我们就讲清楚了。诶,豆芽,说到多线程,这里可以使用volatile关键字,其实还有一种解决方法,那就是原子操作

原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

蒋 豆 芽:感觉这很类似互斥锁啊。

隔壁老李:没错,有点类似,但是原子操作比锁效率更高,这是因为原子操作更加接近底层,它的实现原理是基于总线加锁缓存加锁的方式。因为作为延伸的知识点,它的实现原理我们就不再详细讲解了,豆芽你有兴趣的话可以自己去学习一下。

我们着重看看原子操作的应用,我们以一个例子来说明:

#include <atomic>   
#include <iostream>  
#include <time.h>  
#include <thread>  
#include <vector>  
using namespace std;  

// 全局的结果数据   
long total = 0;  

void func() {  
    for (int i = 0; i < 100000; ++i) {  
        // 对全局数据进行无锁访问   
        ++total;  
    }  
}  

int main() {  
    // 计时开始  
    clock_t start = clock();  
    // 创建100个线程  
    vector<thread *> vec(100);  
    for (int i = 0; i < 100; ++i) {  
        vec[i] = new thread(func);  
    }  

    for (int i = 0; i < 100; ++i) {  
        vec[i]->join();  
    }  
    // 计时结束  
    clock_t finish = clock();  
    // 输出结果  
    cout << "result:" << total << endl;  
    cout << "duration:" << finish - start << "ms" << endl;  

    for (int i = 0; i < 100

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>

全部评论
左右键当场去世😥😥
1 回复 分享
发布于 2021-04-14 15:57
emplace_back()方法是C++11新增的方法,它可以在容器的尾部插入一个新元素,而无需创建一个临时对象,并进行拷贝构造和移动构造。相反,它直接在容器管理的内存区域中构造新的对象,并返回一个指向该元素的迭代器,这样就避免了拷贝元素的成本。 对于自定义的数据类型,需要定义一个适当的构造函数来接收传递给emplace_back()方法的参数,然后在容器内部使用这个构造函数构造新的对象。这样可以避免昂贵的复制和移动操作,从而提高代码的性能。
3 回复 分享
发布于 2023-05-02 18:43 广东
针对左右值部分,“左值就是可以出现在等式左边的值,右值就是可以出现在等式右边的值”,这句话感觉没有表达到位。 左值:表达式结束后仍然存在,可以对其进行取地址,取值。 右值:表达式结束后不存在,无法对其进行取地址,无法取值。
1 回复 分享
发布于 2021-08-19 17:15
果然晕了(=_=)
点赞 回复 分享
发布于 2021-04-12 23:13
😭逐渐懵逼,有点看不懂老,我好菜哦
点赞 回复 分享
发布于 2021-04-13 21:26
刚把爹!
点赞 回复 分享
发布于 2021-05-04 14:51
1.10 中  “编译器每次会从内存里重新读取这个变量的值,而不是从寄存器里读取”表达得不准确吧,应该是CPU每次会从内存里重新读取这个变量的值,volatile关键字避免了编译器将代码被优化成CPU直接从寄存器中读取。
点赞 回复 分享
发布于 2022-03-14 17:14
为什么mutable在类中只能够修饰非静态数据成员?
点赞 回复 分享
发布于 2022-05-05 15:28
读研期间,能遇到一个老李这样的朋友真实太幸运了
点赞 回复 分享
发布于 2023-03-21 21:29 甘肃
可以再多一个移动赋值函数,和移动构造函数 成对
点赞 回复 分享
发布于 2023-08-31 22:35 浙江

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客企业服务