C语言(Head First C)-2_1:存储器和指针

来源:互联网 发布:mac版tomcat下载 编辑:程序博客网 时间:2024/06/17 10:24

 该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!

2 存储器和指针:指向何方?

 

 要理解C语言,就需要理解C语言是如何操作存储器的;

 C语言可以使用存储器赋予你更多的掌控权;

 

 C代码包含指针:

 指针就是存储器中某条数据的地址;

 

 之所以使用指针是因为:

 1)在函数调用时,可以只传递一个指针,而不用传递整份数据;(他会告诉你函数所在地址)

 2)让两段代码处理同一条数据,而不是处理两份独立的副本;

 

 指针做了两件事:避免重复和共享数据;

 指针只是地址,而且是一种间接形式的地址;

 

 深入挖掘存储器:

 每当声明一个变量,计算机都会在存储器中某个地方为它创建空间;

 -如果在函数中声明变量,会保存到一个叫栈(Stack)的存储器区段中;

 -如果在函数以外的地方声明变量,计算机则会把他保存到存储器的全局量段(Globals);

 

 我们可以将存储器分成若干区段:

 -栈;

 -堆;

 -全局量;

 -常量段;

 -代码段;

 

 之所以局部变量保存在栈里,全局变量保存在其他地方,是因为二者的用法不同:你永远只能得到一份全局变量,但如果写了一个调用自己的函数,就会得到同一个局部变量的很多个实例;

 其他区域后续会介绍;

 

 int x = 4;//计算机可能将栈中4100000号存储器单元分配给变量x,把4赋给x的话,计算机就会把4保存在4100000号单元;

 可以使用&运算符(也叫取地址符)找出变量的存储器地址;

 (Code2_1)

/* * 指针 存储器 */#include <stdio.h>int main() {        int x = 4;    printf("x保存在%p\n",&x);        return 0;}


 log:x保存在0x7fff5eecdaa8

 我们看到:

 -使用%p格式化地址(16进制);

 

 变量的地址告诉你去哪里找存储器中的变量,这就是为什么地址有时叫指针,因为他指向了存储器中的变量;

 重复下:

 -在函数中声明的变量通常保存在栈中;

 -在函数外声明的变量保存在全局量区;

 

 和指针起航:

 比如一个航海游戏,游戏中玩家需要控制船的方向;当然还需要控制很多东西;可以通过创建很多函数避免吧游戏写成一段很长的代码;

 每个函数完成一个小功能;

 

 示例:向东南航行

 (Code2_2)

/* * 航海游戏举例 */#include <stdio.h>/* 向指定方向航行:如向东南,则纬度将减少,经度将增加 * 经度参数,纬度参数 * 接收参数,进行加减 */void go_south_east(int lat,int lon) {    lat = lat - 1;    lon = lon + 1;}int main() {        int latitude = 32;    int longitude = -64;        go_south_east(latitude , longitude);        printf("当前位置:【%i,%i】\n",latitude,longitude);        return 0;}


 log:当前位置:【32,-64】

 

 这段代码没有按照我们预想的运行,船停在了原来的位置,为什么呢?

 

 C语言按值传递参数:

 C语言调用函数的方式是导致这段代码不能正确工作的原因;参数传递过程,是个赋值给lon值的过程,在调用函数时,传递的不是变量,而是变量的值;

 修改的lon只是,修改了本地的副本;

 

 如果用指针,就好办多了……

 

 传递指向变量的指针:

 通过指针(地址)不仅能找到原变量latitude的当前值,还能够修改该变量中的内容;

 函数所需要的就是读取和更新存储器相应单元中的内容;

 (Code2_3)

