C语言:初阶指针和结构体

2023-11-16

1. 指针是什么?

指针理解的两个要点:

  1. 指针是内存中的一个最小单元的编号,也就是地址.
  2. 平时口语说的指针,通常指的是指针变量,是用来存放内存地址的变量

为了管理计算机内存空间,会把内存分为一个一个一个小的内存单元,每个内存单元占一个字节的空间.那么该怎么找到这些内存单元呢,这边每个内存单元都给一个编号,这些编号就是地址,也就是指针.

总结:指针就是地址,口语中说的指针通常指的是指针变量.

那我们就可以这样理解:

内存
在这里插入图片描述

指针变量

我们可以通过&(取地址操作符)得到一个变量的内存起始地址,把地址存放到这个变量中,这个变量就是指针变量.

C语言中创建的变量,数组等,都需要在内存开辟相对应的空间来存放数据.

#include <stdio.h>

int main(void)
{
	int a = 10;		//在内存中开辟一块空间
	int* p = &a;	//对变量a取地址,使用&操作符
					//a变量占用4个字节的空间,这里将a的4个字节的第一个字节的地址存放在指针变量p的内存单元中

	return 0;
}

通过&a取出a的地址,这是四个字节内存空间的首地址.
要想把a的地址存放起来,就需要使用指针类型来存放,这边使用int *.
int刚好对应指针变量存放地址对应变量的数据类型,而*表示这是一个指针变量.

通过调试看到a的地址是0x0000005F068FF804,存放了10这个数据.
在这里插入图片描述

再看p所存放的值
在这里插入图片描述

正好存放了a的地址.
因为本机是采用小端模式,即低地址存放低位,所以会呈现如图所示的存储方式.


总结:
指针变量,用来存放地址的变量.(存放在指针中的值都被当作地址处理).

那这里的问题是:

  • 一个小的单元到底是多大? (一个字节)
  • 如何编址?

经过仔细地计算我们发现一个字节给一个对应的地址是比较合适的.

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0).
那么32根地址线产生的地址就会是:

00000000 00000000 0000000 00000000
00000000 00000000 0000000 00000001


11111111 11111111 11111111 11111111

这里一共就有2的32次方个地址.

每个地址标识一个字节空间,那么
2^32 Byte = 2^22 KB = 2^12 MB = 2^2 GB = 4GB
就可以4GB的空间进行编址.

如果是64位,就有2的64次方个地址,那么
2^64 Byte = ... = 16EB
就可以对16EB的空间进行编址. 而市面上目前最大支持128GB的内存空间,这是完全够用的.

32或者64位只是代表管理内存的能力.

这里我们就明白了:

  • 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节.
  • 那如果在64位机器上,如果有64根地址线,那一个指针变量的大小是8个字节,才能存放一个地址.

总结:

  • 指针是用来存放地址的,地址是唯一表示一块地址空间的.
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节.

2. 指针和指针类型

我们都知道,变量有不同的类型,整型,浮点型等等.那指针有没有类型呢?
准确的说是有的.

char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;

这里可以看到,指针的定义方式就是:type + *
char *类型的指针就是为了存放char类型变量的地址.
int *类型的指针就是为了存放int类型变量的地址.
short *类型的指针就是为了存放short类型变量的地址.

通过使用sizeof操作符看不同指针类型的大小,我们发现

#include <stdio.h>

int main(void)
{
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(long*));
	printf("%d\n", sizeof(float *));
	printf("%d\n", sizeof(double*));

	return 0;
}

结果都是一致的:
在这里插入图片描述

那既然都是一样的大小,指针类型存在的意义是什么呢?

2.1 指针的解引用

通过调试下面这段代码,指针类型在解引用的时候影响了访问空间的大小.

#include <stdio.h>

int main(void)
{
	int a = 0x11223344;     //0x开头表示16进制数字
	int* pi = &a;			//int* 类型的指针变量指向了a
	char* pc = &a;			//char* 类型的指针变量指向了a

	*pi = 0;				//通过对指针变量的解引用进行修改
	a = 0x11223344;			//恢复原来的状态
	*pc = 0;				//再次通过对指针变量的解引用进行修改
	
	return 0;
}

首先观察到a地址对应内存,存放了0x11223344.
在这里插入图片描述

接着使用int*类型的指针变量通过解引用对a的值进行修改,发现该地址后连续4个字节大小内存空间的内容都被修改成了0.
在这里插入图片描述

