HeadFirstC笔记_2.存储器和指针:指向何方?

如果真的想玩转C语言,就需要理解C语言如何操纵存储器。
掌握指针和存储器寻址对成为一名地道的C程序员来讲非常重要。

什么是指针?
指针就是存储器中某条数据的地址。

为什么要使用指针?
1.在函数调用时,可以只传递一个指针,而不用传递整份数据。 
2.让两段代码处理同一条数据,而不是处理两份独立的副本。 
简单来讲,指针做了两件事:避免副本和共享数据。

先了解一下存储器结构:
存储器用来保存局部变量的部分。每 当调用函数,函数的所有局部变量都在栈 上创建。之所以叫栈是因为它看起来就像堆积而成的栈板:当进入函数时,变量会放到栈顶;离开函数,把变量从栈顶拿走。奇怪的是,栈做起事来颠三倒四,它从存储器的顶部开始,向下增长。
堆用于 动态存储:程序在运行时创建一些数据, 然后使用很长一段时间,稍后会看到堆的 用法。
全局量 全局量位于所有函数之外,并对所有函数可见。程序一开始运行时就会创建全局量, 你可以修改它们,不像常量
常量 常量也在程序一开始运行时创建,但它们保存在只读存储器中。常量是一些在程序中要用到的不变量,你不会想修改它们的 值,例如字符串字面值。
代码 最后是代码段,很多操作系统都把代码放 在存储器地址的低位。代码段也是只读的, 它是存储器中用来加载机器代码的部分。
在函数中声明的变量通常保存在 栈 中。
在函数外声明的变量保存在 全局量 区。

如果想要找出变量的存储器地址,可以用 & 运算符:
    
     
     
  1. printf("x 保存在 %p\n", &x); //&x是x的地址,即指针,%p用来格式化地址

 在函数参数中直接传递变量的例子:
    
     
     
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. void go_south_east(int lat, int lon) {
  4. lat = lat - 1;
  5. lon = lon + 1;
  6. }
  7. int main() {
  8. int latitude = 32;
  9. int longitude = -64;
  10. go_south_east(latitude, longitude);
  11. printf(" 停! 当前位置 : [%i, %i]\n", latitude, longitude);
  12. return 0;
  13. }
运行结果:


 结论:直接传递变量然后修改,并没有修改成功。
原因:是因为C语言在传递变量的时候,传递的是变量的值,它复制了一个值的副本,你修改的是副本,所以原来变量的值的并没有改变。

解决办法:传递指向变量的指针即可!
如何使用存储器指针:
1.得到变量的地址。
     
      
      
  1. printf("x lives at %p\n", &x);
  2. int *address_of_x = &x; // 这是一个指针变量,它保存的是一个地址,这个地址中保存的是一个int型变量。
2.读取地址中的内容。
      
       
       
  1. int value_stored = *address_of_x; // 用 *变量名 获取指针对应的变量的值
3.改变地址中的内容。
      
       
       
  1. *address_of_x = 99; //它会把原x变量中的内容改成99

以指针形式修改上面的错误代码
      
       
       
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. //void go_south_east(int lat, int lon) {
  4. void go_south_east(int* lat, int* lon) {
  5. // lat = lat - 1;
  6. // lon = lon + 1;
  7. *lat = *lat -1 ; // *x表示地址x所存的数据
  8. *lon = *lon + 1;
  9. }
  10. int main() {
  11. int latitude = 32;
  12. int longitude = -64;
  13. // go_south_east(latitude, longitude);
  14. go_south_east(&latitude, &longitude);
  15. // printf("当前的count地址:%p\n", &count); // &x表示x的地址
  16. // int *address_of_count = &count;
  17. printf(" 停! 当前位置 : [%i, %i]\n", latitude, longitude);
  18. return 0;
  19. }
运行结果:OK了!
 

问: 指针是真实的地址单元,还是某种形式的引用?
答: 它们是进程存储器中真实编号的地址。

问: 为什么存储器是进程的?
答: 计算机会为每个进程分配一个简版存储器,看起来就像是一长串字节。

问: 但存储器并非如此?
答: 实际的存储器复杂多了,但细节对进程隐藏了起来,这样操作系统就可以
在存储器中移动进程,或释放并重新加载到其他位置。

问: 存储器不仅仅是一长列字节?
答: 物理存储器的结构十分复杂,计算机通常会将存储器地址分组映射
到存储芯片的不同的存储体(memory bank)。

问: 为什么我一定要用%p格式串来打印指针?
答: 不一定要用%p,在大多数的现代计算机上可以用%li,但编译器可能会给出警告。

