跳转至

📔 Lecture04 C Intro: Pointers, Arrays, Strings

更新日期:2023.11.02

1. Pointers and Bugs

1.1 Bugs来源

  • 变量的声明

    • 创建一个变量,然后在使用之前要对其进行初始化。
    • 基本上都位于块的开头声明。
    • 如果未对变量进行声明初始化,可能会出现代码问题。
  • 未定义的行为

    • 会出现很多未知的行为。
      • 换个机器执行时,每次现象都不一致。
    • 也可能出现随机却难以复现的错误。
    • 也有可能在调试时消失或者改变。
  • 地址和值的问题

    • 内存可以看作是一个巨大的数组。
      • 对于数组的每个单元都有一个与其关联的地址。
      • 每个单元都存着一些值。
      • 从0开始进行访问。
    • 不要将地址和值混淆。

1.2 指针

指针:指向一个特定的内存位置。是一个包含变量地址的变量。对于指针来说,就是直接指向了这个地址。

此时创建一个新变量,不知道其地址,但如果p指向x并可以访问x,那么x的地址在p的位置上。

1.3 指针的使用

// 创建指针的三种语法格式
int *p, x; // 指向整数的指针,p指向一个整数

x = 3;

p = &x; // 将y的地址直接赋值给p,& 叫做“地址引用”

z = *p; // 将p所在地址的值赋值给z,此时也叫做“解引用运算符”。

printf("p points to %d\n", *p); // 可以使用 *解引用运算符 来获取指向的值。

对于p来说,无需去关注x的地址,因为可以通过p去获取到x的地址,然后获取到其值,并把x的值写入到p中。

👍 Notes:对于例子中, * 主要用于两种形式:

  • 用来将p声明为指向整数的指针。
  • 用来 “解引用运算符”。

对于变量指向的值,如何改变呢?使用 *等号左边完成对值的修改。

1.4 指针和参数传递

在Java和C中,都是通过 值传递 的方式。

1
2
3
4
5
6
void addOne(int x)
{
    x = x + 1;
}
int y = 3;
addOne(y); // 对于结果来说,y的值依然还是3

但是如果想要改变y的值,则可以使用指针的方式。

1
2
3
4
5
6
void addOne(int *p)
{
    *p = *p + 1;
}
int y = 3;
addOne(&y); // 最终值:y = 4

通过指针的方式传值,则子程序addOne可以直接访问y。也就可以修改子程序中的值,从而得到改变后的值。

1.5 C指针的陷阱

对于初始化来说,也会产生一些程序垃圾。例如指针。

对于指针来说,声明一个指针就是分配空间来保存指针。但是它并没有分配要指向的对象。

在C中,局部变量如果没有初始化,那么它可以包含任何内容,也就造成相应的程序垃圾。

1
2
3
4
5
void f()
{
    int *ptr;
    *ptr = 5;
}

对于指针来说,既有好处也有坏处:

  • 益处:

    • 传递一个大的结构体或者数组时,传递指针比传递整个结构体和数组的过程更容易更快,否则就需要使用大量的复制操作数据。
    • 代码紧凑、简洁。
  • 坏处:

    • 指针是C语言中最大的Bug来源。所以使用时需要时刻小心。
    • 特别是在动态内存管理时,需要注意其中的问题。例如:
      • 悬空引用
      • 内存泄漏

2. Using Pointers Effectively

可以声明一个泛型类型的指针。因为指针可以指向 任何数据类型的(如int、char、struct等) 。但指针通常只能指向其中之一的一种类型。

void* 可以指向任何值的类型,另外 void* 也被称为 通用指针

对于指针需要谨慎使用,有助于避免很多程序错误、安全问题、以及不利于程序的事情。

也可以使用函数指针(指向函数的指针)。

// fn 是一个接受两个 void * 指针并返回 int 的函数,最初指向函数 foo。
int (*fn) (void *, void *) = &foo; // 如果想要调用指针,则(*fn) (x ,y) 可以直接调用该函数

2.1 指针和结构体

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

Point p1;
Point p2;
Point *pAddr; // p的地址指向point的指针

pAddr = &p2;

/* dot notation(点表示法) */
int h = p1.x;
p2.y = p1.y;

/* arrow notation (箭头表示法)*/
int h = pAddr->x;
int h = (*pAddr).x;

/* This works too */
p1 = p2;

2.2 NULL指针