然后恢复a的值,使用char*类型的指针变量通过解引用对a的值进行修改,发现该地址后只有1个字节大小内存空间的内容被修改成了0.
在这里插入图片描述


总结:

  • 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)
  • type *type不仅代表指针指向变量的数据类型,还代表指针解引用一次能访问sizeof(type)大小的内存空间

比如:char *类型的指针解引用就只能访问一个字节,而int *类型的指针解引用就能访问四个字节.

2.2 指针±整数

#include <stdio.h>

int main(void)
{
	int n = 10;
	char* pc = &n;
	int* pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pi);
	printf("%p\n", pc + 1);
	printf("%p\n", pi + 1);

	return 0;
}

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


总结:指针的类型决定了指针向前或者向后走一步有多大(距离):sizeof(type)

3. 野指针

概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的).

3.1 野指针成因

  1. 指针未初始化

指针变量未初始化,就和其他比如int类型的局部变量没有初始化一样,该变量存放的是随机值(在vs存放的是0xcccccccccccccccc),随机值被当作地址存放在指针变量中,这个位置是不可知的.

#include <stdio.h>

int main(void)
{
	int* p;		//局部变量未初始化,默认为随机值
	*p = 20;

	return 0;
}

编译器会直接报警这个行为是不合法的:
在这里插入图片描述

  1. 指针越界访问
#include <stdio.h>

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

	return 0;
}

当p指向arr外的空间时,编译器报错了
在这里插入图片描述
在这里插入图片描述

  1. 指针指向的空间释放
#include <stdio.h>

int* test(void)
{
	int a = 100;
	return &a;
}

int main(void)
{
	int* p = test();

	return 0;
}

这里在函数内创建了一个局部变量a,返回了它的地址,但是在函数调用完后,a的空间被返回给了操作系统,这时候再通过p访问这块空间的话,是非法访问.

3.2 如何规避野指针

  1. 指针初始化
  • 明确知道指针指向哪个变量所在的空间,直接将该变量的地址赋值给指针变量.
  • 不明确的话,直接赋值为NULL

NULL存在于头文件<stdio.h>中,表示指针没有指向任何有效的空间.

include <stdio.h>

int main(void)
{
    int* p = NULL;

    return 0;
}
  1. 小心数组越界
    这里给一个不使用下标访问数组,使用指针访问数组的一段代码.
#include <stdio.h>

int main(void)
{
	int arr[10] = { 0, };
	int* p = arr;
	int i = 0;
	int size = sizeof(arr) / sizeof(arr[0]);

	for (i = 0; i < size; i++)
	{
		*p = i;
		p++;
	}

	p = arr;

	for (i = 0; i < size; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

*(p + i) = arr[i] = *(arr + i) = *(i + arr) = i[arr]
这里[]仅仅只是个操作符而已,操作数是iarr,本质都是偏移量寻址.
3. 指针指向空间释放即置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性

#include <stdio.h>

int main(void)
{
    int* p = NULL;
    int a = 10;
    p = &a;
    if (p != NULL)
    {
        *p = 20;
    }

    return 0;
}

4. 指针运算

  • 指针 ± 整数
  • 指针 - 指针
  • 指针的关系运算

4.1 指针 ± 整数

指针存放地址±sizeof(type) * n

#define N_VALUES 5
float values[N_VALUES];
float* vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
	*vp++;
}

4.2 指针 - 指针

指针-指针的绝对值是两指针中间的元素个数
前提是在同一块内存空间中,否则就没有意义

可以用来实现strlen()的功能

int my_strlen(char* s)
{
    char *p = s;
    while(*p != '\0')
    {
        p++;
    }

    return p-s;
}

4.3 指针的关系运算

for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}

代码简化,将代码修改如下:

for(vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
    *vp = 0;
}

实际上在绝大部分的编译器撒谎给你是可以顺利完成任务的,然而我们还是应该比避免这样写,因为标准并不保证它可行.

标准规定:

允许指向数组元素的指针于指向数组的最后一个元素后面的哪个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较.

5. 指针和数组

指针和数组有什么关系呢?

区别:

指针变量就是指针变量,不是数组,指针变量的大小是4/8个字节,专门用来存放地址的.
数组就是数组,不是指针,数组是一块连续的空间,可以存放1个或者多个类型相同的数据.

联系:

数组中,数组名就是数组首元素的地址,数组名 == 地址 == 指针
当我们知道数组首元素的地址的时候,而数组又是连续存放的,所以通过指针就可以遍历访问数组,数组可以通过指针来访问的.

