C进阶

1. 指针常量和常量指针

我总结的简单理解方式 > const挨着谁,就是修饰谁。

指针常量

指针常量是一个常量,其值(即指针所指向的地址)在初始化后不能更改,但可以修改它所指向的数据。

int value = 42;
int data = 10;

int *const ptr = &value;

// ptr = &data;     // 非法
*ptr = 50;          // 合法

常量指针

常量指针是一个指针。因此无法通过这个指针来修改它所指向的数据,但可以改变指针本身所指向的地址。

int value = 42;
int data = 10;
const int temp = 20;

const int *ptr = &value;
// int const *ptr = &value;     // 也可以这样写(但我不喜欢)

// *ptr = 60;       // 非法
ptr = &data;        // 合法
ptr = &temp;        // 合法

指向常量的指针常量

既不能修改指针所指向的数据,也不能改变指针本身所指向的地址。

const int *const ptr = &value; // ptr 是一个指向常量的指针常量

2. 指针数组和数组指针

我总结的简单理解方式 > 数组指针,会用括号强调指针类型

指针数组

指针数组是一个包含多个指针的数组,每个指针可以指向不同类型的对象或相同类型的多个对象。

指针数组的大小在声明时确定,并且每个元素都是一个独立的指针变量。

int *array[3]; // 一个包含3个整型指针的数组

// 初始化指针数组
int x = 10;
int y = 20;
int z = 30;
array[0] = &x;
array[1] = &y;
array[2] = &z;

// 访问指针数组中的值
int value = *array[1]; // value 现在为 20

数组指针

数组指针是一个指向数组的指针,它本身不是一个数组,而是指向一个数组首元素的指针。

数组指针的大小是固定的,它指向的数组大小也是固定的。

int (*pointer)[3]; // 一个指向包含3个整型的数组的指针

// 初始化数组指针
int data[3] = {10, 20, 30};
pointer = &data; // 数组指针指向arr数组的首地址

// 访问数组指针所指向的数组中的值
int value = (*pointer)[1];  // value 现在为 20

// 或者使用数组表示法
value = pointer[0][1];      // value 同样为 20

区别

  • 指针数组 是一个包含多个指针的数组,每个指针可以独立地指向不同的对象。
  • 数组指针 是一个指向数组的指针,它总是指向一个固定大小的数组的首元素。

用途

  • 指针数组 通常用于存储多个指针,这些指针可能指向不同类型的对象或相同类型的多个对象。它们常用于函数参数中,以允许函数接受可变数量的指针。
  • 数组指针 在某些情况下用于处理多维数组,尤其是当需要传递多维数组到函数中时,并保持数组的尺寸信息。它们也用于创建动态大小的数组(尽管这通常通过指针和malloc等函数来完成)。

3. 指针运算

在C语言中,指针运算可以改变指针所指向的内存地址,主要用于数组和内存管理。

不同类型的指针进行算术运算时,移动的内存大小是不同的。

例如,一个char *指针加1将只移动1个字节,而一个int *指针加1将移动4个字节(或更多,取决于系统和编译器)。因此,将不同类型的指针相互转换并进行算术运算通常是不安全的。

注意

在进行指针运算时,必须确保指针所指向的内存区域是有效的,并且你有权限访问它。否则,你可能会遇到未定义的行为,包括程序崩溃和数据损坏。

指针加法

指针加法运算时,指针会按照其指向类型的大小来移动。

例如,如果你有一个指向整型的指针,那么加1将会使指针向前移动一个整型变量的大小(通常是4个字节)。

int array[10];
int *p = &array[0];
p = p + 1;          // p现在指向array[1]的位置

指针减法

指针减法会按照指针指向类型的大小来向后移动指针。

#include <stdio.h>

int main()
{
    int array[10] = {0,1,2,3,4,5,};
    int *p = &array[2];     // p现在指向array[2]的位置
    p = p - 1;
    printf("result = %d\n", *p);

    p = &array[4];          // p现在指向array[4]的位置
    printf("result = %d\n", p[-1]);     // 非常规用法,但是C语言支持
    return 0;
}

指针递增和递减

递增和递减操作分别使指针向前或向后移动一个类型的大小。

int *p = &value;
p++;            //指向下一个整型的位置

