【C++】基础知识及语法介绍(下篇)

❤️欢迎来到我的博客❤️

引用

引用不是一个新的变量,而是给已经存在的变量取了另外一个名字
比如西游记中的主角孙悟空,他的别名有齐天大圣、美猴王、弼马温等,无论你叫哪个名字他指的都是孙悟空
或者是我有两个电话号码一个号码A一个号码B,无论你打的是号码A还是号码B你最后找到的都是我
而指针就像是一个秘书,你想找我必须得去通知我的秘书,然后再由我的秘书来找到我
使用方法 - 类型& 引用变量名(对象名) = 引用实体;
一个变量可以有多个引用

指针:
在这里插入图片描述
引用:
在这里插入图片描述

int main()
{
	int a = 0;
	int& b = a;
	int& c = b;
	//引用必须初始化
	//错误写法 - int& d;

	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;

	a++;
	cout << b << endl;

	c++;
	cout << a << endl;

	return 0;
}

运行效果:
在这里插入图片描述
可以看到他们的地址都是一样的,说明b和c其实就是a
注:引用必须初始化,并且C++中引用是不可以改变指向的 比如:

int main()
{
	int a = 0;
	int& b = a;
	
	int x = 10;
	//把x的值赋给b(a)
	b = x;//b还是a的别名
	return 0;
}

引用的作用

做参数

指针版本:

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int x = 10;
	int y = 20;
	Swap(&x, &y);
	cout << x << ' ' << y << endl;

	return 0;
}

这是一个交换函数,但是每次传值都要取地址显得有些麻烦,我们就可以使用引用来解决这个问题

引用版本:

//形参是实参的别名
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int x = 10;
	int y = 20;
	Swap(x, y);
	cout << x << ' ' << y << endl;

	return 0;
}

引用版本省去了指针,用起来更方便

指针的交换也同样适用:

//形参是实参的别名
void Swap(int*& x, int*& y)
{
	int* tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int x = 10;
	int y = 20;

	int* px = &x;
	int* py = &y;

	cout << px << ' ' << py << endl;
	Swap(px, py);
	cout << px << ' ' << py << endl;

	return 0;
}

两段代码的运行效果:
在这里插入图片描述

做返回值

传值返回:

int Count()
{
	static int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	return 0;
}

在这里插入图片描述
为什么要生成临时变量?
因为出了作用域栈帧就销毁了,所以不能用n做返回值,需要拷贝n做返回值,但是当前代码中的n是在静态区中就算栈帧销毁n也不受影响,但是编译器还是会生成临时变量
传值返回不管是局部变量还是全局变量还是静态区的变量他都会生成拷贝

传引用返回:
引用做返回值就不会再生成临时变量了
不生成临时变量的好处:减少了拷贝,提高了效率

int& Count()
{
	static int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	return 0;
}

在这里插入图片描述

注意

需要注意的是如果用引用返回的是局部对象则会出现问题

错误样例1

int& Count()
{
	int n = 0;
	n++;
	cout << &n << endl;
	return n;
}

int main()
{
	int ret = Count();
	//ret值不确定
	cout << ret << endl;//1或随机值(取决于清不清理栈帧)
	return 0;
}

这段代码的结果是不确定的,n所在的空间已经被销毁(释放)了

如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的
如果Count函数结束,栈帧销毁,并清理栈帧,那么ret的结果侥幸是随机值

错误样例2

int& Count()
{
	int n = 0;
	n++;
	cout << &n << endl;
	return n;
}

int main()
{
	//引用接收更危险
	int& ret = Count();
	//运行结果不确定
	//第一次打印有可能侥幸正确
	cout << ret << endl;
	cout << &ret << endl;
	//在调用其他函数之后ret就会变成随机值
	cout << ret << endl;
	return 0;
}

运行结果:
在这里插入图片描述

可以看到他们的地址一样,虽然可以访问ret但是他的值并没有保障,因为ret引用的空间已经被销毁了
虽然第一次打印的结果侥幸是正确的,但是这种场景下使用引用返回还是十分危险

修改返回值+获取返回值

以前的顺序表需要查找修改两个函数配合着使用

struct SeqList
{
	int a[100];
	size_t size;
};

int SLGet(SeqList* ps, int pos)
{
	assert(pos >= 0 && pos < 100);

	return ps->a[pos];
}

void SLModify(SeqList* ps, int pos, int x)
{
	assert(pos >= 0 && pos < 100);

	ps->a[pos] = x;
}

int main()
{
	SeqList s;
	SLModify(&s, 0, 1);
	cout << SLGet(&s, 0) << endl;
	//对第0个位置的值+5
	//查找第0个位置的值
	int ret = SLGet(&s, 0);
	//修改第0个位置的值
	SLModify(&s, 0, ret + 5);
	cout << SLGet(&s, 0) << endl;
	return 0;
}

运行结果:
在这里插入图片描述

有了引用之后我们可以这样实现:

struct SeqList
{
	int a[100];
	size_t size;
};

int& SLAt(SeqList& s, int pos)
{
	assert(pos >= 0 && pos < 100);
	
	return s.a[pos];
}

int main()
{
	SeqList s;
	SLAt(s, 0) = 5;
	cout << SLAt(s, 0) << endl;
	//对第0个位置的值+5
	SLAt(s, 0) += 5;
	cout << SLAt(s, 0) << endl;

	return 0;
}

运行结果:
在这里插入图片描述
引用做返回值的特点还在后面,这只是其中的一种方式,后续还会有更多更好的方式

