C语言指针,结构体与动态内存分配与释放复习

一、指针

1.1 指针的基本介绍

指针的重要性:指针是C语言的灵魂。
指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。
在这里插入图片描述
地址:

是从内存单元的编号 ,是从0开始的非负整数 ,如:0–FFFFFFFF【4G-1】

指针:

指针就是地址 ,地址就是指针 。指针变量是存放在内存单元地址的变量, 指针的本质是一个操作受限的非负整数

基本概念

int i = 0;
int *p = &i;	//等价于int *p; 	p = &i;

详解这两步操作:
(1)p 存放了 i 的地址,所以我们说 p 指向了 i
(2)p 和 i 是完全不同的两个变量,修改其中的任意一个变量的值不影响另一个变量的值
(3)p 指向 i, *p 就是i变量本身。更形象的说所有出现 *p 的地方都可以换成 i,所有出现 i 的地方都可以换成 *p

注意:

指针变量也是变量,只不过它存放的不能算是内存单元的内存地址
普通变量前不能加 * ,常量和表达式前不能加&

实例:

#include<stdio.h>
int main()
{
	int *p;//p是个变量名,int * 表示该p变量只能存储int类型变量的地址
	int i = 10;
	int j;

	//j = *p;//p没保存地址,那么*p就不知道指向哪个地址
	
	p = &i;

	j = *p;	//等价于 j = i;
	//p = 10	//error
	printf("i = %d, j = %d, *p = %d\n", i, j, *p);
	return 0;
}

内存分析图:
在这里插入图片描述

1.2 指针的运算(++,–,+,-)

指针是一个用数值表示的地址。可以对指针执行算术运算。

数组名

一维数组名是个指针常量, 它存放的是一维数组第一个元素的地址, 它的值不能被改变,一维数组名指向的是数组的第一个元素

下标和指针的关系

a[i] <<==>> *(a+i)

假设指针变量的名字为p, 则 p+i 的值是p+i*(p所指向的变量所占的字节数)

实例:

#include<stdio.h>
int main()
{
	int arr[5] = {1,2,3,4,5};
	int i, *p;	//p是一个int* 指针
	
	p = arr;//p指向了arr数组的首地址
	for(i = 0;i < 5;i++)
	{
		printf("arr[%d] 地址=%p\n",i,p);
		printf("存储值:arr[%d]=%d\n",i,*p);
		p++;
	}

	return 0;
}

内存结构图:
在这里插入图片描述
● 数组在内存中是连续分布的
● 当对指针进行++时,指针会按照它指向的数据类型字节数大小增加,比如int* 指针,每++,就增加 4 个字
节。
所有指针变量只占4个字节,用第一个字节的地址表示整个变量的地址。

1.3 指针数组

要让数组的元素指向 int 或其他 数据类型的地址(指针)。可以使用指针数组。

指针数组定义:

数据类型 *指针数组名[大小]

如:int *arr[3];

arr 声明为一个指针数组 。
arr 由 3 个整数指针组成。因此,arr 中的每个元素,都是一个指向 int 值的指针。

比如:

#include<stdio.h>
int main()
{
	int arr[3] = {100,200,300};
	int i, *p[3];	//p是一个int* 指针

	for(i = 0;i < 3;i++)
	{
		p[i] = &arr[i];	//赋值为整数的地址
	}

	for(i = 0;i < 3;i++)//指针数组获取各个值
	{
		printf("arr[%d]=%d  p[%d]地址=%p\n",i,*p[i],i,&p[i]);
		
	}
	
	return 0;
}

内存布局:
在这里插入图片描述

1.4 指向指针的指针(多重指针)

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
在这里插入图片描述
(1)一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。例如,声明了一个指向int 类型指针的指针:int **ptr;
(2)当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,比如**ptr

实例:

