指针

指针

指针是C语言的灵魂,想学好C语言必须要搞懂指针,如果不懂指针,相当于没学C语言

1. 指针的重要性

  • 表示一些复杂的数据结构
  • 快速的传递数据
  • 使函数返回一个以上的值
  • 能直接访问硬件
  • 能够方便的处理字符串
  • 是理解面向对象语言中引用的基础

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

2. 指针的定义

2.1 地址

地址就是内存单元的编号

从零开始到非负整数

总体来说cpu操作内存条分为三条线,控制线负责数据的传输的方向,数据线负责数据的传输,地址线负责数据传输的地址

现在我们只讨论地址线

在地址线中cpu操作内存条,有很多条线,一条线只有0、1两种变化,所以只能操作两个字节,两条线则有22种变化,一般像32位的机器,也就是32条线,有232种变化,可以操作232个字节,也就是232B,而

1K = 2^10B

1M = 2^10K = 2^20B

1G = 2^10M = 2^20K = 2^30B

所以,2^32B = 2^30B * 2^2 = 4G ,也就是说,32位的计算机的内存条大小就是4G

故地址的范围为(按32位的情况):4G 【0 --- 4G-1】

2.2 指针

指针就是地址,地址就是指针

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

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

注意:有时会把指针变量简称为指针,但是要明白他们不一样

指针的本质就是一个操作受限(意思就是不能加乘除,但是可以相减)的非负整数

3. 指针的分类

3.1 基本类型指针【重点】

3.1.1 基本类型指针例子如下:

# include <stdio.h>

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
		*/

	j = *p; //等价于j = i;

			/*
				*p 就是以p的内容为地址的变量
			*/

	printf("i = %d, j = %d, \n", i, j);
	


	return 0;
}

3.1.2 *的含义:

  1. 乘法

  2. 定义指针变量

    int * p;
    //定义了一个名字为p的变量,int *表示p只能存放int变量的地址
    
  3. 指针运算符

    *p = b;
    //该运算符表示以p的内容为地址的变量,完整例子如下
    
    # include <stdio.h>
    
    int main(void)
    {
    	int a;
    	int b = 6;
    	int * p;
    	p = &a;
    	*p = b;
    
    	printf("a = %d, *p = %d\n",a ,*p);//运行结果为 a = 6, *p = 6
    
    	return 0;
    }
    

3.1.3 基本类型指针常见错误

在c语言中操作指针时,会有以下常见错误

  1. # include <stdio.h>
    
    int main(void)
    {
    	int i = 5;
    	int * p;
    
    	*p = i;	// warning,因为 p变量 并没有初始化,里面是垃圾值,也就是里面保存的是我们这个程序外的内存空间的编号
    			// 所以这样操作会导致我们在这里修改了 这个程序外 的内存空间的数值
    
    	printf("%d\n", *p);
    
    	return 0;
    }
    
  2. # include <stdio.h>
    
    int main(void)
    {
    	int i = 5;
    	int * p;
    	int * q;
    
    	p = &i;
    	//*q = p;	//error 语法错误,编译报错 *q 为 int 类型,p 为 int * 类型
    
    	//*q = *p;	//warning,因为变量q并没有初始化
    
    	p = q;		//q是垃圾值,q赋给p,p也变成垃圾值
    
    
    
    	printf("%d\n", *p); //q的空间是属于本程序的,所以可以对里面的垃圾值进行操作
    						//但是*q是 以q里面的垃圾值为地址的内存空间里面的数值,是不属于本程序的内存,所以不能对其操作
    
    	return 0;
    
    }
    

3.1.4 经典指针程序_互换两个数字

定义以下三种函数来进行互换两个变量数值,三种函数虽然都是互换变量的内容,但是其本质含义各不相同,具体的区别由下面的例子进行解析

# include <stdio.h>

void huhuan(int , int );
void huhuan2(int *, int *);
void huhuan3(int *, int *);


//经典指针程序-互换两个数字
int main(void)
{
	int a = 3;
	int b = 5;
	int t;

	//huhuan(a, b);

	//huhuan2(&a, &b);

	huhuan3(&a, &b);

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

	printf("a = %d, b = %d\n", a, b);

	return 0;
};

