1、constexpr
在 C++11 中添加了一个新的关键字 constexpr,这个关键字是用来修饰常量表达式的。所谓常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。
2、委托构造和继承构造函数
委托构造函数
托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化:
#include <iostream>
using namespace std;
class Test
{
public:
Test() {};
Test(int max)
{
this->m_max = max > 0 ? max : 100;
}
Test(int max, int min)
{
this->m_max = max > 0 ? max : 100; // 冗余代码
this->m_min = min > 0 && min < max ? min : 1;
}
Test(int max, int min, int mid)
{
this->m_max = max > 0 ? max : 100; // 冗余代码
this->m_min = min > 0 && min < max ? min : 1; // 冗余代码
this->m_middle = mid < max && mid > min ? mid : 50;
}
int m_min;
int m_max;
int m_middle;
};
int main()
{
Test t(90, 30, 60);
cout << "min: " << t.m_min << ", middle: "
<< t.m_middle << ", max: " << t.m_max << endl;
return 0;
}
三个构造函数中都有重复的代码,在 C++11 之前构造函数是不能调用构造函数的,加入了委托构造之后,我们就可以轻松地完成代码的优化了:
#include <iostream>
using namespace std;
class Test
{
public:
Test() {};
Test(int max)
{
this->m_max = max > 0 ? max : 100;
}
Test(int max, int min):Test(max)
{
this->m_min = min > 0 && min < max ? min : 1;
}
Test(int max, int min, int mid):Test(max, min)
{
this->m_middle = mid < max && mid > min ? mid : 50;
}
int m_min;
int m_max;
int m_middle;
};
int main()
{
Test t(90, 30, 60);
cout << "min: " << t.m_min << ", middle: "
<< t.m_middle << ", max: " << t.m_max << endl;
return 0;
}
在修改之后的代码中可以看到,重复的代码全部没有了,并且在一个构造函数中调用了其他的构造函数用于相关数据的初始化,相当于是一个链式调用。在使用委托构造函数的时候还需要注意一些几个问题:
- 这种链式的构造函数调用不能形成一个闭环(死循环),否则会在运行期抛异常。
- 如果要进行多层构造函数的链式调用,建议将构造函数的调用的写在初始列表中而不是函数体内部,否则编译器会提示形参的重复定义。
Test(int max)
{
this->m_max = max > 0 ? max : 100;
}
Test(int max, int min)
{
Test(max); // error, 此处编译器会报错, 提示形参max被重复定义
this->m_min = min > 0 && min < max ? min : 1;
}
在初始化列表中调用了代理构造函数初始化某个类成员变量之后,就不能在初始化列表中再次初始化这个变量了。
// 错误, 使用了委托构造函数就不能再次m_max初始化了
Test(int max, int min) : Test(max), m_max(max)
{
this->m_min = min > 0 && min < max ? min : 1;
}
继承构造函数
C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。先来看没有继承构造函数之前的处理方式:
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
int m_i;
double m_j;
string m_k;
};
class Child : public Base
{
public:
Child(int i) :Base(i) {}
Child(int i, double j) :Base(i, j) {}
Child(int i, double j, string k) :Base(i, j, k) {}
};
int main()
{
Child c(520, 13.14, "i love you");
cout << "int: " << c.m_i << ", double: "
<< c.m_j << ", string: " << c.m_k << endl;
return 0;
}
在子类中初始化从基类继承的类成员,需要在子类中重新定义和基类一致的构造函数,这是非常繁琐的,C++11 中通过添加继承构造函数这个新特性完美的解决了这个问题,使得代码更加精简。
继承构造函数的使用方法是这样的:通过使用 using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
int m_i;
double m_j;
string m_k;
};
class Child : public Base
{
public:
//这样就可以在子类中直接继承父类的所有的构造函数,通过他们去构造子类对象了。
using Base::Base;
//但是如果Child类有了自己的构造函数,就要手工调用父类构造函数了
};
int main()
{
Child c1(520, 13.14);
cout << "int: " << c1.m_i << ", double: " << c1.m_j << endl;
Child c2(520, 13.14, "i love you");
cout << "int: " << c2.m_i << ", double: "
<< c2.m_j << ", string: " << c2.m_k << endl;
return 0;
}
另外如果在子类中隐藏了父类中的同名函数,也可以通过 using 的方式在子类中使用基类中的这些父类函数:
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
void func(int i)
{
cout << "base class: i = " << i << endl;
}
void func(int i, string str)
{
cout << "base class: i = " << i << ", str = " << str << endl;
}
int m_i;
double m_j;
string m_k;
};
class Child : public Base
{
public:
using Base::Base;
using Base::func;
void func()
{
cout << "child class: i'am luffy!!!" << endl;
}
};
int main()
{
Child c(250);
c.func();
c.func(19);
c.func(19, "luffy");
return 0;
}
子类中的 func() 函数隐藏了基类中的两个 func() 因此默认情况下通过子类对象只能调用无参的 func(),在上面的子类代码中添加了 using Base::func; 之后,就可以通过子类对象直接调用父类中被隐藏的带参 func() 函数了。
3、右值引用
右值
C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。
- 左值是指存储在内存中、有明确存储地址(可取地址)的数据;
- 右值是指可以提供数据值的数据(不可取地址);
C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):
- 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
- 将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。
右值引用
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”:其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
int&& value = 520;
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
Test(const Test& a)
{
cout << "Test(const Test& a)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
};
Test getObj()
{
return Test();
}
void test1()
{
int a1;
//int&& a2 = a1; // error,无法将右值引用绑定到左值
//Test& t = getObj(); // error,非常量引用的初始值必须为左值
Test&& t = getObj(); // getObj() 返回的临时对象被称之为将亡值,t 是这个将亡值的右值引用。
const Test& t2 = getObj(); //ok,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。
getchar();
}
int main(void)
{
test1();
system("pause");
return 0;
}
运行结果:
代码停留在test1最后一行。我们看到即使getObj函数运行完后,返回的临时变量也没有被析构, 通过右值引用的声明,该临时对象又“重获新生”,生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
性能优化
在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "Test()" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "Test(const Test& a)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test tmp;
return tmp;
}
void test1()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
getchar();
}
int main(void)
{
test1();
system("pause");
return 0;
}
通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的匿名对象进行了深拷贝,得到了对象 t。在 getObj() 函数中创建的对象tmp,虽然进行了内存的申请操作,但是没有使用就释放掉了。
如果能够使用临时对象tmp,已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了
右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象,这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "Test()" << endl;
}
Test& operator=(const Test& obj)
{
cout << "Test& operator=" << endl;
if (!m_num)
delete m_num;
m_num = new int(*obj.m_num);
return *this;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "Test(const Test& a)" << endl;
}
// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num) //首先先接管传进来的右值代表的那片堆空间
{
a.m_num = nullptr; //让 右值a 的那片堆空间置空
cout << "Test(Test&& a)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test tmp; //调用无参Test无参构造函数
return tmp; //利用tmp构造返回的匿名对象(没有移动构造函数时)
//函数返回后,tmp析构
}
void test1()
{
Test t = getObj(); //返回的匿名对象进行赋值操作,然后匿名对象析构
cout << "t.m_num: " << *t.m_num << endl;
getchar();
}
int main(void)
{
test1();
system("pause");
return 0;
}
通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对返回的匿名对象进行深拷贝,提高了性能。
&&的特性
在 C++ 中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中。
如果是模板参数需要指定为 T&&,如果是自动类型推导需要指定为 auto &&,在这两种场景下 && 被称作未定的引用类型。另外还有一点需要额外注意 const T&& 表示一个右值引用,不是未定引用类型。
template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10);
int x = 10;
f(x);
f1(x);
- 第 4 行中,对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
- 第 6 行中,对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
- 第 7 行中,f1(x) 的参数是 const T&& 不是未定引用类型,不需要推导,本身就表示一个右值引用
int main()
{
int x = 520, y = 1314;
// 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&&
// 或者 auto&& 得到的是一个左值引用类型
auto&& v1 = x; // auto&& 表示一个整形的左值引用,用左值x推导auto&&
//通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
auto&& v2 = 250; //auto&& 表示一个整形的右值引用,右值推导auto&&
// decltype(x)&& 等价于 int&& 是一个右值引用不是未定引用类型,
// y 是一个左值,不能使用左值初始化一个右值引用类型。
decltype(x)&& v3 = y; // error
cout << "v1: " << v1 << ", v2: " << v2 << endl;
return 0;
};
由于上述代码中存在 T&& 或者 auto&& 这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在 C++11 中引用折叠的规则如下:
- 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
- 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
int&& a1 = 5; //a1是右值引用
auto&& bb = a1; //使用右值引用(非右值)推导auto&&,bb 为左值引用类型
auto&& bb1 = 5; // 5 为右值,推导出的 bb1 为右值引用类型
int a2 = 5;
int &a3 = a2;
auto&& cc = a3; //a3 为左值引用,推导出的 cc 为左值引用类型
auto&& cc1 = a2; //a2 为左值,推导出的 cc1 为左值引用类型
const int& s1 = 100;
const int&& s2 = 100;
auto&& dd = s1; //s1 为常量左值引用,推导出的 dd 为常量左值引用类型
auto&& ee = s2; //s2 为常量右值引用,推导出的 ee 为常量左值引用类型
const auto&& x = 5; //x 为右值引用,不需要推导
#include <iostream>
using namespace std;
void printValue(int &i)
{
cout << "l-value: " << i << endl;
}
void printValue(int &&i)
{
cout << "r-value: " << i << endl;
}
void forward(int &&k)
{
//编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue)
printValue(k);
}
int main()
{
int i = 520;
printValue(i); //void printValue(int &i)
printValue(1314); //void printValue(int &&i)
forward(250); //orward () 接收的是一个右值,但是在这个函数中调用函数 printValue () 时,
// 参数 k 变成了一个命名对象,编译器会将其当做左值来处理。
return 0;
};
编译器会将已命名的右值引用(上面的k)视为左值,将未命名的右值引用视为右值。
4、转移和完美转发
move
在 C++11 添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助 std::move () 函数。
使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
class Test
{
public:
Test(){}
......
}
int main()
{
Test t;
Test && v1 = t; // error
Test && v2 = move(t); // ok
list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls; // 需要拷贝, 效率低
list<string> ls2 = move(ls); //
return 0;
}
如果不使用 std::move,拷贝的代价很大,性能较低。使用 move 几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用 move () 就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。
forward
右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值(编译器会将已命名的右值引用视为左值),并不是原来的类型了。
如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward () 函数,该函数实现的功能称之为完美转发。
// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;
// 精简之后的样子
std::forward<T>(t);
- 当T为左值引用类型时,t将被转换为T类型的左值
- 当T不是左值引用类型时,t将被转换为T类型的右值
#include <iostream>
using namespace std;
template<typename T>
void printValue(T& t)
{
cout << "l-value: " << t << endl;
}
template<typename T>
void printValue(T&& t)
{
cout << "r-value: " << t << endl;
}
template<typename T>
void testForward(T && v)
{
printValue(v);
printValue(move(v));
printValue(forward<T>(v));
cout << endl;
}
int main()
{
testForward(520);
int num = 1314;
testForward(num);
testForward(forward<int>(num));
testForward(forward<int&>(num));
testForward(forward<int&&>(num));
return 0;
}
打印结果如下:
l-value: 520
r-value: 520
r-value: 520l-value: 1314
r-value: 1314
l-value: 1314l-value: 1314
r-value: 1314
r-value: 1314l-value: 1314
r-value: 1314
l-value: 1314l-value: 1314
r-value: 1314
r-value: 1314
- testForward(520); 函数的形参为未定引用类型 T&&,实参为右值(520),初始化后被推导为一个右值引用。
- printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
-
printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
-
printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为 ``右值`
testForward(num); 函数的形参为未定引用类型 T&&,实参为左值,初始化后被推导为一个左值引用
- printValue(v); 实参为左值
- printValue(move(v)); 通过 move 将左值转换为右值,实参为右值
- printValue(forward<T>(v));forward 的模板参数为左值引用,最终得到一个左值引用,实参为左值
testForward(forward<int>(num));forward 的模板类型为 int,不是左值引用类型,最终会得到一个右值,函数的形参为未定引用类型 T&& 被右值初始化后得到一个右值引用类型
- printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
- printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
- printValue(forward<T>(v)); forward 的模板参数为右值引用,最终得到一个右值,实参为右值
testForward(forward<int&>(num));forward 的模板类型为 int&,最终会得到一个左值,函数的形参为未定引用类型 T&& 被左值初始化后得到一个左值引用类型
- printValue(v); 实参为左值
- printValue(move(v)); 通过 move 将左值转换为右值,实参为右值
- printValue(forward<T>(v)); forward 的模板参数为左值引用,最终得到一个左值,实参为左值
testForward(forward<int&&>(num));forward 的模板类型为 int&&,最终会得到一个右值,函数的形参为未定引用类型 T&& 被右值初始化后得到一个右值引用类型
- printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
- printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
- printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为右值
5、std::initializer_list
在 C++ 的 STL 容器中,可以进行任意长度的数据的初始化。
使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。
- 它是一个轻量级的容器类型,内部定义了迭代器 iterator 等容器必须的概念,遍历时得到的迭代器是只读的
- 对于 std::initializer_list<T> 而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型 T
- 在 std::initializer_list 内部有三个成员接口:size(), begin(), end()。
- std::initializer_list 对象只能被整体初始化或者赋值。
#include <iostream>
#include <string>
using namespace std;
void traversal(std::initializer_list<int> a)
{
for (auto it = a.begin(); it != a.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
}
int main(void)
{
initializer_list<int> list;
cout << "current list size: " << list.size() << endl;
traversal(list);
list = { 1,2,3,4,5,6,7,8,9,0 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;
list = { 1,3,5,7,9 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;
// 直接通过初始化列表传递数据 //
traversal({ 2, 4, 6, 8, 0 });
cout << endl;
traversal({ 11,12,13,14,15,16 });
cout << endl;
return 0;
}
结果:
current list size: 0
current list size: 10
1 2 3 4 5 6 7 8 9 0
current list size: 5
1 3 5 7 9
2 4 6 8 0
11 12 13 14 15 16
- std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list
- 因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,
- std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Test
{
public:
Test(std::initializer_list<string> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
cout << *it << " ";
m_names.push_back(*it);
}
cout << endl;
}
private:
vector<string> m_names;
};
int main(void)
{
Test t({ "jack", "lucy", "tom" });
Test t1({ "hello", "world", "nihao", "shijie" });
return 0;
}
6、using的使用
定义别名
typedef 旧的类型名 新的类型名;
// 使用举例
typedef unsigned int uint_t;
using 新的类型 = 旧的类型;
// 使用举例
using uint_t = int;
// 使用typedef定义函数指针
typedef int(*func_ptr)(int, double);
// 使用using定义函数指针
using func_ptr1 = int(*)(int, double);
模板的别名
使用 typedef 重定义类似很方便,但是它有一点限制,比如无法重定义一个模板
template <typename T>
typedef map<int, T> type; // error, 语法错误
使用 typename 不支持给模板定义别名,这个简单的需求仅通过 typedef 很难办到,需要添加一个外敷类:
#include <iostream>
#include <functional>
#include <map>
using namespace std;
template <typename T>
// 定义外敷类
struct MyMap
{
typedef map<int, T> type;
};
int main(void)
{
MyMap<string>::type m;
m.insert(make_pair(1, "luffy"));
m.insert(make_pair(2, "ace"));
MyMap<int>::type m1;
m1.insert(1, 100);
m1.insert(2, 200);
return 0;
}
在 C++11 中,新增了一个特性就是可以通过使用 using 来为一个模板定义别名,对于上面的需求可以写成这样:
template <typename T>
using mymap = map<int, T>;
#include <iostream>
#include <functional>
#include <map>
using namespace std;
template <typename T>
using mymap = map<int, T>;
int main(void)
{
// map的value指定为string类型
mymap<string> m;
m.insert(make_pair(1, "luffy"));
m.insert(make_pair(2, "ace"));
// map的value指定为int类型
mymap<int> m1;
m1.insert(1, 100);
m1.insert(2, 200);
return 0;
}
最后在强调一点:using 语法和 typedef 一样,并不会创建出新的类型,它们只是给某些类型定义了新的别名。using 相较于 typedef 的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名。
7、 可调用对象包装器、绑定器
可调用对象
在 C++ 中存在 “可调用对象” 这么一个概念。准确来说,可调用对象有如下几种定义:
- 是一个函数指针
-
int print(int a, double b) { cout << a << b << endl; return 0; } // 定义函数指针 int (*func)(int, double) = &print;
-
- 是一个具有operator()成员函数的类对象(仿函数)
-
struct Test { // ()操作符重载 void operator()(string msg) { cout << "msg: " << msg << endl; } }; int main(void) { Test t; t("我是要成为海贼王的男人!!!"); // 仿函数 return 0; }
-
- 是一个可被转换为函数指针的类对象
-
using func_ptr = void(*)(int, string); struct Test { static void print(int a, string b) { cout << "name: " << b << ", age: " << a << endl; } // 将类对象转换为函数指针 operator func_ptr() { return print; //只能返回静态函数 } }; int main(void) { Test t; // 对象转换为函数指针, 并调用 t(19, "Monkey D. Luffy"); return 0; }
-
- 是一个类成员函数指针或者类成员指针
-
struct Test { void print(int a, string b) { cout << "name: " << b << ", age: " << a << endl; } int m_num; }; int main(void) { // 定义类成员函数指针指向类成员函数 void (Test::*func_ptr)(int, string) = &Test::print; // 类成员指针指向类成员变量 int Test::*obj_ptr = &Test::m_num; Test t; // 通过类成员函数指针调用类成员函数 (t.*func_ptr)(19, "Monkey D. Luffy"); // 通过类成员指针初始化类成员变量 t.*obj_ptr = 1; cout << "number is: " << t.m_num << endl; return 0; }
-
在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型。
C++ 中的可调用类型虽然具有比较统一的操作形式,但定义方式五花八门,这样在我们试图使用统一的方式保存,或者传递一个可调用对象时会十分繁琐。现在,C++11通过提供std::function 和 std::bind统一了可调用对象的各种操作。
可调用对象的包装器
std::function是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
基本用法
std::function 必须要包含一个叫做 functional 的头文件,可调用对象包装器使用语法如下:
#include <functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;
#include <iostream>
#include <functional>
using namespace std;
int add(int a, int b)
{
cout << a << " + " << b << " = " << a + b << endl;
return a + b;
}
class T1
{
public:
static int sub(int a, int b)
{
cout << a << " - " << b << " = " << a - b << endl;
return a - b;
}
};
class T2
{
public:
int operator()(int a, int b)
{
cout << a << " * " << b << " = " << a * b << endl;
return a * b;
}
};
int main(void)
{
// 绑定一个普通函数
function<int(int, int)> f1 = add;
// 绑定以静态类成员函数
function<int(int, int)> f2 = T1::sub;
// 绑定一个仿函数
T2 t;
function<int(int, int)> f3 = t;
// 函数调用
f1(9, 3);
f2(9, 3);
f3(9, 3);
return 0;
}
输出如下:
9 + 3 = 12
9 - 3 = 6
9 * 3 = 27
通过测试代码可以得到结论:std::function 可以将可调用对象进行包装,得到一个统一的格式,包装完成得到的对象相当于一个函数指针,和函数指针的使用方式相同,通过包装器对象就可以完成对包装的函数的调用了
作为回调函数使用
因为回调函数本身就是通过函数指针实现的,使用对象包装器可以取代函数指针的作用,来看一下下面的例子:
#include <iostream>
#include <functional>
using namespace std;
class A
{
public:
// 构造函数参数是一个包装器对象
A(const function<void()>& f) : callback(f)
{
}
void notify()
{
callback(); // 调用通过构造函数得到的函数指针
}
private:
function<void()> callback;
};
class B
{
public:
void operator()()
{
cout << "我是要成为海贼王的男人!!!" << endl;
}
};
int main(void)
{
B b;
A a(b); // 仿函数通过包装器对象进行包装
a.notify();
return 0;
}
使用对象包装器 std::function 可以非常方便的将仿函数转换为一个函数指针,通过进行函数指针的传递,在其他函数的合适的位置就可以调用这个包装好的仿函数了。
另外,使用 std::function 作为函数的传入参数,可以将定义方式不相同的可调用对象进行统一的传递,这样大大增加了程序的灵活性。
绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
- 将可调用对象与其参数一起绑定成一个仿函数。
- 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。
// 绑定非类成员函数/变量
auto f = std::bind(可调用对象地址, 绑定的参数/占位符);
// 绑定类成员函/变量
auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);
#include <iostream>
#include <functional>
using namespace std;
void callFunc(int x, const function<void(int)>& f)
{
if (x % 2 == 0)
{
f(x);
}
}
void output(int x)
{
cout << x << " ";
}
void output_add(int x)
{
cout << x + 10 << " ";
}
int main(void)
{
// 使用绑定器绑定可调用对象和参数
/*
std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,
在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。
*/
auto f1 = bind(output, placeholders::_1);
for (int i = 0; i < 10; ++i)
{
callFunc(i, f1);
}
cout << endl;
/*
placeholders::_1 是一个占位符,
代表这个位置将在 函数调用时被传入的第一个参数所替代。
*/
auto f2 = bind(output_add, placeholders::_1);
for (int i = 0; i < 10; ++i)
{
callFunc(i, f2);
}
cout << endl;
return 0;
}
在上面的程序中,使用了 std::bind 绑定器,在函数外部通过绑定不同的函数,控制了最后执行的结果。std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。
placeholders::_1 是一个占位符,代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符 placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5 等……
#include <iostream>
#include <functional>
using namespace std;
void output(int x, int y)
{
cout << x << " " << y << endl;
}
int main(void)
{
// 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数
bind(output, 1, 2)();
bind(output, placeholders::_1, 2)(10); //调用output时,第一个参数将会被,传进来的第一个参数10所代替
bind(output, 2, placeholders::_1)(10);
// error, 调用时没有第二个参数
// bind(output, 2, placeholders::_2)(10);
// 调用时第一个参数10被吞掉了,没有被使用
bind(output, 2, placeholders::_2)(10, 20);
bind(output, placeholders::_1, placeholders::_2)(10, 20);
bind(output, placeholders::_2, placeholders::_1)(10, 20);
return 0;
}
1 2 // bind(output, 1, 2)();
10 2 // bind(output, placeholders::_1, 2)(10);
2 10 // bind(output, 2, placeholders::_1)(10);
2 20 // bind(output, 2, placeholders::_2)(10, 20);
10 20 // bind(output, placeholders::_1, placeholders::_2)(10, 20);
20 10 // bind(output, placeholders::_2, placeholders::_1)(10, 20);
通过测试可以看到,std::bind 可以直接绑定函数的所有参数,也可以仅绑定部分参数。在绑定部分参数的时候,通过使用 std::placeholders 来决定空位参数将会属于调用发生时的第几个参数。
可调用对象包装器 std::function 是不能实现对类成员函数指针或者类成员指针的包装的,但是通过绑定器 std::bind 的配合之后,就可以完美的解决这个问题了,再来看一个例子,然后再解释里边的细节:
#include <iostream>
#include <functional>
using namespace std;
class Test
{
public:
void output(int x, int y)
{
cout << "x: " << x << ", y: " << y << endl;
}
int m_number = 100;
};
int main(void)
{
Test t;
// 绑定类成员函数
function<void(int, int)> f1 =
bind(&Test::output, &t, placeholders::_1, placeholders::_2);
// 绑定类成员变量(公共)
function<int&(void)> f2 = bind(&Test::m_number, &t);
// 调用
f1(520, 1314);
f2() = 2333;
cout << "t.m_number: " << t.m_number << endl;
return 0;
}
x: 520, y: 1314
t.m_number: 2333
在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。
f1的类型是function<void(int, int)>,通过使用std::bind将Test的成员函数output的地址和对象t绑定,并转化为一个仿函数并存储到对象f1中。
使用绑定器绑定的类成员变量m_number得到的仿函数被存储到了类型为function<int&(void)>的包装器对象f2中,并且可以在需要的时候修改这个成员。其中int是绑定的类成员的类型,并且允许修改绑定的变量,因此需要指定为变量的引用,由于没有参数因此参数列表指定为void。
示例程序中是使用 function 包装器保存了 bind 返回的仿函数,如果不知道包装器的模板类型如何指定,可以直接使用 auto 进行类型的自动推导,这样使用起来会更容易一些。
8、POD类型
POD 是英文中 Plain Old Data 的缩写,翻译过来就是 普通的旧数据 。POD 在 C++ 中是非常重要的一个概念,通常用于说明一个类型的属性,尤其是用户自定义类型的属性。
在 C++11 中将 POD 划分为两个基本概念的合集,即∶平凡的(trivial) 和标准布局的(standard layout ) 。
“平凡” 类型
一个平凡的类或者结构体应该符合以下几点要求:
- 拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)。
- 平凡的默认构造函数就是说构造函数什么都不干
- 通常情况下,不定义类的构造函数,编译器就会为我们生成一个平凡的默认构造函数。
- 一旦定义了构造函数,即使构造函数不包含参数,函数体里也没有任何的代码,那么该构造函数也不再是"平凡"的。
- 关于析构函数也和上面列举的构造函数类似,一旦被定义就不平凡了。但是这也并非无药可救,使用 =default 关键字可以显式地声明默认的构造函数,从而使得类型恢复 “平凡化”
- 拥有平凡的拷贝构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)。
-
平凡的拷贝构造函数基本上等同于使用 memcpy 进行类型的构造。
-
同平凡的默认构造函数一样,不声明拷贝构造函数的话,编译器会帮程序员自动地生成。
-
可以显式地使用 = default 声明默认拷贝构造函数。
-
而平凡移动构造函数跟平凡的拷贝构造函数类似,只不过是用于移动语义。
-
- 拥有平凡的拷贝赋值运算符(trivial assignment operator)和移动赋值运算符(trivial move operator)。
- 不包含虚函数以及虚基类。
“标准布局” 类型
标准布局类型主要主要指的是类或者结构体的结构或者组合方式。
标准布局类型的类应该符合以下五点定义,最重要的为前两条:
- 所有非静态成员有相同 的访问权限(public,private,protected)。
-
//类成员拥有相同的访问权限(标准布局类型) class Base { public: Base() {} int a; int b; int c; };
-
- 在类或者结构体继承时,满足以下两种情况之一(真难记,吐了..........)∶
- 派生类中有非静态成员,基类中包含静态成员(或基类没有变量)。
- 基类有非静态成员,而派生类没有非静态成员。
-
struct Base { static int a;}; struct Child: public Base{ int b;}; // ok struct Base1 { int a;}; struct Child1: public Base1{ static int c;}; // ok struct Child2:public Base, public Base1 { static int d;); // ok struct Child3:public Base1{ int d;}; // error struct Child4:public Base1, public Child // error { static int num; };
非静态成员只要同时出现在派生类和基类间,即不属于标准布局。
对于多重继承,一旦非静态成员出现在多个基类中,即使派生类中没有非静态成员变量,派生类也不属于标准布局。
- 子类中第一个非静态成员的类型与其基类不同。
- 没有虚函数和虚基类。
- 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局,这是一个递归的定义。
对 POD 类型的判断
template <class T> struct std::is_trivial;
std::is_trivial 的成员 value 可以用于判断 T 的类型是否是一个平凡的类型(value 函数返回值为布尔类型)。除了类和结构体外,is_trivial 还可以对内置的标准类型数据(比如 int、float 都属于平凡类型)及数组类型(元素是平凡类型的数组总是平凡的)进行判断。
#include <iostream>
#include <type_traits>
using namespace std;
class A {};
class B { B() {} };
class C : B {};
class D { virtual void fn() {} };
class E : virtual public A { };
int main()
{
cout << std::boolalpha;
cout << "is_trivial:" << std::endl;
cout << "int: " << is_trivial<int>::value << endl;
cout << "A: " << is_trivial<A>::value << endl;
cout << "B: " << is_trivial<B>::value << endl;
cout << "C: " << is_trivial<C>::value << endl;
cout << "D: " << is_trivial<D>::value << endl;
cout << "E: " << is_trivial<E>::value << endl;
return 0;
}
非受限联合体
联合体又叫共用体,我将其称之为 union,它的使用方式和结构体类似,程序猿可以在联合体内部定义多种不同类型的数据成员,但是这些数据会共享同一块内存空间(也就是如果对多个数据成员同时赋值会发生数据的覆盖)。
在新的 C++11 标准中,任何非引用类型都可以成为联合体的数据成员,这样的联合体称之为非受限联合体(Unrestricted Union)。