c++八股文整理(七)

1. 顶层const, 底层const 的区别

 

  1. 顶层const和底层const都是相当于指针而言的,比如说const int a = 10,这个const既不是顶层也不是底层,因为a不是指针。是啥????
  2. 如何看const是修饰的什么的原则是:谁在const的左边就修饰谁,如果const的左边没有任何东西,就修饰右边的第一个值,以下举例说明:
  • const int const * p,第一个const的左边没有东西,所以修饰的是右边的int,第二个const左边有int,所以修饰的也是int,这两个const都修饰的是int,所以这句话会报错
  • const int * const p,第一个const的左边没有东西,所以修饰右边的int,第二个const的左边有*,所以修饰的是*。
  • int const * const p,第一个const的左边是int,所以修饰int,第二const的左边是*,所以修饰的是*。

     3. 怎么区分顶层const和底层const呢?

  • 顶层const修饰的是*,指针本身不能变,但它指向的内容可以改变。比如说int * const p,可以改变 * p,但不能改变p本身。
  • 底层const修饰的是类型,指针可以改,它变量本身不能改变,比如说const int* p,p是可以改变的,但是p所指向的内容*p不能改变。指针指向的对象不可变。不能使用解引用运算符*对其进行修改。
  • 对于类型转换const_cast,只能去除底层const
    int a = 10;
    // 顶层const
    int *const pTop = &a;
    // 底层const
    const int *pDeep = &a;
    // 正确。const_cast去除了pDeep指针的底层const属性
    int *pNormal = const_cast<int*>(pDeep); 
    *pNormal = 20;
    cout << a << endl; // 20
    
    // const_cast无法去除顶层const
    int b = 100;
    const_cast<int*>(pTop) = &b; // 错误
    
  • 顶层const的指针可以拷贝赋值给普通的指针,但是底层const的指针无法拷贝赋值给普通的指针。这便是两者在拷贝时的差别。
    int a = 10;
    // 顶层const
    int *const pTop = &a;
    // 底层const
    const int *pDeep = &a;
    
    int *pNormal = pTop; // 正确。带有顶层const的指针能够赋值给普通指针
    int *pNormal = pDeep;// 错误,const int * 不能转换为 int *
    

2.  auto、decltype和decltype(auto)的用法

auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值。

decltype 作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值

auto和 decltype的区别主要有:

第一:auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。

第二:编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层const,而把底层const保留下来。与之相反,decltype会保留变量的顶层const。

decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:

int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

3. 关于lambda函数的全部知识

一个是lambda表达式没有函数名;没有声明返回类型。实际上lambda的返回类型是根据返回值推断得到的。
lambda表达式的语法形式:

[ capture ] ( params ) opt -> ret { body; };

//例 int count = count_if(vec.begin(),vec.end(),[](int x){return x % 7 == 0;} 

其中carpture是捕获列表,params是参数,opt是选项,ret则是返回值的类型,body则是函数的具体实现。
1.捕获列表描述了lambda表达式可以访问上下文中的哪些变量。
[] :表示不捕获任何变量
[=]:表示按值捕获变量
[&]:表示按引用捕获变量
[this]:值传递捕获当前的this
但是捕获列表不允许变量的重复传递:例如

[=,x]

上面这种捕获是不允许的,=表示按值的方式捕获所有的变量,x相当于被重复捕获了。
2.params表示lambda的参数,用在{}中。
3.opt表示lambda的选项,例如mutable,后面会介绍一下mutable的用法。
4.ret表示lambda的返回值,也可以显示指明返回值,lambda会自动推断返回值,但是值得注意的是只有当lambda的表达式仅有一条return语句时,自动推断才是有效的。像下面这种的表达式就需要加上返回类型。

[](double x )->double{int y = x ;return x - y;};

虽然lambda表达式是匿名函数,但是实际上也可以给lambda表达式指定一个名称,如下表示:

auto f = [](int x ){return x % 3 ==0;};

此后再需要使用该lambda表达式,就可以使用f()来代替。举一个例子:

#include <iostream>
using namespace std;
int main()
{
    int a = 5,b = 6;
    auto f = [=]{return a+b;};//[=]按值捕获了a和b
    cout << f() << endl;
    return 0;
}

运行程序输出结果为11,由上栗看出可以给lambda指定一个名称,再用该名称来取代调用该表达式。

按值捕获和按引用捕获

按值捕获和按引用捕获的用法通过下面这个例子来看一下。

#include <iostream>
using namespace std;
int main()
{
    int a = 5;
    auto f1 = [=]{return a+1;};//按值捕获a。a是const修饰,不可变
    auto f2 = [&]{return a+1;};//按引用捕获a。 a 可变
    cout << f1() << endl;
    cout << f2()<< endl;
    a++;
    cout << f1() << endl;
    cout << f2() << endl;
    return 0;
}

// 结果:
// 6
// 6
// 6
// 7

按值捕获可以理解为一旦lambda按值捕获某个变量相当于在表达式内部已经生成了一个被捕获变量的副本,而lambda表达式使用的就是这个副本,简单可以理解为f1表达式赋值了一个和a同名的const变量。从这我们也可以得出一个结论:
如果希望lambda函数在调用时访问的外部变量是最新的,我们就需要使用按引用捕获。
下面可以看一下这个例子:

#include <iostream>
using namespace std;
int main()
{
    int a = 5;
    auto f = [=]{return a*=5;};//按值捕获a
    cout << f() << endl;
    return 0;
}

运行程序,有如下编译报错:

 提示a是一个只读的,不允许修改,这就验证上面例子中说明的按值捕获实际上是lambda拷贝了一个与被捕获变量同名的const 副本并进行操作。
如果实在需要改变lambda中的值,这时就需要使用上文提到过的选项mutable。
默认情况下,lambda函数是一个const函数,而mutable也可以取消常量性。例如有如下代码:

#include <iostream>
using namespace std;
int main()
{
    int a = 5;
    auto f = [=]()mutable{return a*=5;};//取消常量性
    cout << f() << endl;
    return 0;
}

// 结果
// 25

这里需要注意,被 mutable 修饰的 lambda 表达式就算没有参数也要写明参数列表
可以看到原先lambda函数中的a是只读的,加上mutable就可以修改lambda函数中a的值了。

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

//普通指针
int *q = new int(42), *r = new int(100);
r = q;
//智能指针
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;

原理

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

(1) shared_ptr

实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象。

  • 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
  • 每次创建类的新对象时,初始化指针并将引用计数置为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;//指向引用计数的指针
};

5. 智能指针的作用

  • 引入智能指针,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
  • 智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的​​​​​​​
  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.

6. 说说你了解的auto_ptr作用

  • auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;
  • auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr<Type>类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;
  • auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;
  • 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;
  • Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;
  • auto_ptr支持所拥有的指针类型之间的隐式类型转换。
  • 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;
  • T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。

7.  手写实现智能指针类需要实现哪些函数?

      一个构造函数、拷贝构造函数、复制构造函数、析构函数、移动函数;

8. 智能指针出现循环引用怎么解决?

弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方。弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值