C语言——指针详解(第六讲)(全)(让你不再害怕指针,一万字超详细讲解,快来看看吧!)

前言

  亲爱的CSDN的小伙伴们,我是陪伴你们学编程的莹莹同学。今天,我给大家分享的是指针,相信大家就算没有学过指针,也应该听说过它吧!没错,尽管指针听起来让人望而生畏,但是相信大家看过这篇文章,可以对你们有所帮助,好了,话不多说去,我们开始吧!

指针引言

  我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
   我们把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。而一个比特位可以存储⼀个2进制的位1或者0。
   其中,每个内存单元,相当于⼀个学⽣宿舍,⼀个⼈字节空间⾥⾯能放8个⽐特位,就好⽐同学们住的⼋⼈间,每个⼈是⼀个⽐特位
   每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的⻔牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间
⽣活中我们把⻔牌号也叫地址,在计算机中我们
把内存单元的编号也称为地址。C语⾔中给地址起
了新的名字叫:指针

结论:内存单元的编号=地址=指针

补充: 1byte = 8bit;
   1KB = 1024byte;

指针变量,取地址操作符,解引用操作符

取地址操作符

在C语⾔中创建变量其实就是向内存申请空间,比如int a = 10;上述的代码就是创建了整型变量a,内存中申请4个字节。
而使用取地址操作符”&“便可以取出a的地址。

#include<stdio.h>

int main()
{
	int a = 10;
	printf("%p\n", &a);
	int* p = &a;//指针变量
	
	return 0;
}

指针变量

   我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?
答案是:指针变量中。(如上代码)
注:指针变量也是变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。

拆解指针类型

   对于int* p = &a;
   我们看到p的类型是 int*,* 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)类型的对象。

指针变量的⼤⼩

指针变量的⼤⼩取决于地址的⼤⼩

32位平台下(x86环境)地址是32个bit位(即4个字节)
64位平台下(x64环境)地址是64个bit位(即8个字节)

注意:指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。

字符指针变量

#include<stdio.h>

int main()
{
	char ch = 'w';
	char* pc = &ch;//pc是指针变量

	char arr[10] = "abcdef";//数组字符串,可以被修改
	char* p1 = arr;
	*p1 = 'w';

	const char* p2 = "abcdef";//常量字符串,不能被修改;
	//打印
	printf("%s\n", p1);

	return 0;
}

数组指针变量, 指针数组

指针数组,顾名思义是存放指针的数组。
指针数组的每个元素都是⽤来存放地址(指针)的。
数组指针变量
• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

int *p1[10];//p1是指针数组
int (*p2)[10];//p2是指针变量
int (*p)[10];

解释:p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。

函数指针变量

函数指针变量是用来存放函数的地址的

#include<stdio.h>

int main()
{
	return 0;
}


#include<stdio.h>
void Add(int x, int y)
{
	return x + y;
}
char* test(char c, int n)//此函数的返回类型是char*
{
	//
}
int main()
{
	printf("%p\n", &Add);
	int (*pf)(int,int) = &Add;//pf是专门用来存放函数的地址,pf就是函数指针变量
	char* (*p)(char, int) = &test;
	return 0;
}


函数指针变量的使用
#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = Add;
	int c = Add(2, 3);//函数名调用
	printf("%d\n", c);

	int d = (*pf)(3, 4);//函数指针调用
	printf("%d\n", d);

	int d = pf(3, 4);//*可以不写
	printf("%d\n", d);

	return 0;
}

函数指针变量类型
在这里插入图片描述

解引⽤操作符

#include<stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	*p;//解引用操作符(间接访问操作符)
	printf("%d\n", *p);
	return 0;
}

*pa 的意思就是通过pa中存放的地址,找到指向的空间,pa其实就是a变量了;所以pa=0,这个操作符是把a改成了0.
意义:其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活,后期慢慢就能理解了。

指针变量类型的意义

//代码1 
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}
//调试:代码1会将n的4个字节全部改为0
//代码2 
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}
//调试:代码2只是将n的第⼀个字节改为0

以上:char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节

结论:指针的类型决定了,对指针解引⽤的时候访问几个字节。

拓展:

指针类型指向解引用访问字节数
char*指向字符的指针解引用访问1个字节数
short*指向短整型的指针解引用访问2个字节数
int*指向整型的指针解引用访问4个字节数
float*指向单精度浮点型的指针解引用访问4个字节数

指针±整数

#include<stdio.h>

int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	printf("pa=%p\n", pa);
	printf("pa+1=%p\n", pa+1);

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

调试后,我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

void*指针

   在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void 类型的指针不能直接进⾏指针的±整数和解引⽤的运算。*

#include<stdio.h>

