一.指针的概念

指针(Pointer)是一种在编程语言中常用的概念,简单概括它,其实就是Java中地址值的概念。

二.指针变量

1.概念

  • 指针变量是一种特殊的变量,用于存储内存地址
  • 指针变量的值是一个内存地址,该地址指向存储在计算机内存中的数据

2.定义

格式:数据类型 *指针变量名

示例:

int *ptr; // 定义一个整数指标变数ptr
float *ptr_float; // 定义一个浮点数指标变数ptr_float
char *ptr_char; // 定义一个字符指标变数ptr_char

三.指针的分类

  • 字符指针

    char *p

  • 短整型指针

    short int *p

  • 整形指针

    int *p

  • 长整型指针

    long int *p

  • float型指针

    float *p

  • double型指针

    double *p

四.指针和变量

1.指针的概念

  • 指标是一种特殊的变数,用于存储内存地址
  • 指标变数的值是一个内存地址,它指向存储在该地址的数据

2.变量的概念

  • 变数是在程式中用于存储和操作数据的名称化位置
  • 变数具有数据类型,可以存储不同类型的数据,如整数、浮点数、字符等

3.使用指针操作变量

(1)取址运算符(&)

int num = 10;
int *ptr = # // 将指标ptr指向变数num的地址

(2)取值运算符(*)

int num = 10;
int *ptr = #
printf("%d", *ptr); // 输出ptr所指向的变数num的值,即10

4. 指标和变数的关系

  • 指针可以用于间接访问变量的内容,从而实现对变量的操作。
  • 指针还可以用于动态内存分配,使程序能够灵活地管理内存空间。

五.指针和数组元素

(1)指针数组取值

**概述:**可以通过指针变量加取值的方法应用数组元素

示例:

p 是第 0 个元素的地址,p+2 是 a[2]这个元素的地址。对第二个元素的地址取值,即 a[2]

int a[5];
int *p;
p=a;
*(p+2)=100;//相当于a[2]=100

(2)指针运算

一般只有在一片连续的内存空间中才有意义,而这种空间一般是数组,所以放在这里。

(1)指针与整数的加减运算

① 概述

数据类型占多少字节,每单位就移动多少字节

指针与整数的加减运算,表示是指针所指向的内存地址的移动整数就是移动的单位(字节)而这个移动的单位与其指向的数据类型有关

② 注意点
  • 只有指向连续的同类型数据区域,指针加、减整数才有实际意义

例如数组:对指针的加减就相当于改变索引,使其指向下一个数据

image-20230808164019404

③ 格式:

格式:指针±整数

范例:

int类型占用内存为4字节,*h+1相当于向前移动了4个字节

int p = 1;
int *h = &p;
*h+1;//相当于向前移动了四个字节
④ 例子

尝试使用指针整数加减的方式改变索引,间接实现遍历数组的目的

#include 
#define LENGTH 5

int main() {

    int arr[LENGTH] = {10,20,30,40,50};

    //方式1:传统直接访问的方式
    for(int i = 0;i < LENGTH;i++){
        printf("%d ",arr[i]);
    }

    printf("\n");

    //方式2:使用指针访问
    int *p = &arr[0];
    for(int i = 0;i < LENGTH;i++){
        printf("%d ",*(p+i));
    }

    return 0;
}

(2)指针比较大小、

① 概述

通常用于确定数据在数组中的位置

**前提:**只有两个相同类型的指针指向同一个数组的元素的时候,比较大小才有意义。

**大小规则:**指针的大小随索引的增大而增大,即越往后的索引越大

② 示例

结果: p < q,即在数组中q元素的索引在p索引后面

