该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
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,也是为了防止数组越界;