C++入门

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

前言

简单介绍一下C++的入门语法
在这里插入图片描述
祖师爷敬上

一、C++关键字C++98

相比于C语言来说呢C++的关键字比C语言多了一倍C语言有32个关键字C++有63个关键字
具体如下表
在这里插入图片描述
单个看起来就是63个单独的单词这对学习者来说痛苦的但是我们没必要一口气就全部记住我们在后面的学习会一个一个的不断遇到然后再加上不断的重复练习就差不多能全部记住了就好比当时我们学习C语言的关键字的时候我们也没有死记硬背的把那32个关键字给像背单词一样记下来都是通过后期的不断练习、重复记下来的C++关键字的学习也是如此

二、命名空间

首先我们学习C++的第一段代码就是

#include<iostream>
using namespace std;
int main()
{
	cout << "Hello World" << endl;
	return 0;
}

这时C++的第一段代码也是C++的基本格式或许我们在学校的时候老师告诉我们以后写C++代码第一步先把框架敲出来

#include<iostream>
using namespace std;
int main()
{
	//代码…………
	return 0;
}

你说#include我到还能理解包含头文件嘛那这个using namespace std是个什么玩意似乎去掉它cout、cin这些基本操作都用不了了甚至编译器直接报出了错误
在这里插入图片描述
在回答这个问题之前我们先来看一个例子

命令空间的定义

在C语言环境下面我们来看这段代码

#include<stdio.h>
#include<stdlib.h>
int rand=10;
int main()
{
printf("%d\n",rand);
return 0;
}

首先我们先猜一猜这段代码又没有问题
在这里插入图片描述
我们可以发现代码出现了错误很明显最显眼的错误是重定义为什么会出现重定义呢
主要是因为在stdlib.h这个文件中包含了rand()这个函数而我们定义的rand全局变量刚好和这个函数重名了编译不知道你到底想用这个rand表示什么东西这个rand符号出现了歧义编译器不知道怎么处理为此给我们报出了错误当然当我们不包含stdlib.h这个文件时我们的rand全局变量就能正常使用
在这里插入图片描述
因为这时候stdlib.h文件没有被展开rand函数也就不会被放出来在全局域中只有一个rand而这个rand是一个int型全局变量因此我们能使用
当然我们不注释掉stdlib.h文件在main函数内部定义rand也是可以的
在这里插入图片描述
因为当全局和局部都出现相同的标识符时优先使用局部的标识符这时C语言规定的首先我要打印rand我先在main局部作用域找一下有没得rand如果有的话我就直接用了如果没有的话编译器就会去全局作用域寻找有没得rand有的话就用如果还没得就报错了未定义标识符
在这里插入图片描述
对于上面出现的这种命名冲突的问题我们在做项目的时候也会很常见比如小王和小李都是做的同一个项目但是两人都是自己做自己的部分某一天小王写了一个xyz()函数用来实现两个数的减法而小李也写了一个xyz()函数但是小李写的这个函数是实现两个数的加法当将小王和小李写的代码合并在一起编译的时候代码就会出现问题
在这里插入图片描述
很明显出现了重复定义在C语言环境下没有很好的办法解决这个问题唯一办法就是改函数名将两个函数改为不同的名字像这样需要修改源码的操作很是麻烦C++作为C语言的扩展C++提出了命名空间的概念这个概念其实很好理解就是在不同的作用域中是允许存在相同的名称的变量、类名、函数名等比如test1函数中定义了一个int a那么我也可以在test2定义一个int a两者是互不影响的。
C++中利用了namespace这个关键字来实现了这一操作利用namespace + 命名空间的名字然后接一对{ } { }中即为命名空间的成员。

总结
为了解决命名污染、命名冲突的问题C++提出了命名空间的概念我们可以多个不同的命名空间中利用相同的标识符进行使用避免了命名污染、命名冲突的问题其中C++的标准库就存在与std标准空间中 利用关键字namespace+命名空间的名字可以创建命名空间
命名空间本质上就是创建一个范围
注意一个命名空间就定义了一个新的作用域命名空间中的所有内容都局限于该命名空间中

命名空间的使用

简单的命名空间的创建