#include 
int main(int argc, char *argv[])
{
    int a[10];
    int *p, *q, n; // 如果在一行上定义多个指针变量的,每个变量名前面加* 
    //上边一行定义了两个指针 p 和 q ,定义了一个整型的变量 n 
    p=&a[1];
    q = &a[6];
    if (p < q)
    {
        printf("p q)
    {
        printf("p>q\n");
    }
    else
    {
        printf("p == q\n");
    }
    return 0;
}

六.指针数组

1.概述

**指针数组:**指的是存放相同类型指针变量的数组

作用:

  • 存储和操作多个指针。
  • 在函数中传递和操作多个指针。
  • 处理需要动态管理的数据集合

2.定义

格式:类型说明符 * 数组名[元素个数];

定义示例:

int *ptr_arr[5]; // 整數指標數組,包含5個元素
char *ptr_arr[10]; // 字符指標數組,包含10個元素
float *ptr_arr[3]; // 浮點數指標數組,包含3個元素

3.示例

int num1 = 10, num2 = 20, num3 = 30;
int *ptr_arr[3] = {&num1, &num2, &num3};

for (int i = 0; i < 3; i++) {
    printf("%d ", *ptr_arr[i]); // 輸出指向的值
}

七.多级指针

二级指针存储目标指针,必须对目标指针进行取址操作

例如:**q要存储*p的方式是q = &p

1.概述

**多级指针:**是指指向指针的指针,每一个指针都包含一个内存地址,该地址指向另一个指针

作用:

  • 在函数传递和操作多层数据结构,以便对其进行访问和修改

2.定义

(1)简述

定义几级指针,就在指针变量后加几个*号,取值时也与之对应即可取到最终指向的数据

int num = 10;
int *ptr = #
int **ptr_ptr = &ptr; // 定義了一個指向指標的指標

八.字符串和指针

1.字符串的概念

(1)字符串的含义

字符串就是以\0结尾的若干的字符的集合:比如“helloworld”,是由多个字符组成的,其结尾也有个隐藏的\0

(2)字符串的地址

字符串的地址,是第一个字符的地址。

如:字符串“helloworld”的地址,其实是字符串中字符h的地址。

2.字符串的存储形式

(1)数组

**概述:**字符串存储在数组中,就是在内存(栈、静态全局区)中开辟了一段空间存放字符串

**简述:**用数组定义并存储字符串,可修改

示例:char string[100] = "I love C!",数组内每一个索引都对应字符串中的字符,有字符构成数组

(2)文字常量

**概述:**在文字常量区开辟了一段空间存放字符串,将字符串的首地址付给指针变量。

**简述:**用指针定义字符串,不可修改,但可修改其指向

示例:char * str = "I love C!",指针变量str只记录了第一个字符的地址,字符串本身其实是存储在字符常量区的

(3)堆区

**简述:**使用malloc等函数,动态申请内存,将字符串拷贝到堆区

示例:

char *str =(char*)malloc(10);//动态申请了 10 个字节的存储空间,首地址给 str 赋值。
strcpy(str,"I love C");//将字符串“Ilove C!”拷贝到 str 指向的内存里

3.修改字符串

(1)概述

字符串是否可以修改,与其存储的方式有关

  • 数组存储:可修改
  • 文字常量区存储:不可修改
  • 堆区存储:可修改

(2)字符串的修改

① 数组
Ⅰ.概述

**前提:**数组没有被定义成不可修改,即用const修饰

存储在数组中的字符串是可以修改

Ⅱ.修改示例

通过索引修改存储字符串的字符数组中字符的值,以达到修改字符串的目的

代码:

在C语言中,如果不指定数组的长度,那么该数组长度会与初始化字符串一致。

这里涉及修改,且要修改的长度大于现有数组,所以必须指定数组长度,否则会报错

#include
int main() {
	char str[20] = "I love you!";//这里必须指定数组长度,因为下面涉及修改的句子比现有句子长
	printf("old str=%s\n", str);
	str[0] = 'y';//通过修改对应索引的值实现修改字符串的目的
	printf("new str=%s\n", str);
	strcpy(str, "You love me!");//使用strcpy直接修改整个句子
	printf("new str=%s\n", str);
}

输出:

old str=I love you!
new str=y love you!
② 文字常量区
Ⅰ.概述

存放在文字常量区的内容是不可以修改的,但是可以通过修改指针的指向,让其指向其它地方,进而间接的实现"修改"数组的操作

Ⅱ.示例

可以通过将指针str指向另一个字符串,间接实现"修改"字符串,实际他修改的是指针所指向的值

代码:

#include
int main() {
	char* str = "I love you!";
	printf("str的值%s\n",str);
	str = "you love me!";
	printf("new的值是%s\n", str);
	return 0;
}

输出:

str的值I love you!
new的值是you love me!
③ 堆区
Ⅰ.概述

存储在堆区的字符串是可以修改

Ⅱ.示例

这里第一次使用strcpy设置字符串,紧接在再次使用strcpy拷贝新的字符串进入堆,实现了修改的操作

#include
#include 
int main() {
	char * str = (char*)malloc(10);//使用malloc函数,去堆申请10个字节的空间
	strcpy(str,"I love you!");//使用strcpy函数,将字符串复制到堆空间中
	printf("old str 的值:%s\n", str);//str其实是一个指针,其指向了堆空间中的字符串
	strcpy(str, "you love me!");//再次使用strcpy对其值进行修改
	printf("new str的值:%s\n", str);

	return 0;
}

输出:

old str 的值:I love you!
new str的值:you love me!

4.总结

1、指针可以指向文字常量区

  • 指针指向的文字常量区的内容不可以修改

  • 指针的指向可以改变,即可以给指针变量重新赋值,指针变量指向别的地方。

2、指针可以指向堆区

  • 指针指向的堆区的内容可以修改。

  • 指针的指向可以改变,即可以给指针变量重新赋值,指针变量指向别的地方。

3、指针也可以指向数组(非 const 修饰)

例:char buf[20]="hello world"; char *str=buf;

这种情况下

  • 可以修改 buf 数组的内容。

  • 可以通过 str 修改 str 指向的内存的内容,即数组 buf 的内容

  • 不能给 buf 赋值 buf=“hello kitty”;错误的。

  • 可以给 str 赋值,及 str 指向别处。 str=“hello kitty”

九.数组(的)指针

区别于指针数组,二者不是一个东西

1.概念

简单讲,就是指向数组的指针啦

数组指针是一个指向包含固定数量元素的数组的指针。通过使用数组指针,可以更方便地操作数组元素,包括遍历数组、访问特定元素等。

2.格式

格式:指向数组的数据类型 (*指针变量名) [指向的数组的元素个数]

例如:int(*p)[5],指向Int型数组,指向数组的元素个数是5个

3.应用

  • 函数参数的传递
  • 函数接收数据用的形参
  • 多维数组的处理
    • 数组指针本身就能代表其指向的数组,对其操作直接作用于其指向的数组,可以很方便的处理多维数组
  • 动态内存分配
  • ...(学海无涯)

(1)函数参数的传递

① 概述

当需要将一个数组作为参数传递给函数时,可以将数组的指针传递给函数。这样,函数可以通过指针操作数组,而不需要复制整个数组。

② 原理

数组名≠数组指针

数组本质上是一个具有固定大小和类型的内存块,而数组指针则是指向这个内存块的指针。通过将数组名赋值给一个指向正确类型的指针,可以获得指向数组的指针。

③ 代码

**数组指针的定义:**确保数组指针的参数与要传递的二维数组列一致即可

#include 
void fun();

int main() {
	int arr[3][5] = {
		{3,6,7,2,3},
		{7,0,2,1,6},
		{2,5,4,8,6,}
	};
	fun(arr, 10);
	return 0;
}
void fun(int(*p)[5],int a) {
	int b = p[0][1] + a;
	printf("计算结果:%d", b);//计算结果:16
}

(2)多维数组的处理

① 概述

在处理多维数组时,这种指针类型特别有用。

  • 它可以指向一个二维数组的行,使得可以更方便地遍历和操作数组的每一行。
② 功能

下面例程实现了下面二者

  • 一维数组指针配合二维数组使用

    一位数组指针每+1,就会下移一个一维数组(二维数组列与一维数组参数一致)

  • 通过指针数组进行多维数组的遍历

③ 代码
  • 先用数组指针去表示array数组
  • 之后再进行遍历
    • (*p)[j]表示第一行的数组的第j个元素
    • 遍历完毕后,通过p++将其指向下一个数组
      • 走完这后,p会++,j的值会清零,重新从0开始
#include 

int main() {
    int array[3][5] = {
        {1, 2, 3, 4, 5}, 
        {6, 7, 8, 9, 10},
        {11, 12, 13, 14, 15}
    };

    int(*p)[5] = array;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d ", (*p)[j]);
        }
        p++;//一维数组指针+1,指向二维数组中的第二个一位数组,进而完成遍历的目的
        printf("\n");
    }
    /*
    1 2 3 4 5
    6 7 8 9 10
   11 12 13 14 15
    */
    return 0;
}

