目录
1 内联函数
1.1 内联函数的概念
1.2 内联函数定义及查看
1.3 内联函数的特性
1.3.1 特性1
1.3.2 特性2
1.3.3 特性3
2 auto关键字
2.1 类型名思考
2.2 auto 的概念
2.3 auto 的使用细则
2.3.1 auto与指针和引用结合起来使用
2.3.2 在同一行定义多个变量
2.4 auto不能推导的场景
3 基于范围的for循环(C++11)
3.1 范围for的用法
3.2 范围 for 的使用条件
4 指针空值 nullptr (C++11)
一个函数在开始调用的时候会建立函数栈帧,结束调用时会销毁函数栈帧,而函数栈帧的建立与销毁是有空间和时间上的开销的;
那么对于功能简单,但是调用次数很多的函数,频繁的开辟栈帧就会导致效率的低下;例如hoare法的快速排序中,而Swap函数功能简单,也会被频繁调用。
在C语言中,我们就会使用宏函数来进行优化,可以直接将 Swap 函数写成宏函数,这样使得程序在预处理阶段直接将 Swap 函数替换成相应的代码,从而不再建立函数栈帧。
//源代码
#include
#define Add(x,y) ((x)+(y)) //宏函数
int main()
{
int ret = Add(2, 3);
printf("%d\n", ret);
}
//经过预处理之后的代码
{
//...此处是 stdio.h 展开的内容
}
int main()
{
int ret = ((2) + (3));
printf("%d\n", ret);
}
但是宏有如下主要缺点:
①宏不能调试;
②宏没有类型安全检查;
③宏非常容易写错;
基于C语言宏函数的这些缺陷,C++就设计了内联函数:
①以 inline 关键字修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开 (用函数体替换函数的调用),没有函数调用建立栈帧的开销,内联函数可以提升程序运行的效率;
②内联函数的编写和正常函数一样,仅仅是在函数的返回值类型前添加一个 inline 关键字 (这样就解决了C语言宏函数容易写错以及没有类型安全检查的缺陷);
③同时,在 debug 模式下,内联函数不会自动展开,需要我们对编译器进行相关设置;在 release 模式下,内联函数会自动展开 (这样解决了C语言宏函数无法调试的缺陷);
结论:内联函数在继承了C语言宏函数优点的同时几乎避免了其所有的缺陷。
内联函数的定义
//普通函数
int Add(int x, int y)
{
return x + y;
}
//内联函数--添加inline关键字
inline int Add(int x, int y)
{
return x + y;
}
内联函数的查看
1. 在 release 模式下,编译器会自动将内联函数展开,但由于 release 模式无法调试,所以我们这里无法观察;
2.在 debug 模式下,需要在 项目->属性 中对编译器进行如下设置,否则不会展开 (因为 debug 模式下,编译器默认不会对代码进行优化,以下给出 VS2022 的设置方式)
在完成上述设置后我们 F10 进入调试,然后单击右键转到反汇编查看汇编代码:
普通函数的汇编代码
call:Add函数的调用指令
内联函数的汇编代码
注意:在测试完成之后把编译器设置还原。
inline 对于编译器而言只是一个建议 (类似于C语言的 register 关键字),不同编译器关于 inline 的实现机制可能不同,一般建议将具有如下特点的函数采用 inline 修饰:
①函数规模较小 (具体取决于编译器内部实现);
②不是递归;
③频繁调用;
在《C++prime》第五版中对于 inline 的建议:
下面来验证一下当加上inline时,编译器也未必按照内联函数处理
//把Add的内部逻辑复杂化
inline int Add(int x, int y)
{
int sum = x + y;
sum += x + y;
sum = x + y;
sum /= x + y;
sum = x + y;
sum = x + y;
sum *= x + y;
sum = x + y;
sum = x + y;
sum -= x + y;
sum = x + y;
sum += x + y;
return sum;
}
int main()
{
int ret = Add(2, 3);
cout << ret << endl;
}
通过验证,我们可以看到,尽管使用了inline修饰函数,但是在调用的时候还是要建立栈帧。
inline 是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段会用函数体替换函数调用;这样做的优势是减少了栈帧建立的开销,提高了程序运行效率;缺陷是可能会使目标文件变大。
需要注意的是,这里的空间指并不是指程序运行时占用的内存空间,而是指经过编译链接后得到的可执行程序 (.exe/.o文件) 所占用的空间;对于可执行程序变大的原因,我们以下面这个例子为例进行理解:
假设一个Func函数的汇编指令有50条,且这个函数要被重复调用1W次;
那么对于普通Func函数来说:我们每次调用Func都要转换出一条 call 汇编代码,调用1W次就有1W条汇编指令;但是Func函数本身只会在函数定义处被转换为汇编代码;所以普通Func函数经过编译之后的汇编指令一共有 1W+50 条;
而对于 inline 函数来说,由于 inline 函数会在所有调用的地方展开,也就是说,每调用一次Func函数,就会转换成50条对应的汇编代码;所以 inline Func函数经过编译之后的汇编指令一共有 50W 条;
而汇编指令的增多可能会导致我们编写的静态库/动态库增大,也有可能导致编写的 .exe 增大;这其实就是所谓的 “代码膨胀”,这也在一定程度上解释了为什么当内联函数过长时编译器不进行展开。
inline 不建议声明和定义分离,分离会导致链接错误,具体原因如下:
我们知道:程序在编译阶段进行符号汇总,汇编阶段生成符号表,链接阶段进行符号表的合并和重定位;
对于定义在本文件内的函数来说,编译器在汇编阶段会直接调用该函数,在调用过程中会生成对应的符号表,且此符号表中的地址一定是有效的,所以程序不会进行后续的链接操作;
而对于定义在其他文件中的函数,编译器会先在本文件内寻找该函数的声明,且声明生成的符号表中的地址是无效的;此时编译器会继续后续的链接操作;
链接过程中符号表的合并会将汇编阶段生成的所有符号表合并到一起,合并的意思是如果两个符号表中的函数名相同,那么编译器会选取与有效地址相关联的符号表,丢弃掉另一个与无效地址关联的;这样同时具有声明和定义的函数经过链接就只有一个符号表了;
而如果一个函数只有声明,而没有定义的话,那么它经过符号表的合并之后关联的仍然是一个无效地址,则在进行符号表的重定位时就会发生链接性错误;如果符号表中关联的是一个有效地址,重定位时编译器就会根据这个地址来调用函数,这样就可以实现跨文件调用函数;
对于 inline 函数来说,如果我们将函数的定义和声明分离,那么函数的声明在汇编阶段会生成一个符号表,里面关联的是一个无效的地址;但是由于 inline 函数是直接展开的,所以函数定义部分在汇编阶段并不会生成符号表;这时候就出现了上面的问题,程序经过符号表的合并之后 inline 函数仍然关联一个无效地址,会在重定位的时候发生链接性错误。如下:
图中,我们将 inline 函数的定义放在 Add.h 中,将其实现放在 Add.cpp 中,然后在 test.cpp 中包含 Add.h,这样经过预处理之后,test.cpp 中就包含了 inline 函数的声明;那么经过汇编,Add.cpp 中的 Add 函数由于是内联函数,会直接展开,所以不会生成符号表;
而在 test.cpp 中,经过汇编,Add 函数的声明会生成一个符号表,且符号表中的地址是无效的;而在链接阶段,Add 声明对应的符号表又不能匹配到有效的地址 (因为 test.cpp 中并没有生成 Add 函数的符号表),所以重定位时发生链接型错误 (LNK 错误);
正确的使用方法如下:如果有 .h 文件,将 inline 函数的定义直接放在 .h 文件中;如果没有 .h 文件,就直接放在本文件内部;
一、类型名过长,难以拼写
二、含义不明确导致容易出错
例如下面的m和it变量的类型:
#include
#include
#include
std::map::iterator 是一个类型,但是该类型太长了,特别容易写错;可能聪明的同学可能已经想到:我们可以通过 typedef 给类型取别名,比如:
#include
#include
使用typedef给类型取别名确实可以简化代码,但是 使用typedef可能会遇到其他问题 ,例如:
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它;因为当函数调用结束后,函数的栈帧会被销毁,那么存在于函数栈帧中的局部变量自然也会被销毁,这就使得 auto 修饰失去了意义;
而在C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
//auto e; //无法通过编译,使用auto定义变量时必须对其进行初始化
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型;因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&:
int main()
{
int a = 10;
auto b = &a;
auto* c = &a;
auto& d = a;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
}
在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量:
void TestAuto()
{
auto a = 2, b = 4;
auto c = 4.5, d = 3;
auto e = 3, f = 4.5;
}
①auto不能作为函数的参数,因为不是所有的参数都有初始化表达式,因此编译器可能无法推导出a的实际类型,所以直接规定auto不能作为函数形参:
② auto不能直接用来声明数组:数组需要根据元素类型及个数来开辟空间,而数组名代表指针,因此 auto 无法推导:
③为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法;
④auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。
在C++98中使用for循环遍历数组方法可如下图所示:
void TestFor()
{
int array[] = { 2, 4, 5, 7, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
}
int main()
{
TestFor();
return 0;
}
对于一个有范围的集合而言C++11 中引入了基于范围的for循环。for循环后的括号被冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = {1, 2, 3, 4, 5, 6, 7, 8};
//使用引用进行迭代--可以修改原数组
for (auto& e : array)
e *= 2;
//使用局部变量进行迭代--不能修改原数组
for (auto e : array)
cout << e << " ";
cout << endl; //换行
}
int main()
{
TestFor();
return 0;
}
注意:范围for循环也可以用continue来结束本次循环,也可以用break来跳出整个循环
①for循环迭代的范围必须是确定的:对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围;比如下面代码的范围就是不确定的:
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <
②迭代的对象要实现++和==的操作;(以后学习)
在C语言中,通常我们在定义一个指针变量的时候会将其初始化为 NULL,避免后面对其错误使用造成野指针越界访问问题;其实这里的 NULL 是C语言中定义的一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
我们可以看到,对于C语言来说,NULL 其实是数字0被强转为指针类型,相当于0处的地址;而对于C++来说,NULL 则被直接解释为数字0;虽然 0 和 (void*)0 二者在数值上相同,但是他们的类型是不相同的,一个是整形,另一个是指针;这就导致使用时会出现一些问题,比如下面这个例子:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
由于NULL被定义成0,程序想通过 f(NULL) 调用指针版本的 f(int*) 函数,因此与程序的初衷相悖;
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器 默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转为 (void *)0。
为了解决这个问题,C++11中专门为空指针设计了一个关键字 – nullptr,用来弥补C++98中空指针NULL存在的缺陷。(可以认为,nullptr 就是 (void*)0 )
型是不相同的,一个是整形,另一个是指针;这就导致使用时会出现一些问题,比如下面这个例子:
nullptr 注意事项
①在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的;
②在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同;
③为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr;