重温C语言 | 指针基础(指针与内存)

C语言在编译之后会以三种形式使用内存静态/全局内存
静态/全局内存
静态声明的变量分配在这里,全局变量也使用这部分内存。这些变量在程序开始运行时分配,直到程序终止才消失。所有函数都能访问全局变量,静态变量的作用域则局限在定义它们的函数内部。
自动内存
这些变量在函数内部声明,并且在函数被调用时才创建。它们的作用域局限于函数内部,而且生命周期限制在函数的执行时间内。
动态内存
内存分配在堆上,可以根据需要释放,而且直到释放才消失。指针引用分配的内存,作用域局限于引用内存的指针,这是第2章的重点。
表1-1总结了这些内存区域中用到的变量的作用域和生命周期。
在这里插入图片描述
指针变量包含内存中别的变量、对象或函数的地址。对象就是内存分配函数(比如malloc)分配的内存。指针通常根据所指的数据类型来声明。对象可以是任何C数据类型,如整数、字符、字符串或结构体。然而,指针本身并没有包含所引用数据的类型信息,指针只包含地址。

为什么要精通指针
 写出快速高效的代码;
 为解决很多类问题提供方便的途径;
 支持动态内存分配;
 使表达式变得紧凑和简洁;
 提供用指针传递数据结构的能力而不会带来庞大的开销;
 保护作为参数传递给函数的数据
用指针可以写出快速高效的代码是因为指针更接近硬件。也就是说,编译器可以更容易地把操作翻译成机器码。指针附带的开销一般不像别的操作符那样大。很多数据结构用指针更容易实现,比如链表可以用数组实现,也可以用指针实现。然而,指针更容易使用,也能直接映射到下一个或上一
个链接。用数组实现需要用到数组下标,不直观,也没有指针灵活。

指针形式不仅更清晰,也更灵活。通常创建数组时需要知道数组的长度,这样就会限制链表所能容纳的元素数量。使用指针没有这个限制,因为新节点可以根据需要动态分配。
C的动态内存分配实际上就是通过使用指针实现的。malloc和free函数分别用来分配和释放动态内存。动态内存分配可以实现变长数组和数据结构(如链表和队列)。不过,新的C11标准也支持变长数组
了。
紧凑的表达式有很强的表达能力,但也比较晦涩,因为很多程序员并不能完全理解指针表示法。紧凑的表达式应该用来满足特定的需要,而不是为了晦涩而晦涩。比如说,下面的代码用了两个不同
的printf函数调用来打印names的第二个元素的第三个字符。如果对指针的这种用法感到困惑,不用担心,我们会在1.1.6节中详细介绍解引(dereference)的工作原理。尽管两种方式都会显示字母n,
但是数组表示法更简单。

char names[] = {“Miller”,“Jones”,“Anderson”}; printf("%c\n",(*(names+1)+2)); printf("%c\n",names[1][2]);

指针是创建和加强应用的强大工具,不利之处则是使用指针过程中可能会发生很多问题,比如:

 访问数组和其他数据结构时越界;
 自动变量消失后被引用;
 堆上分配的内存释放后被引用;
 内存分配之前解引指针。

声明指针
通过在数据类型后面跟星号,再加上指针变量的名字可以声明指针。下面的例子声明了一个整数和一个整数指针:
int num; int pi;
星号两边的空白符无关紧要,下面的声明都是等价的:
int
pi; int * pi; int pi; intpi;

星号将变量声明为指针。这是一个重载过的符号,因为它也用在乘法和解引指针上。
典型的内存分配是什么样的。三个方框表示三个内存单元,每个方框左边的数字是地址,地址旁边的名字是持有这个地址的变量,这里的地址100只是为了说明原理。就这个问题来说,指针或者其他变量的实际地址通常是未知的,而且在大部分的应用程序中,这个值也没什么用。三个点表示未初始化的内存。

变量num和pi分别位于地址100和104。假设这两个变量都占据4字节空间,就像1.2节中所说,实际的长度取决于系统配置。除非特别指明,我们所有的例子都使用4字节长的整数。
在这里插入图片描述
本书用100这样的地址来解释指针如何工作,这样会简化例子。当你运行示例代码时会得到不同的地址,而且这些地址甚至在同一个程序运行几次的时候都可能变化。
记住这几点:
 pi的内容最终应该赋值为一个整数变量的地址;
 这些变量没有被初始化,所以包含的是垃圾数据;
 指针的实现中没有内部信息表明自己指向的是什么类型的数据或者内容是否合法;
 不过,指针有类型,而且如果没有正确使用,编译器会频繁抱怨。
说到垃圾数据,我们是指分配的内存中可能包含任何数据。当内存刚分配时不会被清理,之前的内容可能是任何东西。如果之前的内容是一个浮点数,那把它当成一个整数就没什么用。就算
确实包含了整数,也不大可能是正确的整数。所以我们说内容是垃圾。

尽管不经过初始化就可以使用指针,但只有初始化后,指针才会正常工作。

如何阅读声明
现在介绍一种阅读指针声明的方法,这个方法会让指针更容易理解,那就是:倒过来读。尽管我们还没讲到指向常量的指针,但可以先看看它的声明:
const int *pci;
倒过来读可以让我们一点点理解这个声明(见图1-3)。
在这里插入图片描述
地址操作符
地址操作符&会返回操作数的地址。我们可以用这个操作符来初始化pi指针,如下所示:

num = 0; pi = #

num变量设置为0,而pi设置为指向num的地址,如图1-4所示。
在这里插入图片描述
可以在声明变量pi的同时把它初始化为num的地址,如下所示:

int num; int *pi = #

有了以上声明,下面的语句在大部分编译器中都会报语法错误:
GCC编译器(LINUX环境):

[csy@local pointer]$ gcc poter_base1.c 
poter_base1.c: 在函数‘main’中:
poter_base1.c:8: 警告:赋值时将整数赋给指针,未作类型转换 

Code Blocks:

F:\A-LearningRecord\C&单片机\C Programming Language\demo\demo1\arr_demo1.c
|8|warning: assignment makes pointer from integer without a cast [enabled by default]|

pi变量的类型是整数指针,而num的类型是整数。这个错误消息是说整数不能转换为指向整数类型的指针。

把整数赋值给指针一般都会导致警告或错误。

指针和整数不一样。在大部分机器上,可能两者都是存储为相同字节数的数字,但它们不一样。不过,也可以把整数转换为指向整数的指针:

pi = (int *)num;
#include <stdio.h>
int main()
{
	int num;
    int *pi ;
    num = 0;
    pi = (int*)num;

}

0 error(s), 0 warning(s) (0 minute(s), 0 second(s))

记录一个小失误:

#include <stdio.h>
int main()
{
    int num;
    int *pi ;

    pi = (int*)num;
    printf("%#\r\n%#\r\n",pi,&num);
    pi = (int*)num;
   printf("%#\r\n%#\r\n",pi,&num);
   
   }

[csy@local pointer]$ ./base1
%#
%#
%#
%#
原因在于

原文栗子:

#include <stdio.h>
int main()
{


        int num = 0;
        int *pi = &num;
        printf("Address of num: %d Value: %d\n",&num, num);
        printf("Address of pi: %d Value: %d\n",&pi, pi);

}
[csy@local pointer]$ ./base1 
Address of num: -1081145604 Value: 0
Address of pi: -1081145608 Value: -1081145604

printf函数还有其他几种格式说明符在打印指针的值时比较有用,如
表1-2所示。

在这里插入图片描述