4.数组名字变指针

(1)概述

数组名字取地址就可以将其变成一个数组指针

(2)结论

a&a虽然指向一个地址,但a&a的指针类型是不同的

  • aint*类型的指针
  • &a是数组指针
  • 对几维数组取地址,就可以得到几维数组指针
  • 对其加一操作,是几维数组指针就跳一个几维数组的元素

(3)示例

&a使一维数组变成了一个一维数组指针,是 int(*p)[10]类型的。

(&a) +1&a相差一个数组即 10个元素即 40个字节。

#include 

int main() {
	int a[10]; printf("a=%p\n", a);//000000A6340FFC68
	printf("a+1=%p\n", a + 1);//a+1=000000A6340FFC6C
	printf("&a=%p\n", &a);//&a=000000A6340FFC68
	printf("&a +1=%p\n", &a + 1);//&a +1=000000A6340FFC90
	//000000A6340FFC68 - 000000A6340FFC90 = 40(十进制)
	return 0;
}

(4)数组名字和数组指针的区别

① 相同点

a 是数组的名字,是 a[0]的地址,p=a 即 p 保存了 a[0]的地址,即 a 和 p 都指向 a[0],所以在引用数组元素的时候,a 和 p 等价

**例如:**a[2]、(a+2)、p[2]、(p+2) 都是对数组 a 中 a[2]元素的引用。

