C初阶 - 06指针.

我决定了要成为海贼王便要为此而战,我必须变的更强。———路飞 

阶段1目标:

基本了解C语言语法,站在全局,避去晦涩难懂,鲜明梳理C语言基本概念,为算法竞赛等计算机专业比赛铺好道路。传统功夫讲究点到为止,此阶段仅点明语法知识,后续阶段再进一步精进学习。

目录

1.本章重点

2.什么是指针

2.指针和指针类型

3.野指针

3.1野指针的成因

3.1.1指针未初始化

千万注意指针未初始化的情况,往往在应用中会忘了这一点!!   经典易错问题:结构体指针的初始化

3.1.2指针越界访问

3.1.3指针指向的空间释放

3.3.4如何规避野指针

4.指针运算

4.1指针+-运算

4.2指针-指针

4.3指针的关系运算

4.4指针的解引用操作

5.指针和数组

6.二级指针

 7.指针数组


1.本章重点

1. 指针是什么
2. 指针和指针类型
3. 野指针
4. 指针运算
5. 指针和数组
6. 二级指针
7. 指针数组

2.什么是指针

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

那我们就可以这样理解:

内存

 指针

 指针是个变量,存放内存单元的地址(编号)

对应的代码:

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

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

那这里的问题是:

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

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的,原因解释如下:

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0)
那么32根地址线产生的地址就会是

2^32次方个不同的编码(即:地址),能够管理2^32次方个内存空间
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111

我们知道内存中的数据单位大小分为:

(顺序由小到大)bit   byte   kb   mb   gb   tb   pb    .......

这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址
那问题来了,为什么要按照字节为单位来分配内存地址呢?为什么不按照bit?kb? mb?

因为我们试想一下:

假设啊,我们是按照bite为单位来分配地址,那么char a  = 10,(char是一个字节),那么变量a仅仅一个变量,就要8个bite,即:8个内存地址来,那浪费是不是有点太过于严重了?

那再假设啊,我们是按照kb为单位来分配地址,char a  = 10,仅仅占用一个地址的一小部分,是不是依然浪费很严重?

所以我们取中间的单位:byte,作为地址单位。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结
指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
 

2.指针和指针类型

引子:

	int a = 10;
	int* pa = &a;
	
	char b = 6;
	char* pb = &b;

我们知道,不同数据类型创建的变量,在用指针接收的时候,指针的数据类型要和起初创建变量的数据类型保持一致,我们可以已经用的非常习惯了,但是,我们想一想,指针在32位平台下都是4个字节,为什么要用相对应原始变量的数据类型来定义指针变量的数据类型呢?

我们把各种变量都统一定为 ptr*这种万能的数据变量类型可以吗?(ptr* 只是个例子,有没有我不知道,不要在意~~)

我们来看看下面的不同点,对比一下,你就知道~~

 那我们再用char类型定义的指针变量来接收试试:

 这个时候就出现问题了,char类型定义,改变的内存中的值不全!!!!!!!出大问题

所以,我们明白了不同类型来接收指针变量的意义所在:

1.指针类型决定了指针解引用操作的时候,一次访问几个字节(访问内存大小)

char* 指针解引用访问1个字节

int*   指针解引用访问1个字节

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

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

那么指针类型对 指针+-整数 的意义是什么呢?

总结

2.指针的类型决定了指针向前或者向后走一步有多大(距离)


3.野指针

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

3.1野指针的成因

3.1.1指针未初始化

#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;//通过p中存的随机值作为地址,找到一个空间,这个空间不属于我们当前这个程序,就造成了非法访问,这就是野指针
	return 0;
}

千万注意指针未初始化的情况,往往在应用中会忘了这一点!!   经典易错问题:结构体指针的初始化

(摘文链接:c语言结构体指针初始化 - FreedomQQkiko - 博客园 (cnblogs.com))

定义了指针变量,没有为指针分配内存,即指针没有在内存中指向一块合法的内存,尤其是结构体成员指针未初始化,往往导致运行时提示内存错误。

由于结构体成员指针未初始化,因此在运行时提示内存错误,如此代码:

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

struct student
{
       char *name;
       int score;
       struct student *next;      
}stu, *stul;

int main()
{
       strcpy(stu.name,"Jimy");   
       stu.score = 99;  

       return 0;
}

改:

#include<stdio.h>
#include<malloc.h>
#include<string.h>
  
