C++右值引用
右值引用的概念
书接上回: C++实现MyString.
这里继续使用MyString代码
int main()
{
String s1;
s1 = fun();
int a = 10;
int& b = a;
//int&& c = a; 错误!!! 只能引用右值
int&& c = 10;//只能引用右值,数字常量是纯右值
//int&& d = c; 错误!!! 具有名字 不再是一个右值
String& sx = s1; //String& sx = String("hello");错误!!!左值引用只能引用具名对象
String&& sy = String("hello");//无名对象可以进行右值引用
}
右值的概念:左值拥有地址,生存期与名字存在时间相同;右值不能取地址,没有名字
当右值具有名字,便会成为左值概念
String&& fun()
{
String s2("456");
return s2;
}
这里是错误的,s2具有名字不能进行右值引用
右值引用清楚之后,就可以进行编写右值拷贝构造与右值赋值语句也叫做移动拷贝构造与移动赋值语句
String(String&& s)
{
cout<< "move copy construct:" << this << endl;
str = s.str;
s.str = NULL;
}
String& operator=(String&& s)
{
if(this != &s)
{
str = s.str;
s.str = NULL;
}
cout<< this << "move operator= "<< &s << endl;
return *this;
}
String fun()
{
String s2("456");
return s2;
}
int main()
{
String s1;
s1 = fun();
cout << s1 << endl;
return 0;
}
首先在main栈帧中构建s1对象,接着调用fun()函数,创建s2对象;这里我们要拿s2构建不具名对象,调动移动拷贝构造,将资源进行转移至将亡值对象,随后接着调用移动赋值语句,并将资源转移至s1,自始至终资源都是s2对象原本创建的堆区资源,只是进行了转移
为什么移动拷贝构造右值引用了有名对象
当返回fun()函数,将s2以值返回构建不具名对象,也就是将亡值对象,通过移动拷贝构造,其中移动拷贝构造中的this指针就是将亡值对象的空间,通过s2进行初始化
String(String&& s)
{
cout<< "move copy construct:" << this << endl;
str = s.str;
s.str = NULL;
}
但是这样看来,s2是有名对象,为什么有名对象可以通过右值引用进行初始化;这属于系统的特点(系统强行规定传递s2),构建将亡值若没有移动构造,则调动普通构造,若有移动构造则优先调动移动构造(c11标准);我们将s2作为参数给到s,而将亡值对象并不是s
当将亡值对象构建完成后,从fun()函数退出,s2生存期结束并将其析构,此时s2中str指向空没有任何空间需要进行释放
回到主函数,将亡值对象给s1进行赋值,此处调动移动赋值;s1的str指向原本属于s2,此时属于将亡值的空间,并把将亡值str变为空值
由此看来,系统在构建将亡值的时候,把s2具名对象通过右值引用给到了s;实际上在底层,进行了下面的构架过程
String fun()
{
String s2("456");
return std::move(s2);
}
内存泄漏问题
在这里我们将原本属于s2的空间,通过将亡值最终传递给了s1,但是原本属于s1的一字节空间并没有释放,导致了内存泄漏
那么如何解决内存泄漏的问题:
- 第一种方法是我们在移动赋值中,加入delete将空间释放
String& operator=(String&& s)
{
if(this != &s)
{
delete[]str;
str = s.str;
s.str = NULL;
}
return *this;
}
- 通过一个交换函数,将两个对象指向的空间进行交换
String& operator=(String&& s)
{
if(this != &s)
{
s.str = Relese(s.str);//将亡值析构,会将原本s1指向的空间进行释放
}
return *this;
}
char* Relese(char *p)
{
char* old = str;
str = p;
return old;
}
写时拷贝
按照原本我们的代码,在构建s1,s2,s3,s4时,每个对象都具有自己的堆区空间
int main()
{
String s1("123");
String s2(s1);
String s3(s2);
String s4(s2);
}
若将s2,s3,s4都指向s1的堆区空间,继而会节省一部分空间,但是又需要加上引用计数值,进行统计字符串有多少对象进行共享
在析构时,假若我们析构s4,对引用计数进行减一,再继续析构s3,s2继续进行减一,直到我们析构s1引用计数值减一为零,也就是没有对象指向该空间,那么就对该空间进行释放
以该类型定义的结构体称为:柔性数组
class String
{
private:
struct StrNode
{
int ref;//对象引用个数
int len;//字符串长度
int size;//字符串空间大小
char data[];
};
};
柔性数组是一种数组大小待定的数组
在C语言中,可以使用结构体产生柔性数组,结构体的最后一个元素可以是大小未知的数组
在 struct_sd_node
结构体中 data
,仅仅是一个待使用的标识符,不占用存储空间,所以sizeof(struct_sd_data) = 8
- 用途:长度为 0 的数组的主要用途是为了满足长度可变的结构体
- 用法:在一个结构体的最后,声明一个长度为 0 的数组,就可以使得这个结构体是可变长的;对于编译器来说,此时长度为 0 的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量;但对于这个数组大小,我们可以进行动态分配
注意:如果结构体是通过calloc、malloc或realloc等动态分配方式生产,在不需要时要释放相应的空间
优点:比起在结构体中声明一个指针变量,在进行动态分配的办法,这种方法效率要高,因为简单
缺点:在结构体中,数组为 0 的数组必须在最后声明,在设计结构体类型中有一定限制
实际举例说明
class String
{
private:
struct StrNode
{
int ref;//对象引用个数
int len;//字符串长度
int size;//字符串空间大小
char data[];
};
StrNode *pstr;
public:
String(const char *p = NULL) :pstr(NULL)
{
if(p != NULL)
{
int len = strlen(p);
pstr = (StrNode*)malloc(sizeof(StrNode) + len * 2 + 1);//开辟柔性数组长度为p存放内存的二倍加一长度
pstr->ref = 1;
pstr->len = len;
pstr->size = len * 2;
strcpy(pstr->data, p);
}
}
String(const String& s) :pstr(NULL)
{
if(s.pstr != NULL)
{
pstr = s.pstr;
pstr->ref += 1;
}
}
};
int main()
{
String s1("123456");
String s2(s1);
String s3(s1);
String s4(s2);
return 0;
}
由于每个对象都共享一个字符串,当我们需要将空间还给堆区,就要将引用计数降下来,只有引用计数为零时,再归还空间
~String()
{
if(pstr != NULL)
{
pstr->ref--;
if(ref == 0)
{
free(pstr);
}
}
pstr = NULL;
}
接下来是赋值函数,输出流重载,下标重载
class String
{
String& operator=(const String& s)
{
if(this != &s)
{
~String();
new(this) String(s);
}
}
ostream& operator<<(ostream& out) const
{
if(pstr != NULL)
{
out << pstr->data;
}
return out;
}
char& operator[](const int index) //下标重载
{
if(index >= 0)
{
for(int i = 0; i < pstr->len; i++)
{
pstr->data++;
}
return data;
}
}
const char& operator[](const int index) const
{
if(index >= 0)
{
for(int i = 0; i < pstr->len; i++)
{
pstr->data++;
}
return data;
}
}
}
ostream& operator<<(ostream& out, const String& s)
{
s << out;
return out;
}
int main()
{
String s1("123");
char x = s1[2];
s1[2] = 'x';
}