C语言-指针了解

mac2025-09-05  14

文章目录

一.什么是指针?二.为什么要使用指针?二.指针的声明与应用1.字符指针2.指针与数组2.1指针与数组的关系2.2指针数组2.3指针数组2.4数组名 VS 数组名2.5.数组传参、指针传参2.5.1 一维数组传参2.5.2 二维数组传参2.5.3 一级指针传参2.5.4 二级指针传参 3.指针与函数3.1 指针作为函数的参数3.2 函数指针3.2.1函数指针3.2.2 函数指针数组3.2.2 指向函数指针数组的指针3.2.2 回调函数 4.指针与结构体4.1 结构体4.2 结构指针 5.指针的运算5.1指针加减整数 :5.2指针 - 指针

一.什么是指针?

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。  C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量地址的变量。    内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。如下图:   这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。

总结:

1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。 2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。 3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。

二.为什么要使用指针?

在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。

好处: 1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效; 2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等; 3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。

二.指针的声明与应用

指针的声明比普通变量的声明多了一个一元运算符 “”。运算符 “” 是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针所指向的对象。  声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:或是使他指向现有的内存,或者给他动态分配内存,否则我们并不知道指针指向哪儿。

1.字符指针

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

int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }

还有一种使用方式如下:

int main() { char* pstr = "hello bit.";//将字符串首元素的地址放入pstr中 printf("%s\n", pstr); return 0; }

代码 char* pstr = “hello bit.”; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是其本质是把字符串 hello bit. 首字符的地址放到了pstr中。

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

例如:

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

这里最终输出的是: 这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。 指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开 辟出不同的内存块。所以str1和str2不同,str3和str4不同。

2.指针与数组

2.1指针与数组的关系

在C语言中,指针与数组之间的关系十分密切。实际上,许多可以用数组完成的工作都可以使用指针来完成。一般来说,用指针编写的程序比用数组编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。

我们先声明一个数组:

int a[10]; // 声明一个int类型的数组,这个数组有10个元素

我们可以用 a[0]、a[1]、…、a[9] 来表示这个数组中的10个元素,这10个元素是存储在一段连续相邻的内存区域中的。

接下来,我们再声明一个指针:

int *p; // 声明一个int类型的指针变量

p 是一个指针变量,指向内存中的一个区域。如果我们对指针 p 做如下的初始化:

p = &a[0]; // 对指针进行初始化,p将指向数组 a 的第 1 个元素 a[0]

我们知道,对指针进行自增操作会让指针指向与当前元素相邻的下一个元素,即 *(p + 1) 将指向 a[1] ;同样的, *(p + i) 将指向 a[i] 。因此,我们可以使用该指针来遍历数组 a[10] 的所有元素。可以看到,数组下标与指针运算之间的关系是一一对应的。而根据定义,数组类型的变量或表达式的值是该数组第 1 个元素的地址,且数组名所代表的的就是该数组第 1 个元素的地址,故,上述赋值语句可以直接写成:

p = a; // a 为数组名,代表该数组最开始的一个元素的地址

很显然,一个通过数组和下标实现的表达式可以等价地通过指针及其偏移量来实现,这就是数组和指针的互通之处。但有一点要明确的是,数组和指针并不是完全等价,指针是一个变量,而数组名不是变量,它数组中第 1 个元素的地址,数组可以看做是一个用于保存变量的容器。更直接的方法,我们可以直接看二者的地址,并不一样:

#include "stdio.h" int main() { int x[10] = {1,2,3,4,5,6,7,8,9,0}; int *p = x; printf("x的地址为:%p\n",x); printf("x[0]的地址为:%p\n",&x[0]); printf("p的地址为:%p\n",&p);      // 打印指针 p 的地址,并不是指针所指向的地方的地址 p += 2; printf("*(p+2)的值为:%d\n",*p);    // 输出结果为 3,*(p+2)指向了 x[2] return 0; }

结果如下:

可以看到, x 的值与 x[0] 的地址是一样的,也就是说数组名即为数组中第 1 个元素的地址。实际上,打印 &x 后发现,x 的地址也是这个值。而 x 的地址与指针变量 p 的地址是不一样的。故而数组和指针并不能完全等价。

2.2指针数组

指针是一个变量,而数组是用于存储变量的容器,因此,指针也可以像其他变量一样存储在数组中,也就是指针数组。 指针数组是一个数组,数组中的每一个元素都是指针。声明一个指针数组的方法如下:

int *p[10]; // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向int类型的指针

在上述声明中,由于 [] 的优先级比 * 高,故 p 先与 [] 结合,成为一个数组 p[];再由 int * 指明这是一个 int 类型的指针数组,数组中的元素都是 int 类型的指针。数组的第 i 个元素是 *p[i],而 p[i] 是一个指针。由于指针数组中存放着多个指针,操作灵活,在一些需要操作大量数据的程序中使用,可以使程序更灵活快速。      其它:

int* arr1[10]; //整形指针的数组 char *arr2[4]; //一级字符指针的数组 char **arr3[5];//二级字符指针的数组

2.3指针数组

我们已经熟悉: 整形指针: int * pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够指向浮点型数 据的指针。 那数组指针应该是:能够指向数组的指针

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

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

解释:

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

数组指针是一个指针,它指向一个数组。声明一个数组指针的方法如下:

int (*p)[10]; // 声明一个数组指针 p ,该指针指向一个数组

由于 () 的优先级最高,所以 p 是一个指针,指向一个 int 类型的一维数组,这个一维数组的长度是 10,这也是指针 p 的步长。也就是说,执行 p+1 时,p 要跨过 n 个 int 型数据的长度。数组指针与二维数组联系密切,可以用数组指针来指向一个二维数组,如下:

#include "stdio.h" int main() { int arr[2][3] = { 1,2,3,4,5,6 };// 定义一个二维数组并初始化 int(*p)[3]; // 定义一个数组指针,指针指向一个含有3个元素的一维数组 p = arr; // 将二维数组的首地址赋给 p,此时 p 指向 arr[0] 或&arr[0][0] printf("%d\n", (*p)[0]);// 输出结果为 1 p++;// 对 p 进行算术运算,此时 p 将指向二维数组的下一行的首地址,即 &arr[1][0] printf("%d\n", (*p)[1]);// 输出结果为5 return 0; }

2.4数组名 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.

数组指针的使用 那数组指针是怎么使用的呢? 既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。 看代码:

#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,0}; int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p //但是我们一般很少这样写代码 return 0; }

一个数组指针的使用:

#include <stdio.h> void print_arr1(int arr[3][5], int row, int col) { int i = 0; for (i = 0; i < row; i++) { for (int j = 0; j < col; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } void print_arr2(int(*arr)[5], int row, int col) { int i = 0; for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } int main() { int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 }; print_arr1(arr, 3, 5); //数组名arr,表示首元素的地址 //但是二维数组的首元素是二维数组的第一行 //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址 //可以数组指针来接收 print_arr2(arr, 3, 5); return 0; }

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

int arr[5]; int *parr1[10]; int (*parr2)[10]; int (*parr3[10])[5];

2.5.数组传参、指针传参

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

2.5.1 一维数组传参

示例:

void test(int arr[])//ok? {} void test(int arr[10])//ok? {} void test(int *arr)//ok? {} 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); }

上述传参方式都可以。

2.5.2 二维数组传参

示例:

#include<stdio.h> void test(int arr[3][5])//ok? {} void test(int arr[][])//ok? 错误 {} void test(int arr[][5])//ok? {} void test(int *arr)//ok? {} void test(int* arr[5])//ok? {} void test(int(*arr)[5])//ok? {} void test(int **arr)//ok? {} int main() { int arr[3][5] = { 0 }; test(arr); }

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

2.5.3 一级指针传参

示例:

#include<stdio.h> void print(int *p, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d\n", *(p + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9 }; int *p = arr; int sz = sizeof(arr) / sizeof(arr[0]); //一级指针p,传给函数 print(p, sz); return 0; }
2.5.4 二级指针传参

示例:

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

3.指针与函数

C语言的所有参数均是以“传值调用”的方式进行传递的,这意味着函数将获得参数值的一份拷贝。这样,函数可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。

3.1 指针作为函数的参数

传值调用的好处是是被调函数不会改变调用函数传过来的值,可以放心修改。但是有时候需要被调函数回传一个值给调用函数,这样的话,传值调用就无法做到。为了解决这个问题,可以使用传指针调用。指针参数使得被调函数能够访问和修改主调函数中对象的值。 例如:

#include "stdio.h" void swap1(int a, int b)// 参数为普通的 int 变量 { int temp; temp = a; a = b; b = temp; } void swap2(int *a, int *b)// 参数为指针,接受调用函数传递过来的变量地址作为参数,对所指地址处的内容进行操作 { int temp;// 最终结果是,地址本身并没有改变,但是这一地址所对应的内存段中的内容发生了变化,即x,y的值发生了变化 temp = *a; *a = *b; *b = temp; } int main() { int x = 1, y = 2; swap1(x, y); // 将 x,y 的值本身作为参数传递给了被调函数 printf("%d %5d\n", x, y); // 输出结果为:1 2 swap(&x, &y); // 将 x,y 的地址作为参数传递给了被调函数,传递过去的也是一个值,与传值调用不冲突 printf("%d %5d\n", x, y); // 输出结果为:2 1 return 0; }

3.2 函数指针

3.2.1函数指针

在C语言中,函数本身不是变量,但是可以定义指向函数的指针,也称作函数指针,函数指针指向函数的入口地址。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。 声明一个函数指针的方法如下:

返回值类型 (* 指针变量名)([形参列表]; int (*pointer)(int *,int *); // 声明一个函数指针

上述代码声明了一个函数指针 pointer ,该指针指向一个函数,函数具有两个 int * 类型的参数,且返回值类型为 int。

应用:

#include "stdio.h" #include "string.h" int str_comp(const char *m,const char *n); // 声明一个函数 str_comp,该函数有两个 const char 类型的指针,函数的返回值为 int 类型 void comp(char *a,char *b,int (*prr)(const char *,const char*)); // 声明一个函数 comp ,注意该函数的第三个参数,是一个函数指针 int main() { char str1[20]; // 声明一个字符数组 char str2[20]; int (*p)(const char *,const char *) = str_comp;            // 声明并初始化一个函数指针,该指针所指向的函数有两个 const char 类型的指针,且返回值为 int 类型 gets(str1); // 使用 gets() 函数从 I/O 读取一行字符串 gets(str2); comp(str1,str2,p); // 函数指针 p 作为参数传给 comp 函数 return 0; } int str_comp(const char *m,const char *n) {    // 库函数 strcmp 用于比较两个字符串,其原型是: int strcmp(const char *s1,const char *s2); if(strcmp(m,n) == 0) return 0; else return 1; } /* 函数 comp 接受一个函数指针作为它的第三个参数 */ void comp(char *a,char *b,int (*prr)(const char *,const char*)) { if((*prr)(a,b) == 0) printf("str1 = str2\n"); else printf("str1 != str2\n"); }

这段代码的功能是从键盘读取两行字符串(长度不超过20),判断二者是否相等。

注意,声明一个函数指针时,() 不能漏掉,否则:

 int *p(void *,void*);

这表明 p 是一个函数,该函数返回一个指向 int 类型的指针。

3.2.2 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:

int *arr[10]; //数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢? 下面哪个是呢?

int (*parr1[10]])(); int *parr2[10](); int (*)() parr3[10];

答案是:parr1 。parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。

函数指针数组的用途:转移表(计算器)

#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int div(int a, int b) { return a / b; } int main() { int x, y; int input = 1; int ret = 0; int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表 while (input) { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf("*************************\n"); printf("请选择:"); scanf("%d", &input); if ((input <= 4 && input >= 1)) { printf("输入操作数:"); scanf("%d %d", &x, &y); ret = (*p[input])(x, y); printf("ret = %d\n", ret); } else printf("输入有误\n"); } return 0; }
3.2.2 指向函数指针数组的指针

指向函数指针数组的指针是一个指针,指针指向一个 数组 ,数组的元素都是 函数指针 ; 如何定义?

void test(const char* str) { printf("%s\n", str); } int main() { //函数指针pfun void (*pfun)(const char*) = test; //函数指针的数组pfunArr void (*pfunArr[5])(const char* str); pfunArr[0] = test; //指向函数指针数组pfunArr的指针ppfunArr void (*(*ppfunArr)[10])(const char*) = &pfunArr; return 0; }
3.2.2 回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这 个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而 是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应

首先演示一下qsort函数的使用:

//qosrt函数的使用者得实现一个比较函数 int int_cmp(const void * p1, const void * p2) { return (*( int *)p1 > *(int *) p2); } int main() { int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 }; int i = 0; qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp); for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++) { printf( "%d ", arr[i]); } printf("\n"); return 0; }

使用回调函数,模拟实现qsort(采用冒泡的方式)。

#include <stdio.h> int int_cmp(const void * p1, const void * p2) { return (*(int *)p1 > *(int *)p2); } void _swap(void *p1, void * p2, int size) { int i = 0; for (i = 0; i < size; i++) { char tmp = *((char *)p1 + i); *((char *)p1 + i) = *((char *)p2 + i); *((char *)p2 + i) = tmp; } } void bubble(void *base, int count, int size, int(*cmp)(void *, void *)) { int i = 0; int j = 0; for (i = 0; i < count - 1; i++) { for (j = 0; j < count - i - 1; j++) { if (cmp((char *)base + j * size, (char *)base + (j + 1)*size) > 0) { _swap((char *)base + j * size, (char *)base + (j + 1)*size, size); } } } } int main() { int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 }; //char *arr[] = {"aaaa","dddd","cccc","bbbb"}; int i = 0; bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp); for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }

4.指针与结构体

4.1 结构体

结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。由于结构将一组相关的变量看做一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。声明一个结构的方式如下:

struct message{       // 声明一个结构 message char name[10]; // 成员 int age; int score; }; typedef struct message s_message;     // 类型定义符 typedef s_message mess = {"tongye",23,83};    // 声明一个 struct message 类型的变量 mess,并对其进行初始化  -------------------------------------------------------------------------------------------------------------- /* 另一种更简便的声明方法 */ typedef struct message{   char name[10];   int age;   int score; }message;

可以使用 结构名.成员 的方式来访问结构中的成员,如下:

#include "stdio.h" int main(){   printf("%s\n",mess.name);    // 输出结果:tongye   printf("%d\n",mess.age);     // 输出结果:23   return 0; }

4.2 结构指针

结构指针是指向结构的指针,以上面的结构为例,可以这样定义一个结构指针:

message *p; // 声明一个结构指针 p ,该指针指向一个 message 类型的结构 *p = &mess;      // 对结构指针的初始化与普通指针一样,也是使用取地址符 &

C语言中使用 -> 操作符来访问结构指针的成员,举个例子:

#include "stdio.h" typedef struct{ char name[10]; int age; int score; }message; int main(){ message mess = {"tongye",23,83}; message *p = &mess; printf("%s\n",p->mess);      // 输出结果为:tongye printf("%d\n",p->score); // 输出结果为:83 return 0; }

5.指针的运算

C 指针的算术运算只限于两种形式:

5.1指针加减整数 :

可以对指针变量 p 进行 p++、p–、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数。用一张图来说明一下:  在上图中,10000000等是内存地址的十六进制表示(数值是假定的),p 是一个 int 类型的指针,指向内存地址 0x10000008 处。则 p++ 将指向与 p 相邻的下一个内存地址,由于 int 型数据占 4 个字节,因此 p++ 所指的内存地址为 1000000b。其余类推。不过要注意的是,这种运算并不会改变指针变量 p 自身的地址,只是改变了它所指向的地址。举个例子:

5.2指针 - 指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。两个指针相减的结果的类型是 ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。举个例子:

#include "stdio.h" int main(){ int a[10] = {1,2,3,4,5,6,7,8,9,0}; int sub; int *p1 = &a[2]; int *p2 = &a[8]; sub = p2-p1; printf("%d\n",sub);    // 输出结果为 6 return 0;
最新回复(0)