C++学习篇(4)

更多精彩请关注微信公众号“爱折腾的码农”,二维码见下图。
在这里插入图片描述

本篇内容主要介绍一些面试中常问的C++知识,包括C++11特性、构造函数/析构函数是否可以定义为虚函数、类中的this指针、宏定义define与const/内联函数的区别、内存对齐等内容。内容主要从网上和《c++ primer》中查找总结的,希望大家多多关注微信公众号

图片

扩展知识1:this指针

为什么会有this指针?

      在类实例化对象时,只有非静态成员变量属于对象本身,剩余的静态成员函数、静态成员变量都不属于对象本身,而属于类,因此非静态成员函数/成员变量只有一份,多个同类型对象共用这一份,由于类生成对象时都有自己的内存空间,所以都有独一无二的地址,为了能够让函数知道是那个对象在调用它,因此引用this指针。

this指针的作用

      this指针是隐含在对象成员函数内的一种指针,当一个对象被创建后,它的每一个成员函数都会含有一个系统自动生成的隐含指针this。this指针指向被调用的成员函数所属的对象(谁调用成员函数,this指向谁),“this表示对象本身,非静态成员函数中才有this,静态成员函数/成员变量没有”,这就是static和const不能同时使用的原因。

       1.this不是一个常规变量,而是一个右值,所以不能取得this的地址(即不能&this);

        2.对非静态成员函数默认添加this指针,类型为classname *const this。

来源于网络

this指针使用

          1.当形参类型与成员变量名相同时,用this指针来区分; 

        2.为实现对象的链式引用,在类的非静态成员函数中返回对象本身,可以用return *this,*this指向对象本身


扩展知识2:宏定义

        定义: #define 标识符 字符串 

                   #define maxValue 100 

                   #deine Max(num1, num2) ((num1) > (num2) ? (num1) : (num2))   

        之所以在形参时候带括号的原因是:宏定义只是简单的字符替换,如果没有括号的话,会出现结果与期望值有误差。

    例如:#deine Add(num1, num2) num1 + num2   

    若传入参数为Add(n1, n2)*Add(n3, n4), 其实真实结果是n1 + n2 * n3 + n4

宏定义和const区别?

      1.宏替换发生在预处理阶段,只是简单的文本替换;const处理发生在编译期间;

      2.宏定义不会进行类型安全检查功能;而const 会检查数据类型;    

      3.宏定义内容不会分配内存空间,在预处理阶段替换掉;而const定义的变量只是定义的变量值不能改变,但要分配内存空间。

宏定义和函数的区别?

        1.宏定义只是简单的字符串替换,发生在预处理阶段,相当于直接插入到代码里面, 因为不存在函数调用的过程,所以执行起来更快;但函数在调用在时需要跳转到具体调用函数,在这个过程中有压栈、入栈等操作,所以执行效率相比于宏定义来说,就没那么高(注意是相对于宏定义来说的)。  

        2.宏定义只是简单字符串替换,没有返回值;但函数调用可以具有返回值;  

        3.宏定义的传入参数没有具体类型,因此不会进行类型检查;但函数参数具有类型,需要检查类型。  

        4.宏定义不需要最后加分号,如果一行不够的话,需要加入‘/’字符。

                #define Add(num1, num2)     /

                                                            (nums1 + num2)

宏定义和内联函数(inline)区别?

        1.宏定义只是简单字符串替换,发生在预处理阶段;而内联函数可以进行参数类型检查,在编译期处理,且可以具有返回值;    

        2.内联函数本身是函数,具有重载功能;    

        3.内联函数可以作为类的成员函数,使用类的保护成员和私有成员,但宏定义无法 使用类的保护和私有。

    内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求(是否内敛由编译器决定,inline只是用户建议内联); 

    一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数。 

    内联函数的好处是可以避免函数调用的开销,同时具有函数的功能,能够进行类型安全检查,通常就是将它在每个调用点上“内联地”展开。

把内联函数和constexpr放在头文件内

        和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于以上原因,内联函数和constexpr函数通常定义在头文件中

《c++ primer》

复杂函数说明

函数:( *(int*)  (*(int*)(&a)) )     

    分解:

      1.(*(int*)(&a)):从对象a的起始 地址所指向的那个字节的位置算起,取4个 字节的一个整形值。我们知道,在VC++32位 编译器下,指针和int 型一样,也是占4个 字节。

      2.( *(int*)  (*(int*)(&a)) ): 将(*(int*)(&a))取出的指针,强制转换 为 int* 的指针,然后取出该指针所指的 整形值(同样,也可以看作是一个地址)。

扩展知识3:虚函数