② 不同点
  • a是常量(既是数组地址又是第一个元素地址),p是变量

    可以用=号给p赋值,不能用=号给a赋值

  • 对a取地址和对p取地址的结果不同

    • a是数组名字,取地址的结果是数组指针
    • p是指针变量,取地址的结果是二级直至

5.数组指针取*

(1)概述

数组指针取*,不是取值的意思,而是指针类型的转换

例如:

对数组指针取*,会得到"降维"效果

  • 一维数组指针取*,会变成int*类型的指针,指向第0个元素的地址

  • 二维数组指针取*,会变成一维数组指针,指向第0个一位数组

  • 三位数组指针取*,会变成二维数组指针,指向第0个二维数组

  • 以此类推

(2)演示

#include 
int main()
{
	int a[3][5];
	int(*p)[5]; p = a;

	printf("a=%p\n", a);//a 是一维数组指针,指向第 0 个一维数组,即第 0 行 
	printf("*a=%p\n", *a);//*a 是 第 0 行第 0 个元素的地址,即 &a[0][0] 
	printf("*a +1=%p\n", *a + 1);//*a +1 是第 0 行第 1 个元的地址,即&a[0][1]

	printf("p=%p\n", p);//p 是一维数组指针,指向第 0 个一维数组,即第 0 行 
	printf("*p=%p\n", *p);//*p 是第 0 行第 0 个元素的地址,即 &a[0][0] 
	printf("*p +1=%p\n", *p + 1);//*p +1 是第 0 行第 1 个元的地址,即&a[0][1] return 0;
}

十.指针与函数

1.指针作函数参数

函数修改值直接作用于主调函数变量所存储的数据

(1)概述

**含义:**简单来说就是将指针即地址作为参数传递给函数。

**作用:**通过函数去修改变量的值(数组用另外一套)

(2)特点

  • 函数可以直接修改地址值指向的值

    即改变主调函数中变量的值

(3)注意点

① 传值

必须传变量的地址值,即是指针变量

  • 通过用&取地址,以地址(指针)作为参数传递
② 赋值

因为接收的是指针,需要通过*地址变回其指向的值才能进行赋值操作

  • 进行取值,相当于复原回了主调函数变量的值

(4)示例程序

#include 

void fun(char** p);//函数的声明
int main() {
	char* str = "Hello World!";//其本身也是一个指针
	fun(&str);//这里要对指针进行取址
	printf("修改后的值:%s\n", str);//修改后的值:Hello C !
	return 0;
}
/*这种方式直接修改了str所指向数据的值*/
void fun(char** p) {//用二级指针进行接收
	*p = "Hello C !";//直接修改指针指向位置的数据
}