namespace Hero
{
//在命名空间中可以定义变量、定义类型、定义函数创建类等操作你没有命名空间的时候是怎么操作的在命名空间中就可以怎样操作
   int a=10;
   void Show()
   {
   cout<<"Hello Hero!"<<endl;
   }
}
namespace Tom
{
int a=10;
void Show()
{
cout<<"Hello Tom!"<<endl;
}
}
上面是两个独立的命名空间两个空间里面的a、Show函数互不影响

利用命名空间解决命名冲突的问题的同时也改变了命名空间内变量、函数、类的作用域也就是说它们只能在命名空间这个作用域中使用在空间外面如果使用的话会被编译器识别为未定义的标识符但是在空间里面的变量、函数、类等的生命周期是没有变的
我们既然把这些变量、函数定义在了命名空间里面我们是为了解决命名冲突的问题同时也要满足我们能够正常使用下面有三种常见使用命名空间里面的方式

1、利用“ :: ”域操作符来访问
比如我像使用Hero空间里面的a或者Show函数我们就可以这样使用
在这里插入图片描述
显然如果某个命名空间里面的某个函数或者变量需要被重复使用的话每次都用这种操作方式会显的很繁琐为此我们接下来介绍第二种使用命名空间的方式
2、利用关键字using来讲命名空间中部分函数、变量等扩展到全局域
比如Hero中的Show函数会被大量的使用为了简便操作我们讲原本作用域在Hero命名空间的Show函数的作用域扩展到了全局域具体操作如下
在这里插入图片描述

这样的话在局部域没有找Show标识符但是在全局域就找到了于是就可以正常调用了当然我们也可以把Tom空间里面Show函数也扩展到全局域自不过这样做的话在全局域就会有两个一模一样Show函数编译无法明白你到底想调用哪一个就会直接报错
在这里插入图片描述

如果这样做的话就又会照成命名污染、和命名冲突的问题这不又回到了原点namespace也就没有意义了
3、利用using全局展开
上面我们介绍了using 全局站开命名空间中的一部分成员那么同时我们也可以将命名空间中的全部成员都向全局域展开具体操作如下
在这里插入图片描述
这样的话我们就能理解为什么我们在写C++的时候都要写一句using namespace std;了吧因为C++的标准库是在std这个命名空间中实现和定义的我们如果不全局展开的话编译器就不能在全局域搜索到相关库我们所调用的库函数就会被编译器认为是为定义标识符这也是为什么不加using namespace std编译器不认识cout、cin、endl等东西因为我们并没有将其作用域扩展到全局

当然 在实际生活当中我们是不会使用全局展开的因为如果每个人写的命名空间都全局展开的话那么命名空间这个东西就没有什么意义了每写一个命名空间就全局展开这不违背了解决命名冲突的初衷为此我们通常都是使用的局部全局展开或者使用作用域限定符指定使用

命名空间的注意事项

1、命名空间允许嵌套定义
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
  return left + right;
}
namespace N2
{
  int c;
  int d;
  int Sub(int left, int right)
  {
    return left - right;
  }
}
}
2. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
int Mul(int left, int right)
{
  return left * right;
}
}

在这里插入图片描述
从命名空间上来看可以看出C++具有很强的封装性

三、C++输入&输出

C++第一个程序

#include<iostream>
using namespace std;
int main()
{
	cout << "Hello World" << endl;
	return 0;
}
  1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时必须包含< iostream >头文件
    以及按命名空间使用方法使用std;
  2. cout和cin是全局的流对象endl是特殊的C++符号表示换行输出他们都包含在包含<
    iostream >头文件中.
  3. "<<“是流插入运算符>>"是流提取运算符
  4. 使用C++输入输出更方便不需要像printf/scanf输入输出时那样需要手动控制格式。
    C++的输入输出可以自动识别变量类型.

注意早期的C++和C语言是使用的一个库这些库都是在全局域实现的C++为了能与C语言区别希望能有自己的标准库如果直接废弃掉原来C语言的库的话那么已经用C++写好的程序必然会崩溃为了不然以前写的程序崩溃同时也能够让C++拥有自己的标准库开发者们将原来C语言的标准库拷贝到了std这个命名空间里面为此C++拥有了自己的标准库同时在头文件上为了与C语言进行区分C++委员会规定C++的头文件不带.h旧版编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已经不支持了因此我们强烈推荐使用 < iostream >+std 的方式。