struct student
{   
    char *name;   
    int score;   
    struct student* next;   
}stu,*stu1;    
  
int main()
{    
    stu.name = (char*)malloc(sizeof(char)); /*1.结构体成员指针需要初始化*/  
    strcpy(stu.name,"Jimy");   
    stu.score = 99;   
    
    stu1 = (struct student*)malloc(sizeof(struct student));/*2.结构体指针需要初始化*/  
    stu1->name = (char*)malloc(sizeof(char));/*3.结构体指针的成员指针同样需要初始化*/  
    stu.next  = stu1;   
    strcpy(stu1->name,"Lucy");   
    stu1->score = 98;   
    stu1->next = NULL;   
    printf("name %s, score %d \n ",stu.name, stu.score);   
    printf("name %s, score %d \n ",stu1->name, stu1->score);   
    free(stu1);   

    return 0;   
}  

3.1.2指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int* p = arr;
	for (i = 0; i <= 10; i++)//等于10的时候,指针越界访问,造成指向一块不属于自己的空间,野指针
	{
		*p = i;
		p++
	}

	return 0;
}

3.1.3指针指向的空间释放

#include <stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);

	return 0;
}

解释:

我们定义test()作为一个函数被调用,test返回值是a的地址,所以int* test( ),然而,函数调用完就会在内存中释放这块空间,在main函数中*p接收test()函数,即:a的地址,其实这个时候是存在风险的,因为虽然地址还存在但是这块空间已经在内存中不存在了,再访问是存在风险的

举个栗子~:

小明在学校旁边开了个宾馆,只住了一晚上,之后他住完感觉宾馆环境不错很舒适,于是推荐-朋友小军第二天来住,小明打电话说:”小军,学校旁边的宾馆很不错,我住的是606房间,你明天来试试,很舒适的!!“,第二天,小明退房了,半下午小军来了,对服务员说:”带我去606房间“,这个时候就很奇怪了,小明走了,房间已经不属于他了,那606是一个不属于自己的房间,随意进出是存在风险的(比如:被揍)

我们运行一下,看看结果:

咦~?结果为啥还是10?

接着拿上个例子:

这个时候小军再去访问606房间的确是有风险的,但是有可能这个房间此时是还没有入住人的,还依旧存在小明入住过的痕迹

那我们来提前输出打印一串别的呢?

 随机打印了7这个数字。

接着上个例子:

小军访问606房间,而这个房间已经没有了小明的入住痕迹,并且已经有了其他人继续入住(房间被人打扫了),这时候就风险发生了。

 用堆栈的知识解释就是:被覆盖了

3.3.4如何规避野指针

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

1.指针初始化

	//当指针有明确指向的时候
	int a = 10;
	int* p = &a;

	//当指针不知道事先不知道要只想谁的时候
	int* p = NULL;

解释:NULL实质上是0,写0在语法上也是行得通的,只不过在初始化指针的时候习惯上写成NULL。(一般都写成NULL

2.小心指针越界

在写边界值的时候要特别小心,以防越界访问。

3.指针指向空间释放及时置NULL

~后续讲解动态内存释放的时候会补充。

4.指针使用之前检查有效性

 这样也是不对的,对空指针不能进行解引用操作,是非法的!!

举个例子(.滑稽.dog~~)

就相当于路上有一条野狗(未初始化的野指针),好心人看到了,把他拴在了路口的树上(野指针进行初始化成空指针,即:int* p = NULL),那么我们只要不去碰他就不会遭到伤害(不去对空指针解引用),如果访问就会遭到攻击(非法!!)

 5.避免返回局部变量的地址

尽量避免这样写

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指针-指针

你可能会像指针有运算:指针-指针,那有没有  指针+指针  呢?

——————没有,类比一下:

日期 +- 天数 (指针+-运算)

日期 - 日期  (计算日期之间的差值)(指针-指针)

日期 + 日期 (没有任何意义!!!!)

思考这段代码运行结果:

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

	return 0;
}

 结果是9  -9 ,这是为什么呢?

指针-指针,得到的数字的绝对值是指针与指针之间的元素个数。

 那么再继续思考,这样一段代码:

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

	printf("%d\n", &a[9] - &b[0]);

	return 0;
}

两个不同的指针相减,得到的是什么?—————— 这种写法是错误的!!不允许这样写!

