C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr

目录

一. 内联函数

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版本下内联函数展开的方法:

  1. 打开属性设置,选择C/C++ -> 常规,将调试信息格式改为程序数据库。
  2. 选择C/C++ -> 优化,将内联函数扩展改为:只适用于_inline (Ob1)。
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第1张图片 图1.1  更改设置使编译器在Debug版本下也展开内联函数

图1.2和1.3展示了使用内联函数的不使用内联函数时,调用add函数的汇编代码的区别。不使用内联函数时,调用add函数要先为通过call指令来跳转,建立函数栈帧后才会执行函数中的指令。使用inline时,汇编语言中不再有call指令,函数的指令直接展开在主函数中。

C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第2张图片 图1.2  不使用内联函数时的汇编代码
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第3张图片 图1.3 使用内联函数时的汇编代码

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
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;
}
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第4张图片 图1.4 代码报错信息

总结:内联函数与宏类似,适用于函数代码量少且频繁被调用的场景。

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;
}
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第5张图片 图2.1  程序报错信息

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;
}
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第6张图片 图2.2  程序报错信息

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;
}
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第7张图片 图2.3 程序运行结果

三. 基于范围的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;
}

不需要程序员去计算数组中元素个数,这里编译器会自动处理。同时,有两点注意事项:

  1. auto x : arr中的x可以被替换为i、j等任意名称。
  2. 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;
}
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第8张图片 图3.1 程序运行结果

看到这里,可能会有疑惑:引用在有了引用实体之后,就不能再引用其他实体,那么为什么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;
}
C++入门:内联函数、auto关键字、基于范围for循环及指针空值nullptr_第9张图片 图4.1 程序运行结果

 

你可能感兴趣的:(C++从入门到精通,c++,开发语言)