重生之我要学c++第二课

    在上期内容,我们讲述了c++相比于C语言的更简易之处,本期就让我们继续学习相关的知识,了解c++宇宙

引用(起别名)

   

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

举个例子,诸葛亮,字孔明,号卧龙先生,你可以叫他诸葛亮,这是他的本名,也可以叫他卧龙先生,这是他的别名。

话不多说上代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;

int main()
{
	int a = 10;
	int& b = a;//<====定义引用类型
	cout << &b << endl;
	cout << &a << endl;
	return 0;
}

看第七行和第八行,我们定义了一个变量a,像第八行那样的操作,就是给a起一个别名,b同a指向的是同一块地址,我们可以验证一下:

运行截图:

重生之我要学c++第二课_第1张图片

可以发现a和b的地址是一样的,也就可以得到一个结论,b值的改变,会导致a的变化

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

现在大家有一个疑问,这个起外号有啥用呢? 可以告诉大家,在普通场景下,引用操作毫无意义,但是在以下的场景下,作用巨大:

1.输出型参数

   我们以前写一个交换函数,是不是得用指针,因为形参的改变不影响实参,C语言阶段不需要传地址才能实现,而现在我们只需要简单的引用:

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;

void swap(int& ra, int& rb)
{
	int t = ra;
	ra = rb;
	rb = t;
	cout << ra << rb << endl;
}
int main()
{
	int a ,b;
	cin >> a >> b ;
	swap(a, b);
	
	return 0;
}

现在我们不需要复杂的指针引用,只需要把传进的参数换成别名,就可以直接对数值进行更改,因为别名和本身是指向同一个地址,二者相互影响。

引用特性

1. 引用在定义时必须初始化
也就是:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;

int main()
{
	int a = 1;
	int& b;//错误,要进行初始话
	int& b = a;//正确,在定义的地方初始化
	cout << b << endl;
	return 0;
}
2. 一个变量可以有多个引用
    这句话是什么意思呢,通俗的说就是可以给别名起别名,给外号起外号
上代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;

int main()
{
	int a = 10;
	int& b = a;//b是a的别名
	int& c = b;//c是b的别名
	//打印看看他们的地址是否相同:
	cout << &a << &b << &c << endl;
	return 0;
}

    仔细阅读代码会发现,我们定义了一个变量a,然后给a取了别名叫b,又给b取了个别名叫c,这样做是完全允许的。谁说人只能由=有一个外号?我不仅仅叫诸葛亮,也叫诸葛孔明,亦可以叫卧龙先生,这三者都是我。

    那么改变c的数值会不会改变b呢,亦会不会改变c呢?让我们运行以下代码:

运行截图:

重生之我要学c++第二课_第2张图片

地址完全一样,这其中的意思相信大家都明白了,三者任意一个的改变,都会改变其他,就像是你问诸葛亮先生吃饭了吗?他回答吃了,那么孔明也吃了,卧龙先生也吃过了。哈哈哈哈是不是很形象。

3. 引用一旦引用一个实体,再不能引用其他实体

当一个变量名称为另一个变量的别名的时候,就不能在称为别的变量的别名:

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
int main()
{
	int a = 10;
	int& b = a;//b是a的别名

	int x = 1;
	b = x;
	cout << b << endl;
	return 0;
}

大家阅读这段代码,b是a的别名,思考一下b=x这句话,是给b赋x的值还是起别名

答案是赋值

这就是 “引用一旦引用一个实体,再不能引用其他实体”的含义。(只能和一个人结婚)

引用复杂的使用场景

1.引用做返回值

大家来看一段这样的代码:

//传值返回
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
int count()
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = count();
	cout << ret << endl;
	return 0;
}

大家思考一下,我们在调用函数之后,是不是把n返回到主函数之中

相信大家都知道肯定不是,因为这是传值返回,当函数调用结束后,n是不是就被销毁了,因为n是在函数中被定义的临时变量。

只要我们在这段代码稍加改动,就可以将其改成传引用返回

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
int& count()
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = count();
	cout << ret << endl;
	return 0;
}

