数组与指针(一) - 基本概念 C/C++

1. C 数组基本概念

        C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。数组的声明并不是声明一个个单独的变量,比如 runoob0、runoob1、...、runoob99,而是声明一个数组变量,比如 runoob,然后使用 runoob[0]、runoob[1]、...、runoob[99] 来代表一个个单独的变量。所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。总结下来:

(1)数组是相同类型的变量的有序集合

(2)数组在一片连续的内存空间中存储元素

(3)数组元素的个数可以显示或隐式指定

1.1. 声明数组

        在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:type arrayName [ arraySize ]; 这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:double balance[10];

1.2. 初始化数组

在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

1.3. 访问数组元素

数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:double salary = balance[9];

2. 数组地址(&a)与数组名a

(1)数组名a代表数组首元素的地址。因此,第2个元素的地址为a+1,以此类推……。注意a或a+i表示元素的地址。可以用*(a+i)取出元素的值,也可以用a[i]来取出元素的值,因为当编译中遇到a[i]会自动转为*(a+i)。反过来也可知,第1个元素的地址为a或&a[0],第2个元素的地址为a+1或&a[1],第i个元素的地址为(a+i)或&a[i]……

(2)数组的地址需要用取地址符&才能得到。即形如&a取的是整个数组的地址,所以&a+1表示指向整个数组的最后面的位置。

(3)数组的首元素的地址值与数组的地址值相同,但是两个不同的概念。

【编程实验】数组名和数组地址

#include <stdio.h>

int main(){
    //将数组每个元素初始化为0
    int a[5] = {0};//含义,将第1个元素初始化为0,其余为0.

    printf("a = %p\n",a); //首元素的地址
    printf("&a = %p\n",&a); //整个数组的地址,从数值与看,与a一样。
    printf("&a[0] = %p\n",&a[0]);//第1个元素的地址

    return 0;
}

2.1. 数组名的盲点

(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组。如int a[5]表示a的类型为int[5],所以sizeof(a)表示取整个数组的大小,&a表示数组的地址。

(3)数组名的外延:除了sizeof(a)和&a外,数组名经常可看作是一个常量指针。但要注意这里仅仅是“看作”,而不是真正的指针。不同于指针,数组名只是编译过程中的一个符号,编译器并不为其分配内存,有人称之为“伪变量”。因此,形式a++\a—或a=b(其中b是另一个数组名)这些都是错误的,因为a只是一个符号,编译器会把数组信息(如大小,地址)放入符号表中,每次遇到数组名a时,就会从符号表中取出这个数组的地址,然后用这个固定的地址代替 a,所以这个符号并没有被分配内存空间,而上述操作都是针对变量而言的,故数组名只能做为右值使用。

(4)对数组的引用,如a[i]或*(a+i),只需访问内存一次,而指针的引用如*(p+i)则需要两次,首选通过&p找到p指针,然后加i,再从p+i里面取出的内容。

(5)当数组名作为形参时,将退化为指针。即可以把数组名当成指针来用,这里的sizeof(数组名)为4,即指针的长度。

【实例分析】数组和指针并不相同

#include <stdio.h>

int main(){
    //将数组每个元素初始化为0
    int a[5] = {0};
    int b[2];
    int* p = NULL;

    p = a;
    printf("a = %p\n",a);  //首元素的地址
    printf("p = %p\n",p);  //p==a。
    printf("&p = %p\n",&p);//指针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);  //p==b。
    printf("&p = %p\n",&p);//指针p的地址
    printf("sizeof(b) = %d\n",sizeof(b));//数组的大小:8
    printf("sizeof(p) = %d\n",sizeof(p));//指针的大小为4.

    //a = b; //编译错误,数组名不能作为左值;
    //a++;   //编译错误,数组名被编译一个固定地址,相当于0xaabbccdd++的错误

    return 0;
}

2.2. 小结

(1)数组是一片连续的内存空间

(2)数组的地址和数组首元素的地址意义不同

(3)数组名在大多数情况下被当成常量指针处理

(4)数组名其实并不是指针,不能将其等同于指针。

3. 指针

        变量的回顾,程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段内存空间呢?

3.1. *号的意义

        *号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值。

(1)在指针声明时,*号表示所声明的变量为指针

(2)在指针使用时,*号表示取指针所指向的内存空间中的值。

【实例分析】指针使用示例

#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); //p == &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;
}

3.2. 传值调用与传址调用

(1)指针是变量,因此可以声明指针参数

(2)当一个函数体内部需要改变实参的值,则需要使用指针参数

(3)函数调用时,实参值将复制到形参

(4)指针适用于复杂数据类型作为参数的函数中

【编程实验】利用指针交换变量

#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);

    swap(&aa, &bb);

    printf("aa = %d, bb = %d\n", aa, bb);

    return 0;
}

3.3. 常量与指针

常量指针

定义: 又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。

关键点:

  • 1.常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
  • 2.常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
  • 3.指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;

代码形式:

int const* p;  const int* p;

指针常量

定义:本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。

关键点:

  • 1.它是个常量!
  • 2.指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
  • 3.指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;
  •  

    代码形式:

    int* const p;

指向常量的常指针

定义:指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。

关键点:

代码形式:

const int* const p;
  • 那如何区分这几类呢? 带两个const的肯定是指向常量的常指针,很容易理解,主要是如何区分常量指针和指针常量:

    • 1.一个指针常量,指向的是一个指针对象;
    • 2.它指向的指针对象且是一个常量,即它指向的对象不能变化;

