本博客基于 上一篇博客的 序章,主要对 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的类型 输出:double
return 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 * __ptr64
cout << typeid(prev2).name() << endl; //int * __ptr64
推导出来的类型是一样的。
auto& ppa = a;
cout << typeid(ppa).name() << endl;//int
ppa++;
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.cpp
int 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) 1
f(NULL); //f(int) 2
f((int*)NULL); //f(int*) 3
return 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++中 形参是不一定一定要接收的: