当前位置: 首页 > article >正文

指针详解之 多层嵌套的关系

1 例子之指向3个字符串的指针数组,易混淆!

1.1过程详解:

char *str[3]={ 
    "Hello,thisisasample!", 
    "Hi,goodmorning.", 
    "Helloworld" 
}; 
char s[80]; 
strcpy(s,str[0]); //也可写成strcpy(s,*str); 
strcpy(s,str[1]); //也可写成strcpy(s,*(str+1)); 
strcpy(s,str[2]); //也可写成strcpy(s,*(str+2));

上例中,str 是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str 当作一个指针的话,它指向数组的第0 号单元,它的类型是char **,它指向的类型是char *。
*str 也是一个指针,它的类型是char *,它所指向的类型是char,它指向的地址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。注意:字符串相当于是一个数组,在内存中以数组的形式储存,只不过字符串是一个数组常量,内容不可改变,且只能是右值.如果看成指针的话,他即是常量指针,也是指针常量.
str+1 也是一个指针,它指向数组的第1 号单元,它的类型是char**,它指向的类型是char*。
*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,goodmorning."的第一个字符'H'

1.2 分析:哪里易错

在你提供的代码片段中,char *str[3] 定义了一个包含三个元素的字符指针数组,每个元素都是一个指向字符(char)的指针。这些指针分别初始化为指向三个字符串字面量。

现在我们具体来看 str*str 在这种上下文中的含义:

1 str/*str 的指针类型、指向的类型、内部的值

  • 类型:str 是一个指针数组,其类型是 char *[3],也就是说它是一个含有三个 char * 类型元素的数组。
  • 值:str 本身代表这个数组的起始地址,即第一个指针 str[0] 的地址。因此,str 可以被视为一个指向 char * 类型的指针,即 char **
  • 使用场景:当你使用 str 时,你可以通过索引来访问数组中的各个指针,例如 str[0]str[1] 和 str[2],它们分别指向不同的字符串。
     

2 *str 或 str[0]

  • 类型:*str 是对 str 进行一次解引用的结果。由于 str 是一个 char *[3] 类型的数组,*str 的类型就是 char *,也就是指向字符的指针。
  • 值:*str 等同于 str[0],它指向了第一个字符串 "Hello,thisisasample!" 的第一个字符 'H'
  • 使用场景:当你使用 *str 时,实际上是在访问 str 数组的第一个元素,也就是指向第一个字符串的指针。如果你想访问该字符串中的字符,你可以进一步解引用,例如 **str 或 (*str)[0] 将会得到字符 'H'

3 具体例子

假设你有以下代码:

c  深色版本

#include <stdio.h>

int main() {
    char *str[3] = { 
        "Hello,thisisasample!", 
        "Hi,goodmorning.", 
        "Helloworld" 
    };

    // 打印 str 的值,即指针数组的地址
    printf("Address of str: %p\n", (void *)str);

    // 打印 *str 的值,即第一个指针所指向的地址
    printf("Address pointed by *str: %p\n", (void *)*str);

    // 打印 *str 解引用后的值,即第一个字符串的第一个字符
    printf("First character of the first string: %c\n", **str);

    // 打印第二个指针所指向的字符串
    printf("Second string: %s\n", str[1]);

    return 0;
}

这段代码将会输出类似如下的结果(具体的内存地址会有所不同):

 

深色版本

Address of str: 0x7ffee4b8a960
Address pointed by *str: 0x105f41010
First character of the first string: H
Second string: Hi,goodmorning.

在这个例子中,str 表示整个指针数组的地址,而 *str 表示数组中第一个元素(即第一个字符串的指针)的值。**str 则是进一步解引用,得到了第一个字符串的第一个字符。

3.数组和结构体


3.1结构体用法

struct  book
    {
        /* data */
        char name[MaxL];
        char press[MaxL];
        float price;
    };
    
    char *str[3] = {"123",
                    "456",
                    "789"};
    // printf("%s \n",typeof(*str));
    // printf("%s \n",typeof(str));
    struct book zgs = {
        "哈佛史",
        "asml出版社",
        100
    };
    printf(" 名字:%s \n 出版社:%s \n 价格:%f 'n",
    zgs.name,zgs.press,zgs.price);
    

 


3.1  结构体易错点

 

不能直接结构体的指针赋值:必须要用strcpy(s1.name,"张三");

3.2 代码:
 

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

int main()
{
	struct student
	{
		char name[20];
		int age;
		char sex;
	};
    struct student s1 ;
    strcpy(s1.name,"zhangsan");
    s1.age = 19;
    s1.sex = 'm';
    printf("%s %d %c",s1.name,s1.age,s1.sex);
    // // {
    //     "zhangsan",
    //     18,
    //     'm'
    // }


    return 0 ;
}


4 指针和函数


4.1求一个字符串的ascii码之和

#include <stdio.h>

int fun(char *s)
{
    int num = 0;
    for (int i = 0; *s!= '\0';)
    {
        num += *s;
        s++;
    }
    return num;
}
int main(void)
{

    char str[] = "asdflkadlkgaslkdlkdfsjlkdsfjdlkfj87UHJNBN*&^^)(*&^) &**&^TYH";
    // int fun(char *str );
    // int num = 0;

    int out = fun(str);

    printf("%d\n", out);
    return 0;
}

截图:

函数申明、引用、循环条件
 

5.指针安全问题

5.1指针类型转换与越界写入

char s = 'a';
int *ptr;
ptr = (int *)&s;
*ptr = 1298;
1. 代码解析
  • 变量声明char s = 'a'; 声明了一个字符变量 s,并将其初始化为字符 'a'。在内存中,s 占用一个字节。
  • 指针声明int *ptr; 声明了一个指向 int 类型的指针 ptr
  • 类型转换ptr = (int *)&s; 将 s 的地址强制转换为 int * 类型,并赋值给 ptr。这意味着 ptr 现在指向 s 的首地址,但它的类型是 int *,而不是 char *
  • 写入操作*ptr = 1298; 试图通过 ptr 写入一个整数值 1298 到 s 所在的内存位置。