//不能完成互换功能
//因为只是互换了形参a,b的值,和主函数中的变量a,b并没有关系,并且当此函数运行完以后该形参a,b都会被释放
void huhuan(int a, int b)
{
	int t;

	t = a;
	a = b;
	b = t;
}

//不能完成互换功能
//因为该函数只是互换了p指针变量和q指针变量中的内容,和主函数的a,b变量也没有关系
/*
	注意:没有任何一门语言可以改变一个静态变量已经分配好的空间,也就是说,不能改变一个静态变量(例如 int a)的内存空间地址(指针);
	假如 int a 所分配的内存空间编号位1000H,那么在a变量释放之前,它的空间编号一直并且只能是1000H,不能修改
*/
void huhuan2(int * p, int * q)
{
	int * t;

	t = p;
	p = q;	//p是int *类型,存放int类型变量的地址
	q = t;
}

//可以完成互换功能
/*
	因为其中*p指的是 以指针变量p中的值为地址的内存空间中的值,相当于变量a的内存空间中的值;
	所以改变*p的值相当于改变了变量a的值,改变了*q的值相当于改变了变量b的值
*/
void huhuan3(int * p, int * q)
{
	int t;

	t = *p;
	*p = *q; //*p是int类型,以指针变量p中的值为地址的内存空间中的值
	*q = t;
}

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

  1. 实参必须为该普通变量的地址
  2. 形参必须为指针变量
  3. 必须要在被调函数中修改 *变量名 的值

3.2 指针和数组

