找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2761

积分

0

好友

367

主题
发表于 5 小时前 | 查看: 4| 回复: 0

一、一些最基础的概念

1、指针与数组的核心——地址

地址是理解指针与数组的基石,后续所有概念都建立在这一基础上。每个字节存储单元都对应着唯一且不重复的内存地址;变量、常量或数组元素在内存中也必然占据特定地址。内存由连续字节组成,每个字节按序编号,地址即其唯一标识符。

当我们声明一个变量时,编译器为其分配内存空间,并返回该空间的起始地址。这个地址可由指针变量保存和操作。

回顾几个关键基础概念:

  • 计算机存储数据的基本单位:字节(Byte)  
  • 计算机表示数据的基本单位:位(bit)
  • 表示数据的方式:二进制
  • 整型数据占4个字节
  • 1 Byte = 8 bit

那么,为什么需要地址?为什么每个字节都要有地址?

可类比一栋楼:每个房间是一个字节,门牌号就是地址。计算机存数据时,不是说“把数据放进去”,而是明确指定“放到 0x00000001 这个地址空间里”。

C语言变量地址示意图:红色圆圈标出 &a,下方展示 int a = 0x0fd9a0 及其四字节内存布局

如何获取变量地址?使用取址运算符 &,例如:

int x;
printf("%p", &x);

会打印出变量 x 在内存中的地址。而通过解引用操作 *,可用指针访问其所指向地址的值:

printf("%d", *p); // 输出指针 p 所指向的整数值

理解地址对高级编程至关重要——动态内存分配、函数参数传递、数据结构实现等均依赖对地址的精准控制。但同时也需警惕空指针引用、野指针、内存泄漏等风险。

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int x = 123;

    /* int *y = 456;
     * 初始化指针 y 时将一个整数(456)强制转换为了指针类型。
     * 这是因为你试图直接将一个整数值赋给指针变量 y,而没有为它分配内存地址
     */
    int *y; // 初始化一个指针变量
    y = &x; // 将变量 x 的地址赋给变量 y

    printf("&x = %p\n", (void *)&x);
    printf("&x = %p\n", &x); // %p: 打印变量 x 在内存中的地址
    printf("&y = %p\n", y);

    /* 
    printf("*y = %p\n", *y);
    这里会警告,是因为在 printf() 函数中,格式说明符 %p 需要一个指向内存地址的指针作为参数,
    而这里传递了一个整型变量。
    */

    printf("y = %p\n", (void *)y); // 打印出指针变量 y 的值(也就是变量 x 的地址)

    return 0;
}

Linux终端执行 address.c:vi 编辑、gcc 编译、./a.out 输出 &x 和 &y 地址

以上展示了获取并输出变量地址的几种标准方式。

2、什么是十六进制数字

我们熟悉十进制(逢十进一),计算机底层基于二进制(逢二进一)。十六进制(逢十六进一,0~9、A~F)则成为人机交互的理想桥梁——它能无损映射二进制,又比纯二进制简洁得多。

十六进制表示法的好处:

  • 简化二进制转换:每4位二进制精确对应1位十六进制(如 0000=01111=F1101=D),远优于十进制需8位才能近似表达。
  • 编程便捷性:广泛用于内存地址、颜色代码(如 #FF0000)、文件格式等。C语言中 0x 前缀即表示十六进制字面量。
  • 数据存储与传输:MAC地址、调试信息常用十六进制,更易读、易校验。
  • 硬件与底层开发:汇编指令、寄存器值、固件镜像普遍采用十六进制表示。
#include <stdio.h>

int main(int argc, const char *argv[])
{
    int a = 0x6a;

    printf("a(10) = %d\n", a);
    printf("a(16) = %x\n", a); // 注意:%x 表示输出小写十六进制
    printf("a(16) = %X\n", a); // 注意:%X 是大写,输出也为大写

    int int_max = 0x7fffffff;  // 最大值:符号位0 + 后31位全1 → 0x7 + 31个 f
    int int_min = 0x80000000;  // 最小值:符号位1 + 后31位全0 → 0x8 + 31个 0
    printf("int_max = %d\n", int_max); // 输出整型最大值
    printf("int_min = %d\n", int_min);

    printf("input hex:");
    scanf("%x", &a); // 输入十六进制,终端输入无需加 0x
    printf("a(10) = %d\n", a);
    printf("a(16) = %x\n", a);
    printf("a(16) = %X\n", a);

    return 0;
}

Linux终端执行 hex.c:显示 a(10)=106、a(16)=6a、int_max/min 及输入 hex:ff 后的输出

整型最大值原理:符号位为0(正数),后31位全1 → 0x7fffffff;最小值:符号位为1(负数),后31位全0 → 0x80000000

3、地址到底是一个几位的二进制数据

地址的位宽取决于系统架构,而非数据类型本身。以下代码验证了这一点:

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int a;
    double b;
    char c;
    float d;

    printf("sizeof(int &) = %u\n", sizeof(&a));
    printf("sizeof(double &) = %u\n", sizeof(&b));
    printf("sizeof(char &) = %u\n", sizeof(&c));
    printf("sizeof(float &) = %u\n", sizeof(&d));

    return 0;
}

