指针重难、易错点

(一)函数指针

在前边的blog中,已经整理归纳了数组和初级指针,接下来,我来继续整理高级指针和如何正确使用指针。

我们说过,指针数组是一个数组,每个元素是指针;数组指针是个指针,指向的是数组。所以:

函数指针就是指向函数的指针。我们先看以下代码:

<pre name="code" class="cpp">void fun()
{
}
int main()
{
printf("%p",fun);
printf("%p",&fun);
printf("%p",fun+1);
printf("%p",&fun+1);
fun();
(*fun)();
}

我们来分析以下这个程序运行出来的结果是什么。这个程序如果你拿到自己的编译环境下测试,是编译不通过的。函

数名被编译之后其实就是一个地址,所以最后两句代码作用一样。对函数名加1的操作编译不通过。

注意区分一下两句代码:

void *p(int,char);//这是一个函数,返回值类型void*

void(*q)(int,char);//定义了函数指针变量q,q指向的函数有两个参数,int和char,返回值void

我们之前说过,给定数组指针int (*parr)[5];该数组指针的类型是int (*)[5].同样类比,函数指针变量的类型是什么(针

对上边例子的函数指针)?它的类型是void (*)(int,char);所以我们可以将上式理解为:void(*)(int,char) q;这样子是不是

是不是好理解一点呢?只是编译器不同意你这么写~~~知道这个很重要。

下边我们来看一下《c陷阱与缺陷》里的这个例子:

(*(void(*)())0)()这个表示的是什么??

void(*)(),这是函数指针类型;

(void(*)())0,将0强制转化成函数指针类型

(*(void(*)())0),取0开始的一段内存里边的内容,其内容就是保存在首地址为0的一段区域内的函数;

(*(void(*)())0)(),这就是对这个函数的调用。

再看下边一个例子:

(*(char **(*)(char**,char**))0)(char**,char **)表示什么呢??

char **(*)(char**,char**)这是一个函数指针类型;

*(char **(*)(char**,char**))0,将0强制转化成函数指针类型,并解引用找到该内存的内容,即就是一个函数。

这个函数的参数有两个,char**,char**。

继续下边一个例子:void (*signal(int,void(*)(int)))(int);

分析:signal是一个函数,有两个参数,int型和void(*)(int),返回值是一个函数指针,该函数指针指向的函数参数为int

型,返回值为void。

由于void(*)(int)的多次出现,我们可以使用typedef关键字简化,如下:

typedef void(*)(int)  p;//我们可以这样理解,但是不可以这样对编译器~~

typedef void(*pfun)(int);

所以根据上边的分析,原式子就可以简化写为:pfun signal(int,pfun);

函数指针数组:这是一个数组,数组里的每一个元素都是指向函数的指针。

void (*parr[3])(int);这就是函数指针数组,如果看不出来我们这样来:void (*)(int)  parr[3];这样就可以理解了,只是编

译器不同意我们这样~~

函数指针数组指针:原型举例:void (*(*parr)[3])(int)  ;可以理解为:void
(*)(int)  (*parr)[3];

写了这么多,貌似我们还没有了解函数指针可以用来干什么。其实,函数指针可以用于很多地方。当我们需要对一组

整形数排序时,我们可以用最基本的冒泡排序(虽然效率不是很高)来实现,可是,如果我要求你排序结构体时,你

是不是又要写一个函数去被调用,我们发现,这两个函数基本是一样的,所以,我们可以直接用回调函数来实现~~

下边我们就用回调函数实现排序各种类型的数~~

