---
url: /notes/dev/20250506-1.md
---
\[\[toc]]

# C语言指针与字符串

## 指针

虽然一直对`指针`这个概念有所耳闻, 但这次才开始深入了解指针.

`指针`是一个特殊的变量, 它存储的是一个内存中的位置, 类型代表其位置的大小.

```c
int a;
int *pa;
```

`int a` 定义了一个整型的变量`a`, `int *pa`则定义了一个指针`pa`, 它的类型是`int`, 也就是指向一个4字节的区域.

```c
*pa = a;
```

这样`pa`指针就指向`int a`时在内存中开辟的区域了.

我们讨论一下指针在数组(尤其是`char`数组(C风格字符串))的应用.

```c
int arr[8];
```

这样我们定义一个`int`类型数组, 它的长度是`8`, 也就是在内存中开辟了连续的`4 * 8 = 32`字节空间, `arr[i]`代表着一个4字节的空间.

```c
int *p = arr;
```

这样定义指针`p`指向`arr`, 也就是开辟的32字节空间的起始点. 由于`p`类型为`int`, 所以`p`代表着从起始点开始的4字节空间.

$$
\begin{aligned}
& arr \\
\cdots \Box \Box | & \underline{\blacksquare \Box \Box \Box} \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box | \Box \Box \cdots \\
& \uparrow \\
& \*p = arr\[0]
\end{aligned}
$$

此时的`p`就代表着`arr[0]`的值.

```c
printf("%d %d", arr[0], *p);
```

使用`*p`访问`p`指代的值, 可以得到和`arr[0]`相同的结果.

但如果`printf("%d", p);`, 将输出一串数字, 代表的是该数据在内存中的位置, 由于前面未知, 无法正常读取.

这时我们`p ++;`, 则`p`将指向下一个位置, 由于`p`是`int`类型指针, 则下一个位置也就是移动`4`个字节.

$$
\begin{aligned}
\cdots \Box \Box | \Box \Box \Box \Box & \underline{\blacksquare \Box \Box \Box} \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box | \Box \Box \cdots \\
& \uparrow \\
& \*p = arr\[1]
\end{aligned}
$$

同理, 我们可以让`p`一共自增7次, 当`p`指向最后一组时, 再让`p`自增, 由于内存是非常大的, `p`仍然可以指向一个新位置.

但该位置可能并未被定义或初始化, 我们无法确定`p`指向位置有什么数据, 写入数据有什么影响.

$$
\begin{aligned}
\cdots \Box \Box | \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box & \underline{\blacksquare \Box \Box \Box} | \Box \Box \cdots \\
& \uparrow \\
& \*p = arr\[7]
\end{aligned}
\\
\begin{aligned}
\cdots \Box \Box | \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box \Box | & \underline{\blacksquare \Box} \cdots \\
& \uparrow \\
&\*p = ???
\end{aligned}
$$

## C风格字符串

对于`C风格字符串`, 也就是`char数组`, 我们可以用指针来方便的传参, 获取某位. 同时`string.h`标准库中的各个函数也都是接受/返回指针作为参数的.

```c
char str[100] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
char *p = str;
```

`char`恰好只占`1`个字节, 所以我们画图表示, 也方便很多.

$$
\begin{aligned}
\cdots \Box \Box | & \underset{L}{\Box} \underset{o}{\Box} \underset{r}{\Box} \underset{e}{\Box} \underset{m}{\Box} \underset{\_}{\Box} \underset{i}{\Box} \cdots \underset{\backslash 0}{\Box} \cdots \Box \Box | \Box \Box \cdots \\
& \uparrow \\
& \*p
\end{aligned}
$$

我们可以使用`p`访问到`'L'`字符.

### 获取长度

字符串长度指的是从第一个字符到结束字符`\0`的长度, 而非数组长度(显然数组长度是大于实际长度的). 我们可以通过循环的方式来计算长度.

```c
int len = 0;
char *p = str;
while (*p != '\0') {
    len ++;
    p ++;
}
```

