【C++】面试基础准备(00)

1、extern关键字

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

也就是说,extern有两个作用:

当它与"C"一起连用时,如:extern "C" void fun(int a, int b);,则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的,这要看编译器的"脾气"了(不同的编译器采用的方法不一样)。为什么这么做呢,因为C++支持函数的重载;

当extern不与"C"在一起修饰变量或函数时,如在头文件中:extern int g_Int; ,它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块和其他模块中使用,记住它是一个声明不是定义!也就是说B模块要是引用模块A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

在一个文件中定义的全局变量默认为外部的,即关键字extern可以省略,但如果在其他的文件中使用这个文件定义的全局变量,则必须在使用前用extern进行外部声明。

也就是说,用extern修饰只是表示声明变量,不表示定义变量。如果不用extern修饰的全局变量,就默认声明加定义了。

 

2、static关键字

static关键字的作用有:

  • static修饰局部变量:被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。局部静态变量使得该变量在退出函数后,不会被销毁,因此再次调用该函数时,该变量的值与上次退出函数时值相同;
  • static修饰全局变量:全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限定该全局变量在本编译单元内,一个编译单元就是指一个cpp和它包含的头文件;
  • static修饰普通函数:限定该函数只能在本编译单元内被调用;
  • static修饰成员变量:所有的对象都只维持同一个实例,可以采用static可以实现不同对象之间数据共享;
  • static修饰成员函数:类中的static成员函数属于整个类所拥有,这个函数不接收this指针,所以只能访问类的static成员变量。

static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量;其次,static修饰的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了;最后,static修饰全局变量的作用域 只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它。

参考文章:extern与static用法

 

3、volatile关键字

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

  • 存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义;
  • 在中断函数中的交互变量,一定要加上volatile关键字修饰,这样每次读取非自动存储类型的值(全局变量,静态变量)都是在其内存地址中读取的,确保是我们想要的数据;
  • 多任务环境下各任务间共享的标志应该加volatile。

一个参数既可以是const还可以是volatile吗?

可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。软件不能改变,并不意味着我硬件不能改变你的值,这就是单片机中的应用。

 

4、const关键字

简单地说,const意味着常数。

  • const定义的变量,它的值不能被改变,在整个作用域中都保持固定;
  • 同宏定义一样,可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改;
  • 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。const是通过编译器在编译的时候执行检查来确保实现的。

const与指针:修饰指针、修饰指针指向的对象;

const与函数:

  • 修饰函数形参,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制;
  • 修饰返回值,表明函数的返回值是一个常量;
  • 修饰类成员函数(放在函数的参数表之后,函数体之前),表示该函数的this指针是一个常量,不能修改该对象的数据成员。

 

5、new与malloc区别

  • new分配内存按照数据类型进行分配(不需要指定大小),返回的是指定对象的指针。malloc分配内存按照大小分配,返回的是void*;
  • new不仅分配一段内存,而且会调用构造函数,但是malloc则不会;
  • new是一个操作符可以重载,malloc是一个库函数;
  • new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会;
  • new如果分配失败了会抛出bad_alloc的异常,而malloc失败了会返回NULL;
  • new[]与delete[]来专门处理数组类型,而malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事;
  • new和delete可以被重载,但是malloc/free并不允许重载;
  • 使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充,new没有这样直观的配套设施来扩充内存。

注:realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

参考文章:细说new与malloc的10点区别

接下来讲解一下,new/delete的内部实现原理:

new 运算符的内部实现分为两步:

  • 内存分配:调用相应的 operator new(size_t) 函数,动态分配内存。如果 operator new(size_t) 不能成功获得内存,则调用 new_handler() 函数用于处理new失败问题。如果没有设置 new_handler() 函数或者 new_handler() 未能分配足够内存,则抛出 std::bad_alloc 异常。“new运算符”所调用的 operator new(size_t) 函数,按照C++的名字查找规则,首先做依赖于实参的名字查找(即ADL规则),在要申请内存的数据类型T的内部(成员函数)、数据类型T定义处的命名空间查找;如果没有查找到,则直接调用全局的 ::operator new(size_t) 函数;
  • 构造函数:在分配到的动态内存块上 初始化 相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*, void*) 函数释放已经分配到的内存。

delete 运算符的内部实现分为两步:

  • 析构函数:调用相应类型的析构函数,处理类内部可能涉及的资源释放;
  • 内存释放:调用相应的 operator delete(void *) 函数。调用顺序参考上述 operator new(size_t) 函数(ADL规则)。

