什么是指针
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样。
例如 int 占用4个字节,char 占用1个字节
为了正确地访问这些数据,必须知道它们在内存中的准确位置,这个位置就叫做地址(Address)
或指针(Pointer)
内存地址通常使用16进制数表示,地址从0开始依次增加,对于32位环境,程序所能使用的内存为4G,最小的地址为0,最大的地址为0xFFFFFFFF
下面的代码演示了如何输出一个地址:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int a = 10;
int b[5] = {0, 1, 2, 3, 4};
char str1[] = "https://www.zfl9.com";
char *str2 = "https://www.zfl9.com";
printf("a: %p\nb: %p\nstr1: %p\nstr2: %p\n", &a, b, str1, str2);
return 0;
}
1
2
3
4
5
$ ./a.out
a: 0x7ffcafdb1fe4
b: 0x7ffcafdb1fd0
str1: 0x7ffcafdb1fb0
str2: 0x400650
%p
表示输出一个地址,也可以用%#x
,&
是取地址符,因为数组名本身就表示数组的首地址,所以不需要&
指针变量
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量
在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。
指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量
定义指针变量:type *ptr;
、type *ptr = value;
*
表示变量ptr
是一个指针变量,而type
是该指针指向的数据类型
如:int *p
,p是一个指针变量,指向的数据类型为int,是一个整型指针
注意,指针变量和普通变量没有什么区别,都是变量,只不过存放的数据类型不同而已
- 对于
int a
,它定义了一个变量a
,数据的类型为int
- 对于
int a[6]
,它定义了一个变量a
,是一个拥有6个元素的数组,元素的数据类型为int
,数组的数据类型为int [6]
- 对于
int *p
,它定义了一个变量p
,数据的类型为int *
给指针变量赋值
1
2
int a = 10;
int *p = &a;
指针变量需要的是一个地址,所以需要使用&
取出变量a
的地址,并将其赋给变量p
同时,指针变量的值可以随时改变,和普通的变量一样,可以多次赋值
1
2
3
4
int a = 10, b = 20, c = 30;
int *p = &a;
p = &b;
p = &c;
通过指针变量取得数据
1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
int a = 10;
int *p = &a;
printf("addr: %p, var: %d\n", p, *p);
return 0;
}
1
2
$ ./a.out
addr: 0x7fffadfc0fd4, var: 10
*
是指针运算符,用来取得某个地址上的数据
使用指针是间接获取数据,要经过两步运算,而通过变量名是直接获取数据
指针除了可以获取数据,还能修改数据
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int a = 10;
int *p = &a;
*p = 100;
printf("addr: %p, var: %d\n", p, *p);
return 0;
}
1
2
$ ./a.out
addr: 0x7ffedc87fe64, var: 100
通过指针交换两个变量的值
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int a = 10, b = 20, temp;
printf("swap before: a = %d, b = %d\n", a, b);
int *pa = &a, *pb = &b;
temp = *pa;
*pa = *pb;
*pb = temp;
printf("swap afer: a = %d, b = %d\n", a, b);
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
swap before: a = 10, b = 20
swap afer: a = 20, b = 10
指针变量的运算
指针变量保存的是一个地址,本质上是一个整数,可以进行部分运算,如:加法,减法,比较等
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
char a = 'a', *pa = &a;
int b = 10, *pb = &b;
double c = 3.14, *pc = &c;
printf("pa: %p, pb: %p, pc: %p\n", pa, pb, pc);
pa++; pb++; pc++;
printf("pa: %p, pb: %p, pc: %p\n", pa, pb, pc);
pa--; pb--; pc--;
printf("pa: %p, pb: %p, pc: %p\n", pa, pb, pc);
return 0;
}
1
2
3
4
5
6
$ gcc a.c
$ ./a.out
pa: 0x7fff4574be77, pb: 0x7fff4574be70, pc: 0x7fff4574be68
pa: 0x7fff4574be78, pb: 0x7fff4574be74, pc: 0x7fff4574be70
pa: 0x7fff4574be77, pb: 0x7fff4574be70, pc: 0x7fff4574be68
可以看出,pa、pb、pc每次加1,它们的地址分别加1、4、8,正好是char、int、double类型的长度
而每次减1,它们的地址分别减1、4、8,也正好是char、int、double类型的长度
看来指针偏移量与指针所指的数据类型相关,刚好等于数据类型的长度,而不仅仅是简单的算数加减,这就使得指针的加减运算有了实际意义
不过对于指向普通变量的指针,对其进行加减运算没有什么意义,因为我们并不知道它们前后都是什么数据
但是对于指向数组的指针,由于数组的元素时连续存储的,这时候就可以用指针来访问元素了
除了加减,还可以对指针变量进行比较,也就是比较它们的值,也就是数据的地址
但不可以对指针变量进行乘法、除法、取余等运算,没有实际意义,编译器也会报错
数组指针
数组指针:也就是指向数组的指针(严格来说,是指向数组的首个元素)
数组(Array)是一系列相同数据类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。
在定义数组的时候,需要给出数组名和数组长度,数组名在有些时候可以认为是一个指针,它指向数组的第0个元素,即数组的首地址。
下面的例子演示了通过指针来访问数组:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int a[5] = {0, 1, 2, 3, 4};
int i, len = sizeof(a) / sizeof(int);
for(i=0; i<len; i++){
printf("index: %d, var: %d\n", i, *(a + i));
}
return 0;
}
1
2
3
4
5
6
7
8
$ gcc a.c
$ ./a.out
index: 0, var: 0
index: 1, var: 1
index: 2, var: 2
index: 3, var: 3
index: 4, var: 4
*(a + i)
等价于a[i]
也可以定义一个指针,指向数组,通过指针访问数组
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int a[5] = {0, 1, 2, 3, 4};
int *p = a;
int i, len = sizeof(a) / sizeof(int);
// int i, len = sizeof(p) / sizeof(int); 错误,sizeof(p)求得的只是一个指针变量的长度,而不是数组的长度
for(i=0; i<len; i++){
printf("index: %d, var: %d\n", i, *(p + i));
}
return 0;
}
1
2
3
4
5
6
7
8
$ gcc a.c
$ ./a.out
index: 0, var: 0
index: 1, var: 1
index: 2, var: 2
index: 3, var: 3
index: 4, var: 4
将数组作为指针使用时,该指针变量被 const 修饰,不能改变指针自身的值,只能操作指向的数据
比如,我们可以使用指针,通过自增来遍历数组元素
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int a[5] = {0, 1, 2, 3, 4};
int *p = a;
int i;
for(i=0; i<5; i++){
printf("%d ", *(p++));
}
printf("\n");
return 0;
}
1
2
3
4
$ gcc a.c
$ ./a.out
0 1 2 3 4
但是换成数组名就不行了,因此下面的代码编译就会报错
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
int a[5] = {0, 1, 2, 3, 4};
int i;
for(i=0; i<5; i++){
printf("%d ", *(a++)); //编译错误
}
printf("\n");
return 0;
}
假设指针p指向数组a的第n个元素,那*p++
,*++p
,(*p)++
分别代表什么?
对于*p++
,++
优先级比*
高,等价于*(p++)
,由于是后自增,所以先进行其他操作,最后进行自增操作,所以是先取得第n个元素的值,然后再将指针p指向第n+1个元素
对于*++p
,等价于*(++p)
,前自增,先将p指向第n+1个元素,然后再取第n+1个元素的值
对于(*p)++
,先取得第n个元素的值,然后将第n个元素的值加1
字符串指针
字符串指针:也就是指向字符串的指针
由于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
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "https://www.zfl9.com";
char *p = str;
int i, len = strlen(str);
for(i=0; i<len; i++){
printf("%c", str[i]);
}
printf("\n");
for(i=0; i<len; i++){
printf("%c", *(str + i));
}
printf("\n");
for(i=0; i<len; i++){
printf("%c", p[i]);
}
printf("\n");
for(i=0; i<len; i++){
printf("%c", *(p + i));
}
printf("\n");
return 0;
}
1
2
3
4
5
6
7
$ gcc a.c
$ ./a.out
https://www.zfl9.com
https://www.zfl9.com
https://www.zfl9.com
https://www.zfl9.com
c语言还支持另外一种表示字符串的方法,就是直接用指针指向一个字符串,叫做字符串常量
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
char str1[] = "hello, world!";
char *str2 = "www.zfl9.com";
printf("%s\n", str1);
printf("%s\n", str2);
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
hello, world!
www.zfl9.com
字符串的所有字符在内存中都是连续存储的,和数组一样
注意字符数组和字符串常量的区别,字符数组的内容是可写的,而字符串常量的内容是只读的
因为字符数组存放在内存的栈区(局部变量)或全局数据区(全局变量),拥有读写权限
而字符串常量中的字符串存放在常量区域,常量区域只有读的权限,不能写入
比如,下面的代码尝试修改字符串常量的值,这会发生段错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(){
char str1[] = "hello, world!";
char *str2 = "www.zfl9.com";
printf("%s\n", str1);
printf("%s\n", str2);
str1[0] = 'H';
printf("%s\n", str1);
str2[0] = 'W';
printf("%s\n", str2);
return 0;
}
1
2
3
4
5
6
7
$ gcc a.c
$ ./a.out
hello, world!
www.zfl9.com
Hello, world!
[1] 76302 segmentation fault ./a.out
再次强调,字符串常量是字符串本身不能被修改,但是指向它的字符指针可以改变指向,指向别的数据
如果需要经常修改字符串,那么应该选择字符数组,如果不需要经常变更字符串的值,就选择字符串常量
数组的各种访问方式
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
#include <stdio.h>
int main(){
char str[20] = "www.zfl9.com";
char *s1 = str;
char *s2 = str+2;
char c1 = str[4];
char c2 = *str;
char c3 = *(str+4);
char c4 = *str+2;
char c5 = (str+1)[5];
int num1 = *str+2;
long num2 = (long)str;
long num3 = (long)(str+2);
printf(" s1 = %s\n", s1);
printf(" s2 = %s\n", s2);
printf(" c1 = %c\n", c1);
printf(" c2 = %c\n", c2);
printf(" c3 = %c\n", c3);
printf(" c4 = %c\n", c4);
printf(" c5 = %c\n", c5);
printf("num1 = %d\n", num1);
printf("num2 = %ld\n", num2);
printf("num3 = %ld\n", num3);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
$ ./a.out
s1 = www.zfl9.com
s2 = w.zfl9.com
c1 = z
c2 = w
c3 = z
c4 = y
c5 = l
num1 = 121
num2 = 140727275501696
num3 = 140727275501698
s1 是指向数组 str 第 0 个元素的指针,所以输出 www.zfl9.com
s2 是指向数组 str 第 2 个元素的指针,所以输出 w.zfl9.com
c1 是数组 str 第 4 个元素的值,也就是 z
c2 是数组 str 第 0 个元素的值,也就是 w
c3 是数组 str 第 4 个元素的值,也就是 z
c4 是数组 str 第 0 个元素的值加2,也就是 w 的 ASCII 码加2,也就是字符 y
c5 是先计算 (str+1),指向数组的第 1 个元素,再转换为 *(str + 1 + 5),取得第 6 个元素的值,为 l
num1 是第 0 个元素的值,也就是字符 w 的 ASCII 码加 2,为 121
num2 是数组 str 第 0 个元素的地址
num3 是数组 str 第 2 个元素的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
char str[20] = {0};
int i;
for(i=0; i<10; i++){
str[i] = 97 + i; // 97为字符a的ASCII码值
}
printf("%s\n", str);
printf("%s\n", str+2);
printf("%c\n", str[2]);
printf("%c\n", (str+2)[2]);
return 0;
}
1
2
3
4
5
6
7
$ gcc a.c
$ ./a.out
abcdefghij
cdefghij
c
e
(str+2)[2]
相当于*(str+2+2) == *(str+4) == str[4] == 'e'
指针作为函数参数
在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针
用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁
像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合
有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值
如果不使用指针,就不能改变函数外的变量的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
void swap(int m, int n){
int temp;
temp = m;
m = n;
n = temp;
}
int main(){
int m = 10, n = 20;
printf("before: m = %d, n = %d\n", m, n);
swap(m, n);
printf("after: m = %d, n = %d\n", m, n);
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
before: m = 10, n = 20
after: m = 10, n = 20
而通过指针就能交换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
void swap(int *p1, int *p2){
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(){
int m = 10, n = 20;
printf("before: m = %d, n = %d\n", m, n);
swap(&m, &n);
printf("after: m = %d, n = %d\n", m, n);
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
before: m = 10, n = 20
after: m = 20, n = 10
用数组作函数参数
数组是一组数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,就应该传递数组指针
下面的例子中,max() 函数用于查找数组中值最大的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int max(int *array, int len){
int i, max = array[0]; // 假定 max = array[0]
for(i=1; i<len; i++){
if(array[i] > max){
max = array[i];
}
}
return max;
}
int main(){
int array[6], i, maxVal;
int len = sizeof(array) / sizeof(int);
printf("enter 6 nums: ");
for(i=0; i<len; i++){
scanf("%d", array + i);
}
maxVal = max(array, len);
printf("max: %d\n", maxVal);
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
enter 6 nums: 12 55 30 8 93 27
max: 93
参数 array 仅仅是一个数组指针,在函数内部无法通过该指针获得数组长度,必须传入一个数组长度的参数
int max(int *array, int len)
也可写成 int max(int array[6], int len)
、int max(int array[], int len)
看似好像传入了整个数组,但无论那种方式都不会创建一个数组,他们都会转换为一个int *
类型的指针
为什么C语言不允许传递整个数组的元素呢?
这是考虑到效率问题,由于数组的元素个数并没有限制,可能很小也可能很大
而参数传递的本质实际上是一次赋值的过程,也就是内存的拷贝,对于基本数据类型和一些其他的类型,数据量也就几个字节,拷贝很快
而拷贝一个很长的数组,既浪费时间又浪费空间,所以为了不影响效率,C语言不允许传递数组所有的元素
指针作为函数返回值
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数
下面的例子定义了一个函数 longStr(),用于返回两个字符串中较长的那个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
char *longStr(char *str1, char *str2){
int len1 = strlen(str1), len2 = strlen(str2);
return len1 > len2 ? str1 : str2;
}
int main(){
char str1[50], str2[50], *str;
printf("string1: ");
scanf("%[^\n]", str1);
printf("string2: ");
setbuf(stdin, NULL);
scanf("%[^\n]", str2);
str = longStr(str1, str2);
printf("longStr: %s\n", str);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
$ gcc a.c
$ ./a.out
string1: 12345 12345
string2: 1234 123 1
longStr: 12345 12345
$ ./a.out
string1: www zfl9 com
string2: https://www.zfl9.com
longStr: https://www.zfl9.com
二级指针
所谓的二级指针就是指向指针的指针
指针可以指向一份普通类型的数据,例如 int
、double
、char
等,也可以指向一份指针类型的数据,例如 int *
、double *
、char *
等
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针
假设有一个整型变量 a,p 是指向 a 的指针,pp 是指向 p 的指针:
1
2
3
int a = 10;
int *p = &a;
int **pp = &p;
指针变量也是一种变量,也会占用存储空间,也可以使用 & 获取它的地址
C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号
p1 是一级指针,指向普通类型的数据,定义时有一个星号
p2 是二级指针,指向一级指针 p1,定义时有两个星号
实际开发中常用的也就是一级指针和二级指针,二级以上比较少用
想要获取指针指向的数据时,一级指针加一个星号,二级指针加两个星号,三级指针加三个星号,以此类推
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(){
int a = 10;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
printf("var of a: %d, %d, %d, %d\n", a, *p1, **p2, ***p3);
printf("addr of p2: %p, %p\n", &p2, p3);
printf("addr of p1: %p, %p, %p\n", &p1, p2, *p3);
printf("addr of a: %p, %p, %p, %p\n", &a, p1, *p2, **p3);
return 0;
}
1
2
3
4
5
6
7
$ gcc a.c
$ ./a.out
var of a: 10, 10, 10, 10
addr of p2: 0x7fff58ac1b80, 0x7fff58ac1b80
addr of p1: 0x7fff58ac1b88, 0x7fff58ac1b88, 0x7fff58ac1b88
addr of a: 0x7fff58ac1b94, 0x7fff58ac1b94, 0x7fff58ac1b94, 0x7fff58ac1b94
NULL 指针
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕
指针使用之前一定要记得初始化,使用未初始化的指针是非常危险的,因为未初始化的指针乱指一气,运行程序时,在Linux下表现为段错误
,在Windows下直接奔溃
强烈建议对未初始化的指针变量指向NULL
,如int *p = NULL;
NULL
是零值、等于零的意思,在C语言中表示空指针
从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果
很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息
其实,NULL
是在stdio.h
中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
(void *)0
表示把数值 0 强制转换为void *
类型,最外层的()把宏定义的内容括起来,防止发生歧义
从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据
C语言没有规定 NULL 的指向,只是大部分标准库都约定成俗地将 NULL 指向 0,不要将 NULL 和 0 等同
void 指针
我们知道,void 可以表示函数没有返回值,也可以表示函数不接受参数
但当 void 与指针一起使用时(void *
),它表示指向的数据类型是未知的
void *
变量通常被用来存储、传递内存地址,而不关心指向的数据类型以及长度
如果要对 void *
变量解引用,需要先将其转换为指向具体类型的指针,如 int *
指针数组
指针数组:是一个数组,数组的元素是一个指针
type *array[len];
由于[]
的优先级比*
高,所以应理解为type *(array[len]);
括号里面的array[len]
说明这是一个数组,外面的type *
说明数组元素的类型为指针
比如int *a[5];
:a 是一个数组,拥有 5 个元素,每个元素的数据类型为 int *
除了元素的数据类型不同,指针数组和普通数组没有区别
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int a = 10, b = 20, c = 30;
int *array[3] = {&a, &b, &c};
int **p = array; // 指向指针数组的指针
printf("%d, %d, %d\n", *array[0], *array[1], *array[2]);
printf("%d, %d, %d\n", **(p + 0), **(p + 1), **(p + 2));
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
10, 20, 30
10, 20, 30
int **p = array;
应该理解为int *(*p)
,括号里的*p
表示这是一个指针,括号外的int *
表示指针指向的数据类型为一个整型指针
指针数组
还可以和字符串数组
结合使用,请看下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
char *strings[3] = {
"www.zfl9.com",
"www.zflyun.com",
"www.otokaze.top"
};
int i;
for(i=0; i<3; i++){
printf("%s\n", strings[i]);
}
return 0;
}
1
2
3
4
5
6
$ gcc a.c
$ ./a.out
www.zfl9.com
www.zflyun.com
www.otokaze.top
需要注意的是,字符数组 strings 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的
指针数组和二级指针
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
#include <stdio.h>
int main(){
char *lines[5] = {
"COSC1283/1284",
"Programming",
"Techniques",
"is",
"great fun"
};
char *str1 = lines[1];
char *str2 = *(lines + 3);
char c1 = *(*(lines + 4) + 6);
char c2 = (*lines + 5)[5];
char c3 = *lines[0] + 2;
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf(" c1 = %c\n", c1);
printf(" c2 = %c\n", c2);
printf(" c3 = %c\n", c3);
return 0;
}
1
2
3
4
5
6
7
8
$ gcc a.c
$ ./a.out
str1 = Programming
str2 = is
c1 = f
c2 = 2
c3 = E
lines 是一个拥有 5 个元素的数组,数组元素的数据类型为 char *
,即各个字符串的首地址
str1:lines[1]
,即数组 lines 的第1个元素,Programming 的首地址,所以输出 Programming
str2:*(lines + 3)
,等价于lines[3]
,即数组 lines 的第3个元素,is 的首地址,输出 is
c1:*(*(lines + 4) + 6)
,先看*(lines + 4)
,即数组 lines 的第4个元素,great fun 的首地址,然后加6,表示指针往后偏移6个长度,然后再取值,即 f
c2: (*lines + 5)[5]
,*lines
等价于*(lines+0)
等价于lines[0]
,即第0个元素,COSC1283/1284 的首地址,然后指针往后偏移5个长度,然后看到[5]
,等价于*(lines[0] + 5 + 5)
,即字符2
c3:*lines[0] + 2
,等价于*(lines[0]) + 2
,括号内表示数组第0个元素,即 COSC1283/1284 首地址,也就是字符 C 的地址,然后再取它的值,即字符 C,然后与数值2相加,即ASCII码67 + 2 = 69,即字符 E
二维数组指针
二维数组指针:它是一个指针,指向一个二维数组
1
2
3
4
5
6
7
int a[3][4] = {
{1, 2, 3, 4},
{2, 2, 3, 4},
{3, 2, 3, 4}
};
int (*p)[4] = a;
由于[]
优先级比*
高,所以这个括号是必须的
首先看*p
,表示这是一个指针,然后看int [4]
,表示这个指针指向一个拥有4个元素的数组
对指针 p 进行加1时,因为指向的数据类型为 int [4],所以前进的长度为 4 * 4 = 16 字节,刚好指向数组 a 下一个元素,减1同理
1) p 指向数组 a 开头,p + 1 指向数组下一行,p - 1 指向数组上一行
2) *(p + 1)
表示取地址上的数据,也就是第1行的数据,注意是一行数据,也就是一个一维数组,是由4个 int 类型组成的集合,所以用 sizeof 求它的长度为 4 * 4 = 16 字节
3) *(p + 1) + 1
,表示的是第1行第1个元素的地址,上面说了,*(p + 1)
是一个一维数组,还记得数组什么时候会转换为指向第0个元素的地址吗,除了数组定义、sizeof、&中,其他时候都会转换为指向第0个元素的指针,所以在这里就转换为了指针,然后加1,表示是一个指向第1行第1个元素的指针
4) *(*(p + 1) + 1)
,很明显,加了*
操作符,表示第1行第1个元素的值
1
2
3
a + i == p + i
a[i] == p[i] == *(a + i) == *(p + i)
a[i][j] == p[i][j] == *(a[i] + j) == *(p[i] + j) == *(*(a + i) + j) == *(*(p + i) + j)
使用二维数组指针遍历二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main(){
int a[3][4] = {
1, 2, 3, 4,
2, 2, 3, 4,
3, 2, 3, 4,
};
int i, j;
int (*p)[4] = a;
for(i=0; i<3; i++){
for(j=0; j<4; j++){
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
return 0;
}
1
2
3
4
5
6
$ gcc a.c
$ ./a.out
1 2 3 4
2 2 3 4
3 2 3 4
函数指针
函数指针,即指向函数的指针
本质上,变量名,数组名,函数名都是地址的助记符,但是我们认为变量名表示数据本身,而数组名、函数名分别表示数组首个元素的地址、函数的入口地址
我们也可以把一个指针指向一个函数,然后通过指针来调用函数
定义的形式为:return_type (*ptr)(param_list)
其中参数列表可以只给出参数的类型,省略参数名,这和函数声明类似
用指针来实现对函数的调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int max(int a, int b){
return a > b ? a : b;
}
int main(){
int a, b;
printf("enter 2 nums: ");
scanf("%d %d", &a, &b);
int (*p)(int, int) = max;
printf("max: %d\n", p(a, b));
return 0;
}
1
2
3
4
5
$ gcc a.c
$ ./a.out
enter 2 nums: 10 20
max: 20
复杂的指针声明
1
2
3
4
5
int *p[6]; // 指针数组
int *(p[6]); // 指针数组,等价于上面的形式
int (*p)[6]; // 二维数组指针
int (*p)(int, int); // 函数指针
int (*p)(int a, int b); //函数指针,等价于上面的形式
C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析
对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!
与指针相关的运算符优先级,由高到低
- 定义中被
()
扩起来的部分 - 后缀操作符:括号
()
表示这是一个函数,方括号[]
表示这是一个数组 - 前缀操作符:星号
*
表示这是一个指针变量
1) int *p[6];
从 p 开始理解,编译器先解析 p[6],p 是一个拥有6个元素数组,然后解析 int *
,它说明这个数组的元素的数据类型为 int *
,也就是指向 int 的指针,所以,从整体上讲,这是一个拥有6个元素的数组,数组元素的数据类型为整型指针,也就是指针数组
2) int (*p)[6];
从 p 开始理解,编译器先解析 *p,p 是一个指针,然后解析 int [6],它说明这个数组的元素的数据类型为 int [6],每个元素都是一个拥有6个元素的一维数组,所以,从整体上讲,这是一个指针,该指针指向拥有6个元素的一维数组,也就是说,这是一个二维数组指针
3) int (*p)(int, int);
从 p 开始理解,编译器先解析 *p,p 是一个指针,然后解析 int (int, int),说明 p 指向一个函数,该函数的参数列表为两个整数,而前面的 int 说明该函数的返回值类型为 int,所以,从整体上讲,这是一个指针,该指针指向一个函数原型为 int func(int, int) 的函数,也就是说,这是一个函数指针
4) char *(* c[10])(int **p);
从 c 开始理解,c[10]
表示这是一个拥有10个元素数组,然后解析 (* c[10])
,前面的 *
说明该数组的元素的是一个指针,但是指针指向的数据类型还不确定,剩下 char * (int **p)
,这是一个函数,函数参数为 int **p
,返回值类型为 char *
,所以,整体上讲,这是一个拥有10个元素的指针数组
,每个元素(指针)都指向函数原型为 char *func(int **p)
的函数
5) int (*(*(*pfunc)(int *))[5])(int *);
从 pfunc 开始理解,(*pfunc)
表示这是一个指针,然后看它外面一层的* (int *)
,这是一个函数,函数参数为 int *
,返回值为一个指针,但是还不知道该指针指向的是什么数据类型,然后看它外面一层* [5]
,这说明该函数返回的指针指向一个指针数组,那么这个指针数组的元素又是什么类型呢?继续看,int (int *)
,这是一个函数,函数参数为 int *
,返回值类型为 int,所以,这个指针数组的元素(指针)指向一个函数原型为int func(int *)
的函数,综上所述,pfunc是一个函数指针
,该函数的返回值是一个指针,它指向一个指针数组,指针数组的元素(指针)指向原型为int func(int *)
的函数
指针与 const
有时候我们希望定义这样一种变量,它的值不能被改变,在整个作用域中都保持固定
例如,用一个变量来表示班级的最大人数,或者表示缓冲区的大小
为了满足这一要求,可以使用const关键字对变量加以限定
const int MAX = 100;
这样 MAX 的值就不能更改了,在后续的代码中,如果尝试更改它的内容,编译器会报错
我们经常将const变量称为常量,定义const常量的格式为:
const type name = value;
(推荐),或 type const name = value;
由于常量一旦被创建后其值就不能再改变,所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误
const 和指针一起使用
const int *ptr
:表示指针指向的数据是只读的,但是指针本身可以改变指向int const *ptr
:同上int *const ptr
:表示指针本身是只读的,但是指针指向的数据是可以读写的
const 和函数的指针参数
在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替
const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制
在C语言标准库中,有很多函数的形参都被 const 限制了
我们自己在定义函数时也可以使用 const 对形参加以限制,例如查找字符串中某个字符出现的次数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>
void strnchr(const char *str, char chr){
int count = 0;
int len = strlen(str);
for(int i=0; i<len; i++){
if(str[i] == chr){
count++;
}
}
printf("%d\n", count);
}
int main(){
char str[] = "www.zfl9.com", chr = 'w';
strnchr(str, chr);
return 0;
}
根据 strnchr() 的功能可以推断,函数内部要对字符串 str 进行遍历,不应该有修改的动作,用 const 加以限制,不但可以防止由于程序员误操作引起的字符串修改,还可以给用户一个提示,函数不会修改你提供的字符串,请你放心
const 和非 const 类型转换
当一个指针变量 str1 被 const 限制时,并且类似const char *str1
这种形式,说明指针指向的数据不能被修改
如果将 str1 赋值给另外一个未被 const 修饰的指针变量 str2,就有可能发生危险
因为通过 str1 不能修改数据,而赋值后通过 str2 能够修改数据了,意义发生了转变,所以编译器不提倡这种行为,会给出错误或警告
也就是说,const char *
和char *
是不同的类型,不能将const char *
类型的数据赋值给char *
类型的变量
但反过来是可以的,编译器允许将char *
类型的数据赋值给const char *
类型的变量
这种限制很容易理解,char *
指向的数据有读取和写入权限,而const char *
指向的数据只有读取权限,降低数据的权限不会带来任何问题,但提升数据的权限就有可能发生危险
不过,我们依旧可以通过指针修改 const 变量的值,因为 const 仅仅是写给编译器看的,只要能够获得它的地址就能修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main(void) {
const int arr[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)arr, len = sizeof(arr) / sizeof(int);
for (int i = 0; i < len; i++)
printf("%d, ", ptr[i]);
printf("\b\b \n");
for (int i = 0; i < len; i++)
ptr[i] += 100;
for (int i = 0; i < len; i++)
printf("%d, ", ptr[i]);
printf("\b\b \n");
return 0;
}
1
2
3
4
5
$ gcc -o main main.c
$ ./main
1, 2, 3, 4, 5
101, 102, 103, 104, 105
带参数的main()函数
我们之前的程序中,main()函数都是没有参数的,其实main()函数有两种标准函数原型
1
2
int main();
int main(int argc, char *argv[]);
如果要向程序传递命令行参数,就要使用第二种原型
第一个参数 int argc,是参数的个数,就算不传递参数,系统也会默认传递一个参数:当前执行文件的文件名,所以最小也为1
第二个参数 char *argv[],这是一个指针数组,每一个指针都指向一个字符串的首地址,第0个元素总是当前可执行文件的文件名的首地址
1
2
3
4
5
6
#include <stdio.h>
int main(int argc, char *argv[]){
printf("argc: %d, argv[0]: %s\n", argc, argv[0]);
return 0;
}
1
2
3
4
$ gcc a.c
$ ./a.out
argc: 1, argv[0]: ./a.out
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc, char *argv[]){
int i;
for(i=0; i<argc; i++){
printf("第 %d 个参数为: %s\n", i, argv[i]);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
$ gcc a.c
$ ./a.out
第 0 个参数为: ./a.out
$ ./a.out 1 2 3
第 0 个参数为: ./a.out
第 1 个参数为: 1
第 2 个参数为: 2
第 3 个参数为: 3
指针总结
指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。
指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。
程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符
在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址
程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址
定义 | 含义 |
---|---|
int *p; | p是一个整型指针,它可以指向int 类型的数据,也可以指向类似int arr[n] 的数组 |
int **p; | p是一个二级指针,指向int * 类型的数据 |
int *p[n]; | p是一个指针数组,数组的每个指针都指向类型为int * 的数据 |
int (*p)[n]; | p是一个二维数组指针 |
int *p(); | p是一个函数,它的返回值类型为int * |
int (*p)(); | p是一个函数指针,指向原型为int func() 的函数 |
1) 指针变量可以进行加减运算,例如 p++
、p+i
、p-=i
。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关
2) 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如 int *p = 1000 是没有意义的,使用过程中一般会导致程序崩溃(段错误)
3) 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL
4) 两个指针变量可以相减,如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数
5) 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名表示整个数组,表达式中的数组名会被转换为指针