栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

c艹进阶编程(3)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

c艹进阶编程(3)

前排提示:本文不适合初学者阅读

目录

赋值操作的注意事项

链式反应

考虑本身

记得拷贝基类

优先使用智能指针而不是new

std::unique_ptr

std::shared_ptr

std::weak_ptr

结束语


赋值操作的注意事项

赋值操作一节很简单,熟悉这一部分的人可以跳过

链式反应

用引用接收返回值,比如常见的赋值操作可以这样写

using namespace std;
struct test
{
	test(int input) :num(input) {};
	int num;
	test& operator=(const test& t) 
	{
		this->num = t.num;
		return *this;
	}
};

int main() 
{
	test t(0);
	test t1(1);
	test t2(1);
	t2 = t1 = t;
	cout << t2.num << endl;
	system("pause");
}

这个连等解析出来时这样:

	(t2 = (t1 = t));

输出为0,很好理解。

考虑本身
using namespace std;
struct test
{
	test(int input) :num(input) {};
	int num;
	test& operator=(const test& t)
	{
		this->num = t.num;
		return *this;
	}
};

上面这种情况其实考不考虑都没什么区别,因为没有涉及堆区内存的分配和释放,但是如果下面这种,就必须要写一个判断,否则会出现无法预测的赋值。

struct test
{
	test(int input)
	{
		num = new int(input);
	};
	int* num;
	test& operator=(const test& t)
	{
		if (this == &t) return const_cast(t);
		delete this->num;
		this->num = new int(*t.num);
		return *this;
	}
	~test() 
	{
		delete this->num;
	}
};

int main()
{
	test* t1 = new test(1);
	test* t2 = t1;
	*t2 = *t1;
	cout << *t2->num << endl;
	system("pause");
}

但是如果是移动赋值函数,也要考虑这些,不然也会出现未知赋值操作:

#include
using namespace std;
struct test
{
	test(int input)
	{
		num = new int(input);
	};
	int* num;
	test& operator=(const test&& t) noexcept
	{
		if (this == &t) return const_cast(t);
		delete this->num;
		this->num = new int(*t.num);
		return *this;
	}
	~test()
	{
		delete this->num;
	}
};

int main()
{
	test* t1 = new test(1);
	test* t2 = t1;
	*t2 = move(*t1);
	cout << *t2->num << endl;
	system("pause");
}

一种写法可以替代这种检验机制,我们只需要注意在赋值之前不要删除就行,这样就算是同一个,也不会有影响。

#include
using namespace std;
struct test
{
	test(int input)
	{
		num = new int(input);
	};
	int* num;
	test& operator=(const test& t)
	{
		int *newnum=this->num;
		this->num = new int(*t.num);
		delete newnum;
		return *this;
	}
	~test()
	{
		delete this->num;
	}
};

int main()
{
	test* t1 = new test(1);
	test* t2 = t1;
	*t2 = *t1;
	cout << *t2->num << endl;
	system("pause");
}

 只是这样会有一个问题:如果是进行自身的赋值操作,会重新分配一次内存空间,这样效率相对于直接一个判断语句不免效率有点低,我们可以根据自己代码的具体情况比如堆区空间的大小或者自我复制的频率选择用哪一种。

复制构造不用考虑,因为要复制的对象都不存在

记得拷贝基类

我们考虑下面这种情况,乍一看似乎没有任何不妥:

struct base
{
	int basenum;
	base operator=(const base& b) 
	{
		this->basenum = b.basenum;
		return *this;
	}
	virtual ~base() {};
};
struct test:public base
{
	int num;
	test(int i) :num(i) {};
	test& operator=(const test& t)
	{
		this->num = t.num;
		return *this;
	}
	~test(){}
};

int main()
{
	test t1(1);
	t1.basenum = 2;
	test t2(2);
	t2 = t1;
	cout << t2.basenum << endl;
	system("pause");
}

 看一下结果:

这显然不是我们想要的,因此记得要调用父类的对应函数:

#include
using namespace std;
struct base
{
	int basenum;
	base operator=(const base& b) 
	{
		this->basenum = b.basenum;
		return *this;
	}
	virtual ~base() {};
};
struct test:public base
{
	int num;
	test(int i) :num(i) {};
	test& operator=(const test& t)
	{
		this->num = t.num;
		base::operator=(t);//add one line
		return *this;
	}
	~test(){}
};

int main()
{
	test t1(1);
	t1.basenum = 2;
	test t2(2);
	t2 = t1;
	cout << t2.basenum << endl;
	system("pause");
}

复制构造不用考虑,因为要复制的对象都不存在

