【C++】C++入门(下)

引用


概念与使用

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间

如何定义一个引用:类型&引用名 = 要引用的实体。引用类型必须和引用实体的类型一致

int a = 0;
int& b = a;

如上代码,b就是a的引用, 相当于给a取了一个别名,ab共用一块空间

我们可以取出这两个变量的地址看一下

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

	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

image-20240128225734811

特性

1.一个变量可以有多个引用

一个变量也可以有很多别名,别名也可以有别名

就像一个人可以有很多名字一样,比如李逵,别名铁牛,铁牛和李逵的别名是黑旋风

int main()
{
	int a = 0;
	int& b = a;
	int& c = a;
	int& d = c; // 引用也可以有引用

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

	a++;
	b++;
	c++;
	d++;

	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;
	return 0;
}

image-20240128231230342

2.必须初始化

在定义一个引用时,必须对其进行初始化,不能像下面这样定义引用

// 错误示例
int main()
{
	int a = 0;
	int& b;
	b = a;
	return 0;
}

运行上面的这个案例会发生错误:

image-20240128231402837

所以,定义引用的同时必须初始化

// 正确示例
int main()
{
	int a = 0;
	int& b = a;
	return 0;
}

3.不能更改指向

一个引用一旦定义,他就不能成为别的实体的别名了

int main()
{
	int a = 0;
	int b = 1;
	int& c = a;	// c 是 a 的引用

	c = b;	// 赋值
	return 0;
}

如上代码,c已经是a的引用了,无法再更改指向。c = b也只是将b的值赋给c

其实这一点和必须初始化相呼应

还是来看下面这个错误示例

// 错误示例
int main()
{
	int a = 0;
	int& b;
	b = a;
	return 0;
}

b = a是要把a的值赋给b呢,还是b要作为a的别名呢?这就会有歧义

常引用

我们知道,被 const修饰的变量,只能查看,不可修改

当一个变量被const修饰时,其引用也要加const修饰,这就是常引用。如下

int main()
{
	const int a = 0;	// a 被const修饰
	const int& ra = a;	// 引用也要加const
	return 0;
}

可以这样理解,引用实体被const修饰时,只有只读权限;如果引用不加const,权限就是可写可读,造成了权限的放大,这是不行的

如下就是权限放大,会报错

int main()
{
	const int a = 0;   // 只读
	int& ra = a;   // 可写可读,权限放大
	return 0;
}

image-20240129162700779

对应地,也会有权限的缩小

权限缩小是指:引用实体可写可读,而其引用只读。权限的缩小是被允许的,如下

int main()
{
	int a = 0;	// 可写可读
	const int& ra = a;	// 只读,权限缩小

	cout << "a:" << a << endl;
	cout << "ra:" << ra << endl;

	return 0;
}

image-20240129163427255

引用的应用场景

1.作参数

引用作为参数通常都是输出型参数。输出型参数是指:形参改变,实参也会改变

例如,写一个函数来交换两个变量的值。在学习引用之前,我们通常会用指针来写

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

int main()
{
	int a = 1;
	int b = 2;
	cout << "a:" << a << " b:" << b << endl;

	Swap(&a, &b);
	cout << "a:" << a << " b:" << b << endl;
	return 0;
}

image-20240129165002974

那么学习了引用后,我们那就可以使用引用作为参数,形参的改变也会影响实参

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

int main()
{
	int a = 1;
	int b = 2;
	cout << "a:" << a << " b:" << b << endl;

	Swap(a, b);
	cout << "a:" << a << " b:" << b << endl;
	return 0;
}

image-20240129165213158

作为输出型参数这个功能,指针和引用两者都可以实现,但是引用使用起来更加便捷

另外,当参数很大时,使用引用也可以避免拷贝,从而提高运行效率

2.作返回值

我们先来看一般的整型作返回值

int func()
{
	int a = 1;
	return a;
}
int main()
{
	int ret = func();
	return 0;
}

调用func后,ret的值:

image-20240129172015119

func中的a是局部变量,函数结束后生命周期就结束了,会销毁,为什么还会给返回给ret呢?

其实在函数销毁前,会在函数栈帧之外形成一份a的临时拷贝,返回的是a的拷贝,所以函数结束后,依然能赋值给ret

image-20240129172903368

这时我们对代码稍作修改,把函数的返回值类型改为引用

int& func()
{
	int a = 1;
	return a;
}
int main()
{
	int ret = func();
	return 0;
}

此时,这份代码就有问题了:

func返回的是a的别名,就相当于a所对应的空间已经销毁了,但是func结束后还是把那块空间的数据返回,给了ret

image-20240129174903809

此时ret的值是不确定的,如果编译器没有对那块空间进行清理,那就还是a的值;如果清理了,就是随机值了

再接着修改,将ret改为引用

int& func()
{
	int a = 1;
	return a;
}
int main()
{
	int &ret = func();
	return 0;
}

此时的代码问题就更大了,func返回的是a的别名,而ret是引用,也即是说,ret是a的引用

image-20240129175556137

