【C++】2.C++入门(2)


6.引用

6.1 引用概念

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

比如:孙悟空,又叫齐天大圣

类型& 引用变量名(对象名) = 引用实体;

void TestRef() // 定义一个没有返回值和参数的函数 TestRef
{
     int a = 10; // 定义一个整型变量 a 并初始化为 10
     int& ra = a; // 定义一个引用 ra,它是变量 a 的别名
     printf("%p\n", &a); // 打印变量 a 的地址
     printf("%p\n", &ra); // 打印引用 ra 的地址,由于 ra 是 a 的别名,这将打印出与 &a 相同的地址
}

==注意:==引用类型必须和引用实体是同种类型的

int main(){
    int a = 0;
    int& d = a;
    int x = 11;
    d = x;//这里是把x的值赋值给d,而不是让d变成x的引用(别名)
    printf("%d %d %d", a, d, x);
    //CPP里面引用的指向是不会变的
    return 0;
}

打印:

11 11 11

6.2 引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错,因为引用在定义时必须初始化
   int& ra = a;
   int& rra = a;
   printf("%p %p %p\n", &a, &ra, &rra);  
}

不是什么时候都能使用引用返回的!

//引用访问
// 不危险
//引用实体是 n
//ret现在是对Count函数返回的静态局部变量n的引用
//ret可以被视为引用变量名
int& Count() {
	static int n = 0;
	n++;
	return n;
}

int main() {
	int ret = Count();
	return 0;
}
//引用访问
// 危险
//这个程序相当于做了一个野指针的访问,结果是不确定的
int& Count() {
	int n = 0;
	n++;
	return n;
}
//这里把n的值给ret的时候,n已经被销毁了
//如果count函数结束,栈帧销毁后没有清理,那么ret的结果侥幸正确
//如果count函数结束,栈帧销毁后被清理,那么ret的结果是随机值
int main() {
	int ret = Count();
	return 0;
}
int& Count(int x) {
    int n = x;
    //cout << &n << endl;
    n++;
    return n;
}

int main() {
    int& ret = Count(10);
    //cout << &ret << endl;

    cout << ret << endl;//11/随机值
    Count(20);
    //rand();//调用函数后就是随机值了,没调用的话就是21/随机值
    cout << ret << endl;//21/随机值
    return 0;
}
int& Count(int x) {
    int n = x;
    n++;
    return n;
}

int main() {
    int& ret = Count(10);
    //cout << &ret << endl;

    cout << ret << endl;//11/随机值
    Count(20);
    printf("SSSSSSSSSSS\n");//调用函数后就是随机值了
    cout << ret << endl;//21/随机值
    return 0;
}
int& Count(int& x) {
    int n = x;
    n++;
    return n;
}

int main() {
    int a = 0;
    Count(a);
    cout << a << endl;

    int ret = Count(a);
    cout << ret << endl;

    //不安全
    int& ret1 = Count(a);
    printf("SSSSSSSSSSS\n");
    cout << ret1 << endl;

    return 0;
}

总结:

  1. 基本任意场景都可以引用传参

  2. 谨慎使用引用做返回值。出了作用域,对象不在了,就不能引用返回。对象还在就可以用引用返回。


6.3 使用场景

  • 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。

  • 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。

  • 引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。

  • 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。

  • 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针,但是没学过引用,就会导致一头雾水。

1.做参数

例如原来我们这么写,是不会交换x,y的值的,因为这个是传值调用,而不是传址调用。

typedef:给类型取别名,不能给变量取别名

引用:给变量取别名

void Swap(int a, int b) {
	int tmp = a;
	a = b;
	b = tmp;
}

int main() {
	int x = 0;
	int y = 1;
	Swap(x, y);

	cout << x << " " << y << endl;

	return 0;
}

打印:

0 1

但是学完引用后,我们可以这么干:

void Swap(int& a, int& b) {//a是x的引用,b是y的引用
	int tmp = a;
	a = b;
	b = tmp;
}

int main() {
	int x = 0;
	int y = 1;
	Swap(x, y);

	cout << x << " " << y << endl;

	return 0;
}

打印:

1 0

我们也可以这样:

void Swap(int& a, int& b) {
	int tmp = a;
	a = b;
	b = tmp;
}

void Swap(int*& a, int*& b) {
	int* tmp = a;
	a = b;
	b = tmp;
}

int main() {
	int x = 0;
	int y = 1;
    cout << x << " " << y << endl;
    
	Swap(x, y);

	cout << x << " " << y << endl;

	int* px = &x;
	int* py = &y;
	cout << px << " " << py << endl;
	Swap(px, py);
	cout << px << " " << py << endl;

	return 0;
}

打印:

0 1
1 0
000000C0C2CFF4E4 000000C0C2CFF504
000000C0C2CFF504 000000C0C2CFF4E4