<pre name="code" class="cpp">int compare(const void *elem1, const void *elem2)
{
	return *(const int *)elem1 - *(const int *)elem2;
}
void sort(void *base, unsigned int num,
 unsigned int byte, int(*cmp)(const void *elem1, const void *elem2))
{
	char *pbase = (char *)base;
	int flag = 0;
	int i = 0;
	int j = 0;
	int k = 0;
	const void *p1 = NULL;
	const void *p2 = NULL;
	for (i = 0;i < num - 1;i++)
	{
		flag = 0;
		for (j = 0;j < num - 1 - i;j++)
		{
			p1 = (const void *)(pbase + j*byte);
			p2 = (const void *)(pbase + (j + 1)*byte);
			int ret = cmp(p1, p2);
			if (ret > 0)
			{
				for (k = 0;k < byte;k++)
				{//交换秘诀
					pbase[j*byte + k] = pbase[j*byte + k] + pbase[(j + 1)*byte + k];
					pbase[(j + 1)*byte + k] = pbase[j*byte + k] - pbase[(j + 1)*byte + k];
					pbase[j*byte + k] = pbase[j*byte + k] - pbase[(j + 1)*byte + k];
				}
				flag = 1;
			}
			if (flag == 0)
				break;
		}
	}
}
int main()
{
	int(*cmp)(const void *elem1, const void *elem2) = compare;
	int arr[4] = { 6,7,4,2 };
	int i = 0;
	sort(arr,4,4,cmp);
	for (i = 0;i < 4;i++)
	{
		printf("%d ",arr[i]);
	}
	system("pause");
	return 0;
}

交换两个数有多种方法,创建临时变量,想必大家都知道~而不创建临时变量的交换,我们来归纳一下:

比如,交换整形数a和b:

方法一:创建临时变量;

方法二:利用加减的方法;

代码:

a = a+b;
b = a-b;
a = a-b;

方法三:利用乘除的方法;

a = a *b;
b = a/b;
a= a/b;

方法四:异或的方法:

int a= 3;//011
int b = 5;//101
a = a^b;//110
b=a^b;//011
a = a^b;//101

在《剑指offer》这本书中又看到这样一道题:将一个数组中的所有奇数调到所有偶数之前。我们用指针或者数组的方

法,都可以实现~下边给出指针的方法~~

void swap_odd_even(int arr[], int n)
{
	if (n <= 1)
		return;
	int *pstart = arr;
	int *pend = arr + n - 1;
	int tmp = 0;
	while (pstart < pend)
	{
		while (pstart < pend && *pstart % 2 == 1)//如果是奇数,继续向后遍历
		{
			pstart++;
		}
		while (pstart < pend && *pend % 2 == 0)//如果是偶数,就向前遍历
		{
			pend--;
		}
		tmp = *pstart;
		*pstart = *pend;
		*pend = tmp;
	}

}
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int i = 0;
	swap_odd_even(arr,10);
	for (i = 0;i < 10;i++)
	{
		printf("%d ",arr[i]);
	}
	system("pause");
	return 0;
}

看了上述代码之后,会不会有人觉得奇偶交换之后指针没有变动呢~~告诉你吧,交换之后质变指针指向奇数,右边指

针指向偶数,两个while循环依然可以执行向后遍历~~

但是,但是,如果要求我们将能被3整除的放在数组左边,不能被3整除的放在数组右边,我们是不是又要写出一个函

数呢??不用的,我们可以使用回调函数来实现~

int ope(const void *elem)
{
	return *(const int *)elem % 3;
}
void swap_odd_even(int arr[], int n, void(*p)(const void *))
{
	if (n <= 1)
		return;
	int *pstart = arr;
	int *pend = arr + n - 1;
	int tmp = 0;
	while (pstart < pend)
	{
		while (pstart < pend && ope((const void *)pstart))//如果是不能整除的数,继续向后遍历
		{
			pstart++;
		}
		while (pstart < pend && ope((const void *)pend) == 0)//如果是可以整除,就向前遍历
		{
			pend--;
		}
		tmp = *pstart;
		*pstart = *pend;
		*pend = tmp;
	}

}
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int i = 0;
	void(*p)(const void *) = ope;
	swap_odd_even(arr,10,p);
	for (i = 0;i < 10;i++)
	{
		printf("%d ",arr[i]);
	}
	system("pause");
	return 0;
}

我们只需要加上一个小小的操作函数判断一下就可以了~~

(二)指针使用过程中的易错点和难点:

1.空指针:前边模拟实现memmove,memcpy等函数时,我们就使用了空指针,希望她可以接收任意类型的数,我们

也说过,空指针不能自加,也不能解引用,但是只是在windows下,那么我们来看看在gcc环境下的情况:

