C语言指针的入门详细介绍

Posted 意愿三七

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言指针的入门详细介绍相关的知识,希望对你有一定的参考价值。

文章目录


前言

指针是c语言的重中之重


(一)指针初阶:


一 : 什么是指针

其实指针有两层含义:

1.指针是地址是一层含义

2.当地址或者指针需要存起来的时候,我们需要一个指针变量这个指针变量是存放地址的,我们经常说的“指针“,”指针“,其实是指针变量,指针变量也被称为指针


二 : 什么是野指针

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

简单的说:你在大街上看见一条狗,那个狗没有栓链子,没有主人,我们称它为野狗,指针也是一样,不知道指向内存的什么地址,就被称为野指针。

什么情况下会造成野指针呢?

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;

这里画一个图表示一下:

arr数组就给了10个空间,只到下标九,但是上面的for循环循环了11次,可以得出已经访问超过我们的arr数组大小了所以会出现指针越界访问的情况。

3 . 指针指向的空间释放

int* test()

	int a = 10;  //(1)第一次进来创建a   出去test函数 空间给操作系统
	return &a;


int main()

    int *p=	test();  
	*p = 20;   //(2)还是第一次的地址 ,但是出来了第一次的地址已经还回去了,
				//(3)这时候进行赋值,会造成非法访问  p野指针
	return 0;

来个例子:你和你女朋友分手了,但是你还有女朋友的电话,你就三番两次打电话给她,但是她已经不是你女朋友了,你这样就属于非法骚扰了…


如何避免野指针

1.指针初始化

要养成好的编程习惯,当变量不知道赋什么的话,可以先赋个0,或者相对应类型的值 int a = 0;

指针不知道应该初始化成什么值的时候,建议直接初始化为NULL。

#include <stdio.h>
int main() 

	int* p = NULL; //NULL要引用头文件
	return 0;

2.小心越界

C语言本身不会检查数组的越界,要我们自己保存头脑清晰


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

要使用的代码被释放了,及时置NULL

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

先来看看下面的代码可以运行起来不?

int main() 

	int* p = NULL;
	*p = 10;
	return 0;


答案是:不可以的,为什么呢?
NULL其实是一个地址位置是在0处的一个地址,这个地址在操作系统里,没有分配给用户,所以不可以把10放进去赋值。

那么怎么规避呢?可以在使用之前先判断一下

int main() 

	int* p = NULL;
	if (p!= NULL)  //不是空才继续使用,增加代码的健壮性
	
		*p = 10;

	
	return 0;

上面的方法讲完就不会遇见野指针了吗?其实不然,你要写BUG谁也难不住,教的是怎么避免少写,养成好习惯!!!


三 : 指针的运算

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

1.指针的加减整数运算

代码包含指针加减整数和指针的关系运算

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

     *vp++ = 0;

上面的代码大家可以试着看一下,下面有解析:

解析:

定义了一个值为5



定义了一个数组长度为5,下面是名为values的数组


指针变量 vp


for (vp = &values[0]; vp < &values[N_VALUES] ; )

*vp++ = 0;


vp < &values[N_VALUES] 小于第5个元素


*vp++ = 0;

vp指向的是第一个数组下标为0的,条件是小于数组第5个元素就停止,所以最后代码全部运行起来,就是把数组0-4的下标全部赋为0


随着数组下标的增长, 地址其实是由低到高变化的


2.指针 - (减)指针

问:下面代码显示的是什么?

int main()

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

答案:9

解析:

指针减指针 得到的两个指针之间的元素个数
指针和指针相减的前提是:两个指针指向的同一个空间

&arr[9] 和&arr[0] 分别指向的是,他们之间有9个元素,所以是9


指针和指针相减的前提是:两个指针指向的同一个空间

int main()

	int arr[10] =  1,2,3,4,5,6,7,8,9,10 ;
	char c[5]; 
	printf("%d\\n",&arr[9] -&c[0]);//e
	return 0;

上面代码块是错误的。


3.使用指针减指针模仿写一个strlen

