初始C语言(7)-指针和结构体

不学习学习结构体和指针怎么深入学习C呢!!!今天就来简单讲解讲解!

我们先来看看指针!!

一、指针

1.指针是什么?

指针理解的 2 个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
其实也可以看成 编号 == 地址 == 指针

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

内存:

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

#include <stdio.h>
int main()
{
    int a = 10;//在内存中开辟一块空间

    int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。

    //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
    中,p就是一个指针变量。

    return 0;
}

 总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:

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

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111

这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==
2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,我们一下这篇文章大致就能了解:32位和64位系统支持的最大内存_64位最大内存_Aheaboy的博客-CSDN博客

这里我们就明白:

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

总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。

2.指针和指针类型

指针的类型有哪些呢?

我们往下看

int num = 10;
p = &num;

要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?
我们给指针变量相应的类型。

char *pc = NULL;

short *ps = NULL;

int *pi = NULL;

long *pl = NULL;

float *pf = NULL;

double *pd = NULL;

这里可以看到,指针的定义方式是: type + *
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
那指针类型的意义是什么?

2.1指针+-整数

#include <stdio.h>
//演示实例
int main()
{
    int n = 10;
    char *pc = (char*)&n;
    int *pi = &n;

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

看到运行结果我们就知道了,指针的类型决定了指针向前或者向后走一步有多大(距离)。


 

2.2指针的解引用 

int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0; 
	*pi = 0;

	printf("%p\n", pc);
	printf("%p", pi);
	return 0;
}

总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。


3.野指针

那么何为野指针呢

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

3.1 野指针成因

1. 指针未初始化

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

2.指针越界访问 

#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如何规避野指针 

1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性

#include <stdio.h>
int main()
{
        int *p = NULL;
        //....
        int a = 10;
        p = &a;
        if(p != NULL)
        {
                *p = 20;
        }
        return 0;
}

4.指针运算 

那么指针又有那些运算呢

指针+- 整数
指针-指针
指针的关系运算

 4.1指针+-整数

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

 4.2指针+-指针

注意:

两个指针要指向同一个空间,指针的类型要一致;指针-指针其实得到的就是指针和指针之间元素的个数。

int my_strlen(char * str)
{
	char* start = str;
	while (*str != '\0')
		str++;
	return str - start;//指针-指针得到的是元素个数
}

int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);

	return 0;
}

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.指针和数组

我们先举个例子看看:

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;
}

 我们发现地址一样,由此可得:数组名表示的是数组首元素的地址,但也有两种情况除外哦。

 既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。
举个例子:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
	}
	return 0;
}

我们可以看到 p+i 其实计算的是数组 arr 下标为i的地址。
那我们就可以直接通过指针来访问数组

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",  *(p + i));
		
	}
	printf("\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ",  arr[i]);
		
	}
	return 0;
}

 运行结果:

 6.二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
这就是 二级指针

  • *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa
  • **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a 

7.指针数组

指针数组是数组还是指针呢?

答案是数组哟

那具体是怎么样的呢,我们看下图

int* arr3[5];

arr3是一个数组,有五个元素,每个元素是一个整形指针

那肯定就有小伙伴问那之前看的整形数组那些呢,话不多说直接上图。

int arr1[5];
char arr2[6];

这下明白了吧,指针就先讲解到这里,后面还会有指针的文章,更加详细的讲解指针,初始C语言嘛,不能太难的啦!!!!

下面我们来看看结构体!!!

二、结构体

1.结构体的声明

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。 

例如我们描述一个学生,要描述他的名字,年龄,班级,等等。这个时候就可以使用结构体。

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

描述一个学生 

typedef struct Stu
{
        char name[20];//名字
        int age;//年龄
        char sex[5];//性别
        char id[20];//学号
}Stu;//分号不能丢

typedef就是对结构体名字重命名,方便定义。

int main()

{

        Stu s1;

        return 0;

}
这样就可直接使用,编译器也不会报错

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

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

结构体定义和初始化很简单,用什么定义什么。

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};//结构体嵌套初始化

3.结构体成员的访问

结构体变量访问成员
结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。
例如:

我们可以看到 s 有成员 name 和 age ;
那我们如何访问s的成员?

struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员

结构体指针访问指向变量的成员
有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。
那该如何访问成员。
如下:

struct Stu
{
    char name[20];
    int age;
};

void print(struct Stu* ps)
{
    printf("name = %s age = %d\n", (*ps).name, (*ps).age);
    //使用结构体指针访问指向对象的成员
    printf("name = %s age = %d\n", ps->name, ps->age);
}

int main()
{
    struct Stu s = {"zhangsan", 20};
    print(&s);//结构体地址传参
    return 0;
}

4.结构体传参

我们直接通过代码来看

struct S
{
    int data[1000];
    int num;
};

struct S s = {{1,2,3,4}, 1000};

//结构体传参
void print1(struct S s)
{
    printf("%d\n", s.num);
}

//结构体地址传参
void print2(struct S* ps)
{
    printf("%d\n", ps->num);
}

int main()
{
    print1(s); //传结构体
    print2(&s); //传地址
    return 0;
}

上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:

函数传参的时候,参数是需要压栈的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论:
结构体传参的时候,要传结构体的地址。

以上就是这次内容的讲解,如有不对不妥之处,欢迎大家留言!!!

最后也希望大家看到这里能动动发财的小手点点赞!!!

 

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王朵拉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值