C语言的指南针——指针与结构体

举报
未见花闻 发表于 2022/04/29 23:02:45 2022/04/29
【摘要】 如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的空间。内存区的每一个字节有一个编号,这就是“地址”。

⭐️前面的话⭐️

如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的空间。内存区的每一个字节有一个编号,这就是“地址”。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年4月29日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《C语言程序设计》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


1.一分钟搞懂指针是什么

如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的空间。内存区的每一个字节有一个编号,这就是“地址”。
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元,将地址形象化地称为“指针”。

#include <stdio.h>
int main()
{
 int a = 10;//在内存中开辟一块空间
 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
   //将a的地址存放在p变量中,p就是一个之指针变量。
 return 0; }

对于一个变量我们可以使用取地址符&获取这个变量的地址,对一个指针使用解引用符*可以访问指针所指向的那块空间。

int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };

比如一个数组,它的每个元素都有一个指向其元素的地址。数组名通常指该数组首元素的地址(指针)。

储存单元的地址(指针) 储存单元的内容
0x00EFFB30 01 00 00 00
0x00EFFB34 02 00 00 00
0x00EFFB38 03 00 00 00
0x00EFFB3C 04 00 00 00
0x00EFFB40 05 00 00 00
0x00EFFB44 06 00 00 00
0x00EFFB48 07 00 00 00
0x00EFFB4C 08 00 00 00
0x00EFFB50 09 00 00 00
0x00EFFB54 00 00 00 00

C语言中的地址包括位置信息(内存编号,或称纯地址)和它所指向的数据的类型信息,或者说它是“带类型的地址”。
存储单元的地址和存储单元的内容是两个不同的概念。
在程序中一般是通过变量名来引用变量的值。
直接按变量名进行的访问,称为“直接访问”方式。

//直接访问
arr[0];
arr[2];
//...

还可以采用另一种称为“间接访问”的方式,即将变量的地址存放在另一变量(指针变量)中,然后通过该指针变量来找到对应变量的地址,从而访问变量。

//间接访问
*(arr);//arr[0]
*(arr+2);//arr[2]

2.指针

2.1指针类型

指针和变量一样有不同种类的类型,变量有字符型,整型,浮点型…;指针也有字符型指针,整型指针,浮点型指针…
char*:字符指针类型,指向数据类型为字符型的指针
int*:整型指针类型,指向数据类型为整型的指针
double*:双精度浮点型指针。
float*:单精度浮点型指针。
unsigned int*:无符号整型指针。
int (*arr)[10]:数组指针,指向一个数组的指针。
使用指针类型定义的变量称为指针变量。如,

int* a = 8;
double* b = 8.6;
char* ch = 'A';

像上面所定义的变量a b ch就称为指针变量。
正如上举例所示定义指针变量的格式就是类型名 * 变量名;
对于指针变量的定义,要注意以下几点:
:bulb: 指针变量前面的*表示该变量为指针型变量。指针变量名则不包含*
:bulb: 在定义指针变量时必须指定基类型。一个变量的指针的含义包括两个方面,一是以存储单元编号表示的纯地址(如编号为2000的字节),一是它指向的存储单元的数据类型(如int,char,float等)。
:bulb: 如何表示指针类型。指向整型数据的指针类型表示为“int *”,读作“指向int的指针”或简称“int指针”。
:bulb: 指针变量中只能存放地址(指针),不要将一个整数赋给一个指针变量。

2.2指针变量的引用

了解指针类型,那指针能干什么呢?
① 给指针变量赋值
②引用指针变量指向的变量
③引用指针变量的值

int a  = 8;
int *p = NULL;
p=&a;				//把a的地址赋给指针变量p										        ①
printf("%d\n",*p);	//以整数形式输出指针变量p所指向的变量的值,即a的值			            ②
*p=1;				//将整数1赋给p当前所指向的变量,由于p指向变量a,相当于把1赋给a,即a=1	②
printf("%p\n",p);		//以输出指针变量p的值,由于p指向a,相当于输出a的地址,即&a	③

例题:
输入a和b两个整数,不交换a,b变量的值,通过交换a,b地址的方式实现按先大后小的顺序输出a和b。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int* p1 = NULL;
	int* p2 = NULL;
	int* p = NULL;						//p1,p2,p的类型是int *类型
	int a = 0;
	int b = 0;					
	printf("请输入两个整数:\n");
	scanf("%d%d", &a, &b);				//输入两个整数 
	p1 = &a;							//使p1指向变量a
	p2 = &b;							//使p2指向变量b
	if (a < b)							//如果a<b
	{
		p = p1; 
		p1 = p2; 
		p2 = p;
	}			//使p1与p2的值互换
	printf("a=%d,b=%d\n", a, b);			//输出a,b
	printf("max=%d,min=%d\n", *p1, *p2);	//输出p1和p2所指向的变量的值
	return 0;
}

