三、指针与字符串

指针

导入

我们从sizeof谈起:它是一个运算符``。给出某个类型或变量在内存中占据的位置,以字节大小表示(1字节=4比特) 接着是&,它实质上是一个运算符``,它能够获得变量的地址(这也意味着它的操作对象必须是变量),&不能对没有地址的东西取地址,比如a++,++a,a+b等等

简单来说,利用sizeof,我们可以得到一个类型/变量内存占据位置的大小
使用&,我们可以得到该变量在内存中的地址

顺带一提,用printf输出地址时,使用%p , 而且取出的地址大小是否和int相同取决于编译器(32位架构还是64位架构)

1
2
3
4
int i = 0 ;
printf("%p" , & i) ;

return 0 ;

指针是什么

我们一直所说的“指针”,是真正的能够存储地址的变量

指针,指的就是保存地址的变量
我们使用星号* 表示某个变量,是一个指针

1
2
3
4
5
6
7
8
9
10
11
int *p,q ; 这里面的p是指针,而q就是普通的整型  

#### 指针变量
普通变量的值是实际的值
指针变量的值是**具有实际值的变量**的**地址**

\*是一个单目运算符,用来**访问**指针的值所表示的地址上的变量
它可以做右值,也可以做左值
```c
int k = *p ;
*p = k+1 ;

由于指针变量的特殊性,我们若在函数中修改了指针变量,那么也会修改它所指向的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void f(int *p);
void g(int k);

int main(void){
int i = 6 ;
printf("&i = %p\n",&i);
f(&i);
g(i);
return 0 ;
}

void f (int *p){
printf(" p = %p\n" , p);
printf(" p = %p\n" , *p);
*p = 26 ; //就在这里,把p变量的地址指向的那个变量(就是i)改为26
}

void g (int k){
printf("k = %d \n " , k);
}

小结: 指针,地址,*和&

到这里也许有一些混乱,为了接下来内容的进行先进行一次小结:
1.什么是地址?
地址指的是”变量地址”,意思是在内存中,某个变量的值,被放置在了这里

2.怎么获得地址?
利用&操作符可以获得地址,引用一句话:

每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址

3.怎么记住地址/怎么操作这块地址/什么是指针?
利用*对某个变量进行操作,表示该变量存储着一个地址,这种变量就被叫为指针

其实意会一下就简单了,”指针”,意思就是这个变量的意义,就是”指向”某块地址

利用*在一个变量之前,就是代表这个变量是个指针(用来存地址的)
那么怎么对这个指针操作呢,一般而言,可以使用&符号来获得某个变量的地址,然后将其赋给指针(写)
而使用*,则是得到所指定地址的变量的值(读)

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值.

4.实例
来自菜鸟教程的两个简单易懂的实例
C指针

第一个实例:

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

int main ()
{
int var = 20; /* 实际变量的声明 */
int *ip; /* 指针变量的声明 */

ip = &var; /* 在指针变量中存储 var 的地址 */

printf("var 变量的地址: %p\n", &var );

/* 在指针变量中存储的地址 */
printf("ip 变量存储的地址: %p\n", ip );

/* 使用指针访问值 */
printf("*ip 变量的值: %d\n", *ip );

return 0;
}

上述代码编译会得到结果:
var 变量的地址: 0x7ffeeef168d8
ip 变量存储的地址: 0x7ffeeef168d8
*ip 变量的值: 20

第二个实例:

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

int main ()
{
int var_runoob = 10;
int *p; // 定义指针变量
p = &var_runoob;

printf("var_runoob 变量的地址: %p\n", p);
return 0;
}

得到结果:
var_runoob 变量的地址: 0x7ffeeaae08d8
这里用图说明即是

指针与数组

函数的参数表中的数组,实质上就是个指针(这也是为什么写a[]和a[10]之类的是一样的),
因此在函数中我们不能直接用sizeof得到正确的数组长度

函数参数表中的数组实际上是指针,但是可以用数组的运算符[]来运算

数组变量是特殊的指针,这使得它有如下性质
1.数组变量本身表达地址,所以我们取数组的地址时无需使用&

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
int a[10] ; 
int *p = a ;
```

2.但是数组的单元表达的是变量,我们需要用&来取它。数组a的地址,等于数组单元a[0]的地址
> 可以想象为数组是一系列连续的指针地址构成的,其中第一位(下标为0的)那一位代表整个数组的开始
3.*运算符可以对指针做,也可以对数组做
4.数组变量是const的指针,所以不能被**赋值**

## 字符类型

### CHAR
char是最小的整数类型,同时也是一特殊的类型:字符
原因在于:
1.用单引号表示的字符字面量 'a', '1'
2.''也是字符
3.printf scanf 里用```%c```来输入输出字符

