13. 再论C语言中的指针

指针与数组

数组名与指针

数组作为函数的入口参数

函数参数为数组时,传递的是数组的首地址,而不是数组本身。
在函数的内部,用来表示数组的形参已经退化为一个局部指针变量,通过这个指针变量,函数可以访问数组的元素。
由于数组会马上蜕变为指针,数组事实上从没有传入过函数。允许指针参数声明为数组只是为了让它看起来像传入了数组,因为该参数再函数在可能会当做数组使用。

#include <stdio.h>

char b[10]="123456789";

void f(char *a)
{
	char c;
	a++;  //因为a是局部指针变量,可以进行运算
    c = a[4];
	printf("%s ", a);  //结果:23456789
	printf("%d ", sizeof(a));  //结果:4,a是指针
	printf("%c ", c);  //结果:6
}
int main()
{
    printf("%s ", b);  //结果:123456789
    printf("%d ", sizeof(b));  //结果:10,10*1
    f(b);
    return 0;
}

字符串数组与指向字符串的指针

C中没有字符串类型,所以构建原始的字符串,2中方法:字符串数组、字符串指针。
char *p = “hello world”;
char a[] = “hello world”;
在这里插入图片描述
采用字符串指针,会将声明的常量集中存放在特殊的内存区域(常量池),并将这个字符串的首地址赋值给指针变量p。关于存放在常量池中的字符串常量有两点需要注意:
第一:常量池的内容一般被存放在只读的内存区域,比如ROM或Flash,不能对该块区域的内容进行修改。这就是为什么表达式p[0] = 'H’在很多系统中都不能正确被执行的原因。
第二:存放在常量池中的字符串常量,不会被编译器分配相应的“符号”(Symbol),一旦p的值被赋值为其他值,原来这个字符串首地址将会永远丢失,即是这个字符串常量将永远丢失。

函数指针

函数指针的声明与引用

函数指针即指向函数地址的指针。利用该指针可以知道函数在内存中的位置,也可以利用该指针调用函数。
声明函数指针:
<类型> (*函数指针变量名) (函数参数列表);
比如对2个变量声明,
第一个是函数指针fp,该函数指针变量指向一个入口参数为整数,返回值为另一个整数的函数。

int (*fp)(int);

第二个是函数指针数组fp_array[],该数组的每个元素都是一个函数指针,该函数指针指向入口参数为一个整数,返回值为指向整数的指针的函数。

int *(*fp_array[10])(int);

在C中,函数名就是函数的入口地址,因此可以用已定义的函数名作为初值,赋值给一个相应的的函数指针。“相应的”是指函数指针和函数的返回值类型和入口参数一致。程序员可以通过函数指针调用函数,但要保证函数指针有初值,也就是该函数指针指向了某个具体的函数。

int *myfunction(int);
int *(*fp)(int);
int *ptr;

fp = myfunction;  /*fp赋初值,指向函数myfunction */
ptr1 = (*fp)(3);   /* 通过函数指针调用函数,与myfunction(3)效果一样 */
ptr2 = fp(4);  /*通过函数指针调用函数,与(*fp)(4)效果一样 */

函数总是通过指针进行调用的,因此所有“真正的”函数名总是隐式地退化为指针。

函数指针的作用

多态

多态指用一个名字定义不同的函数,这个函数执行不同但类似的操作,从而“一个接口,多种方法”。
例如实现一个计算器的程序,该程序将用户输入的2个操作数执行某一运算并得到结果。一般采取switch…case…语句来处理不同的运算符:
但是这会变得很冗长,而且switch…case…语句是通过判断来进行分支处理的,处在这个语句最后的子句最后被判断,会造成效率下降。可以通过函数指针来实现同样的功能:

double add(double, double);
double sub(double, double);
...
double (*oper_func[])(double, double) = {add, sub, ...};  /* 声明函数指针数组oper_func[] */
...
result = oper_func[oper](opt1, opt2);