a对应的空间已经销毁,但是ret作为a的引用,还是能访问到这块空间。与野指针类似,这里是野引用

综上所述,引用不能返回局部变量,因为局部变量除了作用域就会销毁,再用引用访问就是非法的,上面所说的都是引用作返回值的错误示例

引用不能返回局部变量,但是可以返回全局变量、静态变量、堆上变量,下面我们就来看一下引用作为返回值的正确用法

如果我们要实现一个顺序表,取出某个位置的数据修改某个位置的数据,是要分别实现两个接口的,如下:

// 顺序表结构
typedef struct SList
{
	int* data;
	int size;
	int capacity;
}SList;

// 初始化
void SLInit(SList* ps)
{
	ps->data = (int*)malloc(sizeof(int) * 4);
	ps->capacity = 4;
	ps->size = 0;
}

// 尾插
void SLPushBack(SList* ps, int x)
{
	ps->data[ps->size] = x;
	ps->size++;
}

// 取得pos位置的值
int SLGet(SList* ps, int pos)
{
	return ps->data[pos];
}

// 修改pos位置的值
void SLModify(SList* ps, int pos, int x)
{
	ps->data[pos] = x;
}

现在我们要查看数据,修改数据,要调用两个接口

int main()
{
	SList sl;
	SLInit(&sl);

	// 尾插
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);

	// 遍历查看
	for (int i = 0; i < 4; i++)
	{
        // 调用 SLGet()
		cout << SLGet(&sl, i) << " ";
	}
	cout << endl;
}

再把偶数位乘二

int main()
{
	SList sl;
	SLInit(&sl);

	// 尾插
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);

	// 遍历查看
	for (int i = 0; i < 4; i++)
	{
		cout << SLGet(&sl, i) << " ";
	}
	cout << endl;

	// 偶数位乘二
	for (int i = 0; i < 4; i++)
	{
		int val = SLGet(&sl, i);
		if (val % 2 == 0)
		{
			SLModify(&sl, i, val * 2);
		}
	}

	// 遍历查看
	for (int i = 0; i < 4; i++)
	{
		cout << SLGet(&sl, i) << " ";
	}
	cout << endl;
}

image-20240129182830179

通过引用作返回值,就可以将SLGetSLModify的功能合并为一个接口,只需将SLGet的返回改为引用即可

int& SLGet(SList* ps, int pos)
{
	return ps->data[pos];
}

返回的是引用,也就是说,返回的是pos位置的别名

我们可以通过引用来获取pos位置的值,也可以对pos位置的值作出修改

image-20240129184038058

修改后的测试代码

// 遍历查看
	for (int i = 0; i < 4; i++)
	{
		cout << SLGet(&sl, i) << " ";
	}
	cout << endl;

	// 偶数位乘二
	for (int i = 0; i < 4; i++)
	{
		if (SLGet(&sl,i) % 2 == 0)
		{
             // 直接就能修改
			SLGet(&sl, i) *= 2;
		}
	}

	// 遍历查看
	for (int i = 0; i < 4; i++)
	{
		cout << SLGet(&sl, i) << " ";
	}
	cout << endl;

image-20240129184305394

引用和指针的关系、区别

关系

现在我们已经对引用有了初步了解,也可以发现引用和指针是类似的,作用是有重叠的

那么引用可不可以替代指针呢?——不可以。指针和引用虽然相似,但不可以相互替代

比如,链表的中的指针,链表可以通过修改指针来修改各个节点的指向关系。而引用是不能改变指向的,自然也替代不了指针

总的来说,虽然引用使用起来更加便利,但是不能替代指针;引用与指针是相辅相成的

区别

语法上

  1. 引用是给变量取别名,不需要开空间;而指针是变量的地址,需要开空间
  2. 引用必须初始化;指针可以不初始化
  3. 引用不可以改变指向;指针可以改变指向
  4. 引用没有空引用,野引用也不容易出现;指针有空指针,且空指针和野指针都容易出现、
  5. sizeof含义不同:引用为引用类型的大小;指针为4或8个字节
  6. ++含义不同:引用是引用的实体加一,而指针是地址偏移一个类型的大小

底层上

引用在语法上是没有开空间的,在底层上是有开空间的,即引用的语法含义与底层实现是背离的

为什么呢?因为在底层上,引用是用指针实现的,在底层只有指针,没有引用

我们可以来验证一下,有以下代码

int main()
{
	int a = 0;

	// 引用
	int& ra = a;
	// 指针
	int* pa = &a;
	return 0;
}

使用VS进入调试模式,再转到反汇编

image-20240129190734693

image-20240129190832104

以上就是汇编代码,先不管能不能看懂,至少我们可以看到引用和指针的操作都是一样的

这就说明,在汇编层面,是没有引用的,引用是用指针实现的,所以引用在底层也会开空间

内联函数


在C语言阶段,我们有时会用宏定义一些简单的函数,以此来增强代码的复用性,提高性能

例如使用宏定义一个加法函数

#define ADD(a,b) ((a) + (b))