缺省参数

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实
参则采用该形参的缺省值否则使用指定的实参

void func(int a=10)
{
cout<<a<<endl;
}
int main()
{
func();//无传参使用默认值输出10
func(66);//有传参使用传参值输出66
return 0;
}

在这里插入图片描述

注意
1、缺省参数只能从右往左 连续 定义不能跳跃着定义也不能从左往右定义
在这里插入图片描述
2、 函数参数全部缺省的叫做全缺省参数如图
在这里插入图片描述
不是全缺省的叫半缺省参数 如图
在这里插入图片描述
3、缺省参数不能再函数定义和函数申明中同时出现没有为什么这是本贾尼祖师爷规定的
在这里插入图片描述
主要是因为怕编程者误操作将函数声明时的缺省值与函数定义时的缺省值搞的不一样这回让编译器陷入歧义不知道该用那个缺省值
在这里插入图片描述
在这里插入图片描述
4、缺省值必须是常量或者全局变量
在这里插入图片描述

四、函数重载

函数重载的概念

函数重载 是函数的一种特殊情况C++允许在同一作用域中声明几个功能类似的同名函数
些同名函数的形参列表 (参数个数 或 类型 或 类型顺序)不同 常用来处理实现功能类似数据类型
不同的问题。
1、参数个数不同
在这里插入图片描述
2、参数类型不同
在这里插入图片描述
3、参数顺序不同

在这里插入图片描述

函数重载的底层原理

为什么C++支持函数重载而C不支持呢
也就是回答C++是如何区别出 Adddoubleint和Addintdouble是不同的两个函数
主要是因为两个语言对于函数名的处理规则不同各自的编译器都有着自己对于函数名的修饰规则
由于Windows的修饰规则过于复杂我们在Linux环境下进行演示
gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】
C/C++一个程序想要运行起来需要经历预处理、编译、汇编、链接这几个阶段当汇编阶段结束链接阶段还未开始时编译器会每一个.c生成对应一个.o文件这是一个二进制文件现在我们在a.o文件里面调用了Add函数但是却是在b.o文件里面实现的Add函数为此a.o文件中生成的符号表中Add符号对应的是个 “假地址” 而在b.o文件里面生成的符号表中Add符号对应的是Add的 “真地址” 我们调用Add函数的时候肯定是用它的真地址啊为了让整个程序运行起来在链接阶段我们需要将各个.o文件所生成的符号表进行合并同时对每个符号所对应的地址进行重定位扔掉虚假的地址合并成真的地址就比如上述,a.o生成的符号表中Add符号对应的是“假地址”而在b.o文件里Add符号对应的是真地址那么链接器就会将a.o文件里面Add符号对应的“假地址”扔掉并且保留真实的符号的地址这就是链接器干的事
在这里插入图片描述
在这里插入图片描述

接下来我们回归主题我们现在在C环境下演示如果写重载函数会发生什么呢
在这里插入图片描述
不出意外报错了
主要是因为C语言编译器对于函数名的修饰规则C语言编译器仅仅只用函数名来修饰一个函数或者形成一个标识符我们也可以认为没有对函数名进行修饰直接就用函数名作为区分不同函数的办法比如Linux下的gcc编译器这对于不同函数名的函数来说C语言编译器是可以区别出这是两个不同的函数但是对于同名函数名不同参数的函数来说C语言编译器是区分不出这是两个不同的函数的因为C语言编译器仅仅只用函数名来形成函数的标识符对于相同函数名不同参数的函数来说它们形成的函数标识符是一样的就比如上面定义的两个Add函数在C语言编译器看来它们都叫Add在汇编阶段进行语法检查的时候就会发现Add被重复定义了在汇编阶段都过不去自然也就无法支持函数重载了图解如下
在这里插入图片描述
下面我们通过测试以下代码来验证上面的理论
在这里插入图片描述
下面我们将这段代码编译到汇编阶段就结束我们再来看看其汇总的符号
在这里插入图片描述

