C程序设计语言——指针

一. 指针概述

1. 了解内存空间
  • 内存中每个字节的编号就是我们常说的内存地址,是按一个字节接着一个字节的次序进行编址。

  • 0x 开头代表以十六进制来表示的意思。

  • 1个内存地址只存1个字节 (Byte)数据;
    C程序设计语言——指针_第1张图片

  • 内存给数据类型地址分配如下:

      char:占一个字节分配一个地址;
      int: 占四个字节分配四个地址;
    
  • *的三种用法

  1. 乘法

  2. 定义指针变量

     int *p;//定义了一个名字叫p的变量,int *表示p只能存放int变量的地址
    
  3. 指针运算符
    &的意思是获取一个变量的地址,它的操作数必须是变量
    *的意思是读一个地址指向的内容
    这两者是一对逆运算符

     该运算符放在已经定义好的指针变量的前面,
     如果p是一个已经定义好的指针变量,
     则*p表示以p的内容为地址的变量
    
指针热身举例1
#include 

int main(void)
{
	int * p;	//p是变量的名字,int * 表示p变量存放的是int类型变量的地址 
	int i = 3;	
	
	p = &i;		//OK
//	p = i;		//error;因为类型不一致,p只能存放int类型变量的地址,不能存放int类型变量的值
//	p = 55;		//error;原因同上 
	
	return 0; 
} 

图示:
C程序设计语言——指针_第2张图片

二. 指针的重要性

  • 表示一些复杂的数据结构

      对做一些系统需要:把现实的问题先存储为数据->操作数据->输出数据
      数据结构研究的就是:数据存储和数据操作
    
  • 快速的传递数据,减少了内存的耗用

  • 使函数返回一个以上的值

    • 如何通过被调函数修改主调函数普通变量的值

        1. 实参必须为该普通变量的地址
        2. 形参必须为指针变量
        3. 在被调函数中通过 *形参名 = .... ;的方式就可以修改主调函数相关变量的值
      
  • 能直接访问硬件

  • 能够方便的处理字符串

  • 是理解面向对象语言中引用的基础

总结:指针是C语言的灵魂

三. 指针的定义

定义指针变量的一般形式:类型名 * 指针变量名

类型名:表示指针变量所指向变量单元的数据类型(也就是表示确定几个字节为一个单元)
指针本身的大小就是8个(或4个)字节,有编译器所占位数决定
指针变量只存放的是其他类型变量的第一个字节的地址

例如:int * p;

地址
1. 内存单元的编号
2. 从零开始的非负整数
3. 范围:0~4G-1(FFFFFFFF)(原来内存条大小)

注:内存的编号是唯一的,但是内存里面存储的内容可以充分,不同的解读含义是不同的,(或解读为地址,或解读为变量的内容)

C程序设计语言——指针_第3张图片

指针
  1. 指针就是地址,地址就是指针

  2. 指针变量就是存放内存单元编号的变量,或者说指针变量就是存放地址的变量

  3. 指针和指针变量是两个不同的概念

  4. 但是要注意:通常我们叙述时会把指针变量简称为指针,实际上他们的含义不同

  5. 指针的本质就是一个操作受限的非负整数

  6. 指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址

     指针不能乘除,只能相减。
    
拿普通变量跟指针变量做比较:
char a;   // 定义一个变量a,用于保存char类型的数据;
char * b; // 定义一个指针变量b,用于保存一个内存地址,这个内存地址上的数据必须是char类型的。
给指针变量进行赋值:
#include
int main ()
 {
    char a = 5;      // char 类型占一个字节; 
    char * b = &a;    // “&”是取变量的地址,取出a在内存中的地址;
                   	 // 赋值给b指针,此时b变量存储的就是a地址。
    printf("我是a变量的值:%d\n",*b);        // *b表示输出b里面存储的地址上的数据; 
    										// 证明b上存储的是a的地址;
    printf("我是a变量的地址:%p\n",&a);
    printf("我是b变量的值:%p\n",b);
	return 0;
}

