C语言指针讲解


前言

指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。作为一个C语言初学者,我对指针也有了一定的了解,正好这几天在做一个C语言指针的知识点汇总,于是就有了这篇文章,向大家分享一些我的见解,与大家一起共勉。

为什么要学习指针

  1. 如果你想通过函数改变一个变量的值,就得用指针而不能用值传递,很多时候不同的函数存在很多不同的变量,当程序的数据量十分庞大的时候,我们就需要用指针来作为形参,通过传递地址,达到传递变量的目的。
  2. 指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。
  3. 同样,指针也使得一些复杂的内容变得简单,例如:链表等等;也有一些操作它必须使用指针,例如:申请内存等等。

什么是指针

在计算机中,每一个数据都是存放在储存器中的,而不同的数据类型所占的内存空间不同(例如:int类型占用4个字节,double类型占用8个字节等等),内存空间又是以字节为单位的,每一个字节又对应了一个编号,这个编号我们称为这一个内存单元的地址。

而系统在内存中又为变量分配了存储空间的首个字节单元的地址即变量的地址。为了方便用户对存储空间进行正确的访问,指针便应运而生了。

指针相对于一个内存单元来说,指的是单元的地址,该单元的内容里面存放的是数据。在 C 语言中,允许用指针变量来存放指针,因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。

指针变量是存放一个内存地址的变量,不同于其他类型变量,它是专门用来存放内存地址的,也称为地址变量。定义指针变量的一般形式为:类型说明符*变量名。

指针详解

一、基础知识

在C语言中,定义变量时,如果在变量名前加上一个 “ * ”,那么这个变量就变成了对应变量类型的指针变量。

1、取地址

对于一个指针变量,我们需要让它来保存其他变量的地址的时候,就需要用 到 &运算符。
下面举个例子:

例1

#include<stdio.h>
int main()
{
	int num = 10;//在内存中开辟一块空间
	int* p = &num;//这里我们对变量num取地址,使用了&运算符
				  //将num的地址存放在p变量中,p就是一个指针变量。
	return 0;
}

&num就取得了num的地址,指针p指向的num的地址,就形成了一个简单的指针变量。

但是对于某些特殊情况,我们可以不需要用&运算符,例如:数组,函数等等,我们后边会讲到。

2、解引用

上面我们了解了如何取得一个数据的地址,那么接下来我们可以尝试运用指针来解地址,从而得到这个变量的内存数据。

在指针前加一个“ * "即解引用地址,也就是从指针指向的内存中,取出这段内存地址对应的数据。

输出例1中的指针p:

例2

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

运行结果为:10

3、指针变量

指针就是一个变量,用来存放地址的变量。(存放在指针中的值,均会被看作地址)

一个小的单元会有多大:(一个字节)
那么地址是如何编写的呢?

目前经过仔细的技术及思考后,最合适的结论为:一个字节对应了一个地址。

对于32位机器,可以看作有32根地址线,每一根地址线在寻址的时候都会产生一个电信号(正电1/负电0),所以它的地址从000……000(32个0)到111……111(32个1)共有2的32次方个地址。

在32位机器上,地址是32个0或1组成的二进制序列,一个地址需要用4个字节的空间来存储,所以一个指针变量的大小就是4个字节。

同样,可以类比推理得到:64位机器有2的64次方个地址,一个指针变量的大小位8个字节。

我们来做一个练习:

例3

#include<stdio.h>
int main()
{
	printf("%dn", sizeof(char*));
	printf("%dn", sizeof(short*));
	printf("%dn", sizeof(double*));
	printf("%dn", sizeof(int*));
	return 0;
}

运行结果为:
4
4
4
4

总结
(1)指针是一个变量,它存放的是地址。
(2)指针在32位机器中占4个字节,在64位机器中占8个字节,与类型无关。

二、指针与指针类型

1、指针类型