Linux终端执行 address_len.c:输出 sizeof(int &)=4 等,全部为4

无论 intdoublecharfloat,其地址大小(sizeof(&x))在当前系统下均为 4 字节 —— 这正是32位地址总线的体现。32位地址可寻址 $2^{32} = 4\text{GB}$ 内存;64位系统则可达 $2^{64}$ 字节,远超当前物理内存上限。


二、数组的定义与使用

1、初识数组

数组是一组相同类型元素的有序集合,通过下标访问。C语言中,数组名本质上是首元素地址的别名。

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int i;
    int a[i]; // C99+ 支持变长数组(VLA),但此处 i 未初始化,属未定义行为(UB)

    for(i = 0; i < 10; i++)
    {
        a[i] = 2 * i;
        printf("a[%d] = %d\n", i, a[i]);
    }

    return 0;
}

Linux终端执行 array.c:输出 a[0]=0 到 a[9]=18

什么是可变长数组(VLA)?

  • 先读入一个数字 n
  • 动态声明长度为 2*n 的数组(如 int a[2*n];
  • for 循环遍历
#include <stdio.h>

int arr1()
{
    int i;
    int a[i];
    for(i = 0; i < 10; i++)
    {
        a[i] = 2 * i;
        printf("a[%d] = %d\n", i, a[i]);
    }
    return 0;
}

int arr2()
{
    int i;
    int n;
    printf("input:");
    scanf("%d", &n);
    int a[2*n];
    for(i = 0; i < 2*n; i++)
    {
        a[i] = 3 * i;
        printf("a[%d] = %d\n", i, a[i]);
    }
}

int main(int argc, const char *argv[])
{
    arr2();
    return 0;
}

更安全、更常用的静态数组定义方式:

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int i;
    int x;
    int a[10] = {1, 2, 3, 4, 5}; // 后5个自动补0
    int b[10] = {0};             // 全部初始化为0
    int c[10] = {};              // 同上,C99+ 支持
    int d[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 编译器推导长度为10

    printf("sizeof(a)/sizeof(int) = %u\n", sizeof(a)/sizeof(int));
    printf("sizeof(b)/sizeof(int) = %u\n", sizeof(b)/sizeof(int));
    printf("sizeof(c)/sizeof(int) = %u\n", sizeof(c)/sizeof(int));
    printf("sizeof(d)/sizeof(int) = %u\n", sizeof(d)/sizeof(int));
    printf("\n");

    size_t size = sizeof(a)/sizeof(int);
    printf("sizeof(a)/sizeof(int) = %u\n", size);
    printf("size = %zu\n", size);
    printf("a = %p\n", a); // 数组名即首地址

    for(i = 0; i < size; i++)
    {
        printf("a[%d] = %p\n", i, &a[i]); // 打印每个元素地址
    }

    return 0;
}

size_t 是C语言中专用于表示大小、长度或容量的无符号整数类型,定义于 <stddef.h>。它确保足够容纳任何对象大小,避免溢出,是 sizeof 运算符的返回类型及 mallocstrlen 等函数的标准参数/返回类型。

#include <stddef.h>
#include <stdio.h>
#include <string.h>

size_t length = strlen("Hello, World!");
printf("The length of the string is: %zu\n", length);

数组的掌握是指针学习的必经之路,务必多练、多敲、多调试。

2、关于数组的两个算法

2.1 素数筛算法

素数筛(埃氏筛)是一种高效生成指定范围内所有素数的算法,核心思想是“用已知素数筛去其倍数”。

  • 时间复杂度:$O(N \log \log N)$  
  • 空间复杂度:$O(N)$
  • 关键逻辑:若 i 是素数,则 2*i, 3*i, 4*i... 均为合数,标记即可。
#include <stdio.h>

int prime[1000] = {0};
// 定义一个数组,求1000以内的所有素数,并初始化为0

void init_prime(int n)
{
    int i,j;
    prime[0] = prime[1] = 1; // 0和1非素数

    // 外层循环只需到 sqrt(n),因合数必有 ≤√n 的因子
    for(i = 2; i*i <= n; i++)
    {
        if(prime[i]) continue; // i 已被标记为合数,跳过

        printf("%d is prime:", i);
        for(j = i*i; j <= n; j += i) // 从 i*i 开始,避免重复标记
        {
            prime[j] = 1;
            printf(" %d", j);
        }
        printf("\n");
    }
}

int main(int argc, const char *argv[])
{
    init_prime(100);
    int x;
    while(~scanf("%d", &x))
    {
        printf("prime[%d] = %d\n", x, prime[x]);
    }
    return 0;
}

2.2 二分查找算法

二分查找适用于有序数组,通过每次比较中间元素,将搜索范围缩小一半,效率远高于线性查找。

随机数补充知识

C语言中 rand() 生成伪随机数,但需用 srand(time(0)) 设置种子,否则每次运行结果相同。

Linux终端执行随机数程序:多次运行 ./a.out 均输出 rand() = 1804289383,说明未设种子

二分查找思想(难点)

假设在 [min, max] 区间查找 x

  • 计算中点 mid = (left + right) / 2
  • arr[mid] == x → 找到
  • arr[mid] < xx 在右半段 → left = mid + 1
  • arr[mid] > xx 在左半段 → right = mid - 1
  • 重复直至 left > right

二分查找示意图:有序数组 [1,2,3,4,5,6,7,8,9],绿色箭头标出 min/mid/max 指针

#include <stdlib.h>
#include <time.h>

int binary_search(int arr[], int l, int r, int target)
{
    while(l <= r)
    {
        int mid = (l + r) / 2;
        if(arr[mid] == target)
        {
            return mid;
        }
        else if(arr[mid] < target)
        {
            l = mid + 1;
        }
        else
        {
            r = mid - 1;
        }
    }
    return -1;
}

int main(int argc, const char *argv[])
{
    int arr[] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
    int n = sizeof(arr)/sizeof(arr[0]);
    int target = 16;
    int result = binary_search(arr, 0, n-1, target);
    if(result != -1)
    {
        printf("目标元素%d在数组中的索引为%d\n", target, result);
    }
    else
    {
        printf("目标元素%d不在数组中\n", target);
    }
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int i;
int main() {
    srand(time(0));
    int arr[10] = {0};
    for(i = 1; i < 10; i++) {
        arr[i] = arr[i - 1] + (rand() % 10);
    }
    int len = 0;
    for(i = 0; i < 10; i++) {
        len += printf("%4d", i);
    }
    printf("\n");
    for(i = 0; i < len; i++) printf("-");
    printf("\n");
    for(i = 0; i < 10; i++) {
        printf("%4d", arr[i]);
    }
    printf("\n");
    int x, i;
    while(scanf("%d", &x) != EOF) {
        int cnt1 = 0, cnt2 = 0, flag1 = 0, flag2 = 0;
        for(i = 0; i < 10; i++) {
            cnt1 += 1;
            if(arr[i] != x) continue;
            flag1 = 1;
            break;
        }

        int l = 0, r = 9, mid;
        while(l <= r) {
            cnt2 += 1;
            mid = (l + r) >> 1;
            if(arr[mid] == x) {
                printf("(%d) arr[%d] = %d, find %d\n", cnt2, mid, arr[mid], x);
                flag2 = 1;
                break;
            }
            if(arr[mid] > x) {
                printf("(%d) arr[%d] = %d > %d, change [%d, %d] to [%d, %d]\n",
                       cnt2, mid, arr[mid], x, l, r, l, mid - 1);
                r = mid - 1;
            } else {
                printf("(%d) arr[%d] = %d < %d, change [%d, %d] to [%d, %d]\n",
                       cnt2, mid, arr[mid], x, l, r, mid + 1, r);
                l = mid + 1;
            }
        }
        printf("flag1 = %d, cnt1 = %d\n", flag1, cnt1);
        printf("flag2 = %d, cnt2 = %d\n", flag2, cnt2);
    }
    return 0;
}

Linux终端执行 6.binary_search.c:输出数组、二分步骤及 flag/cnt 统计


三、多维数组的定义与使用

多维数组通过嵌套方括号 [][] 定义,二维数组最常见。其本质是“数组的数组”。

二维数组内存布局示意图:3行4列,标注 b[0][0]、b[1][1]、b[2][0],强调 b = &b[0][0]

#include <stdio.h>

int i, j;
int main(int argc, const char *argv[])
{
    int b[3][4], cnt = 1;
    for(i = 0; i < 3; i++)
    {
        for(j = 0; j < 4; j++)
        {
            b[i][j] = cnt;
            cnt += 1;
            printf("%4d", b[i][j]);
        }
        printf("\n");
    }
    return 0;
}

Linux终端执行 mult_arr.c:输出 3×4 矩阵 1~12


四、字符数组、字符串数组及操作

C语言中字符串本质是空终止字节串(Null-terminated byte string),即以 \0 结尾的 char 数组。

内存布局示例 "hello"

h e l l o \0

标准库提供丰富字符串处理函数(定义于 <string.h>):

函数 说明
strlen(str) 计算字符串长度(遇 \0 停止)
strcmp(str1, str2) 字符串比较
strcpy(dest, src) 字符串拷贝
strncmp(str1, str2, n) 安全比较(最多 n 字符)
strncpy(str1, str2, n) 安全拷贝
memcpy(str1, str2, n) 内存块拷贝(无 \0 限制)
memcmp(str1, str2, n) 内存块比较
memset(str1, c, n) 内存块填充
// 定义字符数组
char str[size];

// 初始化字符数组
char str[] = "hello world";  // 长度为12(11字符+1个\0)
char str[size] = {'h','e','l','l','o'};

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

int main(int argc, const char *argv[])
{
    char str1[10] = "abc";
    printf("str1 = %s\n", str1);
    strcpy(str1, "def"); // 正确:用 strcpy 拷贝
    printf("str1 = %s\n", str1);

    char str2[] = "hello\0 world";
    printf("strlen(str2) = %u\n", strlen(str2)); // 输出5(遇\0停止)
    printf("sizeof(str2) = %u\n", sizeof(str2)); // 输出13(整个数组大小)
    printf("str2 = %s\n", str2); // 输出"hello"

    str2[5] = 'A'; // 修改\0为'A'
    printf("str2 = %s\n", str2); // 输出"helloA world"

    char str3[] = "abcdef", str4[] = "abc";
    printf("strcmp(str3, str4) = %d\n", strcmp(str3, str4)); // 输出正数(str3 > str4)
    printf("str3[3] = %d\n", str3[3]); // 输出'd'的ASCII码100

    int i;
    int arr[10];
    srand(time(0));
    for(i = 0; i < 10; i++) {
        arr[i] = rand() % 100;
    }
    for(i = 0; i < 10; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }

    // 将arr全部置0
    memset(arr, 0, sizeof(arr));
    for(i = 0; i < 10; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }

    return 0;
}

五、数组的存储方式

1)行序优先(Row-major order)

C语言默认按行优先存储多维数组:先存第0行所有列,再存第1行...

行序优先示意图:标注“行序优先”,计算 b[1][2] 地址 = 63 + (1*4+2)*4 = 87

2)列序优先(Column-major order)

