C_指针

C_指针

指针基础

指针变量

现代计算机都将内存分割为字节,每个字节都有唯一的地址。程序中的每一个变量占一个或多个字节的内存,第一个字节的地址称为是变量的地址。

指针就是地址,而指针变量就是存储地址的变量。(有时候也把指针变量称作为指针)

当指针变量 p 存储变量 i 的地址时, p 指向了 i

指针变量

指针变量的声明

指针变量的声明与普通变量的声明基本一样,唯一不同的就是必须在指针变量名的前面加上星号:

1
int *p;

注意:

  1. 指针变量名为 p 而非 * p

  2. 指针变量的类型为 int* ,而非 int 类型。

指针变量可以和其它变量一起出现在声明中:

1
int i, j, a[10], b[20], *p, *q;

&*

获取变量的地址,取地址运算符 & 。访问指针变量所指向的对象,解引用运算符 *p 是指针, *p 就表示 p 当前指向的对象。

取地址运算符 &

& 运算符可以获取变量的地址,并可以把变量的地址赋值给指针变量:

1
2
3
int i, *p;
...
p = &i;

解引用运算符 *

一旦指针变量指向了对象,就可以使用 * 运算符访问被指向的对象。

p 指向了 i ,下面的语句将显示 i 的值:

1
printf("%d\n", *p);

p 指向了 i*p 就是 i别名*p 不仅拥有和 i 相同的值,而且对 *p 的改变也会改变 i 的值:

1
2
i: 直接访问,读取内存一次 
*p: 间接访问,读取内存两次

可以把 &* 看作是一对逆运算。

优先级顺序:自增自减运算符 > 解引用 * *和取地址 & > 算术运算

警惕野指针问题

野指针:指针变量 p 未被初始化或者指向了一个未知的区域。对野指针使用解引用运算符会导致未定义的行为:

1
2
3
int *p;
printf("%d\n", *p); /*** WRONG ***/
*p = 1; /*** WRONG ***/

指针赋值

  1. 把变量的地址赋值给指针变量:
1
2
int i, j, *p, *q;
p = &i;
  1. 通过另一个指针变量进行赋值:
1
q = p;
  1. q = p*q = *p 的区别
  • q = p 是将指针的值进行复制,两个指针指向相同的内存位置。

  • *q = *p 是将指针所指向的内存位置的值进行复制,两个指针可以指向不同的内存位置。

指针作为参数

C 语言函数调用时,是进行值传递的,在函数调用中,无法改变实参的值。

解决方法:不传递 a 和 b 作为函数的实际参数,而是传递 &a&b。形式参数声明为对应的指针类型。函数参数列表中*p*q 分别为变量 a 和 b 的别名,因此可以通过 *p*q 改变变量 a 和 b 的值。

1
2
3
4
5
6
7
void swap(int* p, int* q) 
{
   int temp = *p;
   *p = *q;
   *q = temp;
}
swap(&a, &b);

scanf 函数格式串之后的参数必须是指针,但并不是必须使用 & 运算符。

1
2
3
int i, *p;
p = &i;
scanf("%d", p);

指针作为返回值

返回指针的函数

1
2
3
4
int* find_middle(int a[], int n) 
{
   return &a[n/2];
}

注意:永远不要返回指向当前栈帧区域的指针: 一旦当前函数执行完毕,其所在的栈帧会被释放。

1
2
3
4
5
6
/*** 错误 ***/
//int* f(void)
//{
//    int i;
//    return &i;
//}

指针与数组

指针的算术运算

指针可以指向数组的元素

1
2
int a[10], *p;
p = &a[0];
指针指向数组

通过对指针 p 执行指针算术运算,可以访问数组 a 的其它所有元素。

C 语言支持 3 种格式的指针算术运算:

  • 指针加上一个整数,结果是指针

  • 指针减去一个整数,结果是指针

  • 两个指针相减,结果是整数

指针加上整数

p 指向数组元素 a[i]p + j 将指向 a[i+j]

1
2
3
4
int a[10], *p, *q;
p = &a[2];
q = p + 3;
p += 6;

指针加上整数

指针减去整数

p 指向数组元素 a[i]p - j 指向 a[i-j]

1
2
3
4
int a[10], *p, *q;
p = &a[8];
q = p - 3;
p -= 6;

指针减去整数

两个指针相减

当两个指针相减时,结果为指针之间的距离。

