C++面经汇总之C++基础(四)

27、 C++中内存泄漏的几种情况

内存泄漏是指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

1)类的构造函数和析构函数中new和delete没有配套

2)在释放对象数组时没有使用delete[],使用了delete

3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露

4)没有正确的清楚嵌套的对象指针

28、 友元函数和友元类⭐️

通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。

1)友元函数

友元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。

friend 类型 函数名(形式参数);一个函数可以是多个类的友元函数,只需要在各个类中分别声明。

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

friend class 类名;

使用友元类时注意:

(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

29、C++如何防止内存泄露

内存泄露本质就是new 和delete没有配对使用,因此可以用只能指针和RAII机制(在构造函数中调用new,在析构函数中调用delete)

30、智能指针问题⭐️

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

1、shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
2、在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
3、如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4、如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

shared_ptr的线程安全问题

1、智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或– ,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数++、-–是需要加锁的,也就是说引用计数的操作是线程安全的。

2、智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

shared_ptr的引用计数是线程安全的,但是指向的对象不是线程安全的! 虽然通过原子操作解决了引用计数的计数的线程安全问题, 但是智能指针指向的对象的线程安全问题,智能指针没有做任何的保证。 首先智能指针有两个变量,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象, 当智能指针发生拷贝的时候,标准库的实现是先拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子的,隐患就出现在这里

循环引用问题

class B;
class A {
public:
    shared_ptr<B> p; //weak_ptr<B> P;
};

class B {
public:
    shared_ptr<A> p; //weak_ptr<A> P;
};

int main() {
    while (true) {
        shared_ptr<A> pa(new A());
        shared_ptr<B> pb(new B());
        pa->p = pb;
        pb->p = pa;
    }
    return 0;
}

在while循环中,先是在栈中构造了两个智能指针,分别管理两块堆内存,记为A, B。然后两个赋值语句,使得在shared_ptr中,A,B的引用计数均为2,所以在析构掉pa与pb时,他们的引用计数都没能到达0,于是发生了循环引用,于是开始内存泄露

解决方案也很简单,将类A,B中的一个shared_prt改为weak_ptr即可,weak_ptr不会增加shared_ptr的引用计数,所以在pa,pb中会有一个的引用计数为1,在它析构时,会正确的释放掉内存。

所以shared_ptr是用来共享内存,而weak_ptr是用来避免循环引用的。

下面总结一下四种智能指针:

1、shared_ptr

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

2、weak_ptr

(1)引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。 需要使用weak_ptr打破环形引用。

(2) weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数

(3) 如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。

3、 unique_ptr

​ 一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空; 所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中; 如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。

​ unique_ptr 是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个指针拥有。一旦拥有着被销毁或编程empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。 unique_ptr 用于取代 auto_ptr。

4、auto_ptr

auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,auto_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个auto_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空。

31、大端小端

小端:高地址存放高位字节、低地址存放低位字节

大端:高地址存放低位字节、低地址存放高位字节

比如0x12 34 56 78

在这里插入图片描述

32、定义一个只能在堆上(栈上)生成对象的类⭐️

【前提知识】 在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。 静态建立一个类对象,是由编译器为对象在栈空间中分配内存 , 直接调用类的构造函数。 动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

1、只能建立在堆上: 将析构函数设置为私有

原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

2、只能在栈上生成对象:将new 和 delete 重载为私有

原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。 将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。

33、排序算法⭐️

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUozVs3k-1632102148077)(image_面经/1628667529759.png)]

排序稳定性是指,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。

1、插入排序

插入排序有两种,一是直接插入排序,二是希尔排序。

**插入排序的思想:**一个数组a[0:n]需要进行排序,假设认为a[0:n-1]都是有序的,a[n]和前面已经有序的元素向比较,直到找到第一个小于a[n]的元素,并将a[n]插入到这个元素后面。

希尔排序的思想: 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

2、选择排序

思想:第一次:从未排序的元素里找到最小的,放在第一个位置;第二次:仍然从为排序的数组里找剩下的最小的元素,放在第二个位置…以此类推,直到所有元素都找完。

3、冒泡排序

思想:从为排序的元素里进行两两比较,根据大小交换位置,直到最后将最大的元素交换到最末尾;下一轮仍然从为排序元素里进行两两比较,重复这个过程。算法核心是 每次通过两两比较交换位置,选出剩余无序序列里最大的数据元素放到队尾。

4、快速排序

思想:将数组中的任意一个数作为基准数key,然后把比Key小的数放在Key的左边,大的放在右边,然后再对左右两个区间进行递归调用,直到两个区间只剩一个元素。

34、C++的四种强制类型转换⭐️

1、const_cast:用于去常量化(将原本是常量的指针或引用变为普通的指针和引用)

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

2、static_cast:静态转换

(1)用于基本数据类型的转换,如int转换为char等。

(2)用于基类和子类之间指针或引用的上下转换。(上行转换:子类指针转换为基类指针,这个是安全的;下行转换:基类指针转换成子类表示,这是不安全的)

/* 常规的使用方法 */
float f_pi=3.141592f
int   i_pi=static_cast<int>(f_pi); /// i_pi 的值为 3

/* class 的上下行转换 */
class Base{
    // something
};
class Sub:public Base{
    // something
}

//  上行 Sub -> Base
//编译通过,安全
Sub sub;
Base *base_ptr = static_cast<Base*>(&sub);  

//  下行 Base -> Sub
//编译通过,不安全
Base base;
Sub *sub_ptr = static_cast<Sub*>(&base);    

3、dynamic_cast:动态转换

(1) dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

(2) 使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。这是因为: 类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。而dynamic_cast是要在运行中才确定转换类型的,因此需要虚函数。

(3) 在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

比如定义了一个派生类指针,当我们将其指向一个基类对象时,这是错误的,因此我们需要将这个派生类指针转换成基类指针

4、reinterpret_cast

reinterpret_cast主要有三种用途: 改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。 他是用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换,在下面的文章中将给出。请看一个简单代码

#include<iostream>
#include<cstdint>
using namespace std;
int main() {
    int *ptr = new int(233);
    uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);
    cout << "ptr 的地址: " << hex << ptr << endl
        << "ptr_addr 的值(hex): " << hex << ptr_addr << endl;
    delete ptr;
    return 0;
}
/*
ptr 的地址: 0061E6D8
ptr_addr 的值(hex): 0061e6d8
*/

35、右值引用

(1)什么是左值和右值

<1>可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。比如:

int a = 5;
5 = a; //错误,5 不能为左值
其中,变量 a 就是一个左值,而字面量 5 就是一个右值。值得一提的是,C++ 中的左值也可以当做右值使用,例如:
int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用    

<2>有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。

以上面定义的变量 a、b 为例,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。

(2)右值引用

​ 其实 C++98/03 标准中就有引用,使用 “&” 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:

int num = 10;
int &b = num; //正确
int &c = 10; //错误
编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。

虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:

int num = 10;
const int &b = num;
const int &c = 10;

和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
int && a = 10;
a = 100;
cout << a << endl;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值