《逆袭进大厂》第二弹之C++进阶篇59问59答(超硬核干货)下

简介: 笔记

83、构造函数或者析构函数中可以调用虚函数吗


简要结论:

  • 从语法上讲,调用完全没有问题。
  • 但是从效果上看,往往不能达到需要的目的。

《Effective C++》的解释是:

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。

举个例子:

#include<iostream>
using namespace std;
class Base
{
public:
    Base()
    {
       Function();
    }
    virtual void Function()
    {
        cout << "Base::Fuction" << endl;
    }
    ~Base()
    {
        Function();
    }
};
class A : public Base
{
public:
    A()
    {
      Function();
    }
    virtual void Function()
    {
        cout << "A::Function" << endl;
    }
    ~A()
    {
        Function();
    }
};
int main()
{
    Base* a = new Base;
    delete a;
    cout << "-------------------------" <<endl;
    Base* b = new A;//语句1
    delete b;
}
//输出结果
//Base::Fuction
//Base::Fuction
//-------------------------
//Base::Fuction
//A::Function
//Base::Fuction

语句1讲道理应该体现多态性,执行类A中的构造和析构函数,从实验结果来看,语句1并没有体现,执行流程是先构造基类,所以先调用基类的构造函数,构造完成再执行A自己的构造函数,析构时也是调用基类的析构函数,也就是说构造和析构中调用虚函数并不能达到目的,应该避免

《构造函数或者析构函数中调用虚函数会怎么样?》:https://dwz.cn/TaJTJONX


84、智能指针的原理、常用的智能指针及实现


原理

智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源

常用的智能指针


(1) shared_ptr

实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。

  • 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
  • 每次创建类的新对象时,初始化指针并将引用计数置为1
  • 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
  • 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
  • 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)


(2) unique_ptr

unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。


(3) weak_ptr

weak_ptr:弱引用。引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。


(4) auto_ptr

主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存。

auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。

auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能在STL中使用。

智能指针shared_ptr代码实现:

template<typename T>
class SharedPtr
{
public:
    SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1))
    {}
    SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount){
        *(_pcount)++;
    }
    SharedPtr<T>& operator=(const SharedPtr& s){
        if (this != &s)
        {
            if (--(*(this->_pcount)) == 0)
            {
                delete this->_ptr;
                delete this->_pcount;
            }
            _ptr = s._ptr;
            _pcount = s._pcount;
            *(_pcount)++;
        }
        return *this;
    }
    T& operator*()
    {
        return *(this->_ptr);
    }
    T* operator->()
    {
        return this->_ptr;
    }
    ~SharedPtr()
    {
        --(*(this->_pcount));
        if (this->_pcount == 0)
        {
            delete _ptr;
            _ptr = NULL;
            delete _pcount;
            _pcount = NULL;
        }
    }
private:
    T* _ptr;
    int* _pcount;//指向引用计数的指针
};

《智能指针的原理及实现》:https://blog.csdn.net/lizhentao0707/article/details/81156384


85、构造函数的几种关键字


default

default关键字可以显式要求编译器生成合成构造函数,防止在调用时相关构造函数类型没有定义而报错

#include <iostream>
using namespace std;
class CString
{
public:
    CString() = default; //语句1
    //构造函数
    CString(const char* pstr) : _str(pstr){}
    void* operator new() = delete;//这样不允许使用new关键字
    //析构函数
    ~CString(){}
public:
     string _str;
};
int main()
{
   auto a = new CString(); //语句2
   cout << "Hello World" <<endl;
   return 0;
}
//运行结果
//Hello World

如果没有加语句1,语句2会报错,表示找不到参数为空的构造函数,将其设置为default可以解决这个问题

delete

delete关键字可以删除构造函数、赋值运算符函数等,这样在使用的时候会得到友善的提示

#include <iostream>
using namespace std;
class CString
{
public:
    void* operator new() = delete;//这样不允许使用new关键字
    //析构函数
    ~CString(){}
};
int main()
{
   auto a = new CString(); //语句1
   cout << "Hello World" <<endl;
   return 0;
}

