C缺陷与陷阱 — 4 指针与数组进阶
目录
1 理解指针与数组
(1)数组名在C语言中代表数组首元素的地址,因此它可以被当作指向数组首元素的指针使用。例如,如果有一个数组 int arr[10];,arr 就是指向数组第一个元素的指针。
(2)指针可以进行算术运算,特别是加上或减去整数,这会改变指针所指向的内存地址。对于数组的指针,每次加1,指针就会指向数组的下一个元素。例如,arr + 1 指向 arr[1]。
(3)任何指针都是指向某种类型的变量。例如,如下的语句可以帮助我们理解指针:
表达式 | 说明 |
int i; | 整形变量i |
int a[3]; | 声明一个数组,a表示指向数组第一个元素的指针 |
*a = 84; | 将数组a中下标为0的元素的值设置为84 |
*(a+i) | 数组a中下标为i的元素的引用,这种写法常用,因此它被简记为a[i]。 |
int *p; | 表明p是一个指向整型变量的指针 |
p=&i; | 将整型变量i的地址赋给指针p |
*p=17; | 如果我们给p赋值,就能够改变i的取值 |
int *q = p + i; | 如果 p 和 q 指向同一个数组中的元素,则 q-p 得到的是两个指针之间的元素个数差。在 p 指向数组第一个元素且 q 指向 p 后第 i 个元素的情况下,q-p 等于 i |
p = a; | 将数组 a 的首地址(即下标为0的元素的地址)赋值给指针 p |
p = &a; | 非法语句,因为&a是一个指向数组的指针,而p是一个指向整型变量的指针,它们的类型不匹配 |
(4)上面的案例针对以为数组,接下来看看二维数组的情形:
表达式 | 说明 |
int calendar[12][31]; | 声明了一个二维数组calendar,它有12行31列,总共可以存储12个月份,每个月份31天的整数数据(假设每个月31天)。 |
int *p; | 表明p是一个指向整型变量的指针 |
int i; | 整形变量i |
calendar[4] | calendar数组的第5个行的第一个元素的地址。 |
p = calendar[4]; | 指针p指向calendar数组的第5行的第一个元素的地址 |
p = calendar; | p指向calendar数组的首地址 |
i = calendar[4][7]; i = *(calendar[4] + 7); i = *(*(calendar + 4) + 7); | 3个式子具有相同的含义,表示calendar数组第5行的第8个元素的值赋给变量i |
2 非数组的指针
在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符('\0')结尾的内存区域的地址。假定我们有两个这样的字符串 s 和 t,我们希望将这两个字符串连接成单个字符串r。要实现这一点,我们可以借助、库函数strcpy和strcat。分析如下几段代码的实现:
char *r;
strcpy(r, s);
strcat(r, t);
这段实现可能存在一些问题,首先我们并不知道为r所指向的位置,此外r并没有分配足够的内存,使用strcy和strcat将会导致缓冲区溢出,r应该指向一个足够大的内存区域,以存储s和t字符串的复制或连接结果。
char r[100];
strcpy(r, s);
strcat(r, t);
上面的代码同样存在相同的问题,C语言强制要求我们必须声明数组大小为一个常量,因此我们不够确保r足够大。C语言为实现此类问题提供了一个库函数malloc,该函数接受一个整数,然后分配能够容纳同样数目的字符的一块内存。代码如下:
char *r;
// 分配足够的内存来存储字符串s和t的连接结果,+1因为字符串末尾的空字符'\0'。
r = malloc(strlen(s) + strlen(t) + 1);
// 如果内存分配失败,调用complain函数来处理错误,并退出程序
if(!r){
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
// 释放之前malloc分配的内存
free(r);
3 作为参数的数组声明
将数组作为函数参数毫无意义,所以C语言中会自动地将作为参数的数组声明转换为相应的指针声明。也就是说,下面两种写法等价:
int strlen(char s[])
{
}
int strlen(char* s)
用数组形式的记法经常会起到误导作用。如果一个指针参数代表一个数组,情况又是如何呢?一个常见的例子就是函数main的第二个参数:
main(int argc, char* argv[])
{
}
main(int argc, char* *argv)
{
}
前一种写法强调的重点在于argv是一个指向某数组的起始元素的指针,该数组的元素是一个char*类型的指针。后一种char** argv 表示 argv 是一个指向 char* 类型指针的指针,这意味着 argv 可以被用来存储指向字符数组(字符串)的指针的地址。
4 指针与字符串
4.1 通过指针修改字符串
C语言中一个常见的“陷阱”:混淆指针与指针所指向的数据,对于字符串的情形更是经常犯这种错误。例如下面的语句:
char *p, *q;
p = "xyz";
q = p;
第2行代码将字符串 "xyz" 的地址赋给指针变量 p,P的值是一个指向由'x'、'y'、'z'和'\0'这4个字符组成的数组的起始元素的指针。因此,如果我们第3行代码,可以用下图来表示这种情况:
因此,当我们执行完下面的语句之后:
q[1]='Y':
q所指向的内存现在存储的是字符串xYz。因为p和q所指向的是同一块内存,所以p指向的内存中存储的当然也是字符串xYz。
4.2 空指针并非空字符串
空字符串是一个以空字符(\0)结尾的字符串字面量,它的长度为0。空字符串在内存中占用一个字符的空间,即空字符本身。
在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。但是常数0是一个特殊情况,编译器保证由0转换而来的指针不等于任何有效的指针。常数0这个值经常用一个符号来代替:
#define NULL 0
需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用。换句话说,当我们将0赋值给一个指针变量时,绝对不能使用该指针所指向的内存中存储的内容。下面的写法是完全合法的:
if(p == (char*) 0);
但是如果要写成这样:
if (strcmp(p, (char *0)) == 0);
就是非法的了,原因在于库函数stremp的实现中会包括查看它的指针参数所指向内存中的内容的操作。
如果P是一个空指针,下面的的行为也是未定义的。而且,与此类似的语句在不同的计算机上会有不同的效果。
printf(p);
printf("&s",p);
5 边界计算与不对称边界
一个拥有n个元素的数组,却不存在下标为n的元素,它的元素的下标范围是从0到n-1为此,分析如下的一段代码:
int i, a[10];
for(i = 1; i <= 10; i++)
a[i] = 0;
这段代码本意是要设置数组a中所有元素为0,却产生了一个出人意料的“副效果”。在for语句的比较部分本来是i<10,却写成了i<=10,数组 a 只有10个元素,索引范围是0到9。当循环尝试访问 a[10] 时,它实际上访问了一个数组之外的内存位置,这将导致未定义行为,通常是程序崩溃或内存损坏。
6 野指针
野指针是指不指向任何有效内存块的指针。野指针的存在通常是由于指针所指向的内存已经被释放、删除或者回收,但是指针本身未被正确地更新或置空。野指针产生有如下原因:
指针变量的值未被初始化: 声明一个指针的时候,没有显示的对其进行初始化,那么该指针所指向的地址空间是不确定的。
#include <stdio.h>
int main() {
int *ptr; // 指针未初始化
printf("%d\n", *ptr); // 未定义行为,因为ptr指向的地址不确定
return 0;
}
指针所指向的地址空间已经被free或delete:在堆上malloc或者new出来的地址空间,如果已经free或delete,那么此时堆上的内存已经被释放,但是指向该内存的指针如果没有人为的修改过,那么指针还会继续指向这段堆上已经被释放的内存,这时还通过该指针去访问堆上的内存,就会造成不可预知的结果,给程序带来隐患。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = malloc(sizeof(int)); // 动态分配内存
if (ptr != NULL) {
*ptr = 10; // 存储一个值
free(ptr); // 释放内存
}
printf("%d\n", *ptr); // 未定义行为,因为ptr指向的内存已经被释放
return 0;
}
指针操作超越了作用域:指针指向的内存区域不在作用域内,但程序仍然尝试通过这个指针来访问或修改内存。
void func() {
int localVar = 20;
int *ptr = &localVar;
}
int main() {
func(); // localVar 在func的作用域内
int *ptr = NULL;
ptr = &localVar; // 错误:localVar的作用域在func内,而ptr在func外
printf("%d\n", *ptr); // 未定义行为,因为ptr指向的localVar已经不再有效
return 0;
}
解决办法:
(1)初始化置NULL
(2)申请内存后判空:malloc申请内存后需要判空,而在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常。
(3)指针释放后置NULL
原文地址:https://blog.csdn.net/qq_41921826/article/details/143828533
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!