这也算是C语言的一个坑了于是为了填补这个坑在C++的环境中C++编译器不仅仅将函数名当作函数标识符还将参数类型当作也考虑了进来就比如在Linux环境中C++编译器将【_Z+函数名+函数长度+参数类型首字母】作为函数的标识符这样就解决了相同函数名不同参数的函数之间无法区分的问题就比如上面的两个Add函数在C++编译器看来Add(double x,int y)的标识符就是 _ZAdd3di ,C++编译器是以这个标识符来表示Add(double x,int y)函数的而与之对应的Add(int x,double y)的标识符在C++编译器看来就是 _ZAdd3id ,这样子一看两个是Add函数就是不同的函数了在汇编阶段进行语法检查的时候就不会检查出重定义了因为C++编译器是跟据修饰过后的函数标识符来检查的也就是说在编译器眼里它看到的都是经过修饰过后的函数名只要函数名、参数其中有一个不同那么所形成的函数标识符也就是不同的这样子相同函数名、不同参数的函数之间也就能区别了编译器自然不会报错函数重载的技术也就得以实现具体见下图
在这里插入图片描述
下面我们可以通过测试下面代码来验证
在这里插入图片描述
首先我们将这段代码编译到汇编结束还没开始链接阶段我们看一看编译器汇总的符号
在这里插入图片描述

注意函数的返回值类型不能作为函数重载的条件
为什么函数的返回值类型不能作为函数重载的条件
首先如果我们如果将函数返回值类型也作为形成函数标识符的一部份在定义的时候是不会发生重复定义的错误的但是我们调用函数的时候会出现问题
在这里插入图片描述
我的test(1,2)到底该调用那个函数呢主要是因为我们在调用函数的时候并没指定函数的返回值类型这对于编译器来说就陷入了“选择困难”编译器到底该怎么选就会出现歧义这是编译器不允许的
为此函数的返回值类型不能作为函数重载的条件

五、引用

基本语法引用类型 +& +名字
egint a=10;
int & b =a;
引用在我们简单理解起来就是取别名
比如现在有一块空间叫a那么我们也可以再给这块空间取个名字叫ba、b表示的都是同一块空间
在这里插入图片描述
就好比黑旋风表示是李逵李逵也表示是李逵虽然有两个名字但是都是表示同一个人

引用的基本规则

1、引用必须赋初值
eg
在这里插入图片描述
2、引用一旦初始化过后后面就不能再去充当其它空间的引用了
eg
在这里插入图片描述
3、一个变量可以有多个引用
eg
在这里插入图片描述

常引用

eg1:
int &a=10;//×
const &b=10;//√
///
eg2:
const int a=10;
int &b=a;//×
const int &b=a;//√
//
eg3:
double a=3.14;
int &b=(int)a;//×
const int &b=(int)a;//√

那么为什么上面那样写就行这样写就不行呢
首先我们需要明确两点
1、一块空间的读写权限可以被放小但是不能被放大
2、临时变量具有常性
明确这两点我们解释起来就比较轻松了
eg1
int &a=10;首先引用是对一块空间进行取别名单独的一个10属于右值没有存在于内存空间中无法对其取地址一个空间都没有如何对其进行引用为此编译器会将其值赋值进一个临时变量中去然后在对这个临时变量进行引用又因为临时变量具有常性因此int &a=10,就会将其临时空间的权限放大这是编译器不允许的就好比一个空间你原本是只读的但是你换了一个名字过后就可读可写了这属于权限放大了为此我们对其引用也应该加const进行修饰让其权限进行保持或者缩小
因此const int &b=10;是可以的
eg2
这个就是典型的权限放大了从const int a=10;可以看出这块空间原本就是只读的但是现在我们换个名字表示这块空间int &b=a;却发现没有了只读的限制了这属于权限的放大编译器不允许
eg3
在强转的时候会产生临时变量因此int &b=(int)a发生的主要流程是将a的值强转成int类型然后将这个强转过后的值赋值给临时变量然后在对临时变量进行引用临时变量具有常性简单用int&b来接受属于权限的放大编译器不允许但是加上constconst int &b 来接受属于权限的保持编译器运行
强转会产生临时变量的证明
在这里插入图片描述
a、b的地址不一样就说明了a、b表示的不是同一块空间我们除了开辟过a空间外就没有再主动开辟过其它空间那么b所表示的空间是谁开辟的除了编译器没人做到
其次a空间里面的值并没有发生改变强转并不会破坏被强转的空间里面的值