#include <stdio.h>
int main(void)
{
	int arr[10] = { 0, };
	int size = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;	//定义指针p指向数组首元素地址
	int i = 0;

	for (i = 0; i < size; i++)
	{
		printf("%p == %p\n", &arr[i], p + i);
	}
	return 0;
}

这里定义了一个指针指向数组首元素地址,通过遍历数组,发现

  1. 数组是一串连续的空间
  2. 通过指针也是可以遍历数组的,与通过数组索引来访问数组是一样的,p+iarr[i]是一样的.
    在这里插入图片描述

总结:数组和指针的唯一联系就是,可以通过指针来访问数组.

6.二级指针

二级指针就是用来存放一级指针变量的地址的.

谈到二级指针,首先了解一下什么一级指针

#include <stdio.h>

int main(void)
{
	int a = 0;
	int* p = &a;	//p是一级指针变量,指针变量也是变量,变量是在内存中开辟空间的,有变量就有地址

	return 0;
}

指向非指针变量的指针,就是一级指针.
那么既然变量都有地址,也就是说指针变量也是有地址的,如果定义一个指针变量存放指针变量的地址,这个指针变量就是二级指针.二级指针变量就是用来存放一级指针变量的地址.

#include <stdio.h>

int main(void)
{
	int a = 0;
	int* p = &a;	//p是一级指针变量,指针变量也是变量,变量是在内存中开辟空间的,有变量就有地址
	int** pp = &p;	//pp是二级指针变量

	return 0;
}

