上篇: 【C++】-- C++11基础常用知识点(上)_川入的博客-CSDN博客
目录
新的类功能
默认成员函数
可变参数模板
可变参数
可变参数模板
empalce
lambda表达式
C++98中的一个例子
lambda表达式
lambda表达式语法
捕获列表
lambda表达底层
包装器
function包装器
bind绑定
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值运算符重载。
所以到了C++11后有8个默认成员函数。
移动构造函数和移动赋值运算符重载的又来以及原理:
【C++】-- C++11 - 右值引用和移动语义(上万字详细配图配代码从执行一步步讲解)_川入的博客-CSDN博客
只有在深拷贝的情况下才会有移动构造函数和移动赋值运算符重载。可以认为:
移动构造函数和移动赋值重载,编译器自行生成的默认成员函数,能用的条件的复杂度与苛刻程度远远大于:构造函数、析构函数 、拷贝构造函数 、拷贝赋值重载4个默认成员函数。(由于:取地址重载 、const 取地址重载几乎不用自己写,用编译器的即可,所以忽略)
没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝。自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
class Person
{
public:
Person(const char *name = "", int age = 0)
: _name(name), _age(age)
{}
Person(const Person &p)
: _name(p._name), _age(p._age)
{}
Person(Person &&p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
class Person
{
public:
Person(const char *name = "", int age = 0)
: _name(name), _age(age)
{}
Person(const Person &p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
可以使用default关键字强行让编译器生成,但是需要注意析构函数 、拷贝构造、拷贝赋值重载也会收到影响,需要自己写或也强制生成。没有什么意义,所以一般default关键字是用于构造,因为拷贝构造也属于构造,如果写了拷贝构造就不会默认生成构造了。
#问:如何用delete关键字实现一个类,只能再堆上创建对象?
平时我们创建的类,是可以在栈区、全局数据区上创建的。
class HeapOnly
{
};
int main()
{
HeapOnly hp1; // 栈区
static HeapOnly h2; // 全局数据区
return 0;
}
我们可以通过delete析构函数,然后使用new开辟类。
class HeapOnly
{
public:
// HeapOnly()
// {
// str_ = new char[10];
// }
// void Destroy()
// {
// delete[] str_;
// operator delete(this); // 内存管理之重载operator delete
// }
~HeapOnly() = delete;
private:
char* str_;
};
int main()
{
// HeapOnly hp1; // 栈区 -- 会调析构
// static HeapOnly h2; // 全局数据区 -- 会调析构
// new出来的对象会调用构造 -- 这个时候会导致资源泄漏
HeapOnly *ptr = new HeapOnly;
operator delete(ptr);
return 0;
}
- new是c++中的操作符,malloc是c中的一个函数。
- new不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数
- malloc只会单纯的分配内存,不会进行初始化类成员的工作,同样free也不会调用析构函数。
#问:
class HeapOnly { public: HeapOnly() { str_ = new char[10]; } ~HeapOnly() = delete; private: char* str_; };
对于构造函数是new空间,因为不能调用析构而不能使用delete,导致值空间泄漏怎么办?
我们可以搞一个函数,利用函数将其释放。
class HeapOnly
{
public:
HeapOnly()
{
str_ = new char[10];
}
void Destroy()
{
delete[] str_;
operator delete(this); // 内存管理之重载operator delete
// 也可以使用free
}
~HeapOnly() = delete;
private:
char* str_;
};
int main()
{
// HeapOnly hp1; // 栈区 -- 会调析构
// static HeapOnly h2; // 全局数据区 -- 会调析构
// new出来的对象会调用构造 -- 这个时候会导致资源泄漏
HeapOnly *ptr = new HeapOnly;
ptr->Destroy();
return 0;
}
继承的时候要小心,因为指针是可能出现偏移的,继承之后,切片可能成员位置发生变化,operator delete(this);的释放位置就可能不对。
可变参数最早的出现是在C语言:
以printf,不确定参数传多少个参数,后面可以传一串值,也就可变参数,可以有0 ~ n个参数。底层是用数组实现的。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template
void ShowList(Args... args)
{}
// (不一定非要写作:Args、args,可以换一个名字,只是这两个常用)
#include
// 可变参数的函数模板
template
void ShowList(Args... args)
{}
int main()
{
std::string str("hello");
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', str);
return 0;
}
#include
#include
// 可变参数的函数模板
template
void ShowList(Args... args)
{
std::cout << sizeof...(args) << std::endl;
}
int main()
{
std::string str("hello");
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', str);
return 0;
}
Note:for(int i = 0; i< sizeof...(args); i++) { std::cout << args[i] << " "; // error:args[i]不支持 }
语法不支持使用args[i]这样方式获取可变参数,所以我们需要用一些奇招来 一一 获取参数包的值。
第一种:递归函数方式展开参数包
将参数包改一改,增加一个参数。
#include
#include
// 递归终止函数
template
void ShowList()
{
std::cout << std::endl;
}
// 展开函数
template
void ShowList(const T& value, Args... args) // 第一个参数传给value,剩下的传给参数包args。
{
cout << value << " ";
ShowList(args...); // 参数超过0个递归调自己,参数0个调递归终止函数。
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
利用递归不断地推出参数包中的内容。
第二种:逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在ShowList函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式,因为逗号表达式会按顺序执行逗号前面的表达式。
#include
#include
template
void PrintArg(cosnt T t)
{
std::cout << t << " ";
}
// 展开函数
template
void ShowList(Args... args)
{
// 利用逗号表达式去初始化arr,arr编译的时候就会知道要开多大,这个时候就会依次展开args参数包。
// 利用逗号表达式去取右边的值0。(逗号表达式会按顺序执行逗号前面的表达式)
int arr[] = {(PrintArg(args), 0)...};
std::cout << std::endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
同理,也可以优化为不适用逗号表达式展开参数包:
#include
#include
template
int PrintArg(cosnt T t)
{
std::cout << t << " ";
return 0;
}
// 展开函数
template
void ShowList(Args... args)
{
// arr编译的时候就会知道要开多大,这个时候就会依次展开args参数包。
int arr[] = { PrintArg(args)... };
std::cout << std::endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
分析STL容器中的empalce相关接口函数:
https://cplusplus.com/reference/vector/vector/emplace/
emplace_back是在一个函数模板里面,把一个成员函数是实现成可变参数包。其就是通过将可变参数包不断不断的往下传,传到最下面去初始化对应数据,或者是链表的话就初始化节点里的数据。
template
void emplace_back (Args&&... args);
#问:那么相对insert 和emplace系列接口的优势到底在哪里呢?
// vector::emplace_back
#include
#include
int main ()
{
std::vector myvector;
myvector.push_back(100);
myvector.emplace_back(200);
return 0;
}
// vector::emplace_back
#include
#include
#include
#include
int main()
{
std::vector> myvector;
myvector.push_back(std::make_pair("sort", 1));
myvector.emplace_back(std::make_pair("sort", 1));
myvector.emplace_back("sort", 1);
return 0;
}
效率上就emplace_back更好,因为make_pair是先构造,构造了一个pair。如此push_back就传了一个pair对象。所以调push_back是:
emplace_back是不用着急创建pair对象,我们可将这个参数包一直向下传递,直到最后需要插入数据的时候,直接用这个数据包创建pair对象。
所以emplace系列比insert系列接口不一定高效。
通过代码凸显区别:
不一定所有容器都会出现,于源码的实现有关系,此处使用list容器,并在VS2019实现出来的:
#include
#include #include
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { std::cout << "Date(int year = 1, int month = 1, int day = 1)" << std::endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { std::cout << "Date(const Date& d)" << std::endl; } private: int _year; int _month; int _day; }; int main() { std::list lt1; lt1.push_back(Date(2022, 11, 16)); std::cout << "---------------------------------" << std::endl; lt1.emplace_back(2022, 11, 16); return 0; }
所以建议:这个这种场景下直接使用emplace系列接口。
lambda也叫做匿名函数。
像函数使用的对象 / 类型:
- 函数指针 -- C++不喜欢的操作,所以有了仿函数。(全局的函数)
- 仿函数 / 函数对象。(全局的类)
- lambda。(局部)
因为由于仿函数有诸多的不便。如果待排序元素为自定义类型,需要用户定义排序时的比较规则,对于以下的三个成员一个就要创建2个(less、greater),就是6个。
#include
struct Goods
{
std::string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
注意:
#include
int main()
{
// 两个数相加的lambda
// 没有函数名,加一个捕捉列表[]而已。因为没有名字,所以调用不好调
// 但是[](int a, int b) -> int{ return a + b; }整体是一个对象,所以就可以巧用auto。
auto add1 = [](int a, int b) -> int{ return a + b; };
std::cout << add1(1, 2) << std::endl;
// 省略返回值
auto add2 = [](int a, int b){ return a + b; };
std::cout << add2(1, 2) << std::endl;
}
于是对于前面的三个成员一个就要创建2个(less、greater),就是6个。解决:
#include
#include
#include
struct Goods
{
std::string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
//...
Goods(const char *str, double price, int evaluate)
: _name(str), _price(price), _evaluate(evaluate)
{
}
};
int main()
{
std::vector v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._name < g2._name; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._name > g2._name; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._evaluate > g2._evaluate; });
}
#问:如何写一个交换swap函数?
可以像上面那样写,但是会非常的难看。
#include
int main()
{
// 交换变量的lambda - 行数会多
int x = 0, y = 1;
auto swap1 = [](int &x1, int &x2) -> void{int tmp = x1; x1 = x2; x2 = tmp; };
swap1(x, y);
std::cout << x << ":" << y << std::endl;
}
我们可以这样写:
#include
int main()
{
// 交换变量的lambda - 行数会多
int x = 0, y = 1;
auto swap1 = [](int &x1, int &x2) -> void
{
int tmp = x1;
x1 = x2;
x2 = tmp;
};
swap1(x, y);
std::cout << x << ":" << y << std::endl;
}
#问:假如我们想不传参数交换x,y呢?
利用捕捉列表实现,注意:
#include
int main()
{
// 交换变量的lambda - 行数会多
int x = 0, y = 1;
// 可以理解为:改变形参,不会改变实参
auto swap = [x, y]()mutable
{
int tmp = x;
x = y;
y = tmp;
};
swap();
std::cout << x << ":" << y << std::endl;
}
所以mutable在实际中不起价值作用。
捕获列表说明:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
注意:
#include
void (*PF)();
int main()
{
auto f1 = []
{ std::cout << "hello world" << std::endl; };
auto f2 = []
{ std::cout << "hello world" << std::endl; };
// f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象,与范围for很像。
范围for:
并没有看起来这么的智能,实际上是底层运用迭代器实现的。
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year) -> double
{
return monty * rate * year;
};
r2(10000, 2);
return 0;
}
仿函数的名称就是:lambda_uuid。所以lambda表达式对于我们是匿名的,对于编译器而言是有名的。实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
#include
template
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
std::cout << useF(f, 11.11) << std::endl;
// 仿函数对象
std::cout << useF(Functor(), 11.11) << std::endl;
// lamber表达式对象
std::cout << useF([](double d)->double{ return d/4; }, 11.11) << std::endl;
return 0;
}
因为上述的 f 的类型不同,于是会被实例化成三个。
包装器可以很好的解决上面的问题,将其变为1份。
std::function在头文件 < functional >-// 类模板原型如下template < class T > function ; // undefinedtemplate < class Ret , class ... Args >class function < Ret ( Args ...) > ;-
模板参数说明:
Ret : 被调用函数的返回类型Args…:被调用函数的形参
// 使用方法如下:
#include
#include
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function func1 = f;
std::cout << func1(1, 2) << std::endl;
// 函数对象
std::function func2 = Functor();
std::cout << func2(1, 2) << std::endl;
// lamber表达式
std::function func3 = [](const int a, const int b)
{ return a + b; };
std::cout << func3(1, 2) << std::endl;
return 0;
}
对于静态成员函数与非静态成员函数的不同:
//使用方法如下:
#include
#include
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
//类的成员函数 -- 语法规定
// 静态成员函数可以不用加&,可以加&。并且可以直接调用。
std::function func4 = Plus::plusi;
std::cout << func4(1, 2) << std::endl;
// 非静态成员函数需要加&,并且不能直接调用,需要传对象,此处为Plus。(成员函数多传一个)
std::function func5 = &Plus::plusd;
std::cout << func5(Plus(), 1.1, 2.2) << std::endl;
return 0;
}
如果对于非静态成员函数,不想多传一个类对象的参数,可以通过绑定的方式解决这个问题。
所以对于上面的,因为上述的 f 的类型不同,于是会被实例化成三个,就可以解决了:
#include
#include
template
T useF(F f, T x)
{
static int count = 0;
std::cout << "count:" << ++count << std::endl;
std::cout << "count:" << &count << std::endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数指针
std::function f1 = f;
std::cout << useF(f1, 11.11) << std::endl;
// 函数对象
std::function f2 = Functor();
std::cout << useF(f2, 11.11) << std::endl;
// lamber表达式对象
std::function f3 = [](double d)->double{ return d / 4; };
std::cout << useF(f3, 11.11) << std::endl;
return 0;
}
包装器的其他一些场景:
class Solution
{
public:
int evalRPN(vector &tokens)
{
stack st;
map> opFuncMap =
{
{"+", [](long long i, long long j)
{ return i + j; }},
{"-", [](long long i, long long j)
{ return i - j; }},
{"*", [](long long i, long long j)
{ return i * j; }},
{"/", [](long long i, long long j)
{ return i / j; }}};
for (auto &str : tokens)
{
if (opFuncMap.find(str) != opFuncMap.end())
{
long long right = st.top();
st.pop();
long long left = st.top();
st.pop();
st.push(opFuncMap[str](left, right));
}
else
{
// 1、atoi itoa
// 2、sprintf scanf
// 3、stoi to_string C++11
st.push(stoll(str));
}
}
return st.top();
}
};
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下:
template
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template
/* unspecified */ bind (Fn&& fn, Args&&... args);
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
库中就是使用了placeholders来占位:
https://legacy.cplusplus.com/reference/functional/placeholders/
其中的_1、_2、_3等,就是用来占位的。_1代表第1个参数,_2代表第2个参数……。调整的是形参的顺序。
#include
#include
int Div(int a, int b)
{
return a / b;
}
int main()
{
int x = 10, y = 2;
std::cout << Div(x, y) << std::endl;
// 调整顺序 -- 鸡肋,一般用不上
// _1, _2.... 定义在placeholders命名空间中,代表绑定函数对象的形参,
// _1,_2... 分别代表第一个形参、第二个形参...
//std::function bindFunc = bind(Div, std::placeholders::_2, std::placeholders::_1);
auto bindFunc = bind(Div, std::placeholders::_2, std::placeholders::_1);
// 传时候不会变
std::cout << bindFunc(x, y) << std::endl;
return 0;
}
可以理解为:
// x -> _1 ->a
// y -> _2 ->b。
auto bindFunc = bind(Div, _1, _2);
bindFunc(x, y);
// x -> _2 ->b
// y -> _1 ->a。
auto bindFunc = bind(Div, _2, _1);
bindFunc(x, y);
可以用绑定解决前面的非静态成员函数,需要传类对象(成员函数多传一个),以绑定参数解决 -> 调整个数。
#include
#include
#include