引用的使用场景

1、作为函数参数
比如原来我们写一个交换函数我们是用指针来完成的
在这里插入图片描述
但是现在我们学了引用我们也可以利用引用来实现
在这里插入图片描述
2、作为函数返回值
就比如一个统计次数的函数
在这里插入图片描述
这里我们返回的就是i这空空间的别名我们可以用用引用接受当然也可以利用同类型的变量接受
在这里插入图片描述
当然当我们利用引用做返回值时我们可以直接将函数返回值作为左值使用
在这里插入图片描述
这在C语言中是无法实现的
当然让引用作为函数的参数或者返回值都是有条件的不是随便就能做的
比如下面这段代码就是一段问题代码问题出现在哪里呢

int& Add(int x, int y)
{
	int z = x + y;
	return z;
}
int main()
{
	int &ret = Add(1, 2);
	Add(3,4);
	cout << ret << endl;
	return 0;
}

在这里插入图片描述

首先ret接受的是z的别名但是在调用完Add函数过后Add函数的栈帧就被销毁了z属于这块栈帧同样的也就跟着会被销毁也就是说我们的ret引用表示的是一块“野空间”使用权不属于我们不受保护的空间为此当我们去输出ret所表示的空间的值的时候输出结果是未定义的或许在回收这块空间过后OS就分配给了别的程序去使用里面的数据就被修改了也或许OS暂时还没有分配给其他程序里面数据还存在到底是那种情况是由编译器来决定的这时候再来讨论输出结果也就没有意义了至于上面的输出结果为啥是7也就显得不是那么重要了接连2次连续调用Add函数两次Add函数都在同一块空间上建立栈帧自然z也就是对应一样的空间同时又恰巧碰到编译器在Add函数调用结束过后没有清理该空间我们偶然的就发现输出结果为7了对于上面的输出结果我们不必过多在意我们只需要理解为什么不重要就行

为此我们可以总结出要想使用引用作为函数的返回值或者参数我们必须满足以下条件
引用的空间在出了被调用的函数栈帧时生命周期依然存在不会随着被调用的函数栈帧的销毁而销毁

既然说到这里了我们再来了解一下以值传递指针、普通类型作为返回值的函数都是需要借助临时变量来完成返回的
eg

int Add(int x,int y)
{
int z=x+y;
return z;
}
int main()
{
int ret=Add(1,2);
return 0;
}

首先z的值并不是直接返回给ret的而是先在执行return z这条语句的时候会将z的值拷贝进临时变量中去数据所占空间比较小的话一般由寄存器充当临时变量如果比较大的话那么临时空间一般都是有上即调用函数栈帧中的部分空间充当待被调用函数栈帧被销毁过后才会将临时变量的值赋值给ret
那么z为什么不能先赋值给ret在销毁呢
首先函数栈帧是由esp和ebp两个栈帧维护的在这个栈帧中开辟的任何空间编译器都能找到但是ret实在main函数的栈帧中开辟的编译器无法通过esp和ebp来寻找到ret自然也就无法完成先赋值在销毁
那么为什么需要临时变量呢
假设我们不需要临时变量在函数栈帧销毁后在对ret进行赋值那么这时候z空间已经不属于我们了我们自然也就无法保证z空间里面存的数据是否安全可能这时取出来的值是个随机值、也可能是原有的值这是由编译器决定的为此为了保证的数据安全我们需要一个临时变量来暂时存储z的值让z空间在销毁过后也能完成正确的赋值

传值、传引用效率比较

以值作为参数在传参期间编译器不会将实参直接传递给形参而是将实参拷贝一份给形参而对于以引用作为参数在传参阶段则不会发生拷贝
以值作为函数返回值编译器也会将返回值拷贝进临时参数然后再返回
对于用引用作为函数返回值这无需发生拷贝
以上皆在正确操作的情况下!!!

值和引用的作为参数的性能比较

测试代码

#include <time.h>
struct A{ int a[10000]; };
void TestFunc1(A a){}
void TestFunc2(A& a){}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

在这里插入图片描述
总体上来说都是差不多但是细节起来的话还是引用比较快一点

