标准库String底层实现
方式1:深拷贝
- EC(eager copy)
- 无论什么情况,都是采用拷贝字符串内容的方式解决,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下
方式2:写时复制
-
COW(copy on write)
浅拷贝+引用计数refcount,string占8个字节
-
当两个std::string发生复制构造或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当需要修改其中一个字符串内容时,才执行真正的复制
-
引用计数放在哪?
-
放在堆上数据后面,当数据大小改变的时候,应用计数也要改变
-
int大小就够用2^31,先申请5个字节之后偏移4字节
先不偏移,赋值1之后再偏移有什么不妥?初始化的含义,返回字符串首地址
-
栈对象不方便修改
-
全局对象会共用,不同的对象可能不同
-
String &operator=(const String &rhs){
if(this!=&rhs){//自复制
--*(int *)(_pstr-4);//释放左操作数
if(0==*(int *)(_pstr-4)){//只有它一个,需要回收空间
delete [] (_pstr-4); //初始化时一个字符数组
}
_pstr=rhs._pstr;//浅拷贝
++*(int *)(_pstr-4);//引用计数加一
}
return *this;//返回自己
}
~String(){
--*(int *)(_pstr-4);//引用计数-1
if(0==*(int *)(_pstr-4)){//只有它一个,需要回收空间
delete [] (_pstr-4);
_pstr=null;
}
}
char &operator[](size_t idx){//读也会改变,如何区分?将整体新定义成一个类,而后对新类的=重载,带=是写不带是读
if(idx<size()){
if(getRefcount()>1){//判断共享
char *ptmp=new char[size()+5]()+4;
strcpy(ptmp,pstr);
decreaseRefcount();//引用计数减一
_pstr=ptemp;//在对象销毁的时候ptmp会delete
initRefcount();
}
return _pstr[idx];
}else{
static char nullchar='\0';
return nullchar;
}
}
方式:短字符串优化
-
SSO(short string optimization)
-
当字符串的长度小于16个字节时,buffer直接存放整个字符串,数据在栈上;当字符串大于等于16个字节时,buffer存放的就是一个指针,数据在堆上
(16字节即2个指针,string占32字节)
-
这样做的好处是,当字符串较小时,直接拷贝字符串,放在string内部,不用获取堆空间,开销小。
面向对象之继承
- 在C 语言中重用代码的方式就是拷贝代码、修改代码
- C++ 中代码重用的方式之一就是采用继承
继承的基本概念
-
面向对象角度:客观现实的层次体系的一个体现
-
C++:在原有类的基础上产生新的类
-
原有类型:父类、基类
-
新类型:子类、派生类
class 派生类
: public/protected/private 基类
{
//控制权限
public:
protected:
private:
};
继承过程
- 吸收基类的成员(一定发生)
- 改造基类的成员
- 添加自己新的成员
继承局限
- 下面5种情况不能继承
- 构造函数
- 析构函数
- 用户重载的operator new/delete运算符
- 用户重载的operator=运算符
- 友元关系
继承方式
-
三种:public/private/protected
- 默认继承方式私有
- protected继承与private继承区别:
- protected继承可以访问基类中protected成员
- protected多次继承仍然是protected,private二次继承之后一直private
-
总结
- 不管是什么继承方式,派生类内部都不能访问基类的私有成员,其他的都可以访问
- 派生类对象只能访问公有继承基类中的公有成员,其他的一律不能访问
-
图示
(继承方式和基类访问权限取交集
严格程度private>protected>public)
单继承
派生类对象的构造
-
原则:先初始化基类部分,然后初始化派生类部分。
错误的说法是:派生类对象的构造是“先调用基类的构造函数,然后调用派生类的构造函数”//没有基类对象的创建
在创建派生类对象的时候,会调用派生类的构造函数,在调用派生类构造函数的时候,为了完成基类吸收的数据成员的初始化,调用基类的构造函数,完成初始化
-
构造函数情况:派生类+基类→
- 有显式定义+无显式定义→
- 创建派生类的对象时,派生类相应的构造函数会被自动调用,此时自动调用了基类缺省的无参构造函数
- 无显式定义+有显式定义→
- 基类必须拥有默认构造函数
- 有显式定义+有默认→
- 如果你想调用基类的有参构造函数,必须要在派生类构造函数的初始化列表中显示调用基类的有参构造函数
- 有显式定义+有显示定义无默认→
- 派生类的每一个构造函数必须在其初始化列表中显示的去调用基类的某个带参的构造函数。如果派生类的初始化列表中没有显示调用则会出错,因为基类中没有默认的构造函数。
- 总结:必须将基类构造函数放在派生类构造函数的初试化列表中
- 有显式定义+无显式定义→
-
调用顺序:
- 完成对象所占整块内存的开辟,由系统在调用构造函数时自动完成。
- 调用基类的构造函数完成基类成员的初始化。
- 派生类特殊成员:对象成员、const 成员或、static成员、引用成员,在初始化表中完成其初始化
- 派生类的构造函数体执行
派生类对象销毁
- 当派生类对象被删除时,派生类的析构函数被执行。析构函数同样不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用。
- 调用顺序:
- 先调用派生类的析构函数
- 再调用派生类中成员对象的析构函数
- 最后调用普通基类的析构函数
表中完成其初始化
4. 派生类的构造函数体执行