$$
\begin{aligned}
\cdots \Box \Box | & \underset{L}{\blacksquare} \underset{o}{\Box} \underset{r}{\Box} \underset{e}{\Box} \underset{m}{\Box} \underset{\_}{\Box} \underset{i}{\Box} \cdots \underset{\backslash 0}{\Box} \cdots \Box \Box | \Box \Box \cdots \\
& \uparrow \\
& \*p \ (len = 1)
\end{aligned}
\\
$$

`p`自增:

$$
\begin{aligned}
\cdots \Box \Box | \underset{L}{\Box} & \underset{o}{\blacksquare} \underset{r}{\Box} \underset{e}{\Box} \underset{m}{\Box} \underset{\_}{\Box} \underset{i}{\Box} \cdots \underset{\backslash 0}{\Box} \cdots \Box \Box | \Box \Box \cdots \\
& \uparrow \\
& \*p \ (len = 2)
\end{aligned}
$$

`p`一直自增到读取到结束符`\0`:

$$
\begin{aligned}
\cdots \Box \Box | \underset{L}{\Box} \underset{o}{\Box} \underset{r}{\Box} \underset{e}{\Box} \underset{m}{\Box} \underset{\_}{\Box} \underset{i}{\Box} \cdots & \underset{\backslash 0}{\blacksquare} \cdots \Box \Box | \Box \Box \cdots \\
& \uparrow \\
& \*p \ (len = 56)
\end{aligned}
$$

### 复制字符串

当我们定义好字符串, 可以直接定义新的指针指向该字符串, 创建浅拷贝, 然而这样几个拷贝直接会互相影响, 所以我们要创建新的空字符串, 之后将字符一个一个输入到新字符串中.

```c
char a[100] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
char b[100];

char *pa = a;
char *pb = b;

while (*pa != '\0') {
    *pb = *pa;
    pa ++;
    pb ++;
}
```

我们定义一个指向`a`的指针`pa`, 之后用相同的方法遍历数组, 将每个值复制到指向`b`的指针`pb`中, 即写入`b`数组的对应位中.

$$
\begin{aligned}
a: \cdots \Box | & \blacksquare \Box \Box \cdots \backslash 0 \cdots \Box | \Box \cdots \\
& \uparrow \\
& \*pa \\
&\*pb \\
& \downarrow \\
b: \cdots \Box | & \blacksquare \Box \Box \cdots \backslash 0 \cdots \Box | \Box \cdots
\end{aligned}
$$

### 字符串比较

首先比较字符串长度, 若长度不相同, 则字符串不可能相等, 先比较长度可以省去部分算力.

而长度比较函数我们刚才已经写了出来, 不过现在没有现成的函数可以用, 我们还是直接用指针遍历字符串, 按位比较.

```c
char a[100] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
char b[100] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";

char *pa = a;
char *pb = b;

while (*pa != '\0') {
    if (*pa != *pb) {
        printf("Not Equal");
        return 0;
    }
    pa ++;
    pb ++;
}
printf("Equal");
```

原理和上述字符串复制基本相同.

同理, 我们可以使用指针实现字符串查找某字符, 字符串查找字符串等.

## `string.h`标准库函数

上式操作已经被封装到`string.h`标准库中, 我们可以直接使用标准库的函数进行操作.

* `strlen(str)`, 返回字符串`str`的长度.
* `strcpy(a, b)`, 将`b`字符串复制到`a`字符串中.
* `strcat(a, b)`, 将`b`字符串追加到`a`字符串后面.
* `strcmp(a, b)`, 比较`a`, `b`字符串的字典顺序.
* `strchr(a, c)`, 在字符串`a`中查找字符`c`.
* `strstr(a, b)`, 在字符串`a`中查找子字符串`c`.

上述函数中传入的字符串, 实际上都是指针, 表示的从指针位置开始到结束符`\0`的一段字符串.

示例:

```c
#include <stdio.h>
#include <string.h>

int main () {
    char a[100] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
    char b[200] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";

    printf("%d\n", strlen(a)); // 56

    char c[100];
    strcpy(c, a);
    printf("%s\n", c); // Lorem ...

    strcat(b, a);
    printf("%s\n", b); // Lorem ... * 2

    printf("%d\n", strcmp("abc123", "abc124")); // "abc123" < "abc124", return -1

    return 0;
}
```

复杂操作接下来例题中再做讲解.