int main()
{
	int a = 1;
	int b = 1;
	int ret = ADD(a, b);
	cout << "ret:" << ret << endl;
}

image-20240129192351463

但是宏有很多缺点:

  1. 语法复杂,使用起来容易出错
  2. 预编译阶段会进行宏替换,不容易调试
  3. 不会进行类型检查

概念及使用

而在C++中,我们可以使用内联函数实现类似宏函数的功能,调用内联函数不会创建栈帧,从而减少损耗

只需在函数前加inline关键字修饰,就可以将其改为内联函数

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

调用内联函数时不会创建栈帧,而是在调用处展开。不同于宏,内联不是将代码替换到调用处,而是以灵活的方式将函数的逻辑在调用处展开

我们可以将普通函数和内联函数对比

// 普通函数
int Add(int x, int y)
{
	return x + y;
}

查看此函数的汇编层

image-20240129193636798

call时,就会发生跳转,创建函数栈帧

再来看看内联函数是怎样的

// 内联函数
inline int Add(int x, int y)
{
	return x + y;
}

在debug模式下,需要对编译器进行设置,以下是 VS2019 的设置方法

右击项目

image-20240129194101544

点击属性

image-20240129194129723

常规->调试信息格式->程序数据库

image-20240129194223861

再点击 优化->内联函数扩展->只适用于_inline

image-20240129194401959

然后就可以进入调试模式,转到汇编层面了

image-20240129194625076

可以看到,调用内联函数时并没有call,而是将函数逻辑展开了

特性

1.从上面可以看出,内联函数不会建立函数栈帧,但是会在调用处展开;这样可以减少调用开销,提升程序运行效率,但是会使文件体积增大。这是一种以空间换取时间的做法

2.也不是任何函数都可以成为内联函数。一般建议:将函数规模小调用频繁不是递归、的函数用inline修饰

3.内联函数的声明与定义不可分离。现有如下代码

// .h文件
inline void func(int i);
// .cpp文件
void func(int i)
{
	cout << i << endl;
}
// test.cpp文件
int main()
{
	func(10);
	return 0;
}

image-20240129201823823

由于内联函数不会调用,是直接展开的,那就没有call,也就不会有函数地址,在链接阶段就会找不到

auto关键字(C++11)


auto关键字可以自动识别变量的类型,如下

int a = 0;
auto aa = 0;
char b = 'a';
auto ab = 'a';
double c = 1.1;
auto ac = 1.1;

我们可以使用typeid(变量).name()查看变量的类型

cout << typeid(a).name() << endl;
	cout << typeid(aa).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(ab).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(ac).name() << endl;

image-20240129203012974

用法

1.必须初始化

下面这种用法是会报错的

auto a;
a = 10;

image-20240129203457285

2.可以搭配引用、指针使用

int main()
{
	int a = 0;

	// 指针
	auto pa1 = &a;
	auto* pa2 = &a;

	// 引用
	auto& ra = a;

	cout << typeid(pa1).name() << endl;
	cout << typeid(pa2).name() << endl;
	cout << typeid(ra).name() << endl;
}

image-20240129203821786

注意:如果左边是auto*,那么右边必须给地址;引用必须是auto&

3.一行可以定义多个变量,但是一定得是同一类型

auto a = 1, b = 2;	//符合规范
auto c = 3, d = 4.0;	//不符合规范,会报错

auto不能使用的场景

1.auto不能在函数形参中使用

void TestAuto(auto a)
{
    cout << a << endl;
}

int main()
{
    TestAuto(10);
    return 0;
}

image-20240129204407442

2.不能直接用来声明数组

int main()
{
    int arr[] = { 1,2,3 };
    auto arr2[] = { 4,5,6 };
    return 0;
}

image-20240129204610790

基于范围的for循环


在C语言中,要是想遍历一个数组,我们一般是这样:

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

而在C++中,对于有范围的集合,我们一般是这样:for循环后的括号由冒号“ :”分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围

将上面的代码改造一下:

int main()
{
	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto i : array)
	{
		cout << i << " ";
	}
	cout << endl;
}

意思是将数组中的元素依次赋给变量i,自动迭代,自动判断结束。auto 也可以写成数组中元素的类型,但是一般都写auto

如果想修改数组中的元素,可以在auto后加个&,将i写成引用

int main()
{
	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto& i : array)
	{
		i *= 2;
		cout << i << " ";
	}
	cout << endl;
}

image-20240129205911484

指针空值(C++11)


在C++98中,指针空值的定义有一些问题,先看以下代码

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

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

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

我们期望的是f(0)调用f(int)f(NULL)调用f(int*),代码也理应该是这样

但是运行结果确是这样:

image-20240129210807710

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器
默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void
*)0。

所以,C++11引入了新关键字:nullptr来表示指针空值

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

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

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

image-20240129211218952

这样就符合程序的初衷了

注意:nullptr是个关键字,不需要包含头文件了;为了提高代码健壮性,建议使用nullptr

结束,再见 😄

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿洵Rain

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

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

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

打赏作者

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

抵扣说明:

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

余额充值