在执行语句1时,会提示new方法已经被删除,如果将new设置为私有方法,则会报惨不忍睹的错误,因此使用delete关键字可以更加人性化的删除一些默认方法

=0

将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处;当然,也可以为纯虚函数提供定义,不过函数体必须定义在类的外部)

《C++构造函数的default和delete》:https://blog.csdn.net/u010591680/article/details/71101737


86、C++的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast


reinterpret_cast

reinterpret_cast(expression)

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

const_cast

const_cast(expression)

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

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

static_cast

static_cast < type-id > (expression)

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
  • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
  • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
  • 把空指针转换成目标类型的空指针
  • 把任何类型的表达式转换成void类型

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

dynamic_cast

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

dynamic_cast(expression)

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

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

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

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

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

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

举个例子:

#include <bits/stdc++.h>
using namespace std;
class Base
{
public:
    Base() :b(1) {}
    virtual void fun() {};
    int b;
};
class Son : public Base
{
public:
    Son() :d(2) {}
    int d;
};
int main()
{
    int n = 97;
    //reinterpret_cast
    int *p = &n;
    //以下两者效果相同
    char *c = reinterpret_cast<char*> (p); 
    char *c2 =  (char*)(p);
    cout << "reinterpret_cast输出:"<< *c2 << endl;
    //const_cast
    const int *p2 = &n;
    int *p3 = const_cast<int*>(p2);
    *p3 = 100;
    cout << "const_cast输出:" << *p3 << endl;
    Base* b1 = new Son;
    Base* b2 = new Base;
    //static_cast
    Son* s1 = static_cast<Son*>(b1); //同类型转换
    Son* s2 = static_cast<Son*>(b2); //下行转换,不安全
    cout << "static_cast输出:"<< endl;
    cout << s1->d << endl;
    cout << s2->d << endl; //下行转换,原先父对象没有d成员,输出垃圾值
    //dynamic_cast
    Son* s3 = dynamic_cast<Son*>(b1); //同类型转换
    Son* s4 = dynamic_cast<Son*>(b2); //下行转换,安全
    cout << "dynamic_cast输出:" << endl;
    cout << s3->d << endl;
    if(s4 == nullptr)
        cout << "s4指针为nullptr" << endl;
    else
        cout << s4->d << endl;
    return 0;
}
//输出结果
//reinterpret_cast输出:a
//const_cast输出:100
//static_cast输出:
//2
//-33686019
//dynamic_cast输出:
//2
//s4指针为nullptr

从输出结果可以看出,在进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。


87、C++函数调用的压栈过程


从代码入手,解释这个过程:

#include <iostream>
using namespace std;
int f(int n) 
{
    cout << n << endl;
    return n;
}
void func(int param1, int param2)
{
    int var1 = param1;
    int var2 = param2;
    printf("var1=%d,var2=%d", f(var1), f(var2));
}
int main(int argc, char* argv[])
{
    func(1, 2);
    return 0;
}
//输出结果
//2
//1
//var1=1,var2=2

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

《C/C++函数调用过程分析》:https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601204.html

《C/C++函数调用的压栈模型》:https://blog.csdn.net/m0_37717595/article/details/80368411


88、说说移动构造函数


1)  我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

2)  拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。

所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;

3)  移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。


89、C++中将临时变量作为返回值时的处理过程


首先需要明白一件事情,临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是可以被分配给其他变量,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了

C语言里规定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit

由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了。当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系

如果我们需要返回值,一般使用赋值语句就可以了

《【C++】临时变量不能作为函数的返回值?》:https://www.wandouip.com/t5i204349/

(栈上的内存分配、拷贝过程)


90、关于this指针你知道什么?全说出来


  • this指针是类的指针,指向对象的首地址。
  • this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。
  • this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。

this指针的用处

一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候(全局函数,静态函数中不能使用this指针),编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行

this指针的使用

一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;

另外一种情况是当形参数与成员变量名相同时用于区分,如this->n = n (不能写成n = n)

类的this指针有以下特点

(1)this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数T * const this

如:

class A{
public:
    int func(int p){}
};

其中,func的原型在编译器看来应该是:

int func(A * const this,int p);

