内联函数
在c中存在着一种宏函数,和宏常量但是宏函数,宏函数的优点很明显,它不需要额外的去调用堆栈,因为宏函数和宏常量的本质都是替换,但正因为是替换所以如果在使用宏函数的时候不去手动添加括号,那么极为有可能出现优先级顺序问题。例如下面这样
#include<iostream>
#define ADD(a,b) a+b//这是一个宏函数,但是没有增加括号
#define Add(a,b)((a)+(b))
//宏函数必须这么写,至于为什么外部已经加了一个括号了,内部为何
//还要加括号因为如果a和b中也包含运算符并且运算符优先级比+低则
//会出现问题例如a|b + a&b
//就会出现问题
using namespace std;
int main()
{
int a = 10;
int b = 10;
cout<<"ADD:" << ADD(a, b) * 10 << endl;//正确结果是200但是这里的答案是110
//原因就是ADD(a,b)这里被替换为了10+10*10乘号的优先级顺序是大于+的
cout<<"Add:" << Add(a, b) * 10 << endl;
return 0;
}
所以宏函数在使用时要增加括号,但是为什么a和b也要增加括号呢?假设在使用宏函数的时候,a和b是一个计算式,并且a中的计算符号优先级小于+,那么如果a和b不额外增加括号依会出现问题。
除此之外宏函数还不可调试,因为宏函数本质是替换。所以调试是无法进入到函数内部的。
如果使用普通的函数那么就不会出现问题,因为函数在传递值之前就会将a和b表达式计算的值给求出来,再传参。那么在c++中为了解决宏函数的问题,就提出了内联函数。以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
例如下面这样
using namespace std;
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 10;
cout<<"Add:" << Add(a, b) * 10 << endl;
cout<<"Add:" << Add(a, b) * 10 << endl;
return 0;
}
既然内联函数是不会去额外调用堆栈的,那么怎样知道它是直接展开的呢?可以通过汇编代码去查看,既然内联函数没有去额外的调用堆栈,那么在汇编代码中就不会存在call语句。但是在Visual Studio 2022
这个IDE,内联函数在debug时是不会展开的,只会在release时展开,但是release时又无法使用调试。
所以我会设置一下IDE让它能够在debug时将内联函数展开,
从图可以看到Add函数在这里并没有使用call去跳转,而是直接在这里展开了,这下面的这些call是cout函数调用所产生的。
虽然从上面看起来内联函数非常的好,但是内联函数并不是什么函数都可以声明成内联函数的,内联函数一般只有在小于10行的代码才会生成,如果你声明的函数至少大于10行那么即使你添加了inline,vs依旧会将你声明的这个函数当作普通函数处理而不是内联函数直接展开。例如下面
inline int Add(int a, int b)
{
int a1 = 10;
int a2 = 20;
int a3;
a3 = a1 * a2;
int a4;
a4 = a3 / 15;
int a5;
a5 = a4 / 3;
int a6;
a6 = a3 + a4 + a5;
int a7;
a7 = (a6 + a5) * a3 / (a4 - a6);
return a + b;
}
int main()
{
int a = 10;
int b = 10;
printf("%d", Add(a, b) * 10);
return 0;
}
可以看到虽然我将Add函数声明为了内联函数,但是底层实现的时候依旧是当作了普通的函数并没有展开。
因为inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不 是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
除此之外内联函数也不建议声明和定义分开,否则会出现问题。
首先是Test.h文件
#include<iostream>
#include<stdio.h>
using namespace std;
using namespace std;
inline int Add(int a, int b);//这里我声明了一个内联函数
然后是Test.cpp文件
#include"Test.h"
inline int Add(int a, int b)
{
return a + b;
}
最后是主函数文件
#include"Test.h"
int main()
{
int a = 10;
int b = 10;
printf("%d", Add(a, b) * 10);
return 0;
}
可以看到在运行之后,出现了两个错误警告,而这两个警告出现的原因就是没有找到在主函数要使用的Add函数,虽然在头文件中声明了这个文件,但是IDE在运行代码的时候到编译那一步没有找到对应的函数体,因为内联函数在test.cpp文件中直接展开了,也就意味着内联函数并不会进入到符号表中去,所以当链接的时候,虽然声明了那个函数但是并没有找到那个函数导致出现了错误。
那么如何解决这个问题呢?如果你在test.cpp中只定义了一个内联函数,那么你可以让主函数直接包含test.cpp但是如果你在test.cpp中定义了其它的函数,那么这种方法就是不可以的,因为test.cpp文件就会被编译两次,就会出现重定义问题,最后再链接的时候就会出现问题。所以最好的解决方法也就是内联函数声明和定义不分离。
auto关键字
首先auto用于自动类型推断。它允许编译器根据变量初始化值的类型自动推断变量的类型,简化代码书写。
例如下面:
int main()
{
int a = 10;
auto b = a;//这里编译器就可以直接自己推断b的类型
auto* c = &a;
auto&d = a;
//当然在一般情况下这个关键字是没有用的但是在类型名很长的时候这个变量就能够简化书写
return 0;
}
auto关键字还有使用要求:
- auto不能作为函数的参数
- auto不能直接用来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有 lambda表达式等进行配合使用。
基于范围的for循环
假设现在你要给一个数组赋值然后打印一般的代码都是这么写的.
void TestFor()
{
int array[] = { 1, 2, 3, 4, 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;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范 围内用于迭代的变量,第二部分则表示被迭代的范围。
下面就是使用基于范围的for循环修改的上面的代码
void test()
{
cout << "test" << endl;
int array[] = { 1,2,3,4,5 };
for (auto& e : array)
{
e *= 2;
}
for (auto b : array)
{
cout << b ;
}
cout << endl;
}
当然你也可以使用for(int& e : array)来写只是使用auto更加便捷而已。
但是如果你将上面代码的第五行的引用去掉之后就会发现打印出的数字不是246810,而是12345,为什么呢?因为基于这个语法在底层实现的时候是将数组中的每一个数都先赋值给了e,相当于如果没有引用那么e就是原数组的拷贝,修改原数组的拷贝自然不会影响原数组,所以下面打印的时候就不会是246810,而是12345了。
nullptr指针
在c++中使用指针的时候如果要给指针赋值为空,那么就不再和c一样使用NULL了而是使用nullptr,
因为在C++11标准之前,通常使用NULL
来表示空指针,它被定义为整数0的常量表达式。然而,这种表示方式存在一个潜在的问题,即NULL
实际上被定义为整数0,而不是指针类型,这可能导致一些类型安全性问题。
使用nullptr
相比NULL
具有以下优点:
- 类型安全性:
nullptr
是一个指针类型,可以与其他指针类型进行比较,而NULL
是一个整数类型,与指针类型进行混合使用可能导致潜在的类型安全问题。 - 重载:
nullptr
可以被函数重载所识别,可以通过函数重载来区分空指针与整数的传递。 - 明确性:
nullptr
明确地表示空指针的含义,可以提高代码的可读性和可理解性。
例如下面这样
#include <iostream>
void foo(char* ptr) {
std::cout << "Calling foo(char* ptr)" << std::endl;
}
void foo(int value) {
std::cout << "Calling foo(int value)" << std::endl;
}
int main() {
char* ptr = nullptr;
foo(ptr); // 调用void foo(char* ptr)
int x = nullptr; // 编译错误,nullptr不能隐式转换为整数类型
return 0;
}
在这个例子中,我们定义了两个名为foo
的重载函数,一个接受char*
类型的参数,另一个接受int
类型的参数。
在main
函数中,我们声明了一个指向char
类型的指针ptr
并将其初始化为nullptr
。然后,我们通过调用foo(ptr)
将ptr
作为参数传递给foo
函数,这会导致调用foo(char* ptr)
重载函数。
另外,如果您尝试编译int x = nullptr;
这一行,会发生编译错误,因为nullptr
不能隐式转换为整数类型。
这个例子展示了nullptr
在函数重载和类型安全性方面的优势。使用nullptr
可以更明确地表示空指针,避免潜在的类型错误和歧义
这篇文章只是浅浅的说了,这些语法的基本使用规则,没有涉及到底层实现。
最后如果您发现了任何的错误,恳请您能告诉我,我一定修改。