仔细区分这两段代码的区别,这段函数的返回值就是n的别名,也就是n的引用,大家想想,这个时候会发生什么事情,返回n的引用会引发什么问题?

前文我们提到,出了作用域,n就会被销毁,既然n已经销毁了,还要返回n的别名,是不是就成了类似于野指针的样子呀。

有些小伙伴会发出这样的疑问,空间都被销毁了,为了什么还能返回他的别名?

这是因为空间的销毁就像是退房一样,这个房间住着n这个人,现在这个房间n不用了,n退了这个房间,但是n还是存在的,而引用这个做法,就像是房间虽然退了,但是悄悄地把钥匙留下了。

这就造成了我们这段程序的结果不准确,有可能是1,也可能是随机值

只要我稍微一变招:

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
int& count()
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int& ret = count();
	cout << ret << endl;
	cout << ret << endl;

	return 0;
}

 打印的值就会变成这样:

第一次打印ret还是1,第二次就变成了随机值

这涉及到以后要学习的知识,先给大家卖个关子哈哈哈哈,大家记住这个小tip

 再来看这样一段代码:

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <

大家觉得这段代码的返回值是3还是7,把自己的答案写在评论区

答案:

为什么会是7呢,这是栈帧有关的知识,给大家看一张图,C语言学的稳定的小伙伴应该是可以看得懂,当然C语言学的不好的同学可以看看博主曾经C语言的博客,希望能给大家带来帮助

重生之我要学c++第二课_第3张图片 

注意: 如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。

 

传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
光说无用,我们来用代码验证一下
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
#include 
struct A { int a[10000]; };

A a;
// 值返回
A TestFunc1() { return a; }

// 引用返回
A& TestFunc2() { return a; }

void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();

	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();

	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
	return 0;
}

这段代码里我分别调用了传值返回和传引用返回,并且计算它们的运行时间供大家比较:

重生之我要学c++第二课_第4张图片

可以看出传值返回比传引用返回慢了很多很多,这仅仅只是传个参的小路就差了这么多!!!

所以衍生出一个问题:

什么时候该用传值返回,什么时候该用传引用返回?

1.返回的参数是一个全局变量或者静态对象,就不需要考虑生命周期的事,所以就用引用返回正正好好,提高很多的效率

2.在堆上动态申请的用引用返回,提高效率

引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。(同一地址)

 

int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。  
   
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}

(仔细观察代码,自己运行一下)

我们来看下引用和指针的汇编代码对比:
重生之我要学c++第二课_第5张图片

 

引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
在某些角度上,引用和指针是一样的,引用使用的更方便

内联函数

内联函数也是c++来补C语言的一个坑。

宏在C语言的使用相信大家都很熟悉了

宏的优点 :可以批量置换数值

宏最大的缺陷在于宏函数,现给大家看一段实现两数之和的宏函数

define add(x,x) ((x)+(y))
不会写宏函数的自觉去学习C语言,看博主以往的作品就够了。
宏的根本含义就是替换,把前边的表达式替换成后边的,在替换的过程中是很容易出错的,语法错误太多。我们写宏函数的目的就是替换一般函数,宏函数一句话的事,图个方便,但是宏太容易出错,方便又成了麻烦,并且宏还不能调试
1.容易出错,语法坑多
2.不能调试
3.没有类型安全的检查
优点:
1.没有类型的严格限制
2.针对频繁调用的小函数不需要建立栈帧,提高效率
c++祖师爷用了很久很不耐烦,所以祖师爷发明了 inline修饰
我们只需要在函数定义前加上inline
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
inline int Add(int x, int y)
{
	return x + y;
}
int main()
{
	Add(1, 2);
	return 0;
}

inline的作用就是和宏大差不差,但是没有宏的缺陷,宏函数没有调用的消耗,内联函数也没有,内联函数的特点是在release优化下把这个函数给展开,像编译器展开

博主通过汇编语言给大家看一下内联函数的展开
重生之我要学c++第二课_第6张图片

 

