指针与字符串、函数知识点详解

指针与字符串、函数知识点详解

一、const指针

1.1 const的基本概念

  • const限定符:用于声明不可修改的左值表达式
  • 作用:保护数据不被意外修改,提高代码安全性

1.2 const与指针的三种组合形式

1.2.1 指向常量的指针

const int *p;  // 或 int const *p;
  • 特点:指针指向的内容不可修改,但指针本身可以指向其他地址
  • 示例
int x = 10;
const int *p = &x;
// *p = 20;  // 错误:不能修改指向的内容
p = &y;      // 正确:指针本身可以改变

1.2.2 常量指针

int *const p = &x;
  • 特点:指针本身不可修改,但指向的内容可以修改
  • 示例
int x = 10, y = 20;
int *const p = &x;
*p = 30;    // 正确:可以修改指向的内容
// p = &y;  // 错误:指针本身不能改变

1.2.3 指向常量的常量指针

const int *const p = &x;
  • 特点:指针本身和指向的内容都不可修改

1.3 const在函数参数中的应用

// 数组参数中的const
double x[const]      // 等价于 double* const x
const double x[]     // 等价于 const double* x

// 保护函数参数不被修改
void func(const char *str) {
    // str[0] = 'A';  // 错误:不能修改const内容
}

二、字符串字面量

2.1 字符串字面量的特性

  • 存储位置:存放在只读代码段(编译时确定)
  • 不可修改性:尝试修改会导致段错误
  • 内存优化:相同字面量只有一份拷贝

2.2 两种声明方式的区别

2.2.1 字符指针方式

char *s = "STUDENT";  // 等价于 (char*){"STUDENT"}
  • 特点:指向只读字面量,不能修改内容
  • 内存布局:s指向代码段中的只读数据

2.2.2 字符数组方式

char t[] = "STUDENT";  // 等价于 (char[]){"STUDENT"}
  • 特点:在栈上创建数组并复制字面量内容,可以修改
  • 内存布局:先开辟t[8]空间,再用memcpy初始化

2.3 字符串数组的两种形式

2.3.1 指针数组(不连续)

char *ss[] = {"hello", "hello", "!"};
  • 特点:每个元素指向独立的只读字面量
  • 内存优化:相同字面量共享同一地址(ss[0] == ss[1])

2.3.2 二维字符数组(连续)

char sa[][10] = {"hello", "hello", "!"};
  • 特点:在连续内存中存储字符串副本,可以修改

三、字符相关库函数

3.1 主要头文件分类

3.1.1 ctype.h(字符处理)

// 字符分类函数
isalnum(c)  // 是否为字母或数字
isalpha(c)  // 是否为字母
isdigit(c)  // 是否为数字
islower(c)  // 是否为小写字母
isupper(c)  // 是否为大写字母
isspace(c)  // 是否为空白字符
isprint(c)  // 是否为可打印字符

// 字符转换函数
tolower(c)  // 转换为小写
toupper(c)  // 转换为大写

3.1.2 string.h(字符串操作)

// 字符串操作
strcpy(dest, src)    // 字符串复制
strncpy(dest, src, n) // 安全字符串复制
strcat(dest, src)    // 字符串连接
strncat(dest, src, n) // 安全字符串连接

// 字符串检验
strlen(s)           // 字符串长度
strcmp(s1, s2)      // 字符串比较
strchr(s, c)        // 查找字符首次出现
strrchr(s, c)       // 查找字符最后一次出现
strstr(s, substr)   // 查找子串

// 内存操作
memchr(s, c, n)     // 在内存块中查找字符
memcmp(s1, s2, n)   // 内存块比较
memset(s, c, n)     // 内存块设置
memcpy(dest, src, n) // 内存块复制
memmove(dest, src, n) // 安全内存块复制

3.1.3 stdlib.h(数值转换)

atof(s)  // 字符串转浮点数
atoi(s)  // 字符串转整数
// 注意:itoa是非标准函数

3.2 库函数实现原理

3.2.1 isspace的实现分析

int isSpace(int ch) {
    const char *p = "\t\n\v\f\r ";
    for (; *p; p++) {
        if (ch == *p) break;
    }
    return *p;  // 返回匹配的字符或0
}
  • 参数类型选择int的原因
    1. 整数提升(Integer Promotion)
    2. 历史原因(早期C语言没有布尔类型)
    3. EOF处理(需要能返回-1)

3.2.2 strcat的实现分析

char* StrCat(char* dest, const char* src) {
    assert(dest && src);  // 检查空指针
    
    char* p = dest;
    for (; *p; p++);  // 找到dest的结尾
    
    // 检查内存重叠:确保src不在dest的范围内
    assert(!(src >= dest && src <= p));
    
    for (; *src; src++, p++)
        *p = *src;  // 复制src到dest末尾
    
    *(++p) = 0;  // 添加终止符
    return dest;
}

