> 文章列表 > C++ auto 内联函数 指针空值

C++ auto 内联函数 指针空值

C++ auto 内联函数 指针空值

本博客基于 上一篇博客的 序章,主要对 C++ 当中对C语言的缺陷 做的优化处理。

上一篇博客:C++ 命名空间 输入输出 缺省参数 引用 函数重载_chihiro1122的博客-CSDN博客

auto关键字

 auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

 也就是说,auto可以作为类型来使用,他的意思就是,他会根据右边的 表达式自动的推出我们定义的这个变量的类型,如这个例子:

int main()
{int a = 10;auto b = a;auto c = 1 + 11.11;cout << typeid(b).name() << endl; // 打印b的类型  输出:int cout << typeid(c).name() << endl; // 打印c的类型  输出:doublereturn 0;
}

我们发现,auto自动的推导出了 变量的类型。

需要注意的是:auto 是必须在编译时期就要 推导出来的,也就是说,auto 类型的变量必须要初始化:

 如果在定义的时候不初始化,就会向上述代码一样报错。

我们上述的几个例子还不能体现出 auto 的真正用途,auto 主要用在 有一些类型很长的 变量 在定义的时候,很难书写,这时候,我们就可以用 auto 来自动推导出 变量的类型,从而减少麻烦:

#include<map>
#include<string>
#include<vector>int main()
{int a = 0;int b = a;auto c = a; // 根据右边的表达式自动推导c的类型auto d = 1 + 1.11; // 根据右边的表达式自动推导d的类型cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;vector<int> v;// 类型很长//vector<int>::iterator it = v.begin();// 等价于auto it = v.begin();std::map<std::string, std::string> dict;//std::map<std::string, std::string>::iterator dit = dict.begin();// 等价于auto dit = dict.begin();return 0;
}

auto不能同时推导

 我们在定义的时候,可能会这样写:

int i = 0, b = 0, c = 0;

这样一次定义多个相同类型的变量,但是auto不能这样写,假设我两个的变量 推导出来的类型是不相同的,那么就会报错:

 auto不能用来作为函数的参数

 这个想想也知道,auto是需要在程序编译时期就要进行推导的,那么都作为函数的参数了,如何进行推导呢?

如这个例子:

 直接报错了。

auto不能用来作为数组的类型

 同样,不能根据右边的推导出类型。

 auto作为指针类型和引用类型

 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须
加&。

	int a = 10;int* pa = &a;auto prev1 = pa;auto* prev2 = pa;cout << typeid(prev1).name() << endl; //int * __ptr64cout << typeid(prev2).name() << endl; //int * __ptr64

推导出来的类型是一样的。

	auto& ppa = a;cout << typeid(ppa).name() << endl;//intppa++;cout << "a = " << a << endl;//a = 11

 auto* 只能推导指针类型:

int a = 10;
auto* b = 10;  // 报错
auto* b = &a;

以前的auto

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的
是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是向上述一样是一个类型了。

为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

 auto在实际中最常见的优势用法就是C++11提供的新式for循环,还有lambda表达式等进行配合使用。

 以前我们如果要用for循环访问一个数组那么我们会这样去使用:

	int arr[] = { 1,2,3,4,5 };for (int i = 0; i < sizeof(arr) / sizeof(int); i++){printf("%d", arr[i]);}printf("\\n");

我们用了新式的for循环之后,可以这样遍历数组:

for (auto e : arr){cout << e << endl;}//1//2//3//4//5

既然我们可以直接用e来访问数组,那么我们是否可以用e来进行修改呢?

我们来看这个例子:

	// 修改数组for (auto e : arr){e = 1;}for (auto e : arr){cout << e << endl;}//1//2//3//4//5

我们发现是不行的,这是因为 e 只是一个变量,这个for循环代表的意思是,每一次从arr数组中依次取出一个数据,拷贝到e当中,那么我们去修改e的值,是不会改变 arr数组当中的值的。

 这时候,如果我们想通过e 来修改arr数组当中的内容,我们可以用一个 引用去接收这个数组当中的元素:

		// 修改数组for (auto& e : arr){e = 1;}for (auto e : arr){cout << e << endl;}//1//1//1//1//1

这样我们就修改成功了。

内联函数

解决调用函数需要创建函数栈帧的问题

 假设我们需要大量的调用某一个函数,那么此时就会创建很多次函数栈帧,就会有消耗,如下面这个例子:

int Add(int x, int y)
{return (x + y) * 10;
}int main()
{for(int i = 0; i < 10000;i++){cout << Add(i, i+1 ) << endl;}return 0;
}

我们调用了10000次 Add函数,大量的调用就会多次生辰函数栈帧。

要解决这样的问题,我们可以使用宏函数,这个概念是在C当中就有定义的,也就是说,我们可以在C的语法当中去实现它。

宏只是一种替换,不需要传参,也不需要有返回值。

 错误的宏函数定义:

#define Add(int x, int y) x + y   // 宏函数不是传参,而是替换,而且后面的 x + y 需要打括号
#define Add(x , y) (x + y)  // 后面的 (x + y) 里面的 x 和 y 都需要打括号
//  原因是  假设我写的是这个 ,那么就会有运算符优先级的问题:Add(a | b , a & b);  // 这样在宏替换进去的时候,就会发生问题
#define Add(x , y) ((x) + (y))   //错误写法,宏的定义后面不需要分号