## 例题

### 统计字符串中英文字符, 数字字符和其他字符的数量

直接上代码, 前后略.

```c
int alpha = 0, digit = 0, other = 0;
char *p = str;  

while (*p != '\0') {
    if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) {
        alpha ++;
    } else if (*p >= '0' && *p <= '9') {
        digit ++;
    } else {
        other ++;
    }
    p ++;
}
```

使用`ASCII`字符的顺序来分类字符.

### 删除字符串中的指定字符

使用`write`和`read`两个指针来读写.

```c
void delChar (char *p, char c) {
    char *write = p;
    char *read = p;
    
    while (*read) {
        if (*read != c) {
            *write = *read;
            write ++;
        }
        read ++;
    }
    *write = '\0';
}
```

`read`指针用于读取内容, 正常情况下, 它们同时移动.

当`read`指针读取到需要删除的字符时, `write`则停止移动, 这时`read`读取到内容, `write`会直接覆盖掉被删去的字符和它之后的位置.

同步读写:

$$
\begin{aligned}
\*r & ead \\
& \downarrow \\
\cdots \Box | & \ Lorem \ ipsum \ dolor \ sit \ amet. \backslash \Box | \Box \cdots \\
& \uparrow \\
\*w & rite
\end{aligned}
$$

读取到需要删除的字符(这里以空格为例):

$$
\begin{aligned}
\*r & ead \\
& \downarrow \\
\cdots \Box | Lorem & \ \_ ipsum \ dolor \ sit \ amet. \backslash \Box | \Box \cdots \\
& \uparrow \\
\*w & rite
\end{aligned}
$$

这时仅`read ++`, `write`保持不动:

$$
\begin{aligned}

* & read \\
  & \ \ \downarrow \\
  \cdots \Box | Lorem & \ \_ ipsum \ dolor \ sit \ amet. \backslash \Box | \Box \cdots \\
  & \uparrow \\
  \*w & rite
  \end{aligned}
  $$

之后`write`写入`*read`:

$$
\begin{aligned}

* & read \\
  & \ \downarrow \\
  \cdots \Box | Lorem & \ \underline{i} ipsum \ dolor \ sit \ amet. \backslash \Box | \Box \cdots \\
  & \uparrow \\
  \*w & rite
  \end{aligned}
  $$

接下来`read`和`write`继续前进, 覆盖字符串:

$$
Lorem \underline{i} \hat{i} psum \ dolor \ sit \ amet. \\
Lorem i \underline{p} \hat{p} sum \ dolor \ sit \ amet. \\
Lorem ip \underline{s} \hat{s} um \ dolor \ sit \ amet. \\
Lorem ips \underline{u} \hat{u} m \ dolor \ sit \ amet. \\
Lorem ipsu \underline{m} \hat{m} \ dolor \ sit \ amet. \\
\ \ \\
\begin{aligned}

* & read \\
  & \ \ \downarrow \\
  \cdots \Box | Loremipsum & \underline{m} \_ dolor \ sit \ amet. \backslash \Box | \Box \cdots \\
  & \uparrow \\
  \*w & rite
  \end{aligned}
  $$

这时`read`再次读取到空格, `write`再停止一步, 之后同步:

$$
\begin{aligned}
& \*read \\
& \ \ \ \ \ \downarrow \\
\cdots \Box | Loremipsum & \ \underline{d} \_ dolor \ sit \ amet. \backslash \Box | \Box \cdots \\
& \uparrow \\
\*w & rite
\end{aligned}
\ \ \\
Loremipsum \underline{d} \ \hat{d} olor \ sit \ amet. \\
Loremipsumd \underline{o} d \hat{o} lor \ sit \ amet. \\
Loremipsumdo \underline{l} l \hat{l} or \ sit \ amet. \\
Loremipsumdol \underline{o} l \hat{o} r \ sit \ amet. \\
Loremipsumdolo \underline{r} o \hat{r} \ sit \ amet. \\
Loremipsumdolor \underline{o} r \hat{\ } sit \ amet. \\
$$

此时`write`指针再次等待.