int *p2 = &value;
p2--;            //指向前一个整型的位置

指针之间的减法

当你对两个指针进行减法运算时,结果将是两个指针之间元素数量的差值,而不是内存地址的差值。这两个指针必须指向同一块连续的内存区域(例如数组的元素)。

int array[10];
int *p = &array[0];
int *q = &array[5];
int diff = q - p; // diff的值是5,因为q指向p后面5个整型的位置

4. 函数指针

函数指针本质上是一个指针变量,它存储的是函数的地址,而不是函数调用的结果。

  • 可以将函数作为参数传递给其他函数
  • 可以将函数作为数据结构(如结构体或联合体)的一部分,
  • 可以将函数作为返回值从函数中返回。

定义

定义函数指针,你需要指定函数指针的类型。这通常包括函数的返回类型和参数类型。示例如下:

// 表示一个指向函数的指针,该函数接受一个int类型的参数并返回一个int类型的值。
typedef int (*function_t)(int);

// 或者直接定义函数指针变量
int (*pfunction)(int);

应用

函数指针通常用于实现回调函数(callback functions)和函数表(function tables)。

回调函数

回调函数是一种通过函数指针调用的函数,它允许一个函数在运行时确定将要调用哪个函数。这在事件驱动编程和异步编程中非常常见。

#include <stdio.h>

// 定义一个回调函数类型
typedef void (*callback_t)(int);

// 一个回调函数的示例
void print_number(int number) {
    printf("Number: %d\n", number);
}

// 一个接受回调函数作为参数的函数
void process_data(int data, callback_t callback) {
    callback(data);
}

int main() {
    process_data(42, print_number);
    return 0;
}

函数表

函数表是一个包含多个函数指针的数组,它允许程序在运行时选择并调用不同的函数。

#include <stdio.h>

// 函数表类型定义
typedef void (*func_table_t)(int);

// 两个示例函数
void functionA(int x) {
    printf("Function A called with %d\n", x);
}

void functionB(int x) {
    printf("Function B called with %d\n", x);
}

int main() {
    // 创建一个函数表
    func_table_t functionTable[] = {functionA, functionB};

    // 使用函数表调用函数
    functionTable[0](10); // 输出 "Function A called with 10"
    functionTable[1](20); // 输出 "Function B called with 20"

    return 0;
}

5. 数组和指针注意点

用数组做函数参数

传递数组给函数时,实际上是将数组的首地址作为参数传递给函数。

void print_array(int a[])
{
    printf("sizeof(a) = %d\n", sizeof(a));
}

上述示例中,无论实参数组的大小是多少,sizeof(a)都等于4(32位系统)。

对数组取地址

int a[4] = {1,2,3,4};

虽然,在数值上有 a == &a[0] == &a ,但它们有不同的含义。

  • 数组名或对数组元素取地址,相当于是元素类型的指针。比如a 和 &a[0]
  • 对数组名取地址,相当于是数组指针。比如&a
#include <stdint.h>
#include <stdio.h>

#define COUNT        16

int main(void) {
    // int型数组
    int a[COUNT] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};

    printf("%d\n", *a);
    printf("%d\n", *(&a[0] + 4));       // 4
    printf("%d\n", *((&a)[0] + 4));     // 4

    printf("%d\n", *(&a[1] + 6));       // 6
    printf("%d\n", *((&a)[1] + 6));     // 未知数据

    // 从数据上不好分清,但是从地址上可以区分。
    // ------------------------------
    // 首地址,三种表达方式在数值上相等。
    // 但是含义不同
    printf("%p\n", a);
    printf("%p\n", &a);
    printf("%p\n", &a[0]);

    // &a[x] 本质上是int型指针
    printf("%p\n", &a[1]);              // &a[0] + 1 * sizeof(int)
    printf("%p\n", a + 1);

    // &a 本质上是数组指针
    printf("%p\n", (&a)[1]);            // &a[0] + 1 * (COUNT * sizeof(int))

    return 0;
}
/*
// 输出结果
0
4
4
7
-1572082288
0x7ffd36ba8100
0x7ffd36ba8100
0x7ffd36ba8100
0x7ffd36ba8104
0x7ffd36ba8104
0x7ffd36ba8140
*/