引言:该系列第四篇文章。指针、数组是C语言中的重要内容,也是C语言学习者的痛点和难点,本文将尽力去说明其中的陷阱和槽点。
文章向导
* 指针的本质与总结
* 数组的本质与总结
一、指针的本质与总结
1.何为指针?
指针是一个其数值为地址的变量,就如 char 类型的变量用字符作为其数值,而 int 型变量的数值是整数。既然指针也是变量,那么其自身也应该被分配有地址,这与指针所指向的地址不同。
2.与指针相关的运算符
· 在指针声明时,*号表示所声明的变量为指针
· 在指针使用时,*号表示取指针所指向的内存空间中的值
· &运算符后跟一个变量名时,给出该变量的地址
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语言中使用int *pi;这强调*pi是一个int类型的值。C++中更倾向于使用int* pi;这强调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
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.数组的大小与初始化
1) 初始化时应注意的细节
int a[5] = {1,2,3,4,5}; //显示指定数组元素的个数
int b[] = {1,2}; //隐式指定
int c[5] = {0}; //第一个元素为 0,其余未指定的编译器默认初始化为 0,故全为0
2) 如何自动计算数组元素的个数
假定有一int型数组a,可通过下面的算式快速得出数组元素个数。
int a[5] = {0};
printf("count for a : %d\n", sizeof(a)/sizeof(int));
3.数组首元素地址与数组地址
1)需要明确的几点
- 数组名代表数组首元素的地址
- 数组的地址需要用取地址符&才能得到
- 数组首元素的地址与数组的地址,两者在值上相同
- 数组首元素的地址与数组的地址是两个不同的概念(即虽然两者在值上是相
等的,但所指示的数据长度不同:前者为第一个数据的长度,后者为整个数
组的数据长度。)
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 Primer Plus(第五版)
C++ Primer Plus(第六版)
狄泰软件学院-C进阶剖析教程
高质量嵌入式Linux C编程