值得注意的是,如果拷贝构造和复制赋值的代码有大段重复,可以学python写个init函数,然后全部调用这个init就行。

优先使用智能指针而不是new

学过c++的人多少都知道智能指针,但是很多初学者八股文背得溜,却很少去使用这种智能指针,对于这种程序员,

我的评价是,寄。

C11提供了很多个选择,四个:std::auto_ptr, std::unique_ptr, std::shared_ptr,但是其std::auto_ptr基本寄了,因为没有move语法强行复制后变成null,这种操作让开发人员很难去维护。

因此我们主要讲三个:

std::unique_ptr

如果我们要用智能指针,这个玩意儿是第一个选择, 我们看一下他的大小:

#include
using namespace std;
struct test
{
	int* num;
	test(int input) {
		num = new int(input);
	}
	~test() {
		delete this->num;
	}
};
int main()
{
	using func_ptr = void(*)(test*);
	unique_ptr t1(new test(1), [](test* t) {delete t; });
	std::cout << sizeof(t1) << endl;
	unique_ptr t2(new test(1));
	std::cout << sizeof(t2) << endl;
	std::system("pause");
}

看一下输出:

大小会多一点点 ,因为我们指定了一个function,因此俩指针合起来是8个byte,如果不指定就是4;

我们考虑一个很常见的现象:作为一个工厂函数返回一个继承层级中的一个特定类型的对象,比如我们有下面代码,实现一个对不同文件的分片操作:

class mainform :public ui//主界面
{
	string path; //需要分片的文件路径
	int num;//文件分片的数量
	SpliterFactory *factroy;//一个工厂就够了
public:
	explicit mainform(SpliterFactory* fac, string str, int inputnum) :factroy(fac), path(str), num(inputnum){};
	//假设点击了一个按钮,则发生,qt的函数
	connect(ui.pushbuttom, &QPushButton::clicked, this, [=](){
		Ispliter *spliter = factroy->CreateSpliter(path, num);
		spliter->split();
	});
};

大概意思就是我们定义了工厂基类以及衍生类的某一个 CreateSpliter函数,返回值为指向另一个基类的衍生类:

class Ispliter
{
protected:
   string path;
   int num;
public:
   Ispliter(string path, int number) :path(path), num(number){};
   virtual void split() = 0;
   virtual ~Ispliter(){};
};
class filespliter: public Ispliter
{
public:
	videospliter(string str, int innum) :Ispliter(str,innum){};
	void split() override
	{
		//读取文件
		//.......
		//分批次写入
		for (int i = 0; i < num; i++)
		{
			//....
		}
	}
};
class SpliterFactory
{
public:
	virtual Ispliter* CreateSpliter(string path, int number) = 0;
	virtual ~SpliterFactory(){};
};
class filespliterFactory :public SpliterFactory
{
public:
	Ispliter*CreateSpliter(string path, int number) override
	{
		return new filespliter(path, number);
	}
};

 这样就把具体的类元素往接口上赶,更加adaptive,高聚合低耦合。

那么问题是这里的返回值是一个指针,当不再需要使用时,调用者来决定是否删除这个对象, std::unique_ptr 在自己即将被销毁时,自动销毁它所指向的空间,因此我们改成这样:

class SpliterFactory
{
public:
	virtual std::unique_ptr CreateSpliter(string&& path, int number) = 0;
	virtual ~SpliterFactory()noexcept {};
};
class filespliterFactory :public SpliterFactory
{
public:
	std::unique_ptr CreateSpliter(string&& path, int number) override
	{
		return unique_ptr(new filespliter(path, number));
	}
};

正常情况下释放就直接delete掉了,但是如果我们希望在析构 加入一条日志,那么我们可以这样自定义删除函数:

void myLog(T* t) 
{
	cout << typeid(t).name() << endl;
}
auto myDel = [](Ispliter* s)
{
	myLog(s);
	delete s;
};

这样就写成这样:

class SpliterFactory
{
public:
	virtual std::unique_ptr CreateSpliter(string&& path, int number) = 0;
	virtual ~SpliterFactory()noexcept {};
};
class filespliterFactory :public SpliterFactory
{
public:
	std::unique_ptr CreateSpliter(string&& path, int number) override
	{
		std::unique_ptr p(nullptr, myDel);
		p.reset(new filespliter(forward(path), number));
		return p;
	}
};

值得注意的是, std::unique_ptr 会产生两种格式,一种是独立的对象(std::unique_ptr),另外一种是数组( std::unique_ptr ),但是这种数组形式根本没啥用,因为我们完全可以用arry ,vector来替代。

