VectorLu

C 语言指针总结

指针是 C 语言的魅力所在,也是陷阱所在。

疑惑:取址符在声明和语句中

我在学习指针的时候,觉得声明 int *p = &i; 和语句 p = &i; 不一致。相信很多人也有同样的疑惑。

造成困惑的根源在于,根据使用上下文的不同,C 语言中的 * 号可以有多种含义。

在声明 int *p = &i; 中,* 号不是间接寻址运算符,其作用是告知编译器 p 是一个指向 int 类型变量的指针。

而在语句中出现时,* 号(作为一元运算符使用时)会执行间接寻址。

打印

任何指针使用转换说明符 %p 来显示。

数组

可以把指针指向数组元素。

1
2
3
4
int a[10];
int *p;
p = &a[0];
*p = 5;

通过在 p 上执行指针算术运算(或者地址算术运算)可以访问数组 a 的其他所有元素。C 语言支持 3 种格式的指针算术运算:

  • 指针 + 整数
  • 指针 - 整数
  • 两个指针相减(只有在两个指针指向同一个数组时,把它们相减才有意义)

指向同一数组的指针可以用关系运算符进行比较。

虽然可以把数组名 int a[10] 用作指针,但是不能给数组名赋新的值。试图使数组名指向其他地方是错误的。

1
2
3
4
while (*a != 0)
{
a++; // WRONG
}

可以将 a 复制给一个指针变量,然后改变该指针变量。

1
2
3
p = a;
while (*p != 0)
{p++;}

指针与地址

指针通常是地址,但是指针不是真正的内存地址,CPU 必须把它和存储在专用寄存器中的段值结合起来,来表示“偏移量”。

const 与指针

当某个函数使用指针时,可能只是为了检查其中的某个值,而不改变它。可以用 const 保护参数。

1
2
3
4
void f(const int *p)
{
*p = 0; // WRONG
}

这一用法表明 p 是指向“常整数”的指针。试图改变 *p 是编译器会检查的一种错误。

再来仔细学习一下 const

1
2
int const a;
const int a;

两种方式一样,a 的值无法修改,要让它一开始就拥有一个值

  1. 声明时初始化 int const a = 1;
  2. 在函数中声明为 const 的形参在函数被调用的时候会获得实参的值

