【C++初探:简单易懂的入门指南】二

【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的使用规则

❤️博客主页: 小镇敲码人
欢迎关注:点赞 留言 收藏
任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。
❤️ 什么?你问我答案,少年你看,下一个十年又来了

【C++初探:简单易懂的入门指南】二_第1张图片

1.引用

引用是C++与C不同的点之一,它虽然是给变量取别名,算不上一个新定义的概念,但是它和typedef的区别还是存在的,例如它可以在函数做参数和返回值时使用,但是typedef没有这种功能,&是一个操作符,表示引用,你可以理解为要给一个变量取别名。

1.1引用做函数的参数

下面我们给出一段代码帮助你理解引用的最常见的功能:

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

运行结果截图:

【C++初探:简单易懂的入门指南】二_第2张图片
上面Add函数的两个参数就是实参ab的别名,就相当于我们人类社会里的绰号,就比如你叫张三,你的同学可能叫你老张,你的家里人可能叫你三儿,虽然叫法不同,但是它们都代表你这个人,而且引用是不额外开空间的,我们可以利用下面的代码简单的验证一下:

#include
using namespace std;

void Fun(int& b)
{
	cout <<  "b的地址为:" << &b << endl;
}

int main()
{
	int a = 3;
	Fun(a);
	cout << "a的地址为:" << &a << endl;
	return 0;
}

运行截图为:
【C++初探:简单易懂的入门指南】二_第3张图片
可以看到这里引用是不开空间的,因为ba的别名,所以编译器不会给它们开两份空间。

1.2 引用做返回值

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

运行结果:

【C++初探:简单易懂的入门指南】二_第4张图片
这里d就是c的别名,所以它们的地址是一样的,static修饰c,是因为临时变量开在栈区,出了函数的作用域,它就销毁了,但是如果加了staticc就变成了静态变量,静态变量的空间是开在静态区的,程序的结束,它的生命周期才算结束,至于为什么d作为c的别名,在外面还可以访问,可以类比,函数以引用传参理解,这里博主认为引用扩大了c的的作用域,只要c的生命周期没结束,它以引用返回,在main函数里面我们就是能访问到c
如果你不相信,我们可以通过如下代码简单的验证一下:

#include
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++初探:简单易懂的入门指南】二_第5张图片

  • 这里注意一点,由于c开在静态区,只要程序不结束,它的空间一直不会被系统回收,所以定义c那部分代码是不会重新执行一次的,这恰好可以帮助我们验证以引用返回,就可以在main函数里面访问原本作用域在Add函数里面的静态变量c

1.2.1 关于引用做返回值的几点补充

细心的朋友可能会在vs2019上发现这样的问题,上述代码,即使不加static似乎也能正常运行。
【C++初探:简单易懂的入门指南】二_第6张图片
这里博主认为是编译器检查机制的一个漏洞,系统没有将空间及时的回收,如果我把代码改成这样,系统把空间回收使用后,d的值就变成随机值了,

【C++初探:简单易懂的入门指南】二_第7张图片

但是如果你加了static就不会出现这种问题:

【C++初探:简单易懂的入门指南】二_第8张图片

1.3 多引用(对一个变量取多个别名)

在C++中我们是支持对一个变量进行多次引用的:

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

运行结果截图:

【C++初探:简单易懂的入门指南】二_第9张图片
而且由于引用只是取别名,本质上它们是同一个变量,所以修改一个就修改了它们所有的值。

1.4 引用类型一致性原则以及权限的问题阐述

上述代码如果加上这样一行就会报错:

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

报错截图:

【C++初探:简单易懂的入门指南】二_第10张图片
所以我们在定义引用时,不能改变原变量的类型。
关于权限的问题,主要围绕const这个关键词展开:

  • const修饰的变量,当对其引用时,不能不加const,因为其是不可修改的常量,不加const是对其权限的放大,编译器是不允许的。
  • 但是如果一个变量没有被const修饰,在引用时,可以加上const,进行权限的缩小,这个编译器是允许的。
    【C++初探:简单易懂的入门指南】二_第11张图片
    但是缩小权限又是允许的:
    【C++初探:简单易懂的入门指南】二_第12张图片
    【C++初探:简单易懂的入门指南】二_第13张图片
    这里有个比较奇怪的现象,为什么我const修饰b这个别名,b不能修改,我却可以通过修改a来修改a的值,与此同时b的值也被修改了,这里本博主也比较疑惑,大家可以在评论区或者私信来教教博主。
  • 关于引用还有一点需要说明,引用必须给初始值,引用的对象可以是全局变量、临时变量、但不能是常量(const修饰的变量例外)。

1.5引用的效率问题

引用的效率是很高的,因为它不会额外的去开空间,下面两段代码希望可以帮助你来理解:

  1. 传值和引用传参的效率比较
#include
using namespace std;
#include 

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

运行截图:

