cpp简单总结

1.简述智能指针的特点,简述new和malloc的区别。

  • shared_ptr,显现共享式特点,多个同类型的shared指针可以共享一个对象,当持有者的计数归0,shared_ptr指向的指针就会被释放;
  • weak_ptr,share的小弟,可以和shared_ptr共享同一个对象,但不会纳入持有者计数,并且在shared_ptr指向对象被释放后,指针自动归空,所以使用前需要有个检测;
  • unique_ptr,和shared不同,显现独占式的特点,同一时间只有一个拥有,一旦更换指向对象,原有对象被销毁,对应资源就会被释放。

new是运算符,malloc是函数,这是最基础的不同,另外由于malloc是基于c编程继承过来的,主要用于内置对象的动态资源申请,对应free用于释放,针对面向对象大放光彩的c++就显得不够用了。针对于此,new和delete两个运算符就可以针对自定义类对象来进行构造和析构的调用来达到对应对象的构造和释放。要注意的就是单个对象和对应成序列的数组的new和delete了,这种情况下是使用的new[]和delete[]。

2.c++中虚函数和纯虚函数的区别

首先,虚函数是类中用virtual进行修饰的成员函数,而纯虚函数则是更进一步在虚函数的基础上,给成员函数添加"=0"的标识的虚函数,这是声明时的区别。另外,虚函数是用来实现动态绑定的一个c++的特色,基类声明了虚函数,继承了该基类的子类重写了该虚函数,在声明了一个基类指针的情况下调用该虚函数,其根据其指向的实际对象是子类对象还是基类对象,虚函数呈现不同的响应,这就是c++的动态多态,需要注意的是,如果不是通过指针,而是简单的普通对象,那就没有动态绑定的特性的呈现。

纯虚函数,则是另外的标识,声明了纯虚函数的类就是抽象类,是没有具体对象的特殊类,声明了就为了继承,产生不同的类,从而展现不同的特性。就好比书这个类,可以派生出哲学书、历史书、教科书和技工书种等类,但它本身不应该产生实际对象,在这个意义上,这情况就该实现抽象类然后使用纯虚函数。

3.STL中vector和list的实现,常见操作的时间复杂度。

作为STL中的两个经典容器,vector是动态数组的实现,list是链式表的实现,我们经常探究系统存储的发现两大存储形式,一种就是连续元素的成块存储,另一种就是散列式的元素,互相指引成链的链式存储。
就实现而言,vector是拥有连续内存的动态数组,针对随机访问的时间复杂度为O(1),但正因为存储是连续的,想要往数组首部和中部插入元素就会导致大块的数据拷贝或者移动,所以时间复杂度为O(n);list则是由双向链表实现,因为内存是散列式的,不连续的,所以插入和删除的操作都很快,事件复杂度为O(1),而查询就需要顺着链表一路向下或向上来逐个查询,所以时间复杂度为O(n)。

4.c++11的新特性

  • 使用using来定义别名,而非typedef;
  • constexpr,用在编译时的常量和常量函数,相对于const表示只读语义保证了运行时不被修改,但它修饰的依然是动态的变量,constexper对应修饰的则是真正的常量,是在编译时就计算确认了的值,另外它用来修饰函数,就会使得该函数的返回值在编译期间尽量被计算出来当做真正的常量,如果不能,就当做正经函数;
  • nullptr,表示空指针的常量值,从c继承下来的NULL实质上是一个int类型的0,并不是指针;
  • final和override,final修饰类,表示这个类是不能被继承的,这条派生线到此为止了,override则是用在子类的虚函数上的,被修饰的虚函数一定要重写,不然就会编译报错,算是提醒;
  • default,和switch的default不同,这个default是用来修饰构造函数或者析构函数的,显式的向编译器要求,生成默认的函数版本;
  • explicit,修饰类的构造函数,表示该构造函数不能被隐式的调用,禁止这种构造函数方式的隐式转换;

5.c++中智能指针和普通指针的区别

智能指针是行为类似指针的类对象,智能指针实现的原理:

  • 将一个计数器与类指向对象关联,引用技术跟踪有多少个类对象共享同一指针;
  • 创建类的新对象,初始化指针,引用计数置1,析构函数调用,指针置空,引用计数减一;

6.简述c++右值引用和转移语义

  • 先点明左值,左值是在表达式左边,在堆或栈上的实名对象,有明确的的内存地址;右值就是和左值反过来,在表达式右边的没有实际内存地址的,只存在于临时寄存器的。常见的左值引用是不能和右值绑定的,但const type&可以,因为const常量不可修改,是存在于内存中的临时量,具有只读语义,用在函数传参中,可以避免创建不必要的临时对象,但这种使用不能被修改,基于此,引入右值引用--type&&。