null比较特殊,NULL指针意味着不指向任何东西。但是声明NULL指针式相对比较安全的方式。

NULL类似于指针的哨兵值。可以声明为NULL,不代表在任何时候将任何内容都存储为0。

对于NULL指针,需要注意点:禁止写入或者读取空指针,会让程序直接奔溃。

1
2
3
4
5
6
7
8
// 由于 0 为 False,所以可以直接对null进行测试:
if (!p) {
    /*P is a null pointer */
}

if (q) {
    /* Q is not a null pointer */
}

2.3 指向不同大小的对象

现代机器都是字节可寻址的方式可以访问每一个字节。C指针是抽象内存地址的一种思考方式。

对于硬件来说,存储器都是由8位存储单元组成,每一个存储单元都有一个唯一的地址。

类型声明告诉编译器每次通过指针访问要获取多少字节。

对于一些字节需要对齐,如果使用的机器是32位,那么对应的就是4字节,那么就需要进行4字节值对齐。

2.4 sizeof() 操作符

sizeof(type) 返回一个对象的字节大小。

  • 在C99标准定义中,sizeof(char) == 1

  • 可以直接使用 sizeof(arg)sizeof(structType)

    • 对于结构体来说,需要考虑到字节对齐,所以在32位系统中,都是通过4字节的方式进行对齐。

2.5 指针运算

1
2
3
4
5
6
7
// 表示指向某个东西的指针,移动对应的元素类型大小的n倍。

// 指针加法表示向前走
pointer + n;

// 指针减法表示向后走
pointer - n;

如何更改指针?

// 增量指针
void IncrementPtr(int **p)
{
    *p = *p + 1;
}

int A[3] = {5, 6, 7};
int *q = A;

IncrementPtr(&q)
printf("*q = %d\n", *q);

3. Arrays

C中数组和Java中类似,通过声明类型并指定数量。例如:

  • 数组是一个请求连续内存块的一种方式。

1
2
3
4
5
6
7
int ar[2]; // 表示两个一行的整数

// 也可以声明并初始化
int arr[] = {3, 5};

// 对于数组的方式,可以直接使用方括号表示法
printf("arr[1] = %d", arr[1]);
对于一个数组变量,看起来像是一个指针。例如两种方式:

  • ar[0] 可以表示为 *ar
    • 数组变量是指向第一个元素的指针。
  • ar[2] 表示为 *(ar+2)
    • 这也是通过指针运算进行访问数组的一种方式。

在大多数情况下,数组和指针实际上绝大部分是相同的。

char *str;  // 指向字符数组的指针
char str[]; // 字符串数组
- 字符串是指向连续字符块的指针。 - 字符串就是一个字符数组。

如果数组的大小为n,要去访问 0 到 n-1,也就是数组可以访问的范围

// 错误编码方式
int i arr_01[10];
for (int i = 0; i < 10; i++)
{
    // ......
}

// 正确的编码方式

// 使用宏定义的好处:变量修改时,只需要维护一个位置。
#define ARR_SIZE 10 // 定义数组的大小为10,此时就是全局定义的方式

int ARRAY_SIZE = 10;
int i, a[ARRAY_SIZE];

for (i = 0; i < ARRAY_SIZE; i++)
{
    // ......
}

对于数组来说,也存在一定的陷。一些错误方式:

  • 在C语言中数组是无法清楚何时会超出边界,因为C语言不会检查任何健全性检查相关的问题(包括数组的边界问题)。

  • 分段错误 和 总线错误。

    • 分段错误:访问内存异常(一般是访问了一些无权访问的区域)。
    • 总线错误:对齐方式错误。

4. Function Pointer Example

#include <stdio.h>

int x10(int), x2(int);
void mutate_map(int [], int n, int(*)(int));
void print_array(int [], int n);

int x2 (int n) 
{
    return 2 * n;
}

int x10(int n) 
{
    return 10 * n;
}

void mutate_map(int A[], int n, int(*fp)(int))
{ 
    for (int i = 0; i < n; i++)
        A[i] = (*fp)(A[i]);
}
void print_array(int A[], int n) 
{
    for (int i = 0; i < n; i++)
    printf("%d ",A[i]); printf("\n");
}

int main(void) 
{
    int A[] = {3,1,4}, n = 3;
    print_array(A, n);
    mutate_map (A, n, &x2);
    print_array(A, n);
    mutate_map (A, n, &x10);
    print_array(A, n);
}