C/C++语言中的指针
这几天在接触一些C语言的项目,发现自己对C语言,包括C++的知识理解不透彻,尤其是指针。导致项目完全看不懂。因此这一篇就来补补C/C++中指针的知识。
参考书籍《C++ Primer》
指针
简单比喻
什么是指针,假如你住5楼503号房间。那么有一张纸条,纸条上写着5楼503。那么,我们就说这张纸条就是指向你房间的一个指针。
定义
指针(Pointer)是指向(Point to)另外一种类型的复合类型。
指针有两个特点:
- 本身是一个对象,允许对指针进行赋值和拷贝,而且在指针的生命周期内可以先后指向几个不同的对象。
- 无须在定义时赋初始值
一个简单例子
1 | #include <stdio.h> |
输出
1 | p是一个指针,P为0x7ffe09031b1c |
在这个例子中,我们用 int *p
来定义指针,这时候p
是一个指针。&ival
的意思是取int
型变量ival
的地址。
而在输出的时候,*p
是指针指向的变量,也就是说,*
号在这里成了解引用符号。仅在指针确实指向了某个对象,即指针确实有效的情况下,*p
才适用。
理解的关键:在定义阶段, 用
int *p
用来定义指针。在操作阶段,*p
是解引用。
空指针
1 | int *p1 = nullptr; // only C++11 |
第一种方法仅在C++11中可用,也是C++编程中最推荐的;第二种是最常见的,直接给指针赋值0,即是空指针;第三种 NULL 在 cstdlib 中定义,其实 NULL 的值就是0;
假设p是一个指针,那么以下做法是错误的,即使zero等于0:
1 | int zero = 0; |
一个编程建议是,指针一定要初始化,最好是先有对象(变量),然后再去定义指向这个对象的指针。假设真的要在定义对象前定义指针,不知道让它指向哪里,那就初始化为0或者nullptr。不要让它悬空。
赋值
有时候我们会搞混究竟是改变了指针本身,还是改变了指针指向的对象。一个好方法是,改变的永远是等号左边的。
1 | pi = &val; // 指针指向了val的地址 |
void* 指针
1 | double mynum = 3.66; |
从上面的例子可以看到, void *
型指针跟普通指针也没什么区别。但是,void *
指针可以指向任何类型。当然,它不能用于操作指针所指的对象,因为我们不知道这个对象的类型(void *
一会儿可指向int型变量的地址,一会儿可以指向double型)
指向指针的指针
1 | #include <stdio.h> |
不要被**ppi 吓到了,其实它就是指向了pi这个对象。只不过恰好pi这个对象也是一个指针罢了。
输出:
1 | 1) val是一个int,值为1024 |
可以看到, *ppi
其实就是pi
,他们都是0x7ffc86a9d6b4
指向常量的指针
C语言中的const
限定了对象不能被改变,一把用来表示常量。相当于 java 的 final
。
而指向常量的指针(point to const),不能用于改变其所指对象的值。point to const一般用来存放常量的地址。
有一点值得注意,允许一个指向常量的指针指向非常量,但是却不能通过这个指针操作这个非常量。
1 | const double d = 3.33; |
常量指针
指针是对象,跟int、double等一样,所以可以用 const int
来表示常量, 那当然也可以用 *const int
来表示常量指针啦。
只是,一旦定义了*const int
,那这个指针必须初始化,且只能指向初始化的这个地方,不能再改变了。
注意下面的定义
1 | int i = 6; |
常见的定义
1 | int x; |
p->mem 是什么意思
有时候我们会看到 p->mem
这种用法,实际上它等价于 (*p).mem
。
1 | #include <stdio.h> |
输出
1 | answer1:50 |
首先定义了一个Point
结构体,包含 int 类型的 x, y。然后实例化·
我们当然可以用pos.x = 10
,pos.y = 5
这样的方式来给结构体的每个变量赋值。
但有时候我们要用指针操作对象,我们可以先定义一个Point *
类型的指针pPos
。
然后查看两种用指针间接给结构体赋值的方法:
- (*pPos).x = 15;
- pPos->y = 20;
第一种是先将指针 pPos
解引用,让它变成指向的对象(pos
), 然后用 对象.成员 的方式来赋值。第二种直接在指针pPos
上操作,也就是用->
来表示,对指针指向的结构体对象(pos
)的某个成员(y
)进行操作。
换言之,
.
直接成员访问操作符,但操作前需对指针解引用->
间接成员访问操作符
实质上两种方式是等价的。
结构体和指针
定义结构体
在C语言中,我们可以这样定义结构体:
1 | //方法一: |
可见,用方法二比较方便一点。
用指针访问结构体成员
1 | typedef struct{ |
如果要给结构体的 name[MAXNAME] 赋值,下面的做法是错误的
1 | ps->name = "jerry"; |
应该用strcpy函数。
1 | char *name = "jerry"; |
结构体偏移量问题
假设现在有一个结构体
1 | struct fun{ |
我们已知道结构体成员 c 的地址,如何求结构体的起始地址呢?
答案就是:(char *) & ((struct fun*)0)->c
1 | int main(){ |
输出:
1 | jerrysheh@ubuntu:~$ ./fun |
链表和指针
定义一个链表
1 | //定义链表中的节点 |
顺序遍历链表
1 | void TraverseList(pNode h){ |