int main()
{
	int a = 0;
	//float f= 0.0f;
	void* p = &a;//int*
	//p = &f;//float*
	//*p = 10;//无法解引用
	return 0;
}

优点:⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。

const修饰指针

const可以修饰变量,使变量加上⼀些限制,不能被修改。

#include<stdio.h>
int  main()
{
	const int a = 10;//a具有了常属性(不能被修改了)
	//a是不是常量呢?
	//虽然a是不能被修改的,但本质上还是变量
	int* p = &a;
	*p = 20;
	printf("%d\n", a);
	return 0;
}

#include<stdio.h>

int main()
{
	int a = 10;
	int b = 20;
	//const放在*的右边
	int* const p = &a;
	//p = &b;
	*p = 100;


	return 0;
}
#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	//const放在*的左边
	int const* p = &a;
	p = &b;
	return 0;
}

经调试后,得出结论:

const修饰指针变量的时候

• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
• const如果放在
的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

指针运算

指针的基本运算有三种,分别是:
• 指针±整数
• 指针-指针
• 指针的关系运算

• 指针±整数

#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[i];
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *p);
		p++;
	}
	
	return 0;
}

//或者
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[i];
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i));
		
	}

	return 0;
}

指针-指针

前提条件:两个指针同时指向同一空间
|指针 - 指针| = 两个指针之间的元素个数


#include<stdio.h>

int main()
{
	int a[10] = { 0 };
	printf("%d\n", &a[9] - &a[0]);
	printf("%d\n", &a[0] - &a[9]);
	return 0;
}


指针运算关系

本质:指针和指针比较大小

#include<stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	while (p < arr + sz)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;

}

野指针

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

  1. 指针未初始化
#include<stdio.h>

int main()
{

	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}
  1. 指针越界访问
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    *p = &arr[0];
    int i = 0;
    for(i=0; i<=11; i++)
    {
      //当指针指向的范围超出数组arr的范围时,p就是野指针 
     *(p++) = i;
    }
 return 0;
 }
  1. 指针指向的空间释放
#include <stdio.h>
int* test()
{
   int n = 100;
    return &n;//在p指向n的同时,n已经返还给空间了
}
int main()
{
    int*p = test();
     printf("%d\n", *p);
 return 0;
}

如何规避野指针

1.指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
2.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。

int main()
{
 int arr[10] = {1,2,3,4,5,67,7,8,9,10};
 int *p = &arr[0];
 for(i=0; i<10; i++)
 {
   *(p++) = i;
 }
 //此时p已经越界了,可以把p置为NULL 
   p = NULL;
 //下次使⽤的时候,判断p不为NULL的时候再使⽤ 
 //...
   p = &arr[0];//重新让p获得地址 
   if(p != NULL) //判断 
   {
   //...
   }
 return 0;
}

assert断⾔

  assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏**。这个宏常常被称为“断⾔”。**

  assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

  优点:assert出现错误的时候,直接会报错,指明在文件什么位置,哪一行,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。

#define NDEBUG
#include <assert.h>

  然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

指针的使⽤和传址调⽤

写一个函数,交换两个整数的值

//Swap
#include <stdio.h>
void Swap1(int x, int y)
{
 int tmp = x;
 x = y;
 y = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(a, b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

编译后发现a,b值并没有发生交换
因为实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

所以Swap是失败的了。

//Swap 2
#include<stdio.h>
void sweep(int* x, int* y)
{
	int typ;
	typ = *x;
	*x = *y;
	*y = typ;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d b = %d\n", a, b);
	sweep(&a,&b);
	printf("交换后:a = %d b = %d\n", a, b);

	return 0;
}




编译发现,交换成功
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。

数组名结论

1.数组名就是数组⾸元素(第⼀个元素)的地 址。 2.但两个例外: • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节 • &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的) 除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
#include<stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	//或者
	int* p2 = arr;//数组名是数组首个元素的地址
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);

	return 0;
}

使⽤指针访问数组

使用指针来访问数组,实现数组的输入输出

#include<stdio.h>

int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", p + i);
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d", *(p + i));//*(p+i) = p[i]或者arr[i] = *(arr + 1);
	}

	return 0;
}

函数实现数组的打印

#include<stdio.h>

void Print(int* p,int sz)//一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print(arr, sz);
	return 0;
}

数组传参的本质

1.对于一维数组:
数组传参本质上传递的是数组⾸元素的地址。
2.对于二维数组:
二维数组传参的本质也是传递了地址,传递的是第一行这个一维数组的地址。

二级指针

#include<stdio.h>

int main()
{
	int a = 10;
	int* p = &a;//p是一级指针
	int** pp = &p;//pp是二级指针
	//打印p的地址
	printf("%p\n", *pp);
	//**pp先通过*pp找到p,然后对p进行解引用操作:*p,那找到的是a
	printf("%d\n",* *pp);

	return 0;
}

