【C++初探:简单易懂的入门指南】二
- 1.引用
- 1.1引用做函数的参数
- 1.2 引用做返回值
- 1.2.1 关于引用做返回值的几点补充
- 1.3 多引用(对一个变量取多个别名)
- 1.4 引用类型一致性原则以及权限的问题阐述
- 1.5引用的效率问题
- 1.6引用和指针的比较
- 2.auto关键字
- 2.1 auto关键字的使用细则
- 2.2 auto关键字不能使用的场景
- 3.特殊的for循环(基于范围)
- 3.1基于范围for的语法
- 3.2 基于范围for的使用规则
❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
1.引用
引用是C++与C不同的点之一,它虽然是给变量取别名,算不上一个新定义的概念,但是它和
typedef
的区别还是存在的,例如它可以在函数做参数和返回值时使用,但是typedef
没有这种功能,&
是一个操作符,表示引用,你可以理解为要给一个变量取别名。
1.1引用做函数的参数
下面我们给出一段代码帮助你理解引用的最常见的功能:
#include<iostream>
using namespace std;
int Add(int& a, int& b)
{
return a + b;
}
int main()
{
int a = 3;
int b = 4;
int c = Add(a, b);
cout << "a+b = " << c << endl;
return 0;
}
运行结果截图:
上面Add
函数的两个参数就是实参a
和b
的别名,就相当于我们人类社会里的绰号,就比如你叫张三,你的同学可能叫你老张,你的家里人可能叫你三儿,虽然叫法不同,但是它们都代表你这个人,而且引用是不额外开空间的,我们可以利用下面的代码简单的验证一下:
#include<iostream>
using namespace std;
void Fun(int& b)
{
cout << "b的地址为:" << &b << endl;
}
int main()
{
int a = 3;
Fun(a);
cout << "a的地址为:" << &a << endl;
return 0;
}
运行截图为:
可以看到这里引用是不开空间的,因为b
是a
的别名,所以编译器不会给它们开两份空间。
1.2 引用做返回值
#include<iostream>
using namespace std;
int& Add(int& a, int& b)
{
static int c = a + b;
cout << "c的值为" << c << endl;
cout << "c的地址为" << &c << endl;
return c;
}
int main()
{
int a = 3;
int b = 4;
int& d = Add(a, b);
cout << "d的值为" << d << endl;
cout << "d的地址为" << &d << endl;
return 0;
}
运行结果:
这里d
就是c
的别名,所以它们的地址是一样的,static
修饰c
,是因为临时变量开在栈区,出了函数的作用域,它就销毁了,但是如果加了static
,c
就变成了静态变量,静态变量的空间是开在静态区的,程序的结束,它的生命周期才算结束,至于为什么d
作为c
的别名,在外面还可以访问,可以类比,函数以引用传参理解,这里博主认为引用扩大了c
的的作用域,只要c
的生命周期没结束,它以引用返回,在main函数里面我们就是能访问到c
。
如果你不相信,我们可以通过如下代码简单的验证一下:
#include<iostream>
using namespace std;
int& Add(int& a, int& b)
{
static int c = a + b;
cout << "c的值为" << c << endl;
cout << "c的地址为" << &c << endl;
return c;
}
int main()
{
int a = 3;
int b = 4;
int& d = Add(a, b);
d++;
Add(a, b);
return 0;
}
运行结果截图:
- 这里注意一点,由于
c
开在静态区,只要程序不结束,它的空间一直不会被系统回收,所以定义c
那部分代码是不会重新执行一次的,这恰好可以帮助我们验证以引用返回,就可以在main函数里面访问原本作用域在Add
函数里面的静态变量c
。
1.2.1 关于引用做返回值的几点补充
细心的朋友可能会在vs2019上发现这样的问题,上述代码,即使不加static
似乎也能正常运行。
这里博主认为是编译器检查机制的一个漏洞,系统没有将空间及时的回收,如果我把代码改成这样,系统把空间回收使用后,d的值就变成随机值了,
但是如果你加了static
就不会出现这种问题:
1.3 多引用(对一个变量取多个别名)
在C++中我们是支持对一个变量进行多次引用的:
#include<iostream>
using namespace std;
int main()
{
int a = 3;
int& b = a;
int& c = a;
int& d = a;
cout << "b的地址为:" << &b << endl << "b的值为" << b << endl;
cout << "c的地址为:" << &c << endl << "c的值为" << c << endl;
cout << "d的地址为:" << &d << endl << "d的值为" << d << endl;
c = 2;
cout << "b的地址为:" << &b << endl << "b的值为" << b << endl;
cout << "c的地址为:" << &c << endl << "c的值为" << c << endl;
cout << "d的地址为:" << &d << endl << "d的值为" << d << endl;
return 0;
}
运行结果截图:
而且由于引用只是取别名,本质上它们是同一个变量,所以修改一个就修改了它们所有的值。
1.4 引用类型一致性原则以及权限的问题阐述
上述代码如果加上这样一行就会报错:
#include<iostream>
using namespace std;
int main()
{
int a = 3;
int& b = a;
double& c = a;
int& d = a;
cout << "b的地址为:" << &b << endl << "b的值为" << b << endl;
cout << "c的地址为:" << &c << endl << "c的值为" << c << endl;
cout << "d的地址为:" << &d << endl << "d的值为" << d << endl;
c = 2;
cout << "b的地址为:" << &b << endl << "b的值为" << b << endl;
cout << "c的地址为:" << &c << endl << "c的值为" << c << endl;
cout << "d的地址为:" << &d << endl << "d的值为" << d << endl;
return 0;
}
报错截图:
所以我们在定义引用时,不能改变原变量的类型。
关于权限的问题,主要围绕const
这个关键词展开:
const
修饰的变量,当对其引用时,不能不加const
,因为其是不可修改的常量,不加const
是对其权限的放大,编译器是不允许的。- 但是如果一个变量没有被
const
修饰,在引用时,可以加上const
,进行权限的缩小,这个编译器是允许的。
但是缩小权限又是允许的:
这里有个比较奇怪的现象,为什么我const
修饰b
这个别名,b
不能修改,我却可以通过修改a
来修改a
的值,与此同时b
的值也被修改了,这里本博主也比较疑惑,大家可以在评论区或者私信来教教博主。 - 关于引用还有一点需要说明,引用必须给初始值,引用的对象可以是全局变量、临时变量、但不能是常量(
const
修饰的变量例外)。
1.5引用的效率问题
引用的效率是很高的,因为它不会额外的去开空间,下面两段代码希望可以帮助你来理解:
- 传值和引用传参的效率比较
#include<iostream>
using namespace std;
#include <time.h>
struct A
{
int a[100000];
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
运行截图:
这里clock
函数是表示当前程序运行的时间,单位是毫秒,可以看到,传值和传引用的效率差的还是很大。
- 传值返回和传引用返回的效率比较
include<iostream>
using namespace std;
#include <time.h>
struct A
{
int a[100000];
};
A a;
//传值返回
A TestFunc1()
{
return a;
}
//传引用返回
A& TestFunc2()
{
return a;
}
void TestRefAndValue()
{
// 以值作为函数的返回值类型
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(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
运行截图:
可以看到,由于传值传参和传值返回都需要额外拷贝开一份空间,而传引用不需要,所以效率是差别还是很大的,所以一般情况下,如果不是特殊需求,传引用的性价比还是更高的。
1.6引用和指针的比较
相同点:
- 效率都很高。
- 底层汇编代码很相似,引用的底层实现是指针,也就是引用在底层实现上是开了空间的。
- 传址引用和传引用返回都可以改变该变量的值,当然特殊情况例外(
const
修饰的变量)。
不同点:
1.引用在语法概念上没有开空间。
2.指针不初始化不会报错,但是引用不行。
3.创建一个引用之后,这个引用就不能再作为其它变量的别名了,但是指针变量可以指向其它相同类型的变量。
2.auto关键字
auto
是C++上面的一个关键字,它可以自动识别右值的类型,我们主要介绍C++11标准的auto
关键字。
#include<iostream>
using namespace std;
int& Add(int& a, int& b)
{
static int c = a + b;
return c;
}
int main()
{
int a = 2;
int b = 3;
auto c = Add(a, b);
cout << "b的类型为:" << typeid(b).name() << endl;
return 0;
}
运行结果:
- 注意:引用的类型和被引用的对象是一致的,
typeid(变量名).name()
可以用来打印变量的类型。
可能会有人认为这样没有什么实质的作用,但是当那个函数的返回值类型(因为C++有很多自定义类型)非常复杂时,auto
关键字就非常方便了。
2.1 auto关键字的使用细则
auto
关键字可以和指针、引用结合起来使用,但是必须给它初始化,否则语法上是无法通过的。
#include<iostream>
using namespace std;
int main()
{
int a = 2;
int b = 3;
auto* c = &a;
auto d = &a;
auto& e = a;
cout << "c的类型为:" << typeid(c).name() << endl;
cout << "d的类型为:" << typeid(d).name() << endl;
cout << "e的类型为:" << typeid(e).name() << endl;
return 0;
}
运行截图:
- 这里在定义指针时
auto
和auto*
没有什么区别,但是在定义引用时必须加上&
操作符。
如果不初始化,就会报这样的错误:
这也间接说明了auto
不是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量的类型必须由编译器在编译时期推导而得,所以如果你不给初始值,那么编译器就无法进行推导,auto在编译期间会被替换。
auto关键词可以同时定义很多变量,但是它只会对第一个变量的类型进行推导,从而用这个变量的类型来定义其它变量,所以这些变量的类型要相同,详细请看下图:
2.2 auto关键字不能使用的场景
- auto关键字不能作为函数的参数,因为编译器无法对其类型进行推导。
- auto不能用来声明数组。
-
这里有一点需要说明的时,虽然VS2019上,以
auto
作为返回值的类型是可以编译通过的,但还是不建议这样去做,因为如果我们后期想要去找到其返回值的类型还是比较麻烦的,因为可能出现这样的情况:
这里还只有两个嵌套,如果工程量一大,嵌套的次数变多,想知道某个函数的返回值就是一件困难的事情,有人说可以用typeid(变量名).name()
来知道其类型,我直接写出来不是更方便吗? -
auto
在实际中最常见的优势用法就是跟以后会提到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
3.特殊的for循环(基于范围)
3.1基于范围for的语法
在C语言/C++98中,如果我们想遍历一个数组,你可能会这样做:
#include<iostream>
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5,6,7,8 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
array[i] *= 2;
}
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); p++)
{
cout << *p << " ";
}
return 0;
}
但是对于一个本身就有范围的集合来说,我们程序员自己去控制范围似乎有点多余了,而且还很容易出错,因此C++11中引出了基于范围的for循环。它的for循环括号里被:
分为两部分,左边是用来迭代的变量(迭代可以理解为遍历),右边是范围,代码是这样实现的:
#include<iostream>
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5,6,7,8 };
for (auto& p : array)
{
p *= 2;
}
for (auto p : array)
{
cout << p << " ";
}
return 0;
}
这里auto
可以换成数组相应的类型,但是使用auto
编译器可以帮助我们在编译期间推导类型,十分方便,但是我们如果想要改变数组的值就得使用引用了,因为如果不是引用左边的变量只是我们数组值的一个拷贝,改变它不能改变我们数组中的值。
下面一段代码希望帮助你完全理解它们:
#include<iostream>
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5,6,7,8 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
cout << &array[i] << " ";
}
cout << endl;
for (auto p : array)
{
cout << &p << " ";
}
cout << endl;
for (auto& p : array)
{
cout << &p << " ";
}
return 0;
}
运行结果:
我们可以看到引用是控制台第三行,它的地址与数组每个元素的地址是相同的,说明编译器在迭代时如果是引用,每执行一次for
循环,p
的空间就会被回收,不然由于创建引用变量后,这个引用变量不能作为其它变量的别名可知,打印出的地址应该是相同的才对,此时不相同,所以博主猜测应该是回收了,一次for
循环执行一次引用变量的创建,至于普通的迭代,可以看出第二行的地址是完全相同的,说明编译器只给这个变量开了一次空间,剩下的每次for
循环都是简单的把数组中的值赋值给它。
3.2 基于范围for的使用规则
- for循环的范围必须是确定的。
对于数组而言,它的范围就是从第一个元素到最后一个元素
- 注意,以下代码就有问题,它的范围是不确定的。
#include<iostream>
using namespace std;
void Fun(int array[])
{
for (auto p : array)
{
cout << p << " ";
}
}
int main()
{
int array[] = { 0,1,2,4,3,5 };
Fun(array);
return 0;
}
报错截图:
这是你这样写array似乎是一个数组,其实不然,它是一个保存了数组首元素的指针,你在里面计算数组的范围是无法计算出来的:
所以你也无法知道范围,自然就会报错。