指针与函数

指针与函数

  • 一、程序的栈和堆
    • 1、程序栈
    • 2、栈帧的组织
  • 二、通过指针传递和返回数据
    • 1、用指针传递数据
    • 2、用值传递数据
    • 3、传递指向常量的指针
    • 4、返回指针
    • 5、局部数据指针
    • 6、传递空指针
    • 7、传递指针的指针
  • 三、函数指针
    • 1、声明函数指针
    • 2、使用函数指针
    • 3、传递函数指针
    • 4、返回函数指针
    • 5、使用函数指针数组
    • 6、比较函数指针
    • 7、转换函数指针

导读:理解程序栈和堆有助于更深入彻底地理解程序的工作方式以及指针的行为。我们研究了 栈、堆和栈帧,这些概念对解释将指针传递给函数和从函数返回指针的机制有帮助。比如说, 返回指向局部变量的指针是错误的,原因是为局部变量分配的内存会在后续的函数调用中被覆盖传递指向常量数据的指针很高效,还可以防止函数修改传入的数据。传递指针的指针可以让参数指针指向不同的内存地址,栈和堆可以帮助深入解释这些功能。我们也初步理解函数指针**,这类指针允许应用程序根据需要执行不同的函数,对于控制应用程序内的执行序列很有用**。

一、程序的栈和堆

  • 局部变量也称为自动变量,它们总是分配在栈帧上。
  • 堆区存放用户手动申请的的内存数据。

两者的区别

1、随着程序运行,栈区的内存可能会频繁的初始化和释放
2、堆区的内存除了用户手动申请释放,否则不会存在内存大小的变化

1、程序栈

程序栈是支持函数执行的内存区域,通常和堆共享。也就是说,它们共享同一块内存区域。程序栈通常占据这块区域的下部,而堆用的则是上部。
下图就简单说明程序栈堆的模型
指针与函数_第1张图片
调用函数时,函数的栈帧被推到栈上,栈向上“长出”一个栈帧。当函数终止时,其栈帧从程序栈上弹出。可以理解为直到main函数从程序栈里弹出后,程序就结束了。

  • 栈帧所使用的内存不会被清理,但最终可能会被推到程序栈上的另一个栈帧覆盖

有时候我们听到的一个术语:栈溢出

可以理解为程序栈一直压栈,导致程序的的内存需要超过了真实物理内存,然后程序奔溃;我了解的情况就是有几个:

  • 1、递归函数的递归条件错误,导致一直递归,一直无限调用递归函数
  • 2、存在调用函数的死循环

示意图:

指针与函数_第2张图片

2、栈帧的组织

栈帧由以下几种元素组成。

  • 返回地址
    函数完成后要返回的程序内部地址。

  • 局部数据存储
    为局部变量分配的内存。

  • 参数存储
    为函数参数分配的内存。

  • 栈指针和基指针
    运行时系统用来管理栈的指针。(我们写代码不用管,系统帮我们来完成)栈指针通常指向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素。这两个指针都不是C指针,它们是运行时系统管理程序栈的地址

注意要点:

  • 系统在创建栈帧时,将参数以跟声明时相反的顺序推到帧上,然后是返回地址,最后推入局部变量,
float average(int*arr,int size){
	int sum = 0;
	printf("arr:%p\n",&arr);
	printf("size:%p\n",&size);
	printf("sum:%p\n",&sum);
	for(int i=0;i<size;i++)
	{
		sum+=arr[i];
	}
	return(sum*1.0f)/size;
}

这个代码中,在函数栈里分配空间顺序是: size–>arr–>返回计算值地址–>sum.

二、通过指针传递和返回数据

1、传递:就是指函数参数是指针,传递的内容是地址
2、返回:就是函数返回值是数据,传递的内容是地址或者数据值

指针传递的优点:

  • 传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。这意味着只有需要访问这个对象的函数才有访问权限,而且也不需要复制对象。
  • 要在某个函数中修改数据,需要用指针传递数据。通过传递一个指向常量的指针,可以使用指针传递数据并禁止其被修改。
  • 传递参数(包括指针)时,传递的是它们的值。也就是说,传递给函数的是参数值的一个副本。当涉及大型数据结构时,传递参数的指针会更高效。比如说一个表示雇员的大型结构体,如果我们把整个结构体传递给函数,那么需要复制结构体的所有字节,这样会导致程序运行变慢,栈帧也会占用过多内存。传递对象的指针意味着不需要复制对象,但可以通过指针访问对象。

