2024年【Effective C++ 笔记】( 三 (1),2024年最新一文搞懂

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


关于C++中堆空间、栈空间的使用方式,可以参考:[进程的地址空间:TEXT,DATA,BSS,HEAP,STACK](https://bbs.csdn.net/topics/618668825)  
 在f()调用结束时pInv退出作用域,析构函数被调用,最终使得资源被释放。 事实上,让createInvestment直接返回智能指针是更好的设计。 可以看到,使用对象来管理资源的关键在于:创建资源后立即放入资源管理对象中,并利用资源管理对象的析构函数来确保资源被释放。  
 资源管理对象的实现框架正是RAII原则:acquisition is initialization,用一个资源来初始化一个智能指针。指针的析构函数中释放资源。  
 值得注意的是,为了防止对象被多次释放,auto\_ptr应当是不可复制的。 复制一个auto\_ptr会使它变成空,资源被交付给另一个只能指针。



std::auto_ptr p1 (new int);
*p1.get()=10;

std::auto_ptr p2 (p1);

std::cout << "p2 points to " << *p2 << ‘\n’;
// p2 points to 10
// (p1 is now null-pointer auto_ptr)


.get方法返回资源的指针。  
 auto\_ptr古怪的复制行为导致它并不是管理资源的最佳方式,甚至在STL中auto\_ptr的容器也是不允许的: 可以创建这样的容器,但往里面添加元素(例如push\_back)时会导致编译错。



auto_ptr p1(new int);

vector<auto_ptr> v; // OK,可以编译
v.push_back(p1); // 编译错!


此处我们引入一个引用计数(`reference-counting smart pointer`,`RCSP`)的指针`shared_ptr`。 它在没有任何其他指针引用到该资源时,进行资源的释放。不同于垃圾回收器,shared\_ptr未能解决环状引用的问题。  
 值得注意的是auto\_ptr和shared\_ptr只能管理单个资源,因为它们是使用delete而非delete[]来实现资源释放的。常见的错误便是传递数组进去:



std::tr1::shared_ptr spi(new int[1024]);


在最新的C++标准中,智能指针已经归入std命名空间了。我们可以这样使用:std::shared\_ptr。  
 虽然智能指针有这样的问题,但C++并未提供管理数组的智能指针,因为vector等容器就可以很好地完成这个工作。 如果你真的需要,可以求助与Boost社区的boost::scoped\_array和boost::shared\_array。


## 条款 14 : 在资源管理类中小心 copying 行为



> 
> Think carefully about copying behavior in resource-managing classes.
> 
> 
> 


在[Item 13:使用对象来管理资源](https://bbs.csdn.net/topics/618668825)中提出了基于RAII的资源管理对象,auto\_ptr和shared\_ptr。 智能指针可以有不同的拷贝策略。当你实现这样一个资源管理对象时,需要特别注意。比如一个典型的RAII风格的互斥锁实现:



class Lock {
public:
explicit Lock(Mutex *pm):mutexPtr(pm){
lock(mutexPtr);
}
~Lock(){ unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};


用explicit限定构造函数,可以防止隐式转换的发生:`Lock l = pm`。  
 该互斥锁的使用方式很简单,只需要为每个临界区创建一个C++代码块,在其中定义Lock的局部变量:



Mutex m; // 定义互斥锁
{ // 创建代码块,来定义一个临界区
Lock m1(&m); // 互斥锁加锁
… // 临界区操作
} // m1退出作用域时被析构,互斥锁自动解锁


当m1被复制时情况会怎样?把当前作用域的代码加入到同一个临界区中?拷贝互斥锁并定义一个新的临界区?还是简单地给互斥锁换一个资源管理者? 资源管理对象的拷贝行为取决于资源本身的拷贝行为,同时资源管理对象也可以根据业务需要来决定自己的拷贝行为。可选的拷贝行为不外乎下面这四种:


1. 禁止拷贝。简单地私有继承一个Uncopyable类便可以让它禁止拷贝。参见:[Item 6:禁用那些不需要的缺省方法](https://bbs.csdn.net/topics/618668825)。
2. 引用计数,采用shared\_ptr的逻辑。恰好shared\_ptr构造函数提供了第二个参数`deleter`,当引用计数到0时被调用。 所以`Lock`可以通过聚合一个`shared_ptr`成员来实现引用计数:



class Lock{
public:
explicit Lock(Mutex *pm): mutexPtr(pm, unlock){
lock(mutexPtr.get()); // .get 在条款15里
}
private:
std::shared_ptr mutexPtr;
}
// Lock的析构会引起mutexPtr的析构,而mutexPtr计数到0时unlock(mutexPtr.get())会被调用。
};


3. 拷贝底层资源。当你可以任意拥有底层资源时,可以直接拷贝它。比如string的行为:内存存有指向对空间的指针,当它被复制时会复制那片空间。
4. 转移底层资源的所有权。auto\_ptr就是这样做的,把资源移交给另一个资源管理对象,自己的资源置空。


## 条款15 :在资源管理类中提供对原始资源的访问



> 
> 在资源管理类中提供对原始资源的访问 (显式 隐式都搞一份)  
>  `Provide access to raw resources in resource-managing classes.`
> 
> 
> 


在一个完美的设计中,所有的资源访问都应通过资源管理对象来进行,资源泄漏被完美地克服。然而世界是不完美的, 很多`API`会直接操作资源,尤其是一些`C`语言的`API`。总之,你会时不时地发现有需要直接访问资源, 所以资源管理对象需要提供对原始资源访问。获取资源的方式有两类:`隐式地获取和显式地获取`。 通常来讲,显式的资源获取会更好,它最小化了无意中进行类型转换的机会。


### 显式地获取资源


`shared_ptr`提供了`get`方法来得到资源。



shared_ptr pInv;
int daysHeld(Investment *pi);

int days = daysHeld(pInv.get()); // 通过get获取指针 执行显示装换


为了让`pInv`表现地更像一个指针,`shared_ptr`还重载了解引用运算符(`dereferencing operator`)`operator->`和`operator*`:



class Investment{
public:
bool isTaxFree() const;
};

shared_ptr pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree());
bool texable2 = !((*pi1).isTaxFree());


### 隐式地获取资源


提供`get`方法、`operator->`、`operator*`已经让资源访问很方便了。然而不幸的是,程序员是懒惰的,我们还是希望能够更加简便。 隐式转换操作符便可以完成这个工作,比如操作系统提供了`FontHandle`来操作字体:



FontHandle getFont(); // C API
void releaseFont(FontHandle fh); // CAPI
void changeFontSize(FontHandle f, int newSize);// 将 Font 隐式装换为 FontHandle


我们封装了`Font`来管理资源:



class Font{ // RAII class
private:
FontHandle f;
public:
explicit Font(FontHandle fh): f(fh){} // pass-by-value
~Font(){ releaseFont(f); };
FontHandle get() const { // 显示转换
return f;
}
};


通过`get`方法来访问`FontHandle`:  
 显示转换确实麻烦, 但是这样可以有效地避免资源(字节)泄漏



Font f(getFont());
int newFontSize;
changeFontSize(f.get(), newFontSize);


如果提供一个隐式类型转换运算符将`Font`转换为`FontHandle`,那么接受`FontHandle`类型作为参数的函数将会同样地接受`Font`类型。 一切将会变得简单:



class Font{
operator FontHandle() const{ // 隐式装换
return f;
}
};

Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); // 隐式转换 为 forthandle


然而问题也随之出现:



Font f1(getFont());
FontHandle h2 = f1; // 愿意拷贝一个Font对象 但是把f1隐式装换为FontHandle


用户无意间拷贝了一份资源!该资源并未被管理起来。这将会引发意外的资源泄漏。所以隐式转换在提供便利的同时, 也引起了资源泄漏的风险。在考虑是否提供隐式转换时,需要权衡考虑资源管理类的设计意图,以及它的具体使用场景。 通常来讲,显式的资源获取会更好,它最小化了无意中进行类型转换的机会。



> 
> must remenber :
> 
> 
> 


* APIs 往往要求访问原始资源,所以没用过RAII class 应该提供一个“取得其所管理的资源”的办法
* 对原始资源的访问可能经有显示转换or隐式转换。 一般而言显示转换比较安全,但隐式转换对客户比较方便 。


## 条款16: 成对使用new 和 delete 时要采用相同形式



> 
> 成对使用new 和 delete 时要采用相同形式  
>  Use the same form in corresponding uses of new and delete.
> 
> 
> 


这是C++界中家喻户晓的规则:如果你用`new`申请了动态内存,请用`delete`来销毁;如果你用`new xx[`]申请了动态内存,请用`delete[]`来销毁。 不必多说了,来个例子吧:


![img](https://img-blog.csdnimg.cn/img_convert/07b55f826e37aff8406801c3f2fdefb8.png)
![img](https://img-blog.csdnimg.cn/img_convert/7b527de2bd1e1ff989d82e6cd7880fbd.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

[外链图片转存中...(img-fKsyMedQ-1715590416240)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值