函数指针数组

#include<stdio.h>
void Add(int x, int y)
{
	return x + y;
}
void Sub(int x, int y)
{
	return x - y;
}
void Mul(int x, int y)
{
	return x * y;
}
void Div(int x, int y)
{
	return x / y;
}

int main()
{
	int(*pf1)(int, int) = Add;//函数指针变量
	int(*pfarr[4])(int, int) = { Add,Sub,Mul,Div };//pfar是函数指针数组
  int i =0;
 for(i = 0;i < 4;i++)
 {
    int r = pfarr[i](8,4);
    printf("%d\n",r);
 }
	return 0;
}

typedef关键字

typedef是⽤来类型重命名的,可以将复杂的类型,简单化
你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:

typedef unsigned int uint;
//将unsigned int 重命名为uint 

如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:

typedef int* ptr_t;

但是对于数组指针和函数指针稍微有点区别:
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

typedef int(*parr_t)[5]; //新的类型名必须在*的右边 

函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:

typedef void(*pfun_t)(int);//新的类型名必须在*的右边 

qsort函数

介绍

void qsort(void * base,      //指针,指向的是待排序的数组的第一个元素
	       size_t num,         //base指向的待排序数组的元素个数
	       size_t size,        //base指向的待排序数组的元素大小
	       int(*compar)(const void*, const void*))//函数指针——指向的是两个元素的比较函数

写一段代码使用qsort排序整型数据

#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* p1, const void* p2)
{
	/*if (*(int*)p1 > *(int*)p2)
		return 1;
	else if (*(int*)p1 > *(int*)p2)
		return 0;
	else
		return -1;*/
	//简化
	return (*(int*)p1 - *(int*)p2);
}

void print_arr(int arr[], int sz)
{
	int  i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
void test1()
{
	int arr[] = { 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 );
	print_arr(arr, sz);
}
int main()
{
	test1();
	return 0;
}

使用qsort排序结构体数据

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[20];
	int age;
};
int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

void test2()
{
	struct Stu arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}


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

qsort函数的模拟实现

#include<stdio.h>
void bubble_sort(int arr[], int sz)   //局限性:这个函数只能排序整型数组
{
	int i = 0;
	for (i = 0; i < sz -1; i++)
	{
		int j = 0;
		for (j = 0; j < sz -1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int t = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = t;
			}
		}
	}
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	print_arr(arr, sz);
	return 0;
}

sizeof和strlen对比

sizeofstrlen
1. sizeof是操作符1. strlen是库函数,使⽤需要包含头⽂件 string.h
2. sizeof计算操作数所占内存的⼤⼩,单位是字节2. srtlen是求字符串⻓度的,统计的是 \0 之前字符的隔个数
3. 不关注内存中存放什么数据3. 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能会越界
(完)
  • 30
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
指针是C编程语言中非常重要的概念之一,对于初学者来说可能会感到害怕和困惑。但只要理解了指针的原理和用法,就会发现它其实并不可怕。 首先,指针是一个存储变量地址的变量,它可以指向任何数据类型的变量,包括整型、浮点型、字符型等等。我们可以通过指针访问变量的值,也可以通过指针修改变量的值,这是指针的一大优势。 其次,理解指针的应用场景能够帮助我们更好地使用它。比如,当我们需要在函数之间传递大量的数据时,通过传递指针可以提高程序的执行效率。另外,在动态内存分配和释放中,指针也是一个必不可少的工具。 理解指针的用法也是很重要的。首先,我们需要理解指针的声明和初始化。指针的声明使用“类型 * 变量名”的语法,例如 int *ptr; 表示声明了一个指向整型变量的指针指针的初始化可以通过给指针赋值一个变量的地址或者通过取地址符&获取变量的地址。 然后,我们需要了解指针的运算。指针可以进行四种基本的运算:取地址运算符&,取值运算符*,指针加法和指针减法。取地址运算符&用于获取变量的地址,取值运算符*用于获取指针所指向的变量的值。指针加法和指针减法用于指针地址的增加和减少,不同数据类型的指针相加或相减会有不同的结果。 最后,我们需要注意指针的安性。在使用指针的过程中,需要特别小心避免空指针、野指针等问题的出现,因为这些问题容易引发程序的崩溃或者产生不可预知的结果。 总结来说,指针作为C语言中的重要概念,我们应该尽早学习和掌握。只要理解指针的原理和用法,我们就能够更加灵活地操作内存,提高程序的效率和功能。通过不断的实践和学习,我们可以逐渐摆脱对指针的恐惧,让指针成为我们编程的得力助手。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值