总的来说:为了效率、安全、方便

1、用指针传递数据

函数传递的是地址,调用的函数作为处理手段,函数直接修改地址对应的数据值(用指针接引 *pi = 值)。颇有单例模式的味道。

//交换两个变量的值;传入指针,函数对指针进行接引;
//从而获取到指针对应的数据,进而操作数据
void swapwithPointers(int*pnum1,int*pnum2)
{
	int tmp;
	tmp=*pnum1; 
	*pnuml=*pnum2;
	*pnum2=tmp;
}

// *pnum1  就是接引指针,在main中调用时,其接引值是 n1=5
// *pnum2  就是接引指针,在main中调用时,其接引值是 n2=10
int main()
{
	int n1 = 5;
	int n2=10;
	swapwithPointers(&n1,&n2);
	return 0;
}

下图是函数运行的程序栈示意图
指针与函数_第3张图片

2、用值传递数据

函数用值传递数据传递的值是一个实参的副本。
用值传递数据一般只为获得一个特定返回值或者是函数的控制参数

void swapwithPointers(int pnum1,int pnum2)
{
	int tmp;
	tmp = pnum1; 
	pnuml = pnum2;
	pnum2 = tmp;
}
//程序并不能交换数据,
int main()
{
	int n1 = 5;
	int n2=10;
	swapwithPointers(n1,n2);
	return 0;
}

下图是函数运行的程序栈示意图
指针与函数_第4张图片

3、传递指向常量的指针

指向常量的指针,就是为了 保护指针指向的数据不被修改,设置为只读模式

  • 强烈建议:只读的指针一定要声明为 const
void passingAddressofConstants(const int*numl, int*num2)
{
	*num2 = *num1;
}

void passingAddressofConstantsTest(const int*numl, int*num2)
{
	*num2 = 100;
	//这里编译报错,说num1是const不能修改;
	*num1 = 200;
}
int main()
{
	const int limit = 100;
	int result = 5;
	passingAddressofConstants(&limit,&result);
	
	//使用会报错
	passingAddressofConstantsTest(&limit,&result);
	return 0;
}

4、返回指针

返回指针很容易,只要返回的类型是某种数据类型的指针即可。从函数返回对象时
经常用到以下两种技术。

  • 使用malloc在函数内部分配内存并返回其地址。调用者负责释放返回的内存。
    【数据结构经常用】
  • 传递一个对象给函数并让函数修改它。这样分配和释放对象的内存都是调用者的责任。
    【可以理解为,一个内存数据,需要多种函数来计算这个数据;通过一个地址,函数从不同方面来计算,并把这个地址返回去,以便下一个函数接龙计算,直到计算完拿到我们需要的,就把这个内存给释放;这个函数调用的流程都是由我们程序员来设计的,此时的地址就有点儿像Windows的句柄韵味,反正是标识数据用的】
//申请size x int类型的大小,并用value值初始化的堆区数组
int* allocateArray(int size, int value)
{
	int*arr=(int*)malloc(size*sizeof(int));
	for(int i=0;i<size;i++)
	{
		arr[i]=value;
	}
	
	 return arr;
}

//调用部分
int*vector=allocateArray(5,45);
//这个vector其实我们就可以拿着去干其他事儿
for(int i=0; i < 5; i++)
{
	printf("%d\n",vector[i]);
}

下图是函数运行的程序栈示意图
指针与函数_第5张图片

尽管上例可以正确工作,但从函数返回指针时可能存在几个潜在的问题,包括:

  • 返回未初始化的指针;
  • 返回指向无效地址的指针;
  • 返回局部变量的指针;
  • 返回指针但是没有释放内存。

前三种其实是指针地址不合法问题;最后一种就是内存泄漏问题

5、局部数据指针

函数局部变量的特性是:函数执行完就会销毁
那返回的地址是局部变量的地址,但是地址的值是有问题的,这个值可能已经被系统给清空、或被其他函数栈给覆盖了

//申请size x int类型的大小,并用value值初始化的堆区数组
int* allocateArray(int value)
{
	int arr[20];
	for(int i=0;i<size;i++)
	{
		arr[i]=value;
	}
	
	 return arr;
}