#include<stdio.h>
int main()
{
	int var;
	int *p;//一级指针
	int **pp;//二级指针
	var = 1000;
	p = &var;//var变量的地址赋值给p
	pp = &p;//表示将p存放的地址,赋值给pp

	printf("var的地址=%p  var = %d\n",&var,var);
	printf("p本身的地址=%p  p存放的地址 = %d   *p = %d\n",&p,p,*p);
	printf("pp本身的地址=%p  pp存放的地址 = %d   *pp = %d\n",&pp,pp,*pp);

	return 0;
}

内存结构图:
在这里插入图片描述

1.4 传递指针(地址)给函数

当函数的形参类型是指针类型时,使用该函数时,需要传递指针,或者地址,或者数组给该形参。

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

  1. 实参为相关变量的地址
  2. 形参为以该变量的类型为类型的指针变量
  3. 在被调函数中通过 *形参变量名 的方式就可以修改主函数中的变量的值

实例1(传地址或指针给指针变量):

#include<stdio.h>

void f(int *p)	//不是定义了一个名字叫做*p的形参,而是定义了一个形参,该形参名字叫做p,它的类型是int *
{
	*p += 1;
}

int main()
{
	
	int i = 10;
	int *p = &i;//将i的地址赋值给p
	
	f(&i);//传地址
	printf("i = %d\n", i);//i=11
	
	f(p);//传指针
	printf("i = %d\n", i);//i=12
	return 0;
}

内存分析:
在这里插入图片描述

实例2(传数组给指针变量):

#include <stdio.h>

/* 函数声明 */
double getAverage(int *arr, int size); //

int main ()
{
   /* 带有 5 个元素的整型数组  */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;
   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;
   /* 输出返回值  */
   printf("Average value is: %f\n", avg );
   return 0;
}
//说明
//1. arr 是一个指针,
double getAverage(int *arr, int size)
{
  int i, sum = 0;       
  double avg;          
  for (i = 0; i < size; ++i)
  {
	  // arr[0] = arr + 0
	  // arr[1] = arr + 1个int字节(4) 
	  // arr[2] = arr +  2个int字节(8)
    sum += arr[i];// arr[0] =>数组第一个元素的地址 
	printf("\n arr存放的地址=%p \n", arr);
  }
  avg = (double)sum / size;
  return avg;
}

在前面说过 a[i] <<==>> *(a+i),所以上面的 arr[i] 还可以写成 *(arr + i)。也可以这样:

    sum += *arr;
    arr++;  // 指针的++运算, 会对arr 存放的地址做修改

1.5 返回指针的函数

允许函数的返回值是一个指针(地址),这样的函数称为指针函数。

实例:

#include <stdio.h>
#include <string.h>

char *strlong(char *str1, char *str2){ //函数返回的char * (指针)
	printf("\nstr1的长度:%d str2的长度:%d", strlen(str1), strlen(str2));
	if(strlen(str1) >= strlen(str2)){
		return str1;
	}else{
		return str2;
	}
}
int main(){
	char str1[30], str2[30], *str; // str 是一个指针类型,指向一个字符串
	printf("\n请输入第1个字符串:");
	gets(str1);
	printf("\n请输入第2个字符串:");
	gets(str2);
	str = strlong(str1, str2);
	printf("\nLonger string: %s \n", str);
	return 0;
}

注意事项:

  1. 用指针作为函数返回值时需要注意,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针不能指向这些数据。
  2. 函数运行结束后会销毁该函数所有的局部数据,这里所谓的销毁并不是将局部数据所占用的内存全部清零,而是程序放弃对它的使用权限,后面的代码可以使用这块内存
  3. C语言不支持在调用函数时返回局部变量的地址,如果确实有这样的需求,需要定义局部变量为static变量

1.6 函数指针(指向函数的指针)

  • 一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。
  • 函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针定义:


returnType (* pointerName)(param list);

  1. returnType 为函数指针指向的函数返回值类型
  2. pointerName 为函数指针名称
  3. param list为函数指针指向的函数的参数列表
  4. 参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称
  5. 注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType*