我们前边提到,指针的定义方式是:type + * :
char* 类型的指针是为了存放char类型变量的地址;
int* 类型的指针是为了存放int类型变量的地址;
short* 类型的指针是为了存放short类型变量的地址;
double* 类型的指针是为了存放double类型变量的地址。

指针的类型决定了指针向前或者向后的空间有多大。

指针类型决定了指针进行解引用操作的时候,能够访问空间的大小:
(1) int* p:* p能够访问4个字节。
(2)char* p:* p能够访问1个字节。
(3)double* p: *p能够访问8个字节。

指针类型的意义

先来看一个例子:

例4

#include<stdio.h>
int main()
{
	int a = 10;
	char* p1 = (char*)&a;
	int* p2 = &a;

	printf("%pn", &a);
	printf("%pn", p1);
	printf("%pn", p1+1);
	printf("%pn", p2);
	printf("%pn", p2+1);
	return 0;
}

输出的结果为:
003CFC80
003CFC80
003CFC81
003CFC80
003CFC84

我们可以看到不同类型的指针p1和p2都指向了变量a的地址,但是p1+1指向的是003CFC81,而p2+1指向了003CFC84。一个地址向后移动了1,一个地址向后移动了4。

这是因为:指针的类型决定了指针向前或者向后移动一步有多大的距离。

2、野指针

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

(2)成因:
①指针未初始化
例5

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

②指针越界访问
例6

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

③指针指向的空间释放
例7

#include<stdio.h>
int* func()
{
	int a = 10;
	printf("%pn", &a);
	return &a;
}
int main()
{
	int* p = func();
	//返回的地址是跟a的地址一致 
	printf("%pn", p);
	//但指针对应的值却不是变量a原来的值了 
	printf("%dn", *p);
	return 0;
}

运行结果为:
0093F7F4
0093F7F4
10361167

这里的func函数虽然确实返回了地址,而p也确实接受到了返回的地址,但是当返回的时候,已经来不及保存了,因为func函数一结束,函数申请的内存等等就返回给操作系统了,已经无法再通过指针p去访问a的地址了,后边再用*p=20去访问的是已经被释放的a的地址。

(3)如何避免野指针
①指针初始化
例8

#include<stdio.h>
int main()
{
	int a = 10;
	int* p1 = &a;
	int* p2 = NULL;//NULL:用来初始化指针的,给指针赋值。
	return 0;
}

②小心指针越界
③指针指向空间释放即设置NULL
④指针使用之前检查有效性
例9

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

3、指针运算

(1)指针±整数

例10:输出数组的每一个元素

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

运行结果为:1 2 3 4 5 6 7 8 9 10

(2)指针-指针
指针-指针得到的是中间的元素个数。(注意尽量大-小)

易错点提示:两个指针不能进行加法运算,这是非法的!!!两个指针在进行减法运算时,类型要相同,否则结果不可预知!!!

例11

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p1 = &arr[0];
	int* p2 = &arr[9];
	printf("%dn", *p2 - *p1);
	return 0;
}

运行结果为:9

我们在之前学过了用递归和迭代两种方法来实现strlen函数,那么今天我们就学会了第三种方法——指针相减法:

例12:自己的strlen函数(指针相减法)

#include<stdio.h>
int my_strlen(char* str)
{
	char* ret = str;
	while (*str != '')
	{
		str++;
	}
	return str - ret;
}
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d", len);
	return 0;
}

(3)指针的关系运算——"<" “>” “<=” “>=” “==” “!=”
指针进行关系运算的前提是它们都指向同一个数组中的元素。

易错点提示:指针的关系运算是相同类型的指针之间的关系运算,不同类型的指针之间的关系运算没有意义,指针与非0整数的关系运算也没有意义。
例13

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int* q = &arr[9];
	p < q; //当p所指的元素在q所指的元素之前时,表达式的值为1;反之为0。
	p > q; //当p所指的元素在q所指的元素之后时,表达式的值为1;反之为0。
	p == q; //当p和q指向同一元素时,表达式的值为1;反之为0。
	p != q; //当p和q不指向同一元素时,表达式的值为1;反之为0。
	printf("%d %d %d %d", p < q, p > q, p == q, p != q);
	return 0;
}