还有一个非常重要的特性是可以转化为std::shared_ptr,比如我们可以用std::shared_ptr来接收上面的函数返回值,如果从unique_ptr指向shared_ptr,那么shared_ptr接管unique_ptr所指向对象的所有权,unique_ptr指向null。

int main() 
{
	filespliterFactory f;
	shared_ptr p=f.CreateSpliter("ABC", 2);
	return 1;
}

std::shared_ptr

众所周知,share_ptr,里面有个引用计数,我们瞅一眼源码:

 因为里面包含一个原生指针,还有个引用计数,因此size都是8byte。

而且值得注意的三点:

  1. 引用计数必须是原子型的,如果是int,那自动就是原子型的,一般都是32位机器至少。但是遗憾的是一般都是一个word大小,也就是16个byte,因此加上原子限制符后,多线程环境下,执行会比较慢(相对于非原子类型)
  2. 引用计数是动态分配的,因为我们要求指向的对象跟引用计数分离,因此没有提前准备空间
  3. 调用一次析构就会少一个,但是拷贝构造或者赋值操作可能导致一个增加一个减少,移动构造会使之前的对象指向null

shared_ptr也可以跟unique_ptr一样,支持自定义的deleter,而且更灵活:

auto customDeleter1 = [](Widget* pw) {...};
auto customDeleter2 = [](Widget* pw) {...};//自定义的deleter,属于不同的类型
std::shared_prt pw1(new Widget, customDeleter1);
std::shared_prt pw2(new Widget, customDeleter2);

这意味着他们可以放到同一个容器中去:

std::vector> vpw{ pw1, pw2 };

与unique_ptr不同的是,不管我们自不自定义都是8个byte,那么问题在于:我们自定义的指针去哪里了呢?

其实shared_ptr在堆上额外分配了一块空间来存储这个函数,我们去源码瞅一眼:

如你所见,这是一块新开辟的空间。

除此之外,我们对引用计数的理解也需要进一步深入,要知道, shared_ptr确实包含了一个对象的引用计数的指针,但是其实这不仅仅是一个int型那么简单,如我之前所说,足足有16个btye,里面除了包含了引用计数,还包含了一份自定义deleter的拷贝(在指定好的情况下).如果指定了一个自定义的allocator,也会被包含在其中。

因此我们有以下推论:

  1. 如果从原生指针或者unique_ptr创建shared_ptr,那么会创建控制块,
  2. 如果是从shared_ptr或者weak_ptr创建,只是简单接管对应的对象对应的控制块。

有一点需要注意,不能用同一个原生指针创建多个shared_ptr,尽管它支持这样做。

int* temp = new int(1);
shared_ptrp1(temp);
shared_ptrp2(temp);

这意味着会创建多个控制块,对象会被多次摧毁,第二次析构会对未定义行为负责,这样的设计是糟糕透顶的。

因此我们有以下替代方案:

  1. 直接new传入
    shared_ptrp1(new int(1));
  2. 使用make_shared
     
    shared_ptrp2 = make_shared(temp);

 那么问题来了,如果我们希望在类内部实现对本身的智能指针构造,应该怎么办,正常人都会这样写:

class test 
{
public:
	void process();
};
vector> pool;//use to track
void test::process() 
{
	pool.emplace_back(this);
}

这样没什么错,但是如果我们在这样写的同时用了shared_ptr来管理新建立的对象,这样就有多个shared_ptr都用了原生指针构建,这样显然是灾难性的:

	shared_ptr p(new test());
	p->process();

 针对上面的问题,C++给了一套解决方法:

class test : public std::enable_shared_from_this 
{
public:
	void process();
};
vector> pool;//use to track
void test::process() 
{
	pool.emplace_back(shared_from_this());
}

shared_from_this不会创建控制块,只会寻找前对象的控制块,然后建立一个新的shared_ptr来引用这个控制块,比如我们运行:

class test : public std::enable_shared_from_this 
{
public:
	void process();
};
vector> pool;//use to track
void test::process() 
{
	pool.emplace_back(shared_from_this());
}
int main() 
{
	shared_ptr p(new test());
	p->process();
	cout << p.use_count() << endl;
	system("pause");
}

 输出:

 但是这样设计有个前提,就是必须要有控制块才能运行。也就是说必须要有一个shared_ptr指向当前的对象,否则会抛出异常。

为了阻止用户在没有shared_ptr前使用shared_from_this,继承的子类通常会吧构造函数声明为private,然后用工厂模式来构造对象,这样就不会出错了。

还有一点值得注意就是很多程序员用std::shared_ptr来指向一个数组,然后自作聪明地重载deleter(delete [])。这样可以通过编译,但是有两个问题:

  1. 没有std::shared_ptr这样的用法,即没有对应的重载
  2. 支持unique的转换,但是std::unique_ptr不支持转换

