Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

  • Golang

  • Kubernetes

  • 硅谷课堂

  • C

    • 数据类型、运算符与表达式
    • 选择与循环
    • 数组
    • 指针
      • 指针的本质
        • 指针的定义
        • 取地址操作符与取值操作符
      • 指针的使用场景
        • 指针的传递
        • 指针的偏移
        • 指针与自增、自减运算符
        • 指针与一维数组
        • 指针与动态内存申请
        • realloc 动态扩容
        • 字符指针与字符数组的初始化
        • 深入理解 const
        • memcpy 函数与 memmove 函数的差异
      • 数组指针与二维数组
        • 数组指针的应用
        • 二维数组的偏移计算
      • 二级指针
        • 二级指针的传递
        • 二级指针的偏移
      • 函数指针
    • 函数
    • 结构体
    • 常用的数据结构与算法
    • 文件操作
  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • C
Iekr
2022-09-19
目录

指针

# 指针

前面介绍了整型、浮点型、字符型数据,并在实际代码的运行过程中展示了这些数据的地址。如果我们在程序中需要将某个变量的地址保存下来,那么这时应该怎么做?为解决这个问 题,C 语言为我们提供了指针。

# 指针的本质

# 指针的定义

内存区域中的每字节都对应一个编号,这个编号就是 “地址”。如果在程序中定义了一个变量,那么在对程序进行编译时,系统就会给这个变量分配内存单元。按变量地址存取变量值的方 式称为 “直接访问”,如 printf("%d",i); 、 scanf("%d",&i); 等;另一种存取变量值的方式称为 “间接访问”,即将变量 i 的地址存放到另一个变量中。在 C 语言中,指针变量是一种特殊的变量, 它用来存放变量地址。

指针变量的定义格式如下:

基类型 *指针变量名;
1

例如,

int *i_pointer;
1

指针与指针变量是两个概念,一个变量的地址称为该变量的 “指针”。例如,地址 2000 是变量 i 的指针。如果有一个变量专门用 来存放另一变量的地址(即指针),那么称它为 “指针变量”。

image-20220919161710899

int main(){
    int i=3;
    int *i_pointer; //指针变量
    scanf("%d",&i); 
    printf("%d\n",i); //直接访问
    i_pointer = &i; //&对i进行取地址 取是起始地址
    print("%d\n",*i_pointer); //间接访问 通过指针变量访问
}
1
2
3
4
5
6
7
8

image-20220919162137677

从上图可以看到 i_pointer 存储的是 i 的地址

# 取地址操作符与取值操作符

取地址操作符为 & ,也称引用,通过该操作符我们可以获取一个变量的地址值;取值操作符为 * ,也称解引用,通过该操作符我们可以得到一个地址对应的数据。

编写时有 4 个注意点

  1. 指针变量前面的 “*” 表示该变量为指针型变量。例如,

    float *pointer_1;
    
    1

    注意指针变量名是 pointer_1 ,而不是 * pointer_1。

  2. 在定义指针变量时必须指定其类型。需要注意的是,只有整型变量的地址才能放到指向整型变量的指针变量中。例如,下面的赋值是错误的:

    float a;
    int *pointer_1;
    pointer_1=&a; //毫无意义而且会出错
    
    1
    2
    3
  3. 如果已执行了语句

    pointer_1=&a;
    
    1

    那么 &* pointer_1 的含义是什么呢?

    &* pointer_1
    
    1

    “&” 和 “*” 两个运算符的优先级别相同,但要按自右向左的方向结合。因此,&* pointer_1 与 & a 相同,都表示变量 a 的地址,也就是 pointer_1。 *&a 的含义是什么呢?

    *&a
    
    1

    首先进行 & a 运算,得到 a 的地址,再进行 * 运算。*&a 和 * pointer_1 的作用是一样的,它们都等价于变量 a,即 *&a 与 a 等价。

  4. C 语言本质上是一种自由形式的语言,这很容易诱使我们把 “*” 写在靠近类型的一侧。要声明三个指针变量,正确的语句 如下:

    // 正确例子
    int *a,*b,*c;
    
    // 错误例子 
    int *a,b,c;
    
    1
    2
    3
    4
    5

