关于我转生从零开始学C++这件事:获得冒险者资格

❀❀❀ 文章由@不准备秃的大伟原创 ❀❀❀

♪♪♪ 若有转载,请联系博主哦~ ♪♪♪

❤❤❤ 致力学好编程的宝藏博主,代码兴国!❤❤❤

         俗话说:“天上一小时,地上一年”,啊不对,是“明明上午才相见,下午就想你了”。有铁汁问我这是谁说的,umm....大伟自己独创的。

        好好好,别骂了别骂了,虽然我们离上次博客之间的更新时间很近啊,但是我相信好学的宝宝已经开始想大伟了,是不是?嗯,嗯。(^▽^ ) 那我们事不宜迟开始今天的冒险吧!

        上一篇博客我们学了命名空间,C++特有的输入输出,缺省参数,还有名字修饰,内容不多也不算难,所以大伟今天就早早地给大家更新新的一篇博客。

        引用:

        首先请大家回忆一下,我们C语言写Swap交换函数时是怎么实现的。大家看看下面代码可以实现吗?

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

int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	printf("%d,%d", a, b);
	return 0;
}

        是的,有基础的铁汁都知道,上面这段代码是无法对 a 和 b 进行交换的,因为Swap函数中我们只将a 和 b 的值进行交换,而当函数结束的时候,空间销毁,里面的交换操作不改变 a 和 b 地址上的值,所以最后ab的值没有交换成功。那我们C语言是怎么实现两个数的交换的?这时候就得使用指针了,代码如下:

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

int main()
{
	int a = 1;
	int b = 2;
	Swap(&a, &b);
	printf("%d,%d", a, b);
	return 0;
}

         像上面这样传 a 和 b 的地址进去,然后在地址的层面上对两数进行交换,这样才可以实现两个数的交换。

        但其实吧,大伟都懂,因为大伟本人也是这样的,我们都对指针有些心里阴影,特别是要改变某个数的地址还需要用到二维指针,雀食很麻烦。那我们的本贾明博士呢,在学习C语言的时候也是对指针感到很困扰(由此可见,本贾明博士也只是个普通人啊)。所以他发明了引用

        那大伟讲了这么多C语言的知识,却一个字没提引用,那这引用是个啥啊?诶,正要提呢!引用其实也算是补了C语言中指针难用的一个坑。

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

        打个比方,大伟在你们面前的名字就是“大伟”(这不是废话吗),大伟在自己的妈妈面前的名字就是“宝宝”(假的),大伟在舍友面前的名字就是那个“捐勾(卷狗)”,大伟我本身还有自己的名字。也就是说我这个人可以有很多个名字,而每个名字所指向的人都是大伟本人。引用也是如此,为一个对象起不同的名字。

        概念懂了,引用该怎么使用呢?其实我相信大家老早就见过应用的符号了,没错,就是C语言的取地址 “&” 。而在C++中, “&” 被称作引用。

        引用有个简单的公式:类型& 引用变量名(对象名) = 引用实体;下面简单给大家举个例子看一下:

int main()
{
	int a = 1;
	int& b = a;
	cout << a << " " << b << endl;
	a++;
	cout << a << " " << b << endl;

	return 0;
}

        大家觉得会输出什么呢?让我们来看一看 77c61509dda042dbbcd666a2b7968151.png

         对吧,别忘了我们在b初始化的时候前面加上了个 “&” 符号,这就表明 b 是 a 的别名,那既然 a 的值改变了,b 是不是也会跟着变?具体的说,大伟今天穿了个紫色的性感的衣服,那在设有面前是不是那个“捐勾”穿了个紫色的性感衣服?

        嗯,根据引用的这个特性,我们的Swap函数是不是可以改进改进?如下:

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

int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	cout << a << " " << b;
	return 0;
}

        这样子是不是感觉清爽了不少?没错,引用就是这么牛逼。

        但是引用使用也会有些限制,或者说是特性:

        引用的特性:

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

  2. 一个变量可以有多个引用

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

        此外,引用不仅可以为变量取别名,也可以为常量取别名,可是引用权限就需要小于等于变量本体。这个时候会有铁汁问:“诶,权限小于等于是个什么意思?” ,别急嘛,先看代码:

int main()
{
//第一段
	const int a = 10;
	//int& ra = a;   // 该语句编译时会出错,a为常量
	const int& ra = a;//正确
//第二段
	// int& b = 10; // 该语句编译时会出错,b为常量
	const int& b = 10;//正确
//第三段
    int c = 5;
    const int& rc = c;
//第四段
	double d = 12.34;
    double& rd = d;
	//int& rrd = d; // 该语句编译时会出错,类型不同
	const int& rrd = d;//为什么正确?等会解释

	return 0;
}

        好的,上面一段代码,我们从头慢慢来。

        第一段中:首先是定义了一个被const修饰的变量a,我们知道,被const修饰后,该变量的值就是无法改变,那么我们可以说这个a的权限小。而如果此刻我们再简单的对a取别名,ra默认是可以被修改的,此刻我们可以认为ra的权限大,所以会编译失败。而如果我们在ra前面同样加上const,此时ra的权限被缩小和a同样大,这时就可以编译成功了。

        第二段中和第一段类似,10是个常量,我们需要用const修饰b才可以编译成功。

        第三段就是典型的权限放小,我们的c是个可被改变的变量,权限大,而rc被const修饰,权限小,这时候前面权限小于后面,可以编译。

        第四段我们先定义了个浮点型的变量d,根据上面的讲述,当然可以取d的别名 rd。但是如果我们用 int 类型的 rrd 来给 d 取别名呢?显然是编不过的,那类型都不一样,怎么可能编得过,是吧?

        那如果将 rra 加上个const呢?......

         哈哈,头铁的铁汁们可以自己下去试试啊,其实原理上,为int类型的 rrd 加上 const 是可以取到d的别名的,为什么?正要给大家解释:

        我们C语言中不是有个强制类型转换嘛,如:

double a = 3.14;
//将浮点数a强制转换成整形
printf("%d",(int)a);

        此时,我们强转的过程中发生了什么呢?19c5c11e064842148c34ef459e84e27b.png         由图可知,我们在转换类型的其中生成了一个临时变量,我们知道临时变量具有常性,此时导致 a 的权限被缩小,所以我们就需要const 来修饰rrd啦!。但是如果这样操作的话此时就会出现一些比较奇怪的现象,我们可以通过改变d来同时改变d和rrd的值,但是我们不可以通过改变 rrd ,因为rrd是被const修饰的。

        好的,我们现在了解了引用的一些特性,那么接下来给大家来看看引用的一些应用场景吧!

        引用的应用场景:

        1.做参数

        其实我们刚刚举的那个Swap的例子就是引用做参数的很好的一个例子。再来复习一下吧!:

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

         2. 做返回值

        其实我们不仅可以对变量取别名,还可以对函数取别名,如下:

int& Count()
{
   static int n = 0;
   n++;
   // ...
   return n;
}

        那...按照我们前面学的,大家来看看下面一段代码的输出结果是什么:

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

           答案是不是3?哈哈,出乎你意料:      9551a5b7c2b1411d90a06ad67ef56a11.png

        其实这里涉及到一些新的知识点,但是大伟之后再讲,我们先来谈谈这段代码的结果为什么是7。

         我们知道,在用函数的时候会先创建一个函数栈帧,而 c 变量还是个局部变量,在函数结束后,函数栈帧销毁,c 的空间就会被释放,但是我们 ret 是Add函数的别名,所以 ret 还拥有着 Add 的空间的地址。简单说就是虽然 c 的空间被释放了,但是 c 的值还留在之前的那块空间里,而我们继续调用Add函数,后面算的7覆盖在了当前的空间上,所以虽然最后空间被回收了,但是我们还是可以用那块空间,所以最后的答案就是7。

        OK,讲了一堆,我们还是来看结论吧:

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

         思考:引用和传值效率那个更好?

    这里大伟直接给出结论:引用比传值效率更好,因为减少了拷贝,此外,传值返回比引用返回消耗的时间也更多,大家可以直接看一下下面的代码,这里大伟就不多赘述:

#include <time.h>
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;//148ms
	cout << "TestFunc2 time:" << end2 - begin2 << endl;//0ms
}
int main()
{
	TestReturnByRefOrValue();
	return 0;
}

        OK,有关引用我们再来谈最后一点:

        引用和指针的区别:

        在语法层面上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。但是实际上C++嘛,就是在C的基础上来的,底层上引用实际是有空间的,因为引用是按照指针方式来实现的。上面了解就好了。

        那最后总结一下引用和指针的不同点:

1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

2. 引用在定义时必须初始化,指针没有要求

3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体

4. 没有NULL引用,但有NULL指针

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

6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

7. 有多级指针,但是没有多级引用

8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

9. 引用比指针使用起来相对更安全

        好的,到这里我们也是把引用这个重要的知识点给学完了啊,大伟这里给大家总结一下:引用就是取别名,可以给变量,函数等等,引用表明是取别名,但是底层还是指针,此外,引用还有一些独特的,和指针不同的特性。

        内联函数: 

        我们知道,在C语言中有些短小精悍的函数我们可能会经常用,就比如上面举例的Add函数,但是我们每一次调用函数是不是都会建立栈帧,这样子的话如果调用的多了,对消耗也是很大的。

        所以,为了解决这个问题,我们的本贾明博士就发明了内联函数,那么内联函数是什么呢?

  定义:

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

         就比如,我们把Add函数改为如下:

inline int Add(int a, int b)
{
	return a+b;
}
int main()
{
	int ret = Add(1, 2);
	Add(2, 3);
	cout << ret << endl;
	return 0;
}

        通过这样我们就可以实现函数的快速调用。

        内联函数的特性:

inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运 行效率。

        但是实际上,inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不 是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

        此外需要提醒的是,内联函数不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

        OK,那我们的一个小知识点,内联函数到这里也就学完了,大伟带大家来总结一下:内联函数适用于短小且频繁调用的函数,是一种空间换时间的做法,利于提升代码运行速度。

        auto关键字:

        好的,我们也是接着马不停蹄的开始学接下来的内容啊。我们在用C语言写代码的时候,随着我们的代码量的逐渐提升,程序也是越来越复杂,程序中用到的类型也是越来越复杂,其一般体现在:

  1.类型难于拼写

  2.含义不明确导致容易出错

        虽然我们的C语言有 typedef 可以对类型进行重命名,会一定程度上简化代码,但是又会遇到新的问题:

typedef char* pstring;
int main()
{
 const pstring p1;    // 编译成功还是失败?
 const pstring* p2;   // 编译成功还是失败?
 return 0;
}

         各位铁汁们怎么认为呢?是不是有些迷茫了?害,这不仅说明了我们的C语言学的不扎实,其实这也说明了typedef的局限性。大伟直接给出结论:

        第一行会出现编译错误,为什么呢?将char*带入,我们会发现p1是由const修饰的,是个常量指针,而由const修饰的必须要初始化。

        而第二行的代码呢?我们可以把它理解为:

char* const *p2;

        这样一转换我们就会发现,*p2(p2指向的内容)是不可变的,但是p2指针本身是可以变化的,所以可以编译成功。

        但是,这样子读着是不是很难受?所以,为了解决这个问题,我们的本贾明博士发明了auto,使之能清楚地知道表达式的类型。

        auto简介:

        在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的 是一直没有人去使用它,大家可思考下为什么?

        C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

        下面是auto使用的一个简单的例子:

96c373f68fdb420aa35ab605100a14a3.png         我们发现 c 自动推导了 b 的类型并做了一些相应的操作。

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

         auto的使用细则:

        我们在用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&,如下:

int main()
{
    int x = 10;
    auto a = &x;
    auto* b = &x;//与上一行相同
    auto& c = x;

    *a = 20;
    *b = 30;
     c = 40;
    return 0;
}

        此外,我们可以在同一行定义多个变量,但是当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

auto c = 3,d = 4.0//失败

        auto不能推到的场景:

   1.auto不能作为函数的参数,如下:

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

  2.auto不能直接用来声明数组,如下

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}

        OK,关于auto的这个小知识也是学完了,大伟带大家总结一下:auto可以自动推导变量的类型,推导指针的时候加不加 * 都是一样的,auto可以在同一行声明多个变量,但是得类型相同,此外,auto不能作为函数的参数,也不能声明数组。

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

        正常我们用C语言来实现一个数组的遍历一般都是这么写的:

int main()
{
	int a[] = { 1,2,3,4,5 };
	int sz = sizeof(a) / sizeof(int);
	for (int i = 0; i < sz; i++)
	{
		a[i] *= 2;
	}

    for(int i = 0; i < sz; i++)
    {
        printf("%d ",a[i]);
    }

	return 0;
}

        但是,对于一个有范围的集合而言,由我们程序员来说明循环的范围是多余的,有时候还会容易犯错误。所以本贾明博士基于auto和引用发明了基于范围的for循环,修改后的代码如下:

int main()
{
	int a[] = { 1,2,3,4,5 };
	for (auto& e:a)
	{
		e *= 2;
	}

	for (auto e : a)
		cout << e << " ";
	return 0;
}

       上面代码中for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

        注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

        范围for的使用条件:

  1.for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围

  2.迭代的对象要实现++和==的操作(以后会讲,现在就先了解吧)

        指针空值nullptr(C++11)

        OK,最后我们来更新一下以前学习的知识,在C语言中我们一直把NULL认为是空,但是在C++中nullptr才是真的空,但是实际上NULL实际是一个宏。在传统的C头文件(stddef.h)中,可以看到如下代码:

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

        可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。

        注意:

1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的。

2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。 

         好的鸭,我们的C++第二节课:获得冒险资格到这里也就全部结束了,下一篇大伟会给大家继续带来新的队伍,也就是很经典的类和对象,希望大家继续支持大伟哦,谢谢啦!拜拜!(╯ε╰) 1aebc514006e4016ba28c77e1e46663a.png

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大伟听风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值