目录
1、引言
2、C++11 新增关键字详解
2.1、auto
2.2、override
2.3、final
2.4、nullptr
2.5、使用=delete阻止拷贝类对象
2.6、decltype
2.7、noexcept
2.8、constexpr
2.9、static_assert
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html C++11新特性很重要,作为C++开发人员很有必要去学习,不仅笔试面试时会涉及到,开源代码中会大规模的使用。以很多视频会议及直播软件都在使用的开源WebRTC项目为例,WebRTC代码中大篇幅地使用了C++11及以上的新特性,要读懂其源码,必须要了解这些C++的新特性。所以,接下来一段时间我将结合工作实践,给大家详细讲解一下C++11的新特性,以供借鉴或参考。
为了提升C++语言的灵活性和效率,C++11引入了多个关键字,比如auto、overide、final、nullptr、decltype、constexpr、noexcept、static_assert等。本文结合编码实战,主要介绍一下C++11中引入的常用新关键字。
C++11及以上新标准引入了很多新特性,使C++变得更加灵活,但也使得C++的特性变得更加臃肿,使C++变得更加难以驾驭。
编程时常常需要将表达式的值赋值给变量,这要求在声明变量时清楚地指出表达式的类型,但有时很难确定。C++11标准引入了auto类型说明符,用它能让编译器替我们去分析表达式所属的类型。auto变量必须要初始化,因为编译器要通过初始值来推算该auto变量的类型。
自动类型推导,用于从初始化表达式中推断出变量的数据类型(实际上是在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响)。示例如下:
auto i = 2; // int类型
auto d = 1.0; // double类型
auto str = "hello word"; // const char*
auto ch = 'A'; // char类型
auto func = less(); // 函数指针
vector vtList;
auto iter = vtList.begin(); // 选代器类型
auto p = new foo(); // 自定义类型
派生类在重写基类的虚函数时,可以在函数前添加virtaul标记,这样我们看到这个标识后就知道该函数是重写基类的虚函数了。在C++11引入override关键字之后,我们就可以使用override来更明显标识重写的函数。
给派生类的函数添加override标识的好处是,一方面使程序员重写基类的意图更加清晰,另一方面让编译器发现一些错误,如果我们用override标识了派生类的某个函数,但该函数没有覆盖基类的虚函数,则编译器会报错。
class Base
{
public:
virtual void func() const
{
cout << __func__ << std::endl;
}
}
class Derived :public Base
{
public:
virtual void func() overide
{
cout << __func__ << std::endl;
}
}
有时我们需要定义这样一种类,我们不希望其他类继承它。或者不想考虑它是否适合做一个基类。为了实现这一目的,C++11引入了一个用来阻止继承的关键字final,将该关键字放到类名之后,即表示该类不能被继承。示例如下:
class Base{ /* */};
class Last final : public Base { /* */};
还可以用该关键字去修饰一个类的成员函数时,来阻止该函数被派生类重写:
class Base
{
public:
virtual void func() const
{
cout << __func__ << std::endl;
}
}
class Derived :public Base
{
public:
virtual void func() overide final
{
cout << __func__ << std::endl;
}
}
nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0。最好使用字面值nullptr去初始化一个指针,表示当前是空指针。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。
为什么说nullptr可以解决NULL的二义性呢?可以来看个实例:
void func( int );
void func( int* );
比如上面的两个重载函数,我们要调用func函数,如果传入NULL(NULL实际上代表的是0,可以隐式转换成void*,进而转成int*),两个函数感觉都能调进去,编译器不知道调用的是哪个,这样就产生了二义性,编译时会报错。而传nullptr参数时,编译就不报错了,就比较明确了,调用的就是void func( int* )。
nullptr_t是变量类型,其值就是nullptr,可以看看nullptr_t的定义:
#ifdef __cplusplus
namespace std
{
typedef decltype(__nullptr) nullptr_t; // nullptr_t被声明为__nullptr的类型
}
using ::std::nullptr_t;
#endif
如果函数参数中有nullptr_t时,不用指定变量名的,直接在函数体中对应的参数设置成nullptr就可以了。以shared_ptr智能指针的某个构造函数为例:(代码来源Visual C++中的智能指针的源码实现)
template,
_Can_call_function_object<_Dx&, nullptr_t&>
>, int> = 0>
shared_ptr(nullptr_t, _Dx _Dt, _Alloc _Ax) // 参数类型为nullptr_t,对应的值就是nullptr
{ // construct with nullptr, deleter, allocator
Setpda(nullptr, _STD move(_Dt), _Ax);
}
其实这个地方的=delete不是关键字!
在C++11新标准中,我们通过将拷贝构造函数和拷贝复制函数定义为删除的函数来阻止拷贝。在函数后面加上=delete,函数就变成了删除的函数。对于删除的函数,我们虽然声明了它,但不能用任何方式使用它。
以智能指针类std::unique_ptr为例,该智能指针类不支持拷贝构造和赋值操作,主要支持所有权移动操作。所以在类的定义中,将该类的拷贝构造函数和赋值函数设置为=delete函数,用来阻止拷贝构造和赋值:(这是一个经典的面试题:std::unique_ptr智能指针是如何禁止拷贝和复制的?)
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
我们不能通过不实现拷贝构造函数和赋值函数去实现这个目标,因为当我们不实现这两个函数时,编译器会自动帮我们生成默认的拷贝构造函数和赋值函数。
在没有C++的=delete的标识之前,可以将拷贝构造函数和赋值函数设置为private的,这样在类外部不能使用这两个函数。
有时希望通过表达式的类型推断出要定义的变量的类型,但不想用该表达式的值来初始化变量去推断变量的类型,为了满足这一要求,C++11标准引入了decltype类型指示符,它的作用是推断操作数的数据类型并返回,编译器只分析推断表达式的类型,但不去计算表达式的值(不对表达式求值)。
const int nVal = 0;
decltype(nVal) y = 1;
decltype(t + u); // 其中t和u是数据类型,不对表达式t+u进行求值,只去推断t+u表达式的数据类型。
类型说明符生成指定表达式的类型,编译时根据表达式的类型去推导出类型,示例如下:
int i;
struct A
{
double x;
};
const A* a = new A();
decltype(i) x2; // int
dec1type (a->x) x3; // double
dec1type((a->x)) x4; // double&
在C++11标准中,提供了noexcept关键字,用来指定某个函数不抛出异常。将该关键字放到函数的参数列表之后,如下:
void func() noexcept; // 这里noexcept作为修饰符
对于用户及编译器来说,预先知道某个函数不抛出异常很有好处。首先,知道函数不会抛出异常有助于简化调用该函数的代码;其次,如果编译器确认 函数不抛出异常,它就能执行某些特殊的优化操作,而这些优化操作并不适用于可能出错的代码。
可能会出现一种情况,虽然函数使用noexcept关键字声明不抛出异常,但实际上函数内部还是抛出了异常。一旦一个noexcept函数抛出了异常,程序就会调用std::terminate()终止程序,以确保不在运行时抛出异常的承诺。
指明某个函数不抛出异常,这样调用者不必考虑如何处理异常了,无论是函数确实不抛出异常,还是抛出异常后被强行终止,调用者都无需为此负责。
在C++98中,用throw()来声明不抛出异常,throw(异常类型)声明可能抛出的异常类型。noexcept效率比throw更高一些,因为编译器可以用std::terminate()来终止程序运行,而throw异常机制会有一些额外开销,如函数栈依次展开并析构自动变量。
在程序进程的内存不足时,new操作会抛出bad_alloc异常,内存分配失败,这个问题我们以前讲过,处理办法是:在new时传如一个std::nothrow参数,让new在申请不到内存时不要抛出异常,直接返回为NULL,这样我们就可以通过返回的地址是否为NULL(空),判断是否是内存申请失败了,代码如下:
#include
int main(){
char *p = NULL;
int i = 0;
do{
p = new(std::nothrow) char[10*1024*1024]; // 每次申请10MB
i++;
Sleep(5);
}
while(p);
if(NULL == p){
std::cout << "分配了 " << (i-1)*10 << " M内存" //分配了 1890 Mn内存第 1891 次内存分配失败
<< "第 " << i << " 次内存分配失败";
}
return 0;
}
当然,程序进程内存不足时,业务没法正常展开和执行了,让程序还活着,其实也没多大意义了。
指值不会改变并且在编译过程就能得到计算结果的表达式。 C++11 新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量是否是一个常量表达式。声明为constexpr 的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf= 20; // 20 是常量表达式
constexor int limit = mf + 1; // mf + 1 是常量表达式
constexor int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的语句
constexpr也可以修饰函数,一旦函数声明为constexpr,则函数的返回值类型及所有形参的类型都必须是字面值类型,而且函数体中有且只能有一条retrun语句。
constexpr int new_sz(){ retrun 20; };
static_assert用来做编译期间的断言,因此叫做静态断言。语法如下:
static_assert(常量表达式,提示字符串)
比如:
static_assert(sizeof(int) < sizeof(unsigned int), "int is not smaller than unsigned int");
如果第一个参数常量表达式的值为真(true或者非零值),那么static_assert不做任何事情,就像它不存在一样,否则会产生一条编译错误,错误位置就是该static_assert语句所在行,错误提示就是第二个参数提示字符串。
使用static_assert,我们可以在编译期间发现更多的错误,用编译器来强制保证一些契约,并帮助我们改善编译信息的可读性。