实例(用函数指针来实现对函数的调用,返回两个整数中最大值):

#include <stdio.h>

int max(int a,int b)
{
	return a>b?a:b;
}
int main(){
	int x,y,maxVal;
	//说明函数指针
	//1.函数指针的名字pmax
	//2. int表示该函数指针指向的函数是返回int类型
	//3. (int, int)表示该函数指针指向的函数形参是接收两个int
	//4.在定义函数指针时,也可以写上形参名int (*pmax)(int x, int y) = max;
	int (*pmax)(int , int)= max; 

	printf("Input two numbers:"); 
	scanf("%d %d", &x, &y);
	// (*pmax)(x, y)通过函数指针去调用函数max
	maxVal = (*pmax)(x, y);
	printf("Max value: %d pmax=%p pmax本身的地址=%p\n", maxVal, pmax, &pmax);
	return 0;
}

内存结构图分析:
在这里插入图片描述

1.7 指针的注意事项和细节

  1. 指针变量存放的是地址,从这个角度看指针的本质就是地址。
  2. 变 量声明的时候,如果没有确切的地址赋值,为指针变量赋一个NULL值是好的编程习惯。
  3. 赋为NULL值的指针被称为空指针,NULL指针是一个定义在标准库<stdio.h>中 的值为零的常量 #define NULL 0

二、结构体

2.1 为什么要有结构体

比如有一个学生有学号,名字,年龄等属性,多个学生如果按常规变量赋值就会使得代码冗余,那么就要把其各个属性抽象出来,形成一个新的类型,所以结构体诞生了。也就是:为了表示一些复杂的数据,而普通的基本类型变量无法满足要求。

2.2 什么是结构体

结构体是用户根据实际需要自己定义的复合数据类型。其实结构体就是面向对象语言中类,只是结构体中没有方法,并且最后有分号。

class Student
{
	int sid;
	String name;
	int sage;

	void inputStudent()
	{
	}
	void showStudent()
	{
	}
}
struct Student
{
	int sid;
	char *name;
	int sage;
}; //分号不能省
2.2.1 结构体和结构体变量的区别与联系
  1. 结构体是自定义的数据类型,表示的是一种数据类型.
  2. 结构体变量代表 一个具体变量,好比:
int numl; //int 是数据类型,而num1是一个具体的int变量
struct Student stu;// Student 是结构体数据类型,而 stu是一个Cat变量

2.3 如何使用结构体

两种方式:

struct Student st = {1000,"zhangsan",20};
struct Student *pst = &st;
  1. st.sid
  2. pst->sid

实际使用如下:

#include <stdio.h>
#include<string.h>

struct Student
{
	int sid;
	char *name;//名字,用指针,指向一个字符串
	int age;
};//分号不能省

int main(){
	
	struct Student st = {1000,"zhangsan",20};

	//第一种方式
	/*
	st.sid = 99;
	//st.name = "lisi"; //error
	st.name = "lisi";
	st.age = 19;
	*/

	//第二种方式(常用)
	struct Student *pst;
	pst = &st;
	pst->sid = 99;	//pst->sid 等价于 (*pst).sid, 而(*pst).sid等价于st.sid,所以pst->sid等价于st.sid
	pst->name = "wangwu";
	pst->age = 21;

	printf("%d %s %d \n",st.sid,st.name,st.age);
	return 0;
}

2.4 注意事项

  • 结构体变量不能加减乘除,但可以相互赋值
  • 普通结构体变量和结构体指针变量作为函数传参问题
#include <stdio.h>
#include<string.h>

typedef struct Student
{
	int sid;
	char *name;//名字,用指针,指向一个字符串
	int age;
}Student;//分号不能省


void f(Student *pst)
{
	pst->sid = 88;
	pst->name = "mayun";
	pst->age = 18;
}

//这种方式消耗内存,耗时间,不推荐
void g(Student st)
{
	printf("%d %s %d \n",st.sid,st.name,st.age);
}

