跳过正文
P8 指针
  1. S/
  2. C/C++/
  3. C/
  4. C语言程序设计/

P8 指针

Li
作者
Li
往前走,别回头!
目录
C 语言程序设计笔记 - 这篇文章属于一个选集。

知识点
#

9.1 指针的基本概念详解
#

  • @ 指针基本概念图
    • ![[白板/P23 指针基本概念.canvas|P23 指针基本概念]]

9.1.1 前提知识
#

  • 变量存储在内存中,不同类型的变量保存在不同存储区(静态存储区、动态存储区)
  • 变量的内存在编译时或程序执行时分配
  • 各类型占用字节数(x86 平台):
类型字节数
int4
char1
float4
double8

9.1.2 地址的概念
#

[!tip] 地址

  • @ 地址: 计算机中用一个数字来描述内存位置
  • 内存地址从上到下从小到大排列
  • 每个变量占若干字节,系统通过变量名与地址的对应表来访问变量

9.1.3 直接访问和间接访问
#

[!tip] 访问方式

  • @ 直接访问: 按变量地址存取变量值(如 printf("%d", i);
  • @ 间接访问: 通过存放地址的特殊变量来访问另一个变量的值
  • @ 指针变量: 专门用来存放另一个变量地址的变量
  • ! 指针变量本身也占内存(x86 下 4 字节,x64 下 8 字节)
  • ! 区分"指针"和"指针变量":==指针就是地址==,==指针变量是存放地址的变量==

9.2 变量的指针和指向变量的指针变量
#

  • @ 指针变量图
    • ![[白板/P24 指针变量.canvas|P24 指针变量]]

9.2.1 指针变量的定义
#

[!tip] 指针变量定义

  • 一般形式: 类型标识符 *标识符;
  • 三种写法等价: int *p; / int* p; / int * p;
int i = 7, j = 9;
float k = 12.6f;
int *mypoint1, *mypoint2;  // 指向整型变量的指针
float *pm3;                 // 指向实型变量的指针
char *pm4;                  // 指向字符型变量的指针

mypoint1 = &i;  // mypoint1 指向 i
mypoint2 = &j;  // mypoint2 指向 j

定义要点
#

  • ! 定义时 * 表示正在定义指针变量,使用时变量名前没有 *
  • ! 指针变量名是 mypoint1,不是 *mypoint1
  • ! 一个指针变量只能指向同一类型的变量

9.2.2 指针变量的引用
#

[!tip] 两个关键运算符

  • @ &: 取地址运算符
  • @ *: 指针运算符(间接访问运算符),访问指针所指向的变量
int a = 100, b = 200;
int *p1, *p2;
p1 = &a;   // p1 指向 a
p2 = &b;   // p2 指向 b
int *p3 = &a;  // 定义时初始化

printf("%d, %d\n", a, b);       // 100, 200
printf("%d, %d\n", *p1, *p2);   // 100, 200,*p1 代表 a,*p2 代表 b

易混淆写法
#

写法含义
&*p1等价于 &a,也就是 p1 本身
*&a等价于 *p1,也就是 a 本身
(*p1)++等价于 a++,值加 1
*p1++等价于 *(p1++),先取值再移动指针

指针自增的含义
#

  • ! p1++ 不是地址简单 +1,而是跳过所指向类型占用的字节数
  • int 指针 p1++ 地址 +4,char 指针 p1++ 地址 +1
// 交换两个指针指向的值
int *pmax, *pmin, *p, a = 5, b = 8;
pmax = &a;
pmin = &b;
if(a < b) {
    p = pmax;
    pmax = pmin;  // pmax 指向大的值
    pmin = p;     // pmin 指向小的值
}
printf("max = %d, min = %d\n", *pmax, *pmin);  // max = 8, min = 5

9.2.3 指针变量作为函数参数
#

[!tip] 指针作函数参数

  • 将变量的地址传送到函数中,可以在函数中改变调用者的变量值
  • ! 形参和实参依然是指针变量的值传递,但因为传递的是地址,所以可以间接修改原变量
void swap(int *pdest1, int *pdest2) {
    int temp;
    temp = *pdest1;       // 取 pdest1 指向的值
    *pdest1 = *pdest2;    // 赋值给 pdest1 指向的变量
    *pdest2 = temp;       // 赋值给 pdest2 指向的变量
}

int main() {
    int a = 5, b = 6;
    int *p1 = &a, *p2 = &b;
    if(a < b) swap(p1, p2);
    printf("a=%d, b=%d\n", a, b);  // a=6, b=5
    return 0;
}

9.3 数组的指针和指向数组的指针变量
#

  • @ 数组指针图
    • ![[白板/P25 数组指针.canvas|P25 数组指针]]

9.3.1 指向数组元素的指针
#

int a[5];
int *p;
p = &a[0];  // 等价于 p = a;  数组名就是首地址
// 定义时初始化
int *p = &a[0];  // 等价于 int *p = a;

9.3.2 通过指针引用数组元素
#

表达式含义类型
p + i / a + ia[i] 的地址地址
*(p + i) / *(a + i)a[i] 的值
p[i]等价于 *(p + i),即 a[i]
int a[5] = {12, 14, 20, 18, 50};
int *p;

// 方法1: 下标法
for(int i = 0; i < 5; i++)
    printf("%d\n", a[i]);

// 方法2: 地址法
for(int i = 0; i < 5; i++)
    printf("%d\n", *(a + i));

// 方法3: 指针法(最快)
for(p = a; p < (a + 5); p++)
    printf("%d\n", *p);

注意事项
#

  • ! a++ 不合法,数组名是常量,不能自增
  • ! 指针法中循环结束后 p 已指向数组后面的内存,不能操作
  • ! a[5] 虽然有对应地址,但不能操作

指针自增/自减运算
#

写法含义
*p++先取 *p 的值,再 p++ 指向下一元素
*++pp++,再取新指向的值
(*p)++p 指向的元素值 +1,指针不变

9.3.3 数组名作为函数参数
#

[!tip] 实参与形参的 4 种对应关系

实参形参
数组名数组名
数组名指针变量
指针变量指针变量
指针变量数组名
// 实参用数组名,形参用指针变量
void changevalue(int *p) {
    *(p + 2) = 888;  // 等价于给 a[2] 赋值 888
}

int main() {
    int a[5] = {85, 70, 98, 92, 78};
    changevalue(a);  // 传递数组首地址
    return 0;
}

9.3.4 回顾二维数组和多维数组的概念
#

  • 二维数组 int a[3][4] 可看作 3 个一维数组 a[0]a[1]a[2]
  • 每个一维数组包含 4 个元素
  • 二维数组在内存中按行连续存放
  • 多维数组:最右边维度下标变化最快

9.3.5 指向多维数组的指针和指针变量
#

[!tip] 二维数组地址与值

表现形式含义类型
a第 0 行首地址地址
a + ii 行首地址地址
a[i] / *(a + i)i 行首地址地址
a[i] + j / *(a + i) + ja[i][j] 的地址地址
*(a[i] + j) / *(*(a + i) + j)a[i][j] 的值
  • ! a*a 都是地址,值相同但含义不同
  • ! a[i] 在二维数组中代表地址(一维数组名),不是值
int a[3][4];
int *p;
p = (int *)(a + 1);  // 第 1 行首地址
*p = 56;              // 相当于 a[1][0] = 56
p++;
*p = 78;              // 相当于 a[1][1] = 78

9.3.6 指针数组和数组指针
#

[!tip] 指针数组 vs 数组指针

  • @ 指针数组: 本质是数组,每个元素都是指针
    • int *p[10]; — 10 个指针变量的数组
  • @ 数组指针: 本质是指针,指向一个数组
    • int (*p)[4]; — 指向含 4 个元素的一维数组的指针
  • ! 记忆技巧:以什么结尾就是什么。“指针数组"是数组,“数组指针"是指针
// 指针数组:适合指向若干字符串
const char *pName[] = {"C++", "Java", "Python", "Go", "CSharp"};

// 交换指针数组中的指针
const char *ptmp;
ptmp = pName[0];
pName[0] = pName[1];  // pName[0] 指向 "Java"
pName[1] = ptmp;       // pName[1] 指向 "C++"

9.3.7 多维数组的指针作为函数参数
#

  • 与一维数组类似,多维数组的地址也可以作为函数参数传递

9.4 字符串的指针和指向字符串的指针变量
#

  • @ 字符串指针图
    • ![[白板/P26 字符串指针.canvas|P26 字符串指针]]

9.4.1 字符串表示形式
#

用字符数组实现
#

char mystr[] = "I love China";  // 字符串复制到数组中
printf("%s\n", mystr);

用字符指针实现
#

const char *pmystr = "I love China";  // 指针指向字符串常量
printf("%s\n", pmystr);
  • ! 字符数组:每个数组有独立内存,字符串常量被复制到数组中
  • ! 字符指针:指向字符串常量的首地址,多个指针可能指向同一地址
  • ! 字符串常量所在内存是只读的,不能修改

字符串复制范例
#

// 下标法
char a[] = "I love China!";
char b[100];
for(int i = 0; *(a + i) != '\0'; i++)
    *(b + i) = *(a + i);
*(b + i) = '\0';

// 指针法
char *p1 = a, *p2 = b;
for(; *p1 != '\0'; p1++, p2++)
    *p2 = *p1;
*p2 = '\0';

9.4.2 字符串指针作为函数参数
#

  • 用字符数组名或指向字符串的指针变量作为参数,可以在被调函数中改变字符串内容
void copystr(char *from, char *to) {
    while(*from)           // 不遇到 '\0' 就继续
        *to++ = *from++;   // 先赋值再各自后移
    *to = '\0';
}

9.4.3 字符指针变量与字符数组
#

对比项字符数组字符指针变量
存储方式各元素存放字符存放字符串首地址
赋值方式定义时初始化或 strcpy可以直接指向字符串常量
值能否改变数组名值不能改变指针值可以改变
// 字符数组:不能直接赋值
char str[100];
// str = "I love China!";  // 不允许!
strcpy(str, "I love China!");  // 正确

// 字符指针:可以直接指向
const char *a = "I love China!";
a = a + 7;  // 指针可以改变,现在指向 "China"
printf("%s\n", a);  // China

9.5 函数指针和返回指针值的函数
#

  • @ 函数指针图
    • ![[白板/P27 函数指针.canvas|P27 函数指针]]

9.5.1 用函数指针变量调用函数
#

[!tip] 函数指针

  • @ 函数指针: 指向函数的指针变量,通过它可以调用所指向的函数
  • 定义形式: 数据类型 (*指针变量名)(形参列表);
int max(int x, int y) {
    if(x > y) return x;
    return y;
}

int (*p)(int, int);  // 定义函数指针变量
p = max;             // 将函数入口地址赋给 p
int c = (*p)(5, 19); // 通过函数指针调用,等价于 c = max(5, 19);
// 也可以写成 c = p(5, 19);
  • ! int *p(int, int); 是返回指针的函数声明,不是函数指针!
  • ! (*p) 的括号不能省,否则优先级不同含义完全不同
  • ! 对函数指针做 p++p--p+n 等运算没有意义,也不合法

9.5.2 把指向函数的指针变量作为函数参数
#

  • 将函数地址传递给形参,可以在函数中调用不同的函数
int callmax(int x, int y, int (*idfunc)(int, int)) {
    int result = idfunc(x, y);  // 通过函数指针调用
    return result;
}

int main() {
    int c = callmax(5, 19, max);  // 传递函数名
    // 也可以用函数指针变量
    int (*p)(int, int) = max;
    c = callmax(45, 21, p);
    return 0;
}

9.5.3 返回指针值的函数
#

[!tip] 返回指针的函数

  • 定义形式: 数据类型 *函数名(参数列表) { ... }
  • ! 不要返回局部变量的地址!函数调用完毕后局部变量内存被回收
// 错误示范:返回局部变量地址
int *add(int x, int y) {
    int sum = x + y;
    return &sum;  // 致命问题!sum 的内存会被回收
}

// 正确做法:使用全局变量
int sum;  // 全局变量,生存期到程序结束
int *add(int x, int y) {
    sum = x + y;
    return &sum;  // 安全
}

9.6 指针数组、指针的指针与 main 函数参数
#

  • @ 指针进阶图
    • ![[白板/P28 指针进阶.canvas|P28 指针进阶]]

9.6.1 指针数组概念回顾
#

  • 指针数组定义: 类型标识符 *数组名[数组长度];
  • int *p[4]; — 4 个指针元素的数组
  • int (*p)[4]; — 指向含 4 个元素的一维数组的指针

9.6.2 指向指针的指针
#

[!tip] 指向指针的指针

  • @ 指向指针的指针: 一个指针变量指向另一个指针变量
  • 定义: int **p;p 指向一个 int * 类型的指针变量
  • *pp 指向的指针变量,**p 是最终指向的值

9.6.3 main 函数参数
#

  • main 函数可以接收命令行参数: int main(int argc, char *argv[])
  • argc: 参数个数
  • argv: 指针数组,每个元素指向一个参数字符串
  • ! argv[0] 是可执行文件的完整路径文件名
int main(int argc, char *argv[]) {
    for(int i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);
    return 0;
}

9.7 本章小结
#

指针数据类型小结
#

定义含义
int i;整型变量 i
int *p;指向整型数据的指针变量
int a[n];整型数组,n 个元素
int *p[n];指针数组,n 个指向整型的指针
int (*p)[n];指向含 n 个元素的一维数组的指针
int f();函数 f,返回整型值
int *p();函数 p,返回指向整型的指针
int (*p)();指向函数的指针,函数返回整型值
int **p;指向指针的指针

指针运算小结
#

  • p++/p--: 地址加减所指向类型占用的字节数
  • 指针变量不能直接赋数字地址,只能赋已分配的变量地址
  • 指针可以指向 NULL(空),表示不指向任何有效内容

void * 型指针
#

  • void * 是万能型指针,可以指向任意数据类型
  • 使用时需要强制类型转换回原类型
void *pvn = NULL;
int a = 3;
int *p = &a;
float f = 5.6f;
float *pf = &f;

pvn = p;    // 不报错
pvn = pf;   // 不报错
pf = (float *)pvn;  // 强制转换回原类型
Reply by Email
C 语言程序设计笔记 - 这篇文章属于一个选集。

相关文章