frankfan的胡思乱想

学海无涯,回头是岸

友元和引用

友元 引用计数

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编辑  收藏  举报

导航