```c
char c ;
char d ;
c = 1 ;
d = '1' ;

printf("c = %d \n" , c) ; // 结果是1
printf("d = %d \n" , d) ; // 结果是49,对应1在ASCII的值

以上的两个1,一个是整型,而另外一个是字符(因此d打印出来是49)

%c 表示以字符的形式输入/输出

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
char c = 'A' ; 
printf("%c \n" , c) ;
c++ ;
printf("%c \n" , c) ;

int i = 'Z' - 'A' ;
printf("%d \n" ,i ) ;s

```
> a+'a'-'A' 可以把一个大写字母变成小写字母
> a+'A'-'a' 可以把一个小写字母变成大写字母

### 逃逸字符
逃逸字符用来表示无法印出来的控制字符或特殊字符,它由一个反斜杆"\\" 开头 , 后面跟上对应的字符

\\b 回退一格
\\t 到下一表格位 (也就是制表位上的位置,是每行固定的位置(试着敲一下tab),利用\\t 可以使上下行对齐)
\\n 换行
\\r 回车
\\" 双引号
\\' 单引号
\\\ 反斜杠本身

## 字符串
在C语言中,字符串指以0(整数0)结尾的一串字符
0和'\\0'是一样的,但是和'0'是不一样的
0标志着字符串的结束,但是它不是字符串的一部分,计算字符串长度的时候也不包含这个0
字符串以数组的形式存在,也以数组或指针的形式访问(更多的是以指针的形式)
在string.h中有很多处理字符串的函数

### 字符串变量
我们有多种方式表达字符串
```c
char *str = "Hello" ;
char word[] = "Hello" ;
char line[10] = "Hello" ;
```

这里面 "Hello"被称为**字符串常量**,"Hello"会被编译器变成一个字符数组放在某处,这个数组长度是6,结尾还有表示结束的0(Hello五位,0一位,共六位)
两个相邻的字符常量会自动连接

### 小结
C语言的字符串是以字符数组的形态存在的,不能用运算符对字符串做运算,通过数组的方式可以遍历字符串
唯一特殊的地方是字符串字面量可以用来初始化字符数组
以及标准库提供了一系列字符串函数

### 字符串常量(续)
```c
char* s = "Hello , world!"; //我要指向某个地方的字符串
```
s是一个指针,初始化为指向一个字符串常量
由于这个常量所在的地方,实质上是s是const char* s , 不过由于历史原因,编译器接受不带const的写法
但是当我们试图对s所指的字符串做写入的时候会导致严重后果
当我们编译过程中有两个相同的字符串(比如s1 s2 两个字符串都是Hello world),它们会指向同一个地方

如果想要制作一个能修改的字符串,那么在**一开始**就需要用**数组**定义
```c
char s[] = "Hello, world!" ; //某个地方的字符串就在这里
```

##### 区别
```c
int i =0 ;
char *s = "Hello , World";
char *s2 = "Hello,World" ;
char s3[] = "Hello,World";

printf("&i=%p\n", &i) ;
printf("&s =%p\n", &s) ;
printf("&s2=%p\n", &s2) ;
printf("&s3=%p\n", &s3) ;

s3[0] = 'B' ;
printf("Here!s3[0] = %c\n",s3[0]);

return 0 ;
```
该部分输出会类似:
&i=0x7ffe7f63052c
&s =0x7ffe7f630520
&s2=0x7ffe7f630518
&s3=0x7ffe7f63050c
Here!s3[0] = B


数组字符串:这个字符串在这,作为本地变量会被自动回收
指针字符串:不知道这个字符串在哪,需要处理参数,可以动态分配空间

如果要构造一个字符串-->数组
如果要处理一个字符串-->指针

>字符串可以表达为char\*的形式,char\*不一定是字符串,只有在它所指的字符数组有结尾0,我们才能说它所指的是字符串


## 字符串计算

### 赋值
```c
char *t = "title" ;
char *s ;
s = t ;
```
实际上并没有产生新的字符串,只是让指针s指向了t所指的字符串。对s的任何操作就是对t做的,**因为二者指向同一块地址**

### 输入输出
%s代表输入输出的是字符串
```c
char string[8];
scanf("%s",string);
printf("%s",string);
```
>scanf读入一个单词,到空格、tab、回车为止

想要在空格tab回车之后继续读,我们需要再来一个scanf,而且第二个scanf是不会读到"空格tab回车"的
但是scanf实质上是不安全的,因为不知道要读入的内容的长度
在百分号和s中间,可以增加一个数字,表示我们希望最多可以读入多少字符,以此提高安全性。此时就不一定是以空格tab回车来区分了,读完了,这个scanf就结束了

##### 常见错误
```c
char *string ;
scanf("%s",string);

因为char*是字符串类型,定义了一个字符串变量string就可以直接使用了,但实际上这种做法是十分危险的,因为你不知道使用者会读入多少内容

char buffer[100] = ""; //空字符串,buffer[0] == '\0' 
char buffer[] = "" ;//数这个数组的长度只有1!

字符串_附录

字符串相关的更多补充请见《五、字符串补充》