C++重生之引用的究极奥义


引用

引用是C++有别于C语言的一个全新的概念,本文章主要讲解引用是个撒子^ - ^

什么是引用

引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。引用就是取别名
语法结构:
类型& 引⽤别名=引⽤对象; 直接看下面的实例

int main()
{
	int a = 0;
	int& b = a;
	b++;
	cout << b << endl;
	cout << a << endl;
	cout << &b << endl;
	cout << &a << endl;
	return 0;
}

想要看懂这个代码就要理解引用最重要的两条

  • 引用就是取别名
  • 编译器不会为引用变量开辟内存空间

引用的特点

  • 引⽤在定义时必须初始化
  • ⼀个变量可以有多个引⽤
  • 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
    前两条就是字面意思很好理解,最后一条看下面的代码
int main()
{
	int a = 10; 
	int& b = a;
	int& d = b;
	int e = 20;
	d = e; // 注意这里只是一个赋值操作,不是让d引用e,C++引用不可以改变指向

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

使用说明书

  • 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象
  • 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
  • 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。
    下面我们看一下引用做返回值的用法
using namespace std;
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{
	rs.a = (STDataType*)malloc(n * sizeof(STDataType));
	rs.top = 0;
	rs.capacity = n;
}
// 栈顶

void STPush(ST& rs, STDataType x)
{
	//assert(ps);
	// 满了,扩容
	if (rs.top == rs.capacity)
	{
		printf("扩容\n");
		int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
		STDataType * tmp = (STDataType*)realloc(rs.a, newcapacity *sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		rs.a = tmp;
		rs.capacity = newcapacity;
	}
	rs.a[rs.top] = x;
	rs.top++;
}

//int STTop(ST& rs)
int& STTop(ST& rs)
{
	assert(rs.top > 0);
	return rs.a[rs.top];
}

int main()
{
	// 调⽤全局的
	ST st1;
	STInit(st1); // 用引用接收,相当于是st1的别名,所以这里不需要传地址
	STPush(st1, 1);
	STPush(st1, 2);
	cout << STTop(st1) << endl;
	STTop(st1) += 10;
	cout << STTop(st1) << endl;
	return 0;
}

重点看一下取顶部元素的代码,是使用引用来做返回值,如果只用int也就是元素类型来做返回值的话,返回值会先存入一个临时变量然后返回,又因为临时变量具有常性,所以不可以修改值,而使用引用返回,就是返回了顶部元素的别名,可以进行修改

const引用

我们直接看下面的代码

int main()
{
	const int a = 0;
	int& b = a; // err
	return 0;
}

这种引用代码会直接报错,因为这里的引用对a的权限进行了放大

//我们类比一下c语言的指针
int mian()
{
	const int a = 0;
	int* p = &a;
	*p = 10;
	return 0;
}

.c文件中这样的代码是没有问题的,在.cpp文件中这样的代码也会报错

  • 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
//权限的缩小
int main()
{
	int b = 0;
	const int &rb = b;
}

这里rb对b的空间是只读的,而b对其空间是可读可写

  • 需要注意的是类似int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中,int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,rbrd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
    我们看下面的代码
int main()
{
	int a = 10;
	int& b = a * 3; // err
	return 0;
}

这样代码就会报错,原因是a*3的结果会存储在一个临时变量中,临时变量具有常性,这里相当于对权限进行了放大,所以报错,因改为下面的代码

int main()
{
	int a = 10;
	const int& b = a * 3;
	return 0;
}

再看以下这种情况

int main()
{
	float a = 3.14;
	int& b = a; // err
	return 0;
}

这里报错的原因不是因为将一个浮点型用一个整形引用,实际上,用一个整形来存储一个浮点型数据是存在隐式转换的,转换的结果会存在一个临时变量中,我们用过上面的代码又知道,临时变量具有常性,所以要进行以下修改

int main()
{
	float a = 3.14;
	const int& b = a;
	return 0;
}

引用和他的好哥哥指针到底是啥关系

C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。

  • 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间
  • 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的
  • 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象
  • 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象
    * sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
  • 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些

inling 内联函数

  • ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴栈帧了,就可以提⾼效率
  • inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略
  • C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数
    我们看下inline对标C语言中的宏函数
#define ADD(a,b) ((a)+(b))
int main()
{
	int ret = ADD(1, 2);
	cout << ret << endl;
	return 0;
}

C语言中的宏函数注意的点

  • 宏定义后面不可以加分号,宏被替换后,分号会重复
  • 要加上外面的括号,有优先级的问题
  • 要加上里面的括号比方说参数是 (x&y, x|y)如果不加里面的括号,就会打乱原有的优先级
    我们再看下inline修饰函数
inline int ADD(int x = 0, int y = 0)
{
	return x + y;
}
int main()
{
	int ret = ADD(1, 2);
	cout << ret << endl;
	return 0;
}

在函数前加上inline使函数称为内联函数,在使用时不需要创建函数栈帧,而是直接展开,就对标了C中的宏函数,且没有宏函数的坑点

  • vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试
  • inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错

nullptr

我们先看下面代码,预测一下运行的结果

void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	return 0;
}

以我们的理解,两个函数构成函数重载,两个函数都会被调用,但结果都是调用第一个函数
C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(intx),因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。
为了解决这个问题C++中提出了nullptr这个关键字
C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值