C语言深度剖析—指针和数组(上)

一、指针基础

变量只是一段存储空间的别名,那么是不是必须通过这个别名才可以使用这段存储空间?答案是否定的。我们还可以通过指针也就是地址的方式来访问某段存储空间。

示例代码:

#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字节空间

# 在作为函数参数时,数组参数和指针参数等价

# 数组名在多数情况下可以看做常量指针,其值不能改变

# 指针的本质变量,保存的值被看做内存中的地址



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值