结果:
在这里插入图片描述
通过图示来理解:
C程序设计语言——指针_第4张图片

指针类型的概念

我们知道char类型的数据只占一个字节,有很多类型是需要多个字节来存储的,像int类型的数据就需要四个字节来存储(根据平台不同,长度也有可能不一致)。

对于int类型的指针从当前字节(地址)开始共四个字节(地址)都是属于该变量的值, 而对于char类型则只表示当前字节(地址)。代码如下:

#include 

int main(void)
{
	int a = 259;
	int * p1 = &a;
	char * p2 = (char *)&a; // 这里需要强制转换一下类型
	printf("*p1=%d, *p2=%d\n",*p1,*p2);
	
	//验证一下它们存储地址
	printf("&a = 0x%p\n",&a);
	printf("p1 = 0x%p\n",p1);
	printf("p2 = 0x%p\n",p2);
} 

/*
结果为: 
*p1=259, *p2=3
&a = 0x000000000062FE0C
p1 = 0x000000000062FE0C
p2 = 0x000000000062FE0C
*/ 

通过图示来理解:
C程序设计语言——指针_第5张图片
解释:
因为计算机是使用二进制来表示数字的,上面(259)十进制转换二进制是 [1 00000011],由于一个int类型变量占用四个字节,8位二进制为一个字节,补齐高位的0后,
则为 [00000000 00000000 00000001 00000011],
每8位二进制(一个字节)换算为十进制,则 [0  0  1  3]。

但是内存地址中有个概念叫"大小端模式",就会有两种不同的排序:[0  0  1  3] or [3  1  0  0]。

四. 指针的分类

