c++中引用、指针、函数的深思

长期一直以来一个困惑,就是引用、指针、函数到底如何快速记忆它们并且使用它们。从它们的存储形式啊还有操作过程啊进行一个简单的初探。

首先,从以下几个方面着重的理解一下:

1. 变量(variable)的表现形式;

2. 指针的结构和原理;

3. 引用的结构和原理;

4. c语言中函数调用的本质;

5. 引用比指针的优越感;

6. 链表或者树的操作

一:变量的形式

说道变量,很多人都觉得非常简单,每天都在定义变量,应用变量。可是有没有停下脚步细细的品味一下具体什么是变量呢?变量(variable)的定义在计算机科学中到底是如何定义的?然后variable到底是在内存中如何存储值的呢?那么跟着上面的问题,我们来一一的解答,首先最重要的,variable的定义,当你申明一个变量的时候,计算机会将指定的一块内存空间和变量名进行绑定;这个定义很简单,但其实很抽象,例如:int x = 5; 这是一句最简单的变量赋值语句了, 我们常说“x等于5”,其实这种说法是错误的,x仅仅是变量的一个名字而已,它本身不等于任何值的。这条statement的正确翻译应该是:“将5赋值于名字叫做x的内存空间”,其本质是将值5赋值到一块内存空间,而这个内存空间名叫做x。切记:x只是简单的一个别名而已,x不等于任何值。

 变量在内存中的操作其实是需要经过2个步骤的:

1)找出与变量名相对应的内存地址。

2)根据找到的地址,取出该地址对应的内存空间里面的值进行操作。

二:指针的结构和原理

首先介绍到底什么是指针?指针变量和任何变量一样,也有变量名,和这个变量名对应的内存空间,只是指针的特殊之处在于:指针变量相对应的内存空间存储的值恰好是某个内存地址。这也是指针变量区别去其他变量的特征之一。例如某个指针的定义如下:

int x = 5;
int *ptr = &x;

ptr即是一个指正变量名。通过指针获取这个指针指向的内存中的值称为dereference。

特别提醒:这里千万千万不要钻进变量名x, ptr的牛角尖里面,不要去思考这些变量名存储在哪里,变量名仅仅是一块内存空间的代号名字而已,我们应该关心的是这些变量名相对应的内存地址。根据上面的分析可以看出,指针变量和任何变量在内存中的形式是相同的,仅仅在于其存储的值比较特殊而已。

三:引用在内存中的结构和原理

引用(reference)在C++中也是经常被用到,尤其是在作为函数参数的时候,需要在函数内部修改更新函数外部的值的时候,可以说是引用场景非常丰富。但程序员一般很难或者不注意分析reference和pointer,只是知道怎么应用而已,而不去具体分析这个reference。下面我就来简单的分析一下这个reference。首先我们必须明确的一点就是:reference是一种特殊的pointer。从这可以看出reference在内存中的存储结构应该跟上面的指针是一样的,也是存储的一块内存的地址。

&:“&引用” 和“&取地址符”的区别和作用

两者区别
引用的格式:
类型名  &  别名 = var;
1.定义的时候必须初始化,即& 前面有类名或类型名,&别名后面一定带 “=” (在= 左边);
2.&后面的别名是新的名字,之前不存在。

&取地址时:
如果&是取址运算符,也就意味着取一个变量的地址并付给指针变量。&后面紧跟的是变量(已存在);

例如reference的定义如下:

int x = 5;
int &y = x;

reference 和 pointer主要有以下3中不同点:

1)reference不需要dereference即可直接获取到指向的内存空间的值。例如上例中,直接y就可以获取reference y所指向的内存空间的值,而不需要*y来获取。

2)reference的赋值操作也不需要取地址符来赋值,可以直接通过变量名,例如上例中,int &y = x, 而不需要 int &y = &x;

3) reference 在申明的时候就必须要有初始值,而且reference变量指向的内存地址是不能变化,不像pointer那样可以很灵活的重新指向其他地址。

四:c语言中函数调用的本质