Fortran等语言采用列优先:先存第0列所有行,再存第1列...

列序优先示意图:计算 b[1][2] 地址 = 63 + (2*3+1)*4 = 91

验证代码:

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int a[10][10];
    printf("&a[0][0] = %p\n", &a[0][0]);
    printf("&a[0][1] = %p\n", &a[0][1]);
    printf("&a[1][0] = %p\n", &a[1][0]);
    return 0;
}

Linux终端执行 row_col_array.c:输出 &a[0][0]、&a[0][1]、&a[1][0] 地址

输出证实:&a[0][1] - &a[0][0] = 4(1个int),&a[1][0] - &a[0][0] = 40(10个int),符合行序优先。


六、初识指针

1、指针变量也是变量

指针变量与其他变量一样,有名字、占内存、可赋值、可传参。区别在于:它存储的是地址,而非具体数值

// 定义一个存储整型地址的指针变量(不赋值)
int *ptr;

// 定义一个存储双精度浮点型的指针变量(赋值)
double *ptr1;
double a = 88.88;
ptr1 = &a;

// 输出指针指向的所存储的字符型的地址
char *ptr2;
char b = 'a';
ptr2 = &b;
printf("ptr2 = %p\n", ptr2);
printf("&b = %p\n", &b);

// 输出指针指向的所存储的字符型的地址的值
printf("ptr2 = %c\n", *ptr2);
#include <stdio.h>

