C++入门

C++入门

  • 前言
  • 一、C++关键字(C++98)
  • 二、命名空间
    • 命令空间的定义
    • 命名空间的使用
    • 命名空间的注意事项
  • 三、C++输入&输出
    • 缺省参数
  • 四、函数重载
    • 函数重载的概念
    • 函数重载的底层原理
  • 五、引用
    • 引用的基本规则
    • 常引用
    • 引用的使用场景
    • 传值、传引用效率比较
      • 值和引用的作为参数的性能比较
      • 值和引用的作为返回值类型的性能比较
    • 小小总结一下
    • 引用与指针的对比
  • 六、内联函数
    • 背景
    • 内联函数的使用
    • 内联函数的优点及注意事项
  • 七、auto关键字(C++11)
    • auto实操
    • auto注意事项
  • 八、基于范围的for循环(C++11)
  • 九、指针空值nullptr(C++11)

前言

简单介绍一下C++的入门语法;
C++入门_第1张图片
祖师爷敬上!!!

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

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

二、命名空间

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

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

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

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

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

命令空间的定义

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

#include
#include
int rand=10;
int main()
{
printf("%d\n",rand);
return 0;
}

首先我们先猜一猜这段代码又没有问题?
在这里插入图片描述
我们可以发现代码出现了错误,很明显最显眼的错误是重定义!,为什么会出现重定义呢?
主要是因为在stdlib.h这个文件中包含了rand()这个函数,而我们定义的rand全局变量刚好和这个函数重名了,编译不知道你到底想用这个rand表示什么东西,这个rand符号出现了歧义,编译器不知道怎么处理,为此给我们报出了错误!当然当我们不包含stdlib.h这个文件时,我们的rand全局变量就能正常使用:
C++入门_第4张图片
因为这时候stdlib.h文件没有被展开,rand函数也就不会被放出来,在全局域中只有一个rand,而这个rand是一个int型全局变量,因此我们能使用;
当然我们不注释掉stdlib.h文件,在main函数内部定义rand,也是可以的
C++入门_第5张图片
因为当全局和局部都出现相同的标识符时,优先使用局部的标识符!这时C语言规定的!首先我要打印rand,我先在main局部作用域找一下有没得rand如果有的话,我就直接用了,如果没有的话,编译器就会去全局作用域寻找有没得rand,有的话就用,如果还没得就报错了,未定义标识符!
C++入门_第6张图片
对于上面出现的这种命名冲突的问题,我们在做项目的时候也会很常见,比如小王和小李都是做的同一个项目,但是两人都是自己做自己的部分,某一天小王写了一个xyz()函数用来实现两个数的减法,而小李也写了一个xyz()函数,但是小李写的这个函数是实现两个数的加法,当将小王和小李写的代码合并在一起编译的时候,代码就会出现问题:
C++入门_第7张图片
很明显出现了重复定义,在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函数,我们就可以这样使用:
C++入门_第8张图片
显然如果某个命名空间里面的某个函数或者变量需要被重复使用的话,每次都用这种操作方式会显的很繁琐,为此我们接下来介绍第二种使用命名空间的方式;
2、利用关键字using来讲命名空间中部分函数、变量等扩展到全局域;
比如Hero中的Show()函数会被大量的使用,为了简便操作,我们讲原本作用域在Hero命名空间的Show函数的作用域扩展到了全局域,具体操作如下:
C++入门_第9张图片

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

如果这样做的话,就又会照成命名污染、和命名冲突的问题,这不又回到了原点,namespace也就没有意义了;
3、利用using全局展开
上面我们介绍了using 全局站开命名空间中的一部分成员,那么同时我们也可以将命名空间中的全部成员都向全局域展开,具体操作如下:
C++入门_第11张图片
这样的话我们就能理解为什么我们在写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++入门_第12张图片
从命名空间上来看,可以看出C++具有很强的封装性!!!

三、C++输入&输出

C++第一个程序:

#include
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 >+std 的方式。

缺省参数

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

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

C++入门_第13张图片

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

四、函数重载

函数重载的概念

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

C++入门_第22张图片

函数重载的底层原理

