八股文:C++

前言

先写这么多,后续再补充

面向对象三大特性:封装、继承、多态

类的访问权限:private、protected、public

在类的内部,无论成员被声明成什么,都可以彼此访问。
在类的外部、只能通过对象访问成员、其中public代表从外部可以访问到该关键字修饰的成员、而private则不能访问、protected则只能允许其类的派生类访问。

类的拷贝控制操作

对于类的拷贝控制操作有5种、其中分为拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数、析构函数
拷贝构造函数:如果一个构造函数的第一个参数是其本身、且额外参数都有其默认值、则称为拷贝构造函数

class Foo{
	public:
	Foo();//默认构造函数
	Foo(const Foo&);//拷贝构造函数
};

拷贝构造函数的第一个参数必须要是一个引用类型,解释如下:
拷贝构造函数用来初始化非引用类类型参数,如果不是引用类型,将会因为
拷贝而无限循环下去
拷贝赋值运算符:用来控制对象如何赋值、需要重载赋值运算符

class Foo{
public:
	Foo&operator=(const Foo&);
};

析构函数:析构函数做与构造函数相反的事情、构造函数初始化对象的非static成员、以及其他工作等。而析构函数则释放对象所用的资源并销毁对象所用的非static成员
什么时候调用析构函数:
无论何时当对象被销毁的时候、就会调用析构函数。
变量离开作用域、当一个对象被销毁、当容器被销毁时、对于动态分配的内存当指向向的它的指针被delete运算符调用时被销毁、对于临时对象当创建它的完整表达式结束时被销毁。
移动构造函数:类似于拷贝构造函数、只不过其参数是右值引用
移动赋值运算符:类似于拷贝赋值运算符、只不过其参数是右值引用

移动构造函数与拷贝构造函数对比

移动构造函数采用浅拷贝,而拷贝构造函数采用深拷贝,其中拷贝构造函数要采用深拷贝会消耗资源其效率也更低,而对于移动构造函数来说采用浅拷贝则直接将传入参数的地址空间给新对象,这样就相当于只是指针的拷贝,其新对象这个”指针“指向的地址空间是传入参数的。

深拷贝和浅拷贝区别

我理解的深拷贝和浅拷贝的区别主要是在有指针这个数据的情况下,浅拷贝就是单纯的拷贝数据,如果有指针类型的数据的话,则会造成有多个指针指向相同的地址空间。而深拷贝会把指针指向的地址空间也会拷贝一份,这样每个指针指向的地址空间就是不同的,当然地址空间中的值是相同的。

空类有哪些函数?空类的大小?

对于一个空类来所,如果什么也不干的话,编译器会生成默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、以及一个取地址运算符、一个const取地址运算符。
空类的大小为1字节,主要是为了区分实例化,如果作为基类的话,则大小为0,这也即是空白基类最优化。这个只适用于单一继承中。
对于类的大小,主要与其中的非静态数据的类型大小相关,同时还与编译器加入的成员变量相关、用来表现语言的某些特性,比如用于动态绑定的特性。同时还与为了优化存储效率而作的某些调整,即字节对齐。

内存分区:全局区,堆区、栈区、常量区、代码区

内存分区自上而下分别是栈区、堆区、全局静态区、常量区、代码区
栈区:地址由大到小,由系统自动管理,主要在函数调用时会进行压栈等操作。由运行时系统自动分配。
堆区:地址由小到大,由人手动分配,需要通过free或者delete手动释放,由运行时分配。
全局静态区:存放全局变量和静态变量,编译时分配。
常量区:存放常量,编译器分配
代码区:存放代码编译时分配。

c++与c的区别

C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制)。
C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。 所以C与C++的最大区别在于它们的用于解决问题的思想方法不一样。之所以说C++比C更先进,是因为“ 设计这个概念已经被融入到C++之中

struct与class的区别

struct 与class几乎没区别,最大的区别就是class默认的修饰符是private而对于struct来说默认的修饰符是public

struct内存对齐

对于其中的数据来说,前一个单元的大小必须是后一个单元的整数倍,如果不是就要对齐。
对于整个结构体的大小来说其大小必须要为最大单元的整数倍。

new/delete与malloc/free的区别