#include <stdio.h>
int main()
{
        int num=0;
        int *pi = &num;
 printf("Address of num: %d Value: %d\n",&num, num);
printf("Address of num: %x Value: %x\n",&num, num);
printf("Address of num: %o Value: %o\n",&num, num);
printf("Address of num: %p Value: %p\n",&num, num);

printf("Address of pi: %d Value: %d\n",&pi, pi);
printf("Address of pi: %x Value: %x\n",&pi, pi);
printf("Address of pi: %o Value: %o\n",&pi, pi);
printf("Address of pi: %p Value: %p\n",&pi, pi);
}
Address of num: 6356732 Value: 0
Address of num: 60fefc Value: 0
Address of num: 30177374 Value: 0
Address of num: 0060FEFC Value: 00000000
Address of pi: 6356728 Value: 6356732
Address of pi: 60fef8 Value: 60fefc
Address of pi: 30177370 Value: 30177374
Address of pi: 0060FEF8 Value: 0060FEFC

%p和%x的不同之处在于:%p一般会把数字显示为十六进制大写。如果没有特别说明,我们用%p作为地址的说明符。

虚拟内存和指针

让打印地址变得更为复杂的是,在虚拟操作系统上显示的指针地址一般不是真实的物理内存地址。虚拟操作系统允许程序分布在机器的物理地址空间上。应用程序分为页(或帧),这些页表示内存中的区域。应用程序的页被分配在不同的(可能是不相邻的)内存区域上,而且可能不是同时处于内存中。如果操作系统需要占用被某一页占据的内存,可以将这些内存交换到二级存储器中,待将来需要时再装载进内存中(内存地址一般都会与之前的不同)。这种能力为虚拟操作
系统管理内存提供了相当大的灵活性。

每个程序都假定自己能够访问机器的整个物理内存空间,实际上却不是。程序使用的地址是虚拟地址。操作系统会在需要时把虚拟地址映射为物理内存地址。

这意味着页中的代码和数据在程序运行时可能位于不同的物理位置。应用程序的虚拟地址不会变,就是我们在查看指针内容时看到的地址。操作系统会帮我们将虚拟地址映射为真实地址。

操作系统处理一切事务,程序员无法控制也不需要关心。理解这些问题就能解释在虚拟操作系统中运行的程序所返回的地址。

用间接引用操作符解引指针
间接引用操作符(*)返回指针变量指向的值,一般称为解引指针。
下面的例子声明和初始化了num和pi:

int num = 5;
int *pi = #

我们也可以把解引操作符的结果用做左值。术语“左值”是指赋值操作符左边的操作数,所有的左值都必须可以修改,因为它们会被赋值。
下面的代码把200赋给pi指向的整数。因为它指向num变量,200会被赋值给num。图1-5说明了这个操作如何影响内存。

*pi = 200;
printf("%d\n", num); //显示200

利用解引操作符给内存赋值

指向函数的指针
指针可以声明为指向函数,声明的写法有点难记。下面的代码说明如何声明一个指向函数的指针。函数没有参数也没有返回值。指针的名字是foo:
void (*foo)();
指向函数的指针有很多值得讨论的地方,详见第3章。

null的概念
null很有趣,但有时候会被误解。之所以会造成迷惑,是因为我们会遇到几种类似但又不一样的概念,包括:
null概念;
null指针常量;
NULL宏;
ASCII字符NUL;
null字符串;
null语句。

NULL被赋值给指针就意味着指针不指向任何东西。null概念是指指针包含了一个特殊的值,和别的指针不一样,它没有指向任何内存区域。两个null指针总是相等的。尽管不常见,但每一种指针类型(如字符指针和整数指针)都可以有对应的null指针类型。
null概念是通过null指针常量来支持的一种抽象。这个常量可能是也可能不是常量0,C程序员不需要关心实际的内部表示。
NULL宏是强制类型转换为void指针的整数常量0。在很多库中定义如下:

#define NULL ((void *)0)