2.函数传数组

我觉得啊!咱函数传指针,就用Java那种形式传就好了

(1)特性

Java中的数组是按数值传递,它相当于复制一份给函数,函数没办法操作数组的原始数据,只能操作复制过去的数据

C语言中的函数传数组是按引用传递的,即传递的是地址,函数可以改变传入数组的值。

(2)一维数组

① 格式

下面两种格式其实都是一样的,按格式一书写的,编译器编译时会自动转化成格式二的形式

Ⅰ.格式一

直接在函数处定义一个一位数组来接收,更贴近于Java之类的高级语言

void fun(int p[])
Ⅱ.格式二

通过指针的方式作为形参

void fun(int *p)
② 示例

代码:

#include 
void fun(int* p, int x) {
	//p就是在这个函数内数组的指针
	p[0] = 100;

	//打印函数修改后的数组
	for (int i = 0; i < x; i++)
	{
		printf("%d ", p[i]);
	}
}
int main() {
	//arr其实就是一个Int类型的指针
	//所以传递的时候传的也是一个指针,所以当然要用指针来接收啦
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	fun(arr, 10);

	//j打印查看原始数组是否被改变
	printf("\n");
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

输出:

接着输出再强调一下,C语言中函数传数组传的是引用,函数内的更改会作用到主调处的数组

100 2 3 4 5 6 7 8 9 10
100 2 3 4 5 6 7 8 9 10

(3)二维数组

① 格式

传入数组的指针,p指针等价于主调函数数组的指针

void fun( int (*p)[4] ) 
② 示例
#include 

//void fun( int (*p)[4] ) 
void fun(int p[][4]) {
	p[0][1] = 86;
}

int main() {
	int arr[3][4] = {
		{1,2,3,4},
		{2,4,6,7},
		{6,3,1,9},
	};
	fun(arr);

	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 4 ; j++) {
			printf("%d ",arr[i][j]);
		}
	}
	//1 86 3 4 2 4 6 7 6 3 1 9
	return 0; 
}

(4)指针数组

① 格式

传入指针的指针,q指针在函数内等价于主调函数指针数组的指针

void fun(char **q)
② 代码
#include 
void fun(char** q) {
	//q记录的是p的指针,等价于p[1]
	//请注意,这里只是修改指针应用而已
	q[1] = "I love you";
}

int main() {
	char* p[4] = {"hello","doraemon","love","yes"};
	//传入的p指针
	fun(p);
	printf("%s ",p[1]);
	return 0;
}

3.函数返回指针

(1)前提

返回指针的时候,其指向的内存的内容不能释放。如果返回的指针指向的内容已经被释放了,那这个返回的地址就没有意义了

举例:

  • 返回局部变量的指针,该指针指向的内容会在该局部变量所处函数结束后释放,所以这个指针是野指针,无意义

(2)可以返回的指针

  • 静态关键字static修饰的直至
  • 存储在文字常量区字符串的地址
  • 存储在堆中的数据

(3)static的地址

① 概述

由static静态关键字修饰的变量,其数据只会被初始化一次,之后就会一直存储,所以,其指针指向的数据在程序运行期间是一直存在的,是有意义的指针

② 要点

给要返回的指针添加静态static关键字

③ 示例
#include 
char* fun() {
	//使用static修饰字符串,使其值存入静态存储区
	static char str[] = "HelloWorld!";
	return str;
}
int main() {
	char* str2;
	str2 = fun();
	printf("%s\n", str2);
	return 0;
}

(4)文字常量区的地址

① 概述

文字常量区,通常存储在静态存储区,其内数据也是只会被初始化一次,之后就会一直存储,所以,其指针指向的数据在程序运行期间是一直存在的,是有意义的指针

② 格式

函数格式:

指定返回值的位置要填写相应类型的指针

char* fun()
{
	char* str = "hello world";
	return str;
}
③ 示例
#include 
char* fun()
{
	char* str = "hello world";
	return str;
}
int main()
{
	char* p; 
	p = fun();
	printf("%s\n", p);//hello world
}

(5)堆内存中的地址

① 概述