//调用部分
int*vector=allocateArray(45);
//这里就会出问题
for(int i=0; i < 5; i++)
{
	printf("%d\n",vector[i]);
}

解决办法:

  • 1、使用指针传递法
    【好处多多:内存手动申请释放、方便扩容、开销小等】
  • 2、使用static 关键字来保存数据
    【明显问题:内存资源全局占用、不能动态扩容】

6、传递空指针

就是检查指针是否有效,初始化声明指针时一般都要置其值为:NULL

  • 将指针传递给函数时,使用之前先判断它是否为空是个好习惯
int*allocateArray(int*arr,int size,int value)
{
	if(arr!=NULL){
	for(int i=0;i&lt;size;i++)
	{
		arr[i]=value;
	}
	return arr;
}

//调用
int*vector=(int*)malloc(5·sizeof(int));
allocateArray(vector,5,45);

7、传递指针的指针

其实就是用来保存指针变量地址的地址;我们使用这样的二维指针来操作一维指针内容。
将指针传递给函数时,传递的是值。如果 我们想修改原指针而不是指针的副本,就需要传递指针的指针。

指针与函数_第6张图片

void allocateArray (int **arr, int size, int value)
{
	*arr=(int*)malloc(size*sizeof(int));
	if(*arr!=NULL)
	{
		for(int i=0;i<size;i++)
		{
			*(*arr+i)=value;
		}
	}
}

//这样调用
int *vector = NULL
allocateArray(&vector,5,45);

下图是函数运行的程序栈示意图
指针与函数_第7张图片

三、函数指针

1、声明函数指针

第一次看到声明函数指针的语法时你可能会感到迷惑。不过跟C的很多方面一样,一旦熟悉这种表示法,理解起来就顺理成章了。
下面我们声明一个函数指针,该函数接受空参数,返回空值。

void (*foo)(double, int, /*other parameters*/);

// void : 返回类型
// foo : 函数指针变量名
// double, int, /*other parameters*/ : 函数指针参数类型

  • 使用函数指针时一定要小心,因为C不会检查参数传递是否正确。
  • 所以要注意两个方面:1.是参数个数一定匹配2.是参数对应位置类型要匹配

几个例子:

  • int (*f1)(double);
    //传入double.返回int
  • void (*f2)(char);
    //传入char指针,没有返回值
  • double *(*f3)(int,int);
    //传递两个整数,返回double指针
  • int * f4(int number1)
    //这是普通函数,传递一个整数,返回整数指针

2、使用函数指针

//声明一个函数指针
int (*fptrl)(int);

//定义一个函数
int square(int num)
{
	return num*num;
}

int n=5;
fptrl=square;
// 也可以使用 取地址符
// fptrl=□
printf("%d squared is %d\n",n, fptr1(n));
  • 为函数指针声明一个类型定义会比较方便。为了方便绑定不同功能但参数一致的函数
//这样定义
typedef int (*funcptr)(int);

使用的例子:

//定义平方函数
int square(int num)
{
	return num*num;
}
//定义立方函数
int cube(int num)
{
	return num*num*num;
}

typedef int (*funcptr)(int);

funcptr fptr2;
fptr2=square;
printf("%d squared is %d\n", n, fptr2(n));

fptr2=cube;
printf("%d cube is %d\n", n, fptr2(n));

3、传递函数指针

传递函数指针很简单,只要把函数指针声明作为函数参数即可。我们会用下面这个
例子中的add、sub和compute函数来说明如何传递函数指针:

//加法
int add(int numl,int num2)
{
	return numl+num2;
}

//减法
int subtract(int numl,int num2)
{
	return numl-num2;
}

//定义一个函数指针
typedef int (*fptrOperation)(int,int);

//把函数指针当成一个变量传入
int compute(fptroperation operation,int num1,int num2)
{
	return operation(numl,num2);
}

//下面的代码片段说明如何使用这些函数:
printf("%d\n",compute(add,5,6));
printf("%d\n",compute(sub,5,6));

输出是11和-1。add和sub函数的地址被传递给compute函数,后者使用这些地
址来调用对应的操作。本例也说明了使用函数指针可以让代码变得更灵活。
我们可以自主的控制函数流。

4、返回函数指针

指针是一个变量,就可以当成函数返回值来使用
我们用下面的select函数基于输入的字符来返回一个指向对应操作的函数指针。
取决于传入的操作码,它要么返回add函数,要么返回sub函数。

//加法
int add(int numl,int num2)
{
	return numl+num2;
}

//减法
int subtract(int numl,int num2)
{
	return numl-num2;
}

//定义一个函数指针
typedef int (*fptrOperation)(int,int);

//函数指针返回
fptroperation select(char opcode)
{
	switch(opcode)
	{
		case '+': return add;
		case '-': return subtract ;
	}
}

//综合来根据计算符号计算
int evaluate(char opcode,int numl,int num2)
{
	fptroperation operation=select(opcode);
	return operation(num1,num2);
}

// evaluate函数及printf语句的用法如下所示:
printf("%d\n",evaluate('+',5,6));
printf("%d\n",evaluate('-',5,6));

输出是11和-1。

5、使用函数指针数组

函数指针数组可以基于某些条件选择要执行的函数,声明这种数组很简单,只要把
函数指针声明为数组的类型即可,如下所示。这个数组的所有元素都被初始化为
NULL。如果数组的初始化值是一个语句块,系统会将块中的值赋给连续的数组元
素。

其实就两步:
1、使用typedef把函数指针取为 别名
2、这个别名就可以看出一个变量类型,这个变量类型和基本数据类型一样的声明数组
就像int array[10]; 函数指针别名 array[10]

//这种方法更好理解,一眼能看明白
typedef int (*operation)(int, int);
operation operations [128] = {NULL};

//这个就观感不好,不太好看明白;推荐上面那种
//也可以不用typedef来声明这个数组,如下:
int (*operations[128])(int,int)={NULL};

函数指针数组我知道的就是用在 状态机编程 里比较多。我们可以通过一系列算法,把该调用的函数映射为函数指针数组的索引位置。

就如下面的例子(我们利用的是ASCIl字符的性质,该字符是特殊的int类型,就对应数组的索引)
数组初始化为NULL后,我们把add和sub函数赋给加号和减号对应的元素:

//加法
int add(int numl,int num2)
{
	return numl+num2;
}

//减法
int subtract(int numl,int num2)
{
	return numl-num2;
}

typedef int (*operation)(int, int);
operation operations [128] = {NULL};

//也可以不用typedef来声明这个数组,如下:
int (*operations[128])(int,int)={NULL};

void initializeOperationsArray()
{
	operations['+']=add
	operations ['-'] = subtract;
}

int evaluateArray ( char opcode , int numl , int num2 )
{
	fptroperation operation;
	operation=operations[opcode];
	return operation(numl,num2);
}

//用下面的代码测试这些函数:
initializeoperationsArray();
printf("%d\n",evaluateArray('+',5,6));
printf("%d\n",evaluateArray('-',5,6));

执行结果是11和-1。更健壮的evaluateArray函数版本需要在执行函数之前检
查空指针。

6、比较函数指针

一般就是配合函数指针数组进行使用,就是看计算的函数索引值会不会越界。
同时,数组索引的功能,函数指针数组一样都是支持的。【支持等于,数组位置大小比较之类的】

fptroperation fptrl=add;
if(fptrl==add)
{
	printf("fptrl points to add function\n");
} else 
{
	printf("fptrl does not point to add function\n");
}

7、转换函数指针

1、我们可以将指向某个函数的指针转换为其他类型的指针,不过要谨慎使用,因为运行时系统不会验证函数指针所用的参数是否正确。
2、也可以把一种函数指针转换为另一种再转换回来,得到的结果和原指针相同,但函数指针的长度不一定相等。

总之需要慎重使用。

解决参数不同的情况时,我们可以统一定义一个结构体作为通信的包
就像如下例子

typedef struct 
{
	int operationOne;
	int operationTwo;
	/*
	其他结构体,其他数据
	*/
} Parameters; 

//加法
int add(Parameters numbers)
{
	return Parameters.operationOne + Parameters.operationTwo;
}

//减法
int subtract(Parameters numbers)
{
	return Parameters.operationOne - Parameters.operationTwo;
}

typedef int (*operation)(Parameters);

operation ptrFun;
Parameters operations = {11, 23};
ptrFun = subtract;
printf("subtract outcome: %d", ptrFun(operations));


你可能感兴趣的:(#,▶,C指针,c语言,内存管理,函数指针,指针,编程语言)