问: 为什么%p以十六进制显示存储器地址?
答: 工程师通常以十六进制表示存储器地址。

问: 如果我们把读取存储器单元的内容称为“解引用”,那么指针是不是应该叫“引用”?
答: 人们有时会把指针叫做“引用”,因为它引用了存储器中的某个地址单元。
但C++程序员通常用“引用”表示C++中一个稍有不同的概念。

怎么把字符串传给函数?
      
       
       
  1. void fortune_cookie(char msg[]) // 可以这样传
  2. {
  3. printf("Message reads: %s\n", msg);
  4. }
  5. char quote[] = "Cookies make you fat";
  6. fortune_cookie(quote);

 sizeof 的运算符
它能告知某样东西在存储器中占多少字节,既可以对数据类型使用,也可以对某条数据使用。如:
        
         
         
  1. sizeof(int); // 在大多数计算机中,将返回4这个值
  2. sizeof("Turtles!");// 将返回9, 其中包含8个字符外加\0结束符。

打印字符串长度代码示例:
         
          
          
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. void fortune_cookie(char msg[]) {
  4. printf("Message reads: %s\n", msg);
  5. printf("msg occupies %i bytes\n", sizeof(msg));
  6. }
  7. int main() {
  8. char quote[] = "Cookies make you fat";
  9. fortune_cookie(quote);
  10. return 0;
  11. }
运行结果:
 

为什么程序并没有显示字符串总长,而是返回了4或8个字节
这是因为数组变量好比指针,上述quote变量代表字符串中第一个字符的地址。
当你将数组作为参数传递给函数时,其实传递的是指针,于是你用sizeof(参数名)
想计算数组长度,其实计算的是一个指针变量的长度。
而指针在32位操作系统中占4字节,在64位操作系统中占8字节。
        
         
         
  1. printf("The quote 字符串保存在 : %p\n", quote);//quote虽然是数组,但可以当指针变量来用

问: sizeof是一个函数吗?
答: 不是,它是一个运算符。

问: 有什么区别?
答: 编译器会把运算符编译为一串指令;而当程序调用函数时,会跳到一段独立的代码中执行。
所以,程序是在编译期间计算sizeof的,编译器可以在编译时确定存储空间的大小。

问: 为什么在不同的计算机上指针变量的大小不同?
答: 在32位操作系统中,存储器地址以32位数字的形式保存,所以它叫32位操作系统。
32位==4字节,所以64位操作系统要用8个字节来保存地址。

问: 我可以把指针转化为一般的数字吗?
答: 在大多数操作系统中,可以这样做。指针变量只不过是一个保存数字的变量罢了,
C编译器通常会把long数据类型设为和存储器地址一样长。如果想要把指针p保存在
long变量a中,可以输入a=(long)p,过几章我们会学习这种方法。

代码示例:
       
        
        
  1. #include <stdio.h>
  2. int main() {
  3. int contestants[] = { 1, 2, 3};
  4. int *choice = contestants;
  5. contestants[0] = 2;
  6. contestants[1] = contestants[2];
  7. contestants[2] = *choice;
  8. printf(" 我选 %i 号男嘉宾 ", contestants[2]);
  9. return 0;
  10. }

运行结果:

 因为:contestants[2] == *choice == contestants[0] == 2。

数组变量与指针并不完全相同
        
         
         
  1. char s[] = "How big is it?";
  2. char *t = s;
1. 对数组直接 sizeof(数组)返回的是 数组的大小。
sizeof(s)  返回 15,而sizeof(t) 返回的是 4或8。

2. 对数组用&,获取的还是数组本身。
即:&s = s;但 &t != t ;

3.数组变量不能指向其他地方。
当创建指针变量时,计算机会为它分配4或8字节的存储空间。但如果创建的是数组呢?计算机会为数
组分配存储空间,但不会为数组变量分配任何空间,编译器仅在出现它的地方把它替换成数组的起始地
址。但是由于计算机没有为数组变量分配空间,也就不能把它指向其他地方。s = t;  // 会报编译错误

指针退化
假如把数组赋给指针变量,指针变量只会包含数组的地址信息,而对数组的长度一无所知,相当于指针
丢失了一些信息。我们把
这种信息的丢失称为退化。
只要把数组传递给函数,数组免不了退化
为指针,
但需要记清楚代码中有哪些地方
发生过数组退化,因为它们会引发一些不
易察觉的错误。

