指针是C语言中的精华,也是一把双刃剑,关系到安全和效率。
1 系统内存布局
2 存储变量的内存地址
3 指针定义:变量,地址,类型(宽度)
4 指针声明
5 &与*运算符
6 定义指针与解引用
7 指针初始化
8 指针指向类型长度计算:sizeof(*p)
9 void* 类型指针
10 指针应用:判断系统大小端
11 指针加减运算
12 常量指针与指针常量
13 数组名是一const指针
14 指针与数组关系
15 字符指针
16 二级指针
17 函数指针与指针函数
18 数组指针与指针数组
19 函数不要返回局部变量的指针
20 指针与引用
21 指针引用做函数参数
22 返回指针和指针引用
23 指针使用注意事项
1 系统内存布局
指针其实就是一个变量,和其他类型的变量一样,在32位机器上,它占用四字节(64位上占8个字节),它与其他变量的不同就在于它的值是一个内存地址,指向内存的另外一个地方。
以X86的32位系统为例,如下图所示,系统的内存虚拟地址范围为4GB(0x0-0xFFFFFFFF)。 其中低2GB主要为应用程序使用(Ring3级别),而高2GB为系统内核使用(Ring0级别):
需要注意的是,程序在执行时,传递给CPU的地址是逻辑地址,它由两部分组成,一部分是段选择符(比如cs和ds等段寄存器的值), 另一部分为有效地址(即偏移量,比如eip寄存器的值)。逻辑地址必须经过映射转换变为线性地址, 线性地址再经过一次映射转为物理地址,才能访问真正的物理内存。
2 存储变量的内存地址
变量是存放在内存中的,比如下图中的变量i和a,分别对应一块内存单元首地址的一个命名,变量i和a的地址,可以用&取址运算符获得。
而对于指针p来说,它本身也是一个变量,存放在内存中,只不过它的值是一个内存地址,这个内存地址,可以是其它变量的地址。
如:
int i=10;
int *p = &i; // i的地址赋值给p,也就是指针p指向变量i:p→i
int a=100;
如上图所示,内存地址空间是一个线性空间。
数据类型除了表示可以执行的操作(可以使用的运算符)、编码与解码格式以外,还用来表示需要内存空间的长度,如上面的int就是使用4个字节的内存空间,变量a是这个内存空间地址的命名,在C/C++,变量名做右值取得的是其内存空间的二进制位组成的值按数据类型编码规则解析出来的值(变量名做左值可以更新其内存空间的值),其地址可以通过“&"加变量解析出来。int*p表示p是指向一个int大小的内存空间。
内存的地址可以分为有效地址,即这个所对应的内存是可访问的;还有无效地址,访问无效地址,会导致程序崩溃,比如NULL地址就是一个无效地址。
3 指针定义:变量、地址、类型(宽度)
指针其实就是一个变量,和其他类型的变量一样。它与其他变量的不同就在于它的值是一个内存地址,指向内存的某一个地方。即指针是一种存放另一个变量的地址的变量。
指针含义可以分为3个方面来理解:
I 它是一个变量,所以也占用一定的内存空间(在X86上占用4个字节,X64上占用8个字节)
II 它的值是一个内存地址。这个地址可以是其它变量的地址。
III 它的地址指向的内存空间具有确定的长度。这是指针与地址的本质区别。如果只告诉你一个内存地址,你不会知道从这个地址开始的内存有多长。但如果告诉你一个指针,你会明确的知道从这个内存地址开始的内存有多长。因为指针都是有类型的。知道了指针的类型,就确定了所指向的内存地址对应的长度。
4 指针声明
指针声明如同变量声明一样,只是多了一个“*”:
char *pch;
int *pi;
float *pf;
double *pd;
当然需要类型,表示这个地址可以解析的内存空间长度。
其右值只能是一个有效的内存地址:
char *pch = "abc"; // "abc"存储在静态区,对应一个内存地址
int i = 0;
int *p = &i; // “&”可以取得变量的地址
5 &与*运算符
因为变量是内存地址的一个可以理解的命名标识,这个标识的直接使用是值的访问与更新。通过这个标识我们可以取得其地址,C/C++中,可以使用“&”(取址运算符)来获取某个变量的地址,比如:
int i = 1;
int *p = &i; // *这里是指针声明,&这里是取址
*p中,p必须是有效的地址,否则会引发程序崩溃。比如:
int *p = NULL;
*p = 0;//此时,p无NULL地址,会引发程序异常
6 指针声明与解引用
在编程语言中,许多情况下一个符号会有多种用途,取决于上下文,如“*”即可用于定义指针,又可用于解引用:
int i = 1;
int *p = &i; //p is reference of i, 这里把p当做是对i的一个引用(reference)
*p += 1;
// *p is dereference for i ,*p equals i,*p代表的就是i
// 把p所指向的地址的内存(i)的值加1
“*”既是指针声明符,又是解引用(dereference)运算符,与&运算符互为逆运算。
一个小技巧区分“*”做为指针声明与解引用的上下文:
“*“写在数据类型之后时表示声明指针变量,“*“前没有数据类型时表示解引用。
7 指针初始化
当定义了一个指针,有多种方法进行初始化(赋值):
① 声明与赋值分开进行
int i, *p;//声明了一个整型变量i,一个指针p
p= &i;
② 声明与赋值分开进行并先赋NULL值
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;
用于指针初始化的右值的取值空间为:0x0000FFFF-ox7FFF0000,这个值不能由程序员直接给出,因为操作系统统一管理各程序的内存空间,程序中使用的是虚拟内存并通过虚拟内存地址来访问数据和代码的,操作系统再将虚拟内存地址映射成为实际的物理内存的,所以合法的地址只能是通过定义变量或由此产生的偏移来获取,可直接通过malloc()或new申请动态内存来获取。
8 指针指向类型长度计算:sizeof(*p)
指针的长度(在32位机器系统上)为4。
double x=3.14;
double* p = &x;
int n = sizeof(p); // 4
当使用sizeof()和数组名(常量指针)求数组的长度时,求的是这个数组整个空间的长度,等于元素个数乘以单个元素大小,字符串数组的长度必须包含字符串的结束标志符’\0’。(关于数组名做函数参数时退化为指针的细节,见后续)
char *p1 = “Hello, word!” // 字面量 “Hello, word!”存储在静态区.rdata段
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 *strbak = (char *)malloc(strlen(str)+1);
既然在这里谈到了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大小计算,请考虑数据对齐。
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*可以让数据类型有一定的泛型特征,因为C/C++是强类型语言,如标准库中的很多函数便以void*为参数:
void *memcpy(void *dst, void *src, size_t len);
void qsort (void* base, size_t num, size_t size,
int (*compar)(const void*,const void*));
10 指针应用:判断系统大小端
对于int x=0x1; 在小端系统中,低位存放整数的低位,因此低地址的第一个字节的值为01。而大端系统中,低位存放整数的高位,因此低地址的第一个字节为00。如果把这个内存地址所在的第一个字节取出来,就可以区别系统是小端还是大端了。
/*return value :0—big-endian ;1—little-endian*/
int get_endian()
{
int x=0x1;
char *p=(char*)&x; // 取x的地址并将这个地址转换为char*类型,这样就取出了一个字节的地址
return *p ;
}
int main(void)
{
printf(”The platform %s \n ”,get_endian() ? ”is little-endian”:”is big-endian”);
return 0;
}
11 指针加减运算与移动
指针运算一般只有算术和有限的比较运算。通过一个指针与一个整数的加减,形成的内存地址变化或偏移可以视为指针的移动。
指针移动,最常用的就是“++”运算符了,下述表达式注意与解引用区分:
*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
如以下利用指针移动和两个指针的差值求字符串长度的函数:
int len(char* str)
{
char* pMove=str;
while(*pMove!='\0')
pMove++;
return pMove-str;
}
对于链表,其移动稍有区别:
struct node //定义结点结构类型
{
char data; //用于存放字符数据
node *next; //用于指向下一个结点(后继结点)
};
void Showlist(node *head)
{
node *pRead=head; //访问指针一开始指向表头
cout<<"链表中的数据为:" <<endl;
while (pRead!=NULL) //当访问指针存在时(即没有达到表尾之后)
{
cout<<pRead->data; //输出当前访问结点的数据
pRead=pRead->next; //访问指针向后移动(指针偏移)
}
cout<<endl;
}
12 常量指针与指针常量
当const修饰指针声明时,根据const的位置不同,const可以修饰指针本身,也可以修饰指针指向的内容:
1)const int *a; // 指针常量,指针指向的变量不能改变值
2)int const *a; // 指针常量,与const int *a等价
3)int * const a; // 常量指针,指针本身不能改变值
4)const int * const a; // 两者均为常量
小技巧:以“*”为分界,const靠近哪部分,哪部分为常量
13 数组名是一const指针
数组名所代表的值就是数组的首地址,一旦定义了数组之后,数组名所代表的值就不能再改变。从指针的角度来看,数组名就是一个常量指针,比如:
int a[10];
那么a就是一个常量指针,即:int *const a。因此,不能再用其它的值赋值给a。因为a是常量。
在计算数组长度的时候,我们需要注意数组作为函数的参数,将退化为指针,所以,其长度大小为指针的长度。现在我们来看下面这段代码:
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
}
下面来看以下有问题的代码:
void UpperCase( char str[] ) // 将 str 中的小写字母转换成大写字母
{
for( size_t i=0; i<sizeof(str)/sizeof(str[0]); ++i )
if( 'a'<=str[i] && str[i]<='z' )
str[i] -= ('a'-'A' ); // str[i]^=32;
}
char str[] = "aBcDe";
cout << "str字符长度为: " << sizeof(str)/sizeof(str[0]) << endl;
UpperCase( str );
cout << str << endl;
分析:函数内的sizeof有问题。根据语法,sizeof如用于数组,只能测出静态数组的大小,无法检测动态分配的或外部数组大小。函数外的str是一个静态定义的数组,因此其大小为6,函数内的str实际只是一个指向字符串的指针,没有任何额外的与数组相关的信息,因此sizeof作用于上只将其当指针看,一个指针为4个字节,因此返回4。
数组名虽然代表了数组的首地址,虽然a与&a的值一样,都是数组的首地址,但是,a与&a在特定的上下文中的含义并不一样。对于一维数组来说:
int a[10];
&a+1中的1代表的是整个数组的长度10*sizeof(int);
a+1中的1代表的是一个元素的长度sizeof(int)。
&a[0]+1中的1也代表的是一个元素的长度。
再看下面实例:
#include <stdio.h>
int main(void)
{
int a[5] = {1,2,3,4,5};
int *ptr1 = (int *)(&a+1); // &a表示含有长度信息的数组变量,+1的1相当于数组长度
int *ptr2 = (int *)((int)a+1); // 首地址a的下一个字节的地址
// 数组按小端存储是这样的:10002000(1个数字表示一个字节)
printf("%x\n%x\n",ptr1[-1],*ptr2);// 0002的十六进制就是02000000
printf("%x\n%x\n",a,ptr2);
getchar();
return 0;
}
/*output:
5
2000000
12ff34
12ff35
*/
前面已经提到,指针加减法运算,后面的数字表示指针指向的数据类型的大小的倍数。比如&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)指向了首地址的下一个字节,如上图所示。
所以,*ptr2所代表的整数(四个字节,小端存储),我们从上图可以看出是:2000000。
对于多维数组来说:
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。
看下面实例:
#include <stdio.h>
int main(void)
{
int a[5][10];
printf("%d\n", int(a+1)-int(a)); // 40
printf("%d\n", int(&a+1)-int(a));// 200
getchar();
return 0;
}
大家知道,指针运算中的“1”代表的是指针类型的长度。所以a+1和&a+1中的1代表的长度分别为a的类型a[10],即sizeof (int) * 10 。
&a的类型a[5][10]即sizeof (int)*10*5。
更抽象点的说,如果定义一个数组int a[M1][M2][…][Mn],那么a + 1 = a首地址+M2*M3*…*Mn *sizeof (int);而&a + 1 = a首地址 + M1*M2*…*Mn*sizeof (int)。
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]的值。
15 字符指针
字符指针的定义是:
char *p;
字符指针,既可以指向字符变量,也可以指向字符串(其实就是字符串中首字符的地址)。比如:
char *str=“hello world”;// 这里str是一个字符指针,它是”hello world”字符串中首字符’h’的地址。
因为字符串是以’\0’结尾的,所以可以通过字符指针来遍历字符串:
while(*str!=‘\0’)
{
printf(“%c”, *str);
str++;
}
字符指针也可以指向某个字符变量,比如:
char ch=‘a’;
char *pch=&ch;
C的字符串用一个以'\0'结尾的字符数组来表示。注意以下初始化方式的细微区别:
#include <stdio.h>
char* func1()
{
char arr[] = "abc"; // "abc"等同于{'a','b','c','\0'}
return arr; // error,arr是一局部变量地址
}
char* func2()
{
char* arr2 = "abcd"; // 存储于"abcd"静态区.rdata段
return arr2; // ok,arr2是一静态区.rdata段的地址
}
int main(void)
{
printf("%s\n",func1()); // 随机值
printf("%s\n",func2());
getchar();
return 0;
}
/*output:
?@
abcd
*/
16 二级指针
所谓二级指针,就是指向指针的指针,即该指针的值是另外一个一级指针的地址。与此类似,如果一个指针中存放的是二级指针的地址,那么该指针就是三级指针,与此类推。
char c;
char *pch = &c; //pch为一级指针
char **ppch = &pch; //ppch为二级指针,存放这一级指针的地址
printf(“%c”, **ppch);
printf(“%p,%p,%p”, pch, ppch, *ppch);
如上图所示,pch是一级指针,存放着变量c的地址;ppch是二级指针,存放这一级指针pch的地址。只要画出了上面的关系图,那么一次*运算,就是向右移动一次,两次*运算,就是往右移动两次,即*pch即为c,*ppch为pch,**ppch即为c。
17 函数指针与指针函数
函数指针是指指向一个函数的指针,指针函数是指返回一个指针的函数。
17.1 函数指针
函数名,就是函数的首地址。如果一个指针变量,存放的是函数的地址,那么就把这个指针叫做函数指针。定义函数指针有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);
F f2= print_int; // f2是通过函数指针类型F定义出来的函数指针,把print_int赋值给f2。
f2(a);
print_int(a);
return 0;
}
声明函数指针,通常的做法是,先声明一个函数,然后将函数名改为函数指针名,再加一个“*”号,用括号括起来即可。
函数指针通常用做函数参数:
#include <iostream>
using namespace std;
int ascend(int a,int b)
{
return a>b;
}
int descend(int a,int b)
{
return a<b;
}
typedef int (*pfunc)(int,int);
void sort(int arr[], int size,pfunc comp)
{
for(int i=0;i<size;i++)
for(int j=0;j<size-i-1;j++)
if(comp(arr[j],arr[j+1]))
{
int t=arr[j];
arr[j]=arr[j+1];
arr[j+1]=t;
}
}
void printArr(int arr[],int size)
{
for(int i=0;i<size;i++)
cout<<arr[i]<<" ";
cout<<endl;
}
int main()
{
int arr[]={3,9,9,2,8,5,3,4};
int size = sizeof(arr)/sizeof(arr[0]);
sort(arr,size,ascend);
printArr(arr,size);
sort(arr,size,descend);
printArr(arr,size);
cin.get();
return 0;
}
/*output:
2 3 3 4 5 8 9 9
9 9 8 5 4 3 3 2
*/
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);
}
// 上面代码要注意malloc()与free()匹配的问题,因为分散在不同的函数中
注意:指针函数不能返回局部变量的指针(地址),只能返回堆上内存的地址,或者函数参数中的内存地址以及全局变量或静态变量的地址。因为局部变量存放在栈上,当函数运行结束后,局部变量就被销毁了,这个时候返回一个被销毁的变量的地址,调用者得到的就是一个野指针。
18 指针数组与数组指针
与“指针数组”和“数组指针”类似的有“函数指针”与“指针函数”,“常量指针”与“指针常量”。这些概念都符是偏正关系,所以指针数组其实就是数组,里面存放的是指针;数组指针就是指针,这个指针指向的是数组;
函数指针就是指针,这个指针指向的是函数,指针函数就是函数,这个函数返回的是指针;常量指针就是指针,只不过这个指针是常量的,不能再修改值指向别的地方;指针常量,就是指指针本身不是常量指针指向的内存是常量,不能修改。
int *a[10]; //指针数组
int (*a)[10]; //数组指针
int (*a)(int); //函数指针
int *a(int); //指针函数,返回指针的函数
int (*a[10])(int); //函数指针数组。注意:*与[]的优先级来判断这组的区别
理解上述声明的含义,关键是要明白[],*,和()(这里的()不是函数声明的90)运算符的优先级:() > [] > *。比如int *a[10],由于[]的运算级别高于*。掌握了这一点,再按下面的思路去分析就行了:
I 找括号(函数参数的括号通常写在最后,除外),做为核心,其它部分是修饰;如char* (*pf) (int i); 核心是指针,其它是修饰,所以是函数指针。
II 优先级高的是核心,其它是修饰,如char* arr[12]; 核心是数组,指针修饰数组,所以是指针数组。
所以现在来分析int (*a[10])(int);就简单了, 核心是*a[3],然后是[3],是一个数组,一个指针数组,一个函数指针数组。
19 函数不要返回局部变量的指针
函数一定不要返回局部变量的指针或者引用。如下面的代码:
char *func(void)
{
char c = ‘A’;
char *p = &c;
return p;
}
char &func(void)
{
char c='A';
return c;
}
int main(void)
{
char * pc = NULL;
pc = func();
printf(“%c”, *pc);
return 0 ;
}
在func函数中,我们将局部变量c的地址当做一个指针返回,那么在main函数中,我们是不能够再次使用或者访问这个指针所指的内存的。因为局部变量c的生命周期只存在于函数func运行期间。一旦func结束运行之后,那么c就被销毁了,c的地址就是一个无效的内存地址,因此,当在main函数中执行了:
pc=func() ;
pc指向的内存是无效的内存,因此pc是一个野指针,试图访问一个野指针,其后果是未定义的,程序有可能崩溃,有可能访问的是垃圾值。
20 指针与引用
引用是一种没有指针语法的指针,与指针一样,引用提供对对象的间接访问。引用为所指对象的一个别名(alisas)。如下面的例子:
#include <stdio.h>
int main(void)
{
int a[5][10];
printf("%d\n", int(a+1)-int(a)); // 40
printf("%d\n", int(&a+1)-int(a));// 200
getchar();
return 0;
}
引用必须初始化,而指针没有这个要求(尽管没有初始化的指针很危险);引用总是指向它最初获得的那个对象,而指针可以被重新赋值。
引用可以理解为由编译器实现了自动解引用的const指针。
如果是主调函数给被调函数传递指针,那么会先复制该指针,在函数内部使用的是复制后的指针,这个指针与原来的指针指向相同的地址,如果在函数内部将复制后的指针指向了另外的新的对象,那么不会影响原有的指针。所以要想在函数中改变指针,必须传递指针的指针或者指针的引用。
使用对象指针作为函数参数要比使用对象作函数参数更普遍一些。因为使用对象指针作函数参数有如下两点好处:
1)实现传址调用。可在被调用函数中改变调用函数的参数对象的值,实现函数之间的信息传递。
2)使用对象指针实参仅将对象的地址值传给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时空开销。
使用对象引用作函数参数要比使用对象指针作函数参数更普遍,这是因为使用对象引用作函数参数具有用对象指针作函数参数的优点,而用对象引用作函数参数将更简单,更直接(无须在函数体内解引用即是对主调函数实参的操作)。
21 指针引用做函数参数
在C语言中经常使用指针,指针的指针,指针的引用做函数的参数。那么它们的区别是什么呢?
1)指针做参数:
void func( MyClass *pBuildingElement );// 指针,不能修改指针本身
通常要求实参是一个变量的地址,函数体内通过操作*pBuildingElement来改变pBuildingElement指向的值。而如果实参是一个指针,函数体内操作pBuildingElement,通常不是函数设计的初衷,没有意义。
void GetMemory(char *p, int num) // 指针做参数,本意是改变指针指向的内容
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL,是因为p=str, 对p的操作影响不到str
// 被调函数体内的*p才能影响到主调函数str指向的内容;
strcpy(str, "hello"); // 运行错误
}
2)指针的指针做参数:
void func( MyClass **pBuildingElement );//指针的指针,能修改指针
通常在函数体内操作的是*pBuildingElement
void GetMemory2(char **p, int num) // p解引用后*p是一个指针,
{
*p = (char *)malloc(sizeof(char) * num); // 可以被返回地址的函数赋值
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // p = &str;
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
3)指针引用做参数:
void func(MyClass *&pBuildingElement ); // 指针的引用,能修改指针
pBuildingElemen是MyClass*的别名,既是传址,又因为是引用(实现了自动解引用),函数体内pBuildingElemen与MyClass*两者的运算是一致的。
void GetMemory2(char *&p, int num) //p引用一个指针char*,
{
p = (char *)malloc(sizeof(char) * num); // 可以被返回地址的函数赋值
}
void Test2(void)
{
char *str = NULL;
GetMemory2(str, 100); // 注意参数是str,而不是&str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
对于一个返回动态内存的函数,另外一种方式就是利用指针函数返回。
22 返回指针和指针引用
对于引用,用做参数时,结合了指针和变量的性质,实参和形参结合时,是传址的特性,引用用在函数体时,无需解引用即是对主调函数的实参的操作。
看下面实例:
#include <stdio.h>
int g = 0;
int *pg = &g;
int &func(int &i) // 引用做为参数和返回值
{
i = - 1;
return g ;
}
int* &func2(int* &i) // 指针引用做为参数和返回值
{
*i = - 11;
return i ;
}
void main(void)
{
int j=10;
func(j)=100;
printf("%d\n",j); // -1
printf("%d\n",g); // 100
int* p = func2(pg);
printf("%d\n",g); // -11
printf("%d\n",*p); // -11
*func2(pg)=22;
printf("%d\n",g); // 22
getchar();
}
返回指针和返回引用都可以做为左值,但返回引用做左值时操作更直观。
总结一下指针、引用用做函数参数和返回值:
首先要理解主调函数和被调函数的关系。如果想保持两者的独立性,当然是用传值的方式进行。如果想让被调函数能够修改主调函数的变量,就使用传址,指针传值因为需要在函数体内解引用,操作不方便,C++引入了引用的语法机制,被调函数既能修改主调函数的实参,在函数体中有无须解引用。
另外可以从抽取函数的角度去理解。如果是引用传址,相对于直接抽取一部分代码封装为函数,实参与形参的相互影响仍在。而指针传址呢?需要在函数体内改写成解引用的形式。如果是传值呢,抽取的是相互没有关联性的代码(一般是一个值的计算,仍返回一个值)。
23 指针使用注意事项
C语言中最复杂最容易出错的要数指针了。指针让一些初级程序员望而却步,而一些新的开发语言(如Java,C#)干脆就放弃了指针。
大家已经知道,C语言最适合于底层的开发,一个重要的原因就是因为它支持指针,能够直接访问内存和操作底层的数据,可以通过指针直接动态分配与释放内存:
// 下面是用typedef定义一个新结构最常用的定义形式
// 在微软的面试中,在考查你某个算法前,一般会让你先定义一个与算法相关的结构。
// 比如链表排序的时候,让你定义一个链表的结构。
typedef struct _node
{
int 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,以防止野指针。因为指针所指向的内存虽然释放了,但是指针依然指向某一内存区域。
指针使用注意事项总结:
1)指针在声明的时候最好初始化
指针变量没有被初始化,任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会随机的指向任何一个地址(即野指针),访问野指针会造成不可预知的后果。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
2)指针的加减运算移动的是指针所指类型大小
前面已经提到,指针的加法运算p = p + n中,p向前移动的位置不是n个字节,而是n * sizeof(*p)个字节,指针的减法运算与此类似。
3)当用malloc或new为指针分配内存时应该判断内存分配是否成功,并对新分配的内存进行初始化。
用malloc或new分配内存,应该判断内存是否分配成功。如果失败,会返回NULL,那么就要防止使用NULL指针。在分配成功时,会返回内存的地址。这个时候内存是一段未被初始化的空间,里面存在的可能是垃圾数据。因此,需要用memset()等对该段内存进行初始化或直接使用calloc()。
此外,应该防止试图使用指针作为参数,去分配一块动态内存。如果非要这么做,那么请传递指针的指针或指针的引用。
4)如果指针指向的是一块动态分配的内存,那么指针在使用完后需要释放内存,做到谁分配谁释放的原则,防止内存泄漏。
5)指针在指向的动态内存释放后应该重新置为NULL,防止野指针。
野指针不是NULL指针,是指向“垃圾”内存的指针。野指针是很危险的,它可能会造成不该访问的数据或不该改的数据被访问或者篡改。在应用free或者delete释放了指针指向的内存之后,应该将指针重新初始化为NULL。这样可以防止野指针。
分析下面的程序:
void GetMemory(char **p,int num)
{
*p=(char *)malloc(num);
}
int main(void)
{
char *str=NULL;
GetMemory(&str,100);
strcpy(str,"hello");
free(str);
if(str!=NULL)
{
strcpy(str,"world");
}
printf("\n str is %s",str);
getchar();
}
分析:上面的代码经常出现在各大外企的笔试题目里,它通过指针的指针分配了一段内存,然后将”hello”拷贝到该内存。使用完后再释放掉。到此为止,代码没有任何问题。但是,在释放之后,程序又试图去使用str指针。那么这里就存在问题了。由于str没有被重新置为NULL,它的值依然指向了该内存。因此后面的程序依然能够打印出”world” 字符串。
6)指针操作不要超出变量的作用范围,防止野指针。
分析下面的代码:
har *func()
{
char c = ‘A’;
char *p = &c;
return p;
}
void main(void)
{
char * pc = NULL;
p = func();
printf(“%c”, *p);
}
在上面的代码中,func()函数试图返回一个指向局部变量c的指针。然而局部变量的生命期为func()函数执行期,即变量c分配在栈上,func()函数执行完后,c就不存在了。返回的指针就是一个无效的野指针。因此,打印*p时,可能会出现任何一个不可确定的字符。
7) 对于复杂指针的使用,如果做不到“谁分配,谁释放”,那么可以使用引用计数来管理这块内存的使用。 引用计数方式来管理内存,即在类中增加一个引用计数,跟踪指针的使用情况。当计数为0了,就可以释放指针了。 此种方法适合于通过一个指针申请内存之后,会经过程序各种复杂引用的情况。
下面是一个实际例子:
class CXData
{
public:
CXData()
{
m_dwRefNum = 1; //引用计数赋初值
}
ULONG AddRef() //增加引用
{
ULONG num = InterlockedIncrement(&m_dwRefNum);
return num;
}
ULONG Release() //减少引用
{
ULONG num = InterlockedDecrement(&m_dwRefNum);
if(num == 0) //当计数为0了,就释放内存
{
delete this;
}
return num;
}
private: ULONG m_dwRefNum; //引用计数
}
void test()
{
CXData *pXdata = new CXData;
pXdata->AddRef(); //使用前增加计数
pXdata->Release(); //使用后减少计数,如果计数为零,则释放内存
}
以上实例的目的就是试图封装裸指针以达到指针安全的目的,相当于C++STL的智能指针shared_ptr的雏形。
-End-
本文暂时没有评论,来添加一个吧(●'◡'●)