该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
2 存储器和指针:指向何方?
前文《C语言(Head First C)-2_1:存储器和指针》
scanf()与fgets():
scanf()会导致缓冲区溢出:
如果忘记限制scanf()读取字符串的长度,可能会输入远远超出程序空间的数据,多余的数据会写到计算机还没有分配好的存储器中;(运气好的话,不但能保存,而且不会有问题。)
但缓冲区溢出很可能会导致程序出错,这种情况通常被称为段错误或abort trap,不管出现什么错误,程序都会崩溃;
fgets():
同样可以输入文本数据:接收char指针,并给出最大长度;
如:
char food[5];
fgets(food,sizeof(food),stdin);//或将最大长度指定为5
food:接收的指向缓冲区的指针;
指定的长度是所接收字符串(包括\0)的最大长度;(这样就不用像scanf()那样把长度减1)
stdin标示数据将来自键盘;
(Code2_1)
/*
* fgets
*/
#include <stdio.h>
int main() {
puts("请输入:");
char name[10];
fgets(name , sizeof(name) , stdin);
printf("name is :%s\n",name);
return 0;
}
log:
请输入:
uawehfawyhiefwahe
name is :uawehfawy
注意:这里配合使用的sizeof运算符,计算的是数组的大小;如果是指针的话,计算的仅仅是指针的大小;
不要使用gets()!!!因为它没有任何限制;
scanf()与fgets()对比:
1)限制:
scanf()需要在格式串中加入长度;
fgets()强制限制用户输入字符串的长度;
2)多字段:
scanf()允许输入多个字段,而且允许输入结构化数据,可以指定两个字段之间以什么字符分割;
fgets()只允许向缓冲区中输入一个字符串,而且只能是字符串,只能有一个缓冲区;
3)字符串中的空格:
scanf()使用%s读取字符串时,遇到空格就会停止,相输入多个单词可以调用多次scanf(),或是使用复杂的正则表达式技巧;
fgets()总能读取整个字符串;
(Code2_2)
/*
* scanf
*/
#include <stdio.h>
int main() {
puts("请输入:");
char name[10];
scanf("%9s",name);
printf("name is :%s\n",name);
return 0;
}
log:
请输入:
gu wefha auewfh
name is :gu
字符串指针和字符串数组:
接下来看一个例子:三猜一
(Code2_3)
/*
* 三猜一
*/
#include <stdio.h>
int main() {
char * cards = "JQK";
char a_card = cards[2];
cards[2] = cards[1];
cards[1] = cards[0];
cards[0] = cards[2];
cards[2] = cards[1];
cards[1] = a_card;
puts(cards);
return 0;
}
log:Bus error: 10
代码旨在交换字符串JQK中的三个字母;玩家把钱压好之后,运行程序;
存储器故障!
问题出在:字符串字面值不能更新:
指向字符串字面值的指针变量不能用来修改字符串的内容;
但如果是字符串字面值的数组就可以修改了;
(Code2_4)
/*
* 三猜一
*/
#include <stdio.h>
int main() {
char cards[] = "JQK";
char a_card = cards[2];
cards[2] = cards[1];
cards[1] = cards[0];
cards[0] = cards[2];
cards[2] = cards[1];
cards[1] = a_card;
puts(cards);
return 0;
}
log:QKJ
这是由C语言使用存储器的方式决定的;
存储器中的char * cards = "JQK";究竟发生了什么?
1)计算机加载字符串字面值:
当计算机把程序载入存储器时,会把所有常数值(字符串"JQK")放到常量存储区,这部分存储器是只读的;
2)程序在栈上创建cards变量:
栈是存储器中计算机用来保存局部变量的部分,局部变量也是位于函数内的变量,cards变量就在这个地方;
3)cards变量设为"JQK"的地址:
cards变量将会保存字符串字面值"JQK"的地址;为了防止修改,字符串字面值通常保存在只读存储器中;
4)计算机试图修改字符串:
程序试图修改cards变量指向的字符串中的内容时就会失败,因为字符串是只读的;
所以,问题出在像"JQK"这样的字符串字面值保存在只读存储器中,他们是常量;
解决方案:在存储器的非只读区域创建字符串副本,进行修改;
在看可行的方式:char cards[] = "JQK"; cards不只是指针,cards现在是数组;
那么什么时候是数组(cards[])什么时候是指针呢(*cards)?
这取决于在什么地方看到它,如果是普通的变量声明,cards就是一个数组,而且需要立即赋值(因为声明时没有给出数组的大小);
如果cards以函数参数的形式声明,那么cards就是一个指针;(我们前面有过类似的例子,sizeof的那个,还记得不?)
存储器中的char cards[] = "JQK";究竟发生了什么?
1)计算机载入字符串字面值:(和刚才一样)
2)程序在栈上新建了一个数组:
声明了数组,程序会创建一个足够大的数组来保存字符串,这里4个足以;
3)程序初始化数组:
分配空间,并把字符串字面值"JQK"的内容复制到栈上;
我们看到区别在于:
原来的代码使用了指向只读字符串字面值的指针;现在,使用字符串字面值初始化了一个数组,从而得到了这些字母的副本(非只读区域),这样就可以随意修改了;
为了避免这个错误,我们可以在把指针设成字符串字面值时,确保使用const关键字:const char * s = "flower";
这样,如果编译器发现有代码视图修改字符串,就会提示;
(Code2_5)
/*
*
*/
#include <stdio.h>
int main() {
char cardsY[] = "JQK";
char * cards = cardsY;
printf("字符串数组(B):%s\n",cardsY);
printf("字符串指针(B):%s\n",cards);
cardsY[0] = '1';
cardsY[1] = '2';
cardsY[2] = '3';
printf("字符串数组(A):%s\n",cardsY);
printf("字符串指针(A):%s\n",cards);
return 0;
}
log:
字符串数组(B):JQK
字符串指针(B):JQK
字符串数组(A):123
字符串指针(A):123
看看这个例子,是不是更清晰了;
要点:
-如果在变量声明中看到*,说明变量是指针;
-字符串字面值保存在只读存储器上;
-如果想修改字符串,需要在新的数组中创建副本;
-可以将char指针,声明为const char *,以防代码用它修改字符串;
const:
加不加const修饰的字符串字面值都是只读的,const修饰符表示,一旦你试图用const修饰过的变量去修改数组,编译器会报错;
我们还要理解一点,数组变量并不保存在存储器中:
程序在编译期间,会把所有的数组变量的引用换成数组的地址,也就是说,在最后的可执行文件中,数组变量并不存在;
scanf()其实表示“scan formatted”,用来扫描带格式的输入;
把存储器保存在大脑里(各个存储器段):
1)栈:
存储器用来保存局部变量的部分;调用函数时,函数的所有局部变量都在栈上创建;看起来就像堆积而成的栈板;
栈做事颠三倒四,他从存储器的顶端开始,向下增长;
2)堆:
堆用于动态存储:程序运行时创建的一些数据,然后使用很长一段时间;(后续会介绍用法)
3)全局量:
全局量位于函数之外,对所有函数可见;程序一开始运行时就会创建全局量,你可以修改他们;
4)常量:
常量也是在程序一开始创建的,但是他们保存在只读存储器中;
常量是一些程序中要用到的不变量,你不会想修改这些值;
5)代码:
最后是代码段,多数操作系统都吧代码放在存储器的低位;它是只读的,他在存储器中用来加载机器代码的部分;
高位地址-> |栈
^堆
|全局量
只读存储器-> ^常量
低位地址+只读存储器-> |代码
工具箱:
1)scanf("%i",&x)可以让用户直接输入数字x;
2)不同计算机int的大小不同;
3)char指针变量x的声明:char * x;
4)字符串字面值保存在只读存储器中;
5)用字符串初始化数组会复制字符串中的内容;
6)&x会返回x的地址;
7)&x称为指向x的指针;
8)局部变量保存在栈上;
9)数组变量可以用作指针;
10)用*a读取地址a中的内容;
11)可以用fgets(buf,size,stdin)输入文本;