指针和内存

不同内存中变量的作用域和声明周期

作用域

生命周期

全局内存

整个文件

应用程序的生命周期

静态内存

声明它的函数内部

应用程序的生命周期

自动内存(局部内存)

声明它的函数内部

限制在函数执行时间内

动态内存

由引用该内存的指针决定

知道内存释放

理解这些内存类型可以更好地理解指针。大部分指针用来操作内存中的数据,因此理解内存的分区和组织方式有助于我们弄清楚指针如何操作内存。

为什么要精通指针

指针有几种用途,包括:

  • 写出快速高效的代码;
  • 为解决很多类问题提供方便的途径;
  • 支持动态内存分配;
  • 使表达式变得紧凑和简洁;
  • 提供用指针传递数据结构的能力而不会带来庞大的开销;
  • 保护作为参数传递给函数的数据。

使用指针过程出现的问题:

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

声明指针

星号两边的空白符无关紧要,下列声明时等价的:

int *pi;
int* pi;
int * pi;
int*pi;

记任这儿点:

  • pi的内容最终应该赋值为一个整数变量的地址;
  • 这些变量没有被初始化,所以包含的是垃圾数据;
  • 指针的实现中没有内部信息表明自己指向的是什么类型的数据或者内容是否合法;
  • 不过,指针有类型,而且如果没有正确使用,编译器会频繁抱怨。

如何阅读声明

const int *pci;

倒过来读可以让我们一点点理解这个声明:

《深入理解C指针》——认识指针_void指针

地址操作符

地址操作符&会返回操作数的地址。

num = 0;
pi = #

打印指针的值

格式说明符

含义

%x

将值显示为十六进制数

%o

将值显示为八进制数

%p

将值显示为实现专用的格式,通常是十六进制数(大写)

在不同的平台上用一致的方式显示指针的值比较困难。一种方法是把指针转换为void指针,然后用%p格式说明符来显示,如下:

printf("value of pi:%p\n",(void*)pi);

用间接引用操作符解引指针

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

int num = 5;
int *pi = #
# 返回指针指向的值
printf("%p\n",*pi); //5

指向函数的指针

函数没有参数也没有返回值,指针的名字是foo:

void (*foo)();

null的概念

null不一样的概念:

  • null概念;
  • null指针常量;
  • NULL宏;
  • ASCII字符NULL;
  • 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的字符串表示为以О值结尾的字符序列。null字符串是空字符串,不包含任何字符。最后,null语句就是只有一个分号的语句。

接下来我们会看到,null指针对于很多数据结构的实现来说都是很有用的特性,比如链表经常用null指针来表示链表结尾。

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

pi = NULL;


我们可以给指针赋0,但是不能赋任何别的整数值。


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

void指针

void指针是通用指针,用来存放任何数据类型的引用。

void *pv;

它有两个有趣的性质:

  • void指针具有与char指针相同的形式和内存对齐方式;
  • void指针和别的指针永远不会相等,不过,两个赋值为NULL的void指针是相等的。


任何指针都可以被赋给void指针,它可以被转换回原来的指针类型,这样的话指针的值和原指针的值是相等的。


int num;
int *pi = #
printf("value of pi: %p\n",pi); # 100
void* pw = pi;
pi = (int*) pw ;
printf ("value of pi: %p\n",pi); # 100

注:

void指针只能做数据指针,而不能用做函数指针。

指针被声明为全局或静态,在程序启动时被初始化为NULL。

指针的长度和类型

指针长度取决于使用的机器和编译器。比如,在现代Windows 上,指针是32位或64位长。对于DOS和 Windows 3.1来说,指针则是16位或32位长。

内存模型

64位机器的出现导致为不同数据类型分配的内存在长度上的差异变得明显。

指针相关的预定义类型

使用指针时经常用到以下四种预定义类型:

  • size_t:用于安全地表示长度。
  • ptrdiff_t:用于处理指针算术运算。
  • intptr_t和uintptr_t:用于存储指针地址。

size_t

size_t类型表示C中任何对象所能达到的最大长度,它是无符号整数。

size_t用做sizeof操作符的返回值类型,同时也是很多函数的参数类型,包括malloc和strlen。


在声明诸如字符数或者数组索引这样的长度变量时用size_t是好的做法。它经常用于循环计数器.数组索引,有时候还用在指针算术运算上。


intptr_t和uintptr_t

int num;
intptr_t *pi = #
uintptr_t *pu = # //出错
uintptr_t *pu = (uintptr_t*)# //可强制转换

指针操作符

操作符

名称

含义

*

用来声明指针

*

解引

用来解引指针

->

指向

用来访问指针引用的结构的字段