运行结果为:1 0 0 1

4、二级指针

例14

#include<stdio.h>
int main()
{
	int a = 10;
	int* p1 = &a;
	int** p2 = &p1;//p2就是二级指针。
	printf("%dn", **p2);
	
	**p2 = 20;//二级指针p2改变,其指向的a随之改变。
	printf("%dn", **p2);
	printf("%dn", a);
	return 0;
}

运行结果为:
10
20
20

三、指针的分类

1、字符指针

指向字符型数据的指针变量。每个字符串在内存中都占用一段连续的存储空间,并有唯一确定的首地址。即将字符串的首地址赋值给字符指针,可让字符指针指向一个字符串。

下面举一个例子:
例15

#include<stdio.h>
int main()
{
    char arr[] = "abcdef";
    char* p1 = arr;
    printf("%sn", arr);
    printf("%sn", p1);
    //这里的字符指针p1指向的是arr[]中首位的地址,所以在打印时不用解引用,它表示的是从arr[]的首位开始打印至""停止。

    char* p2 = "abcdef";//"abcdef"是一个常量字符串。
    printf("%cn", *p);//说明p存的只是首元素a的地址。
    printf("%sn", p);//同上
    return 0;
}

运行结果为:
abcdef
abcdef
a
abcdef

易错点提示:在常量字符串前加一个“const”。

例16:常量字符串

#include<stdio.h>
int main()
{
    const char* p = "abcdef";
    //*p = 'w';
    printf("%sn", p);
    return 0;
}

运行结果为:abcdef

有了const以后,我们就不能对指针p进行赋值修改了,这样可以使其更加安全的储存数据。

例17:经典易错题

#include<stdio.h>
int main()
{
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";
    char* p1 = "abcdef";
    char* p2 = "abcdef";
    if (arr1 == arr2)
        printf("1n");
    else
        printf("0n");
    if (p1 == p2)
        printf("1n");
    else
        printf("0n");
    return 0;
}

运行结果为:
0
1
这是因为arr1和arr2是分别开辟的内存,虽然元素相同,但所处内存空间不同,所以arr1 != arr2;但是字符指针p1和p2都指向了字符串"abcdef",指向相同,所以p1 == p2。

2、指针和数组

(1)数组名

数组名表示的是数组首元素的地址

例18

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%pn", arr);//arr:首元素的地址。
	printf("%pn", arr + 1);

	printf("%pn", &arr[0]);//&arr[i]:数组中对应元素的地址。
	printf("%pn", &arr[0] + 1);

	printf("%pn", &arr);//&arr:整个数组的地址。
	printf("%pn", &arr + 1);

	//1. &arr — &数组名:数组名不是首元素地址,数组名表示整个数组,&数组名-取出的是整个数组的地址。
	//2.sizeof(arr) — sizeof(数组名):数组名表示的整个数组,sizeof(数组名)计算的是整个数组的大小。

	return 0;
}

运行结果为:
00B3F828
00B3F82C
00B3F828
00B3F82C
00B3F828
00B3F850

也就是说,对于上述代码,arr与&arr[0]通过加1运算后发现地址加了4,而&arr通过加1运算后发现地址加了40,从而说明了&arr表示的是整个数组的地址。

(2)指针数组

概念:数组元素全为指针变量的数组称为指针数组。
也就是指针数组是一个数组,用来存放指针的数组。
如:*p[10]是一个指针数组。

举一个例子:
例19