然而 const 和 指针一起使用时——有两样东西有可能成为常量——指针变量和它指向的实体,示例(这里是《C和指针》里的实例,变量名取得非常有意义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//pi是一个普通的指向整型的指针
int *pi;
/*
** pci 是一个指向整型常量的指针
** 怎样理解?
** 理解为对pci进行间接访问操作后得到的是一个整型常量
** 是不是超简单
** 可以修改指针的值,但是不能修改它指向的整型数
*/
int const *pci;
/*
** 指向整型的常量指针
** 指针是常量,无法修改,但是可以修改它指向的值
** 理解为对某个常量进行间接访问得到的是一个整型
** 而这个常量是一个指向整型的指针
*/
int * const cpi;
/*
** 指针本身和它指向的变量都不允许修改
*/
int const * const cpci;

为了指明数组型形式参数不会被改变,可以在其声明中包含单词 const

1
2
3
4
int findLargest(const int a[], int n)
{
...
}

主要是看 const 右边是什么。

指针作为返回值

可以作为函数的返回值。但是永远不要返回指向自动局部变量的指针:

1
2
3
4
5
6
int *f(void)
{
int i;
...
return &i;
}

一旦 f() 返回,变量 i 就不存在了,所以指向变量 i 的指针将是无效的。有的编译器会在这种情况下给出类似 “function returns address of local variable” 的警告。

动态存储分配

内存分配函数

为了动态地分配存储空间,需要调用三种内存分配函数的一种,这些函数都是声明在 <stdlib.h> 头中的。

  • malloc() ——分配内存块,但是不对内存进行初始化。
  • calloc() ——分配内存块,并且对内存块进行清零。
  • realloc() ——调整先前分配的内存块大小。

当为申请内存块而调用内存分配函数时,由于函数无法知道存储在内存块中的数据是什么类型的,所以它不能返回 int 类型、char 类型等普通类型的指针。取而代之的,函数会返回 void * 类型的值,void * 类型的值是“通用”指针,本质上它只是内存地址。

空指针

当调用内存分配函数时,如果找不到满足需要的内存块,函数会返回空指针。把函数的返回值存储到指针变量中以后,需要判断该指针变量是否为空指针。如果为空,需要采取适当的动作。

所有非空指针都为真,只有空指针为假。

1
2
3
4
5
if (!p)
// if (p == NULL)
if (p)
// if (p != NULL)

NULL 进行比较可读性更好

动态分配字符串

使用 malloc() 为字符串分配内存

函数原型:

1
void *malloc(size_t size);

size_t 表示某块内存的大小,取值范围是目标平台下最大可能的数组尺寸。

1
reminds[i] = (char *)malloc(2+strlen(msg_str)+1);

使用 calloc()

1
void *calloc(size_t nmemb, size_t size);

calloc() 函数会将内存初始化为 0(清除分配的内存)

1
2
struct point {int x; int y;} *p;
p = calloc (1, sizeof (struct point));

使用 realloc()

1
void *realloc(void *ptr, size_t size);

要确定传递给 realloc 函数的指针来自于先前 malloc, calloc 或 realloc 的调用。否则,程序可能行为异常。

  • 当扩展内存块时,realloc() 不回对添加进内存块的字节进行初始化。
  • 如果 realloc() 不能按要求扩大内存块,那么它会返回空指针,并且在原有内存块中的数据不会改变。
  • 如果 realloc() 被调用时以 0 作为第二个实际参数,那么它会释放掉内存块。

一旦 realloc 函数返回,一定要对指向内存块的所有指针进行更新,因为 realloc 函数可能会使内存块移动到了其他地方。

释放存储空间

对于程序而言,不可再访问到的内存块被称为是垃圾 (garbage)

free()

1
void free(void *ptr);

要确定传递给 realloc 函数的指针来自于先前 malloc, calloc 或 realloc 的调用。否则,程序可能行为异常。

悬空指针

虽然 free() 允许收回不再需要的内存,但是使用该函数会导致:悬空指针 (dangling pointer)。调用 free(p) 函数会释放 p 指向的内存块,但是不会改变 p 本身。如果忘记了 p 不再指向有效的内存块,修改其指向的内存是严重的错误,因为程序不再对此内存有任何控制权了。

1
2
3
4
5
char *p = malloc(4);
...
free(p);
...
strcpy(p, "abc"); // wrong

试图访问或修改释放掉的内存块会导致未定义的行为。

悬空指针很难发现,因为及格指针可能指向相同的内存块。在释放内存块后,全部的指针都悬空了。

链表

运算符 . 的优先级高于运算符 *

运算符 right arrow selection -> 产生左值:

1
scanf("%d", &new_node->value);

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
struct node {
int value;
struct node *next;
};
struct node *first = NULL;
struct node *addToList(struct node *list, int n)
{
struct node *newNode;
newNode = (struct node *)malloc(sizeof(struct node));
if (newNode == NULL)
{
printf("Error: malloc failed in addTolist()\n");
exit(EXIT_FAILURE);
}
newNode->value = n;
newNode->next = list;
return newNode;
}
struct node *readNumbers(void)
{
struct node *first = NULL;
int n;
printf("Enter a series of integers (0 to terminame): ");
for ( ; ; )
{
scanf("%d", &n);
if (n == 0){return first;}
first = addToList(first, n);
}
}
struct node *searchList(struct node *list, int n)
{
struct node *p;
for (p = list; p != NULL; p = p->next)
{
if (p->value == n){return p;}
}
return NULL;
}
struct node *searchList(struct node *list, int n)
{
for ( ; list != NULL; list = list->next)
{
if (list->value == n)
{return list;}
}
return NULL;
}
// BETTER WAY
struct node *searchList(struct node *list, int n)
{
while (list != NULL && list->value != n)
{list = list->next;}
return list;
}

删除示例

  1. 定位要删除的结点;(保留一个指向前一个结点的指针 previous,还有指向当前结点的指针 current)
  2. 改变前一个结点,从而使它“绕过”删除结点;
  3. 调用 free() 收回删除结点占用的内存空间。
1
2
3
4
5
6
7
for (current = list, previous = NULL;
current != NULL && current->value != n;
previous = current, current = current->next)
{ ;}
previous->next = current->next;
free(current);

删除结点的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct node *deleteFromList(struct node *list, int n)
{
struct node *current, *previous;
for (current = list, previous = NULL;
current != NULL && current->value != n;
previous = current, current = current->next)
{ ;}
// n was not found
if (current == NULL){return list;}
// n is the first node
if (previous = NULL)
{list = list->next;}
else
{previous->next = current->next;}
free(current);
return list;
}

指向函数的指针

一个很好的例子

快速排序的函数原型

1
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

inventory 进行排序。

1
qsort(inventory, numParts, sizeof(struct part), compareParts);

qsort() 要求其形式参数类型为 void *,但我们不能通过 void * 型的指针访问 part 结构成员,而是需要指向结构 part 的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int compareParts(const void *p, const void *q)
{
// void * 通用指针可以通过这种方式很容易地转换为
// 别的类型的指针
const struct part *p1 = p;
const struct part *q1 = q;
if (p1->number < q1->number)
{return -1;}
else if (p1->number == q1->number)
{return 0;}
else
{return 1;}
}

更加简明的实现:

1
2
3
4
5
6
7
8
9
int compareParts(const void *p, const void *q)
{
if (((struct part *) p)->number == ((struct part *) q)->number)
{return -1;}
else if (((struct part *) p)->number == ((struct part *) q)->number)
{return 0;}
else
{return 1;}
}

通过移除 if 语句可以把函数 compareParts() 变得更短:

1
2
3
4
int compareParts(const void *p, const void *q)
{
return ((struct part *) p)->number - ((struct part *) q)->number;
}
您的支持将鼓励我继续创作!

热评文章