引用
理解:什么是引用
引用其实就是给已经存在的变量取别名
,也就是说引用不会定义出一个新的变量,实际上是不会开辟新的空间,二者共同使用同一块内存空间。
举个例子:也就是大家给你起了一个绰号,不管叫你的名字还是叫你的绰号,实质上大家想叫的都是你这个人
引用的使用(初识引用)
演示如下
int main()
{
int a = 10;
int& ra = a;
printf("%d\n", a);
printf("%d\n", ra);
return 0;
}
可以看到这里ra是a的引用,二者使用同一块内存空间,因此类型与值都相同
由此我们可以得到,应该如下使用引用
类型& 引用变量名(对象名)= 引用实体
此处要记得区别指针定义
int a = 10;
int* b = &a;//这个是指针
int& ra = a;//这个是引用
引用特性
正是基于引用存在上述的特点,也就决定了引用有自身的特性
1.引用在定义时必须初始化(也就是必须指明引用对象,否则报错)
int a = 10;
int & ra;//报错,引用在定义时必须初始化
2.一个变量可以有多个引用(一个人可以有多个绰号)
int a = 10;
int& ra = a;
int& rra = a;
3.引用存在强对应关系,一旦引用一个实体,再不能引用其他实体(否则报错:重定义;多次初始化)
int a = 10;
int b = 20;
int& ra = a;
int& ra = b;//报错:重定义;多次初始化
特殊:const 修饰的常引用
对于常引用,我们需要知晓两条结论
1.指针和引用的赋值中,权限可以缩小,但是不可以扩大
const int a = 10;
// int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
在此处 a 的权限由于加了const修饰,a为常量权限就只能读,不支持修改。如果写成int& ra = a 的话权限就被扩大成可读可写了,因此会报错,所以要引用定义时再加const修饰
2.临时变量具有常属性,也是不允许修改的
例1:类型转换
double d = 13.14
//int& rd = d;//类型不同,转换后权限扩大编译出错
const int& rd = d;//这里的d时类型转换后的临时变量
在这里,类型转换时会生成临时变量,临时变量具有常属性,也是不允许修改的
例2:函数返回接收
int count()
{
int n = 0;
n++;
return 0;
}
int main()
{
//int& rn = count()//权限扩大,报错
const int& rn = count()//权限平移
return 0;
}
在这里,对于函数返回值来说,当返回函数时,临时变量会存在上一个调用它的函数栈帧中,因此也是临时变量,具有常属性
引用的使用场景
在了解了引用的基本介绍之后,我们来看一下引用的基本使用场景
0.引用和指针的区别与联系
在正式了解引用的基本使用场景时我们还得先搞清楚指针和引用有什么关系
1.联系
按图所示,指针和引用在一些功能上是可以相互替代的,有一定的交叉区间,但是由于c++的引用具有强连接性,一旦引用一个引用实体便不能再引用其他实体,所以注定了c++里的引用无法替代指针
此外:
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
2.区别
引用概念上定义一个变量的别名,指针存储一个变量地址。
引用在定义时必须初始化,指针没有要求
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体没有NULL引用,但有NULL指针
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
有多级指针,但是没有多级引用
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
引用比指针使用起来相对更安全
1.做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
return 0;
}
在这个交换两个数的函数中,使用指针也可以达到相同的效果,这个作用是指针和引用的共同作用
但是在这里使用引用替代指针可以达到如下效果:
1.减少拷贝,提高效率(改变引用就是改变本体,不需拷贝)
2.做输出型参数,同时修改形参和实参
2.做返回值
1.在这里我们得先明白内存空间销毁意味这什么
1.这个空间还在,只是使用权已经不属于我们,我们存放的数据已经不受保护了
2.空间还能被访问,只是我们读写的数据是不确定了
因为空间销毁了还存在三种情况
1.空间还没有被分配,里面的值也没有被覆盖,此时就可以正常访问
2.空间还没有被分配,但是里面的数据已经被销毁了,此时访问出来的值随机
3.空间被分配了,空间里面的值也被覆盖了,此时就直接无法访问
2.第二个预备知识我们要了解函数是如何返回参数回去的
(也就是一般的传值返回)
int Count()
{
int n = 0;
n++;
return n;
}
main()
{
int a =Count();
return 0;
}
在调用函数的时候,会开辟函数栈帧,而栈帧是向下生长的(如下图)
在将n返回时,不会直接返回n,而是创建一个临时变量(比较小的话存在寄存器中,大一点的话存在上一层的函数栈帧,总之保证它不被销毁);临时变量的类型为返回值的类型,然后会返回临时变量
错误写法
int& Count()
{
int n = 0;
n++;
return n;
}
main()
{
int a =Count();
return 0;
}
此时就是传引用返回,a就是n的别名了,但是n是一个局部变量,存在Count函数的栈帧中,一旦出了函数栈帧,n就会被销毁,那就会出现越界访问,可能会出现随机值造成错误
正确写法
int& Count()
{
static int n = 0;
n++;
return n;
}
main()
{
int a =Count();
return 0;
}
使用static修饰就可以将变量存到静态区,不会随着函数栈帧销毁而销毁,从而避免越界访问
总结
出了函数作用域返回时变量不存在的话不能用引用返回,返回变量还存在,才能使用引用返回
内联
概念
以inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
int Add(int left, int right)
{
return left + right;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
return 0;
}
这个函数我们可以在汇编上看到进行了函数调用
inline int Add(int left, int right)
{
return left + right;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
return 0;
}
如果将其添加成内联函数,则会在编译期间编译器会用函数体替换函数的调用。
查看方式:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不显示)
先右击整个项目,选择属性
此时我们可以观察到不再对Add函数进行调用了,因为已经展开
内联的优点(与宏的区别与联系)
内联函数没有函数调用建立栈帧的开销,提升程序运行的效率
c语言
中使用宏
来解决这个问题:但是宏有一定缺陷
- 宏函数不能进行调试,会在预处理阶段就直接进行替换
- 没有类型安全检查
- 容易写错(记得多加括号)
总结:内联函数代替了宏函数的功能,同时又弥补了宏函数的不足
内联的特点
- inline是一种以空间(程序编译出来的可执行程序的大小)换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性最终是否展开取决于编译器
内联函数适合使用在频繁调用的小函数上,对于太长的函数会选择不展开,避免出现代码膨胀 - . inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。
原因在于内联函数不会进入到符号表当中去,在编译时也就不会生成汇编指令(不会开辟函数栈帧)
auto关键字
出现条件
- 类型难于拼写
- 含义不明确导致容易出错
auto简介
- 早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,自动变量意为拥有自动的生命期,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期:
- C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一
个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
#include <iostream>
using namespace std;
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;//typeid 返回一个指明变量类型的字符串
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”
,编译器在编译期会将auto替换为变量实际的类型。
使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;//引用类型时则必须加&
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
不能推导使用的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};//报错
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有
lambda表达式等进行配合使用。
范围for
简介(语法)
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto e : array)
cout << e << " ";
}
int main()
{
TestFor();
return 0;
}
此时的e是一个形式参数,是不能改变原本数组里面的值的,如果需要改变则需要使用引用
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
{
e *= 2;
}
for (auto e : array)
cout << e << " ";
}
int main()
{
TestFor();
return 0;
}
范围for 的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。 - 迭代的对象要实现++和==的操作。(不属于入门阶段,后期细说)
指针空值nullptr
c++98中的NULL,可以被认为是有bug
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
因此在使用指针空值时会混淆类型,将NULL认为是int 类型的0
//语言是向前兼容的,因此错了也只能错了,不能改只能修补丁(除了python3)
以关键字的形式引入nullptr来补坑
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
的。 - 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。