从C向C++20——C++11(1)
一.C++11特性
1.概述
C++不停的更新版本,现在以及更新到20多,每次版本系统都会封装不同的特性,这些特性在C++基础上进行实际开发有用,所以我们每一次版本更新都要拿学习一些新特性,这个是选取C++11和C++14版本的部分常用特性学习。
2.原始字符变量
string str1="D:\\text\\abc.cpp";
cout<<str1<<endl;
这是平时我们定义的字符串变量,如果需要\
必须使用\\
进行转义,但是C++11中添加了字符串原始字面量,可以替换成:
string str1=R"(D:\text\abc.cpp)";
cout<<str1<<endl;
这样可以省去了转义,直接输出想要的字符串。
最后强调一个细节:在R "xxx(raw string)xxx"
中,原始字符串必须用括号()
括起来,括号的前后可以加其他字符串(作为注释),所加的字符串会被忽略,并且加的字符串必须在括号两边同时出现。
3.指针空值类型
在C++中的NULL
默认值是0
void func(char* p)
{printf("char func run!\n");
}void func(int p)
{printf("int func run!\n");
}int main()
{func(NULL);
}
该代码的输出结果显示调用的是func(int p)
,这是因为在C++中NULL其实就是0的一个别名而已,默认是int
类型。
所以一般更多的是用nullptr
来代替NULL
,因为nullptr
可以进行隐式转换,比如:
int *p1=nullptr;
char *p2=nullptr;
double *p3=nullptr;
每一次后nullptr
都转化为对应的类型。
4.关键字constexpr
复习一下const
关键字的两个作用:修饰常量、变量可读。
void func(const int num)
{int a1=20;const int p = 20;int count[a1]; //错误,a1是变量int count[p]; //可以,p是常量
}
我们暂且把修饰常量称为一个常量表达式,C++11后添加了constexpr
关键字,用来修饰常量表达式。在以后的使用过程中,建议如果修饰函数的变量可读,使用const
关键字,修饰常量表达式则使用constexpr
关键字。
const int i=520;
constexpr int j=i+1;
这两个都是一个常量表达式。但需要注意,如果自定义的数据类型struct
和class
不能用constexpr
修饰,其更多的是在一个实例对象时修饰。
可以使用constexpr
修饰函数的返回值,这种函数被称作常量表达式函数,如何判断一个函数是不是常量表达式函数,根据其能否在编译阶段计算出返回值的值。常量表达式函数必须满足:
- 函数必须要有返回值,并且
return
返回的表达式必须是常量表达式 - 函数在使用之前,必须有对应的定义语句(如果在main函数调用常量表达式函数,其定义必须在main函数前面)
二.自动类型推导
1.auto推导
语法:
auto 变量名 = 变量值;
使用auto
声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto
占位符替换为真正的类型
- 当变量不是指针或者引用类型时,推导的结果中不会保留
const、volatile
关键字 - 当变量是指针或者引用类型时,推导的结果中会保留
const、volatile
关键字
auto关键字并不是万能的,在以下这些场景中是不能完成类型推导的:
- 不能作为函数参数使用
- 不能用于类的非静态成员变量的初始化
- 不能使用auto关键字定义数组
- 无法使用auto推导出模板参数
auto
的典型应用:
- 使用
suto
定义迭代器- 用于泛型编程
2.decltype关键字
语法:
decltype(exp) varname = value;
其中,varname
表示变量名,value 表示赋给变量的值,exp 表示一个表达式。
decltype
可以写成下面的形式:
decltype(exp) varname;
所以,一般来说,decltype
用来判断varname
变量的类型,判断依据是exp
表达式,与等号右边无关。
decltype
推导规则:
- 如果 exp 是一个不被括号
( )
包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么decltype(exp)
的类型就和 exp 一致,这是最普遍最常见的情况。 - 如果 exp 是函数调用,那么
decltype(exp)
的类型就和函数返回值的类型一致。 - 如果 exp 是一个左值,或者被括号
( )
包围,那么decltype(exp)
的类型就是 exp 的引用:假设 exp 的类型为 T,那么decltype(exp)
的类型就是 T&。
例如:
//推导1
decltype(Student::total) c = 0; //total 为类 Student 的一个 int 类型的成员变量,c 被推导为 int 类型//推导2
int&& func_int_rr(void); //返回值为 int&&
decltype(func_int_rr()) b = 0; //b 的类型为 int&&//推导3
class Base{
public:int x;
};//带有括号的表达式decltype(obj.x) a = 0; //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 intdecltype((obj.x)) b = a; //obj.x 带有括号,符合推导规则三,b 的类型为 int&。//加法表达式int n = 0, m = 0;decltype(n + m) c = 0; //n+m 得到一个右值,符合推导规则一,所以推导结果为 intdecltype(n = n + m) d = c; //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&
上面我们知道auto
不能用在模板中,我现在定义一个模板容器的遍历操作,就是decltype
的典型应用:
template <typename T>
class Base {
public:void func(T& container) {m_it = container.begin();}
private:decltype(T().begin()) m_it; //注意这里
};
3.auto与decltype的区别
「cv 限定符」是 const 和 volatile 关键字的统称:
- const 关键字用来表示数据是只读的,也就是不能被修改;
- volatile 和 const 是相反的,它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取。
- 1.语法格式是有区别的
- 2.auto 要求变量必须初始化,也就是在定义变量的同时必须给它赋值;而 decltype 不要求
- 3.auto 和 decltype 对 cv 限制符的处理是不一样的
三.左值和右值
1.左值和右值(C语言)
理解两种方式:
-
简单来说:赋值语句中=左边就是左值,=右边就是右值
int main() {int a; // 正确,定义一个变量aa = 10; // 正确,赋值表达式,a是一个左值,10是一个右值a + 1 = 10; // 错误,a + 1不能作为=号的左侧,它是不能作为左值的,10是可以作为一个右值的10 = a; // 错误,10不能作为左值,a是可以作为右值的a = a + 1; // 正确,a可以作为左值,a + 1可以作为右值... }
-
更加严谨的说法:左值(
lvalue
)指的是可以表示内存中一个存储空间的表达式,它是可以出现在赋值运算符(=
)的左侧的,比如一个变量的名称,它就是代表一个对象,该对象在内存中具有存储空间。... int main() {int a; // 定义了一个变量a,a在内存中有存储空间(栈内存),所以a是一个左值(lvalue => locator value,可以定位的一个内存对象)a = 10; // 因为a是一个左值,所以a可以放在赋值运算符的左侧... }
右值(
rvalue
),代表的是可以表示一个值的表达式,比如字面值(100
)、算术表达式(1 + 2
、a + 1
)、或者直接一个变量名(a
)、具有返回值的函数调用等等,只要是可以代表一个值的表达式可以是右值。
理解:
...
int main()
{int a; // 正确,定义一个变量a = 1; // 正确,a是代表的一个存储空间,可以用作左值,1代表的是一个值,可以用作右值。10 = a; // 错误,10是一个字面值常量,不是内存中的一个存储空间,不能作为左值,a作为右值没有问题a + 1 = 10; // 错误,a + 1是一个求值的表达式,该表达式代表的是a + 1的运算结果,是一个值,不是代表内存中的一个存储空间,所以a + 1不能作为左值,10可以作为右值a ++; // 正确,等价于a = a + 1;a = a + 1; // 正确,a是一个变量,代表的是内存中的一个存储空间,可以作为左值,而a + 1是一个求值表达式,求出来的是值,是一个右值,放在=的右侧没问题&a = 100; // 错误,&a代表的是变量a在内存中的地址,&a是一个常量,在程序运行过程中不会改变的值,本质上是&a没有存储空间的,或者理解为&a不是代表的内存中的一个存储空间,它只是代表了一个存储空间的地址而已,而并没有分配内存空间去存储这个地址值,它是一个常量...
}
2.左值引用和右值引用(C++)
首先C++中的左值和右值与C语言的概念差不多,而之前学习的C++引用一个对象更多的是称为左值引用
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x
在C++11之前就有引用**“&”**,但是此种引用有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。
int num = 10;
int &b = num; //正确
int &c = 10; //错误
编译器允许我们为 num
左值建立一个引用,但不可以为 10 这个右值建立引用。为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
int && a = 10;
a = 11;
cout << a << endl; //输出结果为11
- 右值引用可以修改右值
- C++语法之词定义常量右值引用
右值引用最常见的典型是移动语义。
3.未定引用类型的推导(万能引用)
在C++中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下 &&被称作未定的引用类型(万能引用)。另外还有一点需要额外注意const T&&
表示一个右值引用,不是未定引用类型。
- 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
- 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
- 万能引用发生在模板中
- 引用折叠时,任意有一个左值引用返回的结果就是左值引用,只有都为右值引用时,结果才为右值引用。
右值引用如果再次进行传递,结果是左值引用。
4.move函数
move函数的两个用法:
- 将左值转换为右值
- 进行移动构造
Test t;Test && v1 = t; // errorTest && v2 = move(t); // ok
list<string> ls1 = ls; // 需要拷贝, 效率低
list<string> ls2 = move(ls);
5.forward函数(完美转发)
作用:确保右值引用在传递过程中不发生改变
// 精简之后的样子
std::forward<T>(t);
当T为左值引用类型时,t将被转换为T类型的左值
当T不是左值引用类型时,t将被转换为T类型的右值
四.其他
1.返回值类型后置
在泛型编程中,可能需要通过参数的运算来得到返回值的类型。考虑下面这个场景:
template <typename R, typename T, typename U>
R add(T t, U u)
{return t+u;
}
int a = 1; float b = 2.0;
auto c = add<decltype(a + b)>(a, b);
我们并不关心 a+b 的类型是什么,因此,只需要通过 decltype(a+b)
直接得到返回值类型即可。但是像上面这样使用十分不方便,因为外部其实并不知道参数之间应该如何运算,只有 add 函数才知道返回值应当如何推导。
因此,在 C++11 中增加了**返回类型后置(trailing-return-type,又称跟踪返回类型)**语法,将 decltype 和 auto 结合起来完成返回值类型的推导。返回类型后置语法是通过 auto 和 decltype 结合起来使用的。上面的 add 函数,使用新的语法可以写成:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{return t + u;
}
2.final和override
C++中增加了final关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和Java的final关键字的功能是类似的。如果使用final修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。
如果使用final修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数;使用final关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类。注意final关键字写在函数或类的后面。
//修饰虚函数,基类已有
class Child : public Base
{
public:void test() final //后面的孙子类不能再重写test函数{cout << "Child class...";}
};//修饰类
class Child final: public Base //child类不能再有孩子
{
public:void test(){cout << "Child class...";}
};
override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和final一样这个关键字要写到方法的后面。
3.函数模板的默认参数
在C++98/03标准中,类模板可以有默认的模板参数;但是不支持函数的默认模板参数,在C++11中添加了对函数模板默认参数的支持:
优先级:指定>自动推导>默认参数
4.using起别名
在 C++中可以通过 typedef 重定义一个类型,语法格式如下:
typedef 旧的类型名 新的类型名;
// 使用举例
typedef unsigned int uint_t;
使用using定义别名的语法格式是这样的:
using 新的类型 = 旧的类型;
// 使用举例
using uint_t = int;
通过using和typedef的语法格式可以看到二者的使用没有太大的区别,假设我们定义一个函数指针,using的优势就能凸显出来了,看一下下面的例子:
// 使用typedef定义函数指针
typedef int(*func_ptr)(int, double);// 使用using定义函数指针
using func_ptr1 = int(*)(int, double);