c语言---指针&&结构体篇

【前言】本系列(初阶)适用于初学者课前预习或者课后复习资料,包含了大部分基础知识点梳理与代码例方便看官理解,有问题请指出。本人邮箱地址:p772307283@outlook.com

 可爱捏

目录

1.指针

1.1引入

1.2指针与指针类型

1.3二级指针

1.4指针数组

2.结构体

2.1结构体是什么以及如何声明,定义,初始化

2.2访问与传参


1.指针

1.1引入

我们口头上所指的指针一般来讲指的是指针变量。指针变量是一个专门用来存放地址的变量。而真正的指针代表着地址,是内存中最小的单元的编号。

那什么叫内存呢?

精炼的讲,电脑在运行程序时会将其加载到内存中,在此期间会对内存空间进行一个调用。一个个的内存单元是内存最小组成单位,每一个内存单元都有独特的一串数字为其进行标识。我们称为地址。并且我们可以通过&(取地址操作符)取出变量的内存实际地址,其实就是第一个字节的地址。例如int占4个字节,那么&会取出其第一个字节的地址。

来看一个简单的指针的例子。

int main()
{
	int a = 10;
	int* p = &a;
	//为a开辟了一块空间存储了10,使用了取址操作符取出了a的第一个字节地址,并存到了p中
	return 0;
}

这就是一个简单的指针的运用。

那指针变量的大小是多少呢?是否和前面的数据类型有关呢?往下看。

对于32位机器而言,有32位地址线,那么通过产生高低电压的情况,会在每位上生成1或者0,那么就会有2^32种情况了。相应的,就有2^32个地址了。

每个地址占一个字节,那2^32个地址大概就是4gb的空间。同时,因为一个地址是32位,所以是4btye,也就是4个字节。

相应的,如果是64位机器,则为8个字节。

综上,指针的大小在32位平台上是4个字节,但是在64位平台上就是8个字节了。

接下来进入正题。

1.2指针与指针类型

变量都有各种类型来进行区分,那么指针呢?肯定也是存在的。

但是正如上面讲的,指针的大小被地址线所限制,也就是和类型并没有关系

那为何要指针规定类型呢?他仅仅的只是为了存储相同类型的变量的地址吗?

让我们通过几个示例来了解指针类型的作用。

指针加减整数:

int main()
{
	int n = 1;
	int* pi = &n;
	char* pc = (char*)&n;//必须要强制类型转换,否则会有警告或报错 
	printf("%p\n", &n);
	printf("%p\n", pi);
	printf("%p\n", pi+1);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	return 0;

}

结果

000000492372F7C4
000000492372F7C4
000000492372F7C8
000000492372F7C4
000000492372F7C5

可以看到的是1,2,4都得到了相同的地址。说明pi和pc作为指针发挥的作用是正常的。但是pc+1和pi+1却有所区别了。pi+1从c4变到了c8,pc+1却从c4变成了c5

说明了什么问题?指针的类型决定了指针向前或向后走一步的距离有多大

还有其他的用处吗? 

解引用操作:

在指针前加上一个*就是解引用操作,作用是修改*pi相当于修改n。此时的*pi就等于n

int main()
{
	int n = 0x12345678;
	int* pi = &n;
	char* pc = (char*)&n;
	*pc = 0;
	*pi = 0;
	return 0;
}

通过监视窗口来看内存的变化。

初始的时候

 当经过了*pc

 可以发现78变成了00

再经过*pi

 全部变成了0

这是怎么一回事呢?

指针的类型决定了对指针解引用时可以有多大的权限,很明显char型指针就只有访问一个字节的权限,所以只把78改成了00,而int型则具备访问4字节的权利。

综上,指针的不同类型其实就是提供了不同的视角来观看访问内存时的情况。

再扩展讲讲野指针

什么叫野指针呢?指针的指向位置是随机的或者不正确的,没有明确的限制的,诸如此类我们就把他称作野指针。

什么情况会出现野指针呢?

指针未初始化

int* p;

 指针越界访问

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* p = arr;
	for (int i = 0; i < 6; i++)
	{
		*(p++) = i;
	}
}

指针越界了,i在此循环了6次。

指针指向的空间被释放 

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d", *p);
}

运行会成功,这是因为test所创建的栈帧暂时还没有被销毁

如果在printf前加上一句其他的,结果就变了。

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("you're wrong!\n");
	printf("%d", *p);
}

结果为

you're wrong!
-858993460

这是因为函数在调用时会创建栈帧,当函数调用完,为了节省空间就会对栈帧进行销毁。当我调用另一个printf时,原先的test就会被销毁,那么指针就找不到地址了,也就成了一个野指针。

具体的博客可以参考

(2条消息) 从底层深度分析栈帧(c语言)_丞子_516的博客-CSDN博客

所以对于野指针一定要小心又小心。

指针与指针也可以相减。但前提是两个指针都同时指向同一片空间才行。

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d", &arr[9] - &arr[0]);
}

得到的结果是9,由此可见,其相减得到的结果是指针之间的元素个数

