c++
一、static
- 全局区(静态存储区)
- static属于类名,多个对象共享
-
默认是extern类型,其他文件也可访问,static修饰后,由external转化为internal,只能文件内部访问
-
局部static变量,作用域不变,函数执行完后,生命周期结束,但不释放内存,下一次调用该函数时值不变
- 类中static方法只能调用static成员,想要调用非static成员,与要通过对象调用static方法,正常通过
类名::方法名
调用static方法
二、类型转换
1、static_cast
static_cast<目标类型>(标识符)
所谓的静态,即在编译期内即可决定其类型的转换,用的也是最多的一种。
类似C风格的强制转换,进行无条件转换,静态类型转换:
- 基类和子类之间的转换:其中子类指针转换为父类指针是安全的,但父类指针转换为子类指针是不安全的(基类和子类之间的动态类型转换建议用dynamic_cast)。
- 基本数据类型转换,enum,struct,int,char,float等。static_cast不能进行无关类型(如非基类和子类)指针之间的转换。
- 把任何类型的表达式转换成void类型。
- static_cast不能去掉类型的const、volatile属性(用const_cast)。
2、const_cast
const_cast<目标类型>(标识符):目标类型只能是指针或者引用
去掉类型的const或volatile属性
目标类型只能是引用或指针
3、dynamic_cast
dynamic_cast<目标类型>(标识符)
用于多态中父子类之间的多态转换
有条件转换,动态类型转换,运行时检查类型安全(转换失败返回NULL):
- 安全的基类和子类之间的转换。
- 必须有虚函数。
- 相同基类不同子类之间的交叉转换,但结果返回NULL。
转换情况
- 至于“先上转型”(即派生类指针或引用类型转换为其基类类型),本身就是安全的,尽管可以使用dynamic_cast进行转换,但这是没必要的, 普通的转换已经可以达到目的,毕竟使用dynamic_cast是需要开销的。
-
dynamic_cast主要用于“安全地向下转型”
- 一种是基类指针所指对象是派生类类型的,这种转换是安全的;
- 另一种是基类指针所指对象为基类类型,在这种情况下dynamic_cast在运行时做检查,转换失败,返回结果为NULL;
工作原理
需要虚函数表中的地址。所以父类必须是多态的。
如何判断当前的父类指针指向哪个子类对象?
可以用dynamic_casta;就是根据虚函数表地址判断是不是B的
在需要继承时会把父类的析构函数定义成虚函数:
- 为了能够析构子类的空间
- 方便dynamic_cast工作
class A {
public:
virtual ~A(){}
};
class B : public A{
};
A *a = new B();
if (dynamic_cast<B *>a) {
}
4、reinterpret_cast 重新解释类型转换
reinterpret_cast<目标类型>(标识符)
数据的二进制重新解释,但是不改变其值。
仅重新解释类型,但没有进行二进制的转换:
- 转换的类型必须是一个指针,应用、算术类型、函数指针或者成员指针。
- 在比特级别上进行转换,可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。但不能将非32bit的实例转成指针。
- 最普通的用途就是在函数指针类型之间进行转换。
- 很难保证移植性。
总结
- 去const属性用const_cast
- 基本类型转换用static_cast
- 多态类之间的类型转换用dynamic_cast
- 不同类型的指针类型转换用reinterpret_cast
三、volatile
参考博客:
https://baijiahao.baidu.com/s?id=1663045221235771554&wfr=spider&for=pc
四、虚函数和非虚函数
动态绑定和静态绑定
参考博客:
相关概念
对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
绑定方式
虚函数是动态绑定的,非虚函数是静态绑定的
只有虚函数才使用的是动态绑定,其他的全部是静态绑定。目前我还没有发现不适用这句话的,如果有错误,希望你可以指出来。
class B {
void DoSomething();
virtual void vfun();
};
class C : public B {
void DoSomething();//首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。
virtual void vfun();
};
class D : public B {
void DoSomething();
virtual void vfun();
};
D* pD = new D();//pD的静态类型是它声明的类型D*,动态类型也是D*
B* pB = pD;//pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
C* pC = new C();
pB = pC;//pB的动态类型是可以更改的,现在它的动态类型是C*
1、pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?
不是的,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对 象的静态类型来选择函数。pD的静态类型是
D*
,那么编译器在处理pD->DoSomething()的时候会将它指向 D::DoSomething()。同理,pB的静态类型是B*
,那pB->DoSomething()调用的就是 B::DoSomething()。
2、让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。
上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。指针和引用的动态类型和静态类型可能会不一致,但是对象的动态类型和静态类型是一致的。
特别需要注意的地方
当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。
class B {
virtual void vfun(int i = 10);
};
class D : public B {
virtual void vfun(int i = 20);
};
D* pD = new D();
B* pB = pD;
pD->vfun();
pB->vfun();
1、有上面的分析可知pD->vfun()和pB->vfun()调用都是函数D::vfun(),但是他们的缺省参数是多少?
分析一下,缺省参数是静态绑定的,pD->vfun()时,pD的静态类型是D*,所以它的缺省参数应该是20;
同理,pB->vfun()的缺省参数应该是10。
对于这个特性,估计没有人会喜欢。所以,永远记住:
“绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters value.)”
五、智能指针
参考博客:
https://blog.csdn.net/weixin_39731083/article/details/81534333
https://blog.csdn.net/weixin_43705457/article/details/97617676
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
不能混合使用普通指针和智能指针,因为智能指针不是单纯的赤裸裸的指针
为什么要使用智能指针:
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
1、auto_ptr(c++98的方案,cpp11已经抛弃)
采用所有权模式。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
2、unique_ptr(替换auto_ptr)
简单解释
- 与shared_ptr不同,某一时刻,只能有一个unique_ptr指向一个给定的对象。因此,当unique_ptr被销毁,它所指的对象也会被销毁。
- unique_ptr的初始化必须采用直接初始化
使用方法
unique_ptr<string> p(new string("China"));
//没问题
unique_ptr<string> p (q);
//错误,不支持拷贝
unique_ptr<string> q;
q = p;
//错误,不支持赋值
unique_ptr<T> p;
//空智能指针,可指向类型是T的对象
if(p)
//如果p指向一个对象,则是true
(*p)
//解引用获取指针所指向的对象
p -> number == (*p).number;
p.get();
//返回p中保存的指针
swap(p,q);
//交换p q指针
p.swap(q);
//交换p,q指针
unique_ptr<T,D>p;
//p使用D类型的可调用对象释放它的指针
p = nullptr;
//释放对象,将p置空
p.release();
//p放弃对指针的控制,返回指针,p置数空
p.reset();
//释放p指向的对象
p.reset(q);
//让u指向内置指针q
举例
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
采用所有权模式,还是上面那个例子
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3;//此时会报错!!
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
3、shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count | 返回引用计数的个数 |
---|---|
unique | 返回是否是独占所有权( use_count 为 1) |
swap | 交换两个 shared_ptr 对象(即交换所拥有的对象) |
reset | 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 |
get | 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的 |
使用方法
shared_ptr<int> p1;
//被初始化成为一个空指针
shared_ptr<int> p2 (new int(4));
//指向一个值是4的int类型数据
shared_ptr<int> p3 = new int(4);
//错误,必须直接初始化
shared_ptr<T> p;
//空智能指针,可指向类型是T的对象
if(p)
//如果p指向一个对象,则是true
(*p)
//解引用获取指针所指向的对象
p -> number == (*p).number;
p.get();
//返回p中保存的指针
swap(p,q);
//交换p q指针
p.swap(q);
//交换p,q指针
make_shared<T>(args)
//返回一个shared_ptr的对象,指向一个动态类型分配为T的对象,用args初始化这个T对象
shared_ptr<T> p(q)
//p 是q的拷贝,q的计数器++,这个的使用前提是q的类型能够转化成是T*
shared_pts<T> p(q,d)
//p是q的拷贝,p将用可调用对象d代替delete
//上面这个我其实没懂,也没有查出来这个的意思
p =q;
//p的引用计数-1,q的+1,p为零释放所管理的内存
p.unique();
//判断引用计数是否是1,是,返回true
p.use_count();
//返回和p共享对象的智能指针数量
p.reset();
p.reset(q);
p.reset(q,d);
//reset()没懂,这个以后再来补充吧
4、weak_ptr
简单解释
weak_ptr是一种不控制所指向对象生存期的智能指针,指向shared_ptr管理的对象,但是不影响shared_ptr的引用计数。它像shared_ptr的助手,一旦最后一个shared_ptr被销毁,对象就被释放,weak_ptr不影响这个过程。
weak_ptr不可以从unique_ptr构造出来
weak_ptr获得指向的对象的方法是使用lock函数,这个函数返回的是shared_ptr。首先我们就没有办法把unique_ptr和shared_ptr混在一起用,因为独占和分享是互斥的,同时只能选择一个。其次,如果我们真的有一个unique_ptr版本的,那么lock函数到底要返回什么呢?如果返回的是一个unique_ptr的话,那么所有权已经被拿过来了,我们就等于破坏了原本用来构造weak_ptr的unique_ptr的所有权。如果大家想要完成的是这种功能的话,为什么不直接使用unique_ptr
使用方法
weak_ptr<T> w(sp);
//定义一个和shared_ptr sp指向相同对象的weak_ptr w,T必须能转化成sp指向的类型
w = p;
//p是shared_ptr或者weak_ptr,w和p共享对象
w.reset();
//w置为空
w.use_count();
//计算与w共享对象的shared_ptr个数
w.expired();
//w.use_count()为0,返回true
w.lock();
//w.expired()为true,返回空shared_ptr,否则返回w指向对象的shared_ptr
举例
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print()
; 英文pb_
是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();
5、注意
智能指针没有重载 [] 运算符
shared_ptr 在默认情况下是不能指向数组的。
原因:因为我们的 shared_ptr 默认的删除器是使用 Delete 对智能指针中的对象进行删除,而 delete 要求 new 时是单一指针 Delete时也应该是指针 new时是数组 delete 也应该用数组类型去delete
1、shared_ptr
所以我们如果想让我们的 share_ptr 去指向指针 我们只需要去使用一个可调用对象即可 在这种情况下比较常用的函数或者lambda表达式均可
bool del(int *p){
delete [] p;
}
shared_ptr<int> shared(new int[100],del);//使用函数
shared_ptr<int> ptr(new int[100], [](int *p){delete [] p;});//使用lambda表达式
因为智能指针没有重载下标运算符 意味着我们不能想数组那样去使用这个指针 那怎么样才可以使用呢
shared_ptr 有一个函数可以返回当前智能指针的内置指针的函数 就是成员函数get() 但是我们要注意当智能指针指针已经释放内存以后,get得到的指针就成了空悬指针
有一点需要注意 在我们得到get返回的指针以后,智能指针对象的引用计数其实并没有增加
get() const noexcept { return _M_ptr; }
但是如果我们delete从get得到的指针 并不会出现多次delete的错误
auto x = ptr.get();
cout << *(x+i) << endl;
2、unique_ptr
其实只是C++11 中不支持而已 C++17中已经支持
相比与shared_ptr unique_ptr对于动态数组的管理就轻松多了 我们只需要直接使用即可
unique_ptr<int[]>unique(new int[100]);
而且unique_ptr是重载了下标运算符的,意味着我们可以方便把其当数组一样使用
3、Boost C++库
著名的Boost库其实是支持指向数组的,使用方法与unique_ptr差不多
类名为boost::shared_array
,定义在<boost/shared_ptr.hpp>
boost::shared_array<int> arr(new int[100]);
6、野指针
1. 什么是野指针?
野指针和空指针不一样,是一个指向垃圾内存的指针。
2. 为什么会产生野指针?
①指针变量没有被初始化:
任何指针变量被刚创建时不会被自动初始化为NULL指针,它的缺省值是随机的。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
②指针被free或者delete之后,没有设置为NULL,让人误以为这是一个合法指针:
free和delete只是把指针所指向的内存给释放掉,但并没有把指针本身给清理掉。这时候的指针依然指向原来的位置,只不过这个位置的内存数据已经被毁尸灭迹,此时的这个指针指向的内存就是一个垃圾内存。但是此时的指针由于并不是一个NULL指针(在没有置为NULL的前提下)。
③指针操作超越了变量的作用范围:
由于C/C++中指针有++操作,因而在执行该操作的时候,稍有不慎,就容易指针访问越界,访问了一个不该访问的内存,结果程序崩溃。
另一种情况是指针指向一个临时变量的引用,当该变量被释放时,此时的指针就变成了一个野指针。
六、C++存储区
对于局部常量,存放在栈区;对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;字面值常量,比如字符串,放在常量区。
- 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
- 堆,就是那些由malloc等分配的内存块,用free来结束自己的生命的。
- 3.自由存储区,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
- 4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
- 5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)
- 代码区:存放程序的代码。
常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
const 和一般变量的存储去没有区别只是是只读,”hello”这种字面值常量才存放在常量存储区
堆栈的区别
- 管理方式不同;
- 空间大小不同;
- 能否产生碎片不同;
- 生长方向不同;
- 分配方式不同;
- 分配效率不同;
自由存储区和堆“free store” VS “heap”
当我问你C++的内存布局时,你大概会回答:
“在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区”。
如果我接着问你自由存储区与堆有什么区别,你或许这样回答:
“malloc在堆上分配的内存块,使用free释放内存,而new所申请的内存则是在自由存储区上,使用delete来释放。”
这样听起来似乎也没错,但如果我接着问:
自由存储区与堆是两块不同的内存区域吗?它们有可能相同吗?
你可能就懵了。
事实上,我在网上看的很多博客,划分自由存储区与堆的分界线就是new/delete与malloc/free。然而,尽管C++标准没有要求,但很多编译器的new/delete都是以malloc/free为基础来实现的。那么请问:借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?
从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:
堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。
结论
- 自由存储是C++中通过new与delete动态分配和释放对象的抽象概念,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。
- new所申请的内存区域在C++中称为自由存储区。藉由堆实现的自由存储,可以说new所申请的内存区域在堆上。
- 堆与自由存储区还是有区别的,它们并非等价。
假如你来自C语言,从没接触过C++;或者说你一开始就熟悉C++的自由储存概念,而从没听说过C语言的malloc,可能你就不会陷入“自由存储区与堆好像一样,好像又不同”这样的迷惑之中。这就像Bjarne Stroustrup所说的:
usually because they come from a different language background.
七、const 和 constexpr
https://blog.csdn.net/u010843408/article/details/107288934/
c++11 constexpr:https://www.cnblogs.com/ljwgis/p/13095739.html
运行期常量和编译期常量:https://blog.csdn.net/quinta_2018_01_09/article/details/95727536
常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
const 和一般变量的存储去没有区别只是是只读,”hello”这种字面值常量才存放在常量存储区
const 是运行期常量,constexpr 是编译期常量
在 C++11 以后,建议凡是「常量」语义的场景都使用 constexpr,只对「只读」语义使用 const。
八、缺省参数
在C++中,允许实参的个数与形参的个数不同。在声明函数原型时,为一个或者多个形参指定默认值,以后调用这个函数时,若省略某一个实参,c++则自动的以默认值作为相应参数的值。
在定义的过程中,一定要把缺省参数放在最后
void func(int x, int y = 1, int z = 2) // 合法
void func(int x = 1, int y, int z = 2); // 不合法
在调用的过程中会按照顺序复制
double func(int x, int y = 1, double z = 2.9) {
return x + y + z;
}
func(1, 2, 5.7); // 8.7
func(1, 2.3); // 5.9
九、栈溢出
https://blog.csdn.net/sinat_31054897/article/details/82223889
十、friend
模板类中的友元函数
模板类中的友元函数,直接定义在模板类中会比较方便。
在外面定义时,由于类模板编译时的规则:
- 下面代码中,operator +=被定义在类模板内部。其他3个函数先被声明(需提前声明类模板,如果模板函数的参数中含有类模板),然后在类模板中被声明为友元函数, 之后被定义在类模板体之外。
- 请注意当模板函数被声明为类模板的友元时,在函数名之后必须紧跟模板实参表,用来代表该友元声明指向函数模板的实例。否则友元函数会被解释为一个非模板函数,链接时无法解析。
- 友元模板函数的模板参数类型,并不一定要求是类模板的参数类型,也可以另外声明。
#include <iostream>
#include <vector>
template <typename T>
class Number;
template <typename T>
void print(const Number<T>& n);
template <typename T>
std::ostream& operator << (std::ostream& os, const Number<T>& n);
template <typename T>
std::istream& operator>>(std::istream& is, Number<T>& n);
template <typename T, typename T2>
void printVector(const std::vector<T2>& vt, const Number<T>& n);
template <typename T>
class Number {
public:
Number(T v) : val(v) {}
~Number() {}
private:
T val;
public:
friend void print<T> (const Number<T>& n);
friend std::ostream& operator << <T>(std::ostream& os, const Number<T>& n);
friend std::istream& operator>> <T>(std::istream& is, Number<T>& n);
friend Number<T>& operator += (Number<T>& a, const Number<T>& b) {
a.val += b.val;
return a;
}
template <typename T2>
friend void printVector<T>(const std::vector<T2>& vt, const Number<T>& n);
template <typename T2>
void memFunc(const std::vector<T2>& vt, const Number<T>& n);
};
template <typename T>
std::ostream& operator <<(std::ostream& os, const Number<T>& n) {
os << n.val << std::endl;
return os;
}
template <typename T>
std::istream& operator >>(std::istream& is, Number<T>& n) {
is >> n.val;
return is;
}
template <typename T>
void print<T> (const Number<T>& n) {
std::cout << n;
}
template <typename T, typename T2>
void printVector(const std::vector<T2>& vt, const Number<T>& n) {
for (unsigned int i = 0; i < vt.size(); i++)
std::cout << vt.at(i) << " ";
std::cout << "=> " << n;
}
template <typename T>
template <typename T2>
void Number<T>::memFunc(const std::vector<T2>& vt, const Number<T>& n) {
for (unsigned int i = 0; i < vt.size(); i++)
std::cout << vt.at(i) << " ";
std::cout << "=> " << n;
}
友元类
class CatFactory; // 一定要声明,否则 Cat 会认为在给 Cat::CatFactory 开后门
class Cat {
friend class CatFactory;
private:
Cat() = default;
~Cat() = default;
};
class CatFactory {
public:
};
十一、访问权限
private,public,protected的访问范围:
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数
第二:类的继承后方法属性变化:
使用private继承,父类的所有方法在子类中变为private;
使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
使用public继承,父类中的方法属性不发生改变;
三种访问权限
public:可以被任意实体访问
protected:只允许子类及本类的成员函数访问
private:只允许本类的成员函数访问
三种继承方式
public 继承
protect 继承
private 继承
组合结果
基类中 继承方式 子类中
public & public继承 => public
public & protected继承 => protected
public & private继承 = > private
protected & public继承 => protected
protected & protected继承 => protected
protected & private继承 = > private
private & public继承 => 子类无权访问
private & protected继承 => 子类无权访问
private & private继承 = > 子类无权访问
由以上组合结果可以看出
1、public继承不改变基类成员的访问权限
2、private继承使得基类所有成员在子类中的访问权限变为private
3、protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
4、基类中的private成员不受继承方式的影响,子类永远无权访问。
此外,在使用private继承时,还存在另外一种机制:准许访问 。
我们已经知道,在基类以private方式被继承时,其public和protected成员在子类中变为private成员。然而某些情况下,需要在子类中将一个或多个继承的成员恢复其在基类中的访问权限。
C++支持以两种方式实现该目的
方法一,使用using 语句,这是C++标准建议使用的方式
方法二,使用访问声明,形式为 base-class::member;, 位置在子类中适当的访问声明处。(注,只能恢复原有访问权限,而不能提高或降低访问权限)
十二、内部类和外部类
内部类的概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
即说:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
总结一下:其实内部类和友元类很像很像。只是内部类比友元类多了一点权限:可以不加类名的访问外部类中的static、枚举成员。其他的都和友元类一样。
在一个类中定义的类被称为嵌套类,定义嵌套类的类被称为外部类。对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。
作用域
class queue
{
public:
struct Coach{...};
private:
struct Node{ Item item;struct Node *next };
...
};
嵌套类的声明位置决定了嵌套类的作用域,即它决定了程序的那部分可以创建嵌套类的对象。
- 如果嵌套类声明在一个类的私有部分,则只有嵌套类的外部类可以知道它。上面的类就是这种情况。在queue这个类中,queue成员可以使用Node对象或Node对象的指针,但是程序的其他部分甚至不知道存在Node类。对于queue派生下来的类,也不知道Node的存在。
-
如果嵌套类声明在一个类的保护部分,对于后者是可见的,对于外界是不可见的。派生类知道该嵌套类,并且可以直接创建这种类型的对象。
-
如果嵌套类声明在一个类的公有部分,则允许后者,后者的派生类以及外部世界使用。然后在外部使用时,必须加上外部类的外部类作用域限制符。
如果使用 Coach,定义方式:queue:Coach coa
;
嵌套结构和枚举的作用域
嵌套结构和枚举的作用域于此相同。许多程序员使用公有的枚举提供客户使用的类常量。
下面表格总结了,嵌套类、结构、枚举的作用域特征。
声明位置 | 包含它的类是否可以使用它 | 从包含它的类派生下来的类是否可以使用它 | 外部是否可以使用它 |
---|---|---|---|
公有 | 是 | 是 | 是 |
私有 | 是 | 否 | 否 |
保护 | 是 | 是 | 否 |
访问权限
在外部类中声明嵌套类并没有赋予外部类任何对嵌套类的访问权限,也没有赋予任何嵌套类对于外部类的访问权限。与一般类的访问控制相同(私有,公有,保护)。
内部类可以定义在外部类的public、protected、private都是可以的。
如果内部类定义在public,则可通过 外部类名::内部类名
来定义内部类的对象。
如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。
代码测试
1、在外部类访问嵌套类
class test
{
public:
test()
{
i = 10; //不能访问
mytest::i = 10;//不能访问
}
private:
class mytest
{
int i;
int j;
};
};
2、不能直接访问mytest的成员,试着通过对象访问。
class test
{
public:
test()
{
cc.i = 10; //通过对象可以访问,如果i为私有则不可访问
}
private:
class mytest
{
public:
int i;
int j;
};
mytest cc;
};
通过测试,在外部类中声明一个嵌套类的对象。然后再外部类中利用该对象访问嵌套类,访问的规则与普通类相同。
3、注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
class A
{
private: static int k;
int h;
public: class B{
void foo(){
cout<<k<<endl;//OK
//cout<<h<<endl;//ERROR
}
};
};
int A::k=3;
这里cout<<h<<endl;是一个非常常见的错误。因为内部类是一个独立的类,不属于外部类,所以此时还没有外部类的对象,显然也不存在h。而k就不同了,不需要外部类的对象就已存在,所以这里k是OK的。
这和友元类的使用也是同样的道理。“想要使用另一个类的成员,必须要存在这个类的对象”。
class A
{
private: static int k;
int h;
public: class B{
void foo(A a){
cout<<k<<endl;//OK
cout<<a.h<<endl;//OK
}
};
};
int A::k=3;
这样就没问题了。
4、在堆中创建内部类对象:
class A
{
public: class B{};
};
int _tmain(int argc, _TCHAR* argv[])
{
A::B*b=new A::B();
return 0;
}
5、内部类可以现在外部类中声明,然后在外部类外定义:
class A
{
private: static int i;
public: class B;
};
class A::B{
public:void foo(){cout<<i<<endl;}//!!!这里也不需要加A::i.
};
int A::i=3;
这形式上就更像友元类了。注意这里和友元类,不要混淆了。
6、sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
public:
class B{int o;};
};
int _tmain(int argc, _TCHAR* argv[])
{
cout<<sizeof(A)<<endl;//1
return 0;
}
在嵌套类中访问外部类
因为嵌套类中没有任何对外部类的访问权限。因此只有在嵌套类中定义了该对象,才能够访问其非静态成员,但此时外部类是一个不完整的类型(类没有定义完成)。因此想在嵌套类内部访问外部类是无法做到的。
总结
- 其实内部类和友元类很像很像。只是内部类比友元类多了一点权限:可以不加类名的访问外部类中的static、枚举成员。其他的都和友元类一样。
- 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 如果内部类定义在public,则可通过
外部类名::内部类名
来定义内部类的对象。 - 如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。
- 如果内部类定义在public,则可通过
- sizeof(外部类)=外部类,和内部类没有任何关系。
十三、左值、右值
左值引用与右值引用
基本概念
我们平时经常使用的引用就是左值引用,通过 & 获取左值引用。为了支持移动操作,新标准引入 右值引用 (rvalue reference)。顾名思义,右值引用就是将引用绑定到右值。可以通过 && 来获取右值引用。比如:
int i = 42;
int &&r = i * 2; // 将 i * 2 的结果绑定到 r 上Copy
要注意只能将右值绑定到右值引用上,并且左值引用不能绑定右值,以下几种用法都是错误的:
int i = 42;
int &&rr = i; // 错误,i 是左值
int &r = i * 2; // 错误,i * 2 是右值Copy
但是右值可以绑定到 const 的左值引用上:
int i = 42;
const int &r = i * 42; // 正确,可以将 const 引用绑定到右值上Copy
可能一开始会觉得 const 引用可以绑定右值很奇怪,引用并不是对象,他只是为已经存在的对象另取一个名字而已,所以像 int &r = 42
这样的语法是不允许的,因为 42 不是一个对象,只是一个普通字面数值而已,而 const 引用却是一个例外,如果我们将这个过程拆开来看,就可以知道为什么有这个例外了:
// 我们可以先看看常量引用被绑定到另外一种类型上时到底发生了什么
double dval = 3.14;
const int &ri = dval;
// 以上将一个双精度浮点数绑定到整型引用上,所以编译器会对其进行类型转换:
const int tmp = dval; // 先将双精度浮点数变成一个整型常量
const int &ri = tmp; // 让常量引用 ri 绑定到这个临时变量上Copy
这种情况下,ri 会被绑定到一个 临时量 (temporary)对象上,所谓临时量就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。 创建临时量是需要消耗资源的,这也是本文的主题之一:右值引用的由来。
我们再来看看如果 ri 不是常量引用会发生什么:
double dval = 3.14;
int &ri = dval;
// 以上将一个双精度浮点数绑定到整型引用上,所以编译器会对其进行类型转换:
const int tmp = dval; // 还是先将双精度浮点数变成一个整型常量
int &ri = tmp; // 再让这个普通引用和 tmp 绑定Copy
可以看到,这样的情况下,我们可以对 ri 进行赋值,因为 ri 并不是常量引用。但有两个问题:
- tmp 是常量,而我们却用一个不是常量的引用与其绑定,这是不被允许的。
- 就算我们可以对其进行绑定,而我们绑定的是 tmp,而不是 dval,所以我们对 ri 赋值并不会改变 dval 的值。
于是乎,编译器直接禁止了这种行为。而常引用就不一样了,反正我们也不会改变其值,只是需要知道它的值而已,并且也不违反语法规则,所以将常引用与这种临时量绑定的行为是被允许的。
通过上例我们又可以推出一个规则:引用类型必须与其所引用的对象类型保持一致,不然就会引发临时量的产生,从而引发以上两个问题。同样,常引用除外。
同理我们推出原问题的解:一个字面值如 42,也会产生一个临时量,若不是常引用,同样会引发上文提到的两个问题,所以,我们只可以用常引用绑定右值,而普通引用是不可以的。
什么情况下使用右值引用
要了解什么情况下使用右值引用,我们就要先了充分了解右值的特性。首先与左值不同,右值是非常短暂的,它要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于这样的特性我们可以总结:
- 所引用的对象将要被销毁
- 该对象没有其他用户
这就意味着,使用右值引用的代码可以自由的接管被引用对象的资源。
需要注意的是,所有的变量都是左值,我们不能将一个右值引用绑定到一个右值引用类型的变量上。比如:
int &&rr1 = 42; // 正确:字面量是右值
int &&rr2 = rr1; // 错误:rr1 是变量,虽然他是右值引用,但任然是左值
标准库 move 函数
上次说到以下两种情况下,可以使用右值引用:
- 所引用的对象将要被销毁时
- 该对象没有其他使用者
并且所有的变量都是左值(包括右值引用类型的变量),因此不能将右值引用直接绑定到一个变量上。
虽然不能将一个右值引用绑定到左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。这就是 move 的作用,move 定义在头文件 utility 中。可以用如下方式使用 move(注意,为了避免潜在的名字冲突,使用 move 时不应使用 using 声明):
int&& rr1 = 42; // 虽然 rr1 是右值引用类型,但其任然是变量,所以还是左值
int&& rr2 = std::move(rr1); // 使用 std::move() 而不是 move()Copy
调用 move 就相当于告诉编译器:我们有一个左值,但现在要像右值一样处理这个左值,并且调用 move 就意味着:除了对 rr1 赋值或销毁意外,我们将不再使用它。再调用 move 后,我们不能再对 rr1 的值做任何假设。也就是说,对于 rr1,我们可以赋予它新值,也可以销毁它,但是不能再使用其值了。
移动构造函数和移动赋值运算符
移动构造
有了 move 函数,我们就可以引出移动构造函数和移动赋值运算符了,之前只是提到什么情况下可以使用右值引用,而这里就介绍右值引用该如何应用到实际程序当中了。
和拷贝构造函数不同的是,移动构造函数接受的是右值引用而非左值引用,并且经过移动构造函数,被移动的对象的资源将被”窃取“掉。在完成资源的移动之后,源对象将不在拥有任何资源,其资源所有权已经转交给新创建的对象了。
可以用一个程序直观的感受一下移动构造:
#include <cstring>
#include <iostream>
class MyString {
private:
char* string;
public:
MyString() : string(nullptr) {}
MyString(const char* str) {
// 这里采用深拷贝
string = (char*)malloc(strlen(str) + 1);
strcpy(string, str);
std::cout << "I'm constructor of class MyString" << std::endl;
}
MyString(const MyString& mystr) {
// 这里同样是深拷贝,mystr 任然持有它自己的资源
string = (char*)malloc(strlen(mystr.string) + 1); // 为 string 分配新的资源
strcpy(string, mystr.string);
std::cout << "I'm copy constructor of class MyString" << std::endl;
}
MyString(MyString&& mystr) noexcept : string(mystr.string) {
// 注意!移动构造函数,这里 mystr 已经不再持有任何资源
// mystr.string 所指向的资源已经被当前对象窃取
// 这里切记要将被移动的资源的指针置为空,为了防止析构函数析构其已经被转移的资源
mystr.string = nullptr;
std::cout << "I'm move constructor of class MyString" << std::endl;
}
~MyString() {
std::cout << "I'm destructor of class MyString" << std::endl;
if (string) {
// 如果 string 指针还持有资源的话,就将其释放
free(string);
std::cout << "free string!" << std::endl;
}
}
};
int main(int argc, char* argv[]) {
MyString s1("hello world");
MyString s2(s1);
std::cout << std::endl;
return 0;
}Copy
以下为程序运行结果:
可以看到第 44 行调用了普通的构造函数,45 行调用了拷贝构造函数,这里的拷贝构造函数是深拷贝,也就是说,s1 和 s2 各自持有自己的资源,所以在程序结束调用析构函数时,有两次 free 操作被调用,分别去将 s1 和 s2 所持有的资源 free 掉。
然而有些时候,我们不需要原来的对象了,比如我们用 s1 初始化 s2 之后,s1 将不再被需要了,这时候我们进行深拷贝就非常的浪费时间浪费内存了,我们可以直接将 s1 的资源交给 s2 就行了,这时,移动构造就派上用场了。在 main 函数中这样使用移动构造函数:
int main(int argc, char* argv[]) {
MyString s1("hello world");
MyString s2(std::move(s1));
std::cout << std::endl;
return 0;
}Copy
我们使用 std::move() 将 s1 由左值转变为右值,这样,程序将调用移动构造函数,程序运行结果如下:
可以看到,程序结束调用析构函数时,虽然调用了两次析构函数,但只进行了一次 free 操作,因为我们没有分配多余的内存,而是直接将 s1 的资源转交给 s2 了。
移动构造在 STL 当中非常的常用。使用移动构造可以节省大量的时间和空间开销,可以做一个小实验,还是用我们的 MyString 类,我们用普通的拷贝构造函数和移动构造函数分别进行 10000000 次的初始化操作,看看时间上的差距,代码如下:
int main(int argc, char* argv[]) {
clock_t start, end;
start = clock();
for (int i = 0; i < 10000000; ++i) {
MyString s1("hello world");
MyString s2(s1);
}
end = clock();
std::cout << "- > Time cost without move: "
<< static_cast<double>(end - start) / CLOCKS_PER_SEC << std::endl;
start = clock();
for (int i = 0; i < 10000000; ++i) {
MyString s1("hello world");
MyString s2(std::move(s1));
}
end = clock();
std::cout << "- > Time cost with move: "
<< static_cast<double>(end - start) / CLOCKS_PER_SEC << std::endl;
return 0;
}Copy
运行结果如下:
时间的单位是秒,可以看到,使用移动构造进行初始化,效率提升了一倍!
移动赋值
移动赋值的原理和移动构造是一样的,下面直接给出移动赋值的代码:
MyString& Mystring::operator=(MyString&& rhs) noexcept {
if (this != &rhs) {
if (string) free(string); // 先将原有资源释放
string = rhs.string;
rhs.string = nullptr;
}
return *this;
}Copy
只需要注意自我赋值的情况即可,并且如果自己本身就持有着资源,记得一定要先释放掉。
注意事项
异常保障
由于移动操作只是”窃取”资源,不分配任何资源,所以通常不会抛出异常,当编写一个不抛出异常的移动操作时,我们应该通知使用者,否则为了处理异常可能需要做一些额外的工作(比如标准库中的 vector,在发生异常后,需要将已经改变的部分复原,如:新分配的内存需要还回去,新插入的值需要删掉)。可以用 noexcept 来指明函数不会抛出异常。
源对象必须可析构
从一个对象移动数据并不会销毁此对象,但有时候在移动操作完成后,源对象会被销毁。所以在编写移动操作时,必须确保被移动的后的对象是可以安全析构的。如之前代码所示,在移动后,我们将被移动的对象的 string 指针置为了空,这时候就可以安全析构了,因为在析构函数中我们已经做了判断,如果 string 指针为空,我们就不进行 free 操作。
合成的移动操作
如果不声明自己的拷贝构造或拷贝赋值运算符,那么编译器会自动为我们合成。其要么被定义为逐个成员拷贝,要么被定义为对象赋值,或者直接定义为删除(也就是 MyString(const MyString&) = delete;)。
但与拷贝操作不同,除非你没有定义自己的拷贝构造、拷贝赋值或者析构函数,不然的话,编译器是不会去合成移动操作的。也就是说,只有在你自己没有定义任何拷贝控制成员的情况下,编译器才可能会去合成移动操作。如果一个类没有移动操作,通过正常函数匹配,则会去调用对应的拷贝操作来代替移动操作。
如果我们用 =default 来要求编译器生成移动操作,那么在编译器不能移动所有成员的情况下,还是会将移动操作定义为删除的函数(=delete)。
还有一点值得注意:如果一个类定义了自己的移动操作而不定义拷贝操作,那么拷贝构造和拷贝赋值都将被定义为删除的函数(=delete)。
移动右值,拷贝左值
如果一个类既有移动构造,也有拷贝构造,编译器就使用普通的函数匹配规则来确定使用哪个构造函数。拿 MyString 举例,拷贝构造可以接受任何可以转换为 MyString 类型的数据。而移动构造只能接受 MyString&&,也就是只能接受右值。如果一个类没有移动构造,那么如上节所述,会调用拷贝构造,即使使用 std::move() 传一个右值也是一样。我们将之前 MyString 类的移动构造注释掉,再执行下列程序,看看会发生什么:
int main(int argc, char* argv[]) {
MyString s1("Hello World");
MyString s2(std::move(s1));
std::cout << std::endl;
return 0;
}Copy
运行结果:
可以看到,我们再第 3 行虽然使用了 std::move() 获取一个右值,但还是调用了拷贝构造函数。
三/五法则
所谓三/五法则,就是指将五个拷贝控制成员(三个基本操作:拷贝构造、拷贝赋值、析构,两个移动操作:移动构造、移动赋值)看作一个整体:一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。比如我们的 MyString 类,必须定义拷贝构造函数、拷贝赋值运算符以及析构函数才能正确工作(因为我们需要分配内存和释放内存,这些活编译器可不会为我们做)。拷贝一个资源会导致额外开销,在这种拷贝非必要的情况下,移动操作就可以避免这种开销。
转发
最后一个主题:转发。什么是转发?在我们调用函数的时候,会把实参传递给函数,有时候我们传给函数的是左值,有时候给的是右值,有时候还可能给的是 const 类型。在这些情况下,我们要求函数接收参数后,依然能保持这些类型,这时候就需要用转发了。看下面这个例子:
template <typename F, typename T1, typename T2>
void middle1(F f, T1 t1, T2 t2) {
f(t1, t2);
}Copy
上面这个模板函数有三个参数,第一个 f 是一个函数,t1 和 t2 是传给 f 的参数。这个模板函数当然没啥作用,只是为了演示而已。我们可以直接调用 f,也可以通过模板函数 middle1 来调用 f,下面这段代码演示了两者的区别。
void f(int v1, int& v2) { // v2 是一个引用
++v1;
++v2;
}
int main(int argc, char* argv[]) {
int i = 0;
f(42, i);
cout << "After call the f directly: " << i << endl;
middle1(f, 42, i);
cout << "Call f through middle1: " << i << endl;
}Copy
运行结果如下:
注意函数 f 的参数 v2 是一个引用,在函数内部我们将 v1 和 v2 自增 1,我们预期应该是每次调用 f ,被 v2 引用的变量都可以加一。可以看到,我们在主函数中定义了一个变量 i,其初值为 0,我们直接调用 f 后,i 顺利的被加一,可是我们再次通过 middle1 调用 f 时,i 并没有加一。问题就出在 middle 上,虽然说 f 接收的参数是一个引用,但 middle 的参数却不是。当我们将 i 绑定到 middle 的参数 t2 上后,传给 f 的是 t2,而 t2 只是一个普通的、非引用的类型 int,而不是对 i 的引用。所以 i 的值并没有改变。
保留类型信息(引用折叠)
再来看第二个版本的 middle,将 t1 和 t2 都定义为右值引用:
template <typename F, typename T1, typename T2>
void middle2(F f, T1&& t1, T2&& t2) {
f(t1, t2);
}Copy
我们在刚刚的主函数中调用 middle2 而非 middle1,运行结果如下:
可以看到,i 被顺利的自增了两次,也就是说,在 middle2 中,实参的“左值性“得到了保留。这就要归功于 引用折叠 了,简单说一下引用折叠:
X& &
、X& &&
和X&& &
都会折叠成类型X&
- 类型
X&& &&
折叠成X&&
注意:引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。就是说你不能这样直接用:
int& i = 1;
int&& j = i; // 很显然错了Copy
解释一下,可以看到上面两点中,& 符号之间,有的有空格,有的没有,空格后面的部分可以认为是函数参数(形参)一开始的类型,而空格前面的是传递给函数的参数(实参)的类型。在我们的例子当中,middle2 的两个参数都是 &&(右值引用)类型的,而我们在 main 中调用 middle2 时,传给 t1 的是一个字面值 42,也就是右值,此时 t1 的类型为 int&&
(其中 int
来自 T1),完全没问题,T1 被推导为 int
。而传给 t2 的是一个引用类型 int&
,这时候,t2 的类型就变成了 int& &&
,按照引用折叠的规则,t2 就变成了 int&
类型,而此时的模板参数 T2 将会被推导为 int&
类型,这样,模板函数参数(形参)的完整类型就变成了 int& &&
(其中,int&
来自 T2,&&
本来就存在),进而折叠为 int&
(与实参类型相匹配了)。
所以,通过将函数参数定义为一个指向模板类型参数的右值引用,就可以保持其对应实参的所有类型信息。而使用引用的情况下(无论左值还是右值),const 的属性都是可以保留的,因为 const 是类型的一部分,比如传给 middle2 的参数 t1 的是 const int,那么 T1 本身就会变成 const int,后面的 && 才是附加的部分。
然而现实是残酷的,这个版本的 middle 只解决了一部分问题,考虑如下函数:
void g(int &&v1, int& v2) {
++v1;
++v2;
}Copy
其中,i 是右值引用,我们在 main 函数中通过 middle2 调用 g:
int main(int argc, char* argv[]) {
int i = 0;
middle2(g, 42, i);
cout << "Call g through middle2: " << i << endl;
}Copy
编译器报如下错误:
无法将一个右值引用绑定到左值上。为什么会出现这个错误?我们在将 42 绑定给 t1 后,又将 t1 传递给了 g,本来我们应该期望没问题的,因为 g 的参数 v1 是右值引用,将 42 给右值引用完全没问题,但问题就出在,虽然 42 是右值,但 t1 却不是,上一篇博客说过了,变量都是左值,即使是右值引用,但也是变量,所以还是左值,而左值是无法与右值引用绑定的!
std::forward 登场
那到底该怎么办呢,我们可以使用 forward 这个新标准库来传递 middle2 的参数,它能够保持原始实参的类型,forward 和 move 一起,都定义在 utility 头文件中。move 可以直接调用:std::move(i)
,但 foward 要明确给出模板参数:std::foward<int>(i)
才能使用。forward 会模板参数类型的右值引用,也就是:int&&
。注意,如果我们这样使用:std::forward<int&>(i)
返回的就是 int& &&
,进而折叠为 int&
。
一般来说,模板函数中的参数类型为右值引用的时候,就需要搭配 forward 来使用,通过 forward 和引用折叠,就可以完美的保留参数类型了:
template <typename F, typename T1, typename T2>
void middle3(F f, T1&& t1, T2&& t2) {
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
void g(int&& i, int& j) {
++i;
++j;
}
int main(int argc, char* argv[]) {
int i = 0;
middle3(g, 42, i);
cout << "Call g through middle3: " << i << endl;
}Copy
运行结果如下:
可以看到,没有问题。分析一下:42 传递给 middle3 后,绑定到 t1 上,此时 t1 的类型为 int&&
(其中,T1 被推导为 int),然后我们通过 forward:std::forward<int>(t1)
(T1 是 int 而不是 int&&) ,将返回 int&&
,也就是说现在传给 g 的是一个右值引用了,可能你会有疑问:这不还是 int&&
吗?没变啊,为什么现在就可以和 g 中的右值引用绑定了,之前不也是 int&&
类型吗?注意:之前的右值引用叫“named rvalue“,也就是说,之前的右值引用是有名字的,叫 t1,而 t1 是一个变量!现在我们是返回一个右值引用,是没有名字的,所以是真正的右值。
对于左值引用 i,我们将 i 传递给 middle3 后,t2 的类型将变成 int& &&
,经过引用折叠,变成了 int&
,此时,如前所述,T2 将被推导为 int&
类型。然后通过 std::forward<T2>(t2)
=> std::forward<int&>(t2)
,将会返回一个 int& &&
,折叠后变成 int&
,可以看到,左值引用也得到了保留!
std::move()和std::forward()对比
- std::move执行到右值的无条件转换。就其本身而言,它没有move任何东西。
- std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。
- std::move和std::forward只不过就是执行类型转换的两个函数;std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一byte都没有。
- std::forward
()不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变;
inline
c++类中成员函数默认都是inline,但是声明在类中,定义在类外的不是。
inline要放在定义前,不要放在声明前(虽然放在声明前也不会出错)
inline一般修饰比较简单的函数,不带while、for、switch等,不带递归的
STL
emplace_back
添加一个新元素到结束的容器。该元件是构成在就地,即没有复制或移动操作进行。就是empace_back与push_back相比,替我们省去了拷贝构造。
C++11
{} 和 ()
简述
{}是c++11新的初始化方式
所谓花括号列表初始化,即是用花括号来初始化变量,其形式如: int test = { 0 } ;无论是初始化对象还是为对象赋值 , 在C++11下都可以使用这种形式的初始值。
不同的一点 是:使用这种形式来初始化内置类型的变量时,若存在类型转换且具有丢失信息的风险时,编译器将会报错。
举例说明
C++11之前主要有以下几种初始化方式:
//小括号初始化
string str("hello");
//等号初始化
string str="hello";
//POD对象与POD数组列表初始化
struct Studnet {
char* name;
int age;
};
Studnet s={"dablelv",18}; //纯数据(Plain of Data,POD)类型对象
Studnet sArr[]={{"dablelv",18},{"tommy",19}}; //POD数组
//构造函数的初始化列表
class Class {
int x;
public:
Class(): x(0){}
};
在C++11以前,程序员,或者初学者经常会感到疑惑关于怎样去初始化一个变量或者是一个对象。这么多的对象初始化方式,不仅增加了学习成本,也使得代码风格有较大出入,影响了代码的可读性和统一性。
从C++11开始,对列表初始化(List Initialization)的功能进行了扩充,可以作用于任何类型对象的初始化,至此,列表初始化方式完成了天下大一统。
花括号列表初始化,作为C++11新标准的一部被加入到了C++中。
因为这个原因,c++11提出了统一初始化,以为着使用这初始化列表,下面的做法都是正确的。
class Test {
int a;
int b;
public:
C(int i, int j);
};
Test t{0,0}; //C++11 only,相当于 Test t(0,0);
Test* pT=new Test{1,2}; //C++11 only,相当于 Test* pT=new Test(1,2);
int* a = new int[3]{1,2,0}; //C++11 only
此外,C++11列表初始化还可以应用于容器,终于可以摆脱 push_back() 调用了,C++11中可以直观地初始化容器:
//C++11 container initializer
vector<string> vs={"first", "second", "third"};
map<string,string> singers ={{"Lady Gaga", "+1 (212) 555-7890"},{"Beyonce Knowles", "+1 (212) 555-0987"}};
因此,可以将C++11提供的列表初始化作为统一的初始化方式,既降低了记忆难度,也提高的代码的统一度。
此外,C++11中,类的数据成员在申明时可以直接赋予一个默认值:
class C {
private:
int a=7; //C++11 only
};
所谓花括号列表初始化,即是用花括号来初始化变量,其形式如: int test = { 0 } ;无论是初始化对象还是为对象赋值 , 在C++11下都可以使用这种形式的初始值。
不同的一点 是:使用这种形式来初始化内置类型的变量时,若存在类型转换且具有丢失信息的风险时,编译器将会报错。
通过这一点可以看出,列表初始化比原有的初始化方式具有更严格的安全要求。下面是例子:
cpp
long double ld = 3.1415926536;
int a {ld} , b = {ld} // 编译器报错,存在丢失信息的风险
int c (ld) , d = ld ; //正确
关键字noexcept
用 noexcept 来指明函数不会抛出异常。