C++说爱你不容易-1

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

导 师:豆芽,搜集一下豆芽领域的青年长江学者,今天发给我。

蒋 豆 芽:好的(豆芽脸上笑嘻嘻(#^.^#),嘿嘿)


蒋豆芽最近投递了嵌入式的岗位,现在C++也成了嵌入式的重要技能了,豆芽可不敢掉以轻心,赶紧再复习了一下C++的基础知识,准备应对悲鑫科技的面试。


(蒋豆芽开始进入紧张的面试中。。。。。。)


面 试 官:蒋豆芽啊,你好,请自我介绍一下。

蒋 豆 芽:阿巴阿巴阿巴。。。。。。

面 试 官:(微微点头)豆芽,请问你是怎么理解C语言和C++的?

蒋 豆 芽:emmm,C++里面有一个特别的数据类型——类。C++还有STL、模板等等。大概就是这些吧。

面 试 官:(不置可否)那一个C++程序到可执行文件有几个过程啊?

蒋 豆 芽:(肯定)四个过程。预编译、编译、汇编、链接。

面 试 官:那豆芽你说下,每个过程具体包含什么操作?

蒋 豆 芽:emmm,预编译展开宏定义,编译阶段将源代码转换成汇编语言,汇编阶段将汇编语言转换成机器码,链接阶段将目标文件链接起来形成可执行文件。

面 试 官:那你说说静态链接和动态链接的区别?

蒋 豆 芽:不太清楚。

面 试 官:(不置可否)宏你知道吗?编写宏需要注意什么?C++用什么替代宏?

蒋 豆 芽:emmm,宏是一个文本替换功能。emmm,我只知道这个。

面 试 官:行,那我们的面试就结束了,后面有消息会通知你的。


蒋豆芽当然不会受到什么消息了。蒋豆芽感觉好难啊,学了C语言,感觉C++还是不会啊。哭了呀。

img

1.1 C++与C的区别——看看你的理解是否深刻

img

蒋 豆 芽:老李,我今天面试悲鑫科技,感觉又过不了了,C++好难啊。

隔壁老李:(会心一笑)当然啦,从名字上可以看出来,C++是C的超集,C++保留了C语言原有的所有优点,与C语言兼容,并且增加了面对对象的机制。不仅如此,C++还包含容器、模板、智能指针、引用、lambda表达式等一系列复杂的技术。所以想要掌握好C++当然是比较难的,只能慢慢学习,边学边用,逐步掌握并精通这门语言。

蒋 豆 芽:(卑微)老李,你带带我吧,救救豆芽!

隔壁老李:(无可奈何)好吧。我们一起来学习一下C++吧。说到C++就脱不开C语言,面试官最喜欢问的就是C语言与C++有什么区别?这个问题就很好地考察了一个人的C++基础,C++包罗广泛,会的多才能说得多,而且还要有条理的说。

隔壁老李:区别如下:

  1. C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等。
  2. C++是面向对象的编程语言,C++引入了新的数据类型——,由此引申出了三大特性(划重点)(1)封装。(2)继承。(3)多态。豆芽,这里我们先简要介绍,详细的以后我们再展开学习。而C语言则是面向过程的编程语言。
  3. C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
  4. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。STL的一个重要特点是数据结构和算法的分离,其体现了泛型化程序设计的思想。C++的STL库相对于C语言的函数库更灵活、更通用

C++还在不停发展更新,我们需要不停温习已有知识,学习补充新知识,学好用好C++。

蒋 豆 芽:老李你太厉害了,三言两语就概括了两者的区别,你等等,我拿小本子记一下,这就是标准答案啊!

隔壁老李:(敲脑袋)哈哈,没错,我们一定要养成习惯,每个问题我们可以提前将标准答案(合理答案)整理出来,体现出全面而深入,然后背熟,这样下次应对相同问题就可以又快又准的回答出来了

但是,豆芽,你要明白,这只是应对面试的应试手段,我们还是应该好好学习其答案背后的知识,不仅要知其然,还要知其所以然。不仅要学习全面,还要学习深入,这就是我们上次提到的知识点准备的深度与广度问题,豆芽,你还记得吗?

蒋 豆 芽:(嘻嘻)知道了,你放心。

img

1.2 从代码到可执行文件的过程

img

隔壁老李:我们再说说面试官问你的第二个问题。C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接,这个豆芽你是知道的,但一旦往深入问,你又说不出个所以然了,所以豆芽认真记好哦。

隔壁老李:1、预编译:这个过程主要的处理操作如下:

(1) 将所有的#define删除,并且展开所有的宏定义

(2) 处理所有的条件预编译指令,如#if、#ifdef

(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。

(4) 过滤所有的注释,如//、/**/

(5) 添加行号和文件名标识。

这里我们插入一个小问题,头文件“”与<>什么区别

蒋 豆 芽:这个简单.

  1. 区别:

    (1)尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件

    (2)编译器预处理阶段查找头文件的路径不一样。

  2. 查找路径:

    (1)使用尖括号<>的头文件的查找路径:编译器设置的头文件路径-->系统变量。

    (2)使用双引号""的头文件的查找路径:当前头文件目录-->编译器设置的头文件路径-->系统变量。

所以要养成良好习惯,系统文件就使用尖括号<>,自定义文件就使用双引号"",提高编译效率

隔壁老李:(会心一笑)豆芽基础可以嘛,哈哈,我们继续。

2、编译:这个过程主要的处理操作如下:

(1) 词法分析:将源代码的字符序列分割成一系列的记号。

(2) 语法分析:对记号进行语法分析,产生语法树。

(3) 语义分析:判断表达式是否有意义。

(4) 代码优化:

(5) 目标代码生成:生成汇编代码

(6) 目标代码优化:

隔壁老李:3、汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。

隔壁老李:4、链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。

我们以douya.cpp和main.cpp为例,两者从预编译、编译、汇编、链接的整个过程和linux指令如下:

img

最后我们就生成了可执行目标文件douya。

蒋 豆 芽:我听说链接还分动态链接静态链接,两者有什么区别吗?

隔壁老李:是的。

静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行。

生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。

动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。

生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。

隔壁老李:这就是完整的四个过程了,豆芽,你记好了吗?

蒋 豆 芽:(挥笔疾书)嗯嗯,以后再回答面试官的问题,又快又准又有深度。

img

1.3 extern "C"

img

隔壁老李:讲到编译,我们不得不提到一个知识点,豆芽,你平时有见过以下代码吗?

#ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件,因为cpp文件默认定义了该宏),则采用C语言方式进行编译
extern "C"{
    #include"moduleA.h"
    #endif
    … //其他代码

    #ifdef __cplusplus
}
#endif

蒋 豆 芽:(激动)有有有!这个extern "C"让我感觉到很疑惑,明明是C++代码,怎么出现了“C”?

隔壁老李:这里我们就好好讲讲extern "C"。extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

蒋 豆 芽:两者编译有什么区别吗?

隔壁老李:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名

两种不同的语言,有着不同的编译规则,比如一个函数fun,可能C语言编译的时候为_fun,而C++则是__fun__

蒋 豆 芽:原来是这样啊!

隔壁老李:这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

这个功能主要用在下面的情况:

  1. C++代码调用C语言代码

    //extern示例
    //在C++程序里边声明该函数,会指示编译器这部分代码按C语言的进行编译
    extern "C" int strcmp(const char *s1, const char *s2);
  2. 在C++的头文件中使用

    //在C++程序里边声明该函数
    extern "C"{
        #include <string.h>//string.h里边包含了要调用的C函数的声明
    }
  3. 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

隔壁老李:我们举一个实例。编写的C++工程引用C函数,其中包含的三个文件的源代码如下:

C头文件 cExample.h

#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H

#ifdef __cplusplus
extern "C"{
#endif
    int add( int x, int y );
#ifdef __cplusplus
}
#endif

#endif

C实现文件 cExample.c

#include "cExample.h"
int add(int x,int y){
    return x + y;
}

C++实现文件 cppFile.cpp

#include "cExample.h"
#include <iostream>
using namespace std;
int main( int argc, char* argv[] ){
    cout << add(2, 3); << endl;
    return 0;
}

这样就实现了C++工程引用C函数。

蒋 豆 芽:懂了懂了!

img

1.4 宏——到底是什么

img

隔壁老李:接下来我们来讲面试官的第三个问题。豆芽,你说说什么是宏?

蒋 豆 芽:简单!#define命令是一个宏命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。

该命令有两种格式:一种是不带参数的宏定义,另一种是带参数的宏定义。

  1. 不带参数的宏定义的声明格式如下所示:

    #define  宏名  字符串
    
    例:#define  PI  3.1415
  2. 带参数的宏定义的声明格式如下所示:

    #define  宏名(参数列表)  宏
    
    例:#define  MAX(x,y)  ((x)>(y)?(x):(y))

隔壁老李:豆芽不错嘛!基础还可以哦。由程序编译的四个过程,知道宏是在预编译阶段被展开的。在预编译阶段是不会进行语法检查、语义分析的,宏被暴力替换,正是因为如此,如果不注意细节,宏的使用很容易出现问题。

在使用不带参数的宏命令时,当替换文本所表示的字符串是一个表达式时,需要加上括号,否则引起误解和误用。比如下面的例子,豆芽你说说最后result是多少啊?

#include <stdio.h>  
#include <stdlib.h>  
#include <string>  
#define N 9+3  
using namespace std;  
int main() {  
    int result = N * N;  
    system("Pause");  
    return 0;  
}  

蒋 豆 芽:按照代码的逻辑来看,N是12,result=N*N,结果是144嘛。

隔壁老李:你说的很有道理,这段代码就是想表达result=N*N,然而当我们将宏展开时,表达式变成了result=9+3*9+3,最后的结果却是39。而这并不是我们想表达的逻辑,我们希望的是result=(9+3)*(9+3),所以我们的宏此时就应该加上括号,正确的宏命令应该是:

#define N (9+3)

怎么样,豆芽你明白了吗?

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

隔壁老李:还不止呢。而带参数的宏定义不加括号容易引起误用。比如下面例子,豆芽你看看结果是怎么样的?

#include <stdio.h>  
#include <stdlib.h>  
#include <string>  
#define Circle(x) x*x 
using namespace std;  
int main() {  
    int result = Circle(2+2);  
    system("Pause");  
    return 0;  
}  

蒋 豆 芽:我知道,本来这是求x的平方,但是当我们用宏替换后,表达式变为result=2+2*2+2,结果是8,这不是我们想要的逻辑。所以参数一定记得加括号,正确的宏命令应该是,

#define Circle(x) ((x)*(x))

结果是result=(2+2)*(2+2)=16

隔壁老李:没错,豆芽,你已经学会举一反三了呀。哈哈!

正因为如此,在C++中为了安全性,我们就要少用宏。

不带参数的宏命令我们可以用常量const来替代,比如const int PI = 3.1415,可以起到同样的效果,而且还比宏安全,因为这条语句会在编译阶段进行语法检查。

而带参数的宏命令有点类似函数的功能,在C++中可以使用内联函数或模板(模板在后面章节会详细讲解)来替代,用inline关键字定义函数,即声明此函数为内联函数,内联函数与宏命令功能相似,是在调用函数的地方,用函数体直接替换。但是内联函数比宏命令安全,因为内联函数的替换发生在编译阶段,同样会进行语法检查、语义分析等,而宏命令发生在预编译阶段,属于暴力替换,并不安全。

img

1.5 内联函数

img

隔壁老李:因为这里提到了内联函数,我们还是讲一讲。

  1. 概念

    C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方

    如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,下面是一个实例,使用内联函数来返回两个数中的最大值:

    #include <iostream>
    
    using namespace std;
    
    inline int Max(int x, int y){
       return (x > y)? x : y;
    }
    
    // 程序的主函数
    int main( ){
    
       cout << "Max (20,10): " << Max(20,10) << endl;
       cout << "Max (0,200): " << Max(0,200) << endl;
       cout << "Max (100,1010): " << Max(100,1010) << endl;
       return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    Max (20,10): 20
    Max (0,200): 200
    Max (100,1010): 1010
  2. 为什么使用内联函数?

    函数调用是有调用开销的,执行速度要慢很多,调用函数要先保存寄存器,返回时再恢复,复制实参等等。

    如果本身函数体很简单,那么函数调用的开销将远大于函数体执行的开销。为了减少这种开销,我们才使用内联函数。

  3. 内联函数使用的条件

    • 内联是以代码复制为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:

      (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

      (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

    • 内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。

蒋 豆 芽:原来是这样,我懂了!

img

1.6 条件编译

img

隔壁老李:豆芽,还没完呢。刚才讲预编译时,提到了条件编译,我们再补充一下条件编译的知识点。一般情况下,源程序中所有行的语句都参加编译。但是有时候我们希望程序的一部分参与编译,其他部分不参与编译,这就需要用到条件编译。

条件编译格式为:

#ifdef 标识符  
    程序段1  
#else  
    程序段2  
#endif  

当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则对程序段2进行编译。其中#else部分也可以没有,即:

#ifdef 标识符  
      程序段1  
#endif  

也可以使用表达式,当表达式为真,则对程序段1进行编译,否则对程序段2进行编译。

#ifdef 表达式  
    程序段1  
#else  
    程序段2  
#endif  

隔壁老李条件编译还有个妙用,有时候,我们调试某些代码,我们希望它在调试的时候被编译,而发布版中不编译。就可以使用下面的形式:

#include<iostream>  
using namespace std;  
#define _DEBUG_  
int main(){  
    int x = 10;  
    #ifdef _DEBUG_  
        cout << "1" << endl;  
    #else  
        cout << "2" << endl;  
    #endif  
    return 0;  
}  

若是定义#define _DEBUG_,那么程序编译#ifdef部分,输出1;反之输出2。你明白了吧,豆芽。

蒋 豆 芽:嗯嗯,太厉害了吧!

img

1.7 字节对齐

img

隔壁老李:好,今天我们再讲最后一个知识点,那就是很重要的字节对齐的概念,重要到几乎每场面试官必问的问题,因此我们一定要搞懂。

  1. 什么是内存对齐?

    那么什么是字节对齐?在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同

    我们在前次讲过不同数据类型所占用的内存大小,我们再复习下。

    在windows 32位下测试,

    img

    接着是windows 64位:

    img

    从两个测试中,我们记住两点:(1)各数据类型占用字节大小无论是win32位还是win64位下,都是一样的,所以我们不要被这个条件迷惑。(2)指针类型占用字节大小32位还是64位下是不一样的。注:这里要单独说一下long,比较特殊,只需要记住long在linux 64位下是8字节。

    为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐

    比如在32位cpu下,假设一个整型变量的地址为0x00000004(为4的倍数),那它就是自然对齐的,而如果其地址为0x00000002(非4的倍数)则是非对齐的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

蒋 豆 芽:那老李,我们为什么要字节对齐呢?

隔壁老李:我们继续讲。

  1. 为什么要字节对齐?

    需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。

    而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。

    各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。

隔壁老李:(战术喝水)哎,要讲清楚一个知识点真是不容易。不知不觉讲了这么多东西了。

蒋 豆 芽:(满头大汗)是啊,老李,你慢点讲,我小笔记都记不过来了。

隔壁老李:好了,接下来我们就要看下例子了。豆芽你看看下面例子result的结果是多少?

union example {  
    int a[5];  
    char b;  
    double c;  
};  
int result = sizeof(example);  

蒋 豆 芽:union中变量共用内存,而且以最长的为准,所以最长的是int a[5](5*4=20Byte),那么最终result=20

隔壁老李:不,这是不对的。哎呀,豆芽,忘了告诉你了,这里需要满足一个原则:占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。

如果以最长20字节为准,内部double占8字节,这段内存的地址0x00000014(十六进制,对应十进制20)并不是double的整数倍,只有当最小为0x00000018(十六进制,对应十进制24)时可以满足整除double(8Byte)同时又可以容纳int a[5]的大小,所以正确的结果应该是result=24。

隔壁老李:我们又看看struct的计算方法。豆芽你来说说。

struct example {  
    int a[5];  
    char b;  
    double c;  
}test_struct;
int result = sizeof(test_struct);  

蒋 豆 芽:这次我懂了。如下图,如果我们不考虑字节对齐,那么char b后面的内存地址是0x0015(十六进制,对应十进制21),也即double c的起始地址是0x0015,但0x0015(对应十进制21)不是double c(8Byte)的整数倍,所以需要字节对齐。那么此时满足是double(8Byte)的整数倍的最小整数是0x0018(十六进制,对应十进制24),说明此时char b对齐int扩充了三个字节,这样double c的起始地址从0x0018(十六进制,对应十进制24)开始,加上double c占8个字节。所以最后的结果是result=32

不考虑字节对齐时的内存分布:(为直观一点,采用的十进制)

img

考虑字节对齐时的内存分布:(为直观一点,采用的十进制)

img

隔壁老李:bingo!豆芽,你已经学会了,是否扩充字节,取决于变量的起始地址的特性。但是还不够,我们来举一反三。看看下面的例子。

struct example {  
    char b;  
    double c;  
    int a;  
}test_struct;  
int result = sizeof(test_struct);  

蒋 豆 芽:简单!首先char b要扩充7个字节,此时double c的内存起始地址是0x0008,满足是double(8Byte)的整数倍,然后加上double 8个字节后共16个字节,此时int a的内存起始地址是0x0010(十六进制,对应十进制16),满足是int(4Byte)的整数倍,最后加上一个int 4个字节,共20个字节。

而字节对齐除了内存起始地址要是数据类型的整数倍以外,还要满足一个条件,那就是占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍,所以20不是double(8Byte)的整数倍,我们还要扩充四个字节,最后的结果是result=24

隔壁老李:其实不难理解的,哈哈!

蒋 豆 芽:好的,多谢你,老李!

隔壁老李:(笑容邪魅)等等,还没完呢!你以为就这样了?字节对齐还涉及到强制对齐的概念。

蒋 豆 芽:晕,还有啊?那又是什么?

隔壁老李:别急,听我慢慢讲来。比如我想让结构体按我的想法来对齐,即自定义对齐,我们就可以这样做,如下:

#include <stdio.h>  
#include <stdlib.h>  
#include <iostream>  
#pragma pack(4)  
using namespace std;  
struct example{  
    char a;  
    double b;  
    int c;  
}test_struct;  
int main() {  
    int result = sizeof(test_struct);  
    cout << result << endl;  
    system("Pause");  
    return 0;  
}  

豆芽你注意了没,我们多了一句#pragma pack(4),我们直接看看结果吧。

img

蒋 豆 芽:(困惑)咦?这又是怎么算出来的?#pragma pack(4)代表什么意思?

隔壁老李:#pragma pack(n)表示,我们结构体成员所占用内存的起始地址需要是n的整数倍。这里n是4。所以就需要填充一定的字节数,规定:

对齐字节数 = min(成员起始地址应是n的倍数时填充的字节数, 自然对齐时填充的字节数)

后面我们会具体讲解这句话的意思。

我们结合例子详细说明。(为直观一点,采用的十进制)

img

首先为char a分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),0可以整除4。char a大小为1个字节,这时其偏移量为1,即0x0001,若接着开始为double b分配空间,那么double b的起始地址就是0x0001,但0x0001不能整除4,所以需要补足3个字节,这样使起始地址满足为n=4的倍数(而如果按自然对齐的原则,需补足7个字节,可以使起始地址满足为double的倍数,但规则是取小值,我们补足3个字节就可以了),double b占用8个字节,此时int c的起始地址为0x000C(十六进制,对应十进制12)。接着为int c分配空间,起始地址满足为4的倍数,int c占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,同时满足占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。

隔壁老李:怎么样,豆芽你明白了吗?你看看下面的例子,结果是多少呢?继续看例子:

#pragma pack(4)  
using namespace std;  
struct example{  
    char a;  
    double b;  
    char c;  
}test_struct;  

蒋 豆 芽:同样是16!char a是一个字节,然后需要补齐3个字节,这样内存起始地址为0x0004,为4的整数倍,然后char c的起始地址是0x000C(十六进制,对应十进制12),满足是4的整数倍,一共13个字节,但是总体字节也需要是结构体中占用最大内存空间的类型的整数倍,所以再填充三个字节,最后就是16个字节了。

隔壁老李:没错,继续看例子:

#pragma pack(4)  
using namespace std;  
struct example{  
    int a;  
    char b;  
    short int c;  
    char d;  
}test_struct;  

我们来讲解下,我们首先为int a分配空间,四个字节,char b的起始地址为0x0004,满足是4的倍数,然后看到char bchar b后面是short int c如果按自然对齐,此时只需要填充一个字节,如果按4的倍数来对齐,需要填充三个字节,那么规定是取小值,所以正确的操作是填充一个字节这里要特别注意)。然后short int c占用两个字节,char d占用一个字节,最后加起来是9个字节。同时要满足占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。所以需要填充三个字节,最终就是12个字节了。

隔壁老李:继续看例子:

#pragma pack(8)  
using namespace std;  
struct example{  
    int a;  
    char b;  
    short int c;  
    int d;  
}test_struct;  

我们按照刚才的思路来,首先为int a分配空间,占四个字节(如果按照自然对齐,那么不需要再填充字节,如果按照8字节对齐,那么还需要填充四个字节,按规则取小值,所以不填充),那么char b的起始地址就是0x0004,接着是char b,同理char b只填充一个字节,short int c不需要填充字节,最后int d加起来就是12个字节了。同时满足占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。占用空间就是12字节。你明白了吗?

蒋 豆 芽:原来是这样啊,不难嘛。

隔壁老李:我们总结一下,我们可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来指定我们的对齐系数,其中的n就是我们要指定的“对齐系数”。

规则一:对齐字节数 = min(成员起始地址应是n的倍数时填充的字节数, 自然对齐时填充的字节数)。

规则二:同时满足占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。(当与规则一冲突时,优先考虑规则一

蒋 豆 芽:好,没问题!那老李,如果我不想对齐呢?应该怎么办?

隔壁老李:有办法,看下面的例子:

#include <stdio.h>  
#include <stdlib.h>  
#include <iostream>  
using namespace std;  
struct {  
    char b;  
    double c;  
    int a;  
}__attribute__((packed)) test_struct;  
int main() {  
    int result = sizeof(test_struct);  
    cout << result << endl;  
    return 0;  
}  

答案是13,说明这个时候取消了字节对齐。我们只要定义结构体时加上attribute((packed))就可以了。怎么样,你学会了吗?

蒋 豆 芽:(嘻嘻)是啊,学废了。

隔壁老李:(敲脑袋)

蒋 豆 芽:嘻嘻!多谢你,老李!


导 师:豆芽,豆芽领域的青年长江学者搜集得如何了,发我看看啊。

蒋 豆 芽:(嘤嘤嘤)糟了呀,忘了。


豆芽加班ing。。。。。。

img

故事完

img

参考文献

[1] 徐晓鑫. 后台开发:核心技术与应用实践[M].北京:机械工业出版社,2016.

[2] 蒋豆芽. C++后端面试知识点大全(春秋招必备)[M]. 长沙:豆芽出版社,2020.

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

全部评论
豆芽,本章1.7里面,64位下long占用的字节大小应该是8,图片里是4,是不是贴错图片了。
1
送花
回复
分享
发布于 2021-04-26 09:36
using namespace std;  struct example{      long a;      char b[15];      char c[15];      char d[20];  }test_struct; 我们来补充例子,更好的理解字节对齐。字节自然对齐就两个规则,一是当前变量的起始地址是该变量类型的整数倍,二是整个结构体的大小需要是最大类型变量的倍数。我们来分析一下,a占四个字节,那么b的起始地址为0004,能整除char(占一个字节)吗?可以。同理,c的起始地址为0019,能整除char吗?可以,d的起始地址为0034,可以整除char吗?可以,加起来54,而54能整除其中占类型最大的变量long a(占4个字节)吗?不能,所以需要补齐两个字节,最终大小为56.
3
送花
回复
分享
发布于 2021-08-18 14:10
秋招专场
校招火热招聘中
官网直投
还有位域呢,我做网络协议经常用的,各种位字段
点赞
送花
回复
分享
发布于 2021-04-12 18:12
这时已经为所有成员变量分配了空间,共分配了16个字节,同时满足占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。 文中的这一句话是不是错了,加了#pragma pack后这调规则应该不能加了吧, #pragma  pack(1) struct test {     char a;     int  b; }; 如果是这个代码,结果为5,并没有按照最大的类型int 4字节的倍数进行对齐
点赞
送花
回复
分享
发布于 2021-06-30 11:04
豆芽师兄,有个问题请教一下,对于内联函数可以和静态链接进行等同理解吗?都是把用到的函数复制到了要用的地方
点赞
送花
回复
分享
发布于 2021-08-24 13:58
想问下在字节补齐的时候(规则二)是不是也应该满足min(结构体中占用最大内存空间的类型,自然对齐时填充的字节数)呢?看了好久字节补齐,总感觉有点点小问题
点赞
送花
回复
分享
发布于 2023-02-23 16:01 江苏
这里的字节对齐有些问题
点赞
送花
回复
分享
发布于 2023-03-26 23:24 浙江

相关推荐

点赞 3 评论
分享
牛客网
牛客企业服务