【C++笔记】引用和const引用以及inline和nullptr
🔥个人主页:大白的编程日记
🔥专栏:C++笔记
文章目录
前言
哈喽,各位小伙伴大家好!上期我们讲了命名空间。这期我们来讲引用。话不多说,咱们进入正题!向大厂冲锋!
一.引用
1.1引用的概念和定义
引用不是新定义⼀个变量,而是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同⼀块内存空间。比如:水壶传中李逵,宋江叫"铁牛",江湖上⼈称"黑旋风";林冲,外号豹子头;
C++中为了避免引⼊太多的运算符,会复⽤C语言的⼀些符号,比如前⾯的<<和>>,这⾥引用也和取地址使用了同⼀个符号&,大家注意使用方法⻆度区分就可以。
- 引用的使用
类型& 引用别名=引用对象;
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
注意引用并没有开空间。引用只是给对象取别名。所以d++,a,b,c也都++。他们的地址也是一样的。
1.2引用的特性
- 初始化
引用在定义时必须初始化。
- 引用个数
⼀个变量可以有多个引用。这里我们用b给a取别名。再给用c给b取别名。其实也是给a取别名。
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b=a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
}
- 唯一对象
引用⼀旦引用⼀个实体,再不能引用其他实体。
int main()
{
int a = 10;
// 编译报错:“ra” :必须初始化引⽤
//int& ra;
int& b = a;
int c = 20;
b = c;
// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
// 这⾥是⼀个赋值
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
1.3引用的使用
引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象
- 改变对象
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(&a, &b);
return 0;
}
以前我们交换变量只能传地址。因为形参是拷贝,形参的改变不影响实参。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
现在我们就可以这样写。因为引用就是引用对象本身。只是取别名而已。
引⽤传参跟指针传参功能是类似的,也不用&和*,引用传参相对更方便⼀些。
- 引用传参
int Add(int& x, int& y)
{
int ret=x + y;
return ret;//拷贝生成临时对象,再返回临时对象
}
这是传值返回。他拷贝生成一份临时对象,再将临时对象返回。
int& Add(int& x, int& y)
{
int ret=x + y;
return ret;//传引用返回,直接返回对象的别名
}
这是传引用返回,直接返回ret的别名。这样就能减少传值返回的拷贝。提高效率。
但是这样写会产生野引用的问题(类似野指针)。因为ret是局部变量。函数结束后就会销毁。这时我们再通过别名去访问ret就会产生野引用。
-
引用和指针
引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很⼤的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。 -
教材使用
⼀些主要用C代码实现版本数据结构教材中,使⽤C++引用替代指针传参,目的是简化程序,避开复杂的指针,但是很多同学没学过引用,导致⼀头雾水。
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, * PNode;
// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
void ListPushBack(LTNode** phead, int x);
void ListPushBack(LTNode*& phead, int x);
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
}
}
int main()
{
PNode plist = NULL;
ListPushBack(plist, 1);
return 0;
}
二. const引用
- 权限放大
可以引用⼀个const对象,但是必须用const引用。。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
权限变化有三种情况:权限放大 权限平移 权限缩小。
int main()
{
const int a = 0;
const int& b = a;//权限平移
}
a只读不写,b也只读不写。
int main()
{
const int a = 0;
int& b = a;//权限放大
}
a只读只写,b取别名后却能修改a,这是不合理的。
int main()
{
int a = 0;
const int& b = a;//权限缩小
}
a可读可写,b取别名后只读取a不修改,这是合理的。
int main()
{
const int a = 10;
int& b = a;//b取别名可读可写 权限放大
int c = b;//b拷贝给c c可读可写并不是a可读可写。
}
但是大家注意这段代码c是否涉及权限放大呢?
没有因为这只是将a拷贝给c。c可读可写并没有影响a。a依然还是可读不可写。
- 临时对象
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。
一般临时对象出现在返回值,表达式的结果,类型转化等场景。
需要注意的是临时对象具有常性。不能修改。
int main()
{
int a = 10;
const int& ra = 30;
// 编译报错: “初始化” :⽆法从“int”转换为“int& ”
int& rb = a * 3;
double d = 12.34;
// 编译报错:“初始化” :⽆法从“double”转换为“int& ”
int& rd = d;
return 0;
}
这段代码为什么报错呢?
因为他们都生成了临时对象。临时对象具有常性。可读不可写。
所以要使用const引用。
int main()
{
int a = 10;
const int& ra = 30;
const int& rb = a * 3;
double d = 12.34;
const int& rd = d;
return 0;
}
那临时对象销毁后。这时的引用不就是野引用了吗。
不会,引用临时对象后。临时对象的生命周期和引用一样。
- 使用场景
那const引用有什么用呢?
void f(int x)
{
}
例如我们写一个函数f。这样写就会增加拷贝。所以我们会用引用做参数。
void f(int& x)
{
}
int main()
{
int a = 2;
int b = 1;
double d = 1.5;
f(3);//常量不可改变
f(a + b);//临时对象 常性
f(1.5); // 临时对象 常性
return 0;
}
但是如果我们不修改x的话。这样写常量,表达式来传参都传不过去。因为他们具有常性。
但是如果我们把const加上就可以传参。这样我们的传参范围更宽泛。这就是const引用的价值。
三.指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
- 语法概念上引用是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
- 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
- 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
- sizeof中含义不同,引用结果为引⽤类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
- 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
但是底层上指针和引用都没有区别。转到汇编指令都是一样的。没有引用的概念。
四.inline
- 内联的概念
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,因为建立栈帧还是有消耗的。这样就可以提高效率。
所以inline其实是用来提效的。
其实就相当于在调用的地方替换在函数内部展开。相当于宏的作用。
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;
}
可是我们Add不是内联函数了吗。怎么还有call。还是展开了?
-
Debug展开
vs编译器debug版本下⾯默认是不展开inline的,这样方便调试。
debug版本想展开需要设置⼀下以下两个地方。 -
编译器决定
inline对于编译器而言只是⼀个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
也就是内联对编译器只是建议。如果建议合理他就听,不合理就不听。因为无条件展开也是会付出代价的。
不合理的展开,如大段代码的函数展开就会导致可执行程序变大。
- 宏和内联
C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
所以为了解决宏的问题。同时方便调试。设计出了内联。 - 声明和定义
inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
所以内联函数声明和定义都放在.h文件即可。
五.nullptr
NULL实际是⼀个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL被定义为字面量0,0编译器默认时int类型。
C语言中NULL被定义为void*指针类型。
所以在某些场景下会出现歧义。
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
// 本想通过f(NULL)调⽤指针版本的f(int*)函数,
// 但是由于NULL被定义成0,调⽤了f(intx),因此与程序的初衷相悖。
return 0;
}
这里f(NULL)本想调用第二个函数。却调用到第一个函数。因为C++NULL定义为字面两0,默认属于int整型。
那是不是把NULL改为void就可以了呢?
也不行因为C++中void不能转为int*。这样两个类型都不匹配。
但是C语言检查不严格。void*可以转为任意类型的指针。
为了解决这个问题C++涉及了nullptr.
- nullptr
C++11中引入nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
后言
这就是C++的引用和一些语法。大家多加学习掌握。今天就分享到这,感谢大家的耐心垂阅!咱们下期见!拜拜~