1. 基本类型指针【重点】
  1. 举例:

    #include 
    
    int main(void)
    {
    	int * p;	//p是变量的名字,int * 表示p变量存放的是int类型变量的地址 
    				//int * p;不表示定义了一个名字叫做*p的变量
    				//int * p;应该这样理解:p是变量名,p变量的数据类型是int *类型
    				//所谓int *类型就是存放int变量地址的类型 
    	int i = 3;	
    	int j;
    	
    	p = &i;
    		/*
    			1. p保存了i的地址,因此p指向i
    			2. p不是i,i也不是p,更准确的说:修改p的值不影响i的值,修改i的值也不影响p的值 
    			3. 如果一个指针变量指向了某个普通变量,则*指针变量就完全等同于普通变量
    				例子:如果p是个指针变量,并且p存放了普通变量i的地址,则p指向普通变量i, 
    					  *p,就完全等同于i,或者说:在所有出现*p的地方都可以替换成i,所有出现i的地方都可以替换成*p 
    				
    			*p就是以p的内容为地址变量 
    		*/ 	
    	j = *p;		//等价于j = i; 
    	printf("i = %d, j = %d\n", i, j); 
    	
    	return 0; 
    } 
    
  2. 指针的常见错误
    举例一:

    #include 
    
    /*
    程序没有语法错误,但是一旦运行就会崩溃
    原因:*p代表以p的内容为地址的变量
    	但是指针变量p里面存储了垃圾值
    	那么P就是有指向的变量
    	*p就是以p里面这个垃圾值为地址的单元,那么*p就是我们不知道的单元(非法单元)
    	然后将i=5赋值给了一个不知道单元
    	但是程序就分配了两块空间,p和i
    	但是我们使用了别的单元,就会出现错误
    总结:我们编程就是和内存打交道,我们系统给我们分配了多少空间,我们就只能使用这些空间,如果使用了其他不属于我们的空间
    	系统就应该要报错,如果不报错说明这个可能是病毒(强行读取或者更改其他程序的内存空间)
    	我们一般在编译器中出现这种情况的话,编译器直接就会强行终止我们的程序,用来保护我们的系统 
    */
    int main(void)
    {
    	int * p;	//等价于int *p;也等价于int* p;
    	int i = 5;
    	
    	*p = i;
    	printf("%d\n", *p);
    	
    	return 0;	
    } 
    

    举例二:

    #include 
    
    int main(void)
    {
    	int i = 5;
    	int * p;
    	int * q;
    	
    	p = &i;
    //	*q = p;		//error 语法编译会出错,类型不一致
    //	*q = *p;	//error 编译没有出错,但是q指向的是以垃圾值为地址的变量,所以*q不能被赋值
    //	p = q;	
    				/*
    				  error 编译没有出错,q是垃圾指,q赋值给p,p也是垃圾指,
    				  q可以是垃圾值, 因为q的空间是程序可以控制的,是属于本程序的,本程序可以读写里面的垃圾值 
    				  但是*q代表的是一个你不知道的单元,*q所代表的内存单元的控制权限没有分配给本程序,
    				  所以本程序没有权限,你不能读、写里面的内容 
    				  运行到printf语句就会出错 
    				*/
    	printf("%d\n", *q);
    	
    	return 0;	
    } 
    
  3. 经典指针程序_互换两个数字

    #include 
    
    /*
    不能完成互换功能
    因为当主函数运行到exchange进入子函数,系统为子函数又分配了另外两个a,b存储空间
    在子函数中交换了a,b的值,但是当子函数运行完,系统释放了子函数的存储空间,并且也没有返回任何值
    当程序回到主函数,主函数的a,b的值任然不变,主函数运行完,系统才释放主函数的存储空间。 
    */
    void exchange(int a, int b)
    {
    	int t;
    	
    	t = a;
    	a = b;
    	b = t;
    	
    	return;
    }
    int main(void)
    {
    	int a = 3;
    	int b = 5;
    	
    	exchange(a, b);
    	printf("a = %d, b = %d\n", a, b);
    	
    	return 0;	
    } 
    
    /*
    通过以下程序可以互换两个数字
    通过指针可以改变主函数两个变量的值
    把实参&a和&b的值发送给指针变量p和q
    	等价于:p = &a ;q = &b
    	说明了p指向了a变量,b指向了b变量 
    */
    #include 
    void exchange(int * p, int * q)	//形参的名字是p和q,接受实参数据的是p和q
    {
    	int t;	//如果要互换*p和*q的值,则t必须定义为int,不能定义为int *类型 
    	
    	t = *p;	//p是int *,*q是int类型 
    	*p = *q;
    	*q = t;
    }
    
    int main(void)
    {
    	int a = 3;
    	int b = 5;
    	
    	exchange(&a, &b);
    	printf("a = %d, b = %d\n", a, b);
    	
    	return 0;	
    } 
    

    图示理解:
    C程序设计语言——指针_第6张图片