左值可以寻址,右值不能寻址,这是根本区别,函数传参使用左右值作为形参可以避免拷贝,但右值引用更加灵活,可以修改。

  • 转移语义是右值引用支持的特性,是相对于深拷贝的一个改良式语义,针对被拷贝对象在拷贝动作以后不再使用的场景,使用移动语义,直接构造一个对象得到被拷贝对象的所有信息,并且避免深拷贝,从而提升性能。与之想配合的新特性是std::move。move语义的底层实现实际上是一个static_cast给强制转换成右值引用类型。在针对临时构建的一些个右值对象并用来对新对象进行初始化的时候,没有实现移动构造的要比实现了移动构造的多一次资源分配和释放。

7.简述c++深拷贝和浅拷贝

浅拷贝和深拷贝其实是针对拷贝对象是否存在指针成员的情况而言的,当存在指针成员而且浅拷贝发生,就会使得指针被拷贝一份但指针指向内容没有拷贝,也就是它们指向的内容是同一份,会存在内存释放时造成内存泄漏的风险,而两个对象被释放也会因为两次调用delete,而实际指向内容只有一份而程序崩溃,另外这样也会出现竞争。深拷贝就是基于这种情况,把指针指向的内容也拷贝了一份,一个类要实现深拷贝就要实现拷贝构造函数。

8.c++中的多态的实现

多态分两种,一种是静态多态,一种是动态多态,第一种是同名普通函数的实现,这些函数名字一样,但传入参数和返回参数不同,就会出现一个函数,不同传参得到不同响应的多态效果,这种实现是编译器给后续命名上进行加工实现的,但对于程序员而言,它们还是一个名字,而且是编译确定的多态关系所以是静态多态;动态多态则是运行时确定的关系,实现机制是虚函数,一个声明定义了虚函数的基类,它的子类重写了该虚函数,这样一份基类指针,在可以指向父类也可以指向子类的情况下,调用该虚函数,根据指针实际指向的对象可以呈现不同的响应,这种实现就被称为动态多态。

9.const和static关键字的区别

const是表示只读语义,表示运行时不可修改,根据它修饰的具体对象来决定不可变的对象是什么;被static修饰的对象会修改它的声明域和存放区域,但不改变作用域,局部变量还是作用于局部,全局变量依然作用于全局,但它们从声明开始,就一直存在到程序运行结束,因为它们处于静态区。因为c++是类特性鲜明的语言,所以const和static又有另外意义,被const修饰的成员函数,表明它是不会修改调用对象的内容的,一般被修饰的函数都是只读取信息的函数,而被static修饰的成员函数不再为类对象所拥有,而是被类特有,而且这种函数只可访问同样的静态成员。

10.简述c++内存对齐场景和规则

内存对齐最早是出现在c结构体中的成员偏移,然后继承了c的c++用类似的方法实现了类,所以类也存在内存对齐的现象。内存对齐的存在,是为了使得CPU对内存的访问更有效率,对齐的方法有手动使用#pragma pack(n)来设置,也有让编译器自己拿主意。对齐的规则(n为手动设置或者默认的对齐值):

  • 1.第一个成员首地址为0,某成员类型所占字节数为k,该成员的首地址就是对齐值和k的最小值的整数倍;
  • 2.整体对齐,结构体或者类的整体大小,就是对齐值和最大成员字节类型字节数的最小值,的整数倍,不够就后面占位。

11.指针和引用的区别

在底层上看,其实两者应该是没有区别的,引用和指针对应的都是指向某个地址,但在实际语言特性上看,又是有区别的:

  • 引用在声明的时候就需要初始化,指针可以简单声明,不急着初始化;
  • 引用在初始化以后,就不能更改指向了,它就是初始化时对象的一个别名,所以不能乱安在其他人头上,而指针可以一直变化,除非是常指针,一旦定了指向就不能改了;
  • 引用使用的时候不需要解引用,当做正常变量即可,指针则需要解引用;
  • 引用没有const,指针有const;
  • sizeof一个引用,得到的是指向对象的大小,sizeof一个指针则是固定的4或者8;
  • 指针是一个独立类型,需要分配内存,引用只是别名,不会分配内存

12.c++中类模板和模板类的区别

类模板,template提前修饰,不明确定义其数据成员、成员函数的参数和返回值,这种类是一个模板,可以传入任意符合要求的类型,最后实例化成一个具体的类;
模板类就是类模板实例化后的具体类,就好比一个类模板是做糕点的模具,模板类就是做出来的搞点。

13.简述c++中的内联函数