代码如图:

运行通过~~~

2.野指针:野指针就是对内容不明确的指针进行解引用~

int *p;
*p =10;

p就是一个野指针,我们并不知道p里边存放着哪个变量的地址,所以解引用时,你懂~~再看下边一个例子:

struct student
{
    char *name;
    int score;
}stu,*pstu;
int main()
{
    strcpy(sty.name,"yaoyao");
    stu.score = 100;
    return 0;
}

乍一看这段代码没有任何问题,其实,我们只知道指针变量name被分配了4字节(32位系统),并不知道里边的内容

,所以复制时无效。解决办法:给那么指针动态分配空间,这里就不给出代码了,可以自己实现~

注意:函数入口时,一定要对指针变量是否为NULL进行判断,我们可以通过assert断言实现。需要注意的是,这个宏

只能在debug版本使用,release版本并不可以。原因在于:assert的一切作用就是尽可能的在调试函数的时候吧错误

定位,而不是等到release之后。assert是定位错误,不是排除错误。当程序被完整地测试完毕后,可以在预编译中通

过定义NDEBUG来消除所有的断言。assert宏所在头文件是,assert.h。

3.内存泄漏:没有释放向系统申请的内存,举例:

<pre name="code" class="cpp"><pre name="code" class="cpp">void getmemory(char *p,int num)
{
    p = (char *)malloc(num * sizeof(char));
}
int main()
{
    char *str = NULL;
    getmemory(str,10);
    strcpy(str,"hello");
    free(str);
    return 0;
}


这个程序运行后会崩溃。在main函数中将参数str传进getmemory函数,此时p = NULL,而这个p是在getmemory的栈

帧中,str在main函数栈帧中,在函数中动态分配内存赋给p,改变了p,并不能改变str。如何更正使得程序运行通过

呢?给出两种方法:

1.将str的地址传过去。

2.将分配好的内存首地址返回。下边给出一种更正方法的代码:

char *getmemory(int num)
{
    char *p = (char *)malloc(num * sizeof(char));
    return p;
}
int main()
{
    //char *str = NULL;
    char *ret = getmemory(10);
    strcpy(ret,"hello");
    free(ret);
    ret = NULL;
    return 0;
}

注意:malloc和free要成对,就像上边那个代码,虽然成对,但是跟没成对一样。free可以释放空指针,释放完后要置

空。

不可返回局部变量的地址。上边代码返回的是局部变量,不是局部变量的地址。

看下边的例子:

int* fun()
{
    int tmp = 10;
    return &tmp;
}
int main()
{
    int *pret = fun();
    printf("%d",*pret);
    return 0;
}

这段代码或许可以运行通过,输出结果是10,那是因为fun函数的那块内存还在(没有被别人占用),要是被占用

了,程序就会运行出错,因为出了那个函数,那块空间就已经不属于那个函数,返回的地址肯定是不对的。

指针很重要,使用好指针更重要~~

文章难免存在不足,希望各位朋友指出~~

时间: 2024-10-12 12:29:05

指针重难、易错点的相关文章

C++指针二(易错模型)

规则一:Main(主调函数)分配的内存(在堆区,栈区.全局区)都可以在被调用函数里使用.如果在被调用函数里面的临时区(栈)分配内存,主调用函数是不能使用的. #include "stdio.h" #include "stdlib.h" #include "string.h" char * getstring1() { char *p1 = "abcde"; return p1; } char * getstring2() {

关于Verilog HDL的一些技巧、易错、易忘点(不定期更新)

本文记录一些关于Verilog HDL的一些技巧.易错.易忘点等(主要是语法上),一方面是方便自己忘记语法时进行查阅翻看,另一方面是分享给大家,如果有错的话,希望大家能够评论指出. 关键词: ·技巧篇: 组合逻辑输出类型选择; 语法上的变量交换; ·易忘篇: case/casex/casez语句; 循环语句: 数制和操作符: 数据类型: ·易错: 技巧篇: 1.组合逻辑输出:描述一个纯组合逻辑电路时,尽量不要把输出定义成输出类型,例如描述下面的电路: 1 module mux #(paramet