3.2.1 指针和一维数组

  • 一维数组名

    • 一维数组名是一个指针常量(不能被改变)
    • 它存放的是一维数组第一个元素的地址
    # include <stdio.h>
    
    int main(void)
    {
    	int a[5]; //a是数组名	5是数组元素的个数	元素就是变量 a[0] -- a[4]
        int b[5];
    
    //	a = b; //error a是常量
    
    	printf("%#X\n", &a[0]);
    	printf("%#X\n", a);
    
    	return 0;
    }
    /*
    	在Vc++6.0中的输出结果是:
    ------------------
    0X19FF1C
    0X19FF1C
    Press any key to continue
    ------------------
    */	
    
  • 下标和指针的关系

    • 如果p是个指针变量,则 p[i] 永远等价于 *(p+i)
    # include <stdio.h>
    
    int main(void)
    {
    	int a[5] = {1, 2, 3, 4, 5};
    	int i;
    
    	for (i=0; i<5; ++i)
    	{
    		//printf("%d\n", a[i]);
    		printf("%d\n", *a+i); //输出结果与上面相同,即 a[i] == *(a+i)
    	}
    
    	return 0;
    }
    
  • 确定一个一维数组需要几个参数【如果一个函数要处理一个一维数组,则需要接收该数组的哪些信息】

    • 需要两个参数
      1. 数组的第一个元素地址【数组名】
      2. 数组的长度

    以下由两个例子来简述如何传递一维数组

    1. # include <stdio.h>
      
      //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("%d ", pArr[i]);	//此处的运行结果和上面相同
          }
      	printf("\n");
      }
      
      int main(void)
      {
      	int a[5] = {1, 2, 3, 4, 5};
      	int b[6] = {-1, -2, -3, -4, -5, -6};
      	int c[100] = {1, 99, 22, 33};
      
      	f(a, 5);	//a是 int * 类型
      	f(b, 6);
      	f(c, 100);
      
      	return 0;
      }
      /*
      	在Vc++6.0运行结果
      -------------------
      1 2 3 4 5
      -1 -2 -3 -4 -5 -6
      1 99 22 33 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
      Press any key to continue
      -------------------
      */
      
    2. # include <stdio.h>
      
      void f(int * pArr, int len)
      {
      	pArr[3] = 88; //这里的 aPrr[3] 其实和 a[3] 本质上是一个内存空间 
      }
      
      int main(void)
      {
      	int a[6] = {1, 2, 3, 4, 5, 6};
      
      	printf("%d\n", a[3]);	//输出结果:4
      	f(a, 6);
      	printf("%d\n", a[3]);	//输出结果:88
      
      	return 0;
      }
      

    由以上两个例子可总结出

    *(pArr+i) == pArr[i] == b[i] == *(b+i)

    a == &a[0],a[0] == *(a+0)

  • 指针变量的运算

    • 指针变量不能相加 不能相乘 也不能相除【只能在一定条件下相减】

    • 如果两个指针变量指向的是同一块连续空间中的不同存储单元,则这两个指针变量才可以相减

    # include <stdio.h>
    
    int main(void)
    {
    	int i = 5;
    	int j = 10;
    	int * p = &i;
    	int * q = &j;
    
    	//p - q 没有实际意义	并且i、j空间不连续
    
    	int a[5];
    
    	p = &a[1];
    	q = &a[4];
    
    	printf("p和q各自指向的单元相隔 %d 个单元\n", q - p);
    }
    
  • 一个指针变量到底占几个字节

    • 假设p指向char类型变量(1个字节)
    • 假设q指向int类型变量(4个字节)
    • 假设r指向double类型变量(8个字节)

    p q r 本身所占的字节数是否一样?

    预备知识:
    	sizeof(数据类型)
    	功能:返回该数据类型所占的字节数
    	sizeof(int)=4; sizeof(char)=1; sizeof(double)=8;
    	
    	sizeof(变量名)
    	功能:
    	int i = 1;
    	sizeof(i)=4;
    
    # include <stdio.h>
    
    int main(void)
    {
    	char ch = 'A';
    	int i = 99;
    	double x = 66.6;
    
    	char * p = &ch;
    	int * q = &i;
    	double * r = &x;
    
    	printf("p 占 %d 个字节, q 占 %d 个字节, r 占 %d 个字节\n", sizeof(p), sizeof(q), sizeof(r));
    
    	return 0;
    }
    /*
    	在Vc++6.0中的输出结果是:
    ------------------
    p 占 4 个字节, q 占 4 个字节, r 占 4 个字节
    Press any key to continue
    ------------------
    */	
    

    由以上例子可总结出,一个指针变量,无论它指向的变量占几个字节,该指针变量本身只占4个字节,它们所存放的变量地址都是首个字节的地址,如char类型变量只占一个字节,则char * 指针变量就存放该字节地址; int类型变量占4字节,则int * 指针变量只存放首个字节的地址,并标明该普通变量所占的空间是该地址字节再往下加3个字节;double类型变量占8字节, 则double * 指针变量只存放首个字节地址,并表明该普通变量所占的空间是该地址字节再往下加7个字节。

    为什么指针变量保存的某个字节地址编号需要用到4个字节的空间?

    ​ 答:例如32位机,有232种变化,可以操作232个字节,就像10人的团队,某人的编号就是01、02、03,100人的团队,某人的编号就是001,002,003,所以像2^32个字节的“团队”,它的第一个字节和最后一个字节的编号长度是一样的,所以每一个字节的地址编号数据就需要用到4个字节的空间来保存。

3.2.2 指针和二维数组

3.3 指针和函数

3.4 指针和结构体

3.5 多级指针

附录

1. 动态内存分配【重点难点】

1.1 传统数组的缺点:

  • (1). 数组长度必须提前指定,且只能是常整数(常量),不能是变量

    例子:

    int a[5];	//OK
    int len = 5; int a[len]; //error
    
  • (2). 传统形式定义的数组,该数组的内存程序员无法手动释放。

    在一个函数运行期间,系统为该函数中数组所分配的空间会一直存在,直到该函数运行完毕时,数组的空间才会被系统释放。

  • (3). 数组的长度一旦定义,其长度就不能再更改

    数组的长度不能在函数运行的过程中动态的扩充或缩小

  • (4). 传统方式定义的数组不能跨函数使用

    A函数定义的传统数组,在A函数运行期间可以被其他函数使用,但A函数运行完毕之后,A函数中的数组将无法再被其他函数使用。

1.2 为什么需要动态分配内存

  • 动态数组很好的解决了传统数组的这4个缺陷
  • 传统数组也叫静态数组