总结:

  1. 基本任何场景都可以用引用传参
  2. 要谨慎用引用做返回值,出了函数作用域对象不在了就不能用引用返回,还在就可以用引用返回
  3. 引用做返回值的好处 减少拷贝提高效率,修改返回值+获取返回值

常引用问题

int main()
{
	const int a = 0;
	int& b = a;

	return 0;
}

这种取别名的方式会报错,因为引用的过程中权限不能放大,但是权限可以平移或者缩小
例如:

int main()
{
	int x = 0;

	//权限平移
	int& y = x;

	//权限缩小
	const int& z = x;
	x++;
	y++;
	return 0;
}

缩小的是z作为别名的权限,z不可以修改,但是x和y可以,x和y的改变就是z的改变,只是不能通过z来改变

给常量取别名:

int main()
{
	const int& x = 10;

	return 0;
}

const的引用:

int main()
{
	double x = 1.11;
	//会报错 原因是权限放大
	int& y = x;

	const int& y1 = x;//权限平移
	return 0;
}

为什么加了const反而能编译通过呢?
原因是发生类型转换的时候中间会产生一个临时变量,临时变量具有常性,造成了权限放大,所以会报错
加了const就是权限的平移所以不会报错

一些例子:

int A()
{
	static int x = 0;
	return x;
}

int& B()
{
	static int x = 0;
	return x;
}

int main()
{
	int& ret = A();		   //权限放大会报错
	const int& ret1 = A(); //权限平移
	int ret2 = A();		   //拷贝

	int& ret3 = B();	   //权限平移
	const int& ret4 = B(); //权限缩小
	return 0;
}

引用和指针的不同点

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
但是在底层实现上实际是有空间的,因为引用是按照指针方式来实现的

auto

注:使用auto定义变量时必须对其进行初始化

int main()
{
	int a = 0;
	int b = a;

	auto c = a;
	auto d = 1 + 1.1;

	//typeid().name()打印类型
	cout << "c的类型->" << typeid(c).name() << endl;
	cout << "d的类型->" << typeid(d).name() << endl;

	return 0;
}

auto会根据右边的表达式推导出c的类型,d也同理
运行结果:

在这里插入图片描述

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

 int x = 10;
 auto a = &x;
 auto* b = &x;
 auto& c = x;

auto不能推导的场景

auto不能作为函数的参数
此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导

void TestAuto(auto a)
{
	;
}

auto不能直接用来声明数组

void TestAuto()
{
 int a[] = {1,2,3};
 auto b[] = {456};
}

auto的价值在之后的学习中会体现出来,目前阶段并没有什么用处

范围for

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	//范围for - 适用于数组
	//依次取数组中的数据赋值给x
	//会自动迭代,自动判断结束
	for (auto x : arr) //也可以写成for(int x : arr) 但推荐写auto
	{
		cout << x << " ";
	}
	cout << endl;

	return 0;
}

运行结果:
在这里插入图片描述

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	//修改数据
	for (auto& x : arr)
	{
		x *= 2;
		cout << x << " ";
	}
	cout << endl;

	return 0;
}

运行效果:
在这里插入图片描述
范围for的使用条件

  1. for循环迭代的范围必须是确定的
  2. 迭代的对象要实现++和==的操作。

内联函数

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	for (int i = 0; i < 10000; i++)
	{
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

假设我们有一个函数Add,并且他会被频繁的调用n次,那么就会建立n个栈帧,这会导致负担较大,那么怎么优化?
可以用宏来优化,宏函数不需要建立栈帧,可以提高调用效率

#define Add(x,y) ((x) + (y))

int main()
{
	for (int i = 0; i < 10000; i++)
	{
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

但是宏有缺点:
1.较为复杂 容易出错
2.不方便调试宏(因为预编译阶段进行了替换)
3.导致代码可读性差,可维护性差,容易误用
4.没有类型安全的检查

为了解决这些缺陷C++就设计出了内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

inline int Add(int x, int y)
{
	return x + y;
}

int main()
{
	for (int i = 0; i < 10000; i++)
	{
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

内联函数不需要建立栈帧,提高了效率,并且不复杂 不容易出错 可读性强 可以调试
inline对于编译器而言只是一个建议,最终是否成为inline由编译器自己决定

像类似函数加了inline也会被编译器否决掉

  1. 比较长的函数
  2. 递归函数

默认debug下面,inline不会起作用,否则就不方便调试了

注:inline是一种以空间换时间的做法,省去调用函数额开销,只适用于短小的频繁调用的函数,代码很长或者有循环/递归的函数不适宜
使用作为内联函数,否则会导致代码膨胀(可执行文件变大)因此会不会成为内联函数取决于编译器

空指针

void f(int)
{
 cout<<"f(int)"<<endl;
}

void f(int*)
{
 cout<<"f(int*)"<<endl;
}

int main()
{
 f(0);
 f(NULL);
 f((int*)NULL);
 return 0;
}

运行效果:
在这里插入图片描述
可以看到在C++中NULL是被当作整型0来看待的
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖
NULL的定义:
在这里插入图片描述
因此在C++11中引入了一个关键字nullptr

void f(int)
{
	cout << "f(int)" << endl;
}

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	f(nullptr);
	return 0;
}

运行效果:
在这里插入图片描述

在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr


以上就是本篇文章的全部内容了,希望大家看完能有所收获

❤️创作不易,点个赞吧❤️
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值