1 变量类型推导
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有 人去使用它。
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得,即可理解为一个类型占位符。
int TestAuto() {
return 10;
}
int main() {
int a = 10;
auto b = a; //编译器编译时自动识别为整形
auto c = 'a'; //自动识别为字符型
auto d = TestAuto(); //函数返回值为整形,编译器自动将d编译为整形
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto使用细节
//1. auto与指针和引用结合起来使用 用auto声明指针类型时,
//用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int TestAuto() {
return 10;
}
int main() {
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
int main() {
int x = 10;
auto a = &x;
auto* b = &x; //和上一行没有区别
auto& c = x; //和引用的结合使用
//typeid(a).name() 通过此操作可查看a的类型
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
//2. 在同一行定义多个变量 当在同一行声明多个变量时,这些变量必须是相同的类型,
//否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的
//类型定义其他变量。
void TestAuto() {
auto a = 1, b = 2;
//auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的类型不同
}
auto在以下场景不能使用
1. auto不能作为函数的参数
2. auto不能直接用来声明数组
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等 进行配合使用。
5. auto不能定义类的非静态成员变量
6. 实例化模板时不能使用auto作为模板参数
decltype
decltype主要用于推导表达式或函数返回值的类型
int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a+b) c;
cout<<typeid(c).name()<<endl;
return 0; }
void* GetMemory(size_t size) {
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0; }
2 基于范围的循环
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。
该for循环的意义:原来的for循环迭代范围要程序员自己确定,而此循环可以自己确定迭代的范围,更加简洁和安全。
void TestFor() {
int array[] = {
1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
使用注意:
- for循环的范围必须是确定的
- 迭代的对象要实现++和==的操作
3 指针空值nullptr
c++98中的指针空值:
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 //NULL被定义为整形的0
#else
#define NULL ((void *)0) //NULL 被定义为void* 类型的0
#endif
#endif
从以上可以看出,由于NULL的定义,0具有了空指针的含义,这在使用上有时会给我们带来一定的麻烦,而c++为了兼容c语言,也同时为了解决这个问题,并没有删除NULL,而是定义了一个新的指针空值即nullptr。nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
使用建议:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议好使用nullptr。
4 列表初始化
支持自定义类型的列表初始化,如:
// 标准容器
vector v{1,2,3,4,5};
map<int, int> m{
{1,1},{2,2},{3,3}};
5 新增加的几个关键字
final :禁止重写和继承虚函数。
override:用于检测是否重写了父类的虚函数,若没有报错。
delete:删除函数,将delete赋给函数,表示该函数无法使用。
default:在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
新增加的关键字还有与许多,这里就不一一介绍了
6智能指针
新增了unique_ptr 底层直接禁止了拷贝与赋值,防止指针悬空。
shared_ptr:采用引用计数法,引用计数为0时才真正释放资源。
7 新增容器
静态数组array:c++11新增的固定大小的数组,但功能更加强大,支持容器的一般功能,如迭代器,获取容量等。
用法如:std::array<int, 3> a = {1,2,3};
forward_list:单向链表,用法与一般容器类似
unordered_map/set:无序的关联式容器,底层哈希表实现,随机访问效率o(1),底层为键值对,用法与map,set类似。
8委派构造函数
即支持同一个类构造函数之间的调用,增加代码复用性
class Info{
public:
// 目标构造函数
Info(): _type(0), _a('a')
{
InitRSet();}
// 委派构造函数
Info(int type): Info()
{
_type = type;}
// 委派构造函数
Info(char a): Info()
{
_a = a;}
private:
void InitRSet(){
//初始化其他变量 }
private:
int _type = 0;
char _a = 'a';
}
9右值引用
10.1 移动语义
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将
会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
//A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
A a3(20);
a3 = a2;
return 0; }
class String
{
public:
String(char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) +1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
~String()
{
if (_str) delete[] _str;}
private:
char* _str;
};
假设现在有一个函数,返回值为一个String类型的对象:
String GetString(char* pStr) {
String strTemp(pStr);
return strTemp; }
int main()
{
String s1("hello");
String s2(GetString("world"));
return 0; }
上述代码看起来没有什么问题,但是有一个不太尽人意的地方:GetString函数返回的临时对象,将s2拷贝
构造成功之后,立马被销毁了(临时对象的空间被释放),再没有其他作用;而s2在拷贝构造时,又需要分配
空间,一个刚释放一个又申请,有点多此一举。那能否将GetString返回的临时对象的空间直接交给s2呢?
这样s2也不需要重新开辟空间了,代码的效率会明显提高。
将一个对象中资源移动到另一个对象中的方式,称之为移动语义。在C++11中如果需要实现移动语义,必须使用右值引用。
String(String&& s): _str(s._str)
{
s._str = nullptr; }
C++11中的右值
右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值和将亡值。
纯右值
纯右值是C++98中右值的概念,用于识别临时变量和一些不跟对象关联的值。比如:常量、一些运算表
达式(1+3)等
将亡值
声明周期将要结束的对象。比如:在值返回时的临时对象。
右值引用
右值引用书写格式:
类型&& 引用变量名字 = 实体;
右值引用最长常见的一个使用地方就是:与移动语义结合,减少无必要资源的开辟来提高代码的运行效率。
String&& GetString(char* pStr) {
String strTemp(pStr);
return strTemp; }
int main()
{
String s1("hello");
String s2(GetString("world"));
return 0; }
右值引用另一个比较常见的地方是:给一个匿名对象取别名,延长匿名对象的声明周期。
String GetString(char* pStr) {
return String(pStr);
}
int main()
{
String&& s = GetString("hello");
return 0; }
注意:
1. 与引用一样,右值引用在定义时必须初始化。
2. 通常情况下,右值引用不能引用左值。
int main()
{
int a = 10;
//int&& ra; // 编译失败,没有进行初始化
//int&& ra = a; // 编译失败,a是一个左值
// ra是匿名常量10的别名
const int&& ra = 10;
return 0; }
** std::move()**
C++11中,std::move()函数位于 <utility>头文件中,这个函数名字具有迷
惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用
,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其生明周
期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不
会被销毁。
// 移动构造函数:参数为右值引用类型
class String
{
//....
String(String&& s): _str(s._str)
{
s._str = nullptr; }
// ....
};
int main()
{
String s1("hello world");
String s2(move(s1)); //错误用法
String s3(s2); //错误用法,move更多的是用在声明周期即将结束的对象上
//如将左值参数转化为右值
return 0;
}
class Person
{
public:
Person(char* name, char* sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{
}
Person(const Person& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{
}
#if 0
Person(Person&& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{
}
#else
Person(Person&& p)
: _name(move(p._name)) //转化拥有的资源为右值,实现移动语义
, _sex(move(p._sex))
, _age(p._age)
{
}
#endif
private:
String _name;
String _sex;
int _age;
};
Person GetTempPerson()
{
Person p("prety", "male", 18);
return p; }
int main()
{
Person p(GetTempPerson());
return 0; }
注意:为了保证移动语义的传递,程序员在编写移动构造函数时,
最好使用std::move转移拥有资源的 成员为右值,如上
注意:
1. 如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,
都会导致移动语义无法实现。如下
String(const String&&);
const Person GetTempPerson();
完美转发
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。
C++11通过forward函数来实现完美转发, 比如:
void Fun(int &x){
cout << "lvalue ref" << endl;}
void Fun(int &&x){
cout << "rvalue ref" << endl;}
void Fun(const int &x){
cout << "const lvalue ref" << endl;}
void Fun(const int &&x){
cout << "const rvalue ref" << endl;}
template<typename T>
void PerfectForward(T &&t){
Fun(std::forward<T>(t));}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0; }
10lambda表达式
c++中我们使用sort函数时,经常需要实现一个仿函数来定义比较规则。
而为了简化这一做法,c++11增加lambda表达式来解决这一问题。
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type {
statement }
1. lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,
编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下
文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,
则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其
常量性。
使用该修饰符时,参数列表不可省略(即使参数为空)。
->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,
没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{
statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使
用所有捕获到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{
}; 该lambda函数不能做任何事情。
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{
};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{
return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){
b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{
return b += a+ c; };
cout<<fun2(10)<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable {
x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0; }
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无
法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
lambda底层实际上还是以仿函数的形式实现的。即一个类中重载()运算符
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值
传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编
译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
比特科技
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都
会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)();
int main()
{
auto f1 = []{
cout << "hello world" << endl; };
auto f2 = []{
cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0; }
11线程库thread
C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件,该头文件声明了std::thread 线程类。
线程的启动
C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程
运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID
等,所有操作全部封装在一起,最后在底层统一传递给_beginthreadex()
创建线程函数来实现 (注意:_beginthreadex是windows中创建线程的底层
c函数)。
std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数或
者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,
以及函数指针。
// 使用lambda表达式作为线程函数创建线程
int main()
{
int n1 = 500;
int n2 = 600;
thread t([&](int addNum){
n1 += addNum;
n2 += addNum;
}, 500);
t.join();
std::cout << n1 << ' ' << n2 << std::endl;
return 0; }
线程的结束
启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择:
加入式:join()
join():会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join(),当你调用的join()之后joinable()就将返回false了。
#include <iostream>
using namespace std;
#include <thread>
void foo()
{
this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
thread t(foo);
cout << "before join, joinable=" << t.joinable() << std::endl;
t.join();
cout << "after join, joinable=" << t.joinable()<< endl;
return 0; }
分离式:deatch()
detach:会从调用线程中分理出新的线程,之后不能再与新线程交互。
。分离的线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。
注意:必须在thread对象销毁之前做出选择,这是因为线程可能在你加入或分离线程之前,就已经结束了,之后如果再去分离它,线程可能会在thread对象销毁之后继续运行下去。一般分离式在线程入口函数处最开始定义。
原子性操作库(atomic)
为线程安全问题而生,也可以加锁解决。
原子变量库:#include
atomic_int: 即为int类的原子变量
atomic_char,atomic_short
总之只需要在内置类型前加上atomic_ 即可
原子变量多线程安全无需加锁