【C++初探:简单易懂的入门指南】二_第14张图片
这里clock函数是表示当前程序运行的时间,单位是毫秒,可以看到,传值和传引用的效率差的还是很大。

  1. 传值返回和传引用返回的效率比较
include<iostream>
using namespace std;
#include 

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

运行截图:

【C++初探:简单易懂的入门指南】二_第15张图片
可以看到,由于传值传参和传值返回都需要额外拷贝开一份空间,而传引用不需要,所以效率是差别还是很大的,所以一般情况下,如果不是特殊需求,传引用的性价比还是更高的。

1.6引用和指针的比较

相同点:

  1. 效率都很高。
  2. 底层汇编代码很相似,引用的底层实现是指针,也就是引用在底层实现上是开了空间的。
  3. 传址引用和传引用返回都可以改变该变量的值,当然特殊情况例外(const修饰的变量)。

不同点:
1.引用在语法概念上没有开空间。
2.指针不初始化不会报错,但是引用不行。
3.创建一个引用之后,这个引用就不能再作为其它变量的别名了,但是指针变量可以指向其它相同类型的变量。

2.auto关键字

auto是C++上面的一个关键字,它可以自动识别右值的类型,我们主要介绍C++11标准的auto关键字。

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

运行结果:

【C++初探:简单易懂的入门指南】二_第16张图片

  • 注意:引用的类型和被引用的对象是一致的,typeid(变量名).name()可以用来打印变量的类型。

可能会有人认为这样没有什么实质的作用,但是当那个函数的返回值类型(因为C++有很多自定义类型)非常复杂时,auto关键字就非常方便了。

2.1 auto关键字的使用细则

auto关键字可以和指针、引用结合起来使用,但是必须给它初始化,否则语法上是无法通过的。

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

运行截图:
【C++初探:简单易懂的入门指南】二_第17张图片

  • 这里在定义指针时autoauto*没有什么区别,但是在定义引用时必须加上&操作符。

如果不初始化,就会报这样的错误:

【C++初探:简单易懂的入门指南】二_第18张图片

这也间接说明了auto不是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量的类型必须由编译器在编译时期推导而得,所以如果你不给初始值,那么编译器就无法进行推导,auto在编译期间会被替换。

auto关键词可以同时定义很多变量,但是它只会对第一个变量的类型进行推导,从而用这个变量的类型来定义其它变量,所以这些变量的类型要相同,详细请看下图:
【C++初探:简单易懂的入门指南】二_第19张图片

2.2 auto关键字不能使用的场景

  1. auto关键字不能作为函数的参数,因为编译器无法对其类型进行推导。
  2. auto不能用来声明数组。
    【C++初探:简单易懂的入门指南】二_第20张图片
  • 这里有一点需要说明的时,虽然VS2019上,以auto作为返回值的类型是可以编译通过的,但还是不建议这样去做,因为如果我们后期想要去找到其返回值的类型还是比较麻烦的,因为可能出现这样的情况:
    【C++初探:简单易懂的入门指南】二_第21张图片
    这里还只有两个嵌套,如果工程量一大,嵌套的次数变多,想知道某个函数的返回值就是一件困难的事情,有人说可以用typeid(变量名).name()来知道其类型,我直接写出来不是更方便吗?

  • auto在实际中最常见的优势用法就是跟以后会提到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。

3.特殊的for循环(基于范围)

3.1基于范围for的语法

在C语言/C++98中,如果我们想遍历一个数组,你可能会这样做:

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

运行结果:

【C++初探:简单易懂的入门指南】二_第22张图片

我们可以看到引用是控制台第三行,它的地址与数组每个元素的地址是相同的,说明编译器在迭代时如果是引用,每执行一次for循环,p的空间就会被回收,不然由于创建引用变量后,这个引用变量不能作为其它变量的别名可知,打印出的地址应该是相同的才对,此时不相同,所以博主猜测应该是回收了,一次for循环执行一次引用变量的创建,至于普通的迭代,可以看出第二行的地址是完全相同的,说明编译器只给这个变量开了一次空间,剩下的每次for循环都是简单的把数组中的值赋值给它。

3.2 基于范围for的使用规则

  1. for循环的范围必须是确定的。
    对于数组而言,它的范围就是从第一个元素到最后一个元素
  • 注意,以下代码就有问题,它的范围是不确定的。
#include
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;
}

报错截图:

【C++初探:简单易懂的入门指南】二_第23张图片

这是你这样写array似乎是一个数组,其实不然,它是一个保存了数组首元素的指针,你在里面计算数组的范围是无法计算出来的:

【C++初探:简单易懂的入门指南】二_第24张图片
所以你也无法知道范围,自然就会报错。

【C++初探:简单易懂的入门指南】二_第25张图片

你可能感兴趣的:(C++,c++,java,开发语言,算法,程序人生,笔记,经验分享)