[C++](26)智能指针

2023-11-10

引入

首先看下面这个程序:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0) throw invalid_argument("除以0错误");
	return a / b;
}

void func()
{
	int* p1 = new int[10];
	int* p2 = new int[10];

	div();

	delete[] p1;
	delete[] p2;
}


int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

这个程序最尴尬的地方就在于 func 函数,这里有 3 个地方可能会抛异常,如果 p1 的 new 抛异常,那么直接跳到 main 函数去处理,没问题;如果 p2 的 new 抛异常,跳出去后就会导致 p1 没有释放,内存泄漏;如果 div() 抛异常,那么就会导致 p1 和 p2 都没有释放。

如何解决呢?

如果在 func 函数内进行捕获再抛出呢?

void func()
{
	int* p1 = new int[10];
	int* p2 = new int[10];

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		throw;
	}

	delete[] p1;
	delete[] p2;
}

如果 div() 抛出异常就在 func 函数内部捕获然后释放已经开辟的 p1 和 p2 后抛出。但是这里还有一个问题,如果 p2 抛异常呢?p2 抛异常就会直接跳出到 main 函数,导致 p1 未释放。如果把 p2 放到 try 块里面,又会导致最后一行正常释放的 delete[] p2; 报未定义的错误。

所以这里用 try/catch 解决比较困难也比较麻烦。更好的解决方式就是使用我们下面要学的智能指针。

智能指针的原理

RAII

RAII (Resource Acquisition Is Initialization) “资源获取即初始化”,是C++管理资源,避免泄漏的一种方法。它利用对象的生命周期来控制资源(如内存、文件句柄、网络链接、互斥量等)。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效最后在对象析构的时候释放资源,这实际上是把管理一份资源的责任委托到一个对象上。这样做我们就不需要显式释放资源


下面我们简单地包装一个智能指针类,然后把 new 出来的指针交给这个类的对象管理:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0) throw invalid_argument("除以0错误");
	return a / b;
}


void func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[10]);
	SmartPtr<int> sp3(new int[10]);
	SmartPtr<int> sp4(new int[10]);

	div();
}


int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

这样只要 sp1、sp2、sp3、sp4 这些对象调用析构函数就能释放空间,对象的生命周期跟着函数跑,我们不用担心它不会被销毁。

我们上面实现的智能指针还不完整,还需要实现指针的行为,不过这已经体现了 RAII 的核心思想。

// 重载*和->,使之可以像指针一样使用
T& operator*()
{
    return *_ptr;
}

T* operator->()
{
    return _ptr;
}

C++智能指针及其问题

智能指针的基本思想有了,但是它的实现却面临着两大难题

  1. 拷贝问题
  2. 循环引用问题

我们下面来看C++是如何解决这两个问题的

回想普通指针之间的拷贝,就是让两个指针指向同一块空间,不会开辟新的空间,但是两个智能指针如果指向同一块空间,那么它的两个对象一共析构两次,导致同一块空间被释放两次,程序崩溃。

auto_ptr

我们先看 C++98 是如何解决这个问题的

C++98 的智能指针:auto_ptr

它的核心思想是管理权转移,即直接将指针的管理权移交给要拷贝的对象

auto_ptr<int> sp1(new int(10));
auto_ptr<int> sp2 = sp1;

cout << *sp1; // 运行此处时程序崩溃
cout << *sp2;

这显然不合理啊,原来的智能指针不能用了,不符合拷贝的含义。

我们可以轻松实现它的拷贝构造:

auto_ptr(auto_ptr<T>& sp)
    : _ptr(sp.ptr)
{
    sp._ptr = nullptr;
}

目前 auto_ptr 已被废弃,很多公司命令禁止使用 auto_ptr


unique_ptr

C++11 智能指针发展得较为成熟,引入了 3 类智能指针

该智能指针非常简单粗暴,就是明确表示不允许拷贝。

