条款12:为意在改写得函数添加override声明
//宗旨:派生类中虚函数得实现,会改写基类中对应虚函数得实现
//实例1:基类接口调用派生类函数
class Base{
public:
virtual void doWork() {
cout<<"Base: virtual"<<endl;
}//基类中得虚函数
};
class Derived:public Base{
public:
virtual void doWork(){
cout<<"Derived: virtual"<<endl;
}//改写了 Base::doWork (virtual 可写可不写)
};
//以上改写动作必须满足得要求如下:
/**
1,基类中得函数必须是虚函数
2,基类和派生类中得函数名字必须完全相同,析构函数除外
3,基类和派生类中得函数形参型别必须完全相同
4,基类和派生类中得函数常量性必须完全相同
5,基类和派生类中的函数返回值和异常规格必须兼容
*/
//实例2:C++11新规定:基类和派生类中的函数引用修饰词必须完全相同
//这个概念是为了实现限制成员函数仅仅用于左值或右值,带有引用修饰词的成员函数,不必是虚函数
class Widget{
public:
void doWork() &{
cout<<"left: value"<<endl;
}//仅在 *this是左值时调用
void doWork() &&{
cout<<"right: value"<<endl;
}//仅在 *this是右值时调用
Widget&& makeWidget(){
//Widget w;
return std::move(*w);
}
private:
Widget *w;
};
class DerivedWidget:public Widget{
public:
void doWork() &{
cout<<"son left: value"<<endl;
}//仅在 *this是左值时调用
void doWork() &&{
cout<<"son right: value"<<endl;
}//仅在 *this是右值时调用
};
//以上改写:需要满足
/**
1,如果基类中的虚函数带有引用修饰词,则派生类要对该函数进行改写版本必须也带有完全相同的引用修饰词
2,如果不这样,那么这些声明了的函数在派生类依然存在,只是不好改写基类中的任何函数
*/
//实例3:找毛病,没有改写的错误写法
class BaseE{
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;//改写的函数前面加上 virtual
};
class DerivedE:public BaseE{
public:
virtual void mf1();//非const
virtual void mf2(unsigned int x);//形参不同
virtual void mf3() &&;//右值
void mf4() const;//基类中未声明虚函数
};
//改写上述写法:显式标明派生类中的函数为了改写基类版本,为其加上 override声明
//但前提是要满足 实例 1中的那些条件
//但前提是要满足 实例 1中的那些条件
class DerivedEE:public BaseE{
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
//void mf4() const override;//加 virtual 没问题,但是也没必要
};
//override关键字:不止编译器在你想要改写的函数实际上并未改写时提醒你,它还可以在你打算更改基类中的虚函数的签名时,衡量一下波及的影响面
//final关键字:应用于虚函数,会阻止它在派生类中被改写,final也可以被应用于一个类,在这种情况下,该类会被禁止用作基类
//实例4:成员函数的形参是 左值或 是右值
class WidgetLR{
public:
using DataType = std::vector<double>;
DataType& data() & {
return values; //返回左值
}
DataType data() && {
//此时 values已经不存了,只要有它得地方就有问题,重新定义一个values
DataType values = {1,3,5};
cout<<"bbb"<<values.size()<<endl;
for(auto i:values)
{
cout<<i<<endl;
}
return std::move(values);//返回右值,一个临时对象
}
WidgetLR&& makeWidget() {
WidgetLR w;
cout<<"aaa"<<endl;
return std::move(w);
}
private:
DataType values = {1,2,4};
};
//Widget::data返回值型别是一个左值引用,更准确得说是 std::vector& 左值初始化vals1,复制构造
void doSomething(WidgetLR& w);//仅仅接受左值的Widget
void doSomething(WidgetLR&& w);//仅仅接受右值得Widget型别
客户端测试:
int main()
{
//实例1 测试
//常见基类指针,指向派生类对象
std::unique_ptr<Base> upb = std::make_unique<Derived>();//c++14
upb->doWork();
//实例2:测试
//Widget w;//普通对象返回左值
std::unique_ptr<Widget> w = std::make_unique<DerivedWidget>();//c++14
w->doWork();//以左值调用 Widget::doWork (Widget::doWork &)
w->makeWidget().doWork();//以右值调用 Widget::doWork (Widget::doWork &&)
//实例4:测试
WidgetLR wLR;
auto vals1 = wLR.data();//把 wLR.values复制到 vals1中
for(auto i:vals1)
{
cout<<i<<endl;
}
auto vals2 = wLR.makeWidget().data();//右值重载版本,vals2采用移动构造完成初始化
}
// 要点速记
// • 为意在改写的函数添加 rri de 卢明
// • 成员函数引用饰词使得对于左值和右值对象 (*this 的处理能够区分开来。
//宗旨:任何时候只要你需要一个迭代器而其指涉得内容没有修改必要,你就应该使用 const_iterator
//情况1:C++98 中 const_iterator得到的支持不够全面
//情况1:C++98 中 const_iterator得到的支持不够全面
std::vector<int> values{1990,1983,2000};
std::vector<int>::iterator it = std::find(values.begin(),values.end(),1983);
values.insert(it,1998);
for(auto i:values)
{
cout<<i<<endl;
}
// 以上 运行没有问题,但并非正确选择,因为代码中没有任何地方修改了 iterator指涉的内容
//C++98中 修改上述代码,使其变成 const_iterator
//C++98中 修改上述代码,使其变成 const_iterator
typedef std::vector<int>::iterator IterT; //推荐使用using,这个是 C++11的新特性
typedef std::vector<int>::const_iterator ConstIterT;
std::vector<int> valuess = {1990,1983,2000};
//强制型别转换
ConstIterT ci = std::find(static_cast<ConstIterT>(valuess.begin()),
static_cast<ConstIterT>(valuess.end()),
1983);
//valuess.insert(static_cast<IterT>(ci),1998);//无法通过编译
//理由如下:insert的位置必须是 iterator的型别,不接受 const_iterator
//记住可以从 iterator型别转换到const_iterator, 但是反之不可以
//因此 const_iterator在C++98中并不好用,所以大家都用情况1了
for(auto i:valuess)
{
cout<<i<<endl;
}
//情况2:C++11 获取和使用 const_iterator容易多了
//容器的成员函数 cbegin和 cend都返回 const_iterator型别,甚至对于非 const 容器也是如此
//并且需要记住:STL一些成员函数取用指示位置的迭代器,例如插入,删除,它们也要求使用 const_iterator型别
//改写情况1 如下
//改写情况1 如下
std::vector<int> values2{1990,1983,2000};
auto it2 = std::find(values2.cbegin(),values2.cend(),1983);
values2.insert(it2,1998);
for(auto i:values2)
{
cout<<i<<endl;
}
//情况3:const_iterator的支持不充分的时候,是你在想撰写 最通用化的库 代码的情况下
//因为你需要考虑:某些容器或类似容器的数据结构会以 非成员函数的方式 提供 begin 和 end, cbegin,cend和rbegin等
//而不是用成员函数的方式
//因此:最通用化的代码会使用非成员函数,而不会假定其成员函数版本的存在性
//通用模板改写 情况2的代码
//在容器中查找 targetVal 第一次出现的位置 然后在此处插入 insertVal
//通用模板改写 情况2的代码
//在容器中查找 targetVal 第一次出现的位置 然后在此处插入 insertVal
template<typename C, typename V>
void findAndInsert(C& container,
const V& targetVal,
const V& insertVal)
{
using std::cbegin;
using std::cend;
//非成员函数版本的cbegin
//C++14 中包含非成员函数版本的 cbegin, cend, rbegin, rend, crbegin,crend
//c++11 中仅仅包含了非成员函数版本的 begin 和 end
auto it3 = std::find(cbegin(container),cend(container),targetVal);
container.insert(it3,insertVal);
}
//C++11虽然没有上面那些非成员函数的版本,但是可以自己实现呀 cbegin 的实现
template<class C>
auto cbegin11(const C& container) -> decltype(std::begin(container))
{
//非成员函数的 cbegin并没有调用成员函数版本的 cbegin 是不是吃了一惊?
return std::begin(container);//C++11
}
template<class C>
auto cend11(const C& container) -> decltype(std::end(container))
{
//非成员函数的 cbegin并没有调用成员函数版本的 cbegin 是不是吃了一惊?
return std::end(container);//C++11
}
//以上解释如下:cbegin模板接受一个形参C,实参型别可以是任何表示类似容器的数据结构,并通过引用到const型别的形参 container来访问该实参。
/**
如果C对应一个传统容器型别 std::vector则container就是该型别的引用到 const 的版本,const std::vector&,调用 C++11 提供的非成员函数版本的 begin函数 并传入一个const 容器会产生一个 const_iterator,而模板返回的正是这个迭代器。
*/
template<typename C, typename V>
void findAndInsert(C& container,
const V& targetVal,
const V& insertVal)
{
// using std::cbegin;
// using std::cend;
//非成员函数版本的cbegin
//C++14 中包含非成员函数版本的 cbegin, cend, rbegin, rend, crbegin,crend
//c++11 中仅仅包含了非成员函数版本的 begin 和 end
auto it3 = std::find(cbegin11(container),cend11(container),targetVal);
container.insert(it3,insertVal);
}
/**
要点速记
1, 优先选用 const_iterator,而非 iterator
2, 在最通用的代码中,优先选用非成员函数版本的 begin,end和rbegin等,而非成员函数版本
*/
//宗旨:函数不会发生异常的时候,记得加 noexcept声明,有利于编译器对程序做更多的优化
//如果在运行时候,noexcept函数向外抛出了异常,内部捕获异常并完成处理,这种情况不算抛出异常,程序会直接终止调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
//函数是否会发射异常这一行为,是客户方关注的核心,调用方可以查询函数的 noexcept状态,而查询结果可能会影响调用代码的异常安全性或运行效率
//因此可以理解:函数是否带有 noexcept声明,和成员函数是否带有 const声明是一样的。
//情况1:向调用方保证它们不会接受到异常
//C++98
//情况1:向调用方保证它们不会接受到异常
//C++98
int f1(int x) throw()
{
cout<<"throw: "<<100/x<<endl;
return 0;
}
//c++11
int f2(int x) noexcept(true)
{
cout<<"noexcept: "<<100/x<<endl;
return 0;
}
//测试1
f1(1);
f2(0);
//情况2:能移动则移动,必须复制才复制
/**
当向 std::vector 型别对象中添加新元素,可能空间不够;即 std::vector 型别对象的尺寸 size 和 其容量capacity相等的时刻
此时,std::vector型别对象会分配一个新的,更大的内存块来存储其元素,然后它把元素从现存的内存块转移到新的
1,C++98 :先把元素逐个地从旧内存复制到新内存,然后将旧内存地对象析构
2,C++11 :将复制变成了移动操作
*/
//情况2:能移动则移动,必须复制才复制
class Widget{
};
//测试2
std::vector<Widget> vw;
Widget w;
vw.push_back(w);
//宗旨:constexpr 用于对象,是一个加强版地const,但是应用于函数,却有者相当不同地意义。
//情况1:constexpr对象具备const属性,并且是在编译阶段已知
//编译阶段已知的常量值可以用在 C++ 要求整型常量表达式的语境中,这些语境包括数组的尺寸规格,整型
//模板实参(std::array型别对象的长度),枚举量的值,对齐规格等
//测试1:编译器保证它们具备一个编译期的值
int sz;//非 constexpr变量
// constexpr auto arraySize1 = sz;//错误!sz的值在编译期未知
//std::array<int,sz> data;//错误,一样的问题
constexpr auto arraySize2 = 10;//没问题,10是个编译器常量
std::array<int,arraySize2> data2;//没问题,arraySize2是个constexpr
//情况2:const并未提供和 constexpr同样的保证,因为 const对象不一定经由编译器已知值来初始化
//测试2:
int sz22;//仍是非 constexpr变量
const auto arraySize22 = sz22;//没问题,arraySize是sz的一个const副本
//std::array<int,arraySize22> data22;//错误!arraySize22的值非编译期可知
//总结:所有 constexpr对象都是const对象,而并非所有的const对象都是 constexpr对象。
//情况3:constexpr修饰的函数包含两大类
/**
1, 要求编译期常量的语境中:如你传给一个 constexpr函数的实参值是在编译器已知的,则结果也会在编译期间计算出,如果任何一个实参值在编译期未知,则你的代码将无法通过编译。
2,调用 constexpr函数时,如传入的值有一个或多个在编译期未知,则它的运作方式和普通函数无异,它也是在运行期执行结果的计算。
*/
//pow是个 constexpr函数,且不会抛出异常
//constexpr并不是表面 pow要返回一个const值,它表明的是如果 base和exp是编译期的常量,pow的返回结果就可以当一个编译期常量使用
//如果base和exp中有一个不是编译期常量,则 pow的返回结果就将在执行期计算,因此还可以用于执行语境
constexpr int pow(int base,int exp) noexcept
{
return 20;
}
//constexpr auto numConds = 5;//条件变量
//std::array<int,pow(3,numConds)> results;//result有 3^numConds个元素
int readFromDB(char str)
{
int result = 0;
cout<<"str: "<<str<<endl;
switch(str)//char int short byte
{
case 'b' :
result = 3;
break;
case 'e' :
result = 5;
break;
default :
result = 4;
break;
}
return result;
}
//因为constexpr函数在传入编译期常量时能够返回编译期结果,它们的实现就必须加以限制,而在C++11和C++14中,这样的限制还有所不同
//C++11:constexpr函数不得包含多于一个可执行语句,即一条return语句,不过可以用条件表达式去扩展这种功能,if/else 循环的地方用递归等
//因此pow的实现如下
constexpr int pow11(int base,int exp) noexcept //c++11
{
return (exp == 0? 1: base * pow(base,exp-1));
}
//C++14: 限制条件大大放宽,c++11如下编译不过
constexpr int pow14(int base,int exp) noexcept //c++14
{
auto result =1;
for(int i=0; i<exp; ++i)
{
result *= base;
}
return result;
}
//测试3:
//编译期执行
//编译期执行
constexpr auto numConds = 5;//条件变量
std::array<int,pow(3,numConds)> results;//result有 3^numConds个元素
//运行期执行
auto base = readFromDB('b');
auto exp = readFromDB('e');
auto baseToExp = pow11(base,exp);//pow函数在执行期被调用
cout<<"baseToExp: "<<baseToExp<<" "<<base<<" "<<exp<<endl;
//情况4:constexpr函数仅限于传入和返回字面型别,这样的型别能够持有编译期可以决议的值,void不是
//用户自定义型别同样可能也是字面型别,因为它的构造函数和其他成员函数可能也是 constexpr函数
class Point{
public:
//传入的实参是在编译期可知,构造出来的 Point对象的数据成员,其值也是在编译期可知的
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal),y(yVal)
{}
//访问器也可以声明为 constexpr,原因推导出 xy的值也可以在编译期获知
constexpr double xValue() const noexcept {return x;}
constexpr double yValue() const noexcept {return y;}
//注意 c++11 中有两个限制规定如下不能定义为 constexpr
//1, 它们修改了操作对象,在c++11中 constexpr函数都隐式地被声明为 const的了
//2,它们的返回型别是 void, c++11中 void并不是字面型别
void setX11(double newX) noexcept {x = newX;}
void setY11(double newY) noexcept {y = newY;}
//c++14中,上面两个限制被解除了,可以声明为 constexpr了
constexpr void setX14(double newX) noexcept {x = newX;}
constexpr void setY14(double newY) noexcept {y = newY;}
double x,y;
};
//使用其结果来初始化 constexpr对象,传统上在运行期完成的工作可以迁移到编译期完成。
constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) /2, (p1.yValue() + p2.yValue()) /2};
}
//C++14中可以写出如下代码:返回p相对于原点的中心对称点
constexpr Point reflection(const Point& p) noexcept
{
Point result;//创建一个非 const的Point对象
result.setX14(-p.xValue());
result.setY14(-p.yValue());
return result;//返回 result的副本
}
//测试4:
constexpr Point p1(9.4,27.7);//没问题,在编译期 运行,constexpr构造函数
constexpr Point p2(28.8,5.3);//没问题
//使用constexpr函数的结果来初始化 constexpr对象
constexpr auto mid = midpoint(p1,p2);
constexpr auto refMid = reflection(mid);//在编译期已知这个结果,运行就快了呀,编译慢了
cout<<"refMid: "<<refMid.x<<" "<<refMid.y<<endl;
// 要点速记
// • constexpr 对象都具各 const 属性,并由编译期已知的值完成初始化
// • constexpr 函数在调用时若传入的实参值是编译期已知的,则会产出编译期 结果。
// • 比起非 constexpr 对象或 constexpr 函数而言, constexpr 对象或是constexpr 函数可以用在一个作用域更广的语境中
//宗旨:多个线程同时调用带有 const 得成员函数,如何保证线程的安全性
//const成员函数就一定是线程安全的吗?
//情况1:mutex
//缓存多项式的值
class Polynomial{
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
//加上互斥量
std::lock_guard<std::mutex> g(m);
//如果缓存无效
if(!rootsAreValid)
{
//则计算根,存入 rootVals
for(int i=0;i <3;i++)
{
cnt++;
rootVals.push_back(cnt);
cout<<"i: "<<cnt<<endl;
}
rootsAreValid = true;
}
return rootVals;
} //解除互斥量
// protected:
//mutable 可改变的,不加这个const成员函数用不了
mutable bool rootsAreValid{false};
mutable RootsType rootVals{};
//引入 mutex互斥量 保证线程安全
mutable std::mutex m;
mutable int cnt = 0;
};
//https://blog.csdn.net/weixin_42700740/article/details/126154807
//两个线程同时在同一个 Polynomial 对象上调用 roots
void thread1(Polynomial *pn)
{
cout<<"thread1 begin "<<endl;
pn->rootsAreValid = false;
pn->roots();
cout<<"thread1 end "<<endl;
}
void thread2(Polynomial *pn)
{
cout<<"thread2 begin "<<endl;
pn->rootsAreValid = false;
pn->roots();
cout<<"thread2 end "<<endl;
}
//测试1:两个线程同时在同一个 Polynomial 对象上调用 roots
//测试1:两个线程同时在同一个 Polynomial 对象上调用 roots
Polynomial *pn = new Polynomial();
//vector<double> result = pn.roots();
std::thread subthread1(&thread1,pn);
std::thread subthread2(&thread2,pn);
subthread1.join();
subthread2.join();
for(auto i:pn->rootVals)
{
cout<<"main thread: "<<i<<endl;
}
//危险:const成员函数意味着只读,多个线程在没有同步的条件下执行读操作是安全的
//但是,本案例并不安全,roots()虽然是const成员函数,但是企图改变两个 mutable的成员变量的值
//方法一:保证 const成员函数是安全的,需要加上 mutex
//但是 std::mutex只个只能移动但不能复制的型别,将 m加入 Polynomial的副作用就是 Polynomial
//失去了可复制性,不过它仍然可以移动,因此是否可以考虑替代方案 std::mutex
//https://blog.csdn.net/yzf279533105/article/details/90605172
//https://blog.csdn.net/HW140701/article/details/105267828
//方法二:加入 std::atomic型别的计数器 可以确保其他线程可以以不加分割的方式观察到其操作发生
// 使用 std::atomic型别的对象来计算调用次数
class Point{
public:
double distanceFromOrigin() const noexcept
{
//带原子的自增操作
// ++callCount;
//这个等价于 非原子的 ++callCount 前面加个锁保护
std::lock_guard<std::mutex> g(m);
++callCount;
cout<<"callCount: "<<callCount<<endl;
return std::sqrt((x*x) + (y *y));
}
//private:
//mutable std::atomic<unsigned> callCount{0};
mutable unsigned callCount{0};
mutable std::mutex m;
double x,y;
};
void thread11(Point *pn)
{
cout<<"thread11 begin "<<endl;
pn->distanceFromOrigin();
cout<<"thread11 end "<<endl;
}
void thread22(Point *pn)
{
cout<<"thread22 begin "<<endl;
pn->distanceFromOrigin();
cout<<"thread22 end "<<endl;
}
//方法二中的 std::atomic也是只移动型别,因此 Point中的callCount的存在会使得 Point也变成只移动型别
//方法二中的 std::atomic也是只移动型别,因此 Point中的callCount的存在会使得 Point也变成只移动型别
//测试2:原子操作 std::atomic
//测试2:原子操作 std::atomic
Point *po = new Point();
std::thread subthread11(&thread11,po);
std::thread subthread22(&thread22,po);
subthread11.join();
subthread22.join();
cout<<"main thread: "<<po->callCount<<endl;
//方法二中的 std::atomic也是只移动型别,因此 Point中的callCount的存在会使得 Point也变成只移动型别
//情况3:std::atomic型别的变量 和 加上与解除互斥量相比,开销往往比较小,因此可以考虑用这个代替 mutex
//特别是需要缓存计算开销较大的 int 型别的变量
//实现1
//实现1
class Widget{
public:
int expensiveComputation1() const{
int i =0;
for(; i<100000000;i++)
{
}
cout<<"E1: "<<i<<endl;
return i;
}
int expensiveComputation2() const{
int i =0;
for(; i<100000001;i++)
{
}
cout<<"E2: "<<i<<endl;
return i;
}
int magicValue() const
{
if(cacheValid)
return cachedValue;
else
{
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
//第一部分
cachedValue = val1 + val2;
//第二部分
cacheValid = true;
return cachedValue;
}
}
//private:
mutable std::atomic<bool> cacheValid{false};
mutable std::atomic<int> cachedValue;
};
void thread111(Widget *pn)
{
cout<<"thread111 begin "<<endl;
pn->magicValue();
cout<<"thread111 end "<<endl;
}
void thread222(Widget *pn)
{
cout<<"thread222 begin "<<endl;
pn->magicValue();
cout<<"thread222 end "<<endl;
}
//测试3:双atomic
//测试3:双atomic
Widget *wi = new Widget();
std::thread subthread111(&thread111,wi);
std::thread subthread222(&thread222,wi);
subthread111.join();
subthread222.join();
cout<<"main thread: "<<wi->cachedValue<<endl;
/**
实现1 的缺陷:
一个线程调用 magicValue时,观察到 cacheValid为false时,执行两大开销计算,并将其和赋值给了cacheValue,此时另一个线程也观察到 cacheValid值为false,也执行了第一个线程刚刚完成的两次同样的大开销运算
*/
//实现2
//如何避免实现1的缺陷:将第一部分和第二部分进行顺序互换
/**
实现2的缺陷更大了:一个线程调用 magicValue并执行到了 cacheValid值被置为true的时刻,另一线程也在调用 magicValue并监视 cacheValid的值,观察到其为 true后,该线程就把 cacheValid的值给返回额,即使此时第一个线程还没有执行对 cacheValid的赋值,因此,返回值是不正确的
*/
//实现3
//继续改进:
//1, 对于单个要求同步的变量或内存区域,使用 std::atomic就足够了
//2,但是如果有两个或更多个变量或内存区域需要作为一整个单位进行操作时候,就要用互斥量了
//实现1
class Widget2{
public:
int expensiveComputation1() const{
int i =0;
for(; i<100000000;i++)
{
}
cout<<"E11: "<<i<<endl;
return i;
}
int expensiveComputation2() const{
int i =0;
for(; i<100000001;i++)
{
}
cout<<"E22: "<<i<<endl;
return i;
}
int magicValue() const
{
std::lock_guard<std::mutex> guard(m);
if(cacheValid)
return cachedValue;
else
{
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
//第一部分
cachedValue = val1 + val2;
//第二部分
cacheValid = true;
return cachedValue;
}
}
//private:
mutable bool cacheValid{false};//不再具备原子性
mutable int cachedValue;
mutable std::mutex m;
};
void thread1111(Widget2 *pn)
{
cout<<"thread1111 begin "<<endl;
pn->magicValue();
cout<<"thread1111 end "<<endl;
}
void thread2222(Widget2 *pn)
{
cout<<"thread2222 begin "<<endl;
pn->magicValue();
cout<<"thread2222 end "<<endl;
}
//测试4:mutex
//测试4:mutex
Widget2 *wii = new Widget2();
std::thread subthread1111(&thread1111,wii);
std::thread subthread2222(&thread2222,wii);
subthread1111.join();
subthread2222.join();
cout<<"main thread: "<<wii->cachedValue<<endl;
// 要点速记
// • 保证 const 成员函数的线程安全性,除非可以确信它们不会用在并发语境中
// • 运用 std::atomic 型别的变量会比运用互斥量提供更好的性能,但前者仅
// 适用对单个变量或内存区域的操作
//宗旨:特种成员函数是指那些C++会自行生成的成员函数
//C++98:默认构造函数,析构函数,复制构造函数,复制赋值运算符,public访问层级且是 inline
//C++11: 新增两位成员,移动构造函数和移动赋值运算符
//仅当一个类没有声明任何构造函数时,才会生成默认构造函数,只要指定了一个要求传参的构造函数,就会阻止编译器生成默认构造函数
//https://www.cnblogs.com/yuanwebpage/p/13365038.html
//理解普通函数具体操作的事情:
int Fun(int data)
{
return data;
}
/**
传入形参后,实际执行的是一个赋值操作,临时变量 data_temp = 实参 data。
返回返回时,执行的也是一个赋值操作,data_return = 实参 data,然后返回 data_return,销毁 data
整个函数执行了两次拷贝,在函数完成时候会销毁两个临时变量,一个 data,一个返回赋值的返回参数
*/
//1, 拷贝构造函数
class XML{
public:
//默认构造函数: 不带任何参数,在没有定义其他构造函数的情况下,编译器会自动生成默认构造函数
XML(){
cout<<"default XML"<<endl;
}
//普通构造函数
XML(int data):m_data(data){
cout<<"putong XML"<<endl;
}
//拷贝构造函数:在没有定义拷贝构造函数时,编译器会自动生成拷贝构造函数
/**
注意两点:
1, 类中存在指针,会出现深拷贝和浅拷贝的问题,此时必须自定义拷贝构造函数实现深拷贝
2, 拷贝构造函数第一个参数必须是该类的一个引用,不能是普通参数
*/
XML(XML& xml){
m_data = xml.m_data;
cout<<"copy XML"<<endl;
}
//拷贝赋值运算符
/**
注意:它不会实例化一个对象,因此不对应一个析构函数
XML xx = ll ;//依然调用拷贝构造函数
*/
XML& operator=(const XML& xml){
this->m_data = xml.m_data;
cout<<"= XML"<<endl;
return *this;
}
//移动构造函数
/**
传进来的一定是右值引用 这样保证 指针不会再被引用
*/
XML(XML&& xml):m_ptr(xml.m_ptr){
xml.m_ptr = nullptr;
cout<<"move XML"<<endl;
}
//移动赋值运算符
/**
声明一个移动构造函数会阻止编译器去生成移动赋值运算符,反义一样
*/
XML& operator=(XML&& xml){
if(this != &xml)
{
this->m_data = m_data;
xml.m_ptr = nullptr;
}
cout<<"move= XML"<<endl;
return *this;
}
//声明移动操作又会废除复制操作,所以如果还要可复制性, 加一轮 = default
//成员函数模板在任何情况下都不会抑制特种成员函数的声明
template<typename T>
XML(const T& xml);
template<typename T>
XML& operator=(const T& xml);
//析构函数
~XML(){
cout<<"destructor"<<endl;
}
private:
int m_data = 0;
int* m_ptr;
};
//定义一个函数
XML FunXML(XML xml)
{
XML xxml = xml;
return xxml;
}
//定义一个函数
XML FunXML2(XML xml)
{
XML xxml_;//执行默认构造
xxml_ = xml;//执行等号赋值运算符
return xxml_;//调用一次拷贝构造
}
int main()
{
//测试1
XML xml;//默认构造
XML xxml(3);//普通构造
//FunXML(xxml);//函数调用,会调用两次拷贝构造: 函数内赋值一次 和返回 一次
FunXML2(xxml);//函数调用,会调用两次拷贝构造: 函数内赋值一次 和返回 一次
//移动构造
XML xxxml(move(xxml));//move函数保证传进去的是右值,移动构造
}