/* * 航海游戏举例 */#include <stdio.h>/* 向指定方向航行:如向东南,则纬度将减少,经度将增加 * 经度参数,纬度参数 * 接收参数,进行加减 */void go_south_east(int * lat,int * lon) {    *lat = *lat - 1;    *lon = *lon + 1;}int main() {        int latitude = 32;    int longitude = -64;        go_south_east(&latitude , &longitude);        printf("当前位置:【%i,%i】\n",latitude,longitude);        return 0;}


 log:当前位置:【31,-63】

 

 指针让存储器易于共享:

 使用指针的一个原因就是让函数共享存储器;只要知道数据在存储器中的位置,一个函数就可以修改另一个函数中的数据;

 

 使用存储器指针:

 使用存储器指针,我们需要知道三件事:

 1)得到变量的指针:

 用&运算符获取变量地址,一旦得到变量地址,就需要吧它保存在某个地方,因此,需要指针变量;

 指针变量是一个用来保存存储器地址的变量;声明时,需要说明指针所指向的地址中保存的数据的类型;

 int * address_of_x = &x;//该指针变量保存了一个地址,这个地址中保存的是一个int型变量;

 2)读取地址中的内容:

 使用*运算符,读取地址中的内容;

 int value_stored = *address_of_x;

 &运算符接受一个数据,得到数据存储的地址;

 *运算符接收一个地址,得到地址中保存的数据;

 指针有时也叫引用(C++中引用表示的是不同的概念),所以*运算符也可以描绘成对指针进行解引用;

 3)改变地址中的内容:

 又一个指针变量,想修改其中内容,可再次使用*运算符,只不过需要把指针变量放在赋值运算符的左边;

 *address_of_x = 99;

 

 可以回顾一下上一段代码示例;

 

 要点:

 -计算机会为变量在存储器中分配空间;

 -局部变量位于栈中;

 -全局变量位于全局量段;

 -指针只是一个保存存储器地址的变量;

 -&运算符可以找到变量的地址;

 -*运算符可以读取存储器中的内容;

 -*运算符还可以设置存储器地址中的内容;

 

 指针是进程存储器中真实编号的地址;计算机会为每个进程分配一个简版存储器,看起来就像是一长串字节;实际上很复杂的,但细节对进程隐藏起来了,这样操作系统就可以在存储器中移动进程,或释放并重新加载到其他位置;物理存储器结构很复杂,计算机通常会将存储器地址分组映射到存储芯片的不同的存储体(memory bank);

 

 怎么把字符串传给函数?

 字符串是字符数组,我们可以尝试这样做:

 (Code2_4)

/* * 传递字符串 */#include <stdio.h>void fortune_cookie(char msg[]){    printf("Msg:%s\n",msg);    printf("msg:%lu bytes\n",sizeof(msg));//msg:8 bytes}int main() {        char quote[] = "Flower comes always with you!";//quote:30 bytes 多的一个是\0    printf("quote:%lu bytes\n",sizeof(quote));        fortune_cookie(quote);        return 0;}


 log:Msg:Flower comes always with you!

 

 其中参数msg定义为数组,不知道长度,看似简单,但有奇怪的事发生了……

 log:quote:30 bytes //多的一个是\0

 log:msg:8 bytes

 

 sizeof运算符:

 C中又一个叫sizeof的运算符,他能告知某样东西在存储器中的字节,可作用于数据类型或某条数据;

 

 程序并没有返回字符串总长,而是返回了4或8个字节,看起来像是传进来之后的字符串比实际的要短?

 这是因为。。。

 数组变量好比指针:

 创建一个数组之后,数组变量就可以当做指针使用,它指向数组在存储器中的起始地址,即quote变量代表字符串中第一个字符的地址;

 计算机会为字符串的每一个字符以及结束字符\0在栈上分配空间,并把首字符的地址和quote变量关联起来;其实数组变量就好比一个指针;

 quote虽然是数组,但可以当指针变量来用;

 

 这就是函数调用时发生奇怪的事的原因:看起来把字符串传给了fortune_cookie()函数,但实际上只穿了一个指向字符串的指针;

 8字节表示的是指针的大小;(在64位系统上占8字节);

 

 要点:

 -数组变量可以被用作指针;

 -数组变量指向数组中第一个元素;

 -如果函数参数声明为数组,他会被当做指针使用;

 -sizeof运算符返回某条数据占用空间的大小;

 -也可以对某种数据类型使用sizeof,如sizeof(int);

 -sizeof(指针)在32位系统上返回4,在64位系统上返回8;

 

 sizeof是运算符,不是函数,编译器会把运算符编译为一串指令;而调用的函数的话,会跳到一段独立的代码中执行;

 编译器可以在编译期间确定存储空间的大小;

 

 指针变量只不过是一个存储数字的变量;可以用&运算符找到他的地址;

 

 数组变量和指针:

 

 我们再来看一个示例:

 (Code2_5)