int main(int argc, const char *argv[])
{
    int a = 10;
    int *p = &a;

    printf("a = %p\na = %d\n", p, a);
    printf("sizeof(*p) = %d\n", sizeof(*p));
    printf("sizeof(a) = %d\n", sizeof(a));

    return 0;
}

Linux终端执行 point.c:输出 a 的地址、值、sizeof(*p) 和 sizeof(a) 均为4

2、函数传递指针变量的场景和用途

2.1 用指针去定义传入参数

当需在函数内修改实参值时,传入其地址。

实参a地址赋给形参p,*p+=1 实现 a 值自增

#include <stdio.h>

void add_once(int *p)
{
    *p += 1; // 对整型指针取值后再加1
    return ;
}

int main(int argc, const char *argv[])
{
    int a = 123;
    printf("a = %d\n", a);
    add_once(&a); // 传入地址
    printf("a = %d\n", a);
    return 0;
}

Linux终端执行 func_pointer.c:输出 a=123 → a=124

2.2 用指针去定义传出参数

当函数需返回多个值,或主调函数需接收处理结果时,用指针作“传出参数”。

void f(int n, int *sum_addr)
{
    *sum_addr = (1 + n) * n / 2;
    return ;
}

int main(int argc, const char *argv[])
{
    int a = 123;
    printf("a = %d\n", a);
    add_once(&a);
    printf("a = %d\n", a);

    int n = 10, sum;
    f(n, &sum); // sum 是传出参数
    printf("sum = %d\n", sum);
    return 0;
}