#include<stdio.h>
int main()
{
    int arr1[] = { 1,2,3,4,5 };
    int arr2[] = { 2,3,4,5,6 };
    int arr3[] = { 3,4,5,6,7 };

    int* arr[] = { arr1,arr2,arr3 };
    int i = 0;
    //分别遍历出arr1,arr2,arr3。
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        //分别遍历出arr1,arr2,arr3中的每个元素。
        for (j = 0; j < 5; j++)
        {
            printf("%d ", *(arr[i] + j));
        }
        printf("n");
    }
    return 0;
}

运行结果为:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7

(3)数组指针

概念:指的是数组名的指针,即数组首元素地址的指针。
也就是数组指针是一个指针,指向数组的指针。
如:(*p)[10]是一个数组指针。

举一个例子:
例20

#include<stdio.h>
void print1(int arr[3][5], int x, int y)//二维数组打印。
{
    int i, j = 0;
    for (i = 0; i < x; i++)
    {
        for (j = 0; j < y; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("n");
    }
}
void print2(int (*p)[5], int x, int y)//数组指针打印
{
    int i = 0;
    for (i = 0; i < x; i++)
    {
        int j = 0;
        for (j = 0; j < y; j++)
        {
            printf("%d ", *(*(p+i)+j));
        }
        printf("n");
    }
}
int main()
{
    int arr[3][5] = { { 1,2,3,4,5 },{ 2,3,4,5,6 },{ 3,4,5,6,7 } };
    
    print1(arr, 3, 5);//参数是数组的形式。
    print2(arr, 3, 5);//参数是指针的形式。
    return 0;
}

运行结果为:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7

3、指针和函数

(1)指针传参

对于普通的参数传递,我们传入的值是什么,函数调用完毕后,这个值还是什么;我们无法在函数调用之后,使用修改后的值。

而当我们使用指针进行传递的时候,就可以达到这个目的。因为指针传递的是变量的地址,而不是值。

下面来举一个例子:
例21:输出数组中的最大值和最小值。

#include<stdio.h>
void minmax(int a[],int len,int *min,int *max)
{
	int i;
	*min = *max = a[0];
	for( i=1; i<len;i++){
		if( a[i] < *min ){
			*min = a[i];
		}
		else if( a[i] > *max){
			*max = a[i];
		}
	}
}
int main(void)
{
	int a[] = {1,2,3,4,5,6,13,14,56,12,42,13,9,8,10,19,23};
	int min,max;
	minmax(a,sizeof(a)/sizeof(a[0]),&min,&max);
	printf("min=%d,max=%dn",min,max);
	return 0;
}

运行结果为:min=1,max=56

易错点提示:
①指针变量实质是一个地址,这个地址指向一个内存。
②函数在传递参数时传递的一定是形参。

(2)函数指针

概念:指向函数的指针变量。
也就是函数指针是一个指针,指向函数的指针。
如:int *f(int a)是一个函数指针。

关于函数名,看一个实例。
例22

#include<stdio.h>
int ADD(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int arr[10] = { 0 };
	printf("%pn", &ADD);
	printf("%pn", ADD);
	return 0;
}

运行结果为:
000B13BB
000B13BB

说明:&函数名 和 函数名 都表示函数的地址。

再举一个简单的函数指针的例子。
例23:加法

#include<stdio.h>
int ADD(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int(*p)(int x, int y) = ADD;
	printf("%dn", (*p)(2, 3));
	return 0;
}

运行结果为:5

函数指针和数组指针原理相同,他有两种作用:
①调用函数。②做函数的参数(回调函数)。

划重点:
① void * 类型的指针 可以接受任意类型的地址。
(有点垃圾桶的意思?狗头)
② void * 类型的指针 不能进行解引用操作。
③ void * 类型的指针 不能就行±整数的操作。

下面来举个例子:
例24

#include <stdio.h>
int main() 
{
	int a = 10;
	int* pa = &a;
	//char* pc = &a;
	
	char ch = 'w';
	void* p= &a;
	p = &ch;

	//*p = 0;
	//p++;
	return 0;
}

解读:
①对于char* pc = &a 这种写法,可以过编译( pc中能放下a的地址 ),但是会有一个警告( C4133:“初始化” :从 “int *” 到 “char *” 的类型不兼容)。
但是对于void *类型就不会有警告出现,因为void * 类型指针可以接收任意类型的指针
②同样因为 void * 类型的指针类型未知,所以在进行解引用 和 ±整数时会报错。

最典型的莫过于qsort()函数了,这个函数对应的头文件为#include<stdlib.h>,包含了4个参数:

void qsort( void* base,
			size_t num,
			size_t width,
			int(* cmp)(const void* e1, const void* e2)
			);

有兴趣的朋友,可以自行查阅一下源代码,这里就不过多阐述了。

来看使用qsort排序的函数:
例25:qsort函数的运用

#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* e1, const void* e2)
{
	//比较两个整形值的
	return *(int*)e1 - *(int*)e2;
}

void test1()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("n");
}