参考文章:new/delete 详解

 

6、运算符重载

运算符重载的规则:

  • 不能重载的运算符有:::(作用域运算符)、.(成员运算符)、.*(指针成员运算符)、?:(三目运算符)、sizeof()。
  • 只能使用成员函数重载的运算符有:=、()、[]、->、new、delete;(类型转换函数也是)
  • 单目运算符最好重载为成员函数;
  • 对于复合的赋值运算符如+=、-=、*=、/=、&=、!=、~=、%=、>>=、<<=建议重载为成员函数;
  • 对于其它运算符,建议重载为友元函数;
  • 类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。 

 

7、C++多态性与虚函数表

多态可分为静态多态和动态多态,具体的分类情况如下:

静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的? 

  • 静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早早绑定的,静态多态也往往被叫做静态联编。 静态多态往往通过函数重载和模版来实现;
  • 动态多态则是指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定,这就属于晚绑定,动态多态也往往被叫做动态联编。动态多态通过虚函数和继承关系来实现。

补充一下重载、重写(覆盖)、重定义(隐藏)的区别:

动态多态实现有几个条件: 

  • 虚函数; 
  • 一个基类的指针或引用指向派生类的对象。

基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。 

每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。也就是说,编译器为每一个类维护一个虚函数表,每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。 

虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。

虚函数是通过一张虚函数表(Virtual Table)来实现的。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。在类继承的时候,虚函数表直接从基类也继承过来;如果派生类覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换。在运行时,动态绑定的调用过程是这样的,首先,基类指针被赋值为派生类对象的地址,那么就可以找到指向这个类的虚函数的隐含指针,然后通过该虚函数的名字就可以在这个虚函数表中找到对应的虚函数的地址。然后进行调用就可以了。

参考文章:C++虚函数表解析

 

8、构造函数、析构函数

构造函数能定义成虚函数么?

不能,虚函数的作用在于通过父类的指针或引用来调用子类的那个成员函数,而构造函数是在创建对象时自己主动调用的,不可能说调用子类的构造函数来创建自己(没有意义)。另外,虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

基类析构函数要定义成虚函数么?

要,为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为虚函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。

构造函数中能调用虚函数么?

不能,构造函数是先构造父亲类再构造子类,如果父类构造函数有虚函数,会导致调用子类还没构造的内容。

析构函数中能调用虚函数么?

不能,析构函数是先析构子类再析构父类,如果父类析构函数有虚函数,会导致调用子类的已经析构的内容。

构造函数能抛出异常吗?

不能, 构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。

析构函数能抛出异常吗?

不能,C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。也就是说:

如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题;

通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

 

9、指针和引用的区别

  • 指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问;
  • 指针是一个实体,需要分配内存空间,引用只是变量的别名,不需要分配内存空间;
  • 指针可以改变地址,从而改变所指的对象,而引用必须从一而终;
  • 引用在定义的时候必须初始化,而指针则不需要;
  • 有多级指针,但是没有多级引用,只能有一级引用;
  • 指针有指向常量的指针和指针常量,而引用没有常量引用;
  • 指针和引用的自增运算结果不一样(指针是指向下一个空间,引用时引用的变量值加1);
  • sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小;
  • 指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。

关于常量引用,这是不存在的:

int x = 100;

int& _x = x;

这里还有一个问题,就是int& x = 100;如果写成这样,编译器会报错,对常数的引用必须加const修饰,const int& x = 100(这个const表示的是,引用x是常量100的引用,而不是表示引用x是常量引用)。

引用的底层:

  • 定义一个引用就是定义一个指针,这个指针保存引用对象的地址,且指针类型为const,不可以再指向其他对象; 
  • 每次对引用变量的使用,实际都伴随着解引用,知识我们看不到符号*;
  • 从汇编后的代码看,引用和指针并没有区别,引用也是占用内存空间的。

 

10、指针与数组千丝万缕的联系

一个一维int数组的数组名实际上是一个int* 类型;

一个二维int数组的数组名实际上是一个int (* p)[n](数组指针,行指针);

数组名做参数会退化为指针,除了sizeof()。

 

11、智能指针

使用智能指针的原因至少有以下三点:

  • 智能指针能够帮助我们处理资源泄露问题;
  • 它也能够帮我们处理空悬指针的问题;
  • 它还能够帮我们处理比较隐晦的由异常造成的资源泄露。