2.3 用指针去接收数组参数

数组名退化为指针,函数参数可直接声明为对应类型的指针。

void output(int *p, int n)
{
    int i;
    for(i = 0; i < n; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
        // p[i] 等价于 *(p+i),[] 是解引用运算符的语法糖
    }
    return ;
}

int main(int argc, const char *argv[])
{
    int arr[10] = {1,3,5,7,9,2,4,6,8,0};
    output(arr, 10); // arr 自动转为 &arr[0]
    return 0;
}

2.4 交换指针变量(练习题)

注意:swap(&a, &a) 会导致未定义行为(异或交换失效),应使用临时变量。

swap无法处理两个相同地址的变量交换

#include <stdio.h>

int swap(int *a, int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, const char *argv[])
{
    int a, b;
    scanf("%d%d", &a, &b);
    printf("a = %d\nb = %d\n", a, b);
    swap(&a, &b);
    printf("a = %d\nb = %d\n", a, b);
    return 0;
}

Linux终端执行 swap.c:输入 6 3,输出 a=6→a=3, b=3→b=6


七、深入指针

1、地址的操作与取值规则

1.1 深入理解什么是“p+1”

p+1 不是地址数值加1,而是按指针类型偏移一个单位int *p 加1跳4字节,double *p 加1跳8字节。

图解:p+1 移动4字节(int),p+1 移动8字节(double)

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int a, *p1 = &a;
    double b, *p2 = &b;
    char c, *p3 = &c;

    printf("p1 = %p\n", p1);
    printf("p1+1 = %p\n", p1+1);
    printf("p1+2 = %p\n", p1+2);
    printf("p1+3 = %p\n", p1+3);

    printf("p2 = %p\n", p2);
    printf("p2+1 = %p\n", p2+1);
    printf("p2+2 = %p\n", p2+2);
    printf("p2+3 = %p\n", p2+3);

    printf("p3 = %p\n", p3);
    printf("p3+1 = %p\n", p3+1);
    printf("p3+2 = %p\n", p3+2);
    printf("p3+3 = %p\n", p3+3);

    return 0;
}

1.2 指针和数组的关系

数组名可作指针使用,arr[i]*(arr+i) 完全等价。

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = &arr[0]; // p 指向首元素
    int i;

    for(i = 0; i < 4; i++)
    {
        printf("p + %d = %p\n", i, p+i);
        printf("&arr[%d] = %p\n", i, &arr[i]);
    }
    return 0;
}