输出结果:

请输入两个整数:
6 8
a=6,b=8
max=8,min=6

D:\gtee\C-learning-code-and-project\test_807\Debug\test_807.exe (进程 10556)已退出,代码为 0。
按任意键关闭此窗口. . .

2.3指针加减运算

我们不难从内存中发现,指针实质上也是数字,在内存中我们可以观察到指针是一个十六进制数,既然是一个数,那就可以进行运算,但是我们要进行有意义的运算而不是无意义的运算。比如可以通过指针的加法进行数组元素的访问,使用指针的减法可以得到数组的元素个数。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int i = 0;
	int size = &arr[10] - &arr[0];//只是获取arr[10]的地址,并没有对它进行越界访问
	//使用指针减法获取数组元素个数
	printf("arr数组元素个数为:%d\n", size);
	for (i = 0; i < size; i++)
	{
		printf("%d ", *(arr + i));//使用指针加法访问数组元素

	}
	return 0;
}

在这里插入图片描述
运行结果:

arr数组元素个数为:10
1 2 3 4 5 6 7 8 9 0
D:\gtee\C-learning-code-and-project\test_807\Debug\test_807.exe (进程 15840)已退出,代码为 0。
按任意键关闭此窗口. . .

3.野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

3.1野指针的成因

野指针主要是因为这些疏忽而出现的删除或申请访问受限内存区域的指针

:bulb:指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,部分编译器会报错" ‘point’ may be uninitializedin the function "。

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
 return 0; }

:bulb:指针释放后之后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是"垃圾"内存。释放后的指针应立即将指针置为NULL,防止产生"野指针"。

#include <stdio.h>
#include <malloc.h>
#include <assert.h>
int main()
{
	int* arr = (int*)malloc(sizeof(int) * 4);
	assert(arr);
	int* p = arr;//拷贝arr地址给p
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		*(p + i) = i + 2;
		printf("%d ", *(p + i));

	}
	free(arr);//arr释放后,p未置空,造成p为野指针
	return 0;
}

:bulb:指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0; }

3.2如何规避野指针

:bulb:指针初始化
:bulb:小心指针越界
:bulb:指针指向空间释放即使置NULL
:bulb:避免返回局部变量的地址
:bulb:指针使用之前检查有效性

4.二级指针

指针变量也是变量,是变量就会有地址,指向指针变量的指针称为二级指针

如果在一个指针变量中存放一个目标变量的地址,这就是单级指针一级指针
在这里插入图片描述

指向指针数据的指针用的是“二级指针”方法;
在这里插入图片描述

从理论上说,间址方法可以延伸到更多的级,即多重指针。
在这里插入图片描述

int a = 12;
int* pa = &a;//一级指针
int** ppa = &pa;//二级指针
int*** pppa = &ppa;//三级指针
//各级指针如何访问a变量
//*pa;
//*ppa = pa;  **ppa= a;
//*pppa = ppa;   **ppa = pa;   ***pppa = a;

5.指针与数组

数组名是什么?数组和指针有什么关系?我们先来运行一个简单的程序

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    return 0; }

运行结果

00D0F874
00D0F874

D:\gtee\C-learning-code-and-project\test_807\Debug\test_807.exe (进程 2864)已退出,代码为 0。
按任意键关闭此窗口. . .

唉!我们发现数组名和数组首元素地址是一模一样的,这就说明数组名存放的是数组首元素地址。
既然数组名就是数组首元素地址,那使用数组名访问数组元素就成了可能。

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
		//printf("%d ", *(arr + i));
	}
	printf("\n");
	for (i = 0; i < 10; i++)
	{
		//printf("%d ", arr[i]);
		printf("%d ", *(arr + i));
	}
	return 0;
}

运行结果

1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
D:\gtee\C-learning-code-and-project\test_807\Debug\test_807.exe (进程 28064)已退出,代码为 0。
按任意键关闭此窗口. . .
#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%p  ==  %p\n", &arr[i], arr + i);
	}

	return 0;
}
00D6FC74  ==  00D6FC74
00D6FC78  ==  00D6FC78
00D6FC7C  ==  00D6FC7C
00D6FC80  ==  00D6FC80
00D6FC84  ==  00D6FC84
00D6FC88  ==  00D6FC88
00D6FC8C  ==  00D6FC8C
00D6FC90  ==  00D6FC90
00D6FC94  ==  00D6FC94
00D6FC98  ==  00D6FC98