/* * 非诚勿扰 */#include <stdio.h>int main() {        int contestants[] = {1,2,3};//注意是大括号        int * choice = contestants;    contestants[0] = 2;    contestants[1] = contestants[2];    contestants[2] = *choice;        printf("1:%i\n",contestants[0]);    printf("2:%i\n",contestants[1]);    printf("3:%i\n",contestants[2]);        return 0;}


 log:

     1:2    //赋值2

     2:3    //赋值3

     3:2    //赋值2

 和我们预想的一样是不是;

 

 需要注意的是:

 数组变量与指针又不完全相同:虽然可以将数组变量用作指针,但还是有区别;

 看这段代码:

 char s[] = "flower";

 char * t = s;

 

 1)sizeof(数组)是……数组的大小:

 (sizeof(指针)是操作系统上指针的大小)

 2)数组的地址……是数组的地址:

 指针变量是一个用来保存存储器地址的变量;

 &s == s;//对数组变量使用&,结果是数组变量本身(的地址);

 &t != t; //指针变量t的地址;和指针变量t不是一个东西;

 3)数组变量不能指向其他地方:

 创建数组时,会为数组分配存储空间,但不会为数组变量分配空间;所以不能把它指向其他地方;

 s = t;//会报错

 

 指针退化:

 正是因为有了这些区别,所以把数组赋值给指针时需要小心;将数组赋值给指针变量,指针变量只会包含数组的地址信息,而对数组的长度一无所知;相当于指针丢失了一些信息;

 我们把这种信息丢失成为退化;

 只要把数组传递给函数,数组免不了退化为指针;

 

 为什么数组从0开始?

 数组变量可以用作指针,这个指针指向数组的第一个元素;也就是说有两种方式读取数组第一个元素, array[0] == *array;

 地址只是一个数字,所以可以进行指针算数运算,如 找到存储器中的下一个地址,可以增加指针的值;同样对应两种方式:既可以用方括号加上索引值(如2)来读取元素,也可以对第一个元素的地址加上相应的值(如2);

 array[2] == *(array + 2);//+2表示加二个指针类型地址空间之后的地址单元,存储器地址加2*数组元素所占的字节数;

 这也解释了数组为什么要从索引0开始,所谓索引,其实就是为了找到元素的地址单元,指针需要加上的那个数字;

 

 来看一段代码示例:

 (Code2_6)

/* * 跳过字符串中的一段字符继续输出 * 从第七个字符开始输出 */#include <stdio.h>void skip(char * msg) {    puts(msg + 6);}int main() {        char * msg_any = "LoveFlower";    skip(msg_any);    return 0;}


 log:ower

 第七个字符,对应的数组下标为6;

 

 为什么指针有类型?

 举个例子,对char指针+1,指针指向存储器中下一个地址,那是因为char就占1字节;而int占4字节,如果int指针+1,编译后的代码会对存储器地址加4;

 (Code2_7)

/* * address+1 */#include <stdio.h>int main() {        int nums[] = {1,2,3};    printf("nums的地址是%p\n",nums);    printf("nums+1的地址是%p\n",nums + 1);        printf("1-%i\n",nums[2]);    printf("2-%i\n",*(nums + 2));    printf("3-%i\n",*(2 + nums));    printf("4-%i\n",2[nums]);        return 0;}


 log:

 nums的地址是0x7fff53ce9a9c

 nums+1的地址是0x7fff53ce9aa0

 1-3

 2-3

 3-3

 4-3

 

 我们发现log的两个地址像差4个字节,指针之所以有类型,是因为编译器在指针算术运算时需要知道加几;

 与此同时,我们也看到了几种读取数组元素的方法,他们都是等价的;(3[nums]的写法好特殊)

 

 要点:

 -数组变量可以用作指针……,但又不完全相同;

 -对数组变量和指针变量使用sizeof,效果不同;

 -数组变量不能指向其他地方;

 -把数字变量传给指针,会发生退化;

 -索引的本质是 指针算术运算符,所以数组从零开始;

 指针变量具有类型,这样就能调整整数指针算数运算;

 

 指针算术运算时的减法,使用时别要越过数组的起点;在编译器生成可执行文件时,编译器会根据变量的类型,用变量的大小乘以指针的增量或减量;

 

 用指针输入数据:

 1)使用scanf()函数可以让用户从键盘输入字符串:

 如:char name[40];scanf("%39s",name);

 scanf总共会读取39个字符,以及字符串终结符\0;

 2)使用scanf()函数输入数字:

 int age;scanf("%i",&age);//把变量的地址传入函数,scanf()便可以更新变量的内容;

 

 scanf()允许传递格式字符串,甚至可以用scanf()一次输入多条数据;

 如:scanf("%19s %19s",first_name,last_name);//注意输入时以空格分割,是所以用%19s,也是为了防止数组越界;

 

阅读全文
0 0