C++ C++基础语法入门总结(二)引用-内联函数-C++11新特性

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。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WhiteShirtI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值