值和引用的作为返回值类型的性能比较

测试代码

#include <time.h>
struct A{ int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a;}
// 引用返回
A& TestFunc2(){ return a;}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

在这里插入图片描述
这样的差距就比较大了明显还是引用作为函数的返回值类型快一点

小小总结一下

产生临时变量的情况
1、对不同类型的空间实行强转时
2、对右值进行引用时比如 const int &a=10;
3、函数返回值以值传递的方式实现
4、引用作为参数但是传参的时候出现类型不匹配
什么是左值、什么是右值
左值左值是一个表示数据的表达式如变量名、解引用以后的指针。左值可以取地址和赋值因为变量一旦被声明就会在栈上或者堆上开辟一块相应的空间。我们可以取地址来访问到这块空间
右值右值也是一个表示数据的表达式如字面常量、表达式返回值、函数返回值等。右值不能取地址因为一般右值都是一些临时变量比如函数返回值函数执行完毕以后会将返回值赋值给寄存器或者一个临时变量我们无法获取一个临时变量的地址。

引用与指针的对比

1、在语法上引用是不需要开辟空间的指针需要开辟空间但是在底层的实现上引用其实也是需要开辟空间的
2、引用只有一级引用不存在指针那样的多级
3、引用+1可能只是数值上的+1并不一定是+引用类型的大小指针+1一定是+实际指向类型的大小
4、引用必须初始化指针不需要
5、引用一旦初始化了后续就不能改变引用的对象了指针可以更改其指向
6、对于引用求大小算出来的是所引用的类型的大小对于指针求大小答案是固定的32位下4字节64位下8字节
7、引用并不能完全替代指针的工作有些操作是引用完成不了的比如将链表的指针域用引用来替换那么删除操作就无法完成
8、引用比指针用起来更加安全指针具有对空指针解引用的风险引用不存在

六、内联函数

背景

首先我们可以先用宏写一个加法函数

#define ADD(X,Y) ((X)+(Y))
当然写出一个正确的宏函数出来并不简单
1、我们需要考虑括号的问题避免因为少加括号的问题造成优先级问题导致结果与预期不相符合
2、最好不要再宏后面加分号防止因为我们想使用宏所产生的整体结果而造成的语法错误
3、需要考虑宏函数的代码量由于宏是简单粗暴的文本替换如果宏函数代码量比较大同时代码中也多次使用该宏函数如果在使用宏函数整体的代码量就会上去编译代码所花的时间也会更加长
4、宏函数无法调试
5、宏函数无法对于参数的类型进行检查
这些都是宏的不足之处

但是宏函数也是有优点的

1、宏函数不需要建立栈帧避免了在建立栈帧上的时间消耗效率更高

为此对于宏函数的缺点呢我们想要改进优点呢我们想要保留C++给我们提供了一种技术内联函数
C++提供inline关键字来修饰我们写的函数使我们的函数变成内联函数

内联函数的使用

概念 以inline修饰的函数叫做内联函数编译时C++编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销内联函数提升程序运行的效率。
实操 就比如一个交换函数Swap我们利用 inine 修饰一下
在这里插入图片描述
我们接着来使用一下内联函数
在这里插入图片描述
也能得到同样的效果

