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时,可能会出现任何一个不可确定的字符。
C/C++入门基础---指针(2)的更多相关文章
- C/C++入门基础----指针(1)
指针其实就是一个变量, 和其他类型的变量一样.在32位计算机上, 指针占用四字节的变量.指针与其他变量的不同就在于它的值是一个内存地址,指向内存的另外一个地方, 指针能够直接访问内存和操作底层的数据, ...
- JavaScript入门基础
JavaScript基本语法 1.运算符 运算符就是完成操作的一系列符号,它有七类: 赋值运算符(=,+=,-=,*=,/=,%=,<<=,>>=,|=,&=).算术运 ...
- C++ STL编程轻松入门基础
C++ STL编程轻松入门基础 1 初识STL:解答一些疑问 1.1 一个最关心的问题:什么是STL 1.2 追根溯源:STL的历史 1.3 千丝万缕的联系 1.4 STL的不同实现版本 2 牛刀小试 ...
- C语言入门基础整理
学习计算机技术,C语言可以说是必备的,他已经成为现在计算机行业人学习必备的,而且应用也是十分的广泛,今天就来看看拥有几年c语言工作经验的大神整理的C语言入门基础知识,没有学不会,只有不肯学. 结构化程 ...
- Java入门基础知识点总结(详细篇)
Java入门基础知识点总结(详细篇)~~~~~目录 1.1 图解 1.1.1 Java基础知识点 1.1.2 Java基础语法的相关内容 1.2 关键字 1.3 标识符 1.3.1 标识符概念 1.3 ...
- mybatis入门基础(二)----原始dao的开发和mapper代理开发
承接上一篇 mybatis入门基础(一) 看过上一篇的朋友,肯定可以看出,里面的MybatisService中存在大量的重复代码,看起来不是很清楚,但第一次那样写,是为了解mybatis的执行步骤,先 ...
- 01shell入门基础
01shell入门基础 为什么学习和使用shell编程 shell是一种脚本语言,脚本语言是相对于编译语言而言的.脚本语言不需要编译,由解释器读取程序并且执行其中的语句,而编译语言需要编译成可执行代码 ...
- Markdown入门基础
// Markdown入门基础 最近准备开始强迫自己写博文,以治疗严重的拖延症,再不治疗就“病入骨髓,司命之所属,无奈何”了啊.正所谓“工欲善其事,必先利其器”,于是乎在写博文前,博主特地研究了下博文 ...
- HTML入门基础教程相关知识
HTML入门基础教程 html是什么,什么是html通俗解答: html是hypertext markup language的缩写,即超文本标记语言.html是用于创建可从一个平台移植到另一平台的超文 ...
随机推荐
- tomcat8和7关于自定义tag的处理区别
今天将一直运行在tomcat-7.0.29(jdk1.6)上的应用迁移到tomcat-8.0.26(jdk1.7)上面,老显示如下错误: org.apache.jasper.JasperExcepti ...
- 安卓手机屏幕录像之scr
打开SCR Screen Recorder,屏幕会显示录像控制面板,点击“开始”按钮就可以开始录像: - 停止录像的方法有两种.一种是锁屏,锁屏后等待2秒,录像文件会自动保存到SD卡,另外一种是重新打 ...
- md5算法
md5算法 不可逆的:原文-->密文.用系统的API可以实现: 123456 ---密文 1987 ----密文: 算法步骤: 1.用每个byte去和11111111做与运算并且得到的是int类 ...
- SpringBoot和数据库连接
就像单机Java应用程序一样,和数据库连接需要DataSource,然后生成到数据库的Connection再进行数据库操作 SpringBoot和原生的JDBC 先看SpringBoot项目源码 从上 ...
- 实现ApplicationContextAware接口时,获取ApplicationContext为null
将懒加载关闭,@Lazy(false),默认为true import org.springframework.beans.BeansException; import org.springframew ...
- ReactJS学习笔记(一)
1.依赖的资源: <script type="text/javascript" src='../asset/react.js'></script> &l ...
- .NET (四)委托第四讲:内置委托Comparison
// 摘要: // 表示比较同一类型的两个对象的方法. // // 参数: // x: // 要比较的第一个对象. // // y: // 要比较的第二个对象. // // 类型参数: // T: / ...
- 过渡transitioin
一,什么是过渡(transition)? 1,transition 允许 CSS 元素的属性值在一定的时间区间内平滑地过渡. 2,可以在不使用 Flash 动画或 JavaScript 的情况下,在元 ...
- WPF绘制矢量图形模糊的问题
WPF默认提供了抗锯齿功能,通过向外扩展的半透明边缘来实现模糊化.由于WPF采用了设备无关单位,当设备DPI大于系统DPI时,可能会产生像素自动扩展问题,这就导致线条自动向外扩展一个像素,并且与边缘相 ...
- 最简单的Github入门基础
起因是小伙伴分享给我github上的一个FQ工具,让我看实现过程.于是,就由关键字"github"搜索开始. 一言之,是个开源的SVN.和CVS.SVN类似,但是,里面有千千万万程 ...