【C++修炼之路】C++入门(下)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

如果无聊的话就来逛逛 我的博客栈 吧! 🌹

一、前言

大家好我是 a n d u i n anduin anduin . 这篇文章是 C++ 入门的最后一篇。内容主要为 内联函数、auto、范围 for 循环和 nullptr 空指针 对于后三个都是 C++11 的特性。内容不多也不难让我们开始学习吧

二、内联函数

调用函数需要建立栈帧栈帧中要保存寄存器结束后就要恢复这其中都是有 消耗

int add(int x, int y)
{
	int ret = x + y;
	return ret;
}

int main()
{
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);

	return 0;
}

而针对 频繁调用小函数可以用 优化因为宏是在预处理阶段完成替换的并没有执行时的开销并且因为代码量小也不会造成代码堆积。

例如代码就可以写成这样

#define ADD(x, y) ((x) + (y)) 

int main()
{
	cout << ADD(1, 2) << endl;

	return 0;
}

但是宏也有缺点

  1. 不能调试
  2. 没有类型安全的检查
  3. 有些场景下非常复杂

就拿 add 函数来说可能一不小心就会写成 #define ADD(x + y) x + y 的样子所以写宏时出错要么是替换出错要么是因为优先级出错所以宏并不友好。

而 C++ 针对为了减少函数调用开销又可以在一定程度上替代宏避免宏的出错从而设计出了内联函数

内联函数的关键字为 inline

inline int add(int x, int y)
{
	int ret = x + y;
	return ret;
}

int main()
{
	int ret = add(1, 2);
	cout << ret << endl;

	return 0;
}

1、概念

在 release 版本下inline 内联函数会直接在调用部分展开对于 debug 则需要 主动设置 (debug 下编译器默认不对代码做优化)但是 release 版本下其他版本优化的太多可能就不太好观察所以我们设置一下编译器在 debug 下看

打开解决方案资源管理器右击项目名称选中属性并打开在 C/C++ 区域常规部分在调试信息一栏设置格式为程序数据库

image-20230130103426993

在 C/C++ 优化一栏将内联函数扩展部分选中只适用于 _inline

image-20230130103641075

设置完毕后点击应用。

在设置前、后分别启动调试查看反汇编代码

修改前

image-20230130104123005

修改后

image-20230130104227711

两段反汇编代码最大的区别就是 call 消失了 call 就是函数调用的指令它的消失就说明第二段代码没有进行调用。内联函数直接在局部展开了在 main 函数中完成了操作。有了内联我们就不需要去用 c 的宏了因为宏很容易出错。

2、特性

  1. inline是一种以 空间换时间 的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率。
  2. inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议将函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议
    image-20230130115508988
  3. inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。

要点讲解

1空间换时间是因为反复调用内联函数导致编译出来的可执行程序变大

inline void func()
{
    // 假设编译完成为 10 条指令
}

若不用内联函数不展开若1000次调用 func每次调用的地方为 call 指令的形式总计 1010 行指令。若用内联函数则展开若一千次调用每次调用的地方为都会展开为 10 条指令总计 10 * 1000 行指令。

展开会让编译后的程序变大如果递归函数作内联后果可想而知。所以长函数和递归函数不适合展开

2编译器可以忽略内联请求内联函数被忽略的界限没有被规定一般10行以上就被认为是长函数当然不同的编译器不同

基于上面的解释所以编译器会决策是否使用内联函数。因为如果函数太大导致的结果很糟糕。

3内联函数声明和定义不可分离

// 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 中被引用

由于内联函数在调用的地方展开所以内联函数无地址这里的地址指的是call 指令调用函数的地址通过这个地址会跳到 jmp 指令处再根据 jmp 处指令跳转到函数执行的部分 即 f.cpp->f.o 符号表中不会生成 f 的地址。

当编译时由于头文件要被包含但是这时只有函数声明但是没有函数的定义所以只能在链接时展开这里只能变为 call + 地址的指令但是内联函数并没有地址链接不到就报错了。

所以当声明和定义分离调用函数时由于内联函数无地址编译器链接不到所以就会报错为链接错误

结论简短频繁调用的小函数建议定义成 inline .

三、autoC++ 11)

1、概念

在 c 语言中也存在 auto 关键字。

int main()
{
    auto int a = 0;
}

auto 关键字修饰后a变为自动存储类型即变量会在函数结束以后自动销毁。但是这个语法完全多此一举因为后来对于局部变量默认就是自动存储类型当函数结束后也会自动销毁。

于是 C++ 委员会废弃了 auto 的用法赋予了新的意义auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期推导而得。

int main()
{
	int a = 0;
	int b = 0;
	auto c = a; // 自动推导 c 的类型
	auto d = 'A';
	auto e = 10.11;

	return 0;
}

auto 会自动推导变量的类型。

typeid 可以看对象类型用法为 typeid(c).name() 由此可以打印变量的类型

image-20230130132744135

对于 auto 来说并不会保留 const 属性例如 e 10.11 是常量就丢弃了 const 属性而保留 double .

image-20230130133028869

