sunwengang blog

developer | android display/graphics

  1. 1. C++11被弃用的主要特性
    1. 1.1. char修饰符
    2. 1.2. 智能指针之unique_ptr
      1. 1.2.1. move()函数
      2. 1.2.2. 函数传参
      3. 1.2.3. make_unique()函数创建对象
      4. 1.2.4. unique_ptr 类的成员函数
    3. 1.3. register关键字弃用
    4. 1.4. bool类型的++操作弃用
    5. 1.5. noexcept
    6. 1.6. C语言风格的类型转换被弃用
      1. 1.6.1. static_cast关键字
      2. 1.6.2. const_cast关键字
      3. 1.6.3. reinterpret_cast关键字
      4. 1.6.4. dynamic_cast关键字
  2. 2. 常量nullptr
  3. 3. 常量constexpr
  4. 4. if/switch变量声明强化
  5. 5. decltype
  6. 6. if constexpr
  7. 7. 区间for迭代
  8. 8. 外部模板
  9. 9. 参考

C++11/14/17/20部分新的特性语法

C++11被弃用的主要特性

char修饰符

不再允许字符串字面值常量赋值给一个char *。如果需要用字符串字面值常量赋值和初始化一个char *,应该使用const char * 或者auto

char *str = "hello world!"; // 将出现弃用警告

    • auto,不指定变量类型,编译器将把变量的类型设置成和初始值相同
    • const,限定符

PS:另一个限定符Volatile关键词,易变的。所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。


智能指针之unique_ptr

auto_ptr被弃用,应使用unique_ptr

