初识C++【引用】【inline】【nullptr】

一.引用

1.1引用的概念和定义

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同⼀块内存空间。比如:水浒传中李逵,宋江叫"铁牛",江湖上人称"黑旋风 ";林冲,外号豹子头。简单来说,引用就是起外号。
用法就是:类型& 引用别名 = 引用对象;
举个例子:
#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	//注意这里的b和c就是a的别名
	int& b = a;
	int& c = a;
	//这里打印出来的值都是相同的
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	//打印出来的地址也是相同的
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	return 0;
}

打印出来是这样的:

也就是说,引用其实就是给一个变量在起一个名字,并不会重新创建额外空间。

1.2引用的特性

(1) 引用在定义时必须初始化

因为引用是一个别名,它必须引用一个已经存在的对象。换句话说,引用必须指向某一个具体的对象。如果在定义引用时没有进行初始化,就会导致引用无法指向任何已存在的对象,这将引发编译错误。

(2)个变量可以有多个引用
这个就是上面1.1写的那个代码,一个对象可以有多个外号。
(3) 引用一旦引用一个实体,再不能引用其他实体
可以看一下这个代码,打印出来的值是不是一样的:
int main()
{
	int a = 0;
	int& b = a;
	int c = 20;
	b = c;
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	return 0;
}

打印出来:

自然是不一样的,因为我刚开始就给a取了一个别名叫做b,如果再把c赋值给b,这里的赋值就仅仅只是把c的值给了b。因为b已经引用了一个实体a,不能再引用其他的实体。

1.3引用的使用

(1)  引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
(2)  引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
简单的举个引用传参的例子:在C语言中实现Swap函数我们需要用到指针传参来解决形参不能改变实参的问题,但是这样是有一些麻烦的,我们不仅要传参地址,还要在函数内部解引用地址。所以这里我们就可以用到引用传参。
void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
(3) 引用返回值的场景就有些复杂。
这里我就依然用栈来举例,在C语言学习栈的时候,我们有一个函数叫做STTop,意思是返回栈顶元素。为了方便,我就只把栈的结构和初始化写了一下,还有STTop函数。
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;
}
//修改栈顶元素
int& STTop(ST& rs)
{
	assert(rs.top > 0);
	return rs.a[rs.top-1];
}

这里我用的返回值不是int,而是int&,这里我用int&接收返回值的作用就是我们可以轻松的修改栈顶元素。我们返回的是临时变量。我们可以在main函数里修改栈顶的值。

而如果我们用int接收的话,再这样写就是错误的:

(4)  引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。
这一点就是上面说到的,我已经给a取了别字叫b,b就不能再称为c的别名了。
(5)一 些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的的是简化程序,避开复杂的指针。

1.4const引用

(1) 可以引用⼀个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
比如我用const来修饰一个变量,那么我在给这个变量取别名的时候,就也必须加上const:
	const int a = 0;
	const int& b = a;

因为我在创建变量a的时候,我用了const修饰,我本质是希望a不被修改的,但是如果我给a取了一个别名,这个别名可以用来修改a的值,这是不合理的。这里就是我们所说的权限放大(本来不能改变a,后来我加了一个别名就可以改变a了,这就是权限放大)。

如果我非要这样写,就会报错:

(2) 需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样一些场景下a*3的结果保存在一个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。 (一个是表达式,一个是类型强制转换都会产生临时对象)
(3) 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。

1.5引用和指针的关系

C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
(1)  语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
(2)  引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
(3)  引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
(4)  引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
(5)  sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
(6)  指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
但也不能说的绝对的:

二.inline

(1) 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。
简单写一个代码来看一下是否展开了内联函数:
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的汇编代码是这样的:

 

(2) inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
这段话的意思就是编译器可能不会听我们的话,即使我们加上了inline,如果我们的代码太多,编译器就会忽略掉inline。
(3) C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
(4) vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。
(5) inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

三.nullptr

说到nullptr就肯定会联想到NULL,NULL其实是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
    #ifdef __cplusplus
        #define NULL    
    #else
        #define NULL    ((void *)0)
    #endif
 #endif

先来看一段代码:

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

这里最终打印的结果就是:

我们看到这里的传参的虽然是NULL但是打印出来的是与0相同的值。

(1)C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何定义,在使用空值的指针时,都不可避免的会遇到⼀些麻烦,本想通过f(NULL)调用指针版本f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。即使是f((void*)NULL)(这样的话,传上面的哪个函数都不匹配),调用会报错。

值得一提的是,在C语言中void*被允许隐式类型转换其他类型的指针,比如在C语言中这样写代码是可以的

	void* p1 = NULL;
	int* p2 = p1;

但是在C++里就必须把p1强制转化成int*

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

这个nullptr就可以任意的转化成其他的类型的指针,把它放到这题上面就可以出现不一样的结果:

到这里,我们对C++基本上有了一定的认识,后面我会继续更新C++的内存。感谢大家的观看,若有错误还请多多指出。

相关推荐

  1. C#语言

    2024-07-10 19:50:04       39 阅读
  2. <span style='color:red;'>初</span><span style='color:red;'>识</span><span style='color:red;'>C</span>++

    C++

    2024-07-10 19:50:04      30 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-10 19:50:04       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-10 19:50:04       5 阅读
  3. 在Django里面运行非项目文件

    2024-07-10 19:50:04       4 阅读
  4. Python语言-面向对象

    2024-07-10 19:50:04       7 阅读

热门阅读

  1. 软件开发C#(Sharp)总结(续)

    2024-07-10 19:50:04       9 阅读
  2. CSS 样式链接的多方面应用与最佳实践

    2024-07-10 19:50:04       10 阅读
  3. 西门子7MB2335-0AL00-3AA1

    2024-07-10 19:50:04       7 阅读
  4. How to Describe Figures in a Research Article

    2024-07-10 19:50:04       7 阅读
  5. 常见网络攻击方式及防御方法

    2024-07-10 19:50:04       9 阅读