1
2
3
4
p = &a[5];
q = &a[1];
i = p - q; /* i is 4 */
i = q - p; /* i is -4 */
  1. 对一个不指向数组元素的指针执行算术运算会导致未定义的行为。
  2. 只有在两个指针指向同一个数组时,相减才有意义。

指针比较

可以用关系运算符 (<, <=, >>=) 和判等运算符 (==!=) 对指针进行比较。

1
2
3
p < q 等价于 p - q < 0
p = q 等价于 p - q = 0
p > q 等价于 p - q > 0

只有当两个指针指向同一数组时,比较才有意义。如:

1
2
3
4
p = &a[5];
q = &a[1];
p <= q; /* 0 */
p >= q; /* 1 */

指针用于处理数组

通过对指针变量进行重复的自增来遍历数组中的元素。

1
2
3
4
5
#define N 10
int a[N], *p, sum;
sum = 0;
for(p = &a[0]; p < &a[N]; p++)
   sum += *p;

尽管 a[N] 这个元素不存在,但是对它使用取地址运算符是合法的。因为 &a[N] 只会计算地址,而不会尝试访问 a[N] 的值。

*++ 的组合

对某个元素进行赋值,然后前进到下一个元素:

1
a[i++] = j;

p 指向 a[i],对某个元素进行赋值,然后前进到下一个元素:

1
*p++ = j;

*++ 的其它组合方式(常见的还是 *p++):

优先级顺序:自增自减运算符 > 解引用 * *和取地址 & > 算术运算

表达式 含义
*p++*(p++) 返回指针当前指向的值,指针指向下一个元素
(*p)++ 返回指针当前指向的值,指针指向的值自增
*++p*(++p) 返回指针下一个元素的值,指针指向下一个元素
++*p++(*p) 返回指针当前指向的值自增后的新值,指针指向的值自增

数组元素求和时

1
2
for (p = &a[0]; p < &a[N]; p++)
   sum += *p;

改写成

1
2
3
p = &a[0];
while (p < &a[N])
   sum += *p++;

用数组名作为指针

数组名可以被隐式转换为指向数组第一个元素的指针:

1
2
3
int a[10];
*a = 7; /* a[0] = 7 */
*(a + 1) = 12; /* a[1] = 12 */

a+i 等同于 &a[i]*(a+i)=a[i]

数组名作为指针的场景:

  • 遍历数组元素:通过递增指针来访问数组的每个元素。

    以下遍历数组元素:

    1
    2
    for (p = &a[0]; p < &a[N]; p++)
       sum += *p;

    可以改写成:

    1
    2
    3
    /* idiom */
    for (p = a; p < a + N; p++)
       sum += *p;

    虽然可以把数组名当作指针,但是不能给数组名赋新的值,也就是说数组名是一个指针常量

    1
    2
     >while (*a != 0)
    a++; /*** 错误 ***/
  • 传递数组给函数:函数参数可以接受数组类型或指向数组的指针。数组名在传递给函数时,会退化成指针

    1
    2
    3
    4
    5
    6
    7
    8
    int find_largest(int a[], int n) {
       int max = a[0];
       for (int i = 1; i < n; i++)
           if (a[i] > max)
               max = a[i];
       return max;
    }
    largest = find_largest(b, N);

    在调用 find_largest 函数时,会把指向数组 b 第一个元素的指针赋值给 a,数组本身并没有复制。

    • 传递数组时,只是传递一个指针,可以把数组类型的形式参数声明为指针类型。

      1
      int find_largest(int* a, int n);
      1. 对于形式参数而言,声明数组和声明指针是一样的。
      2. 对于变量而言,声明数组和声明指针是不同的。(声明 int a[10]; 会导致编译器预留 10 个整数的空间。但声明 int *a; ,编译器只会为一个整数的空间。 )
    • 形式参数为数组,可以给函数传递一个数组片段。(数组片段:数组的某个连续部分)

      1
      largest = find_largest(b + 5, 10);

用指针作为数组名

1
2
3
4
#define N 10
int a[N], sum = 0, *p = a;
for (int i = 0; i < N; i++)
   sum += p[i];

编译器会把 p[i] 看作是 *(p+i)

用指针作为数组名的情况:

  • 动态分配内存:使用指针来管理动态分配的数组,比使用固定大小的静态数组更灵活。
  • 处理多维数组:在多维数组的情况下,指针的灵活性比数组名更有用。