#include <iostream>
#include <string>
using namespace std;
class A
{
friend ostream& print(ostream &os, A &a);
public:
A() = default;//默认构造函数
A(int a, string b) :i(a), s(b) { c++; }//有参构造函数
private:
int i;
string s;
static int c; //所有类的对象都能访问,只能在类外初始化 或者用初始化列表
//const static int c = 10; //const int 可以在类内初始化
};
int A::c = 10;//静态成员变量初始化
ostream& print(ostream &os, A &a)
{
os << a.i << " " << a.s << " " << a.c << endl;
return os;
}
int main()
{
A a1(10, "123");
A a2(a1); //没有定义拷贝构造函数,编译器自己合成了一个,叫合成拷贝构造函数。不拷贝static成员。
print(cout, a2);
A a3(10, "123");
print(cout, a3);
system("pause");
return 0;
}
1.拷贝,赋值与销毁
对初学c++的程序员来说,必须定义对象拷贝,移动,赋值或销毁时做什么
拷贝构造函数:如果一个构造函数的参数是本身的引用,且其他的参数都有默认值,此构造函数是拷贝构造函数。
合成拷贝构造函数:在我们没有为类定义时,编译器会帮我们定义一个。编译器从给定对象中一次将每个非static成员拷贝到正在创建的对象中
拷贝构造函数:参数是自身类类型的常量引用,如果不是引用会无限递归
#include <iostream>
#include <string>
class A
{
public:
A() = default;
A(int a, std::string b) :i(a), s(b) { } //直接初始化
A(const A&a) :i(a.i), s(a.s) { std::cout << "调用拷贝构造函数" << std::endl; }//拷贝初始化
private:
int i;
std::string s;
};
int main()
{
A a1(1, "123");
A a2(a1); //拷贝初始化
A a3 = a1;//拷贝初始化
//调用拷贝构造函数
// 调用拷贝构造函数
system("pause");
return 0;
}
拷贝操作不仅会在=发生还会在
向函数传递一个非引用的形参时
从一个返回类型为非引用类型的函数返回一个对象。
用花括号列表初始化一个数组或聚合类。
<2.拷贝赋值运算符Sales_data s1,s2;
s1 = s2;
和拷贝构造函数一样,如果类未定义拷贝赋值运算符,编译器会默认合成一个。合成拷贝赋值运算符会将右侧的除了非static的成员全部拷贝过去
某些运算符必须定义成成员函数,包括赋值运算符,如果一个运算符是一个成员函数,其左侧运算对象就绑定到this指针上。
赋值运算符重载要返回类的引用是*this,因为赋值有可能是a= b = c;这种赋值,所以必须返回它自己的指针,那么也就是*this。所以必须定义成成员函数,和输出函数返回流的引用是一样的,可能os << a << b;
<3.析构函数
析构函数执行的于构造函数执行的相反,析构函数释放所使用的资源,并释放非static数据成员。
析构函数不接受参数,不能被重载,对于一个类只有一个析构函数。
析构函数销毁的顺序是构造函数构造顺寻的逆序。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何首位工作。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象
什么时候会调用析构函数
变量在离开其作用域的时候
当一个对象被销毁时,其成员被销毁
容器被销毁时
当对指向它的指针应用delete运算符时被销毁
对于临时对象,当创建它的完整表达式结束时被销毁
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
合成析构函数
编译器会默认合成一个合成析构函数,析构函数是在合成析构函数的补充,比如堆上申请的对象,必须我们手动释放,就写在析构函数里面。
知道析构函数体自身并不直接销毁成员是非常重要的,成员是在析构函数体之后隐含的析构阶段被销毁的。
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class foo
{
public:
foo() :i(0), s(" "), p(new string){};
foo(int i1, string s1) :i(i1), s(s1), p(new string())//有参构造函数
{
cout << "调用构造函数" << endl;
}
foo(const foo &f)
{
i = f.i;
s = f.s;
p = new string(*f.p);
cout << "调用拷贝构造函数" << endl;
}
foo& operator=(const foo &f)
{
i = f.i;
s = f.s;
p = new string(*f.p);
cout << "调用赋值构造函数" << endl;
return *this;
}
~foo()
{
delete p;
cout << "析构函数" << endl;
}
private:
int i;
string s;
string *p;
};
foo fun1(foo f)
{
foo f2(f);
foo f3;
f3 = f2;
return f2;
}
int main()
{
foo f1(1, "123");
fun1(f1);
system("pause");
return 0;
}
我们可以通过=default让编译器来为我们生成默认版本
阻止拷贝:加上=delete
=delete必须在函数第一次声明的时候,意味着定义必须写到类的外面。
在类内声明在类外定义的不是内联函数,必须显示指定
与=delete不同的是我们可以指定任何函数为=delete,(只能对编译器可以默认合成的使用=default)
虽然删除函数主要用途是禁止拷贝控制成员,但当我们希望引导函数匹配的时候,删除函数也是有用的。
析构函数不能是删除的成员
删除一个类的析构函数或者类的某个成员的类型删除了析构函数,我们都不能定义该类的变量或临时对象
但可以动态分配这种类型,不能释放
<5.行为像值的类和行为像指针的类
行为像值的类:在类中包含需要动态分配的对象时,每次我们都new一个新值,那么在拷贝构造函数和赋值操作符也是要new一个新值。
对类资源的管理,每个类都有自己的一份
行为像指针的类:所有的类共享一份动态分配的对象,在c++11引入了智能指针shared_ptr , 所以我们可以很方便的来操作这些,但是如果我们想自己管理资源的话(不通过智能指针计数为0时释放资源)就要引入引用计数来辅助。
三个例子:
行为像值的类,行为像指针的类(智能指针指针),行为像指针的类(引用计数)
#include <iostream>
#include <string>
//1.行为像值的类
using namespace std;
class HasPtr
{
friend std::ostream& print(std::ostream &os, const HasPtr &hp);
public:
//HasPtr():p(nullptr),i(0) { }//加上会有二义性ambiguous
HasPtr(const std::string s = std::string()) :
p(new std::string(s)), i(0) {
std::cout << "调用单参或无参构造函数" << std::endl;
}
HasPtr(const std::string s, int t) :
p(new std::string(s)), i(t) {
std::cout << "调用两参构造函数" << std::endl;
}
HasPtr(const HasPtr &hp)
{
std::cout << "调用拷贝构造函数" << std::endl;
p = new string(*hp.p);
i = hp.i;
}
//重载等号运算符
HasPtr& operator=(const HasPtr &hp)
{
std::cout << "调用赋值运算符" << std::endl;
auto temp = new std::string(*hp.p);//考给临时变量,万一=左值是自己就释放了。hp.p是指向string p的地址 *hp.p是地址所指内存空间存储的内容
if (p != nullptr)
delete p;
p = temp;
i = hp.i;
return *this;
}
~HasPtr()
{
delete p;
}
private:
std::string *p;
int i;
};
std::ostream& print(std::ostream &os, const HasPtr &hp)
{
std::cout << "string:" << *hp.p << " int:" << hp.i << std::endl;
return os;
}
int main()
{
HasPtr p1;
HasPtr p2("hehe");
print(std::cout, p1);
print(std::cout, p2);
p1 = p2;
print(std::cout, p1);
HasPtr p4 = p2;
system("pause");
return 0;
}
//1.share_ptr类:
//shared_ptr和unique_ptr都支持的操作
//空智能指针。可以指向string类型的对象
shared_ptr<string> sp;
unique_ptr<string> up;
sp //sp可以作为条件判断sp是否指向一个对象
*sp //解引用sp,获得它指向的对象
sp->mem //等价于(*sp).mem
sp.get() //返回sp中所报存的指针。要小心使用,所智能指针释放了对象,则返回的指针所指向的对象也不存在了。
swap(sp, sq)
sp.swap(sq) //交换sp和sq中的指针
//shared_ptr支持的操作
make_shared<T>(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象
shared_ptr<T>p(q) //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T*
p = q //p和q都是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,增加q的引用计数,p引用计数为0时会释放其管理的内存。
p.use_count() //返回与p共享智能指针的数量,可能很慢主要用于调试
p.unique() //当p.use_count()为1时,返回ture,否则返回false。
6.swap交换操作
和拷贝控制成员不同,swap并不是必要的手段,但是对于分配了资源的类,定义swap可能是一种很重要的优化手段
因为我们对类使用系统的版本可能效率会非常的低,交换类对象时可能会是
void swap(A &a1, A &a2)
{
A temp = a1; //调用拷贝构造函数
a1 = a2; //调用赋值运算符
a2 = temp; //调用赋值运算符
}
在这段代码里使用了一次拷贝两次赋值,可见效率是比较低的,尤其对复杂的类
我们可以定义我们自己的swap,提升效率
且在赋值运算符中也可以使用swap来提升效率,
且使用拷贝和交换的赋值运算符自动就是异常安全的,能处理自动赋值
看HasPtr的改版,并使用vector排序
在这个例子中如果交换我们之用交换指针和字符串p,效率较高,但是如果我们用系统的版本就会一次拷贝两次赋值。
C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。
在C++中,类的构造函数可以省略不写,这时C++会为它自动创建一个隐式默认构造函数;也可以由用户定义带参数的构造函数,构造函数也是一个成员函数,他可以被重载;当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。
7.动态内存管理类
某些类需要自己进行内存分配,这些类要自己定义拷贝拷贝控制成员。
在某些操作中,动态内存管理类的性能会好的多,因为部分操作不会自己调用默认构造函数来初始化了,都是我们来直接拷贝赋值。
8.对象移动
是c++11的新特性,也是非常重大的改变之一
很多情况下都会发生拷贝,而拷贝后就立即被销毁了,在这种情况下移动而非拷贝对象会大幅度提升效率。
IO类和unique_ptr 类可以移动但不能拷贝。
2.为了支持移动操作,新标准引入了右值引用&&
右值引用只能绑定在将要销毁的对象上
返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符,都是返回左值的表达式,我们可以将这些值绑定到左值表达式上。
返回非引用类型的函数,连同算数,关系,位以及后置递增递减运算符(可以发现这些值都是临时的,即将销毁的),都生成右值。 我们不能将左值引用绑定到这类运算符上,但是可以将const的左值引用或者一个右值引用绑定到这类表达式上。
简单的说左值引用生存期长,右值引用生存期短
右值引用只能绑定到临时对象,该对象没有其他用户。使用右值引用的代码可以自由的接管所引用对象的资源。
不能将变量绑定到右值上,即使变量是右值引用类型。
标准库move函数
虽然不能将右值绑定到左值上面,但是可以显示的将左值转化为右值类型。
move告诉编译器:我们有一个左值但是我们希望像右值一样处理它,调用move后我们只能销毁对象或者赋值
使用move的代码应该使用std::move
3.移动构造函数和移动赋值运算符
要求:类支持拷贝和赋值
移动构造函数的第一个参数是该类类型的一个右值引用,和拷贝构造函数一样,其他任何参数都必须有默认的实参
移动构造函数确保移动后源对象处于这样一个状态,销毁它是无害的,一旦资源完成移动,源对象就必须不再指向
被移动的资源,这些资源的所有权已经归属新创建的对象
和构造函数不同,移动构造函数不创建任何新的资源。
可以这样理解,上面所说的算数,关系位和后置运算符都产生的是一个临时的对象,即将被销毁的,此时我们调用移动构造函数,将它的
所有权交给一个新创建的对象,也就是即将被销毁的不销毁了交给了别人保管。
移动操作不应该抛出异常
所以我们应该用noexcept来显示的指定
看StrVec的移动构造函数
-
StrVec::StrVec(StrVec &&s) noexcept //noexcept指名不抛出异常
-
:elements(s.elements), first_free(s.first_free),cap(s.cap)
-
{
-
s.elements = s.first_free = s.cap = nullptr; //销毁地动源后的对象
-
}
noexcept是C++11引入的,在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
之所以不允许抛出异常是因为在移动过程中抛出异常会产生问题。移动源改变了,新创建的对象也改变了,源对象不能保证在失败的情况下自身不变的要求
移动赋值运算符
-
StrVec& StrVec:: operator=(StrVec &&s) noexcept //不抛出任何异常
-
{
-
if( this != &s) //检测是否是自己给自己赋值,
-
{
-
free(); //释放this本身资源
-
elements = s.elements;
-
first_free = s.first_free;
-
cap = s.cap;
-
s.elements = s.cap = s.first_free = nullptr;
-
}
-
return * this;
-
}
当我们编写一个移动赋值运算符时,必须保证移后源对象必须保持有效的,可析构的状态,但是我们不能对它的值进行任何假想。