(2)由此可见,this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。如:

A a;
a.func(10);
//此处,编译器将会编译成:
A::func(&a,10);

看起来和静态函数没差别,对吗?不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,例如VC通常是通过ecx(计数寄存器)传递this参数的。


91、几个this指针的易混问题


A. this指针是什么时候创建的?

this在成员函数的开始执行前构造,在成员的执行结束后清除。

但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new的方式创建对象的话,在堆里分配内存,new操作符通过eax(累加寄存器)返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx,之后构造函数里面怎么处理请看上面的回答

B. this指针存放在何处?堆、栈、全局变量,还是其他?

this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它们并不是和高级语言变量对应的。

C. this指针是如何传递类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针又是如何找到“类实例后函数的”?

大多数编译器通过ecx(寄数寄存器)寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。

在call之前,编译器会把对应的对象地址放到eax中。this是通过函数参数的首参来传递的。this指针在调用之前生成,至于“类实例后函数”,没有这个说法。类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那儿,不会跑的

D. this指针是如何访问类中的变量的?

如果不是类,而是结构体的话,那么,如何通过结构指针来访问结构中的变量呢?如果你明白这一点的话,就很容易理解这个问题了。

在C++中,类和结构是只有一个区别的:类的成员默认是private,而结构是public。

this是类的指针,如果换成结构体,那this就是结构的指针了。

E.我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?

this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。

F.每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数?

普通的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。但是,即使是虚函数,如果编译期就能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。正是由于this指针的存在,用来指向不同的对象,从而确保不同对象之间调用相同的函数可以互不干扰

《C++中this指针的用法详解》http://blog.chinaunix.net/uid-21411227-id-1826942.html


92、构造函数、拷贝构造函数和赋值操作符的区别


构造函数

对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数

拷贝构造函数

对象不存在,但是使用别的已经存在的对象来进行初始化

赋值运算符

对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的

举个例子:

#include <iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "我是构造函数" << endl;
    }
    A(const A& a)
    {
        cout << "我是拷贝构造函数" << endl;
    }
    A& operator = (A& a)
    {
        cout << "我是赋值操作符" << endl;
        return *this;
    }
    ~A() {};
};
int main()
{
    A a1; //调用构造函数
    A a2 = a1; //调用拷贝构造函数
    a2 = a1; //调用赋值操作符
    return 0;
}
//输出结果
//我是构造函数
//我是拷贝构造函数
//我是赋值操作符


93、拷贝构造函数和赋值运算符重载的区别?


  • 拷贝构造函数是函数,赋值运算符是运算符重载。
  • 拷贝构造函数会生成新的类对象,赋值运算符不能。
  • 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
  • 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:
    Student s;
    Student s1 = s;    // 调用拷贝构造函数
    Student s2;
    s2 = s;    // 赋值运算符操作

注:类中有指针变量时要重写析构函数、拷贝构造函数和赋值运算符


94、智能指针的作用;


1)  C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

2)  智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

3)  初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptrp4 = new int(1);的写法是错误的

拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象

4)  unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

5)  智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

6)  weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.


95、说说你了解的auto_ptr作用


1)  auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;

2)  auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;

3)  auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;

4)  由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;

5)  Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;

6)  auto_ptr支持所拥有的指针类型之间的隐式类型转换。

7)  可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;

8)  T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。


96、智能指针的循环引用


循环引用是指使用多个智能指针share_ptr时,出现了指针之间相互指向,从而形成环的情况,有点类似于死锁的情况,这种情况下,智能指针往往不能正常调用对象的析构函数,从而造成内存泄漏。举个例子:

#include <iostream>
using namespace std;
template <typename T>
class Node
{
public:
    Node(const T& value)
        :_pPre(NULL)
        , _pNext(NULL)
        , _value(value)
    {
        cout << "Node()" << endl;
    }
    ~Node()
    {
        cout << "~Node()" << endl;
        cout << "this:" << this << endl;
    }
    shared_ptr<Node<T>> _pPre;
    shared_ptr<Node<T>> _pNext;
    T _value;
};
void Funtest()
{
    shared_ptr<Node<int>> sp1(new Node<int>(1));
    shared_ptr<Node<int>> sp2(new Node<int>(2));
    cout << "sp1.use_count:" << sp1.use_count() << endl;
    cout << "sp2.use_count:" << sp2.use_count() << endl;
    sp1->_pNext = sp2; //sp1的引用+1
    sp2->_pPre = sp1; //sp2的引用+1
    cout << "sp1.use_count:" << sp1.use_count() << endl;
    cout << "sp2.use_count:" << sp2.use_count() << endl;
}
int main()
{
    Funtest();
    system("pause");
    return 0;
}
//输出结果
//Node()
//Node()
//sp1.use_count:1
//sp2.use_count:1
//sp1.use_count:2
//sp2.use_count:2

从上面shared_ptr的实现中我们知道了只有当引用计数减减之后等于0,析构时才会释放对象,而上述情况造成了一个僵局,那就是析构对象时先析构sp2,可是由于sp2的空间sp1还在使用中,所以sp2.use_count减减之后为1,不释放,sp1也是相同的道理,由于sp1的空间sp2还在使用中,所以sp1.use_count减减之后为1,也不释放。sp1等着sp2先释放,sp2等着sp1先释放,二者互不相让,导致最终都没能释放,内存泄漏。

在实际编程过程中,应该尽量避免出现智能指针之间相互指向的情况,如果不可避免,可以使用弱指针—weak_ptr,它不增加引用计数,只要出了作用域就会自动析构。

《C++ 智能指针(及循环引用问题)》:https://blog.csdn.net/m0_37968340/article/details/76737395


97、什么是虚拟继承


由于C++支持多继承,除了public、protected和private三种继承方式外,还支持虚拟(virtual)继承,举个例子:

#include <iostream>
using namespace std;
class A{}
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};
int main()
{
    cout << "sizeof(A):" << sizeof A <<endl; // 1,空对象,只有一个占位
    cout << "sizeof(B):" << sizeof B <<endl; // 4,一个bptr指针,省去占位,不需要对齐
    cout << "sizeof(C):" << sizeof C <<endl; // 4,一个bptr指针,省去占位,不需要对齐
    cout << "sizeof(D):" << sizeof D <<endl; // 8,两个bptr,省去占位,不需要对齐
}

上述代码所体现的关系是,B和C虚拟继承A,D又公有继承B和C,这种方式是一种菱形继承或者钻石继承,可以用如下图来表示

虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr,如上图所示。如果既存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针


98、如何获得结构成员相对于结构开头的字节偏移量


使用offsetof()函数

举个例子:

#include <iostream>
#include <stddef.h>
using namespace std;
struct  S
{
    int x;
    char y;
    int z;
    double a;
};
int main()
{
    cout << offsetof(S, x) << endl; // 0
    cout << offsetof(S, y) << endl; // 4
    cout << offsetof(S, z) << endl; // 8
    cout << offsetof(S, a) << endl; // 12
    return 0;
}
在VS2019 + win下 并不是这样的
    cout << offsetof(S, x) << endl; // 0
    cout << offsetof(S, y) << endl; // 4
    cout << offsetof(S, z) << endl; // 8
    cout << offsetof(S, a) << endl; // 16 这里是 16的位置,因为 double是8字节,需要找一个8的倍数对齐,