内联函数的实现是在函数声明或者实现前添加inline修饰,内联函数一般短小精悍,在编译后直接把函数替换到函数调用处,算是早期宏来替换的升级,不过宏定义的函数是没有类型安全检查的,也不能进行调试,但内联函数都有。这样的函数存在大大提升了效率,所以很多在类声明处实现的普通成员函数,基本都是默认内联。不过inline修饰的函数,只是一个对编译器的建议,要不要做成内联函数,实际还要看编译器调动。

14.c++编译过程

编译过程没有太大变动,预处理进行宏替换;然后经编译器进行词法语法分析,翻译生成中间代码或者汇编指令,其中会经过适当的优化;汇编得到上面的成果后转换翻译成目标机器指令,然后经链接器链接动态库或者静态库生成可执行文件。需要注意的是是静态链接和动态链接,前者是把需要函数的二进制代码全部包含到可执行文件中,这样的链接方式是库和可执行文件已经一体了,发行时不需要再发布依赖库,就是体积太大了;后者动态链接是不拷贝可执行代码,只在对应函数执行时,通过标记的参数和签名记录发送给操作系统由系统os把动态库加载到内存中,然后执行,因为是运行时加载,会影响程序前期执行性能。

15.c++中哪些函数不能声明为虚函数

普通函数,因为虚函数的动态绑定是基于类的概念的,声明普通函数为虚函数也没什么意义,编译器没有这方面的优化规则;
构造函数,构造函数是为了初始化对象而制作的,虚函数是为了对象基于某个信息产生不同响应而制作的,两者目的不一样,编译器就不会给这方面的实现;
内联函数,内联函数是在编译时就确定了的,用来替换调用处的,虚函数是运行时确定的,两者没办法归为一起;
静态成员函数,静态成员函数对于类来说是独一份的,没有动态绑定的需要;
友元函数,c++不支持友元的继承,另外友元函数不算类的成员函数,所以没办法。

16.c++的内存管理

c++程序内存分静态区、堆区和栈区,代码区,静态区存放全局数据和静态数据,栈区存放函数调用的局部变量数据,堆区则是程序员自行申请和释放的内存区域,代码区就是执行命令的存放区域。

17.一个空类

一个空类可以被实例化,每个实例在内存中都是有地址的,编译器会给空类隐式添加一字节;一个空类会包含默认构造、默认拷贝、默认析构、默认赋值、默认取址、默认const取址。

18.STL中内存分配器原理(标标标)

内存分配器有两层,第一层,分配大于128kb,直接用operator new,这就是一级内存分配器;第二层,小于128kb,使用二级内存分配器,即内存池。

19.类的默认构造函数

没有定义的情况,默认构造就是一个由编译器提供的,不接受任何参数也不执行任何操作的函数;针对于不显式初始化对象,需要显式定义默认构造;带参数的构造也可以是默认构造,需要所有的参数都有默认值。

20.lambda函数特点,和普通函数相比的优点

c++11后引入,可以理解为匿名的内联函数,和普通函数相比,少了函数名,多了中括号[],lambda的基本语法:
[capture](parameters) specifiers exception attr -> return type { /*code; */ }

capture,捕获列表,外部变量的传递方式;
parameters,参数列表,也即是形参列表;
specifiers exception attr,附加说明符,可以是mutable、noexcept等;
->return,返回类型,大多数情况不需要,编译器会推导,然后后面就是函数体了。
(暂时这么点)

21.简述协程

协程,函数对象,可以设置描点暂停,然后后面在这描点重新运行。后续实现方面和应用方面再添加,具体参考可以看boost方面。

22.c++菱形继承问题和解决方案

在c++继承中,可以声明一个基类A,然后由两个子类分别继承,然后得到子类B和子类C,在此基础上,有一个继承了B和C的子类D,当它想要修改最早基类A的成员的时候,就会出现问题,这就是菱形继承带来的麻烦,解决办法就是B类和C类对基类A进行虚继承,这样得到的就只有一份基类A的元素。

23.只定义析构函数,会自动生成哪些构造函数?

存疑

24.变量的声明和定义的区别

在c++中,变量声明就是只声明了这么个变量,但它没有初始值,这时候对他进行读值的操作得到的是不明确的值,定义就是给一个变量进行初始化使其有值,就是针对一个已经声明的函数进行实现,这就是定义。

25.c++中struct和class的区别

c++中实现类有struct和class两种,前者的成员默认为公有的,后者成员默认是私有的,在继承上看,前者默认公有继承,后者默认私有继承。

26.实现一个函数判断当前是大端序还是小端序

//1
bool isBigEndian(){
    int a = 0x1234;
    char b = *(char*)&a;
    if(b == 0x12)
        return true;
    return false;
}

//2
bool isLittle(){
    {
        union{
            int a;
            char b;
        }c;
        c.a = 1;
        return (c.b==1);
    }
}