自C++11起,C++标准提供两大类型的智能指针:

  • Class shared_ptr实现共享式拥有(shared ownership)概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用(reference)被销毁”时候释放。为了在结构复杂的情境中执行上述工作,标准库提供了weak_ptr、bad_weak_ptr和enable_shared_from_this等辅助类;
  • Class unique_ptr实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(resourece leak)——例如“以new创建对象后因为发生异常而忘记调用delete”——特别有用。

注:C++98中的Class auto_ptr在C++11中已不再建议使用。

几乎每一个有分量的程序都需要“在相同时间的多处地点处理或使用对象”的能力。为此,我们必须在程序的多个地点指向(refer to)同一对象。但往往必须确保当“指向对象”的最末一个引用被删除时该对象本身也被删除,毕竟对象被删除时析构函数可以要求某些操作,例如释放内存或归还资源等等。Class shared_ptr提供了“当对象再也不被使用时就被清理”共享式拥有语义。它使用计数机制来表明资源被几个指针共享。也就是说,多个shared_ptr可以共享(或说拥有)同一对象。对象的最末一个拥有者有责任销毁对象,并清理与该对象相关的所有资源(浅拷贝)。release()时,当前指针会释放资源所有权,计数减一。

unique_ptr是C++标准库自C++11起开始提供的类型。它是一种在异常发生时可帮助避免资源泄露的智能指针。一般而言,这个智能指针实现了独占式拥有概念,意味着它可确保一个对象和其相应资源同一时间只被一个指针拥有。一旦拥有者被销毁或变成空,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源也会被释放。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

参考文章:C++智能指针简单剖析C++智能指针及其简单实现c++ 智能指针用法详解c++11-智能指针和引用计数

 

12、shared_ptr什么时候修改引用计数?

shared_ptr修改引用指针的时机:

  • 构造函数中计数初始化为1;
  • 拷贝构造函数中计数值加1;
  • 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
  • 析构函数中引用计数减一;
  • 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。

 

13、explicit关键字

C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。也就是说,explicit修饰的构造函数必须显示使用。

 

14、C++四种类型转换

C++四种类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast。

为什么不使用C的强制转换?

C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

static_cast:可以实现C++中内置基本数据类型之间的相互转换。它主要有如下几种用法:

用于类层次结构中基类和派生类之间指针或引用的转换,进行上行转换(把派生类的指针或引用转换成基类表示)是安全的,进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的;

  • 用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证;
  • 把空指针转换成目标类型的空指针;
  • 把任何类型的表达式转换为void类型。

注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。

如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。

const_cast:可以强制去掉const这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。通常用于去除const类型返回值的常量性。

为什么要设定为去除指向常数对象的指针或引用呢?

因为,如果可以将变量的常量性去除,那么就和const的常量意思相悖了,这是很可怕的。而如果是指向该常量的指针,尽管该指针指向的是常量,但是去除它的常量性,也是可以理解的。但是,这样就出现了可能指向同一块内存地址的指针和这块内存地址的常量值不同的情况(解释可以看下面的参考文献)。

reinterpret_cast:主要有三种强制转换用途,改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。也就是说,它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。在使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝,因此在使用过程中需要特别谨慎!

dynamic_cast:其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。

  • 不能用于内置的基本数据类型的强制转换;
  • dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL;
  • 使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过;
  • 在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

向上转换,即为子类指针指向父类指针(一般不会出问题);

向下转换,即将父类指针转化子类指针。向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

为什么dynamic_cast基类中必须要有虚函数?

dynamic_cast依赖于RTTI信息。RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。许多编译器都是通过vtable找到对象的RTTI信息的,这也就意味着,如果基类没有虚方法,也就无法判断一个基类指针变量所指对象的真实类型。

dynamic_cast 主要用于执行“安全的向下转型(safe downcasting)”,要实现dynamic_cast,编译器会在每个含有虚函数的类的虚函数表的前四个字节存放一个指向_RTTICompleteObjectLocator结构的指针,当然还要额外空间存放_RTTICompleteObjectLocator及其相关结构的数据。

参考文章:C++ 四种强制类型转换C++ dynamic_cast实现原理C++中的RTTI机制解析

 

15、虚函数和纯虚函数的区别:

虚函数和纯虚函数的区别:

  • 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,抽象类不能直接实例化,否则会出现抽象类不能实例化对象的错误,只有被继承并且重写后才能使用。而只含有虚函数的类不能被称为抽象类;
  • 虚函数可以被直接使用,也可以被子类重载以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类只有声明而没有定义;
  • 虚函数和纯虚函数都可以在子类中被重写,以多态的形式被调用;
  • 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期静态绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样;
  • 虚函数必须实现,如果不实现,编译器将报错;
  • 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖;
  • 虚函数主要强调继承。继承了基类的接口,并且继承了基类的一些定义实现,当然也可以自己去定义,增加类的多态性。而纯函数主要强调的是接口的统一性和规范性,主要用于通讯协议中,具体的实现由子类完成。

 

