C++引用
引用:就是某一变量(目标)的一个别名,共用一块内存空间,对引用的操作就是对变量(目标)的操作。
格式:类型& 引用变量名(对象)= 引用实体
引用大家可以理解为给别人取外号。你喊他的外号或者喊他大名,他都会理你。相信大家都看过西游记吧,在西游记中的孙悟空也有很多外号,例如,美猴王、孙行者、齐天大圣、斗战圣佛等。一提到其中的一个外号,都会知道是猴子孙悟空。
下面给我们来深入了解引用和使用方法
int a = 10;
//ra是a的一个别名
int& ra = a;
我们在vs调试下可以看到,a和ra的值,a和ra的地址都是一样的。也就是说,a就是ra,ra就是a。
引用有三个要注意的地方:
1、引用在定义时必须给予初始化,否则无法通过编译。
2、一个变量可以有多个引用
3、一旦引用被初始化后,无法修改引用目标
int main()
{
int a = 10;
int& ra = a;
int b = 20;
//将b的值赋给ra
ra = b; //ra = 20
return 0;
}
我们可以发现并没有修改ra的指向,ra还是a的引用。改变ra就是改变a。
引用中还有一个比较重要的用法,那就是常引用。
常引用也就是对一个常量进行引用,但是我们要注意的是,一旦引用了常量,引用的类型必须也为常量,否则编译不通过。
int main()
{
const int a = 10;
//编译报错
int& ra = a;
//编译通过
const int& ra2 = a;
return 0;
}
这里就涉及到了权限。权限可以简单理解为可读和可写(可以被修改)。引用中只允许权限的缩小,不允许权限的放大。在上述例子中,a的权限为只读,而ra是一个可读可写的整形引用。从只读变到可读可写,权限放大了。在C++的语法上是不允许的,不能通过编译。
那能不能引用不同类型的变量呢?答案是可以的,但是引用类型必须加const
int main()
{
int a = 10;
double b = 12.34;
a = b; //创建一个临时变量tmp,将tmp的值赋给a
//不加const,无法通过编译
int& c = b;
const int& d = b;//ok
return 0;
}
我们知道,涉及到不同类型赋值会创建临时变量,通过临时变量再赋值给目标值。为什么赋值给不同类型编译不会报错,而不同类型引用就报错呢?那是因为临时变量具有常性。假如在int前加上const
,c不会直接引用b,而是引用了他们之间的临时变量tmp。
从调试的方面去看地址,我们可以知道c并不是b的引用。操作b不等于操作c,b和c没有任何关系。而引用后的临时变量在交换完后不会被归还,而是一直存在栈中,c的地址就是临时变量的地址,c的真正引用是这个临时变量tmp。
说了这么多,引用的用途到底在哪呢,哪些情况适合使用引用呢?
1、做函数参数
我们来看下面的代码:
//参数为指针
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//参数为引用
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(&a, &b);
cout << "a = " << a << " b = " << b << endl;
Swap(a, b);
cout << "a = " << a << " b = " << b << endl;
return 0;
}
运行结果:
交换函数是老生常谈的了,我们都知道,交换两个数的值,必须通过指针来交换两个数的值才能达到目的,现在我们学了引用,我们在C++中写交换函数更多写的是以引用作为参数的函数。那为什么这里用引用也可以达到交换的目的呢。那是因为函数参数为引用,当调用该函数传入的a和b,在该函数中就等价于x和y。相当于x和y是a和b的别名。对x的操作就是对a的操作,对y的操作就是对b的操作。所以能达到交换的作用。
2、做函数的返回值
int& funtion(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int a = 10;
int b = 20;
int& res = funtion(a, b);
cout << "res = " << res << endl;
return 0;
}
运行结果;
我们再来看看res和c的地址
在funtion函数中创建的静态变量c的地址和res的地址是一样的,也就是res是c的别名,两者是等价的。但是这里千万注意的是,当返回值为在函数中创建的变量时,要加上static延长变量的名字。因为当一个函数被调用时,会在函数栈帧中开普一个新的空间,当函数 执行结束时会被系统收回。里面的临时变量都会被系统回收,而此时在主函数中的res是一个已经被回收了的引用。这是不安全的,因为当有别的函数执行时,也会在函数栈帧中开辟空间,而这时会覆盖掉res的值,从而导致res的值被修改,是一个随机值。
int& funtion(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int a = 10;
int b = 20;
int& res = funtion(a, b);
cout << "res = " << res << endl;
cout << "res = " << res << endl;
return 0;
}
运行结果:
我们可以看到,当第二次输出res的值时,是一个随机值,那是在被输出之前,执行了cout
函数,将res原来的地址给覆盖了,从而产生随机值。
总结:当函数的返回值为引用类型时,必须保证返回值的生命周期大于函数的生命周期,也就是即使函数执行完后,这个变量依旧存在。
引用做函数参数,也做函数返回值,有这个必要吗,我们平时传值不也是可以得到我们想要的结果吗。其实,引用做返回值和做函数参数的效率远远大于传值的效率。
我们来测试一下:
//测试函数分别以传值和传引用的性能
struct A
{
int a[10000];
};
void fun1(A a) {}
void fun2(A& a) {}
int main()
{
A a;
int n = 0;
cout << "循环次数 :";
cin >> n;
// 以值作为函数参数
int begin = clock();
for (int i = 0; i < n; ++i)
fun1(a);
int end = clock();
cout << "以值作为函数参数:fun1() time :" << end - begin << endl;
// 以引用作为函数参数
begin = clock();
for (int i = 0; i < n; ++i)
fun2(a);
end = clock();
cout << "以引用作为函数参数 :fun2() time :" << end - begin << endl;
return 0;
}
测试结果:
//测试函数以值做返回值和已引用做返回值的性能
struct A
{
int a[10000];
};
A a;
// 值返回
A fun1()
{
return a;
}
// 引用返回
A& fun2()
{
return a;
}
int main()
{
int n = 0;
cout << "循环次数 :";
cin >> n;
// 以值作为函数返回值
int begin = clock();
for (int i = 0; i < n; ++i)
fun1();
int end = clock();
cout << "以值作为函数参数:fun1() time :" << end - begin << endl;
// 以引用作为函数返回值
begin = clock();
for (int i = 0; i < n; ++i)
fun2();
end = clock();
cout << "以引用作为函数参数 :fun2() time :" << end - begin << endl;
return 0;
}
测试结果:
测试结果可以看出,无论是引用做参数还是返回值,它的性能是高于传值的,因为在引用做返回值和做参数都不需要拷贝,从而提高了程序的性能。
再谈引用和指针
先说结论:引用的底层实现其实就是指针
我们通过以下代码来看看汇编。
int main()
{
int a = 10;
int& ra = a;
int b = 10;
int* pb = &b;
return 0;
}
我们在汇编下可以看到,引用和指针代码是一样的。那有了指针还要引用做啥呢?存在即合理,引用可以说是对指针的一个部分优化吧。当我们去使用引用时,并不会通过解引用,是因为在底层中,编译器帮我们解引用了。在传值和返回值的效率上是一样的,这也可以证明引用时指针实现的有力证据,读者可以通过上文中的测试代码进行修改,自行测试。
左图是虚拟出来的图示,我们可以看到即使是引用,它也会和指针一样存放a的地址。那为什么对ra取地址后,它的地址和a的地址一样呢,为什么不是&ra=0x04
呢,因为在在c++中,编译器会对引用优化,在你使用前就对指针进行了解引用,所以你取地址后的值,相当于地址的值解引用后的值,也就是图中的0x00
。
下面总结一下引用和指针的区别(重要):
1、引用必须在定义时初始化,指针没有要求。
2、引用在初始化时引用一个实体后,不能更改引用实体而指针可以。
3、没有NULL引用,但是有NULL指针
4、有多级指针,但是没有多级引用
5、访问实体方式不同,指针需要解引用,引用是编译器自行处理
6、引用比指针用起来相对安全
7、引用自加自减都是实体值的改变,而指针自加自减是地址的偏移
C++内联函数
内联函数:在函数声明或定义前加inline
关键字,则该函数被称为内联函数。
功能作用:c++编译器会直接将调用内联函数的地方直接展开成相对应的指令,没有了函数栈帧的开销,从而提高了程序的运行效率。
//该函数为内联函数
inline int fun(int a, int b)
{
return a * b;
}
但是并不是你想使用内联就使用内联的,决定权还是在编译器的手上。
inline特性:
1、inline
是一种以空间换时间的做法,省去调用函数的开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2、inline
不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
例如:
fun.cpp文件
inline int fun(int a, int b)
{
return a * b;
}
test.h文件
#pragma once
inline int fun(int a, int b);
test.cpp文件
#include <iostream>
#include "test.h"
using namespace std;
int main()
{
fun(10, 20);
return 0;
}
当我们运行时会报链接错误
附加C++11新特性
auto关键字
auto
:自动类型推导
在C语言中相信大部分同学也遇见过,但是在C语言中auto
的作用几乎可以忽略,然后C++中就直接把auto
换成了另一种有更好的实际应用的语法
用法格式:
//由实际值1推导出该类型为int类型
auto a = 1; //== int a = 1;
我们可以通过函数typeid()来获取变量的类型名
int main()
{
auto a = 1; //int
auto b = 2.0; //double
auto c = 'a'; //char
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
运行截图:
但是auto有一下几点是需要注意的
1、使用auto
的变量必须在定义时给初始化,因为auto
是根据你实际给的值才能推导出该变量的类型。
auto d;
2、auto
的转换是发生在编译阶段。
3、auto
能推导出指针,但是不能推导出引用
int main()
{
auto a = 1;
//通过对变量取地址推导出指针
auto pa = &a;
//明确pa2为指针类型,与上面用法结果一样
auto* pa2 = &a;
//不能推导出是引用,只能推导出与a同类型的类型
auto ra = a;
ra = 2;
cout << a << endl;
//明确ra2是一个引用,与a类型相同的引用
auto& ra2 = a;
ra2 = 3;
cout << a << endl;
cout << typeid(a).name() << endl;
cout << typeid(pa).name() << endl;
cout << typeid(pa2).name() << endl;
return 0;
}
4、当一行定义多个变量时,必须保证变量的类型保持一致。默认以第一个定义的变量类型为准
5、不能当做函数参数来用。
//编译报错
void fun(auto a){}
6、不能用来推导数组的类型,即使元素类型一致也不可以。
auto arr[] = { 0, 1, 2, 3};
基于范围的for循环
基于范围的for循环是C++中的一种“语法糖”,什么是语法糖呢,就是用起来就比较简单,比较容易,作用也大。像平时,我们要遍历一个数组,就必须通过for循环,指定数组的首元素下标和末尾元素下标。
int main()
{
int arr[] = { 0, 1, 2, 3 , 4, 5, 6, 7, 8, 9, 10 };
int n = sizeof(arr) / sizeof(int);
for (int i = 0; i < n; ++i)
{
cout << " " << arr[i];
}
return 0;
}
而现在,我们也不用算出数组长度,只需要使用基于范围的for循环,就能遍历数组。
for (int num : arr)
{
cout << " " << num;
}
运行结果:
但是通常我们用基于范围的for循环时,都可以将num的类型变成自动类型推导,当我们要修改时也可以在类型后加上引用,但是我们不想数据被修改时,可以加上const。
for (const auto& num : arr)
{
cout << " " << num;
}
注意:
1、与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2、for循环迭代的范围必须是确定的
指针空值nullptr
在C语言学习中,我们都知道,定义一个变量尽量给变量初始化。例如int类型就初始化为0,double类型就初始化为0.0,指针初始化为NULL。而在C++中,给指针初始化为NULL,并不是完全正确的初始化,而nullptr才是给指针初始化的最好的值。
我们可以看到,在宏定义中的NULL,其实就是0。把指针初始化为NULL,其实就是把指针初始化为0;
我们再来看看一下代码:
void fun(int a)
{
cout << "fun(int)"<< endl;
}
void fun(int* a)
{
cout << "fun(int*)" << endl;
}
int main()
{
fun(0);
fun(NULL);
fun(nullptr);
return 0;
}
运行结果:
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。