手撕虚函数、拿捏C++11新特性,硬核干货持续更新>>>>> 码字不易,给个支持>>>>>
一.C
1.memset与memcpy
1.1 memset
memset: memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。通常用于对刚申请到的内存进行初始化。
void *memset(void *s, int c, size_t n);
- s指向要填充的内存块。
- c是要被设置的值。
- n是要被设置该值的字符数。
- 返回类型是一个指向存储区s的指针。
注意: 一、不能任意赋值(通常初始化为0) memset函数是按照字节对内存块进行初始化,所以不能用它将int数组出初始化为0和-1(全1)之外的其他值(除非该值高字节和低字节相同)。 因为memset函数只能取c的后八位给所输入范围的每个字节,因此其实c的实际范围应该在0~255。也就是说无论c多大只有后八位二进制是有效的。 例如:对于int a[4]; memset(a, -1, sizeof(a)) 与 memset(a, 511, sizeof(a)) 所赋值的结果一样都为-1: 因为 -1 的二进制码为(11111111 11111111 11111111 11111111);511 的二进制码为(00000000 00000000 00000001 11111111); 后八位均为(11111111),所以数组中的每个字节都被赋值为(11111111)。 注意int占四个字节,例如a[0]的四个字节都被赋值为(11111111),那么a[0](11111111 11111111 11111111 11111111),即a[0] = -1。 二、注意所要赋值的数组的元素类型
int main(){
int a[4];
memset(a,1,4);
for(int i=0; i<4; i++){
cout<<a[i]<<" ";
}
return 0;
}
数组a是整型的,整型占据的内存大小为4Byte,而memset函数还是按照字节为单位进行赋值,将1(00000001)赋给每一个字节。那么对于a[0]来说,其值为(00000001 00000001 00000001 00000001),即十进制的16843009。
1.2 memcpy
void *memcpy(void *dest, const void *src, size_t n);
功能:从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中; 返回值:src和dest所指内存区域不能重叠,函数返回指向dest的指针。
char *s="Golden Global View";
char d[20];
memcpy(d,s,(strlen(s)+1));
printf("%s\n",d);
memcpy(d,s+14*sizeof(char),4*sizeof(char));
d[4]='\0';
printf("%s\n",d);
1.3 memcpy与strcpy的区别:(重点)
- 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、结构体、类等。
- 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
- 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。(src空间大小大于dst,又完全复制时也会造成溢出,但是相对strcpy更易于把控)
2.柔性数组char data[0]
struct Msg
{
...
int nLen;
char data[0];
};
介绍: 在结构体最后加char[0]或char[1]的用法是GNU C的扩展,在ISO/IEC 9899-1999里面,这么写是非法的。这种用法在C99中叫做 柔性数组。柔性数组成员前面必须至少有一个其它类型成员。包含柔性数组成员的结构要用malloc进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。 目的: 主要是为了方便管理内存缓冲区,如果你直接使用指针而不使用数组,那么,你在分配内存缓冲区时,就必须分配结构体一次,然后再分配结构体内的指针一次,(而此时分配的内存已经与结构体的内存不连续了,所以要分别管理即申请和释放)而如果使用数组,那么只需要一次就可以全部分配出来,反过来,释放时也是一样,使用数组,一次释放,使用指针,得先释放结构体内的指针,再释放结构体。还不能颠倒次序。其实就是分配一段连续的的内存,减少内存的碎片化。 使用方法:
int dataBytes = 10;
struct Msg *p = (struct Msg *)malloc(sizeof(struct Msg) + dataBytes);
p->nLen = dataBytes;
优点:
- 只需分配释放一次,速度稍快。
- 减少内存碎片。
使用指针与char[0]的区别:
- 结构体中使用指针:创建时,系统先为结构体分配内存,再分配指针指向的data的内存。两块内存不连续。释放的时候,先释放指针指向的内存,再释放结构体内存。
- 结构体中使用char[0]:创建时,系统一起为其分配结构体的内存和data的内存,两块内存是连续的(更确切的说是一块内存)。释放的时候,一次性释放
对齐对柔性数组的影响: 当结构体没有被明确要求对齐的时候会出现填充的情况,即编译器为了对齐结构体往里面填无效的内容。
struct node
{
int xxx;
char yyy;
char
char
char
}
很遗憾现在data[0]指向的是填充部位。而我们期望的是它要指向node之后。
这样会造成如下错误:
node* p = new char[sizeof(node)+3];
memset((char*)p,0,sizeof(node)+3);
p->xxx =1;
p->yyy =2;
p->data[0]=1;
p->data[1]=2;
p->data[2]=3;
1
2
1
2
3
0
0
0
在实际工作中你会发现你的数据结构后面始终有几个字节是0,而你通过sizeof(结构体)的偏移去读你的char[0]会因为偏移错误而读到错误的数据。 解决方案: 1.强制进行1字节对齐:
#pragma pack(1)
struct node
{
int xxx;
char yyy;
char data[0];
};
#pragma pack()
2.强制不进行字节对齐:
struct node
{
int a;
char data[0];
}__attribute ((packed));
二.C++
1.虚函数与虚表
在一个类中如果有虚函数,那么此类的实例中就有一个虚表指针指向虚表,这个虚表是一块儿专门存放类的虚函数地址的内存。 参考代码:
#include<iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
void h() { cout << "Base::h" << endl; }
};
typedef void(*Fun)(void);
int main()
{
Base b;
printf("虚表地址:%p\n", *(intptr_t *)&b);
printf("第一个虚函数地址:%p\n", *(intptr_t *)*(intptr_t *)&b);
printf("第二个虚函数地址:%p\n", *((intptr_t *)*(intptr_t *)(&b) + 1));
Fun pfun = (Fun)*((intptr_t *)*(intptr_t *)(&b));
printf("f():%p\n", pfun);
pfun();
pfun = (Fun)(*((intptr_t *)*(intptr_t *)(&b) + 1));
printf("g():%p\n", pfun);
pfun();
}
虚表的继承: 原文链接:https://blog.csdn.net/Jacksqh/article/details/105326852
当父类定义了虚函数时,在子类进行继承的时候会将父类的虚函数表也给继承下来所以那一些虚函数在子类中也是virtual类型的,如果要对父类中的虚函数进行重写时或添加虚函数,顺序是: ①先将父类的虚函数列表复制过来; ②重写虚函数时是把从父类继承过来的虚函数表中对应的虚函数进行相应的替换; ③如果子类自己要添加自己的虚函数,则是把添加的虚函数加到从父类继承过来虚函数表的尾部。
继承多个含有虚函数的类时,子类中含有多个虚表指针:
2.构造函数中的虚函数
分析以下程序:
#include <iostream>
using namespace std;
class A {
public:
A() {
this->fun();
}
virtual void fun() {
cout << "A fun" << endl;
}
};
class B :public A {
public:
B() {
this->fun();
}
virtual void fun() {
cout << "B fun" << endl;
}
};
int main() {
B b;
return 0;
}
所以在构造函数中完全可以调用虚函数,只是调用的语义不符预期,在A()中的this类型是A *指针,我们期望调用的是B::fun(),但是实际上调用的是A::fun()。
原因分析:(构造函数的执行流程):
- 调用B的构造函数,先调用A的构造函数,调用A的构造函数,先按照A对象的内存布局进行初始化,因为虚表指针是放在顶部的,先初始化虚表指针,指向虚表(虚表在编译期就生成),之后按照声明顺序初始化成员变量。最后调用构造函数{ }中的代码。
- 由此可见调用this->fun( )时已经设定好虚表指针,所以调用不会有任何问题。之后B将虚表指针指向自己的虚表,初始化自己的成员变量,最后调用B(){ }中的代码this->fun( ),这个时候对象顶部的虚表指针指向B的虚表,调到的自然是B::fun( )。(析构函数完全相反,先调用析构函数{ }中的代码。)
但是:
- 派生类与基类的构造函数初始化时,虚函数表是不一样的,意味着构造函数中调用虚函数是当前类的虚函数,无法多态调用。
- 即使允许多态调用,如果在基类中调用派生类的虚函数,由于派生类还未开始初始化,如果访问了还未初始化的数据,那就有很大的问题了(操作了未初始化的数据,可能会导致无法预料的后果)。
3.析构函数中抛出异常
析构函数中不能抛出异常:
- 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
- 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
如何处理析构函数中的异常:
- 析构函数内部消化异常
~ClassName()
{
try{
do_something();
}
catch(){
}
}
- 主动关闭程序
~ClassName()
{
try{
do_something();
}
catch(){
std::abort();
}
}
通过std::abort()函数来主动关闭程序,而不是任由程序在某个随机时刻突然崩溃,这样能减少潜在的用户风险。对于某些比较严重的异常,就可以使用这个方法。并且我们可以结合使用上面的方法,把能处理的异常消化掉。
三.C++11新特性
1.智能指针
参考文章:https://blog.csdn.net/qq_56673429/article/details/124837626 引入智能指针:
- C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
- 我们知道,一个对象过期时,会自动调用它的析构函数。如果一个常规的指针也有一个析构函数,该析构函数在指针过期时自动释放内存,我们就不用担心忘记释放内存的问题了。智能指针模板定义了类似指针的对象,可以将new获得的地址赋给这种对象,当智能指针过期时,其析构函数将使用delete来释放内存。
- 智能指针是行为行为类似指针的类对象
- 智能指针类是一种模板类
四种智能指针:
四种智能指针模板:auto_ptr , unique_ptr , shared_ptr , weak_ptr
#include <memory>
using namespace std;
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Report
{
private:
string str;
public:
Report(const string s):str(s){cout<<"Object Create!"<<"\n";}
~Report(){cout<<"Object Delete!"<<endl;}
void comment() {cout<<str<<endl;}
};
int main()
{
{
Report* p = new Report("using auto_ptr");
auto_ptr<Report> p1(p);
p1->comment();
}
cout<<"------------------"<<endl;
{
unique_ptr<Report> p2(new Report("using unique_ptr"));
p2->comment();
}
cout<<"------------------"<<endl;
{
shared_ptr<Report> p3(new Report("using shared_ptr"));
p3->comment();
}
return 0;
}
1.1 auto_ptr
auto_ptr 是C++98为管理内存提供的解决方案,C++11已将其摒弃,并提供了其他三种解决方案。但是auto_ptr已经使用了很多年,如果编译器不支持其他智能指针模板,auto_ptr仍是我们有限的选择。
atuo_ptr的弊端: 来看下面一段代码:
auto_ptr<string> ptr1(new string("hello"));
auto_ptr<string> ptr2;
ptr2 = ptr1;
我们可能会产生这样的疑问,ptr1、ptr2先后过期时,会把new string(“hello”)释放两次吗?这是不能接受的。要避免这种问题,有很多种方法:
- 定义赋值运算符,使之执行深拷贝;
- 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可以拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权,这就是用于auto_ptr和unique_ptr的策略,但unique更加严格。当ptr1转让所有权后,再次引用*ptr1时,程序将在运行阶段崩溃。但是使用unique_ptr时,程序不会等到运行阶段崩溃。总之,程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器将禁止这样做。
- 创建智能更好的指针,跟踪引用特定对象的智能指针数,这称为引用计数(reference counting)。例如赋值时,计数加1,指针过期时,计数j减1。仅当最后一个指针过期时才调用delete,这是shared_ptr所采用的策略。
1.2 unique_ptr
-
初始化: std::unique_ptr是一个独占式的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。 可以通过构造函数初始化,移动构造初始化,reset函数初始化。 -
删除器: unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器
#include <iostream>
using namespace std;
#include <string>
#include <memory>
#include <functional>
class Test
{
public:
Test() : m_num(0)
{
cout << "construct Test..." << endl;
}
Test(int x) : m_num(1)
{
cout << "construct Test, x = " << x << endl;
}
Test(string str) : m_num(2)
{
cout << "construct Test, str = " << str << endl;
}
~Test()
{
cout << "destruct Test..." << endl;
}
void setValue(int v)
{
this->m_num = v;
}
void print()
{
cout << "m_num: " << this->m_num << endl;
}
private:
int m_num;
};
int main()
{
unique_ptr<int> ptr1(new int(3));
unique_ptr<int> ptr2 = move(ptr1);
ptr2.reset(new int(7));
unique_ptr<Test> ptr3(new Test(666));
Test* pt = ptr3.get();
pt->setValue(6);
pt->print();
ptr3->setValue(777);
ptr3->print();
unique_ptr<Test, function<void(Test*)>> ptr4(new Test("hello"), [](Test* t) {
cout << "-----------------------" << endl;
delete t;
});
unique_ptr<Test[]> ptr5(new Test[3]);
shared_ptr<Test[]> ptr6(new Test[3]);
return 0;
}
1.3 shared_ptr
- shared_ptr的初始化:
共享式智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count(); - 获取原始指针:
对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的get()方法得到原始地址; - 指定删除器:
当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
#include <iostream>
using namespace std;
#include <string>
#include <memory>
class Test
{
public:
Test() : m_num(0)
{
cout << "construct Test..." << endl;
}
Test(int x) : m_num(0)
{
cout << "construct Test, x = " << x << endl;
}
Test(string str) : m_num(0)
{
cout << "construct Test, str = " << str << endl;
}
~Test()
{
cout << "destruct Test..." << endl;
}
void setValue(int v)
{
this->m_num = v;
}
void print()
{
cout << "m_num: " << this->m_num << endl;
}
private:
int m_num;
};
int main()
{
shared_ptr<int> ptr1(new int(3));
cout << "ptr1.use_count(): " << ptr1.use_count() << endl;
shared_ptr<int> ptr2 = move(ptr1);
cout << "ptr1.use_count(): " << ptr1.use_count() << endl;
cout << "ptr2.use_count(): " << ptr2.use_count() << endl;
shared_ptr<int> ptr3 = ptr2;
cout << "ptr2.use_count(): " << ptr2.use_count() << endl;
cout << "ptr3.use_count(): " << ptr3.use_count() << endl;
shared_ptr<int> ptr4 = make_shared<int>(8);
shared_ptr<Test> ptr5 = make_shared<Test>(7);
shared_ptr<Test> ptr6 = make_shared<Test>("GOOD LUCKLY!");
ptr6.reset();
cout << "ptr6.use_count(): " << ptr6.use_count() << endl;
ptr5.reset(new Test("hello"));
cout << "ptr5.use_count(): " << ptr5.use_count() << endl;
cout << endl;
cout << endl;
Test* t = ptr5.get();
t->setValue(1000);
t->print();
ptr5->setValue(7777);
ptr5->print();
printf("\n\n");
shared_ptr<Test> ppp(new Test(100), [](Test* t) {
cout << "Deleted Test......." << endl;
delete t;
});
printf("----------------------------------------------------------------------\n");
return 0;
}
另外,我们还可以自己封装一个函数模板make_shared_array方法来让shared_ptr支持数组,代码如下:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
int main()
{
shared_ptr<int> ptr1 = make_share_array<int>(10);
cout << ptr1.use_count() << endl;
shared_ptr<string> ptr2 = make_share_array<string>(7);
cout << ptr2.use_count() << endl;
return 0;
}
1.4 weak_ptr
- weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
- weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
- 使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数 expired() 的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
- weak_ptr可以使用一个非常重要的成员函数 lock() 从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr;
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp(new int);
weak_ptr<int> wp1;
weak_ptr<int> wp2(wp1);
weak_ptr<int> wp3(sp);
weak_ptr<int> wp4;
wp4 = sp;
weak_ptr<int> wp5;
wp5 = wp3;
return 0;
}
常用函数:
- 通过调用std::weak_ptr类提供的expired()方法来判断观测的资源是否已经被释放
- 通过调用std::weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象
- 通过调用std::weak_ptr类提供的reset()方法来清空对象,使其不监测任何资源
weak_ptr解决循环引用问题: 参考文章:https://blog.csdn.net/qq_36553387/article/details/115105137
#include <iostream>
#include <memory>
class Child;
class Parent;
class Parent {
private:
std::weak_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl;
std::cout << c.use_count() << std::endl;
}
std::cout << wpp.use_count() << std::endl;
std::cout << wpc.use_count() << std::endl;
std::cout << wpp.expired() << std::endl;
std::cout << wpc.expired() << std::endl;
return 0;
}
2.函数对象(函数符)
很多STL算法都使用函数对象——也叫函数符(functor)。函数符是可以以函数方式与 () 结合使用的任意对象。这包括:
- 函数名;
- 指向函数的指针;
- 重载了()运算符的类对象,即定义了函数
operator()() 的类。
#include <iostream>
using namespace std;
class Test
{
private:
double a;
double b;
public:
Test(double _a=0,double _b=1):a(_a),b(_b){}
double operator () (double x)
{
return a*x+b;
}
};
int main()
{
Test t1;
Test t2(2.5,10.0);
double y1 = t1(1.5);
double y2 = t2(0.4);
cout<<"y1:"<<y1<<endl
<<"y2:"<<y2<<endl;
return 0;
}
3. Lambda表达式
在C++中引出lambda的主要目的是,某些函数的参数需要接受函数指针或函数符,程序员能够将类似于函数的表达式用作这种函数的参数。因此,典型的lambda表达式是测试表达式或者比较表达式(可编写为一条返回语句)。这使得lambda简洁而利于理解,而且可自动推断返回类型。
3.1比较函数指针、函数符、和Lambda函数
示例:生成一个随即列表,判断其中有多少整数可以被3整除,多少可以被13整除 函数指针:
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
bool f3(int x) {return x%3 == 0;}
bool f13(int x) {return x%13 == 0;}
int main()
{
vector<int> numbers(1000);
generate(numbers.begin(),numbers.end(),rand);
int count3 = count_if(numbers.begin(),numbers.end(),f3);
cout<<"Count of numbers divisible by 3: "<<count3<<endl;
int count13 = count_if(numbers.begin(),numbers.end(),f13);
cout<<"Count of numbers divisible by 13: "<<count13<<endl;
return 0;
}
函数符:
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
class F_mod
{
private:
int dv;
public:
F_mod(int _dv = 1):dv(_dv){}
bool operator()(int x){return x%dv == 0;}
};
int main()
{
vector<int> numbers(1000);
generate(numbers.begin(),numbers.end(),rand);
int count3 = count_if(numbers.begin(),numbers.end(),F_mod(3));
cout<<"Count of numbers divisible by 3: "<<count3<<endl;
int count13 = count_if(numbers.begin(),numbers.end(),F_mod(13));
cout<<"Count of numbers divisible by 13: "<<count13<<endl;
return 0;
}
Lambda表达式: 名称Lambda来自于lambda calculus(λ演算)——一种定义、应用函数的数学系统,这个系统让您能够使用匿名函数,即无需给函数命名。在C++11中。对于接受函数指针或函数符的函数,可以使用匿名函数定义(lambda)作为其参数。与前述函数 f3() 对应的lambda如下: bool f3(int x) {return x%3 == 0;} [] (int x) {return x%3 == 0;} 差别:
- 使用[]替代了函数名(匿名);
- 没有声明函数返回类型。返回类型相当于使用decltype(类型推断)根据返回值推断得到的,这里为bool。如果lambda不包含返回语句,推断出的返回类型将为void。
仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用,否则,需要使用新增的返回类型后置语法: [](double x)->double(int y = x;return x-y;) 返回值类型为double
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
int main()
{
vector<int> numbers(1000);
generate(numbers.begin(),numbers.end(),rand);
int count3 = count_if(numbers.begin(),numbers.end(),[] (int x) {return x%3 == 0;});
cout<<"Count of numbers divisible by 3: "<<count3<<endl;
int count13 = count_if(numbers.begin(),numbers.end(),[] (int x) {return x%13 == 0;});
cout<<"Count of numbers divisible by 13: "<<count13<<endl;
return 0;
}
3.2 lambda表达式的优点
- 距离近:
定义尽可能在使用的地方附近,这样无需翻阅多页源代码,以了解函数具体内容;另外,如果需要修改代码,涉及的内容都将在附近;lambda是理想的选择,因为其定义和使用是在同一个地方进行的;而函数是最糟糕的选择,因为不能在函数内部定义其他函数,因此函数的定义可能离使用的地方很远。函数符是不错的选择,因为可在函数内部定义类(包含函数符类),因此定义离使用地点可以很近。 - 较简洁:
从简洁的角度看,函数符代码比函数和lambda表达式代码更加繁琐。也可以在函数内部定义有名称的lambda。 - 效率高:
由于函数地址的概念意味着非内联函数,因此函数指针方法阻止了内联,而lambda通常不会阻止内联。 - 功能多:
下一节详细介绍。
3.3 lambda表达式访问作用域内的变量
- [a] a为变量名,lambda表达式内可以按值访问 a。
- [&a] 可以按引用访问变量a
- [=] 可以按值访问作用域内的所有动态变量
- [&] 可以按引用的方式访问作用域内所有动态变量
可以混合使用以上方式,例如 [&a,=] 可以按引用方式访问a,按值方式访问所有作用域内的变量
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
int main()
{
vector<int> numbers(1000);
generate(numbers.begin(),numbers.end(),rand);
int count3 = 0;
int count13 = 0;
for_each(numbers.begin(),numbers.end(),
[&](int x){count3 += x%3==0;count13 += x%13==0;});
cout<<"Count of numbers divisible by 3: "<<count3<<endl;
cout<<"Count of numbers divisible by 13: "<<count13<<endl;
return 0;
}
4.移动语义和右值引用
4.1 右值引用
- 左值(locator value)的概念:左值是一个表示数据的表达式,如变量名名或解除引用的指针,程序可获取其地址。
- 右值(real value)的概念:右值是可以出现在赋值表达式右边,但不能对其应用地址运算符的值。
右值包括字面常量(c风格字符串除外,它表示地址),诸如x+y等表达式以及函数返回值(返回引用除外)。
传统的C++引用(现在成为左值引用)使得标识符关联到左值。C++11x新增了右值引用,使用&&表示,右值引用可以关联到右值。
int x = 10;
int y = 23;
int &&r1 = 13;
int &&r2 = x+y;
double &&r3 = std::sqrt(2.0);
注意,r2关联到的是当时计算x+y得到的结果33,即使以后修改了x或y,也不会影响到r2;有趣的是,将右值引用关联到右值,导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将取地址运算符&用于13,但可以将其用于r1。通过将数据与特定的地址相关联,使得可以通过右值引用来访问该数据。 示例:
#include <iostream>
using namespace std;
inline double fun(double tf) {return 5.0*tf;}
int main()
{
double tc = 2.5;
double && rd1 = 7.0;
double && rd2 = 1.0*tc + 2.0;
double && rd3 = fun(rd2);
cout<<"tc value and address: "<<tc<<" "<<&tc<<endl;
cout<<"rd1 value and address: "<<rd1<<" "<<&rd1<<endl;
cout<<"rd2 value and address: "<<rd2<<" "<<&rd2<<endl;
cout<<"rd3 value and address: "<<rd3<<" "<<&rd3<<endl;
return 0;
}
4.2 移动语义
vector<string> allcaps(const vector<string> &vs)
{
vector<string> temp;
return temp;
}
vector<string> vstr1(1000,"abcdefghi");
vector<string> vstr2(allcaps(vstr1));
我们发现,allcaps()创建了对象temp,该对象管理着10000个字符,vector和string的拷贝构造函数创建这10000个字符的副本,然后程序删除allcaps()返回的临时对象(迟钝的编译器甚至可能将temp复制给一个临时返回对象,删除temp,再删除临时返回对象)。这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr2 不是更好吗? 也就是说,不将10000个字符复制到新地方再删除原来的字符,而将字符留在原来的地方,并将vstr2与之关联。 这类似与计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录这种方法被称为移动语义(move semantics)。
移动语义实际上避免了移动原始数据,而只是修改了记录。
示例:
#include <iostream>
using namespace std;
class Useless
{
private:
int n;
char* pc;
static int ct;
void ShowObject() const;
public:
Useless();
explicit Useless(int k);
Useless(int k,char ch);
Useless(const Useless &f);
Useless(Useless &&f);
~Useless();
Useless operator + (const Useless &f) const;
void showData() const;
};
int Useless::ct = 0;
int main()
{
Useless one(10,'x');
Useless two = one;
Useless three(20,'o');
Useless four(one+three);
cout<<"object one: ";
one.showData();
cout<<"object two: ";
two.showData();
cout<<"object three: ";
three.showData();
cout<<"object four: ";
four.showData();
return 0;
}
void Useless::ShowObject() const
{
cout<<"number of elements: "<<n<<" "
<<"data address: "<<(void *)pc<<endl;
}
Useless::Useless()
{
++ct;
n = 0;
pc = nullptr;
cout<<"default constructor called;number of objects: "<<ct<<endl;
}
Useless::Useless(int k):n(k)
{
++ct;
pc = new char[n];
cout<<"int constructor called;numberof objects: "<<ct<<endl;
ShowObject();
}
Useless::Useless(int k, char ch):n(k)
{
++ct;
pc = new char[n];
for(int i=0;i<n;i++)
pc[i] = ch;
cout<<"int char constructor called;numberof objects: "<<ct<<endl;
ShowObject();
}
Useless::Useless(const Useless &f):n(f.n)
{
++ct;
pc = new char[n];
for(int i=0;i<n;i++)
pc[i] = f.pc[i];
cout<<"copy constructor called;numberof objects: "<<ct<<endl;
ShowObject();
}
Useless::Useless(Useless &&f):n(f.n)
{
++ct;
pc = f.pc;
f.pc = nullptr;
f.n = 0;
cout<<"move constructor called;numberof objects: "<<ct<<endl;
ShowObject();
}
Useless::~Useless()
{
cout<<"destructor called;numberof objects: "<<--ct<<endl;
ShowObject();
delete[] pc;
}
Useless Useless::operator +(const Useless &f) const
{
cout<<"entering operator + ()\n";
Useless temp(n+f.n);
for(int i=0;i<n;i++)
temp.pc[i] = pc[i];
for(int i=n;i<temp.n;i++)
temp.pc[i] = f.pc[i-n];
cout<<"temp object:\n";
cout<<"leaving operator+()\n";
return temp;
}
void Useless::showData() const
{
if(n==0) cout<<"object empty";
else
for(int i=0;i<n;i++)
cout<<pc[i];
cout<<endl;
}
Useless four(one+three);//calls move constructor 表达式one+three调用Useless::operator+(),而右值引用f将关联到该方法返回的临时对象。
通过分析复制构造与移动构造的区别可以发现,移动构造让pc指向现有的数据,以获取这些数据的所有权(窃取 plifering)。此时,pc和f.pc指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用delete[]两次。为了避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete[]没有问题。由于修改了f对象,这要求不能在参数声明中使用const。
two是one的副本,它们显示的数据输出相同,但是显示的数据地址不同(006F4B68和006F4BB0)。另一方面,在方法Useless::operator+()中创建的对象的数据地址与对象four存储的数据地址相同(都是006F4C48),其中对象four是由移动构造创建的,创建对象four后,为临时对象调用了析构函数。
做了优化的编译器编译该程序,输出将不同: 注意到,没有调用移动构造函数,且之创建了4个对象。创建对象four时,该编译器没有调用移动构造或拷贝构造函数;相反,它推断出对象four是operator+()所做工作的受益人,因此将operator+()创建的对象转让到four的名下。
4.3 移动构造函数解析
要让移动语义的发生,需要两个步骤:
- 右值引用:让编译器知道何时使用移动语义
- 编写移动构造函数。
机智的编译器可能自动消除额外的复制工作,但通过使用右值引用,程序员可以指出何时该使用移动语义。
4.4 移动赋值运算符
适用于构造函数的移动语义也适用于赋值运算符:
Useless & Useless::operator =(const Useless &f)
{
cout<<"calls copy assignment\n";
if(this == &f) return *this;
delete[] pc;
n = f.n;
pc = new char[n];
for(int i=0;i<n;i++)
pc[i] = f.pc[i];
return *this;
}
Useless & Useless::operator =(Useless &&f)
{
cout<<"calls move assignment\n";
if(this == &f) return *this;
delete[] pc;
n = f.n;
pc = f.pc;
f.n = 0;
f.pc = nullptr;
return *this;
}
int main()
{
Useless one(10,'x');
Useless two;
two = one;
Useless three;
three = move(one);
one.showData();
two.showData();
three.showData();
return 0;
}
three = move(one); //calls move constructor 头文件< utility > 中声明的std:move()强制移动将对象one强制转换为右值 Useless &&类型。
C++11在原有的4个特殊成员函数(默认构造、拷贝构造、拷贝赋值运算符和默认析构函数)基础上新增了两个: 移动构造函数和移动赋值运算符
|