Linux终端执行 point_arr.c:p+0=&arr[0], p+1=&arr[1] 等,完全等价

1.3 特殊语法:int (*p)[10] vs int *p[10]

  • int (*p)[10]数组指针,p 是指针,指向一个含10个 int 的数组。
  • int *p[10]指针数组,p 是数组,含10个 int* 元素。

图解:int(*p)[10] 指向10个整型的数组,p+1 跳过整个数组(40字节)

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p1 = &arr[0];
    int (*p2)[10] = 0x0; // 指向10元素int数组的指针

    for(int i = 0; i < 4; i++)
    {
        printf("p + %d = %p\n", i, p1+i);
        printf("&arr[%d] = %p\n", i, &arr[i]);
    }
    printf("p2 = %p\n", p2);
    printf("p2 + 1 = %p\n", p2 + 1);
    printf("p2 + 2 = %p\n", p2 + 2);
    printf("p2 + 3 = %p\n", p2 + 3);
    return 0;
}

Linux终端执行 point_arr.c(含p2):p2+1 地址跳40字节

1.4 int *(*p3[10])[20] 解析

从变量名 p3 开始读:

  • p3[10]p3 是含10个元素的数组  
  • *p3[10] → 数组每个元素是 *(指针)  
  • (*p3[10])[20] → 每个指针指向一个含20个元素的数组  
  • int *(*p3[10])[20] → 每个指针指向含20个 int 的数组  

即:指针数组,每个指针指向一个 int[20]

指针数组示意图:p3[10],每个p指向 int*[20]

2、深入理解 *p 操作

*p 取值的字节数仅由指针类型决定,与内存中实际存储的数据无关。

  • int *p → 取4字节  
  • char *p → 取1字节  
  • double *p → 取8字节  

即使内存中存的是 int n = 0x61626364(4字节整数),用 char *p = (char*)&n 取值,*(p+0)*(p+3) 分别得到 'a''b''c''d' —— 这正是小端序(Little-endian) 的体现:低位字节存低地址。

小端序示意图:内存中 d c b a(ASCII 64 63 62 61),*p+0 取得 d

#include <stdio.h>

int main(int argc, const char *argv[])
{
    int n = 0x61626364; // 十六进制初始化,4字节
    char *p = (char *)&n; // 强制转换:用char指针读取

    printf("*(p + 0) = %c\n", *(p + 0));
    printf("*(p + 1) = %c\n", *(p + 1));
    printf("*(p + 2) = %c\n", *(p + 2));
    printf("*(p + 3) = %c\n", *(p + 3));
    return 0;
}

Linux终端执行 point_getval.c:输出 *(p+0)=d, *(p+1)=c, *(p+2)=b, *(p+3)=a

3、指针的几种等价形式

形式 等价关系 说明
p &arr[0] 指针变量即首地址
p + 1 &arr[1] 指针算术:加1跳过1个元素
*p p[0] 解引用即取首元素
p[i] *(p+i) 方括号是 *(p+i) 的语法糖
&p[i] p+i 取第i个元素地址
#include <stdio.h>

int main(int argc, const char *argv[])
{
    int arr[] = {0,1,2,3,4,5,6};
    int *p = arr;
    int i;

    for(i = 0; i < 3; i++)
    {
        printf("%d\n", (i + 5)[&p[1] - 2]);
        // 推导:&p[1] → p+1;p+1-2 → p-1;(i+5)[p-1] → *(i+5+p-1) → *(p+i+4) → p[i+4]
        // i=0→p[4]=4;i=1→p[5]=5;i=2→p[6]=6
    }
    return 0;
}

八、特殊的指针

1、数组指针与函数指针

  • 数组指针:指向数组的指针,如 int (*p)[10]  
  • 函数指针:指向函数入口地址的指针,声明需匹配返回值与参数列表  
int arr1[10];
int arr2[10][10];
int *p1 = arr1;               // 指向int的指针
int (*p2)[10] = arr2;         // 指向int[10]的指针(必须加括号!)

int (*add)(int, int);         // 指向 int func(int,int) 的函数指针
#include <stdio.h>

void test1() { printf("function_test1\n"); }
void test2() { printf("function_test2\n"); }
void test3() { printf("function_test3\n"); }