3.3 内存操作函数详解

3.3.1 内存和字节的基本概念

  • 字节(Byte):计算机内存的基本单位,1字节=8位(bit)
  • 内存地址:每个字节在内存中的唯一标识
  • 数据类型大小
    • char:1字节
    • int:通常4字节(取决于系统)
    • float:通常4字节
    • double:通常8字节

3.3.2 memcpy函数详解

函数原型

void *memcpy(void *dest, const void *src, size_t n);

参数说明

  • dest:目标内存地址(要复制到哪里)
  • src:源内存地址(要从哪里复制)
  • n:要复制的字节数

返回值:返回目标内存地址dest

使用示例

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

int main() {
    // 示例1:复制基本数据类型
    int a = 42;
    int b;
    memcpy(&b, &a, sizeof(int));  // 复制4个字节
    printf("a = %d, b = %d\n", a, b);  // 输出:a = 42, b = 42
    
    // 示例2:复制数组
    int src_arr[5] = {1, 2, 3, 4, 5};
    int dest_arr[5];
    memcpy(dest_arr, src_arr, 5 * sizeof(int));  // 复制整个数组
    printf("复制后的数组:");
    for (int i = 0; i < 5; i++) {
        printf("%d ", dest_arr[i]);  // 输出:1 2 3 4 5
    }
    printf("\n");
    
    // 示例3:复制字符串
    char src_str[] = "Hello, World!";
    char dest_str[20];
    memcpy(dest_str, src_str, strlen(src_str) + 1);  // +1包含结束符'\0'
    printf("复制后的字符串:%s\n", dest_str);
    
    // 示例4:复制部分内存
    char data[10] = "ABCDEFGHI";
    memcpy(data + 3, data, 4);  // 从位置0复制4字节到位置3
    printf("部分复制结果:%s\n", data);  // 输出:ABCABCDEF
    
    return 0;
}

重要注意事项

  1. 内存重叠问题:如果src和dest的内存区域重叠,memcpy的行为是未定义的
  2. 使用memmove处理重叠:当内存可能重叠时,应该使用memmove
  3. 字节数计算:确保n参数正确,避免缓冲区溢出
  4. 类型安全:memcpy不检查类型,只是简单的字节复制

memcpy与memmove的区别

// memcpy:假设内存不重叠,速度更快
memcpy(dest, src, n);

// memmove:处理内存重叠的情况,更安全但稍慢
memmove(dest, src, n);

// 重叠内存示例:
char str[] = "hello";
memcpy(str + 1, str, 4);  // 危险:未定义行为
memmove(str + 1, str, 4); // 安全:输出"hhell"

四、void指针

4.1 void指针的特性

  • 无类型性:仅保存地址值,不包含类型信息
  • 灵活性:可以指向任意类型的数据
  • 限制:不能直接解引用或进行下标运算

4.2 类型转换规则

// 任意类型指针可以隐式转为void*
int *p1 = malloc(sizeof(int));
void *v = p1;  // 隐式转换

// void*转为具体类型需要显式转换
int *p2 = (int*)v;  // 显式转换

4.3 动态内存管理

// 内存分配函数(在stdlib.h中声明)
void* malloc(size_t size);    // 分配未初始化内存,返回指向内存块的指针
void* calloc(size_t n, size_t size); // 分配n个大小为size的内存并初始化为0
void* realloc(void* ptr, size_t size); // 重新调整已分配内存的大小

// 内存释放函数
void free(void* ptr);  // 释放之前分配的内存

// 使用示例:
int *arr = (int*)malloc(10 * sizeof(int));  // 分配10个整数的空间
if (arr != NULL) {
    for (int i = 0; i < 10; i++) {
        arr[i] = i + 1;  // 初始化数组
    }
    free(arr);  // 使用完毕后释放内存
}

// calloc示例:自动初始化为0
int *zeros = (int*)calloc(5, sizeof(int));  // 分配并初始化为0

// realloc示例:调整内存大小
int *resized = (int*)realloc(arr, 20 * sizeof(int));  // 扩大为20个整数

4.4 泛型编程应用

4.4.1 内存填充函数

void* MemRepeat(void* dest, void* src, size_t src_sz, size_t count) {
    assert(dest && src);  // 检查空指针
    
    void* p = dest;  // 保存目标起始位置
    for (int i = 0; i < count; i++) {
        memcpy(p, src, src_sz);  // 将src内容复制到当前位置
        p += src_sz;  // 移动到下一个位置
        // 注意:这里原代码有错误,src不应该增加
        // src += src_sz;  // 这行应该删除,因为src是源数据,不应该改变
    }
    return dest;  // 返回目标指针
}

