C++新特性
https://subingwen.cn/cplusplus/
爱编程的大丙
〇、四大类型转换
一,稳定性和兼容性
1.原始字面量:
R “xxx(原始字符串)xxx”
2.long long 整形
隐式转换中,低等级整型需要转换为高等级整型,有符号的需要转换为无符号整形。
3.类成员的快速初始化-只针对非静态成员
对于静态成员初始化这样:类外初始化
struct Base
{
Base() {}
int a;
int b ;
static int c;
static const double d ;
static const char* const e ;
const static int f ;
};
int Base::c = 10;
const double Base::d = 3.14;
const char* const Base::e = "lufey";
非静态成员:看到如果使用花括号 {}的方式对类的非静态成员进行初始化,等号是可以省略不写的
class Test
{
private:
int a = 9;
int b = {5};
int c{12};
double array[4] = { 3.14, 3.15, 3.16, 3.17};
double array1[4] { 3.14, 3.15, 3.16, 3.17 };
string s1("hello"); // error不能使用小括号() 初始化对象,应该使用花括号{}
string s1{"helllo"};
};
类内部赋值和初始化列表:通过初始化列表指定的值会覆盖就地初始化时指定的值。
class Init
{
public:
Init(int x, int y, int z) :a(x), b(y), c(z) {}
int a = 1;
int b = 2;
int c = 3;
};
class intit
{
public:
intit(int a,int b):m_y(a),m_x(b){}
int m_y;
int m_x;
};
int main()
{
Init tmp(10, 20, 30);
cout << "a: " << tmp.a << ", b: " << tmp.b << ", c: " << tmp.c << endl;
return 0;
}
4.final和override
如果使用final修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了:
class Base
{
public:
virtual void test() final
{
cout << "Base class...";
}
};
使用final关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类。
class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class child final:public Base
{
public:
void test()
{
cout << "Child class...";
}
};
override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和final一样这个关键字要写到方法的后面。
使用了override关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误,提高了程序的正确性,降低了出错的概率。
class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
void test() override
{
cout << "Child class...";
}
};
class GrandChild : public Child
{
public:
void test() override
{
cout << "Child class...";
}
};
5.模板的优化
,那就是连续的两个右尖括号(>>)
模板在创建时需要指定类型
但对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随<>来实例化。
#include <iostream>
using namespace std;
template <typename T=int, T t=520>
class Test
{
public:
void print()
{
cout << "current value: " << t << endl;
}
};
int main()
{
Test<> t;
t.print();
Test<int, 1024> t1;
t1.print();
return 0;
}
#include <iostream>
using namespace std;
template <typename T=int> // C++98/03不支持这种写法, C++11中支持这种写法
void func(T t)
{
cout << "current value: " << t << endl;
}
int main()
{
func(100);
return 0;
}
当所有模板参数都有默认参数时,函数模板的调用如同一个普通函数。
如果可以推导出参数类型则使用推导出的类型
如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数
如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错。
注意:模板参数类型的自动推导是根据模板函数调用时指定的实参进行推断的,没有实参则无法推导
模板参数类型的自动推导不会参考函数模板中指定的默认参数。
#include <iostream>
#include <string>
using namespace std;
// 函数模板定义
template <typename T, typename U = char>
void func(T arg1 = 100, U arg2 = 100)
{
cout << "arg1: " << arg1 << ", arg2: " << arg2 << endl;
}
int main()
{
// 模板函数调用
func('a');
func(97, 'a');//可以自动推导加上默认类型能全部推到完,就不需要进行<>的书写
// func(); //编译报错,模板类型的推导不会参考模板中的默认参数
return 0;
}
6.数值类型和字符串之间的转换
使用to_string()方法可以非常方便地将各种数值类型转换为字符串类型,这是一个重载函,函数声明位于头文件中
字符串类型不就是本身嘛
数值转化字符串
#include <iostream>
#include <string>
using namespace std;
int main()
{
string pi = "pi is " + to_string(3.1415926);
string love = "love is " + to_string(5.20 + 13.14);
cout << pi << endl;
cout << love << endl;
return 0;
}
字符串转化为数值
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str1 = "45";
string str2 = "3.14159";
string str3 = "9527 with words";
string str4 = "words and 2";
int myint1 = std::stoi(str1);
float myint2 = std::stof(str2);
int myint3 = std::stoi(str3);
// 错误: 'std::invalid_argument'
// int myint4 = std::stoi(str4);
cout << "std::stoi(\"" << str1 << "\") is " << myint1 << endl;
cout << "std::stof(\"" << str2 << "\") is " << myint2 << endl;
cout << "std::stoi(\"" << str3 << "\") is " << myint3 << endl;
// cout << "std::stoi(\"" << str4 << "\") is " << myint4 << endl;
}
注意:函数使用时均有:
std::stoi(stof,stod)//且只有本来就是数字的才可以转换,并不是进行ASCII码进行转换
std::stoi("45") is 45
std::stof("3.14159") is 3.14159
std::stoi("9527 with words") is 9527
7.静态断言
断言
strcpy_s
是 C 和 C++ 中的 strcpy
函数的安全版本。它用于将一个字符串从一个位置复制到另一个位置。 _s
后缀表示这个函数执行额外的检查,以防止缓冲区溢出。
strcpy_s
的函数签名通常如下所示:
cCopy code
errno_t strcpy_s(char *restrict dest, rsize_t destsz, const char *restrict src);
下面是每个参数的含义:
dest
:指向目标缓冲区的指针,字符串将被复制到这里。destsz
:目标缓冲区的大小(以字节为单位)。src
:指向要复制的以 null 结尾的字符串的指针。
在你的例子中:
cCopy code
strcpy_s(buf, 16, "hello, world!");
buf
是目标缓冲区。16
是目标缓冲区的大小,表示buf
可以容纳多达 16 个字符,包括空终止符。"hello, world!"
是要复制到buf
中的字符串。
如果目标缓冲区的大小 (destsz
) 太小以容纳源字符串和空终止符,或者 dest
或 src
是空指针,或者源字符串没有以 null 结尾,strcpy_s
将返回一个非零错误代码(通常表示错误),并且不会执行复制。这有助于防止缓冲区溢出漏洞。
#include<string>
#include <iostream>
#include<cassert>
using namespace std;
//创建指定大小的数组
char* createArray(int size)
{
//通过断言判断数组大小是否大于0
assert(size > 0);//必须大于0,否则程序中断
char* array = new char[size];
return array;
}
int main()
{
char* buf = createArray(20);
strcpy_s(buf, 16, "hello world!");
cout << "buf = " << buf << endl;
delete[] buf;
return 0;
}
注意:
assert是一个运行时断言,也就是说它只有在程序运行时才能起作用
静态断言
#include<iostream>
using namespace std;
int main()
{
static_assert(sizeof(long)==4,"错误,不是32位平台。。。。");
cout << "64bit window 指针大小:" << sizeof(char*) << endl;
cout << "64bit windows long 大小:" << sizeof(long) << endl;
return 0;
}
静态断言static_assert,所谓静态就是在编译时就能够进行检查的断言,使用时不需要引用头文件。静态断言的另一个好处是,可以自定义违反断言时的错误提示信息。静态断言使用起来非常简单,它接收两个参数:
注意:
由于静态断言的表达式是在编译阶段进行检测,所以在它的表达式中不能出现变量,也就是说这个表达式必须是常量表达式。
8.noexcept
#include<iostream>
using namespace std;
int main()
{
try {
throw - 1;
}
catch (int e)
{
cout << "int exception,value:" << e << endl;
}
cout << "This is ok" << endl;
return 0;
}
try {
: 这是一个异常处理的起始块。在这个块中,我们将包含可能会引发异常的代码。throw - 1;
: 这是一个抛出异常的语句。在这里,我们抛出一个整数类型的异常,值为 -1。catch (int e)
: 这是一个异常捕获的块,用于捕获并处理抛出的异常。在这个块中,我们指定了捕获类型为整数类型,并将捕获的异常存储在e
中。cout << "int exception, value: " << e << endl;
: 这行代码用于输出捕获到的异常信息。在这里,我们打印出 "int exception, value: ",并将异常的值e
输出到标准输出流中。cout << "This is ok" << endl;
: 这行代码用于输出 “This is ok”,表示程序正常执行,没有发生未捕获的异常。return 0;
: 这是主函数的返回语句,表示程序正常结束,并将返回值设置为 0。
综上所述,这段代码的作用是在 try
块中抛出一个整数类型的异常,然后在 catch
块中捕获并处理该异常,输出异常的值。最后,程序正常结束并输出 “This is ok”。
注意:
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋。
#include<iostream>
#include<string>
using namespace std;
struct MyException
{
MyException(string s):msg(s){}
string msg;
};
double divisionMethod(int a, int b)throw(MyException,int)
{
if (b == 0)
{
throw MyException("division by zero");
}
return a / b;
}
int main()
{
try {
double v = divisionMethod(100, 0);
cout << "value :" << v << endl;
}
catch (int e)
{
cout << "catch except:" << e << endl;
}
catch (MyException e)
{
cout << "catch except:" << e.msg << endl;
}
cout << "This is ok" << endl;
return 0;
}
第7行代码在divisionMethod函数后添加了throw异常接口声明,其参数表示可以抛出的异常类型,分别为int 和MyException 类型。
struct MyException
{
MyException(string s) :msg(s) {}
string msg;
};
double divisionMethod(int a, int b)
{
if (b == 0)
{
throw MyException("division by zero!!!");
// throw 100;
}
return a / b;
}
第7行代码在divisionMethod 没有添加异常接口声明,表示在该函数中可以抛出任意类型的异常。
struct MyException
{
MyException(string s) :msg(s) {}
string msg;
};
double divisionMethod(int a, int b) throw()
{
if (b == 0)
{
cout << "division by zero!!!" << endl;
}
return a / b;
}
第7行代码在divisionMethod 函数后添加了throw异常接口声明,其参数列表为空**,表示该函数不允许抛出异常。**
最新:
替代了:动态异常声明 throw(MyException, int),该声明指出了divisionMethod可能抛出的异常的类型。事实上,该特性很少被使用,因此在C++11中被弃用了 ,而表示函数不会抛出异常的动态异常声明 throw() 也被新的 noexcept 异常声明所取代。
noexcept 形如其名,表示其修饰的函数不会抛出异常 。不过与 throw()
动态异常声明不同的是,在 C++11 中如果 noexcept 修饰的函数抛出了异常,编译器可以选择直接调用 std::terminate() 函数来终止程序的运行,这比基于异常机制的 throw() 在效率上会高一些
。这是因为异常机制会带来一些额外开销,比如函数抛出异常,会导致函数栈被依次地展开(栈解旋),并自动调用析构函数释放栈上的所有对象。
改写:
#include <iostream>
#include <string>
using namespace std;
struct MyException
{
MyException(string s) : msg(s) {}
string msg;
};
double divisionMethod(int a, int b) noexcept(false)
{
if (b == 0)
{
throw MyException("division by zero");
}
return a / b;
}
int main()
{
try {
double v = divisionMethod(100, 0);
cout << "value: " << v << endl;
}
catch (int e)
{
cout << "catch except: " << e << endl;
}
catch (MyException e)
{
cout << "catch except: " << e.msg << endl;
}
cout << "This is ok" << endl;
return 0;
}
从语法上讲,noexcept 修饰符有两种形式:
- 简单地在函数声明后加上 noexcept 关键字
- 可以接受一个常量表达式作为参数,如下所示∶
`double division(int a,int b)noexcept(常量表达式)`
常量表达式的结果会被转换成一个bool类型的值:
- 值为 true,表示函数不会抛出异常
- 值为 false,表示有可能抛出异常这里
- 不带常量表达式的noexcept相当于声明了noexcept(true),即不会抛出异常。(如果不需要进行异常规范声明,也可以省略
noexcept
关键字,函数会被默认视为noexcept(true)
。)
优点:
省去了异常类型的声明,只需要在捕获部分进行处理就行
二、易学和易用性
1.自动类型推导
auto
1.1推导规则
使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。
- 当变量不是指针或者引用类型时,推导的结果中不会保留const、volatile关键字
- 当变量是指针或者引用类型时,推导的结果中会保留const、volatile关键字
int tmp = 250;
const auto a1 = tmp;
auto a2 = a1;
const auto &a3 = tmp;
auto &a4 = a3;
变量a1的数据类型为 const int,因此auto关键字被推导为 int类型
变量a2的数据类型为 int,但是a2没有声明为指针或引用因此 const属性被去掉, auto被推导为 int
变量a3的数据类型为 const int&,a3被声明为引用因此 const属性被保留,auto关键字被推导为 int类型
变量a4的数据类型为 const int&,a4被声明为引用因此 const属性被保留,auto关键字被推导为 const int类型
1.2推导限制
- 不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
int func(auto a, auto b) // error
{
cout << "a: " << a <<", b: " << b << endl;
}
2.不能用于类的非静态成员变量的初始化
class Test
{
auto v1 = 0; // error
static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化
static const auto v3 = 10; // ok
}
注意:只有类的静态常量成员才可以在类内进行初始化用auto
3.不能使用auto关键字定义数组
int func()
{
int array[] = {1,2,3,4,5}; // 定义数组
auto t1 = array; // ok, t1被推导为 int* 类型,数组名为首地址
auto t2[] = array; // error, auto无法定义数组
auto t3[] = {1,2,3,4,5};; // error, auto无法定义数组
}
4.无法使用auto推导出模板参数
template <typename T>
struct Test{}
int func()
{
Test<double> t;
Test<auto> t1 = t; // error, 无法推导出模板类型
return 0;
}
也就是说auto用于自动推导函数的返回类型或者在函数体内定义变量时进行类型推导
1.3 auto的应用
1.用于STL的容器遍历。
#include<map>
int main()
{
map<int,string>person;
for(auto it = person.begin();it!=person.end();it++)
{
}
return 0;
}
2.用于泛型编程,在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:
#include<iostream>
#include<string>
using namespace std;
class T1
{
public:
static int get()
{
return 10;
}
};
class T2
{
public:
static string get()
{
return "hello world";
}
};
template<class A>
void func(void)
{
auto val = A::get();
cout << "val: " << val << endl;
}
#if 0
template <class A, typename B> // 添加了模板参数 B
void func(void)
{
B val = A::get();
cout << "val: " << val << endl;
}
#endif
int main()
{
func<T1>();//func<T1,int>()
func<T2>();//func<T2,string>()
return 0;
}
decltype : declare type(声明类型),decltype的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。
int a = 10;
decltype(a) b = 99; // b -> int
decltype(a+3.14) c = 52.13; // c -> double
decltype(a+b*c) d = 520.1314; // d -> double
decltype推导的表达式可简单可复杂,在这一点上auto是做不到的,auto只能推导已初始化的变量类型。
2.1推导规则
1.表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用decltype推导出的类型和表达式的类型是一致的。
#include<iostream>
#include<string>
using namespace std;
class Test
{
public:
string text;
static const int value = 100;
};
int main()
{
int x = 99;
const int& y = x;
decltype(x) a = x;
decltype(y) b = x;
decltype(Test::value) c = 0;
Test t;
decltype(t.text) d = "hello world";
return 0;
}
2.表达式是函数调用,使用decltype推导出的类型和函数返回值一致。
class Test{...};
//函数声明
int func_int(); // 返回值为 int
int& func_int_r(); // 返回值为 int&
int&& func_int_rr(); // 返回值为 int&&
const int func_cint(); // 返回值为 const int
const int& func_cint_r(); // 返回值为 const int&
const int&& func_cint_rr(); // 返回值为 const int&&
const Test func_ctest(); // 返回值为 const Test
//decltype类型推导
int n = 100;
decltype(func_int()) a = 0;
decltype(func_int_r()) b = n;
decltype(func_int_rr()) c = 0;
decltype(func_cint()) d = 0; //变量d被推导为 int类型
decltype(func_cint_r()) e = n;
decltype(func_cint_rr()) f = 0;
decltype(func_ctest()) g = Test();
函数 func_cint() 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据),对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量d的类型为 int 而不是 const int。
const int func_cint();
返回的是常量整数,也就是一个无法修改的值。也就是说右值不能接收赋值操作或者传递给非常量引用参数的操作,相当于1,2,3
3.表达式是一个左值,或者被括号( )包围,使用 decltype推导出的是表达式类型的引用(如果有const、volatile限定符不能忽略)。
#include <iostream>
#include <vector>
using namespace std;
class Test
{
public:
int num;
};
int main() {
const Test obj;
//带有括号的表达式
decltype(obj.num) a = 0;//int
decltype((obj.num)) b = a;//带括号,实例化有const:所以const int&
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0;//n+m是右值int
decltype(n = n + m) d = n;//n是左值可以寻址,int&
return 0;
}
2.2 decltype应用
#include<iostream>
#include<string>
#include<list>
using namespace std;
template<class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); m_it++)
{
cout << *m_it << " ";
}
cout << endl;
}
private:
m_it;//迭代器类型的确定问题
};
int main()
{
const list<int> lst;
Container<const list<int>> obj;
obj.func(lst);
return 0;
}
关于迭代器变量一共有两种类型:
只读(T::const_iterator)
读写(T::iterator)
所以无法确定m_it类型,只能用decltype(T().begin()),创造一个匿名对象来进行类型推导
3.返回类型后置
#include <iostream>
using namespace std;
// R->返回值类型, T->参数1类型, U->参数2类型
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
}
int main()
{
int x = 520;
double y = 13.14;
// auto z = add<decltype(x + y), int, double>(x, y);
auto z = add<decltype(x + y)>(x, y); // 简化之后的写法
cout << "z: " << z << endl;
return 0;
}
关于返回值,从上面的代码可以推断出和表达式 t+u的结果类型是一样的,因此可以通过通过decltype进行推导,关于模板函数的参数t和u可以通过实参自动推导出来,因此在程序中就也可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的
template <typename T, typename U>
decltype(t+u) add(T t, U u)
{
return t + u;
}
decltype 中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在,有点心急了。
后置语法:
auto func(参数1, 参数2, ...) -> decltype(参数表达式)
auto 会追踪 decltype() 推导出的类型
#include<iostream>
#include<string>
#include<list>
using namespace std;
template<class T,class U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
int main()
{
int x = 1;
double y = 1.2;
auto z = add<int, double>(x, y);//简写:auto z = add(x,y)
return 0;
}
2.基于范围的for循环
在基于范围的for循环中,不需要再传递容器的两端,循环会自动以容器为范围展开,并且循环中也屏蔽掉了迭代器的遍历细节,直接抽取容器中的元素进行运算,使用这种方式进行循环遍历会让编码和维护变得更加简便
for (declaration : expression)
{
// 循环体
}
declaration表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中
expression是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
int main()
{
vector<int> t{1, 2, 3, 5, 6, 6};
for (auto value : t)
{
cout << value << " ";
}
cout << endl;
return 0;
}
将容器中遍历的当前元素拷贝到了声明的变量value中,因此无法对容器中的元素进行写操作,如果需要在遍历过程中修改元素的值,需要使用引用。
int main(void)
{
vector<int> t{ 1,2,3,4,5,6 };
cout << "遍历修改之前的容器: ";
for (auto &value : t)
{
cout << value++ << " ";
}
cout << endl << "遍历修改之后的容器: ";
for (auto &value : t)
{
cout << value << " ";
}
cout << endl;
return 0;
}
对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用const定义保存元素数据的变量,在定义的时候建议使用const auto &,这样相对于const auto效率要更高一些
for (const auto &value : t)
{
cout << value << " ";
}
cout << endl;
使用细节
2.1关系型容器
#include<iostream>
#include<string>
#include<map>
using namespace std;
int main()
{
map<int, string> m {
{1,"ji"}, {2,"evv"}, {3,"vev"}, {4,"vesve"}
};
for (auto& it : m)
{
cout << it.first << " " << it.second << endl;
}
cout << endl;
for (auto it = m.begin(); it != m.end(); it++)
{
cout << it->first << " " << it->second << endl;
}
return 0;
}
1.使用普通的for循环方式(基于迭代器)遍历关联性容器, auto自动推导出的是一个迭代器类型,需要使用迭代器的方式取出元素中的键值对(和指针的操作方法相同):
it->first
it->second
2.使用基于范围的for循环遍历关联性容器,auto自动推导出的类型是容器中的value_type,相当于一个对组(std::pair)对象,提取键值对的方式如下:
it.first
it.second
2.2元素只读
1.对应set容器来说,内部元素都是只读的,这是由容器的特性决定的,因此在for循环中auto&会被视为const auto & 。
set<int> st{ 1,2,3,4,5,6 };
for (auto &item : st)
{
cout << item++ << endl; // error, 不能给常量赋值
}
2.在遍历关联型容器时也会出现同样的问题,基于范围的for循环中,虽然可以得到一个std::pair引用,但是我们是不能修改里边的first值的,也就是key值。
for (auto& item : m)
{
// item.first 是一个常量
cout << "id: " << item.first++ << ", name: " << item.second << endl; // error
}
2.3访问次数
#include<iostream>
#include<vector>
using namespace std;
vector<int> v{1, 2, 3, 4, 5};
vector<int> getRange()
{
cout << "get vector range..." << endl;
return v;
}
int main()
{
for (auto val : getRange())
{
cout << val << " ";
}
cout << endl;
return 0;
}
**注意:**范围-based for 循环会使用 getRange()
返回的引用,调用 begin()
和 end()
方法来获取容器的开始和结束迭代器,然后通过这些迭代器来遍历容器中的元素。所以,在这种情况下,范围-based for 循环是可以捕捉到 getRange()
函数里面的 v
的。
结论:
对应基于范围的for循环来说,冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围,基于这个范围直接进行遍历。如果是普通的for循环,在每次迭代的时候都需要判断是否已经到了结束边界。
3.nullptr
源码是C++程序NULL就是0
C程序NULL表示(void*)0
C++ 中将 NULL 定义为字面常量 0,并不能保证在所有场景下都能很好的工作,比如,函数重载时,NULL 和 0 无法区分:
#include <iostream>
using namespace std;
void func(char *p)
{
cout << "void func(char *p)" << endl;
}
void func(int p)
{
cout << "void func(int p)" << endl;
}
int main()
{
func(NULL); // 想要调用重载函数 void func(char *p)
func(250); // 想要调用重载函数 void func(int p)
return 0;
}
在C++中NULL和0是等价的,所以输出结果
void func(int p)
void func(int p)
C++11 标准并没有对 NULL 的宏定义做任何修改,而是另其炉灶,引入了一个新的关键字nullptr。nullptr 专用于初始化空类型指针,不同类型的指针变量都可以使用 nullptr 来初始化:
int* ptr1 = nullptr;
char* ptr2 = nullptr;
double* ptr3 = nullptr;
nullptr 隐式转换成 int**、char** 以及 double* 指针类型。
解决重载:
#include <iostream>
using namespace std;
void func(char *p)
{
cout << "void func(char *p)" << endl;
}
void func(int p)
{
cout << "void func(int p)" << endl;
}
int main()
{
func(nullptr);//解决重载问题
func(250);
return 0;
}
**nullptr 无法隐式转换为整形,但是可以隐式匹配指针类型。**在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。
4.lambda表达式
lambda表达式有如下的一些优点:
-
声明式的编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象。
-
简洁:避免了代码膨胀和功能分散,让开发更加高效。
-
在需要的时间和地点实现功能闭包,使程序更加灵活。
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式简单归纳如下:
[capture](params) opt -> ret {body;};
其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。
捕获列表[]: 捕获一定范围内的变量
参数列表(): 和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。
opt 选项, 不需要可以省略
mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();
返回值类型:在C++11中,lambda表达式的返回值是通过返回值后置语法来定义的。
函数体:函数的实现,这部分不能省略,但函数体可以为空。
捕获列表
lambda表达式的捕获列表可以捕获一定范围内的变量,具体使用方式如下:
- [] - 不捕捉任何变量
- [&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
- [=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)****
拷贝的副本在匿名函数体内部是只读的 - [=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo
- [bar] - 按值捕获 bar 变量, 同时不捕获其他变量
- [&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
- [this] - 捕获当前类中的this指针
让lambda表达式拥有和当前类成员函数同样的访问权限
如果已经使用了 & 或者 =, 默认添加此选项
class Test
{
public:
void output(int x, int y)
{
//auto x1 = []() {return m_number;};
auto x2 = [=]() {return m_number + x + y; };
auto x3 = [&]() {return m_number; };
auto x4 = [this]() {return m_number; };
//auto x5 = [this]() {return m_number + x + y; };
auto x6 = [this, x, y]() {return m_number + x + y; };
auto x7 = [this] {return m_number++; };
}
int m_number = 10;
};
x1:错误,没有捕获外部变量,不能使用类成员 m_number
x2:正确,以值拷贝的方式捕获所有外部变量
x3:正确,以引用的方式捕获所有外部变量
x4:正确,捕获this指针,可访问对象内部成员
x5:错误,捕获this指针,可访问类内部成员,没有捕获到变量x,y,因此不能访问。
x6:正确,捕获this指针,x,y
x7:正确,捕获this指针,并且可以修改对象内部变量的值
int main()
{
int a = 10; int b = 20;
//auto f1 = []() {return a; };
auto f2 = [&]() {return a++; };
auto f3 = [=]() {return a; };
//auto f4 = [=]() {return a++; };
//auto f5 = [a]() {return a + b; };
auto f6 = [a, &b]() {return a + (b++); };
auto f7 = [=, &b]() {return a + (b++); };
return 0;
}
f1:错误,没有捕获外部变量,因此无法访问变量 a
f2:正确,使用引用的方式捕获外部变量,可读写
f3:正确,使用值拷贝的方式捕获外部变量,可读
f4:错误,使用值拷贝的方式捕获外部变量,可读不能写
f5:错误,使用拷贝的方式捕获了外部变量a,没有捕获外部变量b,因此无法访问变量b
f6:正确,使用拷贝的方式捕获了外部变量a,只读,使用引用的方式捕获外部变量b,可读写
f7:正确,使用值拷贝的方式捕获所有外部变量以及b的引用,b可读写,其他只读
在匿名函数内部,需要通过lambda表达式的捕获列表控制如何捕获外部变量,以及访问哪些变量。默认状态下lambda表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获。
返回值
一般情况下,不指定lambda表达式的返回值,编译器会根据return语句自动推导返回值的类型,但需要注意的是labmda表达式不能通过列表初始化自动推导出返回值类型。
// ok,可以自动推导出返回值类型
auto f = [](int i)
{
return i;
}
// error,不能推导出返回值类型
auto f1 = []()
{
return {1, 2}; // 基于列表初始化推导返回值,错误
}
函数本质
使用lambda表达式捕获列表捕获外部变量,如果希望去修改按值捕获的外部变量,那么应该如何处理呢?这就需要使用mutable选项,被mutable修改是lambda表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的外部变量的只读(const)属性。
int a =0;
auto f1 = [=]()mutable{return a++;};
lambda表达式的类型在C++11中会被看做是一个带operator()的类,即仿函数。
按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量值的。
mutable选项的作用就在于取消operator()的const属性。
因为lambda表达式在C++中会被看做是一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式:
#include<iostream>
#include<functional>
using namespace std;
int main()
{
//包装可调用函数
std::function<int(int)>f1 = [](int a) {return a; };
//绑定可调用函数
std::function<int(int)>f2 = bind([](int a) {return a; }, placeholders::_1);
//函数调用
cout<<f1(5)<<endl;
cout << f2(2) << endl;
return 0;
}
对于没有捕获任何变量的lambda表达式,还可以转换成一个普通的函数指针
using func_ptr = int(*)(int);
func_ptr f = [](int a) { return a; };
//函数调用
f(1234);
三、通用性能的提升
1.常量表达式修饰符-constexpr
const关键字,从功能上来说这个关键字有双重语义:变量只读,修饰常量
void func(const int num)
{
const int count = 24;
int array[num]; // error,num是一个只读变量,不是常量
int array1[count]; // ok,count是一个常量
int a1 = 520;
int a2 = 250;
const int& b = a1;
b = a2; // error
a1 = 1314;
cout << "b: " << b << endl; // 输出结果为1314
}
函数void func(const int num)的参数num表示这个变量是只读的,但不是常量,因此使用int array[num]; 这种方式定义一个数组,编译器是会报错的,提示num不可用作为常量来使用。
const int count = 24;中的count却是一个常量,因此可以使用这个常量来定义一个静态数组。
变量只读不等价于常量
二者是两个概念不能混为一谈,分析一下这句测试代码
const int& b = a1;:
b是一个常量的引用,所以b引用的变量是不能被修改的,也就是说b = a2; 这句代码语法是错误的。
在const对于变量a1是没有任何约束的,a1的值变了b的值也就变了
引用b是只读的,但是并不能保证它的值是不可改变的,也就是说它不是常量。
1.constexpr
constexpr,这个关键字是用来修饰常量表达式的。
常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。
C++ 程序从编写完毕到执行分为四个阶段:预处理、 编译、汇编和链接4个阶段,得到可执行程序之后就可以运行了。需要额外强调的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
在使用中建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达**“常量”语义的场景都使用 constexpr**。
const int m = f(); // 不是常量表达式,m的值只有在运行时才会获取。
const int i=520; // 是一个常量表达式
const int j=i+1; // 是一个常量表达式
constexpr int i=520; // 是一个常量表达式
constexpr int j=i+1; // 是一个常量表达式
对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。
constexpr struct Test
{
int id;
int num;
};
上述程序无效,定义结构体/类常量对象,实例化时使用
struct Test
{
int id;
int num;
};
int main()
{
constexpr Test t{ 1, 2 };
constexpr int id = t.id;
constexpr int num = t.num;
// error,不能修改常量
t.num += 100;
cout << "id: " << id << ", num: " << num << endl;
return 0;
}
2.常量表达式函数
为了提高C++程序的执行效率,我们可以将程序中值不需要发生变化的变量定义为常量,也可以使用constexpr修饰函数的返回值,这种函数被称作常量表达式函数,这些函数主要包括以下几种:普通函数/类成员函数、类的构造函数、模板函数。
constexpr并不能修改任意函数的返回值,使这些函数成为常量表达式函数,必须要满足以下几个条件:
1.函数必须要有返回值,并且return 返回的表达式必须是常量表达式。
//没有返回值
constexpr void func1()
{
int a = 100;
cout << "a: " << a << endl;
}
// error,不是常量表达式函数
constexpr int func1()
{
int a = 100;//修改 constexpr int a = 100;
return a;
}
2.整个函数的函数体中,不能出现非常量表达式之外的语句(using 指令、typedef 语句以及 static_assert 断言、return语句除外)。
// error
constexpr int func1()
{
constexpr int a = 100;
constexpr int b = 10;
//for循环是一个非法操作
for (int i = 0; i < b; ++i)
{
cout << "i: " << i << endl;
}
return a + b;
}
// ok
constexpr int func2()
{
using mytype = int;
constexpr mytype a = 100;
constexpr mytype b = 10;
constexpr mytype c = a * b;
return c - (a + b);
}
修饰模板函数
constexpr 可以修饰函数模板,但由于模板中类型的不确定性,因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。
string 类型就是:const char*
#include<iostream>
#include<vector>
using namespace std;
struct Person
{
const char* name;
int age;
};
template<class T>
constexpr T display(T t)
{
return t;
}
int main()
{
struct Person p{"luffy",19 };
struct Person ret = display(p);//参数p是变量,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的
constexpr int ret1 = display(250);//参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的
constexpr struct Person p1{"luffy",19 };
constexpr struct Person p2 = display(p1);//参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的
return 0;
}
在上面示例程序中定义了一个函数模板 display(),**但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求。
修饰构造函数
如果想用直接得到一个常量对象(只需要修饰构造函数就行),也可以使用constexpr修饰一个构造函数,这样就可以得到一个常量构造函数了。常量构造函数有一个要求:构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。
#include<iostream>
#include<vector>
using namespace std;
struct Person
{
//构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。
constexpr Person(const char* p,int age):name(p),age(age){}
const char* name;
int age;
};
int main()
{
constexpr struct Person p1("luffy", 19);
return 0;
}
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;
}
有重复代码,使用委托构造
#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初始化了,也不能初始化m_min
Test(int max, int min) : Test(max), m_max(max),m_min(min)
{
this->m_min = min > 0 && min < max ? min : 1;
}
有继承关系的,也可以调用代理构造函数
#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;
}
但是还是会有冗余代码,使用继承构造
如果在子类中隐藏了父类中的同名函数,也可以通过using的方式在子类中使用基类中的这些父类函数:
隐藏的函数加上构造函数,隐藏的函数就是子类中重名的函数
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.右值引用
左值:lvalue(local value )
右值:rvalue(read value)
左值是指存储在内存中、有明确存储地址(可取地址)的数据;
右值是指可以提供数据值的数据(不可取地址);
可以对表达式取地址(&)就是左值,否则为右值
int a = 520;
int b = 1314;
a = b;
一般情况下,位于=前的表达式为左值,位于=后边的表达式为右值。也就是说例子中的a, b为左值,520,1314为右值。a=b是一种特殊情况,在这个表达式中a, b都是左值,因为变量b是可以被取地址的,不能视为右值。
右值可以分为两种
一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):
- 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
- **将亡值:**与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等。
int value = 520;
在上面的语句中,value是左值,520是字面量也就是右值。其中value可以被引用,但是520就不行了,因为字面量都是右值。
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论**声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。**通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
#include<iostream>
using namespace std;
int&& value = 520;//520是纯右值,value是右值引用,所以正确
class Test
{
public:
Test()
{
cout << "123" << endl;
}
Test(const Test& a)
{
cout << "456" << endl;
}
};
Test getObj()
{
return Test();
}
int main()
{
int a1;
//int&& a2 = a1; a1虽然卸载了右边,但是是左值,&&是右值引用,不合法
//Test& t = getObj();getObj()返回的是一个临时对象,在getObj()函数结束调用时就会被销毁,是一个右值(将亡值)
Test&& t = getObj();//正确
const Test& t = getObj();//常量左值引用是一个万能引用类型
return 0;
}
Test getObj()
{
return Test();
}
最后一个 return Test();
返回的确实是一个 Test 类型的匿名对象。在这种情况下,Test()
是一个临时对象,因为它没有被赋给任何变量,也没有被命名。当函数返回时,这个临时对象会被复制到调用方的对象中,这是通过调用 Test(const Test& a)
拷贝构造函数完成的。
Test globalTest;
Test& getObj()
{
return globalTest;
}
想要返回一个对象的引用,你需要确保返回的对象在函数结束后仍然存在。通常情况下,你可以通过在函数外创建对象,然后返回它的引用来实现这一点
-
在上面的例子中int&& value = 520;里面520是纯右值,value是对字面量520这个右值的引用。
-
在int &&a2 = a1;中a1虽然写在了=右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
-
在Test& t = getObj()这句代码中语法是错误的,右值不能给普通的左值引用赋值。
-
在Test && t = getObj();中getObj()返回的临时对象被称之为将亡值,t是这个将亡值的右值引用。
-
const Test& t = getObj()这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。
性能优化(最新vs不支持,已经优化)
在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
#include <iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
~Test()
{
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
construct: my name is jerry
copy construct: my name is tom
t.m_num: 100
通过输出的结果可以看到调用Test t = getObj();的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象t,在getObj()函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。**如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高C++应用程序的性能。**
#include <iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
// 添加移动构造函数,移动构造是一个浅拷贝
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;//防止野指针
cout << "move construct: my name is sunny" << endl;
}
~Test()
{
delete m_num;
cout << "destruct Test class ..." << endl;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...
通过修改,在上面的代码给Test类添加了移动构造函数(参数为右值引用类型),这样在进行Test t = getObj();操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。
在测试程序中getObj()的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果=右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。
总结:
对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。
&&特性
在C++中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下 &&被称作未定的引用类型。另外还有一点需要额外注意const T&&表示一个右值引用,不是未定引用类型。
template<class T>
void f(T && param)
void f1(const T&& param)
f(10);
int x = 10;
f(x);
f1(x);//error
f1(10);//10是右值
注意: &&被称作未定的引用类型
在上面的例子中函数模板进行了自动类型推导,需要通过传入的实参来确定参数param的实际类型。
-
第4行中,对于f(10)来说传入的实参10是右值,因此T&&表示右值引用
-
第6行中,对于f(x)来说传入的实参是x是左值,因此T&&表示左值引用
-
第7行中,f1(x)的参数是const T&&不是未定引用类型,不需要推导,本身就表示一个右值引用
int main()
{
int x = 520,y = 1314;
auto&& v1 = x;
auto&& v2 = 250;
decltype(x)&& v3 = y;//error
return 0;
}
第4行中 auto&&表示一个整形的左值引用
第5行中 auto&&表示一个整形的右值引用
第6行中**decltype(x)&&等价于int&&**是一个右值引用不是未定引用类型,y是一个左值,不能使用左值初始化一个右值引用类型。
由于上述代码中存在T&&或者auto&&这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在C++11中引用折叠的规则如下:
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
int&& a1 = 5;
auto&& bb = a1;
auto&& bb1 = 5; //5是右值,所以bb1是右值引用
int a2 = 5;
int &a3 = a2;
auto&& cc = a3;
auto&& cc1 = a2;
const int& s1 = 100;
const int&& s2 = 100;
auto&& dd = s1;
auto&& ee = s2; //右值引用推导出ee是左值引用
const auto&& x = 5; //右值引用
要把自动类型推导和赋值分开,因为濒危确定auto是何种类型,所以在使用时不需要考虑是否能复制的问题
#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(k);
}
int main()
{
int i = 520;
printValue(i);
printValue(1314);
forward(250);
return 0;
}
l-value: 520
r-value: 1314
l-value: 250
根据测试代码可以得知,编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue),函数forward()接收的是一个右值,但是在这个函数中调用函数printValue()时,参数k变成了一个命名对象,编译器会将其当做左值来处理。
最后总结一下关于&&的使用:
-
左值和右值是独立于他们的类型的,右值引用类型可能是左值也可能是右值。
-
编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。
-
auto&&或者函数参数类型自动推导的T&&是一个未定的引用类型,它可能是左值引用也可能是右值引用类型,这取决于初始化的值类型(上面有例子)。
-
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,其余都是左值引用类型。
整体而言:
浅拷贝
只对对象中的数据成员进行简单的赋值(值拷贝);默认拷贝构造函数是浅拷贝;
深拷贝
对于对象中动态成员,重新动态分配空间,再把内容复制到新的内存空间;
拷贝构造函数
参数是一个左值引用。系统自定义的拷贝构造函数是浅拷贝,自定义拷贝构造一般为深拷贝;**使用默认拷贝构造函数时,如果类对象中有指针成员,会因为新对象的指针指向的地址与被拷贝对象的指针指向的地址相同,造成指针被delete两次的错误;**深拷贝会重新开辟一块内存空间,为了避免拷贝开销,引入移动构造函数。 类的对象需要拷贝时,拷贝构造函数将会被调用。会调用拷贝构造函数的情况:
- 一个对象以值传递的方式传入函数体 ;
- 一个对象以值传递的方式从函数返回 ;
- 一个对象需要通过另外一个对象进行初始化。
移动语义
以移动而非深拷贝的方式初始化含有指针成员的类对象;也就是将其它之后不需要的对象拥有的内存移为己用。
移动构造函数
参数是一个右值引用。移动构造函数首先将传递参数的内存地址空间接管,源对象的数据成员分配给默认值(指针设置为nullptr),并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值。
std::move() 函数
将一个左值强制转化为右值引用,以用于移动语义。将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。通过std::move(),可以避免不必要的拷贝操作。
4.转移和完美转发
1.Move
在C++11添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助std::move()函数,**使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,**将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
从实现上讲,std::move基本等同于一个类型转换:
static_cast<T &&>(value)
函数原型
template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
class Test
{
public:
Test(){}
......
}
int main()
{
Test t;
Test && v1 = t; // error
Test && v2 = move(t); // ok,使用move()函数将左值转换为了右值,这样就可以初始化右值引用了
return 0;
}
假设一个临时容器很大,并且需要将这个容器赋值给另一个容器,就可以执行如下操作:
list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls; // 需要拷贝, 效率低
list<string> ls2 = move(ls);
针对于临时对象的,如果不使用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用move()就可以非常方便的进行数据所有权的转移。
另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义(接收右值)的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。
2.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);//注意move和forward都有小括号()
当T为左值引用类型时(T&,可以取地址),t将被转换为T类型的左值
当T不是左值引用类型时(除T&类型之外的都是),t将被转换为T类型的右值
#include<iostream>
using namespace std;
template<typename T>
void printValue(T& t)
{
cout << "左值:" << t << endl;
}
template<typename T>
void printValue(T && t)
{
cout << "右值:" << 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 = 134;
testForward(num);
testForward(forward<int>(num));
testForward(forward<int&>(num));
testForward(forward<int&&>(num));
return 0;
}
testForward(520);
函数的形参为未定引用类型T&&,实参为右值,初始化后被推导为一个右值引用
printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
printValue(forward(v));forward的模板参数为右值引用,最终得到一个右值,实参为``右值`
testForward(num)
函数的形参为未定引用类型T&&,实参为左值,初始化后被推导为一个左值引用
printValue(v);实参为左值
printValue(move(v));通过move将左值转换为右值,实参为右值
printValue(forward(v));forward的模板参数为左值引用,最终得到一个左值引用,实参为左值
testForward(forward(num));
forward的模板类型为int,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型
printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
printValue(forward(v));forward的模板参数为右值引用,最终得到一个右值,实参为右值
testForward(forward<int&>(num));
forward的模板类型为int&,最终会得到一个左值,函数的形参为未定引用类型T&&被左值初始化后得到一个左值引用类型
printValue(v);实参为左值
printValue(move(v));通过move将左值转换为右值,实参为右值
printValue(forward(v));forward的模板参数为左值引用,最终得到一个左值,实参为左值
testForward(forward<int&&>(num));
forward的模板类型为int&&,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型
printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
printValue(forward(v));forward的模板参数为右值引用,最终得到一个右值,实参为右值
5.列表初始化
#include <iostream>
using namespace std;
class Test
{
public:
Test(int) {}
private:
Test(const Test &);
};
int main(void)
{
Test t1(520);
Test t2 = 520; //拷贝构造函数私有,否则520隐式类型转换Test(int)匿名对象,然后拷贝构造给t2
Test t3 = { 520 };
Test t4{ 520 };
int a1 = { 1314 };
int a2{ 1314 };
int arr1[] = { 1, 2, 3 };
int arr2[]{ 1, 2, 3 };//大括号初始化列表
return 0;
}
使用 new 操作符创建新对象的时候可以使用列表初始化进行对象的初始化
int *p = new int{520};
double b = double{52.431};
int * array = new int[3]{1,2,3};
指针p指向了一个new操作符返回的内存,通过列表初始化将内存数据初始化为了520
变量b是对匿名对象使用列表初始之后,再进行拷贝初始化。
数组array在堆上动态分配了一块内存,通过列表初始化的方式直接完成了多个元素的初始化。
列表初始化还可以直接用在函数返回值上:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person(int id, string name)
{
cout << "id: " << id << ", name: " << name << endl;
}
};
Person func()
{
return { 9527, "华安" };//这里的return (9527,"华安");
}
int main(void)
{
Person p = func();
return 0;
}
return 这里一意思一样,相当于返回了一个匿名对象
列表初始化细节
聚合体
#include <iostream>
#include <string>
using namespace std;
struct T1
{
int x;
int y;
}a = { 123, 321 };
struct T2
{
int x;
int y;
T2(int, int) : x(10), y(20) {}//为主
}b = { 123, 321 };
int main(void)
{
cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
return 0;
}
对象a是对一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表中的数据来初始化T1结构体中的成员。
在结构体T2中自定义了一个构造函数,因此实际的初始化是通过这个构造函数完成的。
判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中。
1.普通数组本身可以看做是一个聚合类型
int x[] = {1,2,3,4,5,6};
double y[3][3] = {
{1.23, 2.34, 3.45},
{4.56, 5.67, 6.78},
{7.89, 8.91, 9.99},
};
char carry[] = {'a', 'b', 'c', 'd', 'e', 'f'};
std::string sarry[] = {"hello", "world", "nihao", "shijie"};
2.满足以下条件的类(class、struct、union)可以被看做是一个聚合类型:
无用户自定义的构造函数。
无私有或保护的非静态数据成员。
无基类。
无虚函数。
类中不能有使用{}和=直接初始化的非静态数据成员(从c++14开始就支持了)。
struct T1
{
int x;
long y;
protected:
int z;
}t{ 1, 100, 2}; // error, 类中有私有成员, 无法使用初始化列表初始化
非聚合体
需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:
#include <iostream>
#include <string>
using namespace std;
struct T1
{
int x;
double y;
// 在构造函数中使用初始化列表初始化类成员
T1(int a, double b, int c) : x(a), y(b), z(c){}
virtual void print()
{
cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
}
private:
int z;
};
int main(void)
{
T1 t{ 520, 13.14, 1314 }; // ok, 基于构造函数使用初始化列表初始化类成员
t.print();
return 0;
}
需要额外注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,比如下面的这个例子:
#include <iostream>
#include <string>
using namespace std;
struct T1
{
int x;
double y;
private:
int z;
};
struct T2
{
T1 t1;
long x1;
double y1;
};
int main(void)
{
T2 t2{ {}, 520, 13.14 };//T2是聚合体类型
return 0;
}
T1并非一个聚合类型,因为它有一个Private的非静态成员。但是尽管T2有一个非聚合类型的非静态成员t1,T2依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。
对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值,而对于非聚合类型,则需要先自定义一个合适的构造函数,此时使用列表初始化将会调用它对应的构造函数。
std::initializer_list
initializer:初始化的意思
在C++的STL容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和STL一样有任意长度初始化的能力,可以使用std::initializer_list这个轻量级的类模板来实现。
- 它是一个轻量级的容器类型,内部定义了迭代器iterator等容器必须的概念,遍历时得到的迭代器是只读的。
- 对于std::initializer_list而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型T
- 在std::initializer_list内部有三个成员接口:size(), begin(), end()。
- std::initializer_list对象只能被整体初始化或者赋值。
如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为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;
}
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 的使用
在C++中using用于声明命名空间,使用命名空间也可以防止命名冲突。在程序中声明了命名空间之后,就可以直接使用命名空间中的定义的类了。
1定义别名
typedef 旧的类型名 新的类型名
typedef unsigned int uint_t
**被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字。**和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。C++11中规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名,即使用using。
在使用的时候,关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。使用typedef定义的别名和使用using定义的别名在语义上是等效的。
using 新的类型 = 旧的类型
using uint_t = int
通过using和typedef的语法格式可以看到二者的使用没有太大的区别,假设我们定义一个函数指针,using的优势就能凸显出来了
//定义函数指针,
tyoedef int(*func_ptr)(int,double)
using func_ptr1 = int(*)(int,double)
在定义函数指针时时没有函数名字的注意
如果不是特别熟悉函数指针与typedef,第一眼很难看出func_ptr其实是一个别名,其本质是一个函数指针,指向的函数返回类型是int,函数参数有两个分别是int,double类型。
使用using定义函数指针别名的写法看起来就非常直观了,把别名的名字强制分离到了左边,而把别名对应的实际类型放在了右边,比较清晰,可读性比较好。
2.模板的命名
**使用typedef重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,**比如我们需要一个固定以int类型为key的map,它可以和很多类型的value值进行映射,如果使用typedef这样直接定义就非常麻烦:
template<class T>
typedef map<int,T> type; //错误
#include<iostream>
#include<string>
#include<vector>
#include<map>
using namespace std;
template<typename T>
//定义外敷类
struct Mymap
{
typedef map<int, T> type;
};
int main()
{
Mymap<string>::type m;//是一个类型别名
m.insert(make_pair(1, "eve"));
m.insert(make_pair(2, "vev"));
return 0;
}
在C++11中,新增了一个特性就是可以通过使用using来为一个模板定义别名,对于上面的需求可以写成这样:
template<typename T>
using mymap = map<int,T>
mymap<string> m;
m.insert(make_pair(1,"vei"));
最后在强调一点:using语法和typedef一样,并不会创建出新的类型,它们只是给某些类型定义了新的别名。using相较于typedef的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名。
7.可调用对象包装器、绑定器
1.可调用对象
有以下几种:
-
是一个函数指针
-
是一个具有operator()成员的类对象(仿函数)
-
是一个可被转换为函数指针的类对象
-
是一个类成员函数指针或者类成员指针
func是一个变量指向print的指针
int print(int a, double b)
{
cout << a << b << endl;
return 0;
}
int (*func)(int, double) = &print;
Test(含有仿函数)
struct Test
{
//操作符重载
void operator()(string name)
{
cout << name << endl;
}
};
Test(转化为了函数指针)
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using func_ptr = void(*)(int, string);
struct Test
{
static void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}
// 将类对象转换为函数指针
operator func_ptr() //func_ptr是一个类型,指针类型
{
return print; //返回的是一个函数名,也就是函数地址
}
};
int main(void)
{
Test t;
// 对象转换为函数指针, 并调用
t(19, "Monkey D. Luffy");
return 0;
}
在main
函数中,首先创建了一个Test
类的对象t
,然后直接通过这个对象来调用它的括号运算符重载函数,传入参数19
和"Bihar"
。因为类型转换操作符被定义,这会隐式地将t
对象转换为一个函数指针,并且这个指针指向了print
静态成员函数。接着,调用这个函数指针,将19
和"Bihar"
传递给print
函数,它会将这两个参数输出到控制台。
补充:类型转换操作符函数
用于自定义类或结构体对象向其他类型的隐式转换。这种函数允许用户在需要时,将自定义类型的对象转换为另一种类型。
对于类型转换操作符函数,虽然函数本身不需要显式地指定返回类型,但它实际上会返回目标类型的值。
operator desired_type() {
// 实现转换逻辑
return desired_type类型的值
}
desired_type
是你希望将对象转换为的目标类型。这个函数应该在类的公共部分声明,并且没有返回类型。(全局变量和静态变量)
func_ptr和obj_ptr(类成员函数指针和类成员指针)
#include<iostream>
#include<string>
#include<vector>
#include<map>
using namespace std;
struct Test
{
void print(int a, string b)
{
cout << a << b << endl;
}
int m_num;
};
int main()
{
//此处是定义指向类成员函数的指针,并不是可调用对象包装器
//定义类成员函数指针指向类成员函数,必须加上作用域,防止同名函数
void (Test::*func_ptr)(int, string) = &Test::print;
//类成员指针指向类成员变量
int Test::* obj_ptr = &Test::m_num;
Test t;
//通过类成员函数指针调用类成员函数,仔细考虑后就这一种写法
(t.*func_ptr)(19, "ev");
//通过类成员指针初始化类成员变量
t.*obj_ptr = 15;
return 0;
}
在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型
- 是一个函数指针
- 是一个具有operator()成员的类对象(仿函数)
- 是一个可被转换为函数指针的类对象
- 是一个类成员函数指针或者类成员指针
2.可调用对象包装器
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 << endl;
return a + b;
}
class T1
{
public:
static int sub(int a, int b)
{
cout << a << b << endl;
return a - b;
}
};
class T2
{
public:
int operator()(int a, int b)
{
cout << a << b << endl;
return a * b;
}
};
int main()
{
//绑定一个普通函数
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;
}
通过测试代码可以得到结论: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 << "那我i哎" << endl;
}
};
int main()
{
B b;
A a(b); //仿函数通过包装器对象进行包装
a.notify();
return 0;
}
A(const function<void()>& f) :callback(f)
{}
构造函数接收一个std::function类型的参数f,也就是符合这个类型的可调用对象(一定是一个对象,实例化对象或者变量指针等),B类的实例化对象正好满足要求,并且初始化列表给callback变量赋值,将回调函数保存到自己内部,方便随时调用。
通过上面的例子可以看出,使用对象包装器std::function可以非常方便的将仿函数转换为一个函数指针,通过进行函数指针的传递,在其他函数的合适的位置就可以调用这个包装好的仿函数了。
另外,使用std::function作为函数的传入参数,可以将定义方式不相同的可调用对象进行统一的传递,这样大大增加了程序的灵活性。
3.绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
将可调用对象与其参数一起绑定成一个仿函数。
将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。
//绑定非类成员函数/变量
auto f = std::bind(可调用对象地址(也即是函数名),绑定的参数/占位符)
//绑定类成员函数/变量
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()
{
//使用绑定器绑定可调用对象
auto f1 = bind(output, placeholders::_1);
for (int i = 0; i < 10; i++)
{
callFunc(i, f1);
}
cout << endl;
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进行自动类型推导就可以了。
技巧:
std::function就是一个变量类型,例如int ,所以不需要实际的对象
std::bind 就相当于一个给变量赋值的变量,相当于int a = 6;中的6
//没有简化的程序
std::function<void(int)> f1 = std::bind(output,placeholders::_1);
//简化之后的程序
auto f1 = std::bind(output,placeholders::_1);
**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()
{
//使用绑定器绑定可调用对象和函数,并调用的到的仿函数
bind(output, 1, 2)();//绑定的函数参数都够用了
bind(output, placeholders::_1, 2)(10);//绑定的函数参数不够用
bind(output, 2, placeholders::_1)(10);
//bind(output, 2, placeholders::_2)(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;
}
通过测试可以看到,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 << " " << y << endl;
}
int m_number = 100;
};
int main()
{
Test t;
//绑定类成员函数
function<void(int, int)> f1 = bind(&Test::output, &t, placeholders::_1, placeholders::_2);
//绑定类成员变量(公共),用引用的返回说明可以修改m_number的值,function是一个对象,bind返回的也是一个对象
function<int& (void)> f2 = bind(&Test::m_number, &t);
//调用
f1(10, 20);
f2() = 2333;//对象调用进行更改m_number的值
cout << t.m_number << endl;
return 0;
}
在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。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++中是非常重要的一个概念,通常用于说明一个类型的属性,尤其是用户自定义类型的属性。 POD划分为两个基本概念的合集,即∶平凡的(trivial) 和标准布局的(standard layout )
平凡类型