为什么C++支持函数重载,而C不支持呢?
也就是回答C++是如何区别出 Add(double,int)和Add(int,double)是不同的两个函数!
主要是因为两个语言对于函数名的处理规则不同;各自的编译器都有着自己对于函数名的修饰规则
(由于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++入门_第23张图片
C++入门_第24张图片

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

这也算是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++编译器是跟据修饰过后的函数标识符来检查的,也就是说在编译器眼里它看到的都是经过修饰过后的函数名!只要函数名、参数其中有一个不同,那么所形成的函数标识符也就是不同的,这样子相同函数名、不同参数的函数之间也就能区别了,编译器自然不会报错,函数重载的技术也就得以实现!(具体见下图:)
C++入门_第29张图片
下面我们可以通过测试下面代码,来验证:
C++入门_第30张图片
首先我们将这段代码编译到汇编结束,还没开始链接阶段,我们看一看编译器汇总的符号:
C++入门_第31张图片

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

五、引用

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

引用的基本规则

1、引用必须赋初值!
eg:
C++入门_第34张图片
2、引用一旦初始化过后,后面就不能再去充当其它空间的引用了;
eg:
C++入门_第35张图片
3、一个变量可以有多个引用;
eg:
C++入门_第36张图片

常引用

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来接受,属于权限的放大,编译器不允许,但是加上const,const int &b 来接受,属于权限的保持,编译器运行!
强转会产生临时变量的证明:
C++入门_第37张图片
a、b的地址不一样就说明了a、b表示的不是同一块空间,我们除了开辟过a空间外就没有再主动开辟过其它空间!那么b所表示的空间是谁开辟的?除了编译器,没人做到!
其次,a空间里面的值并没有发生改变!强转并不会破坏被强转的空间里面的值!

引用的使用场景

1、作为函数参数
比如原来我们写一个交换函数我们是用指针来完成的:
C++入门_第38张图片
但是现在我们学了引用,我们也可以利用引用来实现:
C++入门_第39张图片
2、作为函数返回值
就比如一个统计次数的函数:
C++入门_第40张图片
这里我们返回的就是i这空空间的别名,我们可以用用引用接受,当然也可以利用同类型的变量接受!
C++入门_第41张图片
当然当我们利用引用做返回值时,我们可以直接将函数返回值作为左值使用:
C++入门_第42张图片
这在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;
}

C++入门_第43张图片

首先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 
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;
}

C++入门_第44张图片
总体上来说都是差不多,但是细节起来的话还是引用比较快一点!

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

测试代码:

#include 
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;
}

C++入门_第45张图片
这样的差距就比较大了,明显还是引用作为函数的返回值类型快一点!

小小总结一下

产生临时变量的情况:
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 修饰一下:
C++入门_第46张图片
我们接着来使用一下内联函数:
C++入门_第47张图片
也能得到同样的效果!

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

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add;
  2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2022的设置方式)
    C++入门_第48张图片

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

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

// head.h
#include 
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;
}

C++入门_第50张图片
我们可以看到,无法解析的外部符号,出现这个报错,一般都是出现在链接阶段!
接下来我们来详细分析一下到底哪里出现了问题:

C++入门_第51张图片

七、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可以在同一行定义多个同类型的变量:
C++入门_第52张图片
一般情况下auto就默认是第一次自动识别到的类型,我们对3.1强转为int类型报错也就消失了;
5、 auto不能作为函数参数,因为auto不能对实参类型进行推导;
6、 auto不能用来声明数组;
C++入门_第53张图片
对于一些比较长的类型名,我们可以利用auto来自动推导,当然我们也可以利用typedef;

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

在C++98中如果要遍历一个数组,可以按照以下方式进行
C++入门_第54张图片
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围;
在C++11中我们可以按照下列方式完成相同的操作:
C++入门_第55张图片
这段代码代表的意思是,将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;
}

C++入门_第56张图片
f(0)与我们预想一样,但是f(NULL)似乎与我们预想不一样!
接着我们再来看看C++中NULL的定义:
C++入门_第57张图片

我们可以看到NULL在C++中就是0,0就是NULL,NULL是个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。

你可能感兴趣的:(C++,c++,c语言,开发语言)