// 使用示例:
int a[10], value = 7;
MemRepeat(a, &value, sizeof(value), 10);  // 用7填充整个数组

4.4.2 二进制打印函数

void Printx(void* p, size_t bytes_count) {
    assert(p);  // 检查空指针
    
    unsigned char* byte_ptr = (unsigned char*)p;  // 转换为字节指针
    for (int i = 0; i < bytes_count; i++) {
        printf("%02x ", byte_ptr[i]);  // 以16进制格式打印每个字节
    }
    printf("\n");  // 打印换行
}

// 使用示例:
double f = 3.14;
Printx(&f, sizeof(f));  // 打印double类型的二进制表示

五、函数指针

5.1 基本概念

  • 函数地址:函数代码在内存中的首地址
  • 函数名:代表函数的地址(类似数组名)

5.2 函数指针的定义

// 函数指针声明语法
返回值类型 (*指针变量名)(参数类型列表);

// 示例
int (*p)(int, int);  // 指向接受两个int参数,返回int的函数

5.3 函数指针的使用

5.3.1 基本用法

int Max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int a = 5, b = 6;
    int (*p)(int, int);  // 声明函数指针
    p = Max;             // 赋值(函数名即地址)
    int c = (*p)(a, b);  // 通过指针调用函数
    printf("%d is bigger.", c);
    return 0;
}

5.3.2 简化调用语法

c = p(a, b);  // 等价于 (*p)(a, b)

5.4 回调函数(Callback)

5.4.1 回调函数概念

  • 定义:函数指针作为参数传递给另一个函数
  • 作用:由外部逻辑决定函数行为,实现算法与策略分离

5.4.2 qsort函数详解

qsort函数位置:在stdlib.h头文件中声明

函数原型

void qsort(void* base, size_t nitems, size_t size, 
           int (*compar)(const void*, const void*));

参数说明

  • base:指向要排序的数组的指针
  • nitems:数组中元素的个数
  • size:每个元素的大小(字节数)
  • compar:比较函数的指针

比较函数的要求

  • 接受两个const void*参数
  • 返回int类型:
    • 小于0:第一个参数小于第二个参数
    • 等于0:两个参数相等
    • 大于0:第一个参数大于第二个参数

使用示例

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

// 比较函数:升序排序
int cmpfunc(const void* a, const void* b) {
    return (*(int*)a - *(int*)b);
}

// 比较函数:降序排序
int cmpfunc_desc(const void* a, const void* b) {
    return (*(int*)b - *(int*)a);
}

int main() {
    int arr[] = {5, 3, 1, 2, 4};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    // 升序排序
    qsort(arr, n, sizeof(int), cmpfunc);
    printf("升序排序: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);  // 输出: 1 2 3 4 5
    }
    
    // 降序排序
    qsort(arr, n, sizeof(int), cmpfunc_desc);
    printf("\n降序排序: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);  // 输出: 5 4 3 2 1
    }
    
    return 0;
}

5.4.3 选择排序回调版本

void SelectedSortInt(int* nums, int n, int(*fcmp)(int, int)) {
    for (int i = 0; i <= n-2; i++) {
        int idx = i;
        for (int j = i+1; j <= n-1; j++) {
            if ((*fcmp)(nums[j], nums[idx]) < 0)
                idx = j;
        }
        if (i != idx) {
            int temp = nums[i];
            nums[i] = nums[idx];
            nums[idx] = temp;
        }
    }
}

六、重要编程技巧与注意事项

6.1 内存管理最佳实践

  1. malloc/free必须成对使用
  2. 避免多次释放同一指针
  3. 及时释放不再使用的内存

6.2 字符串处理安全

  1. 检查空指针:使用assert或NULL检查
  2. 防止缓冲区溢出:使用strncpy等安全函数
  3. 处理内存重叠:使用memmove而不是memcpy

6.3 const的正确使用

  1. 保护函数参数:防止意外修改
  2. 提高代码可读性:明确标识不可修改的数据
  3. 编译器优化:帮助编译器进行更好的优化

6.4 函数指针的应用场景

  1. 回调机制:如qsort的比较函数
  2. 策略模式:运行时选择不同算法
  3. 插件架构:动态加载函数

这份详细讲解涵盖了PPT中出现的所有重要知识点,从基础概念到高级应用都有涉及。理解这些内容对于掌握C语言指针、字符串和函数的深入用法至关重要。

posted @ 2025-12-02 19:26  FxorG  阅读(19)  评论(0)    收藏  举报