int cmp_float(const void* e1, const void* e2)
{
	//比较两个浮点型值的
	return ((int)(*(float*)e1 - *(float*)e2));
}

void test2()
{
	float f[] = { 9.0,8.0,7.0,6.0,5.0,4.0,3.0,2.0,1.0 };
	int sz = sizeof(f) / sizeof(f[0]);
	qsort(f, sz, sizeof(f[0]), cmp_float);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%.1f ", f[i]);
	}
	printf("n");
}

int main()
{
	test1();
	test2();
	return 0;
}

运行结果为:
0 1 2 3 4 5 6 7 8 9
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0

(3)指针函数

概念:返回某一类型指针的函数。
也就是指针函数是函数,返回值为指针的函数。
如: int (*f)(int a)是一个指针函数。

下面举一个例子:
例26

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//指针函数的简单例程 malloc strcpy
char *fun()
{
    char *pa;
    pa = (char *)malloc(40); //申请内存
    //一定记得要写出错判断
    if(NULL == pa)
    {
        printf("malloc errorn");
        exit(1);
    }
    strcpy(pa,"Welcome to xiyou!");//字符串拷贝函数
    return pa;
}

int main(void)
{
    char *p;
    p = fun();
    printf("%sn",p);

    return 0;
}

运行结果为:Welcome to xiyou!

4、指针和结构体

(1)结构体内包含指针

声明一个结构体:
例27

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

typedef struct _person {//结构体的声明
	char* name;
	int age;
}Person;
int main() {
	Person stu;//结构体实例声明
	stu.name = "zhangsan";//点表示法访问其字段
	stu.age = 20;
	printf("%s %dn",stu.name,stu.age);
	return 0;
}

运行结果为:
zhangsan 20

我们可以直接使用.来进行结构体数据的访问。更加便捷!

(2)结构体指针

①我们可以使用指针来访问变量,同时还能当做函数的参数进行传递
举一个例子:
例28:按照年龄录入信息

#include<stdio.h>
#include<stdlib.h>
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int main()
{
	struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%8s-%2dn", s[i].name,s[i].age);
	}
	printf("n");
	return 0}

运行结果为:
wangwu-10
zhangsan-20
lisi-30

为了使用的方便和直观,用指针引用结构体变量成员的方式:
(*指针变量名).成员名
可以直接用:
指针变量名->成员名

②用来构造数据结构
链表,二叉树这些数据结构都是由结构体组成的,这里就不展开讲了。
例29

typedef struct Student
{
  int num;
  struct Student *next;
}Student;

void main()
{
  Student* p;
  Student Stu1;
  p = &Stu1;
  p->num = 1;
}

结束语

历经了千难万险,有关指针的介绍到此结束,本文章只是很简单的介绍向大家介绍了指针的冰山一角,指针的奇妙远不止于此,还需要我们在日常学习中慢慢积累,厚积才能薄发,各位,我们以此共勉!!!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>