传值和传指针的区别

        1、函数的参数都是原数据的拷贝,因此函数内无法改变原数据; 

        2、函数中参数都是传值,其实传指针本质也是传值,只不过传的是地址;

        3、如果想要改变入参内容,则需要传入参数的地址(指针和引用都是类似的作用),通过解引用来修改其指向的内容。

网络

构造函数为什么不能是虚函数?

        1.虚函数的使用需要虚函数表和虚表指针,但是虚表指针是在对象的内存空间中的, 但是此时对象还没有构造,因此根本没有内存空间,也没有实例化,根本找不到虚表指针;    

        2、虚函数主要用在多态情况下通过基类指针指向派生类对象,在运行期间能使派生类对象函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义;    

        3.构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。

析构函数为什么是虚函数?

        C++中基类采用virtual虚析构函数是为了防止内存泄漏。在多态情况下派生类会申请内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定, 因而只会调用基类析构函数,而不会调用派生类的析构函数。因此,派生类中申请的空间就得不 到释放而产生内存泄漏。为了防止这种情况,C++基类的析构函数应采用virtual虚析 构函数。

哪些函数不能是虚函数

       1、构造函数;构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;

       2、内联函数;内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;

       3、静态函数;静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义;

    4、友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

    5、普通函数;普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数

网络

c++类构造函数初始化顺序

图片

图片

扩展知识4:auto和decltype的注意事项

auto:用于变量自动类型推导

        1、发生在编译期,不会影响程序运行效率;因为平时在编译期的话,也要右侧推导参数类型,然后判断和左侧是否匹配;    

        2、会覆盖顶层引用和修饰(const);

            比如定义一个const int i = 0;   auto m = i;   //此时m是变量,并不是常量;若想解决这个问题,可以使用const修饰,或者auto&

decltype

相比于auto可以推导变量,而decltype可以修饰变量或表达式中得到返回类型。

扩展知识5:野指针

定义:指向内存被释放的内存或没有访问权限的指针。

原因:1.指针变量定义时未初始化为NULL;

   2.指针p被free或者delete后未置为NULL;

   3.指针操作超越了作用范围。

解决办法:

         1.对指针进行初始化(包括定义时设为NULL,free或者delete后设为NULL)

  2.使用已用合法的可访问的内存地址对指针进行初始化

  3.使用智能指针(即RAII思想,将对象、资源与生命周期绑定,进入作用区域时

     自动调用构造函数,离开作用区域时自动释放内存)


扩展知识6:内存对齐

为什么要内存对齐?

    1、平台移植性好;主要因为不是所有的硬件平台都能访问任意地址上的数据(例如stm32读取数据时只能从偶地址开始读),否则抛出硬件异常。 

    2、cpu处理效率高;比如cpu读取int类型数据,若按照内存对齐来存储,cpu只需要访问一次就可以读取完4个字节,但是没有按照内存对齐的话,可能会出现访问内存两次才能读取一个完整的int类型变量。具体过程为:第一次读取4个字节,丢弃部分字节,然后第二次再读取四个字节,丢弃后面几个字节,最后拼凑出完整的int类型数据。

网络

C++内存对齐原则(来源网络)

图片

扩展知识7:C++11特性

    这在面试中很常见,这部分内容推荐看酷壳的资料,网址:https://coolshell.cn。具体搜索教程如下:

1、第一步:

图片

 2、第二步:


图片

3、第三步:

图片


扩展知识8:源码->可执行文件

    过程:源码(.cpp)->预处理(.ii)->编译(.s)->汇编(.o)->链接->可执行文件

预处理阶段

       主要是对伪指令(#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号(如注释)。经过这一系列步骤后将会生成对应的文件,c++会生成.ii文件。

编译阶段

       对预处理阶段生成的文件进行词法分析、语法分析、语义分析,同时会进行一些优化,当所有的代码都符合语法规则后汇编代码。即.s文件

汇编过程

       主要是将汇编语言代码翻译成目标机器指令的过程。目标文件中所存放的也就是与源程序等效的目标的机器语言代码,.o目标文件。之所以没有一开始就生成汇编文件是因为预处理阶段和编译阶段会对代码进行相应的优化措施。

链接阶段

       将有关的目标文件彼此相连接,主要是将一个文件中引用的符号同该符号在另外一个文件中的定义链接起来,使得所有的这些目标文件成为一个可执行文件。

图片

为什么程序的指令和数据要存放分开?

图片

关于.bss段的一点说明

.bss段( .bss节在目标文件和可执行文件中不占用文件的空间,但是它在装载时占用地址空间)

图片

猜你喜欢

转载自blog.csdn.net/weixin_43305362/article/details/112095201