对于new来说首先是调用operator new或者operator new[]运算符来创建一块足够大的内存空间、之后编译器运行构造函数以构造对象、并且初始化;之后返回一个指向对象的指针。
对于delete首先是调用析构函数来、之后调用operator delete或者operator delete[]运算符来释放内存空间。
malloc则是创建一个内存空间,并返回其首地址
free则是释放指针指向的内存空间

内存泄漏的情况

内存泄漏即指内存没来的及释放,这样就称为内存泄漏。
内存泄漏以发生的情况分类由四种:
常发性:发生内存泄漏的代码多次被执行到。
偶然性:其发生内存泄漏的代码只有在特定的环境下才会执行到。
一次性:发生内存泄漏的代码只会被执行一次。
隐藏性:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。

sizeof与strlen对比

sizeof是运算符,返回的是分配的内存大小,而strlen是函数返回的是字符串的长度。

指针与引用的区别

指针是一个变量存储的是地址,指向存储单元,而引用是变量的别名。

野指针产生和避免

野指针错误访问主要有两种情况,一种是访问了不该访问的内存,比如内核区内存,一种是访问了已经释放的内存。
避免:在定义指针的时候首先把指针初始化为nullptr

多态:动态多态和静态多态

对于静态多态,就是调用的函数时已经在编译期间就已经确定的,如果没有使用virtual关键字修饰,就会是静态多态,其访问函数取决于对象的类型。比如base a=new derived(),a.f(),假设基类和派生类都有函数f(),则a.f()访问的是基类的a.f()而不是派生类的f()
对于动态多态则是在运行期间来确定的,其需要使用virtual关键字修饰需要动态绑定的函数,其利用指针或者引用就能实现动态绑定,其所调用的真正函数取决于对象的真正类型。

虚函数实现动态多态的原理,虚函数与纯虚函数的区别

虚函数实现动态多态的原理主要是利用虚指针和虚表来表示的,只要类中有虚函数,则编译器会生成一个vptr,vptr指向虚函数表,虚函数表中存储的是继承下来的以及自己的虚函数,之后利用*(vptr->vtab[n])(p )的方式调用对应的函数,实现了动态绑定。虚函数与纯虚函数区别就是虚函数是基类需要去实现,派生类可以重写也可以不重写,而纯虚函数基类没有实现,其派生类必须要去实现纯虚函数。

继承时父类的析构函数是否为虚函数,构造函数能不能为虚函数

父类析构函数建议弄虚函数,主要例子是假设这样的情况 base*ptr=new derived(),则当delete ptr时,如果析构函数未用virtual则只会delete 父类,即调用父类的析构函数,如果使用了vitrual 则由于动态绑定,会首先调用子类的析构函数,之后又会调用父类的析构函数。构造函数不能为虚函数,对虚函数的调用是通过虚指针来实现的,而这个是在对象实例化之后才建立好的,而构造函数是为了实例化对象,对象都还没实例化也就无法通过虚指针来调用,虚函数是为了实现动态绑定的,而动态绑定需要通过引用或者指针来实现,构造函数是在对象实例化过程中调用的,没这个用途。

静态多态:重写、重载、模板

static关键字

1.修饰局部变量,即在函数内部,这样的静态变量声明周期长于函数。
2.用在文件级别用来修饰变量或者函数,这样的变量或者函数对其他模块是不可见的。
3.用在修饰class中的数据成员,每个class只有一份静态变量
4.用来修饰class中的成员函数,其中静态成员函数只能访问静态成员数据或者静态成员变量。

const关键字

修饰变量,指针,类对象,类中成员函数,引用

extern关键字

用在变量或者函数前,表示这个变量或者函数是在别的模块定义了,extern只是声明,所以没有给变量或者函数分配内存空间。

volatile关键字

避免编译器指令优化

四种类型转换

const_cast:用于把const属性擦除
static_cast:用于数据类型的转换
dynamic_cast:用于多态中的向上转型。
reinterpret_cast用于二进制层面上的转换,非常危险

右值引用

对于左值和右值的区分
1.拥有身份:即非临时对象
2.可被移动:即可被右值引用类型匹配
其中左值即为拥有身份且不可移动
右值分为两种:将亡值和纯右值
将亡值:即拥有身份且可被移动,std::move一般就是将亡值
纯右值:即不拥有身份且可移动
右值引用即是为了配合移动语义的类型,利用&&匹配,可进行浅拷贝直接将指向对象内存的指针复制过来。从而实现对象的拷贝。