# 指针的使用场景

指针的使用场景通常只有两个,即传递与偏移,我们应时刻记住只有在这两种场景下使用 指针,才能准确地使用指针。

# 指针的传递

#include <stdio.h> 
#include <stdlib.h>

void change(int j) {
	j=5;
}
int main() {
	int i=10;
	printf("before change i=%d\n",i);
	change(i);
	printf("after change i=%d\n",i);
	system("pause");
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在本例的主函数中,定义了整型变量 i,其值初始化为 10,然后通过子函数修改整型变量 i 的值。但是,我们发现执行语句 printf ("after change i=% d\n",i); 后,打印的 i 的值仍为 10,子函数 change 并未改变变量 i 的值。

image-20220919164533946

程序的执行过程其实就是内存的变化过程,我们需要关注的是栈空间的变化。当 main 函数开始执行时,系统会为 main 函数开辟函数栈空间,当程序走到 int i 时,main 函数的栈空间就会为变量 i 分配 4 字节大小的空间。调用 change 函数时,系统 会为 change 函数重新分配新的函数栈空间,并为形参变量 j 分配 4 字节大小的空间。在调用 change (i) 时,实际上是将 i 的值赋值给 j,我们把这种效果称为值传递(C 语言的函数调用均为 值传递)。因此,当我们在 change 函数的函数栈空间内修改变量 j 的值后,change 函数执行结 束,其栈空间就会释放,j 就不再存在,i 的值不会改变。

void change(int* j) {
	*j=5; //间接访问得到变量 i
}

//指针的传递 
int main() {
	int i=10;
	printf("before change i=%d\n",i);
	change(&i);
	//传递变量 i 的地址
	printf("after change i=%d\n",i);
	system("pause");
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们将变量 i 的地址传递给 change 函数时, 实际效果是 j=&i,依然是值传递,只是这时我们的 j 是一个指针变量,内部存储的是变量 i 的 地址 0x0015ff0c,所以通过 * j 就间接访问到了与变量 i 相同的区域,通过 * j=5 就实现了对变量 i 的值的改变。

# 指针的偏移

前面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是 B,那么往前就是 A,往后就是 C,所以应用指针的另一个场景就是对其进行加减,但对指针进行乘除是没 有意义的,就像家庭地址乘以 5 没有意义那样。在工作中,我们把对指针的加减称为指针的偏移, 加就是向后偏移,减就是向前偏移。下面通过代码来学习。

#define N 5 //指针的偏移 

int main() {
	int a[N]= {1,2,3,4,5};
	int *p;
	int i;
	p=a;
	//保证等号两边的数值类型一致
	for (i=0;i<N;i++) //正序输出 
    {
		printf("%3d",*(p+i));//p+1 加的是指针本身的基类型 sizeof(int)
	}
	printf("\n-----------------\n");
	p=&a[4]; //取的是最后一个元素的指针地址
	//让 p 指向最后一个元素 
	for (i=0;i<N;i++) //逆序输出
    {
		printf("%3d",*(p-i));
	}
	printf("\n");
	system("pause");
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

数组名中存储着数组的起始地址 0x28F768,其类型为整型指针,所以可以将其赋值给整型指针变量 p,可以从监视窗口中看到 p+1 的值为 0x28F76C。那么为什么加 1 后不是 0x28F769 呢?因为指针变量加 1 后,偏移的长度是其基类型的长度,也就是偏移 sizeof (int),这 样通过 (p+1) 就可以得到元素 a [1]*。编译器在编译时,数组取下标的操作正是转换为指针偏移来完成的。

# 指针与自增、自减运算符

#include <stdio.h> 
#include <stdlib.h>

//只有比后增优先级高的操作符,才会作为一个整体,如()、[] 
int main() {
	int a[3]= {2,7,8}
	;
	int *p;
	int j;
	p=a;
	j=*p++;	//先把*p 的值赋给 j,然后对 p 加 1 
	printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p); //2 2 7
	j=p[0]++;	//先把 p[0]赋给 j,然后对 p[0]加 1 
	printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p);// 2 7 8
	system("pause");
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

前面讲过当遇到后增操作符时,就要分两步来看。第一步是暂时忽略 ++ 操作,即 j=*p,因为 p 指向数组的第一个元 素,所以 j=2。那么第二步是对 p 进行 ++ 操作,还是对 * p 整体也就是数组第一个元素的值进行 ++ 操作呢?这里实际上是对 p 进行 ++ 操作,因为 * 操作符和 ++ 操作符的优先级相同,只有比 ++ 优先 级高的操作符才会当成一个整体,目前我们用过的比 ++ 操作符优先级高的只有 () 和 [] 两个操作符。

# 指针与一维数组

定义一个指针变量时,指针变量的类型要和数组的数据类型保 持一致,通过取值操作,就可将 “h” 改为 “H”,这种方法称为指针法。获取数组元素时,也可 以通过取下标的方式来获取数组元素并进行修改,这种方法称为下标法。

void change(char *p){
    *p='H';
    p[1]='E';
    *(p+2)='L';
}

int main(){
    char c[10]="hello";
    change(c);
    puts(c); //"HELlo"
}
1
2
3
4
5
6
7
8
9
10
11

# 指针与动态内存申请

很多读者在学习 C 语言的数组后都会觉得数组长度固定很不方便,其实 C 语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编 译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间。

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h>
int main() {
	int i;
    char *p;
	scanf("%d",&i); //输入要申请的空间大小 
    p=(char*)malloc(i); //使用 malloc 动态申请堆空间 	
    strcpy(p,"malloc success"); 
    puts(p); 
    free(p); //free 时必须使用 malloc 申请时返回的指针值,不能进行任何偏移 
    p=NULL; // 要free后的指针置为NULL 否则为野指针 如果在free之后申请新的指针 新的指针会使用free后的地址 然后这时候如果通过p的地址修改内容 则同时修改新的指针的内容 即释放后的p与新申请的指针共用一个地址
    printf("free success\n"); 
    system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

首先我们来看 malloc 函数。

void *malloc(size_t size);
1

需要给 malloc 传递的参数是一个整型变量,因为这里的 size_t 即为 int;返回值为 void 类型的指针,void * 类型的指针只能用来存储一个地址而不能进行偏移 *,因为 malloc 并不知道我 们申请的空间用来存放什么类型的数据,所以确定要用来存储什么类型的数据后,都会将 void 强制转换为对应的类型 *。

如下图所示,定义的整型变量 i、指针变量 p 均在 main 函数的栈空间中,通过 malloc 申请的空间会返回一个堆空间的首地址,我们把首地址存入变量 p。知道了首地址,就可以通过 strcpy 函数往对应的空间存储字符数据。

image-20220919174346475

栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过 free 函数释放堆空间。显然,堆的效率要比栈低得多。free 函数的头文件及格式为

#include <stdlib.h> 
void free(void *ptr);
1
2

其传入的参数为 void 类型指针,任何指针均可自动转为 void * 类型指针,所以我们把 p 传递给 free 函数时,不需要强制类型转换。p 的地址值必须是 malloc 当时返回的地址值,不能进行偏 移,也就是在 malloc 和 free 之间不能进行 p++ 等改变变量 p 的操作,原因是申请一段堆内存空 间时,内核帮我们记录的是起始地址和大小,所以释放时内核用对应的首地址进行匹配,匹配不 上时,进程就会崩溃。如果要偏移进而存储数据,那么可以定义两个指针变量来解决。


栈空间与堆空间的差异

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//函数栈空间释放后,函数内的所有局部变量消失
char *print_stack()
{
    char c[] = "I am print_stack";
    puts(c);
    return c;
}
//堆空间不会因函数执行结束而释放
char *print_malloc()
{
    char *p;
    p = (char *)malloc(20);
    strcpy(p, "I am print_malloc");
    puts(p);
    return p;
}
int main()
{
    char *p;
    p = print_stack();  //数据放在栈空间
    puts(p);            //打印有异常,出现乱码
    p = print_malloc(); //数据放在堆空间
    puts(p);
    system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

image-20220923170710280

为什么第二次打印会有异常而且每次执行打印的效果都不一样呢?原因是 print_stack () 函数中的字符串存放在栈空间中,函数执行结束后, 栈空间会被释放,字符数组 c 的原有空间已被分配给其他函数使用,因此在调用 print_stack () 函数后,执行 puts (p) 就会出现打印乱码。而 print_malloc () 函数中的字符串存放在堆空间中,堆空间只有在执行 free 操作后才会释放,否则在进程执行过程中会一直有效。

# realloc 动态扩容

realloc 格式为:

void *realloc(void *ptr, size_t size)
1
  1. 如果原始数组指向的空间之后有足够的空间可以追加,则直接追加,返回的是原数组的起始地址。
  2. 若原始数组指向的空间之后没有足够的空间可以追加,则 realloc 函数会重新找一个新的内存区域,重新开辟一块指定个字节的动态内存空间,若开辟新的数组空间成功:将原数组中的数据拷贝到新的数组中,释放掉原数组,并返回新的数组起始地址,需要用一个指针变量来保存。
  3. 若开辟新的数组空间失败:不会释放掉原数组,会返回一个 NULL,表示开辟新数组失败,原来的数组不作变动。
#include <stdio.h>
#include <stdlib.h>

#define CAPACITY 10
int main()
{
    char *p = (char *)malloc(CAPACITY);
    char c;
    int i = 0, cap = CAPACITY;
    while (scanf("%c", &c) != EOF)
    {
        if (i == cap - 1)
        {
            cap = 2 * cap;
            p = (char *)realloc(p, cap);
        }
        p[i] = c;
        i++;
    }
    p[i] = 0;
    puts(p);
    system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 字符指针与字符数组的初始化

字符指针可以初始化赋值一个字符串,字符数组初始化也可以赋值一个字符串。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    char *p = "hello";    //把字符串型常量"hello"的首地址赋给 p
    char c[10] = "hello"; //等价于
    strcpy(c, "hello");
    c[0] = 'H';
    printf("c[0]=%c\n", c[0]);
    printf("p[0]=%c\n", p[0]);
    // p[0]='H'; //不可以对常量区数据进行修改
    p = "world"; //将字符串 world 的地址赋给 p
    // c="world"; //非法 
    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

编译器在编译时,将字符串型常量存储在数据区中的常量区,这样做的好处是相同的字符串如 "hello" 只会存储一次,常量区的含义是存储在此区域中的字 符串本身是不可修改的,我们称之为字符串型常量。"hello" 存储在字符串型常量区,占用 6 字节,有自己的首地址,char *p="hello" 将字符串型常量 "hello" 的首地址赋给 p。对于 char c [10]="hello",字符数组 c 在栈空间有 10 字节大小的 空间,这个初始化将字符串 "hello" 通过 strcpy 函数赋给字 符数组 c,因此我们可将 c [0] 修改为 “H”,而 p [0] 得到的是 常量区的空间,所以不可修改。

image-20220923193146149

p 是一个指针变量,因此我们可以将字符串 "world" 的首 地址重新赋给 p,而数组名 c 本身存储的就是数组的首地址,是确定的、不可修改的,c 等价于符号常量。因此,如果 c="world",那么就会造成编译不通。

# 深入理解 const

首先,const int 类型一旦定义,就不能修改,而 int 类型可以随时修改。因为 const int 是用来保存一些全局常量的,因此这些常量在编译期间可以修改,但在运行期间不能修改。听起 来这很像宏,其实它确实是用来取代宏的。例如,比较 #define PI 3 和 const int Pi = 3;。如果 代码中用到了 100 次 PI(宏),那么代码中就会保存 100 个常数 3。

由于使用常数进行运算的机器代码很多时候要比使用变量时的机器代码长,因此换用 100 次 Pi (const int) 后,程序编译后的机器码中就不需要出现 100 次常量 3,而只在需要时引用存 储有 3 的常量。

从汇编的角度看,const 定义的常量只给出了对应的内存地址,而不像 #define 那样给出 的是立即数,所以 const 定义的常量在程序运行过程中只有一份副本,而 #define 定义的常量 在内存中有若干副本。编译器通常不为普通的 const 常量分配存储空间,而将它们保存在符号 表中,这就使得它成为一个编译期间的常量,而没有存储与读内存的操作,因此使得它的效率也很高。

针对 const 修饰指针,存在以下两种情况。

(1)const char *ptr;

定义一个指向字符常量的指针,其中 ptr 是一个指向 char * 类型的常量,不能用 ptr 来修改所指向的内容。换句话说,*ptr 的值为 const,不能修改。但是 ptr 的声明并不意味着它指向的 值实际上就是一个常量,而只意味着对 ptr 而言,这个值是常量。

在下面代码块中,ptr 指向 str, 而 str 不是 const,可以直接通过 str 变量来修改 str [0] 的值,但不能通过 ptr 指针来修改(char const *ptr 与 const char *ptr 等价,通常使用 const char *ptr)。

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char str[] = "hello world";
    const char *ptr = str;
    str[0] = 'H'; //操作合法
    puts(ptr);
    ptr[0] = 'n'; //操作非法,编译错误,提示 error C2166: 左值指定 const 对象
    puts(ptr);
    system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13

(2)char * const ptr;

定义一个指向字符的指针常量,即 const 指针。上面案例可以知道,不能修改 ptr 指针,但可以修改该指针指向的内容。

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char str[] = "hello world";
    char str1[] = "how do you do";
    char * const ptr = str;
    str[0] = 'H';
    puts(ptr);
    ptr[0] = 'n'; //合法
    puts(ptr);
    ptr = str1; //非法,编译错误,error C2166: 左值指定 const 对象
    system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

由上面代码块可以看到,const 直接修饰指针时,指针 ptr 指向的内容可以修改,但是指针 ptr 在第一次初始化后,后面不能再对 ptr 进行赋值,否则会出现编译报错,当然这种场景使用得并 不多。

# memcpy 函数与 memmove 函数的差异

为什么 C 语言提供 memcpy 函数后,还要提供一个 memmove 函数呢?

原因是 memmove 函数在源内存和目的内存发生重叠时,依然可以使用,但这时不能使用 memcpy 函数。既然 memmove 函数比 memcpy 函数强大,为什么不删掉 memcpy 函数呢?原因是 memmove 函 数内部增加了判断,如果内存没有重叠也使用 memmove 函数,那么效率是低于使用 memcpy 函数的。

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h>
void mymemmove(void *to, void *from, size_t count)
{
    char *pTo, *pFrom;
    if (to > from)
    {
        pTo = (char *)to + count - 1; //注意这里是容易出错的地方
        pFrom = (char *)from + count - 1;
        while (count > 0)
        {
            *pTo = *pFrom;
            pFrom--;
            pTo--;
            count--;
        }
    }
    else
    {
        pTo = (char *)to;
        pFrom = (char *)from;
        while (count > 0)
        {
            *pTo = *pFrom;
            pFrom++;
            pTo++;
            count--;
        }
    }
}

#define N 5
int main()
{
    int a[N] = {1, 2, 3, 4, 5};
    int b[N] = {1, 2, 3, 4, 5};
    int i;
    memmove(b + 2, b + 1, 8);
    for (i = 0; i < N; i++)
    {
        printf("%3d", b[i]);
    }
    printf("\n");
    mymemmove(a + 2, a + 1, 8);
    for (i = 0; i < N; i++)
    {
        printf("%3d", a[i]);
    }
    printf("\n");
    system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

memmove 函数之所以在重叠时也能够复制正确,是因为我们需要通过源地址和目的地址的大小进行内存复制,当目的地址的值大于源地址时就从后往前复制,代码中备注的地方是容易出错的地方。这里考查的就是对指针偏移的理解。

# 数组指针与二维数组

# 数组指针的应用

很多人学习 C 语言时,都认为二维数组和二级指针是一回事,认为二维数组是用二级指针偏移实现的。这是错误的!二维数组需要通过两次偏移获取数组中的某个元素,所用的指针是数组指针,数组指针是一级指针而不是二级指针。

#include <stdio.h>
#include <stdlib.h>

//列数必须写
void print(int p[][4], int row)
{
    int i, j;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < sizeof(*p) / sizeof(int); j++)
        {
            printf("%3d", p[i][j]);
            // printf("%3d",*(*(p+i)+j)); //等价于上面的写法只是用指针偏移的方式获取元素值
        }
        printf("\n");
    }
}
//数组指针用于二维数组的传递和偏移
int main()
{
    int a[3][4] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23};
    int b[4] = {1, 2, 3, 4};
    int i = 10;
    int(*p)[4]; //定义一个数组指针
    p = a;
    print(a, 3);
    p = (int(*)[4])malloc(16 * 100); //动态二维数组
    p[99][3] = 1000;
    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

如上面代码块显示,p 是一个数组指针,其指向一个大小为 4 个整型元素的数组,所以 * p 代表一个长度为 4 的整型数组,通过 sizeof (*p) 可以看到其大小为 16 字节。前面我们学习了指针的偏移, p+1 偏移的长度为其基类型的大小,因为 p 指向一个大小为 4 个整型元素的数组,所以 p+1 偏移 16 字节,由于二维数组名 a 中存储的地址类型为数组指针,所以将 a 赋值给 p 不会有编译警告。

image-20220923195003776

通过 print 函数将二维数组以矩阵形式打印出来,可以把形参中的 int p [][4] 改为 int (*p)[4],二者是等价的。但是,二维数组的行数依然无法传递过去,所以需要通过整型变量 row 传递行数。对于打印位置的 p [i][j],可以写成 *((p+i)+j),一般来说,我们更多地使用形式 p [i][j]。首先 p+i 偏移到对应的行,然后 *(p+i) 得到对应的行,等价于一维数组,而一维数组 的数组名存储的就是一级指针,所以 (p+i)+j 就偏移到对应的元素,然后解引用就得到对应的 元素值。

# 二维数组的偏移计算

定义一个二维数组,如

int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
1

假设 a 的地址为 0x2000,表 5.3.1 展示了各种写法的偏移对应的地址值。如果定义数组指针变量 int (*p)[4],且执行 p=a,那么在表的第一列 “表达形式” 中将 a 换成 p,表的内容依然成立。

表示形式 含 义 地 址 值
a 二维数组名,指向一维数组 a [0],即 0 行首地址 0x2000
a[0],
*(a+0),
*a
0 行 0 列元素地址 0x2000
a+1,&a[1] 1 行首地址 0x2010
A[1],
*(a+1)
1 行 0 列元素 a [1][0] 的地址 0x2010
a[1]+2,
*(a+1)+2,
&a[1][2]
1 行 2 列元素 a [1][2] 的地址 0x2018
*(a[1]+2),
*(*(a+1)+2),
a[1][2]
1 行 2 列元素 a [1][2] 的值 元素值为 13

# 二级指针

一级指针的使用场景是传递与偏移,服务的对象是整型变量、浮点型变量、字符型变量等。二级指针也是一种指针,其作用自然也是传递与偏移,其服务对象更加简单,即只服务于一级指针的传递与偏移。

# 二级指针的传递

#include <stdio.h> #include <stdlib.h>
void change(int **p, int *pj)
{
    int i = 5;
    // 函数传递的为二级指针,需要进行解析才能获取一级指针(即pi=&i的地址)
    *p = pj;
}

//要想在子函数中改变一个变量的值,必须把该变量的地址传进去
//要想在子函数中改变一个指针变量的值,必须把该指针变量的地址传进去
int main()
{
    int i = 10;
    int j = 5;
    int *pi;
    int *pj;
    pi = &i;
    pj = &j;
    printf("i=%d,*pi=%d,*pj=%d\n", i, *pi, *pj); //等于 10
    // change(pi, pj); //如果函数参数为*p一级指针,则函数传递过去的为值传递,只是改变了栈中pi的地址,
    //并非改变main函数中pi的地址
    change(&pi, pj); //需要传递一个**P二级指针,并且获取一级指针的地址,才能进行改变main函数中的pi指针的地址
    printf("after change i=%d,*pi=%d,*pj=%d\n", i, *pi, *pj);
    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

整型指针 pi 指向整型变量 i,整型指针 pj 指向整型变量 j。通过子函数 change,我们想改变指针变量 pi 的值,让其指向 j。由于 C 语言的函数调用是值传递,因此要想在 change 中改变 变量 pi 的值,就必须把 pi 的地址传递给 change。

image-20220925015638936

如上图 所示,pi 是一级指针,&pi 的类型 即为二级指针,左键将其拖至内存区域可以看到指针变量 pi 本身的地址为 0x0031F800,对应存 储的地址是标注位置 1 的整型变量 i 的地址值(因为是小端,所以低位在前)。接着将其传入函 数 change,change 函数的形参 p 必须定义为二级指针,然后在 change 函数内对 p 进行解引用, 就可以得到 pi,进而对其存储的地址值进行改变。

# 二级指针的偏移

一级指针的偏移服务于数组,例如整型一级指针就服务于整型数组,所以二级指针的偏移也服务于数组,服务对象为指针数组。

如果让每个指针指向商品信息,在排序比较时我们比较实际的商品信息,但在交换时实际上交换指针,那么交换成本将会极大地降低。这种思想称为索引式排序。在下面代码块中, 我们可以把字符串视为上面说的商品信息,将指针数组 p 赋给二级指针 p2,目的是为了演示二级指针的偏移,p2+1 偏移的就是一个指针变量的大小,即 sizeof (char*),对于 Win 32 控制台 应用程序来说,就是偏移 4 字节。

#include <stdio.h> #include <stdlib.h> #include <string.h>
void print(char *p[]) //这里可以写成 char **p
{
    int i;
    for (i = 0; i < 5; i++)
    {
        puts(p[i]);
    }
}
//二级指针的偏移,服务的是指针数组
int main()
{
    char *p[5]; //定义一个指针数组
    char b[5][10] = {"lele", "lili", "lilei", "hanmeimei", "zhousi"};
    int i, j, tmp;
    char *t;
    char **p2; //定义一个二级指针
    //char **p2=(char**)malloc(sizeof(char*)*N); //动态申请指针数组
    for (i = 0; i < 5; i++) //让指针数组中的每个指针都指向一个字符串
    {
        p[i] = b[i];
    }
    p2 = p;
    for (i = 4; i > 0; i--) //冒泡排序法
    {
        for (j = 0; j < i; j++)
        {
            if (strcmp(p2[j], p2[j + 1]) == 1) //判断 p2[j]是否大于 p2[j+1]
            {
                t = p2[j];
                p2[j] = p2[j + 1];
                p2[j + 1] = t;
            }
        }
    }
    print(p2); //先打印排序结果
    puts("----------------------------");
    for (i = 0; i < 5; i++) //再看数据存储本身有没有变
    {
        puts(b[i]);
    }
    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 函数指针

在 C++ 中函数指针并不重要,原因是 C++ 的重载使用起来更加方便。

#include <stdio.h> 
#include <stdlib.h>
void b()
{
    printf("I am func b\n");
}
void a(void (*p)())
{
    p();
}
//定义函数指针,初始化只能赋函数名
int main()
{
    void (*p)(); //定义一个函数指针变量
    p = b;       //函数指针的返回值及入参要与函数保持一致 
    p(); //直接通过函数指针调用
    a(p); //传递一个函数指针 在函数内部调用 面向接口编程 函数a可以接受任何函数指针并将其调用
    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

上面代码块中,定义了函数指针 p,它将函数 b 赋给 p。这里为什么可以进行赋值呢?其实是因为函数名本身存储的即为函数入口地址,接着将 p 传递给函数 a,相当于把一个行为传递给函数 a,之前我们传递给函数的都是数据,通过函数指针可以将行为传递给一个函数,这样我们调用函数 a 就可以执行函数 b 的行为,当然也可以执行其他函数的行为。

编辑 (opens new window)
上次更新: 2023/12/06, 01:31:48
数组
函数

← 数组 函数→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式