1.类型推导
C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。
1.1auto关键字
在C++11标准的语法中,auto被定义为自动推断变量的类型
//未有新特性之前
for(vector<int>::const_iterator it=ves.begin();it!=ves.end();it++);
//使用auto新特性之后
for(auto it=ves.begin();it!=ves.end();it++);
//auto使用的例子
auto a=3.14;
auto b=new auto(10);
因此auto的出现大大减少了代码量,且简化了在C++编程中需要应对的各种复杂的变量类型书写。
auto的注意事项:
1.auto 不能用于函数传参
2.auto 还不能用于推导数组类型
3.C++11的auto关键字时有一个限定条件,那就是必须给申明的变量赋予一个初始值,否则编译器在编译阶段将会报错
1.2decltype关键字
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的,它可以使编译器自动分析表达式的类型并得到它的类型,最关键是它不会去计算表达式的值。
1. decltype + 变量
const int ci = 0,
// x的类型是const int
decltype(ci) x = 0;
2.decltype + 表达式
int i = 42, *p = &i, &r = i;
// r + 0是一个表达式
// 算术表达式返回右值
// b是一个int类型
decltype(r + 0) b;
// c是一个int &
decltype(*p) c = i;
//解引用运算符*作用于指针类型,得到了p指向的对象的左值(*p = 2很正确),p是指向int的指针,
//因此decltype(*p)得到的类型是int &。
int i = 42;
// 加了括号,变成了表达式
// 返回的是i的左值形式
// 因此ri的类型是int &
decltype((i)) ri = i;
3.decltype+函数
int& fun(int,char);//返回值为int&
decltype(fun(10,'c'))c1=n;//c1为int&
//decltype在调用函数是需要带上括号和参数,但是这仅仅是形式,并不会执行代码
C++中通过函数的返回值和形参列表,定义了一种名为函数类型的东西。它的作用主要是为了定义函数指针。
可以利用decltype简化对函数指针的定义
using FuncType=int(int &,int);
int add_to(int &des,int ori);
//声明一个FuncType类型的指针
//并使用函数add_to初始化
FuncType *pf1=add_to;
int d=4;
//通过函数指针调用add_to;
pf1(d,2);
//可利用decltype获得函数add_to类型
decltype(add_to) *pf2=add_to;
pf2(d,2);
decltype注意事项:
decltype可以作用于变量、表达式及函数名。
1.作用于变量直接得到变量的类型;
2.作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;
3.作用于函数名可变为函数的返回值。
4.可以通过函数名返回函数指针
5.decltype不会去真的求解表达式的值
1.3拖尾返回类型、auto 与 decltype 配合
//传统的C++定义有返回值类型的模板
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y;
}
//C++11后定义有返回值类型的模板
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
//C++14后定义有返回值类型的模板
template<typename T, typename U>
auto add(T x, U y) {
return x+y;
}
2. 初始化列表
在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、POD (plain old data,没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造函数、要么就需要使用 () 进行。这些不同方法都针对各自对象。在传统C++中可以使用初始化列表如下:
// 普通数组
int i_arr[3] = { 1, 2, 3 };
// POD类型:结构体
struct A
{
int x;
struct B
{
int i;
int j;
} b;
} a = { 1, { 2, 3 } };
// 拷贝初始化(copy-initialization)
int i = 0;
class Foo
{
public:
Foo(int) {}
Foo(const Foo &);
} foo = 123; // 需要拷贝构造函数
// 直接初始化:使用 () 进行
int j(0);
Foo bar(123);
在C++11的新特性中:
class Student{
public:
string name;
int age;
float scores;
Student(string name,int age,float scores){
this->name=name;
this->age=age;
this->scores=scores;
}
};
int main() {
//使用{},可以在生成类对象时对类中元素进行初始化
Student st1{"yang",11,11};
Student st2={"yang",11,11};
//使用{},可以对基础变量进行初始化
int a1{3};
int a2={3};
//使用{},直接对堆内存的变量初始化
int* a3=new int{111};
int* a4=new int[3]{1,11,111};
//使用{}可以对STL的容器初始化
vector<int>{1,2,3,4,5,6};
}
使用{}可以对类对象,基础变量,堆内存,STL的容器初始化,极大的简化了初始化相关的代码。
3. 模板增强
3.1 外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化。
C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化:
在一个test.h中定义一个模板
// test.h
template <typename T>
void fun(T) {}
在test1.cpp中调用该模板
// test1.cpp
#include "test.h"
template void fun<int>(int);
void test1()
{
fun(1);
}
在test2.cpp中也调用该模板
//test2.cpp
#include "test.h"
template void fun<int>(int);
void test2(){
fun(1);
}
由于两个源代码使用的函数模板的参数类型一致,所以在编译test1.cpp的时候,编译器实例化出了函数fun(int),而当编译test2.cpp的时候,编译器又再一次实例化出了函数fun(int)。那么可以想象,在test1.o目标文件和test2.o目标文件中,会有两份一模一样的函数fun(int)代码。
代码重复和数据重复不同。数据重复,编译器往往无法分辨是否是要共享的数据;而代码重复,为了节省空间,保留其中之一就可以了(只要代码完全相同)。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码**fun(int)**删除掉,只保留了单个副本。因此发生了重复编译的情况,所有这是需要使用外部模板,修改如下:
// test1.cpp
#include "test.h"
template void fun<int>(int); // 显式地实例化
void test1()
{
fun(1);
}
// test2.cpp
#include "test.h"
extern template void fun<int>(int); // 外部模板的声明
void test2()
{
fun(2);
}
因为test2.cpp使用了外部模板,test2.cpp会调用test1.cpp的实例化模板,因此外部模板的使用可以解决重复的编译过程。
3.2类型别名模板
在C++利用typedef来给函数起别名,在C++11中新增using也可以给函数起别名:
//对变量名起别名
typedef type-id identifier;
using identifier = type-id;
//对STL容器起别名
typedef std::map<int, std::string>::const_iterator map_const_iter;
map_const_iter iter;
using map_const_iter = std::map<int, std::string>::const_iterator;
map_const_iter iter;
//对函数指针起别名
typedef void(*func1)(int, int);
using func2 = void(*)(int, int);
而在C++11新增的using可以给函数模板起别名,这是typedef办不到的:
#include <iostream>
#include <map>
#include <string>
template <typename T>
using intmap = std::map<int, T>;
int main() {
intmap<std::string> intstring;
intstring[9] = "11";
std::cout << intstring[9] << std::endl;
return 0;
}
尽量少使用 using 指示 污染命名空间
一般说来,使用 using 命令比使用 using 编译命令更安全,这是由于它只导入了指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译命令导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。
3.3 默认模板参数
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y
}
//C++11新增默认模板参数可以给模板定义参数类型
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
4. 右值引用和move语义
C++ 中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
4.1左值引用和右值引用
左值引用避免对象的拷贝,可以作为函数传参,也可以作为函数返回值。
右值引用实现移动语义,实现完美转发
左值类型:变量名,返回左值引用的函数调用,前置自增,自减(++i,–i),解引用表达式*p是左值,字符串字面值是左值,具名的右值引用是左值。
右值类型:字面值,返回非引用类型的函数调用,后置自增,自减(i++,i–),算数表达式,逻辑表达式,比较表达式,取地址表达式&a是纯右值,还有将亡值(C++11新引用的右值引用(移动语言)相关的类型)。不具名的右值引用是右值。
将亡值:可以理解为通过盗取其他变量内存空间的方式获取到的值,在确保其他变量不再被使用或即将被销毁时,通过盗取的方式可以避免内存空间的释放与分配,能够延长变量的生命期(通过右值引用来续命)用来触发移动构造或移动赋值构造,并将资源转移之后调用析构函数。
class A {
public:
int a;
};
A getTemp()
{
return A();
}
A && a = getTemp(); //getTemp()的返回值是右值(临时变量)
const左值引用能指向右值,局限是不能修改这个值
引用是别名,声明必须要初始化,通过引用改变变量值。
std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
4.2右值引用的移动语言和完美转发
4.2.1移动语言
在没有右值引用时需要使用拷贝构造函数来进行类对象的信息的转移
A(const A&a){
p=new int(10);
memcpy(p,a.p,10*sizeof(int));//将a对象中的内容拷贝到新构建的函数中
}
在使用C++11的新特性后
A(A&& a){
this->p=a.p;//将a的内容直接转移到新对象中
a.p=nullptr;
}
这就是右值引用移动语言的作用,可以在对象赋值时,避免资源的重新分配,实现移动构造和移动拷贝构造,将对象的信息直接转移到新对象中。
4.2.2完美转发
定义:函数模板可以将自己的参数完美地转发给内部调用的其他函数,完美是指不仅能准确地转发参数的值,还能保证被转发的参数的左右值属性不变。
使用std::forward函数,当在该函数中输入左值,输出也为左值,输入右值,输出也为右值。
void func(int &n){
cout<<"lvalue="<<n<<endl;
}
void func(int &&n){
cout<<"rvalue="<<n<<endl;
}
template<typename T>
void revoke(T &&t){
func(std::forward<T>(t));
}
static_cast<T &>(m);强转为左值类型
static_cast<T &&>(m);强转为右值类型
也可以通过static_cast<T &>(m)和static_cast<T &&>(m)强转为左值类型和右值类型。
5. 智能指针
在C++11中通过引入智能指针的概念,使得C++程序员不需要手动释放
智能指针的种类
std::unique_ptr
std::shared_ptr
std::weak_ptr
注意:std::auto_ptr已被废弃
C++的指针包括两种
原始指针 智能指针
智能指针是原始指针的封装,其优点是会自动分配内存,不用担心潜在的内存泄露。
独占指针:unique_ptr
在任何给定的时刻,只能有一个指针管理内存
当指针超出作用域,内存将自动释放
该类型指针不可Copy,只可以Move
三种创建方式:
1.通过已有裸指针创建(创建后要将裸指针设置为空且销毁,否则无法满足唯一的条件)
2.通过new来创建
3.通过std::make_unique创建(推荐)
unique_ptr可以通过get()获取地址
unique_ptr实现了->与*
可以通过->调用成员函数
可以通过*调用来调用指针里的值
#include <iostream>
#include <memory>
#include "cat.h"
using namespace std;
int main(int argc,char *argv[]){
/*Cat c1("OK");
c1.cat_info();
{
Cat c1("OK");
c1.cat_info();
}
//原始指针
Cat *c_p1=new Cat("yy");
int *i_p1=new int(100);
c_p1->cat_info();
{
i_p1=new int(200);
Cat *c_p1=new Cat("yy");
c_p1->cat_info();
delete c_p1;//需要手动删除
delete i_p1;
}
delete c_p1;//调用会调用析构函数
//delete i_p1;这里再次调用delete相当于delete两次,不安全。*/
//unique_point的创建方式
Cat *c_p2=new Cat("yz");
//1.使用裸指针
std::unique_ptr<Cat> u_c_p2(c_p2);
//c_p2还能使用
c_p2->cat_info();
u_c_p2->cat_info();
c_p2->set_cat_name("ok");//如果c_p2改变,u_c_p2也会改变
u_c_p2->cat_info();
//所以在使用裸指针调用函数时,建议调用后销毁裸指针
c_p2=nullptr;
delete c_p2;
//2.使用new
std::unique_ptr<Cat>u_c_p3{new Cat("dd")};
u_c_p3->cat_info();
u_c_p3->set_cat_name("00");
u_c_p3->cat_info();
//3.std::make_unique(推荐方式)
std::unique_ptr<Cat>u_c_p4=make_unique<Cat>("yy");
std::unique_ptr<int>u_i_p4=make_unique<int>(200);
cout<<"u_i_p4的地址"<<u_i_p4.get()<<endl;
u_c_p4->cat_info();
u_c_p4->set_cat_name("aa");
u_c_p4->cat_info();
//unique_ptr是不可Copy,只可以Move
cout<<"------yz------"<<endl;
return 0;
}
#include <iostream>
#include <memory>
#include "cat.h"
using namespace std;
void do_with_cat_pass_value(std::unique_ptr<Cat> c){
c->cat_info();
}
void do_with_cat_pass_ref(const std::unique_ptr<Cat>&c){
c->set_cat_name("00");
c->cat_info();
//c.reset();
}
std::unique_ptr<Cat> get_unique_ptr(){
std::unique_ptr<Cat>p_dog=std::make_unique<Cat>("Local cat");
cout<<"unique address:"<<p_dog.get()<<endl;
cout<<"unique adddress"<<&p_dog<<endl;
return p_dog;
}
int main(int argc,char *argv[]){
//1.pass value
//需要用std::move来转移内存拥有权
//如果参数直接传入std::make_unique语句自动转换为move
std::unique_ptr<Cat> c1=make_unique<Cat>("ff");
//do_with_cat_pass_value(c1);会报错,因为传入的为一个右值
do_with_cat_pass_value(std::move(c1));
do_with_cat_pass_value(std::make_unique<Cat>());
//打印完成后就直接析构了,因为它变成了右值
//2.pass ref
//如果she'zh
//不加设置参数为const则不能改变指向
//.比方说reset();
//reset()方法为智能指针清空方法
std::unique_ptr<Cat>c2=make_unique<Cat>("ff");
do_with_cat_pass_ref(c2);
cout<<c2.get()<<endl;
c2->cat_info();
//3.Return by value
//指向一个local object
//可以做链式函数
get_unique_ptr()->cat_info();
cout<<"------yz------"<<endl;
return 0;
}
共享指针:shared_str
shared_ptr 计数指针又称共享指针
与unique_ptr不同的是它是可以共享数据的
share_ptr创建一个计数器与类对象所指的内存相关联
Copy则计数加一,销毁则计数减一
api为use_count()
#include <iostream>
#include <memory>
#include "cat.h"
using namespace std;
int main(int argc,char *argv[]){
//常量类型
std::shared_ptr<int>i_p_1=make_shared<int>(10);
cout<<"value:"<<i_p_1<<endl;
cout<<"use count:"<<i_p_1.use_count()<<endl;
//copy
std::shared_ptr<int> i_p_2=i_p_1;
cout<<"use count:"<<i_p_1.use_count()<<endl;
cout<<"use count:"<<i_p_2.use_count()<<endl;
*i_p_2=30;
cout<<"value:"<<i_p_1<<endl;
cout<<"value:"<<i_p_2<<endl;
std::shared_ptr<int>i_p_3=i_p_1;
i_p_1=nullptr;
cout<<"use count:"<<i_p_1.use_count()<<endl;
cout<<"use count:"<<i_p_2.use_count()<<endl;
cout<<"use count"<<i_p_3.use_count()<<endl;
//共有三个指针指向这个地址,无论清空哪一个指针,对其他的指针没有影响,use_count-1.
//自定义类型
std::shared_ptr<Cat> c_p_1=make_shared<Cat>();
cout<<"c_p_1 use count"<<c_p_1.use_count()<<endl;
std::shared_ptr<Cat> c_p_2=c_p_1;
std::shared_ptr<Cat> c_p_3=c_p_1;
cout<<"c_p_1 use count"<<c_p_1.use_count()<<endl;//只析构了一次
c_p_1.reset();
cout<<"c_p_1 use count"<<c_p_1.use_count()<<endl;
cout<<"c_p_2 use count"<<c_p_2.use_count()<<endl;
cout<<"c_p_3 use count"<<c_p_3.use_count()<<endl;//只有没有任何指针指向它的时候,这个对象才会销毁
cout<<"------yz------"<<endl;
return 0;
}
//share_ptr passed by value
//copy
//函数计数器加一
//share_ptr passed by ref
//const表示不可改变指向,调用const就不能调用reset()函数来修改指针的值
//returning by value
//链表调用
#include <iostream>
#include <memory>
#include "cat.h"
using namespace std;
void cat_by_value(std::shared_ptr<Cat> cat){
cout<<cat->get_name()<<endl;
cat->set_cat_name("ee");
cout<<"func use count:"<<cat.use_count()<<endl;
cout<<"func address:"<<cat.get()<<endl;
}
void cat_by_ref(std::shared_ptr<Cat> &cat){
cout<<cat->get_name()<<endl;
cat.reset(new Cat());
cout<<"reffunc use count:"<<cat.use_count()<<endl;
cout<<"c1 address:"<<cat.get()<<endl;
}
std::shared_ptr<Cat> get_shared_ptr(){
std::share_ptr<Cat> cat_p=std::make_shared<Cat>("loacl cat");
return cat_p;
}
int main(int argc,char *argv[]){
std::shared_ptr<Cat> c1=make_shared<Cat>("dd");
cat_by_value(c1);
c1->cat_info();
cout<<"c1 use count:"<<c1.use_count()<<endl;//不需要使用std::move转换,相当于创建了一个新的地址指向它
cout<<"c1 address:"<<c1.get()<<endl;
cat_by_ref(c1);
c1->cat_info();
cout<<"c1 use count:"<<c1.use_count()<<endl;//使用引用count没有增加.
cout<<"c1 address:"<<c1.get()<<endl;
std::shared_ptr<Cat> c_p=get_shared_ptr();
c_p->cat_info();
get_shared_ptr()->cat_info();//两种是一样的
cout<<"-------yz-------"<<endl;
return 0;
}
6. Lambda表达式
Lambda 的组成结构与函数很相似,它拥有一个返回类型,一个形参列表,一个函数体。Lambda 也可以定义在函数内部。它的组成结构如下:
捕获列表:在C ++规范中也称为Lambda导入器, 捕获列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。
- [] 不捕获任何变量。
- [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
- [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望 lambda 表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
- [=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
- [bar] 按值捕获 bar 变量,同时不捕获其他变量。
- [this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量。
参数列表:与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
可变规格*:mutable修饰符, 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明 lambda 表达式为 mutable。被 mutable 修饰的 lambda 表达式就算没有参数也要写明参数列表。
异常说明:用于Lamdba表达式内部函数抛出异常。
返回类型: 追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
lambda函数体:内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。、
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val){
if(!(val & 1)){
++ even_count;
}
});
std::cout << "The number of even is " << even_count << std::endl;