D:\gtee\C-learning-code-and-project\test_807\Debug\test_807.exe (进程 30132)已退出,代码为 0。
按任意键关闭此窗口. . .

通过上面两个程序我们可以知道可以使用解引用数组名的方式访问数组的每一个元素。
但是我们发现arr+1并不是将数组首元素地址加1,而是加了4。
前面,我们讨论过了指针的类型,但是你会发现所有类型指针的大小都是一样的。那现在就有疑问,既然指针大小是一样的,我使用其他类型的指针来访问整型数组会发生什么呢?

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	char* a = (char*)arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(a + i) = 0;
	}
	return 0;
}

数组初始化后,数组各元素是这样的
在这里插入图片描述

然后我们尝试使用char类型的指针对数组进行访问
在这里插入图片描述

改为0,但是如果各类指针作用一样应该是10个元素全部被改为0才对。
其实,虽然各类指针大小都一样(在相同位数平台下指针大小相同,32位指针大小为4字节,64位平台指针大小为8字节),但是在对不同类型指针进行操作时是有区别的,当指针进行加减运算时,指针加1会加上相应数据类型内存大小,比如一个地址0x00000001,char类型指针加1,结果是0x00000002,int类型指针加1是0x00000005,所以也就解释了上面使用指针加法运算可以顺利访问整型数组元素,并且每次指针加1,地址都比原来高了4。

总结
:bulb:在指针已指向一个数组元素时,可以对指针进行以下运算:
:key:加一个整数(用+或+=),如p+1,表示指向同一数组中的下一个元素;
:key:减一个整数(用-或-=),如p-1,表示指向同一数组中的上一个元素;
:key:自加运算,如p++,++p
:key:自减运算,如p--,--p
:bulb:两个指针相减,如p1-p2(只有p1和p2都指向同一数组中的元素时才有意义),结果是两个地址之差除以数组元素的长度。注意: 两个地址不能相加,如p1+p2是无实际意义的。
:bulb:如果p的初值为&a[0],则p+ia+i就是数组元素a[i]的地址,或者说,它们指向a数组序号为i的元素。
:bulb:*(p+i)*(a+i)p+ia+i所指向的数组元素,即a[i][]实际上是变址运算符,即将a[i]a+i计算地址,然后找出此地址单元中的值。
在这里插入图片描述
:bulb:用下标法比较直观,能直接知道是第几个元素。适合初学者使用。
:bulb:用地址法或指针变量的方法不直观,难以很快地判断出当前处理的是哪一个元素。单用指针变量的方法进行控制,可使程序简洁、高效。
:bulb:在使用指针变量指向数组元素时,有以下几个问题要注意:
:key:可以通过改变指针变量的值指向不同的元素。
如果不用p变化的方法而用数组名a变化的方法(例如,用a++)行不行呢?
因为数组名a代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。既然a是常量,所以a++是无法实现的
:key: 要注意指针变量的当前值。

6.指针与结构体

6.1定义和使用结构体变量

C语言允许用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体(structure)。

struct 结构体名
{
	成员表列;
};

结构体类型的名字是由一个关键字struct和结构体名组合而成的。结构体名由用户指定,又称结构体标记(structure tag) 。
花括号内是该结构体所包括的子项,称为结构体的成员(member)。对各成员都应进行类型声明,即

类型名 成员名;

比如自定义一个学生信息的结构体

struct Student
{	int num;				//学号为整型 
	char name[20];			//姓名为字符串 
	char sex;				//性别为字符型 
	int age;				//年龄为整型
	float score;			//成绩为实型 
	char addr[30];			//地址为字符串 
};							//注意最后有一个分号

:bulb:结构体类型并非只有一种,而是可以设计出许多种结构体类型,各自包含不同的成员。
:bulb:成员可以属于另一个结构体类型。
在这里插入图片描述

struct Date					//声明一个结构体类型 struct Date 
{	int month;				//月
	int day;				//日
	int year;				//年
}; 
struct Student				//声明一个结构体类型 struct Student
{	int num;
	char name[20];
	char sex;
	int age;
	struct Date birthday;	//成员birthday属于struct Date类型
	char addr[30]; 
};

:bulb:先声明结构体类型,再定义该类型的变量

struct Student
{	int num;				//学号为整型 
	char name[20];		//姓名为字符串 
	char sex;			//性别为字符型 
	int age;				//年龄为整型
	float score;			//成绩为实型 
	char addr[30];		//地址为字符串 
};						//注意最后有一个分号
struct Student  student;
	    |		    |
  结构体类型名	 结构体变量名