通过调试得到以下结果:
在这里插入图片描述

  • 这里的变量p是一个指针变量,前面我们学过int **表示这个变量是指针变量,int表示这个指针变量指向的变量数据类型是int类型`.
  • 同样的,这里的ppint **类型,第二个*表示pp是指针变量,int *表示这个指针变量指向变量数据类型是int *类型
  • 甚至int*** ppp = &pp,也是一样的,ppp是一个指向int **类型的指针变量.

对二级指针解引用一次,可以找到一级指针.再解引用一次,可以找到一级指针指向的变量.

*(*pp) = 100;

这里就改变了一级指针p所指向的变量a的数值.
**pp = *(&p) = a

7.指针数组

指针数组本质是数组,只是数组存放的变量都是指针类型的

#include <stdio.h>

int main(void)
{
	char arr1[] = "hello";
	char arr2[] = "world";
	char arr3[] = "!";

	char* parr[] = { arr1, arr2, arr3 };	//定义一个指针数组,存放三个数组的首元素地址
	char** p = parr;	//定义一个二级指针,指向指针数组
	
	int i = 0;

	for (i = 0; i < 3; i++)
	{
		printf("%s\n", parr[i]);
	}
	return 0;
}

这里定义了一个指针数组,通过遍历指针数组访问到指针数组的内容.

在这里插入图片描述
在这里插入图片描述

8. 结构体的声明

8.1 结构的基础知识

结构是一些值的集合,这些值称为成员变量.结构的每个成员可以是不同类型的变量.
数组是一组相同类型元素的类型,而结构体则是一组不一定相同类型的元素.

  • 当一些图书信息(包括价格,出版社,作者等等)等复杂的对象,不能用内置类型用来表示的,那么就需要用到结构体类型来描述复杂的类型

8.2 结构的声明

struct tag
{
 member-list;
}variable-list;

例如描述一个学生:

#include <stdio.h>

struct Stu
{
	//成员变量,是用来描述结构体对象的相关属性的
	char name[20];
	int age;
	char sex[5];	//男 女 保密
};

int main(void)
{
	//int a = 1;
	struct Stu s1;	//通过类型创建变量

	return 0;
}
  • 结构体类似于图纸,而创建结构体变量相当于对着图纸造房子.

8.3 结构成员的类型

结构的成员可以是标量,数组,指针,甚至是其他结构体.

8.4 结构体变量的定义和初始化

struct Point
{
 int x;
 int y;
}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};

struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};

struct Stu s = {"zhangsan", 20};//初始化

struct Node
{
 int data;
 struct Point p;
 struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

9.结构体成员的访问

  • 结构体变量访问成员
    结构体变量的成员是通过点操作符(.)来访问的,.操作符接受两个操作符.
  • 结构体指针访问成员
    结构体指针访问成员是通过->来访问的
    例如:
#include <stdio.h>

struct Stu
{
	//成员变量,是用来描述结构体对象的相关属性的
	char name[20];
	int age;
	char sex[5];	//男 女 保密
};

void print(struct Stu* ps)
{
	//使用点操作符访问结构体成员
	printf("name = %s, age = %d, sex = %s\n", (*ps).name, (*ps).age, (*ps).sex);
	//使用->操作符访问结构体成员
	printf("name = %s, age = %d, sex = %s\n", ps->name, ps->age, ps->sex);
}
int main(void)
{
	//int a = 1;
	struct Stu s1 = { "张三", 23, "男" };	//通过类型创建变量
	print(&s1);

	return 0;
}

结果如下:
在这里插入图片描述

上面的代码,我定义了一个print函数用来打印结构体变量的内容,传递了一个结构体指针类型变量.

如果我需要使用点操作符(.)来访问结构体成员,则需要先使用解引用操作符*得到结构体指针所指向的结构体变量,然后使用.操作符来访问结构体成员.

如果我不想用解引用操作符,也可以直接用->操作符用来让结构体指针直接访问结构体成员.

10. 结构体传参

#include <stdio.h>

struct Stu
{
	//成员变量,是用来描述结构体对象的相关属性的
	char name[20];
	int age;
	char sex[5];	//男 女 保密
};

void print1(struct Stu s)
{
	printf("name = %s, age = %d, sex = %s\n", s.name, s.age, s.sex);
}

void print2(struct Stu* ps)
{
	printf("name = %s, age = %d, sex = %s\n", (*ps).name, (*ps).age, (*ps).sex);
}

int main(void)
{
	struct Stu s1 = { "张三", 23, "男" };	//通过类型创建变量
	print1(s1);		//传值
	print2(&s1);	//传址

	return 0;
}

上述代码有两个打印函数.

一个是传参函数,一个是传址函数.这两个函数完成的功能是一样的,那么哪一个更推荐呢?

答案是推荐传址函数.

  • 因为我们自定义的结构体有可能非常大,包含了各种各样的类型,如果我们直接传参,就需要拷贝一份占据同样内存大小的结构体变量,这是需要时间和空间的.
  • 而使用指针变量,无论该指针变量所指向的变量本身所占据多少内存空间,指针变量所占据的空间大小都是固定的4字节/8字节,这大大减小了函数参数压栈的时间和空间.

本章完.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言:初阶指针和结构体 的相关文章

  • 程序员工资真的高吗?

    一直以来都很奇怪的事情 每当别人知道我晚上7点才下班 他们几乎都是会发出这样的惊呼 你居然这么晚才下班 而每当这个时候又轮到我来惊呼了 这么早7点就下班还算晚 每次打听别人的下班时间 要么5点30就下班了 要么6点就下班了 打听完了 这个时
  • 【A-Star算法】【学习笔记】【附GitHub一个示例代码】

    文章目录 一 算法简介 二 应用场景 三 示例代码 Reference 本文暂学习四方向搜索 一 算法简介 一个比较经典的路径规划的算法 相关路径搜索算法 广度优先遍历 BFC 深度优先遍历 DFC Di jkstra算法 最短路径问题 D

随机推荐

  • 实现 strStr() 函数

    实现 strStr 函数 给定一个 haystack 字符串和一个 needle 字符串 在 haystack 字符串中找出 needle 字符串出现的第一个位置 从0开始 如果不存在 则返回 1 示例 1 输入 haystack hell
  • STM32 电机教程 30 - 无刷无感入门2

    前言 无刷直流 Brushless Direct Current BLDC 电机是一种正快速普及的电机类型 它可在家用电器 汽车 航空航天 消费品 医疗 工业自动化设备和仪器等行业中使用 正如名称指出的那样 BLDC 电机不用电刷来换向 而
  • 面渣逆袭:计算机网络六十二问,三万字图文详解

    基础 1 说下计算机网络体系结构 计算机网络体系结构 一般有三种 OSI 七层模型 TCP IP 四层模型 五层结构 三种网络体系结构 简单说 OSI是一个理论上的网络通信模型 TCP IP是实际上的网络通信模型 五层结构就是为了介绍网络原
  • 小程序锚点定位(通讯录)

    1 使用视图容器组件scroll view 2 在scroll view中设置属性scroll into view 属性值为锚点id 3 在需要的位置设置锚点ID值 点击后将scroll into view设置为对应的id即可跳转 示例 名
  • idea空Maven项目部署Tomcat提示:no artifacts configured 、以及后续 No artifacts marked for deployment解决方案

    no artifacts configured 没有配置artifacts 关于artifacts的介绍推荐大家看这篇博文点击查看 我们先退到项目界面 右击项目名导入web程序所需的库 导入成功后继续配置tomcat 点击之后我们发现又报了
  • 聊聊网络安全等级保护“能力验证”:配置核查(Linux系统)

    前言 作为测评机构一定会知道每年会举行 能力验证 活动 申请认可和获准认可的合格评定机构通过参加能力验证活动 包括 CNAS 组织实施或承认的能力验证计划 测评机构间比对和测量审核 证明其技术能力 之前的线上会议也表明了对测评机构能力建设方
  • 关于Layui后台ajax返回问题

    刚接触LayUI 几次出现 返回的数据异常 问题 皆是因为数据的格式不对 这里做个小笔记 一般的格式这个样子 data result array code gt 0 msg gt count gt 100 data gt
  • pycm库详解_机器学习评价

    详细介绍pycm库的使用方法
  • qqkey获取原理_qqkey获取器下载

    qqkey获取工具 QQkey控制接收生成器 是一款类似于灰鸽子的一款软件 不同的是不能不能盗号 在你的好友打开了你配置的文件后 你可以在工具中获取到好友的QQkey信息 可以把对方的QQ当做的自己的QQ一样 想干嘛就干嘛 赶紧来下载使用吧
  • 在 esmodule 环境下使用 jsts

    文章目录 1 说明 Geometry 类 2 安装 3 使用 以 buffer 为例 4 JTS 文档 1 说明 jsts 完全就是根据其老妈 jts 的 java 包结构移植的 除了部分分析功能需要额外注意外 基本上所有的子模块的根路径位
  • C++ 中隐藏DOS调用的命令行窗口

    转自 http hi baidu com jackyho2000 blog item b5c5fabdd3b4db0019d81fbb html 我演示了一下在MFC程序中怎么应用DOS的dir的命令 可是我们遇到了需要解决的问题 首先就是
  • 包含抽象方法的类是抽象类吗?

    如果这个问题的范围是围绕 类 那么 包含抽象方法的类一定是抽象类 因为抽象方法的特征是 必须定义在抽象类中 但是话由说回来了 因为在JAVA中还有一种允许使用抽象方法的结构 那就是接口 在接口中的方法只能是抽象方法 不允许出现具体实现的方法
  • Python,创建map

    import matplotlib pyplot as mpp import os random math matplotlib version 3 5 1 numpy version 1 21 5 创建画布及坐标轴 def set cav
  • C语言二级题库(刷题软件+60套真题+填空题+大题)2022年9月份新题第三套

    文章只能发选择题 刷大题去刷题软件效果会更好 刷题软件 gongzhonghao 露露IT 选择题 1 下面选项中不是关系数据库基本特征的是 A 不同的列应有不同的列名 B 不同的列应有不同的数据类型 C 与行的次序无关 D 与列的次序无关
  • 业内常用即时传输网盘

    工具名称 业内常用即时传输网盘 功能简介 无需登录 短时间内有效 多用于传输小型敏感文件 外部链接 请见文内 内部网盘链接 在线站点 无网盘链接 使用说明 许多安全行内人士在团队内互传敏感文件时 为实现上传和下载文件时提供较快的速度和数据隐
  • 为什么我不使用JetBrains的屌炸天编辑器

    首先解释一下 JetBrains并没有出过一款叫做 屌炸天 的编辑器 作为一个提到编辑器圣战内心就无比激动的码农 我使用过JetBrains的大部分产品 从开始的PhpStorm 到PyCharm Intellij IDEA CLION a
  • XAML基础控件

    常用布局控件
  • QT内存管理

    Qt内存管理机制 Qt 在内部能够维护对象的层次结构 对于可视元素 这种层次结构就是子组件与父组件的关系 对于非可视元素 则是一个对象与另一个对象的从属关系 在 Qt 中 在 Qt 中 删除父对象会将其子对象一起删除 C 中delete 和
  • mmap和常规文件操作的区别

    mmap和常规文件操作的区别 对linux文件系统不了解的朋友 请参阅我之前写的博文 从内核文件系统看文件读写过程 我们首先简单的回顾一下常规文件系统操作 调用read fread等类函数 中 函数的调用过程 1 进程发起读文件请求 2 内
  • C语言:初阶指针和结构体

    1 指针是什么 指针理解的两个要点 指针是内存中的一个最小单元的编号 也就是地址 平时口语说的指针 通常指的是指针变量 是用来存放内存地址的变量 为了管理计算机内存空间 会把内存分为一个一个一个小的内存单元 每个内存单元占一个字节的空间 那