首先定义一个函数指针数组oper_func[],这个数组中存放相应函数的入口地址,然后通过数组下标索引,就可以进行相应函数的调用。
利用函数指针实现多态是很多系统软件常用的方法,比如操作系统为了能够支持不同的硬件的统一管理,定义一个内部的数据结构,这个结构中定义了具体硬件操作函数的函数指针。针对不同的硬件设备,这些函数指针指向不同的操作函数。当上层软件需要访问某个设备时,操作系统将根据这个数据结构调用不同的操作函数。这使得底层的操作函数各不相同,但上层的软件却可以统一。

struct file_operations{
	struct module *owner;
	loff_t (* llseek)(struct file *, loff_t, int);
	ssize_t(* read)(struct file *, char *, size_t, loff_t *);
	ssize_t(* write)(struct file *, const char *, size_t, loff_t *);
	int(* readdir)(struct file *, void *, filldir_t);
	unsigned int(* poll)(struct file *, struct poll_table_struct *);
	int(* mmp)(struct file *, struct vm_area_struct *);
	int(* open)(struct inode *, struct file *);
	...
	#ifdef MAGIC_ROM_PTR
		int (* romptr)(struct file *, struct vm_area_struct *);
	#endif  /* MAGIC_ROM_PTR */
};

上面结构的每个成员名字都对应一个系统调用。当用户进程利用系统调用操作设备文件时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给函数。这就是Linux设备驱动程序的基本原理。因此,编写设备驱动程序的主要工作就是编写这些具体的子函数,并赋给file_operations的各个域。

回调

回调指操作系统调用用户写的函数,底层函数调用上层函数。
由于操作系统的代码在用户写的函数之前就编译完成了,所以要回调,一般都必须通过用户编写函数的函数指针传递给操作系统,再由操作系统实现回调。事件驱动的系统经常通过函数指针来实现回调机制,例如Win32的WinProc就是一种回调,用来处理窗口的信息。

多线程

C的内存陷阱

程序员在C中获得存储空间4种方式:

  1. 从静态存储区分配:全局变量、static变量
  2. 在栈(Stack)上创建:函数局部变量,效率高,内存容量有限
  3. 从堆(Heap)上分配,也叫动态内存分配:malloc/free,new/delete
  4. 空间的存储器空间。C程序中将其绝对地址通过强制类型转换显式转换成一个指向特定类型的指针,对这些存储空间访问。比如直接将常量的系统数据,如拼音库、国标码GB到Uni-code转换表等烧结在Flash或ROM中的特定地址。可以避免每次调试是下载到目标系统中。

局部变量

局部变量,要么在CPU的通用存储器中,要么在堆栈中。

char *DoSomething(...)
{
	long i[32*1024];
	memset(i, 0, 32*1024);
	...
	return i;   
}

上面有个错误,return i;试图返回数组i的首地址。局部数组所占用的内存是由编辑器分配在堆栈中的,当函数返回时,堆栈空间将对添加的代码进行退栈操作,原来存放在堆栈中的数据就变成了无效数据。因此,返回的指针将指向一块无效的堆栈空间,任何通过这个指针对其所指的内容进行访问都是无效的。

void DoSomething(...)
{
	int i;
	int j;
	int k;
	memset(&k, 0, 3*sizeof(int));
}

上面代码目的是将i,j,k清零。但是有错:
首先要能正确运行满足两个条件:

  1. i, j, k连续存放在堆栈中,不能存放在CPU内部的寄存器中
  2. 处理这3个变量的压栈顺序是先压i,再压j,最后压k
    上面第一个,C编译器会优先将局部变量优化在内部存储器中,如果k存放在寄存器中,那么&k就是非法的,因为绝大多数系统寄存器是没有地址的,至少不是和存储器同一编址的