(1)几种情况

  ①const int* p; //p可变,p指向的内容不可变

  ②int const* p; //p可变,p指向的内容不可变

  ③int* const p; //p不可变,p指向的内容可变

  ④const int* const p; //p不可变,p指向的内容不可变

(2)口诀:左数右指

  ①当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;
}

在实际应用中,常量指针要比指针常量用的多,比如常量指针经常用在函数传参中,以避免函数内部修改内容。

size_t strlen(const char* src); //常量指针,src的值不可改变;
char a[] = "hello";
char b[] = "world";
size_t a1 = strlen(a);
size_t b1 = strlen(b);

虽然a、b是可以修改的,但是可以保证在strlen函数内部不会修改a、b的内容。

3.4. 空指针、野指针

既然讲到了指针,那顺便说一下空指针、野指针的问题。

空指针就是保存地址为空的指针,使用指针时必须先判断是否空指针,很多问题都是这一步导致的。

野指针是在delete掉指针之后,没有置0,导致指针随意指向了一个内存地址,如果继续使用,会造成不可预知的内存错误。

另外指针的误用很容易造成BUG或者内存泄漏。

看代码:

//-------空指针-------//
int *p4 = NULL;
//printf("%d",*p4); //运行Error,使用指针时必须先判断是否空指针

//-------野指针(悬浮、迷途指针)-------//
int *p5 = new int(5);
delete p5;
p5 = NULL; //一定要有这一步
printf("%d",*p5);  //隐藏bug,delete掉指针后一定要置0,不然指针指向位置不可控,运行中可导致系统挂掉

//-------指针的内存泄漏-------//
int *p6 = new int(6);
p6 = new int(7); //p6原本指向的那块内存尚未释放,结果p6又指向了别处,原来new的内存无法访问,也无法delete了,造成memory leak

3.5. 小结

(1)指针是C语言中一种特别的变量

(2)指针所保存的值是内存的地址

(3)可以通过指针修改内存中的任意地址内容

4. 函数指针分析

4.1. 函数类型

(1)C语言中的函数有自己特定的类型,这个类型由返回值、参数类型和参数个数共同决定。如int add(int i,int j)的类型为int(int,int)。

(2)C语言中通过typedef为函数类型重命名

    typedef type name(parameter list);//如typedef int f(int,int);

4.2. 函数指针

(1)函数指针用于指向一个函数,函数名是执行函数体的入口地址。

(2)定义函数指针的两种方法

①通过函数类型定义:FuncType* pointer;

②直接定义:type(*pointer)(parameter list);

//其中type为返回值类型,pointer为函数指针变量名,parameter list为参数类型列表

【实例分析】函数指针的使用(技巧:使用函数指针直接跳转到某个固定的地址开始执行)

#include <stdio.h>

typedef int (FUNC)(int);

int test(int i)
{
    return i * i;
}

void f()
{
    printf("Call f()...\n");
}

int main()
{
    FUNC* pt = test; //合法,函数名就是函数体的入口地址

    //直接定义函数指针,&f是旧式写法。函数名只是一个符号(不是变量),
    //与数组名一样,并不为其分配内存,因此&f和f在数值上是相等的。
    void(*pf)() = &f; //如果知道某个函数的地址,这里可以改为一个固定的地址值,实现跳转!

    printf("pf = %p\n",pf);
    printf("f = %p\n",f);
    printf("&f = %p\n",&f); //结果应为:pf == f == &f;

    pf();//利用函数指针调用
    (*pf)(); //旧式写法
    printf("Function pointer call:%d\n",pt(2));

    return 0;
}

4.3. 回调函数

(1)回调函数是利用函数指针实现的一种调用机制

(2)回调机制原理

  ①调用者不知道具体事件发生时需要调用的具体函数

  ②被调函数不知道何时被调用,只知道需要完成的任务

  ③当具体事件发生时,调用者通过函数指针调用具体函数。

(3)回调机制中的调用者和被调用者互不依赖。

【实例分析】回调函数使用示例

#include <stdio.h>

typedef int (*Weapon)(int); //操作某种武器的函数

//使用某种武器与boss进行战斗
void fight(Weapon wp,int arg) //arg为传给函数指针的参数
{
    int result = 0;

    printf("Fight boss!\n");
    result = wp(arg);//调用回调函数,并传入参数arg
    printf("Boss loss:%d\n",result);//Boss失血多少?
}

//使用武器——刀
int knife(int n)
{
    int ret = 0;
    int i = 0;

    for (i=0; i< n; i++)
    {
        printf("Knife attack:%d\n",1);
        ret++;
    }

    printf("\n");

    return ret;  
}

//使用武器——剑
int sword(int n)
{
    int ret = 0;
    int i = 0;

    for (i=0; i< n; i++)
    {
        printf("Sword attack:%d\n",5);
        ret++;
    }

    printf("\n");

    return ret;  
}

//使用武器——枪
int gun(int n)
{
    int ret = 0;
    int i = 0;

    for (i=0; i< n; i++)
    {
        printf("Gun attack:%d\n",10);
        ret++;
    }

    printf("\n");

    return ret;  
}

int main()
{
    fight(knife, 3);//用刀砍3次
    fight(sword, 4);//用剑刺4次
    fight(gun, 5);  //开枪5次

    return 0;
}

4.4. 小结

(1)C语言中的函数都有特定的类型

(2)可以使用函数类型定义函数指针

(3)函数指针是实现回调机制的关键技术,同时函数指针也是C语言面向对象结构体抽象的关键技术

(4)通过函数指针可以在C程序中实现固定地址跳转

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值