引言:该系列第四篇文章。指针、数组是C语言中的重要内容,也是C语言学习者的痛点和难点,本文将尽力去说明其中的陷阱和槽点。
文章向导
- 指针的本质与总结
- 数组的本质与总结
一、指针的本质与总结
1.何为指针?
~~~~ ~~~ 指针是一个其数值为地址的变量,就如 char 类型的变量用字符作为其数值,而 int 型变量的数值是整数。既然指针也是变量,那么其自身也应该被分配有地址,这与指针所指向的地址不同。
2.与指针相关的运算符
1)在指针声明时,*号表示所声明的变量为指针
2)在指针使用时,*号表示取指针所指向的内存空间中的值
3)&运算符后跟一个变量名时,给出该变量的地址
int i = 0;
int j = 0;
int* p = &i; //指针声明
j = *p; //指针使用
这里可以做一个形象的比喻:*号相当于一把钥匙,通过这把钥匙可以打开内存并读取其中的值。从C语言的描述上来说,p等价于&i,*p等价于i 。
3.指针声明时的小问题
~~~~ ~~~ 你是否对以下这几种指针声明感到困惑,它们是一样的吗?
int* pi, int * pi, int *pi, int*pi;
~~~~ ~~~ 事实上星号和指针名之间的空格是可选的,C语言中更倾向于使用 i n t ∗ p i int^{\,\,\,\,*}pi int∗pi,以强调 ∗ p i ^* pi ∗pi是一个int类型的值。C++中则倾向于使用 i n t ∗ p i int^* pi int∗pi,以强调 i n t ∗ int^* int∗是一种类型——即指向int的指针。但实际上在哪里添加空格对于编译器来说没有实质性的区别,即你也可以写成int*pi;
4.指针占用的内存大小
#include <stdio.h>
int main()
{
int i = 0;
int* pI;
char* pC;
float* pF;
pI = &i;
*pI = 10;
printf("%p, %p, %d\n", pI, &i, i);
printf("%d, %d, %p\n", sizeof(int*), sizeof(pI), &pI);
printf("%d, %d, %p\n", sizeof(char*), sizeof(pC), &pC);
printf("%d, %d, %p\n", sizeof(float*), sizeof(pF), &pF);
return 0;
}
~~~~
~~~
上述程序的运行结果如下(32位VC++6.0编译平台):
此处指针所占用的内存大小,取决于系统位数。32位系统,结果为4;64位系统,结果为8.(牢记指针值是地址,而地址值与系统位数有关)
5.可以把指针看作是整数类型吗?
~~~~ ~~~ 在大多数计算机系统内部,地址由一个无符号整数表示,但这并不意味着可把指针看作是整数类型。比如下面这个例子:
#include <stdio.h>
int main()
{
float f = 8.25;
unsigned int * p = (unsigned int*)&f;
//地址在大多数系统内部由无符号整数表示,此处的*表示该变量为一指针。
//右边若改为(unsigned int)&f则会出现警告:初始化时将整数赋给指针,未作类型转换。这是因为C语言中并不会自动地做强制类型转换,两边运算类型要匹配才行。
//08代表最小字段宽度为8,X代表使用十六进制数字0F的无符号十六进制整数。
printf(“0x%08X\n”,*p); //0x41040000,实际上由于指针类型不匹配,无法使用%f打印出8.25
printf(“0x%08X, 0x%08X\n”,p, &f); //0x0070FE4C 0x0070FE4C
printf(“0x%08X\n”,&p); //0x0070FE40
return 0;
}
~~~~ ~~~ 由上面这个例子可知:指针的确是一种新的数据类型,而并非一种整数类型。
6.传值调用和传址调用
- 函数调用时实参值将复制到形参(传值调用)
- 当一个函数体内部需要改变实参的值,则需要使用指针参数(传址调用)
/*传址调用实例*/
#include <stdio.h>
int swap(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
int main()
{
int aa = 1;
int bb = 2;
printf("aa = %d, bb = %d\n", aa, bb); // 1 2
swap(&aa, &bb); //传址调用
printf("aa = %d, bb = %d\n", aa, bb); // 2 1
return 0;
}
7.常量与指针
- 几种指针的声明形式
const int* p;//p可变,p指向的内容不可变—指针常量(指向常量的指针)
int const* p;//p可变,p指向的内容不可变—指针常量
int* const p;//p不可变,p指向的内容可变—常量指针
const int* const p;//p自身和p指向的内容都不可变
//这里的不可变仅仅指的是不能通过p本身来改变,不考虑其他手段。
- 一种名为“左数右指”的记忆方法
-当 const 出现在星号左边时指针指向的数据为常量;
-当 const 出现在星号右边时指针本身为常量。 - 实例验证
#include <stdio.h>
int main()
{
int i = 0;
const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;
*p1 = 1; // compile error
p1 = NULL; // ok
*p2 = 2; // compile error
p2 = NULL; // ok
*p3 = 3; // ok
p3 = NULL; // compile error
*p4 = 4; // compile error
p4 = NULL; // compile error
return 0;
}
二、数组的本质与总结
1.何为数组(array)?
~~~~
~~~
数组是由一组类型相同的元素所组成的有序集合,比如声明一个int[5]的整型数组,书写简单但其内涵却可深入挖掘。具体如下图所示:
2.数组的大小与初始化
- 初始化时应注意的细节
int a[5] = {1,2,3,4,5}; //显示指定数组元素的个数
int b[] = {1,2}; //隐式指定
int c[5] = {0}; //第一个元素为 0,其余未指定的编译器默认初始化为 0,故全为0
- 如何自动计算数组元素的个数
~~~~ ~~~ 假定有一int型数组a,可通过下面的算式快速得出数组元素个数。
int a[5] = {0};
printf("count for a : %d\n", sizeof(a)/sizeof(int));
3.数组首元素地址与数组地址
1)需要明确的几点
- 数组名代表数组首元素的地址
- 数组的地址需要用取地址符&才能得到
- 数组首元素的地址与数组的地址,两者在值上相同
- 数组首元素的地址与数组的地址是两个不同的概念(即虽然两者在值上是相等的,但所指示的数据长度不同:前者为第一个数据的长度,后者为整个数组的数据长度)。
为了说明上述最后一条的观点,不妨从如下的公式着手来理解:
a
+
1
⇔
(
u
n
si
g
n
e
d
i
n
t
)
a
+
s
i
z
e
o
f
(
∗
a
)
⇔
(
u
n
si
g
n
e
d
i
n
t
)
a
+
s
i
z
e
o
f
(
a
[
0
]
)
&
a
+
1
⇔
(
u
n
si
g
n
e
d
i
n
t
)
(
&
a
)
+
s
i
z
e
o
f
(
∗
(
&
a
)
)
⇔
(
u
n
si
g
n
e
d
i
n
t
)
(
&
a
)
+
s
i
z
e
o
f
(
a
)
a+1\Leftrightarrow \left( un\text{si}gned\,\,int \right) a\,\,+\,\,sizeof\left( *a \right) \Leftrightarrow \left( un\text{si}gned\,\,int \right) a\,\,+\,\,sizeof\left( a\left[ 0 \right] \right) \\ \&a+1\Leftrightarrow \left( un\text{si}gned\,\,int \right) \left( \&a \right) \,\,+\,\,sizeof\left( *\left( \&a \right) \right) \Leftrightarrow \left( un\text{si}gned\,\,int \right) \left( \&a \right) +\,\,sizeof\left( a \right)
a+1⇔(unsignedint)a+sizeof(∗a)⇔(unsignedint)a+sizeof(a[0])&a+1⇔(unsignedint)(&a)+sizeof(∗(&a))⇔(unsignedint)(&a)+sizeof(a)
2)实例验证
/*三者值一样,但意义却不相同*/
#include <stdio.h>
int main()
{
int a[5] = { 0 };
printf("a = %p\n", a);
printf("&a = %p\n", &a);
printf("&a[0] = %p\n", &a[0]);
return 0;
}
4.数组名的盲点知识
- 数组名可以视为为常量指针(本质上数组与指针是不同的事物)
- 数组名“指向”的是内存中数组首元素的起始位置
- 数组名不包含数组的长度信息
- 在表达式中数组名只能作为右值使用
- 当数组名作为sizeof操作符的参数或&运算符的参数时,不能视为常量指针
/*32位机上测试结果*/
#include <stdio.h>
int main()
{
int a[5] = {0};
int b[2];
int* p = NULL;
p = a;
printf("a = %p\n", a);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(a) = %d\n", sizeof(a)); //20
printf("sizeof(p) = %d\n", sizeof(p)); //4
printf("\n");
p = b;
printf("b = %p\n", b);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(b) = %d\n", sizeof(b)); //8
printf("sizeof(p) = %d\n", sizeof(p));//4
b = a; //error
return 0;
}
写在最后:关于数组与指针之间更为深入的关联,本文并未详细谈及,若读者还有疑问,可见由浅至深->C语言中指针及数组的经典问题分析(二)这篇文章的阐述与分析。
参阅资料
C Primer Plus(第五版)
Primer C++ 第五版
狄泰软件学院-C进阶剖析教程
高质量嵌入式Linux C编程