std::move函数

可以将左值对象转化成将亡值

四种智能指针及其底层实现

智能指针都是可以实现自动释放资源
auto_ptr:是独占的智能指针,其指向的资源不能再由其他指针指向,但由于其使用的是拷贝语义,容易造成内存泄漏。
unique_ptr:也是独占的智能指针,但是其使用的是移动语义,这样就实现了一份资源只会一个指针占用,且由于移动后其指针会置为空指针,当delete时可以在编译期发现错误。
shared_ptr:采用引用技术机制,可以实现多份指针指向同一个资源,只有当计数为0时才会释放资源。
代码实现:

#include

using namespace std;

template<typename T>class SP
{
public:
    SP() :p(nullptr), use(nullptr) {};//默认构造函数
    explicit SP(T* ptr) :p(ptr), use(new size_t(1)) {};//构造函数
    SP(const SP<T>& sp) :p(sp.p), use(sp.use) { ++* use; };
    SP& operator=(SP<T>& sp);//拷贝赋值运算符
    ~SP();
    T& operator*() { return *p; }
    T& operator*()const { return *p; }
private:
    T* p;
    size_t* use;
};
template<typename T>
SP<T>::~SP()
{
    if(use)std::cout << "现在拥有的引用指针数为" << *use << endl;
    if (use && -- * use == 0)
    {
        delete use;
        delete p;
    }
}
template<typename T>
SP<T>& SP<T>::operator=(SP<T>& rhs)
{
    if (rhs.use)
        ++* rhs.use;
    if (use && -- * use == 0)//为了防止自我赋值,如果是自我赋值的话,则use就直接有了
    {
        delete use;
        delete p;
    }
    p = rhs.p;
    use = rhs.use;
    return *this;
}
int main()
{
    {
        int* ptr = new int;
        *ptr = 10;
        SP<int>sp1(ptr);
        SP<int>sp2;
        sp2 = sp1;
    }
    return 0;


}

weak_ptr:主要是防止shared_ptr实现循环引用而提出的智能指针,它的作用是起到对shared_ptr的观测作用,其在shared_ptr中使用不会导致引用计数的变化。

shared_ptr中的循环引用如何解决

对于循环引用主要是两个类中含有双方的智能指针,则当对这两个类进行初始化后,并且对其中的智能指针数据进行赋值操作,则会造成双方的引用计数都是2,这样的话在进行析构函数的时候就无法释放资源,从而造成内存泄漏。解决的方法是采用weak_ptr,其中weak_ptr是用来辅助shared_ptr,所以需要在shared_ptr中使用,对于weak_ptr使用,不会使对应智能指针类的引用计数增加,这样在进行析构函数的时候就可以有一个类能够释放资源,从而将其中另一个类的智能指针成员也析构掉,从而将另一个类的引用计数减少为1,从而使另一个类能够释放掉。

vector与list比较

vector:
底层结构:动态顺序表,一段连续空间
随机访问:支持
插入和删除:效率较低
空间利用率:底层为连续空间,空间利用率高
迭代器:原生态指针
迭代器失效:插入元素,需要给所以迭代器重新赋值,因为插入元素可能会导致重新扩容。删除时当前迭代器需要重新赋值,否则会失效。
list:
底层结构:带头节点的双向循环链表
支持随机访问:不支持
插入和删除:效率很高
空间利用效率:底层节点动态开辟,小节点容易造成内存碎片,空间利用率低。
迭代器:对原生节点进行封装
迭代器失效:掺入元素不会导致迭代器失效,删除元素只会导致当前迭代器失效。

vector迭代器失效的情况

push_back导致扩容,从而导致失效
插入操作导致扩容或者元素移动,从而导致失效
删除操作导致元素移动,从而导致元素失效。

map与unordered_map对比

map内部实现机制时红黑树,unordered_map内部实现机制是哈希表,map结构会使元素具有有序性,但是由于是使用了红黑树,所以空间占有率高,unordered_map内部实现的是哈希表,所以unordered_map的查找效率很高,但是其建立哈希表的空间消耗较高。

set与unordered_set对比

与上面的对比类似

STL容器空间配置器

你可能感兴趣的:(八股文,八股文)