四、指针与字符串

&

&可以取出一个地址,而取出的地址的大小是由当前的架构环境所确定的

字节

在64位架构之下,int为4个字节,而其所对应的地址是8个字节
在32位架构下,二者都是4个字节

&取地址

首先它不可以对没有地址的东西取地址,比如&(i+P) (i+p是变量)
换言之,必须在&的右侧,*存在一个明确的变量,才可以去取得它的地址

对于数组而言,比如有数组a[]
当我们取出地址时,&a,a,a[0]是相同的
而相邻数组之间的差距一直为4

变量地址

指针

指针地址的变量,就是保存地址的变量

1
2
3
4
5
6
7
int i ; 
int* p = &i ;

//下面两行的意思是一样的
//都是p是一个地址,而q是普通的int
int* p,q;
int *p,q;

指针变量

变量的值是内存的地址
普通变量的值是实际的值
指针变量的值是具体实际值的变量的地址

作为参数的指针

void f(int *p);
在被调用的时候需要了某个变量的地址

int i = 0 ;
f(&i);
在函数里面可以通过这个指针访问外面这个i

运算符*

*是一个单目运算符,用来访问指针的值所表示的地址上的变量
可以做右值(读)也可以做左值(写)

1
2
int k = *p ; 
*p = k+1 ;

小结

综上,我们可以使用&来取得所需要的变量的地址,而使用*来访问某个指针所指向的变量
A.
*&a -> *(&a) -> *(a的地址) ->得到地址上的变量 -> a

B.
&*a -> &(*a)-> &(指针所指向的变量) -> 得到a的地址

指针的使用

使用指针交换两个变量的值

1
2
3
4
5
6
void swap(int *pa , int *pb){
int t = *pa ;
*pa = *pb ;
*pb = t ;
//因为传入的是指针,因此在函数内部的修改可以影响出去
}

函数返回多个值

有的时候函数返回一个值是不够的,想要函数返回多个值,某些值就只能通过指针返回
传入的参数实际上就包含需要保存并带回结果的变量
再次以刚才的swap函数,我们传入的两个参数需要互换值,也就说最后需要两个返回值,于是我们就需要两个指针

虽然这些参数是主函数传进去的参数,但是它们作用的结果是把结果“带”出来

函数返回运算的状态,而结果通过指针返回

实际上和前文的意思是相同的
比如常用的情况就是让函数返回不属于有效范围内的值表示出错(比如下标是-1)
但如果返回任何值都是有效值,就无法通过返回值来表示其结果了,于是就需要分开返回
一般的做法是“运算状态”用函数返回,而实际的值通过指针参数来返回

在java或者c++中,可以通过”异常”这个机制来解决这个问题

常见错误

在任何一个地址变量被赋值/得到一个地址 之前,不能通过它(使用*)访问任何变量

指针和数组

传入的数组

实际上,在调用函数的时候,我们所传入的数组就是一个指针,这也是为什么在函数里面使用sizeof时得到的是4(32位架构)
因为此时所谓的“数组”,其实就是一个指针

事实上,如果我们把原本函数中形参的数组都改成指针,比如int a[] 改为 *a , 并不会影响编译

总言之,函数参数表中的数组实际上就是指针,sizeof(a) == sizeof(int*),而它可以用数组的运算符[]进行运算
所以,下面四种函数原型是等价的

1
2
3
4
int sum(int *ar,int n);
int sum(int * ,int);
int sum(int ar[] , int n);
int sum(int[] , int);s

数组变量是特殊的指针

数组与数组单元

数组变量本身就可以表达地址,所以使用int a[10] ; int*p = a ;的时候,无需使用&来获取其地址
但是数组内的单元,表达的都是变量需要使用&来获取地址
另外,a == &a[0]

[]运算符是可以对数组做,亦可以对指针做
p[0]:当做这里有一个数组,它所指向的第一个位置就是所需的值

1
2
3
4
int *p = &min ; //这里我们假设之前得到了一个最小值min
printf("*p = %d \n" , *p);
printf("p[0] = %d \n" , p[0]);
//此时二者得到的结果是一样的

同理,*运算符可以对指针做,亦可以对数组做

数组变量是const的指针,也就是说它不能被赋值

指针和所对应的值的const情况(C99)

指针是const

指针是const,换言之,就是指针是固定的
也就是说,该指针,指向了某个位置,这个事实是不能改变的

1
2
3
int *const q = &i ; //q指向了i
*q = 26 ; //可行,意思是q指针指的地方的值修改为26
q++; //ERROR,q指针自身的值不可变

所指的位置是const

表示的是,不能再通过这个指针去修改那个变量
值得注意的是,该操作不代表“那个变量”成为了const

1
2
3
4
const int *p = &i ; 
*p = 26 ; //ERROR,在这里(*p)是一个const
i = 26 ; //是可以的,因为i本身并不是const
p = &j ; //p指针也不是const,因此可以随意更改指向

简而言之,就是p指针,和它所指向的变量都是可以改变的
但是“通过p指针来修改该变量(*p)”这一方法是不可行的

判断方式

const在前面:它所指的东西不能被修改

1
2
const int* p1 = &i ; 
int const* p2 = &i ;

const在后面: 表示指针不能被修改

1
int *const p3 = &i ;

const+指针被用于函数

比如void f(const int* x) ;
其表示“在这个函数的范围内,保证int* x 是不会被修改的”

const与数组

比如const int a[] = {1,2,3,4,5,6} ;
实际上所谓“数组变量”就已经是const的指针了
而这里我们加入了const,代表数组内的每个单元都是const int
所以必须且只能通过初始化来赋值

所以说在把数组传到函数里面的时候,如果你不希望函数修改你的数组,则使用const

指针运算