我们都知道指针减指针就可以得到元素个数,那么来看一下接下来的模仿strlen函数的思路吧!

找到a的地址,和 \\0的地址(每一个字符串后面都有一个隐藏的\\0),让它们相减就可以求出字符串的长度了

下面放代码:

int  my_strlen(char* str)

	char* start = str;  //保存一开始的地址   start 里面是 a的地址

	while (*str != '\\0') //  如果str不会最后一个\\0就不会停下来
	
		str++;         //一直加到\\0
	
	 
	return str - start;   //  '\\0' - 'a'    指针减指针

int main() 

	int a = my_strlen("abc");
	printf("%d\\n",a); // 打印3
	return 0;


四 : 指针和数组

想学好指针,你必须先知道数组名是什么!!
数组名是数组首元素地址

代码来验证一下:

int main()

	int arr[10] =  0 ;
	printf("%p\\n",arr);   //数组名是首元素地址
	printf("%p\\n",&arr[0]);  //取第一个元素的地址

	return 0;

果然地址是一样的!


那么数组名是一个地址,是不是也可以拿指针来接收

int arr[10] =  0 ;
int* p = arr;

看下面的代码:

既然arr是首元素地址 ,但是可以拿p指针来接收, 是不是 (arr == p)呢?

跑一下下面的代码:

int main()

	int arr[10] =  0 ;
	int* p = arr;
	int i = 0;

	for ( i = 0; i < 10; i++)
	
		printf("%p <==> %p <==> %p \\n",&arr[i] , p+i ,arr+i);  
		//p+i 访问第i个元素地址
	
	
	return 0;

结果:好家伙3个地址一模一样,所以上面的问题是成立的

有了上面的例子,我们可以推导出来一个更加有趣的代码例子:
(加法支持加法交换律)

int arr[10] = 1,2,3,4,5,6,7,8,9,10;
int* p = arr;

arr[2] <==> *(arr+2) <==> *(2+arr) <==> 2[arr] 
*(p+2) <==> *(2+P) <==> 2[p] <==> p[2]

以上都是可以支持打印出来正确的数字,但是为什么呢?

[ ]是一个操作符 arr[ 2] , arr 和 2 是两个操作数

其实 arr [ 2 ] 这种写法编译器最终会把它转换为 *(arr+2) ,arr表首元素地址 加上2 就是跳过2个元素 解引用就得到最终的结果了


五 : 二级指针

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

看看下面代码:

int main()

	int a = 10;   
	//为什么要int*呢
	int *pa = &a;  //a是int类型 *pa表示pa是个指针变量 所以是int*
	int* *ppa = &pa;  //int* 是因为pa是int*类型  *ppa 表示ppa是个指针
	return 0;

有个a变量,在内存开辟一个内存空间,地址是0x11223344

int a = 10;

int* pa = &a;
把a的地址放在pa的内存空间里面。 pa的地址是0x44332211

int** ppa = &pa;

把pa的地址放在ppa的内存空间里面。 ppa的地址是0x22334411

为什么叫二级指针呢,因为指向的有两层关系,比如我现在要修改 a 的值要怎么修改呢?

**ppa == *(pa) == 把pa解引用 自然可以修改到值


六 : 指针数组

指针数组是指针还是数组?
答案:是数组。是存放指针的数组。

int arr1[5]: 整形数组 -存放整形的数组就是整形数组

char arr2[6]: 字符数组 - 存放的是字符

int* arr3[5]:整形指针数组: arr3是一个数组,有五个元素,每个元素是一个整形指针。


(二)指针进阶:

一 : 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char*, 一般是这样使用的:

int main()

    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;

还有一种使用方式如下: 把字符串放在字符指针变量中。

int main()

    char* pstr = "hello world";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\\n", pstr);
    return 0;

那么出现一个问题:char* 就算是在64位的机器是最多也是存8个字节啊,把hello world放进去可以放的下吗?

其实:关于上面的问题,解释是:本质上是把字符串的首字符地址存储在了pstr中,可以代码验证一下:如果是第一个字符那打印出来的是 h


果然一模一样!!


来一个练题:

