C/C++入门基础---指针(2)
5,数组指针的不同含义
int a[5][10];
printf(%d, %d, %d\n", a, a+1, &a+1); //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[M1][M2][...][Mn], 那么 a + 1 = a 首地址 + M2 * M3*...*Mn * sizeof(int);
而 &a + 1 = a首地址 + M1*M2*...*Mn * sizeof(int)。
练习:分析下面程序计算结果
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的值各是什么?
int *tmp = ptr2; 即 tmp = &j, tmp指针指向了变量j。
**ptr1 *= 10; **ptr1 即 *(*ptr1)即*p1即 i。**ptr1 *= 10 即为 i *= 10; 即为0;所以i 的值为0;
*ptr2 *= 10; *p2 *= 10 即 j *= 10 即为200; 所以j 的值为200。
ptr2 = *ptr1; 则为p2 = *ptr1 即0为 p2 = p1;
*ptr1 = tmp; 则为 p1 = tmp = &j;
因此答案: i = 0; j = 200; p1 = &j; p2 = &j。
6,定义一个宏,求出给定结构中给定成员的偏移量。
#define OFFSET(TYPE, MEMBER) (size_t)(&(((TYPE*)0)->MEMBER))
(size_t)(&(((TYPE*)0)->MEMBER))把0址址转化为TYPE结构的指针,然后获取该结构中MEMBER成员的指针,并将其强制转换为size_t类型。由于结构从0地址开始定义, 因此,这样求出的member成员地址,实际上就是它在结构中的偏移量。在Linux 的内核中就是用这样的宏定义来求成员的偏移量的。(详:include/linux/stddef.h)。
7,分析程序运行结果:
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int *ptr1 = (int*)(&a + 1);
int *ptr2 = (int*)((int)a+1);
printf(%x,%x", ptr[-1], *ptr2);
return 0;
}
&a 和 a 都表示数组的首地址,但是它们代表的类型不同。其中&a代表整个数组,而 a 代表数组的第一个元素,即&a+1 中的1代表的大小是整个数组,而a+1中1的大小代表的是一个元素的大小。
指针加减法运算,后面的数字表示指针指向的数据类型的大小的倍数。
比如 &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。
8,指针与引用的区别
引用是一种没有指针语法的指针。与指针一样,引用提供对对象的间接访问。引用为所指对象的一个别名。
int i = 0;
int &refi = i; // refi指向一个i引用。
引用必须初始化,而指针没有这个要求(尽管没有初始化的指针很危险);引用总是指向它最初获得的那个对象,而指针可以被重新赋值。
C++中向函数中传递指针和传递指针的引用的区别是:
如果是传递指针,那么会先复制该指针,在函数内部使用的是复制后的指针,这个指针与原来的指针指向相同的地址,如果在函数内部将复制后的指针指向了另外的新的对象,那么不会影响原有的指针。所以在函数中改变指针,必须传递指针的指针或者指针的引用。
使用对象指针作为函数参数比使用对象作为函数参数更普遍。因为使用对象指针作函数参数有如下好处。
(1)实现传址调用。可在被调用函数中改变调用函数的参数对象的值,实现函数之间的信息传递。
(2)使用对象指针实参仅将对象的地址传给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时空开销。
使用对象引用作函数参数要比使用对象指针作函数更普遍,这是因为使用对象引用作函数参数具有用对象指针作函数参数的优点,而且更简单、更直接。
9,指针的引用修改指针
在C 语言中经常使用指针、指针的指针、指针的引用作函数的参数。那么它们的区别是什么呢?
(1)指针引用作参数
void func(MyClass *&pBuildingElement); //指针的引用能修改指针
(2)指针作参数
void func(MyClass *pBuildingElement); //指针,不能修改指针
(3)指针的指针作为参数
void func(MyClass **pBuildingElement); //指针的指针,能修改指针
下面是三个实际函数调用的例子:
void func1(MyClass *pMyClass)
{
DoSomething(pMyClass);
pMyClass = pOtherObject; //其他对象的指针
}
MyClass *p = NULL;
func1(p); //指针作参数, p不参被改变值
void func2(MyClass **pMyClass)
{
*pMyClass = new MyClass;
....
}
MyClass *p = NULL;
func2(&p); //指针的指针作参数,p的值可以被改变
void func3(MyClass *&pMyClass)
{
pMyClass = new MyClass;
}
MyClass *p = NULL;
func3(&p); //指针的引用作参数,p的值可以改变
下面指针引用与指针比较:这两个函数功能都是获取给定位置的元素。
Cobject *&GetAt(POSITION position);//返回对象指针的引用,可能修改函数返回的对象
Cobject *GetAt(POSITION position); //返回对象的指针, 不可以修改函数返回的对象。
所以如果写成下面这样:
Cobject * pObj = myList.GetAt(pos);则pObj 返回的是列表中某个对象的指针。如果接着改变pObj的值:
pObj = pSomeOtherObj; 这改变不了在位置 pos 处的对象地址,而仅仅是改变了变量pObj。但是, 如果写成下面这样:
Cobject *& rpObj = myList.GetAt(pos);
现在,rpObj是返回的引用列表中的对象的指针, 所以当改变rpObj时, 也会改变列表中位置pos处的对象地址,也就是说替代了列表中的这个对象。这就是为什么CObList会有两个GetAt函数的缘故。一个可以修改指针的值,另一个则不能。
事实上,函数可以返回任何类型的引用,不仅仅是指针的引用。如:
int& abc(int &x); //函数中&的作用就是对变量的引用。
int x = 0;
int &a(int &i)//传入实参的引用
{
i = -1;
return x; //返回i的引用
}
void main(void)
{
int j = 10;
a(j) = 100;
//这个时候 j = -1, x = 100了, 因为函数a()返回了x的引用,可以修改x的值
}
总之, 返回引用就是返回一个变量的地址里面的内容,就是真正返回这个变量本身,它可以用作左值,以改变返回的引用的变量的值。在上面的代码中, 函数传入的是实参的引用,返回的是x的引用。因此在main()函数调用了a()函数之后, j 和 x的值都会发生改变。
返回一个类型的引用,在操作符重载赋值运算符”=“中, 这种方式是经常用到的。
指针注意的问题总结:
(1)指针在声明的时候最好初始化。
指针变量没有被初始化, 任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会随机的指向任何一个地址(即野指针),访问野指针会造成不可预知的后果,所以,指针变量在创建的同时应该被初始化,要么将指针设置为NULL, 要么让它指向合法的内存。
(2)指针的加减运算移动的是指针所指类型的大小
前面已经提到, 指针的加法运算p = p + n中, p向前移动的位置不是n个字节,而是n*sizeof(*p)个字节,指针的减法运算与此类似。
(3)当用malloc或new为指针分配内存时应该判断内存分配是否成功,并对新分配的内存进行初始化。
用malloc或new分配内存,应该判断内存是否分配成功。如果失败,会返回NULL,那么就是防止使用NULL指针了。在分配成功时,会返回内存的地址。这个时候内存是一段未被初始化的空间,里面存在的可能是垃圾数据。因此,需要用 memset等对该内存进行初始化。
此外,应该防止试图使用指针作为参数,支分配一块动态内存。如果非要这么做,那么请传递指针的指针或指针的引用。
(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\n", str);
getchar();
}
上代码中,它通过指针的指针分配了一段内存,然后将”hello“ 拷贝到该内存。使用完后再释放。到此为止,代码没有任何问题。但是在释放之后,程序又试图去使用str指针。那么这里就存在问题了。由于str没有被重新置为NULL, 它的值依然指向了该内存。因此后面的程序依然能够打印出”world”。
char *func()
{
char c = 'A';
char *p = &c;
return p;
}
void main(void)
{
char *pc = NULL;
pc = func();
printf("%c", *p);
}
在上面的代码中, func()函数试图返回一个指向局部变量c的指针。然而局部变量的生命期为func()函数的执行期,即变量c分配在栈上,func()函数执行完成后, c 就不存在了。返回的指针就是一个无效的野指针。因此,打印*p时,可能会出现任何一个不可确定的字符。