如何证明内联函数没有建立栈帧

  1. 在release模式下查看编译器生成的汇编代码中是否存在call Add
  2. 在debug模式下需要对编译器进行设置否则不会展开(因为debug模式下编译器默认不会对代码进行优化以下给出vs2022的设置方式
    在这里插入图片描述

内联函数的优点及注意事项

1、inline修饰的函数并不一定是内联函数inline只是起一个建议的作用当函数代码量太大时编译器不会将其优化成一个内联函数而是将其当作一个普通函数对待
在这里插入图片描述
在这里插入图片描述
2、内联函数在编译阶段会被用函数体替换少了调用的开销提高了程序运行效率
3、一般情况下我们程序员是在debug模式下开发在这个模式下内联函数还没有被优化替换为了就是方便我们调试在release模式下就开启了优化完成了替换也就不能调试了debug模式下也可以按照上面的设置来实现内联函数的优化只不过这时候我们在按f11vs2022就无法进入内联函数内部了
4、内联函数不建议声明和定义分开因为内联函数的声明和分离分开的话可能会导致内敛函数无法正常使用
eg

// head.h
#include <iostream>
using namespace std;
inline void Swap(int& a, int& b);
// Code2-1.cpp
#include "head.h"
void Swap(int& a, int& b)
{
	int temp = a;
	a = b;
	b = temp;
}
// Code2-2.cpp
#include "head.h"
int main()
{
    int a = 10;
	int b = 20;
	Swap(a,b);
    return 0;
}

在这里插入图片描述
我们可以看到无法解析的外部符号出现这个报错一般都是出现在链接阶段
接下来我们来详细分析一下到底哪里出现了问题

在这里插入图片描述

七、auto关键字(C++11)

在早期C/C++中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量但遗憾的是一直没有人去使用它因为在早期C/C++变量默认就是auto的
在C++11中auto获得了新生auto不再是一个存储类型指示符而是作为一
个新的类型指示符来指示编译器auto定义的变量必须由编译器在编译时期推导而得简单点来说就是auto能够自动的根据赋值操作符右边的类型自动为赋值操作符左边的变量定义其类型

auto实操

测试代码

int main()
{
	int a = 10;
	auto b = 3.14;//b是double类型
	auto pa = &a;//pa是int*类型
	//对于如何验证b和pa类型我们可以通过一下操作方式
	cout << "a的类型:" << typeid(a).name() << endl;
	cout << "b的类型:" << typeid(b).name() << endl;
	cout<< "pa的类型:" << typeid(pa).name() << endl;
	return 0;
}

在这里插入图片描述

auto注意事项

1、 auto定义的变量必须初始化因为它就是根据=右边的类型来为=左边的变量定义合适类型
2、 auto并非是一种“类型”的声明而是一个类型声明时的“占位符”编译器在编译期会将auto替换为变量实际的类型
3、 对于利用auto定义引用变量时必须带上“&”
eg:
int b=10;
auto & a=b;//这个"&“就表示a是个引用auto表示引用类型
当然对于指针就没有这样的必须了
auto * a=&b;” * "表示a是一个指针auto表示其指向类型
auto a=&b;a也是指针类型
4、 auto可以在同一行定义多个同类型的变量
在这里插入图片描述
一般情况下auto就默认是第一次自动识别到的类型我们对3.1强转为int类型报错也就消失了
5、 auto不能作为函数参数因为auto不能对实参类型进行推导
6、 auto不能用来声明数组
在这里插入图片描述
对于一些比较长的类型名我们可以利用auto来自动推导当然我们也可以利用typedef

八、基于范围的for循环(C++11)

在C++98中如果要遍历一个数组可以按照以下方式进行
在这里插入图片描述
对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围;
在C++11中我们可以按照下列方式完成相同的操作
在这里插入图片描述
这段代码代表的意思是将arr数组中每个元素拷贝到i中然后输出i从数组第一个元素开始到最后一个元素结束
这里的arr不是表示的数组首元素地址而是整个数组如果我们传数组首元素地址的话这个范围for循环就不知道这个数组的范围了就会报错
注意与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。

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

问什么会出现nullptr呢不是已经又NULL表示空指针了吗
我们先来看看这段代码

//}
void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	return 0;
}

在这里插入图片描述
f(0)与我们预想一样但是f(NULL)似乎与我们预想不一样
接着我们再来看看C++中NULL的定义
在这里插入图片描述

我们可以看到NULL在C++中就是00就是NULLNULL是个int类型
在C语言中NULL是个void*类型
但是不论采取何种定义在使用空值的指针时都不可避免的会遇到一些麻烦
使用 #define NULL 0 会得到与我们初衷相违背的结果
使用 #define NULL ((void*)0) 也不会去调用f(int*),因此也会得到与我们初衷相违背结果因为void*不能被赋值给其它基本类型指针但是其它基本类型指针能被赋值给void*
现在我们目的是参数为NULL的时候我们预期是调用f(int*)无论是用那种NULL的实现方式都不能得到预期结果
那么自然的我们也能理解为什么f(NULL)会输出f(int)了
那么实际上NULL是个“假”的空指针
为此为了得到预期结果C++给我们提供了关键字nullptr 来解决这个问题

注意

  1. 在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++