2. 指针和数组
  • 指针和一维数组

    1. 一维数组名
      定义:一维数组名是一个指针常量,它存放的是一维数组第一个元素的地址

      它的值不能被改变
      一维数组名指向的是数组的第一个元素

      理解:a <<==>> &a[0]

      //回忆数组
      #include 
      
      int main(void)
      {
      	int a[5];	//a是数组名,5是数组元素,元素就是变量 a[0]~a[4]
      	//int a[3][4];//3行4列 a[0][0]是第一个元素,a[i][j]是第i+1行,j+1列 
      	int b[5];
      	
      	//a = b;	//error,因为a是一个常量,常量不能被赋值
      
      	printf("%#X\n", &a[0]);	//以十六进制形式的输出a[0]的地址 
      	printf("%#X\n", a);		//十六进制形式的输出a的地址
      	return 0;	
      } 
      /* 
      输出结果:两者结果相同 
      ---------
      0X62FE00
      0X62FE00
      ---------
      */ 
      
    2. 下标和指针的关系
      如果p是个指针变量,则p[i] 用于等价于 *(p+i)
      a[ i ] <<==>> *( a+i )

       理解 a+2:因为a是一个地址,a+2 表示a这个地址加上2 乘以(a所指向的变量所占的字节数)
      

      举例:

      #include 
      
      /*
      a[3]是第四个元素原因:a[3]等价于*(a+3),(a+3)是第四个元素地址,所以*(a+3)就是第四个元素 
      *(a+3)等价于a[3]等价于pArr[3]等价于*(pArr+3)也就是说他们四个所代表的是同一个变量
      */ 
      
      void f(int * pArr, int len)
      {
      	pArr[3] = 88;			//pArr[3]等价于*(pArr+3) 
      							//所以在f函数里面对pArr进行操作和在主函数里面对a进行操作本质上是一样的 
      } 
      
      
      int main(void)
      {
      	int a[6] = {1,2,3,4,5,6};
      	
      	printf("%d\n", a[3]);	//结果为:3
      	f(a, 6); 
      	printf("%d\n", a[3]);	//结果为:88 
      	printf("%d\n", &a[3]);	//结果为:6487564 含义是:a[3]代表的那个变量地址的十进制表示 
      	printf("%d\n", &a[4]); 	//结果为:6487568 
      	
      	return 0;	
      } 
      

      图示理解:
      C程序设计语言——指针_第7张图片

    3. 确定一个一维数组需要几个参数(或者说:如果一个函数要处理一个一维数组,则需要接受该数组的哪些信息)
      需要两个参数:

       1. 数组第一个元素的地址
       2. 数组的长度
      

      举例:

      	# include 
      	
      	/*
      	f函数可以输出任何一个一维数组的内容
      	因为数组可以存储任何值,所以没法用某一个值当做数组结束标记的 
      	*/ 
      	void f(int * pArr, int len)	//参数是首地址和长度
      	{
      		int i;
      		
      		for(i=0; i<len; ++i)
      			printf("%d ", *(pArr+i));//	*pArr *(pArr+1) *(pArr+2)
      		printf("\n");	
      	} 
      	
      	
      	int main(void)
      	{
      		int a[5] = {1,2,3,4,5};
      		int b[6] = {6,7,8};
      		
      		f(a, 5);					//a是int *类型 
      		f(b, 6);
      		
      		return 0;	
      	} 
      	```
      
      
    4. 指针变量的运算
      指针变量不能相加 不能相乘 也不能相除
      如果两个指针变量指向的是同一块连续的空间中不同存储单元,则这两个指针变量才能相减

      p+i的值是p+i * (p所指向的变量所占字节数)
      p-i 的值是p-i *(p所指向的变量所占字节数)
      p++ < == > p+1
      p - - < == > p-1

      举例:

      #include 
      
      int main(void)
      {
      	int i = 5;
      	int j = 10;
      	int * p = &i;
      	int * q = &j;
      	
      	//这时候输出q-p是没有意义的,因为系统分配i,j空间是不连续的 
      	int a[5];
      	
      	p = &a[1];
      	q = &a[4];
      	
      	printf("p和q所指向的单元相隔%d个单元\n", q-p); 	
      	
      	return 0;	
      } 
      
    5. 一个指针变量到底占几个字节

       预备知识:
       sizeof(数据类型)
       功能:返回值就是该类型所占字节数
       例子:sizeof(int) = 4, sizeof(char) = 1
       
       sizeof(变量名)
       功能:返回值是该变量所占的字节数
      

      假设p指向char类型变量(1个字节)
      假设q指向int类型变量(4个字节)
      假设r指向double类型变量(8个字节)
      p、q、r本身所占的字节数是否一样

      举例看看:

      #include 
      
      /*
      Dev-c中运行结果为8 8 8
      说明指针变量p、q、r每个都是用8个字节来存储地址长度
      原因是:Dev-c是一个64位的编译器 
      64位说明地址线条数是64根,地址的范围就是:00000.....00000(64位)~11111.....11111(64位)
      64bit等价于8Byte,所以每个指针变量大小为8个字节
      
      */ 
      int main(void)
      {
      	char ch = 'A';
      	int i = 99;
      	double x = 66.6;
      	char * p = &ch;
      	int * q = &i;
      	double * r = &x;
      	
      	printf("%d %d %d\n", sizeof(p), sizeof(q), sizeof(r)); 
      	
      	return 0;	
      } 
      

      图示理解:
      C程序设计语言——指针_第8张图片
      C程序设计语言——指针_第9张图片

    总结:
    p、q、r都只保存了变量第一个字节的地址,所以都指向了变量的第一个字节
    但是*q、*r分别表示了i(占4个字节)和x(占8个字节)的本身,而不是第一个字节。
    这时候就需要靠变量本身的数据类型来确定它所指向的变量占几个字节

      一个指针变量无论它指向的变量占几个字节,该指针变量本身只占4个字节(或者8个字节)
      一个变量的地址使用该变量首字节地址来表示	
    
  • 指针和二维数组

3. 指针和函数
  • 如何通过被调函数修改主调函数中普通变量的值

    1. 实参为相关变量的地址
    2. 形参为以该变量的类型为类型的指针变量
    3. 在被调函数中通过 *形参变量名 的方式就可以修改主调函数中变量的值
  • 什么是函数指针
    如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

  • 指针变量怎么定义

    例如:
    int(*p)(int, int);
    

    这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即(*p);
    其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。
    所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为int类型,且有两个整型参数的函数。p的类型为 int( * )(int, int)

  • 函数指针的定义方式为:
    函数返回值类型 (* 指针变量名) (函数参数列表);

      “函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;
      “函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。
    

    我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。
    但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。
    如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

  • 如何用函数指针调用函数

    举例1
    int Func(int x);   /*声明一个函数*/
    int (*p) (int x);  /*定义一个函数指针*/
    p = Func;          /*将Func函数的首地址赋给指针变量p*/
    

    赋值时函数 Func 不带括号,也不带参数。由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了

    举例2
    # include 
    int Max(int, int);  //函数声明
    int main(void)
    {
        int(*p)(int, int);  //定义一个函数指针
        int a, b, c;
        p = Max;  //把函数Max赋给指针变量p, 使p指向Max函数
        printf("please enter a and b:");
        scanf("%d%d", &a, &b);
        c = (*p)(a, b);  //通过函数指针调用Max函数
        printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
        return 0;
    }
    int Max(int x, int y)  //定义Max函数
    {
        int z;
        if (x > y)
        {
            z = x;
        }
        else
        {
            z = y;
        }
        return z;
    }
    结果:
    --------------------------------
    please enter a and b:3 5
    a = 3
    b = 5
    max = 5
    --------------------------------
    
4. 指针和结构体
5. 指针和字符串
6. 多级指针
  • 二级指针:指针的指针

    char a = 5;    
    char * p1 = &a;
    char ** p2= &p1;
    printf("*p=%d,**p2=%d\n",*p1,**p2);   // 输出:*p1=5,**p2=5
    

    图示理解:
    C程序设计语言——指针_第10张图片

  • 多级指针举例1:

    #include 
    
    int main(void)
    {
    	int i = 10;
    	int * p = &i;
    	int ** q = &p;
    	int *** r = &q;
    	
    	//r = &p; error 因为r是int ***类型,r只能存放int **类型的变量的地址
    	printf("i = %d\n", ***r); 
    	return 0;
    }
    
  • 多级指针图示理解:
    C程序设计语言——指针_第11张图片

  • 多级指针举例2:

    #include 
    
    void f(int ** q)
    {
    	//*q就是p 
    }
    
    void g()
    {
    	int i = 10;
    	int *p = &i;
    	
    	f(&p);	//p是int *类型,&p是int **类型	
    }
    
    int main(void)
    {
    	g();
    	return 0;	
    } 
    

五. 指针的缺点

1. 导致内存泄漏(内存就会越用越少)
2. 产生野指针

C程序设计语言——指针_第12张图片

free(p):把p指向的空间释放

这时候就不能再free(q);因为p,q,r指向的是一个空间,只需要释放一次。

如果再次释放同一个空间程序会出错

当程序特别大的时候,无法检测有哪个空间没有被free。如果出现对用过空间一次都没有free的话,会出现内存泄漏

这个是C语言程序的致命缺点,无法去避免

你可能感兴趣的:(C语言程序设计)