分享一张上课摸鱼画的图
知识储备:
对于知识储备我仅仅说一下概念和理解,稍微举下例子,因为涉及的知识点太多,有兴趣可以自行查阅资料!
一、左值右值:
能出现在赋值号左边的表达式称为“左值”,不能出现在赋值号左边的表达式称为“右值”。一般来说,左值是可以取地址的,右值则不可以。 非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。一般的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。
C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:
类型 && 引用名 = 右值表达式;
引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。? ?
这里引入了移动语义,所谓移动语义(Move语义),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
二、移动构造:
当数据成员中有指针时,深拷贝(重新开辟空间)解决了浅拷贝之后析构同一块内存的问题。
但对于深拷贝需要不断地开辟空间,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。
移动构造可以理解为文件的剪切操作,或者理解为二手交易,比如一个人要挂掉了,临走时,他要将自己的东西交付给其他人,之前的做法是通过拷贝构造进行拷贝,然后自己析构挂掉,但移动构造是直接将自己的东西转交给下个人,自己挂掉。
示例:
class A {
public:
int x;
A(int x) : x(x)
{
cout << "构建对象" << endl;
}
A(A& a) : x(a.x)
{
cout << "拷贝构建" << endl;
}
A& operator=(A& a)
{
x = a.x;
cout << "拷贝赋值" << endl;
return *this;
}
A(A&& a) : x(a.x)
{
cout << "移动构建" << endl;
}
A& operator=(A&& a)
{
x = a.x;
cout << "移动赋值" << endl;
return *this;
}
};
int main()
{
A a(1);
A b = a;
A c(a);
b = a;
A e = move(a);
b = move(e);
return 0;
}
A a(1),调用构造函数。 A b = a,创建新对象b,使用a初始化b,因此调用拷贝构造函数。 A c(a),创建新对象c,使用a初始化c,因此调用拷贝构造函数。 b = a,使用a的值更新对象b,因为不需要创建新对象,所以调用拷贝赋值运算符。 A e = move(a),创建新对象e,使用a的值初始化e,但调用move(a)将左值a转化为右值,所以调用移动构造函数。 b=move(e),移动e的值取赋值给b,调用移动赋值
?
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。
三、柔性数组
struct TEST//柔性数组结构体
{
int state;
int len;
char cData[0];
};
对于柔性数组的解释:
- 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体
- 用法 : 在一个结构体的最后, 申明一个长度为0的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量
-
优点 :比起在结构体中声明一个指针变量、再进行动态分 配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。 -
缺点 :在结构体中,数组为0的数组必须在最后声明,使用上有一定限制。
四、写实拷贝?
在类默认值函数的学习中,我们知道深,浅拷贝:
- 浅拷贝:系统提供的拷贝构造函数,赋值运算符重载函数是浅拷贝。是一个简单的赋值,让两个指针指向相同的数据,数据共享,但是当释放资源时,就会出现一个内存块被释放两次的错误。
- 深拷贝:每一个对象拥有自己独立的资源;浪费内存;当多个对象的内存数据一样,只是对内存进行读取操作,那么就会浪费大量的内存。
结合,深拷贝,浅拷贝的优缺点,产生了写时拷贝,分为两部分理解:
引用计数 :
c++引用计数的可以节省内存,而且同时可以降低构建对象和析构的开销,所谓引用计数简单说来就是对各对象共享一份实体的数据,但是我们需要实现对该数据的引用的对象的记录,这样最后一个对象引用结束后能够安全的删除数据。
用字符串举例,假设我们想要实现字符串的拷贝或者赋值,那么我们想要呈现的客户的是各自独立的字符串。如下:
但是,对于计算机的内部实现而言,这样的方式显然出现了冗余存储的现象,那么我们期待计算机内部是这样实现的:
这样所有的用户拥有的字符串都是同一个,但是,这会出现一个问题,那就是的那个其中一个销毁对象时,其它对象的数据也将不可访问,因此,我们需要对该份数据的引用进行计数,只有最后一个引用对象才能真正的销毁数据。 因此实现是这样的:
虽然这样能够使得多个相同的字符串共享同一份数据,但是,当其中任何一个对象修改数据时,其它对象所拥有的数据也就都改变,因此,为了避免这种情况的发生,采用写时复制写实拷贝的技巧,也就是当某个对象要修改数据时便重新赋值一份进行修改,从而该对象就有一份新的数据,不在和其它对象共享一份数据。
对于写实拷贝推荐一篇大牛的文章:C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
MyString实现:
设计框架:
class MyString
{
private:
enum { ALIGN = 8 };
struct StrNode //占12字节 ,柔性数组不会计算后面数组大小
{
int ref;//引用计数
int len;//字符串长度
int capc;//容量
char data[]; //柔性数组
};
private:
StrNode* pstr;
MyString(StrNode* p) :pstr(p) {}
//获取内存大小,对齐
static size_t round_up(size_t n)//将newcap提升成8的倍数(不一定是8倍数,4,16都行等等),扩容
{
return (n + ALIGN - 1) & ~(ALIGN - 1);
}
}
构造函数:
public:
MyString(const char* p = nullptr) :pstr(nullptr)//构造函数
{
if (p != nullptr)
{
int n = strlen(p);
int total = round_up(2 * n); //扩容后柔型数组大小
pstr = A_Malloc(total); //封装后,不需要考虑结构体大小
if (pstr == nullptr)exit(1);
pstr->ref = 1;
pstr->len = n;
pstr->capc = total - 1;
strcpy_s(pstr->data, total, p);
}
}
MyString(const MyString& p) :pstr(p.pstr) //拷贝构造
{
if (pstr != nullptr)
{
pstr->ref += 1;
}
}
析构函数:
- 首先要判断pstr是否为NULL,不为NULL时再对其引用计数 ref -= 1
- 若减完 ref为0,说明再没有对象指向该字符串,需要将其空间释放。
- 释放完成后将pstr 置为NULL。
static void A_Free(StrNode* p)
{
free(p);
}
void clear() //释放函数
{
if (this->pstr != nullptr && --this->pstr->ref == 0)
{
A_Free(this->pstr);
}
this->pstr = nullptr;
}
~MyString()
{
clear();
}
赋值运算符重载:
- 待赋值对象是否和源对象是同一个对象,若不是则
- 如果待赋值对象不为空且只有他一个对象指向堆空间,则释放该空间,若对象本身为空,不执行
- 将源对象赋值给待赋值对象
- 如果源对象不为空,那么赋值过后的待赋值对象也指向堆空间,则需要将引用计数加一
- 返回this指针
MyString& operator=(const MyString& s) //共享资源
{
//s1=s2 //两个都空 或都相等
if (this == &s || this->pstr == s.pstr)
{
return *this;
}
//s2空 s1不空 s2(s1)
if (this->pstr == nullptr && s.pstr != nullptr)
{
this->pstr = s.pstr;
this->pstr->ref += 1;
}
//s1空 s2不空 s2(s1) //都不空
else
{
if (this->pstr != nullptr && --this->pstr->ref == 0)
{
free(this->pstr);
}
this->pstr = s.pstr;
if (this->pstr != nullptr)
{
this->pstr->ref += 1;
}
}
/*等价
if (this == &sx || this->pstr == sx.pstr)
{
return *this;
}
this->clear();
this->pstr = sx.pstr;
if (this->pstr != nullptr)
{
this->pstr->ref += 1;
}
*/
return *this;
}
?移动构造,移动赋值:
- 移动构造,就是将自己的资源进行转移,转移到要构造的对象里。
- 移动赋值,将自己的资源赋值给目标对象,自身置为空。?
MyString(MyString&& s) //移动构造,我把我的资源给你(资源的转移)
{
this->pstr = s.pstr;
s.pstr = nullptr;
}
MyString& operator=(MyString&& s)//移动赋值 资源转移,资源的引用计数不会改变
{
if (this == &s)return *this;
if (this->pstr == s.pstr)
{
s.clear();
return *this;
}
clear();
this->pstr = s.pstr;
s.pstr = nullptr;
return *this;
}
写实拷贝:
- 边界条件,如果为空,程序退出。如果下标不在范围内,程序终止。
- 判断引用计数是否大于1,如果不大于,那么可以直接返回该下标元素,如果大于1,说明还有其他对象也指向该字符串,需要拷贝。
- 重新申请堆区空间,将源空间内容拷贝进去
- 将新空间的引用计数加一,源空间引用计数减一,最后将新空间赋给该对象的指针pstr。
//共享同一块内存的类发生内容改变 发生写时拷贝
char& operator[](const int index)//写时拷贝
{
assert(index >= 0 && index < pstr->len);
if (pstr->ref > 1) //就要深拷贝了 克隆
{
int total = pstr->capc + 1;
StrNode* newstr = A_Malloc(total);
memmove(newstr, pstr, sizeof(StrNode) + sizeof(char) * (pstr->len + 1));
newstr->ref = 1;
pstr->ref -= 1;
pstr = newstr;
}
return pstr->data[index];
}
其他函数实现:
1、重载“+”,封装字符串连接实现对象和对象,对象和字符串,对象和字符的连接操作
private:
static StrNode* AddString(const char* pa, const char* pb, size_t n)//字符串连接
{
int total = round_up(2 * n);
StrNode* newdata = A_Malloc(total);
// newdata--> ref,len,capc,data 4
newdata->ref = 1;
newdata->len = n;
newdata->capc = total - 1;
newdata->data[0] = '\0';
if (pa != nullptr)
{
strcpy_s(newdata->data, total, pa);
}
if (pb != nullptr)
{
strcat_s(newdata->data, total, pb);
}
return newdata;
}
public:
//对象和对象
MyString operator+(const MyString& s)const
{
int n = this->size() + s.size();
if (0 == n)
{
return MyString();
}
const char* pa = pstr != nullptr ? pstr->data : nullptr;
const char* pb = s.pstr != nullptr ? s.pstr->data : nullptr;
StrNode* newdata = AddString(pa, pb, n);
return MyString(newdata);
}
//对象和字符串
MyString operator+(const char* s)const
{
int n = this->size() + ((s == nullptr) ? 0 : strlen(s));
if (0 == n)
{
return MyString();
}
const char* pa = pstr != nullptr ? pstr->data : nullptr;
StrNode* newdata = AddString(pa, s, n);
return MyString(newdata);
}
//对象和字符
MyString operator+(const char s)const
{
char str[2] = { s };
return *this + str;
}
2、实现扩容操作 对于malloc和realloc进行封装
private:
// 使用静态将其malloc封装
static StrNode* A_Malloc(size_t n)
{
StrNode* s = (StrNode*)malloc(sizeof(StrNode) + sizeof(char) * n);
if (nullptr == s) exit(1);
return s;
}
// realloc
static StrNode* A_Realloc(StrNode* p, size_t ns)//ns新的大小
{
StrNode* s = (StrNode*)realloc(p, sizeof(StrNode) + sizeof(char) * ns);
if (nullptr == s) exit(1);
return s;
}
//进行相应的增容
void reserve(size_t newcap)
{
if (newcap <= capacity()) return;//如果小于容量,不需要扩容
int total = round_up(newcap);
if (pstr == nullptr)
{
pstr = A_Realloc(pstr, total);
pstr->ref = 1;
pstr->len = 0;
pstr->capc = total - 1;
}
else if (pstr->ref > 1) //需要克隆一个新的空间
{ //如果直接realloc,之前的空间再被克隆后会还给堆区,成为失效指针
pstr->ref -= 1;
StrNode* newdata = A_Malloc(total);//自己的malloc
memmove(newdata, pstr, sizeof(StrNode) + pstr->len + 1);//把pstr的值移动到newdata
newdata->ref = 1;
newdata->len = pstr->len;
newdata->capc = total - 1;
pstr = newdata;
}
//如果引用计数等于1,在原有的基础上后面扩容
else{
StrNode* newdata = A_Realloc(pstr, total);//自己的realloc
pstr = newdata;
pstr->capc = total - 1;
}
}
3、其他例如获取字符串首字符,尾字符,容量,长度等等基础函数在下面源码给出(偷波懒~~)?
源码源码:
#include<iostream>
#include<cassert>
using namespace std;
class MyString
{
private:
enum { ALIGN = 8 };
struct StrNode //占12字节 ,柔性数组不会计算后面数组大小
{
int ref;//引用计数
int len;//字符串长度
int capc;//容量
char data[]; //柔性数组
};
private:
StrNode* pstr;
MyString(StrNode* p) :pstr(p) {}
//获取内存大小,对齐
static size_t round_up(size_t n)//将newcap提升成8的倍数(不一定是8倍数,4,16都行等等),扩容
{
return (n + ALIGN - 1) & ~(ALIGN - 1);
}
// 使用静态将其malloc封装
static StrNode* A_Malloc(size_t n)
{
StrNode* s = (StrNode*)malloc(sizeof(StrNode) + sizeof(char) * n);
if (nullptr == s) exit(1);
return s;
}
// realloc
static StrNode* A_Realloc(StrNode* p, size_t ns)//ns新的大小
{
StrNode* s = (StrNode*)realloc(p, sizeof(StrNode) + sizeof(char) * ns);
if (nullptr == s) exit(1);
return s;
}
static void A_Free(StrNode* p)
{
free(p);
}
static StrNode* AddString(const char* pa, const char* pb, size_t n)//字符串连接
{
int total = round_up(2 * n);
StrNode* newdata = A_Malloc(total);
// newdata--> ref,len,capc,data 4
newdata->ref = 1;
newdata->len = n;
newdata->capc = total - 1;
newdata->data[0] = '\0';
if (pa != nullptr)
{
strcpy_s(newdata->data, total, pa);
}
if (pb != nullptr)
{
strcat_s(newdata->data, total, pb);
}
return newdata;
}
public:
void clear() //释放函数
{
if (this->pstr != nullptr && --this->pstr->ref == 0)
{
A_Free(this->pstr);
}
this->pstr = nullptr;
}
//进行相应的增容
void reserve(size_t newcap)
{
if (newcap <= capacity()) return;//如果小于容量,不需要扩容
int total = round_up(newcap);
if (pstr == nullptr)
{
pstr = A_Realloc(pstr, total);
pstr->ref = 1;
pstr->len = 0;
pstr->capc = total - 1;
}
else if (pstr->ref > 1) //需要克隆一个新的空间
{ //如果直接realloc,之前的空间再被克隆后会还给堆区,成为失效指针
pstr->ref -= 1;
StrNode* newdata = A_Malloc(total);//自己的malloc
memmove(newdata, pstr, sizeof(StrNode) + pstr->len + 1);//把pstr的值移动到newdata
newdata->ref = 1;
newdata->len = pstr->len;
newdata->capc = total - 1;
pstr = newdata;
}
//如果引用计数等于1,在原有的基础上后面扩容
else{
StrNode* newdata = A_Realloc(pstr, total);//自己的realloc
pstr = newdata;
pstr->capc = total - 1;
}
}
public:
MyString(const char* p = nullptr) :pstr(nullptr)//构造函数
{
if (p != nullptr)
{
int n = strlen(p);
int total = round_up(2 * n); //扩容后柔型数组大小
pstr = A_Malloc(total); //封装后,不需要考虑结构体大小
if (pstr == nullptr)exit(1);
pstr->ref = 1;
pstr->len = n;
pstr->capc = total - 1;
strcpy_s(pstr->data, total, p);
}
}
MyString(const MyString& p) :pstr(p.pstr) //拷贝构造
{
if (pstr != nullptr)
{
pstr->ref += 1;
}
}
~MyString()
{
clear();
}
MyString& operator=(const MyString& s) //共享资源
{
//s1=s2 //两个都空 或都相等
if (this == &s || this->pstr == s.pstr)
{
return *this;
}
//s2空 s1不空 s2(s1)
if (this->pstr == nullptr && s.pstr != nullptr)
{
this->pstr = s.pstr;
this->pstr->ref += 1;
}
//s1空 s2不空 s2(s1) //都不空
else
{
if (this->pstr != nullptr && --this->pstr->ref == 0)
{
free(this->pstr);
}
this->pstr = s.pstr;
if (this->pstr != nullptr)
{
this->pstr->ref += 1;
}
}
/*等价
if (this == &sx || this->pstr == sx.pstr)
{
return *this;
}
this->clear();
this->pstr = sx.pstr;
if (this->pstr != nullptr)
{
this->pstr->ref += 1;
}
*/
return *this;
}
MyString(MyString&& s) //移动构造,我把我的资源给你(资源的转移)
{
this->pstr = s.pstr;
s.pstr = nullptr;
}
MyString& operator=(MyString&& s)//移动赋值 资源转移,资源的引用计数不会改变
{
if (this == &s)return *this;
if (this->pstr == s.pstr)
{
s.clear();
return *this;
}
clear();
this->pstr = s.pstr;
s.pstr = nullptr;
return *this;
}
//共享同一块内存的类发生内容改变 发生写时拷贝
char& operator[](const int index)//写时拷贝
{
assert(index >= 0 && index < pstr->len);
if (pstr->ref > 1) //就要深拷贝了 克隆
{
int total = pstr->capc + 1;
StrNode* newstr = A_Malloc(total);
memmove(newstr, pstr, sizeof(StrNode) + sizeof(char) * (pstr->len + 1));
newstr->ref = 1;
pstr->ref -= 1;
pstr = newstr;
}
return pstr->data[index];
}
const char& operator[](const int index)const
{
assert(index >= 0 && index < pstr->len);
return pstr->data[index];
}
void Print() const
{
if (pstr != nullptr)
{
cout << pstr->data << endl;
}
}
public:
size_t size()const//字符有效个数
{
return pstr == nullptr ? 0 : pstr->len;
}
size_t length()const
{
return size();
}
size_t capacity()//获取容量
{
return pstr == nullptr ? 0 : pstr->capc;
}
bool empty()const { return size() == 0; }//判空
const char* c_str()const//返回首字符的地址
{
return pstr != nullptr ? pstr->data : nullptr;
}
const char* data()const//返回首字符的地址
{
return c_str();
}
char& front()//取第一个字符
{
assert(pstr != nullptr);
return pstr->data[0];
}
const char& front()const//取第一个字符
{
assert(pstr != nullptr);
return pstr->data[0];
}
char& back()//取最后一个字符
{
assert(pstr != nullptr);
return pstr->data[pstr->len - 1];
}
const char& back()const//取最后一个字符
{
assert(pstr != nullptr);
return pstr->data[pstr->len - 1];
}
char& at(const int index) //取index下标下的字符
{
assert(pstr != nullptr && index > 0 && index < pstr->len);
return pstr->data[index];
}
const char& at(const int index)const //取index下标下的字符
{
assert(pstr != nullptr && index >= 0 && index < pstr->len);
return pstr->data[index];
}
public:
//对象和对象
MyString operator+(const MyString& s)const
{
int n = this->size() + s.size();
if (0 == n)
{
return MyString();
}
const char* pa = pstr != nullptr ? pstr->data : nullptr;
const char* pb = s.pstr != nullptr ? s.pstr->data : nullptr;
StrNode* newdata = AddString(pa, pb, n);
return MyString(newdata);
}
//对象和字符串
MyString operator+(const char* s)const
{
int n = this->size() + ((s == nullptr) ? 0 : strlen(s));
if (0 == n)
{
return MyString();
}
const char* pa = pstr != nullptr ? pstr->data : nullptr;
StrNode* newdata = AddString(pa, s, n);
return MyString(newdata);
}
//对象和字符
MyString operator+(const char s)const
{
char str[2] = { s };
return *this + str;
}
//对象和对象
MyString& operator+=(const MyString& s);
//对象和字符串
MyString& operator+=(const char* s);
//对象和字符
MyString& operator+=(const char s);
};
//字符串和对象
MyString operator+(const char* str, const MyString& s);
//字符和对象
MyString operator+(const char val, const MyString& s);
int main()
{
MyString s1("I love");
s1 = s1 + " you"+" very"+" much";
s1.Print();
}
? ? ? ? ? ? ? ? ? ? ? ? ? ??? 看到这里麻烦点个赞再走~
最近太忙了..代码也碰的少,等忙完这阵子继续更新,虽然也不知道什么时候了
|