这就是我们通常理解为null指针的东西。这个定义一般可以在多种头文件中找到,包括stddef.h、stdblib.h和stdio.h。如果编译器用一个非零的位串来表示null,那么编译器就有责任在指
针上下文中把NULL或0当做null指针,实际的null内部表示由实现定义。使用NULL或0是在语言层面表示null指针的符号。ASCII字符NUL定义为全0的字节。然而,这跟null指针不一样。C的
字符串表示为以0值结尾的字符序列。null字符串是空字符串,不包含任何字符。最后,null语句就是只有一个分号的语句。
接下来我们会看到,null指针对于很多数据结构的实现来说都是很有
用的特性,比如链表经常用null指针来表示链表结尾。

如果要把null值赋给pi,就像下面那样用NULL:

pi = NULL;

有趣的是,我们可以给指针赋0,但是不能赋任何别的整数值。看一下下面的赋值操作:

pi = 0;
pi = NULL;
pi = 100; // 语法错误
pi = num; // 语法错误

指针可以作为逻辑表达式的唯一操作数。比如说,我们可以用下面的代码来测试指针是否设置成了NULL。

if(pi) {
// 不是NULL
} else {
// 是NULL
}

下面两个表达式都有效,但是有冗余。这样可能更清晰,但是没必要显式地跟NULL做比较。
如果这里pi被赋了NULL值,那就会被解释为二进制0。在C中这表示假,那么倘若pi包含NULL的话,else分支就会执行。

if(pi == NULL) ...
if(pi != NULL) ...

任何时候都不应该对null指针进行解引,因为它并不包含合法地址。执行这样的代码会导致程序终止。

用不用NULL
使用指针时哪一种形式更好,NULL还是0?无论哪一种都完全没问题,选择哪种只是个人喜好。有些开发者喜欢用NULL,因为这样会提醒自己是在用指针。另一些人则觉得没必要,因为NULL其实就是
0。
然而,NULL不应该用在指针之外的上下文中。有时候可能有用,但不应该这么用。如果代替ASCII字符NUL的话肯定会有问题。这个字符没有定义在标准的C头文件中。它等于字符’\0’,其值等于十进制
0。

0的含义随着上下文的变化而变化,有时候可能是整数0,有时候又可能是null指针。看一下这个例子:

int num;
int *pi = 0; // 这里的0表示null的指针NULL
pi = &num;
*pi = 0; // 这里的0表示整数0

void指针

void指针是通用指针,用来存放任何数据类型的引用。下面的例子就是一个void指针:
void *pv;

它有两个有趣的性质:
void指针具有与char指针相同的形式和内存对齐方式;
void指针和别的指针永远不会相等,不过,两个赋值为NULL的void指针是相等的。
任何指针都可以被赋给void指针,它可以被转换回原来的指针类型,这样的话指针的值和原指针的值是相等的。在下面的代码中,int指针被赋给void指针然后又被赋给int指针:

int num;
int *pi = &num;
printf("Value of pi: %p\n", pi);
void* pv = pi;
pi = (int*) pv;
printf("Value of pi: %p\n", pi);

运行这段代码后,指针地址是一样的:

Value of pi: 100
Value of pi: 100

void指针只用做数据指针,而不能用做函数指针。在8.4.2节中,我们将再次研究如何用void指针来解决多态的问题。
用void指针的时候要小心。如果把任意指针转换为void指针,那就没有什么能阻止你再把它转换成不同的指针类型了。
sizeof操作符可以用在void指针上,不过我们无法把这个操作符用在void上,如下所示:

size_t size = sizeof(void*); // 合法
size_t size = sizeof(void); // 不合法

size_t是用来表示长度的数据类型,会在1.2.2节中讨论。

全局和静态指针
指针被声明为全局或静态,就会在程序启动时被初始化为NULL。下面是全局和静态指针的例子:

int *globalpi;
void foo() {
static int *staticpi;
...
}i
nt main() {
...
}

图1-6说明了内存布局,栈帧被推入栈中,堆用来动态分配内存,堆上面的区域用来存放全局/静态变量。这只是原理图,静态和全局变量一般放在与栈和堆所处的数据段不同的数据段中。栈和堆将在3.1
节讨论。

发布了65 篇原创文章 · 获赞 36 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_40860986/article/details/97621180