重生之我要学c++第二课_第7张图片

重生之我要学c++第二课_第8张图片 

刚刚学到这块的时候,博主就在想,如果把所有的函数都搞成内联函数,那效率岂不是爽歪歪了 ,不知大家有没有这样的想法,哈哈哈哈这样的想法是很有想象力,现在博主也给大家解答一下这样是不可以的:

首先,我们是不可以把很大的函数建内联的,更不要说所有的函数,如果一个程序很长,变成内联会导致程序变大的,这叫做程序膨胀

内联的特性:

1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为
《C++prime》第五版关于inline的建议:
       《内联说明只是像编译器发出一个请求,编译器可以选择忽略这个要求》
所以你的函数可不可以内联是编译器说的算

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

auto关键字(c++11)

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
1. 类型难于拼写
2. 含义不明确导致容易出错
所以祖师爷创立的auto!
上代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
int main()
{
	int a = 0;
	int b = a;
	return 0;
}

我们先定义了一个a,一般我们要定义一个和a同类型的b的时候,是不是就要像代码那样操作,现在有了auto,我们完全可以换个方式:

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
int main()
{
	int a = 0;
	auto b = a;
	return 0;
}

把b的类型定义为auto,且让b=a,auto就会推到出a的类型,进而影响b,当然大家要分清,这里的a=b不是数值的等于,而是针对于类型。

这种方法走在常规场景下毫无价值,但是随着学习的深入,我们的代码会越来越多,auto的重大作中就显露出来了:

以后我们会学习一个新的类型:

std::vertor::iterator

这就是一个我们以后会学的类型名,很长吧,现在我们用它来定义一个变量it
std::vertor::iterator it = v.begin();
光是定义一个类型变量代码就如此长,但是用上auto,效果就立马不一样:
auto it = v.begin();
直接缩短了一半的长度!!!
 

但是要【注意】:

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的占位符,编译器在编
译期会将auto替换为变量实际的类型

auto不能推导的场景

1. auto 不能作为函数的参数
// 此处代码编译失败, auto 不能作为形参类型,因为编译器无法对 a 的实际类型进行推导
void TestAuto ( auto a )
{}
2. auto 不能直接用来声明数组
void TestAuto ()
{
    int a [] = { 1 , 2 , 3 };
    auto b [] = { 4 5 6 };//错误
}
3. 为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法
4. auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有
lambda 表达式等进行配合使用。

基于范围的for循环(C++11)

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5 };

	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		arr[i] *= 2;
	}
	return 0;
}

以前我们对这个数组进行访问,要先求数组的大小,在进行for循环逐个遍历,现在我们来学用范围for访问数组

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5 };

	for(auto e :arr)
	{
		cout << 2*e << " ";
	}
	return 0;
}

现在我们来访问数组,就可以用这个范围for,这是一个固定的框架,大家把他记下来,就像当初学习for循环的时候那样,会用就行!

指针空值nullptr(C++11)

c++11补了一个c+98的大坑:

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include 
using namespace std;
void f(int)
{
	cout << " f(int)" << endl;
}
void f(int*)
{
	cout << " f(int*)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	return 0;
}

大家看这段代码,根据我们前边的学习,不难分辨这两个函数构成了函数重载,他们的类型不一样,按理说第一次传参传的是数字0,应该调用第一个函数,第二次传参应该调用第二个函数,因为传的是指针,但是实际却很坑:

重生之我要学c++第二课_第9张图片

 
我们发现两次调用都是第一个函数,这就是c++的一个大坑:
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
重生之我要学c++第二课_第10张图片

 

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何
种定义,在使用空值的指针时,都不可避免的会有错误。
所以为了补坑,c++11用nullper来表示空指针,就能解决到上边的问题
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
本期我们把c++补的C语言的坑就基本讲解完了,接下来博主会给大家带来c++的第三课:类与对象。
如果觉得有用的话,不要吝啬你们的点赞收藏关注。

你可能感兴趣的:(c++,开发语言)