C++11更新了许多新的特性,更加方便,强大,语法更加泛化,因此C++11需要重点学习。
统一的列表初始化
{}初始化
在C++98中,允许使用过 {} 来对数组或者结构体进行初始化。
#include<iostream>
#include<vector>
using namespace std;
struct ST {
int _a;
int _b;
};
int main()
{
vector<int> v = { 1,2,3 };
ST s = { 1,2 };
}
而C++11则扩大了列表初始化的范围,所有内置类型和用户自定义的类型也能够采用列表初始化了。
我们可以加 "=",也可以不加。并且我们的自定义类型也可以通过列表初始化来调用构造函数
#include<iostream>
#include<string>
using namespace std;
class Date {
public:
Date(int x, int y, int z)
:_x(x),_y(y),_z(z)
{
}
private:
int _x;
int _y;
int _z;
};
int main()
{
Date d = { 2024,1,18 };
return 0;
}
std::initializer_list
这个类一般作为容器的构造函数的参数,C++11中的容器的构造函数重载了以该类为参数的构造函数,这样就能够采用 {} 赋值,也可以用作operator=的参数。
声明
auto
原本的auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是在局部域中定义的变量本来就是局部自动存储类型,因此auto作用不大,C++11则废除auto原来的作用,用于实现自动判断类型,不过需要显示初始化。
decltype
该关键字可以把变量的类型声明为表达式指定的类型。
int main()
{
int x = 1;
double y = 1.1;
decltype(x * y) ret1;
char z = 'a';
decltype(x * z) ret2;
cout << typeid(ret1).name() << endl;
cout << typeid(ret2).name() << endl;
return 0;
}
nullptr
C++中,NULL本定义为字面量0,那么NULL既表示指针常量,又表示整形常量,可能出现一些问题,因而出现了 nullptr。
智能指针
智能指针是一个用于防止内存泄漏的机制,通过利用类成员的生命周期来将空间释放,这里不做详细叙述。
右值引用和移动语义
C++中本来就有引用的语义,不过C++中新增了右值引用的语义,之前学习的都是左值引用。
左值和左值引用
左值是一个表达数据的表达式,我们可以对它进行取地址,或者对他进行赋值,它能够出现在赋值符号=的左边或右边。
const修饰的左值不能赋值,但是可以对它取地址。
而对这些左值的引用就是左值引用。
int main()
{
//这些就是左值
int a = 1;
int* b = &a;
const int c = 1;
//这些就是左值引用
int& pa = a;
int*& pb = b;
const int& pc = c;
return 0;
}
右值和右值引用
右值也是一个表达数据的表达式,比如字面常量,表达式返回值,函数返回值(不能是左值引用返回)等,不过右值只能出现在赋值符号=的右边,因此称为右值。
为了和左值引用区分,右值引用后面有两个&&。
注:右值虽然不能取地址,但是可以通过右值引用来引用右值,再对右值引用取地址。也可以采用const 右值引用,这样能取地址但是不能修改。
不过这不是右值引用的主要特性。
左值引用和右值引用的比较
左值引用
- 左值引用只能引用左值,不能引用右值。
- const 左值引用可以引用右值。
右值引用
- 右值引用只能引用右值,不能引用左值
- 右值引用可以引用move后的左值
- move是C++的函数,可以将左值强行转换为右值
左值引用的短板
左值引用能够引用左值,const 左值左值右值都能引用,那为啥有个右值引用呢?
这就是因为左值引用的短板了,为了说明这个短板,我们先构建一个类。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;
namespace test {
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s) noexcept
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s) noexcept
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
这个类中包含了深拷贝,移动构造,移动赋值等方法(移动构造和移动赋值等下说明)
我们再写两个函数,然后分别调用它们。
void f1(test::string s)
{
}
void f2(test::string& s)
{
}
int main()
{
test::string s("hello world");
cout << "这是f1" << endl;
f1(s);
cout << "这是f2" << endl;
f2(s);
s += '!';
return 0;
}
我们发现,没有使用左值引用作为参数的函数需要调用一次深拷贝,而使用了左值引用的函数则没有调用。
这是因为当你没有使用左值引用来调用参数时,那么编译器需要通过深拷贝来拷贝一个新的string,而通过左值引用调用参数时,这个参数的地址依旧是原来的地址,只是名称不同了,因此不用调用深拷贝,作为一个函数返回对象时也是如此。
这是左值引用的使用场景——函数参数和函数返回对象。但是当函数返回对象是一个局部对象时,无法使用左值引用。
test::string to_string(int value)
{
test::string tmp;
//...中间过程省略
return tmp;
}
int main()
{
test::string s;
s = to_string(1234);
return 0;
}
像这种状况下,左值引用就无法减少拷贝了,如果函数返回值是一个左值引用,那么当退出函数的时候,这个局部变量就被销毁了,因此这里只能使用普通的函数返回。(这里我将移动构造都给屏蔽了)
这个时候就轮到右值引用出场了(将移动构造和移动赋值解除屏蔽)
我们发现此时只进行了一次移动赋值,那什么是移动赋值呢?移动构造又是什么呢?
移动构造和移动赋值
移动构造:将参数的右值的资源窃取过来,占为己有,不做深拷贝,因此叫它移动构造,就是窃取别人的资源来构造自己。
移动赋值:将右值对象赋值给返回对象,再以左值引用返回。
当遇上类似状况时,采用右值引用能够进行优化。
右值引用引用左值以及一些深入场景分析
虽然右值引用只能引用右值,但是在需要时,可以引用左值来调用移动构造和移动赋值。
使用move将左值变为右值。
int main()
{
test::string s1("hello world");
test::string s2 = s1;
test::string s3 = (std::move(s1));
}
比如这样,我们通过move来使用移动语义,不过需要注意的是,移动构造会将右值的资源全部给搬走,上述代码中,s1的资源就全部给s3了,s1为空了。
- s2深拷贝,s3 移动构造。
当然这种用法一般不怎么使用。
不过STL里面增加了很多右值引用的版本,可以使用一下。
int main()
{
vector<string> v;
v.push_back("1111");
string s("1234");
v.push_back(std::move(s));
}
比如这些,就是使用了移动语义的版本。
int main()
{
vector<string> v;
v.push_back("1111");
string s("1234");
v.push_back(std::move(s));
}
万能引用
万能引用是一种特殊的写法,它能够同时接受左值和右值。
#include<iostream>
using namespace std;
void func(int& i)
{
cout << "左值引用" << endl;
}
void func(int&& i)
{
cout << "右值引用" << endl;
}
//下面的模板函数就是万能引用
//它能够同时接受左值和右值
//但是它唯一的作用就是限制了接受的类型
//后续使用都退化成了左值
template<class T>
void func1(T&& t)
{
func(t);
}
int main()
{
int a = 1;
func1(a);
func1(11);
func1(std::move(a));
return 0;
}
不过万能引用后续都退化成了左值,因此我们需要学习一下完美转发。
退化原因: 在 func 调用中,都是将数据作为 t 来传递,而编译器会将这里识别成左值
完美转发
#include<iostream>
using namespace std;
void func(int& i)
{
cout << "左值引用" << endl;
}
void func(int&& i)
{
cout << "右值引用" << endl;
}
//在模板函数中的传参使用forward即是完美转发
template<class T>
void func1(T&& t)
{
func(std::forward<T>(t));
}
int main()
{
int a = 1;
func1(a);
func1(11);
func1(std::move(a));
return 0;
}
完美转发必须要传递模板参数,否则会出错。
新的类成员函数
由于右值的出现,成员函数中又出现了移动赋值和移动构造。
- 如果类成员中没有移动构造函数时, 且析构、拷贝构造、拷贝赋值重载中的任意一个都没有实现时,编译器会生成一个默认的移动构造,内置类型则按字节拷贝,自定义类型成员则看该成员内部是否实现了移动构造,实现了调用移动构造,没实现就调用拷贝构造。
- 如果类成员没有移动赋值运算符重载时,且析构、拷贝构造、拷贝赋值重载中的任意一个都没有实现时,后面就和移动构造函数一样了
- 如果实现了就不会生成默认的函数了
强制生成默认函数default
C++提供了新的关键字default,可以强制生成默认函数。
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;
};
这里就强制生成了移动构造。
禁止生成默认函数delete
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) = delete;
private:
bit::string _name;
int _age;
};
这里就禁止生成移动构造了。
可变参数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
这个args是一个可变模板参数,带省略号的参数称为参数包,它包含了0~n个参数,不能直接获取参数,只能通过展开参数包来获取参数。
递归获取参数
#include<iostream>
using namespace std;
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
递归获取参数必须要有一个递归终止参数,当参数包只剩一个参数时,它就会调用这个终止参数,来终止递归。
逗号表达式获取参数
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
通过逗号表达式获取参数无需终止函数,PrintArg实际上是处理参数的函数。
这里是通过创建一个数组 arr,并且通过C++的初始化列表,创建了一个变长数组,数组长度是 sizeof args ,数组内容是 (PrintArg(args1),0), (PrintArg(args2),0)...直到参数包的最后一个参数。
这里的逗号表达式会先调用 PrintArg(args),然后获取表达式结果0,也就是说arr实际上是一个长度为 sizeof(args) 的,内容全为0的数组,这个数组的目的是为了在创建数组的时候展开参数包。
lambada表达式
在以往,我们可以通过sort来构造顺序函数,但是如果数组类型是自定义类型就需要自己定义一个新的比较函数,这样比较麻烦,因此C++11提供了lambada表达式,用来简化操作。
#include<string>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct Goods
{
string _name;
double _price;
Goods(string name, double price)
:_name(name)
,_price(price)
{
}
};
int main()
{
vector<Goods> a = { {"pineapple",8} ,{"apple",10},{"banana",12} };
sort(a.begin(), a.end(), [&](const Goods a, const Goods b) {
return a._name < b._name;
});
for (auto t : a)
{
cout << t._name << " : " << t._price << endl;
}
sort(a.begin(), a.end(), [&](const Goods a, const Goods b) {
return a._price < b._price;
});
for (auto t : a)
{
cout << t._name << " : " << t._price << endl;
}
}
sort的第三个参数就是一个lambada表达式。
语法
[capture-list](paremeters)mutable->returntype{statement}
- capture-list:捕捉列表,编译器根据 [] 来判断接下来是否是lambada函数,能够捕捉上下文的变量供lambada使用。
- paremeters: 参数列表,和普通的函数参数传递一样,不需要可以不传。
- mutable : 默认情况下,lambda 是一个const 函数,mutable 可以取消其常量性,使用mutable时,参数列表不可为空。
- ->returntype :返回值类型,可以由编译器推断。
- {statement} :函数体,可以使用捕捉的变量和其参数。
- 注意:在lambada 中,参数列表和返回类型都是可选部分,而捕捉列表和函数体可以为空。最简单的lambada函数为 [] {}。
了解了 lambada 表达式的语法后,还有几点需要说明。
捕捉列表的使用
- [var] : 用值传递的方式捕捉变量 var
- [=] : 用值传递的方式捕捉父作用域的所有变量,包括this指针。
- [&var]:用引用的方式捕捉变量var
- [&] : 用引用传递捕捉父作用域的所有变量,包括this
- [this]: 用值传递的方式捕捉this指针
注意事项
- 其中,父作用域指的是包含lambada表达式的语句块。
- 而且捕捉列表可以由多个捕捉项一起使用,用逗号分隔:[a,this]之类。
- 但是捕捉列表不可重复,否则会编译错误,如[a,=]这种。
- 在块作用域之外的lambada的捕捉列表必须为空。
- 在块作用域之内的lambada只能捕捉父作用域的局部变量,捕捉其他变量会报错。
- lambada表达式之间不可相互赋值。
函数对象和lambada
函数对象的使用方式和lambada几乎一样,实际上底层对于lambada表达式的处理方式和函数对象的处理方式都是一样的。
#include<string>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct Goods
{
string _name;
double _price;
Goods(string name, double price)
:_name(name)
,_price(price)
{
}
};
int main()
{
vector<Goods> a = { {"pineapple",8} ,{"apple",10},{"banana",12} };
auto t1 = [&](const Goods a, const Goods b) {
return a._name < b._name;
};
sort(a.begin(), a.end(),t1);
for (auto t : a)
{
cout << t._name << " : " << t._price << endl;
}
auto t2 = [&](const Goods a, const Goods b) {
return a._price < b._price;
};
sort(a.begin(), a.end(),t2);
for (auto t : a)
{
cout << t._name << " : " << t._price << endl;
}
}
包装器(function)
包装器可以将函数包装起来,更方便的使用。
我们先来看看这个场景。
template<class F, class T>
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()
{
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
return 0;
}
我们发现,模板函数实例化了三份。
但是采用function就不会。
#include<iostream>
#include<functional>
using namespace std;
template<class F, class T>
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::function<double(double)> f1 = f;
// 函数名
cout << useF(f1, 11.11) << endl;
// 函数对象
std::function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;
// lamber表达式
std::function<double(double)> f3 = [](double d)->double { return d / 4; };
cout << useF(f3, 11.11) << endl;
return 0;
}
我们发现function调用函数只实例化了一份。
bind函数
bind函数是一个函数模板,它就像一个函数包装器,生成一个可调用对象来适应原对象的参数列表。
我们可以通过bind来把一个接受N个参数的函数fn,通过绑定参数,返回一个接受M个参数的新参数(M可以大于N,不过一般都是小于N,因为大于N没有意义)。
#include<iostream>
#include<functional>
using namespace std;
int sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int,int)> f = bind(sub,std::placeholders::_1,std::placeholders::_2);
cout<<f(2,1);
return 0;
}
使用bind绑定的函数的参数我们可以通过 std::placeholder::_n 来指定该位置的参数应该是原参数的什么位置。
#include<iostream>
#include<functional>
using namespace std;
int sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int,int)> f = bind(sub,std::placeholders::_2,std::placeholders::_1);
cout<<f(2,1);
return 0;
}
比如这里我将 _2 和 _1 位置调换,结果就不同了。
线程库
C++11提供了线程库,供用户使用。
函数名 | 功能 |
thread() | 创造一个线程对象,没有关联任何函数,也就没有启动任何线程 |
thread(fn,arg1,arg2,...) | 构造一个线程对象,关联函数fn,参数为arg1,arg2... |
get_id() | 获取线程对象的id |
joinable() | 查看某个线程是否还在运行 |
join() | 该函数调用后会阻塞线程,当线程结束后,主线程继续执行 |
detach() | 分离线程与线程对象,分离的线程变为后台线程 |
注意点:
- 通过线程对象可以控制一个线程或者获取线程状态。
- 创建的线程对象有关联线程函数后,该线程就被启动,与主线程一起运行(函数可以是函数指针,函数对象或者lambada表达式)
- thread类防拷贝以及赋值,但是可以移动构造和移动赋值
- 可以通过joinable()查看线程是否有效,若线程对象是无参构造函数构造的,或者线程对象的状态转移给其他线程对象,或者线程已经调用join或detach结束了,就是无效的。
总结
C++11提供了许多强大的功能,并且在C++98的基础上完善了许多。
新增的右值引用提高了C++的效率,lambada也方便了用户使用 algorithm 的函数,十分值得学习。