前言:今天复习结构体数组知识的时候,顺手做了一道编程题,没想到发现了大问题。“指针数组”还是掌握的不好。这篇博客就从那道我发现问题的题目入手,讲解一下学习过程中踩过的坑和自己的课后查漏补缺。
原题目:有3个候选人,每个选民只能投票选一个人,要求编一个统计选票的程序,先后输入备选人的名字,最后输出各人投票结果。(参见谭浩强《c程序设计》第四版p300例9.3)
书上思路:设立一个结构体数组,数组中包含3个元素,每个元素的信息应包括候选人的姓名(字符型)和得票数。输入被选人的姓名,然后与数组元素中的“姓名”比较,如果相同,就给这个元素中的“得票数”成员的值加1.最后输出所有元素的信息。
代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
struct Person
{
char name[20];//候选人姓名
int sore;//得票数
};
int main()
{
struct Person leader[3] = { { "aa", 0 }, { "bb", 0 }, { "cc", 0 } };//三位候选人信息
printf("输入名字:");
char name[20];//定义字符数组
for (int i = 0; i < 10; i++)
{
scanf("%s", name);
for (int j = 0; j < 3; j++)
{
if (strcmp(name, leader[j].name) == 0)
{
leader[j].sore++;
}
}
}
for (int i = 0; i < 3; i++)//遍历结构体数组,输出每位候选人的姓名和票数
{
printf("%s %d\n", leader[i].name, leader[i].sore);
}
return 0;
}
书中是采用定义一个字符数组,从而不断输入10个候选人名字的。我想的是采用指针数组的形式:定义大小为10的指针数组,存放输入的10个人的姓名,其他原理同上。
关键代码如下
char* name[10];//定义大小为10的指针数组,存放输入的10个人姓名
for (int i = 0; i < 10; i++)
{
scanf("%s", name[i]);//输入姓名
for (int j = 0; j < 3; j++)
{
if (strcmp(name[i], leader[j].name) == 0)
{
leader[j].sore++;
}
}
}
然后发现程序崩溃
问题就出在了scanf("%s", name[i]);这句上。我们想象的是指针数组char* name[10]中每一块内容都存放的是char*指针,这个指针指向每个字符串的首字符地址,所以name[0]、name[1]......的类型都为char*,所以这里输入的时候不加“&”符。但是这里踩了一个大坑,也是编程过程中的一个易错点:当字符串以指针的方式存取时,如char*p="abc",这里定义了指针p,p中存放字符串的首地址,abc其实是存储在常量区的,所以只能“读”不能“写”;其次,按目前的声明,name是一个大小为10的指针数组,sizeof(name)=40,说明它只分配了容纳指针的40个字节,数组中的元素其实都是野指针,并没有指向合法可用的内存,因此在写入字符串之前一定要要动态分配内存,并将指针指向新分配的内存。(如下图所示)
再用一个更具体的例子说明问题(这个代码是其中一位层主的回答https://ask.youkuaiyun.com/questions/389563)
#include<stdio.h>
#define N 3
#define len 10
void main()
{
//输入N个字符串
char * str[N];
int i;
for(i=0;i<N;i++)
{
printf("输入第%d个字符串\n",i+1);
scanf("%s",&str[i]);
}
for(i=0;i<N;i++)
{
printf("\n第%d个字符串:",i+1);
printf("%s\n",str+i);
}
}
这里往指针数组中写入字符串是通过 scanf("%s",&str[i]);进行的,通过上面的截图说明还是存在问题。可以看到情况一是正常输出的,但是情况二和三都是输出有一些问题,这是为什么?
答:上面的代码是通过scanf("%s",&str[i]);将字符串写入的实际上它并没有写入每次字符指针指向的空间(6161,6262,6363指向的地方),而是写入了存放字符指针本身所占的4个字节中(即f7c4,f7c8,fc77),当所以每个空间只能写入4个字节的字符串;当输出时,采用 printf("%s\n",str+i);的方式进行输出,也就是&str[i],先找到&str[0]的地址f7c4,然后输出该地址存放的字符串,直至遇到‘\0’;然后再找到&str[1]的地址f7c8,输出该地址存放的字符串,直至遇到‘\0’;依次进行。
以情况一为例:地址f7c4中存放的是一个char*类型的指针,占四个字节,代码中把“aa”字符写入了这四个字节当中,也就是占用了3个字节,输出字符串的时候程序找到&str[0]的地址f7c4,遍历字符串,直至遇到\0,所以输出“aa”截止,“bb”和“cc”的正常输出也是同理。
但是情况二和三就不一样了。以情况二为例来说,“aaaaa”一共占据6个字节,可是&str[0]的地址f7c4时存放指针的空间,只有4个字节,于是在写满这四个字节后,又向下一个空间&str[1]的地址f7c8中写入剩下的一个a和‘\0’,参见-->
随后写入第二个字符串“bbbbb"的时候是找到&str[1]的地址f7c8进行写入,但是前两个字节‘a’和‘\0’会被写入的bb覆盖掉-->
所以最终地址f7c4所在空间的4个字节被“bbbb”占据,最后一个b和‘\0’分别写入了&str[2]的地址f7cc所在空间的第一个和第二个字节中-->
最后再找到&str[2]的地址f7cc,写入“ccccc”,同样,这里的前两个字节又被“cc”覆盖掉-->
最终&str[2]的地址f7cc写入了“cccc”,最后一个‘c’和‘\0’写入了&str[3]的地址f7d0空间的前两个字节-->
字符串输出的时候都是先找到所在地址然后输出,直到遇到‘\0’,于是就出现的情况二的错误输出结果,讲解图例如下:
这也就解释了为什么会出现这样的输出。
总结:网上查询的结果不一定对,一定要亲自验证,调试代码弄懂。