有些数据结构的书上会这么玩:

typedef struct ListNode {
	int val;
	struct ListNode* next;
};

void ListPushBack(struct ListNode*& phead, int x) {

}

int main() {

	struct ListNode* plist = NULL;
	ListPushBack(plist, 1);

	return 0;
}

还有些会这么写:

typedef struct ListNode {
	int val;
	struct ListNode* next;
}LTNode;

void ListPushBack(LTNode*& pphead, int x) {

}

int main() {

	LTNode* plist = NULL;
	ListPushBack(plist, 1);

	return 0;
}

还有些会这么写:

typedef struct ListNode {
	int val;
	struct ListNode* next;
}LTNode, *PLTNode;

void ListPushBack(PLTNode& pphead, int x) {

}

int main() {

	PLTNode plist = NULL;
	ListPushBack(plist, 1);

	return 0;
}

这个写法里面的PLTNode&就相当于ListNode*&就相当于struct ListNode*&,但是更加让人不太好理解。

初衷可能是二级指针的定义难以理解,就用了这个引用,但好多人C语言都没学明白,就来了引用,往往适得其反。


6.4 const引用(常引用)

  • 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以平移或者缩小,但是不能放大

  • 需要注意的是,类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样一些场景下a*3的和结果保存在一个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值rbrd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。

  • 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。

为了方便理解,我举了好几个例子:

在类型转换中会产生临时对象存储中间值

int main() {
	int i = 1;
	double j = 1.1;
	if (j > i) {
		//因为在运算符两边的两个变量的类型不同的时候,会发生提升,一般是小的向大的提升
		//在这里i向j提升,会生成一个double的临时变量,然后和j比较
		cout << "xxxxx" << endl;//打印:xxxxx
	}

	return 0;
}

对象的访问权限在引用过程中可以平移或者缩小,但是不能放大

int main()
{
    const int a = 10;
    //int& ra = a;   // 不可以,这里的引用是对a访问权限的放大
    const int& ra = a;// 这样才可以
    //ra++;// 编译报错:error C3892: “ra”: 不能给常量赋值
    int c = a;//这个可以,这里c的改变不影响a
    
    int b = 20;
    const int& rb = b;// 这里的引用是对b访问权限的缩小 
    //rb++;// 编译报错:error C3892: “rb”: 不能给常量赋值
    
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
    int a = 10;
    const int& ra = 30;
    // int& ra = a * 3;// 编译报错: “初始化”: 无法从“int”转换为“int &”
    const int& rb = a*3;
    
    double d = 12.34;
    // int& rd = d;// 编译报错:“初始化”: 无法从“double”转换为“int &”
    const int& rd = d;
    
    return 0;
}
//可以
//引用过程中,权限可以平移或者缩小
int main() {
	int x = 0;
	int& y = x;
	const int& z = x;//可以,这里是权限的缩小。这里缩小的不是x的权限,缩小的是z作为别名的权限
	++x;//可以,因为x是一个变量,他怎么变没有限制,有限制的是z。const修饰z,缩小了z作为别名的权限。
	//++z;//不可以

	return 0;
}
int main() {
	const int& m = 10;//这里是可以的,因为这里权限是不能被修改的,是权限的平移

	double a = 1.11;
	int b = a;//类型转换的时候会产生一个int类型的临时变量,也就是说这里给b的不是a而是一个int类型的临时变量。
	//临时变量具有常性,相当于被const修饰了

	//int& c = a;//这里权限放大了,所以不可以
	const int& c = a;//这里可以,

	return 0;
}
//辩证
int func1() {
	static int x = 0;
	return x;
}

int& func2() {
	static int y = 0;
	return y;
}

int main() {
	int ret1 = func1();//可以接收,这里就是拷贝
	//int& ret1 = func1();//不可以接收,因为权限的放大。这里返回的其实不是x,而是一个临时变量,临时变量具有常性。
	const int& ret1 = func1();//这样就可以了,这里是权限的平移

	//ret2现在是对Count函数返回的静态局部变量y的引用,ret2可以被视为引用变量名
	int ret2 = func2();//可以,上面有例子
	int& ret2 = func2();//可以,这里返回的是y的别名,是权限的平移
	const int& ret2 = func2();//可以,这里是权限的缩小

	return 0;
}
int main()
{
	// 权限不能放大
	const int a = 10;
	const int* p1 = &a;
	//int* p2 = p1;// 权限不能放大

	// 权限可以缩小
	int b = 20;
	int* p3 = &b;
	const int* p4 = p3;// 权限可以缩小
    int const* p5 = p3;// 权限可以缩小

	// 不存在权限放大,因为const修饰的是p6本身不是指向的内容
    //权限的缩小,p6是一个常量指针,p7是一个非常量指针
	int* const p6 = &b;//不能改变p6,能通过*p6改变b。//*p6=20
    //int const *p = &n;//不能通过*p修改n,但是可以修改p。//p=&m 
	int* p7 = p6;

	return 0;
}