void (*p)();

int main(int argc, const char *argv[])
{
    p = test1; p();
    p = test2; p();
    p = test3; p();
    return 0;
}

Linux终端执行 function_pointer.c:依次输出 function_test1~3

函数指针数组:随机调用

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

void test1() { printf("function_test1\n"); }
void test2() { printf("function_test2\n"); }
void test3() { printf("function_test3\n"); }

int main()
{
    srand(time(0));
    void (*arr[3])() = {test1, test2, test3}; // 函数指针数组
    for(int i = 0; i < 10; i++)
    {
        arr[rand() % 3](); // 随机调用
    }
    return 0;
}

语法记忆图:1. 变量名前加*→指针;2. 变量名后加[]→数组

指针数组:本质是数组,元素为指针;
数组指针:本质是指针,指向数组;
函数指针数组:本质是数组,元素为函数指针。


九、内存管理方法

指针是内存操作的抽象接口。C语言提供标准库函数进行动态内存管理:

函数 作用
malloc(size) 申请 size 字节未初始化内存
calloc(n, size) 申请 n*size 字节,自动初始化为0
realloc(ptr, new_size) 调整已分配内存块大小
free(ptr) 释放内存(必须配对 malloc/calloc/realloc)
memset(ptr, c, n) n 字节内存设为字符 c
memcpy(dst, src, n) 内存拷贝(要求 dst/src 不重叠)
memmove(dst, src, n) 安全拷贝(支持重叠区域)
#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
    int *arr = (int *)malloc(sizeof(int) * 20); // 申请20个int空间(80字节)
    // malloc 返回 void*,需强制转换为所需指针类型

    int i;
    for(i = 0; i < 10; i++)
    {
        arr[i] = rand() % 100;
    }
    for(i = 0; i < 10; i++)
    {
        printf("arr[%d] = %d\n", i, arr[i]);
    }

    return 0;
}

Linux终端执行 1.memory.c:输出 arr[0]=83 到 arr[9]=21

动态分配的内存位于堆区(heap),与函数内局部变量所在的栈区(stack) 独立。使用完毕必须 free(),否则造成内存泄漏。

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

int main(int argc, const char *argv[])
{
    int *arr1 = (int *)malloc(sizeof(int) * 10);
    int *arr2 = (int *)calloc(10, sizeof(int)); // calloc 初始化为0

    // ... 使用 arr1, arr2 ...

    free(arr1);
    free(arr2);

    char s1[100] = "hello world";
    char s2[100], s3[100];
    memcpy(s2, s1, 12);   // 无重叠:安全
    memmove(s3, s1, 12); // 通用:支持重叠

    printf("s2 = %s\n", s2);
    printf("s3 = %s\n", s3);

    memcpy(s2 + 4, s2, 12);   // 重叠!行为未定义(可能崩溃)
    memmove(s3 + 4, s3, 12); // 安全:正确处理重叠

    printf("s2 + 4 = %s\n", s2);
    printf("s3 + 4 = %s\n", s3);

    return 0;
}

Linux终端执行内存函数测试:对比 memcpy 与 memmove 在重叠时的行为


十、结尾:typedef关键字

typedef 用于为现有类型创建别名,提升代码可读性与可维护性,尤其在处理复杂指针、结构体、函数指针时极为关键。

// 内建类型重命名
typedef long long LL;
typedef char * pchar;

// 结构体重命名
typedef struct __node {
    int x, y;
} Node, *PNode;

// 函数指针重命名
typedef int (*func)(int);

#include <stdio.h>
typedef long long LL;
typedef int (*Arr2Dim10)[10];
typedef void (*Func)();

void test() { printf("hello function pointer!\n"); }

int main()
{
    LL a;
    printf("sizeof(a) = %lu\n", sizeof(a));

    int arr[5][10];
    Arr2Dim10 p = arr;

    Func p2 = test;
    p2();

    return 0;
}

typedef 是 C/C++ 中实现类型抽象的重要工具,推荐在项目中系统性使用,以降低认知负荷、减少错误。更多高级用法可参考 C/C++ 技术专题




上一篇:C++无分支优化实践:掩码与数组消除性能瓶颈,性能提升超3倍
下一篇:深入解析TCP与UDP的Socket编程:原理、对比及C语言示例
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-8 10:30 , Processed in 1.177871 second(s), 50 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表