C++11
1.初始化列表
1.1内置类型的初始化列表
int main()
{
//内置类型变量
int a1 = { 10 };
int a2(10);
int a3 = 1 + 1;
int a4 = { 1 + 1 };
int a5{ 1 + 1 };
//数组
int arr1[5]{ 1,2,3,4,5 };
int arr2[]{ 1,2,3,4,5 };
//动态数组
int* arr3 = new int[5]{ 1,2,3,4,5 };
//标准容器
vector<int> v{ 1,2,3,4,5 };
map<int, int> m{ {1,1},{2,2},{3,3} };
return 0;
}
1.2自定义类型的初始化列表
1.标准库主持单个对象的列表初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d{ 2020,1,1 };
return 0;
}
2.多个对象的列表初始化
多个对象的列表初始化,需要给该类添加一个initializer_list模型参数的构造函数即可。
#include <initializer_list>
template<class T>
class Vector
{
public:
Vector(initializer_list<T> l)
:_capacity(l.size())
,_size(0)
{
_array = new T[_capacity];
for (auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> l)
{
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
private:
T* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Vector<int> nums = { 1,2,3,4 };
nums = { 2,3,4 };
return 0;
}
2.变量类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有的情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂。
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便,而且更加简洁。
2.1decltype推导
auto使用的前提是必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候需要根据表达式运行完成之后结果的类型进行推导,auto也就无能为力。
1.推演表达式类型作为变量的自定义类型
int main()
{
double a = 10;
int b = 10;
decltype(a + b) c;
cout << typeid(c).name() << endl;
return 0;
}
2.推演函数返回值的类型
void* func(size_t n)
{
return malloc(n);
}
int main()
{
//如果没有参数,推导函数的类型
cout << typeid(decltype(func)).name() << endl;
//如果带参数列表,推导的是函数返回值的类型,只是推演并不会执行函数
cout << typeid(decltype(func(0))).name() << endl;
return 0;
}
运行时类型识别的缺陷是降低程序运行的效率。
3.默认成员函数控制
3.1显式缺省函数
在C++11中,可以在默认函数定义或者声明时附加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
class A
{
public:
A(int a)
:_a(a)
{}
//显式缺省构造函数,由编译器生成
A() = default;
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
3.2删除默认函数
如果想要限制某些默认函数的生成,在C++98中是该函数设置为平private,并且不给定义。在C++11中只需要在该函数声明加上=delete即可,该语法知识编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class A
{
public:
A(int a)
:_a(a)
{}
//显式缺省构造函数,由编译器生成
A() = delete;
A& operator=(const A& a) = delete;
private:
int _a;
};
int main()
{
A a1(10);
//编译失败,因为没有默认构造函数
//A a2;
//编译失败,因为没有赋值运算符重载
//a2 = a1;
return 0;
}
4.右值引用
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int add(int a, int b)
{
return a + b;
}
int main()
{
int&& ra = 10;
int&& ret = add(1, 2);
return 0;
}
4.1左值与右值
一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。左值通常是变量,右值通常是常量,表达式或者函数返回值(临时对象)。
int g_a = 10;
// 函数的返回值结果为引用,是左值
int& GetG_A()
{
return g_a;
}
//函数的返回值是临时变量,是右值
int Get()
{
return g_a;
}
int main()
{
int a = 10;
int b = 20;
// a和b都是左值,b既可以在=的左侧,也可在右侧,
// 说明:左值既可放在=的左侧,也可放在=的右侧
a = b;
b = a;
const int c = 30;
// 编译失败,c为const常量,只读不允许被修改
//c = a;
// 因为可以对c取地址,因此c严格来说不算是右值
cout << &c << endl;
// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
//b + 1 = 20;
GetG_A() = 100;
int ret = Get();
return 0;
}
因此关于左值与右值的区分不是很好区分,一般认为:
a.普通类型的变量,因为有名字,可以取地址,都认为是左值。
b.const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
c.如果表达式的运行结果是一个临时变量或者对象,认为是右值。
d.如果表达式运行结果是一个引用则认为是左值。
C++11对右值进行了严格的区分:
a.C语言中的纯右值,比如:a+b, 100
b.将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
4.2引用与右值引用比较
int main()
{
int x = 1, y = 2;
// 左值引用的定义
int a = 0;
int& b = a;
// 左值引用不能引用右值, const左值引用可以
//int& e = 10;
//int& f = x + y;
const int& e = 10;
const int& f = x + y;
// 右值引用的定义
int&& c = 10;
int&& d = x + y;
// 右值引用不能引用左值,但是可以引用move后左值
//int&& m = a;
int&& m = move(a);
return 0;
}
普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。右值引用:只能引用右值,一般情况不能直接引用左值,但是可以引用move后的左值。
4.3右值引用的应用
4.3.1做函数参数
int func(int& a)
{
return a;
}
int func(int&& a)
{
return a;
}
int main()
{
int a = 10;
//这里会匹配左值引用参数的func
func(a);
//这里会匹配右值引用参数的func
func(10);
return 0;
}
4.3.2移动语义
即将一个对象中资源移动到另一个对象中的方式,必须使用右值引用。
class String
{
public:
String(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
{
cout<<"String(const String& s)-拷贝构造-效率低"<<endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
String(String&& s)
:_str(nullptr)
{
// 传过来的是一个将亡值,反正你都要销毁了
// 不如把你的空间和值给我
cout << "String(String&& s)-移动构造-效率高" << endl;
swap(_str, s._str);
}
String& operator=(const String& s)
{
cout << "String& operator=(const String& s)-拷贝赋值-效率低" << endl;
if (this != &s)
{
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
delete[] _str;
_str = newstr;
}
return *this;
}
String& operator=(String&& s)
{
cout << "String& operator=(String&& s)-移动赋值-效率高" << endl;
swap(_str, s._str);
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
String f(const char* str)
{
String tmp(str);
return tmp; // 这里返回实际是拷贝tmp的临时对象
}
int main()
{
String s1("左值");
String s2(s1); // 参数是左值
String s3(f("右值-将亡值")); // 参数是右值-将亡值(传递给你用,用完我就析构了)
String s4("左值");
s4 = s1;
s4 = f("右值-将亡值");
//当需要用右值引用引用一个左值时,要用到move函数。
//它并不搬移任何东西,唯一的功能就是讲一个左值强制转化为右值引用。
//下面语句在实现s5的拷贝时就会移动构造,s1移动到s5中,s1变成了无效的字符串。
String s5(move(s1));
return 0;
}
4.4完美转发
完美转发是指在函数模板中,完全按照模板的参数的类型,将参数传递给函数模板中调用另外一个函数。
void Fun(int& x) { cout << "左值" << endl; }
void Fun(const int& x) { cout << "const 左值" << endl; }
void Fun(int&& x) { cout << "右值" << endl; }
void Fun(const int&& x) { cout << "const 右值" << endl; }
template<typename T>
void func(T &&t)
{
// 右值引用会第二次之后的参数传递过程中右值属性丢失,下一层调用会全部识别为左值
//Fun(t);
//C++11通过forward函数来实现完美转发
Fun(forward<T>(t));
}
int main()
{
func(10); //右值
int a;
func(a); //左值
func(move(a)); //右值
const int b = 8;
func(b); //const 左值
func(move(b)); //const 右值
return 0;
}
5.lambda表达式
struct Goods
{
string _name;
double _price;
};
对于一类商品,我们在浏览的时候,可以根据名字升序和降序浏览,还可以根据价格升序和降序浏览等等。那么我们对他进行排序的时候,就要写多个仿函数来实现,比较复杂。因此在C++11中出现了lambda表达式。
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "香蕉", 3 }, { "葡萄", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool
{
return g1._price < g2._price;
});
return 0;
}
5.1lambda表达式语法
//lambda表达式书写格式
[capture-list](parameters)mutable->return-type{statement}
5.1.1lambda表达式各部分说明
[capture-list]:捕捉列表。编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省缺(即使参数为空)。
return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
statement:函数体。在该函数体内,除了可以使用参数外,还可以使用所有捕获到的变量。
5.1.2捕捉列表说明
捕捉列表描述了上下文中那些数据可以被ambda使用,以及使用的方式传值还是传引用。
int main()
{
int a = 5, b = 7;
//传值捕捉a和b
auto add1 = [a, b]()->int {return a + b; };
int ret1 = add1();
//传值捕捉同一作用域中的所有对象
auto add2 = [=]()->int {return a + b; };
int ret2 = add2();
//传引用捕捉a和b
auto swap1 = [&a, &b] {int z = a; a = b; b = z; };
swap2();
//传引用捕捉同一作用域中的所有对象
auto swap2 = [&] {int z = a; a = b; b = z; };
swap3(a, b);
return 0;
}
5.2lambda表达式用法
lambda表达式通常直接写为函数的参数,让代码的可读性更强,可以清晰的看到是按照什么排序。
int x8()
{
Goods gds[] = { { "苹果", 2.1, 3 }, { "相交", 3.0, 5 }, { "橙子", 2.2, 9 }, { "菠萝", 1.5, 10 } };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price > g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price < g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num > g2._num; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num < g2._num; });
return 0;
}
实际在底层编译器对于lambda表达式的处理方式,完全就是按照仿函数的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
6.线程库
6.1thread类的介绍
int x = 0;
void add(int n)
{
for (int i = 0; i < n; i++)
{
++x;
}
}
int main()
{
//构造一个线程对象
thread t1(add, 100);
thread t2(add, 100);
//获取线程id
cout << t1.get_id() << endl;
cout << t2.get_id() << endl;
//线程分离,分离的线程变为后台线程,该线程的“死活”就与主线程无关
t2.detach();
//判断线程是否还在执行
if(t1.joinable())
//该函数会阻塞住线程,当该线程结束后,主线程继续执行
t1.join();
cout << x << endl;
return 0;
}
6.2thread类的使用
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:函数指针/lambda表达式/函数对象。
void threadfunc(int n)
{
cout << "thread1" << n << endl;
}
class TF
{
public:
void operator()(int n)
{
cout << "thread2" << n << endl;
}
};
int main()
{
//线程函数为函数指针
thread t1(threadfunc, 10);
//线程函数为函数对象
thread t2(TF(), 10);
//线程函数为lambda表达式
thread t3([](int n) {cout << "thread2" << n << endl; }, 10);
t1.join();
t2.join();
t3.join();
return 0;
}
6.3线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
void ThreadFunc1(int x)
{
x += 10;
}
void ThreadFunc2(int& x)
{
x += 10;
}
void ThreadFunc3(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc2, ref(a));
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc3, &a);
t3.join();
cout << a << endl;
return 0;
}
如果是类成员函数作为线程参数时,必须将this作为线程函数的参数。
6.4原子性操作库
多线程最主要的问题是共享数据带来的问题。C++中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,使得线程间数据的同步变得非常高效。
atomic_int sum = 0;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++; // 原子操作
}
int main()
{
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << sum << std::endl;
return 0;
}
6.5mutex
C++11提供的最基本的互斥量。
mutex mtx;
int x = 0;
void add(int n)
{
for (int i = 0; i < n; i++)
{
//加锁
mtx.lock();
++x;
//解锁
mtx.unlock();
}
}
int main()
{
thread t1(add, 100);
thread t2(add, 100);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
6.6condition_variable
C++11提供的最基本的同步量。
condition_variable cv1;
condition_variable cv2;
mutex mtx1, mtx2;
void p1(int n)
{
for (int i = 0; i < n; i += 2)
{
if (i != 0)
{
unique_lock<mutex> lck(mtx1);
//p1等待
cv1.wait(lck);
}
cout << this_thread::get_id() << " : "<< i << endl;
//通知p2
cv2.notify_one();
}
}
void p2(int n)
{
for (int i = 1; i < n; i += 2)
{
unique_lock<mutex> lck(mtx2);
//p2等待
cv2.wait(lck);
cout << this_thread::get_id() << " : " << i << endl;
//通知p1
cv1.notify_one();
}
}
int main()
{
thread t1(p1, 100);
thread t2(p2, 100);
t1.join();
t2.join();
return 0;
}
7.其他
1.范围for循环
2.final和override
3.智能指针、静态数组array、forward_list以及unordered系列