27.按照内存地址顺序,从低到高输出一个变量

void show(char* start, size_t len){
    for(size_t i = 0;i<len;++i)
        printf("%.2x", start[i]);
    printf("\n:");
}

int a = 12345;
show((char*)&a, sizeof(int));

28.几种强制转换

static_cast,非多态类型的转换,不执行运行时类型检查,通常来说只用于转换数值数据类型,可以实现子类的向上转换;
dynamic_cast,用于多态类型的转换,转换时进行类型检查,只适用于指针或引用,不明确的指针的转换会返回空指针但不引发异常,可以实现向上转换或者向下转换;
const_cast,取消const的只读语义;
reinterpret_cast,对位的简单重新解释,能实现任何指针转换成其他指针类型,但不安全;
bad_cast,这是一个异常,当强制转换为引用类型失败,dynamic_cast就会引发这个异常。

29.运行时类型信息(RTTI)

typeid,可以在运行时确定对象类型,返回一个type_info对象的引用,只获取对象的实际类型;
type_info,一个类,用来描述编译器在程序中生成的类型信息,可以有效存储指向 类型名 的指针。

30.关于单例

  • c++实现一个不能被继承的类
  • 定义一个只能在堆上(或栈上)生成对象的类
  • 实现一个线程安全的类

上面的问题拆开来其实都是和单例有关,单例模式是保证一个类只有一个实例,类会提供一个全局访问点。要实现一个单例,就需要将构造函数的访问权限设置为protected或者private并提供一个全局访问点也就是一个静态函数来返回实例。

//简单单例
class Singleton{
private:
    Singleton(){}
    ~Singleton(){}
    static Singleton *pInstance;
public:
    static Singleton *getInstance(){
        if(!pInstance)
            pInstance = new Singleton();
        return pInstance;
    }

    static void destroy(){
        if(pInstance){
            delete pInstance;
            pInstance = nullptr;
        }
    }
};

这么一个构造和析构都是私有的类,是不能被继承的,并且它的构造方式只能是通过静态方法getInstance来申请堆上的内存,并通过destroy来销毁,还有一点就是编译器会在编译时检查类的析构函数的可访问性,如果析构是私有的,编译器就不会在栈空间为类对象分配内存,这样就直接实现了上面的两个条件,就差线程安全了。针对只能在栈上生成对象的类:

class Singleton{
private:
    void* operator new(size_t t){}
    void* operator delete(void *ptr){}
public:
    Singleton(){}
    ~Singleton(){}
};

如上,禁用了new和delete就可以了,做法就是operator new()设为私有。至于如何做到线程安全,那就需要引入锁机制了,一般可以是互斥锁,直接在申请实例的时候给上锁,申请结束,解锁:

class Singleton{
private:
    Singleton(){}
    ~Singleton(){}
    static Singleton *pInstance;
public:
    static Singleton *getInstance(){
        if(!pInstance)
            pInstance = new Singleton();
        return pInstance;
    }

    static void destroy(){
        if(pInstance){
            lock();         //伪代码上锁
            delete pInstance;
            pInstance = nullptr;
            unlock();       //伪代码解锁
        }
    }
};

延伸一下的话,还可以优化一下代码,使得它能自动释放,而不用手动调用destroy,因此引入c++的RAII机制,意思是资源获取即初始化机制,有别于RTTI,不同的。这里引入的机制,主要是表示构造中申请资源,析构中释放资源的意思,智能指针就是RAII的代表。具体做法,就是在Singleton类内声明一个静态类,把Singleton的destroy动作交给这个静态类的析构函数:

//简单单例引入RAII
class Singleton{
private:
    Singleton(){}
    ~Singleton(){}
    static Singleton *pInstance;

//存在只为销毁实例
    class Garbo{
    public:
        ~Garbo(){
            if(!pInstance){
                delete pInstance;
                pInstance = nullptr;
            }
        }
    }

    static Garbo garbo;
public:
    static Singleton *getInstance(){
        if(!pInstance)
            pInstance = new Singleton();
        return pInstance;
    }
};

如上所示,garbo静态成员不需要构造,所以不用考虑类外定义的问题,当Singleton退出作用域时,它会被销毁,静态实例的析构会被自动调用,最终得以把pInstance释放掉,不过上面的太大了,还可以进一步优化:

class Singleton{
private:
    Singleton(){}

public:
    ~Singleton(){}
    static Singleton* getInstance(){
        static Singleton instance;
        return &instance;
    }
};

这样就好了,这部分内容是学习自:峰子_仰望阳光的博文,挺受用的。


Jack,不要停。

posted @ 2023-02-02 15:32  夏目&贵志  阅读(113)  评论(0编辑  收藏  举报