1.1.1 指针概述
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。
指针的类型:从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
1) int *ptr; //指针的类型是int *
2) char *ptr; //指针的类型是char *
3) int **ptr; //指针的类型是int **
4) int (*ptr)[3]; //指针的类型是int (*)[3]
5) char *(*ptr)[4]; //指针的类型是char *(*)[4] char *str[4]; &str类型:char *(*)[4] str类型:char **
指针所指向的类型:从语法的角度上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符 * 去掉,剩下的就是指针所指向的类型。当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
1) int *ptr; //指针所指向的类型是int
2) char *ptr; //指针所指向的的类型是char
3) int **ptr; //指针所指向的的类型是int *
4) int (*ptr)[3]; //指针所指向的的类型是int ( )[3]
5) int *(*ptr)[4]; //指针所指向的的类型是int *( )[4]
变量地址---系统分配给变量的存储单元的起始地址。一个变量无论占据有几个存储单元,都只有一个地址(首地址)可以作为它的地址。
指针变量:存放变量地址的变量是指针变量。即在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。一个指针变量的内容一定是另一个变量在内存中的地址。
把普通变量的地址赋予一个指针变量,那么就称该指针变量指向了普通变量。指针变量中存放那个变量的地址,就称指针变量指向哪个变量。
char *names[] = { "Tom","Jerry","Anderson" };
printf("%c
", *(*(names + 1) + 4)); y
printf("%c
", names[1][4]); y
1.1.2 指针变量的定义:
数据类型名 * 指针变量名 int* pi; int * pi; int *pi; int*pi; 这些声明都是等价的。
*两边的空白符无关紧要,*将变量声明为指针,这是一个重载过的符号也用在乘法和解引用指针上。
指针变量赋值只能赋予地址,有趣的是,我们可以给指针赋 0,但是不能赋任何别的整数值。
1.1.3 如何阅读指针声明:倒过来读。
const int *pci;
1) pci 是一个变量
2)*pci 是一个指针变量
3)int *pci 是一个指向整数的指针变量
4)const int *pci 是一个指向整数常量的指针变量
1.1.4 地址操作符
C语言提供了地址运算符 & 来表示变量的地址。 地址操作符&会返回操作数的地址。
1.1.5 打印指针的值
%p 和 %x 的不同之处在于:%p一般会把数字显示为十六进制的大写。
虚拟内存和指针
每个程序都假定自己能够访问机器的整个物理内存空间,实际上却不是。程序使用的地址是虚拟地址。操作系统会在需要时把虚拟地址映射为物理地址。应用程序的虚拟地址不会变,就是我们在查看指针内容时看到的地址。操作系统会帮我们将虚拟地址映射为真实地址。
1.1.6 用间接引用操作符解引指针
间接引用操作符 * 返回指针变量指向的值,一般称为解引指针。我们也可以把解引操作符的结果看作左值,“左值”是赋值操作符左边的操作符,所有的左值都必须可以被修改,因为它们会被赋值。
1.1.8 NULL概念
- NULL被赋给指针,就意味着指针不指向任何东西。
- null 概念是指指针包含一个特殊的值,和别的指针不一样,它们有指向任何内存区域。
- NULL宏是强制类型转换为void指针的整数常量0。在很多库中定义如下:
#define NULL ( (void *) 0
- ASCII字符NUL定义为全0的字节。
- null字符串是空字符串,不包含任何字符。
- null语句就是只有一个分号的语句。
1.用不用NULL
对于指针,使用NULL或0都可以,但NULL不能用于指针之外的上下文中。尤其是替代ASCII字符NUL肯定是有问题的。它等于字符 ' ' ,其值等于十进制 0。
2.void 指针
void指针是通用指针,用来存放任何数据类型的引用。任何指针都可以赋给void指针。
- void指针具有与char指针相同的形式和内存对齐方式;
- void指针和别的指针永远不会相等,不过,两个赋值为NULL的void指针是相等的。
- void指针只能用做数据指针,而不能用做函数指针。
sizeof操作符可以用在void指针上,不过我们无法把这个操作符用在void上。如下所示:
size_t size = sizeof(void *); //合法
size_t size = sizeof(void); //不合法
3.全局和静态指针
指针被声明为全局或静态,就会在程序启动时被初始化为NULL。
1.2 指针的长度和类型
1.2.1 内存模型
模型取决于操作系统和编译器,一种操作系统可能支持多种模型,这通常是用编译器选项来控制的。
1.2.2 指针相关的预定义类型
- size_t:用于安全的表示长度
- ptrdiff_t:用于处理指针算术运算
- intptr_t 和 uintptr_t:用于存储指针地址
1. 理解size_t
size_t类型表示C中任何对象所能达到的最大长度。它是无符号整数,因为负数在这里没有意义。它的目的是提供一种可移植的方法来声明与系统中可寻址的内存区域一致的长度。size_t用做sizeof操作符的返回值类型,同时也是很多函数的参数类型,包括malloc和strlen。
- 在声明诸如字符数或者数组索引这样的长度变量时用size_t是好的做法。
- 它经常用于循环计数器、数组索引、有时候还用在指针算术运算上。
打印size_t类型的值要小心,因为这是无符号的值。推荐的格式说明符是%zu。不过,某些情况下不能用这个说明符,作为替代,可以考虑%u或%lu。
因为size_t是无符号的,一定要给这种类型的变量赋正数。
2. 对指针使用sizeof操作符
sizeof操作符可以用来判断指针长度。当需要使用指针长度时,一定要用sizeof操作符。
3. 使用intptr_t和uintptr_t
ntptr_t和uintptr_t类型用来存放指针地址。他们提供了一种可移植且安全的方法声明指针,而且和系统中使用的指针长度相同,对于把指针转化成为整数形式来说很有用。
uintptr_t是intptr_t的无符号版本。对于大部分操作,用intptr_t比较好。uintptr_t不像intptr_t那样灵活。下面例子说明如何使用intptr_t:
int num;
intptr_t *pi = #
如果像下面那样试图把整数地址赋给uintptr_t类型的指针,得到一个语法错误。
uintptr_t *pu = #
使用强制类型转换可以消除警告: uintptr_t *pu = (uintptr_t *) #
如果不强制转换类型,不能将uintptr_t用于其它类型:
char ch;
uintptr_t *pu = (uintptr_t *)&ch;
当可移植性和安全性变得重要时,就应该使用这些类型。
1.3 指针操作符
1.3.1 指针算术运算
1.给指针加减整数
给指针加上一个整数实际上加的数是这个整数和指针数据类型对应字节数的乘积。其实,p是一个指针,n是一个正整数,对指针p进行加减n后实际地址为:
p +或- n * sizeof(指针数据类型) 指针自增、自减的运算式是使指针指向下一个或上一个数据的首地址。
例如:设 p是指针变量:
C = *p++ 先取指针变量p的值赋给C ,p做自增运算,使p指向下一目标变量
C = *++p 先使指针p做自增运算指向下一目标变量,再将p所指变量的值赋给C
C = (*p)++ 取指针变量*p的值赋给C,然后变量*p的值增1
C = ++(*p) 变量*p先增1再把值赋给C
4.指针相减
一个指针减去另一个指针会得到两个地址的差值。这个差值通常没什么用,但可以判断数组中的元素顺序。指针之间的差值是他们之间相差的“单位”数,差的符号取决于操作数的顺序。
1.3.2 比较指针
指针可以用标准的比较操作符来比较。当把指针和数组元素相比时,比较结果可以用来判断数组元素的相对顺序。
1.4 指针的常见用法
1.4.1 多层间接引用第一个数组用来存储书名列表的字符数组。
char *titles[] = { "A tale of Two Cities","Wuthering Heights","Don Quixote",
"Odyssey","Moby-Dick","Hamlet","Gulliver's Travels" };
//声明两个指向字符指针的指针的数组
char **bestBooks[3]; //数组元素会保存titles数组中元素的地址
bestBooks[0] = &titles[1];
bestBooks[1] = &titles[3];
bestBooks[2] = &titles[5];
char **englishBooks[4]; //每个数组元素都包含一个指向char 指针的指针
englishBooks[0] = &titles[0];
englishBooks[1] = &titles[2];
englishBooks[2] = &titles[4];
englishBooks[3] = &titles[6];
printf("%s", *englishBooks[0]); //A tale of Two Cities
1.4.2 常量与指针
1.指向常量的指针
可以将指针定义为指向常量,这意味着不能通过指针修改它所引用的值。
我们不能解引指向常量的指针并改变指针所引用的值,但可以改变指针。指针的值不是常量。指针可以改为引用另一个整数常量(const int),或者普通整数(int)。这样做不会有问题。声明只是限制我们不能通过指针来修改引用的值。
const int limit = 123;
const int *pci;
pci = &limit;
把pci声明为指向整数常量的指针意味着:
- pci可以被修改为指向不同的整数常量
- pci可以被修改为指向不同的非整数常量
- 可以解引pci以读取数据
- 不能解引pci从而修改他指向的数据
数据类型和const关键字的顺序不重要。下面两个语句是等价的:
const int *pci;
int const *pci;
2.指向非常量的常量指针
一个指向非常量的常量指针,意味着指针不可变,但是它指向的数据可变。
int num;
int *const cpi = #
- cpi必须被初始化为指向非常量变量;
- cpi不能被修改
- cpi指向的数据可以被修改。
无论cpi引用了什么,都可以解引cpi然后赋一个新值。所以如果试图把cpi初始化为指向常量limit,那么常量就可以修改了,这样是不对的,因为常量不可以被修改,所以会产生一个警告。
3.指向常量的常量指针
这种指针本身不能被修改,它指向的数据也不能通过它来修改。
const int * const cpci = &limit; 与指向常量的指针类似,不一定只能将常量的地址赋给cpci。
const int * const cpci = # 声明指针时必须进行初始化,如果不进行初始化就会产生语法错误。
4.指向“指向常量的常量指针”的指针
const int * const * pcpci = &limit;