目录
一. 内联函数
1.1 内联函数的概念
1.2 内联函数的特性
1.3 内联函数和宏的优缺点对比
二. auto关键字(C++11)
2.1 auto的功能
2.2 auto在使用时的注意事项
三. 基于范围的for循环(C++11)
四. 指针空值nullptr(C++11)
一. 内联函数
1.1 内联函数的概念
内联函数,就是使用inline关键字,让C++编译器在调用函数的位置处将函数在开展被调用的位置,从而减少函数栈帧创建和销毁的时间。
内联函数的声明方法:inline 返回值类型 函数名(参数列表)。下面的代码以add函数为例,演示了内联函数的定义和声明的方法。
inline int add(int x, int y)
{
return x + y;
}
int main()
{
int x = 10, y = 10;
int ret1 = add(x, y);
int ret2 = add(x, y);
return 0;
}
- 在Debug模式下,内联函数默认不展开,但可以通过更改编译器设置,来让内联函数在Debug模式下也展开。
- 在Release版本下,内联函数展开。
知识拓展:Debug被称为调试版本,编译器不会对程序进行优化,但可以调试找bug。Release版本被称为发布版本,编译器会对程序进行优化,但是程序员不可以在Release版本下调试。
设置在Debug版本下内联函数展开的方法:
- 打开属性设置,选择C/C++ -> 常规,将调试信息格式改为程序数据库。
- 选择C/C++ -> 优化,将内联函数扩展改为:只适用于_inline (Ob1)。
图1.2和1.3展示了使用内联函数的不使用内联函数时,调用add函数的汇编代码的区别。不使用内联函数时,调用add函数要先为通过call指令来跳转,建立函数栈帧后才会执行函数中的指令。使用inline时,汇编语言中不再有call指令,函数的指令直接展开在主函数中。
1.2 内联函数的特性
内联函数是一种以空间换时间的方法
C++内联函数类似于宏,都是在使用的位置展开,从而减少函数栈帧创建和销毁的开销。假设,一个函数(func)编译完成后有10条汇编指令,调用这个函数1000次,使用内联和不使用内联的情况下,汇编指令的条数为:
- 不使用内联函数:1000 + 10次,call func() 1000次 + 10条函数指令。
- 使用内联函数:1000*10次,每次调用展开函数,每次调用都需要独立的10条指令。
inline对于编译器来说只是建议,展不展开最终由编译器决定
这一点和register寄存器关键字类似,register关键字的功能是建议将变量存储在寄存器,仅仅是建议,到底要不要将变量放在寄存器由编译器决定而不是register。
- 对于比较长(指令较多)的函数,即使不进行展开,创建函数栈帧的开销相对于执行函数指令很小,编译器很可能就不展开。
- 递归函数不适用于inline,因此,对于存在递归调用的函数,即使使用inline进行声明,编译器也不会展开函数。
inline声明和定义不能分离
由于inline会直接在调用函数的位置处展开,在编译阶段生成的符号表中不会存储函数的地址,因此,如果定义和声明分离,则会存在找不到函数的问题,这样会发生链接错误。下面的代码在头文件中使用inline声明sub函数,在func.c文件中定义函数,报错。
//head.h
#include<iostream>
using namespace std;
inline int sub(int x, int y);
//func.cpp
#include "head.h"
inline int sub(int x, int y)
{
return x - y;
}
//test.cpp
#include "head.h"
int main()
{
int x = 10, y = 10;
int ret1 = sub(x, y);
int ret2 = sub(x, y);
return 0;
}
总结:内联函数与宏类似,适用于函数代码量少且频繁被调用的场景。
1.3 内联函数和宏的优缺点对比
内联函数和宏共有的优点:
- 省去了函数栈帧的创建消耗,提高了代码的效率。
内联函数和宏共有的缺点:
- 代码量变大
内联函数相对于宏的优点:
- 可读性好,读内联函数与读普通函数无明显区别。
- 宏本质上是替换,不可以调试,而内联函数在Debug模式下默认不展开,可以进行调试。
- 宏没有类型检查,而内联函数有类型检查。
下面的代码就会报警告:从“double”转换到“int”,可能丢失数据
int add(int x, int y)
{
return x + y;
}
int main()
{
double x = 10, y = 10;
int ret1 = add(x, y);
int ret2 = add(x, y);
return 0;
}
二. auto关键字(C++11)
2.1 auto的功能
在C++98和C++03的标准中,auto关键字的作用是使变量出了定义变量的作用域就自动销毁,但是,在默认情况下,变量都是具有auto属性的且出了定义变量的作用域就会自动销毁。因此,在早期的C++标准下,auto关键字没有任何实质性意义。
C++11标准中,auto被赋予了全新的功能,摒弃了C++98和C++11原来的作用。auto的新功能为:自动推断类型。
下面的代码中通过auto来声明变量类型,再通过typied().name来打印类型。typeid().name()获取类型时,经常会省去const。
int main()
{
int a = 10;
char c = 'a';
auto a1 = a;
auto a2 = c;
auto a3 = 10;
auto a4 = 'a';
auto a5 = 12.345;
//typeid().name() 能够自动识别变量(常量)类型并实现对变量类型的打印
//但是,使用typeid获取类型很多时候会舍去const
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
cout << typeid(a4).name() << endl;
cout << typeid(a5).name() << endl;
//auto类型数据在定义是就必须初始化,因为编译器要通过判断其被初始化的数据的类型来判断auto的类型
//auto a6; //编译不通过
return 0;
}
如果定义const int a = 10,在使用auto b = a将a的值赋给b,这时b是可以被修改的,auto不会将a的const属性带给b。如果希望b不能被修改,则应当使用const auto b = a。
int main()
{
const int a = 10;
auto b = a;
cout << b << endl;
b = 30;
cout << b << endl;
//const auto b = a; //这时b具有只读const属性
//b = 40; //报错
return 0;
}
2.2 auto在使用时的注意事项
1、在使用auto声明变量是必须初始化
auto是根据变量被初始化的数据类型来推断变量类型的,如果不初始化,那么就无法确定auto是什么类型的数据,编译会报错。
int main()
{
int a = 10;
auto b;
return 0;
}
2、使用auto在同一行声明多个变量时,类型必须相同
编译器只会对第一个变量的类型进行推导,用推导出来的类型定义后面的变量。
int main()
{
auto a = 10, b = 20; //编译通过
auto c = 20, d = 12.23; //编译报错
}
3、auto不能做为函数的类型
auto需要通过被初始化的数据来推断,而函数形参类型没有初始化,无法推断函数具体的参数类型。
void func(auto x)
{
cout << "void func(auto x)" << endl;
}
int main()
{
func(10);
return 0;
}
4、auto不能直接用来声明数组
这里不需要纠结原因,明确不用auto声明数组就好。
int main()
{
int a[] = { 1,2,3 };
//auto a1[] = { 1,2,3 }; //编译报错
return 0;
}
5、使用auto声明指针类型和引用类型
在声明指针类型时,auto*和auto没有任何区别,但使用auto声明引用类型时,就必须写为auto&,&不能丢。
int main()
{
int a = 10;
auto pa1 = &a; //auto获取指针类型
auto* pa2 = &a; //auto*获取指针类型
auto& ra = a; //使用auto定义a的引用
cout << typeid(a).name() << endl; //int
cout << typeid(pa1).name() << endl; //int *
cout << typeid(pa2).name() << endl; //int *
cout << typeid(ra).name() << endl; //int
*pa1 = 20; //a = 20;
*pa2 = 30; //a = 30;
ra = 40;
cout << &a << endl;
cout << &ra << endl; //&a和&ra相同
return 0;
}
三. 基于范围的for循环(C++11)
在之前使用C语言的时候,要打印数组中的每个元素,我们需要获取数组元素的个数,通过for循环来实现,就有了下面的代码。但是,普通for循环sizeof(arr)/sizeof(arr[0])使用起来相对复杂,有更简单的方法吗?当然有。
int main()
{
int arr[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
在C++11标准中,给出了基于范围的for循环语法,语法格式为:for(auto x : arr),其实现的功能为:将arr数组中的数据依次赋给x,直到数组的最后一个元素,每一个数据赋给x表示一层循环。下面这段代码使用基于范围的for循环,打印数组中的每个数据。
int main()
{
int arr[] = { 1,2,3,4,5 };
for (auto x : arr)
{
cout << x << " ";
}
cout << endl;
return 0;
}
不需要程序员去计算数组中元素个数,这里编译器会自动处理。同时,有两点注意事项:
- auto x : arr中的x可以被替换为i、j等任意名称。
- auto可以被替换为int,但是,如果数组元素的类型发生变化,就需要更改int,因此最好直接声明为auto。
那么,如何通过基于范围的for循环修改数组中元素的值呢?这里就需要引用。
int main()
{
int arr[] = { 1,2,3,4,5 };
for (auto& a : arr)
{
a += 1; //数组每个元素+1
}
for (auto x : arr)
{
cout << x << " ";
}
cout << endl;
return 0;
}
看到这里,可能会有疑惑:引用在有了引用实体之后,就不能再引用其他实体,那么为什么for (auto& a : arr)不存在改变引用实体的问题呢?答:每一层for循环结束后,变量a都会被销毁,进入下一层循环时,a是再次创建,而不是更改以前的引用实体,所有不存在问题。
四. 指针空值nullptr(C++11)
在C++03、C++98和C语言中,使用NULL来表示指针空值。我们可以看到,C++头文件中对NULL的定义如下:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
也就是说,在C++中,NULL只是将NULL定义为值为0的宏,并没有将其转化为指针类型,所以,NULL存在类型不明确问题。为了解决C++98和C++03中NULL类型冲突的问题,C++11引入了新的指针空值nullptr,其定义为:#define nullptr ((void *)0) -- 将0强转类型转化为void*类型。
下面代码定义了一组函数重载,两个func函数的参数分别为int类型和int*类型,如果传入NULL调用func,我们希望调用的函数为func(int*),但实际上是func(int)被调用了,这是因为NULL被替换为了0,从而误调用了func(int),而传入nullptr就不存在问题。
void fun(int x)
{
cout << "void fun(int x)" << endl;
}
void fun(int* x)
{
cout << "void fun(int* x)" << endl;
}
int main()
{
//C++98、C++03
int* p1 = NULL;
int* p2 = 0; //会与数字0发生冲突
//C++11
int* p3 = nullptr;
fun(NULL); //void fun(int x)
fun(nullptr); //void fun(int* x)
return 0;
}