引用的本质
对于下面这段程序(包含编译后):
void fun(int &a) //————> void fun(int * const a)
{
int *p = &a; //————>int *p = a;//int *p = *&a;
a = 100; //————>*a = 100
*p = 200;
}
int main()
{
int x = 10;
int &y = x; //————>int * const y = &x;
fun(x); //————>fun(&x);
fun(y);
return 0;
}
由此:引用的本质在C++内部实现是一个自身为常性的指针
。C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小和指针相同,这个过程是编译器内部实现,用户不可见。
因此我们也就可以理解,为什么引用需要初始化的原因:如果定义一个int &a
,在编译器底层解释他就是int * const a
,此时a是一个随机值,不清楚其指向,就相当于一个“野指针”,无法修改,因此对于自身为常性的变量在定义时必须给予初始化。
为什么相较于指针,我们更喜欢使用引用呢?
因为引用更加安全,引用是指针的一个语法糖,在使用引用时,我们不需要像指针一样判空,因为不存在空引用,程序代码更为安全。
同时:我们还不允许局部变量的引用返回,因为返回局部变量a的地址后,将&a
的值交给将亡值(中间变量),fun函数的相关栈帧资源会回收,a已经不存在了,此时主函数解引用到a的值(失效指针)时已经找不到了,即便有时候可以获取到正确的值,我们也不认可这样的写法
,因为不能从已失效的空间中获取到这个值(不安全)。
看下面的程序:
int& fun() //————>int * const fun()
{
int a = 10;
return a; //————>return &a;
}
int main()
{
int x = fun();
int& y = fun(); //————>int y = *fun();
cout<<x<<" "<<y<<endl;
return 0;
}
给出一段程序,辨析他们为什么存在不同执行结果差异的底层原因?
int *fun()
{
int arr[10] = {12,23,34,45,56,67,78,89,90,100};
return arr;
}
int main()
{
int *p = fun();
for(int i = 0;i < 10;i++)
{
printf("%p => %d\n",p,*p);
p+=1;
}
return 0;
}
上述程序的执行结果中,数组中的各值的打印结果均为随机值
原因就是因为栈帧空间已经被系统回收,在此之后调用printf()函数会占据之前存放数组arr的那段空间,相当于清除了残存在那段空间里的数组的值,我们访问了失效的空间,自然会打印出随机值
然而若将数据大小改为100.却发现可以正常打印,这是什么原因呢?
因为对于元素个数为1000的数组arr,我们需要为其开辟更大的栈帧空间(从低地址到高地址),返回arr地址后,p指向这段空间,调用printf()函数,之后占据的空间并不会干扰到存放数组前十个值的残存空间,也就是那段空间并没有分配给printf函数,并未对那段空间进行填充,所以残存的值仍然可以正常打印。
由此也就可以解释之前为什么有时候以局部变量的引用返回时也可以获取正常的结果。
总结1
- 引用是必须初始化的,指针可以不初始化
- 引用只有一级引用,没有多级引用,指针可以有一级指针,也可以有多级指针
- 定义一个引用变量,和定义一个指针变量,其汇编指令是一模一样的,
- 通过引用变量修改所引用的内存的值和通过指针修改指向的内存的值,其底层指令也是一模一样的
总结2
什么时候可以将一个变量的值以引用的形式返回呢?
————————此变量的生存期不受函数生存期的影响
(比如此变量是一个全局变量或者静态局部变量[.data],以引用作为形参进入函数的变量以引用形式继续返回)
普通引用和常引用的区别
int a = 10;
const int &b = a;
const int &c = 10;//ok
int &d - 10;//errot
常引用可以引用字面常量,而普通引用不可以引用字面常量
对于常引用引用字面常量,编译器做了如下的工作:
const int &c = 10;
int tmp = 10;
const int &c = tmp;
//const int * const c = &tmp;
const和指针的关系
接下来我们将通过几段代码梳理const和指针的关系
int a = 10;
int* p1 = &a;
int const * p2 = &a;
const int *p2 = &a;
int * const p3 = &a;
const int * const p4 = &a;
由于上述代码中未对变量a做常性约束,因此,之后的代码均可以编译通过。
再来看看下面的代码:
int a = 10;
const int* s = &a;
int* p1 = s;
int const * p2 = s;
int * const p3 = s;
const int * const p4 = s;
上述程序中指针变量s虽然可以指向a,但由于常性约束,不能通过*s
改变a的值,因此p1和p3无法编译通过,因为在这个过程中,可以通过*p1
和*p3
修改变量*s
的值,从而修改变量a的值,这违背了*s
的常性限制。
int a = 10;
int* cosnt p = &a;
int *s0 = p;//1
const int *s1 = p;//2
int *const s2 = p;//3
const int * const s3 = p;//4
上述四条语句都能编译通过,因为虽然对于变量p来说,自身的值无法被改变,但是即使改变了s0
自身的值,也不会带动改变p
自身的值,这一过程中权利并没有扩大。
分析下列程序的执行结果:
const int a = 10;
int b = 0;
int* p = (int *)&a;//这里进行类型强转的原因是,a为常性,不能通过一个普通的指针变量指向他,否则就可以通过该指针变量改变a的值
*p = 100;
b = a;
cout<<"a = "<<a <<"b = "<<b << "*p = "<<*p <<endl;
你的回答可能是:
a = 100 b =100 *p = 100
但是真正的运行结果是:
a = 10 b =10 *p = 100
原因是const修饰的常变量在运行前的编译过程就已经被替换成如下的形式:
const int a = 10;
int b = 0;
int* p = (int *)&a
//还有一种“去常性强转”,和上一句等同
int *s = const_cast<int *>(&a);
*p = 100;
b = 10;
cout<<"a = "<<10 <<"b = "<<b << "*p = "<<*p <<endl;
由此看来,在C++的编译方式中,常变量和宏都是在运行前的编译过程中被替换。
const和引用的关系
和之前一样,接下来我们将通过几段代码梳理const和引用的关系
int a = 10;
int &b = a;//ok
const int a = 10;
int &b = a;//error
const int &b = a;//ok
int &c = (int&)a;//????
对于常变量a,若使用普通引用则可以通过该引用改变a的值,因此这种写法是不能够编译通过的,因此对于引用也需要加上常性限制,这和指针是一个道理.
int a = 10,b = 20;
int *s = &a;
int *&p1 = s;
const int *&p2 = s;
int * const &p3 = s;
const int * const &p4 = s;
上述语句中:
p1
是一个指针类型的引用,即s
的别名,因此可以改变p1
自身的值,此时s
的值也会改变,也可以通过*p1
改变a
的值p2
也是s
的别名,但是由于p2
的指向修改能力被约束,所以不能通过*p2
修改a的值,但可以自身修改p2 = &b
,或者s = &b
(p2跟随改变[别名]),此时两种情况的运行结果*p2
和*s
均为20p3
也是s
的别名,可以通过*p3
改变a
的值,如果改变s
的值,那么p3
也会跟随改变,即可以通过改变s
的值并通过*p3
改变b
的值,但p3
自身的值无法被改变
int a = 10,b = 20;
const int *s = &a;
int *&p1 = s;
const int *&p2 = s;
int * const &p3 = s;
const int * const &p4 = s;
p1
和p3
无法编译通过,这个很简单,再来看下面的程序:
int a = 10,b = 20;
int * const s = &a;
int *&p1 = s;
const int *&p2 = s;
int * const &p3 = s;
const int * const &p4 = s;
p1
和p2
无法编译通过,
- 因为如果改变
p1
自身的值,那么s的值也会改变,这与s
的自身常性约束相矛盾 - 第二句的const修饰的是
p2
的指向能力,那么p2
自身的值就可以改变,这也会产生矛盾 - 最后两句语句不会产生矛盾。
typedef和#define在指针上面的问题
看下面两段程序:
typedef int * PINT
int main()
{
int a = 10,b = 20;
const PINT p = &a;
*p = 100;//1
p = &b;//2
}
- typedef时,程序在编译过程中不会像宏替换那样替换成const int *p = &a,他只会将其理解为cosnt修饰的是一个指针类型,因此第二句无法编译通过。
#define SINT int *
int main()
{
int a = 10,b = 20;
const SINT p = &a;
*p = 100;
p = &b;
}
- #define时,程序编译时会被替换,因此,const约束了指针的指向,所以说第一句无法编译通过。
再拓展:引用数组
int ar[10] = {12,23,34,45,56,67,78,89,100};
int& a = ar[0];
a+=10;
int& br = ar;//无法编译通过
int (*p)[10] = &ar;//从右向左解析,指向一个数组大小为10个int的数组
引用一个数组时,也需要向指针一样,明确该数组的类型和元素个数,写法如下:
int (&br)[10] = ar;