指针详解之 多层嵌套的关系
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
开始的四个字节中:0x1000
:0x12
(最低有效字节)0x1001
:0x05
0x1002
:0x00
0x1003
:0x00
(最高有效字节)
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
。如果不了解这一点,可能会导致跨平台移植时出现错误