+

用来对指针做加法

-

用来对指针做减法

== !=

相等、不等

比较两个指针

>、>=、<、<=

大于、大于等于、小于、小于等于

比较两个指针

(数据类型)

转换

改变指针的类型

指针算法运算

给指针加上整数

给指针加上一个整数实际上加的数是这个整数和指针数据类型对应字节数的乘积。

void指针和加法

作为扩展,大部分编译器都允许给void指针做算术运算,这里我们假设void指针的长度是4。不过,试图给void指针加1可能导致语法错误。

给指针减去整数

就像整数可以和指针相加一样,也能从指针减去整数。减去整数时,地址值会减去数据类型的长度和整数值的乘积。

指针相减

一个指针减去另一个指针会得到两个地址的差值。​​这个差值通常没什么用,但可以判断数组中的元素顺序。​

比较指针

指针可以用标准的比较操作符来比较。通常,比较指针没什么用。然而,当把指针和数组元素相比时,​​比较结果可以用来判断数组元素的相对顺序。​

指针的常见用法

指针的用处包括:

  • 多层间接引用;
  • 常量指针。

多层间接引用

char *titles[] = {"A Tale of Two cities",
"wuthering Heights" , "Don Quixote" ,"odyssey" , "Moby-Dick" , "Hamlet" ."Gulliver's Travels"};

还有两个数组分别用来维护一个"畅销书"列表和一个英文书列表。这两个数组保存的是titles 数组里书名的地址,而不是书名的副本。两个数组都声明为字符指针的指针。数组元素会保存titles数组中元素的地址,这样可以避免对每个书名重复分配内存,确保每个书名的位置唯一。如果需要修改书名,只改一个地方就可以了。

char **bestBooks[3];
char **englishBooks[4];

bestBooks[0]= &titles[0];
bestBooks[1] = &titles[3];
bestBooks[2]= &titles [5];

englishBooks[0]= &titles[0];
englishBooks[1]= &titles[1];
englishBooks[2]= &titles[5];
englishBooks[3] = &titles[6];

printf ( %s\n" ,*englishBooks [1]); // wuthering Heights

《深入理解C指针》——认识指针_c语言_02

用多层间接引用可以为代码的编写和使用提供更多的灵活性。虽然简洁引用没有层次限制,但是使用的层次过多会让人迷惑、很难维护。

常量与指针

C语言的功能强大还表现在const关键字与指针的结合使用上。

指针类型

指针是否修改

指向指针的数据是否可修改

指向非常量的指针

指向常量的指针

指向非常量的常量指针

指向常量的常量指针

指向常量的指针

指向常量的指针:意味着不能通过指针修改它所引用的值。

int num = 5;
const int limit = 500;
int *pi; //指向整数
const int *pci; //指向整数常量

pi = &num;
pci = &limit;

pci = &num; //合法,指针可以改为引用另一个整数常量、或者普通整数
//我们可以解引pci来读取它,但不能解引它来修改它

《深入理解C指针》——认识指针_数据_03

把pci声明为指向整数常量的指针意味着:

  • pci可以被修改为指向不同的整数常量;
  • pci可以被修改为指向不同的非整数常量;
  • 可以解引 pci以读取数据;
  • 不能解引 pci从而修改它指向的数据。

指向非常量的常量指针

指向非常量的常量指针:意味着指针不可变,但是它指向的数据可变。

int num;
int *const cpi = &num;
  • cpi必须被初始化为指向非常量变量;
  • cpi不能被修改;
  • cpi指向的数据可以被修改。
    《深入理解C指针》——认识指针_void指针_04
    无论cpi引用什么,都可以解引cpi然后赋一个新值。
*cpi = limit;
*cpi = 25;

指向非常量的常量指针无法初始化为指向常量:

const int limit = 500;
int *const cpi = &limit; //警告

指向非常量的常量指针在进行初始化后无法再次进行赋值:

int num;
int age;
int *const cpi = &num;
cpi = &age; //非法

指向常量的常量指针

指向常量的常量指针:指向常量的常量指针很少派上用场。这种指针本身不能修改,它指向的数据也不能通过它来修改。

const int * const cpci = &limit;

《深入理解C指针》——认识指针_数据_05

对于指向常量的常量指针,我们不能:

  • 修改指针;
  • 修改指针指向的数据。

指向"指向常量的常量指针"的指针

指向常量的指针也可以有多层间接引用。

const int * const cpci = &limit;
const int * const * pcpci;

《深入理解C指针》——认识指针_指针_06

小结

主要讨论指针基本概念,声明指针,指针长度,以及如何使用指针。还有null和一系列指针操作符。