C++11提供了3种智能指针类型,它们分别由unique_ptr类、shared_ptr类和weak_ptr 类定义,所以又分别称它们为独占指针、共享指针和弱指针(均定义在头文件#include <memory>

智能指针是一个可以像指针一样工作的对象,但是当它不再被使用时,可以自动删除动态分配的内存。智能指针背后的核心概念是动态分配内存的所有权。

智能指针背后的核心概念是动态分配内存的所有权。智能指针被称为可以拥有或管理它所指向的对象。当需要让单个指针拥有动态分配的对象时,可以使用独占指针。对象的所有权可以从一个独占指针转移到另一个指针,其转移方式为:对象始终只能有一个指针作为其所有者。当独占指针离开其作用域或将要拥有不同的对象时,它会自动释放自己所管理的对象。

共享指针将记录有多少个指针共同享有某个对象的所有权。当有更多指针被设置为指向该对象时,引用计数随之增加;当指针和对象分离时,则引用计数也相应减少。当引用计数降低至0时,该对象被删除。

智能指针实际上是一个对象,在对象的外面包围了一个拥有该对象的普通指针。这个包围的常规指针称为裸指针。

move()函数

C++ 提供了一个move()库函数,可用于将对象的所有权从一个独占指针转移到另外一个独占指针:

1
2
3
4
5
unique_ptr<int> uptr1(new int); //定义并初始化
*uptr1 = 15; //赋值
unique_ptr<int> uptr3; // 正确
uptr3 = move (uptr1) ; // 将所有权从 uptr1 转移到 uptr3
cout << *uptr3 << endl; // 打印 15

函数传参

不能直接通过值给函数传递一个智能指针,因为通过值传递将导致复制真正的形参。如果要让函数通过值接收一个独占指针,则在调用函数时,必须对真正的形参使用move()函数:

结果将打印来自于函数fun()中的10

1
2
3
4
5
6
7
8
9
10
11
//函数使用通过值传递的形参
void fun(unique_ptr<int> uptrParam)
{
cout << *uptrParam << endl;
}
int main()
{
unique_ptr<int> uptr(new int);
*uptr = 10;
fun (move (uptr)); // 在调用中使用 move
}

如果通过引用传递的方式,那就不必对真正的形参使用move()函数了。示例代码如下:

结果将打印数字15:

1
2
3
4
5
6
7
8
9
10
11
//函数使用通过引用传递的值
void fun(unique_ptr<int>& uptrParam)
{
cout << *uptrParam << endl;
}
int main()
{
unique_ptr<int> uptr(new int);
*uptr1 = 15;
fun (uptr1) ; //在调用中无须使用move
}

make_unique()函数创建对象

从C++14开始,有一个库函数make_unique<T>()可用于创建unique_ptr对象。该函数分配一个类型为T的对象,然后返回一个拥有该对象的独占指针。例如:

unique_ptr<int> uptr(new int);

现在可以替换成:

unique_ptr<int> uptr = make_unique<int>();

unique_ptr 类的成员函数

unique_ptr成员函数 描 述
reset() 销毁由该智能指针管理的任何可能存在的对象。该智能指针被置为空
reset(T* ptr) 销毁由该智能指针当前管理的任何可能存在的对象。该智能指针继续控制由裸指针ptr指向的对象
get() 返回该智能指针管理的由裸指针指向的对象。如果某个指针需要传递给函数,但是 该函数并不知道该如何操作智能指针,则get()函数非常有用

register关键字弃用

register关键字被弃用,可以使用但不再具备任何实际含义

bool类型的++操作弃用

bool类型的++操作被弃用

noexcept

C++98异常说明、unexpected_handler、set_unexpected() 等相关特性被弃用,应该使用noexcept

C++11新标准引入的noexcept运算符,可以用于指定某个函数不抛出异常。预先知道函数不会抛出异常有助于简化调用该函数的代码,而且编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作。

1
2
void func(int x) noexcept;  //不抛出异常
void func1(int x); //抛出异常

noexcept可以接受一个可选的实参,该参数必须能转换为bool类型:

1
2
void func(int x) noexcept(true);  //不抛出异常
void func(int x) noexcept(false); //抛出异常

C语言风格的类型转换被弃用

即在变量前使用(convert_type),应该使用static_castreinterpret_castconst_castdynamic_cast来进行类型转换

为了使潜在风险更加细化,使问题追溯更加方便,使书写格式更加规范,C++对类型转换进行了分类,并新增了四个关键字来予以支持,它们分别是:

关键字 说明
static_cast 用于良性转换,一般不会导致意外发生,风险很低
const_cast 用于const与非const、volatile与非volatile之间的转换
reinterpret_cast 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的C++类型转换
dynamic_cast 借助 RTTI,用于类型安全的向下转型(Downcasting)

这四个关键字的语法格式都是一样的:

xxx_cast<newType>(data)

老式的C语言转换类型(已弃用):

1
2
double scores = 95.5;
int n = (int)scores;

C++新风格:

1
2
double scores = 95.5;
int n = static_cast<int>(scores);

static_cast关键字

static_cast 是“静态转换”的意思,也就是在编译期间转换,转换失败的话会抛出一个编译错误。

static_cast(静态转换)只能用于良性转换,转换风险较低,例如:

  • 原有的自动类型转换,例如 short 转 int、int 转 double、const 转非 const、向上转型等;
  • void指针和具体类型指针之间的转换,例如void *int *char *void *等;
  • 有转换构造函数或者类型转换函数的类与其它类型之间的转换,例如doubleComplex(调用转换构造函数)、Complexdouble(调用类型转换函数)

static_cast不能用于无关类型之间的转换,因为这些转换都是有风险的,例如:

  1. 两个具体类型指针之间的转换,例如int *double *Student *int *等。不同类型的数据存储格式不一样,长度也不一样,用A类型的指针指向B类型的数据后,会按照A类型的方式来处理数据:如果是读取操作,可能会得到一堆没有意义的值;如果是写入操作,可能会使 B 类型的数据遭到破坏,当再次以 B 类型的方式读取数据时会得到一堆没有意义的值。
  2. int和指针之间的转换。将一个具体的地址赋值给指针变量是非常危险的,因为该地址上的内存可能没有分配,也可能没有读写权限,恰好是可用内存反而是小概率事件
  3. static_cast也不能用来去掉表达式的const修饰和volatile修饰。换句话说,不能将const/volatile类型转换为非const/volatile类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    #include <iostream>
#include <cstdlib>
using namespace std;
class Complex{
public:
Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
operator double() const { return m_real; } //类型转换函数
private:
double m_real;
double m_imag;
};
int main(){
//下面是正确的用法
int m = 100;
Complex c(12.5, 23.8);
long n = static_cast<long>(m); //宽转换,没有信息丢失
char ch = static_cast<char>(m); //窄转换,可能会丢失信息
int *p1 = static_cast<int*>( malloc(10 * sizeof(int)) ); //将void指针转换为具体类型指针
void *p2 = static_cast<void*>(p1); //将具体类型指针,转换为void指针
double real= static_cast<double>(c); //调用类型转换函数

//下面的用法是错误的
- float *p3 = static_cast<float*>(p1); //不能在两个具体类型的指针之间进行转换
- p3 = static_cast<float*>(0X2DF9); //不能将整数转换为指针类型
return 0;
}

const_cast关键字

const_cast用来去掉表达式的const修饰或volatile修饰。换句话说,const_cast就是用来将const/volatile 类型转换为非const/volatile类型。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
int main(){
const int n = 100;
//&n用来获取n的地址,它的类型为const int *,必须使用const_cast转换为int *类型后才能赋值给 p
//由于p指向了n,并且n占用的是栈内存,有写入权限,所以可以通过p修改n的值
int *p = const_cast<int*>(&n);
*p = 234;
cout<<"n = "<<n<<endl;
cout<<"*p = "<<*p<<endl;
return 0;
}

reinterpret_cast关键字

reinterpret_cast 这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高

不建议使用

dynamic_cast关键字

dynamic_cast用于在类的继承层次之间进行类型转换,它既允许向上转型(Upcasting),也允许向下转型(Downcasting)。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转型的前提必须是安全的,要借助RTTI进行检测,所有只有一部分能成功。

dynamic_cast与static_cast是相对的,dynamic_cast是“动态转换”的意思,static_cast是“静态转换”的意思。dynamic_cast会在程序运行期间借助 RTTI进行类型转换,这就要求基类必须包含虚函数;static_cast在编译期间完成类型转换,能够更加及时地发现错误。

dynamic_cast的语法格式为:

dynamic_cast <newType> (expression)


常量nullptr

nullptr出现的目的是为了替代NULL。在某种意义上来说,传统C++会把NULL、0视为同一种东西,这取决于编译器如何定义NULL,有些编译器会将NULL定义为((void*)0),有些则会直接将其定义为0

C++不允许直接将void *隐式转换到其他类型。但如果编译器尝试把NULL定义为((void*)0)

例如:

1
2
3
4
5
6
7
8
void foo(char*);
void foo(int);

//无法通过编译,将会去调用 foo(int),从而导致代码违反直觉
foo(NULL);

//调用foo(char*)
foo(nullptr);

为了解决这个问题,C++11引入了nullptr关键字,专门用来区分空指针、0。

而nullptr的类型为nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

因此NULL不同于0与nullptr。所以,请养成直接使用nullptr的习惯


常量constexpr

C++本身已经具备了常量表达式的概念,比如1+2,3*4这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式

**

if/switch变量声明强化

在传统C++中,变量的声明虽然能够位于任何位置,甚至于for语句内能够声明一个临时变量int,但始终没有办法在if和switch语句中声明一个临时的变量。

C++17消除了这一限制,使得我们可以在if(或switch)中完成这一操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> vec = {1, 2, 3, 4};

//在c++17之前
const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
if (itr != vec.end()) {
*itr = 3;
}
// 需要重新定义一个新的变量
const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);
if (itr2 != vec.end()) {
*itr2 = 4;
}