1.3 动态内存分配举例——动态数组的构造

补充知识:
	数据类型就相当于分配内存空间的数量,例如:
	int就代表4个字节,char就代表1个字节,double就代表8个字节

1.3.1 malloc()函数例子:
  1. /*
    	malloc 是 memory(内存) allocate(分配) 的缩写
    
    */
    # include <stdio.h>
    # include <malloc.h>
    
    int main(void)
    {
    	int i = 5;	//分配了4个字节 静态分配
    	int * p = (int *)malloc(4);
    		/*
    			1. 要使用malloc函数,必须添加malloc.h这个头文件
    			2. malloc函数只有一个形参,并且形参是整型
    			3. 形参 4 表示请求系统为本程序分配4个字节
    			4. malloc函数只能返回第一个字节的地址
    			5. 上一行代码 总共分配了8个字节,p变量占4个字节,p所指向的内存也占4个字节
    			6. p本身所占的内存是静态分配的,p所指向的内存是动态分配的
    		*/
    	*P = 5;		//*p 代表的就是一个int变量,只不过*p这个整形变量的内存分配方式和 int i = 5; 的分配方式不同
    
    	free(p);	//free(p)表示把p指向的内存给释放掉	
    				//p变量本身的内存是静态的,不能由程序员手动释放,p变量本身的内存只能在p变量所在的函数运行终止时由系统自动释放
    				
    	printf("同志们好!\n");
    
    	return 0;
    }
    
  2. # include <stdio.h>
    # include <malloc.h>
    
    void f(int * q)
    {
    	*q = 200;
    	//free(q);	//这里释放后后面就不能继续使用该内存了,如果继续使用该地址操作内存,将会是一个危险的操作
    				//所以在该指针释放后,如果后面还有指针变量的值是该指针,应该设置为NULL,防止野指针的发生
    				//野指针:指针变量中的值是未申请的内存地址
    }
    
    int main(void)
    {
    	int * p = (int *)malloc(sizeof(int)); //sizeof(int)返回值是int所占的字节数
    	*p = 10;
    
    	printf("%d\n", *p);	//10
    	f(p);	//p是int *类型
    	printf("%d\n", *p);	//200
    
    	return 0;
    }
    
1.3.2 动态一维数组的构造

# include <stdio.h>
# include <malloc.h>

int main(void)
{
	//静态数组
	int a[5];	//如果int占4个字节的话,则本数组总共包含20个字节,每4个字节被当做了一个int变量来使用
	
	//动态数组
	int len;
	int * pArr;
	int i;
	
	printf("请输入您要存放的整数元素个数:");
	scanf("%d", &len);	//输入5时
	pArr = (int *)malloc(4 * len);	/*
										需要申请 4 * 5 个字节 
										并且pArr指向前4个字节 (在这4个字节中再分析,pArr就是第一个字节的地址);
										pArr加一时,则pArr指向后4个字节	【不是第二个字节!!】
										结论:pArr = (int *)malloc(4 * len); 就类似于
											  int pArr[len];	【动态构造了一个一维数组】
											  数组名:pArr; 数组类型:int; 数组长度:len;

									*/
	//对一维数组进行操作【赋值】
	for (i=0; i<len; ++i)
	{
		printf("您要保存的第 %d 个数:\n", i+1);
		scanf("%d", &pArr[i]);
	}
	
	//对一维数进行输出
	printf("该一维数组的内容为:");
	for (i=0; i<len; ++i)
		printf("%d ", pArr[i]);
	printf("\n");

	free(pArr);	//动态数组可以在程序还在运行时释放内存

	return 0;
}
1.3.3 realloc()函数
int len; //设len=5
int * pArr;
pArr = (int *)malloc(4 * len);
realloc(pArr, 100); /*当发现pArr内存不够时,可以用realloc函数来修改空间大小
						若原来为20,修改为100后前20个字节的数据依然保留
						若原来为120,修改为100后,后20个字节的数据将丢失
					*/

1.4 静态内存和动态内存的比较

1.5 跨函数使用内存的问题

posted @ 2021-08-18 09:03  djh学习历程  阅读(147)  评论(0)    收藏  举报