:bulb:在声明类型的同时定义变量

struct 结构体名
{	
	成员表列
}变量名表列;

struct Student
{	int num;		
	char name[20];
	char sex;	
	int age;
	float score;
	char addr[30];
}student;

:bulb:不指定类型名而直接定义结构体类型变量

struct
{
	成员表列
}变量名表列;

:bulb:结构体类型与结构体变量是不同的概念,不要混淆。只能对变量赋值、存取或运算,而不能对一个类型赋值、存取或运算。在编译时,对类型是不分配空间的,只对变量分配空间。
:bulb:结构体类型中的成员名可以与程序中的变量名相同,但二者不代表同一对象。
:bulb:对结构体变量中的成员(即“域”),可以单独使用,它的作用与地位相当于普通变量。

6.2结构体的初始化与访问

:bulb:在定义结构体变量时可以对它的成员初始化。初始化列表是用花括号括起来的一些常量,这些常量依次赋给结构体变量中的各成员。对结构体变量初始化,不是对结构体类型初始化。
:bulb:可以引用结构体变量中成员的值,引用方式为结构体变量名.成员名结构体指针->成员名
:bulb:“.”是成员运算符,它在所有的运算符中优先级最高,结构体变量名.成员名结构体指针->成员名作为一个整体来看待,相当于一个变量。
:bulb:不能企图通过输出结构体变量名来输出结构体变量所有成员的值。只能对结构体变量中的各个成员分别进行输入和输出。
:bulb:如果成员本身又属一个结构体类型,则要用若干个成员运算符,一级一级地找到最低的一级的成员。只能对最低级的成员进行赋值或存取以及运算。
:bulb:对结构体变量的成员可以像普通变量一样进行各种运算(根据其类型决定可以进行的运算)。
:bulb:同类的结构体变量可以互相赋值。
:bulb:可以引用结构体变量成员的地址,也可以引用结构体变量的地址(结构体变量的地址主要用作函数参数,传递结构体变量的地址)。但不能用以下语句整体读入结构体变量。

//结构体初始化与访问
struct st
{
	int a;
	double b;
};

struct st c = {24,78.89};//初始化
c.a = 12;
c.b = 86.98;//使用结构体变量名进行结构体访问
(&c) -> a =14;
(&c) -> b = 99.88;//使用指针进行结构体访问

例题:
建立一个结构体,把一个学生的信息(包括学号、姓名、性别、年龄,成绩,住址)放在一个结构体变量中,然后输出这个学生的信息。
在这里插入图片描述

#include <stdio.h>
struct Student							//声明结构体类型struct Student
{
	long int num;						//以下6行为结构体的成员
	char name[20];
	char sex[4];
	int age;
	double score;
	char addr[20];
};										//定义结构体
int main()
{
	struct Student a = { 123456,"张三","男",18,99.5,"湖南"};	//定义结构体变量a并初始化
	printf("num:%ld\nname:%s\nsex:%s\nage:%d\nscore:%.2lf\naddr:%s\n", a.num, a.name, a.sex,a.age,a.score, a.addr);
	return 0;
}

运行结果

num:123456
name:张三
sex:男
age:18
score:99.50
addr:湖南

D:\gtee\C-learning-code-and-project\test_807\Debug\test_807.exe (进程 24868)已退出,代码为 0。
按任意键关闭此窗口. . .

6.3结构体指针

所谓结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针。如果把一个结构体变量的起始地址存放在一个指针变量中,那么,这个指针变量就指向该结构体变量。

将一个结构体变量的值传递给另一个函数,有3个方法:
:key:用结构体变量的成员作参数。
例如,用st1.mame或st2.name作函数实参,将实参值传给形参。用法和用普通变量作实参是一样的,属于“值传递”方式。应当注意实参与形参的类型保持一致。
:key:用结构体变量作实参。
用结构体变量作实参时,采取的也是“值传递”的方式,将结构体变量所占的内存单元的内容全部按顺序传递给形参,形参也必须是同类型的结构体变量。在函数调用期间形参也要占用内存单元。这种传递方式在空间和时间上开销较大,如果结构体的规模很大时,开销是巨大的。此外,由于采用值传递方式,如果在执行被调用函数期间改变了形参(也是结构体变量)的值,该值不能返回主调函数,这往往造成使用上的不便。因此一般较少用这种方法。
:key:用指向结构体变量(或数组元素)的指针作实参,将结构体变量(或数组元素)的地址传给形参。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。