当然了,如果加上  #pragma pack(4)指定 4字节对齐就可以了
#pragma pack(4)
struct  S
{
    int x;
    char y;
    int z;
    double a;
};
void test02()
{
    cout << offsetof(S, x) << endl; // 0
    cout << offsetof(S, y) << endl; // 4
    cout << offsetof(S, z) << endl; // 8
    cout << offsetof(S, a) << endl; // 12

S结构体中各个数据成员的内存空间划分如下所示,需要注意内存对齐


99、静态类型和动态类型以及静态绑定和动态绑定的总结


  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

举个例子:

#include <iostream>
using namespace std;
class A
{
public:
    /*virtual*/ void func() { std::cout << "A::func()\n"; }
};
class B : public A
{
public:
    void func() { std::cout << "B::func()\n"; }
};
class C : public A
{
public:
    void func() { std::cout << "C::func()\n"; }
};
int main()
{
    C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
    B* pb = new B(); //pb的静态类型和动态类型也都是B*;
    A* pa = pc;      //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
    pa = pb;         //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
    C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
    pa->func();      //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
    pc->func();      //C::func() pc的动、静态类型都是C*,因此调用C::func();
    pnull->func();   //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
    return 0;
}

如果将A类中的virtual注释去掉,则运行结果是:

pa->func();      //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
pc->func();      //C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
pnull->func();   //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;

在上面的例子中,

  • 如果基类A中的func不是virtual函数,那么不论pa、pb、pc指向哪个子类对象,对func的调用都是在定义pa、pb、pc时的静态类型决定,早已在编译期确定了。
  • 同样的空指针也能够直接调用no-virtual函数而不报错(这也说明一定要做空指针检查啊!),因此静态绑定不能实现多态;
  • 如果func是虚函数,那所有的调用都要等到运行时根据其指向对象的类型才能确定,比起静态绑定自然是要有性能损失的,但是却能实现多态特性;
    本文代码里都是针对指针的情况来分析的,对于引用的情况也同样适用。

至此总结一下静态绑定和动态绑定的区别:

  • 静态绑定发生在编译期,动态绑定发生在运行期;
  • 对象的动态类型可以更改,但是静态类型无法更改;
  • 要想实现动态,必须使用动态绑定;
  • 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;
    建议:

绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG;另外,在动态绑定也即在virtual函数中,要注意默认参数的使用。当缺省参数和virtual函数一起使用的时候一定要谨慎,不然出了问题怕是很难排查。

看下面的代码:

#include <iostream>
using namespace std;
class E
{
public:
    virtual void func(int i = 0)
    {
        std::cout << "E::func()\t" << i << "\n";
    }
};
class F : public E
{
public:
    virtual void func(int i = 1)
    {
        std::cout << "F::func()\t" << i << "\n";
    }
};
void test2()
{
    F* pf = new F();
    E* pe = pf;
    pf->func(); //F::func() 1  正常,就该如此;
    pe->func(); //F::func() 0  哇哦,这是什么情况,调用了子类的函数,却使用了基类中参数的默认值!
}
int main()
{
    test2();
    return 0;
}

《C++中的静态绑定和动态绑定》:https://www.cnblogs.com/lizhenghn/p/3657717.html


100、C++ 11有哪些新特性?


  • nullptr替代 NULL
  • 引入了 auto 和 decltype 这两个关键字实现了类型推导
  • 基于范围的 for 循环for(auto& i : res){}
  • 类和结构体的中初始化列表
  • Lambda 表达式(匿名函数)
  • std::forward_list(单向链表)
  • 右值引用和move语义


101、引用是否能实现动态绑定,为什么可以实现?


可以。

引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。

举个例子:

#include <iostream>
using namespace std;
class Base 
{
public:
    virtual void  fun()
    {
        cout << "base :: fun()" << endl;
    }
};
class Son : public Base
{
public:
    virtual void  fun()
    {
        cout << "son :: fun()" << endl;
    }
    void func()
    {
        cout << "son :: not virtual function" <<endl;
    }
};
int main()
{
    Son s;
    Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化
    s.fun(); //son::fun()
    b.fun(); //son :: fun()
    return 0;
}

需要说明的是虚函数才具有动态绑定,上面代码中,Son类中还有一个非虚函数func(),这在b对象中是无法调用的,如果使用基类指针来指向子类也是一样的。


102、全局变量和局部变量有什么区别?


生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载,局部变量则分配在堆栈里面 。

《C++经典面试题》:https://www.cnblogs.com/yjd_hycf_space/p/7495640.html


103、指针加减计算要注意什么?


指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。

举个例子:

#include <iostream>
using namespace std;
int main()
{
    int *a, *b, c;
    a = (int*)0x500;
    b = (int*)0x520;
    c = b - a;
    printf("%d\n", c); // 8
    a += 0x020;
    c = b - a;
    printf("%d\n", c); // -24
    return 0;
}

首先变量a和b都是以16进制的形式初始化,将它们转成10进制分别是1280(5*16\^2=1280)和1312(5*16\^2+2*16=1312), 那么它们的差值为32,也就是说a和b所指向的地址之间间隔32个位,但是考虑到是int类型占4位,所以c的值为32/4=8

a自增16进制0x20之后,其实际地址变为1280 + 2*16*4 = 1408,(因为一个int占4位,所以要乘4),这样它们的差值就变成了1312 - 1280 = -96,所以c的值就变成了-96/4 = -24

遇到指针的计算,需要明确的是指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果


104、 怎样判断两个浮点数是否相等?


对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。


105、方法调用的原理(栈、汇编)


1)  机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈;帧栈可以认为是程序栈的一段,它有两个端点,一个标识起始地址,一个标识着结束地址,两个指针结束地址指针esp,开始地址指针ebp;