普通加减

对于

1
2
3
4
char ac[] = {0,1,2,3,4,5,} ;
char *p = ac ;
printf("p = %p \n",p);
printf("p = %p \n" , p+1);

得到的结果(示例)是
p = 0xbffbbd5e
p+1 = 0xbffbbd5f

但是如果是

1
2
3
4
int ai[] = {0,1,2,3,4,5,} ;
int *q = ai[0] ;
printf("q = %p \n",q);
printf("q+1 = %p \n" , q+1);

得到的结果却\是
p = 0xbffbbd2c
p+1 = 0xbffbbd30
差值是4

原因是sizeof(char) = 1 ; sizeof(int) = 4
所以
指针上的+1指的是增加一个sizeof()的单位
比如此时 *p代表的是ac[0],那么*(p+1)代表的就是ac[1]

则指针和数组的转换方式为 *(p+n) –> ac[n]

实质上,如果你的指针原先并不是指向了一片连续的空间,那么这种运算是没有意义的

同理,也可以给指针使用+,+=,-,-=,++,–等等

指针之间的运算

两个指针是可以相减的(相加大概率没有实际意义)
结果并不是地址的差,而是(地址差)/sizeof()
也就是说,指针相减,表示的是二者中间有多少”这种类型的东西”

*p++

意义是“取出p所指的那个数据,然后再利用指针++,把p移到下一个位置去”

*的优先级没有++高

这个操作常用于数组类的连续空间操作,而在某些cpu上,这可以直接被翻译成一条汇编指令

比如我们就可以把遍历数组的代码写为

1
2
3
4
5
6
7
8
9
int main(void){
char ac[] = {0,1,2,3,4,5,6,7,-1} ; //最后一个-1表示这是数组的结尾
char *p = &ac[0] ;

while(*p != -1 ) {
printf("%d \n" , *p++);
}
return 0 ;
}

指针的比较

进行比较的操作,<,<= , == , > , >= , != 都可以被用于指针的比较
当我们进行指针的比较的时候,比较的是指针在内存中的地址
此外,数组中的单元的地址肯定是线性递增的

0地址

理所当然的,我们的内存中是存在0地址的,但是这个位置通常是一个不能随便用的地址

因此我们的指针不可以具有0值

由此,我们可以利用这个特性,用0地址来表示一些特殊的事情,比如:
1.返回的指针是无效的
2.指针并没有被真正初始化(先初始化为0)

在很多时候,NULL就是一个预定义好的符号,它表示0地址(在C语言的编译器里就是NULL,需要全部大写)

此外,有的编译器不愿意你用0来表示0地址,因此想这么用的时候,尽量用NULL

指针类型与大小

无论指向的是什么类型,所有的指针的大小都是一样的,因为它们本质上都是地址
但是指针存在类型的差别,不同类型的指针是不能相互赋值的
(这是避免用错指针)

不过,如果真的需要的话,是可以进行指针类型转换的

指针类型转换

void*表示不知道指向什么东西的指针,在计算时与char* 相同(但二者并不相通)
指针是具有转换类型的,比如int *p = &i ; void*q = (void*)p ;
这种操作并不会改变p所指向的变量的类型,而是让后面的程序以”不同的眼光”看p所指的变量
比如这个时候,后续的程序就不认为p指向的是int,而是认为它指向了p
(尽量不要使用)

void* : 表示这是一个指针,但不确定它指向的是什么

小结:我们可以用指针来做什么

需要传入一个较大的数据的时候的参数
传入数组后对数组进行操作
函数返回不止一个结果
需要用函数来修改不止一个变量
动态申请内存

动态分配

在C99之前的事

在C99之前,我们不可以使用变量作为数组定义的大小,因此需要手动为它分配好内存
int* a = (int*)malloc(n*sizeof(int)) ;
下面我们就尝试一下这件事情

在使用malloc之前,我们需要引入一个全新的头文件stdlib.h>

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
#include <stdio.h>  
#include <stdlib.h>

int main(void) {
int number ;
int* a ;
int i ;
printf("请输入数量:");
scanf("%d" , &number) ;

//在C99,我们可以之间 int a[number] ; 来得到数组

a = (int*)malloc(number * sizeof(int)) ; //注意,我们此时要的并不是“有多少个单元”,而是“这些单元将会占据多少空间”
//因为malloc的结果是void*,我们还需要改变它为int*

//然后利用指针和整数的等同性,我们这时候就直接把a当作数组使用即可
for(i = 0 ; i <number ; i++){
scanf("%d" , &a[i]);
}

for( i = number-1 , i>= 0 ; i--){
printf("%d " , a[i]);
}

//结束后需要把空间归还
free(a);

return 0 ;
}

malloc

来自#include <stdlib.h>
void* malloc(size_t size) ;
向malloc申请的空间的大小是以字节为单位的
返回的结果是void*,需要类型转换为自己需要的类型
(int*)malloc(n*sizeof(int))

申请失败时会返回一个0,或者NULL

free()

free是和malloc配套的函数,把申请来的空间重新归还给系统
只能还申请来的空间的首地址,也就是地址改变之后(比如p++,p–)是不可以归还的
必须归还最开始的,申请来的那个地址

为了配合,建议在初始化指针的时候都给它一个0地址,如 void *p = 0;
如此一来,若我们在运行过程中没有malloc这个指针,最终归还的时候也是free(p)也是就free(NULL),不会报错

free(NULL)总是可以的

常见问题

1.申请了不free
在小程序里面当然没有影响,但是越大越重要的程序,在长时间运行中,内存就会逐渐下降

2.free再free
要是之前已经free过了,系统会把这个地址从申请名单中删去,若是再free,就会崩溃

3.free变过的地址
前文已经说过