为什么数组角标是从0开始,而不是1?
数组变量可以用作指针,该指针指向数组的第一个元素,也
就是说除了方括号表示法,还可以用 * 运算符
读取数组的第一个
元素,像这样:
            
             
             
  1. int drinks[] = { 4, 2, 3};
  2. printf(" 第一单 : %i 杯 \n", drinks[0]);
  3. printf(" 第一单 : %i 杯 \n", *drinks);
  4. // 也就是说下面这两行代码是等价的。
  5. drinks[0] == *drinks
地址只是一个数字,所以可以进行指针算术运算,比如为了找到存储器中的下一个地址,可以 增加 指针的值。
既可以用方括号加上索引值2来读取元素,也可以对第一个元素的地址加2:
            
             
             
  1. printf(" 第三单 : %i 杯 \n", drinks[2]);
  2. printf(" 第三单 : %i 杯 \n", *(drinks + 2));
  3. // 总之,下面的表达式是等价的
  4. drinks[i] ==*(drinks + i)
这就解释了为什么数组要从索引0开始,所谓索引,其实就是为了找到元素的地址单元,指针需要加上的那个数字。
一言以蔽之,索引的本质是指针算术运算。

磨笔上阵
        
         
         
  1. #include <stdio.h>
  2. void skip(char *msg) {
  3. // 为了从第7个字符开始打印这条消息,你需要在这里用什么表达式?
  4. puts(msg+6); // 首字符地址基础上+6,就是把指针往后移动了6位。
  5. }
  6. int main() {
  7. char *msg_from_amy = "Don't call me";
  8. // 函数需要从字符c开始打印这条消息。
  9. skip(msg_from_amy);
  10. return 0;
  11. }
运行结果:
 

为什么指针有类型,不能用一个统一的指针变量来表示呢?
因为指针算术运算会暗渡陈仓。
如果对 char 指针加 1 ,指针会指向存储器中下一个地址,那是因为 char 就占1字节。
如果是 int 指针呢? int 通常占4字节,如果对 int 指针加1,编译后的代码就会对存储器地址加4。

drinks[i] ==*(drinks + i)  公式变换:
          
           
           
  1. int doses[] = { 1, 3, 2, 1000};
  2. printf(" 服用 %i 毫克的药 ", 3[doses]);
  3. // 这里打印结果是1000,原因如下
  4. doses[3] == *(doses + 3) == *(3 + doses) == 3[doses]

问: C语言什么时候对指针算术运算进行调整?
答: 在编译器生成可执行文件时,编译器会根据变量的类型,用变量类型的大小乘以指针的增量或减量。
如:假如编译器看到你对一个指向int数组的指针加2,就会用2乘以4(int类型的长度),然后对地址加8。

用指针输入数据
scanf()是怎么工作的呢?它接收一个char指针,而在下面这个例子中,传给了它一个数组变量。这时你一定在想为什么scanf()
要接收指针,这是因为scanf()函数打算更新数组的内容,一个想要更新变量的函数可不需要变量本身的值,它要的是变量的地址。
           
            
            
  1. char name[40];
  2. printf("Enter your name: ");
  3. scanf("%39s", name);//你将把人名保存在这个数组中。
用scanf()输入数字
            
             
             
  1. int age;
  2. printf("Enter your age: ");
  3. scanf("%i", &age);
  4. // %i 表示用户会输入一个int值
  5. // 用&运算符得到int的地址
scanf()允许传递格式字符串,就像你对printf()函数做的那样,甚至可以用scanf()一次输入多条信息:
             
              
              
  1. char first_name[20];
  2. char last_name[20];
  3. printf("Enter first and last name: ");
  4. scanf("%19s %19s", first_name, last_name);
  5. printf("First: %s Last:%s\n", first_name, last_name);
scanf()若不限制读取长度,会导致缓冲区溢出!
如果忘了限制scanf()读取字符串的长度,用户就可以输入远远超出程序空间的数据,多余的数据会写到计算机还没有分配好的存储器中。
如果运气好,数据不但能保存,而且不会有任何问题。但缓冲区溢出很有可能会导致程序出错,这种情况通常被称为段错误或abort trap,
不管出现什么错误消息,程序都会崩溃。

除了scanf()还可以用fgets()
和scanf()函数一样,fgets()接收char指针,不同的是,你必须给出最大长度!
             
              
              
  1. char food[5]; // 保存输入字符的缓冲区
  2. printf(" Enter favorite food : ");
  3. fgets(food, sizeof(food), stdin);
  4. // food-->指向缓冲区的指针;sizeof(food)-->表示接收字符串(包括“\0”)的最大长度。
  5. // stdin-->标准输入,这里表示数据将来自键盘

