C/C++语言中的指针

这几天在接触一些C语言的项目,发现自己对C语言,包括C++的知识理解不透彻,尤其是指针。导致项目完全看不懂。因此这一篇就来补补C/C++中指针的知识。

参考书籍《C++ Primer》


指针

简单比喻

什么是指针,假如你住5楼503号房间。那么有一张纸条,纸条上写着5楼503。那么,我们就说这张纸条就是指向你房间的一个指针。

定义

指针(Pointer)是指向(Point to)另外一种类型的复合类型。

指针有两个特点:

  • 本身是一个对象,允许对指针进行赋值和拷贝,而且在指针的生命周期内可以先后指向几个不同的对象。
  • 无须在定义时赋初始值

一个简单例子

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
int ival = 42;
int *p = &ival;

printf("p是一个指针,P为%p\n",p);
printf("*p是指针指向的变量,*p为%d\n",*p);
return 0;
}

输出

1
2
p是一个指针,P为0x7ffe09031b1c
*p是指针指向的变量,*p为42

在这个例子中,我们用 int *p来定义指针,这时候p是一个指针。&ival的意思是取int型变量ival的地址。

而在输出的时候,*p是指针指向的变量,也就是说,*号在这里成了解引用符号。仅在指针确实指向了某个对象,即指针确实有效的情况下,*p才适用。

理解的关键:在定义阶段, 用int *p用来定义指针。在操作阶段,*p是解引用。

空指针

1
2
3
int *p1 = nullptr; // only C++11
int *p2 = 0; // ok
int *p3 = NULL; // include cstdlib

第一种方法仅在C++11中可用,也是C++编程中最推荐的;第二种是最常见的,直接给指针赋值0,即是空指针;第三种 NULL 在 cstdlib 中定义,其实 NULL 的值就是0;

假设p是一个指针,那么以下做法是错误的,即使zero等于0:

1
2
int zero = 0;
p = zero;

一个编程建议是,指针一定要初始化,最好是先有对象(变量),然后再去定义指向这个对象的指针。假设真的要在定义对象前定义指针,不知道让它指向哪里,那就初始化为0或者nullptr。不要让它悬空。

赋值

有时候我们会搞混究竟是改变了指针本身,还是改变了指针指向的对象。一个好方法是,改变的永远是等号左边的

1
2
pi = &val;    // 指针指向了val的地址
*pi = 0; // 指针没变,但是指针指向的对象值变为0了

void* 指针

1
2
3
4
5
6
7
double mynum = 3.66;
double *pd = &mynum; // pa指向mynum

int mynum2 = 9;
void *pv = &mynum2; // pv指向mynum2

pv = &mynum1; // pv现在又指向mynum1了

从上面的例子可以看到, void *型指针跟普通指针也没什么区别。但是,void *指针可以指向任何类型。当然,它不能用于操作指针所指的对象,因为我们不知道这个对象的类型(void *一会儿可指向int型变量的地址,一会儿可以指向double型)

指向指针的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
int val = 1024; // 一个int
int *pi = &val; // 一个指向val的指针
int **ppi = &pi; // 一个指向pi的指针

printf("1) val是一个int,值为%d\n\n", val);
printf("2) pi是一个指向int的指针,pi为%p\n\n",pi);
printf("3) ppi是一个指向指针的指针,ppi为%p\n",ppi);
printf("4) *ppi其实就是 2) 中的pi,*ppi为%p",*ppi);
return 0;
}

不要被**ppi 吓到了,其实它就是指向了pi这个对象。只不过恰好pi这个对象也是一个指针罢了。

输出:

1
2
3
4
5
6
1) val是一个int,值为1024

2) pi是一个指向int的指针,pi为0x7ffc86a9d6b4

3) ppi是一个指向指针的指针,ppi为0x7ffc86a9d6b8
4) *ppi其实就是 2) 中的pi,*ppi为0x7ffc86a9d6b4

可以看到, *ppi其实就是pi,他们都是0x7ffc86a9d6b4

指向常量的指针

C语言中的const限定了对象不能被改变,一把用来表示常量。相当于 java 的 final

而指向常量的指针(point to const),不能用于改变其所指对象的值。point to const一般用来存放常量的地址。

有一点值得注意,允许一个指向常量的指针指向非常量,但是却不能通过这个指针操作这个非常量。