但注意,如果是两数掉过来相减,得到的就是-9

这个性质还可以用于计算字符串的长度。 

int main()
{
	char s[] = "abcdef";
	char* p1 = s;
	char* p2 = s;
	while (*p2 != '\0')
	{
		p2++;
	 }
	printf("%d", p2 - p1);
}

需要注意的是,因为s已经代表地址,所以不再需要&,另外,p1默认指向s的首元素地址

再看一例代码

for (p = &arr[5]; p >= &arr[0]; p--)
{
	*p = 0;
}

将arr的五个元素都赋予了0

但需要注意的是

标准规定,允许指向数组元素的指针与指向数组的最后一个元素后面的那个内存位置的指针相比较,但不允许与指向第一个元素前的那个内存位置的指针进行比较。

所以虽然大多数编译器以完成,但你需要明白这样的写法是有错误的。

同时这样并不存在数组越界的情况。因为 指针并未调用越界的下标,只是取到了他的地址而已。这样是不会报错的。

1.3二级指针

变量都是有给分配地址的,那么我们上文提到过,指针也是一个变量,那么指针也是有地址的,其地址就存放在二级指针里。

int a = 10;
int* pa = &a;
int** ppa = &pa;

如上面的代码,为a这块空间放进了10,再将a的地址存放到pa中,将pa的地址存放到ppa中。

从指针的类型就可以看出,a是int型的,pa是int*,*代表他是一个指针int*则代表着这个指针指向int,而ppa是int**,同样的,第二个*代表着ppa是指针,而int*则代表此指针是指向int*的。

二级指针有什么运算呢?

*ppa,也就是解引用操作,可以找到pa,再使用*pa,即得到了a

上面的操作就相当于**ppa

1.4指针数组

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

先来理清一下指针和数组之间的关系。

首先要清楚指针和数组并非是相同的类型。

指针是一个变量,用来存放地址

而数组则是用来存放一组相同类型的元素。

 同时,数组名就是数组首元素的地址

所以我们可以用指针来访问数组。

并且比较灵活的一点,只要知道了首元素的地址,就可以通过下标进行访问。

那么就意味着p[1]可以去访问数组的arr[1].

int arr[]={1,2,3,4,5};
int *p=arr;

而指针数组的本质上仍然是一个数组。而且同整型数组,字符型数组并无太大的区别。

指针数组里面的存放的就是一组相同类型的指针

例如,指针数组可以以1维数组的形式去模拟2维数组。

int main()
{
	int a[] = { 1,2,3,4 };
	int b[] = { 5,6,7,8 };
	int c[] = { 9,10,11,12 };
	int* arr[3] = { a,b,c };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", arr[i][j]);
		}
	}
}

这里的arr[i]就相当于a,b,c,那么arr[i][j]就等于a[j],b[j],c[j]

2.结构体

2.1结构体是什么以及如何声明,定义,初始化

 结构是什么呢?结构就是一些不相同类型的值的集合。

这些值被称作成员变量

就像我们想去描述一个人的特征,可以从名字,年龄,性别,工作入手,将这些成员变量放到结构体中,就完成了声明。

struct person
{
	char name[20];
	char sex[10];
	int age;
	char position[20];
};

当然,这些成员变量可以是各种类型,比如还可以是指针,其他结构体。

如何初始化?

struct person p1;

这样就初始化了一个名为p1的结构体。

也可以进行赋值。

struct person p1 = { "taylor","female",18,"singer" };

结构体的嵌套

struct score
	{
		int math;
		int chinese;
	};
	struct stu
	{
		int age;
		char name[20];
		struct score s;
	};
	struct stu p = { 12,"taylor",{78,89} };

注意,我们一般将结构体放在main函数的外面,因为有时候函数调用时也会用到结构体,倒不如直接放到main的外面,让其可以被反复利用。

2.2访问与传参

结构变量成员是通过点操作符来进行访问的。

struct stu q;
q.age = 12;
strcpy(q.name, "taylor");

这样我们就将q的age改为了12,名字改为了taylor

结构体指针也可以访问变量的成员。

如:

struct stu {
	int age;
	char name[20];
};
void print(struct stu* s)
{
	printf("name:%s,age:%d", (*s).name, (*s).age);
}
int main()
{
	struct stu s = { 30,"taylorswift"};
	print(&s);
}

也有简化操作。

(*s).name

改为

s->name

在这里也可以不用借用传指针的方式传结构体,也可以将整个结构体传进去。

struct stu {
	int age;
	char name[20];
};
void print(struct stu s)
{
	printf("name:%s,age:%d", s.name, s.age);
}
int main()
{
	struct stu s = { 30,"taylorswift"};
	print(s);
}

但仍然只推荐使用指针进行传参

因为函数在传参时,需要进行压栈操作,若是传递一整个结构体进去,那么压栈的开销过大,性能一定会下降。

感兴趣的话可以看看关于栈帧的理解。里面详细解释了函数是如何进行传参的。

 栈帧的底层分析

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值