因此如unique一样,推荐array或者vector之类的。 

std::weak_ptr

这个很简单,weak_ptr只是用来监视shared_ptr,并不会增加额外的引用计数。它通常需要一个shared_ptr来创建:

class test : public std::enable_shared_from_this 
{
};
vector> pool;//use to track
int main() 
{
	shared_ptr p(new test());
	weak_ptr wp(p);
	system("pause");
}

这个玩意儿不允许解引用,也不能检测空,但是有一些api很好用:

if(wp.expired()){}

 这个可以用来检测它是否过期

我们很常见的想法是这样:先看weak_ptr是否过期,如果没有过期,则访问里面的元素,并做一些操作。这样存在两个问题:

  1. weak_ptr缺少解引用操作,因此没办法访问内部的对象
  2. 即使可以访问,多线程下也可能导致未定义行为(其他线程先给你摧毁了)

 因此我们需要将判断是否过期以及访问合并为一个原子操作:

一种简单的实现方法是用lock函数:

	shared_ptr p1 = wp.lock();

另一种方法是直接作为参数传入shared_ptr的构造函数,请结合try和catch来使用,如果过期了会直接抛出异常。

	shared_ptr p1(wp);

 那么这个逼到底在哪里有用呢?

比如有个工厂函数,按道理我们应该返回一个unique_ptr:

	std::unique_ptr CreateSpliter(string&& path, int number) override
	{
		std::unique_ptr p(nullptr, myDel);
		p.reset(new filespliter(forward(path), number));
		return p;
	}

但是如果调用这个函数代价很大,比如会涉及文件IO,数据库的操作。我们一个很自然的方法是写一个缓存函数,把之前调用过的对象暂存起来,以方便下一次直接使用。

但是如果把每一个都存起来肯定会导致缓存性能出现问题,因此一个合理的做法就是当被缓存对象不再使用的时候销毁他。

我们需要俩指针:一个是指向缓存一个指向工厂接受的对象,因为缓存需要被监视,我们就需要用到std::weak_ptr,同时为了兼顾std::weak_ptr,工厂的返回对象应该改为shared_ptr,下面给个例子:

std::shared_ptr CreateSpliter(string&& path, int number) override
{
	std::shared_ptr p(nullptr, myDel);
	p.reset(new filespliter(forward(path), number));
	return p;
}
std::shared_ptr fastLoadWidget(string&& path)
{
	static std::unordered_map> cache;
	auto objPtr = cache[path].lock();//objPtr是std::shared_ptr类型
	//指向了被缓存的对象(如果对象不在缓存中则是null)
	if (!objPtr) {
		objPtr = CreateSpliter(forward(path),10);
		cache[path] = objPtr;
	}//如果不在缓存中,载入并且缓存它
	return objPtr;
}

另一个用法实在观察者模式下 ,观察者模式就是在一个主体钟加入一个数组,内部是抽象基类的指针,然后我们可以不断继承这个基类并针对同一个接口衍生出不同的功能,然后在主体中不断直接用for循环不断调用每个衍生对象的对应函数,一个很简单的例子:

class filespliter
{
private:
	string path;
	int num;
	vector iprogress;//抽象的通知机制,
								//使用vector就可以支持多个观察者
public:
	void add_Iprogress(Iprogress* subprogress)
	{
		iprogress.push_back(subprogress);
	}
	void remove_Iprogress(Iprogress* subprogress)
	{
		for (auto i = iprogress.begin(); i != iprogress.end();i++)
		{
			if (i == &subprogress)
			{
				iprogress.erase(i);
			}
		}
	}
	filespliter(string const str, int const  innum) :path(str), num(innum){};
	void split()
	{
		//读取文件
		//.......
		//分批次写入
		for (int i = 0; i < num; i++)
		{
			//....

			float value = static_cast(num);
			value = (i + 1 )/ num;
			onprogress(value);
		}
	}
protected:
	void onprogress(float value)
	{
		for (auto i : iprogress)
		{
			if (i != nullptr)
			{
				i->doprogress(value);
			}
		}

	}
};

 一样把直接访问改为用weak_ptr监视每一个,然后在每次访问之前判断一下是否悬挂,就不写代码了。

结束语

楼主最近作业很多,功课任务繁重,实在挤不出时间写博客,下一篇估计要12月份了,下期讲一讲make_unique和make_shared底层的原理以及SFINAE机制及相关API,如果篇幅不够会继续讲一下泛型编程的进一步使用技巧。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/587727.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号