友元和引用
友元
引用计数
C++提供完整的面向对象支持,提供体系完善的构造函数与析构函数,对成员也提供相应的访问权限关键字(protect
public
private
)。
在面向对象中,总是倾向于将成员变量设计成private
将成员函数设计成public
,不过这样设计的一个缺点在于当类用在全局函数时,全局函数内部是无法直接使用类成员的(而只能间接使用类成员函数来访问操作类成员),这是很受限的,因此,C++提供关键字friend
来修饰函数或者类,让全局函数或者另一个类有权限直接访问原类的私有成员变量。这里函数或者类又被称作友元函数或者友元类。
友元函数、友元类
#include <iostream>
using namespace std;
class Node{
public:
friend void showValue(const Node &n);//使全局函数成为Node的友元
Node(int v = 0):value(v){}
private:
int value;
};
//普通的全局函数在内部是无法访问node对象的私有成员的,除非这个函数是Node类的友元函数
void showValue(const Node &n){
cout<<n.value<<endl;
}
int main(){
Node node(11);
showValue(node);//11
return 0;
}
不仅有友元函数,也有友元类
#include <iostream>
using namespace std;
class Node;
class TTNode{
public:
static void handleNode(const Node &node);
};
class Node{
public:
friend class TTNode;//友元类。在TTNode内部能访问Node类的任何私有成员
Node(int v):value(v){}
private:
int value;
};
//这个函数是TTNode的静态成员函数,在其内部能访问Node对象的任何成员,因为TTNode是Node类的友元类。
void TTNode::handleNode(const Node &node){
if(node.value){
cout<<node.value<<endl;
}else{
cout<<"opps"<<endl;
}
}
int main(int argc, const char * argv[]) {
Node node(11);
TTNode::handleNode(node);//11
return 0;
}
可见,使用友元函数或者友元类能够无限制的访问另一个类的所有成员,无视其访问权限。这显然破坏了面向对象的封装性,因此友元特性不要滥用。有其特有的应用场景,如运算符重载等。
引用计数
C++的对象不仅能定义在栈上,也能定义在堆上。凡是能够将对象定义在堆上的编程语言都要考虑一个问题,那就是『内存管理』。也就是要能正确的控制对象的生命周期,创建堆对象后要能在合适正确的时机将其释放掉,这是最终要达到的目的,而手段很多,通常可以分为手动与自动。
所谓自动就是完全交由编译器(或者说虚拟机)处理,代表语言如Java,其虚拟机内置垃圾回收模块,可以自动计算对象的生命周期,从而对其进行释放,并不需要程序员介入。垃圾回收这种机制实现的算法有很多,各有优缺点。
手动就是交由程序员通过new
delete
关键字对对象进行控制,优势在于全权交由程序员处理,灵活机动,缺点在于往往无法正确的处理好,最终导致内存泄露。而进阶版的手动就是引用计数
,通过引入这个概念对堆对象进行管理,程序员只要根据事先约定,对堆内存进行引用即可。
引用计数的概念非常简单,一个指针指向一个堆对象,那么就给这个堆对象计数为1,表明有一个对象引用了自己,这就是引用计数,N个指针指向一个堆对象,那么就表示有N个指针需要用到这个堆对象,那么这个堆对象的引用计数就是N,如果一个指针解除了对该堆对象的引用,那么堆对象的引用计数就减1,当堆对象的引用计数等于0时就意味着没有任何指针需要用到该堆了,则该堆应该被释放(delete
)这就是引用计数的全部真相。
根据上文,只要有指针指向堆对象,那么堆对象的引用计数就应该+1,其实这是片面的,也就是说不应该一味的只要指针有指向,就应该给引用计数+1。而是应该可以更细粒度的控制引用计数的增加,为什么呢?
假如这样一种场景:A对象中有B类型成员指针,B对象中有A类型成员指针,那么有:
A *a = new A;
B *b = new B;
a.b = b;
b.a = a;
那么显然堆对象A的引用计数应该为2,同理,堆对象B的应用计数应该也为2,那么就算a,b两个指针离开作用域被释放,那么A、B对象的引用计数即使都减1,两个对象的引用计数仍不为0,无法得到释放。这就是循环引用 。
循环引用是在引用计数机制中不可避免的话题,也是采用引用计数进行内存管理时内存泄露的罪魁祸首,因此要小心处理。
其实并不是只要有指针指向堆对象,该对象的引用计数就应该+1,而是可以将引用分为『强引用』『弱引用』两种,当强引用类型指向堆对象时,引用计数+1,而当弱引用类型指向堆对象时,计数保持不变。这样就能打破循环引用。通常,这种强弱引用关系需要语言提供相关支持。
引用计数是跟随堆中对象一起的,用来描述堆中的对象属性,而非类属性,这是需要注意的
手动模拟引用计数的实现:
//MyString_hpp
#ifndef MyString_hpp
#define MyString_hpp
#include <stdio.h>
class MyString{
public:
MyString(const char *str = nullptr);
MyString(const MyString &ms);
~MyString();
void SetString(char *str);
char *GetString() const;
private:
int str_len;
int capcity;
char *buf;
private:
int *refCount;//引用计数
};
#endif /* MyString_hpp */
//MyString.cpp
#include "MyString.hpp"
#include <stdlib.h>
#include <string.h>
#include <iostream>
using namespace std;
MyString::MyString(const char *str){
if(str != nullptr){
str_len = (int)strlen(str);
capcity = str_len + 10;
buf = new char[capcity];
bzero(buf, capcity);
memcpy(buf, str, str_len+1);
refCount = new int;
*refCount = 1;
}
}
MyString::MyString(const MyString &ms){
str_len = ms.str_len;
capcity = ms.capcity;
buf = ms.buf;
refCount = ms.refCount;
*refCount = *refCount + 1;
}
MyString::~MyString(){
int rfc = *refCount;
rfc -= 1;
*refCount = rfc;
if(rfc == 0 && buf != nullptr){
delete [] buf;
delete refCount;
buf = nullptr;
refCount = nullptr;
cout<<"free buf"<<endl;
}
cout<<"~Mystring()"<<endl;
}
//写时拷贝
void MyString::SetString(char *str){
int rfc = *refCount;
if(rfc == 1){//本体
if(strlen(str) <= str_len){//直接拷贝
bzero(buf, str_len+1);
str_len = (int)strlen(str);
memcpy(buf, str, str_len+1);
}else{//开辟新空间
str_len = (int)strlen(str);
capcity = str_len + 10;
delete [] buf;
delete refCount;
buf = new char[capcity];
memcpy(buf, str, str_len+1);
refCount = new int;
*refCount = 1;
}
}else{//新开辟空间
str_len = (int)strlen(str);
capcity = str_len + 10;
*refCount = *refCount - 1;
buf = new char[capcity];
memcpy(buf, str, str_len+1);
refCount = new int;
*refCount = 1;
}
}
char * MyString::GetString() const{
return buf;
}
这种手动模拟的方式在处理栈对象时是可行的,但是在处理堆对象时却有心无力。
posted on 2021-12-28 00:26 shadow_fan 阅读(72) 评论(0) 编辑 收藏 举报