2. 问题分析
a. 内存布局

在32位系统中,int 类型占用4个字节,而 char 类型只占用1个字节。因此,*ptr = 1298; 这条语句不仅仅是改变了 s 所占的一个字节,还会同时改变 s 后面相邻的三个字节。具体来说:

  • s 占用的内存位置假设为 0x1000
  • *ptr = 1298; 实际上会将 1298(即 0x00000512)写入从 0x1000 开始的四个字节中:
    • 0x10000x12(最低有效字节)
    • 0x10010x05
    • 0x10020x00
    • 0x10030x00(最高有效字节)
b. 越界写入的影响

由于 s 只占用一个字节,而 *ptr = 1298; 写入了四个字节,因此会覆盖 s 后面的三个字节。这些字节可能是其他变量、数据结构的一部分,甚至是程序的代码段或栈中的重要数据。具体影响取决于这些字节的内容:

  • 覆盖其他变量:如果 s 后面的三个字节属于其他变量,那么这些变量的值会被意外修改,导致程序行为异常。
  • 破坏栈帧:如果 s 是局部变量,位于栈中,那么 *ptr = 1298; 可能会破坏栈帧,导致函数返回地址或其他栈上的数据被篡改,进而引发程序崩溃或未定义行为。
  • 覆盖代码段:在某些情况下,*ptr = 1298; 可能会覆盖程序的代码段,导致程序执行非法指令,直接崩溃或产生不可预测的行为。
c. 未定义行为

C语言标准规定,当程序访问未分配的内存或超出变量范围时,会导致未定义行为(undefined behavior)。未定义行为意味着编译器和运行时环境可以以任何方式处理这种情况,包括但不限于程序崩溃、数据损坏、甚至看似正常运行但实际上隐藏了严重的安全隐患

3. 避免此类错误的建议

为了避免这种类型的错误,开发者应该遵循以下最佳实践:

a. 避免不必要的类型转换

类型转换(尤其是强制类型转换)应该谨慎使用。在本例中,将 char * 强制转换为 int * 是不安全的,因为它忽略了类型系统的约束,导致越界写入。如果确实需要进行类型转换,应该确保目标类型和源类型的大小一致,并且不会导致内存越界。

b. 使用适当的数据类型

选择合适的数据类型来存储和操作数据。如果只需要操作单个字符,应该使用 char 类型的指针,而不是 int *。这样可以避免不必要的内存访问和越界风险。

c. 检查指针的有效性

在使用指针之前,始终检查它是否指向有效的内存区域。可以通过以下方式减少错误:

  • 初始化指针:在声明指针时,确保它被正确初始化为 NULL 或指向有效的内存地址。
  • 释放内存后置空:在释放动态分配的内存后,立即将指针设为 NULL,防止悬垂指针

    18

  • 边界检查:在对数组或缓冲区进行操作时,确保指针不会超出其合法范围。
d. 使用现代工具和技术

现代编程语言和工具提供了许多机制来帮助开发者避免指针误用:

  • 静态分析工具:使用静态代码分析工具(如 Clang Static Analyzer、Cppcheck 等)可以在编译时检测潜在的指针错误。
  • 编译器警告:启用编译器的所有警告选项,并确保修复所有警告。编译器通常会提示可能的指针误用或类型不匹配问题。
e. 理解内存模型

深入了解系统的内存模型和字节序(endianness)对于编写正确的指针操作代码至关重要。不同的系统可能有不同的字节序(大端或小端),这会影响多字节数据的存储顺序。例如,在小端系统中,int 类型的值 1298 会被存储为 0x12 0x05 0x00 0x00,而在大端系统中则是 0x00 0x00 0x05 0x12。如果不了解这一点,可能会导致跨平台移植时出现错误


 


http://www.kler.cn/a/455145.html

相关文章:

  • 定义Shape:打造属于你的独特图形
  • OpenCV-Python实战(6)——图相运算
  • LLM常见面试题(26-30题)--langchain篇
  • MetaRename for Mac,适用于 Mac 的文件批量重命名工具
  • 单例模式的写法
  • 雷池 WAF 搭配阿里云 CDN 使用教程
  • ES和MONGODB备份脚本
  • [Android]按下扫描键时启动一个线程来执行某些操作
  • 大语言模型的token和向量
  • PDF书籍《手写调用链监控APM系统-Java版》第7章 插件与链路的结合:Tomcat插件实现
  • 模方要使用多机引擎,有什么要求
  • Vue.js组件开发-实现访问页面自动获取数据
  • 119.【C语言】数据结构之快速排序(调用库函数)
  • AI 神经网络在智能家居场景中的应用
  • springboot系列教程(三十一):springboot整合Nacos组件,环境搭建和入门案例详解
  • QWidget应用封装为qt插件,供其他qt应用调用
  • PDF书籍《手写调用链监控APM系统-Java版》第12章 结束
  • 【论文复现】农作物病害分类(Web端实现)
  • 一文详解MacOS+CLion——构建libtorch机器学习开发环境
  • ASP.NET WebForms:实现全局异常捕获与处理的最佳实践
  • 系统安全——可信计算
  • nginx服务器实现上传文件功能_使用nginx-upload-module模块
  • 22.跳过报错(最简) C#例子
  • 使用jvisualvm远程连接Linux服务器上java进程
  • 简单讲解关于微信小程序调整 miniprogram 后, tabbar 找不到图片的原因之一
  • 【FastAPI】中间件