fgets()配合sizeof一起使用要小心!
如果你要向fgets()函数传递数组变量,就用sizeof;如果只是传指针,就应该输入你想要的长度。
            
             
             
  1. char* food;
  2. printf(" Enter favorite food : ");
  3. fgets(food, 5, stdin); // 这里的5就不能写成sizeof(food)了!

scanf() vs fgets()对比

scanf() fgets()
限制用户输入的字符数吗? 只 要 记 得 在 格 式 串 中 加 入 长 度, scanf() 就能限制用户输入 数据的长度 fgets() 强行限制用户输入字符 串的长度
能输入多个字段吗? scanf() 不但允许输入多个 字段,而且允许输入结构化数 据,可以指定两个字段之间以什 么字符分割。 fgets() 只允许向缓冲区 中输入一个字符串,而且只能是 字符串,不能是其他数据类型, 只能有一个缓冲区
用户能输入带空格的字符串吗? 当 scanf() 用 %s 读取字符串 时,遇到空格就会停止。如果想 要输入多个单词,需要多次调用 scanf() ,或使用一些复杂的正 则表达式技巧。 无论何时, fgets() 都能读取整个字符串。
总结: 如果需要输入由 多个字段构成的结构化数据,可以使用scanf();而如果想要输入一个非结构化 的字符串,fgets()将是你的不二之选

字符串字面值不能更新!
            
             
             
  1. //指向字符串字面值的指针变量不能用来修改字符串的内容
  2. char *cards = "JQK";//不能用cards这个变量修改这个字符串。
  3. //但如果你用字符串字面值创建一个数组,就可以修改了
  4. char cards[] = "JQK";

为什么用指针不能修改?
因为当计算机把程序载入存储器时,会把所有常数值(如字符串常量“JQK”)放到常量存储区,这部分存储器是只读的。
而char *cards=“JQK”;语句实际上是存储器在栈中开辟了一个空间来保存cards这个指针变量,这个变量指向了位于常量区
的“JQK”字面值,而这个是只读的,你通过指针去修改它肯定是不行的

为什么用数组就可以修改?
因为如果声明了一个名为 cards 的字符数组,然后把它设置成字符串字面值, 那么存储器就会在栈上开辟块连续空间给
cards 数组,并在这里保存了一份字符串字面值的副本。 cards 不再只是一个指向字符串字面值的指针,而是一个崭新的数组,
里面保存了字符串字面值的最新副本。对于这个副本是完全可以修改的。

char cards[]在不同地方代表不同的含义:
1.如果是普通的变量声明,cards就是一个数组,而且必须立即赋值:
            
             
             
  1. int my_function()
  2. {
  3. char cards[] = "JQK"; // 因为没有给出数组大小,因此必须立即赋值
  4. ...
  5. }
2.但如果cards以函数参数的形式声明,那么cards就是一个指针:
            
             
             
  1. // 下面这两个函数是等价的
  2. void stack_deck(char cards[])
  3. {
  4. ...
  5. }
  6. void stack_deck(char *cards)
  7. {
  8. ...
  9. }

代码示例:
             
              
              
  1. #include <stdio.h>
  2. int main()
  3. {
  4. // char* cards = "JQK"; 这样写就会报错
  5. char cards[] = "JQK";
  6. char a_card = cards[2];
  7. cards[2] = cards[1];
  8. cards[1] = cards[0];
  9. cards[0] = cards[2];
  10. cards[2] = cards[1];
  11. cards[1] = a_card;
  12. puts(cards);
  13. return 0;
  14. }

注:如果你确实想把指针设成字符串字面值,为了避免运行错误,必须确保使用了const关键字,
这样的话,如果编译器发现有代码试图修改字符串,就会提示编译错误。
             
              
              
  1. const char *s = "some string";

问: 为什么数组变量不保存在存储器中?既然它存在,就应该在某个地方,不是吗?
答: 程序在编译期间,会把所有对数组变量的引用全部替换成数组的地址(第一个元素的地址)。也就是说,
在最后的可执行文件中,数组变量并不存在。既然数组变量从来不需要指向其他地方,有和没有其实都一样。

问: “声明”和“定义”的区别是什么?
答: 声明是一段代码,它声称某样东西(变量或函数)的存在;而定义则说明这个东西到底是什么。
如果在声明了变量的同时将其设为某个值(例如int x = 4;),那么这段代码既是声明又是定义。


猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53484333