细节!重点!易错点!--面试java基础篇(一)

今天来给大家分享一下java的重点易错点部分,也是各位同学面试需要准备的,欢迎大家交流指正. 1.java中的main方法是静态方法,即方法中的代码是存储在静态存储区的. 2.任何静态代码块都会在main方法之前执行. 3.java程序的初始化顺序:原则:静态优先于非静态,且只初始化一次:父类优先于子类:按照成员定义顺序初始化.例顺序:父类静态变量,父类静态代码块,子类静态变量,子类静态代码块,父类非静态变量,父类非静态代码块,父类构造函数,子类非静态变量,子类非静态代码块,子类构造函数. 4.

细节!重点!易错点!--面试java基础篇(二)

今天来给大家分享一下java的重点易错点第二部分,也是各位同学面试需要准备的,欢迎大家交流指正. 1.字符串创建与存储机制:当创建一个字符串时,首先会在常量池中查找是否已经有相同的字符串被定义,其判断的依据是String类型equals的返回值,若已经定义,则直接获取对其的引用.此时不需要创建新的对象,如果没有定义,首先创建这个对象,然后把它加入到字符串池中,再将它的引用返回.(例:new String(”aaa“)可能创建了1个或者2个对象,如果常量池中原来有aaa那么之创建了一个对象,如果没

软考程序员新手易错笔记

1.●标准化对象一般可分为两大类:一类是标准化的具体对象,即需要制定标准的具体事物:另一类是  (4)  ,即各种具体对象的总和所构成的整体,通过它可以研究各种具体对象的共同属性.本质和普遍规律. (4) A.标准化抽象对象    B.标准化总体对象    C.标准化虚拟对象    D.标准化面向对象 [解析]:标准化对象一般可分为两类:一类是标准化的具体对象:另一类是标准化的总体对象. 2.●用二进制加法器对二-十进制编码的十进制数求和,当和大于1010时,  (5)  . (5) A.不需要

开发易错点收集

开发易错点收集 1.java 比较字符串内容是否相等,需要使用方法 boolean java.lang.String.equals(Object arg0),直接使用 == 判断的是两个串的地址是否相等. jvm加载jar是按照文件名排序后加载的,加载一次后不会重复加载.也就是说程序目录lib中同时存在 TestJar1.jar 和 TestJar2.jar,运行时加载的是TestJar1.jar < 2.javaScript ''==0 返回为true ,用全等返回false,对于喜欢用if(

Javascript易错知识点

? JS易错知识点总结: == 和 === 的区别: ==:判断两个变量的值是否相等. ===:判断两个变量的类型和值是否都相等,两个条件同时满足时,表达式为True. switch中break的作用: 如果一个case后面的语句,没有写break,那么程序会向下执行,而不会退出: 例如:当满足条件的case 2下面没有break时,case 3也会执行 1 var num = 2; 2 switch(num){ 3 case 1: 4 alert('case 1'); 5 break; 6 c

黑马程序员---C基础3【变量的易错】【程序结构】【if语句】【Switch语句】

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- [变量的易错] 1.变量为什么要初始化为0 int  sum,a=3: sum = sum+a 如果未初始化则会成为一个不确定的变量,结果也会不确定,容易出错. 2.不同类型的变量之间的转换 切记int  a=1,b=0:b=1-1.5:其中b为一个整型所有结果是保留整数部分的0,而不是-0.5,又因为0没有正负之分,所有保存结果为b=0: 3.关于Xcode的一个快速注释的插件 快捷键://

【C++基础】指针好难啊,一点点啃——基本概念

指针保存的是另一个对象的地址(概念真的很重要!!) int a=1; int *ptr = &a;//*定义一个指向int类型的指针ptr, &a取变量a的地址 一.指针的初始化 几个概念,迷途指针==野指针==悬浮(dangling)指针==失控指针 当对一个指针delete后,会释放原来的内存,但指针变量名依然存在,值却没有设置为NULL或0,这就相当于声明一个指针时没有初始化, 当再次使用该指针时,则会出现严重错误(崩溃是肯定的,本人还没体会到多严重),以上是我的理解 原则:不能使用