堆内存中申请的空间,除非手动释放、程序结束,否则是会一直存在的,所以其直至所指向的地址值在程序运行期间是一直存在的,是有意义的指针

② 示例
#include 
#include 
#include 
char* fun()
{
	char* str;
	str = (char*)malloc(100);
	strcpy_s(str, 100, "hello world");
	return str;
}
int main()
{
	char* p;
	p = fun();
	printf("%s\n", p);//hello world 
	//释放堆数据
	free(p);
}

4.函数指针

(1)概念

① 函数的指针

函数在程序运行时会先被加载到内存中,其首地址即函数名称、指针就是函数的入口

② 函数的调用

当我们调用函数的时候。CPU就会根据函数的指针,去内存中找指令并运行

(2)函数指针的用处

  • 可以将其定义为函数的形参,使函数可以接收主调函数的函数,并在函数内运行该函数

    例如:int process(int(*p)(int,int)){}

    • 传什么函数进process函数,就运行什么函数
  • ...

(3)函数指针的定义

① 格式
  • 参数不需要设置标识符,直接一个数据类型即可
指向函数的返回值类型(*指针变量名)(指向函数的形参列表)
② 例子

所能接收/指向的函数,必须满足函数指针所定义的格式

函数指针:int(*p)(int,int)

含义:定义了一个函数指针变量p,其指向的函数必须有一个整形的返回值,有两个整形参数

例如:

可接收的函数:

int max(int x,int y){
    
}

接收示例:p=max;,直接将函数的名字(首地址)赋值给函数指针即可

(4)函数指针调用函数

① 概述

通过函数指针变量去调用函数

② 格式
(*函数指针变量名)(指向函数的形参)
③ 示例
#include 
int max(int x, int y) {
	int temp = 0;
	if (x > y) {
		temp = x;
	}else {
		temp = y;
	}
	return temp;
}

int main() {
	int(*p)(int, int);
	//将函数赋值给函数指针
	p = max;
	int num;
	//通过指针调用函数
	num = (*p)(8, 10);
	printf("最大值是%d", num);
	return 0;
}

(5)函数指针数组

① 概念

**概念:**函数指针数组是由若干个相同类型的函数指针变量构成的集合,在内存中连续的顺序存储,其每个元素都是一个函数指针变量

② 格式
Ⅰ.创建格式格式:
  • 类型名:是指向函数的返回值类型
  • 数组名:函数指针数组的名称
  • 元素个数:这个函数指针数组所要存储的函数指针的数量
  • 参数列表:指向函数的形参,不用指定标识符,直接用,分隔其形参数据类型即可
类型名(*数组名[元素个数])(形参列表)

例子:

函数指针数组:int(*p[5])(int,int);

这个函数指针数组,数组名是 p,有 5 个元素 p[0]~p[4],每个元素都是函数指针变量,每个函数指针变量指向的函数,必须有整型的返回值,两个整型参数。

Ⅱ.调用函数格式:
(*数组名[索引])(形参列表);
③ 例子
  • 将max、min、add函数指针存入函数指针数组arr中
  • 再通过函数指针数组arr调用min函数,求得最小值
#include 

int max(int x, int y) {
	int temp = 0;
	if (x > y) {
		temp = x;
	}else {
		temp = y;
	}
	return temp;
}
int min(int x, int y) {
	int temp = 0;
	if( x < y) {
		temp = x;
	}
	else {
		temp = y;
	}
	return temp;
}

int add(int x, int y) {
	int temp = x + y;
	return temp;
}

int main() {
	//定义函数指针数组,用函数的名字来初始化数组
	int(*arr[3])(int,int) = {max,min,add};
	//调用函数指针数组里面的指针来调用其指向的函数
	int num;
	num = (*arr[2])(86, 10);
	printf("%d", num);//返回96,成功调用min函数

	return 0;
}

十一.特殊指针

1.通用指针

(1)概述

*void 通用指针:**任何类型的地址(指针)都可以给void*类型的指针变量赋值,无需强制转换

(2)格式

void *

2.空指针

(1)概述

就是给指针赋值NULL,一般用来给指针变量初始化

最后修改:2024 年 03 月 11 日
如果觉得我的文章对你有用,请随意赞赏