1
2
3
4
5
const double d = 3.33;
const double *pd = &d;

double x = 6.66;
pd = &x; // ok,但是不能通过pd去改变 x 的值

常量指针

指针是对象,跟int、double等一样,所以可以用 const int来表示常量, 那当然也可以用 *const int来表示常量指针啦。

只是,一旦定义了*const int,那这个指针必须初始化,且只能指向初始化的这个地方,不能再改变了。

注意下面的定义

1
2
3
int i = 6;
int *const p1 = &i; //常量指针,不能改变p1所指的对象
const int *p2 = &i

常见的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
int x;

// 指针可以被修改,值也可以被修改
int * p1 = &x;

// 指针可以被修改,值不可以被修改(const int)
const int * p2 = &x;

// 指针不可以被修改(* const),值可以被修改
int * const p3 = &x;

// 指针不可以被修改,值也不可以被修改
const int * const p4 = &x;

摘自 《Leetcode 101》


p->mem 是什么意思

有时候我们会看到 p->mem 这种用法,实际上它等价于 (*p).mem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main() {

typedef struct {
int x;
int y;
} Point;

Point pos;
pos.x = 10;
pos.y = 5;
printf("answer1:%d\n", pos.x * pos.y);

Point* pPos = &pos;
(*pPos).x = 15;
pPos->y = 20;

printf("answer2:%d\n", pos.x * pos.y);

}

输出

1
2
answer1:50
answer2:300

首先定义了一个Point结构体,包含 int 类型的 x, y。然后实例化·

我们当然可以用pos.x = 10pos.y = 5这样的方式来给结构体的每个变量赋值。

但有时候我们要用指针操作对象,我们可以先定义一个Point *类型的指针pPos

然后查看两种用指针间接给结构体赋值的方法:

  1. (*pPos).x = 15;
  2. pPos->y = 20;

第一种是先将指针 pPos 解引用,让它变成指向的对象(pos), 然后用 对象.成员 的方式来赋值。第二种直接在指针pPos上操作,也就是用->来表示,对指针指向的结构体对象(pos)的某个成员(y)进行操作。

换言之,

  • . 直接成员访问操作符,但操作前需对指针解引用
  • -> 间接成员访问操作符

实质上两种方式是等价的。


结构体和指针

定义结构体

在C语言中,我们可以这样定义结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//方法一:
struct student{
short age;
char name[MAXNAME];
long phoneNumber;
};

struct student s1; // s1是student类型的一个实例

//方法二:
typedef struct student{
short age;
char name[MAXNAME];
long phoneNumber;
}STUDENT;

STUDENT s2; // s2是student类型的一个实例

可见,用方法二比较方便一点。

用指针访问结构体成员

1
2
3
4
5
6
7
8
9
10
11
typedef struct{
short age;
char name[MAXNAME];
long phoneNumber;
}STUDENT;

STUDENT s2; // s2是student类型的一个实例

student *ps = &s2;
ps->age = 6;
printf("%d\n",s2.age);

如果要给结构体的 name[MAXNAME] 赋值,下面的做法是错误的

1
ps->name = "jerry";

应该用strcpy函数。

1
2
3
char *name = "jerry";
strcpy(ps->name, name);
printf("%s\n",stu1.name);

结构体偏移量问题

假设现在有一个结构体

1
2
3
4
5
struct fun{
int a;
int b;
char c;
};

我们已知道结构体成员 c 的地址,如何求结构体的起始地址呢?

答案就是:(char *) & ((struct fun*)0)->c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(){
//实例化一个结构体变量
struct fun domain;

//结构体起始地址
printf("iic:%u\n",&domain);

//结构体成员 c 的地址
printf("char c:%u\n", &(domain.c));

//偏移量
printf("sub: %d\n\n", (char *) & ((struct fun*)0)->c);

return 0;
}

输出:

1
2
3
4
jerrysheh@ubuntu:~$ ./fun
fun:-1838605600
char c:-1838605592
sub: 8

链表和指针

定义一个链表

1
2
3
4
5
//定义链表中的节点  
typedef struct node{
int data; //链表中的数据
struct node * p_next;//指向下一节点的指针
}Node,*pNode;

顺序遍历链表

1
2
3
4
5
6
7
void TraverseList(pNode h){  
pNode p = h->p_next;
while(p!=NULL){
printf("%d\n",p->data);
p = p->p_next;
}
}