首先我们知道c语言中函数的本质就是一段代码,但是给这段代码起了一个名字,这个名字就是他的的这段代码的开始地址

这也是函数名的本质,其实也就是汇编中的标号。下面我们会接触到一些东西 比如 eip 就是我们常常说的程序计数器,还有ebp和esp 分别为EBP是指向栈底的指针,在过程调用中不变,又称为帧指针。ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称为栈指针,不理解也没关系。

先讲一下函数调用的过程,函数调用的时候其实也就是汇编中的地址的跳转,汇编中的跳转源于标号地址。其实这个也好理解,不知道地址,你让我如何找你。但是在找的开始,我们需要先记录一下回家地址,当前的一些寄存器状态(这是因为调用到里面也可能用到这些寄存器)注意还要压入一些函数调用参数。

我们从上面的图可以看到,函数调用的时候依次压栈从右到左。压栈完毕调用call。call的作用有俩个,就是压栈返回值,然后修改程序计数器eip,实现程序跳转到被调函数。接着压栈ebp里面的内容然后将esp赋值给ebp。也就是ebp里面的内容被改变,变为现在的esp内容,esp不就是栈顶,也就是说现在都指向了栈顶,然后压栈结束了或者可能换有一些其他的参数,比如我们递归调用,那下面就是下一个函数的参数,返回地址等等等。现在我们讨论的是ebp的作用是什么:那就是ebp指向了一个堆栈中一个栈帧的底部。而esp指向了顶部。我们可以利用ebp的偏移实现,局部变量和参数的访问。下面我们要讨论的就是如何返回。其实就是参数依次出栈,最后老ebp弹出到现在ebp。ebp指后到上一次的栈帧底部。但我们问一下参数是如何出栈的,难道是弹出,吗?弹出还有什么用,因为局部变量用完后就没用了呀,也没必要弹出给寄存器,其实是ebp将值赋给esp,esp由以前的栈顶指向栈底也就是ebp的地方。然后老ebp弹出到ebp。ebp归为到以前的ebp。esp再减4。esp回到返回地址处,然后在修改eip返回。然后esp再减4,回到新的栈顶。而返回的指令源于ret。大概知道函数是什么样子了。

五:引用比指针的优越感

先看段代码:

#include<iostream>
int* func1(int* x);
int& func2(int &x);
int main() {
	int a = 0;
	std::cout << *func1(&a) << std::endl;
	std::cout << &func2(a) << std::endl;
	system("pause");
}
int *func1(int* x) {
	(*x)++;
	return x;
}
int &func2(int&x) {
	x++;
	return x;
}

若用reference写,程序好看很多了,再举一个例子,将两个变量的内容交换:

 

#include<iostream>
void swap1(int* x,int* y);
void swap2(int& x,int& y);
int main() {
	int x = 1;
	int y = 2;
	std::cout << "x=" << x << " y=" << y << std::endl;
	swap1(&x, &y);
	std::cout << "x=" << x << " y=" << y << std::endl;
	swap2(x, y);
	std::cout << "x=" << x << " y=" << y << std::endl;
}
void swap1(int*x,int*y) {
	int tmp = *y;
	*y = *x;
	*x = tmp;
}
void swap2(int&x, int&y) {
	int tmp = y;
	y = x;
	x = tmp;
}

