在使用scanf时很需要注意一点就是取地址运算符&,这个运算符不留神就很容易忘记,将会产生是一个针对内存的冲突,错误不易查找并且严重,见下面的代码:
int i = 2; scanf_s("%d", i);
上面的代码不小心失掉了&运算符[s1] ,会发生下面的冲突(之前已为i赋值2,所以是在位置0x00000002处访问冲突。而如果没有赋值,则是在0xcccccccc发生访问冲突)
C中,因变量类型不同而导致编译器为变量分配内存空间的时机不同。在使用scanf时,要求提供变量的内存地址,且必须保证被赋值的空间是正确的空间,所以我们就可以利用这一点来讨论变量类型、变量名和内存见的关系。现将C中的变量类型分为下面的几种情况:
基本值类型:
如声明int i;,在这一句就为i分配了内存[s2] ,但尚未对这块内存进行初始化。可以通过&i直接使用这块内存。
int i; scanf_s("%d", &i);
对基本数据类型,如int、char、float、double等,在声明时就分配了空间,不用担心内存访问冲突问题。而构造类型,指针类型等就不一样的空间分配时机了。
int *pi、char *pc等指针类型:
声明int *pi,为pi分配了存储空间,但并未为pi所指分配空间,这时容易造成野指针。下面的代码展示了一个新手常犯的错误:
char *pc; scanf("%c", pc);
这种情况下,也会遇到为初始化的错误。
位置0xccccccc是指pc。在char *pc;句,为指针pc开辟了存储空间,但未将pc指向一个安全的位置。这就是常说的野指针,下面看详细的分析,会有点绕,但仔细理解下来会很有帮助。
pc这个名称就代表了地址,和普通的变量名不同,普通的变量名代表一个地址中存放的值,对这两种变量名取址和取值的方法很不同。对于普通变量i,i本身代表的是这段内存所存储的值,取址要借助于取址符&i;对于指针变量pi,pi本身代表的是pi所指向内存的地址,要取得值,要借助于取值符号*pi。(至于pi,本身是个变量名,是不占用空间的;但pi所指向的地址是要被存储的,要为这个地址分配空间(就是&pi),听起来很绕,好好好理解消化)。char *pi就是分配了&pc,但未为*pc分配空间。所以才会遇到上面的错误。
再看下编译时的局部变量的值,运行char *pc;句:
修正方法也很明显,将指针pc指向一个安全的空间就可以了。可以自己开辟一段空间,也可以是声明一个所需类型的普通变量取其地址赋值给这个指针,或者赋值为NULL,下面的代码是使用第二种方法:
char c; char *pc; pc = &c;
这三句执行完毕时对局部变量的截图记录:
pc = &c;句,将指针pb指向了变量c的地址,这样就不是一个野指针了。下面看修正后的代码和对各种地址和存储的值的分析,先看完整的代码:
char c = 'p'; char *pc; pc = &c; scanf("%c", pc); //为上下显示一致,这里输入字符p printf("指针pc本身的地址是&pc: %d\n", &pc); printf("指针pc指向的地址是pc: %d\n", pc); printf("字符的地址是&c: %d\n", &c); printf("指针pc所指向地址也可表示为&*pc: %d\n", &*pc); printf("指针pc所指向地址中存储的数据是*pc: %c;和字符的值c: %c相同\n", *pc, c);
运行结果截图为:
注意平常我们并不关心的pc的地址(而不是常用的pc指向的地址),这个有利于分析内存分配问题。和普通变量一样,我们需要一块内存来存放指针,就是&pc。
数组int i[]:
在编写代码时,肯定写过int i[]和int i[5]之类的代码。如果仅写int i[];,是会被报错的。因为这里是声明,是要分配内存的,而不指定数组的大小,编译器是不知道要分配多少内存的,所以报错了。
再回到主题上来,如果下面的代码,会出现什么情况呢?
char c[5]; scanf("%c", &c[0]);
当然是顺利通过啦。char c[5];句分配了5个char的空间,名字分别是c[0]、c[1]、c[2]、c[3]、c[4],然后在取其中的一个地址&c[0]来进行操作。
看到这里,无外乎就是有没有分配内存和怎么取址的问题。C中声明变量的同时就为变量分配了内存空间,但要注意的是分配的是谁的内存空间。上面分析了普通变量,指针变量,数组等情况。再看下struct:
struct类型
先看一段代码,下面的代码定义了一个struct,并声明一个该类型的变量:
struct person { int age; char sex; }; struct person p; scanf("%c", &p.sex);
执行上面的代码会是什么情况呢?分析内存分配和取值:struct person p;句声明了一个变量,并为之分配内存;接着是对内存的访问,p.sex本身是char型,是个普通变量,那么通过&取址应该是没有问题的。执行这段代码,顺利通过。再看上述代码的内存地址分析:
果然,在声明struct person p的时候,就为各个量分配好了地址了。
struct和指针的混合类型
上面分析了单独的指针的情况,声明一个指针char *pc的时候,仅为所要存储的指针pc本身分配了空间,并没有为指针指向的空间*pc做任何处理,这就造成了pc是一个野指针。如果指针和struct或者数组结合起来呢?两种情况原理是一样的,以struct为例,上代码:
struct person { int age; char sex; }; struct person *p;
定义了一个struct person,还声明了一个指向person类型的指针p。这里的struct person *p;句是为指针本身p分配地址,并为指明这个指针指向的地址。
所以我们还需要做这个工作,为p所指分配空间 :
p = (struct person*)malloc(sizeof(struct person));[s3]
之后就可以使用p了。找到我们所需要赋值的变量名(*p).sex,再找到这个变量名的地址&(*p).sex:
scanf("%c", &(*p).sex);
分析各个地址:
p代表结构体的地址,所以和结构体中第一个数据结构.age的地址是一样的。
最后,说一下VS中的scanf_s:
使用VS2010编译下面的代码:
int i; scanf_s("%d", &i);
竟然提示scanf不够安全,建议改用scanf_s函数。大部分的教科书上都在用的scanf竟然有这个待遇,很是费解啊。查了一下发现:scanf是ANSI C中定义的标准输入函数,但在读取时不进行边界检查,所以可能会造成内存泄露。所以MS的VS系列,提供了scanf_s函数,在调用时必须提供一个参数表示做多读取的字符数,这样就变相进行了边界检查。
[s1]
这里还应说明一个&运算符取址所得的结果的数据类型,这个是个指针类型,所以不能定义一个int a = &I;会出现类型不匹配的错误。
而我们常见的指针的使用时,用到了这个&运算符就是恰好的了。
int *p; p = &i;
[s2]
具体分配内存的时机是在编译阶段还是在链接阶段呢?
下面是网络上的资料,好好消化消化:
所谓在编译期间分配空间指的是静态分配空间(相对于用new动态申请空间),如全局变量或静态变量(包括一些复杂类型的常量),它们所需要的空间大小可以明确计算出来,并且不会再改变,因此它们可以直接存放在可执行文件的特定的节里(而且包含初始化的值),程序运行时也是直接将这个节加载到特定的段中,不必在程序运行期间用额外的代码来产生这些变量。
其实在运行期间再看“变量”这个概念就不再具备编译期间那么多的属性了(诸如名称,类型,作用域,生存期等等),对应的只是一块内存(只有首址和大小),所以在运行期间动态申请的空间,是需要额外的代码维护,以确保不同变量不会混用内存。比如写new表示有一块内存已经被占用了,其它变量就不能再用它了; 写delete表示这块内存自由了,可以被其它变量使用了。(通常我们都是通过变量来使用内存的,就编码而言变量是给内存块起了个名字,用以区分彼此)
内存申请和释放时机很重要,过早会丢失数据,过迟会耗费内存。特定情况下编译器可以帮我们完成这项复杂的工作(增加额外的代码维护内存空间,实现申请和释放)。从这个意义上讲,局部自动变量也是由编译器负责分配空间的。进一步讲,内存管理用到了我们常常挂在嘴边的堆和栈这两种数据结构。
最后对于“编译器分配空间”这种不严谨的说法,你可以理解成编译期间它为你规划好了这些变量的内存使用方案,这个方案写到可执行文件里面了(该文件中包含若干并非出自你大脑衍生的代码),直到程序运行时才真正拿出来执行!
[s3]
malloc返回void *类型,所以要强制转换为我们所需要的int *类型;
sizeof返回的size_t类型,是在