对于 auto 如果要加上 const 属性则需要主动加上

int main()
{
	int x = 10;
	const auto y = x;
	cout << typeid(y).name() << endl; // 这里不会打印出需要调试看

	return 0;
}

image-20230130141526876

2、价值

auto 具有两种针对场景

  1. 类型难于拼写
  2. 含义不明确导致容易出错

上面的使用仅仅是样例而它真正的价值体现在 stl 部分范围遍历 中。

在 stl 中有些类型名字很长例如

#include <map>

int main()
{
	map<string, string> dict;
	map<string, string>::iterator it = dict.begin();

	return 0;
}

当要取其迭代器进行遍历时很复杂这时 auto 就派上用场了。

auto it = dick.begin();

它会根据右边的返回值类型自动推导 it 的类型写起来十分方便。

对于这里使用 typedef 进行重命名也可以

int main()
{
	map<string, string> dict;
	typedef map<string, string>::iterator Dict;
	Dict it = dict.begin();

	return 0;
}

虽然 typedef 可以使用但是 typedef 有一个缺点看这个例子

image-20230202194331558

p1 是失败的

因为 typedef 这里不是展开为 const char* p1 实际上这里 typedef 之后是 char* const p1 const 修饰的是 p1 但是 p1 没有初值之后也改不了了所以就失败了但是 p2 展开是 char* const *p2 修饰的是 *p2 此刻 p2 是能改的所以没问题。

验证一下

typedef char* pstring;

int main()
{
	const pstring p1 = nullptr; // 需要一个初值
	const pstring* p2;
}

image-20230202194825557

auto 很灵活甚至可以在后面附上其他属性附上属性后就显示表现它的类型

image-20230130140727872

auto& c 的意思就是c 为 x 的别名但是类型还是 int 不过显示认为它就是推导出来类型的别名。

3、三个不能

  1. auto不能独立定义

image-20230130142147801

  1. auto不能作为函数的参数
// 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导
void TestAuto(auto a) {}

因为编译器无法推导 auto 的类型没有根据。

  1. auto不能定义数组
void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {456};
}

四、范围for循环(C++11)

1、基本使用

之前对于数组的遍历需要使用下标遍历

int main()
{
	int arr[] = { 1, 2, 3, 4,5 };

	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) 
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

而 C++ 中效仿新语言加入了范围遍历

int main()
{
	int arr[] = { 1, 2, 3, 4,5 };

	for (auto num : arr) 
	{
		cout << num << endl;
	}

	return 0;
}

其中就用到了 auto 关键字当复杂类型遇到auto和范围遍历就是天堂。

而范围for循环的原理就是自动取遍历目标的每一个元素再放到给定的临时变量中。在上方就是取 arr 的元素放到 num 中并自动判断结束。auto 会根据遍历目标的元素类型自动推导当然直接写类型 int 也对

如果要使用范围遍历修改遍历目标则可以使用引用

for (auto num : arr) { num ++ ;}

若以这种写法是不会改变的因为 num 是局部变量只是值和 arr 中元素相等本身并没有关联。

但是如果加上引用意义就变了取遍历目标每个元素的值给 num 让 num 变为那些元素的别名

image-20230130144633348

而对于 num 的生命周期则可以认为仅在每次范围遍历中某一次循环才存在。

范围 for 会根据遍历目标的元素类型来取出元素例如上方例子就是 int 如果这时用指针接收就是错误的

image-20230130144836539
因为取出来的每一个元素是 int 类型不匹配。而判断结束我们并不用担心其实和普通遍历类似。

2、使用条件

for循环迭代的范围必须是确定的

对于数组的范围就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。

以下代码就有问题因为for的范围不确定因为函数传参数组就会退化为指针

void TestFor(int array[])
{
	for (auto& e : array)
    {
        cout << e << endl;
    }
}

是错误的。

五、指针空值nullptr(C++11)

对于 c 来说空指针为 NULL是一个宏。

在 C++98/03 时只能使用 NULL 而 C++11 后推荐使用 nullptr 。

NULL

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

实际上 NULL 就是个宏所以说写成 int* p = 0 也可以而j绝大多数情况下这样写都没问题。

但是对于极端场景

void f(int) // 这边由于不使用形参不给形参名也可以
{
	cout << "f(int)" << endl;
}

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	return 0;
}

按道理对于第一次调用应该匹配第一个对于第二次调用应该匹配第二个。

但是实际上它们都匹配了第一个原因是 NULL 是一个宏本质为 0 .

image-20230130151237088

在C++98中字面常量 0 既可以是一个整形数字也可以是无类型的指针(void* )常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void* )0例如(int*)NULL 所以在 C++11 后使用 nullptr 是明智的选择。

注意点

  1. 在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。

六、结语

到这里本篇博客就到此结束了。

对于 C++ 杂七杂八的知识点我们就讲到这里。下一篇文章我们开始讲解类和对象做好准备哦

如果觉得 a n d u i n anduin anduin 写的不错的话可以点赞 + 收藏 + 评论支持一下哦

那么我们下期见

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++