unique_ptr<int> sp1(new int(10));
unique_ptr<int> sp2 = sp1;	//错误	C2280	尝试引用已删除的函数

C++11 直接使用 delete 禁止了其默认拷贝构造和赋值重载的生成

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

防拷贝还有一种方式:将拷贝构造和赋值重载显式声明出来,但是不去实现,并把它设为 private,防止其他人在类外实现。


shared_ptr

该智能指针真正解决了拷贝问题,可以当成普通的指针来使用。

它的核心原理是,采用引用计数共有多少个对象管理同一块资源,最后一个析构的对象负责释放资源。

要让多个对象共用一个成员变量进行计数,静态成员变量行不行?

显然不行,因为静态成员变量是属于所有一个类的所有对象的,不只是指向同一块空间的所有对象。

解决方式:

  • 给一个成员变量 int* _pCount; 它指向的是一个 int,是用来计数的空间,我们让它的计数空间和 _ptr 的资源同时分配(在构造函数中给 _pCount new 一个 int 并初始化为 1),保证一块资源配一个计数空间(计数器)
  • 在拷贝时,只需要浅拷贝这两个成员变量,然后将计数器 *_pCount +1。保证两个对象指向的是同一块资源和同一个计数器。
  • 一个对象析构时,将计数器*_pCount -1,如果减到0,则释放资源和计数器。
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		: _ptr(ptr)
		, _pCount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pCount) == 0 && _ptr)
		{
			delete _ptr;
            delete _pCount;
			_ptr = nullptr;
            _pCount = nullptr;
		}
	}

	shared_ptr(const shared_ptr<T>& sp)
		: _ptr(sp.ptr)
		, _pCount(sp._pCount)
	{
		++(*_pCount);
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	T* get()
	{
		return _ptr;
	}

private:
	T* _ptr;
	int* _pCount;
};

赋值重载的实现稍微麻烦一点

  1. 首先处理自己给自己赋值的情况,即两个相同的智能指针对象之间赋值是没有意义的,另外,两个指向同一块空间的不同智能指针对象之间赋值也是没有意义的,所以我们不去比较 this&sp,而是直接比较_ptrsp._ptr
  2. 被赋值的智能指针会被改变指向,那么其原来管理的资源的计数器应该-1,如果计数器为0,就把原来的资源和计数器释放。
  3. 将要赋值的成员变量赋过去,并且让计数器+1。
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    if (_ptr == sp._ptr) return *this;

    if (--(*_pCount) == 0)
    {
        delete _ptr;
        delete _pCount;
        _ptr = nullptr;
        _pCount = nullptr;
    }
    _ptr = sp._ptr;
    _pCount = sp._pCount;
    ++(*_pCount);
    return *this;
}

weak_ptr

解决了拷贝问题,智能指针还面临一个问题——循环引用

用智能指针让两个双链表的结点互连,代码如下:

struct ListNode
{
	std::shared_ptr<ListNode> _next = nullptr;
	std::shared_ptr<ListNode> _prev = nullptr;
	int _val = 0;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

void test1()
{
	std::shared_ptr<ListNode> p1(new ListNode);
	std::shared_ptr<ListNode> p2(new ListNode);

	p1->_next = p2;
	p2->_prev = p1;
}

程序运行结果发现 ListNode 的析构函数没有被调用,这是怎么回事?

在两个结点互连之后,两个结点资源对应的计数器都为2(一个资源有2个只能指针维护,如 p1 指向的结点还有 p2->_prev 指向),函数结束后智能指针 p1 p2 被销毁,两个结点的计数器都变为1,没有变为0,无法释放。此时如果要让一个结点释放->那么必须先把它的计数器变为0->要让另一个结点的成员指针释放->要让另一个结点释放,这个逻辑死循环了,所以两个都不会释放。

怎么解决呢?

一个方案就是,让结点内部指针 _prev_next 指向的时候不使对应的资源的计数器+1,那么它们俩用原生指针行不行?不行,因为原生指针无法接受智能指针类型 p1->_next = p2; p2->_prev = p1; 这两句会报错。


为了解决这个问题,C++引入了智能指针 weak_ptr

weak_ptr - C++ Reference (cplusplus.com)

该指针不参与资源的创建与释放,它的特点是 share_ptr 拷贝给它的时候不会让计数器+1,它的释放也不会让计数器-1

其构造函数原型:

//default (1)	
constexpr weak_ptr() noexcept;
//copy (2)	
weak_ptr (const weak_ptr& x) noexcept;template <class U> weak_ptr (const weak_ptr<U>& x) noexcept;
//from shared_ptr (3)	
template <class U> weak_ptr (const shared_ptr<U>& x) noexcept;

作为 share_ptr 的辅助,它只支持默认构造,拷贝构造,传入 share_ptr,不支持传入原生指针。

要解决上面的问题,我们只要把 ListNode 定义的 _next_prev 改成 weak_ptr 就可以了:

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val = 0;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

下面是 weak_ptr 的简易实现(库里的实现还要复杂很多,此处不细讲):

template<class T>
class weak_ptr
{
public:
	weak_ptr()
		: _ptr(nullptr)
	{}

	weak_ptr(const shared_ptr<T>& sp)
		: _ptr(sp.get())
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
    
private:
	T* _ptr;
};

总结:

使用智能指针能避免大部分的内存泄漏问题,但是还有少部分场景不注意会出现内存泄漏,比如循环引用问题,如果程序员没识别出这个场景并改用 weak_ptr 还是会内存泄漏。相比之下,unique_ptr 更安全一些,也更节省空间,在不需要拷贝的场景,推荐使用 unique_ptr

删除器

又一个问题,智能指针的析构函数释放资源用的是 delete,是写死的,如果 new 的时候用了 [] 就会出现不匹配的问题。不仅如此,如果指向的资源是 malloc 出来的呢,又或者智能指针是一个文件指针呢?

std::unique_ptr<Date> up1(new Date[10]);
std::unique_ptr<FILE> up2((FILE*)fopen("test.cpp", "r"));
std::unique_ptr<Date> up3((Date*)malloc(sizeof(Date)));

我们来看 C++ 是如何解决的

unique_ptr类模板原型:

//non-specialized	
template <class T, class D = default_delete<T>> class unique_ptr;
//array specialization	
template <class T, class D> class unique_ptr<T[],D>;

可以看到,这里提供了一个模板参数 class D = default_delete<T> ,这就是删除器,它支持传入仿函数类型,可以由我们自己定制。

要 delete 多个对象,我们就可以传入这样一个仿函数:

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

std::unique_ptr<Date, DeleteArray<Date>> up1(new Date[10]);

释放 malloc 出来的内存:

template<class T>
struct Free
{
	void operator()(T* ptr)
	{
		free(ptr);
	}
};

std::unique_ptr<Date, Free<Date>> up3((Date*)malloc(sizeof(Date)));

关文件:

struct Fclose
{
	void operator()(FILE* ptr)
	{
		fclose(ptr);
	}
};

std::unique_ptr<FILE, Fclose> up2((FILE*)fopen("test.cpp", "w"));

unique_ptr 的改造,使其支持传入定制删除器:

template<class T>
struct default_delete
{
	void operator()(T* ptr)
	{
		delete ptr;
	}
};

template<class T, class D = default_delete<T>>
class unique_ptr
{
public:
	~unique_ptr()
	{
		//delete _ptr;
        D()(_ptr);
	}
    
    //...

shared_ptr 支持定制删除器的方式有点不一样,它是在构造函数部分传函数对象支持的

其构造函数原型一部分:

//with deleter (4)	
template <class U, class D> shared_ptr (U* p, D del);
template <class D> shared_ptr (nullptr_t p, D del);

使用方式:

std::shared_ptr<Date> sp1(new Date[10], DeleteArray<Date>()); // 传函数对象
std::shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; }); // 传lambda表达式