void g2(Student *pst)
{
	printf("%d %s %d \n",pst->sid,pst->name,pst->age);
}

int main(){
	Student st;
	f(&st);
	//g(st);
	g2(&st);
	
	return 0;
}

typedef的用法
typedef struct Student
{
	int sid;
	char *name;
	int age;
}Student,*pStudent;//等价于Student代表了struct Student ,pStudent代表了struct Student *  

用法:

#include <stdio.h>
#include<string.h>

typedef struct Student
{
	int sid;
	char *name;
	int age;
}Student,*pStudent;//等价于Student代表了struct Student ,pStudent代表了struct Student *  


int main(){
	Student st;//struct Student st;
	pStudent ps = &st;//struct Student *st = &st; 
	ps->sid = 999;
	printf("%d\n",ps->sid);

	return 0;
}

三、动态内存分配与释放

3.1 不同数据在内存中的分配

在这里插入图片描述

  1. 全局变量一一内存中的静态存储区
  2. 非静态的局部变量——内存中的动态存储区——stack栈
  3. 临时使用的数据——建立动态内存分配区域,需要时随时开辟,不需要时及时释放——heap 堆
  4. 根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名
    来引用这些数据,只能通过指针来引用

3.2 内存动态分配相关函数

头文件 #include <stdlib.h> 声明了四个关于内存动态分配的函数,下面我只介绍最常用的函数malloc和free。

  1. 函数原型 void * malloc (usigned int size)
    ● 作用:在内存的动态存储区(堆区)中分配一个长度为size的连续空间。
    ● 形参size的类型为无符号整型,函数返回值是所分配区域的第-一个字节的地址,即此函数是一个指针型函数,返回的指针指向该分配域的开头位置(函数首地址)。
    malloc(100); 开辟100字节的临时空间,返回值为其第一个字节的地址

  2. 函数原型: void free (void *p)
    ● 作用:释放变量p所指向的动态空间,使这部分空间能重新被其他变量使用。
    ● p 是最近一次调用calloc或malloc函数时的函数返回值
    ● free函数无返回值
    free(p);//释放p所指向的已分配的动态空间

C99标准把以上malloc,calloc,realloc函数的基类型定为void类型,这种指针称为无类型指针(typeless pointer) ,即不指向哪一种具体的类型数据,只表示用来指向一个抽象的类型的数据,即仅提供一个纯地址,而不能指向任何具体的对象。
在这里插入图片描述

代码示例:

#include <stdio.h>
#include<stdlib.h>

int main(){
	int a[5] = {4,10,2,8,6};//静态分配内存

	int len;
	printf("请输入你要分配的数组长度:len = ");
	scanf("%d",&len);

	// 在堆区开辟-一个5*4的空间,并将地址(void*) ,转成(int*),赋给pArr
	int *pArr = (int *)malloc(sizeof(int) * len);

	/*
	*pArr = 4;	//类似于a[0] = 4;
	pArr[1] = 10; //类似于a[1] = 10;
	printf("%d %d\n",*pArr,pArr[1]);//4  10
	*/


	//pArr完全可以当做普通数组来使用
	for(int i = 0;i < len;i++)
	{
		scanf("%d",&pArr[i]);
	}

	for(i = 0;i < len;i++)
	{
		printf("%d\n",*(pArr + i));
	}


	free(pArr);	//把pArr所代表的动态分配的20个字节内存释放
	return 0;
}

内存结构图:
在这里插入图片描述

3.3 动态分配内存注意事项

  1. 避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大
  2. 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它(如果使用动态分配内存,需要遵守原则:(谁分配,谁释放),否则可能出现内存泄漏
  3. 总是确保释放已分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存
  4. 在释放内存之前, 确保不会无意中覆盖堆上已分配的内存地址,否则程序就会出现内存泄漏。在循环中分配内存时,要特别小心
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值