总结,C中关于局部变量需要注意:

  1. 不要对临时变量作取地址操作,因为不确定编译器是否将变量映射到寄存器中。
  2. 不要返回临时变量的地址或临时指针变量,因为出了函数,堆栈中的局部变量就没有意义了。
  3. 临时变量数组不要过大,因为堆栈容量有限,一般几kb空间

动态存储区(Heap)的申请

假设要申请一块存储器来存放LCD屏幕上一个矩形区域的背景图形。该矩形区域的宽为x,高位y,那个这个矩形区域中一共有xy个像素点(pixel),采用4级灰度LCD。每个像素需要使用2个比特来表示,一共要xy*2/8个字节的存储器来保存。代码如下

char *buffer;
buffer = (char *)malloc(x*y*2/8);

上面代码乍一看没有问题,但C中整数除法结果只计商,直接舍弃余数。那么如果x*y如果不能被4整除,则发生数据溢出。
解决办法是:

char *buffer;
buffer = (char *)malloc((x*y*2+7)/8);

内存泄漏

char * DoSomething(...)
{
	char *p, *q;
	if ( (p = (char *)malloc(1024)) == NULL )  return NULL;
	if ( (q = (char *)malloc(2048)) == NULL )  return NULL;
	...
	return p;
}

上面代码有错误,正确的为

char * DoSomething(...)
{
	char *p, *q;
	if ( (p = (char *)malloc(1024)) == NULL )  return NULL;
	if ( (q = (char *)malloc(2048)) == NULL )  
	{
		free(p);
		return NULL;
	}
	...
	return p;
}

注意,上面return p;是没有错的。p和q是两个指针变量,中间存放的是2个地址,通过malloc()保存在p,q中,不是动态内存本身。当函数退出后,p和q会消亡,但它们指向的动态存储器不会自动释放。

“野”指针

“野”指针指不知道指向什么内容或指向的内容已经无效的指针。空指针“NULL”不是野指针。
产生“野”指针的原因:
(1)指针在初始化之前,直接被引用。这个问题主要针对局部变量。因为全局变量被静态分配存储空间,或者以程序中的初值(或0)对其进行初始化。而编译器要么用CPU通用寄存器表示局部变量,要么采用堆栈空间表示局部变量。无论哪种,局部变量的初值都是随机的,对于指针局部变量而言,这意味着没有用初值初始化的这个指针可能指向任何地方——“野指针。
(2)如果一个合法指针p的指向内存空间被释放了(free),但是这个指针的值(被释放内存空间的地址)没有被置为NULL。如果在通过这个指针p继续访问这块已经释放的内存空间是,是非常危险的。
(3)返回局部变量的指针。一旦离开函数,局部变量占用的堆栈空间将被退栈,其所表示的局部变量不复存在。因此返回这些局部变量的指针是没有意义的。

规避动态存储器的内存陷阱

(1)使用指针之前,检查指针是否为NULL。如果指针p是函数参数,在函数的入口处用assert(p!=NULL)进行检查,如果是malloc()函数申请内存,用if(p==NULL)或if(p!=NULL)处理。
(2)分配成功的动态存储区需要将其初始化后再使用。
(3)malloc()函数申请内存空间后,要保存好这个指针,记得释放它。
(4)对于被释放的动态内存,最好立刻将这项这块内存区的指针变量赋值为NULL。(free释放后,内存空间会被释放,但是其内容依然保持着原来的值)

int t 定义整型变量

int *p:p为指向整型数据的指针变量。

int a[n]:定义整型数组a,它有n个元素。

int *p[n]:定义指针数组p,它由n个指向整形数据的指针元素组成。

int (*p)[n]:p为指向含n个元素的一维数组的指针变量。

int f():f为返回整型函数值的函数。

int *f():p为返回一个指针的函数,该指针指向整型数据。

int (*f)():p为指向函数的指针,该函数返回一个整形值。

int **p:p是一个指针变量,它指向一个指向整形数据的指针变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值