C++八股汇总

C++

2、结构体内存对齐问题?

alignas:指定对齐方式;alignof:计算处类型对齐方式;<stddef.h> offsetof宏 得到对象具体偏移

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
  • 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

c++11以后引入两个关键字 alignas (opens new window)alignof (opens new window)。其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式。

41、C++中有几种类型的new

在C++中,new有三种典型的使用方法:plain new,nothrow new和placement new

(1)plain new

言下之意就是普通的new,就是我们常用的new,在C++中定义如下:

void* operator new(std::size_t) throw(std::bad_alloc);
void  operator delete(void *) throw(); 

因此plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的,举个例子:

(2)nothrow new

nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:

char *p = new(nothrow) char[10e11];

(3)placement new

这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:

void* operator new(size_t,void*);
void operator delete(void*,void*);    

使用placement new需要注意两点:

  • palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组
  • placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

43、static的用法和作用?

1.先来介绍它的第一条也是最重要的一条:隐藏。(static函数,static变量均可)

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

2.static的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

3.static的第三个作用是默认初始化为0(static变量)

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

4.static的第四个作用:C++中的类成员声明static

  1. 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
  2. 在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问;
  3. 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
  4. 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
  5. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

类内:

  1. static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
  2. 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;
  3. static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function

48、const关键字的作用有哪些?

  1. 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
  2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  3. 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
  6. const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;
  7. 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
  8. 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。
  9. const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;
  10. const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
  11. 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

54、new和delete的实现原理, delete是如何知道释放内存的大小的?

1、 new简单类型直接调用operator new分配内存;

而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;

对于简单类型,new[]计算好大小后调用operator new;

对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;

① new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;

② 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;

③ 对象被分配了空间并构造完成,返回一个指向该对象的指针。

2、 delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。

3、 需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在**分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,**就知道了需要调用析构函数多少次了。

59、有哪些情况必须用到成员列表初始化?作用是什么?

  1. 必须使用成员初始化的四种情况

① 当初始化一个引用成员时;

② 当初始化一个常量成员时;

③ 当调用一个基类的构造函数,而它拥有一组参数时;

④ 当调用一个成员类的构造函数,而它拥有一组参数时;

  1. 成员初始化列表做了什么

① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;

② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

61、避免内存泄露的几种方式

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
  • 一定要将基类的析构函数声明为虚函数
  • 对象数组的释放一定要用delete []
  • 有new就有delete,有malloc就有free,保证它们一定成对出现

62、对象复用的了解,零拷贝的了解

对象复用

对象复用其本质是一种设计模式:Flyweight享元模式

通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。享元模式(Flyweight Pattern)是一种结构型设计模式,它旨在通过共享尽可能多的细粒度对象来减少内存占用。这种模式通常用于处理大量相似对象的情况,它可以通过共享对象来减少内存占用,提高性能。

享元模式的核心思想是将对象的内部状态(即不会改变的状态)和外部状态(即会改变的状态)分离开来。内部状态被称为享元对象,它可以被多个外部状态共享。外部状态则被称为非享元对象,它不能被共享。

在实现享元模式时,通常会使用一个工厂类来管理享元对象。工厂类维护一个享元池,用于存储已经创建的享元对象。当客户端请求一个享元对象时,工厂类首先检查享元池中是否已经存在该对象,如果存在,则直接返回该对象;否则,工厂类会创建一个新的享元对象并将其添加到享元池中。

下面是一个简单的例子,演示了如何使用享元模式来管理字符:

class CharacterFactory {
private:
    std::unordered_map<char, Character*> characters;
public:
    ~CharacterFactory() {
        for (auto it = characters.begin(); it != characters.end(); ++it) {
            delete it->second;
        }
    }
    Character* getCharacter(char c) {
        if (characters.find(c) == characters.end()) {
            characters[c] = new ConcreteCharacter(c);
        }
        return characters[c];
    }
};

零拷贝

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

零拷贝技术可以减少数据拷贝和共享总线操作的次数。

65、C++的四种强制转换

(1)reinterpret_cast