$$
Loremipsumdolor \underline{o} r \hat{\ } sit \ amet. \\
Loremipsumdolor \underline{s} r \ \hat{s} it \ amet. \\
Loremipsumdolors \underline{r} \ s \hat{i} t \ amet. \\
\cdots \cdots
$$

最终:

$$
Loremipsumdolorsitamet \underline{.} met \hat{.}
$$

这时`read`再次移动, 得到结束符`\0`, 这时`write`再次移动, 写入结束符`\0`, 这时字符串变为:

$$
Loremipsumdolorsitamet. \underline{\backslash 0} et. \hat{\backslash 0}
$$

现在字符串虽然后面扔有残留字符, 但由于倒数第4位处位结束符`\0`, 则说明该字符串到此处结束, 后面有无字符无所谓, 不会参与运算.

### 字符串循环移位

> 对于字符串`str`, 输入数字`n`, 将字符串里面的字符循环向左移动`n`位.
> 例如, 输入`123456789 3`, 则输出`456789123`.

首先, 我们先处理`n`, 对于一个长度为`9`的字符串, 若`n = 9`, 则无需处理; 若`n = 10`, 则相当于`n = 1`.

故先使`n`对字符串长度取模.

之后拆分字符串, 将后半部分先复制到新字符串中, 再追加前半部分, 返回(或使覆盖)新字符串.

代码如下:

```c
void cyclicShift(char *ps,int n){
    int len = strlen(ps);
    n %= len;

    char newStr[N];
    ps += n;
    strcpy(newStr, ps);
    *ps = '\0';
    ps -= n;
    strcat(newStr, ps);
    strcpy(ps, newStr);
}
```

第一步取模部分无需讲解.

第二步创建字符串并复制后半部分, 这里先使指针移动`n`位, 从现在开始就是字符串后半部分了, 复制字符串到`newStr`.

$$
\begin{aligned}
& \*ps \\
& \downarrow \\
str: \Box \Box \Box & \underset{\text{现在的字符串}}{\underline{\blacksquare \Box \Box \Box \Box \Box}} \backslash 0 \Box \Box \cdots
\end{aligned}
\ \ \\
newStr = "456789"
$$

第三步追加前半部分, 这里需要将指针移动回去, 但是如果直接追加, 无法控制长度, 将一直追加到最后面, 变成`456789123456789`.

所以这里先将现在的位置写入终止符`\0`, 再将指针移动回去, 这样就只会追加前半部分了.

$$
\begin{aligned}
& \*ps \\
& \downarrow \\
str: & \underset{\text{现在的字符串}}{\underline{\blacksquare \Box \Box}} \backslash 0 \Box \Box \cdots
\end{aligned}
\ \ \\
newStr = "456789\underline{123}"
$$

### 单词统计

> 统计一行英文中单词的个数. 注意, 由于书写问题, 单词之间可能有多个空格, 标点和单词直接空格未知, 字符串前后可以有若干空格.
> 例如, 输入`   This is my favorite movie.`, 输出`5`.

我们思考如何区分单词. 显然, 单词通过`空格`, `标点`分割, 但上述说明中发现这些数目都不固定, 难以区分.

我们从前往后读字符串: 当由其他字符进入字母时, 这是一个单词的开始; 当从字母变成其他字符时(空格, 标点), 这是一个单词的结束; 而从字母到字母和字符到字符都说明不了什么.

所以我们只要统计字母变成其他字符的情况即可.

```c
int wordCounter (char *ps) {
	int count = 0;
	int isAlpha = 0;
	
	while (*p != '\0') {
		if ((*ps >= 'a' && *ps <= 'z') || (*ps >= 'A' && *ps <= 'Z')) {
            isAlpha = 1;
        } else {
            if (isAlpha) count ++;
            isAlpha = 0;
        }
        ps ++;
	}
	if (isWord == 1) count ++;
	
	return count;
}
```

这里使用`isAlpha`存储是否为字母. 当读取到字母时, 令`isAlpha = 1`, 标记为字母; 当读取到非字母时, 若`isAlpha`, 即上一个标记的是字母, 则计数一个单词, 同时令`isAlpha = 0`, 标记为非字母;

循环结束后, 还要再次进行一次判断, 防止`This is my favorite movie`这样结尾没有字符的漏判.