6.5 引用和指针的区别

C++的引用是无法完全代替指针的

  • 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。

  • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。

  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。

  • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。

  • sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)

  • 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。

// 指针
int* STTop(ST& rs){
	assert(rs.top > 0);
	return rs.a + [rs.top - 1];
}


int main(){
	// 指针
	*(STTop(st1)) += 1;
	return 0;
}

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

int main(){
	//引用
	(STTop(st1)) += 1;
	return 0;
}
int main() {
	int a = 10;
	
	//语法层面:不开空间,是对a取别名
	//底层汇编指令实现角度看:引用是类似指针的方式实现的
	int& ra = a;
	ra = 20;

	//语法层面:开空间,存储a的地址
	int* pa = &a;
	*pa = 20;

	return 0;
}

7.inline

  • inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。

  • inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。

  • C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。

  • vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。

  • inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

f0ffe39b567e2796fc171571d82f80885fdff4eeb20fcb6aadb21d71f8f480da

7.1inline代码举例:

#include<iostream>

using namespace std;

inline int Add(int x, int y)
{
    int ret = x + y;
    ret += 1;
    ret += 1;
    ret += 1;
    return ret;
}
int main()
{
    // 可以通过汇编观察程序是否展开
    // 有call Add语句就是没有展开,没有就是展开了
    int ret = Add(1, 2);
    cout << Add(1, 2) * 5 << endl;
    return 0;
}

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

适用于短小的,频繁调用的函数,对于长代码会出现代码膨胀

比如,Func函数编译后是50行指令,如果有10000个位置调用Func的话:
1.Func不是inline,合计多少行指令?
10000+50
    
2.Func是inline,合计多少行指令?
10000*50

inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器自己决定

像以下的函数就算加了inline,也会被否决掉

  1. 比较长的函数

  2. 递归函数

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


7.2inline代码错误示范

inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

// F.h
#include <iostream>
using namespace std;

inline void f(int i);

// F.cpp
#include "F.h"

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

// main.cpp
#include "F.h"

int main()
{
    // 链接错误:无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)
    f(10);
    return 0;
}

这里的错误是因为

使用了 inline 关键字在F.h头文件中声明函数 f,但是在 F.cpp 文件中,但没有使用 inline 关键字。

这导致链接器在尝试将程序链接在一起时找不到 f 函数的定义,因为 inline 函数的定义需要在每个包含其声明的编译单元中可见。

解决办法:

  1. 移除 inline 关键字:从头文件中移除 inline 关键字,这样函数 f 就不再要求在每个编译单元中都有定义。这样,只需要在 F.cpp 中定义函数即可。
  2. 确保 inline 函数定义在每个编译单元中可见:如果希望保持 inline,则需要在每个包含 F.h 的文件中也包含 F.cpp,或者将 F.cpp 的内容直接放入 F.h 中。

7.3实现一个ADD宏函数的常见问题:

#include<iostream>
using namespace std;
// 实现一个ADD宏函数的常见问题

//#define ADD(int a, int b) return a + b;//错误写法
//替换后函数直接return了

//#define ADD(a, b) a + b;//错误写法
	Add(10, 20) * 20;//这是例子

//#define ADD(a, b) (a + b)//错误写法
	int a = 1, b = 2;
    Add(a | b, a & b); //这是例子
	// (a | b + a & b)//不可以,会出错。因为+号的优先级更高,会出现问题


// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外面的括号?
// 为什么要加里面的括号?
int main()
{
    int ret = ADD(1, 2);
    cout << ADD(1, 2) << endl;
    cout << ADD(1, 2)*5 << endl;
    int x = 1, y = 2;
    ADD(x & y, x | y); // -> ((x&y)+(x|y))
    return 0;
}

宏函数

优点-- 不需要建立栈帧,提高调用效率

缺点-- 复杂,容易出错、可读性差、不能调试


8.nullptr

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
	#ifdef __cplusplus
		#define NULL 0
	#else
		#define NULL ((void *)0)
	#endif
#endif
  • C++NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。

  • C++11中引入nullptrnullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。

#include<iostream>
using namespace std;

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

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

int main()
{
    f(0);//打印:f(int x)
    
    // 本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。
    f(NULL);//打印:f(int x)
    
    f((int*)NULL);//打印:f(int* x)
    
    // f((void*)NULL);// 编译报错:error C2665: “f”: 2 个重载中没有一个可以转换所有参数类型
    //因为C++不允许void*转换成任意类型的指针

    f(nullptr);//打印:f(int* x)
    
    return 0;
}
  • 53
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值