16、重载、覆盖与隐藏

重载、覆盖与隐藏 的区别:

  • 重载(overload):函数名相同 、函数参数不同、 必须位于同一个域(类)中;
  • 覆盖(override):函数名相同 、函数参数相同、 分别位于派生类和基类中、virtual(虚函数);
  • 隐藏(hide):函数名相同、 函数参数相同、 分别位于派生类和基类中、非virtual(即跟覆盖的区别是基类中函数是否为虚函数);函数名相同、 函数参数不同、 分别位于派生类和基类中(即与重载的区别是两个函数是否在同一个域(类)中)。

覆盖达到的效果:

  • 在子类中重写了父类的虚函数,那么子类对象调用该重写函数,调用到的是子类内部重写的虚函数,而并不是从父类继承下来的虚函数;
  • 在子类中重写了父类的虚函数,如果用一个父类的指针(或引用)指向(或引用)子类对象,那么这个父类的指针或用引用调用该重写的虚函数,调用的是子类的虚函数;相反,如果用一个父类的指针(或引用)指向(或引用)父类的对象,那么这个父类的指针或用引用调用该重写的虚函数,调用的是父类的虚函数。

隐藏,即:派生类中函数隐藏(屏蔽)了基类中的同名函数。

关于隐藏的理解,在调用一个类的成员函数时,编译器会沿着类的继承链逐级向上查找函数的定义,如果找到了则停止查找;所以如果一个派生类和一个基类都有一个同名函数(不论函数参数是否相同),而编译器最终选择了在派生类中的函数,那么就说这个派生类的成员函数“隐藏”了基类的同名函数,即它阻止了编译器继续向上查找函数的定义。

 

17、define和const

define和const的区别:

  • const 定义的常数是变量,也带类型, #define 定义的只是个常数,不带类型;
  • define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用;
  • define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误;
  • const变量存放在内存的静态区域中,在程序运行过程中const变量只有一个拷贝,而#define 所定义的宏变量却有多个拷贝,所消耗的内存要比const变量的大得多;
  • 用define可以定义一些简单的函数,const是不可以定义函数的;
  • define可以用来防止头文件重复引用,而const不能;
  • const不足的地方,是与生俱来的,const不能重定义,而#define可以通过#undef取消某个符号的定义,再重新定义;
  • 在编译时, 编译器通常不为const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。 

 

18、内存对齐

内存对齐的原则:

  • 从0位置开始存储;
  • 变量存储的起始位置是该变量大小的整数倍;
  • 结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
  • 结构体中包含结构体,从结构体中最大元素的整数倍开始存;
  • 如果加入pragma pack(n) ,取n和变量自身大小较小的一个。

 

19、内联函数

内联函数有什么优点?

Inline这个名称就可以反映出它的工作方式,函数会在它所调用的位置上展开。这么做可以消除函数调用和返回所带来的开销(寄存器存储和恢复),而且,由于编译器会把调用函数的代码和函数本身放在一起优化,所以也有进一步优化代码的可能。不过这么做是有代价的,代码会变长,这就意味着占用更多的内存空间或者占用更多的指令缓存。

内核开发者通常把那些对时间要求比较高,而本身长度又比较短的函数定义成内联函数。如果你把一个大块头的程序做成了内联函数,却不需要争分夺秒,反而反复调用它,这么做就失去了内联的意义了。

内联函数与宏定义的区别?

  • 宏不是函数,inline函数是函数;
  • 宏定义在预编译的时候就会进行宏替换;
  • 内联函数在编译阶段,由编译器决定是否内联,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译;
  • 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义;
  • 宏定义小心处理宏参数(一般参数要括号起来),否则易出现二义性,而内联定义不会出现。

 

20、C++内存管理

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

堆:堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存;

栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限;

自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区;

全局/静态存储区:这块内存是在程序编译的时候就已经分配好的,在程序整个运行期间都存在。例如全局变量,静态变量;

常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量(const),不允许修改。

堆和栈的区别:

  • 管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak;
  • 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M;
  • 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,它们是如此的一一对应;
  • 生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长;
  • 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。

参考文章:C++内存管理(超长,例子很详细,排版很好)

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值