reinterpret_cast<type_id> (expression)

type_id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。

(2)const_cast

const_cast<type_id> (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象
  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象
  • const_cast一般用于修改底指针。如const char *p形式

(3)static_cast

static_cast < type-id > (expression)

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换

    • 进行上行转换(派生类 → \rarr 基类表示)是安全的
    • 进行下行转换(基类 → \rarr 派生类)时,由于没有动态类型检查,所以是不安全的
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  • 把空指针转换成目标类型的空指针

  • 把任何类型的表达式转换成void类型

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

(4)dynamic_cast

  • dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换

  • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的

  • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全

dynamic_cast (expression)

该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*

如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么 expression 也必须是一个引用

dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)

68、说说移动构造函数

  1. 我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
  2. 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。
    所以我们只要避免第一个指针释放空间就可以了。**避免的方法就是将源对象资源置空(要保证源对象可析构)**这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
  3. 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

77、C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?

1) 指针参数传递本质上是值传递,它所传递的是一个地址值。

值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。

值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。

被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。

因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是:

  1. 任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

  2. 而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用

4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

85、 函数调用过程栈的变化,返回值和参数变量哪个先入栈?

1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;

2、调用者函数使用call指令调用被调函数,并把 call 指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);

3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);

4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即: 这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;

STL

2、解释一下什么是trivial destructor

“trivial destructor”一般是指用户没有自定义析构函数,而由系统生成的,这种析构函数在《STL源码解析》中成为“无关痛痒”的析构函数。

反之,用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露

对于trivial destructor,如果每次都进行调用,显然对效率是一种伤害,如何进行判断呢?

《STL源码解析》中给出的说明是:

首先利用value_type()获取所指对象的型别,再利用__type_traits判断该型别的析构函数是否trivial,若是(__true_type),则什么也不做,若为(__false_type),则去调用destory()函数。

也就是说,在实际的应用当中,STL库提供了相关的判断方法**__type_traits**,感兴趣的读者可以自行查阅使用方式。除了trivial destructor,还有trivial construct、trivial copy construct等,如果能够对是否trivial进行区分,可以采用内存处理函数memcpy()、malloc()等更加高效的完成相关操作,提升效率。

4、迭代器:++it、it++哪个好,为什么

  1. 前置返回一个引用,后置返回一个对象
// ++i实现代码为:
int& operator++()
{
  *this += 1;
  return *this;
} 
  1. 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
//i++实现代码为:                 
int operator++(int)                 
{
int temp = *this;                   
   ++*this;                       
   return temp;                  
} 

5、说一下C++左值引用和右值引用

C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。

  1. 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
  2. C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
  3. 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
  4. 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。

左值和右值

左值:表示的是可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。但是修饰符const的出现使得可以声明如下的标识符,它可以取得地址,但是没办法对其进行赋值

const int& a = 10;

右值:表示无法获取地址的对象,有常量值、函数返回值、lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。

左值引用和右值引用

左值引用:传统的C++中引用被称为左值引用

右值引用:C++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置

这里主要说一下右值引用的特点:

  • 特点1:通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去
  • 特点2:右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值
  • 特点3:T&& t在发生自动类型推断的时候,它是左值还是右值取决于它的初始化。

举个例子:

#include <bits/stdc++.h>
using namespace std;
template<typename T>
void fun(T&& t)
{
	cout << t << endl;
}
int getInt()
{
	return 5;
}
int main() {
	
	int a = 10;
	int& b = a;  //b是左值引用
	int& c = 10;  //错误,c是左值不能使用右值初始化
	int&& d = 10;  //正确,右值引用用右值初始化
	int&& e = a;  //错误,e是右值引用不能使用左值初始化
	const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
	const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化
	const int&& h = 10; //正确,右值常引用
	const int& aa = h;//正确
	int& i = getInt();  //错误,i是左值引用不能使用临时变量(右值)初始化
	int&& j = getInt();  //正确,函数返回值是右值
	fun(10); //此时fun函数的参数t是右值
	fun(a); //此时fun函数的参数t是左值
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值