tips
- 内存栈区的用习惯是先使用高地址,然后使用低地址嘛
- 顺序表数组支持随机下标访问,也是目前已知的仅有的数据结构
- 类当中的话,它不可以不仅可以去定义变量,它也可以定义函数,这个跟c当中的结构体不一样的,也就是给他升级了一下。
引用
- 引用就是取别名,没有去开新的内存空间,在引用当中也可以体现出c++的符号重载这个问题,&已经不是去取地址的意思
- (内存中的数据类型)&别名 = 本名/已有别名。
- 引用在定义的时候必须初始化,也就是说必须得说清楚到底是谁的引用。对于一个变量来说,它可以有多个引用,一个变量一旦引用了一个实体,就不能再去引用其他的实体了,终生制。
引用使用场景1 (输出型参数)部分取代指针+提高效率
- 就是体现在当往函数里面去传参的时候。
- 做一些输出型参数(本质上还是函数的参数,是函数参数的一种),什么叫做输出型参数?就是说形参的改变必须得影响到实参,也就是说当从函数里面退出来的时候,在函数里面的影响得持续到外面。与之相对应的就是输入性参数,输入型参数就是说这些参数传进来,它仅仅是给函数来用的,形参改变不影响实参。而对于输出型参数而言,形参的改变必须得影响到实参。经典操作就是swap。
- 在以前的话,针对输出型参数都是通过传址调用,也就是把指针传过去,从而达到函数出来之后能够保留修改影响的这个效果,但整体来说麻烦一点。尤其是在c语言的oj当中,int* returnSize。但现在可以改为int& returnSize,现在形参是实参的一个别名,一荣俱荣,一损俱损。
- 用引用来做参数还可以提高传参效率(针对大对象或者深拷贝类对象,什么是深拷贝以后会讲),那什么是大对象呢?sizeof比较大的对象。举个例子:
测试代码:
#include <iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
void TestFunc1(A* a)
{
}
void TestFunc2(A& a)
{
}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1(&a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A*)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
- 其实引用一定会有效率提升,因为他不需要去新开空间,像其他传参的话,他需要开一个空间,然后需要去拷贝(传值调用就不用说了),但是对于传址调用的话,其实到后面会讲,就是说如果你从底层的角度来理解,其实引用与指针两者是一模一样的,在底层当中的消耗是一样的,根据上面的测试结果也可以发现两者不分伯仲,在底层里面只有指针,没有引用
- 引用能做到的时候,指针都能做到,其实引用他革命的并不彻底,没有起到质的提升。
- 具体例子:
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
函数的传值返回(必有临时变量)
- 函数返回的话,它也分为两种类型,一种是传值返回,一种是引用返回。
- 如果函数的返回是传值返回,那么也就意味着在函数的外面肯定有一个变量需要去接收它的返回值。从函数栈帧底层的角度来理解,是先需要把这个返回值给他放到一个临时变量(这是由编译器自己生成的,一般可能由寄存器代替,但不一定是寄存器,寄存器一般只有4/8字节,数据量大就over了,以后再说)里面,然后把这个函数栈帧给他全部先销毁掉,然后再把这个临时变量(寄存器一般来说)里面的值给他转移到函数外面(刚刚被销毁的函数栈帧上面高地址处的函数栈帧里面)的那个变量所在的内存空间里。如果说返回值它所在的地方是静态区,那就没事儿了,但编译器他并不会做过多的处理,他还是会像刚才那样生成一个临时变量(一般来说寄存器),编译器不想去搞特例,不来越来越乱可能。编译器他只看你这个函数的返回类型,如果是传值返回,不管你是局部变量还是全局变量,都会生成一个临时变量,先把这个返回值给他保存起来。然后做为函数外面某个式子的一个返回值,不管咋样都会生成临时变量(傻瓜式操作)
- 对于传值返回的话,它对于这个结果会先拷贝到一个临时变量当中,然后等到函数栈帧销毁之后,再把这个临时变量当中的值给到函数外面的某个接收处。
- 然后对于引用返回的话,是不存在拷贝这个环节的,此时此刻我返回的是一个别名,是引用,本来就是指定同一块空间的,拷贝啥?当在传值返回的时候,无论这个值是一个局部变量还是一个全局静态变量,编译器都会自己去生成一个临时变量,一般都是由寄存器来充当,然后把值放到那个临时变量当中,然后再拷贝到外面(我再说传值返回嗷)。
引用使用场景2(函数的引用返回)提高效率+仅限(static修饰,malloc等,全局静态变量)引用
- 函数返回的话,它也分为两种类型,一种是传值返回,一种是引用返回。
- 但也存在着可能,就是不生成临时变量,就是让引用作为返回值(传引用返回)。
- 这时候就不会再生成临时变量了。价值何在?减少了拷贝,提高了效率。尤其是那些大对象返回,效率差距就会拉开。
#include <iostream>
#include <time.h>
using namespace std;
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;
}
- 如果说return n ,然后函数的返回类型是int&,那么这时候他返回的就是n的引用,也就是n的别名。
- 但显而易见,当一个东西是静态对象(在内存静态区)的时候,可以通过引用去返回,没有什么问题。
- 但如果说对这个函数栈帧里面的一个局部变量进行引用返回,那由于当你返回出去的时候,这个函数栈帧自己都老早销毁掉了,然后你居然通过引用等会儿就是去访问已经属于操作系统的内存空间,相当于就是野指针访问,结果就是不确定的。
- 问答1:ret多少? 1
#include <iostream>
using namespace std;
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
- 问答2:ret多少? 侥幸11
#include <iostream>
using namespace std;
int& Count(int x)
{
int n = x;
n++;
return n;
}
int main()
{
int& ret = Count(10);
cout << ret << endl;
return 0;
}
因为此时其实你画张图就很容易明白,此时此刻这个ret就是n的引用,但是变量n所在的内存空间早在函数退出的时候就已经还给操作系统了,这时候就需要去看这个函数栈帧销毁之后到底有没有被清理掉,如果没有清理栈帧,那么ret结果侥幸是正确为11。如果说清理了栈帧,那么ret的结果就是随机值,事实上这时候已经构成了非法访问
9. 问答3:ret分别多少? 侥幸11,21
#include <iostream>
using namespace std;
int& Count(int x)
{
int n = x;
n++;
return n;
}
int main()
{
int& ret = Count(10);
cout << ret << endl;
Count(20);
cout << ret << endl;
return 0;
}
这个还是关于函数栈帧的问题,这边可以发现就是当你这个函数栈帧销毁的时候,但此时此刻原先这个函数栈帧还没有被清理掉,所以此时此刻你去访问ret(n的引用)还是能够访问的到值,一开始是11。然后后面又去调用了count函数,相当于在原先刚刚被摧毁掉的函数栈帧那块空间上又开辟了一个函数栈帧(这两个函数是一模一样的,所以说原先n的那块空间,现在又给到了数字20,然后加一变成了21),但当这个函数的函数栈帧也被销毁之后,它还是没有被清理掉,因此访问的话ret就为21。但实际上两次访问都是构成了非法访问,因为ret此刻所存在的内存空间已经是属于操作系统了,但只需稍微改动,便能破坏原先侥幸尚存的未被处理的内存里面的分步状况
10. 具体例子:比方说现在用一个函数就可以实现对顺序表的指定位置的访问与修改
因为我这个函数返回的是整个顺序表当中第pos个元素的引用,属于是引用返回,返回一方面他确实可以提高效率,减少拷贝,引用的第二个功能就是说可以去修改它的返回值,读写返回值,非常强大
关于临时变量的问题 (临时变量具有常性)
- 在各种类型转换(整形提升,算数转换,自己强制类型转换)当中,以及包括各种截断等,中间它都会产生一个临时变量(就跟函数传值返回一样)
- 是先把源头的数据拷贝给到这个临时变量当中。然后需要特别注意的是临时变量具有常性(你可以理解成相当于被const修饰了一样,不能修改)
- 那为什么类型转换一定要产生临时变量呢?(临时变量具有常性,你可以把它理解成相当于用const的修饰了一样),比方说,举两个例子,变量i是int类型的,然后变量d是double类型的,然后比如说有个表达式是i<d,在这个过程当中很明显会发生类型转化(算术提升),但他其实是先生成一个double类型(8个字节)的一个临时变量,然后再把i的数据拷贝到临时变量当中然后自己提升与类型转换,这个类型转换的过程并不是在变量i自己所在的内存空间当中去进行的,而是先会开辟出一块八个字节的临时变量空间,然后再进行折腾。
- 再说变量i总共就四个字节,他自己内部怎么可能也类型提升不到像double类型这样的八个字节嘛。就是说你再进行这么类型转换的过程当中,你是不能对两个终端进行改动的,谁要进行类型转换,就开辟一块转换后类型大小的内存空间,然后在临时变量空间当中去进行操作。
- 函数的传值返回也会有一个临时变量的问题,因为马上故土要被攻占了,所以说先把返回值放到一个临时变量里面安全一点,至少这个值我要在,当函数栈帧被销毁之后,再把临时变量里面的这个值给他赋给上层函数的某个接收处
7.
8.
常引用与引用的权限大小问题
他这个权限主要针对的就是对于一个常量还是变量的可读可写的问题,但是各个外号与变量名之间的权限互不干涉与交叉。虽然他们指向的都是同一内存空间。
- 首先这边涉及到常引用,然后再去判断一下权限有没有出错,果然,权限放大了,错
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
const int a = 10;
int& b = a;
return 0;
}
- 首先这边涉及到常引用,然后再去判断一下权限有没有出错,权限平移,对
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
const int a = 10;
const int& b = a;
return 0;
}
- 这边根本就没有涉及到引用,这边就是一个简单的赋值拷贝,完全没啥问题,你不要哪壶不开提哪壶,若在应用的过程当中才有权限的放大啊,缩小啊,平移呀等等,在平时的一般赋值啊,拷贝啊什么的,根本就不用去管什么权限的放大平移还是缩小,管什么呢?仅限于引用。
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
const int a = 10;
int c = a;
double n = a;
return 0;
}
- 首先这边涉及到常引用,然后再去判断一下权限有没有出错,权限缩小与平移,对
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
int a = 10;
const int& b = a;
int& c = a;
return 0;
}
- 对,权限平移
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
double a = 1.1;
double& b = a;
return 0;
}
- 错,首先这边就已经发生了类型转换,类型转换就意味着有临时变量的生成,此时此刻你引用的时候,你的权限不能放大,也就是不能可读可写,只能平移,也就是只读
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
double a = 1.1;
int& b = a;
float& c = a;
return 0;
}
- 可以,权限平移
#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
double a = 1.1;
const int& b = a;
const float& c = a;
return 0;
}
- 函数结合起来如下:
int func1()
{
static int x = 0;
return x;
}
int& func2()
{
static int x = 0;
return x;
}
int main()
{
int& ret1 = func1(); // 权限放大
const int& ret1 = func1(); // 权限平移
int ret1 = func1(); // 拷贝
int& ret2 = func2(); // 权限平移
const int& rret2 = func2(); // 权限缩小
return 0;
}