以上的例子,都明显的发现reference的确比pointer优秀。由于argument和parameter的内存地址相同(parameter是指函数定义中参数,而argument指的是函数调用时的实际参数),若function中不小心改了parameter的变量,就造成argument变量值改变了,在其它语言的确没有解决的方案(C#/Java/VB),但在C++中,提供了const reference的方式,让parameter为只读,若程序不小心改了parameter,compiler会挡下来:

 

#include<iostream>
int Add(const int& x, const int& y);
int main() {
	int x = 1;
	int y = 2;
	int s = 0;
	s = Add(x, y);
	std::cout << x << " + " << y << " = " << s << std::endl; // Output : 1 + 2 = 3
}
int Add(const int& x, const int& y) {
	return x + y;
}

所以为了程序效率以及节省内存,应该尽量使用reference来写程序,可以避免copy的动作,而且reference的写法程序的可读性远比pointer高,而写法基乎和一般变量一样。

 

上面的例子涉及到一个指针函数,简单描述一下:

 

顾名思义,指针函数即返回指针的函数。其一般定义形式如下:

     类型名 *函数名(函数参数表列);

    其中,后缀运算符括号“()”表示这是一个函数,其前缀运算符星号“*”表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个“指向返回值为…的指针(地址),“类型名”表示函数返回的指针指向的类型”。

    “(函数参数表列)”中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下:

    int *pfun(int, int);

    由于“*”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。即:

    int *(pfun(int, int));

    接着再和前面的“*”结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。

    我们不妨来再看一看,指针函数与函数指针有什么区别?

    int (*pfun)(int, int);

    通过括号强行将pfun首先与“*”结合,也就意味着,pfun是一个指针,接着与后面的“()”结合,说明该指针指向的是一个函数,然后再与前面的int结合,也就是说,该函数的返回值是int。由此可见,pfun是一个指向返回值为int的函数的指针。

    虽然它们只有一个括号的差别,但是表示的意义却截然不同。函数指针的本身是一个指针,指针指向的是一个函数。指针函数的本身是一个函数,其函数的返回值是一个指针。

《用函数指针作为函数的返回值》

在上面提到的指针函数里面,有这样一类函数,它们也返回指针型数据(地址),但是这个指针不是指向int、char之类的基本类型,而是指向函数。对于初学者,别说写出这样的函数声明,就是看到这样的写法也是一头雾水。比如,下面的语句:

    int (*ff(int))(int *, int);

我们用上面介绍的方法分析一下,ff首先与后面的“()”结合,即:

    int (*(ff(int)))(int *, int);                            // 用括号将ff(int)再括起来

也就意味着,ff是一个函数。

    接着与前面的“*”结合,说明ff函数的返回值是一个指针。然后再与后面的“()”结合,也就是说,该指针指向的是一个函数。

这种写法确实让人非常难懂,以至于一些初学者产生误解,认为写出别人看不懂的代码才能显示自己水平高。而事实上恰好相反,能否写出通俗易懂的代码是衡量程序员是否优秀的标准。一般来说,用typedef关键字会使该声明更简单易懂。在前面我们已经见过:

    int (*PF)(int *, int);

也就是说,PF是一个函数指针“变量”。当使用typedef声明后,则PF就成为了一个函数指针“类型”,即:

    typedef int (*PF)(int *, int);

这样就定义了返回值的类型。然后,再用PF作为返回值来声明函数:

    PF ff(int);

接下来是为什么要使用指针或者地址作为传入参数进行数据传递呢?如果直接用值作为传递的话,形参变化,实参是不变化的。下面看几个简单的例子1:

#include<stdio.h>
void fun(int *s)
{
  *s=100;
}
int main()
{
  int a=10;
  int *q;
  q=&a;
  printf("%d\n",*q);
  fun(q);
  printf("%d\n",*q);
  return 0;
}
结果为
10
100

例子2:

#include<stdio.h>
 
void  fun(int  *p){
    int  b=100;
    p=&b;
}
 
int main(){
    int  a=10;
    int  *q;
    q=&a;
    printf("%d\n",*q);
    fun(q);
    printf("%d\n",*q);
    return  0;
}
运行结果:
10
10

例子3:

#include<stdio.h>
 
void  fun(int  **p){
    int  b=100;
    *p=&b;
}
 
int main(){
    int  a=10;
    int  *q;
    q=&a;
    printf("%d\n",*q);
    fun(&q);
    printf("%d\n",*q);
    return  0;
}
运行结果:
10
100

例子4:

#include<stdio.h>
#include<stdlib.h>
 
void  myMalloc(char  *s){
     s=(char*)malloc(100);
}
 
int main()
{
     char  *p=NULL;
     myMalloc(p);
     if(p==NULL)
        printf("P is not changed!\n");
     else{
        printf("P has been changed!\n");
        free(p);
     }
     return 0;
}
运行结果:
P is not changed!

例子5:

#include<stdio.h>
#include<stdlib.h>
 
void  myMalloc(char  **s){
     *s=(char*)malloc(100);
}
 
int main()
{
     char  *p=NULL;
     myMalloc(&p);
     if(p==NULL)
        printf("P is not changed!\n");
     else{
        printf("P has been changed!\n");
        free(p);
     }
     return 0;
}
运行结果:
P has been changed!

 

知识点一、

在写int *p 时,*可以声明一个指针。还有一个名字就是“解引用”。它的意思就是解释引用,说的通俗一点就是,直接去寻找指针所指的地址里面的内容,此内容可以是任何数据类型,当然也可以是指针(这就是双重指针,后面将会讨论)。需要注意的是,在变量声明的时候,*不能当做解引用使用,只是表示你声明的变量是一个指针类型。

   int a=50;

   int *p=&a;// '&'的作用就是把a变量在内存中的地址给提取出来,具体后面解释。

   *p=5;//这就是解引用,*解释了 p对常量 50的内存地址的引用,解释结果就是直接去寻找p所指内容,因为p指向a,所以a的内容将被修改为5,而不是50。

 在指针操作中,&还有一种叫法叫做“脱去解引用”,在这个表达式中&*p(假设p是指针),&脱去了*对a的解引用,从而得到抵消的效果,得到应该是p所指的变量的地址。

知识点二、

对于指针赋值初始化参考博客:https://blog.csdn.net/hou09tian/article/details/73304756

一个重要的点,

首先是未初始化变量不可怕,指针未初始化就可怕了,它不知道指向哪里,可能指向不在内存中,也可能指向一个好几个G的内存空间,所以必须要记得初始化。

其次,将指针赋值为同一类型的另一个有效指针,int *p6 = p5;*p5是一个初始化后的指针,这行代码看着怪怪的,其实等价于int *p6; p6=p5;这样就不怪了吧,先定义在将p5这个地址赋值给p6。那么如果是函数中二级指针的传递呢?如例子3中二级指针**p,其实*q是一个初始化的指针,将q的地址传递给fun函数的**p,个人理解,其实函数参数传递是这样的,先**p=&q,然后参数传入到函数中之后,*p是指针指向的值100(这里用到知识点一),这时已经更改了*p中的p变量的存的地址,如果直接修改a的值是不可以的,fun函数里面的参数直接修改a的值是不可以的,所以只能通过修改地址。

另一个重要的点,也是我之前一直困惑的地方

这几个例子中fun函数传入的参数是定义好的,可是我在main函数中调用时,什么时候传入&q或者q或者*q呢?其实当你在main函数里面定义q的时候,*q是一个指针,其实也是变量名a(注意我这里说的是变量名)的另一个表现形式,&q是**p的另一种表现形式。

例子1中,fun定义的传入参数是*s,main函数中定义了一个指针*q,我们需要给fun中传入参数,其实就是赋值,*s=q,前面提到的等价语句先定义int *s;然后s=q。这样就传入到fun函数中了。

例子2中参数传递等同,不解释了,只是传入之后直接修改指针*p的变量p的存的地址为b的地址,因此原来存a的地址的指针*q的变量q并没有发生变化,也没有指针指向变量a,所以输出不变。

例子3中参数传递,这个可能有点不太好理解,大家了解了‘&’和‘*’符号之后,是不是可以构思出这样一个画面:&p==>p==>a,这个的意思是什么呢,如果定义个变量a=10,a存的是一个常数10,p存的是a的地址,&p存的是p的地址。*p其实等同于10,这样*p可以看成a的另一种表现形式或者另一个名字,p就是一个指针,&p就是一个二级指针,为了理解这个画面,强行画的注解图。

首先main函数中q指向a,要想修改变量q的值也就是q存的地址,那么通过指向q的指针去修改变量q的值,而q指向a理解为一个一级指针,‘&’是一个取址符号,看成向左移动,‘*’看成向右移动,相当于上面那个画面操作。fun中传入的是一个二级指针,那么这时&q正好是不是一个二级指针啊,如果在VS中的调用的fun中输入q或*q,VS会提示报错,输入的参数int *或者int 的实参与int **的形参不兼容,这就是为什么可以把q理解为一级指针(报错为int *与int **不兼容),*q理解为变量a(报错为int与int**不兼容),这个疑点至少我自己是理解差不多了。

例子5后面的例子,如果不想用二级指针,用带返回值形式的函数也可以实现,修改地址的功能,这个怎么理解呢?首先函数返回指针*s的变量s是一个内存地址,然后函数外面返回指针*p,最后把*s赋值给*p,这样保证了地址的修改,实现数据的修改或者查询。

通过这几个例子的解释,大家对指针的认识应该又上了一个台阶了吧,指针是为后面学习内存啊GPU编程啊做一个简单铺垫,至少指针不再是大家的困惑点了。

什么时候需要传递二级指针?

通过上述例子,我们可以看到,在某些情况下,函数参数传递一级指针时,在函数体内对指针做变动,也不会对原始指针产生变化,而传递二级指针时,则可以,这是为什么呢?

在传递一级指针时,只有对指针所指向的内存变量做操作才是有效的;

在传递二级指针时,只有对指针的指向做改变才是有效的;

下面做简单的分析:

在函数传递参数时,编译器总会为每个函数参数制作一个副本,即拷贝;

例如:

void fun(int *p),指针参数p的副本为_p,编译器使_p=p,_p和p指向相同的内存空间,如果在函数内修改了_p所指向的内容,就会导致p的内容也做相应的改变;

但如果在函数内_p申请了新的内存空间或者指向其他内存空间,则_p指向了新的内存空间,而p依旧指向原来的内存空间,因此函数返回后p还是原来的p。

这样的话,不但没有实现功能,反而每次都申请新的内存空间,而又得不到释放,因为没有将该内存空间的地址传递出来,容易造成内存泄露。

void fun(int **p),如果函数参数是指针的地址,则可以通过该参数p将新分配或新指向的内存地址传递出来,这样就实现了有效的指针操作。

如果觉得二级指针比较难理解,也可以通过函数返回值的形式来传递动态内存(切记不能返回栈内存),如:

#include<stdio.h>  
#include<stdlib.h>  

char* myMalloc() {
	char *s = (char*)malloc(100);
	char a[10] ="asdhjf";
	s = a;
	return s;
}
int main()
{
	char  *p = NULL;
	p = myMalloc();
	if (p == NULL)
		printf("P is not changed!\n");
	else {
		printf("P has been changed\n");
		free(p);
	}
	return 0;
}

运行结果:

P has been changed!

知道了上述这些,就不难理解上面五个小程序的执行结果了。

#include <iostream>
using namespace std;
int main() {
	char *p;
	void *p1;
	int a = 10;
	p1 = &a;
	//p = (char *)10;
	p =(char*) malloc(sizeof(char) * 12);
	system("PAUSE");
}

内存分配函数返回值是一个void*指针,所以要强行转换类型(char*),p1是为了初始化一个地址,不然不初始化就是一块未知数据,p是一个地址,malloc返回也是一个内存地址,所以直接赋值给p。

对于初级学习,肯定会遇到一个坑,概念混淆《指针数组、数组指针、函数指针、函数指针数组、指向函数指针数组的指针》

这一段内容介绍版权归原作者所有,链接见参考。

 

指针数组

表达式为:int *p[5] 
理解:下标运算符[ ]的优先级是要高于指针运算符*,因此p先和下标运算符结合,决定了p首先是个数组,其类型int *,表明数组的元素都是都是指针。而数组占多少个字节由数组本身决定。其实指针数组表达就是一个存放指针的数组。 
其图示如下: 

数组指针

表达式为:int (*p2)[5] 
理解:括号运算符()的优先级是最高的,因此p2先和括号内的指针运算符*结合,因此p2首先是一个指针,它指向了一个数组,该数组的类型是int。注意:该数组在这里并没有名字,是一个匿名数组,只有通过指针p才可以访问它。 
其图示如下: 

函数指针

表达式:(返回值类型)(*fun)(形参变量) 
如:char *(*fun1)(char *p1, char *p2) 
理解:其中fun1 是一个指针变量,它指向一个函数。这个函数有俩个char *指针类型的参数,函数的返回值也是一个char * 。 
注意:指向函数类型的指针变量没有++和–运算。 
下面给出一个实例:

#include <stdio.h>

int* fun(int p1,int p2)
{
    int i = 0;
    i = p1 + p2;
    return i;
}
int main()
{
    int* (*pf)(int p1,int p2);
    int *p;
    pf = &fun;
    p = (*pf)(1,2);
    return 0;
}

其实,函数指针实函数指针与普通指针没什么差别,只是指向的内容不同而已。使用函数指针的好处在于,可以将实现同一功能的多个模块统一起来标识,这样一来更容易后期的维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦合度以及使接口与实现分开。

函数指针数组

表达式:(返回值类型)(*fun[])(形参变量) 
如:char *(*fun2[3])(char *p) 
理解:首先括号的优先级最高,因此我们先看括号里的内容,而下标运算符[ ]的优先级是要高于指针运算符*,因此p先和下标运算符结合,决定了p首先是个数组。该数组内存储了3个指向函数的指针。这些指针的返回值为char * 
其图示如下: 
 
下面给出一个实例:

#include <stdio.h>
#include <string.h>
char* fun1(char* p)
{
    printf("%s\n",p);
    return p;
}
char* fun2(char* p)
{
    printf("%s\n",p);
    return p;
}
char* fun3(char* p)
{
    printf("%s\n",p);
    return p;
} 
int main()
{
    char* (*pf[3])(char* p);
    pf[0]= fun1; // 可以直接用函数名
    pf[1]= &fun2; // 可以用函数名加上取地址符
    pf[2]= &fun3;
    pf[0]("fun1");
    pf[0]("fun2");
    pf[0]("fun3");
    return 0;
}

指向函数指针数组的指针

示例:char* (*(*fun)[3])(char* p) 
理解:这里的 fun和上一节的 fun就完全是两码事了。上一节的 fun 并非指针,而是一个数组名;这里的 fun确实是实实在在的指针。这个指针指向一个包含了 3 个元素的数组;这个数字里面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函数。 
其图示如下: 

下面给出一个实例:

#include <stdio.h>
#include <string.h>
char* fun1(char* p)
{
    printf("%s\n",p);
    return p;
}
char* fun2(char* p)
{
    printf("%s\n",p);
    return p;
}
char* fun3(char* p)
{
    printf("%s\n",p);
    return p;
}
int main()
{
    char* (*a[3])(char* p);
    char* (*(*pf)[3])(char* p);
    pf = &a;
    a[0]= fun1;
    a[1]= &fun2;
    a[2]= &fun3;
    pf[0][0]("fun1");
    pf[0][1]("fun2");
    pf[0][2]("fun3");
    return 0;
}

后面会学习一下链表或者树的操作

=========================未完待续====================

由于篇幅有限,涉及到数据结构算法的内容也比较多,后面将会按照下图学习。

 

 

 

参考:

http://www.cnblogs.com/VIPler/p/4319313.html

https://yq.aliyun.com/articles/297093

https://blog.csdn.net/cherishinging/article/details/72229626

https://blog.csdn.net/majianfei1023/article/details/46629065

http://www.cnblogs.com/oomusou/archive/2006/10/22/536184.html

https://www.cnblogs.com/tangxiaobo199181/p/7989464.html

https://blog.csdn.net/tianxiaolu1175/article/details/46889523.html

http://www.cnblogs.com/AndyJee/p/4630153.html

发布了347 篇原创文章 · 获赞 607 · 访问量 260万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览