总结

指针减指针的前提是这两个指针指向同一块区域。

指针-指针,得到的数字的绝对值是指针与指针之间的元素个数。

指针-指针应用:

我们知道利用库函数strlen可以求得字符串长度,如代码1;当然也可以使用自定义来求字符串,如代码2:

代码1

//求字符串长度
#include <stdio.h>
int main()
{
	char arr[] = "abcdef";
	int len = strlen(arr);

	printf("%d\n", len);

	return 0;
}

代码2

//求字符串长度
int my_strlen(char* s)
{
	int count = 0;
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}
#include <stdio.h>
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);

	printf("%d\n", len);

	return 0;
}

那么可以在自定义函数mystrlen中使用指针减指针的求法来求解长度吗?当然可以!并且很妙~~

//求字符串长度
int my_strlen(char* s)
{
	char* start = s;
	while (*s != '\0')
	{
		s++;
	}
	return s - start;
}
#include <stdio.h>
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);

	printf("%d\n", len);

	return 0;
}

4.3指针的关系运算

我们来比较看看这两种那个代码写法:

代码1:

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

代码2:

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

他们在VS2019编译器环境下,运行结果是一样的,那么这两种有什么区别呢??

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

标准规定

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

4.4指针的解引用操作

不再赘述。。

5.指针和数组

数组和指针的区别

数组:

数组是一块连续的空间,放的是相同类型的元素

数组大小:和元素类型,元素个数有关。

指针(变量):

指针是一个变量,放地址

指针变量的大小:32位平台下是4byte,64位平台下是8byte

数组和指针的联系

数组名是首元素的地址(一般情况下,但是有两个例外)

两个例外

  1. sizeof(数组名)   这里的数组名不是表示首元素的地址,而是表示整个数组的大小,单位是字节
  2. &数组名     这里的数组名不是表示首元素的地址,而是表示整个数组,拿到的是整个数组的地址

指针与数组建立联系

#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));
	}
	return 0;
}

6.二级指针

#include<stdio.h>
int main()
{
	int a = 10;
	int* p = a;//一级指针
	int** pp = &p;//二级指针
	
	return 0;
}

首先来看图叭~

 解释

int类型的a,占4个字节,int* p = &a,所以p中存放的是a的首地址,即:p开辟一块空间,在这个空间中放的是a的地址,int* *pp = &p,所以pp指的是p这块空间的地址,即:pp中存放的是p的地址,那么pp就构成了二级指针,即:指向指针的指针

当然,可以无限套娃,三级指针,四级指针...,但是意义不大,我们今天就拿二级指针来举例子叭~~

可能你觉得直接看int*或者int**不好理解,那么我们来解释一下:


#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;//一级指针
	int* * pp = &p;//二级指针
	int** * ppp = &pp;//三级指针

	return 0;
}

就拿这段代码举例子:

  • int* p = &a;指的是:a的地址,用变量p来存放,因为存的是地址,所以加*,而a是int类型,所以int* p = a;
  • int* * pp = &p;指的是:p的地址,*pp告诉我们,后面的&p是个地址,所以用指针接收,而第一颗*告诉我们,pp指向的p的类型是个指针
  • int** * ppp = &pp;指的是:pp的地址,8ppp告诉我们,后面的&pp是个地址,所以用指针接收,而前面两颗*告诉我们,ppp指向的pp的类型是个int**类型
#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;//一级指针
	int* *pp = &p;//二级指针

	* *pp = 20;//两层解引用
	printf("%d\n", a);//20

	return 0;
}

 7.指针数组

(本质上是数组)

#include<stdio.h>
int main()
{
	int arr[10];//整型数组 - 存放整型的数组
	char ch[5];//字符数组 - 存放字符的数组

	//类比一下
	//指针数组 - 存放指针的数组
	//int* 整型指针的数组
	//char* 字符指针的数组

	int* parr[5];// 整型指针的数组
	char* pch[6];// 字符指针的数组

	return 0;
}

应用:

代码1:

试想一下,这样写很麻烦的,有100个也这样重复着写??肯定不是,我们还要指针数组来解决这个问题~~

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;

	int* pa = &a;
	int* pb = &b;
	int* pc = &c;

	return 0;
}

代码2:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;

	int* arr[3] = { &a,&b,&c };

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d\n", *(arr[i]));
	}

	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

.阿Q.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值