#include <stdio.h>
int main()

	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	char* str3 = "hello bit.";
	char* str4 = "hello bit.";
	if (str1 == str2)
		printf("str1 and str2 are same\\n");
	else
		printf("str1 and str2 are not same\\n");
	if (str3 == str4)
		printf("str3 and str4 are same\\n");
	else
		printf("str3 and str4 are not same\\n");
	return 0;

输出什么?

答案:

为什么呢?来看下面的解析

首先我们可以看到可以有4个变量,先看一下str1 和str2 .

它们是两个不相同的数组,所以分别指向不同的地址

来看看str3和str4,因为str3 和str4存放的是常量字符,常量字符是不可以被改变的,所以在内存存放的是同一个内容,2个变量同时指向同一个地址一起使用,所以应该是下图所示:

所以最后就是上面的那个结果,对于这道题目的最大争议就是 str3 和str4存的变量是不是常量变量不可以被修改,我们可以去编译器看一下:

可以看见这里是报错的,所以说我们上面所述的如实。


二: 指针数组

指针数组本质上是数组,数组中存放的是指针(地址)

应用场景:

int main()

	int a[3] =  1,2,3 ;
	int b[] =  4,5,6 ;
	int* c[3] = a,b;
	int i = 0;
	for ( i = 0; i < 2; i++)
	
		int j = 0;
		for ( j = 0; j < 3; j++)
		
			printf("%d ",c[i][j]);
		
		printf("\\n");
	
	return 0;



三 : 数组指针

数组指针是指针?还是数组?
答案是:指针。

下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

解释:

p1是指针数组
p2是数组指针,
解释:p2先和结合,说明p2是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于
号的,所以必须加上()来保证p先和*结合


&数组名VS数组名

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:

#include <stdio.h>
int main()

int arr[10] = 0;
printf("%p\\n", arr);
printf("%p\\n", &arr);
return 0;


可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:

#include <stdio.h>
int main()

int arr[10] =  0 ;
printf("arr = %p\\n", arr);
printf("&arr= %p\\n", &arr);
printf("arr+1 = %p\\n", arr+1);
printf("&arr+1= %p\\n", &arr+1);
return 0;

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

数组名是数组首元素的地址
但是有2个例外:
1.sizeof(数组名) - 数组名表示整个数组,计算的是整个数组的大小,单位是字节

2.&数组名 - 数组名表示整个数组 ,取出的是整个数组的地址


四 : 一维数组传参、二维数组传参

数组的传参,使用以下形式都可以

(1):一维数组传参

#include <stdio.h>
void test(int arr[])//ok

//不在[]里面写10,是因为传过来的是首元素地址,并不会真的开辟一个10个数组的大小,可省,写了也不影响
void test(int arr[10])//ok

//同上
void test(int *arr)//ok

//传过来的是首元素地址,所以可以拿1级指针。
void test2(int *arr[20])//ok

void test2(int **arr)//ok

int main()

int arr[10] = 0;
int *arr2[20] = 0;
test(arr);
test2(arr2);

这里解释一下为什么 void test2(int **arr) 也可以:

arr里面的类型都是1级指针,所以放在2级指针存放没有什么不合适。


(2):二维数组传参

二维数组,首元素是第一行地址

void test(int arr[3][5])//ok  

//实参是2维数组,
void test(int arr[][])//no

//可以省略行,不可以省略列
void test(int arr[][5])//ok

//只省略了行,没有省略列
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//no

//这个是传过来第一行的地址
void test(int* arr[5])//ok?

void test(int (*arr)[5])//ok?

//arr是一个指针,指向的是5个int类型,实参传过来的是首元素地址,也就是第一行地址,第一行地址是int类型,所以可以
void<

以上是关于C语言指针的入门详细介绍的主要内容,如果未能解决你的问题,请参考以下文章

C语言指针的入门详细介绍

C语言动态开辟指针的指针问题

C语言指针(指针数组数组指针函数指针传参回调函数等)超详细

浅谈C语言的动态内存开辟

C 语言----- 指针

C语言学习笔记(16)动态内存管理