传对象显然更轻松一些,但是 unique_ptr 不支持,因为析构函数必须是无参的,拿不到函数对象,shared_ptr 在库里面实现比较复杂,支持传函数对象。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

[C++](26)智能指针 的相关文章

  • 类变量在其定义范围内?

    这可能是一个愚蠢的问题 我正在尝试制作文本泥 我需要每个 Room 类包含其他 Room 类 以便在尝试移动到它们或从它们获取信息时可以引用 但是 我不能这样做 因为我显然无法在其定义中声明一个类 那么 我该怎么做呢 当我说我做不到时 我的
  • 如何在自定义保存操作 WFFM 中获取 Sitecore.Current.Site 对象?

    我在用着面向营销人员的 Sitecore 网络表单 在里面save action我得到的表格Sitecore Context Site对象 但该对象没有返回正确的上下文 该值为 modules shell 有谁知道我如何才能获得正确的上下文
  • 你好世界,裸机 Beagleboard

    我正在尝试在我的 Beagleboard xm rev 上运行 hello world 类型的程序 C 通过调用 Cputs功能来自装配 到目前为止 我一直使用这个作为参考 http wiki osdev org ARM Beagleboa
  • 递归显式模板实例化以导出库的符号

    在我之前的问题中我问过递归显式模板实例化是否可能 https stackoverflow com questions 7395113 is recursive explicit template instantiation possible
  • 递归显式模板实例化可能吗?

    给定一个类似的模板 template
  • 有没有一种简单的方法来获取 .NET 为参数化查询生成的“sp_executesql”查询?

    背景 如果我有以下程序 public class Program public static void Main using var connection new SqlConnection Server local Database Te
  • 管理多个解决方案中存在的 C# 项目的 Nuget 包

    我现在正处于这样的阶段重构疯狂一个有很多的大项目遗留和不稳定的模块 我决定拆分解决方案目前 所有项目 大约有 20 个 而且由于下个月肯定会出现单元测试项目 还会有更多 链接在一起 使其更加独立和细化 通过这种方法 有一些模块 例如需要在多
  • 字典的线程安全

    如果我有一个 Dictionary
  • 轮廓积分算法 C++

    我正在尝试编写一个应用数学程序来计算复平面中的轮廓积分 对于初学者来说 我想为梯形方法编写一个算法 但我有点坚持理解它会是什么样子 毕竟 我们通常将梯形方法视为 2D 图 而这里我们有 f C gt C 所以我们谈论的是 4D 最终我希望用
  • 可以轻易移动,但不可轻易复制

    是否可以创建类类型 即 普通移动构造 但不是普通复制构造 但仍然可复制构造 普通复制构造 但不是普通移动构造 但仍然可移动构造 可以简单地复制分配 但不能简单地移动分配 但仍然可以移动分配 可以简单地移动分配 但不能简单地复制分配 但仍然可
  • 将控制台输入和输出重定向到文本框

    您好 提前致谢 我正在尝试 非常努力 将控制台输入和输出重定向到文本框 到目前为止 输出工作正常 但问题在于输入 例如 我无法执行一个简单的程序来执行以下操作 Console WriteLine 请输入您的姓名 字符串名称 Console
  • 变形:Opencv 使用 Visual Studio 将图像显示到曲面屏幕

    我正在尝试使用 opencv API 来扭曲图像 以便将其显示到曲面屏幕上 我已经浏览了opencv中提供的翘曲apihere http docs opencv org 2 4 modules stitching doc warpers h
  • 使用 openssl 库获取 x509 证书哈希

    我目前正在开发一个应用程序 它使用 openssl 库 libcrypto 来生成证书 现在我必须获取现有证书的哈希值 当我使用终端时 我可以使用以下命令生成哈希值 openssl x509 hash in cert pem noout 输
  • 给出对象的指针作为参数

    假设我有 void func foo obj 我有 foo object 我该如何制作object进入争论func 只需取消引用它即可 func object
  • 有没有办法对 RichTextBox 的撤消历史进行分组或暂时禁用它?

    我目前正在研究 WPF 中 RichTextBox 内的表格 在 WPF 中 表格没有行和列 只有行 每行都有一定数量的单元格 当用户按下 添加列 按钮时 我的程序会向每一行添加一个新单元格 使用此方法的问题是 在用户添加一列后 如果他们按
  • 如何从 MongoDB 中的 ChangeStream 过滤对特定字段的更新

    我正在设置一个 ChangeStream 以便在集合中的文档发生更改时通知我 以便我可以将该文档的 LastModified 元素更新插入到事件发生的时间 由于此更新将导致 ChangeStream 上发生新事件 因此我需要过滤掉这些更新以
  • 警告从 lambda 返回捕获的引用

    我尝试使用 lambda 有条件地将引用绑定到两个变量之一 int foo bar int choice gt int if true some condition return foo else return bar 这会在 clang
  • wpf中的图像问题(图像不显示)

    我不明白为什么我无法在 WPF 中显示图像 也许我不小心修改了我的资源文件夹 这就是我没有显示的原因 所以我创建了一个新的 wpf 应用程序 我有这个 当我运行该程序时 我的图片显示为 为什么当我尝试在程序中执行相同的操作时 图像没有显示
  • 如何获取打印机设备上下文?

    我在 Windows 上尝试使用以下命令打印增强型图元文件 EMF 播放增强元文件 http msdn microsoft com en us library dd162800 28VS 85 29 aspx 我当前正在使用屏幕上窗口的设备
  • 文件按文件名模式存在

    我在用 File Exists filepath 我想做的是将其替换为模式 因为文件名的第一部分发生了变化 例如 该文件可以是 01 peach xml 02 peach xml 03 peach xml 如何根据某种搜索模式检查文件是否存

随机推荐

  • CISSP-安全与风险管理

    安全管理 安全治理 Management 管理 管理者为了达到特定目的而对管理对象进行的计划 组织 指挥 协调和控制的一系列活动 governance 治理 治理是或公或私的个人和机构进行经营 管理相同事务的诸多方式的总和 安全治理是支持
  • mysql和sqlServer备份表以及表字段同步sql语句

    前言 要备份的表 old tablename 新表 new table name sql创建出新的表 old tablename ddl如下 CREATE TABLE exe type EXE TYPE ID int NOT NULL AU
  • 有趣的代码:一行Python代码能干嘛?

    前言 python有很多优雅有趣的代码写法 同时还很简短 以至于当我刚开始接触这个编程语言的时候 就爱不释手 而前几天的编程语言榜单中python也超越了java成为了第一 挺替python开心的 python到底有多有趣呢 一行代码告诉你
  • 版本号校验 例如V0.0.0.1和V0.0.0.2

    版本号比较 param v1 param v2 return 0代表相等 1代表左边大 1代表右边大 public int compareVersion String v1 String v2 v1 v1 substring 1 v1 le
  • Go学习教程大纲

    以下是Go学习教程的大纲 第一部分 基础知识 Go简介 什么是Go Go的历史和发展 Go的特点和优势 开发环境的搭建 安装Go编译器 配置开发环境 第一个Go程序 Hello World程序 程序的结构 编译和运行程序 数据类型和变量 基
  • 【拆盲盒啦】摸鱼时间到! iPhone 12、AirPods Pro、罗技鼠标等你拆~

    喜迎开学季 C 站开豪礼 最高可开 iphone 12 盲盒开出的不只是一份礼物 更是对于一切美好的期待 拆开一个盲盒 就像开始一场未知的爱丽丝梦游仙境 为 两点一线 朝九晚九 的生活 埋下一刻期待的种子 去收获一份未知的惊喜 这次 价格再
  • C++面向对象求圆的周长和面积

    include
  • 物联网上行数据实现tcp 负载均衡和高可用架构 nginx + keepalived方案

    文章目录 需求介绍 架构设计 具体配置 nginx 配置 keepalived 配置 需求介绍 之前有做过一个物联网设备接入的项目 项目中会启动一个数据接入服务 TCP server 用来接收传感器设备上传的数据 数据接入服务需要分布式部署
  • 文件系统的基本认知笔记

    1 什么是文件系统 常规认知 Linux下的根目录 文件系统是操作系统用于明确存储设备 常见的是磁盘 也有基于NAND Flash的固态硬盘 或分区上的文件的方法 即在存储设备上组织文件的方法 这种所谓的方法就是文件管理系统 程序 简称文件
  • Mac下使用Git和Git客户端

    先到git官网 https git scm com download 下载安装 这里的git服务器使用本地虚拟机centos来模拟 创建一个用户 名为gitter 用于专门管理git相关 adduser gitter passwd gitt
  • 飞翔的圆(Flappy Bird)游戏源码完整版

    这个源码是一个不错的休闲类的游戏源码 飞翔的圆 Flappy Bird 游戏源码V1 0 本项目是一个仿Flappy Bird的小游戏 只不过是把Flappy Bird里面的鸟替换成了简单的圆 感兴趣的朋友可以研究一下 本项目默认编码GBK
  • 【Ubuntu安装 Nginx】

    ubuntu安装nginx 目前支持两种安装方式 一种是apt get的方式 另一种是根据包安装的方式 为方便我统一使用root用户 一 apt get安装nginx 切换至root用户 sudo su root apt get insta
  • C语言深度剖析——bool类型,浮点数相等比较。

    目录 序言 一 bool类型 1 C语言中到底有没有bool类型 2 VS中的BOOL类型 二 浮点数与 0 比较 1 浮点数的精度缺失 2 浮点数判断相等解决方案 3 判断浮点数是否为 0 最后 序言 C语言中有很多类型 但是bool我们
  • 【AI人工智能】 最强大的语言模型镜像 使用起来真的太方便了! 真的要解放代码思维了吗?

    个人主页 极客小俊 作者简介 web开发者 设计师 技术分享博主 希望大家多多支持一下 我们一起进步 如果文章对你有帮助的话 欢迎评论 点赞 收藏 加关注 前言 AI 在某些基础应用领域可以帮助你减少很多工作量 很强大哦 嘿嘿 优点 这玩意
  • VS2017 libTorch cpu 环境搭建

    C libTorch cpu 环境搭建 一 下载libTorch 下载地址 Start Locally PyTorch 可以在图中选择下载cuda版本或cpu版本的 以CPU Release版本的libTorch为例 下载地址为 https
  • Druid连接池 一个设置 removeAbandonedTimeout

    2019独角兽企业重金招聘Python工程师标准 gt gt gt Druid连接池 一个设置 removeAbandonedTimeout 博客分类 数据库
  • 软件开发外包:你有什么选择

    在2019年 软件开发外包提供了各种各样的选择 成千上万的公司在全球范围内提供软件开发外包服务 您将有很多选择 具体取决于许多标准 例如地理位置 时差 语言要求 甚至文化相似性 2019年的软件开发外包状况如何 在过去的10到15年中 发达
  • 自己封装 vue3+ts 组件库并且发布到 NPM

    自己封装 vue3 ts 组件库并且发布到 NPM 创建项目 pnpm create vite 配置 package json 按照提示创建好项目 然后再 package json 中进行如下配置 name tribiani vue too
  • zip解压

    1 使用apache的ant解压 org apache tools zip 2 引入pom
  • [C++](26)智能指针

    文章目录 引入 智能指针的原理 C 智能指针及其问题 auto ptr unique ptr shared ptr weak ptr 删除器 引入 首先看下面这个程序 int div int a b cin gt gt a gt gt b