一、指针基础
变量只是一段存储空间的别名,那么是不是必须通过这个别名才可以使用这段存储空间?答案是否定的。我们还可以通过指针也就是地址的方式来访问某段存储空间。
示例代码:
#include <stdio.h>
int main()
{
int i = 5;
int* p = &i;
printf("%d, %08X\n", i, p);
*p = 10;
printf("%d, %08X\n", i, p);
return 0;
}
运行结果:
5, 0022FF48
10, 0022FF48
2、指针的本质
# 指针在本质上也是一个变量
# 指针需要占用一定的内存空间
# 指针用于保存内存地址的值
不同类型的指针占用内存空间的大小相同。
实例代码:
#include <stdio.h>
int main()
{
int i;
int* pI;
char* pC;
float* pF;
pI = &i;
*((int *)0x22ff4c) = 100;
printf("%0X, %0X, %d\n", pI, &i, i);
printf("%d, %d, %0X\n", sizeof(int*), sizeof(pI), &pI);
printf("%d, %d, %0X\n", sizeof(char*), sizeof(pC), &pC);
printf("%d, %d, %0X\n", sizeof(float*), sizeof(pF), &pF);
return 0;
}
运行结果:
22FF4C, 22FF4C, 100
4, 4, 22FF48
4, 4, 22FF44
4, 4, 22FF40
程序实现的功能:1、指针占用的内存空间2、指针的地址3、通过*号写内存
*号的意义
# 在指针声明时,*号表示所声明的变量为指针
# 在指针使用时,*号表示取指针所指向的内存空间中的值。*号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值
# “*”还代表乘号
3、传值调用与传址调用
# 指针是变量,因此可以声明指针参数
# 当一个函数内部需要改变实参的值,则需要使用指针参数
# 函数调用时实参值将复制到形参
# 指针适用于复杂数据类型作为参数的函数中
利用指针实现变量交换函数:
#include <stdio.h>
#define SWAP(a, b) {int t = a; a = b; b = t;} //宏定义也可以实现变量交换
void swap(int *a, int *b) //函数定义实现变量交换
{
int t = *a;
*a = *b;
*b = t;
}
int main()
{
int i = 1;
int j = 9;
printf("%d, %d \n", i, j);
swap(&i, &j);
printf("%d, %d\n", i, j);
return 0;
}
4、常量与指针
# const int *p; //p可变,p指向的内容不可变
# int const *p; //p可变,p指向的内容不可变
# int *const p; /p不可变,p指向的内容可变
# const int * const p; //p和p指向的内容都不可变
记忆口诀:左数右指
当const出现在*号左边时指针指向的数据为常量,当const出现在*后右边时指针本身为常量
指针小结:
# 指针是C语言中一种特别的变量
# 指针所保存的值是内存的地址
# 可以通过指针修改内存中的任意地址内容
二、数组基础
1、数组的概念
数组是相同类型的变量的有序集合
int a[5];
数组包含5个int类型的数据
a代表数组第一个元素的起始地址,这20个字节的名字为a。a[0], a[1]等都是a中的元素,并非元素的名字,数组中的元素没有名字。每个元素都是int型数据。
2、数组的大小
# 数组在一片连续的内存空间中存储元素
# 数组元素的个数可以显式或隐式指定
实例代码:
#include <stdio.h>
#include <string.h>
int main()
{
int a[5] = {1, 2}; //剩余的三个元素,编译器会初始化为0,这是编译器的行为
int c[5];
int d[5] = {0}; //这里对数组进行了初始化,巧妙的运用了编译器特性,
//它会自动对未定义数组元素初始化为0
int b[] = {1, 2};
int i;
//memset(c, 0, sizeof(c)); //这个不是初始化,是对数组赋值
printf("%d, %d \n", sizeof a, sizeof a/sizeof *a);
printf("%0x, %0x \n", a, &a);
printf("%d, %d \n", sizeof b, sizeof b/sizeof *b);
for(i = 0; i < 5; i++)
{
printf("%d\n", a[i]);
}
for(i = 0; i < 5; i++)
{
printf("%d\n", c[i]);
}
for(i = 0; i < 5; i++)
{
printf("%d\n", d[i]);
}
return 0;
}
运行结果:
20, 5
22ff38, 22ff38
8, 2
1
2
0
0
0
2008950864
-1
2009091625
2009091650
4200820
0
0
0
0
0
3、数组地址与数组名
# 数组名代表数组首元素的地址
# 数组的地址需要用取地址符&才能得到
# 数组首元素的地址值与数组的地址值相同
# 数组首元素的地址与数组的地址是两个不同的概念
你家所在的楼房和你家的GPS地址相同,但意义不同。
4、数组名的盲点
# 数组名可以看做一个常量指针
# 数组名“指向”的是内存中数组首元素的起始位置
# 在表达式中数组名只能作为右值使用
# 只有在下面场合中数组名不能看做常量指针
1、数组名作为sizeof操作数的参数时,它代表整个数组,而非首元素的常量指针
2、数组名作为&的参数,它代表整个数组,所以&a(a为一个数组)表示数组地址。
5、数组和指针并不相同
代码示例:
#include <stdio.h>
#include <stdlib.h>
//another file
//char *p = "hello world";
extern char p[];
/*
int main(int argc, char *argv[])
{
printf("%0x\n", p); //这里打印的是p的内容,它是一个地址。所以出现乱码。
return 0;
}*/
//changes
int main()
{
//编译器不会对数组寻址,这里自己做寻址。
printf("%s\n", (char *)*((unsigned int *)p));
return 0;
}
编译器对待数组和指针使用不同的方法:编译器处理指针时会进行一次寻址操作,所以可以实现间接访问内存内容。而编译器不会对数组进行寻址,所以在上代码中,自己做了寻址处理,但是这样代码的易读性减弱,仅仅演示,不提倡实际使用。
6、数组小结
# 数组是一片连续的内存空间
# 数组的地址和数组首元素的地址意义不同
# 数组名在大多数情况下被当成常量指针处理
# 数组名其实并不是指针,在外部声明时不能混淆
三、数组和指针分析
1、数组本质
# 数组是一段连续的内存空间
# 数组的空间大小为sizeof(array_type)*array_size
# 数组名可看做指向数组第一个元素的常量指针
2、指针的运算
# 指针是一种特殊的变量,与整数的运算规则为: p + n; <->(unsigned int)p + n*sizeof(*p);
结论:当指针p指向一个同类型的数组的元素时:p+1将指向当前元素的下一个元素;p-1将指向当前元素的上一个元素。
# 指针之间只支持减法运算,且必须参与运算的指针类型必须相同
p1 - p2;<->((unsigned int)p1 - (unsigned int)p2) / sizeof(type);
注意:1、只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差
2、当两个指针指向的元素不在同一个数组中时,结果未定义
3、指针的比较
# 指针也可以进行关系运算
< <= > >=
# 指针关系运算的前提是同时指向同一个数组中的元素
# 任意两个指针之间的比较运算(==, !=)无限制
实例代码:
#include <stdio.h>
#include <malloc.h>
#define DIM(a) (sizeof(a) / sizeof(*a))
int main()
{
char s[] = {'H', 'e', 'l', 'l', 'o'};
char* pBegin = s;
char* pEnd = s + DIM(s);//指向字符数组的后一个元素,这里并未访问,所以没有越界。
char* p = NULL;
for(p=pBegin; p<pEnd; p++)
{
printf("%c", *p);
}
printf("\n");
return 0;
}
运行结果:Hello
4、数组的访问
# 以下标的形式访问数组中的元素
int main()
{
int a[5];
a[1] = 3;
a[3] = 5;
return 0;
}
# 以指针的形式访问数组中的元素
int main()
{
int a[5];
*(a + 1) = 3;
*(a + 3) = 5;
return 0;
}
5、下标与指针
# 从理论上而言,当指针以固定增量在数组中移动时,其效率高于下标产生的代码
# 当指针增量为1且硬件具有硬件增量模型时,表现更佳
注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形成的效率已经和指针形式相当;当从可读性和代码维护的角度来看,下标形式更优。
实例代码:
#include <stdio.h>
#include <time.h>
int main()
{
clock_t start;
clock_t end;
int a[10000];
int b[10000];
int* pEnd = &a[10000];
int* pa = NULL;
int* pb = NULL;
int i = 0;
int k = 0;
start = clock();
for(k=0; k<10000; k++)
{
for(i=0; i<10000; i++)
{
b[i] = a[i];
}
}
end = clock();
printf("Index Timing: %d\n", end - start);
start = clock();
for(k=0; k<10000; k++)
{
for(pa=a, pb=b; pa<pEnd;)
{
*pb++ = *pa++;
}
}
end = clock();
printf("Pointer Timing: %d\n", end - start);
return 0;
}
gcc运行结果:
Index Timing: 578
Pointer Timing: 468
显然使用指针的效率要略高于下标,原因分析:
a[i] = b[j];
这条语句在编译时会生产下面代码:
(unsigned int)a + i*sizeof(int);
(unsigned int)b + j*sizeof(int);
*pb++ = *pa++
编译时可生成下面代码:
(unsigned int)pa + 4;
(unsigned int)pb + 4:
很容易就看出下标运算多两个乘法运算
6、a和&a的区别
# a为数组首元素的地址
# &a为整个数组的地址
# a和&a的意义不同其区别在于指针运算
a + 1 (unsigned int)a + sizeof(*a)
&a + 1 (unsigned int)(&a) + sizeof(*&a)
指针运算的经典问题:Motorola面试题
#include <stdio.h>
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1);
int* p2 = (int*)((int)a + 1); //注:高低位书写方式,不要按书写习惯来,低位放在右边。
int* p3 = (int*)(a + 1);
printf("%d, %0x, %0x, %0x\n", *a, (int)a+1, a, a+1);
printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
return 0;
}
运行结果:
1, 22ff21, 22ff20, 22ff24
5, 33554432, 3
注意:33554432对应的十六进制0x02000000
7、数组参数
# C语言中,数组作为函数参数时,编译器将其编译成对应的指针
void f(int a[]) <-> void f(int *a)
void f(int a[5]) <->void f(int *a)
结论:一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小。
#include <stdio.h>
void f(int a[1000]) //void f(int *a)
{
printf("%d\n", sizeof(a));
}
int main()
{
int a[5] = {0};
int i = 0;
f(a);
return 0;
}
总结:指针和数组的对比
# 数组声明时编译器自动分配一片连续内存空间
# 指针声明时只分配了用于容纳指针的4字节空间
# 在作为函数参数时,数组参数和指针参数等价
# 数组名在多数情况下可以看做常量指针,其值不能改变
# 指针的本质是变量,保存的值被看做内存中的地址