说明:
最近学校课程开设了《数据结构》的课程,无疑,数据结构的重要性在IT生涯的进阶路上是不可置疑的,也常说,数据结构是专业性与非专业性的分界线。所以无论以后走的是什么方向,毕竟是读计算机专业的,所以必须学好数据结构的。虽然目前我给自己定的方向是走运维/系统架构方向的,可有句话说得好,不懂开发的运维注定会被淘汰,在IT这一行,要让自己变得更加强大。最近也一直在学Python,感觉还不错,学数据结构相信对自己也肯定有好处的,对一些较为底层的知识有些了解和理解,这样才能走得更远!
无疑C语言就很重要了,而在C语言当中,数组/指针/结构体/链表等这些都是非常重要的,所以根据往前自己的学习经验,需要把C语言中的指针之后等更深入的知识好好再回顾和总结一遍,为学好数据结构打下坚实的基础吧!
一.指针变量
1.定义指针变量
·变量的指针包含两方面信息:指向变量的地址和指向存储单元的数据类型;
·定义指针变量需要指定基类型,方法如下:
1
|
int
*pointer;
|
·基类型说明存储单元的数据类型,指针的值即是所指向变量的地址;
·注意下面的一个赋值为非法的:
1
|
*pointer =
100
; ===>左右类型不一致,赋值非法
|
·基于内存的地址编址可知,内存的值是无符号整型;
·为指针变量赋初值时注意是否有乱指的情况(即指向程序不授权的地址,会出错),会有下面三种情况:
1
2
3
|
1
.正确指向本程序的变量 ===>正确
2
.指向其它程序未授权的地址 ===>非法
3
.未赋初值,只有随机值,指向未知地方 ===>非法
|
·可看下面一个非法的示范:
1
2
3
4
5
6
7
8
9
|
#
include
<stdio.h>
void
swap(
int
*p1,
int
*p2)
{
int
*p3; ===>定义p3指针变量时已有初值,为随机值
*p3 = *p1; ===>修改p3指向的值,会出现“未授权”的情况,即程序试图去修改一个未知地方的值
*p1 = *p2; 这显然是会引起错误的。
*p2 = *p3;
}
|
2.引用指针变量
·两个重要的符号:
1
2
|
& 取地址运算符,&a取变量a的地址
* 指针运算符,*p取指针p所指向的变量的值
|
·常用的三种指针引用:
(1)对指针变量赋值
1
|
pointer = &a ===>把a的地址赋给指针变量,或让pointer指向a
|
(2)引用指针变量指向的值
1
|
printf(
"%d"
,*pointer); ===>输出a的值
|
(3)直接引用
1
|
printf(
"%d"
,pointer); ===>直接输出指针变量的值,是一个内存地址
|
3.指针变量作用之一:作函数参数
·编写一个交换两数值的程序,用指针实现,代码如下:
a.main函数代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#
include
<stdio.h>
int
main(
void
)
{
int
a,b;
int
*p1,*p2;
printf(
"Please input a:"
);scanf(
"%d"
,&a);
printf(
"Please input b:"
);scanf(
"%d"
,&b);
printf(
"a=%d, b=%d\n"
,a,b);
p1 = &a;p2 = &b;
swap(p1,p2);
printf(
"Now, a=%d, b=%d\n"
,a,b);
return
0
;
}
|
b.swap函数代码如下:
1
2
3
4
5
6
7
8
9
|
#
include
<stdio.h>
void
swap(
int
*p1,
int
*p2)
{
int
temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
|
·编译,链接与执行:
1
2
3
4
5
6
|
xpleaf@leaf:~/stuc$ gcc -o swap swap_main.o swap_sec.o
xpleaf@leaf:~/stuc$ ./swap
Please input a:
3
Please input b:
4
a=
3
, b=
4
Now, a=
4
, b=
3
|
二.指针与数组
1.数组元素的指针
·所谓数组元素的指针就是数组元素的地址;
·数组的引用有下标法和指针法,在编译时,最终数组引用的实现都是通过指针实现的;
·数组名实际为一地址常量(指针常量),指向数组的第一个元素,该地址值不可改变;
·可以以指针的操作方式来引用数组中的元素;
2.通过指针运算引用数组元素
·只讨论加减:
1
2
3
|
p1+
1
:表示指向下
1
个元素,指针移动基类型个字节
p1-
1
:表示指向上
1
个元素,指针移动基类型个字节
p1+
2
:表示指向下
1
个元素,指针移动(基类型 *
2
)个字节
|
·a[3]中,[ ]实际是取址运算符,等价于:*(a+3);
·通过指针运算操作,编写一个遍历数组的程序:
a.代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#
include
<stdio.h>
int
main(
void
)
{
int
a[
10
], *p, i;
for
(i =
0
;i <
10
;i++)
a[i] = i +
10
;
p = a;
for
(i =
0
;i <
10
; p++, i++)
printf(
"a[%d]=%d "
, i, *p);
printf(
"\n"
);
return
0
;
}
|
b.执行过程:
1
2
3
4
|
xpleaf@leaf:~/stuc/shuzu$ gcc -c value.c
xpleaf@leaf:~/stuc/shuzu$ gcc -o value value.o
xpleaf@leaf:~/stuc/shuzu$ ./value
a[
0
]=
10
a[
1
]=
11
a[
2
]=
12
a[
3
]=
13
a[
4
]=
14
a[
5
]=
15
a[
6
]=
16
a[
7
]=
17
a[
8
]=
18
a[
9
]=
19
|
3.数组名作函数参数
·显然与指针变量作函数参数是一致的,只是换个写法,即在定义或调用函数时,下面的功能是一致的:
a.创建函数时形式不一样,实际传递的还是指针:
1
2
3
|
void
swap(
int
a[])
等价于
void
swap(
int
*a)
|
b.引用函数时,放入的都是指针:
1
|
swap(a) ===>实际放入的都是指针
|
·原理为:在编译时,最终数组引用的实现都是通过指针实现的;
4.指针与多维数组
--多维数组元素的指针
·创建一个如下的二维数组进行分析:
1
|
a[
5
][
2
] = {{
1
,
2
}, {
3
,
4
}, {
5
,
6
}, {
7
,
8
}, {
9
,
10
}};
|
·只从文字数字上看,二维数组与指针的关系会比较抽象,可用下面的示意图形象化:
·作如下重要说明:
a.二维数组名实则是一个指针变量,包含了指向的存储单元和数据类型;
b.二维数组名指针变量指向的数据类型为:一维数组名,即指针变量;
c.所以二维数组名应为多重指针,即指向指针的指针;
d.前面说[ ]实则为取址运算符,则a[1],即为对多重指针a作运算:*(a+1),从而得到多重指针a指向的第2个指针变量;
e.*(a+1)或者说a[1]实则就是普通的一维数组名,也是一个指针,因此一维数组是如何处理的,即可对其作相应处理;
f.**(a+1)或者a[1][0],在这里就是对应的值3;
g.无论中间的指针指向如何复杂,根据数组的特性,最终的元素(在这里是数值),在内存中的存储空间是连续的。
--指向多维数组元素的指针变量
·根据一维数组和二维数组的特性,定义指向多维数组的指针变量时,有如下的两种方式:
a.定义二维数组指针所指向的一维数组指针变量,与前面指针变量指向一维数组是类似的;
b.定义指向二维数组名的二维数组指针变量,下面重点讨论;
·编写程序,通过指向二维数组的多重指针变量的运算来遍历二维数组:
a.代码如下:
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
)
{
int
a[
5
][
2
] = {
{
1
,
2
},
{
3
,
4
},
{
5
,
6
},
{
7
,
8
},
{
9
,
10
}
};
int
(*p)[
2
], i, j;
p = a;
for
(i =
0
;i <
5
; i++)
for
(j =
0
;j <
2
;j++)
printf(
"a[%d][%d]=%d\n"
, i, j,*(*(p+i)+j));
return
0
;
}
|
b.执行过程如下:
1
2
3
4
5
6
7
8
9
10
11
|
xpleaf@leaf:~/stuc/shuzu2$ ./value2
a[
0
][
0
]=
1
a[
0
][
1
]=
2
a[
1
][
0
]=
3
a[
1
][
1
]=
4
a[
2
][
0
]=
5
a[
2
][
1
]=
6
a[
3
][
0
]=
7
a[
3
][
1
]=
8
a[
4
][
0
]=
9
a[
4
][
1
]=
10
|
·注意的是不能对p执行“(*p)++”操作,*p表示一个指针变量,值为地址常量,执行(*p)++,即要改变地址常量,这是非法的;
·可以执行p++操作,因为改变的只是变量p的值,即相当于重新给p赋值;
·一个有趣的现象如下:
a.在上面代码中添加如下一行代码:
1
|
printf(
"print %d\n"
, *(*p+
3
));
|
b.会多出如下输出:
1
|
print
4
|
·虽然*p表示的是第一个一维数组,最多应该只有2个元素,而上面输出的是第二个一维数组的第二个元素;
·由此也可以知道数组元素的在内存中的连续性,以及对多重指针p执行操作时对取值的影响;
--用指向一维数组的指针作函数参数
·实则可以用指向一维数组元素和指向一维数组的指针作函数参数,注意二者的不同,这里只讨论前者;
·编写一个程序,用指向一维数组的指针变量作函数参数,遍历多维数组:
a.main函数代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#
include
<stdio.h>
int
main(
void
)
{
int
a[
5
][
2
] = {
{
1
,
2
},
{
3
,
4
},
{
5
,
6
},
{
7
,
8
},
{
9
,
10
}
};
int
(*p)[
2
], i,j;
p = a;
fun(p,
5
);
return
0
;
}
|
b.fun函数代码如下:
1
2
3
4
5
6
7
8
9
|
#
include
<stdio.h>
void
fun(
int
(*p)[
2
],
int
n)
{
int
i, j;
for
(i =
0
;i < n;i++)
for
(j =
0
;j <
2
;j++)
printf(
"a[%d][%d]=%d\n"
,i, i, *(*(p+i)+j));
}
|
c.执行过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
xpleaf@leaf:~/stuc/shuzu3$ gcc -o value3 value3.o value3_fun.o
xpleaf@leaf:~/stuc/shuzu3$ ./value3
a[
0
][
0
]=
1
a[
0
][
0
]=
2
a[
1
][
1
]=
3
a[
1
][
1
]=
4
a[
2
][
2
]=
5
a[
2
][
2
]=
6
a[
3
][
3
]=
7
a[
3
][
3
]=
8
a[
4
][
4
]=
9
a[
4
][
4
]=
10
|
三.指针与字符串
1.字符串引用方式
·字符串实则是用数组存放,因此有两种方式可以引用字符串;
--常规方式:通过格式声明%s引用
·编写一个程序,通过%s引用字符串:
a.代码如下:
1
2
3
4
5
6
7
|
#
include
<stdio.h>
int
main(
void
)
{
char string[] =
"I hope CL can be my girlfriend."
;
printf(
"%s\n"
,string);
return
0
;
}
|
b.执行过程如下:
1
2
3
4
|
xpleaf@leaf:~/stuc/str$ gcc -c str1.c
xpleaf@leaf:~/stuc/str$ gcc -o str1 str1.o
xpleaf@leaf:~/stuc/str$ ./str1
I hope CL can be my girlfriend.
|
·通过%s可以输出字符串是因为:string代表字符串的首地址,即string数组的首地址,且后面默认添加'\0'标记;
--字符指针变量方式
·编写一个程序,通过指针变量实现上面的功能:
1
2
3
4
5
6
7
|
#
include
<stdio.h>
int
main(
void
)
{
char *string =
"I hope CL can be my girlfriend."
;
printf(
"%s\n"
,string);
return
0
;
}
|
·原理为把字符串首地址赋给string,string不断指向字符串中的每一个字符,直到指向字符串末尾标记“\0”结束输出;
·根据上面分析,下面三种方法功能一致:
1
2
3
4
5
6
|
*string =
"I hope CL can be my girlfriend."
; ===>直接定义初始化,让string指向字符串首地址
等价于
char *string; ===>先定义char指针类型
string =
"I hope CL can be my girlfriend."
===>让string指向字符串首地址
等价于
char string[] =
"I hope CL can be my girlfriend."
; ===>通过数组方式实现
|
·因为数组最终通过指针实现,上面三种方法实则一致;
2.字符指针作函数参数
·这与前面的一维数组遍历实则是一致的,无非就是对指针的引用,不再涉及;
·需要注意的是:字符数组(字符串)可以通过批量方式赋值(在初始化时才行),但数值型的数组则不可以;
3.指针与字符串的注意事项
--字符数组与字符串变量区分
·字符数组存放每一个字符,字符串变量只是存放一个地址;
--关于赋值方式
·可以对字符指针变量赋值,但不能对数组名赋值,以下赋值方式是非法的:
1
2
|
char str[
14
];
str =
"I hope CL can be my girlfriend."
; ===>非法赋值
|
·非法赋值原因在于,在定义一维数组str时,数组名str已经为一地址常量,因此往后再对其赋值是非法的;
·但下面的初始化是可以的:
1
|
char str[
14
] =
"I hope CL can be my girlfriend."
;
|
·其它初始化方式上面已有提及,不再讨论;
--关于存储单元
·对数组分配若干个存储单元以存放不同元素,对字符指针变量则分配1个存储单元,大小可实际编译器;
·判断指针变量大小,可先定义指针变量,再用sizeof或strlen(不包括'\0')即可;
--关于值是否可变
·可看下面的例子分析:
1
2
3
4
|
char a[] =
"I hope CL can be my girlfriend."
;
char *b =
"I hope CL can be my girlfriend."
;
a[
2
] =
'I'
; ===>合法
b[
2
] =
'I'
===>非法
|
·关于此内容,可用相关Python中的知识进行理解;
小结:
数组和字符指针变量最终在实现上还是有所区别的,数组会创建每一个变量,因此数组元素的值是可变的(对变量再进行赋值),有数组时通过指针操作实则是操作数组中的每一个变量,这些变量被赋值为字符常量;指针直接指向字符串某个字符,中间并没有变量,由于字符串常量都不可改变的,因此不能通过此方式修改字符串。