宏函数最容易误解的点是,宏是替换,不是传参,他是在预处理的时候,会把其中的 x 和 y 换成我们在外部写入的表达式,所以我们要注意上述的几种错误定义宏函数的方式。

我们在外部调用宏函数的时候,和调用其他函数是一样的。

宏函数对比原本的函数调用的优点和缺点

 优点是:宏函数的调用不需要建立栈帧,他只是一个在预处理阶段替换的过程,他提高了调用的效率。

缺点是定义的时候,相对有点复杂,容易出错,而且会让代码的可读性变差。我们上述定义的宏函数只是一个非常简单的函数,如果我们定义一个实现算法相对复杂的宏函数,那么这个宏函数看着会非常的复杂。

而且,因为宏是在预处理的阶段要进行替换的,那么也就意味着,宏函数是不支持调试的。

 内联函数

 那么在C++当中就有一个关键字inline,被这个关键字修饰的函数,就可以解决上述的问题。

内联函数会在函数被调用的地方,进行展开。它的方式就像 我们在引头文件的时候,在编译的预处理阶段会把 头文件当中的内容,在引头文件的位置进行展开一样。他只是在调用函数的时候进行展开。

这样做,就不需要创建函数栈帧,而且函数的定义没有宏函数那么复杂,可读性也比宏函数要高,内联函数提升程序运行的效率

 例子:

inline int Add(int x, int y)
{return x + y;
}int main()
{Add(1, 2);return 0;
}

但是,就算内联函数综合了 宏函数和普通函数的优缺点,但是也不是说 什么函数都适用于做内联函数。

内联函数和宏函数都只适用于 简单的频繁调用的函数,如果我们所定义的内联函数当中实现得很复杂,会出现一个很大问题,就是 代码膨胀。

 假设我们没有使用 内联来定义函数,我们在主函数中调用了10000次这个函数:

如上图,每一次都是 call  去调用 找到函数定义的地址,然后去调用Func()函数的代码,那么此时我们 代码总长度 就是 10000 + 50。

如果我们是使用内联函数去定义这个Func()函数的,那么就会在主函数中的10000个位置都 进行展开,那么总代码行数就是 : 10000 * 50。

那么像上述的内联展开,我们发现,主函数中的代码就已经很多了,那么在最后会生成可执行程序,因为代码行数变得很多,那么生成的可执行程序就会很大。

 如果我们写的是 一个升级程序,或者是一个安装包。如果像上面这样写,这个安装包或者是升级程序就会变得很大,这是我们不希望的。

 其实编译器也会自己判断,也就是说此处的inline 修饰只是一个建议,是建议编译器把这个函数作为内联函数使用,最终是否是inline 修饰内联函数,由编译器自己决定。

 比如,较长的函数,递归的函数这些都是编译器考虑不做内联的函数。

 inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。

/ F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

 如上述代码,他只是在头部声明了 这个内联函数,但是声明是没有地址的,编译器就只能在链接去找,但是编译器找不到地址,他就不能在 调用位置展开。

 如果我们想在 其他文件中写内联函数,在其他文件中调用的话,就不要把这个内联函数的声明和定义分离,也就是让这个函数的声明和定义写在一个文件当中。

// F.h#include <iostream>
using namespace std;inline void f(int i)
{
cout << i << endl;
}// F.cpp
#include <F.h>
void text()
{f(10);
}void text();// main.cppint main()
{f(10);return 0;
}

我们在VS当中可以通过一些设置来对 内联函数进行调试

查看方式:
1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
会对代码进行优化,以下给出vs2013的设置方式)

指针空值nullptr(C++11)

 我们一般在定义指针的时候,一般都要给他初始化的值,如果不知道给谁,一般给一个 NULL,但是,有的时候我们在C++当中使用NULL的时候会出一些问题:
 

void f(int)       //func1
{cout << "f(int)" << endl;
}
void f(int*)       //func2
{cout << "f(int*)" << endl;
}
int main()
{f(0);       //f(int)      1f(NULL);    //f(int)       2f((int*)NULL); //f(int*)    3return 0;
}

如上述例子中的2,我们本来想调用 的是 func2 这个函数,但是却调用了 func1 这个函数,这个不是我们的本意,当时我们把 NULL的类型强转为 int* 之后,发现才调用的是func2 这个函数,说明是NULL有问题,NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

 把NULL替换成0 了才会去调用func1 这个函数。

 在C++11当中对着错误进行了修改,但是如果直接修改上述的 宏定义,那么会导致以前的用户写的代码可能会出现问题,所以在C++11作为新关键字引入nullptr,既然是关键字,那么就不需要引头文件。

此处的sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同,也就是说,我们就可以把 nullptr 理解为  (void)*0   。

 如上述例子,我们来使用这个 nullptr ,发现调用的就是 func2 这个函数了。

int main()
{func(nullptr);  // f(int*)return 0;
}

我们发现上述我们实现 f  函数的时候,只写了形参的类型,没有写形参,我们发现还是编译通过了,在C++中 形参是不一定一定要接收的:

 

emotional sharing