//*****************
//C++17
// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
.....
}

decltype

decltype关键字是为了解决auto关键字只能对变量进行类型推导的缺陷而出现的。它的用法和typeof很相似:

decltype(表达式)

有时候,我们可能需要计算某个表达式的类型,例如:

1
2
3
auto x = 1;
auto y = 2;
decltype(x+y) z;

if constexpr

C++11引入了constexpr关键字,它将表达式或函数编译为常量结果。

如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?

C++17将constexpr这个关键字引入到if语句中,允许在代码中声明常量表达式的判断条件

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
//函数模板
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}

int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}

在编译时,实际代码会表现为:

1
2
3
4
5
6
7
8
9
10
11
12
int print_type_info(const int& t) {
return t + 1;
}

double print_type_info(const double& t) {
return t + 0.001;
}

int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}

区间for迭代

C++11引入了基于范围的迭代写法,我们能够写出像Python一样简洁的循环语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4};
//std::find可用于查找容器中是否存在某个特定值
if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;

for (auto element : vec)
std::cout << element << std::endl; // read only
for (auto &element : vec) {
element += 1; // writeable
}

for (auto element : vec)
std::cout << element << std::endl; // read only
}

外部模板

C++模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。

传统C++中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。

为此,C++11引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化:

1
2
3
template class std::vector<bool>;
// 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板

参考

本文作者 : sunwengang
本文使用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议
本文链接 : https://alonealive.github.io/Blog/2021/06/16/2021/210616_cpp_cpp11_1/

本文最后更新于 天前,文中所描述的信息可能已发生改变