2)  由一系列栈帧构成,这些栈帧对应一个过程,而且每一个栈指针+4的位置存储函数返回地址;每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。

3)  过程实现

①   备份原来的帧指针,调整当前的栈帧指针到栈指针位置;

②   建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存;

③   使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。

④   恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了

⑤   恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。

⑥   释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理。

⑦   恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。

⑧   弹出返回地址,跳出当前过程,继续执行调用者的代码。

4)  过程调用和返回指令

①   call指令

②   leave指令

③   ret指令


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


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

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

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

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

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

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

3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

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

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

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。

符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。


107、类如何实现只能静态分配和只能动态分配


1)  前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建

2)  建立类的对象有两种方式:

①   静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;

②   动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;

3)  只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有。


108、如果想将某个类用作基类,为什么该类必须定义而非声明?


派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。


结语


好家伙,终于完了。你要是能看到这里,那真是个狠人。

整理这些东西真心不容易!眼睛快看瞎了求个三连吧!感谢大家!

就酱,拜了个拜!下期再见!

相关文章
|
6月前
|
安全 算法 编译器
【C++ 泛型编程 进阶篇】深入探究C++模板参数推导:从基础到高级
【C++ 泛型编程 进阶篇】深入探究C++模板参数推导:从基础到高级
744 3
|
6月前
|
算法 安全 编译器
【C++ 泛型编程 进阶篇】C++模板参数推导的场景分析
【C++ 泛型编程 进阶篇】C++模板参数推导的场景分析
96 0
【C++ 泛型编程 进阶篇】C++模板参数推导的场景分析
|
6月前
|
JavaScript 编译器 API
【C++ 函数和过程 进阶篇】全面掌握C++函数返回值:从入门到精通的实战指南
【C++ 函数和过程 进阶篇】全面掌握C++函数返回值:从入门到精通的实战指南
205 1
|
6月前
|
安全 算法 C++
【C++泛型编程 进阶篇】模板返回值的优雅处理(二)
【C++泛型编程 进阶篇】模板返回值的优雅处理
148 0
|
6月前
|
安全 算法 编译器
【C++泛型编程 进阶篇】模板返回值的优雅处理(一)
【C++泛型编程 进阶篇】模板返回值的优雅处理
197 0
|
6月前
|
设计模式 算法 C++
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程(二)
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程
95 0
|
6月前
|
安全 程序员 编译器
【C/C++ 泛型编程 进阶篇 Type traits 】C++类型特征探究:编译时类型判断的艺术
【C/C++ 泛型编程 进阶篇 Type traits 】C++类型特征探究:编译时类型判断的艺术
494 1
|
5月前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
5月前
|
API C++
c++进阶篇——初窥多线程(三)cpp中的线程类
C++11引入了`std::thread`,提供对并发编程的支持,简化多线程创建并增强可移植性。`std::thread`的构造函数包括默认构造、移动构造及模板构造(支持函数、lambda和对象)。`thread::get_id()`获取线程ID,`join()`确保线程执行完成,`detach()`使线程独立,`joinable()`检查线程状态,`operator=`仅支持移动赋值。`thread::hardware_concurrency()`返回CPU核心数,可用于高效线程分配。
|
5月前
|
C++
c++进阶篇(一)——运算符重载
c++进阶篇(一)——运算符重载