8.1
系统内存布局
以
x86
为例,
x86
支持
32
位寻址,因此可以支持最大
2
32
=4GB
的虚拟内存空间(也可以通过
PAE
技术增加到
36
位寻址,因此可以将寻址空间扩大到
64GB
)。如图所示,在
4G
的虚拟地址空间中,
Windows
系统的内存主要分为内核空间和应用层空间上下两部分,每部分各占约
2GB
,其中还包括了一个
64KB
的
NULL
空间以及非法区域。
Windows
内存的逻辑地址包含
2
部分,即段选择符和偏移地址,
CPU
在做地址翻译的时候,通过分段机制计算出一个线性地址,再通过页表机制映射到物理地址以便存取物理内存中的数据和指令。
X64
(
AMD64
)的内存布局与
X86
的内存布局类似,不同的地方在于各自空间的范围和大小不同,同时
X64
下还存在着一些空洞(
hole
),如图所示。在
X64
内存理论上支持最大
2
64
的寻址空间,但实际上这个空间太大了,目前根本用不完,因此实际上的
X64
系统一般都只支持到
40
多位(比如
Windows
支持
44
位最大寻址空间为
16TB
,
Linux
支持
48
位最大寻址空间
256TB
等),支持的空间达到了
TB
级别。但是,无论是在内核空间还是在应用层空间,这些上
TB
的空间并不都是可用的,存在着所谓的空洞。
当程序加载入内存后,程序在内存中可以分为若干个区,这些区包括:静态区(
.data,.rdata,.bss
段等),代码区
(.text)
,堆区,栈区等。
.text
代码段,这个存放代码的,用汇编角度来看就是指令。
.rdata
只读数据段,存放常量,字符常量,
const
常量。
.data
数据段,存放已经初始化好的全局变量和静态变量。
.bss
存放未初始化的全局变量和静态变量。
.rdata
,
.data
,
.bss
都是存放的数据。除了
.bss
段,
.rdata
,
.data
段的值都是在编译的时候就确定了,并且将其编译进了可执行文件,经过反汇编都能找得到。
bss
段是在代码运行的时候手动编写汇编代码将其初始化为
0
的(这就是未初始化的全局和静态变量默认值为
0
的根源)
.stack:
栈区,存放局部变量等。如下图所示,栈区存放这函数调用过程中的形参,返回地址以及局部变量等(将在函数一章讲调用约定的时候具体介绍栈上的数据)。
变量是存放在内存中的,比如上面代码中的变量
i
和
a
,存放变量
i
和
a
的内存的首地址,就是变量
i
和
a
的地址。可以用
&
取址运算符获得。
而对于指针
p
来说,它本身也是一个变量,存放在内存中,只不过它的值是一个内存地址,这个内存地址,可以是其它变量的地址。
内存的地址可以分为有效地址,即这个所对应的内存是可访问的;还有无效地址,访问无效地址,会导致程序崩溃,比如
NULL
地址就是一个无效地址
:
#define NULL ((void *)0)
因此,试图去访问无效地址所指内存,会导致程序崩溃。
8.3
指针定义:变量,地址,类型(宽度)
指针其实就是一个变量,和其他类型的变量一样。它与其他变量的不同就在于它的值是一个内存地址,指向内存的某一个地方。即指针是一种存放另一个变量的地址的变量。
首先定义了一个整型变量
i
,并初始化值为
1
。再定义了一个整型的指针
p
,它的值是
i
的地址。因此,指针
p
指向了
i
所在的内存,该段内存占
4
个字节。
因此指针含义可以分为
3
个方面来理解:
1
,它是一个变量,所以也占用一定的内存空间(在
X86
上占用
4
个字节,
X64
上占用
8
个字节)
2
,它的值是一个内存地址。这个地址可以是其它变量的地址。
3
,它的地址指向的内存空间具有确定的长度。这是指针与地址的本质区别。如果只告诉你一个内存地址,你不会知道从这个地址开始的内存有多长。但如果告诉你一个指针,你会明确的知道从这个内存地址开始的内存有多长。因为指针都是有类型的。知道了指针的类型,就确定了所指向的内存地址对应的长度。
指针是
C
语言中的精华。指针其实就是一个变量,和其他类型的变量一样。在
32
位机器上,它是一个占用四字节的变量,它与其他变量的不同就在于它的值是一个内存地址,指向内存的某一个地方。即指针是一种存放另一个变量的地址的变量。
int main(void)
int
i = 1;
int *p = &i; //
指针
p
指向了
i
的地址
*p += 1;
// *p
将
i
的值增
1
printf(“i = %d\n”, i);
return 0;
C
语言中最复杂最容易出错的要数指针了。指针让一些初级程序员望而却步,而一些新的开发语言(如
Java
,
C#
)干脆就放弃了指针。但是,在
C
语言程序里,你会发现大量的
C
语言指针的使用。因此,从一定程度上来说,只有掌握了指针,才算真正的学会了
C
语言。
指针的定义方式为:
类型名
*
指针名
;
char *pch;
上面的语句就定义了一个名为
pch
的字符指针。在
C
语言里,根据指针的不同类型,可以把指针定义为内建型别指针和自定义结构指针。根据类型不同,会将指针指向的内存数据按照不同的类型解析。即指针如果是
int
类型,那么就将指针指向的内存解析为一个整型;如果指针是
char
类型,那么就将指针指向的内存解析为一个
char
类型。
指针的初始化,可以让指针指向某个变量的地址,也可以让指针指向一个由内存分配函数分配的内存。
8.4
指针声明
char *pch;
int *pi;
float *pf;
double *pd;
那么如何取得一个变量的地址呢?可以在变量前面加
&
运算符。比如:
char c = ‘c’;
那么
c
在内存中的地址为:
&c
。
void main(void)
char c=’a’;
char *pch = &c;
printf(“%p,%p\n”, pch, &c);
所以,可以给字符指针做如下赋值:
char c = ‘c’;
char *pch = &c;
这个时候,
pch
的值就是
c
的地址了。如果要通过指针访问对应的变量的值,可以使用
*
运算符。比如:
char c = ‘c’;
char *pch = &c;
printf(“c=%c\n”, *pch);
8.5 &
与
*
运算符
我们把
&
叫做取址运算符,用来获取某个变量的地址,比如:
int i = 1;
int *p = &i;//p is reference of i,
这里把
p
当做是对
i
的一个引用
(reference)
我们又把
*
叫做解引用
(dereference)
运算符,与
&
运算符互为逆运算。比如:
printf(
“
*p is:%d,i is
%d\n
”
, *p, i);// *p is dereference for i
,
*p equals i
,
*p
代表的就是
i
*p += 1;//
把
p
所指向的地址的内存(
i
)的值加
1
printf("*p is:%d,i is %d\n", *p, i);
这里
*p
代表的就是
i
。
*p
中,
p
必须是有效的地址,否则会引发程序崩溃。比如:
int *p = NULL;
*p = 0;//
此时,
p
无
NULL
地址,会引发程序异常
图解指针
*
与
&
的含义:
char i,j,*p1,*p2;
i='a';
j='b';
p1=&i;
p2=&j;//
图
1
所示,
p1
指向
i
,
p2
指向
j
*p2=*p1;//
图
2
所示,把
i
的值赋值给了
j
。因为
*p2
为
j
,
*p1
位
i
,所以等价为
j=i;
p2=p1;//
图
3
所示,把
p1
的值,赋值给
p2
,因此,
p2
中存放的也是
i
的地址,所以
p2
指向了
i;
8.6 *p
容易混淆的地方:定义指针与解引用
char c=‘a’;
char *s =
“
helloworld
”
;//
此处的
*s
是用于定义指针
s
char *p = &c;//
此处的
*p
是用于定义指针
p
,实际上是
(char*) p=&c
*p += 1;//
此处的
*p
是解引用
(
即
c)
printf(
“
%c\n
”
, *p);//
此处的
*p
是解引用
void func(char *p)//
此处
*p
是定义指针
func(&c);//
传指针
func(p);//
传指针
8.7
指针初始化与引用
当定义了一个指针,对它赋值与初始化的方法:
int i, *p;//
声明了一个整型变量
i
,一个指针
p
,这里的
*p
不是取值,是在定义一个指针
p= &i;
int c;
int *p = NULL;//
声明了一个指针
p
,并初始化为
NULL
p=&c;//
将指针
p
指向变量
c
int
d;
int *p = &d;//
声明了一个指针
p
,并直接初始化为变量
d
的地址
char *p = (char *)malloc(100*sizeof(char));//
声明了一个字符指针
p
,并初始化为堆上的一个地址
char *str = “hello world”;//
声明了一个字符指针
str
,并初始化为字符串的首地址
char c=‘A’;
char *str = &c;//
声明了一个指针
str
并直接初始化为变量
c
的地址
char *pch = &c;
8.8
指针类型与互相转换
,sizeof(p),sizeof(*p)
指针的类型可以有如下几种:
char *p;//
指向内存单元
1
个字节
short *p;//
指向内存单元
2
个字节
int *p;//
指向内存单元
4
个字节
float *p;//
指向内存单元
4
个字节
double *p;//
指向内存单元
8
个字节
void *p//参见8.9一节内容介绍
当然,在学习了结构体等构造型别后,指针还可以包含这些新的构造类型。
指针作为变量,存放在内存中,占有内存空间,因此也是有长度的:
sizeof(p)=4 or 8 //计算指针的长度,在x86 4字节, x64
平台64位程序中8个字节
sizeof(*p)//计算指针
对应类型的长度
通过上面的例子和图,我们可以发现,指针含义可以分为
3
个方面来理解:
1
,它是一个变量,所以也占用一定的内存空间(在
X86
上占用
4
个字节,
X64
上占用
8
个字节)
2
,它的值是一个内存地址。这个地址可以是其它变量的地址。
3
,它的地址指向的内存空间具有确定的长度。这是指针与地址的本质区别。如果只告诉你一个内存地址,你不会知道从这个地址开始的内存有多长。但如果告诉你一个指针,你会明确的知道从这个内存地址开始的内存有多长。因为指针都是有类型的。知道了指针的类型,就确定了所指向的内存地址对应的长度。
8.9 void*
类型指针
我们可以使用
void*
来定义个
void
*
类型的指针如下:
void *p;
p
是
void *
类型指针,其他类型指针隐式转换成该类型,不能直接使用
*p
来取值,必须先转换为特定类型再做取值
p
可以接受任何类型的指针赋值
p
赋值给其它类型的指针,需要强转
p
不能进行解引用
*
运算,必须先转换
int i = 10;
char ch = ‘a’;
int *p1 = &i;
char *p2 = &ch;
void *pv1 = p1;//
把
p1
赋值给
pv1
,不需强转,不能使用
*pv1
void *pv2 = p2;//
把
p2
赋值给
pv2
,不需强转,不能使用
*pv2
int *p3 = (int *)pv1;//
把
pv1
赋值给
p3
,需要强转
char *p4 = (char *)pv2;//
把
pv2
赋值给
p4
,需要强转
也就是说:
void
指针可以指向任意类型的数据,亦即可用任意数据类型的指针对
void
指针赋值。例如:
int * pint;
void *pvoid;
pvoid = pint; /*
不过不能
pint= pvoid; */
如果要将
pvoid
赋给其他类型指针,则需要强制类型转换如:
pint= (int *)pvoid;
在
ANSIC
标准中,不允许对
void
指针进行算术运算如
pvoid++
或
pvoid+=1
等,而在
GNU
中则允许,因为在缺省情况下,
GNU
认为
void *
与
char *
一样。
sizeof(*pvoid
)== sizeof( char)
。
如果函数的参数可以是任意类型指针,那么应声明其参数为
void*
。
void *memcpy(void *dst, void *src, size_t len)
8.10
指针应用:判断系统是低位优先还是高位优先
定义一个整型变量
x
,让它的值为
0x1
。来观察下它在不同系统中的存储情况。在低位优先系统中,低位存放整数的低位,因此低地址的第一个字节的值为
01
。而高位优先系统中,低位存放整数的高位,因此低地址的第一个字节为
00
。因此如果把这个内存地址所在的第一个字节取出来,就可以区别系统是低位优先和高位优先了。
那么如何取得这个整数的低地址所在的这个字节呢?根据指针的定义,只需要定义一个
char
类型的指针,指向整数
x
的地址,那么
char
类型的指针获取的,就是低地址所在的这个字节。于是得到下面的算法:
/*return value :0—big-endian ;1—little-endian*/
int get_endian()
int x=0x1;
char *p=(char*)&x;
return *p ;
int main(void)
printf(”The platform %s
\n ”,get_endian() ? ”is little-endian”:”is big-endian”);
return 0;
8.11
指针加减运算
大家已经知道,
C
语言最适合于底层的开发,一个重要的原因就是因为它支持指针,能够直接访问内存和操作底层的数据,可以通过指针直接动态分配与释放内存:
//
下面是用
typedef
定义一个新结构最常用的定义形式
//
在微软的面试中,在考查你某个算法前,一般会让你先定义一个与算法相关的结构。
//
比如链表排序的时候,让你定义一个链表的结构。
typedef struct _node
value;
struct _node * next;
}node, *link;
node *pnode = NULL; //
声明变量都应该初始化,尤其是指针
pnode = (node *)malloc(sizeof (node)); //
内存分配
//
务必检测内存分配失败情况,程序健壮性的考查
//
加上这样的判断语句,会让你留给面试官一个良好的印象
//
不加这样的判断,如果分配失败,会造成程序访问
NULL
指针崩溃
if (pnode == NULL)
//
出错处理,返回资源不足错误信息
memset(pnode, 0, sizeof(node));
//
新分配的内存应该初始化,否则内存中含有无用垃圾信息
pnode->value = 100;
printf(“pnode->value = %d\n”, pnode->value);
node * ptmp = pnode;
ptmp += 1;
//
指针支持加减运算,但须格外小心
free(pnode);
//
使用完内存后,务必释放掉,否则会泄漏。一般采取谁分配谁释放原则
pnode = NULL;//
释放内存后,需要将指针置
NULL
,防止野指针
上面的这段代码演示了指针的基本使用方式。在指针声明的时候,最好将其初始化为
NULL
,否则指针将随机指向某个区域,访问没有初始化的指针,行为为未定义而为程序带来预想不到的结果;指针释放之后,也应该将指针指向
NULL
,以防止野指针。因为指针所指向的内存虽然释放了,但是指针依然指向某一内存区域。
指针的运算最容易出错。指针支持加减运算。上面代码中
ptmp += 1
运算结束之后,指针指向的区域不是向前移动了一个字节,而是向前移动了
sizeof
(node)
个字节,也就是说“
1
”代表了指针指向的数据结构(
node
)大小个字节。如果要让指针向前移动一个字节,那么需要先对指针进行类型转换:
(char *)ptmp + 1
或者
(unsigned
long)ptmp+1
。
也就是说,对于指针
p
,指针的加法运算
p = p + n
中,
p
向前移动的位置不是
n
个字节,而是
n * sizeof(*p)
个字节,指针的减法运算与此类似。
现在为了让大家对指针有深刻的理解和牢固的掌握,重点研究一些与指针相关的典型问题。这些问题频繁出现在各大知名
IT
企业的笔试或者技术面试中。当然,这也是学习
C
语言必须掌握的问题。
一组易混淆的指针表达式:
*p++;//*p,p++
(*p)++;//(*p)++
,即
*p = *p+1
或者
*p += 1;
b=*p++;//b=*p;p++
b=(*p)++;//b=*p;(*p)+=1;
b=++*p;//(*p)+=1;b=*p;
b=++(*p);//(*p)+=1;b=*p;
b=*++p;//p+=1;b=*p;
b=*(++p);//p+=1;b=*p
//
有关于
*p++,(*p)++,++*p,++(*p),*++p;
的区别
int _tmain(int argc, _TCHAR* argv[])
int a=1;
int *p=&a;
int res=0;
//
一,
*p++
//*p++
,由于单目运算符是右结合律,所以
p
先与
++
结合,即等价于:
//*(p++)
//
由于
p++
在表达式里是先取值,再加
1
,因此等价于:
//res = *p;
//p= p+1;
这里的
1
是
1
个单位长度,这里由于
p
是
int
//
所以,是
4
个字节的长度
//
验证如下:
res = *p++;
printf("res:%d,p:%p,&a:%p\n",res,p,&a);
我们已经学习过数组的定义,并且已经知道,数组名所代表的值就是数组的首地址,一旦定义了数组之后,数组名所代表的值就不能再改变。从指针的角度来看,数组名就是一个常量指针,比如:
int a[10];
那么
a
就是一个常量指针,即:
int *const a
。因此,不能再用其它的值赋值给
a
。因为
a
是常量。
此外,数组名虽然代表了数组的首地址虽然
a
与
&a
值一样,都是数组的首地址,但是,
a
与
&a
的含义并不一样。对于一维数组来说:
int a[10];
&a+1
中的
1
代表的是整个数组的长度
10*sizeof(int);
a+1
中的
1
代表的是一个元素的长度
sizeof(int)
。
&a[0]+1
中的
1
也代表的是一个元素的长度。
对于多维数组来说:
int a[5][10];
a
和
&a
都是数组
a[5][10]
的首地址。那么它们有什么不同呢?实际上,它们代表的类型不同。
a
是
int a[10]
的类型,而
&a
则是
a[5][10]
的类型。大家知道,指针运算中的“
1
”
代表的是指针类型的长度。所以
a+1
和
&a+1
中的
1
代表的长度分别为
a
的类型
a[10]
即
sizeof (int) * 10
和
&a
的类型
a[5][10]
即
sizeof
(int)*10*5
。所以
a
的首地址为
1310392
,那么
a + 1
和
&a + 1
的地址为:
a + 1 = 1310392 + sizeof (int) * 10 = 1310392 + 4 * 10 =
1310432
&a + 1 = 1310392 + sizeof (int) * 10 * 5 = 1310392 + 4 * 10 *
5 = 1310592
更抽象点的说,如果定义一个多维数组
int a[M
1
][M
2
][…][M
n
]
,那么
a + 1 = a
首地址
+M
2
*M
3
*…*M
n
*sizeof (int)
;而
&a + 1 = a
首地址
+ M
1
*M
2
*…*M
n
*sizeof (int)
。
&a[0][0]
:就是数组第一个元素的地址,它的内存字节是
4
个字节。因此
&a[0][0]+1
,此处的
1
代表的长度是
4
个字节。
8.13
常量指针与指针常量
试指出下面的指针的含义与区别:
1
)
const int *a;
2
)
int const *a;
3
)
int * const a;
4
)
const int *const a;
此题考查的是包含
const
关键字声明指针的含义。现分析如下:
1
)
const int *a;
//
指针常量,指针指向的变量不能改变值
2
)
int const *a;
//
指针常量,与
const int *a
等价
3
)
int * const a; //
常量指针,指针本身不能改变值
4) const int *const a;//
常量指针与指针常量
Bjarne
博士在他的《
The C++
Programming Language
》里面给出过一个区别的方法:把一个声明从右向左读。例如:
char *const cp; (
我们把“
*
”
读成 “
pointer to
”
)
cp is a const pointer to char //int* const
指向常量的指针
const char * p;
p is a pointer to const char; //const int*
常指针
char const * p;
同上因为
C
里面没有
const
*
的运算符,所以
const
只能属于前面的类型。
int a[10];//
数组是一个常量指针,等价于
int * const a;a[0],a[1]
int * const a;//
常量指针,指针不能改,但指向的内存能改
example:
int value = 5;
int *const a = &value;
int tmp = 100;
a = &tmp;//error
int const * p;//p is a pointer to const int
const int * const p;//p is a const pointer to int const
8.14
指针与数组关系
现在大家已经明白,数组名,其实是一个常量指针:
int a[10];
a
的类型为:
int * const a;//a
是常量指针
因此在访问数组元素的时候:
a[i],
与
*(a+i)
都可以访问第
i
个元素的值。而
&a[i]
与
a+i
都是第
i
个元素的地址。同样,我们也尅定义一个整数指针
pa
指向数组的首地址:
int *pa=&a[0];
int *pa=a;
因此
pa+i
也是第
i
个元素的地址,而
*(pa+i)
和
pa[i]
引用的也是
a[i]
的值。
利用指针与数组的关系,我们可以动态的分配一段空间,来存放一个班级学生的成绩,这样可以来表示不同班级学生的人数:
int _tmain(int argc, _TCHAR* argv[])
int number = 0;
int sum = 0;
字符指针,既可以指向字符变量,也可以指向字符串(其实就是字符串中首字符的地址)。比如:
char *str=“hello world”;//
这里
str
是一个字符指针,它是
”hello world”
字符串中首字符
’h’
的地址。
因为字符串是以
’\
0’
结尾的,所以可以通过字符指针来遍历字符串:
while(*str!=‘\
0’
)
printf(“%c”, *str);
str++;
字符指针也可以指向某个字符变量,比如:
char ch=‘a’;
char *pch=&ch;
此外,将字符串传给函数做参数,也是通过字符指针完成的:
1
,以字符指针的方式传递字符数值给函数:
void printf_char(char *p)//
字符指针传字符
printf( “%c“,*p);
2
,以字符指针的方式传递字符串给函数:
void printf_str(char *p)//
字符指针传字符串
while(*p)
printf(“%c“,p);
8.16
二级指针
所谓二级指针,就是指向指针的指针,即该指针的值是另外一个一级指针的地址。与此类似,如果一个指针中存放的是二级指针的地址,那么该指针就是三级指针,与此类推。
char c;
char *pch = &c;//pch
为一级指针
char **ppch = &pch;//ppch
为二级指针,存放这一级指针的地址
printf(“%c”, **ppch);
printf(“%p,%p,%p”, pch, ppch, *ppch);
char *p = NULL;//p
是指针,做为实参,初始值为
NULL
get_memory();//
通过该函数,为
p
分配一块内存。如何定义
get_memory
函数和传参
p
?
strcpy_s(p, 100,”hello world”);
printf(“%s\n”, p);
free(p);
p=NULL;
return 0;
因此
get_memory
函数设计如下:
1
,传指针的指针(二级指针):
void get_memory(char **p)
*p=(char *)malloc(100);
调用方式:
get_memory(&p);
2,
传指针的引用:
void get_memory(char *&p)
p=(char *)malloc(100);
调用方式:
g
et_memory(p);
利用指针与函数的知识,分析下面代码的输出:
int i = 0, j = 20, *p1 = &i, *p2 = &j;
void f(int **ptr1, int *ptr2)
int *tmp = ptr2;
**ptr1 *= 10;
*ptr2 *= 10;
ptr2 = *ptr1;
*ptr1 = tmp;
请问调用
f(&p1, p2)
之后
i, j, p1, p2
的值各是什么?
分析与解答:
首先我们画出程序执行过程中调用
f()
函数时堆栈的情况如下图所示:
函数名,就是函数的首地址。如果一个指针变量,存放的是函数的地址,那么就把这个指针叫做函数指针。定义函数指针有
2
中形式:
第一种,首先用
typdef
定义出函数指针的类型,然后,通过函数指针类型来定义函数指针
第二种,直接用函数的签名来定义函数指针
void print_int(int x)
printf("hello, %d\n", x);
typedef void (*F)(int x);//
此处定义了一个函数指针类型
F
int main(void)
int a =100;
void (*f1)(int x);
f1= print_int;//f1
是指针定义出来的函数指针,把函数
print_int
赋值给
f1
f1(a);
f2= print_int; // f2
是通过函数指针类型
F
定义出来的函数指针,把
print_int
赋值给
f2
。
f2(a);
print_int(a);
return 0;
8.17.2
指针函数:即返回指针的函数。比如下面的代码中,我们尝试着调用
get_memory()
获取一个内存,用来存放
“hello world“
这个字符串,那么就可以将
get_memory()
设置成为一个返回指针的函数:
char *get_memory();
int main(void)
char *p = NULL;//p
是指针,做为实参,初始值为
NULL
p=get_memory();//
通过该函数,为
p
分配一块内存。如何定义
get_memory
函数?
strcpy_s(p, 100,”hello world”);
printf(“%s\n”, p);
free(p);
p=NULL;
return 0;
char *get_memory()
return (char *)malloc(100);
注意:指针函数不能返回局部变量的指针(地址),只能返回堆上内存的地址,或者函数参数中的内存地址。因为局部变量存放在栈上,当函数运行结束后,局部变量就被销毁了,这个时候返回一个被销毁的变量的地址,调用者得到的就是一个野指针。
注意下面几个定义的区别:
int f();//
普通的函数
int *fpi();//
指针函数
int (*pfi)();//
函数指针
8.18
数组指针与指针数组
说出下面的指针表示的什么?
int *a[10];
int (*a)[10];
int (*a)(int);
int *a(int);
int (*a[10])(int);
int *a, **a;
char str[];
char *str, **str;
此题经常被用来做为笔试题。笔者毕业当年参加
SYNOPSIS
的面试时便遇到了此题。它综合了指针变量的各种声明形式。下面我们给出各个指针表示的含义:
1
)
int *a[10];
//
指针数组
2
)
int (*a)[10];
//
数组指针
3
)
int (*a)(int);
//
函数指针
int *a(int);
//指针函数,返回指针的函数
4
)
int (*a[10])(int); //
函数指针数组。注意:
*
与
[]
的优先级来判断这组的区别
5
)
int *a, **a;
//
指针和指向指针的指针
6
)
char str[];
//
字符串数组
7
)
char *str, **str;
//
字符指针和指向字符的指针
要掌握这些复杂形式的指针意义并不容易,因为容易混淆。那么有没有特别的要领呢?其实此题的关键是要明白
[]
,
*
,和
()
运算符的优先级:
() > [] > *
。比如
int *a[10]
,由于
[]
的运算级别高于
*
,所以该表达式首先是一个数组。那么它是什么数组呢?由
int *
确定它是个指针数组。又比如
int(*a)[]
,由于
()
高于
[]
,所以它是一个指针。那么它是什么指针呢?由
[]
确定它是个数组指针,即指向数组的指针。
与“指针数组”和“数组指针”类似的有“函数指针”与“指针函数”,“常量指针”与“指针常量”。这些概念都符是偏正关系,所以指针数组其实就是数组,里面存放的是指针;数组指针就是指针,这个指针指向的是数组;函数指针就是指针,这个指针指向的是函数,指针函数就是函数,这个函数返回的是指针;常量指针就是指针,只不过这个指针是常量的,不能再修改值指向别的地方;指针常量,就是指指针本身不是常量指针指向的内存是常量,不能修改。
2
.
sizeof
()计算指针长度。
char *p1 = “Hello, word!”
char p2[] = “Hello, world”
char p3[] = {‘h’, ‘e’, ‘l’,’l’,’o’,’,’, ‘ ‘,
‘w’,’o’,’r’,’l’,’d’}
此题考查的是计算指针与数组的长度。其中,指针的长度(在
32
位机器系统上)为
4
,字符串数组的长度必须包含字符串的结束标志符
’\
0’
,数组的长度为元素个数乘以单个元素大小。因此,该题的答案为:
char *p1 = “Hello, word!”
p1
为字符串指针,所以
sizeof (p1)
= 4
。
char p2[] = “Hello, world”
p2
为字符数组并初始化为
”Hello,
world”
。由于字符串的存储特点,总是以
’\
0’
做为结束标志,因此上面的字符串等价于下面的数组:
char p2[] = {‘h’, ‘e’, ‘l’,’l’,’o’, ‘ ‘, ‘w’,’o’,’r’,’l’,’d’,’\
0’
}
,必须包含字符串的结束标志符
’\
0’
,所以
sizeof (p2) = 13
。
char p3[] = {‘h’, ‘e’, ‘l’,’l’,’o’, ‘ ‘, ‘w’,’o’,’r’,’l’,’d’}
p3
为字符数组,并由
12
个字符初始化,所以
sizeof (p3) = 12
。
注意,
strlen(p)
计算的是字符串中有效的字符数(不含
’\
0’
)。所以
strlen
(
p
)的值为
12
。考察下面拷贝字符串的代码,看看有什么问题没呢?
char *str = “Hello, how are you!”;
char *strbak = (char *)malloc(strlen(str));
if (NULL == strbak)
//
处理内存分配失败,返回错误
strcpy(strbak, str);
显然,由于
strlen()
计算的不是
str
的实际长度(即不包含
’\
0’
字符的计算),所以
strbak
没有结束符
’\
0’
,而在
C
语言中,
’\
0’
是字符串的结束标志,所以是必须加上的。所以上面的代码应该是:
char *str = “Hello, how are you!”;
char *strbak = (char *)malloc(strlen(str)+1);
if (NULL == strbak)
//
内存分配失败,返回错误
strcpy(strbak, str);
既然在这里谈到了
sizeof
,现在我们就把
sizeof
运算在下面做一个系统的总结:
1
)参数为数据类型或者为一般变量。
例如
sizeof(int)
,
sizeof(double)
等等。这种情况要注意的是不同系统或者不同编译器得到的结果可能是不同的。例如
int
类型在
16
位系统中占
2
个字节,在
32
位系统中占
4
个字节。
2
)参数为数组或指针。下面举例说明:
int a[50]; //sizeof(a)=4*50=200;
数组所占的空间大小为
200
字节。
注意数组做函数参数时,在函数体内计算该数组参数则等同于计算指针的长度。
int *a=new int[50];// sizeof(a)=4; a
为一个指针,
sizeof(a)
是求指针的大小,在
32
位系统
//
中,当然是占
4
个字节。
3
)参数为结构或类。
sizeof
应用在类和结构的处理情况是相同的。有两点需要注意,第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。第二、没有成员变量的结构或类的大小为
1
,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。关于更多的结构的
sizeof
大小计算,请参考
1.4
节数据对齐。
4
.计算数组长度
在计算数组长度的时候,我们需要注意数组作为函数的参数,将退化为指针,所以,其长度大小为指针的长度。现在我们来看下面这段代码:
int a[10];
//sizeof (a) = 10*sizeof (int) = 40
;
int a[10];
void func(int a[], int n)
printf(“%d”, sizeof
(a));
//
此时数组退化为指针,所以
sizeof (a) = 4
需要指出的是,数组也是一个指针,但它是常量指针,即
int a[10]
中的
a
可以看做是
int * const a
,所以一旦声明,
a
不能再被改变。
下面来看以下代码中的两个
sizeof
用法有问题吗?
void UpperCase( char str[] ) //
将
str
中的小写字母转换成大写字母
for( size_t i=0;
i<sizeof(str)/sizeof(str[0]); ++i )
'a'<=str[i] && str[i]<='z' )
str[i] -= ('a'-'A' );
char str[] = "aBcDe";
cout << "str
字符长度为
: " <<
sizeof(str)/sizeof(str[0]) << endl;
UpperCase( str );
cout << str << endl;
分析:函数内的
sizeof
有问题。根据语法,
sizeof
如用于数组,只能测出静态数组的大小,无法检测动态分配的或外部数组大小。函数外的
str
是一个静态定义的数组,因此其大小为
6
,函数内的
str
实际只是一个指向字符串的指针,没有任何额外的与数组相关的信息,因此
sizeof
作用于上只将其当指针看,一个指针为
4
个字节,因此返回
4
。
5
.分析下面的代码,试指出数组指针的不同含义。
#include <stdio.h>
int main(void)
int a[5][10];
printf("%d,%d,%d\n", a,
a+1, &a+1);
return 0;
其输出结果为:
1310392,1310432,1310592
。试分析原因。
解答:
a
和
&a
都是数组
a[5][10]
的首地址。那么它们有什么不同呢?实际上,它们代表的类型不同。
a
是
int a[10]
的类型,而
&a
则是
a[5][10]
的类型。大家知道,指针运算中的“
1
”
代表的是指针类型的长度。所以
a+1
和
&a+1
中的
1
代表的长度分别为
a
的类型
a[10]
即
sizeof (int) * 10
和
&a
的类型
a[5][10]
即
sizeof
(int)*10*5
。所以
a
的首地址为
1310392
,那么
a + 1
和
&a + 1
的地址为:
a + 1 = 1310392 + sizeof (int) * 10 = 1310392 + 4 * 10 =
1310432
&a + 1 = 1310392 + sizeof (int) * 10 * 5 = 1310392 + 4 * 10 *
5 = 1310592
更抽象点的说,如果定义一个数组
int a[M
1
][M
2
][…][M
n
]
,那么
a + 1 = a
首地址
+M
2
*M
3
*…*M
n
*sizeof (int)
;而
&a + 1 = a
首地址
+ M
1
*M
2
*…*M
n
*sizeof (int)
。
a的定义类型: int (*a)[10];
&a的定义类型:int (*a)[5][10];
6
.分析下面程序的计算结果。
int i = 0, j = 20, *p1 = &i, *p2 = &j;
void f(int **ptr1, int *ptr2)
1.
int *tmp = ptr2;
2.
**ptr1 *= 10;
3.
*ptr2 *= 10;
4.
ptr2 = *ptr1;
5.
*ptr1 = tmp;
请问调用
f(&p1, p2)
之后
i, j, p1, p2
的值各是什么?
分析与解答:
首先我们画出程序执行过程中调用
f()
函数时堆栈的情况如图所示:
图 程序执行堆栈情况
前面已经提到,指针加减法运算,后面的数字表示指针指向的数据类型的大小的倍数。比如
&a+1
,其中的
1
就表示指针向前移动
1*sizeof(&a)
那么多的字节。而
&a
表示整个数组,所以
ptr1 = (int
*)(&a+1)
,
ptr1
指到了数组的末尾位置(见图)。因为
ptr1[-1]
即为
*((int*)ptr1-1)
,即指针
ptr1
向低地址移动
sizeof(int)
个字节,即向后移动
4
个字节,正好指到
a[4]
的位置,所以
ptr1[-1]
为
5
。对于语句
*ptr2 =(int *)((int)a+1)
,在这里,我们已经将指针
a
强制转换成了整型,
a+1
不是指针运算了。(
int *)((int)a+1)
指向了首地址的下一个字节。我们把数组的存储结构按照
1
字节为单位画一下
(
注意所在的平台为低位优先,所以低位字节存储在低地址),现在内存数据的情况如图所示:
*
s.p[1]
即为
p[1]
(因为
s.p
为
p
,运算符
”.”
与
”[]”
同级,结合率为从左向右,所以
s.p[1] *
等同于
(s.p)[1])
,而
p[1]
为
s.p
成员,所以如图,
s.p=1
;此时
s.p
不指向
s.i
了,而是指向
*
了
1
s.p[1]
= 1;
int &refi
=
i
;
// refi
指向一个
i
的引用
引用必须初始化,而指针没有这个要求(尽管没有初始化的指针很危险);引用总是指向它最初获得的那个对象,而指针可以被重新赋值。
C++
中向函数中传递指针和传递指针的引用的区别:
如果是传递指针,那么会先复制该指针,在函数内部使用的是复制后的指针,这个指针与原来的指针指向相同的地址,如果在函数内部将复制后的指针指向了另外的新的对象,那么不会影响原有的指针。所以要想在函数中改变指针,必须传递指针的指针或者指针的引用。
使用对象指针作为函数参数要经使用对象作函数参数更普遍一些。因为使用对象指针作函数参数有如下两点好处:
1
)实现传址调用。可在被调用函数中改变调用函数的参数对象的值,实现函数之间的信息传递。
2
)使用对象指针实参仅将对象的地址值传给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时空开销。
使用对象引用作函数参数要比使用对象指针作函数更普遍,这是因为使用对象引用作函数参数具有用对象指针作函数参数的优点,而用对象引用作函数参数将更简单,更直接。
11
.指针的引用修改指针:
在
C
语言中经常使用指针,指针的指针,指针的引用做函数的参数。那么它们的区别是什么呢?
1
)指针引用做参数:
void func
(
MyClass
*&pBuildingElement
);
//
指针的引用,能修改指针
2
)指针做参数:
void func
(
MyClass
*pBuildingElement
);
//
指针,不能修改指针
3
)指针的指针做参数:
void func
(
MyClass
**pBuildingElement
);
//
指针的指针,能修改指针
下面是
3
个实际函数调用的例子:
现在,
rpObj
是返回的引用列表中的对象的指针,所以当改变
rpObj
时,也会改变列表中位置
pos
处的对象地址,也就是说替代了列表中的这个对象。这就是为什么
CObList
会有两个
GetAt
函数的缘故。一个可以修改指针的值,另一个则不能。
事实上,函数可以返回任何类型的引用,不仅仅是指针的引用。比如下面的调用:
int& abc(int &x)
;
函数中
&
是什么作用?实际上它的作用就是对变量的引用。
下面举个实际例子:
int
x = 0;
int
&a(int &i)//
传入实参的引用
i = - 1;
return x ; //
返回
x
的引用
void main(void)
int j=10;
a(j)=100;
总之,返回引用就是返回一个变量的地址里面的内容,就是真正地返回这个变量本身,它可以用作左值,以改变返回的引用的变量的值。在上面的代码中,函数传入的是实参的引用,返回的是
x
的引用。因此在
main()
函数调用了
a()
函数之后,
j
和
x
的值都会发生改变。返回一个类型的引用,在操作符重载赋值运算符“
=
”中,这种方式是经常用到的。
8.19
指针做实参,如何修改指针
指针做实参,如果想修改指针的值,必须传指针的指针或者指针的引用。现在
来分析下面的
C
代码:
void GetMemory(char *p)
p = (char *)malloc(100);
void Test(void)
char *str = NULL;
GetMemory(str);
strcpy_s(str, 100,"hello world");
printf(str);
free(str);
str=NULL;
请问运行
Test
函数会有什么样的结果?
分析:上面的代码试图使用指针作为参数,分配动态内存。该代码会存在两个问题:
内存泄漏。
首先,通过指针作为参数无法成功申请一块动态分配的内存。这是因为,
GetMemory()
函数获得的是实参指针变量的一个拷贝。因此,它只是将新分配的内存赋给了形参(即实参指针的拷贝)。而实参并没有获得这块内存。在
Test()
函数中,发现并没有释放
str
指向内存的语句。但这不是内存泄露的根本原因。即使在程序后面加上一句:
free(str);
内存依然会泄漏。这是因为,
str
根本没有获得这块内存,而是由形参获得了。而形参是一个栈上的变量。在函数执行之后就已经被系统收回了。这是造成了内存泄漏的根本原因。
要想成功获得分配的内存,可以采用下面的两种方法:
char*
GetMemory(char *p)
p = (char
*)malloc(100);
return p;
上面的代码直接返回新分配的内存。由于内存是在堆上而不是在栈上分配的,所以函数返回后不存在任何问题。
void GetMemory(char **p)
//传指针的指针
*p = (char *)malloc(100);
NULL
指针引用导致程序崩溃。
由于
str
并没有获得这块内存,那么
str
的值依然为
NULL
,所以
strcpy()
函数访问了一个
NULL
指针,直接导致程序崩溃。
8.20
函数不要返回局部变量的指针或引用
函数一定不要返回局部变量的指针或者引用。如下面的代码:
char *func(void)
char c = ‘A’;
char *p = &c;
return p;
在
func
函数中,我们将局部变量
c
的地址当做一个指针返回,那么在
main
函数中,我们是不能够再次使用或者访问这个指针所指的内存的。因为局部变量
c
的生命周期只存在于函数
func
运行期间。一旦
func
结束运行之后,那么
c
就被销毁了,
c
的地址就是一个无效的内存地址,因此,当在
main
函数中执行了:
pc=func() ;
pc
指向的内存是无效的内存,因此
pc
是一个野指针,试图访问一个野指针,其后果是未定义的,程序有可能崩溃,有可能访问的是垃圾值。
int main(void)
char * pc = NULL;
= func();
printf(“%c”, *pc);
return 0 ;
8.21
指针使用注意事项
分析完指针的一些典型问题之后,现在将使用指针的一些需要注意的问题总结如下:
1
)指针在声明的时候最好初始化。
指针变量没有被初始化,任何指针变量刚被创建时不会自动成为
NULL
指针,它的缺省值是随机的,它会随机的指向任何一个地址(即野指针),访问野指针会造成不可预知的后果。所以,指针变量在创建的同时应当被初始化,要么将指针设置为
NULL
,要么让它指向合法的内存。
2
)指针的加减运算移动的是指针所指类型大小。
前面已经提到,指针的加法运算
p = p + n
中,
p
向前移动的位置不是
n
个字节,而是
n * sizeof(*p)
个字节,指针的减法运算与此类似。
3
)当用
malloc
或
new
为指针分配内存时应该判断内存分配是否成功,并对新分配的内存进行初始化。
用
malloc
或
new
分配内存,应该判断内存是否分配成功。如果失败,会返回
NULL
,那么就要防止使用
NULL
指针。在分配成功时,会返回内存的地址。这个时候内存是一段未被初始化的空间,里面存在的可能是垃圾数据。因此,需要用
memset
等对该段内存进行初始化。
此外,应该防止试图使用指针作为参数,去分配一块动态内存。如果非要这么做,那么请传递指针的指针或指针的引用。
4
)如果指针指向的是一块动态分配的内存,那么指针在使用完后需要释放内存,做到谁分配谁释放的原则,防止内存泄漏。
此点笔者会在第
9
章内存分配中进行专门的介绍,详情请参考
内存泄漏预防与检测
。
5
)指针在指向的动态内存释放后应该重新置为
NULL
,防止野指针。
野指针不是
NULL
指针,是指向“垃圾”内存的指针。野指针是很危险的,它可能会造成不该访问的数据或不该改的数据被访问或者篡改。在应用
free
或者
delete
释放了指针指向的内存之后,应该将指针重新初始化为
NULL
。这样可以防止野指针。
野指针不是
NULL
指针,是指向“垃圾”内存的指针。野指针是很危险的,它可能会造成不该访问的数据或不该改的数据被访问或者篡改。在应用
free
或者
delete
释放了指针指向的内存之后,应该将指针重新初始化为
NULL
。这样可以防止野指针。
分析下面的程序:
void GetMemory(char **p,int num)
*p=(char *)malloc(num);
int main(void)
char *str=NULL;
看文字不过瘾?点击我,进入周哥教IT视频教学
麦洛科菲长期致力于IT安全技术的推广与普及,我们更专业!我们的学员已经广泛就职于BAT360等各大IT互联网公司。详情请参考我们的
业界反馈
,
《周哥教IT.C语言深学活用》视频
。
我们的微信公众号,敬请关注: