风力掀天浪打头
只须一笑不须愁
目录
内联函数
概念💞
性质 ⭐
不建议变量分离
inline的优劣势
inline的局限性
auto关键字
auto的概念💞
auto的使用细则💞
auto不能推导的场景 💞
auto基于范围的for循环💞
指针空值nullptr
内联函数
契子✨
在大型项目中我们会对某一种函数进行反复调用,比如排序中的 Swap ,调用函数所带来的后果是建立栈帧,多次调用就要建立多重栈帧严重影响运行效率。
在 C 语言阶段我们曾采用宏定义的方式来解决这个问题,比如说两数相加函数
#define Add(a,b) ((a)+(b))
优势⭐在调用的地方直接展开(替换),增强代码的复用性,没有函数调用建立栈帧的开销
劣势⭐无法对宏定义中的变量进行类型检查,嵌套定义过多影响程序的可读性,容易出错
为了解决这个问题,我们的 C++ 推出了内联函数:
概念💞
inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率
以下将会用汇编代码解释这个问题:
如果在上述函数前增加 inline 关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用
查看方式🌤️
在release模式下,查看编译器生成的汇编代码中是否存在call Add |
在debug模式下,需要对编译器进行设置,否则不会展开 |
在debug模式下对编译器的设置:
这个时候我们发现,Add函数展开了
性质 ⭐
不建议变量分离
inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址 了,链接就会找不到
例如,我们先来看以下代码:
无法解析的外部符号 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)函数main中引用了该符号 所以 inline 不建议声明和定义分离为好
inline的优劣势
inline 是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
🌤️缺陷:可能会使目标文件变大 |
🌤️优势:少了调用开销,提高程序运行效率 |
inline的局限性
inline 对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同,一般建议:将函数规模较小、不是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略 inline 特性(函数调用不展开)
auto关键字
auto的概念💞
随着程序越来越复杂,程序中用到的类型也越来越复杂
经常体现在:
类型难于拼写 |
含义不明确导致容易出错 |
例如~
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错
在 C语言阶段 我们可以通过 typedef 给类型取别名,比如:
typedef std::map<std::string, std::string> Map
typedef int SListDataType;
为我们 C++ 中提供了一个很方便的关键字 auto(自动类型推导)
std::map<std::string, std::string> m;
<1>std::map<std::string, std::string>::iterator it = m.begin();
<2>auto it = m.begin();
其中 <1>、<2>的写法是等价的,auto的作用就是从右边推导类型、简化代码
例如
#include<iostream>
int main()
{
auto a = 0;
return 0;
}
因为 表达式a 的右边为整型,所以 auto 推导的类型就为 int
我们可以用 typeid 进行验证:
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
int TestAuto(int n = 0)
{
return n;
}
int main()
{
auto a = 0;
auto b = 0.0;
auto c = '0';
auto d = TestAuto();
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
<1>使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型
<2> auto 并非是一种 (类型) 的声明,而是一个类型声明时的 (占位符) ,编译器在编译期会将 auto 替换为变量实际的类型
auto的使用细则💞
注意💞
auto与指针和引用结合起来使用
auto 与指针和引用结合起来使用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用auto* 声明引用类型时则必须加 &
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
}
在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
auto不能推导的场景 💞
auto不能作为函数的参数
错误示范
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
void Fun(auto a)
{
cout << a << endl;
}
int main()
{
Fun(5);
return 0;
}
auto不能直接用来声明数组
错误示范
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
int main()
{
int a[] = { 1,2,3 };
auto b[] = { 4,5,6 };
return 0;
}
那硬是要用 auto 声明数组该怎么办呢?
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
int main()
{
int a[] = { 1,2,3 };
auto b = a;
cout << typeid(b).name() << endl;
return 0;
}
我们可以看到 b 是一个 int* 的类型
如果表达式为数组且 auto 带上&,则推导类型为数组类型
auto基于范围的for循环💞
我们平时写 C语言 的循环是不是特别麻烦,例如以下的代码
int a[] = { 1,3,5,7,9,2,4,6,8,0 };
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误
因此 C++ 中引入了基于范围的for循环
for 循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量(自己喜欢用什么就写什么) |
第二部分则表示被迭代的范围(数组名) |
int a[] = { 1,3,5,7,9,2,4,6,8,0 };
for (int i : a)
cout << i << " ";
这个方法的爽点就在于:自动判断循环条件,执行完语句变量自动 ++ 或 --
到这里就有人问这个循环只能遍历吗?能不能修改数组的值呢?
答案是可以的,以下我们来看:
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
int main()
{
int a[] = { 1,2,3,4,5 };
for (int& i : a)
{
i *= 2;
cout << i << " ";
}
return 0;
}
这里传引用的目的就是为了改变该地址的内容
注意
与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环
优点:简化代码、减少出错率
局限性:范围的 for 循环的底层是 begin、end 两个迭代器相当于从数组的首元素开始到尾元素,一遍历就是整个数组
auto 的出现解决了范围 for 循环变量类型有时不好写的问题以及可以无脑循环整个数组🌤️
int a[] = { 1,2,3,4,5 };
for (auto i : a)
cout << i << " ";
for (auto i : a) 就成了循环遍历整个数组的模板,因为自动类型推导嘛,不用考虑类型
for循环迭代的范围必须是确定的
错误示范
#include<iostream>
#include<typeinfo>
using std::cout;
using std::endl;
void Fun(int* a)
{
for(auto i: a)
cout << i << " ";
}
int main()
{
int a[] = { 1,2,3,4,5 };
return 0;
}
因为数组传参传的是数组首元素地址从而导致 begin 和 end 不完整,所以报错
指针空值nullptr
- 是否可以混着用
NULL
和nullptr
呢? - 这两者到底有什么区别呢?
- 滥用
NULL
到底会带来什么不可思议的错误?
别急接下来我们谈谈他们两者中的那些破事
NULL的介绍
🔥在良好的 C/C++ 编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int* p1 = NULL; int* p2 = 0;
NULL 实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦
#include<iostream> using namespace std; 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 在传参的时候竟然走了 int 类型!!!
我们想 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,走了 f(int) 函数
NULL 是具有一定风险的:
NULL
终究只是一个宏。它是一个整型,它不是指针。因而随之带来的问题是,我们没有办法在不显示声明指针类型的情况下定义一个空指针。nullptr的介绍
🔥而在 C++ 中有一个关键字完美的解决了这个问题——
nullptr
。作为一个字面常量和一个零指针常数,它可以被隐式转换为任何指针类型拿上面那个栗子看
f(0); f(nullptr); f((int*)NULL);
nullptr 只能隐式转换为 int 类型吗?我们来看
#include <iostream> using namespace std; void Fun(void* c) { cout << "Fun(void* c)" << endl; } void Fun(int n) { cout << "Fun(int n)" << endl; } int main() { Fun(NULL); Fun(nullptr); return 0; }
由于 nullptr 无法隐式转换为整形,而可以隐式匹配指针类型
特性:
nullptr
是一个有用的小特性,它能让你的代码更安全sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
#include <iostream> using namespace std; int main() { cout << sizeof(nullptr) << endl; cout << sizeof((void*)0) << endl; return 0; }
先介绍到这里啦~
有不对的地方请指出💞